fix: share one model resolver between the daemon and the web client - #316
Merged
Conversation
`/model opus` was rejected in the web UI as an unknown model while the daemon behind it resolved the very same input fine. Model-value resolution existed twice: `resolveAgainstList` in src/daemon/models.ts and `resolveModelInput` in web/src/state/models.ts, the latter a hand-maintained mirror. They drifted. #315 taught the daemon copy to match a bare alias against a variant-suffixed value (the backend reports `opus[1m]` / "Opus (1M context)", so a typed `opus` matches neither field verbatim) and the mirror kept the old rule — so the client rejected the input before the request was ever sent. Patching the mirror would leave two copies and the same drift. The resolver moves to @highflame/codeoid-core instead, which already exists for exactly this (see the re-export shims in web/src/lib/{approvals,identity,format}.ts): packages/core/src/models.ts — the one implementation src/daemon/models.ts — re-exports it, 37 lines of duplicate deleted web/src/state/models.ts — resolveModelInput delegates to it Behaviour is unchanged on the daemon side and now matches it on the client. The web client keeps its own pre-catalog passthrough: with an empty list the trimmed input goes through untouched so typing isn't blocked before the fetch lands, and the daemon stays the backstop validator. Coverage moves to packages/core alongside the code, with the live payload from claude-agent-sdk 0.3.258 as the fixture, plus a web-side regression test that drives the real symptom through fetchModels -> resolveModelInput. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
abhijitjavelin
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
/model opusis rejected in the web UI as an unknown model, while the daemon behind it resolves the same input fine.Cause
Model-value resolution existed twice:
resolveAgainstListsrc/daemon/models.tsresolveModelInputweb/src/state/models.ts— a hand-maintained mirrorThey drifted. #315 taught the daemon copy to match a bare alias against a variant-suffixed value — the backend reports
{"value": "opus[1m]", "displayName": "Opus (1M context)"}, so a typedopusmatches neither field verbatim. The mirror kept the old rule, so the client returnednulland reported an unknown model before the request was ever sent.Change
Patching the mirror would leave two copies and the same drift. The resolver moves into
@highflame/codeoid-core, which already exists for daemon/frontend shared logic — the re-export shims inweb/src/lib/{approvals,identity,format}.tsare the established pattern.Daemon behaviour is unchanged; the client now matches it. The web client keeps its own pre-catalog passthrough — with an empty list the trimmed input goes through untouched so typing isn't blocked before the fetch lands, and the daemon remains the backstop validator either way.
Verification
packages/core+ daemon model tests — 52 passbun run typecheck(root + protocol + core) andcd web && tsc --noEmit— cleanbun run lint— cleanCoverage moves to
packages/corealongside the code, using the livesupportedModels()payload from claude-agent-sdk 0.3.258 as the fixture, plus a web-side regression test that drives the real symptom end-to-end throughfetchModels→resolveModelInput.Notes for the reviewer
opt.valuebeneath the display name (SessionControls.tsx, unchanged since July), so Fable 5 vs 5.1 is already distinguishable there — the backend's display name is just"Fable"for both. No UI change was needed and none is included.web/distis served at/uiand is built separately, so a daemon restart does not pick up frontend changes — this fix needsbun run build:webto reach a browser.🤖 Generated with Claude Code