diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 76aa0c81413..07a32efba0b 100644 --- a/packages/cli/src/acp-integration/acpAgent.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.test.ts @@ -473,7 +473,10 @@ vi.mock('../config/settings.js', () => ({ reloadEnvironment: vi.fn(() => ({ updatedKeys: [], removedKeys: [] })), })); vi.mock('../config/loadedSettingsAdapter.js', () => ({ - createLoadedSettingsAdapter: vi.fn((settings: unknown) => settings), + createLoadedSettingsAdapter: vi.fn((settings: unknown) => { + (settings as Record)['getValue'] = vi.fn(); + return settings; + }), })); vi.mock('../config/config.js', () => ({ loadCliConfig: vi.fn(), @@ -592,6 +595,7 @@ import { MAX_PERMISSION_RULES_COUNT, } from '../config/permission-settings.js'; import { loadCliConfig } from '../config/config.js'; +import { createLoadedSettingsAdapter } from '../config/loadedSettingsAdapter.js'; import { Session, buildAvailableCommandsSnapshot } from './session/Session.js'; import { SERVE_STATUS_EXT_METHODS, @@ -3993,6 +3997,44 @@ describe('QwenAgent MCP SSE/HTTP support', () => { await agentPromise; }); + it('qwen/providers/connect returns preserved model when adapter getValue returns a non-empty string', async () => { + vi.mocked(createLoadedSettingsAdapter).mockImplementationOnce( + (settings: unknown) => { + (settings as Record)['getValue'] = vi.fn( + (key: string) => + key === 'model.name' ? 'deepseek-flash' : undefined, + ); + return settings as unknown as ReturnType< + typeof createLoadedSettingsAdapter + >; + }, + ); + + const settings = makeSessionSettings(); + const agentPromise = runAcpAgent(mockConfig, settings, mockArgv); + await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined()); + + const agent = capturedAgentFactory!({ + get closed() { + return mockConnectionState.promise; + }, + }) as AgentLike; + + await expect( + agent.extMethod('qwen/providers/connect', { + providerId: 'deepseek', + apiKey: 'sk-test', + modelIds: ['deepseek-chat'], + }), + ).resolves.toMatchObject({ + success: true, + modelId: 'deepseek-flash', + }); + + mockConnectionState.resolve(); + await agentPromise; + }); + it('qwen/providers/list includes existing provider settings', async () => { const settings = { ...makeSessionSettings(), diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 6a087ef02ff..5e80c80cc1e 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -4872,8 +4872,12 @@ class QwenAgent implements Agent { ); const persistScope = readProviderConnectScope(params['scope']); const plan = buildInstallPlan(providerConfig, inputs); + const adapter = createLoadedSettingsAdapter( + this.settings, + persistScope, + ); await applyProviderInstallPlan(plan, { - settings: createLoadedSettingsAdapter(this.settings, persistScope), + settings: adapter, reloadModelProviders: (modelProviders) => this.config.reloadModelProvidersConfig(modelProviders), syncAuthState: (authType, modelId, baseUrl) => @@ -4882,16 +4886,19 @@ class QwenAgent implements Agent { .syncAfterAuthRefresh(authType, modelId, baseUrl), refreshAuth: (authType) => this.config.refreshAuth(authType), }); - + const effectiveModelId = + (adapter.getValue('model.name') as string | undefined) ?? + plan.modelSelection?.modelId; + const effectiveBaseUrl = + (adapter.getValue('model.baseUrl') as string | undefined) ?? + plan.modelSelection?.baseUrl; return { success: true, providerId: providerConfig.id, providerLabel: providerConfig.label, authType: plan.authType, - modelId: plan.modelSelection?.modelId, - ...(plan.modelSelection?.baseUrl - ? { baseUrl: plan.modelSelection.baseUrl } - : {}), + ...(effectiveModelId ? { modelId: effectiveModelId } : {}), + ...(effectiveBaseUrl ? { baseUrl: effectiveBaseUrl } : {}), }; } case 'qwen/skills/install': { diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index 4e88b7670bb..735ee1b2e43 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -1777,26 +1777,32 @@ export async function runQwenServe( }); const plan = core.buildInstallPlan(provider, inputs); const fresh = settingsRuntime.settings.loadSettings(boundWorkspace); + const adapter = + settingsRuntime.loadedSettingsAdapter.createLoadedSettingsAdapter( + fresh, + ); await core.applyProviderInstallPlan(plan, { - settings: - settingsRuntime.loadedSettingsAdapter.createLoadedSettingsAdapter( - fresh, - ), + settings: adapter, doRefreshAuth: false, }); core.emitDaemonLog('Auth provider installed.', { 'qwen-code.daemon.auth.provider_id': provider.id, 'qwen-code.daemon.auth.auth_type': plan.authType, }); + const effectiveModelId = + (adapter.getValue('model.name') as string | undefined) ?? + plan.modelSelection?.modelId; + const effectiveBaseUrl = + (adapter.getValue('model.baseUrl') as string | undefined) ?? + plan.modelSelection?.baseUrl ?? + inputs.baseUrl; return { v: 1, providerId: provider.id, providerLabel: provider.label, authType: plan.authType, - ...(plan.modelSelection?.modelId - ? { modelId: plan.modelSelection.modelId } - : {}), - ...(inputs.baseUrl ? { baseUrl: inputs.baseUrl } : {}), + ...(effectiveModelId ? { modelId: effectiveModelId } : {}), + ...(effectiveBaseUrl ? { baseUrl: effectiveBaseUrl } : {}), message: `Successfully configured ${provider.label}. Use /model to switch models.`, }; }, diff --git a/packages/cli/src/ui/auth/useAuth.test.ts b/packages/cli/src/ui/auth/useAuth.test.ts index 328f49e5dc0..6182040916e 100644 --- a/packages/cli/src/ui/auth/useAuth.test.ts +++ b/packages/cli/src/ui/auth/useAuth.test.ts @@ -30,11 +30,16 @@ vi.mock('../hooks/useQwenAuth.js', () => ({ })), })); -vi.mock('../../utils/settingsUtils.js', () => ({ - backupSettingsFile: vi.fn(), - restoreSettingsFromBackup: vi.fn(), - cleanupSettingsBackup: vi.fn(), -})); +vi.mock('../../utils/settingsUtils.js', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + backupSettingsFile: vi.fn(), + restoreSettingsFromBackup: vi.fn(), + cleanupSettingsBackup: vi.fn(), + }; +}); vi.mock('../../config/modelProvidersScope.js', () => ({ getPersistScopeForModelSelection: vi.fn(() => 'user'), diff --git a/packages/cli/src/ui/hooks/useProviderUpdates.test.ts b/packages/cli/src/ui/hooks/useProviderUpdates.test.ts index e25cd5d15c0..b616057878a 100644 --- a/packages/cli/src/ui/hooks/useProviderUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useProviderUpdates.test.ts @@ -19,11 +19,16 @@ import { } from '@qwen-code/qwen-code-core'; import { useProviderUpdates } from './useProviderUpdates.js'; -vi.mock('../../utils/settingsUtils.js', () => ({ - backupSettingsFile: vi.fn(), - restoreSettingsFromBackup: vi.fn(), - cleanupSettingsBackup: vi.fn(), -})); +vi.mock('../../utils/settingsUtils.js', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + backupSettingsFile: vi.fn(), + restoreSettingsFromBackup: vi.fn(), + cleanupSettingsBackup: vi.fn(), + }; +}); const chinaTemplate = buildProviderTemplate( codingPlanProvider, diff --git a/packages/core/src/providers/install.ts b/packages/core/src/providers/install.ts index ccc4937342f..1774152671d 100644 --- a/packages/core/src/providers/install.ts +++ b/packages/core/src/providers/install.ts @@ -211,11 +211,38 @@ export async function applyProviderInstallPlan( } // Model selection + // Re-applying a plan (manual /auth, ACP reconnect, token refresh, or an + // upgrade that reordered the model list) must not silently move the user + // off a model they chose. If the plan still offers the current model, keep + // it; a genuine first-time setup still adopts the provider default. (#5819) currentStep = 'modelSelection'; - if (plan.modelSelection?.modelId) { - settings.setValue('model.name', plan.modelSelection.modelId); - if (plan.modelSelection.baseUrl) { - settings.setValue('model.baseUrl', plan.modelSelection.baseUrl); + let effectiveModelSelection = plan.modelSelection; + if (effectiveModelSelection?.modelId) { + const currentModelId = settings.getValue('model.name'); + const currentBaseUrl = settings.getValue('model.baseUrl') as + | string + | undefined; + const planOffersCurrentModel = + typeof currentModelId === 'string' && + currentModelId.length > 0 && + (plan.modelProviders ?? []).some((patch) => + patch.models.some((model) => + currentBaseUrl === '' || currentBaseUrl === undefined + ? model.id === currentModelId + : isSameModelIdentity( + { id: currentModelId, baseUrl: currentBaseUrl }, + model, + ), + ), + ); + if (planOffersCurrentModel) { + effectiveModelSelection = undefined; + } + } + if (effectiveModelSelection?.modelId) { + settings.setValue('model.name', effectiveModelSelection.modelId); + if (effectiveModelSelection.baseUrl) { + settings.setValue('model.baseUrl', effectiveModelSelection.baseUrl); } else { // The plan selects by model id only, so clear any baseUrl disambiguator // left by a previous model-picker selection — otherwise the next launch @@ -241,12 +268,12 @@ export async function applyProviderInstallPlan( // Reload runtime config currentStep = 'reloadModelProviders'; reloadModelProviders?.(updatedModelProviders); - if (plan.modelSelection?.modelId) { + if (effectiveModelSelection?.modelId) { currentStep = 'syncAuthState'; syncAuthState?.( plan.authType, - plan.modelSelection.modelId, - plan.modelSelection.baseUrl, + effectiveModelSelection.modelId, + effectiveModelSelection.baseUrl, ); } if (doRefreshAuth && refreshAuth) {