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 16cd1e9131..e773cd7359 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -445,6 +445,29 @@ describe("ClaudeAgentClient.fetchCatalog", () => { } }); + 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 }); + } + }); + test("exposes Ultra Code on xhigh-capable Claude models", async () => { const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-")); try { diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 754316b0d5..463540825d 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -364,7 +364,7 @@ interface ClaudeAgentClientOptions { runtimeSettings?: ProviderRuntimeSettings; queryFactory?: ClaudeQueryFactory; resolveBinary?: () => Promise; - resolveVersion?: () => Promise; + resolveVersion?: () => Promise; configDir?: string; } @@ -1441,7 +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 resolveVersion: () => Promise; private readonly configDir?: string; constructor(options: ClaudeAgentClientOptions) { @@ -1503,7 +1503,7 @@ export class ClaudeAgentClient implements AgentClient { async fetchCatalog(_options: FetchCatalogOptions): Promise { // Claude exposes a global catalog here; cwd/force are intentionally irrelevant. - let claudeCodeVersion: string | null = null; + let claudeCodeVersion: string | undefined; try { claudeCodeVersion = await this.resolveVersion(); } catch (error) { @@ -1637,14 +1637,14 @@ async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): P export async function resolveClaudeCodeVersion( runtimeSettings?: ProviderRuntimeSettings, -): Promise { +): Promise { const launch = await resolveProviderLaunch({ commandConfig: runtimeSettings?.command, defaultBinary: "claude", }); const availability = await checkProviderLaunchAvailable(launch); if (!availability.available) { - return null; + 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"], { @@ -1652,7 +1652,10 @@ export async function resolveClaudeCodeVersion( timeout: 5_000, }); const version = parseClaudeCodeVersion(`${stdout}\n${stderr}`); - return version?.join(".") ?? null; + if (!version) { + throw new Error("Unable to parse Claude Code version from --version output"); + } + return version.join("."); } async function resolveClaudeAuth( 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 1b14469dcd..9c81a1d508 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -184,7 +184,7 @@ function buildThinkingOptions( return options; } -export function getClaudeManifestModels(claudeCodeVersion?: string | null): AgentModelDefinition[] { +export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelDefinition[] { const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter( (model) => isModelAvailableInClaudeCode(model, claudeCodeVersion), ); @@ -221,14 +221,11 @@ export function getClaudeManifestModels(claudeCodeVersion?: string | null): Agen function isModelAvailableInClaudeCode( model: ClaudeModelManifestEntry, - claudeCodeVersion: string | null | undefined, + claudeCodeVersion: string | undefined, ): boolean { if (!model.minimumClaudeCodeVersion || claudeCodeVersion === undefined) { return true; } - if (claudeCodeVersion === null) { - return false; - } return compareVersions(claudeCodeVersion, model.minimumClaudeCodeVersion) >= 0; } diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index f1749cc787..48bf900884 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -17,7 +17,7 @@ const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [ "ANTHROPIC_DEFAULT_HAIKU_MODEL", ] as const; -export function getClaudeModels(claudeCodeVersion?: string | null): AgentModelDefinition[] { +export function getClaudeModels(claudeCodeVersion?: string): AgentModelDefinition[] { return getClaudeManifestModels(claudeCodeVersion); } @@ -34,7 +34,7 @@ export function findClaudeModel( export async function getClaudeModelsWithSettings( logger: Logger, configDir?: string, - claudeCodeVersion?: string | null, + claudeCodeVersion?: string, ): Promise { const hardcodedModels = getClaudeModels(claudeCodeVersion); const settingsModels = await readClaudeSettingsModels(logger, configDir);