You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is the piece I deliberately kept out of #2231 / #2232, filed on its own because the fix shape
is a policy call rather than a parity fix, and I would rather leave that call to you than guess it.
#2232 validates the containers — candidates, content, content.parts. Inside a well-formed
part, the values the adapter actually emits are still unchecked, so three shapes cross the AdapterEvent boundary as something other than what the type declares:
A functionCall that is not a record ("x", 5, []) reaches part.functionCall.name as undefined and the adapter emits a tool_call_startwith no name.
A functionCall.name that is absent, non-string, empty, or whitespace is passed through
verbatim — name: undefined, name: 5, name: "", name: " " all become a dispatched tool
call.
A non-string part.text becomes text_delta.text as a number, object or array — and with thought: true, reasoning_raw_delta.text.
AdapterEvent declares both text and name as string. src/adapters/openai-chat.ts now
rejects the equivalent shapes (diagnoseInvalidToolCalls reasons tool_call_function_name_invalid / _blank, plus unnamedToolCallEvent, whose message is
explicit: "upstream streamed a tool call without a function name — cannot dispatch"). The Google
adapter has no equivalent.
Why this is not simply "port the openai-chat rule". OpenAI Chat assembles a tool call across
streamed deltas, so "the name never arrived by flush time" is a well-defined terminal condition and tests/openai-chat-parallel-stream.test.ts had to be re-negotiated around it. Google delivers a
tool call whole, in one part — there is no assembly window and no "it may still arrive". That
makes the rejection easier to justify here, but it also means the openai-chat precedent is not
automatically the right disposition, and a nameless call could plausibly be dropped rather than made
terminal. Which of terminate / drop / pass-through you want is your call, so there is no PR.
Found by audit, not from a live capture — same bar as #1325 and #2231. Verified on dev@03735eca.
Nothing throws — that is the point. These are all quiet: a nameless or numerically-named tool call
goes on to the bridge, and a numeric text_delta flows into whatever concatenates it downstream.
Deliberately excluded
functionCall.args. Google's wire type for arguments is a structured value, not OpenAI's JSON string, and JSON.stringify(part.functionCall.args ?? {}) already tolerates every shape without
losing information. Constraining it would be importing an OpenAI-shaped assumption, not fixing a
defect. [WRONG BRANCH] fix(zcode): use openai-compatible provider protocol to restore prefix caching #2216-era freeform-argument work touches this area, so I left it entirely alone.
-- functionCall container --
A fc:"x" : [tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done]
B fc:5 : [tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done]
C fc:[] : [tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done]
D fc:"x" (buffered) : [tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done]
-- functionCall.name --
E name absent : [tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done]
F name:5 : [tool_call_start(name=5),tool_call_delta("{}"),tool_call_end,done]
G name:"" : [tool_call_start(name=""),tool_call_delta("{}"),tool_call_end,done]
H name:" " : [tool_call_start(name=" "),tool_call_delta("{}"),tool_call_end,done]
I name:5 (buffered) : [tool_call_start(name=5),tool_call_delta("{}"),tool_call_end,done]
-- part.text --
J text:5 : [text_delta(text=5),done]
K text:{a:1} : [text_delta(text={"a":1}),done]
L text:[1,2] : [text_delta(text=[1,2]),done]
M text:5, thought : [reasoning_raw_delta(text=5),done]
N text:5 (buffered) : [text_delta(text=5),done]
-- control: well-formed --
O good : [text_delta(text="hi"),tool_call_start(name="lookup"),tool_call_delta("{\"q\":1}"),tool_call_end,done]
Version
dev at 03735eca (verified), Bun 1.3.14, Windows 11.
Operating system
Windows 11 Pro 24H2 (10.0.26200). Reproduced through the repository's own isolated test runner
(bun scripts/test.ts) against the adapter directly, so the defect is platform-independent — the
parser control flow is the same on every platform.
Notes
No PR, on purpose. If you tell me which disposition you want — terminal structured error like openai-chat, silent drop of the offending part, or coerce-and-continue — I will write it with
regressions for all fourteen shapes on both paths. It is also perfectly reasonable to close this as
won't-fix: no first-party Google backend produces these, and the exposure is a Gemini-compatible
third-party baseUrl or a non-Google CCA response envelope.
Client or integration
Not client-specific — reproduced by driving the adapter directly with a synthetic
Response.Area
Provider adapter · Streaming / SSE parsing · Non-streaming response parsing
Summary
This is the piece I deliberately kept out of #2231 / #2232, filed on its own because the fix shape
is a policy call rather than a parity fix, and I would rather leave that call to you than guess it.
#2232 validates the containers —
candidates,content,content.parts. Inside a well-formedpart, the values the adapter actually emits are still unchecked, so three shapes cross the
AdapterEventboundary as something other than what the type declares:functionCallthat is not a record ("x",5,[]) reachespart.functionCall.nameasundefinedand the adapter emits atool_call_startwith no name.functionCall.namethat is absent, non-string, empty, or whitespace is passed throughverbatim —
name: undefined,name: 5,name: "",name: " "all become a dispatched toolcall.
part.textbecomestext_delta.textas a number, object or array — and withthought: true,reasoning_raw_delta.text.AdapterEventdeclares bothtextandnameasstring.src/adapters/openai-chat.tsnowrejects the equivalent shapes (
diagnoseInvalidToolCallsreasonstool_call_function_name_invalid/_blank, plusunnamedToolCallEvent, whose message isexplicit: "upstream streamed a tool call without a function name — cannot dispatch"). The Google
adapter has no equivalent.
Why this is not simply "port the openai-chat rule". OpenAI Chat assembles a tool call across
streamed deltas, so "the name never arrived by flush time" is a well-defined terminal condition and
tests/openai-chat-parallel-stream.test.tshad to be re-negotiated around it. Google delivers atool call whole, in one part — there is no assembly window and no "it may still arrive". That
makes the rejection easier to justify here, but it also means the openai-chat precedent is not
automatically the right disposition, and a nameless call could plausibly be dropped rather than made
terminal. Which of terminate / drop / pass-through you want is your call, so there is no PR.
Found by audit, not from a live capture — same bar as #1325 and #2231. Verified on
dev@03735eca.partselementdev@03735eca{ functionCall: "x" }tool_call_start(name=undefined),tool_call_delta("{}"),tool_call_end,done{ functionCall: 5 }{ functionCall: [] }{ functionCall: "x" }, buffered{ functionCall: { args: {} } }(noname)tool_call_start(name=undefined){ functionCall: { name: 5, args: {} } }tool_call_start(name=5){ functionCall: { name: "", args: {} } }tool_call_start(name=""){ functionCall: { name: " ", args: {} } }tool_call_start(name=" "){ functionCall: { name: 5, args: {} } }, bufferedtool_call_start(name=5){ text: 5 }text_delta(text=5){ text: { a: 1 } }text_delta(text={"a":1}){ text: [1, 2] }text_delta(text=[1,2]){ text: 5, thought: true }reasoning_raw_delta(text=5){ text: 5 }, bufferedtext_delta(text=5)Nothing throws — that is the point. These are all quiet: a nameless or numerically-named tool call
goes on to the bridge, and a numeric
text_deltaflows into whatever concatenates it downstream.Deliberately excluded
functionCall.args. Google's wire type for arguments is a structured value, not OpenAI's JSONstring, and
JSON.stringify(part.functionCall.args ?? {})already tolerates every shape withoutlosing information. Constraining it would be importing an OpenAI-shaped assumption, not fixing a
defect. [WRONG BRANCH] fix(zcode): use openai-compatible provider protocol to restore prefix caching #2216-era freeform-argument work touches this area, so I left it entirely alone.
partsnot an array, an element that is not an object,contentnot arecord, a non-record candidate. Those are [Bug] Google adapter: candidate, content and parts containers escape the #1332 nested-shape rule #2231, fixed in fix(google): validate candidate, content and part containers (#2231) #2232.
part.textthat is an empty string. Already falsy, already skipped, already correct.Reproduction
Save as
tests/zz-google-field-probe.test.tsondev, runbun scripts/test.ts tests/zz-google-field-probe.test.ts, then delete it.Logs or error output
Version
devat03735eca(verified), Bun 1.3.14, Windows 11.Operating system
Windows 11 Pro 24H2 (10.0.26200). Reproduced through the repository's own isolated test runner
(
bun scripts/test.ts) against the adapter directly, so the defect is platform-independent — theparser control flow is the same on every platform.
Notes
No PR, on purpose. If you tell me which disposition you want — terminal structured error like
openai-chat, silent drop of the offending part, or coerce-and-continue — I will write it withregressions for all fourteen shapes on both paths. It is also perfectly reasonable to close this as
won't-fix: no first-party Google backend produces these, and the exposure is a Gemini-compatible
third-party
baseUrlor a non-Google CCAresponseenvelope.