Skip to content

Commit b3ffb11

Browse files
committed
fix(translator): reduce Claude to Codex allocations
1 parent 09da52a commit b3ffb11

3 files changed

Lines changed: 194 additions & 47 deletions

File tree

internal/translator/codex/claude/codex_claude_request.go

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,21 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
4646
rootResult := gjson.ParseBytes(rawJSON)
4747
toolNameMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
4848
template, _ = sjson.SetBytes(template, "model", modelName)
49+
inputItems := make([][]byte, 0, len(rootResult.Get("messages").Array())+1)
4950

5051
// Process system messages and convert them to input content format.
5152
systemsResult := rootResult.Get("system")
5253
if systemsResult.Exists() {
53-
message := []byte(`{"type":"message","role":"developer","content":[]}`)
54-
contentIndex := 0
54+
contentItems := make([][]byte, 0, len(systemsResult.Array()))
5555

5656
appendSystemText := func(text string) {
5757
if text == "" || util.IsClaudeCodeAttributionSystemText(text) {
5858
return
5959
}
6060

61-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text")
62-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text)
63-
contentIndex++
61+
content := []byte(`{"type":"input_text","text":""}`)
62+
content, _ = sjson.SetBytes(content, "text", text)
63+
contentItems = append(contentItems, content)
6464
}
6565

6666
if systemsResult.Type == gjson.String {
@@ -75,8 +75,10 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
7575
}
7676
}
7777

78-
if contentIndex > 0 {
79-
template, _ = sjson.SetRawBytes(template, "input.-1", message)
78+
if len(contentItems) > 0 {
79+
message := []byte(`{"type":"message","role":"developer"}`)
80+
message, _ = sjson.SetRawBytes(message, "content", marshalRawJSONArray(contentItems))
81+
inputItems = append(inputItems, message)
8082
}
8183
}
8284

@@ -92,27 +94,21 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
9294
if reminderText, ok := translatorcommon.ClaudeMessageSystemReminderText(messageResult.Get("content")); ok {
9395
message := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
9496
message, _ = sjson.SetBytes(message, "content.0.text", reminderText)
95-
template, _ = sjson.SetRawBytes(template, "input.-1", message)
97+
inputItems = append(inputItems, message)
9698
}
9799
continue
98100
}
99101

100-
newMessage := func() []byte {
101-
msg := []byte(`{"type":"message","role":"","content":[]}`)
102-
msg, _ = sjson.SetBytes(msg, "role", messageRole)
103-
return msg
104-
}
105-
106-
message := newMessage()
107-
contentIndex := 0
108-
hasContent := false
102+
messageContentsResult := messageResult.Get("content")
103+
contentItems := make([][]byte, 0, len(messageContentsResult.Array()))
109104

110105
flushMessage := func() {
111-
if hasContent {
112-
template, _ = sjson.SetRawBytes(template, "input.-1", message)
113-
message = newMessage()
114-
contentIndex = 0
115-
hasContent = false
106+
if len(contentItems) > 0 {
107+
message := []byte(`{"type":"message","role":""}`)
108+
message, _ = sjson.SetBytes(message, "role", messageRole)
109+
message, _ = sjson.SetRawBytes(message, "content", marshalRawJSONArray(contentItems))
110+
inputItems = append(inputItems, message)
111+
contentItems = contentItems[:0]
116112
}
117113
}
118114

@@ -121,17 +117,16 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
121117
if messageRole == "assistant" {
122118
partType = "output_text"
123119
}
124-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), partType)
125-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text)
126-
contentIndex++
127-
hasContent = true
120+
content := []byte(`{"type":"","text":""}`)
121+
content, _ = sjson.SetBytes(content, "type", partType)
122+
content, _ = sjson.SetBytes(content, "text", text)
123+
contentItems = append(contentItems, content)
128124
}
129125

130126
appendImageContent := func(dataURL string) {
131-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image")
132-
message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL)
133-
contentIndex++
134-
hasContent = true
127+
content := []byte(`{"type":"input_image","image_url":""}`)
128+
content, _ = sjson.SetBytes(content, "image_url", dataURL)
129+
contentItems = append(contentItems, content)
135130
}
136131

137132
appendReasoningContent := func(part gjson.Result) {
@@ -154,10 +149,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
154149
flushMessage()
155150
reasoningItem := []byte(`{"type":"reasoning","summary":[],"content":null}`)
156151
reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature)
157-
template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem)
152+
inputItems = append(inputItems, reasoningItem)
158153
}
159154

160-
messageContentsResult := messageResult.Get("content")
161155
if messageContentsResult.IsArray() {
162156
messageContentResults := messageContentsResult.Array()
163157
for j := 0; j < len(messageContentResults); j++ {
@@ -202,17 +196,16 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
202196
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "name", name)
203197
}
204198
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
205-
template, _ = sjson.SetRawBytes(template, "input.-1", functionCallMessage)
199+
inputItems = append(inputItems, functionCallMessage)
206200
case "tool_result":
207201
flushMessage()
208202
functionCallOutputMessage := []byte(`{"type":"function_call_output"}`)
209203
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "call_id", shortenCodexCallIDIfNeeded(messageContentResult.Get("tool_use_id").String()))
210204

211205
contentResult := messageContentResult.Get("content")
212206
if contentResult.IsArray() {
213-
toolResultContentIndex := 0
214-
toolResultContent := []byte(`[]`)
215207
contentResults := contentResult.Array()
208+
toolResultContentItems := make([][]byte, 0, len(contentResults))
216209
for k := 0; k < len(contentResults); k++ {
217210
toolResultContentType := contentResults[k].Get("type").String()
218211
if toolResultContentType == "image" {
@@ -232,27 +225,27 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
232225
}
233226
dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data)
234227

235-
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_image")
236-
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.image_url", toolResultContentIndex), dataURL)
237-
toolResultContentIndex++
228+
toolResultContent := []byte(`{"type":"input_image","image_url":""}`)
229+
toolResultContent, _ = sjson.SetBytes(toolResultContent, "image_url", dataURL)
230+
toolResultContentItems = append(toolResultContentItems, toolResultContent)
238231
}
239232
}
240233
} else if toolResultContentType == "text" {
241-
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_text")
242-
toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.text", toolResultContentIndex), contentResults[k].Get("text").String())
243-
toolResultContentIndex++
234+
toolResultContent := []byte(`{"type":"input_text","text":""}`)
235+
toolResultContent, _ = sjson.SetBytes(toolResultContent, "text", contentResults[k].Get("text").String())
236+
toolResultContentItems = append(toolResultContentItems, toolResultContent)
244237
}
245238
}
246-
if toolResultContentIndex > 0 {
247-
functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", toolResultContent)
239+
if len(toolResultContentItems) > 0 {
240+
functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", marshalRawJSONArray(toolResultContentItems))
248241
} else {
249242
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
250243
}
251244
} else {
252245
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
253246
}
254247

255-
template, _ = sjson.SetRawBytes(template, "input.-1", functionCallOutputMessage)
248+
inputItems = append(inputItems, functionCallOutputMessage)
256249
}
257250
}
258251
flushMessage()
@@ -266,16 +259,17 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
266259

267260
// Convert tools declarations to the expected format for the Codex API.
268261
toolsResult := rootResult.Get("tools")
262+
var toolItems [][]byte
269263
if toolsResult.IsArray() {
270-
template, _ = sjson.SetRawBytes(template, "tools", []byte(`[]`))
271264
webSearchToolNames := buildClaudeWebSearchToolNameSet(toolsResult)
272265
template, _ = sjson.SetRawBytes(template, "tool_choice", convertClaudeToolChoiceToCodex(rootResult.Get("tool_choice"), toolNameMap, webSearchToolNames))
273266
toolResults := toolsResult.Array()
267+
toolItems = make([][]byte, 0, len(toolResults))
274268
for i := 0; i < len(toolResults); i++ {
275269
toolResult := toolResults[i]
276270
// Special handling: map Claude web search tool to Codex web_search
277271
if isClaudeWebSearchToolType(toolResult.Get("type").String()) {
278-
template, _ = sjson.SetRawBytes(template, "tools.-1", convertClaudeWebSearchToolToCodex(toolResult))
272+
toolItems = append(toolItems, convertClaudeWebSearchToolToCodex(toolResult))
279273
continue
280274
}
281275
tool := []byte(toolResult.Raw)
@@ -296,7 +290,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
296290
tool, _ = sjson.DeleteBytes(tool, "cache_control")
297291
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
298292
tool, _ = sjson.SetBytes(tool, "strict", false)
299-
template, _ = sjson.SetRawBytes(template, "tools.-1", tool)
293+
toolItems = append(toolItems, tool)
300294
}
301295
}
302296

@@ -346,10 +340,35 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
346340
template, _ = sjson.SetBytes(template, "stream", true)
347341
template, _ = sjson.SetBytes(template, "store", false)
348342
template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"})
343+
if toolsResult.IsArray() {
344+
template, _ = sjson.SetRawBytes(template, "tools", marshalRawJSONArray(toolItems))
345+
}
346+
template, _ = sjson.SetRawBytes(template, "input", marshalRawJSONArray(inputItems))
349347

350348
return template
351349
}
352350

351+
func marshalRawJSONArray(items [][]byte) []byte {
352+
size := 2
353+
if len(items) > 1 {
354+
size += len(items) - 1
355+
}
356+
for i := 0; i < len(items); i++ {
357+
size += len(items[i])
358+
}
359+
360+
result := make([]byte, 0, size)
361+
result = append(result, '[')
362+
for i := 0; i < len(items); i++ {
363+
if i > 0 {
364+
result = append(result, ',')
365+
}
366+
result = append(result, items[i]...)
367+
}
368+
result = append(result, ']')
369+
return result
370+
}
371+
353372
func codexClaudeTargetAcceptsGrokSignature(modelName string) bool {
354373
baseModel := strings.ToLower(strings.TrimSpace(thinking.ParseSuffix(modelName).ModelName))
355374
return strings.Contains(baseModel, "grok")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package claude
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
"testing"
7+
8+
"github.com/tidwall/gjson"
9+
)
10+
11+
func BenchmarkConvertClaudeRequestToCodexLargeHistory(b *testing.B) {
12+
for _, turns := range []int{16, 64} {
13+
b.Run(strconv.Itoa(turns)+"_turns", func(b *testing.B) {
14+
request := largeClaudeRequest(turns, 32, 8*1024)
15+
if !gjson.ValidBytes(request) {
16+
b.Fatal("benchmark generated an invalid Claude request")
17+
}
18+
if result := ConvertClaudeRequestToCodex("gpt-5.4", request, false); !gjson.ValidBytes(result) {
19+
b.Fatal("translator generated invalid Codex JSON")
20+
}
21+
b.ReportAllocs()
22+
b.SetBytes(int64(len(request)))
23+
b.ResetTimer()
24+
25+
for i := 0; i < b.N; i++ {
26+
ConvertClaudeRequestToCodex("gpt-5.4", request, false)
27+
}
28+
})
29+
}
30+
}
31+
32+
func largeClaudeRequest(turns, toolCount, payloadSize int) []byte {
33+
payload := strings.Repeat("x", payloadSize)
34+
var request strings.Builder
35+
request.Grow((turns + toolCount) * payloadSize)
36+
request.WriteString(`{"model":"claude-test","system":[{"type":"text","text":"`)
37+
request.WriteString(payload)
38+
request.WriteString(`"}],"messages":[`)
39+
40+
for i := 0; i < turns; i++ {
41+
if i > 0 {
42+
request.WriteByte(',')
43+
}
44+
request.WriteString(`{"role":"assistant","content":[{"type":"text","text":"`)
45+
request.WriteString(payload)
46+
request.WriteString(`"},{"type":"tool_use","id":"toolu_`)
47+
request.WriteString(strconv.Itoa(i))
48+
request.WriteString(`","name":"tool_`)
49+
request.WriteString(strconv.Itoa(i % toolCount))
50+
request.WriteString(`","input":{"value":"`)
51+
request.WriteString(payload)
52+
request.WriteString(`"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_`)
53+
request.WriteString(strconv.Itoa(i))
54+
request.WriteString(`","content":[{"type":"text","text":"`)
55+
request.WriteString(payload)
56+
request.WriteString(`"}]}]}`)
57+
}
58+
59+
request.WriteString(`],"tools":[`)
60+
for i := 0; i < toolCount; i++ {
61+
if i > 0 {
62+
request.WriteByte(',')
63+
}
64+
request.WriteString(`{"name":"tool_`)
65+
request.WriteString(strconv.Itoa(i))
66+
request.WriteString(`","description":"`)
67+
request.WriteString(payload)
68+
request.WriteString(`","input_schema":{"type":"object","properties":{"value":{"type":"string"}}}}`)
69+
}
70+
request.WriteString(`]}`)
71+
return []byte(request.String())
72+
}

internal/translator/codex/claude/codex_claude_request_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,62 @@ func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t
481481
}
482482
}
483483

484+
func TestConvertClaudeRequestToCodex_PreservesContentOrderAcrossToolAndReasoningItems(t *testing.T) {
485+
signature := validCodexReasoningSignature()
486+
inputJSON := `{
487+
"system": "system rules",
488+
"messages": [
489+
{"role":"assistant","content":[
490+
{"type":"text","text":"before reasoning"},
491+
{"type":"thinking","signature":"` + signature + `"},
492+
{"type":"text","text":"before tool"},
493+
{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"query":"test"}},
494+
{"type":"text","text":"after tool"}
495+
]},
496+
{"role":"user","content":[
497+
{"type":"tool_result","tool_use_id":"toolu_1","content":[
498+
{"type":"text","text":"tool output"},
499+
{"type":"image","source":{"media_type":"image/png","data":"aW1hZ2U="}}
500+
]},
501+
{"type":"text","text":"continue"}
502+
]}
503+
],
504+
"tools": [{"name":"lookup","input_schema":{"type":"object"}}]
505+
}`
506+
507+
result := ConvertClaudeRequestToCodex("gpt-5.4", []byte(inputJSON), false)
508+
inputs := gjson.GetBytes(result, "input").Array()
509+
if len(inputs) != 8 {
510+
t.Fatalf("got %d input items, want 8. Output: %s", len(inputs), result)
511+
}
512+
513+
wantTypes := []string{"message", "message", "reasoning", "message", "function_call", "message", "function_call_output", "message"}
514+
for i := 0; i < len(wantTypes); i++ {
515+
if got := inputs[i].Get("type").String(); got != wantTypes[i] {
516+
t.Fatalf("input[%d].type = %q, want %q. Output: %s", i, got, wantTypes[i], result)
517+
}
518+
}
519+
520+
if got := inputs[1].Get("content.0.text").String(); got != "before reasoning" {
521+
t.Fatalf("input[1] text = %q, want before reasoning", got)
522+
}
523+
if got := inputs[3].Get("content.0.text").String(); got != "before tool" {
524+
t.Fatalf("input[3] text = %q, want before tool", got)
525+
}
526+
if got := inputs[5].Get("content.0.text").String(); got != "after tool" {
527+
t.Fatalf("input[5] text = %q, want after tool", got)
528+
}
529+
if got := inputs[6].Get("output.0.type").String(); got != "input_text" {
530+
t.Fatalf("tool result output.0.type = %q, want input_text", got)
531+
}
532+
if got := inputs[6].Get("output.1.image_url").String(); got != "data:image/png;base64,aW1hZ2U=" {
533+
t.Fatalf("tool result image_url = %q, want data URL", got)
534+
}
535+
if got := inputs[7].Get("content.0.text").String(); got != "continue" {
536+
t.Fatalf("input[7] text = %q, want continue", got)
537+
}
538+
}
539+
484540
func TestConvertClaudeRequestToCodex_AssistantGrokSignatureToReasoningItem(t *testing.T) {
485541
signature := "HmlYdr2aCAqCYP/m9mr8PS6KOsdMs72FGDigmydR+Jsmuv8KX97yWPlbOwmXJgWn0CbHaCacdQD3+n5EvpgLfPNmafS3kdICBjRuDf4bzHy7uBiUhNVhqPtp/ee1y9q4imPE4LYgD1VZ4J+bp9mTeqA1+nC9Oue58CiNEMV9SVaGenCD+aBnVuSTzQhD32Y+68i6HLJW0Dx6ifaRfb8hxYtA/sPM+/FTvAMW11nRho5a2BBSkpnzfqqAz/e/vGJ77/bygpXM823QA9wL9i0X"
486542
payload := []byte(`{"model":"grok-4.5","messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"summary","signature":""},{"type":"text","text":"answer"}]},{"role":"user","content":"next"}]}`)

0 commit comments

Comments
 (0)