diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx
index 3ce25bfa9c..24e7c1fd0f 100644
--- a/packages/cli/src/ui/components/ModelDialog.test.tsx
+++ b/packages/cli/src/ui/components/ModelDialog.test.tsx
@@ -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('', () => {
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('', () => {
});
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('', () => {
});
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('', () => {
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('', () => {
{
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('', () => {
,
);
+ // MAINLINE_CODER (qwen3-coder-next) 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('', () => {
// 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);
});
});
diff --git a/packages/cli/src/ui/models/availableModels.test.ts b/packages/cli/src/ui/models/availableModels.test.ts
index feac835c66..a17405d384 100644
--- a/packages/cli/src/ui/models/availableModels.test.ts
+++ b/packages/cli/src/ui/models/availableModels.test.ts
@@ -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', () => {
diff --git a/packages/cli/src/ui/models/availableModels.ts b/packages/cli/src/ui/models/availableModels.ts
index 1cff984c82..a68cc69cc7 100644
--- a/packages/cli/src/ui/models/availableModels.ts
+++ b/packages/cli/src/ui/models/availableModels.ts
@@ -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(
- 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
- );
- },
- },
- {
- 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,
);
}
diff --git a/packages/core/src/config/models.ts b/packages/core/src/config/models.ts
index a07dec7cec..5f4d8d0d3f 100644
--- a/packages/core/src/config/models.ts
+++ b/packages/core/src/config/models.ts
@@ -4,6 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
-export const DEFAULT_QWEN_MODEL = 'coder-model';
-export const DEFAULT_QWEN_FLASH_MODEL = 'coder-model';
+export const DEFAULT_QWEN_MODEL = 'qwen3-coder-next';
+export const DEFAULT_QWEN_FLASH_MODEL = 'qwen3-coder-next';
export const DEFAULT_QWEN_EMBEDDING_MODEL = 'text-embedding-v4';
diff --git a/packages/core/src/core/tokenLimits.ts b/packages/core/src/core/tokenLimits.ts
index c20bd16a7f..e79c00029f 100644
--- a/packages/core/src/core/tokenLimits.ts
+++ b/packages/core/src/core/tokenLimits.ts
@@ -115,12 +115,13 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [
// -------------------
// Commercial Qwen3-Coder-Plus: 1M token context
[/^qwen3-coder-plus(-.*)?$/, LIMITS['1m']], // catches "qwen3-coder-plus" and date variants
+ [/^qwen3-coder-next(-.*)?$/, LIMITS['1m']], // catches "qwen3-coder-next" and date variants
// Commercial Qwen3-Coder-Flash: 1M token context
[/^qwen3-coder-flash(-.*)?$/, LIMITS['1m']], // catches "qwen3-coder-flash" and date variants
// Generic coder-model: same as qwen3-coder-plus (1M token context)
- [/^coder-model$/, LIMITS['1m']],
+ [/^coder-model(-.*)?$/, LIMITS['1m']],
// Commercial Qwen3-Max-Preview: 256K token context
[/^qwen3-max(-preview)?(-.*)?$/, LIMITS['256k']], // catches "qwen3-max" or "qwen3-max-preview" and date variants
@@ -198,9 +199,10 @@ const OUTPUT_PATTERNS: Array<[RegExp, TokenCount]> = [
// -------------------
// Qwen3-Coder-Plus: 65,536 max output tokens
[/^qwen3-coder-plus(-.*)?$/, LIMITS['64k']],
+ [/^qwen3-coder-next(-.*)?$/, LIMITS['64k']],
// Generic coder-model: same as qwen3-coder-plus (64K max output tokens)
- [/^coder-model$/, LIMITS['64k']],
+ [/^coder-model(-.*)?$/, LIMITS['64k']],
// Qwen3-Max: 65,536 max output tokens
[/^qwen3-max(-preview)?(-.*)?$/, LIMITS['64k']],
diff --git a/packages/core/src/models/constants.ts b/packages/core/src/models/constants.ts
index 9b4cc2ce70..7a40c73338 100644
--- a/packages/core/src/models/constants.ts
+++ b/packages/core/src/models/constants.ts
@@ -88,24 +88,26 @@ export const AUTH_ENV_MAPPINGS = {
} as const satisfies Record;
export const DEFAULT_MODELS = {
- openai: 'qwen3-coder-plus',
+ openai: 'qwen3-coder-next',
'qwen-oauth': DEFAULT_QWEN_MODEL,
} as Partial>;
-export const QWEN_OAUTH_ALLOWED_MODELS = [
- DEFAULT_QWEN_MODEL,
- 'vision-model',
-] as const;
-
/**
* Hard-coded Qwen OAuth models that are always available.
* These cannot be overridden by user configuration.
*/
export const QWEN_OAUTH_MODELS: ModelConfig[] = [
+ {
+ id: 'qwen3-coder-next',
+ name: 'qwen3-coder-next',
+ description: 'The latest Qwen Coder model from Alibaba Cloud ModelStudio',
+ capabilities: { vision: false },
+ },
{
id: 'coder-model',
name: 'coder-model',
- description: 'The latest Qwen Coder model from Alibaba Cloud ModelStudio',
+ description:
+ 'The stable qwen3-coder-plus model from Alibaba Cloud ModelStudio',
capabilities: { vision: false },
},
{
@@ -115,3 +117,11 @@ export const QWEN_OAUTH_MODELS: ModelConfig[] = [
capabilities: { vision: true },
},
];
+
+/**
+ * Derive allowed models from QWEN_OAUTH_MODELS for authorization.
+ * This ensures single source of truth (SSOT).
+ */
+export const QWEN_OAUTH_ALLOWED_MODELS = QWEN_OAUTH_MODELS.map(
+ (model) => model.id,
+) as readonly string[];
diff --git a/packages/core/src/models/modelConfigResolver.test.ts b/packages/core/src/models/modelConfigResolver.test.ts
index a69ca678ed..1668cac917 100644
--- a/packages/core/src/models/modelConfigResolver.test.ts
+++ b/packages/core/src/models/modelConfigResolver.test.ts
@@ -95,7 +95,7 @@ describe('modelConfigResolver', () => {
},
});
- expect(result.config.model).toBe('qwen3-coder-plus');
+ expect(result.config.model).toBe('qwen3-coder-next');
expect(result.sources['model'].kind).toBe('default');
});
diff --git a/packages/core/src/models/modelRegistry.test.ts b/packages/core/src/models/modelRegistry.test.ts
index 01ccc82079..7fbb9e5702 100644
--- a/packages/core/src/models/modelRegistry.test.ts
+++ b/packages/core/src/models/modelRegistry.test.ts
@@ -16,8 +16,9 @@ describe('ModelRegistry', () => {
const qwenModels = registry.getModelsForAuthType(AuthType.QWEN_OAUTH);
expect(qwenModels.length).toBe(QWEN_OAUTH_MODELS.length);
- expect(qwenModels[0].id).toBe('coder-model');
- expect(qwenModels[1].id).toBe('vision-model');
+ expect(qwenModels[0].id).toBe('qwen3-coder-next');
+ expect(qwenModels[1].id).toBe('coder-model');
+ expect(qwenModels[2].id).toBe('vision-model');
});
it('should initialize with empty config', () => {
@@ -188,12 +189,12 @@ describe('ModelRegistry', () => {
});
describe('getDefaultModelForAuthType', () => {
- it('should return coder-model for qwen-oauth', () => {
+ it('should return qwen3-coder-next for qwen-oauth', () => {
const registry = new ModelRegistry();
const defaultModel = registry.getDefaultModelForAuthType(
AuthType.QWEN_OAUTH,
);
- expect(defaultModel?.id).toBe('coder-model');
+ expect(defaultModel?.id).toBe('qwen3-coder-next');
});
it('should return first model for other authTypes', () => {
diff --git a/packages/core/src/models/modelsConfig.test.ts b/packages/core/src/models/modelsConfig.test.ts
index 03a7248295..c459607f35 100644
--- a/packages/core/src/models/modelsConfig.test.ts
+++ b/packages/core/src/models/modelsConfig.test.ts
@@ -507,7 +507,7 @@ describe('ModelsConfig', () => {
);
const gc = currentGenerationConfig(modelsConfig);
- expect(gc.model).toBe('coder-model');
+ expect(gc.model).toBe('qwen3-coder-next');
expect(gc.apiKey).toBe('QWEN_OAUTH_DYNAMIC_TOKEN');
expect(gc.apiKeyEnvKey).toBeUndefined();
});
@@ -528,8 +528,8 @@ describe('ModelsConfig', () => {
modelsConfig.syncAfterAuthRefresh(AuthType.QWEN_OAUTH, 'gpt-4o');
const gc = currentGenerationConfig(modelsConfig);
- // Should use default qwen-oauth model (coder-model), not the OPENAI model
- expect(gc.model).toBe('coder-model');
+ // Should use default qwen-oauth model (qwen3-coder-next), not the OPENAI model
+ expect(gc.model).toBe('qwen3-coder-next');
expect(gc.apiKey).toBe('QWEN_OAUTH_DYNAMIC_TOKEN');
expect(gc.apiKeyEnvKey).toBeUndefined();
});
@@ -559,7 +559,7 @@ describe('ModelsConfig', () => {
const gc = currentGenerationConfig(modelsConfig);
// Should use default qwen-oauth model, not preserve manual OpenAI credentials
- expect(gc.model).toBe('coder-model');
+ expect(gc.model).toBe('qwen3-coder-next');
expect(gc.apiKey).toBe('QWEN_OAUTH_DYNAMIC_TOKEN');
// baseUrl should be set to qwen-oauth default, not preserved from manual OpenAI config
expect(gc.baseUrl).toBe('DYNAMIC_QWEN_OAUTH_BASE_URL');
@@ -638,7 +638,7 @@ describe('ModelsConfig', () => {
modelProvidersConfig,
generationConfig: {},
});
- expect(config3.getModel()).toBe('coder-model'); // Falls back to DEFAULT_QWEN_MODEL
+ expect(config3.getModel()).toBe('qwen3-coder-next'); // Falls back to DEFAULT_QWEN_MODEL
expect(config3.getGenerationConfig().model).toBeUndefined();
});