Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions open-sse/utils/noThinkingAlias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>
): 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<T extends CatalogModelEntry>(models: T[]): T[] {
export function appendNoThinkingVariants<T extends CatalogModelEntry>(
models: T[],
aliasToCanonical?: Record<string, string>
): 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)`;
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/v1/models/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 52 additions & 11 deletions tests/unit/no-thinking-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { model: "anthropic/claude-opus-4-5", thinking: { type: "enabled" } };
const body: Record<string, unknown> = {
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<string, unknown> = { 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 ───────────────────────────────────────────────────────────
Expand All @@ -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);
Expand All @@ -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"
);
});
Loading