From 07cf616e2b9b3143d4b02c75fe4f94e2e208db6f Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy Date: Mon, 16 Feb 2026 00:20:23 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20handle=20response.function=5Fcall=5F?= =?UTF-8?q?arguments.done=20in=20codex=E2=86=92claude=20streaming=20transl?= =?UTF-8?q?ator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Codex models (e.g. gpt-5.3-codex-spark) send function call arguments in a single "done" event without preceding "delta" events. The streaming translator only handled "delta" events, causing tool call arguments to be lost — resulting in empty tool inputs and infinite retry loops in clients like Claude Code. Emit the full arguments from the "done" event as a single input_json_delta so downstream clients receive the complete tool input. --- .../codex/claude/codex_claude_response.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index b39494b7..6f18e24d 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -177,6 +177,19 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa output += "event: content_block_delta\n" output += fmt.Sprintf("data: %s\n\n", template) + } else if typeStr == "response.function_call_arguments.done" { + // Some models (e.g. gpt-5.3-codex-spark) send function call arguments + // in a single "done" event without preceding "delta" events. + // Emit the full arguments as a single input_json_delta so the + // downstream Claude client receives the complete tool input. + if args := rootResult.Get("arguments").String(); args != "" { + template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.Set(template, "delta.partial_json", args) + + output += "event: content_block_delta\n" + output += fmt.Sprintf("data: %s\n\n", template) + } } return []string{output} From 1cc21cc45bbb51f0c703b76afc4c6eeb127afe69 Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy Date: Mon, 16 Feb 2026 02:48:59 +0300 Subject: [PATCH 2/2] fix: prevent duplicate function call arguments when delta events precede done Non-spark codex models (gpt-5.3-codex, gpt-5.2-codex) stream function call arguments via multiple delta events followed by a done event. The done handler unconditionally emitted the full arguments, duplicating what deltas already streamed. This produced invalid double JSON that Claude Code couldn't parse, causing tool calls to fail with missing parameters and infinite retry loops. Add HasReceivedArgumentsDelta flag to track whether delta events were received. The done handler now only emits arguments when no deltas preceded it (spark models), while delta-based streaming continues to work for non-spark models. --- .../codex/claude/codex_claude_response.go | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index 6f18e24d..cdcf2e4f 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -22,8 +22,9 @@ var ( // ConvertCodexResponseToClaudeParams holds parameters for response conversion. type ConvertCodexResponseToClaudeParams struct { - HasToolCall bool - BlockIndex int + HasToolCall bool + BlockIndex int + HasReceivedArgumentsDelta bool } // ConvertCodexResponseToClaude performs sophisticated streaming response format conversion. @@ -137,6 +138,7 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa itemType := itemResult.Get("type").String() if itemType == "function_call" { (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall = true + (*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = false template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}` template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "content_block.id", itemResult.Get("call_id").String()) @@ -171,6 +173,7 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa output += fmt.Sprintf("data: %s\n\n", template) } } else if typeStr == "response.function_call_arguments.delta" { + (*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = true template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String()) @@ -182,13 +185,16 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa // in a single "done" event without preceding "delta" events. // Emit the full arguments as a single input_json_delta so the // downstream Claude client receives the complete tool input. - if args := rootResult.Get("arguments").String(); args != "" { - template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "delta.partial_json", args) + // When delta events were already received, skip to avoid duplicating arguments. + if !(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta { + if args := rootResult.Get("arguments").String(); args != "" { + template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.Set(template, "delta.partial_json", args) - output += "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output += "event: content_block_delta\n" + output += fmt.Sprintf("data: %s\n\n", template) + } } }