mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
- Comment out verbose routing logs in the API server to reduce noise. - Remove the `tools` field from Qwen client requests when it is an empty array. - Add guards in Claude, Codex, Gemini‑CLI, and Gemini translators to skip tool conversion when the `tools` array is empty, preventing unnecessary payload modifications.
269 lines
8.8 KiB
Go
269 lines
8.8 KiB
Go
// Package openai provides utilities to translate OpenAI Chat Completions
|
|
// request JSON into OpenAI Responses API request JSON using gjson/sjson.
|
|
// It supports tools, multimodal text/image inputs, and Structured Outputs.
|
|
// The package handles the conversion of OpenAI API requests into the format
|
|
// expected by the OpenAI Responses API, including proper mapping of messages,
|
|
// tools, and generation parameters.
|
|
package openai
|
|
|
|
import (
|
|
"github.com/luispater/CLIProxyAPI/internal/misc"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// ConvertOpenAIRequestToCodex converts an OpenAI Chat Completions request JSON
|
|
// into an OpenAI Responses API request JSON. The transformation follows the
|
|
// examples defined in docs/2.md exactly, including tools, multi-turn dialog,
|
|
// multimodal text/image handling, and Structured Outputs mapping.
|
|
//
|
|
// Parameters:
|
|
// - modelName: The name of the model to use for the request
|
|
// - rawJSON: The raw JSON request data from the OpenAI Chat Completions API
|
|
// - stream: A boolean indicating if the request is for a streaming response
|
|
//
|
|
// Returns:
|
|
// - []byte: The transformed request data in OpenAI Responses API format
|
|
func ConvertOpenAIRequestToCodex(modelName string, rawJSON []byte, stream bool) []byte {
|
|
// Start with empty JSON object
|
|
out := `{}`
|
|
store := false
|
|
|
|
// Stream must be set to true
|
|
out, _ = sjson.Set(out, "stream", stream)
|
|
|
|
// Codex not support temperature, top_p, top_k, max_output_tokens, so comment them
|
|
// if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() {
|
|
// out, _ = sjson.Set(out, "temperature", v.Value())
|
|
// }
|
|
// if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() {
|
|
// out, _ = sjson.Set(out, "top_p", v.Value())
|
|
// }
|
|
// if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() {
|
|
// out, _ = sjson.Set(out, "top_k", v.Value())
|
|
// }
|
|
|
|
// Map token limits
|
|
// if v := gjson.GetBytes(rawJSON, "max_tokens"); v.Exists() {
|
|
// out, _ = sjson.Set(out, "max_output_tokens", v.Value())
|
|
// }
|
|
// if v := gjson.GetBytes(rawJSON, "max_completion_tokens"); v.Exists() {
|
|
// out, _ = sjson.Set(out, "max_output_tokens", v.Value())
|
|
// }
|
|
|
|
// Map reasoning effort
|
|
if v := gjson.GetBytes(rawJSON, "reasoning_effort"); v.Exists() {
|
|
out, _ = sjson.Set(out, "reasoning.effort", v.Value())
|
|
out, _ = sjson.Set(out, "reasoning.summary", "auto")
|
|
}
|
|
|
|
// Model
|
|
out, _ = sjson.Set(out, "model", modelName)
|
|
|
|
// Extract system instructions from first system message (string or text object)
|
|
messages := gjson.GetBytes(rawJSON, "messages")
|
|
instructions := misc.CodexInstructions
|
|
out, _ = sjson.SetRaw(out, "instructions", instructions)
|
|
// if messages.IsArray() {
|
|
// arr := messages.Array()
|
|
// for i := 0; i < len(arr); i++ {
|
|
// m := arr[i]
|
|
// if m.Get("role").String() == "system" {
|
|
// c := m.Get("content")
|
|
// if c.Type == gjson.String {
|
|
// out, _ = sjson.Set(out, "instructions", c.String())
|
|
// } else if c.IsObject() && c.Get("type").String() == "text" {
|
|
// out, _ = sjson.Set(out, "instructions", c.Get("text").String())
|
|
// }
|
|
// break
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// Build input from messages, handling all message types including tool calls
|
|
out, _ = sjson.SetRaw(out, "input", `[]`)
|
|
if messages.IsArray() {
|
|
arr := messages.Array()
|
|
for i := 0; i < len(arr); i++ {
|
|
m := arr[i]
|
|
role := m.Get("role").String()
|
|
|
|
switch role {
|
|
case "tool":
|
|
// Handle tool response messages as top-level function_call_output objects
|
|
toolCallID := m.Get("tool_call_id").String()
|
|
content := m.Get("content").String()
|
|
|
|
// Create function_call_output object
|
|
funcOutput := `{}`
|
|
funcOutput, _ = sjson.Set(funcOutput, "type", "function_call_output")
|
|
funcOutput, _ = sjson.Set(funcOutput, "call_id", toolCallID)
|
|
funcOutput, _ = sjson.Set(funcOutput, "output", content)
|
|
out, _ = sjson.SetRaw(out, "input.-1", funcOutput)
|
|
|
|
default:
|
|
// Handle regular messages
|
|
msg := `{}`
|
|
msg, _ = sjson.Set(msg, "type", "message")
|
|
if role == "system" {
|
|
msg, _ = sjson.Set(msg, "role", "user")
|
|
} else {
|
|
msg, _ = sjson.Set(msg, "role", role)
|
|
}
|
|
|
|
msg, _ = sjson.SetRaw(msg, "content", `[]`)
|
|
|
|
// Handle regular content
|
|
c := m.Get("content")
|
|
if c.Exists() && c.Type == gjson.String && c.String() != "" {
|
|
// Single string content
|
|
partType := "input_text"
|
|
if role == "assistant" {
|
|
partType = "output_text"
|
|
}
|
|
part := `{}`
|
|
part, _ = sjson.Set(part, "type", partType)
|
|
part, _ = sjson.Set(part, "text", c.String())
|
|
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
|
} else if c.Exists() && c.IsArray() {
|
|
items := c.Array()
|
|
for j := 0; j < len(items); j++ {
|
|
it := items[j]
|
|
t := it.Get("type").String()
|
|
switch t {
|
|
case "text":
|
|
partType := "input_text"
|
|
if role == "assistant" {
|
|
partType = "output_text"
|
|
}
|
|
part := `{}`
|
|
part, _ = sjson.Set(part, "type", partType)
|
|
part, _ = sjson.Set(part, "text", it.Get("text").String())
|
|
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
|
case "image_url":
|
|
// Map image inputs to input_image for Responses API
|
|
if role == "user" {
|
|
part := `{}`
|
|
part, _ = sjson.Set(part, "type", "input_image")
|
|
if u := it.Get("image_url.url"); u.Exists() {
|
|
part, _ = sjson.Set(part, "image_url", u.String())
|
|
}
|
|
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
|
}
|
|
case "file":
|
|
// Files are not specified in examples; skip for now
|
|
}
|
|
}
|
|
}
|
|
|
|
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
|
|
|
// Handle tool calls for assistant messages as separate top-level objects
|
|
if role == "assistant" {
|
|
toolCalls := m.Get("tool_calls")
|
|
if toolCalls.Exists() && toolCalls.IsArray() {
|
|
toolCallsArr := toolCalls.Array()
|
|
for j := 0; j < len(toolCallsArr); j++ {
|
|
tc := toolCallsArr[j]
|
|
if tc.Get("type").String() == "function" {
|
|
// Create function_call as top-level object
|
|
funcCall := `{}`
|
|
funcCall, _ = sjson.Set(funcCall, "type", "function_call")
|
|
funcCall, _ = sjson.Set(funcCall, "call_id", tc.Get("id").String())
|
|
funcCall, _ = sjson.Set(funcCall, "name", tc.Get("function.name").String())
|
|
funcCall, _ = sjson.Set(funcCall, "arguments", tc.Get("function.arguments").String())
|
|
out, _ = sjson.SetRaw(out, "input.-1", funcCall)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Map response_format and text settings to Responses API text.format
|
|
rf := gjson.GetBytes(rawJSON, "response_format")
|
|
text := gjson.GetBytes(rawJSON, "text")
|
|
if rf.Exists() {
|
|
// Always create text object when response_format provided
|
|
if !gjson.Get(out, "text").Exists() {
|
|
out, _ = sjson.SetRaw(out, "text", `{}`)
|
|
}
|
|
|
|
rft := rf.Get("type").String()
|
|
switch rft {
|
|
case "text":
|
|
out, _ = sjson.Set(out, "text.format.type", "text")
|
|
case "json_schema":
|
|
js := rf.Get("json_schema")
|
|
if js.Exists() {
|
|
out, _ = sjson.Set(out, "text.format.type", "json_schema")
|
|
if v := js.Get("name"); v.Exists() {
|
|
out, _ = sjson.Set(out, "text.format.name", v.Value())
|
|
}
|
|
if v := js.Get("strict"); v.Exists() {
|
|
out, _ = sjson.Set(out, "text.format.strict", v.Value())
|
|
}
|
|
if v := js.Get("schema"); v.Exists() {
|
|
out, _ = sjson.SetRaw(out, "text.format.schema", v.Raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Map verbosity if provided
|
|
if text.Exists() {
|
|
if v := text.Get("verbosity"); v.Exists() {
|
|
out, _ = sjson.Set(out, "text.verbosity", v.Value())
|
|
}
|
|
}
|
|
|
|
// The examples include store: true when response_format is provided
|
|
store = true
|
|
} else if text.Exists() {
|
|
// If only text.verbosity present (no response_format), map verbosity
|
|
if v := text.Get("verbosity"); v.Exists() {
|
|
if !gjson.Get(out, "text").Exists() {
|
|
out, _ = sjson.SetRaw(out, "text", `{}`)
|
|
}
|
|
out, _ = sjson.Set(out, "text.verbosity", v.Value())
|
|
}
|
|
}
|
|
|
|
// Map tools (flatten function fields)
|
|
tools := gjson.GetBytes(rawJSON, "tools")
|
|
if tools.IsArray() && len(tools.Array()) > 0 {
|
|
out, _ = sjson.SetRaw(out, "tools", `[]`)
|
|
arr := tools.Array()
|
|
for i := 0; i < len(arr); i++ {
|
|
t := arr[i]
|
|
if t.Get("type").String() == "function" {
|
|
item := `{}`
|
|
item, _ = sjson.Set(item, "type", "function")
|
|
fn := t.Get("function")
|
|
if fn.Exists() {
|
|
if v := fn.Get("name"); v.Exists() {
|
|
item, _ = sjson.Set(item, "name", v.Value())
|
|
}
|
|
if v := fn.Get("description"); v.Exists() {
|
|
item, _ = sjson.Set(item, "description", v.Value())
|
|
}
|
|
if v := fn.Get("parameters"); v.Exists() {
|
|
item, _ = sjson.SetRaw(item, "parameters", v.Raw)
|
|
}
|
|
if v := fn.Get("strict"); v.Exists() {
|
|
item, _ = sjson.Set(item, "strict", v.Value())
|
|
}
|
|
}
|
|
out, _ = sjson.SetRaw(out, "tools.-1", item)
|
|
}
|
|
}
|
|
// The examples include store: true when tools and formatting are used; be conservative
|
|
if rf.Exists() {
|
|
store = true
|
|
}
|
|
}
|
|
|
|
out, _ = sjson.Set(out, "store", store)
|
|
return []byte(out)
|
|
}
|