From inside a shipping consumer Mac agent

An Anthropic model release reaches the API before it reaches your plan. Seven lines of Swift close that gap on a Mac.

Most coverage of an Anthropic model release in April 2026 recaps the announcement. This page is about the few hours after that announcement, when individual Claude.ai plans have not yet propagated access to the new alias. The function name is isModelAccessError(_:). It lives at lines 1362 to 1368 of Sources/Providers/ChatProvider.swift. Three substring-pair checks, one bridge swap, one byte-for-byte replay of the exact query the user just sent.

isModelAccessErrorretryAfterModelFallbackpendingBridgeModeSwitch = "builtin"ANTHROPIC_API_KEY env swapbuiltinCostCapUsd = $10
M
Matthew Diakonov
11 min read
4.8from Sourced directly from /Users/matthewdi/fazm: Desktop/Sources/Providers/ChatProvider.swift, Desktop/Sources/Chat/ACPBridge.swift, CHANGELOG.json release 2.0.7
Seven lines of Swift carry the entire fallback detection
ANTHROPIC_API_KEY env var is the OAuth-vs-direct-API switch
$10 builtinCostCapUsd guardrail bounds the auto-fallback

The shape of the problem

When Anthropic ships a Claude model in April 2026, the new alias appears on the API before every individual Claude.ai plan has been granted access to it. For a consumer Mac app that boots in personal-OAuth mode, that means the bridge subprocess will, for some users, get a textual agentError back from the agent SDK saying the model is not available on their account. The naive thing to do is render that error and ask the user to wait. The thing Fazm does, since release 2.0.7 on April 5, 2026, is detect that exact error shape and silently re-run the same query through the bundled Anthropic API key.

isModelAccessError(_:)may not exist + not have accessmodel + not foundmodel + not availablependingBridgeModeSwitchretryAfterModelFallbackapplyPendingBridgeModeSwitchANTHROPIC_API_KEY env swap.personalOAuth → .bundledKeybuiltinCostCapUsd = $10Fazm 2.0.7 / 2.3.2 / 2.4.0 / 2.4.1

The matcher, on disk, in seven lines

This is the function that decides whether a personal Claude plan that does not have access to the new alias triggers the bundled-key fallback. It is seven lines of Swift at /Users/matthewdi/fazm/Desktop/Sources/Providers/ChatProvider.swift lines 1362 to 1368. Three independent substring-pair checks handle the three different phrasings the agent SDK has returned over the life of the bridge.

Desktop/Sources/Providers/ChatProvider.swift (lines 1362-1368)

The fallback in motion

Anthropic flips an alias at the API. The bridge in personal mode hits an account access error. The matcher fires. The bridge defers a swap, the in-flight query unwinds, the deferred swap reboots the bridge in bundled-key mode, and the user's exact query replays.

Anthropic model release - the April 2026 fallback path

Anthropic flips a new alias
User asks a question
ACP bridge in personal mode
isModelAccessError(_:)
pendingBridgeModeSwitch = "builtin"
createBridge() in builtin mode
sendMessage(retryText) auto-retry
Floating bar streams the new alias
0 banners

ChatProvider: model access error in personal mode, falling back to builtin: model claude-opus-4-7 may not exist or you do not have access to it

Real bridge log line written by ChatProvider.swift to /tmp/fazm-dev.log on the day Anthropic flipped an Opus alias

The five conditions, all of which must hold

The catch branch is gated on five things at once. If any of them is false, the error surfaces normally and the user sees it in the chat. All five are tracked locally in ChatProvider.swift; nothing depends on a remote feature flag.

What `isModelAccessError + bridgeMode == personal` actually requires

  • agentError surfaces from the ACP bridge subprocess
  • bridgeMode is `personal`
  • Lowercased error contains one of three substring pairs
  • pendingRetryMessage is non-nil
  • errorMessage is set to nil before unwinding

The wire-level catch branch

The catch branch sits inside the agent-error switch in sendMessage(_:). The two consequential lines are the pendingBridgeModeSwitch = "builtin" deferred swap and the retryAfterModelFallback = true flag that handlePostQuery reads after the swap lands. The errorMessage = nil assignment is what suppresses the error banner on the way out.

Desktop/Sources/Providers/ChatProvider.swift (lines 2944-2974)

Why deferring the swap matters

The catch branch is still inside the do { ... } catch { ... } of an in-flight query. Calling switchBridgeMode(to:) directly there would race with the bridge subprocess exit-handler that the failed query is already triggering. The deferral hands the swap to applyPendingBridgeModeSwitch in handlePostQuery, which only runs after the failed query has fully unwound. This is the same deferral pattern used for the voice-toggle restart, so the bridge teardown is sequenced exactly once after the previous query has finished cleaning up.

The actual switch is two characters of environment

Bundled-key mode and personal-OAuth mode differ by exactly one environment variable on the bridge subprocess. In personal mode the bridge calls env.removeValue(forKey: "ANTHROPIC_API_KEY") and the agent SDK falls back to the user's stored OAuth session. In bundled-key mode the bridge writes env["ANTHROPIC_API_KEY"] = apiKey and the SDK uses Fazm's server-issued Anthropic key instead. That is the entire bridge boundary: one variable.

Desktop/Sources/Chat/ACPBridge.swift (lines 194-203, 348-353)
0lines of Swift carry the matcher
0substring-pair checks, OR-joined
$0cumulative cap on the bundled key per user
0auto-replay of the user's exact query

The April 2026 release ladder

Three Fazm releases that touch this seam, and the day Anthropic flipped the alias that fired it in production. Each one is in CHANGELOG.json with a verbatim line.

1

April 5, 2026 - Fazm 2.0.7 ships the model-access fallback itself

CHANGELOG entry, verbatim: `Fixed chat failing silently when personal Claude account lacks access to the selected model, now auto-falls back to built-in account.` That release is the one that introduced isModelAccessError(_:), the catch-branch gate, and retryAfterModelFallback. Before this build, an account-level model rollout window meant the user just saw a generic error and had to manually flip to builtin.

2

April 16, 2026 - Fazm 2.3.2 tightens the privacy language and keeps the seam

The seam survives a release that touches the same area for unrelated reasons: privacy copy in onboarding gets rewritten from `nothing leaves your device` to `local-first`. The fallback path is unchanged. The fact that this release did not perturb the seam is itself a property of the design: the matcher is independent of bridge messaging.

3

April 20, 2026 - Fazm 2.4.0 adds the dynamic models_available frame on top

Two cooperating features. The model-access fallback above. And the new dynamic models_available frame that lets newly released Claude models appear without an app update. Together they handle both halves of an Anthropic model release: the alias propagation (dynamic frame) and the plan-level access propagation (this fallback).

4

April 22, 2026 - Anthropic flips an Opus tier alias and the fallback fires in production

On the day Anthropic ships its model update, a chunk of personal-OAuth users hit the matcher; the bridge swaps to the bundled key and the auto-retry replays the user's query. The 2.4.1 release on the same day ships the partial-list label fix, but the fallback itself fires unchanged from 2.0.7.

The wire path, actor by actor

From the user's send to the new model alias rendering an answer in the floating bar, the fallback crosses five actors. The crucial step is the dashed event from ChatProvider.handlePostQuery back to itself: that is the self-replay of pendingRetryMessage.

The fallback, top to bottom

UserChatProviderACPBridge (personal)ACPBridge (builtin)Floating barsend query (text)agent call with OAuth envagentError: model may not exist…isModelAccessError(msg) → truependingBridgeModeSwitch = builtinapplyPendingBridgeModeSwitchspawn with ANTHROPIC_API_KEY=…auto-retry: sendMessage(retryText)agent call with bundled keystream response from new aliasrender answer, no error UI

The six anchor facts

Six concrete things on disk that make the fallback work. Each one is a string you can grep for in the source tree.

isModelAccessError(_:)

Seven lines at /Users/matthewdi/fazm/Desktop/Sources/Providers/ChatProvider.swift lines 1362 to 1368. Three substring-pair checks, no regex, no API. The whole detection lives in this function.

pendingBridgeModeSwitch = "builtin"

Line 2951. Defers the bridge tear-down so the in-flight query can finish unwinding before the bundled-key bridge boots.

retryAfterModelFallback = true

Line 2952. Local var read at line 2969; signals handlePostQuery to replay the exact pendingRetryMessage after the deferred switch lands.

ANTHROPIC_API_KEY env var swap

ACPBridge.swift lines 348 to 353. Two cases. .personalOAuth removes the var; .bundledKey writes it. That single env var is the entire OAuth-vs-direct-API switch on the agent SDK side.

builtinCostCapUsd = 10.0

ChatProvider.swift line 476. Static cap. After every builtin-mode query the bridge adds queryResult.costUsd to builtinCumulativeCostUsd; crossing the cap auto-switches back to personal.

auto-retry at handlePostQuery

Lines 2968 to 2974. Reads pendingRetryMessage, clears it, calls sendMessage(retryText) once. The user sees one bubble in, one answer back, no error UI.

What the fallback writes to the log on a real Mac

A real launch on April 22, 2026, on a personal-OAuth user whose plan had not yet been granted access to the freshly flipped Opus alias. Each branch logs a line, so the whole sequence is greppable in one pass on /tmp/fazm-dev.log.

/tmp/fazm-dev.log (excerpt)

What every other piece on this topic gets right, and what it skips

Most pieces stop at the model card. This row is for the people who already read the model card and want to know what happens between API rollout and per-account propagation.

FeatureMost explainersFazm runtime
What happens when Anthropic flips a model alias before your plan has access?Generic agent error shown in chat; user has to manually switch to a different model or wait for plan propagationisModelAccessError(_:) substring matcher fires, bridge swaps to bundled-key mode, exact query replays, no error UI
How is the failed-and-retried query stitched together for the user?User retypes the question, or sees a stale bubble that has to be deleted and resentpendingRetryMessage holds the original text; handlePostQuery line 2969 replays it once after applyPendingBridgeModeSwitch lands
What stops a runaway loop from spending the whole bundled API key budget?No such guardrail; either no fallback exists, or the fallback is unboundedbuiltinCostCapUsd = $10 (line 476); after each query the running total is checked, crossing the cap auto-switches back to personal
How does the catch branch know the bridge teardown is safe to perform?Either tears down mid-handler (race with the process-exit handler) or doesn't tear down at allSets pendingBridgeModeSwitch = "builtin" and lets handlePostQuery's applyPendingBridgeModeSwitch run after the in-flight query unwinds
Does the user see a banner during the rollout-window window?Red error banner with `something went wrong` until the user dismisses or retrieserrorMessage is set to nil in the catch branch; one bubble in, one answer back, no banner ever rendered

Want a walk-through of the same fallback in your own Mac app?

Book a 30-minute call. We can pull up the matcher, the deferred swap, and the auto-retry on a screenshare and trace your version of the same path.

Frequently asked questions

What actually goes wrong on a personal Claude account when Anthropic ships a new model in April 2026?

There is a window, sometimes minutes, sometimes hours, where Anthropic flips the new alias on at the API and routing layer but a given user's Claude.ai plan has not yet been granted access to it. Inside Fazm that surfaces as a specific shape of agent error: the bridge subprocess returns an `agentError` whose lowercased message contains either `may not exist` and `not have access`, or `model` and `not found`, or `model` and `not available`. Those three substring pairs are exactly what `isModelAccessError(_:)` at /Users/matthewdi/fazm/Desktop/Sources/Providers/ChatProvider.swift lines 1362 to 1368 matches. Anything else is treated as an unrelated bridge error and surfaces normally.

How does Fazm avoid showing the user a `something went wrong` message when this happens?

Look at lines 2944 to 2954 of ChatProvider.swift. The catch branch is gated on three conditions stacked with commas: `bridgeMode == "personal"`, the error is a `BridgeError.agentError(let msg)`, and `Self.isModelAccessError(msg)` returns true. When all three are satisfied the branch sets `pendingBridgeModeSwitch = "builtin"`, `retryAfterModelFallback = true`, and `errorMessage = nil`. Setting `errorMessage` to nil is what suppresses the UI banner. The user sees no failure, only a brief delay while the bridge reboots in bundled-key mode and replays the query.

Why does the swap go through `pendingBridgeModeSwitch` instead of calling switchBridgeMode directly?

Because the catch is still inside the `do { try ... } catch` of an in-flight query. Tearing down the bridge mid-handler would race with the process-exit path. The actual switch is deferred: handlePostQuery (around line 2965) calls `await applyPendingBridgeModeSwitch()`, which reads `pendingBridgeModeSwitch`, clears it, and runs `await switchBridgeMode(to: pending)`. The deferral pattern is the same one used for the voice-toggle restart, so the bridge teardown is always sequenced after the current query has finished unwinding.

What does the bundled-key bridge actually do differently?

Two characters of environment. The bridge process is restarted by `createBridge()` at ChatProvider.swift lines 492 to 507. In `personal` mode it constructs `ACPBridge(mode: .personalOAuth)` and the `case .personalOAuth` branch at /Users/matthewdi/fazm/Desktop/Sources/Chat/ACPBridge.swift lines 348 to 350 calls `env.removeValue(forKey: "ANTHROPIC_API_KEY")`, forcing the agent SDK to fall back to OAuth. In `builtin` mode it constructs `ACPBridge(mode: .bundledKey(apiKey: apiKey))` and the matching `case .bundledKey(let apiKey)` branch at line 351 to 353 sets `env["ANTHROPIC_API_KEY"] = apiKey`. That single env var is what tells the underlying `@anthropic-ai/claude-agent-sdk` to bill the call against the Fazm Anthropic key instead of the user's OAuth session.

Does Fazm replay the exact query, or does the user have to retype it?

Replays it byte-for-byte. The query text was already stored in `pendingRetryMessage` by `sendMessage(_:)` near line 2305 before the bridge call. The catch branch leaves it in place explicitly (`pendingRetryMessage is already set from sendMessage() — keep it for auto-retry`). Then handlePostQuery at lines 2968 to 2974 reads `if retryAfterModelFallback, let retryText = pendingRetryMessage`, clears the field, logs `auto-retrying query after model access fallback to builtin`, and calls `await sendMessage(retryText)`. The user sees one bubble in the chat, one response back, and the model alias on the pill is the new one Anthropic just shipped.

What stops a malicious or runaway loop from running up the bundled Anthropic API key bill?

ChatProvider.swift line 476 declares `static let builtinCostCapUsd: Double = 10.0`, and line 479 declares `@AppStorage("builtinCumulativeCostUsd") var builtinCumulativeCostUsd: Double = 0.0`. After every successful query in builtin mode, lines 2814 to 2816 add `queryResult.costUsd` to `builtinCumulativeCostUsd`, and when the running total crosses 10 dollars the bridge switches itself back to personal mode. Lines 1697 to 1701 also reseed the running total from the Firestore-backed server-side counter on launch, so the cap survives reinstalls. So the model-access fallback is bounded: it gets a user across the rollout window, not a permanent free tier.

What does this look like in `/tmp/fazm-dev.log` during a real launch on April 22 when Anthropic flipped an Opus alias?

Three log lines in sequence. First the bridge reports the model access failure: `ChatProvider: model access error in personal mode, falling back to builtin: model claude-opus-4-7 may not exist or you do not have access to it`. Second the deferred switch fires after handlePostQuery runs: `ChatProvider: applying deferred bridge mode switch to builtin` followed by `ChatProvider: switching bridge mode to builtin (current stored: personal)`. Third the auto-retry: `ChatProvider: auto-retrying query after model access fallback to builtin`. The whole sequence is in the log because every branch logs its own line, so it is easy to grep on a user's machine.

Why three substring pairs instead of one regex?

Anthropic's API and the various agent SDKs do not return a stable structured error code for `account does not have access to this model`. The text is human-readable and has shifted at least three times: an older surface returned `the model may not exist or you may not have access to it`, an SDK wrapper at one point returned `model not found`, and the most recent shape the bridge has observed is `model is not available on your account`. A single regex would miss the next phrasing on the next rename. Three independent substring pairs are what makes the matcher robust to the wording without ever needing an app update; it just has to remain `[some prefix] (may not exist + not have access | model + not found | model + not available)`.

Does this work for free trial users, paid users, or both?

Both, with a different ceiling. Free trial users hit the bundled key by default (the bridge boots in builtin mode), and the 10 dollar cap is what defines their trial budget. Paid Pro users on personal OAuth mode are the population this fallback was added for: they expect to use their own Claude.ai plan on the new model, and when the rollout window is open they get auto-routed to the bundled key for the duration of that query, with the 10 dollar cap as the bound. Once Anthropic propagates access to their personal plan, the next manual switch (or the next bridge restart) puts them back on personal OAuth; nothing in the auto-fallback path is sticky across launches.

Could a future Anthropic model release break this seam, and what would the warning sign look like?

Two break modes. First, if Anthropic returns the no-access error using a phrase that does not contain any of the three substring pairs, the matcher falls through and the user sees a generic agent error in the chat instead of an automatic fallback. The fix is a single line added to `isModelAccessError(_:)` and a Sparkle update; that is the warning sign. Second, if a future Anthropic release retires the bundled-key path entirely, `KeyService.shared.anthropicAPIKey` would return nil and `createBridge()` line 500 would log `No bundled key available, falling back to personal OAuth`, which means the fallback target itself is no longer available. That has not happened in any release through April 27, 2026.