package main import ( "encoding/json" "strings" "time" ) // ConvertOpenAIRequest converts an OpenAI ChatCompletionRequest to Anthropic format // tempOverride, if provided, overrides any temperature from the request func ConvertOpenAIRequest(req *ChatCompletionRequest, tempOverride *float64) *AnthropicRequest { system, remainingMessages := extractSystemMessage(req.Messages) anthropicReq := &AnthropicRequest{ Model: req.Model, Messages: convertMessages(remainingMessages), System: system, MaxTokens: 32000, Thinking: &AnthropicThinking{ Type: "enabled", BudgetTokens: 10000, }, } if req.MaxTokens != nil { anthropicReq.MaxTokens = *req.MaxTokens } if req.Stream != nil { anthropicReq.Stream = *req.Stream } // Use temperature override if configured, otherwise use request temperature if tempOverride != nil { anthropicReq.Temperature = tempOverride } else if req.Temperature != nil { anthropicReq.Temperature = req.Temperature } if req.TopP != nil { anthropicReq.TopP = req.TopP } if len(req.Stop) > 0 { anthropicReq.StopSequences = req.Stop } if len(req.Tools) > 0 { anthropicReq.Tools = convertTools(req.Tools) } if req.ToolChoices != nil { anthropicReq.ToolChoice = convertToolChoice(req.ToolChoices) } return anthropicReq } // extractSystemMessage pulls role="system" messages and joins them func extractSystemMessage(messages []Message) (string, []Message) { var systemParts []string var rest []Message for _, msg := range messages { if msg.Role == "system" { switch content := msg.Content.(type) { case string: systemParts = append(systemParts, content) case []interface{}: // Extract text from content array for _, part := range content { if partMap, ok := part.(map[string]interface{}); ok { if text, ok := partMap["text"].(string); ok { systemParts = append(systemParts, text) } } } } } else { rest = append(rest, msg) } } return strings.Join(systemParts, "\n"), rest } // convertMessages converts OpenAI messages to Anthropic content blocks func convertMessages(messages []Message) []AnthropicMessage { var result []AnthropicMessage for _, msg := range messages { anthropicMsg := AnthropicMessage{ Role: msg.Role, } switch content := msg.Content.(type) { case string: anthropicMsg.Content = content case []interface{}: var blocks []ContentBlock for _, part := range content { partMap, ok := part.(map[string]interface{}) if !ok { continue } partType, _ := partMap["type"].(string) if partType == "text" { text, _ := partMap["text"].(string) blocks = append(blocks, ContentBlock{Type: "text", Text: text}) } // Image parts: skip for now, Anthropic uses different format } anthropicMsg.Content = blocks } result = append(result, anthropicMsg) } return result } // convertTools converts OpenAI function tools to Anthropic tool format func convertTools(tools []Tool) []AnthropicTool { var result []AnthropicTool for _, tool := range tools { anthropicTool := AnthropicTool{ Name: tool.Function.Name, Description: tool.Function.Description, InputSchema: tool.Function.Parameters, } result = append(result, anthropicTool) } return result } // convertToolChoice converts OpenAI tool_choice to Anthropic format func convertToolChoice(tc *ToolChoice) *AnthropicToolChoice { if tc == nil { return nil } result := &AnthropicToolChoice{} if tc.Type != "" { if tc.Type == "required" { result.Type = "any" } else { result.Type = tc.Type } } if tc.Function != nil { result.Name = tc.Function.Name } return result } // ConvertAnthropicResponse converts an Anthropic response to OpenAI format func ConvertAnthropicResponse(resp *AnthropicResponse, model string) *ChatCompletionResponse { response := &ChatCompletionResponse{ ID: resp.Id, Object: "chat.completion", Created: time.Now().Unix(), Model: model, Choices: make([]Choice, 0), Usage: Usage{ PromptTokens: resp.Usage.InputTokens, CompletionTokens: resp.Usage.OutputTokens, TotalTokens: resp.Usage.InputTokens + resp.Usage.OutputTokens, }, } if len(resp.Content) == 0 { response.Choices = append(response.Choices, Choice{ Index: 0, Message: Message{Role: "assistant", Content: ""}, FinishReason: mapStopReason(resp.StopReason), }) return response } // First pass: collect text and tool calls var textContent string var toolCalls []ToolCall for _, block := range resp.Content { if block.Type == "text" { textContent += block.Text } else if block.Type == "tool_use" { // Serialize the input back to JSON inputJSON, _ := json.Marshal(block.Input) toolCalls = append(toolCalls, ToolCall{ ID: block.Id, Type: "function", Function: FunctionCall{ Name: block.Name, Arguments: string(inputJSON), }, }) } } if len(toolCalls) > 0 { response.Choices = append(response.Choices, Choice{ Index: 0, Message: Message{ Role: "assistant", Content: textContent, ToolCalls: toolCalls, }, FinishReason: mapStopReason(resp.StopReason), }) } else { response.Choices = append(response.Choices, Choice{ Index: 0, Message: Message{Role: "assistant", Content: textContent}, FinishReason: mapStopReason(resp.StopReason), }) } return response } // mapStopReason maps Anthropic stop reasons to OpenAI finish reasons func mapStopReason(reason string) string { switch reason { case "end_turn": return "stop" case "tool_use": return "tool_calls" case "max_tokens": return "length" default: return "stop" } }