fix(responses): apply OpenAI Responses API stream=false spec default (#3708)

resolveStreamFlag now applies the stream=false-when-omitted default for sourceFormat=openai-responses (same as the existing claude path), so spec-compliant /v1/responses upstreams that return JSON no longer fall through to the wildcard-Accept heuristic and trigger STREAM_EARLY_EOF / 502. Codex CLI (stream:true) and explicit text/event-stream clients unaffected.

Integrated into release/v3.8.23.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-12 22:13:13 -03:00
committed by GitHub
parent 858aa4521c
commit 72d6579bb1
3 changed files with 33 additions and 10 deletions

View File

@@ -84,6 +84,7 @@
- fix(antigravity): preserve gemini-3.1-pro High/Low budget tiers (upstream accepts the suffixed ids; stop collapsing to bare gemini-3.1-pro) (#3696)
- fix: streaming combos now fail over to the next target when an upstream returns an empty/content-filtered response instead of surfacing a blank reply (#3685)
- fix(qwen-web): migrate the Qwen Web provider to the v2 chat API. The legacy `/api/chat/completions` endpoint was retired upstream and returned `504` HTML from Alibaba's gateway for every request regardless of credentials, so no token refresh could help (#3288, discussion #2768). The executor now uses the two-step v2 flow (`/api/v2/chats/new``/api/v2/chat/completions?chat_id=`), replays the full browser cookie jar (cna + ssxmod_itna/itna2 + token) required by Alibaba's WAF instead of only a Bearer token, parses the phase-based SSE (think→reasoning, answer→content), and maps the WAF/expired HTML page to a clear actionable auth error instead of surfacing raw HTML. The credential requirement is now a full Cookie header, browser-login capture grabs the WAF cookies, and the model catalog is refreshed to the current upstream ids (`qwen3.7-max`, `qwen3.7-plus`, `qwen3.6-plus`; legacy ids kept as aliases). 17 unit tests.
- fix(responses): `/v1/responses` requests that omit `stream` no longer 502 (`STREAM_EARLY_EOF`) when the upstream returns a valid JSON response. `resolveStreamFlag` now applies the OpenAI Responses API spec default (stream=false when omitted) in addition to the existing Anthropic Messages API default — previously only `sourceFormat=claude` triggered this path, leaving `sourceFormat=openai-responses` to fall through to the wildcard-Accept heuristic (`Accept: */*` → streaming intent), which caused spec-compliant upstreams that return JSON to appear as a dead stream. Codex CLI (always sends `stream: true`) and explicit SSE clients (send `Accept: text/event-stream`) are unaffected. (#3708)
- fix(cache): semantic cache responses are now scoped to the requesting API key — two callers with different API keys that send the same prompt and model will no longer receive each other's cached responses (#3740). `generateSignature` includes the `api_key_id` dimension in the SHA-256 hash; unauthenticated requests (no API key) remain isolated from keyed requests. Existing cache entries (generated without the key dimension) are cleared by migration `098`.
- **Combo strategy fallback coverage** (`tests/unit/combo-strategy-fallbacks.test.ts`, 11 tests — thanks @diegosouzapw): fill-first / p2c / random / cost-optimized / strict-random fallback paths (previously happy-path only), price-tie stability, stale strict-random deck degradation, unknown-strategy normalization to priority, and circuit-breaker HALF_OPEN recovery inside the combo loop + `preScreenTargets` (lazy-recovery contract).
- **`#1731` fast-skip suite restored** (`tests/integration/combo-provider-exhaustion.test.ts` — thanks @diegosouzapw): 5 previously-skipped integration tests rewritten against the current routing policy — 8/8 green.

View File

@@ -42,11 +42,10 @@ export function clientWantsJsonResponse(acceptHeader: unknown): boolean {
*
* Optional `sourceFormat` argument lets callers apply spec-correct defaults
* when both `stream` and `Accept` are ambiguous. The Anthropic Messages API
* defaults to non-stream when the body omits `stream`, regardless of Accept
* header. Without this hint, OmniRoute previously routed Anthropic /v1/messages
* requests with a curl-default wildcard Accept header through the streaming
* branch even though upstream returned JSON, producing STREAM_EARLY_EOF /
* HTTP 502 errors.
* and the OpenAI Responses API both default to non-stream when the body omits
* `stream`. Without this hint, OmniRoute previously routed those requests with
* a curl-default wildcard Accept header through the streaming branch even
* though upstream returned JSON, producing STREAM_EARLY_EOF / HTTP 502.
*/
export function resolveStreamFlag(
bodyStream: unknown,
@@ -64,11 +63,11 @@ export function resolveStreamFlag(
const acceptsEventStream =
typeof acceptHeader === "string" && /text\/event-stream/i.test(acceptHeader);
// Anthropic Messages API spec: stream defaults to false when body omits it.
// Only honor an explicit text/event-stream Accept header as a streaming opt-in
// for /v1/messages — otherwise default to non-stream so upstream JSON responses
// are surfaced correctly instead of triggering stream_early_eof.
if (sourceFormat === "claude") {
// Anthropic Messages API and OpenAI Responses API both specify stream=false
// when the body omits `stream`. Honor an explicit text/event-stream Accept
// header as a streaming opt-in; otherwise default to non-stream so
// spec-compliant upstreams that return JSON don't trigger STREAM_EARLY_EOF.
if (sourceFormat === "claude" || sourceFormat === "openai-responses") {
if (acceptsEventStream) return true;
return false;
}

View File

@@ -120,3 +120,26 @@ test("T26: explicit stream aliases resolve true/false correctly", () => {
assert.equal(resolveExplicitStreamAlias({ disable_streaming: true }), false);
assert.equal(resolveExplicitStreamAlias({}), undefined);
});
test("T26: sourceFormat=openai-responses applies spec default (stream=false when omitted) (#3708)", () => {
// OpenAI Responses API spec: omitting `stream` means non-streaming, same as claude.
// Previously OmniRoute fell through to the wildcard-Accept heuristic, treating
// Accept: */* as streaming intent → STREAM_EARLY_EOF / 502 on spec-compliant upstreams.
// Wildcard and undefined Accept must default to non-stream
assert.equal(resolveStreamFlag(undefined, undefined, "openai-responses"), false);
assert.equal(resolveStreamFlag(undefined, "*/*", "openai-responses"), false);
assert.equal(resolveStreamFlag(undefined, "application/json", "openai-responses"), false);
// Explicit body stream:true still wins
assert.equal(resolveStreamFlag(true, undefined, "openai-responses"), true);
assert.equal(resolveStreamFlag(true, "*/*", "openai-responses"), true);
assert.equal(resolveStreamFlag(false, "text/event-stream", "openai-responses"), false);
// Accept: text/event-stream is honored as an explicit SSE opt-in
assert.equal(resolveStreamFlag(undefined, "text/event-stream", "openai-responses"), true);
assert.equal(
resolveStreamFlag(undefined, "application/json, text/event-stream", "openai-responses"),
true
);
});