Skip to content

fix: model catalog follows the session's backend (web)#160

Merged
saucam merged 2 commits into
mainfrom
fix/model-catalog-provider-switch
Jul 12, 2026
Merged

fix: model catalog follows the session's backend (web)#160
saucam merged 2 commits into
mainfrom
fix/model-catalog-provider-switch

Conversation

@saucam

@saucam saucam commented Jul 12, 2026

Copy link
Copy Markdown
Owner

The bug you hit: switching a session's backend didn't refresh the model catalogue — the picker kept showing the previous backend's models.

Root cause (two compounding issues)

  1. The web client never told the daemon which backend's models it wanted. fetchModels sent {type:"models.list"} with no provider field, so the daemon always answered for its default backend (claude), and the result was cached globally and never refetched. So every session — codex, pi, whatever — showed claude's models.
  2. Backends report their models lazily (claude's #query, codex's #proc, pi's rpc all spawn on the first turn), so a never-used backend has an empty catalog until then. Combined with Memory/sqlite vec retrieval #1, a fresh codex session showed a stale claude list indefinitely — exactly the "never used codex on that machine" case.

Fix (client-side, targeted)

  • fetchModels(provider?, force?) sends the backend on the wire and caches per-backend; the visible catalog tracks the requested backend, with a switch-race guard so a slow response for a backend you already navigated away from is dropped.
  • Switching to an unfetched backend clears the previous list synchronously — the picker never flashes the wrong models.
  • ModelPicker refetches (forced) whenever the focused session's providerId changes; the bare /model slash fetches the focused session's backend, not the daemon default.

A never-used backend still shows an empty catalog until its first turn (the models are genuinely unknown until the backend spawns) — honest, and the picker hides rather than lying. Once it runs once, re-opening the picker populates it.

Why not a daemon fix

listModels() returns empty for every provider until its backend spawns on the first turn — so a daemon "refresh on switch" would have to spawn the backend just to enumerate models, which we don't want. The daemon already caches + persists each backend's catalog after its first turn; the client just has to ask for the right one.

Follow-up worth considering: push a models update to attached clients when a backend first reports its catalog, so the picker auto-populates after the first turn without a manual re-open. Filed as a note, not blocking.

Tests

  • models.test.ts (new, 7): provider sent on the wire, default when omitted, switch swaps the catalog, cached-live served without refetch, not-yet-live refetched, stale-list cleared synchronously on switch, slow-response-after-switch dropped.
  • SessionControls.test.tsx (+2): picker fetches the session's backend on mount; refetches when providerId flips (the reported bug).
  • Full web suite 206 green; tsc -b --noEmit clean.

The TUI (codeoid-ui) has the same class of bug (its models.list has no provider field at all) — fixed in a separate codeoid-ui PR.

🤖 Generated with Claude Code

Switching a session's backend (`/provider` or the picker) left the model
catalog showing the OLD backend's models — because the web client fetched
`models.list` with NO `provider` field (so the daemon always answered for
its default backend, claude) and cached the result globally, never
refetching. Codex made it worse: even a correct request returns an empty
list until that backend has run once (every provider builds its backend
lazily on the first turn), so a never-used codex would have shown a
stale claude list indefinitely.

- `fetchModels(provider?, force?)` now sends the backend on the wire and
  caches per-backend; the visible catalog tracks the requested backend
  and a slow response for a backend the user already switched away from
  is dropped (switch-race guard).
- switching to an unfetched backend clears the previous list synchronously
  so the picker never flashes the wrong models.
- ModelPicker refetches (forced) whenever the focused session's
  providerId changes, and the bare `/model` slash fetches the focused
  session's backend rather than the daemon default.

A never-used backend still shows an empty catalog until its first turn
(models are genuinely unknown until then) — honest, and the picker hides
rather than lying. Follow-up worth considering: push a models update to
attached clients when a backend first reports its catalog, so the picker
auto-populates after the first turn without a re-open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread web/src/components/SessionControls.tsx
@github-actions

Copy link
Copy Markdown

🤖 Gemini code review

This PR successfully resolves the issue where the model catalog failed to update when switching a session's backend. It implements per-backend model catalog caching, clears stale models synchronously upon switching to avoid flashing incorrect options, and adds robust unit tests.

Findings: 🔴 0 · 🟠 0 · 🟡 0 · 🟢 1


Tokens spent · ⬆️ Input: 4,851 · ⬇️ Output: 256 · Σ Total: 11,338
Total may be higher due to thinking token counts.

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.46%. Comparing base (f86384b) to head (589530c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #160   +/-   ##
=======================================
  Coverage   84.46%   84.46%           
=======================================
  Files         104      104           
  Lines       18263    18263           
=======================================
  Hits        15426    15426           
  Misses       2837     2837           
Flag Coverage Δ
daemon 84.46% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Review feedback (#160): the provider-change effect forced a refetch even
for a backend already fetched live, so tabbing between sessions on
different backends hit the daemon needlessly. Drop force — a live-cached
backend now serves from cache instantly, while a not-yet-live backend
still refetches. Kept OUTSIDE the picker-open gate on purpose: the
/model slash and help modal read the same catalog, so it must track the
focused backend regardless of picker state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saucam saucam merged commit 78f353d into main Jul 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant