fix(sse): non-stream OpenAI-compatible requests no longer coerced to SSE when Accept lists application/json (#5305) - #5309
Conversation
…hen Accept lists application/json (#5305) The Vercel AI SDK / OpenAI SDK non-stream path (doGenerate/generateText) omits `stream` in the body and sends `Accept: application/json, text/event-stream`, then parses the response as JSON. OmniRoute coerced such requests into SSE — via the route-level Accept override (src/sse/handlers/chat.ts, #302) and resolveStreamFlag — so the caller got `data: {...}` and failed with "Invalid JSON response". An Accept header that explicitly lists application/json is now treated as a JSON opt-in even when it also lists text/event-stream. Only a pure `Accept: text/event-stream` (no application/json) still opts an omitted-stream request into SSE; an explicit body `stream` value always wins. The two former copies of the decision are unified in a shared `acceptHeaderForcesStream` helper. Regression guard: tests/unit/sse-nonstream-accept-5305.test.ts (10 cases). Co-authored-by: md-riaz <md-riaz@users.noreply.github.com>
There was a problem hiding this comment.
Code Review
This pull request resolves an issue where clients using the OpenAI or Vercel AI SDK non-stream signature (omitting stream in the body but sending Accept: application/json, text/event-stream) were incorrectly forced into SSE streaming mode. It introduces the acceptHeaderForcesStream utility and updates resolveStreamFlag and handleChat to ensure these requests default to JSON. A new test file is also added. The review feedback correctly points out that the new test file should use the repository's standard vitest framework instead of the Node.js built-in node:test and node:assert modules.
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.
| import { describe, it } from "node:test"; | ||
| import assert from "node:assert/strict"; |
There was a problem hiding this comment.
The repository uses Vitest as its primary testing framework (as indicated by vitest.config.ts in the root and the repository style guide references to Vitest files). Importing from node:test and node:assert introduces inconsistency and can cause issues with Vitest's test runner, coverage tools, or mocking features. Using vitest instead of the Node.js built-in test runner ensures consistency across the test suite.
| import { describe, it } from "node:test"; | |
| import assert from "node:assert/strict"; | |
| import { describe, it, assert } from "vitest"; |
…; mixed Accept + omitted stream → JSON) The existing 'Accept text/event-stream → stream=true' test used the mixed `application/json, text/event-stream` header — the exact Vercel/OpenAI SDK non-stream signature that #5305 now resolves to JSON. Switch it to a pure `Accept: text/event-stream` (the canonical SSE opt-in it meant to assert) and add a sibling test proving the mixed header + omitted stream returns JSON.
…with #5278/#5309 - provider-health-autopilot: cross-site mutation rejection moved from the route handler into the authz pipeline (#5278); drive the assertion through runAuthzPipeline (the real enforcement point) → 403 + connection untouched. - chat-pipeline: a mixed 'application/json, text/event-stream' Accept now resolves to JSON (#5305/#5309 Vercel/OpenAI SDK non-stream signature); the SSE-opt-in test now sends a pure 'text/event-stream' Accept, the case #5309 keeps as streaming.
…SSE when Accept lists application/json (diegosouzapw#5305) (diegosouzapw#5309) Integrated into release/v3.8.40
…with diegosouzapw#5278/diegosouzapw#5309 - provider-health-autopilot: cross-site mutation rejection moved from the route handler into the authz pipeline (diegosouzapw#5278); drive the assertion through runAuthzPipeline (the real enforcement point) → 403 + connection untouched. - chat-pipeline: a mixed 'application/json, text/event-stream' Accept now resolves to JSON (diegosouzapw#5305/diegosouzapw#5309 Vercel/OpenAI SDK non-stream signature); the SSE-opt-in test now sends a pure 'text/event-stream' Accept, the case diegosouzapw#5309 keeps as streaming.
Closes #5305
Problem
The Vercel AI SDK / OpenAI SDK non-stream path (
doGenerate()/generateText()) omitsstreamin the body and sendsAccept: application/json, text/event-stream, then parses the response as JSON. OmniRoute coerced these into SSE, so the caller received:Two code paths did the coercion (both confirmed by the reporter's excellent source analysis):
src/sse/handlers/chat.ts— route-level Accept override (FIX fix(docker): use /api/monitoring/health for Docker healthcheck (#296) #302): forcedstream=truewheneverAcceptcontainedtext/event-streamand the body omittedstream.open-sse/utils/aiSdkCompat.ts::resolveStreamFlag—clientWantsJsonResponsereturnsfalseforapplication/json, text/event-stream, so the fallback streamed.Fix
An Accept header that explicitly lists
application/jsonis treated as a JSON opt-in, even when it also liststext/event-stream. Concretely:Accept: text/event-stream(noapplication/json) still opts an omitted-streamrequest into SSE.streamvalue (true/false) always wins (unchanged).*/*/ no Accept → still streams (legacy default unchanged).providerRequiresStreaming(stream-only providers, [BUG]: 400 Improperly formed request when sending requests with 24 tools #2081) → still streams (unchanged).The two former copies of the decision are unified into a shared
acceptHeaderForcesStream(acceptHeader, bodyStream)helper used by bothchat.tsand (mirrored in)resolveStreamFlag.TDD / Validation (Hard Rule #18)
New
tests/unit/sse-nonstream-accept-5305.test.ts(failed on the unfixed code — the helper didn't exist andresolveStreamFlag(undefined, "application/json, text/event-stream", "openai")returnedtrue; passes after):acceptHeaderForcesStream: 5 cases (Vercel signature → no force; pure SSE → force; pure JSON → no force; explicit body wins; no Accept).resolveStreamFlag: 5 cases (openai mixed → JSON; openai pure-SSE → stream; openai*/*/none → stream; explicit stream:true wins; forceStream provider wins).Results:
t26-ai-sdk-accept-header-compat,resolve-stream-flag,chatcore-json-body-to-sse); stream-executor sweep 81/81 (cli-stream,blackbox-web,perplexity-web,mimocode-executor) — no regression.typecheck:coreclean,eslint0 errors,check:file-sizeclean (chat.ts net -3; new test in a fresh file),check:test-discoveryOK.Note
A per-key workaround already exists (
streamDefaultMode: "json"); this makes the default spec-aligned (the bodystreamfield is authoritative;Acceptis a transport hint) so SDK clients work out of the box. The change is scoped to OpenAI-format requests — theclaude/openai-responsesbranches ofresolveStreamFlagare untouched.