From c5707e65dedc223d083e11d8d8e913a4d7d59a70 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 11 Sep 2026 22:05:56 -0300 Subject: [PATCH] fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129) (#13278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit. Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them. - ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243) - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline - 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs - `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243 ⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch. --- ...ontext-handoff-native-passthrough-shape.md | 1 + open-sse/handlers/chatCore.ts | 1 + .../handlers/chatCore/passthroughHelpers.ts | 16 ++++ ...ext-handoff-native-passthrough-bug.test.ts | 86 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 changelog.d/fixes/12129-context-handoff-native-passthrough-shape.md create mode 100644 tests/unit/context-handoff-native-passthrough-bug.test.ts diff --git a/changelog.d/fixes/12129-context-handoff-native-passthrough-shape.md b/changelog.d/fixes/12129-context-handoff-native-passthrough-shape.md new file mode 100644 index 0000000000..f2668facbd --- /dev/null +++ b/changelog.d/fixes/12129-context-handoff-native-passthrough-shape.md @@ -0,0 +1 @@ +- fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a091ad4b00..0b1ac2e3f9 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -732,6 +732,7 @@ export async function handleChatCore({ sourceFormat, endpointPath, providerSpecificData: credentials?.providerSpecificData, + body, }); const responsesInputItems = Array.isArray(body?.input) ? body.input : []; const customToolNames = collectCustomToolNamesForSourceFormat( diff --git a/open-sse/handlers/chatCore/passthroughHelpers.ts b/open-sse/handlers/chatCore/passthroughHelpers.ts index 352415ed89..213f2d7a9f 100644 --- a/open-sse/handlers/chatCore/passthroughHelpers.ts +++ b/open-sse/handlers/chatCore/passthroughHelpers.ts @@ -53,19 +53,35 @@ export function stampNativeResponsesPassthroughBody( return { ...body, _nativeOpenAICompatibleResponsesPassthrough: true }; } +// A body only qualifies for the native-Responses passthrough fast path when it is +// actually shaped like a Responses API request (`input`, no `messages`). Endpoint +// path alone is not sufficient: an internally-synthesized Chat Completions-shaped +// body (e.g. the context-handoff summary request) can be dispatched through a +// closure that still carries the original client request's `/responses` endpoint, +// which otherwise makes `sourceFormat` resolve to "openai-responses" even though +// the body itself was never translated. See issue #12129. +function isResponsesShapedBody(body: unknown): boolean { + if (!body || typeof body !== "object") return false; + const candidate = body as Record; + return candidate.input !== undefined && candidate.messages === undefined; +} + export function shouldUseNativeOpenAICompatibleResponsesPassthrough({ provider, sourceFormat, endpointPath, providerSpecificData, + body, }: { provider?: string | null; sourceFormat?: string | null; endpointPath?: string | null; providerSpecificData?: unknown; + body?: unknown; }): boolean { if (!provider?.startsWith("openai-compatible-")) return false; if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false; + if (body !== undefined && !isResponsesShapedBody(body)) return false; if (providerSpecificData && typeof providerSpecificData === "object") { const psd = providerSpecificData as Record; if (psd.apiType === "responses" || psd._omnirouteForceResponsesUpstream === true) { diff --git a/tests/unit/context-handoff-native-passthrough-bug.test.ts b/tests/unit/context-handoff-native-passthrough-bug.test.ts new file mode 100644 index 0000000000..d142b60649 --- /dev/null +++ b/tests/unit/context-handoff-native-passthrough-bug.test.ts @@ -0,0 +1,86 @@ +// Regression test for issue #12129: an internal context-handoff summary request (built in +// Chat Completions shape -- `messages`, no `input`) is dispatched through the SAME +// handleSingleModel closure that carries the ORIGINAL client request's endpoint. +// When that original endpoint matched `/responses` and the resolved handoff-model +// provider is an openai-compatible-* connection configured with apiType "responses", +// the pipeline used to decide the body was already native-Responses-shaped and skip +// chat->responses translation entirely (`_nativeOpenAICompatibleResponsesPassthrough`), +// so the upstream received `messages` on `/v1/responses` and rejected it with zero input. +// +// Fix: `shouldUseNativeOpenAICompatibleResponsesPassthrough` now requires the body to +// actually look Responses-shaped (`input` present, `messages` absent) before allowing +// the passthrough fast path, so an internally-synthesized chat-shaped body is routed +// through the normal chat->responses translation layer instead. +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { resolveChatCoreRequestFormat } from "../../open-sse/handlers/chatCore/requestFormat.ts"; +import { shouldUseNativeOpenAICompatibleResponsesPassthrough } from "../../open-sse/handlers/chatCore/passthroughHelpers.ts"; + +test("internal chat-shaped handoff body is no longer treated as native Responses passthrough", () => { + const clientRawRequest = { + endpoint: "/v1/responses", + headers: new Headers(), + }; + + const summaryBody = { + model: "some-handoff-model", + messages: [{ role: "user", content: "Summarize this conversation." }], + stream: false, + max_tokens: 800, + temperature: 0.1, + _omnirouteSkipContextRelay: true, + _omnirouteInternalRequest: "context-handoff", + }; + + const { sourceFormat, endpointPath } = resolveChatCoreRequestFormat({ + clientRawRequest, + body: summaryBody, + provider: "openai-compatible-responses-cliproxy", + userAgent: null, + }); + + assert.equal(sourceFormat, "openai-responses"); + assert.equal(endpointPath, "/v1/responses"); + + const providerSpecificData = { apiType: "responses" }; + + const nativePassthrough = shouldUseNativeOpenAICompatibleResponsesPassthrough({ + provider: "openai-compatible-responses-cliproxy", + sourceFormat, + endpointPath, + providerSpecificData, + body: summaryBody, + }); + + assert.equal( + nativePassthrough, + false, + "fixed: chat-shaped internal body must not take the native-Responses passthrough shortcut" + ); + + assert.equal((summaryBody as Record).input, undefined); + assert.ok(Array.isArray(summaryBody.messages) && summaryBody.messages.length > 0); +}); + +test("genuine Responses-shaped body still takes the native passthrough fast path", () => { + const genuineResponsesBody = { + model: "gpt-5.6-sol", + input: [{ role: "user", content: [{ type: "input_text", text: "Hello" }] }], + stream: false, + }; + + const nativePassthrough = shouldUseNativeOpenAICompatibleResponsesPassthrough({ + provider: "openai-compatible-responses-cliproxy", + sourceFormat: "openai-responses", + endpointPath: "/v1/responses", + providerSpecificData: { apiType: "responses" }, + body: genuineResponsesBody, + }); + + assert.equal( + nativePassthrough, + true, + "a genuine Responses-shaped client body must keep the zero-translation fast path" + ); +});