// Package llm contains impure LLM provider implementations. package llm import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" coretypes "github.com/enmanuel/agents/pkg/llm" ) const anthropicAPIBase = "https://api.anthropic.com/v1" const anthropicVersion = "2023-06-01" // NewAnthropicComplete returns a CompleteFunc backed by the Anthropic API. func NewAnthropicComplete(apiKeyEnv, baseURL string) coretypes.CompleteFunc { if baseURL == "" { baseURL = anthropicAPIBase } return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) { apiKey := os.Getenv(apiKeyEnv) if apiKey == "" { return coretypes.CompletionResponse{}, fmt.Errorf("env var %s is not set", apiKeyEnv) } body := toAnthropicRequest(req) raw, err := json.Marshal(body) if err != nil { return coretypes.CompletionResponse{}, fmt.Errorf("marshal request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/messages", bytes.NewReader(raw)) if err != nil { return coretypes.CompletionResponse{}, err } httpReq.Header.Set("x-api-key", apiKey) httpReq.Header.Set("anthropic-version", anthropicVersion) httpReq.Header.Set("content-type", "application/json") resp, err := http.DefaultClient.Do(httpReq) if err != nil { return coretypes.CompletionResponse{}, fmt.Errorf("anthropic request: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return coretypes.CompletionResponse{}, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return coretypes.CompletionResponse{}, fmt.Errorf("anthropic error %d: %s", resp.StatusCode, respBytes) } return fromAnthropicResponse(respBytes) } } // ── private conversion helpers ──────────────────────────────────────────── type anthropicRequest struct { Model string `json:"model"` MaxTokens int `json:"max_tokens"` System string `json:"system,omitempty"` Messages []anthropicMessage `json:"messages"` Tools []anthropicTool `json:"tools,omitempty"` } type anthropicMessage struct { Role string `json:"role"` Content string `json:"content"` } type anthropicTool struct { Name string `json:"name"` Description string `json:"description"` InputSchema map[string]any `json:"input_schema"` } type anthropicResponse struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } `json:"usage"` StopReason string `json:"stop_reason"` } func toAnthropicRequest(req coretypes.CompletionRequest) anthropicRequest { msgs := make([]anthropicMessage, 0, len(req.Messages)) for _, m := range req.Messages { if m.Role == coretypes.RoleSystem { continue // handled as top-level system param } msgs = append(msgs, anthropicMessage{ Role: string(m.Role), Content: m.Content, }) } tools := make([]anthropicTool, len(req.Tools)) for i, t := range req.Tools { tools[i] = anthropicTool{ Name: t.Name, Description: t.Description, InputSchema: t.InputSchema, } } return anthropicRequest{ Model: req.Model, MaxTokens: req.MaxTokens, System: req.SystemPrompt, Messages: msgs, Tools: tools, } } func fromAnthropicResponse(raw []byte) (coretypes.CompletionResponse, error) { var ar anthropicResponse if err := json.Unmarshal(raw, &ar); err != nil { return coretypes.CompletionResponse{}, fmt.Errorf("unmarshal response: %w", err) } var content string for _, c := range ar.Content { if c.Type == "text" { content += c.Text } } return coretypes.CompletionResponse{ Content: content, FinishReason: ar.StopReason, Usage: coretypes.TokenUsage{ InputTokens: ar.Usage.InputTokens, OutputTokens: ar.Usage.OutputTokens, TotalTokens: ar.Usage.InputTokens + ar.Usage.OutputTokens, }, }, nil }