You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Remote TUI uses the client’s local model preference instead of the connected server’s default model #55384
When using the TUI with a remote server via opencode --server, the model catalog and the default model can come from different places. The /model dialog correctly lists models returned by the connected remote server, but the model shown in the TUI for a new session may come from the client machine's local model preferences/configuration (for example, its recent model). This can result in the TUI displaying a model associated with the client machine instead of the default model configured on the remote server.
Thanks for the report. I could reproduce this on 2.0.29, and it still happens on the latest dev (34d8ded).
What I ran:
The server's config defines two models, "Server Default Model" and "Client Recent Model", and has no top-level model setting.
The TUI connects to that server. On the client side, its own ~/.local/state/opencode/model.json lists "Client Recent Model" as the most recent model.
On a new session, the TUI shows Build · Client Recent Model, while /model lists the server's models. The recording and screenshots below show this, and the script below reproduces it.
One thing that may help in the meantime: when the server's config sets model explicitly (e.g. "model": "provider/model" in its opencode config), the TUI does show the server's model. The client's local recent list only wins when the server's default isn't set that way, for example when it falls back to the catalog or a plugin sets it.
The likely cause is that the TUI's model choice for a new session (packages/tui/src/context/local.tsx) checks the server's config model and then the client's local recent list, but never asks the server for its own default model.
Related: #43055, where the v2 TUI ignored the configured default model. It's the same model-choice code, but a different trigger.
recording.mp4
Reproduction script (drive.ts)
// Repro for anomalyco/opencode#55384: the TUI connected to a server shows the model from the// *client's* local recent-model list (XDG_STATE_HOME/opencode/model.json) instead of the// server's default model, while /model lists the server's models.//// Server config: two models, "Server Default Model" (server default) and "Client Recent Model".// Client state: model.json lists "Client Recent Model" as the most recent model.// CASE=noconfig removes the explicit `model` from the server config, so the server default// comes from the catalog (GET /api/model/default) instead.import{Effect}from"effect"import{mkdir,writeFile}from"node:fs/promises"import{join}from"node:path"import{OpenCodeDriver}from"opencode-drive"constnoConfig=process.env.CASE==="noconfig"constmodel=(name: string)=>({
name,capabilities: {tools: true,input: ["text"],output: ["text"]},limit: {context: 128_000,output: 16_000},})exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true,viewport: {cols: 120,rows: 36}},config: {providers: {simulation: {name: "Simulation",package: "@opencode-ai/ai/providers/openai/chat",settings: {apiKey: "sim-key"},models: {"gpt-sim-model": model("Server Default Model"),"client-recent": model("Client Recent Model"),},},},},setup: ({ config })=>Effect.sync(()=>{if(noConfig)deleteconfig.modelelseconfig.model="simulation/gpt-sim-model"}),},({ ui, artifacts, opencode })=>Effect.gen(function*(){yield*ui.screenshot("01-before-client-preference")// Client-side preference file, as written by the TUI on the client machine.constdir=join(artifacts,"home",".local","state","opencode")yield*Effect.promise(async()=>{awaitmkdir(dir,{recursive: true})awaitwriteFile(join(dir,"model.json"),JSON.stringify({recent: [{providerID: "simulation",modelID: "client-recent"}],favorite: [],variant: {}}),)})yield*Effect.sleep(1500)yield*ui.screenshot("02-new-session-model")yield*ui.press("x",{ctrl: true})yield*ui.press("m")yield*Effect.sleep(1000)yield*ui.screenshot("03-model-dialog")yield*ui.press("escape")yield*Effect.sleep(300)constframe=yield*ui.capture()yield*Effect.log(String((frameasany).text??JSON.stringify(frame)).slice(0,4000))}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
I traced this on dev at d0732eadf8. The server's explicit root model already wins over client recents. With no root model, a valid client recent is deliberately selected by the TUI and sent explicitly in session.create; /api/model/default still returns a model, even when it is only the catalog's automatic fallback. A plugin can also set that endpoint's default without a root model.
Should a remote connection's new sessions prefer every server-resolved default over a valid client recent, or should recents still win over the automatic catalog fallback? The current endpoint does not distinguish a plugin-selected default from an automatic fallback. This also intersects with the opposite precedence requested for Mini in #50620 (recents before server default); that PR is not a maintainer decision.
I verified the config/plugin/catalog cases through the real local server endpoint, and the selection through the production TUI context harness, not a deployed terminal. Clarifying this precedence would let a fix preserve the intended last-used-model behavior rather than silently changing it for all connections.
Activity
opencode-agent commentedon Oct 14, 2026
Thanks for the report. I could reproduce this on 2.0.29, and it still happens on the latest
dev(34d8ded).What I ran:
modelsetting.~/.local/state/opencode/model.jsonlists "Client Recent Model" as the most recent model.Build · Client Recent Model, while/modellists the server's models. The recording and screenshots below show this, and the script below reproduces it.One thing that may help in the meantime: when the server's config sets
modelexplicitly (e.g."model": "provider/model"in its opencode config), the TUI does show the server's model. The client's local recent list only wins when the server's default isn't set that way, for example when it falls back to the catalog or a plugin sets it.The likely cause is that the TUI's model choice for a new session (
packages/tui/src/context/local.tsx) checks the server's configmodeland then the client's local recent list, but never asks the server for its own default model.Related: #43055, where the v2 TUI ignored the configured default model. It's the same model-choice code, but a different trigger.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.dajiaohuang commentedon Oct 15, 2026
I traced this on
devatd0732eadf8. The server's explicit rootmodelalready wins over client recents. With no rootmodel, a valid client recent is deliberately selected by the TUI and sent explicitly insession.create;/api/model/defaultstill returns a model, even when it is only the catalog's automatic fallback. A plugin can also set that endpoint's default without a rootmodel.Should a remote connection's new sessions prefer every server-resolved default over a valid client recent, or should recents still win over the automatic catalog fallback? The current endpoint does not distinguish a plugin-selected default from an automatic fallback. This also intersects with the opposite precedence requested for Mini in #50620 (recents before server default); that PR is not a maintainer decision.
I verified the config/plugin/catalog cases through the real local server endpoint, and the selection through the production TUI context harness, not a deployed terminal. Clarifying this precedence would let a fix preserve the intended last-used-model behavior rather than silently changing it for all connections.