Fix Claude 5 context window selection (#2433)

* fix(claude): select the correct Opus 5 context window

Bare Opus 5 IDs resolve to 200K behind third-party gateways. Expose explicit context variants and require a Claude Code version that recognizes the model.

* fix(claude): add Fable and Sonnet context variants

* test(cli): expect Claude context variants

* test(cli): compose Claude catalog expectations

* fix(claude): preserve catalog on version probe failure

* fix(claude): preserve parsed context variants

* chore(release): cut 0.2.2
This commit is contained in:
Mohamed Boudra
2026-07-26 10:03:44 +02:00
committed by GitHub
parent 51ab86bab6
commit c9fb31f709
19 changed files with 371 additions and 125 deletions
@@ -11,6 +11,7 @@ import {
convertClaudeHistoryEntry,
normalizeClaudeAskUserQuestionRequestInput,
normalizeClaudeAskUserQuestionUpdatedInput,
resolveClaudeCodeVersion,
toClaudeSdkMcpConfig,
} from "./agent.js";
import { claudeProjectDirSync } from "./project-dir.js";
@@ -405,6 +406,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
const client = new ClaudeAgentClient({
logger,
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.219",
configDir: emptyConfigDir,
});
const { models } = await client.fetchCatalog({
@@ -414,11 +416,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
});
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5[1m]",
"claude-opus-5",
"claude-fable-5[1m]",
"claude-fable-5",
"claude-opus-4-8[1m]",
"claude-opus-4-8",
"claude-sonnet-5",
"claude-sonnet-5[1m]",
"claude-opus-4-7[1m]",
"claude-opus-4-7",
"claude-opus-4-6[1m]",
@@ -434,7 +439,30 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
}
const defaultModel = models.find((m) => m.isDefault);
expect(defaultModel?.id).toBe("claude-opus-5");
expect(defaultModel?.id).toBe("claude-opus-5[1m]");
} finally {
await fs.rm(emptyConfigDir, { recursive: true, force: true });
}
});
test("preserves the catalog when Claude Code version detection fails", async () => {
const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-"));
try {
const client = new ClaudeAgentClient({
logger,
resolveVersion: async () => {
throw new Error("unrecognized version output");
},
configDir: emptyConfigDir,
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: "/tmp/claude-models",
force: false,
});
expect(models.find((model) => model.isDefault)?.id).toBe("claude-opus-5[1m]");
expect(models.map((model) => model.id)).toContain("claude-fable-5[1m]");
} finally {
await fs.rm(emptyConfigDir, { recursive: true, force: true });
}
@@ -446,6 +474,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
const client = new ClaudeAgentClient({
logger,
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.219",
configDir: emptyConfigDir,
});
const { models } = await client.fetchCatalog({
@@ -475,6 +504,10 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
describe("ClaudeAgentClient binary resolution", () => {
const logger = createTestLogger();
test("resolves the installed Claude Code version", async () => {
await expect(resolveClaudeCodeVersion()).resolves.toMatch(/^\d+\.\d+\.\d+$/);
});
test("loads user, project, and local Claude settings", async () => {
const queryReturn = vi.fn();
queryReturn.mockResolvedValue(undefined);
@@ -2083,10 +2116,10 @@ describe("ClaudeAgentSession context window usage", () => {
}
});
test("native 1M Claude models seed active context window usage from the catalog", async () => {
test("selected 1M Claude models seed active context window usage from the catalog", async () => {
const session = await createSessionForTurns(
[[createInitMessage(), createMessageStartEvent(), createSuccessResult()]],
{ model: "claude-sonnet-5" },
{ model: "claude-sonnet-5[1m]" },
);
try {
@@ -37,6 +37,7 @@ import {
import {
CLAUDE_DISABLED_THINKING_OPTION_ID,
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
parseClaudeCodeVersion,
resolveClaudeDisabledThinkingForModel,
} from "./model-manifest.js";
import { parsePartialJsonObject } from "./partial-json.js";
@@ -363,6 +364,7 @@ interface ClaudeAgentClientOptions {
runtimeSettings?: ProviderRuntimeSettings;
queryFactory?: ClaudeQueryFactory;
resolveBinary?: () => Promise<string>;
resolveVersion?: () => Promise<string>;
configDir?: string;
}
@@ -1439,6 +1441,7 @@ export class ClaudeAgentClient implements AgentClient {
private readonly runtimeSettings?: ProviderRuntimeSettings;
private readonly queryFactory?: ClaudeQueryFactory;
private readonly resolveBinary: () => Promise<string>;
private readonly resolveVersion: () => Promise<string>;
private readonly configDir?: string;
constructor(options: ClaudeAgentClientOptions) {
@@ -1447,6 +1450,8 @@ export class ClaudeAgentClient implements AgentClient {
this.runtimeSettings = options.runtimeSettings;
this.queryFactory = options.queryFactory;
this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings));
this.resolveVersion =
options.resolveVersion ?? (() => resolveClaudeCodeVersion(this.runtimeSettings));
this.configDir = options.configDir;
}
@@ -1498,7 +1503,17 @@ export class ClaudeAgentClient implements AgentClient {
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
// Claude exposes a global catalog here; cwd/force are intentionally irrelevant.
const models = await getClaudeModelsWithSettings(this.logger, this.configDir);
let claudeCodeVersion: string | undefined;
try {
claudeCodeVersion = await this.resolveVersion();
} catch (error) {
this.logger.warn({ err: error }, "Failed to resolve Claude Code version for model catalog");
}
const models = await getClaudeModelsWithSettings(
this.logger,
this.configDir,
claudeCodeVersion,
);
const modes = detectIneligibleAutoModeTransport(
createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings }),
)
@@ -1620,6 +1635,29 @@ async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): P
);
}
export async function resolveClaudeCodeVersion(
runtimeSettings?: ProviderRuntimeSettings,
): Promise<string> {
const launch = await resolveProviderLaunch({
commandConfig: runtimeSettings?.command,
defaultBinary: "claude",
});
const availability = await checkProviderLaunchAvailable(launch);
if (!availability.available) {
throw new Error("Claude binary not found while resolving Claude Code version");
}
const executable = availability.resolvedPath ?? launch.command;
const { stdout, stderr } = await execCommand(executable, [...launch.args, "--version"], {
...createProviderEnvSpec({ runtimeSettings }),
timeout: 5_000,
});
const version = parseClaudeCodeVersion(`${stdout}\n${stderr}`);
if (!version) {
throw new Error("Unable to parse Claude Code version from --version output");
}
return version.join(".");
}
async function resolveClaudeAuth(
launch: ResolvedProviderLaunch,
availability: { resolvedPath: string | null },
@@ -6,7 +6,8 @@ interface ClaudeModelManifestEntry {
id: string;
label: string;
description: string;
isDefault?: boolean;
defaultPriority?: number;
minimumClaudeCodeVersion?: string;
contextWindowMaxTokens?: number;
effortLevels?: readonly ClaudeEffortLevel[];
supportsThinkingDisabled?: boolean;
@@ -31,19 +32,38 @@ export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode";
export const CLAUDE_MODEL_MANIFEST = [
{
id: "claude-opus-5",
label: "Opus 5",
description: "Opus 5 · Latest release",
isDefault: true,
id: "claude-opus-5[1m]",
label: "Opus 5 1M",
description: "Opus 5 with 1M context window",
defaultPriority: 2,
minimumClaudeCodeVersion: "2.1.219",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsThinkingDisabled: true,
},
{
id: "claude-opus-5",
label: "Opus 5",
description: "Opus 5 · 200K context window",
minimumClaudeCodeVersion: "2.1.219",
contextWindowMaxTokens: 200_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsThinkingDisabled: true,
},
{
id: "claude-fable-5[1m]",
label: "Fable 5 1M",
description: "Fable 5 with 1M context window",
minimumClaudeCodeVersion: "2.1.169",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
id: "claude-fable-5",
label: "Fable 5",
description: "Fable 5 · Most powerful model",
contextWindowMaxTokens: 1_000_000,
minimumClaudeCodeVersion: "2.1.169",
contextWindowMaxTokens: 200_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
@@ -59,6 +79,7 @@ export const CLAUDE_MODEL_MANIFEST = [
id: "claude-opus-4-8",
label: "Opus 4.8",
description: "Opus 4.8 · Previous release",
defaultPriority: 1,
contextWindowMaxTokens: 200_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsThinkingDisabled: true,
@@ -68,6 +89,14 @@ export const CLAUDE_MODEL_MANIFEST = [
id: "claude-sonnet-5",
label: "Sonnet 5",
description: "Sonnet 5 · Best for everyday tasks",
contextWindowMaxTokens: 200_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsThinkingDisabled: true,
},
{
id: "claude-sonnet-5[1m]",
label: "Sonnet 5 1M",
description: "Sonnet 5 with 1M context window",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsThinkingDisabled: true,
@@ -155,31 +184,73 @@ function buildThinkingOptions(
return options;
}
export function getClaudeManifestModels(): AgentModelDefinition[] {
return CLAUDE_MODEL_MANIFEST.map((model) => {
export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelDefinition[] {
const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter(
(model) => isModelAvailableInClaudeCode(model, claudeCodeVersion),
);
const defaultModel = availableModels.reduce<ClaudeModelManifestEntry | undefined>(
(selected, candidate) =>
(candidate.defaultPriority ?? 0) > (selected?.defaultPriority ?? 0) ? candidate : selected,
undefined,
);
return availableModels.map((model) => {
const thinkingOptions = buildThinkingOptions(
"effortLevels" in model ? model.effortLevels : undefined,
"supportsThinkingDisabled" in model && model.supportsThinkingDisabled,
model.effortLevels,
model.supportsThinkingDisabled === true,
);
return {
const definition: AgentModelDefinition = {
provider: "claude",
id: model.id,
label: model.label,
description: model.description,
...("isDefault" in model && model.isDefault ? { isDefault: true } : {}),
...(model.contextWindowMaxTokens !== undefined
? { contextWindowMaxTokens: model.contextWindowMaxTokens }
: {}),
...(thinkingOptions
? {
thinkingOptions,
defaultThinkingOptionId: "effortLevels" in model ? model.effortLevels?.[0] : undefined,
}
: {}),
};
if (model === defaultModel) {
definition.isDefault = true;
}
if (model.contextWindowMaxTokens !== undefined) {
definition.contextWindowMaxTokens = model.contextWindowMaxTokens;
}
if (thinkingOptions) {
definition.thinkingOptions = thinkingOptions;
definition.defaultThinkingOptionId = model.effortLevels?.[0];
}
return definition;
});
}
function isModelAvailableInClaudeCode(
model: ClaudeModelManifestEntry,
claudeCodeVersion: string | undefined,
): boolean {
if (!model.minimumClaudeCodeVersion || claudeCodeVersion === undefined) {
return true;
}
return compareVersions(claudeCodeVersion, model.minimumClaudeCodeVersion) >= 0;
}
function compareVersions(left: string, right: string): number {
const leftParts = parseClaudeCodeVersion(left);
const rightParts = parseClaudeCodeVersion(right);
if (!leftParts || !rightParts) {
return -1;
}
for (let index = 0; index < leftParts.length; index += 1) {
const difference = leftParts[index] - rightParts[index];
if (difference !== 0) {
return difference;
}
}
return 0;
}
export function parseClaudeCodeVersion(value: string): [number, number, number] | null {
const match =
value.match(/\b(\d+)\.(\d+)\.(\d+)\s+\(Claude Code\)/i) ??
value.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
}
export interface ClaudeDisabledThinkingResolution {
supported: boolean;
fallbackThinkingOptionId: string | undefined;
@@ -236,7 +307,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const singleSegmentMatch = trimmed.match(
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (singleSegmentMatch) {
return normalizeSingleSegmentClaudeModelId(
@@ -247,7 +318,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const runtimeMatch = trimmed.match(
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (!runtimeMatch) {
return null;
@@ -257,7 +328,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
runtimeMatch[1],
runtimeMatch[2],
runtimeMatch[3],
Boolean(runtimeMatch[4]),
trimmed.toLowerCase().includes("[1m]"),
);
}
@@ -284,7 +355,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
const normalizedModelId = normalizeSingleSegmentClaudeModelId(
singleSegmentMatch[1],
singleSegmentMatch[2],
Boolean(singleSegmentMatch[3]),
trimmed.toLowerCase().includes("[1m]"),
);
if (normalizedModelId) {
return normalizedModelId;
@@ -302,7 +373,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
runtimeMatch[1],
runtimeMatch[2],
runtimeMatch[3],
Boolean(runtimeMatch[4]),
trimmed.toLowerCase().includes("[1m]"),
);
}
@@ -10,6 +10,7 @@ import {
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
claudeManifestModelSupportsFastMode,
normalizeClaudeManifestModelId,
parseClaudeCodeVersion,
resolveClaudeDisabledThinkingForModel,
} from "./model-manifest.js";
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
@@ -38,15 +39,25 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise<s
return configDir;
}
function createCatalogClient(claudeCodeVersion = "2.1.219"): ClaudeAgentClient {
return new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => claudeCodeVersion,
});
}
describe("getClaudeModels", () => {
it("returns all claude models", () => {
const models = getClaudeModels();
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5[1m]",
"claude-opus-5",
"claude-fable-5[1m]",
"claude-fable-5",
"claude-opus-4-8[1m]",
"claude-opus-4-8",
"claude-sonnet-5",
"claude-sonnet-5[1m]",
"claude-opus-4-7[1m]",
"claude-opus-4-7",
"claude-opus-4-6[1m]",
@@ -61,7 +72,7 @@ describe("getClaudeModels", () => {
const models = getClaudeModels();
const defaults = models.filter((m) => m.isDefault);
expect(defaults).toHaveLength(1);
expect(defaults[0].id).toBe("claude-opus-5");
expect(defaults[0].id).toBe("claude-opus-5[1m]");
});
it("defines context window sizes in the catalog", () => {
@@ -71,11 +82,14 @@ describe("getClaudeModels", () => {
expect(contextWindows).toEqual(
new Map([
["claude-opus-5", 1_000_000],
["claude-fable-5", 1_000_000],
["claude-opus-5[1m]", 1_000_000],
["claude-opus-5", 200_000],
["claude-fable-5[1m]", 1_000_000],
["claude-fable-5", 200_000],
["claude-opus-4-8[1m]", 1_000_000],
["claude-opus-4-8", 200_000],
["claude-sonnet-5", 1_000_000],
["claude-sonnet-5", 200_000],
["claude-sonnet-5[1m]", 1_000_000],
["claude-opus-4-7[1m]", 1_000_000],
["claude-opus-4-7", 200_000],
["claude-opus-4-6[1m]", 1_000_000],
@@ -87,6 +101,20 @@ describe("getClaudeModels", () => {
);
});
it("filters models by their minimum Claude Code version", () => {
const oldVersionModels = getClaudeModels("2.1.218");
expect(oldVersionModels.map((model) => model.id)).not.toContain("claude-opus-5[1m]");
expect(oldVersionModels.map((model) => model.id)).not.toContain("claude-opus-5");
expect(oldVersionModels.find((model) => model.isDefault)?.id).toBe("claude-opus-4-8");
expect(getClaudeModels("2.1.219").map((model) => model.id)).toContain("claude-opus-5[1m]");
expect(getClaudeModels("2.1.219").map((model) => model.id)).toContain("claude-opus-5");
expect(getClaudeModels("2.1.168").map((model) => model.id)).not.toContain("claude-fable-5[1m]");
expect(getClaudeModels("2.1.168").map((model) => model.id)).not.toContain("claude-fable-5");
expect(getClaudeModels("2.1.169").map((model) => model.id)).toContain("claude-fable-5[1m]");
expect(getClaudeModels("2.1.169").map((model) => model.id)).toContain("claude-fable-5");
});
it("derives thinking options from model effort capabilities", () => {
const models = new Map(getClaudeModels().map((model) => [model.id, model]));
@@ -115,6 +143,9 @@ describe("getClaudeModels", () => {
?.label,
).toBe("Ultra Code");
expect(models.get("claude-sonnet-5")?.defaultThinkingOptionId).toBe("low");
expect(models.get("claude-sonnet-5[1m]")?.thinkingOptions).toEqual(
models.get("claude-sonnet-5")?.thinkingOptions,
);
expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([
CLAUDE_DISABLED_THINKING_OPTION_ID,
@@ -135,6 +166,9 @@ describe("getClaudeModels", () => {
expect(models.get("claude-fable-5")?.thinkingOptions?.map((option) => option.id)).not.toContain(
CLAUDE_DISABLED_THINKING_OPTION_ID,
);
expect(models.get("claude-fable-5[1m]")?.thinkingOptions).toEqual(
models.get("claude-fable-5")?.thinkingOptions,
);
expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined();
});
@@ -142,6 +176,7 @@ describe("getClaudeModels", () => {
["claude-opus-5", true, "low"],
["claude-opus-5-20260724", true, "low"],
["claude-sonnet-5", true, "low"],
["claude-sonnet-5[1m]", true, "low"],
["claude-sonnet-5-20260101", true, "low"],
["claude-fable-5", false, "low"],
["claude-haiku-4-5", false, undefined],
@@ -175,7 +210,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
},
});
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const client = new ClaudeAgentClient({ logger: createTestLogger() });
const client = createCatalogClient();
const { models } = await client.fetchCatalog({
scope: "workspace",
@@ -228,7 +263,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-"));
createdClaudeConfigDirs.push(configDir);
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const client = new ClaudeAgentClient({ logger: createTestLogger() });
const client = createCatalogClient();
const { models } = await client.fetchCatalog({
scope: "workspace",
@@ -242,7 +277,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
it("falls back to hardcoded models when settings.json is malformed", async () => {
const configDir = await createClaudeConfigDirWithRawSettings("{ nope");
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const client = new ClaudeAgentClient({ logger: createTestLogger() });
const client = createCatalogClient();
const { models } = await client.fetchCatalog({
scope: "workspace",
@@ -262,7 +297,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
},
});
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const client = new ClaudeAgentClient({ logger: createTestLogger() });
const client = createCatalogClient();
const { models } = await client.fetchCatalog({
scope: "workspace",
@@ -282,7 +317,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
},
});
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
const client = new ClaudeAgentClient({ logger: createTestLogger() });
const client = createCatalogClient();
const { models } = await client.fetchCatalog({
scope: "workspace",
@@ -295,13 +330,29 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
"glm-5.1",
]);
});
it("omits models that require a newer Claude Code version", async () => {
const client = createCatalogClient("2.1.218");
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: os.tmpdir(),
force: true,
});
expect(models.map((model) => model.id)).not.toContain("claude-opus-5[1m]");
expect(models.map((model) => model.id)).not.toContain("claude-opus-5");
});
});
describe("normalizeClaudeRuntimeModelId", () => {
it("returns exact match for known model IDs", () => {
expect(normalizeClaudeRuntimeModelId("claude-opus-5[1m]")).toBe("claude-opus-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-opus-5")).toBe("claude-opus-5");
expect(normalizeClaudeRuntimeModelId("claude-fable-5")).toBe("claude-fable-5");
expect(normalizeClaudeRuntimeModelId("claude-fable-5[1m]")).toBe("claude-fable-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5")).toBe("claude-sonnet-5");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5[1m]")).toBe("claude-sonnet-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6")).toBe("claude-opus-4-6");
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
@@ -315,9 +366,17 @@ describe("normalizeClaudeRuntimeModelId", () => {
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6-20260101")).toBe("claude-opus-4-6");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6-20260101")).toBe("claude-sonnet-4-6");
expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5");
expect(normalizeClaudeRuntimeModelId("claude-opus-5-20260724[1m]")).toBe("claude-opus-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-fable-5-20260301[1m]")).toBe("claude-fable-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5-20260101[1m]")).toBe(
"claude-sonnet-5[1m]",
);
});
it("preserves [1m] suffix from runtime model strings", () => {
expect(normalizeClaudeRuntimeModelId("claude-opus-5[1m]")).toBe("claude-opus-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-fable-5[1m]")).toBe("claude-fable-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5[1m]")).toBe("claude-sonnet-5[1m]");
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
});
@@ -346,6 +405,12 @@ describe("normalizeClaudeRuntimeModelId", () => {
});
});
describe("parseClaudeCodeVersion", () => {
it("prefers the Claude Code version over a wrapper banner", () => {
expect(parseClaudeCodeVersion("wrapper 1.0.0\n2.1.219 (Claude Code)")).toEqual([2, 1, 219]);
});
});
describe("findClaudeModel", () => {
it("resolves runtime model IDs to catalog entries", () => {
expect(findClaudeModel("claude-sonnet-5-20260101")?.id).toBe("claude-sonnet-5");
@@ -17,8 +17,8 @@ const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
] as const;
export function getClaudeModels(): AgentModelDefinition[] {
return getClaudeManifestModels();
export function getClaudeModels(claudeCodeVersion?: string): AgentModelDefinition[] {
return getClaudeManifestModels(claudeCodeVersion);
}
export function findClaudeModel(
@@ -34,8 +34,9 @@ export function findClaudeModel(
export async function getClaudeModelsWithSettings(
logger: Logger,
configDir?: string,
claudeCodeVersion?: string,
): Promise<AgentModelDefinition[]> {
const hardcodedModels = getClaudeModels();
const hardcodedModels = getClaudeModels(claudeCodeVersion);
const settingsModels = await readClaudeSettingsModels(logger, configDir);
if (settingsModels.length === 0) {
return hardcodedModels;