Skip to content

fix(tui): repoint the provider label on a model switch, and pick effort in /model - #836

Merged
ericleepi314 merged 1 commit into
mainfrom
fix/model-switch-provider-label
Aug 13, 2026
Merged

fix(tui): repoint the provider label on a model switch, and pick effort in /model#836
ericleepi314 merged 1 commit into
mainfrom
fix/model-switch-provider-label

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

The bug

The session-stats line renders provider · model as one phrase, but the provider half was written exactly once — from the backend's init frame — while every model switch patched model alone. A cross-provider selection showed the new model beside the old provider:

anthropic · gpt-5.6-luna · ~/work/elon-blog · turns: 4 · …
^^^^^^^^^ stale           ^^^^^^^^^^^^^^^^ moved

Three layers each dropped the provider, and the reported case hit all three:

  1. _do_set_model replied {ok, model} with no provider — while both sibling paths, _do_set_fusion_model and _do_set_provider, already echoed it.
  2. applyModel returned only {value, warning}, discarding it even when sent.
  3. session.ts and useMainApp.ts both did { ...state.info, model }, never touching profile_name.

On the cross-provider path the backend had already answered set_provider with {ok: true, provider: "openai"} — it said where the session moved, and the client threw the answer away.

Fix: threaded end to end, with one shared helper (domain/modelSwitch.ts) so the two apply sites cannot drift again. An absent provider means "unchanged", not "unknown", so a same-provider switch never blanks a still-correct label. The /model picker re-enters /model <value>, so it rides the same path.

/model gains a third step

Provider → model → effort, offering the levels that model actually accepts. The ladder is resolved from the same predicates that gate the wire, so the list is what the model will really take rather than the union ladder /effort validates against:

provider / model offered
claude-opus-5, claude-opus-4-8 low, medium, high, xhigh, max
claude-sonnet-4-6 low, medium, high, max — no xhigh (400s on it)
claude-haiku-4-5, gpt-4o, gpt-5-chat-latest step skipped
gpt-5.6-luna (OpenAI reasoning) low, medium, high, xhigh — no max
deepseek / moonshot / zai … full ladder

The asymmetry drives the design: offering a level the model rejects is fatal (a 400 on the effort level is retried or downgraded nowhere, so every subsequent request fails), while omitting one merely hides a choice. So it errs narrow wherever the codebase actually knows, and falls back to the full ladder only where it does not.

Design decisions worth flagging for review:

  • A model with no ladder keeps the two-step flow rather than being shown a list whose every row is a silent no-op. Same for the new-prompt-session picker (allowEffortStep={false}), which captures a draft and has no session to set an effort on.
  • auto is the picker's own row, not a backend level — it means "clear the override", and emits no /effort at all.
  • The step preselects the session's live level, so Enter-through changes nothing.
  • Effort is session-scoped, exactly as /effort already is; the ^g global toggle stays model-only.
  • Queried per selection rather than ridden along on list_model_providers — the ladder is a property of the model, and some providers enumerate hundreds.

Verification

  • +38 tests. Backend resolver across 10 provider/model pairs, the effort_options control, the RPC mapping, the picker's stage transitions (real keys through a mounted component), and the command expansion.
  • 14/14 mutants killed — each behavior reverted individually, confirming a test fails. Two findings from this: the backend mutation initially hit the wrong one of six identical lines, and the back-navigation test was asserting against cumulative Ink output, so step 2/3 was already in the buffer from the earlier visit and the assertion proved nothing. Now asserts the last painted frame.
  • Python: 10079 passed, 12 skipped, 0 failures (the full suite CI runs).
  • ui-tui: 1850 passed, with the same 8 failures that pre-exist on origin/main — baseline captured and compared, zero new. Note ui-tui has no CI gate; that suite was run locally.
  • Typecheck clean; 0 lint errors in changed files.

🤖 Generated with Claude Code

…rt in /model

The stats line renders `provider · model` as one phrase, but the provider
half was written exactly once — from the backend's init frame — while
every switch patched `model` alone. A cross-provider selection therefore
showed the new model beside the old provider (`anthropic · gpt-5.6-luna`).

Three layers each dropped it, and the reported case hit all three: the
`set_model` reply omitted `provider` (the fusion and `set_provider` paths
already echoed it), `applyModel` discarded it even when sent, and both UI
apply sites spread `...info` and overwrote only `model`. On the
cross-provider path the backend had already answered `set_provider` with
the new provider and the client threw it away. Threaded end to end, with
one shared helper so the two apply sites cannot drift again. An absent
provider means "unchanged", so a same-provider switch never blanks a
still-correct label.

/model also grows a third step. Provider and model are now followed by the
effort levels that model actually accepts, resolved from the same
predicates that gate the wire: sonnet-4-6 is offered no `xhigh` (it 400s
on it, and a 400 on the effort level is retried nowhere), OpenAI reasoning
models no `max`, and providers with no per-model table keep the full
ladder. A model with no ladder at all — haiku, gpt-4o — keeps the two-step
flow instead of being shown a list whose every row is a silent no-op, as
does the new-prompt-session picker, which captures a draft and has no
session to set an effort on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericleepi314
ericleepi314 merged commit 4afa283 into main Aug 13, 2026
7 checks passed
@github-actions

Copy link
Copy Markdown

Test Results

     4 files     966 suites   33m 32s ⏱️
14 634 tests 14 617 ✅ 17 💤 0 ❌
29 946 runs  29 891 ✅ 55 💤 0 ❌

Results for commit 2a32ff4.

@ericleepi314
ericleepi314 deleted the fix/model-switch-provider-label branch August 13, 2026 09:04
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