package providers import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" ) const anthropicDefaultEndpoint = "https://api.anthropic.com" const anthropicAPIVersion = "2023-06-01" // AnthropicProvider handles the Anthropic Messages API. type AnthropicProvider struct{} func (p *AnthropicProvider) ID() string { return "anthropic" } // ── Chat Completion (non-streaming) ───────── func (p *AnthropicProvider) ChatCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (*CompletionResponse, error) { req.Stream = false body, err := p.doRequest(ctx, cfg, req) if err != nil { return nil, err } defer body.Close() var resp anthropicResponse if err := json.NewDecoder(body).Decode(&resp); err != nil { return nil, fmt.Errorf("decode response: %w", err) } // Extract text from content blocks var content string for _, block := range resp.Content { if block.Type == "text" { content += block.Text } } return &CompletionResponse{ Content: content, Model: resp.Model, FinishReason: resp.StopReason, InputTokens: resp.Usage.InputTokens, OutputTokens: resp.Usage.OutputTokens, }, nil } // ── Stream Completion ─────────────────────── func (p *AnthropicProvider) StreamCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (<-chan StreamEvent, error) { req.Stream = true body, err := p.doRequest(ctx, cfg, req) if err != nil { return nil, err } ch := make(chan StreamEvent, 64) go func() { defer close(ch) defer body.Close() scanner := bufio.NewScanner(body) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") var event anthropicStreamEvent if err := json.Unmarshal([]byte(data), &event); err != nil { continue } switch event.Type { case "content_block_delta": if event.Delta.Type == "text_delta" { ch <- StreamEvent{Delta: event.Delta.Text} } case "message_delta": ch <- StreamEvent{ Done: true, FinishReason: event.Delta.StopReason, } case "message_stop": ch <- StreamEvent{Done: true} return case "error": ch <- StreamEvent{ Error: fmt.Errorf("anthropic stream error: %s", data), } return } } if err := scanner.Err(); err != nil { ch <- StreamEvent{Error: err} } }() return ch, nil } // ── List Models ───────────────────────────── func (p *AnthropicProvider) ListModels(_ context.Context, _ ProviderConfig) ([]Model, error) { // Anthropic doesn't have a list models endpoint. // Return known models with authoritative capabilities from our table. modelIDs := []struct{ id, name string }{ {"claude-opus-4-20250514", "Claude Opus 4"}, {"claude-sonnet-4-20250514", "Claude Sonnet 4"}, {"claude-haiku-4-20250414", "Claude Haiku 4"}, {"claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet"}, {"claude-3-5-haiku-20241022", "Claude 3.5 Haiku"}, } models := make([]Model, 0, len(modelIDs)) for _, m := range modelIDs { caps, _ := LookupKnownModel(m.id) models = append(models, Model{ ID: m.id, Name: m.name, OwnedBy: "anthropic", Capabilities: caps, }) } return models, nil } // ── HTTP Layer ────────────────────────────── func (p *AnthropicProvider) doRequest(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (io.ReadCloser, error) { endpoint := cfg.Endpoint if endpoint == "" { endpoint = anthropicDefaultEndpoint } endpoint = strings.TrimSuffix(endpoint, "/") + "/v1/messages" // Separate system message from conversation var system string messages := make([]anthropicMessage, 0, len(req.Messages)) for _, m := range req.Messages { if m.Role == "system" { system = m.Content continue } messages = append(messages, anthropicMessage{ Role: m.Role, Content: m.Content, }) } // Build Anthropic-format request antReq := anthropicRequest{ Model: req.Model, Messages: messages, MaxTokens: req.MaxTokens, Stream: req.Stream, } if system != "" { antReq.System = system } if antReq.MaxTokens == 0 { // Anthropic requires max_tokens. Resolve from known model table. antReq.MaxTokens = ResolveMaxOutput(req.Model, ModelCapabilities{}) } if req.Temperature != nil { antReq.Temperature = req.Temperature } if req.TopP != nil { antReq.TopP = req.TopP } body, err := json.Marshal(antReq) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("x-api-key", cfg.APIKey) httpReq.Header.Set("anthropic-version", anthropicAPIVersion) resp, err := http.DefaultClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("provider request: %w", err) } if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) resp.Body.Close() return nil, fmt.Errorf("provider error: HTTP %d: %s", resp.StatusCode, string(b)) } return resp.Body, nil } // ── Anthropic Wire Types ──────────────────── type anthropicMessage struct { Role string `json:"role"` Content string `json:"content"` } type anthropicRequest struct { Model string `json:"model"` Messages []anthropicMessage `json:"messages"` System string `json:"system,omitempty"` MaxTokens int `json:"max_tokens"` Temperature *float64 `json:"temperature,omitempty"` TopP *float64 `json:"top_p,omitempty"` Stream bool `json:"stream"` } type anthropicResponse struct { Model string `json:"model"` StopReason string `json:"stop_reason"` 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"` } type anthropicStreamEvent struct { Type string `json:"type"` Delta struct { Type string `json:"type"` Text string `json:"text"` StopReason string `json:"stop_reason"` } `json:"delta"` }