package apicompat import ( "encoding/json" "fmt" "strings" ) type chatMessageContent struct { Text *string Parts []ChatContentPart } // ChatCompletionsToResponses converts a Chat Completions request into a // Responses API request. The upstream always streams, so Stream is forced to // true. store is always false and reasoning.encrypted_content is always // included so that the response translator has full context. func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest, error) { input, err := convertChatMessagesToResponsesInput(req.Messages) if err != nil { return nil, err } inputJSON, err := json.Marshal(input) if err != nil { return nil, err } out := &ResponsesRequest{ Model: req.Model, Input: inputJSON, Temperature: req.Temperature, TopP: req.TopP, Stream: true, // upstream always streams Include: []string{"reasoning.encrypted_content"}, ServiceTier: req.ServiceTier, } storeFalse := false out.Store = &storeFalse // max_tokens / max_completion_tokens → max_output_tokens, prefer max_completion_tokens maxTokens := 0 if req.MaxTokens != nil { maxTokens = *req.MaxTokens } if req.MaxCompletionTokens != nil { maxTokens = *req.MaxCompletionTokens } if maxTokens > 0 { v := maxTokens if v < minMaxOutputTokens { v = minMaxOutputTokens } out.MaxOutputTokens = &v } // reasoning_effort → reasoning.effort + reasoning.summary="auto" if req.ReasoningEffort != "" { out.Reasoning = &ResponsesReasoning{ Effort: req.ReasoningEffort, Summary: "auto", } } // tools[] and legacy functions[] → ResponsesTool[] if len(req.Tools) > 0 || len(req.Functions) > 0 { out.Tools = convertChatToolsToResponses(req.Tools, req.Functions) } // tool_choice: already compatible format — pass through directly. // Legacy function_call needs mapping. if len(req.ToolChoice) > 0 { out.ToolChoice = req.ToolChoice } else if len(req.FunctionCall) > 0 { tc, err := convertChatFunctionCallToToolChoice(req.FunctionCall) if err != nil { return nil, fmt.Errorf("convert function_call: %w", err) } out.ToolChoice = tc } return out, nil } // convertChatMessagesToResponsesInput converts the Chat Completions messages // array into a Responses API input items array. func convertChatMessagesToResponsesInput(msgs []ChatMessage) ([]ResponsesInputItem, error) { var out []ResponsesInputItem for _, m := range msgs { items, err := chatMessageToResponsesItems(m) if err != nil { return nil, err } out = append(out, items...) } return out, nil } // chatMessageToResponsesItems converts a single ChatMessage into one or more // ResponsesInputItem values. func chatMessageToResponsesItems(m ChatMessage) ([]ResponsesInputItem, error) { switch m.Role { case "system": return chatSystemToResponses(m) case "user": return chatUserToResponses(m) case "assistant": return chatAssistantToResponses(m) case "tool": return chatToolToResponses(m) case "function": return chatFunctionToResponses(m) default: return chatUserToResponses(m) } } // chatSystemToResponses converts a system message. func chatSystemToResponses(m ChatMessage) ([]ResponsesInputItem, error) { parsed, err := parseChatMessageContent(m.Content) if err != nil { return nil, err } content, err := marshalChatInputContent(parsed) if err != nil { return nil, err } return []ResponsesInputItem{{Role: "system", Content: content}}, nil } // chatUserToResponses converts a user message, handling both plain strings and // multi-modal content arrays. func chatUserToResponses(m ChatMessage) ([]ResponsesInputItem, error) { parsed, err := parseChatMessageContent(m.Content) if err != nil { return nil, fmt.Errorf("parse user content: %w", err) } content, err := marshalChatInputContent(parsed) if err != nil { return nil, err } return []ResponsesInputItem{{Role: "user", Content: content}}, nil } // chatAssistantToResponses converts an assistant message. If there is both // text content and tool_calls, the text is emitted as an assistant message // first, then each tool_call becomes a function_call item. If the content is // empty/nil and there are tool_calls, only function_call items are emitted. func chatAssistantToResponses(m ChatMessage) ([]ResponsesInputItem, error) { var items []ResponsesInputItem // Emit assistant message with output_text if content is non-empty. if len(m.Content) > 0 { s, err := parseAssistantContent(m.Content) if err != nil { return nil, err } if s != "" { parts := []ResponsesContentPart{{Type: "output_text", Text: s}} partsJSON, err := json.Marshal(parts) if err != nil { return nil, err } items = append(items, ResponsesInputItem{Role: "assistant", Content: partsJSON}) } } // Emit one function_call item per tool_call. for _, tc := range m.ToolCalls { args := tc.Function.Arguments if args == "" { args = "{}" } items = append(items, ResponsesInputItem{ Type: "function_call", CallID: tc.ID, Name: tc.Function.Name, Arguments: args, }) } return items, nil } // parseAssistantContent returns assistant content as plain text. // // Supported formats: // - JSON string // - JSON array of typed parts (e.g. [{"type":"text","text":"..."}]) // // For structured thinking/reasoning parts, it preserves semantics by wrapping // the text in explicit tags so downstream can still distinguish it from normal text. func parseAssistantContent(raw json.RawMessage) (string, error) { if len(raw) == 0 { return "", nil } var s string if err := json.Unmarshal(raw, &s); err == nil { return s, nil } var parts []map[string]any if err := json.Unmarshal(raw, &parts); err != nil { // Keep compatibility with prior behavior: unsupported assistant content // formats are ignored instead of failing the whole request conversion. return "", nil } var b strings.Builder write := func(v string) error { _, err := b.WriteString(v) return err } for _, p := range parts { typ, _ := p["type"].(string) text, _ := p["text"].(string) thinking, _ := p["thinking"].(string) switch typ { case "thinking", "reasoning": if thinking != "" { if err := write(""); err != nil { return "", err } if err := write(thinking); err != nil { return "", err } if err := write(""); err != nil { return "", err } } else if text != "" { if err := write(""); err != nil { return "", err } if err := write(text); err != nil { return "", err } if err := write(""); err != nil { return "", err } } default: if text != "" { if err := write(text); err != nil { return "", err } } } } return b.String(), nil } // chatToolToResponses converts a tool result message (role=tool) into a // function_call_output item. func chatToolToResponses(m ChatMessage) ([]ResponsesInputItem, error) { output, err := parseChatContent(m.Content) if err != nil { return nil, err } if output == "" { output = "(empty)" } return []ResponsesInputItem{{ Type: "function_call_output", CallID: m.ToolCallID, Output: output, }}, nil } // chatFunctionToResponses converts a legacy function result message // (role=function) into a function_call_output item. The Name field is used as // call_id since legacy function calls do not carry a separate call_id. func chatFunctionToResponses(m ChatMessage) ([]ResponsesInputItem, error) { output, err := parseChatContent(m.Content) if err != nil { return nil, err } if output == "" { output = "(empty)" } return []ResponsesInputItem{{ Type: "function_call_output", CallID: m.Name, Output: output, }}, nil } // parseChatContent returns the string value of a ChatMessage Content field. // Content can be a JSON string or an array of typed parts. Array content is // flattened to text by concatenating text parts and ignoring non-text parts. func parseChatContent(raw json.RawMessage) (string, error) { parsed, err := parseChatMessageContent(raw) if err != nil { return "", err } if parsed.Text != nil { return *parsed.Text, nil } return flattenChatContentParts(parsed.Parts), nil } func parseChatMessageContent(raw json.RawMessage) (chatMessageContent, error) { if len(raw) == 0 { return chatMessageContent{Text: stringPtr("")}, nil } var s string if err := json.Unmarshal(raw, &s); err == nil { return chatMessageContent{Text: &s}, nil } var parts []ChatContentPart if err := json.Unmarshal(raw, &parts); err == nil { return chatMessageContent{Parts: parts}, nil } return chatMessageContent{}, fmt.Errorf("parse content as string or parts array") } func marshalChatInputContent(content chatMessageContent) (json.RawMessage, error) { if content.Text != nil { return json.Marshal(*content.Text) } return json.Marshal(convertChatContentPartsToResponses(content.Parts)) } func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesContentPart { var responseParts []ResponsesContentPart for _, p := range parts { switch p.Type { case "text": if p.Text != "" { responseParts = append(responseParts, ResponsesContentPart{ Type: "input_text", Text: p.Text, }) } case "image_url": if p.ImageURL != nil && p.ImageURL.URL != "" { responseParts = append(responseParts, ResponsesContentPart{ Type: "input_image", ImageURL: p.ImageURL.URL, }) } } } return responseParts } func flattenChatContentParts(parts []ChatContentPart) string { var textParts []string for _, p := range parts { if p.Type == "text" && p.Text != "" { textParts = append(textParts, p.Text) } } return strings.Join(textParts, "") } func stringPtr(s string) *string { return &s } // convertChatToolsToResponses maps Chat Completions tool definitions and legacy // function definitions to Responses API tool definitions. func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []ResponsesTool { var out []ResponsesTool for _, t := range tools { if t.Type != "function" || t.Function == nil { continue } rt := ResponsesTool{ Type: "function", Name: t.Function.Name, Description: t.Function.Description, Parameters: t.Function.Parameters, Strict: t.Function.Strict, } out = append(out, rt) } // Legacy functions[] are treated as function-type tools. for _, f := range functions { rt := ResponsesTool{ Type: "function", Name: f.Name, Description: f.Description, Parameters: f.Parameters, Strict: f.Strict, } out = append(out, rt) } return out } // convertChatFunctionCallToToolChoice maps the legacy function_call field to a // Responses API tool_choice value. // // "auto" → "auto" // "none" → "none" // {"name":"X"} → {"type":"function","function":{"name":"X"}} func convertChatFunctionCallToToolChoice(raw json.RawMessage) (json.RawMessage, error) { // Try string first ("auto", "none", etc.) — pass through as-is. var s string if err := json.Unmarshal(raw, &s); err == nil { return json.Marshal(s) } // Object form: {"name":"X"} var obj struct { Name string `json:"name"` } if err := json.Unmarshal(raw, &obj); err != nil { return nil, err } return json.Marshal(map[string]any{ "type": "function", "function": map[string]string{"name": obj.Name}, }) }