Skip to content

Commit 4dd9581

Browse files
fix(server): Claude effort-cap model keys match like Claude Code
Claude Code names the model a modelSettings key caps more narrowly than the catalog's aliases: only a family alias (opus, sonnet, haiku, fable, optionally [1m]) resolves as an alias, and any other key names the most specific model id it contains. Resolving keys through the catalog capped labels for dotted keys such as claude-opus-5.5 that the CLI ignores, and missed dated or Bedrock ids such as claude-opus-5-5-20260101 that it applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 35f4fc1 commit 4dd9581

2 files changed

Lines changed: 62 additions & 9 deletions

File tree

‎apps/server/src/provider/ClaudeModelCatalog.test.ts‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ describe("Claude model catalog", () => {
382382
slug: "claude-opus-5-5",
383383
name: "Opus 5.5",
384384
isCustom: false,
385-
aliases: ["opus"],
385+
aliases: ["opus", "opus-5.5", "claude-opus-5.5"],
386386
capabilities: {
387387
optionDescriptors: [
388388
{
@@ -400,7 +400,19 @@ describe("Claude model catalog", () => {
400400
},
401401
};
402402
const catalog: ClaudeModelCatalog = {
403-
models: [{ model: opus, runtime: {}, compatibility: {} }],
403+
models: [
404+
{ model: opus, runtime: {}, compatibility: {} },
405+
{
406+
model: {
407+
slug: "claude-opus-5",
408+
name: "Opus 5",
409+
isCustom: false,
410+
capabilities: { optionDescriptors: [] },
411+
},
412+
runtime: {},
413+
compatibility: {},
414+
},
415+
],
404416
};
405417
const maxLabel = (sources: Parameters<typeof capClaudeCatalogEffort>[2]) => {
406418
const effort = capClaudeCatalogEffort(catalog, [opus], sources)[0]?.capabilities
@@ -417,6 +429,20 @@ describe("Claude model catalog", () => {
417429
maxLabel([perModel({ "claude-opus-5-5[1M]": "medium" })]),
418430
"Max (capped to Medium)",
419431
);
432+
assert.strictEqual(maxLabel([perModel({ " OPUS[1m] ": "medium" })]), "Max (capped to Medium)");
433+
// Other keys name the most specific model id they contain, so dated and provider-prefixed ids
434+
// match while the catalog's dotted aliases and a less specific model id do not.
435+
for (const key of [
436+
"claude-opus-5-5-20260101",
437+
"CLAUDE-OPUS-5-5-20260101[1m]",
438+
"us.anthropic.claude-opus-5-5-v1:0",
439+
"claude-opus-5-5-latest",
440+
]) {
441+
assert.strictEqual(maxLabel([perModel({ [key]: "medium" })]), "Max (capped to Medium)", key);
442+
}
443+
for (const key of ["claude-opus-5.5", "opus-5.5", "opus-5-5", "opus [1m]", "claude-opus-5"]) {
444+
assert.strictEqual(maxLabel([perModel({ [key]: "medium" })]), "Max", key);
445+
}
420446
// Keys naming the same model keep the lowest cap, and a `max` key does not exempt it.
421447
assert.strictEqual(
422448
maxLabel([perModel({ opus: "max", "claude-opus-5-5": "low" })]),

‎apps/server/src/provider/ClaudeModelCatalog.ts‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,22 +219,49 @@ export interface ClaudeEffortCapSource {
219219
}
220220

221221
const CLAUDE_1M_SUFFIX = "[1m]";
222+
const CLAUDE_CODE_FAMILY_ALIASES: ReadonlySet<string> = new Set([
223+
"opus",
224+
"sonnet",
225+
"haiku",
226+
"fable",
227+
]);
222228

223229
/**
224-
* Keys `modelSettings` caps by canonical slug. Claude Code matches a key's aliases and `[1m]`
225-
* spelling case-insensitively, and keeps the lowest cap when several keys name one model.
230+
* Names the model a `modelSettings` key caps the way Claude Code does, which is narrower than the
231+
* catalog's aliases: only a family alias (optionally `[1m]`) resolves as an alias, and any other key
232+
* names the most specific model id it contains, so `claude-opus-5-5-20260101` and Bedrock ids match
233+
* while `claude-opus-5.5` matches nothing.
226234
*/
235+
function resolveClaudeModelSettingsKey(
236+
catalog: ClaudeModelCatalog,
237+
key: string,
238+
): string | undefined {
239+
const lowered = key.trim().toLowerCase();
240+
const unsuffixed = lowered.endsWith(CLAUDE_1M_SUFFIX)
241+
? lowered.slice(0, -CLAUDE_1M_SUFFIX.length)
242+
: lowered;
243+
if (CLAUDE_CODE_FAMILY_ALIASES.has(unsuffixed)) {
244+
return catalog.models.find(
245+
(entry) => !entry.model.isCustom && entry.model.aliases?.includes(unsuffixed),
246+
)?.model.slug;
247+
}
248+
let match: string | undefined;
249+
for (const { model } of catalog.models) {
250+
if (!model.isCustom && lowered.includes(model.slug) && model.slug.length > (match?.length ?? 0))
251+
match = model.slug;
252+
}
253+
return match;
254+
}
255+
256+
/** Keys `modelSettings` caps by canonical slug, keeping the lowest cap when several keys name one model. */
227257
function resolveClaudeModelMaxEffortLevels(
228258
catalog: ClaudeModelCatalog,
229259
levels: ReadonlyMap<string, ClaudeEffortLevel> | undefined,
230260
): ReadonlyMap<string, ClaudeEffortLevel> {
231261
const resolved = new Map<string, ClaudeEffortLevel>();
232262
for (const [key, level] of levels ?? []) {
233-
const lowered = key.trim().toLowerCase();
234-
const slug = resolveClaudeModelSlug(
235-
catalog,
236-
lowered.endsWith(CLAUDE_1M_SUFFIX) ? lowered.slice(0, -CLAUDE_1M_SUFFIX.length) : lowered,
237-
);
263+
const slug = resolveClaudeModelSettingsKey(catalog, key);
264+
if (!slug) continue;
238265
const current = resolved.get(slug);
239266
if (!current || claudeEffortRank(level) < claudeEffortRank(current)) resolved.set(slug, level);
240267
}

0 commit comments

Comments
 (0)