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:
114
internal/translator/codex/claude/code/codex_cc_request.go
Normal file
114
internal/translator/codex/claude/code/codex_cc_request.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Package code provides request translation functionality for Claude API.
|
||||
// It handles parsing and transforming Claude API requests into the internal client format,
|
||||
// extracting model information, system instructions, message contents, and tool declarations.
|
||||
// The package also performs JSON data cleaning and transformation to ensure compatibility
|
||||
// between Claude API format and the internal client's expected format.
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/luispater/CLIProxyAPI/internal/misc"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// PrepareClaudeRequest parses and transforms a Claude API request into internal client format.
|
||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||
// from the raw JSON request and returns them in the format expected by the internal client.
|
||||
func ConvertClaudeCodeRequestToCodex(rawJSON []byte) string {
|
||||
template := `{"model":"","instructions":"","input":[]}`
|
||||
|
||||
instructions := misc.CodexInstructions
|
||||
template, _ = sjson.SetRaw(template, "instructions", instructions)
|
||||
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
modelResult := rootResult.Get("model")
|
||||
template, _ = sjson.Set(template, "model", modelResult.String())
|
||||
|
||||
systemsResult := rootResult.Get("system")
|
||||
if systemsResult.IsArray() {
|
||||
systemResults := systemsResult.Array()
|
||||
message := `{"type":"message","role":"user","content":[]}`
|
||||
for i := 0; i < len(systemResults); i++ {
|
||||
systemResult := systemResults[i]
|
||||
systemTypeResult := systemResult.Get("type")
|
||||
if systemTypeResult.String() == "text" {
|
||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", i), "input_text")
|
||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", i), systemResult.Get("text").String())
|
||||
}
|
||||
}
|
||||
template, _ = sjson.SetRaw(template, "input.-1", message)
|
||||
}
|
||||
|
||||
messagesResult := rootResult.Get("messages")
|
||||
if messagesResult.IsArray() {
|
||||
messageResults := messagesResult.Array()
|
||||
|
||||
for i := 0; i < len(messageResults); i++ {
|
||||
messageResult := messageResults[i]
|
||||
|
||||
messageContentsResult := messageResult.Get("content")
|
||||
if messageContentsResult.IsArray() {
|
||||
messageContentResults := messageContentsResult.Array()
|
||||
for j := 0; j < len(messageContentResults); j++ {
|
||||
messageContentResult := messageContentResults[j]
|
||||
messageContentTypeResult := messageContentResult.Get("type")
|
||||
if messageContentTypeResult.String() == "text" {
|
||||
message := `{"type": "message","role":"","content":[]}`
|
||||
messageRole := messageResult.Get("role").String()
|
||||
message, _ = sjson.Set(message, "role", messageRole)
|
||||
|
||||
partType := "input_text"
|
||||
if messageRole == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
|
||||
currentIndex := len(gjson.Get(message, "content").Array())
|
||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", currentIndex), partType)
|
||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", currentIndex), messageContentResult.Get("text").String())
|
||||
template, _ = sjson.SetRaw(template, "input.-1", message)
|
||||
} else if messageContentTypeResult.String() == "tool_use" {
|
||||
functionCallMessage := `{"type":"function_call"}`
|
||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "call_id", messageContentResult.Get("id").String())
|
||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "name", messageContentResult.Get("name").String())
|
||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
|
||||
template, _ = sjson.SetRaw(template, "input.-1", functionCallMessage)
|
||||
} else if messageContentTypeResult.String() == "tool_result" {
|
||||
functionCallOutputMessage := `{"type":"function_call_output"}`
|
||||
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String())
|
||||
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
|
||||
template, _ = sjson.SetRaw(template, "input.-1", functionCallOutputMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
toolsResult := rootResult.Get("tools")
|
||||
if toolsResult.IsArray() {
|
||||
template, _ = sjson.SetRaw(template, "tools", `[]`)
|
||||
template, _ = sjson.Set(template, "tool_choice", `auto`)
|
||||
toolResults := toolsResult.Array()
|
||||
for i := 0; i < len(toolResults); i++ {
|
||||
toolResult := toolResults[i]
|
||||
tool := toolResult.Raw
|
||||
tool, _ = sjson.Set(tool, "type", "function")
|
||||
tool, _ = sjson.SetRaw(tool, "parameters", toolResult.Get("input_schema").Raw)
|
||||
tool, _ = sjson.Delete(tool, "input_schema")
|
||||
tool, _ = sjson.Delete(tool, "parameters.$schema")
|
||||
tool, _ = sjson.Set(tool, "strict", false)
|
||||
template, _ = sjson.SetRaw(template, "tools.-1", tool)
|
||||
}
|
||||
}
|
||||
|
||||
template, _ = sjson.Set(template, "parallel_tool_calls", true)
|
||||
template, _ = sjson.Set(template, "reasoning.effort", "low")
|
||||
template, _ = sjson.Set(template, "reasoning.summary", "auto")
|
||||
template, _ = sjson.Set(template, "stream", true)
|
||||
template, _ = sjson.Set(template, "store", false)
|
||||
template, _ = sjson.Set(template, "include", []string{"reasoning.encrypted_content"})
|
||||
|
||||
return template
|
||||
}
|
||||
129
internal/translator/codex/claude/code/codex_cc_response.go
Normal file
129
internal/translator/codex/claude/code/codex_cc_response.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Package code provides response translation functionality for Claude API.
|
||||
// This package handles the conversion of backend client responses into Claude-compatible
|
||||
// Server-Sent Events (SSE) format, implementing a sophisticated state machine that manages
|
||||
// different response types including text content, thinking processes, and function calls.
|
||||
// The translation ensures proper sequencing of SSE events and maintains state across
|
||||
// multiple response chunks to provide a seamless streaming experience.
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertCliToClaude performs sophisticated streaming response format conversion.
|
||||
// This function implements a complex state machine that translates backend client responses
|
||||
// into Claude-compatible Server-Sent Events (SSE) format. It manages different response types
|
||||
// and handles state transitions between content blocks, thinking processes, and function calls.
|
||||
//
|
||||
// Response type states: 0=none, 1=content, 2=thinking, 3=function
|
||||
// The function maintains state across multiple calls to ensure proper SSE event sequencing.
|
||||
func ConvertCodexResponseToClaude(rawJSON []byte, hasToolCall bool) (string, bool) {
|
||||
// log.Debugf("rawJSON: %s", string(rawJSON))
|
||||
output := ""
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
typeResult := rootResult.Get("type")
|
||||
typeStr := typeResult.String()
|
||||
template := ""
|
||||
if typeStr == "response.created" {
|
||||
template = `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"claude-opus-4-1-20250805","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0},"content":[],"stop_reason":null}}`
|
||||
template, _ = sjson.Set(template, "message.model", rootResult.Get("response.model").String())
|
||||
template, _ = sjson.Set(template, "message.id", rootResult.Get("response.id").String())
|
||||
|
||||
output = "event: message_start\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.reasoning_summary_part.added" {
|
||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output = "event: content_block_start\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.reasoning_summary_text.delta" {
|
||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
template, _ = sjson.Set(template, "delta.thinking", rootResult.Get("delta").String())
|
||||
|
||||
output = "event: content_block_delta\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.reasoning_summary_part.done" {
|
||||
template = `{"type":"content_block_stop","index":0}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output = "event: content_block_stop\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.content_part.added" {
|
||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output = "event: content_block_start\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.output_text.delta" {
|
||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
template, _ = sjson.Set(template, "delta.text", rootResult.Get("delta").String())
|
||||
|
||||
output = "event: content_block_delta\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.content_part.done" {
|
||||
template = `{"type":"content_block_stop","index":0}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output = "event: content_block_stop\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
} else if typeStr == "response.completed" {
|
||||
template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||
if hasToolCall {
|
||||
template, _ = sjson.Set(template, "delta.stop_reason", "tool_use")
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "delta.stop_reason", "end_turn")
|
||||
}
|
||||
template, _ = sjson.Set(template, "usage.input_tokens", rootResult.Get("response.usage.input_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usage.output_tokens", rootResult.Get("response.usage.output_tokens").Int())
|
||||
|
||||
output = "event: message_delta\n"
|
||||
output += fmt.Sprintf("data: %s\n\n", template)
|
||||
output += "event: message_stop\n"
|
||||
output += `data: {"type":"message_stop"}`
|
||||
output += "\n\n"
|
||||
} else if typeStr == "response.output_item.added" {
|
||||
itemResult := rootResult.Get("item")
|
||||
itemType := itemResult.Get("type").String()
|
||||
if itemType == "function_call" {
|
||||
hasToolCall = true
|
||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
template, _ = sjson.Set(template, "content_block.id", itemResult.Get("call_id").String())
|
||||
template, _ = sjson.Set(template, "content_block.name", itemResult.Get("name").String())
|
||||
|
||||
output = "event: content_block_start\n"
|
||||
output += fmt.Sprintf("data: %s\n\n", template)
|
||||
|
||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output += "event: content_block_delta\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
}
|
||||
} else if typeStr == "response.output_item.done" {
|
||||
itemResult := rootResult.Get("item")
|
||||
itemType := itemResult.Get("type").String()
|
||||
if itemType == "function_call" {
|
||||
template = `{"type":"content_block_stop","index":0}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
|
||||
output = "event: content_block_stop\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
}
|
||||
} else if typeStr == "response.function_call_arguments.delta" {
|
||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
||||
template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String())
|
||||
|
||||
output += "event: content_block_delta\n"
|
||||
output += fmt.Sprintf("data: %s\n", template)
|
||||
}
|
||||
|
||||
return output, hasToolCall
|
||||
}
|
||||
199
internal/translator/codex/gemini/codex_gemini_request.go
Normal file
199
internal/translator/codex/gemini/codex_gemini_request.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Package code provides request translation functionality for Claude API.
|
||||
// It handles parsing and transforming Claude API requests into the internal client format,
|
||||
// extracting model information, system instructions, message contents, and tool declarations.
|
||||
// The package also performs JSON data cleaning and transformation to ensure compatibility
|
||||
// between Claude API format and the internal client's expected format.
|
||||
package code
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/luispater/CLIProxyAPI/internal/misc"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// PrepareClaudeRequest parses and transforms a Claude API request into internal client format.
|
||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||
// from the raw JSON request and returns them in the format expected by the internal client.
|
||||
func ConvertGeminiRequestToCodex(rawJSON []byte) string {
|
||||
// Base template
|
||||
out := `{"model":"","instructions":"","input":[]}`
|
||||
|
||||
// Inject standard Codex instructions
|
||||
instructions := misc.CodexInstructions
|
||||
out, _ = sjson.SetRaw(out, "instructions", instructions)
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// helper for generating paired call IDs in the form: call_<alphanum>
|
||||
// Gemini uses sequential pairing across possibly multiple in-flight
|
||||
// functionCalls, so we keep a FIFO queue of generated call IDs and
|
||||
// consume them in order when functionResponses arrive.
|
||||
var pendingCallIDs []string
|
||||
|
||||
// genCallID creates a random call id like: call_<8chars>
|
||||
genCallID := func() string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var b strings.Builder
|
||||
// 8 chars random suffix
|
||||
for i := 0; i < 24; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
b.WriteByte(letters[n.Int64()])
|
||||
}
|
||||
return "call_" + b.String()
|
||||
}
|
||||
|
||||
// Model
|
||||
if v := root.Get("model"); v.Exists() {
|
||||
out, _ = sjson.Set(out, "model", v.Value())
|
||||
}
|
||||
|
||||
// System instruction -> as a user message with input_text parts
|
||||
sysParts := root.Get("system_instruction.parts")
|
||||
if sysParts.IsArray() {
|
||||
msg := `{"type":"message","role":"user","content":[]}`
|
||||
arr := sysParts.Array()
|
||||
for i := 0; i < len(arr); i++ {
|
||||
p := arr[i]
|
||||
if t := p.Get("text"); t.Exists() {
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", "input_text")
|
||||
part, _ = sjson.Set(part, "text", t.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
}
|
||||
}
|
||||
if len(gjson.Get(msg, "content").Array()) > 0 {
|
||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Contents -> messages and function calls/results
|
||||
contents := root.Get("contents")
|
||||
if contents.IsArray() {
|
||||
items := contents.Array()
|
||||
for i := 0; i < len(items); i++ {
|
||||
item := items[i]
|
||||
role := item.Get("role").String()
|
||||
if role == "model" {
|
||||
role = "assistant"
|
||||
}
|
||||
|
||||
parts := item.Get("parts")
|
||||
if !parts.IsArray() {
|
||||
continue
|
||||
}
|
||||
parr := parts.Array()
|
||||
for j := 0; j < len(parr); j++ {
|
||||
p := parr[j]
|
||||
// text part
|
||||
if t := p.Get("text"); t.Exists() {
|
||||
msg := `{"type":"message","role":"","content":[]}`
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
partType := "input_text"
|
||||
if role == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", partType)
|
||||
part, _ = sjson.Set(part, "text", t.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
||||
continue
|
||||
}
|
||||
|
||||
// function call from model
|
||||
if fc := p.Get("functionCall"); fc.Exists() {
|
||||
fn := `{"type":"function_call"}`
|
||||
if name := fc.Get("name"); name.Exists() {
|
||||
fn, _ = sjson.Set(fn, "name", name.String())
|
||||
}
|
||||
if args := fc.Get("args"); args.Exists() {
|
||||
fn, _ = sjson.Set(fn, "arguments", args.Raw)
|
||||
}
|
||||
// generate a paired random call_id and enqueue it so the
|
||||
// corresponding functionResponse can pop the earliest id
|
||||
// to preserve ordering when multiple calls are present.
|
||||
id := genCallID()
|
||||
fn, _ = sjson.Set(fn, "call_id", id)
|
||||
pendingCallIDs = append(pendingCallIDs, id)
|
||||
out, _ = sjson.SetRaw(out, "input.-1", fn)
|
||||
continue
|
||||
}
|
||||
|
||||
// function response from user
|
||||
if fr := p.Get("functionResponse"); fr.Exists() {
|
||||
fno := `{"type":"function_call_output"}`
|
||||
// Prefer a string result if present; otherwise embed the raw response as a string
|
||||
if res := fr.Get("response.result"); res.Exists() {
|
||||
fno, _ = sjson.Set(fno, "output", res.String())
|
||||
} else if resp := fr.Get("response"); resp.Exists() {
|
||||
fno, _ = sjson.Set(fno, "output", resp.Raw)
|
||||
}
|
||||
// fno, _ = sjson.Set(fno, "call_id", "call_W6nRJzFXyPM2LFBbfo98qAbq")
|
||||
// attach the oldest queued call_id to pair the response
|
||||
// with its call. If the queue is empty, generate a new id.
|
||||
var id string
|
||||
if len(pendingCallIDs) > 0 {
|
||||
id = pendingCallIDs[0]
|
||||
// pop the first element
|
||||
pendingCallIDs = pendingCallIDs[1:]
|
||||
} else {
|
||||
id = genCallID()
|
||||
}
|
||||
fno, _ = sjson.Set(fno, "call_id", id)
|
||||
out, _ = sjson.SetRaw(out, "input.-1", fno)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tools mapping: Gemini functionDeclarations -> Codex tools
|
||||
tools := root.Get("tools")
|
||||
if tools.IsArray() {
|
||||
out, _ = sjson.SetRaw(out, "tools", `[]`)
|
||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
||||
tarr := tools.Array()
|
||||
for i := 0; i < len(tarr); i++ {
|
||||
td := tarr[i]
|
||||
fns := td.Get("functionDeclarations")
|
||||
if !fns.IsArray() {
|
||||
continue
|
||||
}
|
||||
farr := fns.Array()
|
||||
for j := 0; j < len(farr); j++ {
|
||||
fn := farr[j]
|
||||
tool := `{}`
|
||||
tool, _ = sjson.Set(tool, "type", "function")
|
||||
if v := fn.Get("name"); v.Exists() {
|
||||
tool, _ = sjson.Set(tool, "name", v.String())
|
||||
}
|
||||
if v := fn.Get("description"); v.Exists() {
|
||||
tool, _ = sjson.Set(tool, "description", v.String())
|
||||
}
|
||||
if prm := fn.Get("parameters"); prm.Exists() {
|
||||
// Remove optional $schema field if present
|
||||
cleaned := prm.Raw
|
||||
cleaned, _ = sjson.Delete(cleaned, "$schema")
|
||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
||||
tool, _ = sjson.SetRaw(tool, "parameters", cleaned)
|
||||
}
|
||||
tool, _ = sjson.Set(tool, "strict", false)
|
||||
out, _ = sjson.SetRaw(out, "tools.-1", tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed flags aligning with Codex expectations
|
||||
out, _ = sjson.Set(out, "parallel_tool_calls", true)
|
||||
out, _ = sjson.Set(out, "reasoning.effort", "low")
|
||||
out, _ = sjson.Set(out, "reasoning.summary", "auto")
|
||||
out, _ = sjson.Set(out, "stream", true)
|
||||
out, _ = sjson.Set(out, "store", false)
|
||||
out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"})
|
||||
|
||||
return out
|
||||
}
|
||||
251
internal/translator/codex/gemini/codex_gemini_response.go
Normal file
251
internal/translator/codex/gemini/codex_gemini_response.go
Normal file
@@ -0,0 +1,251 @@
|
||||
// Package code provides response translation functionality for Gemini API.
|
||||
// This package handles the conversion of Codex backend responses into Gemini-compatible
|
||||
// JSON format, transforming streaming events into single-line JSON responses that include
|
||||
// thinking content, regular text content, and function calls in the format expected by
|
||||
// Gemini API clients.
|
||||
package code
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
type ConvertCodexResponseToGeminiParams struct {
|
||||
Model string
|
||||
CreatedAt int64
|
||||
ResponseID string
|
||||
LastStorageOutput string
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToGemini converts Codex streaming response format to Gemini single-line JSON format.
|
||||
// This function processes various Codex event types and transforms them into Gemini-compatible JSON responses.
|
||||
// It handles thinking content, regular text content, and function calls, outputting single-line JSON
|
||||
// that matches the Gemini API response format.
|
||||
// The lastEventType parameter tracks the previous event type to handle consecutive function calls properly.
|
||||
func ConvertCodexResponseToGemini(rawJSON []byte, param *ConvertCodexResponseToGeminiParams) []string {
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
typeResult := rootResult.Get("type")
|
||||
typeStr := typeResult.String()
|
||||
|
||||
// Base Gemini response template
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T02:52:03.884209Z","responseId":"06CeaPH7NaCU48APvNXDyA4"}`
|
||||
if param.LastStorageOutput != "" && typeStr == "response.output_item.done" {
|
||||
template = param.LastStorageOutput
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "modelVersion", param.Model)
|
||||
createdAtResult := rootResult.Get("response.created_at")
|
||||
if createdAtResult.Exists() {
|
||||
param.CreatedAt = createdAtResult.Int()
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(param.CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
}
|
||||
template, _ = sjson.Set(template, "responseId", param.ResponseID)
|
||||
}
|
||||
|
||||
// Handle function call completion
|
||||
if typeStr == "response.output_item.done" {
|
||||
itemResult := rootResult.Get("item")
|
||||
itemType := itemResult.Get("type").String()
|
||||
if itemType == "function_call" {
|
||||
// Create function call part
|
||||
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", itemResult.Get("name").String())
|
||||
|
||||
// Parse and set arguments
|
||||
argsStr := itemResult.Get("arguments").String()
|
||||
if argsStr != "" {
|
||||
argsResult := gjson.Parse(argsStr)
|
||||
if argsResult.IsObject() {
|
||||
functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
|
||||
}
|
||||
}
|
||||
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall)
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
|
||||
param.LastStorageOutput = template
|
||||
|
||||
// Use this return to storage message
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if typeStr == "response.created" { // Handle response creation - set model and response ID
|
||||
template, _ = sjson.Set(template, "modelVersion", rootResult.Get("response.model").String())
|
||||
template, _ = sjson.Set(template, "responseId", rootResult.Get("response.id").String())
|
||||
param.ResponseID = rootResult.Get("response.id").String()
|
||||
} else if typeStr == "response.reasoning_summary_text.delta" { // Handle reasoning/thinking content delta
|
||||
part := `{"thought":true,"text":""}`
|
||||
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
||||
} else if typeStr == "response.output_text.delta" { // Handle regular text content delta
|
||||
part := `{"text":""}`
|
||||
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
||||
} else if typeStr == "response.completed" { // Handle response completion with usage metadata
|
||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", rootResult.Get("response.usage.input_tokens").Int())
|
||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int())
|
||||
totalTokens := rootResult.Get("response.usage.input_tokens").Int() + rootResult.Get("response.usage.output_tokens").Int()
|
||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if param.LastStorageOutput != "" {
|
||||
return []string{param.LastStorageOutput, template}
|
||||
} else {
|
||||
return []string{template}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToGeminiNonStream converts a completed Codex response to Gemini non-streaming format.
|
||||
// This function processes the final response.completed event and transforms it into a complete
|
||||
// Gemini-compatible JSON response that includes all content parts, function calls, and usage metadata.
|
||||
func ConvertCodexResponseToGeminiNonStream(rawJSON []byte, model string) string {
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Verify this is a response.completed event
|
||||
if rootResult.Get("type").String() != "response.completed" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Base Gemini response template for non-streaming
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||
|
||||
// Set model version
|
||||
template, _ = sjson.Set(template, "modelVersion", model)
|
||||
|
||||
// Set response metadata from the completed response
|
||||
responseData := rootResult.Get("response")
|
||||
if responseData.Exists() {
|
||||
// Set response ID
|
||||
if responseId := responseData.Get("id"); responseId.Exists() {
|
||||
template, _ = sjson.Set(template, "responseId", responseId.String())
|
||||
}
|
||||
|
||||
// Set creation time
|
||||
if createdAt := responseData.Get("created_at"); createdAt.Exists() {
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
// Set usage metadata
|
||||
if usage := responseData.Get("usage"); usage.Exists() {
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
totalTokens := inputTokens + outputTokens
|
||||
|
||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
||||
}
|
||||
|
||||
// Process output content to build parts array
|
||||
var parts []interface{}
|
||||
hasToolCall := false
|
||||
var pendingFunctionCalls []interface{}
|
||||
|
||||
flushPendingFunctionCalls := func() {
|
||||
if len(pendingFunctionCalls) > 0 {
|
||||
// Add all pending function calls as individual parts
|
||||
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
||||
for _, fc := range pendingFunctionCalls {
|
||||
parts = append(parts, fc)
|
||||
}
|
||||
pendingFunctionCalls = nil
|
||||
}
|
||||
}
|
||||
|
||||
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
||||
output.ForEach(func(key, value gjson.Result) bool {
|
||||
itemType := value.Get("type").String()
|
||||
|
||||
switch itemType {
|
||||
case "reasoning":
|
||||
// Flush any pending function calls before adding non-function content
|
||||
flushPendingFunctionCalls()
|
||||
|
||||
// Add thinking content
|
||||
if content := value.Get("content"); content.Exists() {
|
||||
part := map[string]interface{}{
|
||||
"thought": true,
|
||||
"text": content.String(),
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
case "message":
|
||||
// Flush any pending function calls before adding non-function content
|
||||
flushPendingFunctionCalls()
|
||||
|
||||
// Add regular text content
|
||||
if content := value.Get("content"); content.Exists() && content.IsArray() {
|
||||
content.ForEach(func(_, contentItem gjson.Result) bool {
|
||||
if contentItem.Get("type").String() == "output_text" {
|
||||
if text := contentItem.Get("text"); text.Exists() {
|
||||
part := map[string]interface{}{
|
||||
"text": text.String(),
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
case "function_call":
|
||||
// Collect function call for potential merging with consecutive ones
|
||||
hasToolCall = true
|
||||
functionCall := map[string]interface{}{
|
||||
"functionCall": map[string]interface{}{
|
||||
"name": value.Get("name").String(),
|
||||
"args": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
// Parse and set arguments
|
||||
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
||||
argsResult := gjson.Parse(argsStr)
|
||||
if argsResult.IsObject() {
|
||||
var args map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
||||
functionCall["functionCall"].(map[string]interface{})["args"] = args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingFunctionCalls = append(pendingFunctionCalls, functionCall)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Handle any remaining pending function calls at the end
|
||||
flushPendingFunctionCalls()
|
||||
}
|
||||
|
||||
// Set the parts array
|
||||
if len(parts) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
|
||||
}
|
||||
|
||||
// Set finish reason based on whether there were tool calls
|
||||
if hasToolCall {
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
}
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
// mustMarshalJSON marshals data to JSON, panicking on error (should not happen with valid data)
|
||||
func mustMarshalJSON(v interface{}) string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
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