mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-23 17:26:23 +00:00
Compare commits
4 Commits
refactor/a
...
mingholy/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14826dd2a9 | ||
|
|
6eb16c0bcf | ||
|
|
7fa1dcb0e6 | ||
|
|
03f12bfa3f |
@@ -34,6 +34,10 @@ import { ExtensionEnablementManager } from '../config/extensions/extensionEnable
|
||||
|
||||
// Import the modular Session class
|
||||
import { Session } from './session/Session.js';
|
||||
import {
|
||||
formatAcpModelId,
|
||||
parseAcpBaseModelId,
|
||||
} from '../utils/acpModelUtils.js';
|
||||
|
||||
export async function runAcpAgent(
|
||||
config: Config,
|
||||
@@ -381,15 +385,24 @@ class GeminiAgent {
|
||||
private buildAvailableModels(
|
||||
config: Config,
|
||||
): acp.NewSessionResponse['models'] {
|
||||
const currentModelId = (
|
||||
const rawCurrentModelId = (
|
||||
config.getModel() ||
|
||||
this.config.getModel() ||
|
||||
''
|
||||
).trim();
|
||||
const availableModels = config.getAvailableModels();
|
||||
const currentAuthType = config.getAuthType();
|
||||
const allConfiguredModels = config.getAllConfiguredModels();
|
||||
|
||||
const baseCurrentModelId = parseAcpBaseModelId(rawCurrentModelId);
|
||||
const currentModelId =
|
||||
currentAuthType && baseCurrentModelId
|
||||
? formatAcpModelId(baseCurrentModelId, currentAuthType)
|
||||
: baseCurrentModelId;
|
||||
|
||||
const availableModels = allConfiguredModels;
|
||||
|
||||
const mappedAvailableModels = availableModels.map((model) => ({
|
||||
modelId: model.id,
|
||||
modelId: formatAcpModelId(model.id, model.authType),
|
||||
name: model.label,
|
||||
description: model.description ?? null,
|
||||
_meta: {
|
||||
@@ -406,7 +419,7 @@ class GeminiAgent {
|
||||
name: currentModelId,
|
||||
description: null,
|
||||
_meta: {
|
||||
contextLimit: tokenLimit(currentModelId),
|
||||
contextLimit: tokenLimit(baseCurrentModelId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Session } from './Session.js';
|
||||
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { ApprovalMode, AuthType } from '@qwen-code/qwen-code-core';
|
||||
import type * as acp from '../acp.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import * as nonInteractiveCliCommands from '../../nonInteractiveCliCommands.js';
|
||||
@@ -24,14 +24,19 @@ describe('Session', () => {
|
||||
let mockSettings: LoadedSettings;
|
||||
let session: Session;
|
||||
let currentModel: string;
|
||||
let setModelSpy: ReturnType<typeof vi.fn>;
|
||||
let currentAuthType: AuthType;
|
||||
let switchModelSpy: ReturnType<typeof vi.fn>;
|
||||
let getAvailableCommandsSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
currentModel = 'qwen3-code-plus';
|
||||
setModelSpy = vi.fn().mockImplementation(async (modelId: string) => {
|
||||
currentModel = modelId;
|
||||
});
|
||||
currentAuthType = AuthType.USE_OPENAI;
|
||||
switchModelSpy = vi
|
||||
.fn()
|
||||
.mockImplementation(async (authType: AuthType, modelId: string) => {
|
||||
currentAuthType = authType;
|
||||
currentModel = modelId;
|
||||
});
|
||||
|
||||
mockChat = {
|
||||
sendMessageStream: vi.fn(),
|
||||
@@ -40,8 +45,9 @@ describe('Session', () => {
|
||||
|
||||
mockConfig = {
|
||||
setApprovalMode: vi.fn(),
|
||||
setModel: setModelSpy,
|
||||
switchModel: switchModelSpy,
|
||||
getModel: vi.fn().mockImplementation(() => currentModel),
|
||||
getAuthType: vi.fn().mockImplementation(() => currentAuthType),
|
||||
} as unknown as Config;
|
||||
|
||||
mockClient = {
|
||||
@@ -88,17 +94,25 @@ describe('Session', () => {
|
||||
|
||||
describe('setModel', () => {
|
||||
it('sets model via config and returns current model', async () => {
|
||||
const requested = `qwen3-coder-plus(${AuthType.USE_OPENAI})`;
|
||||
const result = await session.setModel({
|
||||
sessionId: 'test-session-id',
|
||||
modelId: ' qwen3-coder-plus ',
|
||||
modelId: ` ${requested} `,
|
||||
});
|
||||
|
||||
expect(mockConfig.setModel).toHaveBeenCalledWith('qwen3-coder-plus', {
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
});
|
||||
expect(mockConfig.switchModel).toHaveBeenCalledWith(
|
||||
AuthType.USE_OPENAI,
|
||||
'qwen3-coder-plus',
|
||||
undefined,
|
||||
{
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
},
|
||||
);
|
||||
expect(mockConfig.getModel).toHaveBeenCalled();
|
||||
expect(result).toEqual({ modelId: 'qwen3-coder-plus' });
|
||||
expect(result).toEqual({
|
||||
modelId: `qwen3-coder-plus(${AuthType.USE_OPENAI})`,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects empty/whitespace model IDs', async () => {
|
||||
@@ -109,17 +123,17 @@ describe('Session', () => {
|
||||
}),
|
||||
).rejects.toThrow('Invalid params');
|
||||
|
||||
expect(mockConfig.setModel).not.toHaveBeenCalled();
|
||||
expect(mockConfig.switchModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates errors from config.setModel', async () => {
|
||||
it('propagates errors from config.switchModel', async () => {
|
||||
const configError = new Error('Invalid model');
|
||||
setModelSpy.mockRejectedValueOnce(configError);
|
||||
switchModelSpy.mockRejectedValueOnce(configError);
|
||||
|
||||
await expect(
|
||||
session.setModel({
|
||||
sessionId: 'test-session-id',
|
||||
modelId: 'invalid-model',
|
||||
modelId: `invalid-model(${AuthType.USE_OPENAI})`,
|
||||
}),
|
||||
).rejects.toThrow('Invalid model');
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SubAgentEventEmitter,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
AuthType,
|
||||
ApprovalMode,
|
||||
convertToFunctionResponse,
|
||||
DiscoveredMCPTool,
|
||||
@@ -58,6 +59,10 @@ import type {
|
||||
CurrentModeUpdate,
|
||||
} from '../schema.js';
|
||||
import { isSlashCommand } from '../../ui/utils/commandUtils.js';
|
||||
import {
|
||||
formatAcpModelId,
|
||||
parseAcpModelOption,
|
||||
} from '../../utils/acpModelUtils.js';
|
||||
|
||||
// Import modular session components
|
||||
import type { SessionContext, ToolCallStartParams } from './types.js';
|
||||
@@ -355,23 +360,39 @@ export class Session implements SessionContext {
|
||||
* Validates the model ID and switches the model via Config.
|
||||
*/
|
||||
async setModel(params: SetModelRequest): Promise<SetModelResponse> {
|
||||
const modelId = params.modelId.trim();
|
||||
const rawModelId = params.modelId.trim();
|
||||
|
||||
if (!modelId) {
|
||||
if (!rawModelId) {
|
||||
throw acp.RequestError.invalidParams('modelId cannot be empty');
|
||||
}
|
||||
|
||||
// Attempt to set the model using config
|
||||
await this.config.setModel(modelId, {
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
});
|
||||
const parsed = parseAcpModelOption(rawModelId);
|
||||
const previousAuthType = this.config.getAuthType?.();
|
||||
const selectedAuthType = parsed.authType ?? previousAuthType;
|
||||
|
||||
if (!selectedAuthType) {
|
||||
throw acp.RequestError.invalidParams('authType cannot be determined');
|
||||
}
|
||||
|
||||
await this.config.switchModel(
|
||||
selectedAuthType,
|
||||
parsed.modelId,
|
||||
selectedAuthType !== previousAuthType &&
|
||||
selectedAuthType === AuthType.QWEN_OAUTH
|
||||
? { requireCachedCredentials: true }
|
||||
: undefined,
|
||||
{
|
||||
reason: 'user_request_acp',
|
||||
context: 'session/set_model',
|
||||
},
|
||||
);
|
||||
|
||||
// Get updated model info
|
||||
const currentModel = this.config.getModel();
|
||||
const currentAuthType = this.config.getAuthType?.() ?? selectedAuthType;
|
||||
|
||||
return {
|
||||
modelId: currentModel,
|
||||
modelId: formatAcpModelId(currentModel, currentAuthType),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -47,30 +47,36 @@ const renderComponent = (
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const mockConfig = contextValue
|
||||
? ({
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => MAINLINE_CODER),
|
||||
setModel: vi.fn().mockResolvedValue(undefined),
|
||||
switchModel: vi.fn().mockResolvedValue(undefined),
|
||||
getAuthType: vi.fn(() => 'qwen-oauth'),
|
||||
const mockConfig = {
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => MAINLINE_CODER),
|
||||
setModel: vi.fn().mockResolvedValue(undefined),
|
||||
switchModel: vi.fn().mockResolvedValue(undefined),
|
||||
getAuthType: vi.fn(() => 'qwen-oauth'),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
})),
|
||||
),
|
||||
|
||||
// --- Functions used by ClearcutLogger ---
|
||||
getUsageStatisticsEnabled: vi.fn(() => true),
|
||||
getSessionId: vi.fn(() => 'mock-session-id'),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
})),
|
||||
getUseSmartEdit: vi.fn(() => false),
|
||||
getUseModelRouter: vi.fn(() => false),
|
||||
getProxy: vi.fn(() => undefined),
|
||||
// --- Functions used by ClearcutLogger ---
|
||||
getUsageStatisticsEnabled: vi.fn(() => true),
|
||||
getSessionId: vi.fn(() => 'mock-session-id'),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
})),
|
||||
getUseSmartEdit: vi.fn(() => false),
|
||||
getUseModelRouter: vi.fn(() => false),
|
||||
getProxy: vi.fn(() => undefined),
|
||||
|
||||
// --- Spread test-specific overrides ---
|
||||
...contextValue,
|
||||
} as unknown as Config)
|
||||
: undefined;
|
||||
// --- Spread test-specific overrides ---
|
||||
...(contextValue ?? {}),
|
||||
} as unknown as Config;
|
||||
|
||||
const renderResult = render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
@@ -308,6 +314,14 @@ describe('<ModelDialog />', () => {
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
})),
|
||||
),
|
||||
} as unknown as Config
|
||||
}
|
||||
>
|
||||
@@ -322,6 +336,14 @@ describe('<ModelDialog />', () => {
|
||||
const newMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
})),
|
||||
),
|
||||
} as unknown as Config;
|
||||
|
||||
rerender(
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
AuthType,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
type AvailableModel as CoreAvailableModel,
|
||||
type ContentGeneratorConfig,
|
||||
type ContentGeneratorConfigSource,
|
||||
type ContentGeneratorConfigSources,
|
||||
@@ -21,10 +22,7 @@ import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSel
|
||||
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 { MAINLINE_CODER } from '../models/availableModels.js';
|
||||
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
@@ -154,13 +152,17 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const sources = readSourcesFromConfig(config);
|
||||
|
||||
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);
|
||||
const allModels = config ? config.getAllConfiguredModels() : [];
|
||||
|
||||
// Group models by authType
|
||||
const modelsByAuthTypeMap = new Map<AuthType, CoreAvailableModel[]>();
|
||||
for (const model of allModels) {
|
||||
const authType = model.authType;
|
||||
if (!modelsByAuthTypeMap.has(authType)) {
|
||||
modelsByAuthTypeMap.set(authType, []);
|
||||
}
|
||||
modelsByAuthTypeMap.get(authType)!.push(model);
|
||||
}
|
||||
|
||||
// Fixed order: qwen-oauth first, then others in a stable order
|
||||
const authTypeOrder: AuthType[] = [
|
||||
@@ -171,15 +173,14 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
AuthType.USE_VERTEX_AI,
|
||||
];
|
||||
|
||||
// Filter to only include authTypes that have models
|
||||
const availableAuthTypes = new Set(modelsByAuthType.map((x) => x.authType));
|
||||
// Filter to only include authTypes that have models and maintain order
|
||||
const availableAuthTypes = new Set(modelsByAuthTypeMap.keys());
|
||||
const orderedAuthTypes = authTypeOrder.filter((t) =>
|
||||
availableAuthTypes.has(t),
|
||||
);
|
||||
|
||||
return orderedAuthTypes.flatMap((t) => {
|
||||
const models =
|
||||
modelsByAuthType.find((x) => x.authType === t)?.models ?? [];
|
||||
const models = modelsByAuthTypeMap.get(t) ?? [];
|
||||
return models.map((m) => ({ authType: t, model: m }));
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
42
packages/cli/src/utils/acpModelUtils.test.ts
Normal file
42
packages/cli/src/utils/acpModelUtils.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
formatAcpModelId,
|
||||
parseAcpBaseModelId,
|
||||
parseAcpModelOption,
|
||||
} from './acpModelUtils.js';
|
||||
|
||||
describe('acpModelUtils', () => {
|
||||
it('formats modelId(authType)', () => {
|
||||
expect(formatAcpModelId('qwen3', AuthType.QWEN_OAUTH)).toBe(
|
||||
`qwen3(${AuthType.QWEN_OAUTH})`,
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts base model id when string ends with parentheses', () => {
|
||||
expect(parseAcpBaseModelId(`qwen3(${AuthType.USE_OPENAI})`)).toBe('qwen3');
|
||||
});
|
||||
|
||||
it('does not strip when parentheses are not a trailing suffix', () => {
|
||||
expect(parseAcpBaseModelId('qwen3(x) y')).toBe('qwen3(x) y');
|
||||
});
|
||||
|
||||
it('parses modelId and validates authType', () => {
|
||||
expect(parseAcpModelOption(` qwen3(${AuthType.USE_OPENAI}) `)).toEqual({
|
||||
modelId: 'qwen3',
|
||||
authType: AuthType.USE_OPENAI,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns trimmed input as modelId when authType is invalid', () => {
|
||||
expect(parseAcpModelOption('qwen3(not-a-real-auth)')).toEqual({
|
||||
modelId: 'qwen3(not-a-real-auth)',
|
||||
});
|
||||
});
|
||||
});
|
||||
55
packages/cli/src/utils/acpModelUtils.ts
Normal file
55
packages/cli/src/utils/acpModelUtils.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* ACP model IDs are represented as `${modelId}(${authType})` in the ACP protocol.
|
||||
*/
|
||||
export function formatAcpModelId(modelId: string, authType: AuthType): string {
|
||||
return `${modelId}(${authType})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the base model id from an ACP model id string.
|
||||
*
|
||||
* If the string ends with `(...)`, the suffix is removed; otherwise returns the
|
||||
* trimmed input as-is.
|
||||
*/
|
||||
export function parseAcpBaseModelId(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const closeIdx = trimmed.lastIndexOf(')');
|
||||
const openIdx = trimmed.lastIndexOf('(');
|
||||
if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {
|
||||
return trimmed.slice(0, openIdx);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ACP model option string into `{ modelId, authType? }`.
|
||||
*
|
||||
* If the string ends with `(...)` and `...` is a valid `AuthType`, returns both;
|
||||
* otherwise returns the trimmed input as `modelId` only.
|
||||
*/
|
||||
export function parseAcpModelOption(input: string): {
|
||||
modelId: string;
|
||||
authType?: AuthType;
|
||||
} {
|
||||
const trimmed = input.trim();
|
||||
const closeIdx = trimmed.lastIndexOf(')');
|
||||
const openIdx = trimmed.lastIndexOf('(');
|
||||
if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {
|
||||
const maybeModelId = trimmed.slice(0, openIdx);
|
||||
const maybeAuthType = trimmed.slice(openIdx + 1, closeIdx);
|
||||
const parsedAuthType = z.nativeEnum(AuthType).safeParse(maybeAuthType);
|
||||
if (parsedAuthType.success) {
|
||||
return { modelId: maybeModelId, authType: parsedAuthType.data };
|
||||
}
|
||||
}
|
||||
return { modelId: trimmed };
|
||||
}
|
||||
@@ -921,6 +921,14 @@ export class Config {
|
||||
return this._modelsConfig.getAvailableModelsForAuthType(authType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured models across authTypes.
|
||||
* Delegates to ModelsConfig.
|
||||
*/
|
||||
getAllConfiguredModels(authTypes?: AuthType[]): AvailableModel[] {
|
||||
return this._modelsConfig.getAllConfiguredModels(authTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch authType+model via registry-backed selection.
|
||||
* This triggers a refresh of the ContentGenerator when required (always on authType changes).
|
||||
|
||||
@@ -102,16 +102,14 @@ export const QWEN_OAUTH_ALLOWED_MODELS = [
|
||||
export const QWEN_OAUTH_MODELS: ModelConfig[] = [
|
||||
{
|
||||
id: 'coder-model',
|
||||
name: 'Qwen Coder',
|
||||
description:
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
|
||||
name: 'coder-model',
|
||||
description: 'The latest Qwen Coder model from Alibaba Cloud ModelStudio',
|
||||
capabilities: { vision: false },
|
||||
},
|
||||
{
|
||||
id: 'vision-model',
|
||||
name: 'Qwen Vision',
|
||||
description:
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
|
||||
name: 'vision-model',
|
||||
description: 'The latest Qwen Vision model from Alibaba Cloud ModelStudio',
|
||||
capabilities: { vision: true },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -679,8 +679,8 @@ describe('ModelsConfig', () => {
|
||||
expect(modelsConfig.getGenerationConfig().model).toBe('updated-model');
|
||||
});
|
||||
|
||||
describe('getAllAvailableModels', () => {
|
||||
it('should return all models across all authTypes', () => {
|
||||
describe('getAllConfiguredModels', () => {
|
||||
it('should return all models across all authTypes and put qwen-oauth first', () => {
|
||||
const modelProvidersConfig: ModelProvidersConfig = {
|
||||
openai: [
|
||||
{
|
||||
@@ -718,7 +718,23 @@ describe('ModelsConfig', () => {
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
const allModels = modelsConfig.getAllAvailableModels();
|
||||
const allModels = modelsConfig.getAllConfiguredModels();
|
||||
|
||||
// qwen-oauth models should be ordered first
|
||||
const firstNonQwenIndex = allModels.findIndex(
|
||||
(m) => m.authType !== AuthType.QWEN_OAUTH,
|
||||
);
|
||||
expect(firstNonQwenIndex).toBeGreaterThan(0);
|
||||
expect(
|
||||
allModels
|
||||
.slice(0, firstNonQwenIndex)
|
||||
.every((m) => m.authType === AuthType.QWEN_OAUTH),
|
||||
).toBe(true);
|
||||
expect(
|
||||
allModels
|
||||
.slice(firstNonQwenIndex)
|
||||
.every((m) => m.authType !== AuthType.QWEN_OAUTH),
|
||||
).toBe(true);
|
||||
|
||||
// Should include qwen-oauth models (hard-coded)
|
||||
const qwenModels = allModels.filter(
|
||||
@@ -752,7 +768,7 @@ describe('ModelsConfig', () => {
|
||||
it('should return empty array when no models are registered', () => {
|
||||
const modelsConfig = new ModelsConfig();
|
||||
|
||||
const allModels = modelsConfig.getAllAvailableModels();
|
||||
const allModels = modelsConfig.getAllConfiguredModels();
|
||||
|
||||
// Should still include qwen-oauth models (hard-coded)
|
||||
expect(allModels.length).toBeGreaterThan(0);
|
||||
@@ -782,7 +798,7 @@ describe('ModelsConfig', () => {
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
const allModels = modelsConfig.getAllAvailableModels();
|
||||
const allModels = modelsConfig.getAllConfiguredModels();
|
||||
const testModel = allModels.find((m) => m.id === 'test-model');
|
||||
|
||||
expect(testModel).toBeDefined();
|
||||
@@ -793,5 +809,56 @@ describe('ModelsConfig', () => {
|
||||
expect(testModel?.isVision).toBe(true);
|
||||
expect(testModel?.capabilities?.vision).toBe(true);
|
||||
});
|
||||
|
||||
it('should support filtering by authTypes and still put qwen-oauth first when included', () => {
|
||||
const modelProvidersConfig: ModelProvidersConfig = {
|
||||
openai: [
|
||||
{
|
||||
id: 'openai-model-1',
|
||||
name: 'OpenAI Model 1',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
anthropic: [
|
||||
{
|
||||
id: 'anthropic-model-1',
|
||||
name: 'Anthropic Model 1',
|
||||
baseUrl: 'https://api.anthropic.com/v1',
|
||||
envKey: 'ANTHROPIC_API_KEY',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const modelsConfig = new ModelsConfig({
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
// Filter: OpenAI only (should not include qwen-oauth)
|
||||
const openaiOnly = modelsConfig.getAllConfiguredModels([
|
||||
AuthType.USE_OPENAI,
|
||||
]);
|
||||
expect(openaiOnly.every((m) => m.authType === AuthType.USE_OPENAI)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(openaiOnly.map((m) => m.id)).toContain('openai-model-1');
|
||||
|
||||
// Filter: include qwen-oauth but request it later -> still ordered first
|
||||
const withQwen = modelsConfig.getAllConfiguredModels([
|
||||
AuthType.USE_OPENAI,
|
||||
AuthType.QWEN_OAUTH,
|
||||
AuthType.USE_ANTHROPIC,
|
||||
]);
|
||||
expect(withQwen.length).toBeGreaterThan(0);
|
||||
const firstNonQwenIndex = withQwen.findIndex(
|
||||
(m) => m.authType !== AuthType.QWEN_OAUTH,
|
||||
);
|
||||
expect(firstNonQwenIndex).toBeGreaterThan(0);
|
||||
expect(
|
||||
withQwen
|
||||
.slice(0, firstNonQwenIndex)
|
||||
.every((m) => m.authType === AuthType.QWEN_OAUTH),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,13 +204,40 @@ export class ModelsConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available models across all authTypes
|
||||
* Get all configured models across authTypes.
|
||||
*
|
||||
* Notes:
|
||||
* - By default, returns models across all authTypes.
|
||||
* - qwen-oauth models are always ordered first.
|
||||
*/
|
||||
getAllAvailableModels(): AvailableModel[] {
|
||||
getAllConfiguredModels(authTypes?: AuthType[]): AvailableModel[] {
|
||||
const inputAuthTypes =
|
||||
authTypes && authTypes.length > 0 ? authTypes : Object.values(AuthType);
|
||||
|
||||
// De-duplicate while preserving the original order.
|
||||
const seen = new Set<AuthType>();
|
||||
const uniqueAuthTypes: AuthType[] = [];
|
||||
for (const authType of inputAuthTypes) {
|
||||
if (!seen.has(authType)) {
|
||||
seen.add(authType);
|
||||
uniqueAuthTypes.push(authType);
|
||||
}
|
||||
}
|
||||
|
||||
// Force qwen-oauth to the front (if requested / defaulted in).
|
||||
const orderedAuthTypes: AuthType[] = [];
|
||||
if (uniqueAuthTypes.includes(AuthType.QWEN_OAUTH)) {
|
||||
orderedAuthTypes.push(AuthType.QWEN_OAUTH);
|
||||
}
|
||||
for (const authType of uniqueAuthTypes) {
|
||||
if (authType !== AuthType.QWEN_OAUTH) {
|
||||
orderedAuthTypes.push(authType);
|
||||
}
|
||||
}
|
||||
|
||||
const allModels: AvailableModel[] = [];
|
||||
for (const authType of Object.values(AuthType)) {
|
||||
const models = this.modelRegistry.getModelsForAuthType(authType);
|
||||
allModels.push(...models);
|
||||
for (const authType of orderedAuthTypes) {
|
||||
allModels.push(...this.modelRegistry.getModelsForAuthType(authType));
|
||||
}
|
||||
return allModels;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user