package convert import ( "encoding/json" "openteam/server/internal/proxy/claude" "openteam/server/internal/proxy/openai" ) // ClientProtocol mirrors the gateway's Protocol but kept here to avoid an // import cycle with the proxy package. type ClientProtocol string const ( ClientOpenAIChat ClientProtocol = "openai-chat" ClientOpenAIResponses ClientProtocol = "openai-responses" ClientAnthropic ClientProtocol = "anthropic" ) // Request converts a request body from the client protocol to the channel // provider's native format. func Request(client ClientProtocol, provider string, body []byte, upstreamModel string) ([]byte, error) { var canon *CanonicalRequest var err error switch client { case ClientOpenAIChat: canon, err = requestFromOpenAIChat(body) case ClientOpenAIResponses: canon, err = requestFromResponses(body) case ClientAnthropic: canon, err = requestFromClaude(body) default: return nil, jsonError("unsupported client protocol") } if err != nil { return nil, err } canon.Model = upstreamModel if canon.Model == "" { canon.Model = "" } switch provider { case "anthropic": return canonicalToClaude(canon) case "openai", "compatible": return canonicalToOpenAIChat(canon) default: return nil, jsonError("unsupported provider") } } // Response converts a non-stream upstream response into the client protocol. func Response(client ClientProtocol, provider string, body []byte) ([]byte, error) { if client == ClientOpenAIChat && provider == "openai" { return body, nil // passthrough } if client == ClientOpenAIResponses && provider == "openai" { return body, nil } if client == ClientAnthropic && provider == "anthropic" { return body, nil } if provider == "anthropic" { var mr claude.MessageResponse if err := json.Unmarshal(body, &mr); err != nil { return nil, err } switch client { case ClientOpenAIChat: return claudeResponseToChat(&mr) case ClientOpenAIResponses: return claudeResponseToResponses(&mr) } } if provider == "openai" || provider == "compatible" { var cc openai.ChatCompletion if err := json.Unmarshal(body, &cc); err != nil { return nil, err } switch client { case ClientAnthropic: return chatResponseToClaude(&cc) } } return nil, jsonError("no conversion path") } // canonicalToOpenAIChat re-emits the canonical request as OpenAI chat JSON. func canonicalToOpenAIChat(c *CanonicalRequest) ([]byte, error) { obj := map[string]any{ "model": c.Model, "messages": c.Messages, } if c.Stream { obj["stream"] = true } if c.Temperature != nil { obj["temperature"] = *c.Temperature } if c.TopP != nil { obj["top_p"] = *c.TopP } if c.MaxTokens != nil { obj["max_tokens"] = *c.MaxTokens } if len(c.Stop) > 0 { obj["stop"] = c.Stop } if len(c.Tools) > 0 { obj["tools"] = c.Tools } if len(c.ToolChoice) > 0 { obj["tool_choice"] = json.RawMessage(c.ToolChoice) } if len(c.ResponseFormat) > 0 { obj["response_format"] = json.RawMessage(c.ResponseFormat) } for k, v := range c.RawOpenAIExtras { obj[k] = json.RawMessage(v) } return json.Marshal(obj) } // canonicalToClaude emits the canonical request as an Anthropic Messages body. func canonicalToClaude(c *CanonicalRequest) ([]byte, error) { body, err := chatToClaudeMessages(c.Messages, c.System) if err != nil { return nil, err } maxTokens := 4096 if c.MaxTokens != nil { maxTokens = *c.MaxTokens } body["model"] = c.Model body["max_tokens"] = maxTokens if c.Stream { body["stream"] = true } if c.Temperature != nil { // Claude clamps temperature to [0,1]. t := *c.Temperature if t > 1 { t = 1 } if t < 0 { t = 0 } body["temperature"] = t } if c.TopP != nil { body["top_p"] = *c.TopP } if len(c.Stop) > 0 { body["stop_sequences"] = c.Stop } if tools := toolsToClaude(c.Tools); len(tools) > 0 { body["tools"] = tools } if len(c.ToolChoice) > 0 { var tc json.RawMessage if err := json.Unmarshal(c.ToolChoice, &tc); err == nil { body["tool_choice"] = tc } } return json.Marshal(body) } // claudeResponseToChat converts a Claude non-stream response to OpenAI chat. func claudeResponseToChat(mr *claude.MessageResponse) ([]byte, error) { var blocks []struct { Type string `json:"type"` Text string `json:"text"` ID string `json:"id"` Name string `json:"name"` Input json.RawMessage `json:"input"` } _ = json.Unmarshal(mr.Content, &blocks) content := "" var toolCalls []map[string]any for _, b := range blocks { switch b.Type { case "text": if content == "" { content = b.Text } else { content += b.Text } case "tool_use": input, _ := json.Marshal(b.Input) toolCalls = append(toolCalls, map[string]any{ "id": b.ID, "type": "function", "function": map[string]any{ "name": b.Name, "arguments": string(input), }, }) } } msg := map[string]any{"role": "assistant", "content": content} if len(toolCalls) > 0 { msg["tool_calls"] = toolCalls } finish := mapClaudeStopReason(mr.StopReason) choices := []any{map[string]any{"index": 0, "message": msg, "finish_reason": finish}} resp := map[string]any{ "id": "chatcmpl-" + mr.ID, "object": "chat.completion", "created": json.Number("0"), "model": mr.Model, "choices": choices, } if mr.Usage != nil { resp["usage"] = map[string]any{ "prompt_tokens": mr.Usage.InputTokens, "completion_tokens": mr.Usage.OutputTokens, "total_tokens": mr.Usage.InputTokens + mr.Usage.OutputTokens, } } return json.Marshal(resp) } // chatResponseToClaude converts an OpenAI non-stream response to Claude. func chatResponseToClaude(cc *openai.ChatCompletion) ([]byte, error) { content := []map[string]any{} if len(cc.Choices) > 0 { ch := cc.Choices[0] if text := contentString(ch.Message.Content); text != "" { content = append(content, map[string]any{"type": "text", "text": text}) } if len(ch.Message.ToolCalls) > 0 { var calls []struct { ID string `json:"id"` Function struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } `json:"function"` } _ = json.Unmarshal(ch.Message.ToolCalls, &calls) for _, call := range calls { var input map[string]any _ = json.Unmarshal(call.Function.Arguments, &input) content = append(content, map[string]any{ "type": "tool_use", "id": call.ID, "name": call.Function.Name, "input": input, }) } } } resp := map[string]any{ "id": mrID(cc.ID), "type": "message", "role": "assistant", "model": cc.Model, "content": content, "stop_reason": mapChatStopReason(cc), "usage": map[string]any{ "input_tokens": usageInt64(cc, true), "output_tokens": usageInt64(cc, false), }, } return json.Marshal(resp) } // claudeResponseToResponses converts a Claude non-stream response to a // Responses API response. func claudeResponseToResponses(mr *claude.MessageResponse) ([]byte, error) { chatCC := &openai.ChatCompletion{ ID: mr.ID, Model: mr.Model, Choices: []openai.ChatChoice{{FinishReason: mapClaudeStopReason(mr.StopReason)}}, } // Reuse the chat conversion then re-shape into responses items. chatBody, err := claudeResponseToChat(mr) if err == nil { var cc openai.ChatCompletion if json.Unmarshal(chatBody, &cc) == nil { chatCC = &cc } } items, status := chatToResponsesOutput(chatCC) resp := map[string]any{ "id": "resp_" + mr.ID, "object": "response", "created": json.Number("0"), "model": mr.Model, "status": status, "output": items, } if mr.Usage != nil { resp["usage"] = map[string]any{ "input_tokens": mr.Usage.InputTokens, "output_tokens": mr.Usage.OutputTokens, "total_tokens": mr.Usage.InputTokens + mr.Usage.OutputTokens, "input_tokens_details": map[string]any{ "cached_tokens": mr.Usage.CacheReadInputTokens, }, } } return json.Marshal(resp) } func mapClaudeStopReason(reason string) string { switch reason { case "end_turn": return "stop" case "max_tokens": return "length" case "stop_sequence": return "stop" case "tool_use": return "tool_calls" case "refusal": return "content_filter" default: return "stop" } } func mapChatStopReason(cc *openai.ChatCompletion) string { if len(cc.Choices) == 0 { return "end_turn" } switch cc.Choices[0].FinishReason { case "stop": return "end_turn" case "length": return "max_tokens" case "tool_calls": return "tool_use" case "content_filter": return "refusal" default: return "end_turn" } } func usageInt64(cc *openai.ChatCompletion, input bool) int64 { if cc.Usage == nil { return 0 } if input { return cc.Usage.PromptTokens } return cc.Usage.CompletionTokens } func mrID(id string) string { if id == "" { return "msg_unknown" } return id }