Skip to content

Show which model each message was sent with and answered by #1059

Description

@nico-stever

Why this is worth having: the model picker can silently fail to apply — that's #981, still open, with several fixes proposed — and while it does, there is no way for a user to find out. You change the model, the UI acknowledges it, and the conversation keeps running on the previous one. I only discovered it had been happening for days on my own instance because I added the label described below and could suddenly see it: sessions I was certain were on one model had been answering with another the whole time.

The underlying reason the UI can't tell you is structural, and outlives that bug. Model selection is per conversation, but the picker's displayed value comes from a single global localStorage key, and there is no GET endpoint to ask the server which model a given session is actually pinned to. So the UI can tell you what you selected; it can never tell you what actually ran. Even with #981 fixed, switching between conversations shows one value while each conversation runs its own pin.

It also hides the case where it matters most: when a turn fails because a model's quota is exhausted, there is no assistant response carrying a model at all, so nothing on screen says what the request went out as — which is exactly the moment you want to know.

So this is a mitigation while the picker bug is open, and a verification mechanism after it's closed.

Proposal: render a small label next to the existing MD / TXT chip in the message footer — Opus, Fable, Sonnet, Haiku — on both sides of the exchange. They answer two different questions and both are needed.

On the user's message — what the request went out with

This is the half that tells you whether your picker change actually applied, and it's the only one that survives a failed turn.

Transcript user events carry no model (0 of ~840 in my install), so it has to come from the server: the resolved value that gets passed as --model for that turn. Recording it per turn and joining it on history fetch covers every turn, including ones that die before any response — which the response-side label cannot.

Two things to get right here:

  • Don't label the optimistic client-side bubble with the picker's value. When a session pin exists, that value is exactly the one that is wrong, so it would reproduce the misreporting this is meant to remove. It also gets pruned against the transcript when the turn completes, so a label placed there disappears seconds later — precisely in the failure case.
  • Don't collapse opus and opus[1m] (or sonnet / sonnet[1m]) into one label. Distinguishing them is the whole point: it's what tells a user whether the change they made in the picker took effect.

On the assistant's message — what answered

The data already exists: every assistant event in the transcript carries message.model. Carrying it through the normalizer makes it available with no migration and it renders retroactively, because history is re-read from the transcript on every open. Around 1500 assistant events in my install all had it.

Worth knowing before implementing: a large share of assistant events are tool-call turns with no text part, so they produce no footer and therefore no label. Coverage on this side is partial by construction — which is another reason the request-side label matters.

Edge cases

  • Events whose model is <synthetic> (local CLI notices, e.g. quota errors) should get no label rather than a guessed one — no model produced them.
  • Deriving the request label from the following assistant event is tempting and should be avoided: it fails on exactly the aborted turns this is meant to surface, and it silently attributes a later turn's model to an earlier request.
  • If a per-turn record is missing, showing nothing is much better than showing the previous turn's model — the stale value is confidently wrong about the one question the label exists to answer.

I have a working implementation of both halves and am happy to open a PR if the approach looks right. Following CONTRIBUTING, opening this first to discuss before investing in the PR.


Implemented and verified with Claude Code (Opus 5) against v1.36.3. The counts above are from my own install.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions