fix(responses): sanitize empty string placeholders from tool-call optional arguments in stream delta accumulation (#1674)

This commit is contained in:
diegosouzapw
2026-04-27 19:39:28 -03:00
parent 3ee0150367
commit b1974dac12
3 changed files with 37 additions and 3 deletions

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -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: "",