diff --git a/src/main/claude/claude-structured-options.ts b/src/main/claude/claude-structured-options.ts index 045742aeea8..7f607755563 100644 --- a/src/main/claude/claude-structured-options.ts +++ b/src/main/claude/claude-structured-options.ts @@ -5,8 +5,8 @@ import { isAgentSessionOptionRejectedError } from '../native-chat/agent-session-wire/structured-agent-session-option-error' import { + claudeCatalogAdmitsModel, readClaudeCurrentModel, - readClaudeListedModelIds, readClaudeModelEffortLevels, readClaudeSettingsEffort } from './claude-structured-session-options' @@ -71,11 +71,8 @@ export async function setClaudeStructuredOption( // fails every turn with zero tokens, so the acceptance proves nothing and only // the catalog does. Restore replays a pick the provider may since have retired, // which reaches here with no user error at all. - if (input.key === 'model') { - const listed = await readClaudeListedModelIds(session, timeoutMs) - if (listed && !listed.has(input.value)) { - throw new AgentSessionOptionRejectedError(`claude does not offer model ${input.value}`) - } + if (input.key === 'model' && !(await claudeCatalogAdmitsModel(session, input.value, timeoutMs))) { + throw new AgentSessionOptionRejectedError(`claude does not list a model named ${input.value}`) } const modelWasConfirmed = readClaudeCurrentModel(session).confirmed const mutationSequence = ++session.optionMutationSequence diff --git a/src/main/claude/claude-structured-session-options.ts b/src/main/claude/claude-structured-session-options.ts index 5dbdcf4d535..2385f362c2a 100644 --- a/src/main/claude/claude-structured-session-options.ts +++ b/src/main/claude/claude-structured-session-options.ts @@ -150,24 +150,25 @@ export async function readClaudeModelEffortLevels( } /** - * Every id the catalog answers to — alias and resolved id alike, so a pick stored - * as either one matches. Null when nothing identified a model: an absent, failed - * or empty `list_models` is not evidence against a model, or a live CLI that - * predates it would have every model refused under it. + * Whether the catalog admits the model, matched by alias or resolved id so a pick + * stored as either one is found. The permissive case lives here rather than at the + * call site: every caller must treat an unidentified catalog the same way, and one + * that forgot would refuse every model on a CLI that cannot answer. */ -export async function readClaudeListedModelIds( +export async function claudeCatalogAdmitsModel( session: ClaudeSession, + modelId: string, timeoutMs: number | undefined -): Promise | null> { +): Promise { const catalog = await session.connection.supportedModels({ timeoutMs }).catch(() => null) const models = listedModels(catalog ? { models: catalog } : null) - return models.length > 0 - ? new Set( - models.flatMap((model) => - model.resolvedModel ? [model.id, model.resolvedModel] : [model.id] - ) - ) - : null + // An empty list identifies no model, so it is not evidence against one — a live + // CLI predating `list_models` would otherwise have every model refused under it. + // Do not turn this into a refusal. + return ( + models.length === 0 || + models.some((model) => model.id === modelId || model.resolvedModel === modelId) + ) } export async function readClaudeStructuredSessionOptions(