fix(claude): preserve catalog on version probe failure

This commit is contained in:
Mohamed Boudra
2026-07-25 22:47:17 +02:00
parent b3bfec027f
commit 7e599db6fe
4 changed files with 36 additions and 13 deletions
@@ -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 {
@@ -364,7 +364,7 @@ interface ClaudeAgentClientOptions {
runtimeSettings?: ProviderRuntimeSettings;
queryFactory?: ClaudeQueryFactory;
resolveBinary?: () => Promise<string>;
resolveVersion?: () => Promise<string | null>;
resolveVersion?: () => Promise<string>;
configDir?: string;
}
@@ -1441,7 +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 | null>;
private readonly resolveVersion: () => Promise<string>;
private readonly configDir?: string;
constructor(options: ClaudeAgentClientOptions) {
@@ -1503,7 +1503,7 @@ export class ClaudeAgentClient implements AgentClient {
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
// 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<string | null> {
): Promise<string> {
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(
@@ -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;
}
@@ -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<AgentModelDefinition[]> {
const hardcodedModels = getClaudeModels(claudeCodeVersion);
const settingsModels = await readClaudeSettingsModels(logger, configDir);