Skip to content

fix(sse): non-stream OpenAI-compatible requests no longer coerced to SSE when Accept lists application/json (#5305) - #5309

Merged
diegosouzapw merged 2 commits into
release/v3.8.40from
fix/5305-sse-nonstream-accept
Jun 29, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.40from
fix/5305-sse-nonstream-accept

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Closes #5305

Problem

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 these into SSE, so the caller received:

APICallError [AI_APICallError]: Invalid JSON response
JSONParseError: Unexpected token 'd', "data: {"id"... is not valid JSON

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): forced stream=true whenever Accept contained text/event-stream and the body omitted stream.
  • open-sse/utils/aiSdkCompat.ts::resolveStreamFlag — clientWantsJsonResponse returns false for application/json, text/event-stream, so the fallback streamed.

Fix

An Accept header that explicitly lists application/json is treated as a JSON opt-in, even when it also lists text/event-stream. Concretely:

  • Only a pure Accept: text/event-stream (no application/json) still opts an omitted-stream request into SSE.
  • An explicit body stream value (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 both chat.ts and (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 and resolveStreamFlag(undefined, "application/json, text/event-stream", "openai") returned true; 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:

Note

A per-key workaround already exists (streamDefaultMode: "json"); this makes the default spec-aligned (the body stream field is authoritative; Accept is a transport hint) so SDK clients work out of the box. The change is scoped to OpenAI-format requests — the claude / openai-responses branches of resolveStreamFlag are untouched.

…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>

@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 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.

Comment on lines +1 to +2
import { describe, it } from "node:test";
import assert from "node:assert/strict";

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.

medium

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.

Suggested change
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.
@diegosouzapw
diegosouzapw merged commit d4c54f6 into release/v3.8.40 Jun 29, 2026
7 checks passed
diegosouzapw added a commit that referenced this pull request Jun 29, 2026
…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.
@diegosouzapw
diegosouzapw deleted the fix/5305-sse-nonstream-accept branch June 29, 2026 14:08
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…SSE when Accept lists application/json (diegosouzapw#5305) (diegosouzapw#5309)

Integrated into release/v3.8.40
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…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.
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