From 1cc21cc45bbb51f0c703b76afc4c6eeb127afe69 Mon Sep 17 00:00:00 2001 From: Kirill Turanskiy Date: Mon, 16 Feb 2026 02:48:59 +0300 Subject: [PATCH] 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) + } } }