diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 7e964088b..3b3c54533 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -219,7 +219,10 @@ Per-field precedence for `generationConfig`: ##### Selection persistence and recommendations -- `/model` persists both `model.name` and `security.auth.selectedType`. We first try to write to the nearest scope whose settings already contain `modelProviders`; otherwise we fall back to user scope. Qwen OAuth selections always persist to the user scope. +> [!important] +> Define `modelProviders` in the user-scope `~/.qwen/settings.json` whenever possible and avoid persisting credential overrides in any scope. Keeping the provider catalog in user settings prevents merge/override conflicts between project and user scopes and ensures `/auth` and `/model` updates always write back to a consistent scope. + +- `/model` and `/auth` persist `model.name` (where applicable) and `security.auth.selectedType` to the closest writable scope that already defines `modelProviders`; otherwise they fall back to the user scope. This keeps workspace/user files in sync with the active provider catalog. - Without `modelProviders`, the resolver mixes CLI/env/settings layers, which is fine for single-provider setups but cumbersome when frequently switching. Define provider catalogs whenever multi-model workflows are common so that switches stay atomic, source-attributed, and debuggable. #### context diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index 0b99eed98..28e13e889 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { AuthDialog } from './AuthDialog.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { LoadedSettings } from '../../config/settings.js'; import { AuthType } from '@qwen-code/qwen-code-core'; import { renderWithProviders } from '../../test-utils/render.js'; import { UIStateContext } from '../contexts/UIStateContext.js'; @@ -536,7 +536,7 @@ describe('AuthDialog', () => { await wait(); // Should call handleAuthSelect with undefined to exit - expect(handleAuthSelect).toHaveBeenCalledWith(undefined, SettingScope.User); + expect(handleAuthSelect).toHaveBeenCalledWith(undefined); unmount(); }); }); diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index 80c13b0bc..44e2affaa 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -8,7 +8,6 @@ import type React from 'react'; import { useState } from 'react'; import { AuthType } from '@qwen-code/qwen-code-core'; import { Box, Text } from 'ink'; -import { SettingScope } from '../../config/settings.js'; import { Colors } from '../colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; @@ -84,7 +83,7 @@ export function AuthDialog(): React.JSX.Element { const handleAuthSelect = async (authMethod: AuthType) => { setErrorMessage(null); - await onAuthSelect(authMethod, SettingScope.User); + await onAuthSelect(authMethod); }; const handleHighlight = (authMethod: AuthType) => { @@ -109,7 +108,7 @@ export function AuthDialog(): React.JSX.Element { ); return; } - onAuthSelect(undefined, SettingScope.User); + onAuthSelect(undefined); } }, { isActive: true }, diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 6125ebdf2..5b97ead5f 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -12,7 +12,8 @@ import { logAuth, } from '@qwen-code/qwen-code-core'; import { useCallback, useEffect, useState } from 'react'; -import type { LoadedSettings, SettingScope } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; +import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js'; import type { OpenAICredentials } from '../components/OpenAIKeyPrompt.js'; import { useQwenAuth } from '../hooks/useQwenAuth.js'; import { AuthState, MessageType } from '../types.js'; @@ -80,33 +81,34 @@ export const useAuthCommand = ( ); const handleAuthSuccess = useCallback( - async ( - authType: AuthType, - scope: SettingScope, - credentials?: OpenAICredentials, - ) => { + async (authType: AuthType, credentials?: OpenAICredentials) => { try { - settings.setValue(scope, 'security.auth.selectedType', authType); + const authTypeScope = getPersistScopeForModelSelection(settings); + settings.setValue( + authTypeScope, + 'security.auth.selectedType', + authType, + ); // Only update credentials if not switching to QWEN_OAUTH, // so that OpenAI credentials are preserved when switching to QWEN_OAUTH. if (authType !== AuthType.QWEN_OAUTH && credentials) { if (credentials?.apiKey != null) { settings.setValue( - scope, + authTypeScope, 'security.auth.apiKey', credentials.apiKey, ); } if (credentials?.baseUrl != null) { settings.setValue( - scope, + authTypeScope, 'security.auth.baseUrl', credentials.baseUrl, ); } if (credentials?.model != null) { - settings.setValue(scope, 'model.name', credentials.model); + settings.setValue(authTypeScope, 'model.name', credentials.model); } } } catch (error) { @@ -139,14 +141,10 @@ export const useAuthCommand = ( ); const performAuth = useCallback( - async ( - authType: AuthType, - scope: SettingScope, - credentials?: OpenAICredentials, - ) => { + async (authType: AuthType, credentials?: OpenAICredentials) => { try { await config.refreshAuth(authType); - handleAuthSuccess(authType, scope, credentials); + handleAuthSuccess(authType, credentials); } catch (e) { handleAuthFailure(e); } @@ -155,11 +153,7 @@ export const useAuthCommand = ( ); const handleAuthSelect = useCallback( - async ( - authType: AuthType | undefined, - scope: SettingScope, - credentials?: OpenAICredentials, - ) => { + async (authType: AuthType | undefined, credentials?: OpenAICredentials) => { if (!authType) { setIsAuthDialogOpen(false); setAuthError(null); @@ -178,12 +172,12 @@ export const useAuthCommand = ( baseUrl: credentials.baseUrl, model: credentials.model, }); - await performAuth(authType, scope, credentials); + await performAuth(authType, credentials); } return; } - await performAuth(authType, scope); + await performAuth(authType); }, [config, performAuth], ); diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index c3e1a128c..6ff9f4aae 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -25,7 +25,6 @@ import { useUIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { useSettings } from '../contexts/SettingsContext.js'; -import { SettingScope } from '../../config/settings.js'; import { AuthState } from '../types.js'; import { AuthType } from '@qwen-code/qwen-code-core'; import process from 'node:process'; @@ -202,7 +201,7 @@ export const DialogManager = ({ return ( { - uiActions.handleAuthSelect(AuthType.USE_OPENAI, SettingScope.User, { + uiActions.handleAuthSelect(AuthType.USE_OPENAI, { apiKey, baseUrl, model, diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index b9842c811..93c0528d8 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -30,7 +30,6 @@ export interface UIActions { ) => void; handleAuthSelect: ( authType: AuthType | undefined, - scope: SettingScope, credentials?: OpenAICredentials, ) => Promise; setAuthState: (state: AuthState) => void; diff --git a/packages/cli/src/ui/hooks/useDialogClose.ts b/packages/cli/src/ui/hooks/useDialogClose.ts index 298f44963..8191e16b8 100644 --- a/packages/cli/src/ui/hooks/useDialogClose.ts +++ b/packages/cli/src/ui/hooks/useDialogClose.ts @@ -25,7 +25,6 @@ export interface DialogCloseOptions { isAuthDialogOpen: boolean; handleAuthSelect: ( authType: AuthType | undefined, - scope: SettingScope, credentials?: OpenAICredentials, ) => Promise; pendingAuthType: AuthType | undefined;