diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 45beee74b0..6d72f1f5f0 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -77,13 +77,28 @@ export function openaiResponsesToOpenAIRequest( } } - if (root.background) { - throw unsupportedFeature( - "Unsupported Responses API feature: background mode is not supported by omniroute" + const result: JsonRecord = { ...root }; + + // background: true requests a deferred Responses API run (the upstream + // returns 202 with response_id and the client polls GET /responses/). + // OmniRoute is a forward proxy that streams responses synchronously — + // implementing the queue/poll contract would require persistence and a + // separate retrieval surface. Degrade: log a marker when true was + // actually requested (operators can observe clients that should be + // reconfigured) and strip the flag. Clients that set background=true + // opportunistically (Capy Captain Pro, Codex agents) work unchanged. + // Clients that strictly require the async contract still observe a + // completed response on the first poll and can adapt. + if (result.background === true) { + const providerStr = toString(credentialRecord.provider); + const modelStr = toString(model); + console.warn( + `BACKGROUND_DEGRADE provider=${providerStr || "unknown"} model=${modelStr || "unknown"}` ); } - - const result: JsonRecord = { ...root }; + if (result.background !== undefined) { + delete result.background; + } const messages: JsonRecord[] = []; result.messages = messages; diff --git a/tests/unit/translator-openai-responses-req.test.ts b/tests/unit/translator-openai-responses-req.test.ts index e66c4c7469..55b8791713 100644 --- a/tests/unit/translator-openai-responses-req.test.ts +++ b/tests/unit/translator-openai-responses-req.test.ts @@ -110,7 +110,7 @@ test("Responses -> Chat filters orphan tool outputs and supports role-based mess }); }); -test("Responses -> Chat rejects unsupported built-in tools and background mode", () => { +test("Responses -> Chat rejects unsupported built-in tools", () => { assert.throws( () => openaiResponsesToOpenAIRequest( @@ -124,20 +124,77 @@ test("Responses -> Chat rejects unsupported built-in tools and background mode", ), (error: any) => error.statusCode === 400 && error.errorType === "unsupported_feature" ); +}); - assert.throws( - () => - openaiResponsesToOpenAIRequest( - "gpt-4o", - { - input: [], - background: true, - }, - false, - null - ), - (error: any) => error.statusCode === 400 && error.errorType === "unsupported_feature" - ); +test("Responses -> Chat strips background flag and degrades to synchronous execution", () => { + // Previously this threw 400 unsupported_feature. OmniRoute is a forward proxy + // and cannot host the deferred run + poll contract, so background=true is + // silently dropped and the request runs synchronously. Clients that set the + // flag opportunistically (Capy Captain Pro, Codex agents) work unchanged. + const warnLog: string[] = []; + const originalWarn = console.warn; + console.warn = (msg: unknown) => warnLog.push(String(msg)); + try { + const result = openaiResponsesToOpenAIRequest( + "gpt-5.5", + { + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + background: true, + }, + true, + { provider: "codex" } + ); + const r = result as Record; + assert.equal(r.background, undefined, "background flag must be stripped from output"); + assert.ok(Array.isArray(r.messages), "translation must complete and produce messages"); + assert.equal((r.messages as unknown[]).length, 1, "user message must be preserved"); + assert.ok( + warnLog.some((m) => m.startsWith("BACKGROUND_DEGRADE provider=codex model=gpt-5.5")), + "BACKGROUND_DEGRADE warning log must be emitted when background=true" + ); + } finally { + console.warn = originalWarn; + } +}); + +test("Responses -> Chat passes through when background flag is unset or false (no degrade log)", () => { + // Verifies the inverse of the degradation case: when background is absent or + // explicitly false, no warning should be emitted and the request body should + // not carry a residual background field. Guards against accidental log spam + // and confirms the degradation logic is conditional on background === true. + const warnLog: string[] = []; + const originalWarn = console.warn; + console.warn = (msg: unknown) => warnLog.push(String(msg)); + try { + // Case 1: background unset + const r1 = openaiResponsesToOpenAIRequest( + "gpt-5.5", + { input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }] }, + true, + { provider: "codex" } + ) as Record; + assert.equal(r1.background, undefined, "background must be absent on output (unset case)"); + + // Case 2: background explicitly false + const r2 = openaiResponsesToOpenAIRequest( + "gpt-5.5", + { + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + background: false, + }, + true, + { provider: "codex" } + ) as Record; + assert.equal(r2.background, undefined, "background must be stripped on output (false case)"); + + assert.equal( + warnLog.filter((m) => m.startsWith("BACKGROUND_DEGRADE")).length, + 0, + "BACKGROUND_DEGRADE must NOT be emitted for unset or false values" + ); + } finally { + console.warn = originalWarn; + } }); test("Chat -> Responses converts messages, tool calls, tool outputs, tools and pass-through params", () => {