From dcaed8008b91c146a89cda87327664e2d402c4b8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 25 Jul 2026 22:04:02 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 6 ++ package-lock.json | 72 +++++++------- packages/server/package.json | 2 +- .../agent/providers/claude/agent.test.ts | 10 +- .../server/agent/providers/claude/agent.ts | 37 +++++++- .../agent/providers/claude/model-manifest.ts | 93 +++++++++++++++---- .../agent/providers/claude/models.test.ts | 47 ++++++++-- .../server/agent/providers/claude/models.ts | 7 +- 8 files changed, 206 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6287f6a59..bc51d2a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.2 - 2026-07-25 + +### Fixed + +- Claude Opus 5 now defaults to its 1M context window. + ## 0.2.1 - 2026-07-24 ### Added diff --git a/package-lock.json b/package-lock.json index 2d9747350..059bb9249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38106,7 +38106,7 @@ "version": "0.2.1", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "^0.3.214", + "@anthropic-ai/claude-agent-sdk": "^0.3.220", "@anthropic-ai/sdk": "^0.104.2", "@getpaseo/client": "0.2.1", "@getpaseo/highlight": "0.2.1", @@ -38155,22 +38155,22 @@ } }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.214.tgz", - "integrity": "sha512-wt5ImwhU+p259Zt4K/Q9v5xVi6ruxYO5+KFICyJxnjs/QFEClAeSRqhcXx1J8jgGfatPW1faw09hkm66680UjA==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz", + "integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" }, "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.214", - "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.214", - "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.214", - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.214", - "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.214", - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.214", - "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.214", - "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.214" + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", @@ -38179,9 +38179,9 @@ } }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.214.tgz", - "integrity": "sha512-vAAOeVtlXs3p7MpFVfvfu9ja32eaKtB+MDZnPxCbdzxJk5nofCHlNsHa1UJwXHOsP9lOnFYNIccYztsZ4DNaJA==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz", + "integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==", "cpu": [ "arm64" ], @@ -38192,9 +38192,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.214.tgz", - "integrity": "sha512-NTbV8U2yucxCWqEiDC7L0MUehNmd8x3Op8OGzLZRhMF3xmQBy2DxpXiNZgSwwKWNDC3HdgKmJM5ZBbT7XrF/0g==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz", + "integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==", "cpu": [ "x64" ], @@ -38205,9 +38205,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.214.tgz", - "integrity": "sha512-KBCf+BlusG0ZcgvpjjHwv1kh+6WiR8vJbPjpR2udwkrtcQgKLN+24l+FeaQEzO2TL+ExCmM1KC4tNPvnPpy+tw==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz", + "integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==", "cpu": [ "arm64" ], @@ -38218,9 +38218,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.214.tgz", - "integrity": "sha512-i06wQRsmevE7spY1ryYfs+NP+xdZ1FwAyTjDaF0k/xG+cgtzZYghTFUemdYF3GVwgWgpcava9GiCFDT3DoHI6w==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz", + "integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==", "cpu": [ "arm64" ], @@ -38231,9 +38231,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.214.tgz", - "integrity": "sha512-vqadSceJkBKHaTUszxI35uTuECGWtsHWK/mOwIJ4b9DUtYYySz6EJEk4gHg4ccutisJ/oRVCpFXHIKFb+osrKw==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz", + "integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==", "cpu": [ "x64" ], @@ -38244,9 +38244,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.214.tgz", - "integrity": "sha512-948RstHDhs0E69h+2dKYWIAL1kcwEme4zvtgNUwP8XpKXzWOhrTKmd6MAokHn25zwfuSx5d+HE0XHfFH6R4hLg==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz", + "integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==", "cpu": [ "x64" ], @@ -38257,9 +38257,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.214.tgz", - "integrity": "sha512-CEKlPFCPv+ee79utQMEDSsgeo2f27ulhNHpjrKIt1jXz5G04J7lAWN/QwvPDv9XaLBkWpyfqZWiPXlRcwuaO/w==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz", + "integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==", "cpu": [ "arm64" ], @@ -38270,9 +38270,9 @@ ] }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.3.214", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.214.tgz", - "integrity": "sha512-cJMJfFoR9IWBZWnTtt9PnEm89hGOsZjoDNjOCEOF3+x+g/ANIXAjMJTcaQYv2HKh6MylWZ0AkxBASI+twkukaQ==", + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz", + "integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==", "cpu": [ "x64" ], diff --git a/packages/server/package.json b/packages/server/package.json index 61d3e3ea8..9e1ed8388 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "^0.3.214", + "@anthropic-ai/claude-agent-sdk": "^0.3.220", "@anthropic-ai/sdk": "^0.104.2", "@getpaseo/client": "0.2.1", "@getpaseo/highlight": "0.2.1", diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 60c6cc16e..1bc026c8f 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -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,6 +416,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { }); expect(models.map((m) => m.id)).toEqual([ + "claude-opus-5[1m]", "claude-opus-5", "claude-fable-5", "claude-opus-4-8[1m]", @@ -434,7 +437,7 @@ 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 }); } @@ -446,6 +449,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 +479,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); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 9b0429fff..754316b0d 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -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; + resolveVersion?: () => Promise; configDir?: string; } @@ -1439,6 +1441,7 @@ export class ClaudeAgentClient implements AgentClient { private readonly runtimeSettings?: ProviderRuntimeSettings; private readonly queryFactory?: ClaudeQueryFactory; private readonly resolveBinary: () => Promise; + private readonly resolveVersion: () => Promise; 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 { // Claude exposes a global catalog here; cwd/force are intentionally irrelevant. - const models = await getClaudeModelsWithSettings(this.logger, this.configDir); + let claudeCodeVersion: string | null = null; + 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,26 @@ async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): P ); } +export async function resolveClaudeCodeVersion( + runtimeSettings?: ProviderRuntimeSettings, +): Promise { + const launch = await resolveProviderLaunch({ + commandConfig: runtimeSettings?.command, + defaultBinary: "claude", + }); + const availability = await checkProviderLaunchAvailable(launch); + if (!availability.available) { + return null; + } + 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}`); + return version?.join(".") ?? null; +} + async function resolveClaudeAuth( launch: ResolvedProviderLaunch, availability: { resolvedPath: string | null }, diff --git a/packages/server/src/server/agent/providers/claude/model-manifest.ts b/packages/server/src/server/agent/providers/claude/model-manifest.ts index b569f0da1..1aa360e1f 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -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; @@ -30,12 +31,22 @@ export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off"; export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode"; export const CLAUDE_MODEL_MANIFEST = [ + { + 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 · Latest release", - isDefault: true, - contextWindowMaxTokens: 1_000_000, + description: "Opus 5 · 200K context window", + minimumClaudeCodeVersion: "2.1.219", + contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, supportsThinkingDisabled: true, }, @@ -59,6 +70,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, @@ -155,31 +167,74 @@ function buildThinkingOptions( return options; } -export function getClaudeManifestModels(): AgentModelDefinition[] { - return CLAUDE_MODEL_MANIFEST.map((model) => { +export function getClaudeManifestModels(claudeCodeVersion?: string | null): AgentModelDefinition[] { + const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter( + (model) => isModelAvailableInClaudeCode(model, claudeCodeVersion), + ); + const defaultModel = availableModels.reduce( + (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 | null | undefined, +): boolean { + if (!model.minimumClaudeCodeVersion || claudeCodeVersion === undefined) { + return true; + } + if (claudeCodeVersion === null) { + return false; + } + 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+)\b/); + return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null; +} + export interface ClaudeDisabledThinkingResolution { supported: boolean; fallbackThinkingOptionId: string | undefined; diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index 5b069dce1..56e49ff77 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -38,10 +38,18 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise 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", "claude-opus-4-8[1m]", @@ -61,7 +69,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,7 +79,8 @@ describe("getClaudeModels", () => { expect(contextWindows).toEqual( new Map([ - ["claude-opus-5", 1_000_000], + ["claude-opus-5[1m]", 1_000_000], + ["claude-opus-5", 200_000], ["claude-fable-5", 1_000_000], ["claude-opus-4-8[1m]", 1_000_000], ["claude-opus-4-8", 200_000], @@ -87,6 +96,15 @@ 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"); + }); + it("derives thinking options from model effort capabilities", () => { const models = new Map(getClaudeModels().map((model) => [model.id, model])); @@ -175,7 +193,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 +246,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 +260,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 +280,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 +300,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,10 +313,24 @@ 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-sonnet-5")).toBe("claude-sonnet-5"); @@ -318,6 +350,7 @@ describe("normalizeClaudeRuntimeModelId", () => { }); it("preserves [1m] suffix from runtime model strings", () => { + expect(normalizeClaudeRuntimeModelId("claude-opus-5[1m]")).toBe("claude-opus-5[1m]"); expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]"); }); diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index 0e82cb018..f1749cc78 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -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 | null): AgentModelDefinition[] { + return getClaudeManifestModels(claudeCodeVersion); } export function findClaudeModel( @@ -34,8 +34,9 @@ export function findClaudeModel( export async function getClaudeModelsWithSettings( logger: Logger, configDir?: string, + claudeCodeVersion?: string | null, ): Promise { - const hardcodedModels = getClaudeModels(); + const hardcodedModels = getClaudeModels(claudeCodeVersion); const settingsModels = await readClaudeSettingsModels(logger, configDir); if (settingsModels.length === 0) { return hardcodedModels;