Skip to content

Show which model actually answered - #2

Closed
android6 wants to merge 1 commit into
openchamber:mainfrom
android6:feat/model-fallback-note
Closed

Show which model actually answered#2
android6 wants to merge 1 commit into
openchamber:mainfrom
android6:feat/model-fallback-note

Conversation

@android6

Copy link
Copy Markdown

Claude Code can switch models mid-conversation: when the safety classifiers refuse a request on Fable 5, the CLI silently re-runs it on Opus. OpenCode keeps displaying the model the user picked, so there is no way to tell.

This adds a short callout at the end of the answer whenever the answering model differs from the selected one:

  • model switched - from the SDK model_refusal_fallback event (models, safeguard category, scope).
  • model refused - model_refusal_no_fallback: the turn ended with an error and no retry ran.
  • model mismatch - init.model or assistant.message.model differs from the user's pick; catches switches the CLI doesn't announce.

One note per leg, always after the text so it can't land inside a code fence. OPENCODE_CLAUDE_MODEL_NOTES=reasoning moves it into the thinking trace instead. CLI system/notification events (medium+) are forwarded to the reasoning trace as [notice].

Seen live with CLI 2.1.237: bio -> claude-opus-5, cyber -> claude-opus-4-8.

Claude Code switches models behind OpenCode's back in several ways and the
proxy dropped every signal, so OpenCode kept showing the requested model:

* Safety-classifier fallback. When a request is blocked on Fable 5 the CLI
  re-runs it on Opus (4.8 for cyber, 5 for bio) and emits
  'system/model_refusal_fallback'; with scope 'session' the session model
  stays on Opus for every later turn. Map it (and
  'model_refusal_no_fallback') to a notice naming both models, the refusal
  category, the scope, the CLI's own banner text and - when the API
  provides one - the refusal explanation, which Claude Code's UI does not
  show.

* Any other switch. 'assistant.message.model' is the model that produced an
  answer. The baseline is the model OpenCode asked for (the user's picker
  choice); a differing init.model means the CLI kept the session latched on
  an earlier fallback and is announced too ('[model] session is on
  claude-opus-4-8 (selected: fable)'). Track the model the user was last
  told is answering per bridge and announce a differing answer model once:
  '[model] answered by claude-opus-4-8 (session model: claude-fable-5)'.
  Ids are compared prefix/date-insensitively; a bare alias ('opus') matches
  its pinned versions so ambiguity never raises an alarm.

* CLI notices. 'system/notification' mirrors the interactive REPL's
  notification area (deprecations, limits, account state). Forward medium
  and higher priorities as '[notice] ...' in the reasoning trace;
  low-priority tips stay silent.

Model-identity notices go into the answer content as a Markdown callout
(blockquote, ahead of the model's text) so they are visible regardless of
how the host renders reasoning traces - hosts commonly collapse or hide
them. OPENCODE_CLAUDE_MODEL_NOTES=reasoning keeps them in the thinking
trace like the compact and rate-limit notes. All notices are mirrored to
the warn log.

Field names follow the SDK wire shape (snake_case, as built by the CLI's
SDK serializer); the CLI's internal camelCase shape of the same events is
accepted too.

Tests: unit coverage for the formatters, modelsMatch, the notice channel
and the callout renderer (incl. a fixture mirroring real
model_refusal_fallback events from claude CLI 2.1.197-2.1.237), plus proxy
round-trips with mocked SDK streams for the fallback case (no duplicate
notice, notifications stay in reasoning), the quiet-switch case (notice
exactly once, buffered and streaming, both channels) and a session latched
on a fallback model.
@android6
android6 force-pushed the feat/model-fallback-note branch 2 times, most recently from ae22093 to c81642e Compare August 20, 2026 21:06
@android6

Copy link
Copy Markdown
Author

Closing in favour of #1 (reopened).

@android6 android6 closed this Aug 20, 2026
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