From 0fe1bb2390793c5889aa58ccdf5fbcda5a3c1e3c Mon Sep 17 00:00:00 2001 From: Alex Chan Date: Sun, 23 Aug 2026 18:01:07 +0800 Subject: [PATCH] fix(routing): preserve combo precedence and skip hidden models in alias resolver (#11107) Validated on the combined batch board over tip 8a42aeeb: static gates clean (changelog, file-size 159 frozen, complexity 2621<=2774, cognitive 1181<=1223, dead-code 408<=416), typecheck:core clean, 107 focused tests green. Combo precedence preserved when a requested name matches an existing combo or combo/* prefix, and hidden/disabled models are skipped during alias resolution (wildcard + mapped). model-alias-seed-fallback green. Related to #10124. Thank you @SCys! --- src/lib/modelAliasResolver.ts | 47 ++++++++++++++- tests/unit/model-alias-seed-fallback.test.ts | 60 ++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/lib/modelAliasResolver.ts b/src/lib/modelAliasResolver.ts index 7e0a079521..1be331fac8 100644 --- a/src/lib/modelAliasResolver.ts +++ b/src/lib/modelAliasResolver.ts @@ -10,11 +10,21 @@ */ import { getModelAliases } from "@/lib/db/models/aliases"; import { DEFAULT_MODEL_ALIAS_SEED } from "@/lib/modelAliasSeed"; +import { getComboByName } from "@/lib/db/combos"; +import { getModelIsHidden } from "@/lib/db/models"; +import { resolveProviderId } from "@/shared/constants/providers"; let cachedAliases: Record | null = null; let lastFetch = 0; const CACHE_TTL_MS = 60_000; // 1 minute +function isTargetModelHidden(provider: string, modelId: string): boolean { + if (getModelIsHidden(provider, modelId)) return true; + const canonicalProvider = resolveProviderId(provider); + if (canonicalProvider !== provider && getModelIsHidden(canonicalProvider, modelId)) return true; + return false; +} + async function loadAliases(): Promise> { const now = Date.now(); if (cachedAliases && now - lastFetch < CACHE_TTL_MS) { @@ -40,21 +50,52 @@ export async function resolveModelAliasWithSeedFallback( ): Promise { if (!model) return model; + // Combo routing takes precedence over individual model aliases (#10124 / #9020) + if (model.startsWith("combo/")) return model; + const existingCombo = await getComboByName(model).catch(() => null); + if (existingCombo) return model; + const aliases = await loadAliases(); const target = aliases[model] ?? (DEFAULT_MODEL_ALIAS_SEED as Record)[model]; if (target === undefined) return model; - if (typeof target === "string") return target; + if (typeof target === "string") { + const slashIndex = target.indexOf("/"); + if (slashIndex > 0) { + const targetProvider = target.slice(0, slashIndex); + const targetModel = target.slice(slashIndex + 1); + if (isTargetModelHidden(targetProvider, targetModel)) { + return model; + } + } + return target; + } if (Array.isArray(target) && target.length > 0) { const first = target[0]; - return typeof first === "string" ? first : model; + if (typeof first === "string") { + const slashIndex = first.indexOf("/"); + if (slashIndex > 0) { + const targetProvider = first.slice(0, slashIndex); + const targetModel = first.slice(slashIndex + 1); + if (isTargetModelHidden(targetProvider, targetModel)) { + return model; + } + } + return first; + } + return model; } if (typeof target === "object" && target !== null) { const t = target as { provider?: string; model?: string }; - if (t.provider && t.model) return `${t.provider}/${t.model}`; + if (t.provider && t.model) { + if (isTargetModelHidden(t.provider, t.model)) { + return model; + } + return `${t.provider}/${t.model}`; + } } return model; diff --git a/tests/unit/model-alias-seed-fallback.test.ts b/tests/unit/model-alias-seed-fallback.test.ts index 8a3ff1ff11..25f9513fb9 100644 --- a/tests/unit/model-alias-seed-fallback.test.ts +++ b/tests/unit/model-alias-seed-fallback.test.ts @@ -64,3 +64,63 @@ test("resolveModelAliasWithSeedFallback: export name is distinct from the sync r assert.equal(typeof mod.resolveModelAliasWithSeedFallback, "function"); assert.equal(mod.resolveModelAlias, undefined, "must not export the colliding sync name"); }); + +test("resolveModelAliasWithSeedFallback: preserves model name when a combo exists with the same name", async () => { + await withEmptyAliasDb(async () => { + const { createCombo } = await import("../../src/lib/db/combos"); + const { setModelAlias } = await import("../../src/lib/db/models/aliases"); + const { invalidateAliasCache } = await import("../../src/lib/modelAliasResolver"); + + // Simulate managed alias synced from provider + await setModelAlias("gemini-3.7-flash", "oc/gemini-3.7-flash"); + invalidateAliasCache(); + + // Create a combo named "gemini-3.7-flash" + await createCombo({ + id: "test-combo-gemini-3-7-flash", + name: "gemini-3.7-flash", + models: [ + { + id: "target-1", + model: "agy/gemini-3.7-flash-high", + providerId: "agy", + weight: 100, + }, + ], + strategy: "round-robin", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + // Should NOT be rewritten to "oc/gemini-3.7-flash" because the combo takes precedence + const resolved = await resolveModelAliasWithSeedFallback("gemini-3.7-flash"); + assert.equal(resolved, "gemini-3.7-flash"); + + // Explicit combo/ prefix should also remain unchanged + const explicitCombo = await resolveModelAliasWithSeedFallback("combo/gemini-3.7-flash"); + assert.equal(explicitCombo, "combo/gemini-3.7-flash"); + }); +}); + +test("resolveModelAliasWithSeedFallback: skips alias when the target model is hidden", async () => { + await withEmptyAliasDb(async () => { + const { setModelAlias } = await import("../../src/lib/db/models/aliases"); + const { mergeModelCompatOverride } = await import("../../src/lib/db/models"); + const { invalidateAliasCache } = await import("../../src/lib/modelAliasResolver"); + + // Set alias pointing to opencode/glm-5 + await setModelAlias("glm-5", "opencode/glm-5"); + invalidateAliasCache(); + + // Before hiding, alias resolves to target + const beforeHidden = await resolveModelAliasWithSeedFallback("glm-5"); + assert.equal(beforeHidden, "opencode/glm-5"); + + // Hide the model + mergeModelCompatOverride("opencode", "glm-5", { isHidden: true }); + + // After hiding, alias should be skipped and return original model name + const afterHidden = await resolveModelAliasWithSeedFallback("glm-5"); + assert.equal(afterHidden, "glm-5"); + }); +});