mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
feat: update model configs and tests for qwen3.5-plus
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
44b8ff729e
commit
f90ed9efe9
10 changed files with 147 additions and 82 deletions
|
|
@ -16,7 +16,7 @@ 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,
|
||||
getFilteredQwenModels,
|
||||
MAINLINE_CODER,
|
||||
MAINLINE_VLM,
|
||||
} from '../models/availableModels.js';
|
||||
|
|
@ -29,6 +29,19 @@ const mockedUseKeypress = vi.mocked(useKeypress);
|
|||
vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({
|
||||
DescriptiveRadioButtonSelect: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
// Helper to create getAvailableModelsForAuthType mock
|
||||
const createMockGetAvailableModelsForAuthType = () =>
|
||||
vi.fn((t: AuthType) => {
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect);
|
||||
|
||||
const renderComponent = (
|
||||
|
|
@ -54,7 +67,7 @@ const renderComponent = (
|
|||
switchModel: vi.fn().mockResolvedValue(undefined),
|
||||
getAuthType: vi.fn(() => 'qwen-oauth'),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -116,24 +129,38 @@ describe('<ModelDialog />', () => {
|
|||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
expect(props.items).toHaveLength(AVAILABLE_MODELS_QWEN.length);
|
||||
expect(props.items).toHaveLength(getFilteredQwenModels(true).length);
|
||||
expect(props.items[0].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`,
|
||||
);
|
||||
expect(props.items[1].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_VLM}`,
|
||||
// Find vision model in the list (it's not necessarily at index 1 anymore)
|
||||
const visionModelItem = props.items.find(
|
||||
(item) =>
|
||||
typeof item.value === 'string' &&
|
||||
item.value.endsWith(`::${MAINLINE_VLM}`),
|
||||
);
|
||||
expect(visionModelItem).toBeDefined();
|
||||
expect(props.showNumbers).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes with the model from ConfigContext', () => {
|
||||
const mockGetModel = vi.fn(() => MAINLINE_VLM);
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
renderComponent(
|
||||
{},
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
// Calculate expected index dynamically based on model list
|
||||
const qwenModels = getFilteredQwenModels(true);
|
||||
const expectedIndex = qwenModels.findIndex((m) => m.id === MAINLINE_VLM);
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialIndex: 1,
|
||||
initialIndex: expectedIndex,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
|
@ -151,10 +178,15 @@ describe('<ModelDialog />', () => {
|
|||
});
|
||||
|
||||
it('initializes with default coder model if getModel returns undefined', () => {
|
||||
const mockGetModel = vi.fn(() => undefined);
|
||||
// @ts-expect-error This test validates component robustness when getModel
|
||||
// returns an unexpected undefined value.
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
const mockGetModel = vi.fn(() => undefined as unknown as string);
|
||||
renderComponent(
|
||||
{},
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
|
||||
|
|
@ -170,7 +202,21 @@ describe('<ModelDialog />', () => {
|
|||
});
|
||||
|
||||
it('calls config.switchModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', async () => {
|
||||
const { props, mockConfig, mockSettings } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
const { props, mockConfig, mockSettings } = renderComponent(
|
||||
{},
|
||||
{
|
||||
getAvailableModelsForAuthType: vi.fn((t: AuthType) => {
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
|
|
@ -203,7 +249,7 @@ describe('<ModelDialog />', () => {
|
|||
return [{ id: 'gpt-4', label: 'GPT-4', authType: t }];
|
||||
}
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
return getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
|
|
@ -305,8 +351,10 @@ describe('<ModelDialog />', () => {
|
|||
{
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -321,14 +369,16 @@ describe('<ModelDialog />', () => {
|
|||
</SettingsContext.Provider>,
|
||||
);
|
||||
|
||||
// MAINLINE_CODER (qwen3.5-plus) is at index 0
|
||||
expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0);
|
||||
|
||||
mockGetModel.mockReturnValue(MAINLINE_VLM);
|
||||
const newMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAvailableModelsForAuthType: createMockGetAvailableModelsForAuthType(),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels(true).map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -347,6 +397,9 @@ describe('<ModelDialog />', () => {
|
|||
|
||||
// Should be called at least twice: initial render + re-render after context change
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(2);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(1);
|
||||
// Calculate expected index for MAINLINE_VLM dynamically
|
||||
const qwenModels = getFilteredQwenModels(true);
|
||||
const expectedVlmIndex = qwenModels.findIndex((m) => m.id === MAINLINE_VLM);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(expectedVlmIndex);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,26 +11,23 @@ import {
|
|||
getOpenAIAvailableModelFromEnv,
|
||||
isVisionModel,
|
||||
getDefaultVisionModel,
|
||||
AVAILABLE_MODELS_QWEN,
|
||||
MAINLINE_VLM,
|
||||
MAINLINE_CODER,
|
||||
} from './availableModels.js';
|
||||
import { AuthType, type Config } from '@qwen-code/qwen-code-core';
|
||||
|
||||
describe('availableModels', () => {
|
||||
describe('AVAILABLE_MODELS_QWEN', () => {
|
||||
describe('Qwen models', () => {
|
||||
const qwenModels = getFilteredQwenModels(true);
|
||||
|
||||
it('should include coder model', () => {
|
||||
const coderModel = AVAILABLE_MODELS_QWEN.find(
|
||||
(m) => m.id === MAINLINE_CODER,
|
||||
);
|
||||
const coderModel = qwenModels.find((m) => m.id === MAINLINE_CODER);
|
||||
expect(coderModel).toBeDefined();
|
||||
expect(coderModel?.isVision).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should include vision model', () => {
|
||||
const visionModel = AVAILABLE_MODELS_QWEN.find(
|
||||
(m) => m.id === MAINLINE_VLM,
|
||||
);
|
||||
const visionModel = qwenModels.find((m) => m.id === MAINLINE_VLM);
|
||||
expect(visionModel).toBeDefined();
|
||||
expect(visionModel?.isVision).toBe(true);
|
||||
});
|
||||
|
|
@ -39,7 +36,8 @@ describe('availableModels', () => {
|
|||
describe('getFilteredQwenModels', () => {
|
||||
it('should return all models when vision preview is enabled', () => {
|
||||
const models = getFilteredQwenModels(true);
|
||||
expect(models.length).toBe(AVAILABLE_MODELS_QWEN.length);
|
||||
const expected = getAvailableModelsForAuthType(AuthType.QWEN_OAUTH);
|
||||
expect(models.length).toBe(expected.length);
|
||||
});
|
||||
|
||||
it('should filter out vision models when preview is disabled', () => {
|
||||
|
|
@ -91,23 +89,34 @@ describe('availableModels', () => {
|
|||
|
||||
it('should return hard-coded qwen models for qwen-oauth', () => {
|
||||
const models = getAvailableModelsForAuthType(AuthType.QWEN_OAUTH);
|
||||
expect(models).toEqual(AVAILABLE_MODELS_QWEN);
|
||||
expect(models).toEqual(getFilteredQwenModels(true));
|
||||
});
|
||||
|
||||
it('should return hard-coded qwen models even when config is provided', () => {
|
||||
it('should use config models for qwen-oauth when config is provided', () => {
|
||||
const mockConfig = {
|
||||
getAvailableModels: vi
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ id: 'custom', label: 'Custom', authType: AuthType.QWEN_OAUTH },
|
||||
]),
|
||||
getAvailableModelsForAuthType: vi.fn().mockReturnValue([
|
||||
{
|
||||
id: 'custom',
|
||||
label: 'Custom',
|
||||
description: 'Custom model',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
isVision: false,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const models = getAvailableModelsForAuthType(
|
||||
AuthType.QWEN_OAUTH,
|
||||
mockConfig,
|
||||
);
|
||||
expect(models).toEqual(AVAILABLE_MODELS_QWEN);
|
||||
expect(models).toEqual([
|
||||
{
|
||||
id: 'custom',
|
||||
label: 'Custom',
|
||||
description: 'Custom model',
|
||||
isVision: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use config.getAvailableModels for openai authType when available', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
DEFAULT_QWEN_MODEL,
|
||||
type Config,
|
||||
type AvailableModel as CoreAvailableModel,
|
||||
QWEN_OAUTH_MODELS,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
|
|
@ -22,27 +23,18 @@ export type AvailableModel = {
|
|||
export const MAINLINE_VLM = 'vision-model';
|
||||
export const MAINLINE_CODER = DEFAULT_QWEN_MODEL;
|
||||
|
||||
export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
|
||||
{
|
||||
id: MAINLINE_CODER,
|
||||
label: MAINLINE_CODER,
|
||||
get description() {
|
||||
return t(
|
||||
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: MAINLINE_VLM,
|
||||
label: MAINLINE_VLM,
|
||||
get description() {
|
||||
return t(
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
|
||||
);
|
||||
},
|
||||
isVision: true,
|
||||
},
|
||||
];
|
||||
const CACHED_QWEN_OAUTH_MODELS: AvailableModel[] = QWEN_OAUTH_MODELS.map(
|
||||
(model) => ({
|
||||
id: model.id,
|
||||
label: model.name ?? model.id,
|
||||
description: model.description,
|
||||
isVision: model.capabilities?.vision ?? false,
|
||||
}),
|
||||
);
|
||||
|
||||
function getQwenOAuthModels(): readonly AvailableModel[] {
|
||||
return CACHED_QWEN_OAUTH_MODELS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Qwen models filtered by vision model preview setting
|
||||
|
|
@ -50,10 +42,11 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
|
|||
export function getFilteredQwenModels(
|
||||
visionModelPreviewEnabled: boolean,
|
||||
): AvailableModel[] {
|
||||
const qwenModels = getQwenOAuthModels();
|
||||
if (visionModelPreviewEnabled) {
|
||||
return AVAILABLE_MODELS_QWEN;
|
||||
return [...qwenModels];
|
||||
}
|
||||
return AVAILABLE_MODELS_QWEN.filter((model) => !model.isVision);
|
||||
return qwenModels.filter((model) => !model.isVision);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,18 +97,12 @@ function convertCoreModelToCliModel(
|
|||
* Get available models for the given authType.
|
||||
*
|
||||
* If a Config object is provided, uses config.getAvailableModelsForAuthType().
|
||||
* For qwen-oauth, always returns the hard-coded models.
|
||||
* Falls back to environment variables only when no config is provided.
|
||||
*/
|
||||
export function getAvailableModelsForAuthType(
|
||||
authType: AuthType,
|
||||
config?: Config,
|
||||
): AvailableModel[] {
|
||||
// For qwen-oauth, always use hard-coded models, this aligns with the API gateway.
|
||||
if (authType === AuthType.QWEN_OAUTH) {
|
||||
return AVAILABLE_MODELS_QWEN;
|
||||
}
|
||||
|
||||
// Use config's model registry when available
|
||||
if (config) {
|
||||
try {
|
||||
|
|
@ -134,6 +121,9 @@ export function getAvailableModelsForAuthType(
|
|||
|
||||
// Fall back to environment variables for specific auth types (no config provided)
|
||||
switch (authType) {
|
||||
case AuthType.QWEN_OAUTH: {
|
||||
return [...getQwenOAuthModels()];
|
||||
}
|
||||
case AuthType.USE_OPENAI: {
|
||||
const openAIModel = getOpenAIAvailableModelFromEnv();
|
||||
return openAIModel ? [openAIModel] : [];
|
||||
|
|
@ -156,7 +146,7 @@ export function getDefaultVisionModel(): string {
|
|||
}
|
||||
|
||||
export function isVisionModel(modelId: string): boolean {
|
||||
return AVAILABLE_MODELS_QWEN.some(
|
||||
return getQwenOAuthModels().some(
|
||||
(model) => model.id === modelId && model.isVision,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue