mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 20:20:57 +00:00
Merge pull request #1291 from QwenLM/mingholy/feat/multi-provider-support
feat: multi-provider models config support
This commit is contained in:
commit
b7ac94ecf6
76 changed files with 5885 additions and 1047 deletions
|
|
@ -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 (
|
||||
<OpenAIKeyPrompt
|
||||
onSubmit={(apiKey, baseUrl, model) => {
|
||||
uiActions.handleAuthSelect(AuthType.USE_OPENAI, SettingScope.User, {
|
||||
uiActions.handleAuthSelect(AuthType.USE_OPENAI, {
|
||||
apiKey,
|
||||
baseUrl,
|
||||
model,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import { ModelDialog } from './ModelDialog.js';
|
|||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
AVAILABLE_MODELS_QWEN,
|
||||
MAINLINE_CODER,
|
||||
|
|
@ -36,18 +40,29 @@ const renderComponent = (
|
|||
};
|
||||
const combinedProps = { ...defaultProps, ...props };
|
||||
|
||||
const mockSettings = {
|
||||
isTrusted: true,
|
||||
user: { settings: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const mockConfig = contextValue
|
||||
? ({
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => MAINLINE_CODER),
|
||||
setModel: vi.fn(),
|
||||
setModel: vi.fn().mockResolvedValue(undefined),
|
||||
switchModel: vi.fn().mockResolvedValue(undefined),
|
||||
getAuthType: vi.fn(() => 'qwen-oauth'),
|
||||
|
||||
// --- Functions used by ClearcutLogger ---
|
||||
getUsageStatisticsEnabled: vi.fn(() => true),
|
||||
getSessionId: vi.fn(() => 'mock-session-id'),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: 'mock' })),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
})),
|
||||
getUseSmartEdit: vi.fn(() => false),
|
||||
getUseModelRouter: vi.fn(() => false),
|
||||
getProxy: vi.fn(() => undefined),
|
||||
|
|
@ -58,21 +73,27 @@ const renderComponent = (
|
|||
: undefined;
|
||||
|
||||
const renderResult = render(
|
||||
<ConfigContext.Provider value={mockConfig}>
|
||||
<ModelDialog {...combinedProps} />
|
||||
</ConfigContext.Provider>,
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<ConfigContext.Provider value={mockConfig}>
|
||||
<ModelDialog {...combinedProps} />
|
||||
</ConfigContext.Provider>
|
||||
</SettingsContext.Provider>,
|
||||
);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
props: combinedProps,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
};
|
||||
};
|
||||
|
||||
describe('<ModelDialog />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Ensure env-based fallback models don't leak into this suite from the developer environment.
|
||||
delete process.env['OPENAI_MODEL'];
|
||||
delete process.env['ANTHROPIC_MODEL'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -91,8 +112,12 @@ describe('<ModelDialog />', () => {
|
|||
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
expect(props.items).toHaveLength(AVAILABLE_MODELS_QWEN.length);
|
||||
expect(props.items[0].value).toBe(MAINLINE_CODER);
|
||||
expect(props.items[1].value).toBe(MAINLINE_VLM);
|
||||
expect(props.items[0].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`,
|
||||
);
|
||||
expect(props.items[1].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_VLM}`,
|
||||
);
|
||||
expect(props.showNumbers).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -139,16 +164,93 @@ describe('<ModelDialog />', () => {
|
|||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => {
|
||||
const { props, mockConfig } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
it('calls config.switchModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', async () => {
|
||||
const { props, mockConfig, mockSettings } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
|
||||
childOnSelect(MAINLINE_CODER);
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`);
|
||||
|
||||
// Assert against the default mock provided by renderComponent
|
||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(MAINLINE_CODER);
|
||||
expect(mockConfig?.switchModel).toHaveBeenCalledWith(
|
||||
AuthType.QWEN_OAUTH,
|
||||
MAINLINE_CODER,
|
||||
undefined,
|
||||
{
|
||||
reason: 'user_manual',
|
||||
context: 'Model switched via /model dialog',
|
||||
},
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'model.name',
|
||||
MAINLINE_CODER,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
AuthType.QWEN_OAUTH,
|
||||
);
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls config.switchModel and persists authType+model when selecting a different authType', async () => {
|
||||
const switchModel = vi.fn().mockResolvedValue(undefined);
|
||||
const getAuthType = vi.fn(() => AuthType.USE_OPENAI);
|
||||
const getAvailableModelsForAuthType = vi.fn((t: AuthType) => {
|
||||
if (t === AuthType.USE_OPENAI) {
|
||||
return [{ id: 'gpt-4', label: 'GPT-4', authType: t }];
|
||||
}
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const mockConfigWithSwitchAuthType = {
|
||||
getAuthType,
|
||||
getModel: vi.fn(() => 'gpt-4'),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
})),
|
||||
// Add switchModel to the mock object (not the type)
|
||||
switchModel,
|
||||
getAvailableModelsForAuthType,
|
||||
};
|
||||
|
||||
const { props, mockSettings } = renderComponent(
|
||||
{},
|
||||
// Cast to Config to bypass type checking, matching the runtime behavior
|
||||
mockConfigWithSwitchAuthType as unknown as Partial<Config>,
|
||||
);
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`);
|
||||
|
||||
expect(switchModel).toHaveBeenCalledWith(
|
||||
AuthType.QWEN_OAUTH,
|
||||
MAINLINE_CODER,
|
||||
{ requireCachedCredentials: true },
|
||||
{
|
||||
reason: 'user_manual',
|
||||
context: 'AuthType+model switched via /model dialog',
|
||||
},
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'model.name',
|
||||
MAINLINE_CODER,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
AuthType.QWEN_OAUTH,
|
||||
);
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
@ -193,17 +295,25 @@ describe('<ModelDialog />', () => {
|
|||
it('updates initialIndex when config context changes', () => {
|
||||
const mockGetModel = vi.fn(() => MAINLINE_CODER);
|
||||
const mockGetAuthType = vi.fn(() => 'qwen-oauth');
|
||||
const mockSettings = {
|
||||
isTrusted: true,
|
||||
user: { settings: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
const { rerender } = render(
|
||||
<ConfigContext.Provider
|
||||
value={
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
} as unknown as Config
|
||||
}
|
||||
>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>,
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<ConfigContext.Provider
|
||||
value={
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
} as unknown as Config
|
||||
}
|
||||
>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>
|
||||
</SettingsContext.Provider>,
|
||||
);
|
||||
|
||||
expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0);
|
||||
|
|
@ -215,9 +325,11 @@ describe('<ModelDialog />', () => {
|
|||
} as unknown as Config;
|
||||
|
||||
rerender(
|
||||
<ConfigContext.Provider value={newMockConfig}>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>,
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<ConfigContext.Provider value={newMockConfig}>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>
|
||||
</SettingsContext.Provider>,
|
||||
);
|
||||
|
||||
// Should be called at least twice: initial render + re-render after context change
|
||||
|
|
|
|||
|
|
@ -5,52 +5,210 @@
|
|||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
AuthType,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
type ContentGeneratorConfig,
|
||||
type ContentGeneratorConfigSource,
|
||||
type ContentGeneratorConfigSources,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import {
|
||||
getAvailableModelsForAuthType,
|
||||
MAINLINE_CODER,
|
||||
} from '../models/availableModels.js';
|
||||
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface ModelDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function formatSourceBadge(
|
||||
source: ContentGeneratorConfigSource | undefined,
|
||||
): string | undefined {
|
||||
if (!source) return undefined;
|
||||
|
||||
switch (source.kind) {
|
||||
case 'cli':
|
||||
return source.detail ? `CLI ${source.detail}` : 'CLI';
|
||||
case 'env':
|
||||
return source.envKey ? `ENV ${source.envKey}` : 'ENV';
|
||||
case 'settings':
|
||||
return source.settingsPath
|
||||
? `Settings ${source.settingsPath}`
|
||||
: 'Settings';
|
||||
case 'modelProviders': {
|
||||
const suffix =
|
||||
source.authType && source.modelId
|
||||
? `${source.authType}:${source.modelId}`
|
||||
: source.authType
|
||||
? `${source.authType}`
|
||||
: source.modelId
|
||||
? `${source.modelId}`
|
||||
: '';
|
||||
return suffix ? `ModelProviders ${suffix}` : 'ModelProviders';
|
||||
}
|
||||
case 'default':
|
||||
return source.detail ? `Default ${source.detail}` : 'Default';
|
||||
case 'computed':
|
||||
return source.detail ? `Computed ${source.detail}` : 'Computed';
|
||||
case 'programmatic':
|
||||
return source.detail ? `Programmatic ${source.detail}` : 'Programmatic';
|
||||
case 'unknown':
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readSourcesFromConfig(config: unknown): ContentGeneratorConfigSources {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
const maybe = config as {
|
||||
getContentGeneratorConfigSources?: () => ContentGeneratorConfigSources;
|
||||
};
|
||||
return maybe.getContentGeneratorConfigSources?.() ?? {};
|
||||
}
|
||||
|
||||
function maskApiKey(apiKey: string | undefined): string {
|
||||
if (!apiKey) return '(not set)';
|
||||
const trimmed = apiKey.trim();
|
||||
if (trimmed.length === 0) return '(not set)';
|
||||
if (trimmed.length <= 6) return '***';
|
||||
const head = trimmed.slice(0, 3);
|
||||
const tail = trimmed.slice(-4);
|
||||
return `${head}…${tail}`;
|
||||
}
|
||||
|
||||
function persistModelSelection(
|
||||
settings: ReturnType<typeof useSettings>,
|
||||
modelId: string,
|
||||
): void {
|
||||
const scope = getPersistScopeForModelSelection(settings);
|
||||
settings.setValue(scope, 'model.name', modelId);
|
||||
}
|
||||
|
||||
function persistAuthTypeSelection(
|
||||
settings: ReturnType<typeof useSettings>,
|
||||
authType: AuthType,
|
||||
): void {
|
||||
const scope = getPersistScopeForModelSelection(settings);
|
||||
settings.setValue(scope, 'security.auth.selectedType', authType);
|
||||
}
|
||||
|
||||
function ConfigRow({
|
||||
label,
|
||||
value,
|
||||
badge,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
badge?: string;
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Box minWidth={12} flexShrink={0}>
|
||||
<Text color={theme.text.secondary}>{label}:</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row" flexWrap="wrap">
|
||||
<Text>{value}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{badge ? (
|
||||
<Box>
|
||||
<Box minWidth={12} flexShrink={0}>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={theme.text.secondary}>{badge}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const uiState = useContext(UIStateContext);
|
||||
const settings = useSettings();
|
||||
|
||||
// Local error state for displaying errors within the dialog
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Get auth type from config, default to QWEN_OAUTH if not available
|
||||
const authType = config?.getAuthType() ?? AuthType.QWEN_OAUTH;
|
||||
const effectiveConfig =
|
||||
(config?.getContentGeneratorConfig?.() as
|
||||
| ContentGeneratorConfig
|
||||
| undefined) ?? undefined;
|
||||
const sources = readSourcesFromConfig(config);
|
||||
|
||||
// Get available models based on auth type
|
||||
const availableModels = useMemo(
|
||||
() => getAvailableModelsForAuthType(authType),
|
||||
[authType],
|
||||
);
|
||||
const availableModelEntries = useMemo(() => {
|
||||
const allAuthTypes = Object.values(AuthType) as AuthType[];
|
||||
const modelsByAuthType = allAuthTypes
|
||||
.map((t) => ({
|
||||
authType: t,
|
||||
models: getAvailableModelsForAuthType(t, config ?? undefined),
|
||||
}))
|
||||
.filter((x) => x.models.length > 0);
|
||||
|
||||
// Fixed order: qwen-oauth first, then others in a stable order
|
||||
const authTypeOrder: AuthType[] = [
|
||||
AuthType.QWEN_OAUTH,
|
||||
AuthType.USE_OPENAI,
|
||||
AuthType.USE_ANTHROPIC,
|
||||
AuthType.USE_GEMINI,
|
||||
AuthType.USE_VERTEX_AI,
|
||||
];
|
||||
|
||||
// Filter to only include authTypes that have models
|
||||
const availableAuthTypes = new Set(modelsByAuthType.map((x) => x.authType));
|
||||
const orderedAuthTypes = authTypeOrder.filter((t) =>
|
||||
availableAuthTypes.has(t),
|
||||
);
|
||||
|
||||
return orderedAuthTypes.flatMap((t) => {
|
||||
const models =
|
||||
modelsByAuthType.find((x) => x.authType === t)?.models ?? [];
|
||||
return models.map((m) => ({ authType: t, model: m }));
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const MODEL_OPTIONS = useMemo(
|
||||
() =>
|
||||
availableModels.map((model) => ({
|
||||
value: model.id,
|
||||
title: model.label,
|
||||
description: model.description || '',
|
||||
key: model.id,
|
||||
})),
|
||||
[availableModels],
|
||||
availableModelEntries.map(({ authType: t2, model }) => {
|
||||
const value = `${t2}::${model.id}`;
|
||||
const title = (
|
||||
<Text>
|
||||
<Text bold color={theme.text.accent}>
|
||||
[{t2}]
|
||||
</Text>
|
||||
<Text>{` ${model.label}`}</Text>
|
||||
</Text>
|
||||
);
|
||||
const description = model.description || '';
|
||||
return {
|
||||
value,
|
||||
title,
|
||||
description,
|
||||
key: value,
|
||||
};
|
||||
}),
|
||||
[availableModelEntries],
|
||||
);
|
||||
|
||||
// Determine the Preferred Model (read once when the dialog opens).
|
||||
const preferredModel = config?.getModel() || MAINLINE_CODER;
|
||||
const preferredModelId = config?.getModel() || MAINLINE_CODER;
|
||||
const preferredKey = `${authType}::${preferredModelId}`;
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
|
|
@ -61,25 +219,81 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
{ isActive: true },
|
||||
);
|
||||
|
||||
// Calculate the initial index based on the preferred model.
|
||||
const initialIndex = useMemo(
|
||||
() => MODEL_OPTIONS.findIndex((option) => option.value === preferredModel),
|
||||
[MODEL_OPTIONS, preferredModel],
|
||||
() => MODEL_OPTIONS.findIndex((option) => option.value === preferredKey),
|
||||
[MODEL_OPTIONS, preferredKey],
|
||||
);
|
||||
|
||||
// Handle selection internally (Autonomous Dialog).
|
||||
const handleSelect = useCallback(
|
||||
(model: string) => {
|
||||
async (selected: string) => {
|
||||
// Clear any previous error
|
||||
setErrorMessage(null);
|
||||
|
||||
const sep = '::';
|
||||
const idx = selected.indexOf(sep);
|
||||
const selectedAuthType = (
|
||||
idx >= 0 ? selected.slice(0, idx) : authType
|
||||
) as AuthType;
|
||||
const modelId = idx >= 0 ? selected.slice(idx + sep.length) : selected;
|
||||
|
||||
if (config) {
|
||||
config.setModel(model);
|
||||
const event = new ModelSlashCommandEvent(model);
|
||||
try {
|
||||
await config.switchModel(
|
||||
selectedAuthType,
|
||||
modelId,
|
||||
selectedAuthType !== authType &&
|
||||
selectedAuthType === AuthType.QWEN_OAUTH
|
||||
? { requireCachedCredentials: true }
|
||||
: undefined,
|
||||
{
|
||||
reason: 'user_manual',
|
||||
context:
|
||||
selectedAuthType === authType
|
||||
? 'Model switched via /model dialog'
|
||||
: 'AuthType+model switched via /model dialog',
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
const baseErrorMessage = e instanceof Error ? e.message : String(e);
|
||||
setErrorMessage(
|
||||
`Failed to switch model to '${modelId}'.\n\n${baseErrorMessage}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const event = new ModelSlashCommandEvent(modelId);
|
||||
logModelSlashCommand(config, event);
|
||||
|
||||
const after = config.getContentGeneratorConfig?.() as
|
||||
| ContentGeneratorConfig
|
||||
| undefined;
|
||||
const effectiveAuthType =
|
||||
after?.authType ?? selectedAuthType ?? authType;
|
||||
const effectiveModelId = after?.model ?? modelId;
|
||||
|
||||
persistModelSelection(settings, effectiveModelId);
|
||||
persistAuthTypeSelection(settings, effectiveAuthType);
|
||||
|
||||
const baseUrl = after?.baseUrl ?? '(default)';
|
||||
const maskedKey = maskApiKey(after?.apiKey);
|
||||
uiState?.historyManager.addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text:
|
||||
`authType: ${effectiveAuthType}\n` +
|
||||
`Using model: ${effectiveModelId}\n` +
|
||||
`Base URL: ${baseUrl}\n` +
|
||||
`API key: ${maskedKey}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
onClose();
|
||||
},
|
||||
[config, onClose],
|
||||
[authType, config, onClose, settings, uiState, setErrorMessage],
|
||||
);
|
||||
|
||||
const hasModels = MODEL_OPTIONS.length > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
|
|
@ -89,14 +303,73 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
width="100%"
|
||||
>
|
||||
<Text bold>{t('Select Model')}</Text>
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={MODEL_OPTIONS}
|
||||
onSelect={handleSelect}
|
||||
initialIndex={initialIndex}
|
||||
showNumbers={true}
|
||||
/>
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Current (effective) configuration')}
|
||||
</Text>
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<ConfigRow label="AuthType" value={authType} />
|
||||
<ConfigRow
|
||||
label="Model"
|
||||
value={effectiveConfig?.model ?? config?.getModel?.() ?? ''}
|
||||
badge={formatSourceBadge(sources['model'])}
|
||||
/>
|
||||
|
||||
{authType !== AuthType.QWEN_OAUTH && (
|
||||
<>
|
||||
<ConfigRow
|
||||
label="Base URL"
|
||||
value={effectiveConfig?.baseUrl ?? ''}
|
||||
badge={formatSourceBadge(sources['baseUrl'])}
|
||||
/>
|
||||
<ConfigRow
|
||||
label="API Key"
|
||||
value={effectiveConfig?.apiKey ? t('(set)') : t('(not set)')}
|
||||
badge={formatSourceBadge(sources['apiKey'])}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{!hasModels ? (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.status.warning}>
|
||||
{t(
|
||||
'No models available for the current authentication type ({{authType}}).',
|
||||
{
|
||||
authType,
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t(
|
||||
'Please configure models in settings.modelProviders or use environment variables.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={MODEL_OPTIONS}
|
||||
onSelect={handleSelect}
|
||||
initialIndex={initialIndex}
|
||||
showNumbers={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<Box marginTop={1} flexDirection="column" paddingX={1}>
|
||||
<Text color={theme.status.error} wrap="wrap">
|
||||
✕ {errorMessage}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>{t('(Press Esc to close)')}</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { BaseSelectionList } from './BaseSelectionList.js';
|
|||
import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
|
||||
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
description: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue