From 970e14725be19213c5ac921add17e746cdd995ae Mon Sep 17 00:00:00 2001 From: OmniRoute Ops Date: Mon, 11 May 2026 13:23:57 +0000 Subject: [PATCH 1/2] feat(responses): degrade background mode to synchronous execution OpenAI Responses API supports background: true for async/deferred runs (returns 202 with response_id + GET /responses/ polling). Implementing the full background contract requires queue/poll infrastructure that OmniRoute does not have as a forward proxy. Previously the translator rejected such requests with HTTP 400 "Unsupported Responses API feature: background mode is not supported by omniroute". This breaks clients that opportunistically set background=true for long-running tasks (Capy Captain Pro, Codex agents) even when the underlying execution would complete in a normal HTTP timeout window. Degrade silently: strip the background flag and run synchronously. The client receives the full response in one round-trip with status=completed. Clients that strictly require the async contract still observe a completed response on the first poll and can adapt. The existing test that asserted background=true throws is split into two: - the unsupported-tool-type branch keeps the throw assertion - the background branch now asserts the flag is stripped and translation completes with the expected messages Co-Authored-By: Claude Opus 4.7 (1M context) --- .../translator/request/openai-responses.ts | 20 ++++++++---- .../translator-openai-responses-req.test.ts | 32 +++++++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 45beee74b0..97c0b77a27 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -77,13 +77,21 @@ 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 silently: strip the flag and run + // synchronously. The client receives the full response in one round-trip + // with status=completed. 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 !== 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..68863af30b 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,26 @@ 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 result = openaiResponsesToOpenAIRequest( + "gpt-5.5", + { + input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], + background: true, + }, + true, + null ); + 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"); }); test("Chat -> Responses converts messages, tool calls, tool outputs, tools and pass-through params", () => { From 8b1da7b92cd3b073f95fb9b069838d1cb3a1e434 Mon Sep 17 00:00:00 2001 From: OmniRoute Ops Date: Mon, 11 May 2026 16:26:07 +0000 Subject: [PATCH 2/2] address review: emit BACKGROUND_DEGRADE log + test for background:false case Per gemini-code-assist review on #2164: 1. Re-introduce the BACKGROUND_DEGRADE warning log (mentioned in PR description but missing from code). Operators can grep for this to identify clients still requesting background mode that should be reconfigured. 2. Make the log conditional on `background === true` (not on the strip itself). background:false / unset now passes through silently with no log spam. 3. Add unit test for both background unset AND background:false cases asserting no BACKGROUND_DEGRADE log is emitted, and update the existing background:true test to assert the log IS emitted. --- .../translator/request/openai-responses.ts | 19 +++-- .../translator-openai-responses-req.test.ts | 77 +++++++++++++++---- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 97c0b77a27..6d72f1f5f0 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -83,12 +83,19 @@ export function openaiResponsesToOpenAIRequest( // 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 silently: strip the flag and run - // synchronously. The client receives the full response in one round-trip - // with status=completed. 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. + // 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"}` + ); + } if (result.background !== undefined) { delete result.background; } diff --git a/tests/unit/translator-openai-responses-req.test.ts b/tests/unit/translator-openai-responses-req.test.ts index 68863af30b..55b8791713 100644 --- a/tests/unit/translator-openai-responses-req.test.ts +++ b/tests/unit/translator-openai-responses-req.test.ts @@ -131,19 +131,70 @@ test("Responses -> Chat strips background flag and degrades to synchronous execu // 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 result = openaiResponsesToOpenAIRequest( - "gpt-5.5", - { - input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }], - background: true, - }, - true, - null - ); - 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"); + 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", () => {