package proxy import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "sort" "strings" "time" ) // ---------- Anthropic request/response types ---------- // AnthropicRequest is the incoming Anthropic Messages API request. type AnthropicRequest struct { Model string `json:"model"` Messages []AnthropicMessage `json:"messages"` System any `json:"system,omitempty"` // string or []AnthropicTextBlock MaxTokens int `json:"max_tokens"` Temperature *float64 `json:"temperature,omitempty"` TopP *float64 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` StopSequences []string `json:"stop_sequences,omitempty"` Stream bool `json:"stream,omitempty"` Tools []AnthropicTool `json:"tools,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` } // AnthropicMessage is a message in Anthropic format. // Content can be either a plain string or an array of content blocks. type AnthropicMessage struct { Role string `json:"role"` // "user" or "assistant" Content AnthropicContentBlocks `json:"content"` } // AnthropicContentBlocks handles both string and array content formats. type AnthropicContentBlocks []AnthropicContent func (a *AnthropicContentBlocks) UnmarshalJSON(b []byte) error { // Try string first: "content": "hello" if len(b) > 0 && b[0] == '"' { var s string if err := json.Unmarshal(b, &s); err != nil { return err } *a = []AnthropicContent{{Type: "text", Text: s}} return nil } // Otherwise it's an array: "content": [{"type":"text","text":"hello"}] var blocks []AnthropicContent if err := json.Unmarshal(b, &blocks); err != nil { return err } *a = blocks return nil } // AnthropicContent is a content block (text, tool_use, tool_result, image). type AnthropicContent struct { Type string `json:"type"` Text string `json:"text,omitempty"` ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Input json.RawMessage `json:"input,omitempty"` ToolUseID string `json:"tool_use_id,omitempty"` Content any `json:"content,omitempty"` // for tool_result IsError *bool `json:"is_error,omitempty"` Source *AnthropicImage `json:"source,omitempty"` } // AnthropicImage is an image source in Anthropic format. type AnthropicImage struct { Type string `json:"type"` MediaType string `json:"media_type"` Data string `json:"data"` } // AnthropicTool defines a tool in Anthropic format. type AnthropicTool struct { Name string `json:"name"` Description string `json:"description,omitempty"` InputSchema map[string]any `json:"input_schema"` } // AnthropicResponse is the non-streaming Anthropic response. type AnthropicResponse struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Model string `json:"model"` Content []AnthropicContent `json:"content"` StopReason string `json:"stop_reason"` StopSeq string `json:"stop_sequence,omitempty"` Usage AnthropicUsage `json:"usage"` } // AnthropicUsage holds token usage in Anthropic format. type AnthropicUsage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } // AnthropicError is an error response in Anthropic format. type AnthropicError struct { Type string `json:"type"` Error AnthropicErrorBody `json:"error"` } // AnthropicErrorBody is the inner error details. type AnthropicErrorBody struct { Type string `json:"type"` Message string `json:"message"` } // ---------- Anthropic SSE event types ---------- type anthropicSSE struct { Type string `json:"type"` Index *int `json:"index,omitempty"` Delta *struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` PartialJSON string `json:"partial_json,omitempty"` StopReason string `json:"stop_reason,omitempty"` StopSequence string `json:"stop_sequence,omitempty"` } `json:"delta,omitempty"` Message *anthropicSSEMessage `json:"message,omitempty"` Usage *AnthropicUsage `json:"usage,omitempty"` ContentBlock *struct { Type string `json:"type"` Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` } `json:"content_block,omitempty"` } type anthropicSSEMessage struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Model string `json:"model"` Content []AnthropicContent `json:"content"` StopReason string `json:"stop_reason"` StopSeq string `json:"stop_sequence,omitempty"` Usage AnthropicUsage `json:"usage"` } // ---------- Anthropic → OpenAI conversion ---------- // AnthropicToOpenAI converts an Anthropic Messages request to OpenAI Chat Completions format. func AnthropicToOpenAI(ar *AnthropicRequest) *OpenAIChatRequest { var messages []OpenAIMessage // System prompt: Anthropic system → OpenAI system message systemText := extractSystemText(ar.System) if systemText != "" { messages = append(messages, OpenAIMessage{ Role: "system", Content: systemText, }) } // Convert each message for _, am := range ar.Messages { msg := anthropicMessageToOpenAI(am) messages = append(messages, msg) } // Convert tools var tools []OpenAITool for _, at := range ar.Tools { tools = append(tools, OpenAITool{ Type: "function", Function: OpenAIFunction{ Name: at.Name, Description: at.Description, Parameters: at.InputSchema, }, }) } temperature := 0.7 if ar.Temperature != nil { temperature = *ar.Temperature } oaReq := &OpenAIChatRequest{ Model: ar.Model, Messages: messages, MaxTokens: ar.MaxTokens, Temperature: temperature, Stream: ar.Stream, Tools: tools, } return oaReq } func extractSystemText(system any) string { switch s := system.(type) { case string: return s case []any: var parts []string for _, block := range s { if b, ok := block.(map[string]any); ok { if b["type"] == "text" { if text, ok := b["text"].(string); ok { parts = append(parts, text) } } } } return strings.Join(parts, "\n") } return "" } func anthropicMessageToOpenAI(am AnthropicMessage) OpenAIMessage { msg := OpenAIMessage{ Role: am.Role, } var textParts []string var imageParts []map[string]any var toolCalls []OpenAIToolCall for _, block := range am.Content { switch block.Type { case "text": textParts = append(textParts, block.Text) case "tool_use": toolCalls = append(toolCalls, OpenAIToolCall{ ID: block.ID, Type: "function", Function: OpenAIToolFunc{ Name: block.Name, Arguments: string(block.Input), }, }) case "tool_result": msg.Role = "tool" msg.ToolCallID = block.ToolUseID if block.Content != nil { msg.Content = flattenContent(block.Content) } if block.IsError != nil && *block.IsError { msg.Content = fmt.Sprintf("Error: %v", msg.Content) } return msg case "image": if block.Source != nil { imageParts = append(imageParts, map[string]any{ "type": "image_url", "image_url": map[string]any{ "url": fmt.Sprintf("data:%s;base64,%s", block.Source.MediaType, block.Source.Data), }, }) } } } if len(imageParts) > 0 { var content []map[string]any for _, t := range textParts { content = append(content, map[string]any{"type": "text", "text": t}) } content = append(content, imageParts...) msg.Content = content } else if len(toolCalls) > 0 { msg.ToolCalls = toolCalls msg.Content = strings.Join(textParts, "\n") } else { msg.Content = strings.Join(textParts, "\n") } return msg } // flattenContent converts tool_result content (string or array of content blocks) to a plain string. func flattenContent(v any) string { if s, ok := v.(string); ok { return s } if arr, ok := v.([]any); ok { var parts []string for _, item := range arr { if m, ok := item.(map[string]any); ok { if t, ok := m["text"].(string); ok { parts = append(parts, t) } } } return strings.Join(parts, "\n") } return fmt.Sprintf("%v", v) } // requestHasImages checks whether any message in the Anthropic request contains an image block. func requestHasImages(ar *AnthropicRequest) bool { for _, m := range ar.Messages { for _, block := range m.Content { if block.Type == "image" { return true } } } return false } // ---------- OpenAI → Anthropic response conversion ---------- // OpenAIToAnthropicResponse converts a non-streaming OpenAI response to Anthropic format. func OpenAIToAnthropicResponse(oaResp *OpenAIChatResponse, model string) *AnthropicResponse { ar := &AnthropicResponse{ ID: oaResp.ID, Type: "message", Role: "assistant", Model: model, Content: make([]AnthropicContent, 0), } if len(oaResp.Choices) > 0 { choice := oaResp.Choices[0] msg := choice.Message // Extract text content if text, ok := msg.Content.(string); ok && text != "" { ar.Content = append(ar.Content, AnthropicContent{ Type: "text", Text: text, }) } // Extract tool calls for _, tc := range msg.ToolCalls { ar.Content = append(ar.Content, AnthropicContent{ Type: "tool_use", ID: tc.ID, Name: tc.Function.Name, Input: json.RawMessage(tc.Function.Arguments), }) } // Map finish reason ar.StopReason = mapFinishReason(choice.FinishReason) } // Map usage if oaResp.Usage != nil { ar.Usage = AnthropicUsage{ InputTokens: oaResp.Usage.PromptTokens, OutputTokens: oaResp.Usage.CompletionTokens, } } return ar } func mapFinishReason(oaReason string) string { switch oaReason { case "stop": return "end_turn" case "length": return "max_tokens" case "tool_calls": return "tool_use" case "content_filter": return "end_turn" default: return "end_turn" } } // ---------- OpenAI SSE → Anthropic SSE streaming ---------- // ---------- Anthropic SSE state machine ---------- type anthropicSSEState struct { w http.ResponseWriter flusher http.Flusher model string msgID string messageStarted bool finished bool inputTokens int outputTokens int textBlockIdx int // -1 = not started nextBlockIdx int // next sequential block index // toolCallIdx maps OpenAI tool call index → anthropic block state toolByOpenAIIdx map[int]*anthropicToolBlock // openBlocks tracks blocks in open order for sequential close openBlocks []int // values are anthropic block indices } type anthropicToolBlock struct { anthropicIdx int id string name string } func newAnthropicSSEState(w http.ResponseWriter, flusher http.Flusher, model, requestID string) *anthropicSSEState { return &anthropicSSEState{ w: w, flusher: flusher, model: model, msgID: fmt.Sprintf("msg_%s", requestID), textBlockIdx: -1, toolByOpenAIIdx: make(map[int]*anthropicToolBlock), } } func (s *anthropicSSEState) startMessage() { writeAnthropicSSE(s.w, anthropicSSE{ Type: "message_start", Message: &anthropicSSEMessage{ ID: s.msgID, Type: "message", Role: "assistant", Model: s.model, Content: []AnthropicContent{}, }, }, s.flusher) s.messageStarted = true } func (s *anthropicSSEState) ensureTextBlock() { if s.textBlockIdx >= 0 { return } s.textBlockIdx = s.nextBlockIdx s.nextBlockIdx++ s.openBlocks = append(s.openBlocks, s.textBlockIdx) writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_start", Index: &s.textBlockIdx, ContentBlock: &struct { Type string `json:"type"` Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` }{Type: "text"}, }, s.flusher) } func (s *anthropicSSEState) handleText(text string) { s.ensureTextBlock() writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_delta", Index: &s.textBlockIdx, Delta: &struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` PartialJSON string `json:"partial_json,omitempty"` StopReason string `json:"stop_reason,omitempty"` StopSequence string `json:"stop_sequence,omitempty"` }{Type: "text_delta", Text: text}, }, s.flusher) } func (s *anthropicSSEState) handleToolCall(openaiIdx int, tc map[string]any) { id, _ := tc["id"].(string) fn, _ := tc["function"].(map[string]any) name, _ := fn["name"].(string) args, _ := fn["arguments"].(string) block, exists := s.toolByOpenAIIdx[openaiIdx] if !exists { // Close text block if open — text must precede all tool blocks if s.textBlockIdx >= 0 { writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_stop", Index: &s.textBlockIdx, }, s.flusher) // textBlockIdx stays set (don't reopen) } block = &anthropicToolBlock{ anthropicIdx: s.nextBlockIdx, id: id, name: name, } s.nextBlockIdx++ s.toolByOpenAIIdx[openaiIdx] = block s.openBlocks = append(s.openBlocks, block.anthropicIdx) writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_start", Index: &block.anthropicIdx, ContentBlock: &struct { Type string `json:"type"` Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` }{Type: "tool_use", Name: block.name, ID: block.id}, }, s.flusher) } else { // Update id/name if present (first chunk may have both, later only args) if id != "" { block.id = id } if name != "" { block.name = name } } if args != "" { writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_delta", Index: &block.anthropicIdx, Delta: &struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` PartialJSON string `json:"partial_json,omitempty"` StopReason string `json:"stop_reason,omitempty"` StopSequence string `json:"stop_sequence,omitempty"` }{Type: "input_json_delta", PartialJSON: args}, }, s.flusher) } } func (s *anthropicSSEState) finish(stopReason string) { // Close all open blocks in order for _, blockIdx := range s.openBlocks { writeAnthropicSSE(s.w, anthropicSSE{ Type: "content_block_stop", Index: &blockIdx, }, s.flusher) } s.openBlocks = nil writeAnthropicSSE(s.w, anthropicSSE{ Type: "message_delta", Delta: &struct { Type string `json:"type,omitempty"` Text string `json:"text,omitempty"` PartialJSON string `json:"partial_json,omitempty"` StopReason string `json:"stop_reason,omitempty"` StopSequence string `json:"stop_sequence,omitempty"` }{StopReason: stopReason}, Usage: &AnthropicUsage{ InputTokens: s.inputTokens, OutputTokens: s.outputTokens, }, }, s.flusher) writeAnthropicSSE(s.w, anthropicSSE{Type: "message_stop"}, s.flusher) s.finished = true } // StreamOpenAIAsAnthropic reads OpenAI SSE chunks and converts them to Anthropic SSE events. func StreamOpenAIAsAnthropic(w http.ResponseWriter, resp *http.Response, model string, requestID string) error { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("anthropic-version", "2023-06-01") w.Header().Set("x-request-id", requestID) w.WriteHeader(200) flusher, ok := w.(http.Flusher) if !ok { return fmt.Errorf("streaming not supported") } scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) state := newAnthropicSSEState(w, flusher, model, requestID) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { if !state.finished { state.finish("end_turn") } continue } var chunk map[string]any if err := json.Unmarshal([]byte(data), &chunk); err != nil { continue } choices, _ := chunk["choices"].([]any) if len(choices) == 0 { if u, ok := chunk["usage"].(map[string]any); ok { if it, ok := u["prompt_tokens"].(float64); ok { state.inputTokens = int(it) } if ot, ok := u["completion_tokens"].(float64); ok { state.outputTokens = int(ot) } } continue } choice := choices[0].(map[string]any) delta, _ := choice["delta"].(map[string]any) if !state.messageStarted { state.startMessage() } if content, ok := delta["content"].(string); ok && content != "" { state.handleText(content) } if tcs, ok := delta["tool_calls"].([]any); ok { for _, tcAny := range tcs { tc := tcAny.(map[string]any) idxFloat, ok := tc["index"].(float64) if !ok { continue } state.handleToolCall(int(idxFloat), tc) } } if fr, ok := choice["finish_reason"].(string); ok && fr != "" && !state.finished { state.finish(mapFinishReason(fr)) } } if _, err := fmt.Fprint(w, ""); err != nil { return nil } flusher.Flush() return nil } func writeAnthropicSSE(w io.Writer, event anthropicSSE, flusher http.Flusher) { data, _ := json.Marshal(event) fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, data) if flusher != nil { flusher.Flush() } } // ---------- Forwarding for Anthropic ---------- // ForwardAnthropicMessages converts an Anthropic request, forwards to Zen, and returns Anthropic response. func (p *Proxy) ForwardAnthropicMessages(w http.ResponseWriter, r *http.Request, body []byte) error { var ar AnthropicRequest if err := json.Unmarshal(body, &ar); err != nil { return fmt.Errorf("invalid Anthropic request: %w", err) } // Resolve model resolved, _ := p.ResolveModel(ar.Model) // Keep original client-requested model name for response rewriting. // Claude Code validates that the response model matches the request model. clientModel := ar.Model // Auto-route to vision model if the request has images and resolved model can't handle them hasImages := requestHasImages(&ar) if hasImages { mi := ModelByID(resolved) if mi == nil || !mi.SupportsVision { fmt.Printf("[anthropic] model=%s lacks vision, auto-routing to mimo-v2.5-free\n", resolved) resolved = "mimo-v2.5-free" } } fmt.Printf("[anthropic] model=%s → %s, max_tokens=%d, stream=%v, messages=%d, tools=%d\n", clientModel, resolved, ar.MaxTokens, ar.Stream, len(ar.Messages), len(ar.Tools)) // Convert Anthropic → OpenAI oaReq := AnthropicToOpenAI(&ar) oaReq.Model = resolved oaBody, err := json.Marshal(oaReq) if err != nil { return fmt.Errorf("failed to marshal OpenAI request: %w", err) } fmt.Printf("[anthropic] → openai request (%d bytes)\n", len(oaBody)) // Generate request/session IDs requestID := RequestID() sessionID := p.SessionID("anthropic") // Build upstream request upstreamReq, err := http.NewRequestWithContext(r.Context(), "POST", ZenChatURL(), bytes.NewReader(oaBody)) if err != nil { return fmt.Errorf("failed to create upstream request: %w", err) } for k, v := range UpstreamHeaders(requestID, sessionID) { upstreamReq.Header.Set(k, v) } // Execute client := &http.Client{Timeout: 10 * time.Minute} resp, err := client.Do(upstreamReq) if err != nil { return fmt.Errorf("upstream request failed: %w", err) } defer resp.Body.Close() fmt.Printf("[anthropic] upstream status=%d, content-type=%s\n", resp.StatusCode, resp.Header.Get("Content-Type")) if resp.StatusCode != 200 { bodyBytes, _ := io.ReadAll(resp.Body) fmt.Printf("[anthropic] upstream error body (%d bytes): %s\n", len(bodyBytes), string(bodyBytes)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(resp.StatusCode) json.NewEncoder(w).Encode(AnthropicError{ Type: "error", Error: AnthropicErrorBody{ Type: "api_error", Message: string(bodyBytes), }, }) return nil } // Handle streaming vs non-streaming if ar.Stream { return StreamOpenAIAsAnthropic(w, resp, clientModel, requestID) } // Non-streaming: collect full response and convert return p.anthropicNonStreamResponse(w, resp, clientModel) } func (p *Proxy) anthropicNonStreamResponse(w http.ResponseWriter, resp *http.Response, model string) error { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read upstream: %w", err) } contentType := resp.Header.Get("Content-Type") if strings.Contains(contentType, "text/event-stream") { // Accumulate SSE chunks into a single OpenAI response, then convert var accumulated OpenAIChatResponse accumulated.ID = "msg_" + RequestID() accumulated.Object = "chat.completion" accumulated.Created = time.Now().Unix() accumulated.Model = model accumulated.Choices = []OpenAIChoice{{ Index: 0, Message: OpenAIMessage{ Role: "assistant", }, }} var contentBuf strings.Builder toolCallsByIndex := make(map[int]*OpenAIToolCall) var toolIndices []int scanner := bufio.NewScanner(bytes.NewReader(bodyBytes)) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { continue } var chunk map[string]any if err := json.Unmarshal([]byte(data), &chunk); err != nil { continue } if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 { choice := choices[0].(map[string]any) if delta, ok := choice["delta"].(map[string]any); ok { if content, ok := delta["content"].(string); ok { contentBuf.WriteString(content) } if tcs, ok := delta["tool_calls"].([]any); ok && len(tcs) > 0 { for _, tcAny := range tcs { tc := tcAny.(map[string]any) idxFloat, ok := tc["index"].(float64) if !ok { continue } idx := int(idxFloat) fn, _ := tc["function"].(map[string]any) existing, exists := toolCallsByIndex[idx] if !exists { id, _ := tc["id"].(string) name, _ := fn["name"].(string) existing = &OpenAIToolCall{ ID: id, Type: "function", Function: OpenAIToolFunc{ Name: name, Arguments: "", }, } toolCallsByIndex[idx] = existing toolIndices = append(toolIndices, idx) } if name, ok := fn["name"].(string); ok && name != "" { existing.Function.Name = name } if args, ok := fn["arguments"].(string); ok { existing.Function.Arguments += args } if id, ok := tc["id"].(string); ok && id != "" { existing.ID = id } } } } if fr, ok := choice["finish_reason"].(string); ok && fr != "" { accumulated.Choices[0].FinishReason = fr } } if u, ok := chunk["usage"].(map[string]any); ok { accumulated.Usage = &OpenAIUsage{} if it, ok := u["prompt_tokens"].(float64); ok { accumulated.Usage.PromptTokens = int(it) } if ot, ok := u["completion_tokens"].(float64); ok { accumulated.Usage.CompletionTokens = int(ot) } accumulated.Usage.TotalTokens = accumulated.Usage.PromptTokens + accumulated.Usage.CompletionTokens } } accumulated.Choices[0].Message.Content = contentBuf.String() sort.Ints(toolIndices) for _, idx := range toolIndices { accumulated.Choices[0].Message.ToolCalls = append(accumulated.Choices[0].Message.ToolCalls, *toolCallsByIndex[idx]) } if accumulated.Choices[0].FinishReason == "" { accumulated.Choices[0].FinishReason = "stop" } anthroResp := OpenAIToAnthropicResponse(&accumulated, model) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(anthroResp) } // Direct JSON response from upstream var oaResp OpenAIChatResponse if err := json.Unmarshal(bodyBytes, &oaResp); err != nil { // Pass through if not valid OpenAI JSON w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write(bodyBytes) return nil } anthroResp := OpenAIToAnthropicResponse(&oaResp, model) w.Header().Set("Content-Type", "application/json") w.Header().Set("anthropic-version", "2023-06-01") w.Header().Set("x-request-id", "req_"+RequestID()) w.WriteHeader(200) return json.NewEncoder(w).Encode(anthroResp) }