fix(desktop): the main agent's model pick persists as the profile default (#86414)

* 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.
This commit is contained in:
Teknium
2026-08-14 15:40:53 -07:00
committed by GitHub
parent 31e571acf6
commit 7d96537bc8
2 changed files with 22 additions and 3 deletions
@@ -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())
})
@@ -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