Repository navigation
fix(sse): project non-streaming JSON back to the Gemini/Antigravity envelope - #7255
diegosouzapw merged 2 commits into
Conversation
…nvelope
The streaming and non-streaming response paths disagreed on how a response is
projected back into a non-OpenAI client's wire format.
Streaming goes through the translator registry, where the
FORMATS.OPENAI -> FORMATS.ANTIGRAVITY translator
(open-sse/translator/response/openai-to-antigravity.ts) projects each OpenAI
chunk into the `{ response: { candidates: [...] } }` envelope, mapping
tool_calls to `functionCall` parts and reasoning to `thought` parts.
The non-streaming path uses translateNonStreamingResponse() instead. Its
"Phase 3: translate back to client source format" step only special-cased
FORMATS.CLAUDE — every other non-OpenAI client format fell through and returned
the raw OpenAI chat.completion intermediate. A Gemini/Antigravity client issuing
a non-streaming request therefore received `choices[]`/`tool_calls` instead of
`candidates[]`/`functionCall`: the client's parser sees no candidates and the
function calls are effectively dropped, so tool-calling silently breaks on the
JSON path while working over SSE.
Adds convertOpenAINonStreamingToGeminiFamily() and wires it into Phase 3 for
FORMATS.GEMINI / FORMATS.ANTIGRAVITY, mirroring the shape the streaming
translator already emits so both paths agree. Tool-call `arguments` are parsed
through a non-throwing helper: a provider emitting truncated JSON degrades that
call's args to `{}` rather than raising an uncaught SyntaxError in the shared
response hot path (matching the streaming translator's behaviour).
Scoped deliberately narrow: only the Gemini-family projection gap proven by the
failing test is closed. The Ollama/Responses projections and the SSE terminal
tracker from the upstream change are not ported — OmniRoute has no OLLAMA format
in FORMATS, and its Responses/[DONE] handling already lives in
nonStreamingSse.ts + the registry.
Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Inspired-by: decolua/9router#2348
There was a problem hiding this comment.
Code Review
This pull request implements non-streaming response translation for Gemini-family clients (Gemini and Antigravity) in open-sse/handlers/responseTranslator.ts. It introduces convertOpenAINonStreamingToGeminiFamily to project OpenAI-shaped chat completions into the expected Gemini response envelope, matching the existing streaming translation behavior. It also adds robust parsing for tool-call arguments and comprehensive unit tests in tests/unit/nonstreaming-gemini-family-response-projection.test.ts to prevent regression. No review comments were provided, so there is no additional feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Aprovado. Rodei o probe: 3/3 testes passam com a correção e os 3 falham revertendo só o hunk de produção (fail-without-fix confirmado). Suíte de regressão plan3-p0 (38/38) segue verde. Conferi a colisão com #7207 — arquivos distintos (streaming vs non-streaming), sem sobreposição textual. Pronto para merge. |
…nvelope (diegosouzapw#7255) * fix(sse): project non-streaming JSON back to the Gemini/Antigravity envelope The streaming and non-streaming response paths disagreed on how a response is projected back into a non-OpenAI client's wire format. Streaming goes through the translator registry, where the FORMATS.OPENAI -> FORMATS.ANTIGRAVITY translator (open-sse/translator/response/openai-to-antigravity.ts) projects each OpenAI chunk into the `{ response: { candidates: [...] } }` envelope, mapping tool_calls to `functionCall` parts and reasoning to `thought` parts. The non-streaming path uses translateNonStreamingResponse() instead. Its "Phase 3: translate back to client source format" step only special-cased FORMATS.CLAUDE — every other non-OpenAI client format fell through and returned the raw OpenAI chat.completion intermediate. A Gemini/Antigravity client issuing a non-streaming request therefore received `choices[]`/`tool_calls` instead of `candidates[]`/`functionCall`: the client's parser sees no candidates and the function calls are effectively dropped, so tool-calling silently breaks on the JSON path while working over SSE. Adds convertOpenAINonStreamingToGeminiFamily() and wires it into Phase 3 for FORMATS.GEMINI / FORMATS.ANTIGRAVITY, mirroring the shape the streaming translator already emits so both paths agree. Tool-call `arguments` are parsed through a non-throwing helper: a provider emitting truncated JSON degrades that call's args to `{}` rather than raising an uncaught SyntaxError in the shared response hot path (matching the streaming translator's behaviour). Scoped deliberately narrow: only the Gemini-family projection gap proven by the failing test is closed. The Ollama/Responses projections and the SSE terminal tracker from the upstream change are not ported — OmniRoute has no OLLAMA format in FORMATS, and its Responses/[DONE] handling already lives in nonStreamingSse.ts + the registry. Co-authored-by: W ARELIK <warelik@users.noreply.github.com> Inspired-by: decolua/9router#2348 * chore(changelog): fragment for diegosouzapw#7255 --------- Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
…nvelope (diegosouzapw#7255) * fix(sse): project non-streaming JSON back to the Gemini/Antigravity envelope The streaming and non-streaming response paths disagreed on how a response is projected back into a non-OpenAI client's wire format. Streaming goes through the translator registry, where the FORMATS.OPENAI -> FORMATS.ANTIGRAVITY translator (open-sse/translator/response/openai-to-antigravity.ts) projects each OpenAI chunk into the `{ response: { candidates: [...] } }` envelope, mapping tool_calls to `functionCall` parts and reasoning to `thought` parts. The non-streaming path uses translateNonStreamingResponse() instead. Its "Phase 3: translate back to client source format" step only special-cased FORMATS.CLAUDE — every other non-OpenAI client format fell through and returned the raw OpenAI chat.completion intermediate. A Gemini/Antigravity client issuing a non-streaming request therefore received `choices[]`/`tool_calls` instead of `candidates[]`/`functionCall`: the client's parser sees no candidates and the function calls are effectively dropped, so tool-calling silently breaks on the JSON path while working over SSE. Adds convertOpenAINonStreamingToGeminiFamily() and wires it into Phase 3 for FORMATS.GEMINI / FORMATS.ANTIGRAVITY, mirroring the shape the streaming translator already emits so both paths agree. Tool-call `arguments` are parsed through a non-throwing helper: a provider emitting truncated JSON degrades that call's args to `{}` rather than raising an uncaught SyntaxError in the shared response hot path (matching the streaming translator's behaviour). Scoped deliberately narrow: only the Gemini-family projection gap proven by the failing test is closed. The Ollama/Responses projections and the SSE terminal tracker from the upstream change are not ported — OmniRoute has no OLLAMA format in FORMATS, and its Responses/[DONE] handling already lives in nonStreamingSse.ts + the registry. Co-authored-by: W ARELIK <warelik@users.noreply.github.com> Inspired-by: decolua/9router#2348 * chore(changelog): fragment for diegosouzapw#7255 --------- Co-authored-by: W ARELIK <warelik@users.noreply.github.com>
Summary
{ response: { candidates: [...] } }envelope via the registeredFORMATS.OPENAI -> FORMATS.ANTIGRAVITYtranslator; the non-streaming path only special-casedFORMATS.CLAUDEand returned the raw OpenAIchat.completionintermediate for everything else.choices[]/tool_callsinstead ofcandidates[]/functionCall. The client's parser finds no candidates and the function calls are effectively dropped — tool-calling silently breaks on the JSON path while working fine over SSE.Attribution
Thanks to @warelik for the original implementation.
Changes
open-sse/handlers/responseTranslator.ts: addconvertOpenAINonStreamingToGeminiFamily()and wire it into the Phase-3 "translate back to client format" step forFORMATS.GEMINI/FORMATS.ANTIGRAVITY.argumentsparse through a non-throwing helper — a provider emitting truncated JSON degrades that call's args to{}rather than raising an uncaughtSyntaxErrorin the shared response hot path (matches the streaming translator's behaviour).tests/unit/nonstreaming-gemini-family-response-projection.test.ts: new regression guard (3 cases).Scope
Deliberately narrow — only the Gemini-family projection gap proven by the failing test is closed. No refactor of the shared hot response path. Not ported from the original change: the Ollama projection (OmniRoute has no
OLLAMAinFORMATS), the Responses projection, and the SSE terminal/[DONE]tracker — that handling already lives innonStreamingSse.ts+ the registry. Adjacent to #7207 (which registered anOPENAI -> GEMINIstreaming projection); this covers the non-streaming path and does not overlap it.Test plan
expected {response:{candidates:[...]}} envelope for an antigravity client, got: {"id":"chatcmpl-1","object":"chat.completion","choices":[...]}(2/2 failing)tests/unit/plan3-p0.test.ts(pre-existing suite coveringtranslateNonStreamingResponse) — 38/38 passing, no regressionnpx tsc --noEmiton the changed file — clean (file IS in thetsconfig.typecheck-core.jsonallowlist)npx eslinton changed files — cleannpx prettier --checkon changed files — clean