@khalilgharbaoui/opencode-claude-code-plugin 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khalilgharbaoui/opencode-claude-code-plugin",
3
- "version": "0.37.1",
3
+ "version": "0.39.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -26,7 +26,7 @@
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
28
  "typecheck": "tsc --noEmit",
29
- "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 tsx --test test-bridge.ts test-broker.ts test-proxy-mcp.ts test-proxy-task.ts test-auto-continue.ts test-has-new-user-content.ts test-get-claude-user-message.ts test-logger.ts test-cli-args.ts test-permission-presets.ts test-session-manager.ts test-compaction-model.ts test-tool-mapping.ts test-cwd-resolution.ts test-todo-ledger.ts test-session-affinity.ts test-config-models.ts test-ask-user-question.ts test-claude-session-wrapper.ts test-spawn-env.ts test-respawn.ts test-startup-diagnostics.ts test-subagent-hint.ts test-exit-plan-mode-question.ts test-compress-tool.ts test-agent-models.ts test-side-question.ts test-btw-command.ts test-effort-sessions.ts test-tool-block-index.ts test-context-usage.ts test-skill-bridge.ts test-turn-stats.ts test-cli-events.ts test-cli-events-stream.ts test-result-fallback.ts test-doctor.ts test-configure-skill.ts test-unattended-replay.ts test-process-lifecycle.ts test-account-failover.ts test-host-tools.ts test-v2-entrypoint.ts test-v2-client.ts test-tmp-dir.ts test-cleanup-stale.ts test-account-wrapper.ts test-runtime-status-sessions.ts test-index-hooks.ts test-silent-turn.ts test-mcp-tool-result-name.ts test-model-fallback.ts test-do-generate.ts test-interactive-usage.ts test-background-subagents.ts test-interactive-result.ts test-cli-probe-cache.ts test-mcp-late-connect.ts test-prologue-abort.ts test-diagnostic-bundle.ts test-ids.ts test-session-fork.ts test-deferred-cli-tool-result.ts test-stale-build.ts test-tui-log-sink.ts",
29
+ "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 XDG_STATE_HOME=$(mktemp -d) tsx --test test/*.test.ts",
30
30
  "generate:log-messages": "tsx scripts/generate-log-messages.ts"
31
31
  },
32
32
  "dependencies": {
@@ -42,7 +42,7 @@ under its permissions instead of inside the CLI.
42
42
  This file ships with the package, so upgrading that package updates the bundled
43
43
  reference without a separate skill install. Do not copy it into a personal skill
44
44
  directory: a user override can shadow the bundled version. Match guidance to the
45
- version actually loaded, not a newer checkout. `test-configure-skill.ts` checks name
45
+ version actually loaded, not a newer checkout. `test/configure-skill.test.ts` checks name
46
46
  coverage against source declarations; it does not verify defaults or runtime
47
47
  semantics or regenerate prose. For behavior, inspect the matching version's
48
48
  `src/types.ts`, consumers in `src/index.ts` / `src/claude-code-language-model.ts`, and
@@ -112,12 +112,12 @@ Defaults below describe normal headless opencode use when the key is absent.
112
112
  |---|---|---|---|
113
113
  | `cliPath` | string | `"claude"` | Executable, not a shell command with flags. Use an absolute path for a non-PATH install. The opencode config hook supplies this default; only direct `createClaudeCode()` use falls back to `CLAUDE_CLI_PATH`. Account providers wrap it; never select a generated wrapper yourself. |
114
114
  | `accounts` | string[] | unset | Unset keeps provider `claude-code`. Any array, including `[]`, expands to `claude-code-default` plus normalized, deduplicated names. Non-default accounts use `~/.claude-<name>`; default uses the CLI's normal environment/auth. |
115
- | `accountFailover` | `"ask"` / `"off"` | `"ask"` | When the account a conversation runs on is out of usage, end the turn on opencode's native `question` form listing the other configured accounts, and continue the task on the pick inside the same opencode turn. Only ever fires with more than one account configured, so a single-account install is unaffected by the default. The pick is sticky for the LIMITED account until the limit's reset time (or until opencode restarts when the CLI reported none), so it covers every session on that account and subagents follow their parent; child sessions are never shown the form. Leaving it unanswered waits and costs nothing. `stop`, a dismissal, or text that is not one of the offered accounts ends the turn as the rate-limit error does. Triggered only by a rejected `rate_limit_event`, one of the two known account-limit error texts, or one of the five account-level failure kinds the CLI names on its own error reply (`authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `verification_required`, `billing_error`); never by a generic failure. Never on compaction turns or the interactive transport. A switch cannot resume the Claude session (transcripts live under the account's own config dir), so the conversation is replayed into a fresh one: it costs input tokens on the new account, and MCP servers configured only in the limited account's Claude profile are gone. `"off"` keeps the plain rate-limit error. |
115
+ | `accountFailover` | `"ask"` / `"off"` | `"off"` | **Opt-in: only an explicit `"ask"` opens the switch form**, so unset and `"off"` behave identically. `"ask"` ends a usage-limited turn on opencode's native `question` form listing the other configured accounts, and continues the task on the pick inside the same opencode turn. Only ever fires with more than one account configured. The pick is sticky for the LIMITED account until the limit's reset time (or until opencode restarts when the CLI reported none), so it covers every session on that account and subagents follow their parent; child sessions are never shown the form. Leaving it unanswered waits and costs nothing. `stop`, a dismissal, or text that is not one of the offered accounts ends the turn the way a limited turn ends without the form. Triggered only by a rejected `rate_limit_event`, one of the two known account-limit error texts, or one of the five account-level failure kinds the CLI names on its own error reply (`authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `verification_required`, `billing_error`); never by a generic failure. Never on compaction turns or the interactive transport. A switch cannot resume the Claude session (transcripts live under the account's own config dir), so the conversation is replayed into a fresh one: it costs input tokens on the new account, and MCP servers configured only in the limited account's Claude profile are gone. With the default `"off"`, a limited turn ends on one `▌ **usage limit:**` note instead (see "When an account runs out of usage"). |
116
116
  | `failoverAccounts` | string[] | unset/derived | Account expansion supplies the resolved account list so a limited account can offer the others. Do not hand-wire it; set `accounts` instead. |
117
117
  | `baseCliPath` | string | unset/derived | The `cliPath` before the per-account wrapper substitution, so a failover can build another account's wrapper on the same binary. Supplied by the config hook. Do not hand-wire it. |
118
118
  | `defaultSubagentModel` | string | unset | Seed-config default for discovered `mode: subagent` agents without a full `provider/model` pin; `forceModel` takes precedence. Keeps the caller's account. Unknown ids warn and keep the inherited model. Not independently read per expanded account. |
119
- | `defaultSubagentCacheTtl` | string | unset | Prompt cache TTL (`5m` / `1h`) for discovered `mode: subagent` agents that declare no `cacheTtl`; the agent's own value takes precedence. Unset leaves the CLI's default (1 hour on a subscription). Unknown values warn and change nothing. Headless spawns only (not compaction, not the interactive transport). |
120
- | `fallbackModels` | string[] | unset | Ordered models to try when the model a turn would run on is refused. Default for agents declaring no `fallbackModels`; a per-agent list replaces it rather than extending it. Same account throughout, never a switch. Armed only by the CLI refusing the model (`model_not_found`) or by a usage limit when `accountFailover` has no other account to offer; with another account the switch form wins. Entries must be registered model ids, unknown ones warn and are skipped, the current model is dropped from its own chain, each entry is tried at most once per turn, and an exhausted chain surfaces the original error. Never on compaction, title stubs or the interactive transport. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
119
+ | `defaultSubagentCacheTtl` | string | unset | Prompt cache TTL (`5m` / `1h`) for discovered `mode: subagent` agents that declare no `cacheTtl`; the agent's own value takes precedence. Unset leaves the CLI's default (1 hour on a subscription). Unknown values warn and change nothing. Headless and interactive spawns, never compaction. |
120
+ | `fallbackModels` | string[] | unset | Ordered models to try when the model a turn would run on is refused. Default for agents declaring no `fallbackModels`; a per-agent list replaces it rather than extending it. Same account throughout, never a switch. Armed only by the CLI refusing the model (`model_not_found`) or by a usage limit when the `accountFailover` form is not taking the turn, which is the case whenever it is `"off"` (its default) or has no other account to offer; with `"ask"` and another account the switch form wins. Entries must be registered model ids, unknown ones warn and are skipped, the current model is dropped from its own chain, each entry is tried at most once per turn, and an exhausted chain surfaces the original error. Never on compaction, title stubs or the interactive transport. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
121
121
  | `cwd` | string | automatic | Pin an absolute existing directory. Otherwise: session directory from SDK, usable `process.cwd()`, captured project directory, final `process.cwd()` fallback. Startup diagnostics cannot show the per-call session tier. |
122
122
  | `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to headless Claude, even with proxies enabled. Proxied calls still use opencode permissions, but unproxied CLI tools do not. `false` removes the bypass flag; it does not by itself create human approval prompts. Ignored when `permissionMode` is `"plan"`, which always drops the flag. |
123
123
  | `permissionMode` | `acceptEdits` / `auto` / `bypassPermissions` / `default` / `dontAsk` / `plan` | unset | Headless `--permission-mode`, not version-gated: verify the installed CLI supports the value. `plan` is enforced: it overrides `skipPermissions: true` and the plugin drops `--dangerously-skip-permissions` for it, so claude cannot edit or run commands. Every other value governs prompting and still passes the skip flag, so `plan` is the only one that makes a run read-only. Nothing releases plan mode mid-session (no headless `ExitPlanMode`), so leaving it means a config change and an opencode restart; the plugin warns once at startup. Not forwarded by the current interactive spawn path. |
@@ -145,11 +145,12 @@ Defaults below describe normal headless opencode use when the key is absent.
145
145
  | `idleProcessTimeoutMs` | number | unset | Kill a conversation's idle `claude` worker this many ms after a finished turn. The timer starts when a turn completes, reuse cancels it, and a worker found mid-turn when it fires is re-timed rather than killed. The session id is kept, so the next message resumes transparently. Unset or `0` keeps workers until LRU eviction (16 processes, oldest idle first). Values above `2147483647` are ignored. Not applied to the interactive transport. Deleting a chat in opencode releases its workers and session ids immediately regardless. |
146
146
  | `turnStats` | boolean | `false` | Append one `▌ **stats:**` line to each finished turn: cost, wall duration, CLI turn count, input/output/cache-read/cache-write tokens, and a permission-denial count when the turn had any, taken from the CLI's own `result`. Never on a compaction turn or a turn that ended in error. Its own text part, stripped from transcripts rebuilt for the CLI, so the model never sees it. The same numbers are logged at INFO regardless, and `modelUsage` plus `permission_denials` always reach `providerMetadata`. Reported cost is the CLI's figure, not a billing guarantee. |
147
147
  | `forkSessions` | boolean | `false` | When a new opencode session turns out to be a fork of one this provider already served, branch the parent's Claude conversation with `claude --resume <parent> --fork-session` instead of re-rendering the whole thread as text into the first message. Measured on CLI 2.1.280 with haiku 4.5 over a ~13k-token thread: 814 cache tokens written and 39,710 read, against 22,355 written and 17,385 read for the replay, so $0.0058 against $0.0467 for that turn; the parent's transcript is byte-identical afterwards. Neither opencode major tells a provider that a session is a fork, so the parent is found by matching this prompt's history against what each sibling session key was last asked to continue. Off by default because a resumed Claude conversation reuses the system prompt recorded on its FIRST request (`--system-prompt-snapshot`, default `on`), so a forked session answers under the parent's appended system prompt rather than this turn's; measured directly, a parent seeded with codename ZEBRA and forked while passing QUAIL answered ZEBRA. Falls back to the replay, unchanged, for: another account, an unknown or released parent session id, a busy parent (live process, proxied call in flight, unanswered plan-mode question), a fork cut mid-conversation, a fork taken mid tool round trip, a different cwd / model / agent / effort / prompt-cache TTL, compaction, the interactive transport, an account-failover switch, and a `claude` whose `--help` does not advertise `--fork-session`. Recording costs nothing while the option is off. |
148
+ | `resumeAfterRestart` | boolean | `true` | After an opencode restart, resume the conversation's Claude session (`--resume`) instead of replaying the thread as text. Persists session id + conversation digest per session key in `$XDG_STATE_HOME/opencode-claude-code-plugin/claude-sessions.json` (0600, 256 entries, 30 days) after each successful turn. Resumes only on the same session key and binary, with the transcript on disk and the history equal to the recorded conversation plus Claude's reply; anything else (edit, revert, compaction, account switch, open tool round trip) replays. Not on compaction. Log: `resuming the claude session from before the restart` (NOTICE) or `not resuming ...` with a reason (INFO). `false` disables reading and writing. |
148
149
  | `bridgeOpencodeSkills` | boolean | `false` | Stage the user's opencode skills for Claude's native Skill tool as `opencode-skills:<name>`, on the headless and interactive spawns (never compaction). Covers every root opencode reads: project `.opencode/`, `.claude/`, `.agents/` walking up, the opencode config dirs (`skill/` and `skills/`), and global `~/.claude/skills` and `~/.agents/skills` under opencode's own `OPENCODE_DISABLE_EXTERNAL_SKILLS` / `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` switches. Requires the CLI's `--help` to advertise `--plugin-dir`; otherwise no-op. Bridged skills are also listed in opencode's forwarded system prompt, so a large skill set costs prompt tokens twice, which is why it is off by default; `true` opts the user's skills in. Bundled skill staging ignores this option, but still requires flag support and successful discovery/staging. |
149
150
  | `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it: from `<CLAUDE_CONFIG_DIR>/skills`, the project's `.claude/skills`, or an installed plugin's `skills/`. Matched by resolved directory, by byte-identical SKILL.md, or (user/project scope only, since plugin skills are namespaced `<plugin>:<name>`) by name. A name match means `Skill("<name>")` answers from Claude's copy, not opencode's, so it is logged at WARN with both paths. The plugin scan reads `installed_plugins.json` and does not check whether the plugin is enabled. `false` bridges everything and reinstates the duplicates. |
150
- | `interactive` | boolean | unset (headless) | Experimental PTY transport; explicit boolean wins over `CLAUDE_CODE_INTERACTIVE_TRANSPORT`. Needs `Bun.Terminal`; otherwise headless fallback. Compaction stays headless. Does not wire the headless proxy server or disallowed-tools controls; no equivalent opencode permission guarantee or `/btw`. The skill bridge does apply. Never enable to bypass a billing/access restriction. |
151
+ | `interactive` | boolean | unset (headless) | Experimental PTY transport; explicit boolean wins over `CLAUDE_CODE_INTERACTIVE_TRANSPORT`. Needs `Bun.Terminal`; otherwise headless fallback. Compaction stays headless. Does not wire the headless proxy server or disallowed-tools controls; no equivalent opencode permission guarantee or `/btw`. The skill bridge, effort, prompt cache TTL and CLI hygiene env do apply. Stopping a reply sends Esc and keeps the session; a dead or evicted TUI is replaced with `--resume`; folder trust is accepted (moving off the default "No, exit"), a login or first-run screen fails the start with the fix, the usage-limit auto-continue is cancelled. Never enable to bypass a billing/access restriction. |
151
152
  | `interactiveBypass` | boolean | `false` | Deprecated no-op. The TUI asks for a manual safety confirmation on `bypassPermissions`, so the plugin never passes it. |
152
- | `interactiveAllowTools` | string[] | `["Bash", "Edit", "Write", "Read", "WebFetch"]` | With `interactive`: replaces the built-in pre-allow list. MCP wildcards from discovered bridge names plus `mcp__opencode_proxy__*` are added even with `[]`. Not a capability denylist; review permissions before enabling. |
153
+ | `interactiveAllowTools` | string[] | `["Bash", "Edit", "Write", "Read", "WebFetch"]` | With `interactive`: replaces the built-in pre-allow list. MCP wildcards from discovered bridge names plus `mcp__opencode_proxy__*` are added even with `[]`. Not a capability denylist; review permissions before enabling. A tool outside the list raises the TUI's permission dialog, which the transport denies with Esc (nobody is there to answer): the turn ends interrupted and the result's `permission_denials` names the tool. |
153
154
  | `interactiveSystemPrompt` | boolean | `true` | With `interactive`: append the plugin's own prompt. opencode's forwarded system prompt is deliberately not sent on this transport (it can trip Claude's third-party usage gate). `false` is for diagnostics only. |
154
155
  | `logging` | object | see below | File logging plus how much reaches the operator. |
155
156
  | `name` | string | unset | Low-level `createClaudeCode()` provider identity fallback after `providerID`, not the opencode display-name setting. Display name lives at `provider.<id>.name`; account expansion supplies its own label. Leave this option unset. |
@@ -193,7 +194,7 @@ their secret values. Arbitrary MCP `{env:NAME}` placeholders are outside this li
193
194
  | Variable | Effect |
194
195
  |---|---|
195
196
  | `CLAUDE_CLI_PATH` | Direct factory fallback for absent `cliPath`. Normal opencode registration supplies `"claude"`; set the option explicitly there. |
196
- | `CLAUDE_CONFIG_DIR` | CLI auth/settings/session directory. Non-default account wrappers override it; default headless account inherits it if set. Login is a user-approved interactive action, never a diagnostic probe. |
197
+ | `CLAUDE_CONFIG_DIR` | CLI auth/settings/session directory. Non-default account wrappers override it; the default account inherits it if set, on both transports. Never set it to the default `~/.claude` by hand: on CLI 2.1.288 an explicit value, even that one, makes `claude` report itself logged out. Login is a user-approved interactive action, never a diagnostic probe. |
197
198
  | `CLAUDE_CODE_EFFORT_LEVEL` | Shell-level CLI effort. Request variant/agent effort wins on a normal spawn. Compaction omits request/agent effort, but still inherits the shell env. |
198
199
  | `CLAUDE_CODE_PROMPT_CACHE_TTL` | Shell-level CLI prompt cache TTL for the main conversation. An agent's `cacheTtl` (or `defaultSubagentCacheTtl`) wins on that agent's spawn; with neither set the plugin writes nothing and the shell value, or the CLI's own default, stands. |
199
200
  | `CLAUDE_CODE_DISABLE_THINKING` | CLI-owned, conventionally `1` to disable thinking. Plugin leaves it intact and suppresses its own thinking flags/summary defaults if enabled. |
@@ -308,13 +309,49 @@ suffix and sets the config dir. Existing `CLAUDE.md`, `settings.json`, `skills/`
308
309
  missing; existing targets stay untouched. This shares capabilities/settings, not an
309
310
  isolation boundary. Auth/session files are not part of the shared list.
310
311
 
312
+ ### When an account runs out of usage
313
+
314
+ By default (`accountFailover` unset or `"off"`) a usage-limited turn ends with one
315
+ `▌ **usage limit:**` note and nothing else, on every limited turn rather than once per
316
+ process:
317
+
318
+ ```text
319
+ ▌ **usage limit:** the Claude account "work" is out of usage in the 5-hour window, which
320
+ resets at 2026-09-20 20:00. Pick a model from the "personal" or "default" account and
321
+ resend your message, or wait for the window to reset.
322
+ ```
323
+
324
+ - The reset time is **local** to the operator, to the minute, and is omitted when the
325
+ CLI reported none. The window name comes from the CLI's `rateLimitType`.
326
+ - With no other account configured the advice is "Wait for the window to reset, or
327
+ enable extra usage on the account." instead.
328
+ - It replaces the CLI's own error text for that turn: the raw sentence names no account,
329
+ and the `▌ **rate limit:**` paragraph the plugin used to write is log-only now, so a
330
+ limited turn carries exactly one block.
331
+ - Written on a child session too (a subagent that died on a limit is what the parent's
332
+ `task` result should say), but never on a compaction turn, whose text becomes the
333
+ stored summary. How the turn finishes is unchanged.
334
+ - `permission_denials`, `turnStats` and the finish reason are untouched: the note
335
+ replaces text, not control flow.
336
+
337
+ When a user says the limit message is unhelpful, check the plugin version before
338
+ anything else: this note is recent and a long-lived opencode window may predate it.
339
+
311
340
  ### Account failover
312
341
 
313
- With more than one account configured, `accountFailover` is `"ask"` by default. When a
314
- turn is rejected for usage, the turn ends on opencode's `question` form instead of an
315
- error: one option per other configured account, plus `stop`. Picking an account applies
316
- it inside the same opencode turn, with no new user message, and the task carries on.
317
- Leaving the form unanswered waits and costs nothing.
342
+ **Opt-in.** `{ "accountFailover": "ask" }` and more than one account configured makes a
343
+ usage-rejected turn end on opencode's `question` form instead of that note: one option
344
+ per other configured account, plus `stop`. Picking an account applies it inside the same
345
+ opencode turn, with no new user message, and the task carries on. Leaving the form
346
+ unanswered waits and costs nothing.
347
+
348
+ It is opt-in rather than on by default because of what was measured against a real
349
+ five-hour limit on 2026-10-03. Two of the three findings are not the plugin's to fix:
350
+ typing a message instead of picking **dismisses** the form in opencode, and that message
351
+ then runs on the still-limited account and raises a second form (the "I had to send two
352
+ messages before anything ran" report); and a switch is always a full replay. The third,
353
+ every pick being refused as `unrecognised answer` because another plugin appends a
354
+ routing tag to tool results, is fixed.
318
355
 
319
356
  Tell the user what a pick actually does before recommending one:
320
357
 
@@ -329,7 +366,7 @@ Tell the user what a pick actually does before recommending one:
329
366
  - MCP servers configured only in the limited account's Claude profile will be **missing**
330
367
  on the target account.
331
368
  - `stop`, dismissing the form, or answering with anything that is not one of the offered
332
- accounts ends the turn exactly as the rate-limit error does today. The limit is
369
+ accounts ends the turn the way a limited turn ends without the form. The limit is
333
370
  unchanged either way; failover moves the work, it does not create usage.
334
371
  - Only a rejected `rate_limit_event`, one of the two known account-limit error texts, or
335
372
  an account-level failure the CLI reports on its own error reply opens the form. The
@@ -340,10 +377,12 @@ Tell the user what a pick actually does before recommending one:
340
377
  command to fix it: `claude auth login` for the default account, or
341
378
  `CLAUDE_CONFIG_DIR=<that account's config dir> claude auth login` for a named one. When
342
379
  a user reports "Failed to authenticate: OAuth session expired", that command is the
343
- fix; a switch made from that form lasts until opencode restarts.
380
+ fix; a switch made from that form lasts until opencode restarts. With the form off and
381
+ another account configured the note ends with "Or pick a model from the
382
+ `"<account>"` account and resend." instead of offering the form.
344
383
  - Not available on the interactive transport or on compaction turns.
345
384
 
346
- `{ "accountFailover": "off" }` keeps the plain rate-limit error.
385
+ `{ "accountFailover": "off" }`, which is also the default, keeps the note.
347
386
 
348
387
  ### Subagents on one model, on the caller's account
349
388
 
@@ -426,9 +465,10 @@ Per-agent replaces provider-level; it never merges. Unset means no chain, which
426
465
  the default. Two triggers only, never a generic error: the CLI refusing the model
427
466
  (assistant `error: "model_not_found"`, or a failed result whose text is *"There's an
428
467
  issue with the selected model"*, measured on CLI 2.1.280, where the result `subtype`
429
- is misleadingly `success`), and a usage limit **only when `accountFailover` has no
430
- other account to offer**. With another account configured the switch form wins and
431
- the chain stays out of it: model is a capability choice, account is a billing choice.
468
+ is misleadingly `success`), and a usage limit **only when the `accountFailover` form is
469
+ not taking the turn**, meaning it is `"off"` (its default) or has no other account to
470
+ offer. With `"ask"` and another account configured the switch form wins and the chain
471
+ stays out of it: model is a capability choice, account is a billing choice.
432
472
  An expired login, a billing hold and every other error kind are excluded because they
433
473
  fail the same way on the next model.
434
474