package llm import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" ) type AnthropicClient struct { baseClient apiKey string baseURL string client *http.Client } func NewAnthropicClient(cfg ProviderConfig) (*AnthropicClient, error) { baseURL := cfg.BaseURL if baseURL == "" { baseURL = "https://api.anthropic.com" } return &AnthropicClient{ baseClient: baseClient{ providerID: cfg.ProviderID, modelKey: cfg.ModelKey, }, apiKey: cfg.APIKey, baseURL: strings.TrimSuffix(baseURL, "/"), client: &http.Client{}, }, nil } type anthropicRequest struct { Model string `json:"model"` Messages []anthropicMessage `json:"messages"` System string `json:"system,omitempty"` MaxTokens int `json:"max_tokens"` Stream bool `json:"stream"` 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 interface{} `json:"input_schema"` } type anthropicStreamEvent struct { Type string `json:"type"` Index int `json:"index,omitempty"` Delta struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` } `json:"delta,omitempty"` ContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` } `json:"content_block,omitempty"` } func (c *AnthropicClient) StreamText(ctx context.Context, req StreamRequest) (<-chan StreamChunk, error) { var systemPrompt string messages := make([]anthropicMessage, 0) for _, m := range req.Messages { if m.Role == RoleSystem { systemPrompt = m.Content continue } role := string(m.Role) if role == "tool" { role = "user" } messages = append(messages, anthropicMessage{ Role: role, Content: m.Content, }) } maxTokens := req.Options.MaxTokens if maxTokens == 0 { maxTokens = 4096 } anthropicReq := anthropicRequest{ Model: c.modelKey, Messages: messages, System: systemPrompt, MaxTokens: maxTokens, Stream: true, } if len(req.Tools) > 0 { anthropicReq.Tools = make([]anthropicTool, len(req.Tools)) for i, t := range req.Tools { anthropicReq.Tools[i] = anthropicTool{ Name: t.Name, Description: t.Description, InputSchema: t.Schema, } } } body, err := json.Marshal(anthropicReq) if err != nil { return nil, err } httpReq, err := http.NewRequestWithContext(ctx, "POST", c.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", c.apiKey) httpReq.Header.Set("anthropic-version", "2023-06-01") resp, err := c.client.Do(httpReq) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) resp.Body.Close() return nil, fmt.Errorf("anthropic API error: %d - %s", resp.StatusCode, string(body)) } ch := make(chan StreamChunk, 100) go func() { defer close(ch) defer resp.Body.Close() scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { return } var event anthropicStreamEvent if err := json.Unmarshal([]byte(data), &event); err != nil { continue } switch event.Type { case "content_block_delta": if event.Delta.Text != "" { ch <- StreamChunk{ContentChunk: event.Delta.Text} } case "message_stop": ch <- StreamChunk{FinishReason: "stop"} return } } }() return ch, nil } func (c *AnthropicClient) GenerateText(ctx context.Context, req StreamRequest) (string, error) { ch, err := c.StreamText(ctx, req) if err != nil { return "", err } return readAllChunks(ch), nil }