From inside a shipping consumer Mac agent

On Opus 4.7 GA day, the alias renamed. Seven lines of Swift kept every user's pill labelled Smart.

April 22, 2026 was the day Anthropic flipped Claude Opus 4.7 to GA. Same day, the agent SDK started reporting the Opus tier under the alias default instead of any string containing opus. Inside Fazm, a consumer Mac app, every user who had ever selected the Smart pill had the legacy string written into UserDefaults. This guide traces the seven-line Swift function that silently rewrote the persisted preference on the next launch, and the one-line CHANGELOG entry that documented it four days later.

normalizeModelId(_:)shortcut_selectedModelACP SDK v0.29+Fazm 2.4.2 (2026-04-26)
M
Matthew Diakonov
11 min read
4.7from Sourced directly from /Users/matthewdi/fazm: ShortcutSettings.swift, ChatProvider.swift, ACPBridge.swift, acp-bridge/src/index.ts, CHANGELOG.json
Single Swift function (normalizeModelId) plus a four-row family map
Migration runs on the first models_available frame after launch
2.4.2 changelog literally names the legacy string and the new alias

The short version

A new Claude model release does not just land as a number on a benchmark. Inside a consumer Mac app, it lands as a JSON frame from the agent bridge whose contents may not match the strings every existing user has saved on disk. On April 22, 2026, the @anthropic-ai/claude-agent-sdk packaged inside Fazm's acp-bridge began reporting the Opus tier under the alias default. Every user who had ever picked the Smart pill had the literal string opus persisted in UserDefaults at shortcut_selectedModel. The picker stopped finding a match. The Smart pill quietly fell back to whatever the SDK could resolve next, which on most launches was Sonnet. Fazm 2.4.2, dated 2026-04-26, fixed it with a single CHANGELOG line and a single Swift function. Most articles about this Claude release ranked the model on benchmarks. This one walks through the seven lines that kept the consumer surface stable across the rename.

Claude Opus 4.7 GAACP SDK v0.29.2modelId defaultshortcut_selectedModelnormalizeModelId(_:)selectedModelShortLabelmodelFamilyMap (4 rows)models_available frameChatProvider.setModelsAvailableHandlerFazm 2.4.0 / 2.4.1 / 2.4.2April 20 / 22 / 26

The seven-line migration in source

Three substring branches, one fall-through return. The comment above the function is unusually direct about why the third branch exists.

ShortcutSettings.swift

The doc comment ACP SDK v0.29+ uses default for Opus 4.7; migrate stored opus to match is rare in the codebase: it names the upstream SDK version, the new alias string, and the legacy persisted value, all in one breath. It is the kind of line you write when you know the next person reading it might be a future you trying to remember why a public alias is the word default rather than a number.

Where a fresh model list flows the moment the bridge reports it

ACP SDK v0.29+
acp-bridge
ACPBridge.swift
ShortcutSettings.updateModels
normalizeModelId
selectedModelShortLabel
set_model RPC
UserDefaults

The upgrade ladder, in order

The migration does not just check the new list. It runs three attempts in order, and each one is a fall-through. Exact match first. Normalized alias second. Substring upgrade third. None of them block the user.

ShortcutSettings.swift

The third rung, the substring upgrade, is the one that survives the next rename. If Anthropic ever ships an Opus 4.8 under a versioned id like claude-opus-4-8, a user with the persisted string opus gets matched into it because the new id contains the old substring. The log line records the upgrade so you can see it happen in the bridge stderr stream.

7 lines

Fixed Smart (Opus) model preference not persisting after app update - now correctly maps stored 'opus' to the new ACP model ID.

Fazm 2.4.2 release notes, /Users/matthewdi/fazm/CHANGELOG.json, dated 2026-04-26

What the picker did across the four-day window

Users who picked Smart before April 22 had the literal string 'opus' written into UserDefaults at the key shortcut_selectedModel. After Anthropic flipped Opus 4.7 to GA and the ACP SDK started reporting the tier under 'default', the picker's availableModels list no longer contained any entry whose id was 'opus'. The Smart pill silently fell back to whatever the picker resolved next.

  • stored shortcut_selectedModel = 'opus'
  • availableModels = [haiku-..., sonnet-..., default]
  • no exact match; silent downgrade

The four-row table behind the label

The migration alone is not enough. Even after a Smart user is rewritten from opus to default, the floating bar still has to render the right word on the pill. That happens through the static modelFamilyMap declared above the function. Two of its four rows point at the same display label, deliberately.

ShortcutSettings.swift

The two rows for opus and default share the same short label (Smart), the same family name (Opus), and the same display order (2). That is what holds the pill stable across a rename. The duplication is intentional. It is the visible side of the same migration that the seven-line function does on the persistence side.

Three numbers behind the migration

0

Lines of Swift in the migration function

0

Days between Opus 4.7 GA and Fazm 2.4.2

0

Pills on the floating bar (Scary, Fast, Smart)

0

Lines in the 2.4.2 CHANGELOG entry that fixed it

The seven-line function is short on purpose. The four-day gap is the cost of the migration not being part of the original 2.4.0 release. The three-pill design is what makes the rename invisible on the consumer surface, because the family map collapses every alias of the same tier into one word.

How three Fazm releases responded to one Claude flip

The story is not in any one release. It is in the ordering of three releases across six days, where each one closes a gap the previous one opened.

Fazm 2.4.0 (April 20)

Available AI models populate dynamically from the agent SDK. Static array out, JSON-RPC frame in. The picker can absorb a new Claude release without an App Store push.

Fazm 2.4.1 (April 22)

Same day Anthropic flips Opus 4.7 to GA. Sonnet users on a partial model list start seeing the wrong label. Fazm 2.4.1 patches shortLabel(for:) to fall back to Fast, not Smart.

Fazm 2.4.2 (April 26)

The legacy 'opus' string in UserDefaults is silently rewritten to 'default'. One-line changelog, seven-line Swift function, zero settings dialogs.

ACP SDK v0.29+

Agent SDK now uses 'default' as a stable handle for the live Opus tier. Lets Anthropic swap weights from 4.5 to 4.6 to 4.7 to 4.8 without breaking downstream apps; downstream apps in turn must migrate their persisted IDs once.

modelFamilyMap (4 rows)

Two rows, ('opus', 'Smart', 'Opus', 2) and ('default', 'Smart', 'Opus', 2), share the same display label and the same display order. That duplication is what keeps the pill rendering 'Smart' across the rename.

selectedModelShortLabel

Computed property that falls back to 'Fast' rather than 'Smart' when the model is unknown. The 2.4.1 fix changed the exact word a Sonnet user saw on a partial-list launch.

What the bridge log looks like across the migration

Fazm bridge stderr captured on a launch immediately after upgrading from 2.4.1 to 2.4.2

The third line is the verbatim emit from acp-bridge/src/index.ts line 1280. The fifth line is the log statement at ShortcutSettings.swift line 200. The seventh line is the log statement at line 207. The pill on the user's screen never changed. The persisted value did, exactly once, the first time the new bridge spoke up.

The April 2026 timeline as it actually unfolded

Five dated events across six days. Three of them are consumer-app shipping events. One is an upstream agent SDK rename. The fifth is the part that generalises beyond this Claude release.

1

April 20, 2026 - Fazm 2.4.0 ships dynamic model list

The picker stops being a static array baked into the binary. ChatProvider.swift line 1016 wires the bridge's models_available frame to ShortcutSettings.updateModels. The agent SDK is bumped to v0.29.2. Newly released Claude models can now appear in the picker without an App Store push.

2

April 22, 2026 - Anthropic flips Opus 4.7 to GA, ACP rename ships

Inside @anthropic-ai/claude-agent-sdk v0.29+, the Opus tier is now reported under the modelId 'default'. Users with the legacy persisted 'opus' value in UserDefaults stop seeing 'Smart' on the floating bar; the picker silently falls back to a model whose ID does match. Same day, Fazm 2.4.1 patches selectedModelShortLabel to fall back to 'Fast' rather than 'Smart' on a partial model list.

3

April 22-26, 2026 - the four-day gap

Between the GA flip and 2.4.2, Smart-preferring users land on Sonnet on launch unless they re-pick the pill manually. The agent itself still serves the right model when the user clicks Smart explicitly; the bug is only in the persisted preference round-trip after a launch.

4

April 26, 2026 - Fazm 2.4.2 ships normalizeModelId(_:)

The seven-line Swift function lands. The 2.4.2 CHANGELOG entry reads verbatim: 'Fixed Smart (Opus) model preference not persisting after app update - now correctly maps stored opus to the new ACP model ID.' On the next launch after the update, every Smart-preferring user is silently migrated from 'opus' to 'default'. The pill label, the persisted value, and the model behind the API call are all back in sync.

5

Beyond April 2026 - what generalises

The substring-match upgrade fallback at lines 208 to 211 is the part that survives the next rename. If a future Claude release reports the Opus tier under, say, 'opus-4-8' or 'default-2', the existing code picks it up without a code change. The seam is fragile only to an entirely new tier label outside Haiku, Sonnet, Opus.

Two ways to react when an agent SDK renames a model alias

One requires a release per alias. The other is a seven-line Swift function and a duplicated row in a static table.

FeatureStatic array baked into a releaseFazm (normalizeModelId + family map)
Adding a brand-new Claude model to a shipping consumer Mac appUpdate a static array of model IDs in source, ship a build, push to App Store review, wait for users to updateBridge subprocess emits the live list from the agent SDK; Swift handler ingests it on the next session/new RPC
Surviving an ACP SDK alias rename mid-flightStored preference falls out of the new list; picker silently downgrades; user sees the wrong label and the wrong model on launchnormalizeModelId rewrites the legacy substring to the new alias; pill label and model both stay correct, no settings dialog
Partial model list from the SDK (Sonnet rate-limited)Picker label collapses to a default of 'Smart' even for Sonnet usersselectedModelShortLabel falls through availableModels, defaultModels, normalized alias, then gives up to 'Fast'
Where the user picks the modelDrop-down listing every model ID for every releaseThree pills (Scary, Fast, Smart) at Cmd+Shift+Space; family map collapses every alias of the same tier into one pill
What ships with each Claude releaseA binary update per supported modelA four-day gap, fixed by a seven-line Swift function and a one-line CHANGELOG entry

Why the consumer surface stayed three pills wide

The floating bar at Cmd+Shift+Space shows three labels: Scary, Fast, Smart. They map to Anthropic's Haiku, Sonnet, and Opus tiers respectively, regardless of which specific version is currently behind each tier. The modelFamilyMap collapses every alias of the same family into one row of the same display label, on purpose. That is what made the April 22 rename a non-event for almost every user: the pill they had selected stayed the same word, the pill said the same thing the next morning, the same agent loop ran behind it. The migration function and the duplicated row in the family map are the two halves of why a consumer app does not need to ship a release every time the agent SDK renames an alias.

What this guide is not

This is not a benchmark roundup. It does not compare Opus 4.7 to GPT-6 on agentic harnesses, and it does not score DeepSeek V4 against Qwen 3.5-Omni on tool use. The published articles about the April 2026 Claude release that already exist do that part well. What the existing articles do not do is walk through the part of a consumer app that has to react when the alias underneath those benchmarks gets renamed in production. That is the part where a stored string in UserDefaults silently goes stale, where a pill that used to say Smart stops saying Smart, and where a seven-line Swift function decides whether the next morning's launch is a non-event or a support ticket.

Want to see normalizeModelId run on a real Mac?

Walk through the seven-line Swift function, the four-row family map, and the bridge log on a 20-minute call.

Frequently asked questions

What actually broke between Fazm 2.4.0 and Fazm 2.4.2 in April 2026, and which model release caused it?

When Anthropic flipped Claude Opus 4.7 to GA on April 22, 2026, the @anthropic-ai/claude-agent-sdk packaged inside Fazm's acp-bridge began reporting the Opus tier under the modelId 'default' instead of a versioned ID containing the substring 'opus'. Users who had previously chosen 'Smart (Opus, latest)' had the literal string 'opus' written into UserDefaults at the key shortcut_selectedModel. After the SDK rename, the picker's availableModels array no longer contained any entry whose id was 'opus'. The Fazm 2.4.1 changelog records the visible symptom: the floating bar would render the Sonnet label and treat the user as if they had picked Fast. Fazm 2.4.2, dated 2026-04-26, ships a one-line CHANGELOG entry that resolves it: 'Fixed Smart (Opus) model preference not persisting after app update - now correctly maps stored "opus" to the new ACP model ID.'

Where is the migration function in the source, and how long is it?

It is a seven-line Swift function called normalizeModelId(_:) at /Users/matthewdi/fazm/Desktop/Sources/FloatingControlBar/ShortcutSettings.swift lines 170 to 177. The body is three guarded substring checks: 'if modelId.contains("haiku") { return "haiku" }', 'if modelId.contains("sonnet") { return "sonnet" }', and 'if modelId.contains("opus") { return "default" }'. The third branch is the migration that the 2.4.2 changelog refers to. The doc comment above it spells out the rationale verbatim: 'ACP SDK v0.29+ uses default for Opus 4.7; migrate stored opus to match.' The function returns the original modelId unchanged if none of the three substrings match.

How does that function actually run on launch, and where does it fire?

It is called from updateModels(_:) in the same file, lines 180 to 216, which is the handler the floating bar registers with the agent bridge. The flow is three hops. First, /Users/matthewdi/fazm/Desktop/Sources/Providers/ChatProvider.swift line 1016 calls 'await acpBridge.setModelsAvailableHandler { models in Task { @MainActor in ShortcutSettings.shared.updateModels(models) } }'. Second, /Users/matthewdi/fazm/Desktop/Sources/Chat/ACPBridge.swift line 1131 dispatches the JSON-RPC frame typed 'models_available' from the bridge subprocess, parses it, and at line 1211 calls onModelsAvailable. Third, updateModels iterates the new list, and for each modelId checks whether the user's currently selected model is missing. If it is, the function calls normalizeModelId on the stored value, and if the normalized value matches a row in the new list, it overwrites selectedModel in UserDefaults. The migration runs every time a fresh model list arrives, which on a typical user happens within roughly two seconds of app launch.

Why did Anthropic rename the alias to 'default' instead of leaving 'opus' alone?

Inside the Claude agent SDK, 'default' is a stable handle that always resolves to whichever Opus tier is currently the highest-quality production model on the account. The acp-bridge runtime inside Fazm at /Users/matthewdi/fazm/acp-bridge/src/index.ts line 1245 declares 'const DEFAULT_MODEL = "claude-sonnet-4-6"', then at line 1271 emits an availableModels payload that includes the SDK-reported aliases. The bridge filters out the literal 'default' pseudo-model at line 1274 only when computing the user-visible picker, but the underlying payload from the SDK on April 22, 2026 carried the new alias for the Opus tier. Stable aliases let the SDK swap underlying weights from Opus 4.5 to Opus 4.6 to Opus 4.7 without forcing every consumer app to ship a release per swap. The cost of that decision is that any consumer app holding a versioned legacy ID like 'opus' in its own user defaults has to migrate it on first contact, which is exactly what the seven-line function does.

What is the rest of the upgrade ladder inside updateModels(_:)?

Lines 203 to 213 of ShortcutSettings.swift implement the upgrade fallback. If the stored selectedModel is not present in the new list, the code first tries the normalized alias from normalizeModelId. If the normalized value is in the list, it overwrites selectedModel and logs 'ShortcutSettings: normalized selectedModel to ' followed by the new value. If the normalized value is also missing, the code searches the new list for any modelId whose substring matches the normalized form, and if it finds one, overwrites selectedModel with that and logs 'ShortcutSettings: upgraded selectedModel ' followed by the old and new values. If nothing matches, it logs 'ShortcutSettings: current selectedModel ' followed by 'not in new model list' and leaves the value alone, so the picker shows the legacy label until the next session reports a fresher list.

Why does the floating bar still show the right label when only two of three pills come back from the SDK?

Because of a related fix shipped in Fazm 2.4.1 on April 22, 2026. The 2.4.1 CHANGELOG entry is verbatim: 'Fixed model label showing "Smart" for Sonnet users when Anthropic reports a partial model list.' The function that does this is shortLabel(for:) at ShortcutSettings.swift lines 222 to 229. It tries availableModels first, then defaultModels, then the normalized alias against defaultModels, and only then gives up. The selectedModelShortLabel computed property at lines 232 to 234 calls into it and falls back to 'Fast' rather than to 'Smart', which is the exact bug the 2.4.1 changelog fixed. So when the SDK is rate-limited and only reports Haiku and Opus on a given launch, a user who selected Sonnet still sees Fast on the pill, not Smart.

How does normalizeModelId interact with the four-row substring table that labels the model pills?

The substring table is the static modelFamilyMap at lines 159 to 164 of ShortcutSettings.swift. It contains four rows: ('haiku', 'Scary', 'Haiku', 0), ('sonnet', 'Fast', 'Sonnet', 1), ('opus', 'Smart', 'Opus', 2), and ('default', 'Smart', 'Opus', 2). The two latter rows are why the picker still labels the new alias 'Smart (Opus, latest)' even though its id is now the literal string 'default'. The migration in normalizeModelId rewrites the persisted preference; the duplicate row in modelFamilyMap rewrites the rendered label. Together they keep the user's pill stable across the rename: same word on the pill, same model behind it, no surprise.

What did Fazm 2.4.0 ship on April 20 that made the seven-line migration possible at all?

Fazm 2.4.0, dated 2026-04-20, ships the upstream piece. The 2.4.0 CHANGELOG entry reads verbatim: 'Available AI models now populate dynamically from the agent, so newly released Claude models appear without an app update.' That release also bumped the agent protocol: 'Upgraded Claude agent protocol to v0.29.2.' Before 2.4.0, the picker's availableModels was a static array baked into the app binary; the only way a new Claude model could appear in the picker was an App Store release. After 2.4.0, the bridge subprocess reports the live list from the agent SDK, and the Swift updateModels(_:) handler ingests it. That meant Opus 4.7 was reachable from the picker on April 22 with zero binary churn, but it also exposed the legacy 'opus' UserDefaults string to the new alias. Hence 2.4.2 four days later.

What does this look like in the actual log output?

On a fresh launch after April 22, the bridge subprocess writes 'Raw models from ACP SDK:' followed by a JSON array containing entries with modelId 'haiku-...', 'sonnet-...', and 'default'. The Swift side then writes 'ShortcutSettings: updated availableModels to [haiku-... = Scary (Haiku, latest), sonnet-... = Fast (Sonnet, latest), default = Smart (Opus, latest)]'. If the user had previously selected the legacy id 'opus', the next log line is 'ShortcutSettings: normalized selectedModel to default'. From the user's point of view, the floating bar opened, the pill said Smart, and a Cmd+Shift+Space query routed through Opus 4.7. Nothing else moved.

Could this same migration pattern handle the next Claude rename, or does each one need a code change?

Each rename inside an existing tier (Opus, Sonnet, Haiku) is automatically picked up because the upgrade fallback at lines 203 to 213 does a substring match against the new list, not an exact equality check. If the SDK ever reports the next Opus under a modelId containing 'opus' again, the existing code path picks it up without any change. If the next tier comes back under a brand-new alias unrelated to any of the three substrings, the modelFamilyMap at lines 159 to 164 has to grow a fifth row, but the pill design at the floating bar (Scary, Fast, Smart) is intentionally fixed at three entries. The picker hides anything outside the family map behind the 'unknown model family: use the API name directly' branch at lines 189 to 191. So the seam designed in April 2026 is robust to model renames inside the existing tiers; it is fragile only to an entirely new tier label, which is itself a different design conversation.