fix: align authType & model persisting behavior across dialogs

This commit is contained in:
mingholy.lmh 2026-01-07 20:04:00 +08:00
parent fe2ed889b9
commit afe6ba255e
7 changed files with 26 additions and 33 deletions

View file

@ -219,7 +219,10 @@ Per-field precedence for `generationConfig`:
##### Selection persistence and recommendations ##### 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. - 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 #### context

View file

@ -6,7 +6,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AuthDialog } from './AuthDialog.js'; 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 { AuthType } from '@qwen-code/qwen-code-core';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
import { UIStateContext } from '../contexts/UIStateContext.js'; import { UIStateContext } from '../contexts/UIStateContext.js';
@ -536,7 +536,7 @@ describe('AuthDialog', () => {
await wait(); await wait();
// Should call handleAuthSelect with undefined to exit // Should call handleAuthSelect with undefined to exit
expect(handleAuthSelect).toHaveBeenCalledWith(undefined, SettingScope.User); expect(handleAuthSelect).toHaveBeenCalledWith(undefined);
unmount(); unmount();
}); });
}); });

View file

@ -8,7 +8,6 @@ import type React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { AuthType } from '@qwen-code/qwen-code-core'; import { AuthType } from '@qwen-code/qwen-code-core';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { SettingScope } from '../../config/settings.js';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
@ -84,7 +83,7 @@ export function AuthDialog(): React.JSX.Element {
const handleAuthSelect = async (authMethod: AuthType) => { const handleAuthSelect = async (authMethod: AuthType) => {
setErrorMessage(null); setErrorMessage(null);
await onAuthSelect(authMethod, SettingScope.User); await onAuthSelect(authMethod);
}; };
const handleHighlight = (authMethod: AuthType) => { const handleHighlight = (authMethod: AuthType) => {
@ -109,7 +108,7 @@ export function AuthDialog(): React.JSX.Element {
); );
return; return;
} }
onAuthSelect(undefined, SettingScope.User); onAuthSelect(undefined);
} }
}, },
{ isActive: true }, { isActive: true },

View file

@ -12,7 +12,8 @@ import {
logAuth, logAuth,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { useCallback, useEffect, useState } from 'react'; 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 type { OpenAICredentials } from '../components/OpenAIKeyPrompt.js';
import { useQwenAuth } from '../hooks/useQwenAuth.js'; import { useQwenAuth } from '../hooks/useQwenAuth.js';
import { AuthState, MessageType } from '../types.js'; import { AuthState, MessageType } from '../types.js';
@ -80,33 +81,34 @@ export const useAuthCommand = (
); );
const handleAuthSuccess = useCallback( const handleAuthSuccess = useCallback(
async ( async (authType: AuthType, credentials?: OpenAICredentials) => {
authType: AuthType,
scope: SettingScope,
credentials?: OpenAICredentials,
) => {
try { 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, // Only update credentials if not switching to QWEN_OAUTH,
// so that OpenAI credentials are preserved when switching to QWEN_OAUTH. // so that OpenAI credentials are preserved when switching to QWEN_OAUTH.
if (authType !== AuthType.QWEN_OAUTH && credentials) { if (authType !== AuthType.QWEN_OAUTH && credentials) {
if (credentials?.apiKey != null) { if (credentials?.apiKey != null) {
settings.setValue( settings.setValue(
scope, authTypeScope,
'security.auth.apiKey', 'security.auth.apiKey',
credentials.apiKey, credentials.apiKey,
); );
} }
if (credentials?.baseUrl != null) { if (credentials?.baseUrl != null) {
settings.setValue( settings.setValue(
scope, authTypeScope,
'security.auth.baseUrl', 'security.auth.baseUrl',
credentials.baseUrl, credentials.baseUrl,
); );
} }
if (credentials?.model != null) { if (credentials?.model != null) {
settings.setValue(scope, 'model.name', credentials.model); settings.setValue(authTypeScope, 'model.name', credentials.model);
} }
} }
} catch (error) { } catch (error) {
@ -139,14 +141,10 @@ export const useAuthCommand = (
); );
const performAuth = useCallback( const performAuth = useCallback(
async ( async (authType: AuthType, credentials?: OpenAICredentials) => {
authType: AuthType,
scope: SettingScope,
credentials?: OpenAICredentials,
) => {
try { try {
await config.refreshAuth(authType); await config.refreshAuth(authType);
handleAuthSuccess(authType, scope, credentials); handleAuthSuccess(authType, credentials);
} catch (e) { } catch (e) {
handleAuthFailure(e); handleAuthFailure(e);
} }
@ -155,11 +153,7 @@ export const useAuthCommand = (
); );
const handleAuthSelect = useCallback( const handleAuthSelect = useCallback(
async ( async (authType: AuthType | undefined, credentials?: OpenAICredentials) => {
authType: AuthType | undefined,
scope: SettingScope,
credentials?: OpenAICredentials,
) => {
if (!authType) { if (!authType) {
setIsAuthDialogOpen(false); setIsAuthDialogOpen(false);
setAuthError(null); setAuthError(null);
@ -178,12 +172,12 @@ export const useAuthCommand = (
baseUrl: credentials.baseUrl, baseUrl: credentials.baseUrl,
model: credentials.model, model: credentials.model,
}); });
await performAuth(authType, scope, credentials); await performAuth(authType, credentials);
} }
return; return;
} }
await performAuth(authType, scope); await performAuth(authType);
}, },
[config, performAuth], [config, performAuth],
); );

View file

@ -25,7 +25,6 @@ import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js';
import { useConfig } from '../contexts/ConfigContext.js'; import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js'; import { useSettings } from '../contexts/SettingsContext.js';
import { SettingScope } from '../../config/settings.js';
import { AuthState } from '../types.js'; import { AuthState } from '../types.js';
import { AuthType } from '@qwen-code/qwen-code-core'; import { AuthType } from '@qwen-code/qwen-code-core';
import process from 'node:process'; import process from 'node:process';
@ -202,7 +201,7 @@ export const DialogManager = ({
return ( return (
<OpenAIKeyPrompt <OpenAIKeyPrompt
onSubmit={(apiKey, baseUrl, model) => { onSubmit={(apiKey, baseUrl, model) => {
uiActions.handleAuthSelect(AuthType.USE_OPENAI, SettingScope.User, { uiActions.handleAuthSelect(AuthType.USE_OPENAI, {
apiKey, apiKey,
baseUrl, baseUrl,
model, model,

View file

@ -30,7 +30,6 @@ export interface UIActions {
) => void; ) => void;
handleAuthSelect: ( handleAuthSelect: (
authType: AuthType | undefined, authType: AuthType | undefined,
scope: SettingScope,
credentials?: OpenAICredentials, credentials?: OpenAICredentials,
) => Promise<void>; ) => Promise<void>;
setAuthState: (state: AuthState) => void; setAuthState: (state: AuthState) => void;

View file

@ -25,7 +25,6 @@ export interface DialogCloseOptions {
isAuthDialogOpen: boolean; isAuthDialogOpen: boolean;
handleAuthSelect: ( handleAuthSelect: (
authType: AuthType | undefined, authType: AuthType | undefined,
scope: SettingScope,
credentials?: OpenAICredentials, credentials?: OpenAICredentials,
) => Promise<void>; ) => Promise<void>;
pendingAuthType: AuthType | undefined; pendingAuthType: AuthType | undefined;