package openai import ( "encoding/json" "errors" ) // --- Responses API request parsing & conversion --- // ParseResponsesRequest converts a raw OpenAI Responses API (POST /v1/responses) // request body into a ChatCompletionRequest suitable for the upstream gateway. // It translates input→messages, instructions→system message, max_output_tokens→ // max_tokens, and reshapes tools from the Responses flat format to the Chat // Completions nested {function:{…}} format. func ParseResponsesRequest(body []byte) (ChatCompletionRequest, error) { var raw map[string]json.RawMessage if err := json.Unmarshal(body, &raw); err != nil { return ChatCompletionRequest{}, err } var req ChatCompletionRequest if v, ok := raw["model"]; ok { _ = json.Unmarshal(v, &req.Model) } if v, ok := raw["stream"]; ok { _ = json.Unmarshal(v, &req.Stream) } // Build messages from instructions + input. messages := make([]map[string]any, 0, 4) if v, ok := raw["instructions"]; ok { var s string if json.Unmarshal(v, &s) == nil && s != "" { messages = append(messages, map[string]any{"role": "system", "content": s}) } } if v, ok := raw["input"]; ok { msgs, err := responsesInputToMessages(v) if err != nil { return req, err } messages = append(messages, msgs...) } req.Messages = messages // Extra: renamed + pass-through fields sent to upstream as-is. extra := map[string]json.RawMessage{} if v, ok := raw["max_output_tokens"]; ok { extra["max_tokens"] = v } for _, key := range []string{ "temperature", "top_p", "top_k", "frequency_penalty", "presence_penalty", "stop", "seed", "user", } { if v, ok := raw[key]; ok { extra[key] = v } } if v, ok := raw["tools"]; ok { if translated, err := translateResponsesTools(v); err == nil { extra["tools"] = translated } else { extra["tools"] = v // fall back to pass-through } } if v, ok := raw["tool_choice"]; ok { extra["tool_choice"] = v } // Structured Outputs: Responses API uses text.format instead of // response_format. Translate text.format → response_format for the // upstream Chat Completions endpoint. if v, ok := raw["text"]; ok { if rf, err := translateTextFormat(v); err == nil { extra["response_format"] = rf } } req.Extra = extra return req, nil } // translateTextFormat converts the Responses API "text" field (containing a // "format" sub-object) into a Chat Completions response_format value. // - {text:{format:{type:"json_object"}}} → {type:"json_object"} // - {text:{format:{type:"json_schema",name,schema,strict}}} → // {type:"json_schema",json_schema:{name,schema,strict}} func translateTextFormat(textRaw json.RawMessage) (json.RawMessage, error) { var text struct { Format map[string]any `json:"format"` } if err := json.Unmarshal(textRaw, &text); err != nil { return nil, err } if len(text.Format) == 0 { return nil, errors.New("empty text.format") } t, _ := text.Format["type"].(string) switch t { case "json_object": return json.Marshal(map[string]any{"type": "json_object"}) case "json_schema": js := map[string]any{} for _, k := range []string{"name", "schema", "strict"} { if v, ok := text.Format[k]; ok { js[k] = v } } return json.Marshal(map[string]any{"type": "json_schema", "json_schema": js}) default: return json.Marshal(text.Format) // pass through unknown types } } // ResponsesMeta holds fields from the Responses API request that should be // echoed back in the response object (the OpenAI SDK requires them). type ResponsesMeta struct { ParallelToolCalls any ToolChoice any Tools any Temperature any TopP any MaxOutputTokens any } // ParseResponsesMeta extracts echo-back fields from the raw request body. func ParseResponsesMeta(body []byte) ResponsesMeta { var m map[string]any _ = json.Unmarshal(body, &m) meta := ResponsesMeta{ ParallelToolCalls: false, ToolChoice: "auto", Tools: []any{}, } if v, ok := m["parallel_tool_calls"]; ok { meta.ParallelToolCalls = v } if v, ok := m["tool_choice"]; ok { meta.ToolChoice = v } // Tools are echoed back in the Responses API flat format (not the // translated Chat Completions format). if v, ok := m["tools"]; ok { meta.Tools = v } else { meta.Tools = []any{} } if v, ok := m["temperature"]; ok { meta.Temperature = v } if v, ok := m["top_p"]; ok { meta.TopP = v } if v, ok := m["max_output_tokens"]; ok { meta.MaxOutputTokens = v } return meta } // responsesInputToMessages converts the Responses API "input" field (which may // be a plain string or an array of input items) into Chat Completions messages. func responsesInputToMessages(input json.RawMessage) ([]map[string]any, error) { // Case 1: input is a plain string. var s string if json.Unmarshal(input, &s) == nil { return []map[string]any{{"role": "user", "content": s}}, nil } // Case 2: input is an array of items. var items []map[string]json.RawMessage if err := json.Unmarshal(input, &items); err != nil { return nil, err } messages := make([]map[string]any, 0, len(items)) for _, item := range items { // Determine the type — most items are message-like with a role. var role string if v, ok := item["role"]; ok { _ = json.Unmarshal(v, &role) } var itemType string if v, ok := item["type"]; ok { _ = json.Unmarshal(v, &itemType) } switch { case itemType == "function_call": // An assistant tool call from a previous turn. msg := map[string]any{"role": "assistant"} tc := map[string]any{"type": "function"} inner := map[string]any{} if v, ok := item["name"]; ok { var name string _ = json.Unmarshal(v, &name) inner["name"] = name } if v, ok := item["arguments"]; ok { var args string _ = json.Unmarshal(v, &args) inner["arguments"] = args } if v, ok := item["call_id"]; ok { var id string _ = json.Unmarshal(v, &id) tc["id"] = id } tc["function"] = inner msg["tool_calls"] = []any{tc} messages = append(messages, msg) case itemType == "function_call_output": // A tool result from a previous turn. msg := map[string]any{"role": "tool"} if v, ok := item["call_id"]; ok { var id string _ = json.Unmarshal(v, &id) msg["tool_call_id"] = id } if v, ok := item["output"]; ok { var out string _ = json.Unmarshal(v, &out) msg["content"] = out } messages = append(messages, msg) default: // Standard message item with role + content. if role == "" { role = "user" } // "developer" maps to "system" for broad upstream compatibility. if role == "developer" { role = "system" } msg := map[string]any{"role": role} if v, ok := item["content"]; ok { msg["content"] = convertContentParts(v) } else { msg["content"] = "" } messages = append(messages, msg) } } return messages, nil } // convertContentParts converts a Responses API content field (string or array // of content parts) into the Chat Completions content format. func convertContentParts(raw json.RawMessage) any { // Content is a plain string. var s string if json.Unmarshal(raw, &s) == nil { return s } // Content is an array of parts. var parts []map[string]any if json.Unmarshal(raw, &parts) != nil { return string(raw) // fallback } result := make([]map[string]any, 0, len(parts)) for _, p := range parts { pt, _ := p["type"].(string) switch pt { case "input_text", "output_text", "text": result = append(result, map[string]any{"type": "text", "text": p["text"]}) case "input_image": img := map[string]any{"type": "image_url"} if url, ok := p["image_url"]; ok { img["image_url"] = map[string]any{"url": url} } else if d, ok := p["image"]; ok { img["image_url"] = map[string]any{"url": d} } result = append(result, img) default: // Pass through unknown part types as-is. result = append(result, p) } } if len(result) == 0 { return "" } return result } // translateResponsesTools converts tools from the Responses API flat format // to the Chat Completions nested {type:"function",function:{…}} format. func translateResponsesTools(raw json.RawMessage) (json.RawMessage, error) { var tools []map[string]any if err := json.Unmarshal(raw, &tools); err != nil { return nil, err } out := make([]map[string]any, 0, len(tools)) for _, t := range tools { tt, _ := t["type"].(string) if tt != "function" && tt != "" { // Non-function tool types (web_search, file_search, etc.) — // pass through as-is; upstream may or may not support them. out = append(out, t) continue } fn := map[string]any{} for _, key := range []string{"name", "description", "parameters", "strict"} { if v, ok := t[key]; ok { fn[key] = v } } out = append(out, map[string]any{"type": "function", "function": fn}) } return json.Marshal(out) } // --- Responses API response building --- // UsageToResponses converts a Chat Completions usage value (as decoded by // json.Unmarshal into any) to the Responses API usage field names // (input_tokens / output_tokens instead of prompt_tokens / completion_tokens). func UsageToResponses(v any) any { if v == nil { return nil } b, err := json.Marshal(v) if err != nil { return v } var m map[string]any if json.Unmarshal(b, &m) != nil { return v } out := map[string]any{} if pt, ok := m["prompt_tokens"]; ok { out["input_tokens"] = pt } if ct, ok := m["completion_tokens"]; ok { out["output_tokens"] = ct } if tt, ok := m["total_tokens"]; ok { out["total_tokens"] = tt } if d, ok := m["prompt_tokens_details"]; ok { out["input_tokens_details"] = d } else if d, ok := m["input_tokens_details"]; ok { out["input_tokens_details"] = d } if d, ok := m["completion_tokens_details"]; ok { out["output_tokens_details"] = d } else if d, ok := m["output_tokens_details"]; ok { out["output_tokens_details"] = d } return out }