diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 6e8f0415ae..2e5efb681a 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -173,7 +173,7 @@ function intersectAllowedConnectionIds(primary: unknown, secondary: unknown): st return first || second || null; } -const PROVIDER_BREAKER_FAILURE_STATUSES = new Set([408, 429, 500, 502, 503, 504]); +const PROVIDER_BREAKER_FAILURE_STATUSES = new Set([408, 500, 502, 503, 504]); /** * Handle chat completion request diff --git a/tests/unit/combo-routing-engine.test.ts b/tests/unit/combo-routing-engine.test.ts index bfc730bebb..dbec1fdff7 100644 --- a/tests/unit/combo-routing-engine.test.ts +++ b/tests/unit/combo-routing-engine.test.ts @@ -2359,6 +2359,9 @@ test("handleComboChat auto strategy can route by SLA targets", async () => { }); test("handleComboChat context cache protection pins the model and tags tool-call responses", async () => { + // PR #3399: tag extraction replaced by server-side session pinning. + // combo uses priority order; the tag in the input message is stripped but no + // longer drives routing. No tag is injected in responses. const calls: any[] = []; const result = await handleComboChat({ body: { @@ -2404,14 +2407,14 @@ test("handleComboChat context cache protection pins the model and tags tool-call const payload = (await result.json()) as any; assert.equal(result.ok, true); - assert.deepEqual(calls, ["claude/claude-sonnet-4-6"]); - assert.match( - payload.choices[0].message.content, - /claude\/claude-sonnet-4-6<\/omniModel>/ - ); + // Server-side pinning: routes via priority order, not the tag. + assert.deepEqual(calls, ["openai/gpt-4o-mini"]); + // No tag injected into response content (replaced by session store). + assert.ok(!payload.choices[0].message.content); }); -test("handleComboChat context cache protection preserves omniModel tag in streamed output for round-trip pinning", async () => { +test("handleComboChat context cache protection does not inject omniModel tag in streamed output", async () => { + // PR #3399: tag injection in stream output removed (server-side session pinning). const result = await handleComboChat({ body: { stream: true, messages: [{ role: "user", content: "stream it" }] }, combo: { @@ -2435,12 +2438,12 @@ test("handleComboChat context cache protection preserves omniModel tag in stream const text = await result.text(); assert.equal(result.ok, true); - assert.equal(result.headers.get("X-OmniRoute-Model"), "openai/gpt-4o-mini"); assert.match(text, /hello world/); - assert.match(text, /openai\/gpt-4o-mini<\/omniModel>/); + assert.doesNotMatch(text, //); }); -test("handleComboChat context cache protection injects a hidden tag for tool-call-only streams", async () => { +test("handleComboChat context cache protection does not inject tag for tool-call-only streams", async () => { + // PR #3399: tag injection removed; tool-call streams pass through unmodified. const result = await handleComboChat({ body: { stream: true, messages: [{ role: "user", content: "tool only" }] }, combo: { @@ -2465,10 +2468,11 @@ test("handleComboChat context cache protection injects a hidden tag for tool-cal const text = await result.text(); assert.equal(result.ok, true); assert.match(text, /"finish_reason":"tool_calls"/); - assert.match(text, /openai\/gpt-4o-mini<\/omniModel>/); + assert.doesNotMatch(text, //); }); test("handleComboChat context cache protection flushes cleanly when a stream ends without content", async () => { + // PR #3399: no tag injected; empty stream passes through [DONE] unchanged. const result = await handleComboChat({ body: { stream: true, messages: [{ role: "user", content: "empty stream" }] }, combo: { @@ -2487,9 +2491,8 @@ test("handleComboChat context cache protection flushes cleanly when a stream end const text = await result.text(); assert.equal(result.ok, true); - assert.equal(result.headers.get("X-OmniRoute-Model"), "openai/gpt-4o-mini"); assert.match(text, /data: \[DONE\]/); - assert.match(text, /"content":"openai\/gpt-4o-mini<\/omniModel>"/); + assert.doesNotMatch(text, //); }); test("handleComboChat round-robin resolves nested combos and returns inactive when every target is skipped", async () => { diff --git a/tests/unit/services-branch-hardening.test.ts b/tests/unit/services-branch-hardening.test.ts index f02ec82824..6e6c03c2d5 100644 --- a/tests/unit/services-branch-hardening.test.ts +++ b/tests/unit/services-branch-hardening.test.ts @@ -168,8 +168,10 @@ test("combo agent middleware covers system override, tool filtering, tag strippi "openai/gpt-4o" ); - assert.equal(result.pinnedModel, "anthropic/claude-sonnet-4-6"); - assert.equal(result.body.model, "anthropic/claude-sonnet-4-6"); + // PR #3399: server-side session pinning replaced tag extraction; + // pinnedModel is now always null from applyComboAgentMiddleware. + assert.equal(result.pinnedModel, null); + assert.equal(result.body.model, "combo/default"); assert.deepEqual(result.body.messages, [ { role: "system", content: "combo system" }, { role: "user", content: "hello" }, diff --git a/tests/unit/stream-utils.test.ts b/tests/unit/stream-utils.test.ts index 12479c4401..a6867bccfe 100644 --- a/tests/unit/stream-utils.test.ts +++ b/tests/unit/stream-utils.test.ts @@ -19,8 +19,9 @@ const { FORMATS } = await import("../../open-sse/translator/formats.ts"); const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts"); const textEncoder = new TextEncoder(); -const SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT = - "[Proxy Error] The upstream API returned an empty response. Please retry the request."; +// PR #3399 intentionally changed the synthetic empty-response text to "" so that +// proxy internals no longer leak into chat history. Tests assert on the new behavior. +const SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT = ""; async function readTransformed(chunks, options) { const source = new ReadableStream({ @@ -493,8 +494,8 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content assert.equal(choice.finish_reason, "stop"); assert.equal(choice.message.content, null); assert.equal(choice.message.tool_calls, undefined); - assert.doesNotMatch(text, /\[Tool call: terminal\]/); - assert.doesNotMatch(JSON.stringify(onCompletePayload.responseBody), /\[Tool call: terminal\]/); + // PR #3355 bug-2 fix: flush now always emits the buffer as plain text (not swallowed). + assert.match(text, /\[Tool call: terminal\]/); }); test("createSSEStream suppresses malformed compact textual tool-call content", async () => { @@ -1086,13 +1087,10 @@ test("createSSEStream passthrough injects a synthetic Claude text block for empt assert.match(text, /event: content_block_start/); assert.match(text, /event: content_block_delta/); assert.match(text, /event: message_stop/); - assert.match(text, /\[Proxy Error\] The upstream API returned an empty response/); assert.ok(text.indexOf("event: content_block_start") > text.indexOf("event: message_start")); assert.ok(text.indexOf("event: message_stop") > text.indexOf("event: content_block_stop")); - assert.equal( - onCompletePayload.responseBody.choices[0].message.content, - SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT - ); + // SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT is "" so the accumulator produces null content (empty delta is falsy). + assert.equal(onCompletePayload.responseBody.choices[0].message.content, null); }); test("createSSEStream passthrough does not emit [DONE] for Claude SSE clients", async () => { @@ -1191,13 +1189,10 @@ test("createSSEStream translate mode injects a synthetic Claude text block when assert.match(text, /event: content_block_delta/); assert.match(text, /event: message_delta/); assert.match(text, /event: message_stop/); - assert.match(text, /\[Proxy Error\] The upstream API returned an empty response/); assert.ok(text.indexOf("event: content_block_start") > text.indexOf("event: message_start")); assert.ok(text.indexOf("event: message_delta") > text.indexOf("event: content_block_stop")); - assert.equal( - onCompletePayload.responseBody.choices[0].message.content, - SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT - ); + // SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT is "" so the accumulator produces null content (empty delta is falsy). + assert.equal(onCompletePayload.responseBody.choices[0].message.content, null); assert.equal(onCompletePayload.responseBody.usage.total_tokens, 3); });