Skip to content

bug(tui): persisted per-model variant state in model.json shadows agent.variant config for primary agents #38363

Closed as not planned

Description

@andresnator

Description

Changing agent.<name>.variant in opencode.json has no effect on primary agents driven from the TUI, even after a full restart. The config is persisted and served correctly (GET /config shows the new variant), but requests keep running with the old one.

Cause — a precedence inversion between the TUI's persisted per-model variant memory and the agent config (file/line refs against the v1.18.0 tag):

  1. The TUI keeps a per-model variant map in ~/.local/state/opencode/model.json (packages/tui/src/context/local.tsx:155-189).
  2. The map is auto-seeded: opening or switching to a session copies the last user message's variant into it and saves (local.model.variant.set(msg.model.variant), packages/tui/src/component/prompt/index.tsx:328). No native variant pick is needed — using a session once is enough.
  3. Every prompt sends the map value as the explicit variant (prompt/index.tsx:988). Unlike model resolution (local.tsx:236-245, which falls back to the agent's configured a.model), variant resolution (local.tsx:362-374) never consults the agent's configured variant.
  4. Server-side the client-sent variant always wins: input.variant ?? (ag.variant && ...) (packages/opencode/src/session/prompt.ts:654).

So once the map has an entry for a model — automatic after the first message — later agent.<name>.variant changes are silently shadowed forever for TUI-driven agents. Restarting doesn't help (model.json is persisted). Picking via /variants "works" because it writes the map that actually wins, which makes the config path look broken by comparison.

A per-model memory also can't represent this config at all — several agents legitimately share a model with different variants:

"agent": {
  "orchestrator": { "model": "openai/gpt-5.6-sol", "variant": "low" },
  "sdd-design":   { "model": "openai/gpt-5.6-sol", "variant": "medium" },
  "sdd-spec":     { "model": "openai/gpt-5.6-sol", "variant": "high" }
}

One "openai/gpt-5.6-sol": "medium" entry in model.json overrides all three.

Expected behavior — mirror the model precedence: treat a native pick as an ephemeral per-agent override (like the in-memory /model override), fall back to the agent's configured variant otherwise, and don't auto-seed the persisted map from session sync (or key it per agent+model and invalidate it when the configured variant changes).

Related: #13718 reported this same shadowing on v1.2.4 (closed not_planned — this adds the root-cause trace and confirms it on v1.18.0). #28803 is the adjacent session-lock case (SessionTable.model consulted first, session/prompt.ts:615-627). #36250 covers the subagent vector (handleSubtask resolves taskModel before taskAgent, prompt.ts:267 vs :313, and stamps the parent's variant at :275 — also still present at v1.18.0).

Plugins

None — reproduces on a clean install with no plugins

OpenCode version

1.18.0

Steps to reproduce

With isolated XDG_CONFIG_HOME / XDG_STATE_HOME / XDG_DATA_HOME:

  1. Config: agent.build.model = "openai/gpt-5.5", agent.build.variant = "high". Start the TUI, send one message — it correctly runs with high (the state map is still empty, so config applies). The stored message carries model.variant: "high".
  2. Restart the TUI and resume/switch to that session. model.json now contains "variant": { "openai/gpt-5.5": "high" }, seeded without any user action.
  3. Change the config to "variant": "medium". Restart the TUI.
  4. Send a message. Actual: still high (persisted map beats ag.variant). Expected: medium.
  5. Pick medium via /variants — now it applies, confirming the inversion.

Also observed on a real setup: config orchestrator → openai/gpt-5.6-sol variant=low, model.json holding "openai/gpt-5.6-sol": "medium", orchestrator observably running at medium across restarts.

Screenshot and/or share link

No response

Operating System

macOS 26 (Darwin 25.4.0)

Terminal

Ghostty

Activity

  1. samiralibabic commented on Jul 23, 2026

    @samiralibabic

    Confirmed on current upstream/dev. I traced the behavior to ed0c0d90be (feat: add variants toggle), with the global per-model variant state carried into the v2 TUI by e6f660fecf.

    The terminal TUI stores active variants in ~/.local/state/opencode/model.json under provider/model, and restoring a session writes its variant into that global store. This contaminates other sessions and agents sharing the model. The desktop/web app already keeps { agent, model, variant } state per session.

    A complete fix appears to be:

    1. Keep terminal model/variant selection per session, plus separate new-session draft state.
    2. Stop persisting active variants in model.json; retain only recents/favorites.
    3. Ignore the legacy global variant map during migration.
    4. Resolve session selection first, then configured agent model/variant, global configured model, and fallback.
    5. Add tests for cross-session isolation, Build/Plan isolation, old-session restoration, and new-session defaults.

    I can prepare a focused PR implementing this approach if that matches the intended semantics.

  2. andresnator commented on Jul 27, 2026

    @andresnator
    Author

    What do you think, mate @kommander ? Should we open the PR?

  3. fobiat commented on Sep 17, 2026

    @fobiat

    Current dev 88c6c7a still has distinct client behavior: full TUI local.model.variant.current() reads the per-model saved map, while currentModel resolves an explicit per-agent model override before the agent model. The prompt footer and submission both consume that variant getter. Restoring a session writes its variant back into the saved map. This can be a resolution bug, not merely a stale label; #47076 is the existing fix to coordinate with.

    The web app instead keeps per-session selection and applies agent.set to the selected model/variant. A source explanation and running-versus-next-turn display need a separate design decision; they should not silently change explicit-override precedence. Core session-runner-model tests pass locally, but that alone does not verify client-to-provider agreement.

  4. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions