From 7d96537bc8627f05ff3617e7d48c14fb5736ad92 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:40:53 -0700 Subject: [PATCH] fix(desktop): the main agent's model pick persists as the profile default (#86414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(desktop): the main agent's model pick persists as the profile default Reported: the default bot switches to the OpenAI API account instead of the user's subscription, and doesn't retain the previous selection. Root cause: the composer model picker always sent the switch as --session scope, even for the PRIMARY profile's main agent. So the pick never wrote config.yaml model.provider — and with model.provider unset, resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env var and picks OpenAI/OpenRouter. The subscription the user selected was only ever a per-session override that evaporated on the next session. Fix: when the pick targets the primary profile's main agent (touchesPrimary), send --global so it persists to config.yaml (model.default + model.provider) via the existing model-switch persist path. A SET model.provider already outranks the OPENAI_API_KEY env var in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the chosen provider across restarts. Secondary chat tiles stay --session so picking a model in one chat never rewrites the profile default (the cross-session-contamination guard the old comment protected). No change to resolve_provider's priority chain, so #29285 (an explicit env key beating a STALE oauth login) is untouched — we simply make the user's explicit main-agent selection the config default it always should have been. * MoA presets stay session-scoped; update tests for primary-persist intent Fix CI (ui shard 3of3): the primary main-agent pick now persists via --global, but MoA (mixture-of-agents) presets must NOT — a transient orchestration choice can't become the global gateway default. Exclude provider==='moa' from the persist path (stays --session). Update the primary-picker test to assert --global (the new intent) and keep the MoA + secondary-tile tests asserting --session (the guards that prove the narrowing). 19/19 green locally. --- .../session/hooks/use-model-controls.test.tsx | 7 +++++-- .../app/session/hooks/use-model-controls.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx index 8a3ceb589a..f8fbeb511a 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx @@ -219,7 +219,7 @@ describe('useModelControls', () => { }) }) - it('routes active-session picker changes through config.set with an explicit session-scoped provider', async () => { + it('persists an active primary-session picker change as the profile default via config.set --global', async () => { $activeSessionId.set('session-1') const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never) let controls!: Controls @@ -233,10 +233,13 @@ describe('useModelControls', () => { }) ).resolves.toBe(true) + // The primary main agent's pick IS the profile default, so it persists to + // config.yaml (model.default + model.provider) — which is what lets a + // chosen subscription provider outrank a leftover OPENAI_API_KEY env var. expect(requestGateway).toHaveBeenCalledWith('config.set', { session_id: 'session-1', key: 'model', - value: 'claude-sonnet-4.6 --provider anthropic --session' + value: 'claude-sonnet-4.6 --provider anthropic --global' }) expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) }) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts index d3b9ee20a3..f2487e25ca 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.ts +++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts @@ -207,10 +207,26 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO } try { + // The PRIMARY profile's main agent is the profile's default — its + // model/provider choice IS the default, so persist it to config.yaml + // (model.default + model.provider) via --global. This is what makes + // the selection "stick": a set model.provider outranks a leftover + // OPENAI_API_KEY env var in resolve_provider(), so the main agent + // keeps the chosen (e.g. subscription) provider across restarts + // instead of silently falling back to an env key. + // + // Two things stay --session, deliberately: + // - a SECONDARY chat tile: picking a model there must not rewrite the + // profile default (the cross-session-contamination guard). + // - MoA (mixture-of-agents) presets: a transient orchestration choice + // that must never become the persisted global gateway default. + const isSessionOnlyPreset = (selection.provider || '').toLowerCase() === 'moa' + const persistsAsDefault = touchesPrimary && !isSessionOnlyPreset + const scope = persistsAsDefault ? '--global' : '--session' const result = await requestGateway<{ deferred?: boolean }>('config.set', { session_id: liveSessionId, key: 'model', - value: `${selection.model} --provider ${selection.provider} --session` + value: `${selection.model} --provider ${selection.provider} ${scope}` }) // A pick made DURING a turn is queued by the gateway and applied at the