refactor(claude): keep the model pre-flight's permissive case in the authority

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.
This commit is contained in:
Merge Sim
2026-09-10 16:54:45 -07:00
parent a458f73743
commit 829dafc2e7
2 changed files with 17 additions and 19 deletions
+3 -6
View File
@@ -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
@@ -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<ReadonlySet<string> | null> {
): Promise<boolean> {
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(