From ddbd054e49a64cec9463dbf94ef91354f256fbcd Mon Sep 17 00:00:00 2001 From: Ridho Pratama Date: Fri, 24 Jul 2026 19:36:00 +0700 Subject: [PATCH] fix(sse): preserve Responses combo payloads (#8310) --- .../fixes/8310-combo-responses-payload.md | 1 + open-sse/services/comboAgentMiddleware.ts | 19 ++++++++++---- tests/unit/services-branch-hardening.test.ts | 26 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/8310-combo-responses-payload.md diff --git a/changelog.d/fixes/8310-combo-responses-payload.md b/changelog.d/fixes/8310-combo-responses-payload.md new file mode 100644 index 0000000000..631b1043d8 --- /dev/null +++ b/changelog.d/fixes/8310-combo-responses-payload.md @@ -0,0 +1 @@ +- **fix(sse):** Combo middleware preserves OpenAI Responses request bodies and maps combo system overrides to `instructions`, preventing Responses-native fallbacks from receiving an unsupported `messages` parameter. ([#8310](https://github.com/diegosouzapw/OmniRoute/pull/8310)) — thanks @ridho9 diff --git a/open-sse/services/comboAgentMiddleware.ts b/open-sse/services/comboAgentMiddleware.ts index f93de4d285..f9b2419020 100644 --- a/open-sse/services/comboAgentMiddleware.ts +++ b/open-sse/services/comboAgentMiddleware.ts @@ -180,16 +180,24 @@ export function applyComboAgentMiddleware( ): { body: Record; pinnedModel: string | null } { if (!comboConfig) return { body, pinnedModel: null }; - let messages: Message[] = Array.isArray(body.messages) ? [...body.messages] : []; + const hasMessages = Array.isArray(body.messages); + const isResponsesRequest = + Object.prototype.hasOwnProperty.call(body, "input") || + Object.prototype.hasOwnProperty.call(body, "instructions"); + const systemMessage = + typeof comboConfig.system_message === "string" && comboConfig.system_message.trim() + ? comboConfig.system_message + : null; + let messages: Message[] = hasMessages ? [...(body.messages as Message[])] : []; let pinnedModel: string | null = null; // Context cache pinning is handled server-side in combo.ts via // session_model_history. No client-side tag extraction needed. pinnedModel = null; - // 2. System message override - if (comboConfig.system_message && comboConfig.system_message.trim()) { - messages = applySystemMessageOverride(messages, comboConfig.system_message); + // 2. System message override. Responses API uses top-level instructions instead of messages. + if (systemMessage && !isResponsesRequest) { + messages = applySystemMessageOverride(messages, systemMessage); } // 3. Tool filter @@ -206,7 +214,8 @@ export function applyComboAgentMiddleware( return { body: { ...body, - messages, + ...(isResponsesRequest && systemMessage ? { instructions: systemMessage } : {}), + ...(hasMessages ? { messages } : {}), ...(filteredTools !== body.tools && { tools: filteredTools }), }, pinnedModel, diff --git a/tests/unit/services-branch-hardening.test.ts b/tests/unit/services-branch-hardening.test.ts index 6e6c03c2d5..fa6458eab5 100644 --- a/tests/unit/services-branch-hardening.test.ts +++ b/tests/unit/services-branch-hardening.test.ts @@ -184,6 +184,32 @@ test("combo agent middleware covers system override, tool filtering, tag strippi assert.equal(passthrough.body, body); }); +test("combo agent middleware preserves Responses API input and instructions", () => { + const body = { + model: "combo/default", + input: "Reply with exactly: pong", + }; + + const unchanged = comboAgentMiddleware.applyComboAgentMiddleware( + body, + { context_cache_protection: true }, + "openai/gpt-4o" + ); + assert.deepEqual(unchanged.body, body); + assert.equal(Object.hasOwn(unchanged.body, "messages"), false); + + const overridden = comboAgentMiddleware.applyComboAgentMiddleware( + { ...body, instructions: "client instruction" }, + { system_message: "combo instruction" }, + "openai/gpt-4o" + ); + assert.deepEqual(overridden.body, { + ...body, + instructions: "combo instruction", + }); + assert.equal(Object.hasOwn(overridden.body, "messages"), false); +}); + test("rate limit semaphore covers immediate acquire, timeout, cooldown drain and reset", async () => { const release = await rateLimitSemaphore.acquire("model-a", { maxConcurrency: 1 }); assert.deepEqual(rateLimitSemaphore.getStats()["model-a"], {