package anthropic import ( gocontext "context" "fmt" "sort" "strings" "time" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/http" goullm "github.com/yaoapp/gou/llm" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" "github.com/yaoapp/yao/agent/llm/adapters" "github.com/yaoapp/yao/agent/llm/providers/base" "github.com/yaoapp/yao/agent/output/message" "github.com/yaoapp/yao/share" ) // Provider Anthropic Messages API provider type Provider struct { *base.Provider adapters []adapters.CapabilityAdapter } // New create a new Anthropic provider func New(conn connector.Connector, capabilities *goullm.Capabilities) *Provider { return &Provider{ Provider: base.NewProvider(conn, capabilities), adapters: buildAdapters(capabilities), } } // buildAdapters builds capability adapters based on model capabilities func buildAdapters(cap *goullm.Capabilities) []adapters.CapabilityAdapter { if cap == nil { return []adapters.CapabilityAdapter{} } result := make([]adapters.CapabilityAdapter, 0) // Tool call adapter result = append(result, adapters.NewToolCallAdapter(cap.ToolCalls)) // Vision adapter (always registered to strip unsupported image content) visionSupport, visionFormat := context.GetVisionSupport(cap) if visionSupport { result = append(result, adapters.NewVisionAdapter(true, visionFormat)) } else { result = append(result, adapters.NewVisionAdapter(false, context.VisionFormatNone)) } // Audio adapter result = append(result, adapters.NewAudioAdapter(cap.Audio)) // Reasoning adapter if cap.Reasoning { result = append(result, adapters.NewReasoningAdapter(adapters.ReasoningFormatOpenAI, cap)) } else { result = append(result, adapters.NewReasoningAdapter(adapters.ReasoningFormatNone, cap)) } return result } // Stream stream completion from Anthropic API func (p *Provider) Stream(ctx *context.Context, messages []context.Message, options *context.CompletionOptions, handler message.StreamFunc) (*context.CompletionResponse, error) { trace, _ := ctx.Trace() if trace != nil { trace.Debug("Anthropic Stream: Starting stream request", map[string]any{ "message_count": len(messages), }) } maxRetries := 3 var lastErr error goCtx := ctx.Context if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Context != nil { goCtx = ctx.Stack.Options.Context } if goCtx == nil { goCtx = gocontext.Background() } currentMessages := make([]context.Message, len(messages)) copy(currentMessages, messages) for attempt := 0; attempt < maxRetries; attempt++ { select { case <-goCtx.Done(): return nil, fmt.Errorf("context cancelled: %w", goCtx.Err()) default: } if ctx.Interrupt != nil { if signal := ctx.Interrupt.Peek(); signal != nil && signal.Type == context.InterruptForce { return nil, fmt.Errorf("force interrupted by user") } } if attempt > 0 { backoff := time.Duration(1< // data: var currentEventType string streamHandler := func(data []byte) int { select { case <-goCtx.Done(): return http.HandlerReturnBreak default: } if ctx.Interrupt != nil { if signal := ctx.Interrupt.Peek(); signal != nil && signal.Type == context.InterruptForce { return http.HandlerReturnBreak } } if len(data) == 0 { return http.HandlerReturnOk } dataStr := string(data) trimmed := strings.TrimSpace(dataStr) if trimmed == "" { return http.HandlerReturnOk } // Parse event type line // Support both "event: type" (with space) and "event:type" (without space) formats if strings.HasPrefix(trimmed, "event:") { currentEventType = strings.TrimSpace(strings.TrimPrefix(trimmed, "event:")) return http.HandlerReturnOk } // Parse data line // Support both "data: {...}" (with space) and "data:{...}" (without space) formats if !strings.HasPrefix(trimmed, "data:") { // Check for error response if strings.HasPrefix(trimmed, "{") && strings.Contains(trimmed, `"error"`) { var apiErr APIError if err := jsoniter.UnmarshalFromString(trimmed, &apiErr); err == nil && apiErr.Error.Message != "" { if handler != nil { handler(message.ChunkError, []byte(apiErr.Error.Message)) } } } return http.HandlerReturnOk } jsonStr := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:")) if jsonStr == "" { return http.HandlerReturnOk } // Process based on event type switch currentEventType { case "message_start": var event MessageStartEvent if err := jsoniter.UnmarshalFromString(jsonStr, &event); err == nil { accumulator.id = event.Message.ID accumulator.model = event.Message.Model accumulator.role = event.Message.Role if event.Message.Usage != nil { accumulator.usage = &message.UsageInfo{ PromptTokens: event.Message.Usage.InputTokens, TotalTokens: event.Message.Usage.InputTokens, } } } case "content_block_start": var event ContentBlockStartEvent if err := jsoniter.UnmarshalFromString(jsonStr, &event); err == nil { accumulator.currentBlockIndex = event.Index accumulator.currentBlockType = event.ContentBlock.Type switch event.ContentBlock.Type { case "thinking": startMessage(msgTracker, message.ChunkThinking, handler) case "text": startMessage(msgTracker, message.ChunkText, handler) case "tool_use": accumulator.toolCalls[event.Index] = &accumulatedToolCall{ id: event.ContentBlock.ID, name: event.ContentBlock.Name, } toolCallInfo := &message.EventToolCallInfo{ ID: event.ContentBlock.ID, Name: event.ContentBlock.Name, Index: event.Index, } startToolCallMessage(msgTracker, toolCallInfo, handler) // Send initial ChunkToolCall with id and function name // to match OpenAI format so CUI can resolve tool name from stored chunks if handler != nil { toolCallData, _ := jsoniter.Marshal([]map[string]interface{}{ { "index": event.Index, "id": event.ContentBlock.ID, "type": "function", "function": map[string]interface{}{ "name": event.ContentBlock.Name, }, }, }) handler(message.ChunkToolCall, toolCallData) incrementChunk(msgTracker) } } } case "content_block_delta": var event ContentBlockDeltaEvent if err := jsoniter.UnmarshalFromString(jsonStr, &event); err == nil { switch event.Delta.Type { case "thinking_delta": if event.Delta.Thinking != "" { accumulator.thinkingContent += event.Delta.Thinking if handler != nil { handler(message.ChunkThinking, []byte(event.Delta.Thinking)) incrementChunk(msgTracker) } } case "text_delta": if event.Delta.Text != "" { accumulator.content += event.Delta.Text if handler != nil { handler(message.ChunkText, []byte(event.Delta.Text)) incrementChunk(msgTracker) } } case "input_json_delta": if event.Delta.PartialJSON != "" { if tc, exists := accumulator.toolCalls[event.Index]; exists { tc.inputJSON += event.Delta.PartialJSON // Update tracker if msgTracker.active && msgTracker.toolCallInfo != nil { msgTracker.toolCallInfo.Arguments = tc.inputJSON } } if handler != nil { // Send tool call delta toolCallData, _ := jsoniter.Marshal([]map[string]interface{}{ { "index": event.Index, "function": map[string]interface{}{ "arguments": event.Delta.PartialJSON, }, }, }) handler(message.ChunkToolCall, toolCallData) incrementChunk(msgTracker) } } case "signature_delta": // Handle thinking signature delta (for extended thinking) // The signature is accumulated but not sent to handler var sigDelta struct { Type string `json:"type"` Signature string `json:"signature"` } if err := jsoniter.UnmarshalFromString(jsonStr, &struct { Delta *struct { Signature string `json:"signature"` } `json:"delta"` }{Delta: &struct { Signature string `json:"signature"` }{}}); err == nil { _ = sigDelta // signature tracking if needed } } } case "content_block_stop": endMessage(msgTracker, handler) case "message_delta": var event MessageDeltaEvent if err := jsoniter.UnmarshalFromString(jsonStr, &event); err == nil { accumulator.stopReason = event.Delta.StopReason if event.Usage != nil { if accumulator.usage == nil { accumulator.usage = &message.UsageInfo{} } accumulator.usage.CompletionTokens = event.Usage.OutputTokens accumulator.usage.TotalTokens = accumulator.usage.PromptTokens + event.Usage.OutputTokens } } case "message_stop": // Message complete endMessage(msgTracker, handler) case "ping": // Keep-alive, ignore case "error": var apiErr struct { Type string `json:"type"` Error struct { Type string `json:"type"` Message string `json:"message"` } `json:"error"` } if err := jsoniter.UnmarshalFromString(jsonStr, &apiErr); err == nil && apiErr.Error.Message != "" { if handler != nil { handler(message.ChunkError, []byte(apiErr.Error.Message)) } } } return http.HandlerReturnOk } // Log request if trace != nil { if requestBodyJSON, marshalErr := jsoniter.Marshal(requestBody); marshalErr == nil { trace.Debug("Anthropic Stream Request", map[string]any{ "url": url, "body": string(requestBodyJSON), }) } } // Error buffer for non-SSE error responses var errorBuffer strings.Builder errorDetected := false wrappedHandler := func(data []byte) int { dataStr := string(data) trimmed := strings.TrimSpace(dataStr) if trimmed == "" { return http.HandlerReturnOk } // SSE event/data lines - pass to stream handler // Support both "event: type" (with space) and "event:type" (without space) formats if strings.HasPrefix(trimmed, "event:") || strings.HasPrefix(trimmed, "data:") { return streamHandler(data) } // Detect JSON error response if strings.HasPrefix(trimmed, "{") && strings.Contains(dataStr, `"error"`) { errorDetected = true } if errorDetected { errorBuffer.Write(data) errorBuffer.WriteString("\n") return http.HandlerReturnOk } return streamHandler(data) } // Make streaming request log.Trace("[LLM] Starting Anthropic Stream request: url=%s", url) err = req.Stream(goCtx, "POST", requestBody, wrappedHandler) _ = streamStartTime // Check for captured error response if errorDetected && errorBuffer.Len() > 0 { errorJSON := errorBuffer.String() if trace != nil { trace.Error(i18n.T(ctx.Locale, "llm.anthropic.stream.api_error"), map[string]any{"response": errorJSON}) } var apiErr APIError if parseErr := jsoniter.UnmarshalFromString(errorJSON, &apiErr); parseErr == nil && apiErr.Error.Message != "" { err = fmt.Errorf("Anthropic API error: %s (type: %s)", apiErr.Error.Message, apiErr.Error.Type) } else { err = fmt.Errorf("Anthropic API error: %s", strings.TrimSpace(errorJSON)) } } // Handle context cancellation if err != nil && goCtx.Err() != nil { return nil, fmt.Errorf("stream cancelled: %w", goCtx.Err()) } if err != nil { endMessage(msgTracker, handler) if handler != nil { handler(message.ChunkError, []byte(err.Error())) } return nil, fmt.Errorf("streaming request failed: %w", err) } // Check for empty response if accumulator.id == "" { endMessage(msgTracker, handler) errMsg := fmt.Errorf("no data received from Anthropic API") if handler != nil { handler(message.ChunkError, []byte(errMsg.Error())) } return nil, errMsg } // Build final response (convert to unified CompletionResponse) response := &context.CompletionResponse{ ID: accumulator.id, Object: "message", Model: accumulator.model, Role: accumulator.role, Content: accumulator.content, ReasoningContent: accumulator.thinkingContent, FinishReason: mapStopReason(accumulator.stopReason), Usage: accumulator.usage, } // Convert accumulated tool calls // Note: tool call indices may not start at 0 (e.g. if text blocks precede tool_use blocks) if len(accumulator.toolCalls) > 0 { // Collect all indices and sort them to ensure deterministic order indices := make([]int, 0, len(accumulator.toolCalls)) for idx := range accumulator.toolCalls { indices = append(indices, idx) } sort.Ints(indices) toolCalls := make([]context.ToolCall, 0, len(accumulator.toolCalls)) for _, idx := range indices { tc := accumulator.toolCalls[idx] toolCalls = append(toolCalls, context.ToolCall{ ID: tc.id, Type: "function", Function: context.Function{ Name: tc.name, Arguments: tc.inputJSON, }, }) } response.ToolCalls = toolCalls } endMessage(msgTracker, handler) return response, nil } // Post non-streaming completion request to Anthropic API func (p *Provider) Post(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) (*context.CompletionResponse, error) { trace, _ := ctx.Trace() maxRetries := 3 var lastErr error goCtx := ctx.Context if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Context != nil { goCtx = ctx.Stack.Options.Context } if goCtx == nil { goCtx = gocontext.Background() } currentMessages := make([]context.Message, len(messages)) copy(currentMessages, messages) for attempt := 0; attempt < maxRetries; attempt++ { select { case <-goCtx.Done(): return nil, fmt.Errorf("context cancelled: %w", goCtx.Err()) default: } if attempt > 0 { backoff := time.Duration(1< 0 { contentBlocks := make([]map[string]interface{}, 0) // Add text content if present if contentStr, ok := msg.Content.(string); ok && contentStr != "" { contentBlocks = append(contentBlocks, map[string]interface{}{ "type": "text", "text": contentStr, }) } // Add tool_use blocks for _, tc := range msg.ToolCalls { var input interface{} if tc.Function.Arguments != "" { jsoniter.UnmarshalFromString(tc.Function.Arguments, &input) } if input == nil { input = map[string]interface{}{} } contentBlocks = append(contentBlocks, map[string]interface{}{ "type": "tool_use", "id": tc.ID, "name": tc.Function.Name, "input": input, }) } apiMsg["content"] = contentBlocks } apiMessages = append(apiMessages, apiMsg) } // Build request body body := map[string]interface{}{ "model": model, } if len(apiMessages) > 0 { body["messages"] = apiMessages } if systemContent != "" { body["system"] = systemContent } if streaming { body["stream"] = true } // max_tokens is required for Anthropic maxTokens := 4096 // default if options.MaxTokens != nil { maxTokens = *options.MaxTokens } else if options.MaxCompletionTokens != nil { maxTokens = *options.MaxCompletionTokens } else if mt, ok := setting["max_tokens"].(int); ok && mt > 0 { maxTokens = mt } if lc, ok := p.Connector.(goullm.LLMConnector); ok { if caps := lc.GetCapabilities(); caps != nil && caps.MaxOutputTokens > 0 && maxTokens > caps.MaxOutputTokens { maxTokens = caps.MaxOutputTokens } } body["max_tokens"] = maxTokens // Temperature if options.Temperature != nil { body["temperature"] = *options.Temperature } if options.TopP != nil { body["top_p"] = *options.TopP } if options.Stop != nil { body["stop_sequences"] = options.Stop } // Tools (convert from OpenAI format to Anthropic format) if len(options.Tools) > 0 { anthropicTools := convertTools(options.Tools) if len(anthropicTools) > 0 { body["tools"] = anthropicTools } } if options.ToolChoice != nil { body["tool_choice"] = convertToolChoice(options.ToolChoice) } // Merge connector-level body params (thinking, etc.) // filtered through the SupportedParams / default whitelist. connParams := connector.FilterRequestBodyParams(setting, p.Connector) for k, v := range connParams { if _, exists := body[k]; !exists { body[k] = v } } return body, nil } // convertTools converts OpenAI-format tools to Anthropic format func convertTools(tools []map[string]interface{}) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(tools)) for _, tool := range tools { function, ok := tool["function"].(map[string]interface{}) if !ok { continue } anthropicTool := map[string]interface{}{ "name": function["name"], } if desc, ok := function["description"]; ok { anthropicTool["description"] = desc } if params, ok := function["parameters"]; ok { anthropicTool["input_schema"] = params } result = append(result, anthropicTool) } return result } // convertToolChoice converts OpenAI tool_choice to Anthropic format func convertToolChoice(choice interface{}) interface{} { switch v := choice.(type) { case string: switch v { case "auto": return map[string]interface{}{"type": "auto"} case "none": return map[string]interface{}{"type": "none"} case "required": return map[string]interface{}{"type": "any"} } case map[string]interface{}: if fn, ok := v["function"].(map[string]interface{}); ok { if name, ok := fn["name"].(string); ok { return map[string]interface{}{ "type": "tool", "name": name, } } } } return map[string]interface{}{"type": "auto"} } // convertImagePart converts an OpenAI image_url content part to Anthropic image format func convertImagePart(part context.ContentPart) map[string]interface{} { if part.ImageURL == nil { return map[string]interface{}{"type": "text", "text": "[image not available]"} } url := part.ImageURL.URL // Check if it's a base64 data URL if strings.HasPrefix(url, "data:") { // Parse data URL: data:image/jpeg;base64, parts := strings.SplitN(url, ",", 2) if len(parts) == 2 { mediaInfo := strings.TrimPrefix(parts[0], "data:") mediaInfo = strings.TrimSuffix(mediaInfo, ";base64") return map[string]interface{}{ "type": "image", "source": map[string]interface{}{ "type": "base64", "media_type": mediaInfo, "data": parts[1], }, } } } // URL-based image (Anthropic supports URL images) return map[string]interface{}{ "type": "image", "source": map[string]interface{}{ "type": "url", "url": url, }, } } // buildAPIURL builds the API URL for Anthropic func buildAPIURL(host, endpoint string) string { return connector.BuildAPIURL(host, endpoint) } // mapStopReason maps Anthropic stop_reason to OpenAI finish_reason func mapStopReason(stopReason string) string { switch stopReason { case "end_turn": return "stop" case "max_tokens": return "length" case "tool_use": return "tool_calls" case "stop_sequence": return "stop" default: return stopReason } } // Message tracker helper functions func startMessage(mt *messageTracker, messageType message.StreamChunkType, handler message.StreamFunc) { if mt.active { endMessage(mt, handler) } mt.active = true if mt.idGenerator != nil { mt.messageID = mt.idGenerator.GenerateMessageID() } else { mt.messageID = message.GenerateNanoID() } mt.messageType = messageType mt.startTime = time.Now().UnixMilli() mt.chunkCount = 0 mt.toolCallInfo = nil if handler != nil { startData := &message.EventMessageStartData{ MessageID: mt.messageID, Type: string(messageType), Timestamp: mt.startTime, } if startJSON, err := jsoniter.Marshal(startData); err == nil { handler(message.ChunkMessageStart, startJSON) } } } func startToolCallMessage(mt *messageTracker, toolCallInfo *message.EventToolCallInfo, handler message.StreamFunc) { if mt.active { endMessage(mt, handler) } mt.active = true if mt.idGenerator != nil { mt.messageID = mt.idGenerator.GenerateMessageID() } else { mt.messageID = message.GenerateNanoID() } mt.messageType = message.ChunkToolCall mt.startTime = time.Now().UnixMilli() mt.chunkCount = 0 mt.toolCallInfo = toolCallInfo if handler != nil { startData := &message.EventMessageStartData{ MessageID: mt.messageID, Type: string(message.ChunkToolCall), Timestamp: mt.startTime, ToolCall: toolCallInfo, } if startJSON, err := jsoniter.Marshal(startData); err == nil { handler(message.ChunkMessageStart, startJSON) } } } func incrementChunk(mt *messageTracker) { if mt.active { mt.chunkCount++ } } func endMessage(mt *messageTracker, handler message.StreamFunc) { if !mt.active { return } if handler != nil { endData := &message.EventMessageEndData{ MessageID: mt.messageID, Type: string(mt.messageType), Timestamp: time.Now().UnixMilli(), DurationMs: time.Now().UnixMilli() - mt.startTime, ChunkCount: mt.chunkCount, Status: "completed", } if mt.toolCallInfo != nil { endData.ToolCall = mt.toolCallInfo } if endJSON, err := jsoniter.Marshal(endData); err == nil { handler(message.ChunkMessageEnd, endJSON) } } mt.active = false mt.messageID = "" mt.toolCallInfo = nil } // isRetryableError checks if an error is retryable func isRetryableError(err error) bool { if err == nil { return false } errStr := err.Error() retryablePatterns := []string{ "timeout", "connection refused", "connection reset", "EOF", "HTTP 429", "HTTP 500", "HTTP 502", "HTTP 503", "HTTP 504", "overloaded", } for _, pattern := range retryablePatterns { if strings.Contains(strings.ToLower(errStr), strings.ToLower(pattern)) { return true } } return false } // resolveHostKeyVersion extracts host, key, and version via LLMConnector or Setting(). // Setting() is called at most once, and only when needed. func (p *Provider) resolveHostKeyVersion() (host, key, version string, err error) { setting := p.Connector.Setting() if lc, ok := p.Connector.(goullm.LLMConnector); ok { host = lc.GetURL() key = lc.GetKey() } else { host, _ = setting["host"].(string) key, _ = setting["key"].(string) } // Version is Anthropic-specific, not on LLMConnector interface version = "2023-06-01" if v, ok := setting["version"].(string); ok && v != "" { version = v } if host == "" { return "", "", "", fmt.Errorf("no host found in connector settings") } if key == "" { return "", "", "", fmt.Errorf("API key is not set") } return host, key, version, nil } // setAnthropicAuthHeaders sets auth headers based on LLMConnector.GetAuthMode(). func setAnthropicAuthHeaders(req *http.Request, conn connector.Connector, key string) { if lc, ok := conn.(goullm.LLMConnector); ok { switch lc.GetAuthMode() { case goullm.AuthAPIKey: req.SetHeader("api-key", key) return case goullm.AuthBearer: req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", key)) return } } // Default for Anthropic: x-api-key req.SetHeader("x-api-key", key) }