Conversation
filterToOpenAIFormat took two shortcuts for assistant messages carrying tool_calls: skip content normalisation, and never drop the message. Both were gated on plain truthiness, and an empty array is truthy. An assistant turn arriving as tool_calls: [] therefore kept its Claude-only blocks (thinking, redacted_thinking) instead of having them stripped, and survived the empty-message filter with blank content. Upstreams that reject either shape rejected the whole request. Switches both guards to tool_calls?.length, matching claude-to-openai.js and the kiro executor. Messages with real tool calls are unaffected.
ecf90bb to
c7095f7
Compare
|
Rebased check against current Since I opened this, #10a923da1 ("fix(responses): don't close message on empty tool_calls array", @chisewaguri) landed and fixed the same class of bug — an empty This PR is the sibling case in the other translator. 26: // Keep assistant messages with tool_calls as-is
27: if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg;
...
67: // Always keep assistant messages with tool_calls
68: if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return true;Both are truthiness checks on the array itself, so Diff is 4 lines in the source plus a regression test. Happy to rebase or split it further if that helps. |
|
Superseded by #3878, which carries this commit unchanged, rebased onto current |
Summary
filterToOpenAIFormattreats an assistant message as "carries tool calls" whenevermsg.tool_callsis truthy. An empty array is truthy, sotool_calls: []trips both shortcuts that check it:Two things follow from that. An assistant turn with
tool_calls: []keeps its Claude-only blocks —thinking,redacted_thinking, unstrippedsignature— because it never reaches the block filter. And a turn whose content is blank survives the filter that exists to drop exactly those. Providers that reject either shape reject the whole request, and the failure surfaces far from here.tool_calls: []is not hypothetical in this codebase — #3234 and #3236 both come from upstreams that attach an empty array to ordinary content deltas.The fix
Both guards become
msg.tool_calls?.length, which is what the rest of the codebase already does:translator/request/claude-to-openai.js:100—msg.tool_calls && msg.tool_calls.length > 0executors/kiro.js:208—delta.tool_calls?.lengthMessages with real tool calls are unaffected.
Tests
tests/unit/openai-format-empty-tool-calls.test.js, three cases:tool_calls: []is presenttool_callsand its contentThe first two fail on
masterand pass with the change:Regression check
Per CLAUDE.md the suite is not green on a plain checkout, so I ran it both ways and diffed the failing set rather than reading the totals.
Identical failing sets — 192 failure lines each way, no test moved in either direction. The delta is exactly the three added cases.
One thing I noticed
open-sse/transformer/responsesTransformer.js:374has the sameif (delta.tool_calls)shape, andopen-sse/handlers/responsesHandler.jsis the only importer of it — while nothing importshandleResponsesCorein turn. The live/v1/responsesroute (src/app/api/v1/responses/route.js) goes throughhandleChatand the translator registry instead. So that pair looks like it has been left behind rather than being a second live path, and I left it alone. Worth confirming on your side — if it is dead, it is a duplicate of the conversion intranslator/response/openai-responses.jsand probably wants removing.