diff --git a/open-sse/utils/noThinkingAlias.ts b/open-sse/utils/noThinkingAlias.ts index febafc954a..9acbb2b6c8 100644 --- a/open-sse/utils/noThinkingAlias.ts +++ b/open-sse/utils/noThinkingAlias.ts @@ -102,22 +102,47 @@ export function shouldExposeNoThinkingAlias(model: CatalogModelEntry): boolean { if (spec.noThinkingAlias === false) return false; return ( - spec.supportsThinking === true && - spec.rejectsThinkingDisabled !== true && - /claude/i.test(name) + spec.supportsThinking === true && spec.rejectsThinkingDisabled !== true && /claude/i.test(name) ); } +/** + * Normalize the provider prefix inside a qualified model id using an alias→canonical map. + * e.g. "cc/claude-opus-4-6" → "claude/claude-opus-4-6" when aliasToCanonical["cc"]="claude". + * Ids without a "/" or whose prefix is not in the map are returned unchanged. + */ +function normalizeProviderPrefix( + qualifiedId: string, + aliasToCanonical: Record +): string { + const slash = qualifiedId.indexOf("/"); + if (slash < 0) return qualifiedId; + const prefix = qualifiedId.slice(0, slash); + const canonical = aliasToCanonical[prefix]; + return canonical && canonical !== prefix + ? `${canonical}${qualifiedId.slice(slash)}` + : qualifiedId; +} + /** * Append a no-thinking variant for every eligible model. Returns the original array * reference unchanged when nothing is eligible (no allocation in the common case). + * + * @param aliasToCanonical - When provided, the inner provider prefix of each variant id is + * normalized to its canonical form (e.g. "cc" → "claude"). Pass this when the catalog is + * emitting canonical-prefixed ids so no-think variants stay consistent with the prefix mode. */ -export function appendNoThinkingVariants(models: T[]): T[] { +export function appendNoThinkingVariants( + models: T[], + aliasToCanonical?: Record +): T[] { if (!Array.isArray(models)) return models; const variants: T[] = []; for (const model of models) { if (!shouldExposeNoThinkingAlias(model)) continue; - const aliasId = toNoThinkingAlias(model.id as string); + 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 }; if (typeof model.name === "string" && model.name) { variant.name = `${model.name} (no thinking)`; diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index ae747cb384..21df30ac10 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -1397,7 +1397,10 @@ export async function getUnifiedModelsResponse( // Advertise no-thinking gateway variants (Fase 8.1). Derived from the already // key-filtered list, so a variant only appears when its real model is permitted. - finalModels = appendNoThinkingVariants(finalModels); + finalModels = appendNoThinkingVariants( + finalModels, + prefixMode === "canonical" ? aliasToProviderId : undefined + ); // #4424 follow-up — drop exact-duplicate ids that slip through the per-source push // guards (e.g. `codex/gpt-5.5`, `veo-free/seedance` listed twice). Keyed by listing diff --git a/tests/unit/no-thinking-alias.test.ts b/tests/unit/no-thinking-alias.test.ts index eba93de349..e385698bc5 100644 --- a/tests/unit/no-thinking-alias.test.ts +++ b/tests/unit/no-thinking-alias.test.ts @@ -75,18 +75,29 @@ test("applyNoThinkingAlias strips reasoning fields without a thinking block (Ope }); test("applyNoThinkingAlias is a no-op for plain models", () => { - const body: Record = { model: "anthropic/claude-opus-4-5", thinking: { type: "enabled" } }; + const body: Record = { + model: "anthropic/claude-opus-4-5", + thinking: { type: "enabled" }, + }; const res = applyNoThinkingAlias(body, { claudeFormat: true }); assert.equal(res.applied, false); assert.equal(body.model, "anthropic/claude-opus-4-5"); - assert.deepEqual(body.thinking, { type: "enabled" }, "thinking is left untouched when not an alias"); + assert.deepEqual( + body.thinking, + { type: "enabled" }, + "thinking is left untouched when not an alias" + ); }); test("applyNoThinkingAlias ignores a malformed prefix-only model", () => { const body: Record = { model: "claude-3-omniroute-no-thinking/" }; const res = applyNoThinkingAlias(body, { claudeFormat: true }); assert.equal(res.applied, false); - assert.equal(body.model, "claude-3-omniroute-no-thinking/", "left untouched when nothing follows the prefix"); + assert.equal( + body.model, + "claude-3-omniroute-no-thinking/", + "left untouched when nothing follows the prefix" + ); }); // ── catalog gating ─────────────────────────────────────────────────────────── @@ -113,16 +124,21 @@ test("shouldExposeNoThinkingAlias rejects models where suppression is meaningles }); 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 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("claude-3-omniroute-no-thinking/claude-opus-4-5"), "eligible model gets a variant"); - assert.ok(!ids.includes("claude-3-omniroute-no-thinking/gpt-4o"), "non-thinking model has no variant"); - assert.ok(!ids.includes("claude-3-omniroute-no-thinking/claude-fable-5"), "reject-disabled model has no variant"); + assert.ok( + ids.includes("claude-3-omniroute-no-thinking/claude-opus-4-5"), + "eligible model gets a variant" + ); + assert.ok( + !ids.includes("claude-3-omniroute-no-thinking/gpt-4o"), + "non-thinking model has no variant" + ); + assert.ok( + !ids.includes("claude-3-omniroute-no-thinking/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); @@ -132,3 +148,28 @@ test("appendNoThinkingVariants returns the same array reference when nothing is const models = [entry("gpt-4o", "openai")]; assert.equal(appendNoThinkingVariants(models), models); }); + +test("appendNoThinkingVariants normalizes alias prefix to canonical when aliasToCanonical map is provided", () => { + const models = [entry("cc/claude-opus-4-5")]; + const aliasToCanonical = { cc: "claude" }; + const out = appendNoThinkingVariants(models, aliasToCanonical); + const ids = out.map((m) => m.id); + assert.ok( + ids.includes("claude-3-omniroute-no-thinking/claude/claude-opus-4-5"), + "uses canonical prefix" + ); + assert.ok( + !ids.includes("claude-3-omniroute-no-thinking/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("claude-3-omniroute-no-thinking/cc/claude-opus-4-5"), + "alias prefix preserved" + ); +});