mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
fix(translator): reduce Claude to Codex allocations
This commit is contained in:
@@ -46,21 +46,21 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
toolNameMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
|
||||
template, _ = sjson.SetBytes(template, "model", modelName)
|
||||
inputItems := make([][]byte, 0, len(rootResult.Get("messages").Array())+1)
|
||||
|
||||
// Process system messages and convert them to input content format.
|
||||
systemsResult := rootResult.Get("system")
|
||||
if systemsResult.Exists() {
|
||||
message := []byte(`{"type":"message","role":"developer","content":[]}`)
|
||||
contentIndex := 0
|
||||
contentItems := make([][]byte, 0, len(systemsResult.Array()))
|
||||
|
||||
appendSystemText := func(text string) {
|
||||
if text == "" || util.IsClaudeCodeAttributionSystemText(text) {
|
||||
return
|
||||
}
|
||||
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text")
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text)
|
||||
contentIndex++
|
||||
content := []byte(`{"type":"input_text","text":""}`)
|
||||
content, _ = sjson.SetBytes(content, "text", text)
|
||||
contentItems = append(contentItems, content)
|
||||
}
|
||||
|
||||
if systemsResult.Type == gjson.String {
|
||||
@@ -75,8 +75,10 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
}
|
||||
}
|
||||
|
||||
if contentIndex > 0 {
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", message)
|
||||
if len(contentItems) > 0 {
|
||||
message := []byte(`{"type":"message","role":"developer"}`)
|
||||
message, _ = sjson.SetRawBytes(message, "content", marshalRawJSONArray(contentItems))
|
||||
inputItems = append(inputItems, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,27 +94,21 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
if reminderText, ok := translatorcommon.ClaudeMessageSystemReminderText(messageResult.Get("content")); ok {
|
||||
message := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
|
||||
message, _ = sjson.SetBytes(message, "content.0.text", reminderText)
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", message)
|
||||
inputItems = append(inputItems, message)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newMessage := func() []byte {
|
||||
msg := []byte(`{"type":"message","role":"","content":[]}`)
|
||||
msg, _ = sjson.SetBytes(msg, "role", messageRole)
|
||||
return msg
|
||||
}
|
||||
|
||||
message := newMessage()
|
||||
contentIndex := 0
|
||||
hasContent := false
|
||||
messageContentsResult := messageResult.Get("content")
|
||||
contentItems := make([][]byte, 0, len(messageContentsResult.Array()))
|
||||
|
||||
flushMessage := func() {
|
||||
if hasContent {
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", message)
|
||||
message = newMessage()
|
||||
contentIndex = 0
|
||||
hasContent = false
|
||||
if len(contentItems) > 0 {
|
||||
message := []byte(`{"type":"message","role":""}`)
|
||||
message, _ = sjson.SetBytes(message, "role", messageRole)
|
||||
message, _ = sjson.SetRawBytes(message, "content", marshalRawJSONArray(contentItems))
|
||||
inputItems = append(inputItems, message)
|
||||
contentItems = contentItems[:0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,17 +117,16 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
if messageRole == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), partType)
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text)
|
||||
contentIndex++
|
||||
hasContent = true
|
||||
content := []byte(`{"type":"","text":""}`)
|
||||
content, _ = sjson.SetBytes(content, "type", partType)
|
||||
content, _ = sjson.SetBytes(content, "text", text)
|
||||
contentItems = append(contentItems, content)
|
||||
}
|
||||
|
||||
appendImageContent := func(dataURL string) {
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image")
|
||||
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL)
|
||||
contentIndex++
|
||||
hasContent = true
|
||||
content := []byte(`{"type":"input_image","image_url":""}`)
|
||||
content, _ = sjson.SetBytes(content, "image_url", dataURL)
|
||||
contentItems = append(contentItems, content)
|
||||
}
|
||||
|
||||
appendReasoningContent := func(part gjson.Result) {
|
||||
@@ -154,10 +149,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
flushMessage()
|
||||
reasoningItem := []byte(`{"type":"reasoning","summary":[],"content":null}`)
|
||||
reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature)
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem)
|
||||
inputItems = append(inputItems, reasoningItem)
|
||||
}
|
||||
|
||||
messageContentsResult := messageResult.Get("content")
|
||||
if messageContentsResult.IsArray() {
|
||||
messageContentResults := messageContentsResult.Array()
|
||||
for j := 0; j < len(messageContentResults); j++ {
|
||||
@@ -202,7 +196,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "name", name)
|
||||
}
|
||||
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", functionCallMessage)
|
||||
inputItems = append(inputItems, functionCallMessage)
|
||||
case "tool_result":
|
||||
flushMessage()
|
||||
functionCallOutputMessage := []byte(`{"type":"function_call_output"}`)
|
||||
@@ -210,9 +204,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
|
||||
contentResult := messageContentResult.Get("content")
|
||||
if contentResult.IsArray() {
|
||||
toolResultContentIndex := 0
|
||||
toolResultContent := []byte(`[]`)
|
||||
contentResults := contentResult.Array()
|
||||
toolResultContentItems := make([][]byte, 0, len(contentResults))
|
||||
for k := 0; k < len(contentResults); k++ {
|
||||
toolResultContentType := contentResults[k].Get("type").String()
|
||||
if toolResultContentType == "image" {
|
||||
@@ -232,19 +225,19 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
}
|
||||
dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data)
|
||||
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_image")
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.image_url", toolResultContentIndex), dataURL)
|
||||
toolResultContentIndex++
|
||||
toolResultContent := []byte(`{"type":"input_image","image_url":""}`)
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, "image_url", dataURL)
|
||||
toolResultContentItems = append(toolResultContentItems, toolResultContent)
|
||||
}
|
||||
}
|
||||
} else if toolResultContentType == "text" {
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_text")
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.text", toolResultContentIndex), contentResults[k].Get("text").String())
|
||||
toolResultContentIndex++
|
||||
toolResultContent := []byte(`{"type":"input_text","text":""}`)
|
||||
toolResultContent, _ = sjson.SetBytes(toolResultContent, "text", contentResults[k].Get("text").String())
|
||||
toolResultContentItems = append(toolResultContentItems, toolResultContent)
|
||||
}
|
||||
}
|
||||
if toolResultContentIndex > 0 {
|
||||
functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", toolResultContent)
|
||||
if len(toolResultContentItems) > 0 {
|
||||
functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", marshalRawJSONArray(toolResultContentItems))
|
||||
} else {
|
||||
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
|
||||
}
|
||||
@@ -252,7 +245,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
|
||||
}
|
||||
|
||||
template, _ = sjson.SetRawBytes(template, "input.-1", functionCallOutputMessage)
|
||||
inputItems = append(inputItems, functionCallOutputMessage)
|
||||
}
|
||||
}
|
||||
flushMessage()
|
||||
@@ -266,16 +259,17 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
|
||||
// Convert tools declarations to the expected format for the Codex API.
|
||||
toolsResult := rootResult.Get("tools")
|
||||
var toolItems [][]byte
|
||||
if toolsResult.IsArray() {
|
||||
template, _ = sjson.SetRawBytes(template, "tools", []byte(`[]`))
|
||||
webSearchToolNames := buildClaudeWebSearchToolNameSet(toolsResult)
|
||||
template, _ = sjson.SetRawBytes(template, "tool_choice", convertClaudeToolChoiceToCodex(rootResult.Get("tool_choice"), toolNameMap, webSearchToolNames))
|
||||
toolResults := toolsResult.Array()
|
||||
toolItems = make([][]byte, 0, len(toolResults))
|
||||
for i := 0; i < len(toolResults); i++ {
|
||||
toolResult := toolResults[i]
|
||||
// Special handling: map Claude web search tool to Codex web_search
|
||||
if isClaudeWebSearchToolType(toolResult.Get("type").String()) {
|
||||
template, _ = sjson.SetRawBytes(template, "tools.-1", convertClaudeWebSearchToolToCodex(toolResult))
|
||||
toolItems = append(toolItems, convertClaudeWebSearchToolToCodex(toolResult))
|
||||
continue
|
||||
}
|
||||
tool := []byte(toolResult.Raw)
|
||||
@@ -296,7 +290,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
tool, _ = sjson.DeleteBytes(tool, "cache_control")
|
||||
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
|
||||
tool, _ = sjson.SetBytes(tool, "strict", false)
|
||||
template, _ = sjson.SetRawBytes(template, "tools.-1", tool)
|
||||
toolItems = append(toolItems, tool)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,10 +340,35 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
||||
template, _ = sjson.SetBytes(template, "stream", true)
|
||||
template, _ = sjson.SetBytes(template, "store", false)
|
||||
template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"})
|
||||
if toolsResult.IsArray() {
|
||||
template, _ = sjson.SetRawBytes(template, "tools", marshalRawJSONArray(toolItems))
|
||||
}
|
||||
template, _ = sjson.SetRawBytes(template, "input", marshalRawJSONArray(inputItems))
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
func marshalRawJSONArray(items [][]byte) []byte {
|
||||
size := 2
|
||||
if len(items) > 1 {
|
||||
size += len(items) - 1
|
||||
}
|
||||
for i := 0; i < len(items); i++ {
|
||||
size += len(items[i])
|
||||
}
|
||||
|
||||
result := make([]byte, 0, size)
|
||||
result = append(result, '[')
|
||||
for i := 0; i < len(items); i++ {
|
||||
if i > 0 {
|
||||
result = append(result, ',')
|
||||
}
|
||||
result = append(result, items[i]...)
|
||||
}
|
||||
result = append(result, ']')
|
||||
return result
|
||||
}
|
||||
|
||||
func codexClaudeTargetAcceptsGrokSignature(modelName string) bool {
|
||||
baseModel := strings.ToLower(strings.TrimSpace(thinking.ParseSuffix(modelName).ModelName))
|
||||
return strings.Contains(baseModel, "grok")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func BenchmarkConvertClaudeRequestToCodexLargeHistory(b *testing.B) {
|
||||
for _, turns := range []int{16, 64} {
|
||||
b.Run(strconv.Itoa(turns)+"_turns", func(b *testing.B) {
|
||||
request := largeClaudeRequest(turns, 32, 8*1024)
|
||||
if !gjson.ValidBytes(request) {
|
||||
b.Fatal("benchmark generated an invalid Claude request")
|
||||
}
|
||||
if result := ConvertClaudeRequestToCodex("gpt-5.4", request, false); !gjson.ValidBytes(result) {
|
||||
b.Fatal("translator generated invalid Codex JSON")
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(request)))
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConvertClaudeRequestToCodex("gpt-5.4", request, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func largeClaudeRequest(turns, toolCount, payloadSize int) []byte {
|
||||
payload := strings.Repeat("x", payloadSize)
|
||||
var request strings.Builder
|
||||
request.Grow((turns + toolCount) * payloadSize)
|
||||
request.WriteString(`{"model":"claude-test","system":[{"type":"text","text":"`)
|
||||
request.WriteString(payload)
|
||||
request.WriteString(`"}],"messages":[`)
|
||||
|
||||
for i := 0; i < turns; i++ {
|
||||
if i > 0 {
|
||||
request.WriteByte(',')
|
||||
}
|
||||
request.WriteString(`{"role":"assistant","content":[{"type":"text","text":"`)
|
||||
request.WriteString(payload)
|
||||
request.WriteString(`"},{"type":"tool_use","id":"toolu_`)
|
||||
request.WriteString(strconv.Itoa(i))
|
||||
request.WriteString(`","name":"tool_`)
|
||||
request.WriteString(strconv.Itoa(i % toolCount))
|
||||
request.WriteString(`","input":{"value":"`)
|
||||
request.WriteString(payload)
|
||||
request.WriteString(`"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_`)
|
||||
request.WriteString(strconv.Itoa(i))
|
||||
request.WriteString(`","content":[{"type":"text","text":"`)
|
||||
request.WriteString(payload)
|
||||
request.WriteString(`"}]}]}`)
|
||||
}
|
||||
|
||||
request.WriteString(`],"tools":[`)
|
||||
for i := 0; i < toolCount; i++ {
|
||||
if i > 0 {
|
||||
request.WriteByte(',')
|
||||
}
|
||||
request.WriteString(`{"name":"tool_`)
|
||||
request.WriteString(strconv.Itoa(i))
|
||||
request.WriteString(`","description":"`)
|
||||
request.WriteString(payload)
|
||||
request.WriteString(`","input_schema":{"type":"object","properties":{"value":{"type":"string"}}}}`)
|
||||
}
|
||||
request.WriteString(`]}`)
|
||||
return []byte(request.String())
|
||||
}
|
||||
@@ -481,6 +481,62 @@ func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_PreservesContentOrderAcrossToolAndReasoningItems(t *testing.T) {
|
||||
signature := validCodexReasoningSignature()
|
||||
inputJSON := `{
|
||||
"system": "system rules",
|
||||
"messages": [
|
||||
{"role":"assistant","content":[
|
||||
{"type":"text","text":"before reasoning"},
|
||||
{"type":"thinking","signature":"` + signature + `"},
|
||||
{"type":"text","text":"before tool"},
|
||||
{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"query":"test"}},
|
||||
{"type":"text","text":"after tool"}
|
||||
]},
|
||||
{"role":"user","content":[
|
||||
{"type":"tool_result","tool_use_id":"toolu_1","content":[
|
||||
{"type":"text","text":"tool output"},
|
||||
{"type":"image","source":{"media_type":"image/png","data":"aW1hZ2U="}}
|
||||
]},
|
||||
{"type":"text","text":"continue"}
|
||||
]}
|
||||
],
|
||||
"tools": [{"name":"lookup","input_schema":{"type":"object"}}]
|
||||
}`
|
||||
|
||||
result := ConvertClaudeRequestToCodex("gpt-5.4", []byte(inputJSON), false)
|
||||
inputs := gjson.GetBytes(result, "input").Array()
|
||||
if len(inputs) != 8 {
|
||||
t.Fatalf("got %d input items, want 8. Output: %s", len(inputs), result)
|
||||
}
|
||||
|
||||
wantTypes := []string{"message", "message", "reasoning", "message", "function_call", "message", "function_call_output", "message"}
|
||||
for i := 0; i < len(wantTypes); i++ {
|
||||
if got := inputs[i].Get("type").String(); got != wantTypes[i] {
|
||||
t.Fatalf("input[%d].type = %q, want %q. Output: %s", i, got, wantTypes[i], result)
|
||||
}
|
||||
}
|
||||
|
||||
if got := inputs[1].Get("content.0.text").String(); got != "before reasoning" {
|
||||
t.Fatalf("input[1] text = %q, want before reasoning", got)
|
||||
}
|
||||
if got := inputs[3].Get("content.0.text").String(); got != "before tool" {
|
||||
t.Fatalf("input[3] text = %q, want before tool", got)
|
||||
}
|
||||
if got := inputs[5].Get("content.0.text").String(); got != "after tool" {
|
||||
t.Fatalf("input[5] text = %q, want after tool", got)
|
||||
}
|
||||
if got := inputs[6].Get("output.0.type").String(); got != "input_text" {
|
||||
t.Fatalf("tool result output.0.type = %q, want input_text", got)
|
||||
}
|
||||
if got := inputs[6].Get("output.1.image_url").String(); got != "data:image/png;base64,aW1hZ2U=" {
|
||||
t.Fatalf("tool result image_url = %q, want data URL", got)
|
||||
}
|
||||
if got := inputs[7].Get("content.0.text").String(); got != "continue" {
|
||||
t.Fatalf("input[7] text = %q, want continue", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_AssistantGrokSignatureToReasoningItem(t *testing.T) {
|
||||
signature := "HmlYdr2aCAqCYP/m9mr8PS6KOsdMs72FGDigmydR+Jsmuv8KX97yWPlbOwmXJgWn0CbHaCacdQD3+n5EvpgLfPNmafS3kdICBjRuDf4bzHy7uBiUhNVhqPtp/ee1y9q4imPE4LYgD1VZ4J+bp9mTeqA1+nC9Oue58CiNEMV9SVaGenCD+aBnVuSTzQhD32Y+68i6HLJW0Dx6ifaRfb8hxYtA/sPM+/FTvAMW11nRho5a2BBSkpnzfqqAz/e/vGJ77/bygpXM823QA9wL9i0X"
|
||||
payload := []byte(`{"model":"grok-4.5","messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"summary","signature":""},{"type":"text","text":"answer"}]},{"role":"user","content":"next"}]}`)
|
||||
|
||||
Reference in New Issue
Block a user