fix(runner): classify Claude's rejected selected model as an unavailable model

claude-agent-acp passes the agent's model to Claude Code at session start
and lists it, so ACPX admission accepts an unknown model. The first turn
then ends with an AIR request failure titled "There's an issue with the
selected model (<id>). It may not exist or you may not have access to
it." The runner reported it as an unknown provider failure, the server
saw native_provider_terminal_failed, and the task stopped with
provider_effect_inventory_unavailable instead of asking for a model.

Classify that Claude title as model_unavailable (requires action), the
category the native runtime already turns into
native_provider_model_rejected.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DottaandClaude committed 2026-11-10 03:51:04 -06:00
1 parent 6b2142eb00
commit e552b2445d
2 files changed
+31 -1

No files matched your search

@@ -48,6 +48,18 @@ function sessionLimitCategory(failure: AcpxTerminalSessionFailure | undefined):
return undefined; return undefined;
} }
/**
* Claude Code reports a model it cannot run (unknown, retired, or not on the
* account) as an AIR `request` failure with this fixed title. The user must
* choose another model, so it is an unavailable model, not a generic request.
*/
const CLAUDE_SELECTED_MODEL_REJECTED =
/^There's an issue with the selected model \([^()\n]{1,200}\)\. It may not exist or you may not have access to it\./;
function sessionModelCategory(agent: string, failure: AcpxTerminalSessionFailure | undefined): ProviderFailure["category"] | undefined {
return agent === "claude" && failure?.category === "request" && typeof failure.title === "string" &&
CLAUDE_SELECTED_MODEL_REJECTED.test(failure.title.trim()) ? "model_unavailable" : undefined;
}
/** /**
* `ownMessage` keeps an error's own message when it carries no ACP error. Use * `ownMessage` keeps an error's own message when it carries no ACP error. Use
* it only where every rejection belongs to the provider: a failed terminal * it only where every rejection belongs to the provider: a failed terminal
@@ -72,7 +84,8 @@ export function acpxProviderFailure(error: unknown, agent: string, phase: Provid
const code = detail.type ?? detail.code ?? data.errorType ?? data.errorKind; const code = detail.type ?? detail.code ?? data.errorType ?? data.errorKind;
const category = (typeof code === "string" ? CATEGORIES.get(code) : undefined) const category = (typeof code === "string" ? CATEGORIES.get(code) : undefined)
?? (acp?.code === ACP_AUTH_REQUIRED ? "authentication" : undefined) ?? (acp?.code === ACP_AUTH_REQUIRED ? "authentication" : undefined)
?? sessionLimitCategory(sessionFailure); ?? sessionLimitCategory(sessionFailure)
?? sessionModelCategory(agent, sessionFailure);
return createProviderFailure({ provider: agent === "claude" ? "anthropic" : agent === "codex" ? "openai" : agent, return createProviderFailure({ provider: agent === "claude" ? "anthropic" : agent === "codex" ? "openai" : agent,
phase, message, httpStatus: status, providerCode: code, requestId: data.request_id, category, environment }); phase, message, httpStatus: status, providerCode: code, requestId: data.request_id, category, environment });
} }
@@ -79,6 +79,23 @@ describe("provider failure boundary", () => {
.toMatchObject({ category: "rate_limit", message: `${generic}\nClaude is temporarily rate limited.` }); .toMatchObject({ category: "rate_limit", message: `${generic}\nClaude is temporarily rate limited.` });
}); });
it("classifies Claude's rejection of the selected model as an unavailable model", () => {
const generic = "ACP agent reported a terminal request failure.";
const failed = { status: "failed", error: { code: "RUNTIME", message: generic } };
// The AIR sessionFailure claude-agent-acp 0.73.0 returns for an unknown model (live probe, 2026-11-10).
const title = "There's an issue with the selected model (chaos-missing-model-abc12345). It may not exist or you may not have access to it. Run --model to pick a different model.";
expect(acpxTerminalResult(failed, "claude", {}, { category: "request", title })).toMatchObject({ status: "failed", error: { providerFailure: {
provider: "anthropic", phase: "turn", category: "model_unavailable", retryability: "requires_action", message: `${generic}\n${title}` } } });
// The thrown form of the same terminal is classified the same way.
expect(acpxProviderFailure(new Error(generic), "claude", "turn", {}, false, { category: "request", title }))
.toMatchObject({ category: "model_unavailable", retryability: "requires_action" });
// Another category, another agent, or another request failure stays unknown.
for (const [agent, category, text] of [["claude", "service", title], ["codex", "request", title],
["claude", "request", "Claude could not process this request."]] as const) {
expect(acpxTerminalResult(failed, agent, {}, { category, title: text })).toMatchObject({ error: { providerFailure: { category: "unknown" } } });
}
});
it("keeps the ACPX terminal cause when the raw result exceeds its payload limit", () => { it("keeps the ACPX terminal cause when the raw result exceeds its payload limit", () => {
const terminal = acpxTerminalResult({ status: "failed", error: { ...acp, const terminal = acpxTerminalResult({ status: "failed", error: { ...acp,
data: { ...acp.data, responseBody: "private-body".repeat(7000) } } }, "claude"); data: { ...acp.data, responseBody: "private-body".repeat(7000) } } }, "claude");