Skip to content

fix(sse): project non-streaming JSON back to the Gemini/Antigravity envelope - #7255

Merged
diegosouzapw merged 2 commits into
release/v3.8.49from
fix/port-pr-2348-unify-json-response-projection
Jul 17, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.49from
fix/port-pr-2348-unify-json-response-projection

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

  • The streaming and non-streaming response paths disagreed on how a response is projected back into a non-OpenAI client's wire format. Streaming projects OpenAI chunks into the Gemini/Antigravity { response: { candidates: [...] } } envelope via the registered FORMATS.OPENAI -> FORMATS.ANTIGRAVITY translator; the non-streaming path only special-cased FORMATS.CLAUDE and returned the raw OpenAI chat.completion intermediate for everything else.
  • Effect: a Gemini/Antigravity client on a non-streaming request received choices[]/tool_calls instead of candidates[]/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.
  • Fix closes exactly that projection gap, mirroring the shape the streaming translator already emits so both paths agree.

Attribution

Thanks to @warelik for the original implementation.

Changes

  • open-sse/handlers/responseTranslator.ts: add convertOpenAINonStreamingToGeminiFamily() and wire it into the Phase-3 "translate back to client format" step for FORMATS.GEMINI / FORMATS.ANTIGRAVITY.
  • Tool-call arguments parse 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 (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 OLLAMA in FORMATS), the Responses projection, and the SSE terminal/[DONE] tracker — that handling already lives in nonStreamingSse.ts + the registry. Adjacent to #7207 (which registered an OPENAI -> GEMINI streaming projection); this covers the non-streaming path and does not overlap it.

Test plan

  • targeted unit test — new, fails before / passes after:
    • before: expected {response:{candidates:[...]}} envelope for an antigravity client, got: {"id":"chatcmpl-1","object":"chat.completion","choices":[...]} (2/2 failing)
    • after: 3/3 passing
  • tests/unit/plan3-p0.test.ts (pre-existing suite covering translateNonStreamingResponse) — 38/38 passing, no regression
  • targeted npx tsc --noEmit on the changed file — clean (file IS in the tsconfig.typecheck-core.json allowlist)
  • npx eslint on changed files — clean
  • npx prettier --check on changed files — clean

diegosouzapw and others added 2 commits July 15, 2026 04:19
…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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@diegosouzapw

Copy link
Copy Markdown
Owner Author

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.

@diegosouzapw
diegosouzapw merged commit 98966fd into release/v3.8.49 Jul 17, 2026
15 checks passed
@diegosouzapw
diegosouzapw deleted the fix/port-pr-2348-unify-json-response-projection branch July 19, 2026 21:01
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant