mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
committed by
GitHub
parent
858aa4521c
commit
72d6579bb1
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user