diff --git a/open-sse/utils/ccDiscoveryAliases.ts b/open-sse/utils/ccDiscoveryAliases.ts index fe491bfac0..0f36cc1e29 100644 --- a/open-sse/utils/ccDiscoveryAliases.ts +++ b/open-sse/utils/ccDiscoveryAliases.ts @@ -69,6 +69,13 @@ function isMirrorableId(id: string): boolean { return !EFFORT_SUFFIX_RE.test(id); } +/** Strip a `/` prefix to get the bare model name, matching the convention in + * claudeEffortVariants.ts / noThinkingAlias.ts. */ +function bareModelName(id: string): string { + const slash = id.lastIndexOf("/"); + return slash >= 0 ? id.slice(slash + 1) : id; +} + export function appendCcDiscoveryAliases( models: T[], isEnabled: (entry: T) => boolean @@ -90,7 +97,10 @@ export function appendCcDiscoveryAliases( aliases.push({ ...model, id: aliasId, - root: id, + // Combo names may legally contain "/" (comboNameSchema allows it), so a combo's + // root must stay the full name verbatim — only real provider-qualified ids get + // the "/" stripped down to the bare model name. + root: isCombo ? id : bareModelName(id), display_name: `${label} (OmniRoute)`, } as T); } diff --git a/open-sse/utils/noThinkingAlias.ts b/open-sse/utils/noThinkingAlias.ts index f83cefc69b..e783de2601 100644 --- a/open-sse/utils/noThinkingAlias.ts +++ b/open-sse/utils/noThinkingAlias.ts @@ -31,6 +31,15 @@ import { getModelSpec } from "@/shared/constants/modelSpecs"; export const NO_THINKING_PREFIX = "no-think/"; +// Ids that already carry a Claude reasoning-effort suffix (see +// claudeEffortVariants.ts's identical constant) — a no-think variant of an effort +// variant would combine two independent OmniRoute catalog conventions on the same +// id. Dispatch-time, applyNoThinkingAlias pre-sets reasoning_effort:"none" before +// applyClaudeEffortVariant's hasExplicitClaudeEffort() check runs, so the pre-set +// "none" is treated as explicit and the suffix's implied effort is silently +// discarded — semantically incoherent, so never advertise the combination. +const CLAUDE_EFFORT_SUFFIX_RE = /-(?:xhigh|high|medium|low)$/i; + /** True when `modelId` carries the no-thinking gateway prefix. */ export function isNoThinkingAlias(modelId: unknown): modelId is string { return typeof modelId === "string" && modelId.startsWith(NO_THINKING_PREFIX); @@ -108,6 +117,7 @@ export function shouldExposeNoThinkingAlias(model: CatalogModelEntry): boolean { if (typeof id !== "string" || id.length === 0) return false; if (model.owned_by === "combo") return false; // combos are virtual if (isNoThinkingAlias(id)) return false; // never double-alias + if (CLAUDE_EFFORT_SUFFIX_RE.test(id)) return false; // never combine with an effort-suffix id const name = bareModelName(id); const spec = getModelSpec(name); @@ -158,7 +168,8 @@ export function appendNoThinkingVariants( const rawId = model.id as string; const qualifiedId = aliasToCanonical ? normalizeProviderPrefix(rawId, aliasToCanonical) : rawId; const aliasId = toNoThinkingAlias(qualifiedId); - const variant: T = { ...model, id: aliasId, root: aliasId }; + const bareRoot = toNoThinkingAlias(bareModelName(qualifiedId)); + const variant: T = { ...model, id: aliasId, root: bareRoot }; if (typeof model.name === "string" && model.name) { variant.name = `${model.name} (no thinking)`; } diff --git a/tests/unit/cc-discovery-aliases-append.test.ts b/tests/unit/cc-discovery-aliases-append.test.ts index 7068cdf517..910e469798 100644 --- a/tests/unit/cc-discovery-aliases-append.test.ts +++ b/tests/unit/cc-discovery-aliases-append.test.ts @@ -30,11 +30,25 @@ test("adds a claude/ mirror with display_name and root for an eligible model", ( assert.deepEqual(out[0], models[0]); const alias = out[1]; assert.equal(alias.id, "claude/kimi/kimi-k2.6"); - assert.equal(alias.root, "kimi/kimi-k2.6"); + assert.equal(alias.root, "kimi-k2.6"); assert.equal(alias.display_name, "Kimi K2.6 (OmniRoute)"); assert.equal(alias.owned_by, "kimi"); }); +test("keeps root bare even when the original id carries a provider prefix", () => { + const models: CatalogEntry[] = [ + { id: "vertex/claude-sonnet-5", owned_by: "vertex", name: "Claude Sonnet 5 (Vertex)" }, + ]; + const out = appendCcDiscoveryAliases(models, alwaysEnabled); + const alias = out.find((m) => m.id === "claude/vertex/claude-sonnet-5"); + assert.ok(alias, "mirror entry with the fully-qualified id must exist"); + assert.equal( + alias!.root, + "claude-sonnet-5", + "root must be bare, matching the no-think/effort-variant convention" + ); +}); + test("falls back to the id for display_name when name is missing", () => { const models: CatalogEntry[] = [{ id: "kimi/kimi-k2.6" }]; const out = appendCcDiscoveryAliases(models, alwaysEnabled); @@ -86,6 +100,17 @@ test("mirrors combo names containing spaces (comboNameSchema allows them)", () = assert.equal(out[1].root, "Custo Otimizado BR"); }); +test("keeps a combo's root the full name verbatim when the combo name contains a slash", () => { + // comboNameSchema (src/shared/validation/schemas/combo.ts) explicitly allows "/" in + // combo names, so bareModelName must NOT be applied to combo entries — only to real + // provider-qualified model ids. + const models: CatalogEntry[] = [{ id: "Team/Alpha", owned_by: "combo", name: "Team/Alpha" }]; + const out = appendCcDiscoveryAliases(models, alwaysEnabled); + assert.equal(out.length, 2); + assert.equal(out[1].id, "claude/combo/Team/Alpha"); + assert.equal(out[1].root, "Team/Alpha", "root must be the full combo name, not truncated"); +}); + test("skips disabled entries and returns the same array reference when nothing is eligible", () => { const models: CatalogEntry[] = [{ id: "kimi/kimi-k2.6", owned_by: "kimi" }]; const out = appendCcDiscoveryAliases(models, () => false); diff --git a/tests/unit/no-thinking-alias.test.ts b/tests/unit/no-thinking-alias.test.ts index 5818dc6465..d7de24baa2 100644 --- a/tests/unit/no-thinking-alias.test.ts +++ b/tests/unit/no-thinking-alias.test.ts @@ -73,7 +73,11 @@ test("applyNoThinkingAlias expresses reasoning_effort:none without a thinking bl // #6879: a thinks-by-default OpenAI-shape model must carry reasoning_effort:"none" // explicitly (not merely have the field deleted), so suppression actually takes // effect downstream; the Responses-shaped `reasoning` object is still dropped. - assert.equal(body.reasoning_effort, "none", "reasoning_effort must express none, not be stripped"); + assert.equal( + body.reasoning_effort, + "none", + "reasoning_effort must express none, not be stripped" + ); assert.ok(!("reasoning" in body), "reasoning object must be dropped"); }); @@ -96,11 +100,7 @@ test("applyNoThinkingAlias ignores a malformed prefix-only model", () => { const body: Record = { model: "no-think/" }; const res = applyNoThinkingAlias(body, { claudeFormat: true }); assert.equal(res.applied, false); - assert.equal( - body.model, - "no-think/", - "left untouched when nothing follows the prefix" - ); + assert.equal(body.model, "no-think/", "left untouched when nothing follows the prefix"); }); // ── catalog gating ─────────────────────────────────────────────────────────── @@ -120,28 +120,16 @@ test("shouldExposeNoThinkingAlias rejects models where suppression is meaningles // combos are virtual, never aliased assert.equal(shouldExposeNoThinkingAlias(entry("my-combo", "combo")), false); // never double-alias - assert.equal( - shouldExposeNoThinkingAlias(entry("no-think/anthropic/claude-opus-4-5")), - false - ); + assert.equal(shouldExposeNoThinkingAlias(entry("no-think/anthropic/claude-opus-4-5")), false); }); test("appendNoThinkingVariants adds one variant per eligible model and preserves the rest", () => { const models = [entry("claude-opus-4-5"), entry("gpt-4o", "openai"), entry("claude-fable-5")]; const out = appendNoThinkingVariants(models); const ids = out.map((m) => m.id); - assert.ok( - ids.includes("no-think/claude-opus-4-5"), - "eligible model gets a variant" - ); - assert.ok( - !ids.includes("no-think/gpt-4o"), - "non-thinking model has no variant" - ); - assert.ok( - !ids.includes("no-think/claude-fable-5"), - "reject-disabled model has no variant" - ); + assert.ok(ids.includes("no-think/claude-opus-4-5"), "eligible model gets a variant"); + assert.ok(!ids.includes("no-think/gpt-4o"), "non-thinking model has no variant"); + assert.ok(!ids.includes("no-think/claude-fable-5"), "reject-disabled model has no variant"); assert.equal(out.length, models.length + 1, "exactly one variant appended"); // originals preserved up front assert.deepEqual(out.slice(0, 3), models); @@ -157,22 +145,42 @@ test("appendNoThinkingVariants normalizes alias prefix to canonical when aliasTo const aliasToCanonical = { cc: "claude" }; const out = appendNoThinkingVariants(models, aliasToCanonical); const ids = out.map((m) => m.id); - assert.ok( - ids.includes("no-think/claude/claude-opus-4-5"), - "uses canonical prefix" - ); - assert.ok( - !ids.includes("no-think/cc/claude-opus-4-5"), - "alias prefix not used" - ); + assert.ok(ids.includes("no-think/claude/claude-opus-4-5"), "uses canonical prefix"); + assert.ok(!ids.includes("no-think/cc/claude-opus-4-5"), "alias prefix not used"); }); test("appendNoThinkingVariants keeps alias prefix when no map is provided", () => { const models = [entry("cc/claude-opus-4-5")]; const out = appendNoThinkingVariants(models); const ids = out.map((m) => m.id); + assert.ok(ids.includes("no-think/cc/claude-opus-4-5"), "alias prefix preserved"); +}); + +test("appendNoThinkingVariants keeps root bare even when id carries a provider prefix", () => { + const models = [entry("vertex/claude-opus-4-5", "vertex")]; + const out = appendNoThinkingVariants(models); + const variant = out.find((m) => m.id === "no-think/vertex/claude-opus-4-5"); + assert.ok(variant, "variant with the fully-qualified id must exist"); + assert.equal( + variant!.root, + "no-think/claude-opus-4-5", + "root must be bare (no embedded provider segment), matching the effort-variant convention" + ); +}); + +test("shouldExposeNoThinkingAlias rejects an already effort-suffixed id", () => { + assert.equal(shouldExposeNoThinkingAlias(entry("vertex/claude-sonnet-5-high")), false); + assert.equal(shouldExposeNoThinkingAlias(entry("claude-opus-4-5-xhigh")), false); +}); + +test("appendNoThinkingVariants does not synthesize a no-think variant of an effort variant", () => { + // Simulates the real pipeline order in catalogResponse.ts: appendClaudeEffortVariants + // runs first and produces an id like this before appendNoThinkingVariants ever sees it. + const models = [entry("vertex/claude-sonnet-5-high")]; + const out = appendNoThinkingVariants(models); + assert.equal(out, models, "no variant should be added for an effort-suffixed id"); assert.ok( - ids.includes("no-think/cc/claude-opus-4-5"), - "alias prefix preserved" + !out.some((m) => m.id === "no-think/vertex/claude-sonnet-5-high"), + "the incoherent combined id must never be advertised" ); });