Merge pull request #4366 from Johnnybyzhang/fix/claude-codex-allocation-fix

perf(translator): 降低 Claude → Codex 请求转换的内存分配
This commit is contained in:
Luis Pater
2026-07-19 04:29:52 +08:00
committed by GitHub
3 changed files with 194 additions and 47 deletions
@@ -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, 16)
// 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, 2)
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, 4)
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)
}
}
@@ -350,10 +344,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())
}
@@ -508,6 +508,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"}]}`)