package llm import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // AnthropicAdapter implements the Provider interface for Anthropic API type AnthropicAdapter struct { apiKey string baseURL string defaultModel string httpClient *http.Client } // NewAnthropicAdapter creates a new Anthropic adapter func NewAnthropicAdapter(apiKey, defaultModel string) *AnthropicAdapter { return &AnthropicAdapter{ apiKey: apiKey, baseURL: "https://api.anthropic.com", defaultModel: defaultModel, httpClient: &http.Client{ Timeout: 5 * time.Minute, }, } } // Name returns the provider name func (a *AnthropicAdapter) Name() string { return ProviderAnthropic } // IsAvailable checks if Anthropic API is reachable func (a *AnthropicAdapter) IsAvailable(ctx context.Context) bool { if a.apiKey == "" { return false } // Simple check - we can't really ping Anthropic without making a request // Just verify we have an API key return true } // ListModels returns available Anthropic models func (a *AnthropicAdapter) ListModels(ctx context.Context) ([]Model, error) { // Anthropic doesn't have a models endpoint, return known models return []Model{ { ID: "claude-3-opus-20240229", Name: "Claude 3 Opus", Provider: ProviderAnthropic, Description: "Most powerful model for complex tasks", ContextSize: 200000, Capabilities: []string{"chat"}, }, { ID: "claude-3-sonnet-20240229", Name: "Claude 3 Sonnet", Provider: ProviderAnthropic, Description: "Balanced performance and speed", ContextSize: 200000, Capabilities: []string{"chat"}, }, { ID: "claude-3-haiku-20240307", Name: "Claude 3 Haiku", Provider: ProviderAnthropic, Description: "Fast and efficient", ContextSize: 200000, Capabilities: []string{"chat"}, }, { ID: "claude-3-5-sonnet-20240620", Name: "Claude 3.5 Sonnet", Provider: ProviderAnthropic, Description: "Latest and most capable model", ContextSize: 200000, Capabilities: []string{"chat"}, }, }, nil } // Complete performs text completion (converted to chat) func (a *AnthropicAdapter) Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) { // Anthropic only supports chat, so convert completion to chat chatReq := &ChatRequest{ Model: req.Model, Messages: []Message{ {Role: "user", Content: req.Prompt}, }, MaxTokens: req.MaxTokens, Temperature: req.Temperature, TopP: req.TopP, Stop: req.Stop, } chatResp, err := a.Chat(ctx, chatReq) if err != nil { return nil, err } return &CompletionResponse{ ID: chatResp.ID, Model: chatResp.Model, Provider: chatResp.Provider, Text: chatResp.Message.Content, FinishReason: chatResp.FinishReason, Usage: chatResp.Usage, Duration: chatResp.Duration, }, nil } // Chat performs chat completion func (a *AnthropicAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) { if a.apiKey == "" { return nil, fmt.Errorf("anthropic API key not configured") } model := req.Model if model == "" { model = a.defaultModel } start := time.Now() // Extract system message if present var systemMessage string var messages []map[string]string for _, m := range req.Messages { if m.Role == "system" { systemMessage = m.Content } else { messages = append(messages, map[string]string{ "role": m.Role, "content": m.Content, }) } } maxTokens := req.MaxTokens if maxTokens == 0 { maxTokens = 4096 } anthropicReq := map[string]any{ "model": model, "messages": messages, "max_tokens": maxTokens, } if systemMessage != "" { anthropicReq["system"] = systemMessage } if req.Temperature > 0 { anthropicReq["temperature"] = req.Temperature } if req.TopP > 0 { anthropicReq["top_p"] = req.TopP } if len(req.Stop) > 0 { anthropicReq["stop_sequences"] = req.Stop } body, err := json.Marshal(anthropicReq) if err != nil { return nil, err } httpReq, err := http.NewRequestWithContext(ctx, "POST", a.baseURL+"/v1/messages", bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("x-api-key", a.apiKey) httpReq.Header.Set("anthropic-version", "2023-06-01") resp, err := a.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("anthropic request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("anthropic error (%d): %s", resp.StatusCode, string(bodyBytes)) } var result struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` Model string `json:"model"` StopReason string `json:"stop_reason"` StopSequence string `json:"stop_sequence,omitempty"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } `json:"usage"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } duration := time.Since(start) // Extract text from content blocks var responseText string for _, block := range result.Content { if block.Type == "text" { responseText += block.Text } } return &ChatResponse{ ID: result.ID, Model: result.Model, Provider: ProviderAnthropic, Message: Message{ Role: "assistant", Content: responseText, }, FinishReason: result.StopReason, Usage: UsageStats{ PromptTokens: result.Usage.InputTokens, CompletionTokens: result.Usage.OutputTokens, TotalTokens: result.Usage.InputTokens + result.Usage.OutputTokens, }, Duration: duration, }, nil } // Embed creates embeddings (Anthropic doesn't support embeddings natively) func (a *AnthropicAdapter) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) { return nil, fmt.Errorf("anthropic does not support embeddings - use Ollama or OpenAI") }