Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,8 @@ export default {
// ============================================================================
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).':
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).',
'Switch the model for this session (--default to persist, --fast for suggestion model).':
'Switch the model for this session (--default to persist, --fast for suggestion model).',
'Set a lighter model for prompt suggestions and speculative execution':
'Set a lighter model for prompt suggestions and speculative execution',
'Content generator configuration not available.':
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,8 @@ export default {
'生成摘要失敗 - 未從 LLM 響應中接收到文本內容',
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).':
'切換此會話的模型(--fast 可設置建議模型)',
'Switch the model for this session (--default to persist, --fast for suggestion model).':
'切換此會話的模型(--default 可持久化,--fast 可設置建議模型)',
'Set a lighter model for prompt suggestions and speculative execution':
'設置用於輸入建議和推測執行的輕量模型',
'Content generator configuration not available.': '內容生成器配置不可用',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,8 @@ export default {
// ============================================================================
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).':
'切换此会话的模型(--fast 可设置建议模型)',
'Switch the model for this session (--default to persist, --fast for suggestion model).':
'切换此会话的模型(--default 可持久化,--fast 可设置建议模型)',
'Set a lighter model for prompt suggestions and speculative execution':
'设置用于输入建议和推测执行的轻量模型',
'Content generator configuration not available.': '内容生成器配置不可用',
Expand Down
223 changes: 202 additions & 21 deletions packages/cli/src/ui/commands/modelCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ describe('modelCommand', () => {
it('should have the correct name and description', () => {
expect(modelCommand.name).toBe('model');
expect(modelCommand.description).toBe(
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).',
'Switch the model for this session (--default to persist, --fast for suggestion model).',
);
expect(modelCommand.argumentHint).toBe('[--default|--fast] [<model-id>]');
});

it('should return error when config is not available', async () => {
Expand Down Expand Up @@ -151,7 +152,7 @@ describe('modelCommand', () => {
});
});

it('should switch the main model directly in interactive mode when args are provided', async () => {
it('should switch the main model for the current session without persisting when args are provided', async () => {
const setValue = vi.fn();
const switchModel = vi.fn().mockResolvedValue(undefined);
mockContext = createMockCommandContext({
Expand All @@ -178,15 +179,204 @@ describe('modelCommand', () => {
'qwen-max',
undefined,
);
expect(setValue).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Model: qwen-max',
});
});

it('should persist the main model only when --default is provided', async () => {
const setValue = vi.fn();
const switchModel = vi.fn().mockResolvedValue(undefined);
mockContext = createMockCommandContext({
invocation: {
raw: '/model --default gpt-4',
name: 'model',
args: '--default gpt-4',
},
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'gpt-3.5',
authType: AuthType.USE_OPENAI,
}),
getAvailableModelsForAuthType: vi
.fn()
.mockReturnValue([{ id: 'gpt-4', label: 'GPT-4' }]),
switchModel,
},
settings: createMockSettings(setValue),
},
});

const result = await modelCommand.action!(mockContext, '--default gpt-4');

expect(switchModel).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
'gpt-4',
undefined,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'model.name',
'qwen-max',
'gpt-4',
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This test for unqualified --default gpt-4 only asserts model.name but not security.auth.selectedType. The persistMainModelDefault function (line 70) also writes security.auth.selectedType for unqualified IDs. If that line were accidentally deleted, this test would still pass.

The provider-qualified test below correctly asserts both settings.

Suggested change
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'model.name',
'gpt-4',
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'security.auth.selectedType',
AuthType.USE_OPENAI,
);

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Model: qwen-max',
content: 'Default model: gpt-4',
});
});

it('should persist provider-qualified models when --default is provided', async () => {
const setValue = vi.fn();
const switchModel = vi.fn().mockResolvedValue(undefined);
mockContext = createMockCommandContext({
invocation: {
raw: `/model --default gpt-4(${AuthType.USE_OPENAI})`,
name: 'model',
args: `--default gpt-4(${AuthType.USE_OPENAI})`,
},
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'qwen-plus',
authType: AuthType.QWEN_OAUTH,
}),
getAvailableModelsForAuthType: vi
.fn()
.mockReturnValue([{ id: 'gpt-4', label: 'GPT-4' }]),
switchModel,
},
settings: createMockSettings(setValue),
},
});

const result = await modelCommand.action!(
mockContext,
`--default gpt-4(${AuthType.USE_OPENAI})`,
);

expect(switchModel).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
'gpt-4',
undefined,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'security.auth.selectedType',
AuthType.USE_OPENAI,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'model.name',
'gpt-4',
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Default model: gpt-4',
});
});

it('should reject qwen-oauth models when --default is provided', async () => {
const setValue = vi.fn();
const switchModel = vi.fn().mockResolvedValue(undefined);
mockContext = createMockCommandContext({
invocation: {
raw: '/model --default qwen-max',
name: 'model',
args: '--default qwen-max',
},
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'qwen-plus',
authType: AuthType.QWEN_OAUTH,
}),
getAvailableModelsForAuthType: vi.fn(),
switchModel,
},
settings: createMockSettings(setValue),
},
});

const result = await modelCommand.action!(
mockContext,
'--default qwen-max',
);

expect(switchModel).not.toHaveBeenCalled();
expect(setValue).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'error',
content:
'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider.',
});
});

it('should return settings error when --default is used without settings', async () => {
const switchModel = vi.fn();
mockContext = createMockCommandContext({
invocation: {
raw: '/model --default gpt-4',
name: 'model',
args: '--default gpt-4',
},
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'gpt-3.5',
authType: AuthType.USE_OPENAI,
}),
switchModel,
},
},
});
mockContext.services.settings = undefined as unknown as LoadedSettings;

const result = await modelCommand.action!(mockContext, '--default gpt-4');

expect(switchModel).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Settings service not available.',
});
});

it('should return usage when --default is missing a model id', async () => {
const setValue = vi.fn();
const switchModel = vi.fn();
mockContext = createMockCommandContext({
invocation: {
raw: '/model --default',
name: 'model',
args: '--default',
},
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
model: 'qwen-plus',
authType: AuthType.QWEN_OAUTH,
}),
switchModel,
},
settings: createMockSettings(setValue),
},
});

const result = await modelCommand.action!(mockContext, '--default');

expect(switchModel).not.toHaveBeenCalled();
expect(setValue).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Usage: /model --default <model-id>',
});
});

Expand Down Expand Up @@ -343,7 +533,7 @@ describe('modelCommand', () => {
});
});

it('should switch provider-qualified models through switchModel', async () => {
it('should switch provider-qualified models for the current session without persisting', async () => {
const setValue = vi.fn();
const switchModel = vi.fn().mockResolvedValue(undefined);
mockContext = createMockCommandContext({
Expand Down Expand Up @@ -378,16 +568,7 @@ describe('modelCommand', () => {
'gpt-4',
undefined,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'security.auth.selectedType',
AuthType.USE_OPENAI,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'model.name',
'gpt-4',
);
expect(setValue).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
Expand Down Expand Up @@ -572,11 +753,7 @@ describe('modelCommand', () => {
'--fast-model',
undefined,
);
expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'model.name',
'--fast-model',
);
expect(setValue).not.toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
Expand Down Expand Up @@ -604,7 +781,11 @@ describe('modelCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('qwen-max'),
content:
'Current model: qwen-max\n' +
'Use "/model <model-id>" to switch models (session only), ' +
'"/model --default <model-id>" to persist, or ' +
'"/model --fast <model-id>" to set the fast model.',
});
expect((result as { type: string }).type).toBe('message');
});
Expand Down
Loading
Loading