From 4a5037c654b706019e90da8cb97695b34f4faf62 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 10 Aug 2026 05:42:58 +0900 Subject: [PATCH 1/2] fix(router): pin combo child routes --- src/router.ts | 5 +++++ src/server/responses/core.ts | 16 ++++++++++---- tests/server-combo-failover-e2e.test.ts | 28 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/router.ts b/src/router.ts index 719d74f789..5c84c095c8 100644 --- a/src/router.ts +++ b/src/router.ts @@ -684,6 +684,11 @@ export function routeModel( return route; } +/** Resolve a combo-selected provider/model target without consulting public combo aliases again. */ +export function routeConcreteModel(config: OcxConfig, modelId: string): RouteResult { + return routeModelInternal(config, modelId, true, undefined); +} + function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined { for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) { if (prefixes.some(prefix => modelId.startsWith(prefix))) { diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 01a58b7949..a9d21deec1 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -16,7 +16,13 @@ import { previousResponseReplayFailure, rememberResponseState, } from "../../responses/state"; -import { comboRouteDecisionTrace, NoEligiblePolicyCandidateError, routeModel, type RouteResult } from "../../router"; +import { + comboRouteDecisionTrace, + NoEligiblePolicyCandidateError, + routeConcreteModel, + routeModel, + type RouteResult, +} from "../../router"; import { evidenceFromBody } from "../../routing/request-evidence"; import { advanceComboAfterFailure, @@ -1069,7 +1075,7 @@ export async function handleComboResponses( const provider = config.providers[target.provider]; if (!provider || provider.disabled === true) return false; try { - const route = routeModel(config, `${target.provider}/${target.model}`); + const route = routeConcreteModel(config, `${target.provider}/${target.model}`); return isCanonicalOpenAiForwardProvider(route.provider); } catch { return false; @@ -1103,7 +1109,7 @@ export async function handleComboResponses( ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}), ...(logCtx.surface ? { surface: logCtx.surface } : {}), }; - const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`); + const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`); const childBody = concreteComboRequestBody( rawBody, pick.target, @@ -1469,7 +1475,9 @@ async function handleResponsesInner( let route: RouteResult; try { - route = routeModel(config, parsed.modelId, evidenceFromBody(parsed._rawBody)); + route = options.comboAttempt + ? routeConcreteModel(config, parsed.modelId) + : routeModel(config, parsed.modelId, evidenceFromBody(parsed._rawBody)); logCtx.routeDecision = route.routeDecision; } catch (err) { if (err instanceof NoAvailableComboTargetsError) { diff --git a/tests/server-combo-failover-e2e.test.ts b/tests/server-combo-failover-e2e.test.ts index 490743f33c..f1064cf67a 100644 --- a/tests/server-combo-failover-e2e.test.ts +++ b/tests/server-combo-failover-e2e.test.ts @@ -369,6 +369,34 @@ async function within(promise: Promise, ms = 2_000): Promise { } describe("server combo failover 030 activation matrix", () => { + test("dispatches a selected concrete target despite a shadowing combo alias", async () => { + const hits: string[] = []; + const a = serve(async request => { + const body = await request.json() as { model?: string; messages?: Array<{ content?: string }> }; + hits.push(`a:${body.model}:${body.messages?.[0]?.content}`); + return chatSuccess("intended", "m1"); + }); + const b = serve(async request => { + const body = await request.json() as { model?: string; messages?: Array<{ content?: string }> }; + hits.push(`b:${body.model}:${body.messages?.[0]?.content}`); + return chatSuccess("shadow", "m2"); + }); + const config = comboConfig({ + a: provider("openai-chat", baseUrl(a), "key-a"), + b: provider("openai-chat", baseUrl(b), "key-b"), + }, [{ provider: "a", model: "m1" }]); + config.combos!.shadow = { + alias: "a/m1", + targets: [{ provider: "b", model: "m2" }], + }; + + const response = await post(config, { input: "SECRET_PROMPT_X" }); + + expect(response.status).toBe(200); + expect(JSON.stringify(await response.json())).toContain("intended"); + expect(hits).toEqual(["a:m1:SECRET_PROMPT_X"]); + }); + test("ordinary openai-chat 503 hops to backup for non-stream and stream", async () => { const hits: string[] = []; const a = serve(async request => { From fdd53cf7d8e8933e735bdebbc5d688f795005cb3 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:08:39 +0900 Subject: [PATCH 2/2] test(combo): distinguish concrete provider pinning --- tests/server-combo-failover-e2e.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/server-combo-failover-e2e.test.ts b/tests/server-combo-failover-e2e.test.ts index f1064cf67a..311d672507 100644 --- a/tests/server-combo-failover-e2e.test.ts +++ b/tests/server-combo-failover-e2e.test.ts @@ -385,6 +385,7 @@ describe("server combo failover 030 activation matrix", () => { a: provider("openai-chat", baseUrl(a), "key-a"), b: provider("openai-chat", baseUrl(b), "key-b"), }, [{ provider: "a", model: "m1" }]); + config.defaultProvider = "b"; config.combos!.shadow = { alias: "a/m1", targets: [{ provider: "b", model: "m2" }],