diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index f024da8dbb..fc78182c61 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -106,7 +106,11 @@ import { isFallbackDecision, EMERGENCY_FALLBACK_CONFIG, } from "../services/emergencyFallback.ts"; -import { resolveStreamFlag, stripMarkdownCodeFence } from "../utils/aiSdkCompat.ts"; +import { + resolveExplicitStreamAlias, + resolveStreamFlag, + stripMarkdownCodeFence, +} from "../utils/aiSdkCompat.ts"; import { generateRequestId } from "@/shared/utils/requestId"; import { normalizePayloadForLog } from "@/lib/logPayloads"; import { extractFacts } from "@/lib/memory/extraction"; @@ -782,6 +786,22 @@ export async function handleChatCore({ ? clientRawRequest.headers.get("accept") || clientRawRequest.headers.get("Accept") : (clientRawRequest?.headers || {})["accept"] || (clientRawRequest?.headers || {})["Accept"]; + const explicitStreamAlias = resolveExplicitStreamAlias(body); + + // Remove non-standard non-stream aliases before provider translation/execution. + // They are accepted for compatibility at the OmniRoute API boundary only. + if (body && typeof body === "object") { + const b = body as Record; + if (explicitStreamAlias !== undefined) { + b.stream = explicitStreamAlias; + } + + delete b.non_stream; + delete b.disable_stream; + delete b.disable_streaming; + delete b.streaming; + } + const stream = resolveStreamFlag(body?.stream, acceptHeader); // ── Phase 9.1: Semantic cache check (non-streaming, temp=0 only) ── diff --git a/open-sse/utils/aiSdkCompat.ts b/open-sse/utils/aiSdkCompat.ts index a439ae928e..91d284bfc1 100644 --- a/open-sse/utils/aiSdkCompat.ts +++ b/open-sse/utils/aiSdkCompat.ts @@ -26,6 +26,33 @@ export function resolveStreamFlag(bodyStream: unknown, acceptHeader: unknown): b return !clientWantsJsonResponse(acceptHeader); } +/** + * Resolves explicit stream aliases used by non-standard clients. + * Returns: + * - `true` -> explicit streaming intent + * - `false` -> explicit non-stream intent + * - `undefined` -> no explicit alias present + */ +export function resolveExplicitStreamAlias(body: unknown): boolean | undefined { + if (!body || typeof body !== "object") return undefined; + const b = body as Record; + + if (b.streaming === true) return true; + if (b.streaming === false) return false; + if (b.non_stream === true) return false; + if (b.disable_stream === true) return false; + if (b.disable_streaming === true) return false; + + return undefined; +} + +/** + * Backward-compatible helper used by tests/legacy call sites. + */ +export function hasExplicitNoStreamParam(body: unknown): boolean { + return resolveExplicitStreamAlias(body) === false; +} + /** * Removes surrounding markdown code fences when Claude wraps JSON payloads. * Example: ```json\n{"ok":true}\n``` -> {"ok":true} diff --git a/tests/unit/t26-ai-sdk-accept-header-compat.test.mjs b/tests/unit/t26-ai-sdk-accept-header-compat.test.mjs index c4f9b1ba0f..4b2b398316 100644 --- a/tests/unit/t26-ai-sdk-accept-header-compat.test.mjs +++ b/tests/unit/t26-ai-sdk-accept-header-compat.test.mjs @@ -1,8 +1,13 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { clientWantsJsonResponse, resolveStreamFlag, stripMarkdownCodeFence } = - await import("../../open-sse/utils/aiSdkCompat.ts"); +const { + clientWantsJsonResponse, + resolveStreamFlag, + resolveExplicitStreamAlias, + hasExplicitNoStreamParam, + stripMarkdownCodeFence, +} = await import("../../open-sse/utils/aiSdkCompat.ts"); test("T26: explicit stream:true takes priority over Accept application/json (#656)", () => { assert.equal(clientWantsJsonResponse("application/json"), true); @@ -42,3 +47,20 @@ test("T26: explicit stream:false always prevents streaming", () => { assert.equal(resolveStreamFlag(false, "text/event-stream"), false); assert.equal(resolveStreamFlag(false, undefined), false); }); + +test("T26: explicit non-stream aliases are detected", () => { + assert.equal(hasExplicitNoStreamParam({ non_stream: true }), true); + assert.equal(hasExplicitNoStreamParam({ disable_stream: true }), true); + assert.equal(hasExplicitNoStreamParam({ disable_streaming: true }), true); + assert.equal(hasExplicitNoStreamParam({ streaming: false }), true); + assert.equal(hasExplicitNoStreamParam({ streaming: true }), false); + assert.equal(hasExplicitNoStreamParam({ stream: false }), false); + assert.equal(hasExplicitNoStreamParam({}), false); +}); + +test("T26: explicit stream aliases resolve true/false correctly", () => { + assert.equal(resolveExplicitStreamAlias({ streaming: true }), true); + assert.equal(resolveExplicitStreamAlias({ streaming: false }), false); + assert.equal(resolveExplicitStreamAlias({ disable_streaming: true }), false); + assert.equal(resolveExplicitStreamAlias({}), undefined); +});