From 829dafc2e7cbf406d521fc946fe5cd2c2d888e1d Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Thu, 10 Sep 2026 16:54:45 -0700 Subject: [PATCH] refactor(claude): keep the model pre-flight's permissive case in the authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claudeCatalogAdmitsModel now answers the question outright instead of handing back a nullable id set the caller had to interpret. The rule that an unidentified catalog refuses nothing lives inside the function, so a second caller cannot get it wrong by omission — and getting it wrong is silent, because restore swallows the rejection into restoreSkippedOptions. The refusal message names the model the user asked for, since it reaches them as the chat error row. --- src/main/claude/claude-structured-options.ts | 9 +++---- .../claude-structured-session-options.ts | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) 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(