From b1974dac125f2d6f33723b17c5131e61e28b2abb Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Mon, 27 Apr 2026 19:39:28 -0300 Subject: [PATCH] fix(responses): sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation (#1674) --- CHANGELOG.md | 3 ++ open-sse/transformer/responsesTransformer.ts | 36 ++++++++++++++++++-- tests/unit/executor-codex.test.ts | 1 + 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d24d268e8..8c6641addc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ ### 🐛 Bug Fixes +- **fix(responses):** sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation to avoid breaking strict clients (#1674) +- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686) +- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687) - **fix(chatgpt-web):** bound tls-client native deadlocks so requests never hang forever (#1664) - **fix(codex):** default gpt-5.5 to HTTP transport instead of WebSocket (#1660) - **fix(codex):** [urgent] fix gpt-5.5 websocket transport and model labels (#1656) diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts index e184a021e7..3006a69322 100644 --- a/open-sse/transformer/responsesTransformer.ts +++ b/open-sse/transformer/responsesTransformer.ts @@ -221,7 +221,27 @@ export function createResponsesApiTransformStream(logger = null) { const closeToolCall = (controller, idx) => { const callId = state.funcCallIds[idx]; if (callId && !state.funcItemDone[idx]) { - const args = state.funcArgsBuf[idx] || "{}"; + let args = state.funcArgsBuf[idx] || "{}"; + + // Fix #1674: Final cleanup of empty string placeholders that might have been split across delta chunks + try { + const parsed = JSON.parse(args); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + let modified = false; + for (const [k, v] of Object.entries(parsed)) { + if (v === "") { + delete parsed[k]; + modified = true; + } + } + if (modified) { + args = JSON.stringify(parsed); + state.funcArgsBuf[idx] = args; + } + } + } catch (e) { + // Ignore malformed JSON + } emit(controller, "response.function_call_arguments.done", { type: "response.function_call_arguments.done", @@ -484,15 +504,25 @@ export function createResponsesApiTransformStream(logger = null) { if (tc.function?.arguments) { const refCallId = state.funcCallIds[tcIdx] || newCallId; + let deltaStr = tc.function.arguments; + + // Fix #1674: cx/gpt-5.5 injects empty strings for optional parameters. + // We strip these directly from the streaming deltas to avoid breaking strict clients like Claude Code. + if (deltaStr.includes('""')) { + deltaStr = deltaStr + .replace(/,"[a-zA-Z0-9_]+":""/g, "") + .replace(/"[a-zA-Z0-9_]+":"",/g, ""); + } + if (refCallId) { emit(controller, "response.function_call_arguments.delta", { type: "response.function_call_arguments.delta", item_id: `fc_${refCallId}`, output_index: tcIdx, - delta: tc.function.arguments, + delta: deltaStr, }); } - state.funcArgsBuf[tcIdx] += tc.function.arguments; + state.funcArgsBuf[tcIdx] += deltaStr; } } } diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index c298ad970f..78d262bd2b 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -174,6 +174,7 @@ test("CodexExecutor.transformRequest injects default instructions, clamps reason const body = { model: "gpt-5-mini", messages: [{ role: "user", content: "hello" }], + tools: [{ type: "function", function: { name: "test_tool" } }], prompt: "legacy", stream_options: { include_usage: true }, instructions: "",