mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Add openai codex support
This commit is contained in:
227
internal/translator/codex/openai/codex_openai_request.go
Normal file
227
internal/translator/codex/openai/codex_openai_request.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Package codex 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.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"github.com/luispater/CLIProxyAPI/internal/misc"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertOpenAIChatRequestToCodex 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.
|
||||
func ConvertOpenAIChatRequestToCodex(rawJSON []byte) string {
|
||||
// Start with empty JSON object
|
||||
out := `{}`
|
||||
store := false
|
||||
|
||||
// Stream must be set to true
|
||||
if v := gjson.GetBytes(rawJSON, "stream"); v.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", true)
|
||||
}
|
||||
|
||||
// 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
|
||||
if v := gjson.GetBytes(rawJSON, "model"); v.Exists() {
|
||||
out, _ = sjson.Set(out, "model", v.Value())
|
||||
}
|
||||
|
||||
// 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, skipping system/tool roles
|
||||
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()
|
||||
if role == "tool" || role == "function" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prepare message object
|
||||
msg := `{}`
|
||||
if role == "system" {
|
||||
msg, _ = sjson.Set(msg, "role", "user")
|
||||
} else {
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
}
|
||||
|
||||
msg, _ = sjson.SetRaw(msg, "content", `[]`)
|
||||
|
||||
c := m.Get("content")
|
||||
if c.Type == gjson.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.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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 out
|
||||
}
|
||||
231
internal/translator/codex/openai/codex_openai_response.go
Normal file
231
internal/translator/codex/openai/codex_openai_response.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Package codex provides response translation functionality for converting between
|
||||
// Codex API response formats and OpenAI-compatible formats. It handles both
|
||||
// streaming and non-streaming responses, transforming backend client responses
|
||||
// into OpenAI Server-Sent Events (SSE) format and standard JSON response formats.
|
||||
// The package supports content translation, function calls, reasoning content,
|
||||
// usage metadata, and various response attributes while maintaining compatibility
|
||||
// with OpenAI API specifications.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
type ConvertCliToOpenAIParams struct {
|
||||
ResponseID string
|
||||
CreatedAt int64
|
||||
Model string
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToOpenAIChat translates a single chunk of a streaming response from the
|
||||
// Codex backend client format to the OpenAI Server-Sent Events (SSE) format.
|
||||
// It returns an empty string if the chunk contains no useful data.
|
||||
func ConvertCodexResponseToOpenAIChat(rawJSON []byte, params *ConvertCliToOpenAIParams) (*ConvertCliToOpenAIParams, string) {
|
||||
// Initialize the OpenAI SSE template.
|
||||
template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
|
||||
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
|
||||
typeResult := rootResult.Get("type")
|
||||
dataType := typeResult.String()
|
||||
if dataType == "response.created" {
|
||||
return &ConvertCliToOpenAIParams{
|
||||
ResponseID: rootResult.Get("response.id").String(),
|
||||
CreatedAt: rootResult.Get("response.created_at").Int(),
|
||||
Model: rootResult.Get("response.model").String(),
|
||||
}, ""
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
return params, ""
|
||||
}
|
||||
|
||||
// Extract and set the model version.
|
||||
if modelResult := gjson.GetBytes(rawJSON, "model"); modelResult.Exists() {
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
}
|
||||
|
||||
template, _ = sjson.Set(template, "created", params.CreatedAt)
|
||||
|
||||
// Extract and set the response ID.
|
||||
template, _ = sjson.Set(template, "id", params.ResponseID)
|
||||
|
||||
// Extract and set usage metadata (token counts).
|
||||
if usageResult := gjson.GetBytes(rawJSON, "response.usage"); usageResult.Exists() {
|
||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int())
|
||||
}
|
||||
if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int())
|
||||
}
|
||||
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
|
||||
}
|
||||
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
|
||||
}
|
||||
}
|
||||
|
||||
if dataType == "response.reasoning_summary_text.delta" {
|
||||
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", deltaResult.String())
|
||||
}
|
||||
} else if dataType == "response.reasoning_summary_text.done" {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", "\n\n")
|
||||
} else if dataType == "response.output_text.delta" {
|
||||
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.Set(template, "choices.0.delta.content", deltaResult.String())
|
||||
}
|
||||
} else if dataType == "response.completed" {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "stop")
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop")
|
||||
} else if dataType == "response.output_item.done" {
|
||||
functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
|
||||
itemResult := rootResult.Get("item")
|
||||
if itemResult.Exists() {
|
||||
if itemResult.Get("type").String() != "function_call" {
|
||||
return params, ""
|
||||
}
|
||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", itemResult.Get("name").String())
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", itemResult.Get("arguments").String())
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
||||
}
|
||||
|
||||
} else {
|
||||
return params, ""
|
||||
}
|
||||
|
||||
return params, template
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToOpenAIChatNonStream aggregates response from the Codex backend client
|
||||
// convert a single, non-streaming OpenAI-compatible JSON response.
|
||||
func ConvertCodexResponseToOpenAIChatNonStream(rawJSON string, unixTimestamp int64) string {
|
||||
template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
|
||||
|
||||
// Extract and set the model version.
|
||||
if modelResult := gjson.Get(rawJSON, "model"); modelResult.Exists() {
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
}
|
||||
|
||||
// Extract and set the creation timestamp.
|
||||
if createdAtResult := gjson.Get(rawJSON, "created_at"); createdAtResult.Exists() {
|
||||
template, _ = sjson.Set(template, "created", createdAtResult.Int())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
||||
}
|
||||
|
||||
// Extract and set the response ID.
|
||||
if idResult := gjson.Get(rawJSON, "id"); idResult.Exists() {
|
||||
template, _ = sjson.Set(template, "id", idResult.String())
|
||||
}
|
||||
|
||||
// Extract and set usage metadata (token counts).
|
||||
if usageResult := gjson.Get(rawJSON, "usage"); usageResult.Exists() {
|
||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int())
|
||||
}
|
||||
if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int())
|
||||
}
|
||||
if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int())
|
||||
}
|
||||
if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int())
|
||||
}
|
||||
}
|
||||
|
||||
// Process the output array for content and function calls
|
||||
outputResult := gjson.Get(rawJSON, "output")
|
||||
if outputResult.IsArray() {
|
||||
outputArray := outputResult.Array()
|
||||
var contentText string
|
||||
var reasoningText string
|
||||
var toolCalls []string
|
||||
|
||||
for _, outputItem := range outputArray {
|
||||
outputType := outputItem.Get("type").String()
|
||||
|
||||
switch outputType {
|
||||
case "reasoning":
|
||||
// Extract reasoning content from summary
|
||||
if summaryResult := outputItem.Get("summary"); summaryResult.IsArray() {
|
||||
summaryArray := summaryResult.Array()
|
||||
for _, summaryItem := range summaryArray {
|
||||
if summaryItem.Get("type").String() == "summary_text" {
|
||||
reasoningText = summaryItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "message":
|
||||
// Extract message content
|
||||
if contentResult := outputItem.Get("content"); contentResult.IsArray() {
|
||||
contentArray := contentResult.Array()
|
||||
for _, contentItem := range contentArray {
|
||||
if contentItem.Get("type").String() == "output_text" {
|
||||
contentText = contentItem.Get("text").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "function_call":
|
||||
// Handle function call content
|
||||
functionCallTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
|
||||
|
||||
if callIdResult := outputItem.Get("call_id"); callIdResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", callIdResult.String())
|
||||
}
|
||||
|
||||
if nameResult := outputItem.Get("name"); nameResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", nameResult.String())
|
||||
}
|
||||
|
||||
if argsResult := outputItem.Get("arguments"); argsResult.Exists() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", argsResult.String())
|
||||
}
|
||||
|
||||
toolCalls = append(toolCalls, functionCallTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
// Set content and reasoning content if found
|
||||
if contentText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.content", contentText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
|
||||
if reasoningText != "" {
|
||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningText)
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
|
||||
// Add tool calls if any
|
||||
if len(toolCalls) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
||||
for _, toolCall := range toolCalls {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", toolCall)
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and set the finish reason based on status
|
||||
if statusResult := gjson.Get(rawJSON, "status"); statusResult.Exists() {
|
||||
status := statusResult.String()
|
||||
if status == "completed" {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "stop")
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop")
|
||||
}
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
Reference in New Issue
Block a user