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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user