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"], {