Repository navigation
feat(translator): OpenAI SSE → Gemini SSE for /v1beta/models route - #4453
Merged
Merged
Conversation
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…ls route
The @google/genai SDK (Gemini CLI) always calls
`:streamGenerateContent?alt=sse` for chat and expects Gemini SSE chunks
— no `[DONE]` sentinel, the stream just closes. The v1beta route was
forwarding OpenAI SSE from `handleChat` verbatim, so the SDK crashed
on the `[DONE]` line with:
SyntaxError: Unexpected token 'D', "[DONE]" is not valid JSON
Add a new `transformOpenAISSEToGeminiSSE()` (and a sibling
`convertOpenAIResponseToGemini()` for the non-streaming JSON path) in
`open-sse/translator/response/openai-to-gemini-sse.ts`. The TransformStream
re-emits each OpenAI delta as a Gemini-shape candidate, maps
`finish_reason` → `finishReason` (STOP / MAX_TOKENS / SAFETY), attaches
`usageMetadata` + `modelVersion` on the final chunk, and surfaces
`reasoning_content` as `{ thought: true }` parts for thinking models.
Streaming intent is now derived from the URL action suffix
(`:streamGenerateContent` vs `:generateContent`), which is the canonical
Gemini API convention — `generationConfig.stream` is not a real Gemini
field and the SDK never sets it.
TDD: 11 new tests in `tests/unit/translator-openai-to-gemini-sse.test.ts`
covering the finish-reason map, per-chunk transform, full SSE conversion
(including chunk-boundary buffering and pass-through of non-OK upstreams),
and the non-streaming JSON conversion.
Ported from decolua/9router#225 by SteelMorgan.
Co-authored-by: SteelMorgan <steelmorgan33@gmail.com>
diegosouzapw
force-pushed
the
feat/port-pr-225-gemini-sse-v1beta
branch
from
June 20, 2026 23:26
0253bd3 to
8c7ed3d
Compare
tkgo11
pushed a commit
to tkgo11/OmniRoute
that referenced
this pull request
Sep 23, 2026
…ls route (diegosouzapw#4453) Integrated into release/v3.8.32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports decolua/9router#225 by @SteelMorgan.
The
@google/genaiSDK (Gemini CLI) always calls/v1beta/models/{model}:streamGenerateContent?alt=ssefor chat and expectsGemini SSE chunks. The previous v1beta route forwarded raw OpenAI SSE from
handleChat, so the SDK crashed on the[DONE]sentinel that OpenAI SSEterminates with (Gemini SSE has no sentinel — the stream just closes):
Changes
New translator
open-sse/translator/response/openai-to-gemini-sse.tstransformOpenAISSEToGeminiSSE(upstream, model)—TransformStreamthatre-emits each OpenAI delta as a Gemini-shape candidate, maps
finish_reason→finishReason(STOP / MAX_TOKENS / SAFETY), attachesusageMetadata+modelVersionon the final chunk, and surfacesreasoning_contentas{ thought: true }parts for thinking models.convertOpenAIResponseToGemini(response, model)— sibling JSONconverter for the non-streaming
:generateContentpath.OPENAI_TO_GEMINI_FINISH_REASONexported as a shared constant.TextDecoderboundaries so it neverJSON.parses a half-event.Route rewrite
src/app/api/v1beta/models/[...path]/route.ts(
:streamGenerateContentvs:generateContent), the canonical GeminiAPI convention.
generationConfig.streamis not a real Gemini fieldand the SDK never sets it.
transformOpenAISSEToGeminiSSE; non-streaming branch hands the JSONto
convertOpenAIResponseToGemini.buildClientRawRequestplumbing, andsanitizeErrorMessageerror path are preserved untouched.Skipped from upstream: the first hunk of chore(deps): bump actions/cache from 4 to 5 #225 (filter nameless
Responses-API tools in
openai-to-responses.js) is not ported —OmniRoute's
open-sse/translator/request/openai-responses.tsalready hasa much richer tool-conversion pipeline (web-search passthrough, MCP
namespace flattening,
local_shellmapping, etc.) and theempty-name-filter case is unreachable in practice in this codebase.
Test plan
node --import tsx/esm --test tests/unit/translator-openai-to-gemini-sse.test.ts— 11/11 passnode --import tsx/esm --test tests/unit/v1beta-models-route.test.ts— 2/2 pass (route GET path unaffected)npm run typecheck:core— cleannpx eslinton changed files — cleannode scripts/check/check-file-size.mjs— cleanTDD coverage
11 new tests in
tests/unit/translator-openai-to-gemini-sse.test.ts:attaches usage/modelVersion on the final chunk, maps each
finish_reasoncorrectly[DONE]sentinel and produces the exactGemini chunk shape the SDK expects
and asserts the transformer re-assembles it
bodies, surfaces upstream errors with the right status code