-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(core): add fastOnly/voiceOnly flags to hide models from main model list #5632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7f1c4ab
89e8b7a
3c500f6
2bf0775
cce8f1d
7cfd5ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1034,4 +1034,188 @@ describe('modelCommand', () => { | |||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The new test suite covers the action handler paths well (reject fastOnly from main, accept fastOnly in --fast, etc.), but the Consider adding at least one test that calls — qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||
| describe('fastOnly/voiceOnly filtering', () => { | ||||||||||||||||||||||||||
| it('should reject fastOnly models from normal /model selection', async () => { | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { raw: '/model fast-model', name: 'model', args: 'fast-model' }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAvailableModelsForAuthType: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main' }, | ||||||||||||||||||||||||||
| { id: 'fast-model', label: 'Fast', fastOnly: true }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(), | ||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The four new rejection tests in this block ("should reject fastOnly models from normal /model selection" here, "should reject voiceOnly models from normal /model selection", "should reject voiceOnly models from --fast selection", and "should reject fastOnly models from --voice selection") only verify This breaks the pattern established by 14 existing rejection tests in this same file (e.g., lines 226-975), which all capture the mock reference and assert Capture the const setValue = vi.fn();
// ... pass to createMockSettings(setValue) ...
const result = await modelCommand.action!(mockContext, 'fast-model');
expect(result).toMatchObject({
type: 'message',
messageType: 'error',
content: expect.stringContaining('fast-model'),
});
expect(setValue).not.toHaveBeenCalled();For the — qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, 'fast-model'); | ||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Three of the four error-case assertions in this test block only verify For example: expect(result).toMatchObject({
type: 'message',
messageType: 'error',
content: expect.stringContaining('fast-model'),
});— qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复。所有 error-case 断言现在都检查 |
||||||||||||||||||||||||||
| messageType: 'error', | ||||||||||||||||||||||||||
| content: expect.stringContaining('fast-model'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it('should reject voiceOnly models from normal /model selection', async () => { | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { raw: '/model voice-model', name: 'model', args: 'voice-model' }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAvailableModelsForAuthType: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main' }, | ||||||||||||||||||||||||||
| { id: 'voice-model', label: 'Voice', voiceOnly: true }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, 'voice-model'); | ||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
| messageType: 'error', | ||||||||||||||||||||||||||
| content: expect.stringContaining('voice-model'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it('should allow fastOnly models in --fast selection', async () => { | ||||||||||||||||||||||||||
| const setValue = vi.fn(); | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { | ||||||||||||||||||||||||||
| raw: '/model --fast fast-model', | ||||||||||||||||||||||||||
| name: 'model', | ||||||||||||||||||||||||||
| args: '--fast fast-model', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAllConfiguredModels: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main' }, | ||||||||||||||||||||||||||
| { id: 'fast-model', label: 'Fast', fastOnly: true }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| setFastModel: vi.fn(), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(setValue), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, '--fast fast-model'); | ||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The positive
Suggested change
And similarly for the expect(setValue).toHaveBeenCalledWith(
expect.any(String),
'voiceModel',
'qwen3-asr-flash',
);— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
| messageType: 'info', | ||||||||||||||||||||||||||
| content: expect.stringContaining('fast-model'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it('should reject voiceOnly models from --fast selection', async () => { | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { | ||||||||||||||||||||||||||
| raw: '/model --fast voice-model', | ||||||||||||||||||||||||||
| name: 'model', | ||||||||||||||||||||||||||
| args: '--fast voice-model', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAllConfiguredModels: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main' }, | ||||||||||||||||||||||||||
| { id: 'voice-model', label: 'Voice', voiceOnly: true }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| setFastModel: vi.fn(), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, '--fast voice-model'); | ||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
| messageType: 'error', | ||||||||||||||||||||||||||
| content: expect.stringContaining('voice-model'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it('should not filter out voiceOnly models from --voice selection', async () => { | ||||||||||||||||||||||||||
| const setValue = vi.fn(); | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { | ||||||||||||||||||||||||||
| raw: '/model --voice qwen3-asr-flash', | ||||||||||||||||||||||||||
| name: 'model', | ||||||||||||||||||||||||||
| args: '--voice qwen3-asr-flash', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAllConfiguredModels: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main', authType: AuthType.USE_OPENAI }, | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| id: 'qwen3-asr-flash', | ||||||||||||||||||||||||||
| label: 'ASR', | ||||||||||||||||||||||||||
| voiceOnly: true, | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(setValue), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, '--voice qwen3-asr-flash'); | ||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
| messageType: 'info', | ||||||||||||||||||||||||||
| content: expect.stringContaining('qwen3-asr-flash'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it('should reject fastOnly models from --voice selection', async () => { | ||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||
| invocation: { | ||||||||||||||||||||||||||
| raw: '/model --voice fast-model', | ||||||||||||||||||||||||||
| name: 'model', | ||||||||||||||||||||||||||
| args: '--voice fast-model', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| services: { | ||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||
| getContentGeneratorConfig: vi.fn().mockReturnValue({ | ||||||||||||||||||||||||||
| model: 'main-model', | ||||||||||||||||||||||||||
| authType: AuthType.USE_OPENAI, | ||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||
| getAllConfiguredModels: vi.fn().mockReturnValue([ | ||||||||||||||||||||||||||
| { id: 'main-model', label: 'Main' }, | ||||||||||||||||||||||||||
| { id: 'fast-model', label: 'Fast', fastOnly: true }, | ||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| settings: createMockSettings(), | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const result = await modelCommand.action!(mockContext, '--voice fast-model'); | ||||||||||||||||||||||||||
| expect(result).toMatchObject({ | ||||||||||||||||||||||||||
| type: 'message', | ||||||||||||||||||||||||||
| messageType: 'error', | ||||||||||||||||||||||||||
| content: expect.stringContaining('fast-model'), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,15 +140,21 @@ function formatUnavailableVoiceModelMessage( | |
| ); | ||
| } | ||
|
|
||
| // Get an array of the available model IDs as strings | ||
| function getAvailableModelIds(context: CommandContext) { | ||
| // Get an array of the available model IDs as strings, filtered by mode | ||
| function getAvailableModelIds( | ||
| context: CommandContext, | ||
| mode: 'main' | 'fast' | 'voice' = 'main', | ||
| ) { | ||
| const { services } = context; | ||
| const { config } = services; | ||
| if (!config) { | ||
| return []; | ||
| } | ||
| const availableModels = config.getAvailableModels(); | ||
| // Convert AvailableModel[] to string[] on AvailableModel.id | ||
| const availableModels = config.getAvailableModels().filter((m) => { | ||
| if (mode === 'fast') return !m.voiceOnly; | ||
| if (mode === 'voice') return !m.fastOnly; | ||
| return !m.fastOnly && !m.voiceOnly; | ||
| }); | ||
| return availableModels.map((model) => model.id); | ||
| } | ||
|
|
||
|
|
@@ -180,9 +186,19 @@ export const modelCommand: SlashCommand = { | |
| if (flagCompletions.length > 0) { | ||
| return flagCompletions; | ||
| } | ||
| if (partialArg.trim()) { | ||
| return getAvailableModelIds(context).filter((id) => | ||
| id.startsWith(partialArg.trim()), | ||
| const trimmed = partialArg.trim(); | ||
| if (trimmed) { | ||
| let mode: 'main' | 'fast' | 'voice' = 'main'; | ||
| let modelPrefix = trimmed; | ||
| if (trimmed.startsWith('--fast ')) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This flag-aware branch is not reached from the actual slash-completion flow for — GPT-5 Codex via Qwen Code /review |
||
| mode = 'fast'; | ||
| modelPrefix = trimmed.slice('--fast '.length); | ||
| } else if (trimmed.startsWith('--voice ')) { | ||
| mode = 'voice'; | ||
| modelPrefix = trimmed.slice('--voice '.length); | ||
| } | ||
| return getAvailableModelIds(context, mode).filter((id) => | ||
| id.startsWith(modelPrefix), | ||
| ); | ||
| } | ||
| return null; | ||
|
|
@@ -239,7 +255,9 @@ export const modelCommand: SlashCommand = { | |
| }; | ||
| } | ||
|
|
||
| const availableModels = config.getAllConfiguredModels(); | ||
| const availableModels = config | ||
| .getAllConfiguredModels() | ||
| .filter((m) => !m.fastOnly); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] When a Before returning the error, check the unfiltered list. If the model is found there, return a targeted message like: — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 好建议,不过改进错误消息文案超出了本 PR 的范围。当前行为与其他不可选模型(如 discontinued qwen-oauth models)保持一致 — 都是"not available"。后续可以统一优化。 |
||
| const matches = availableModels.filter((model) => model.id === modelName); | ||
| if (matches.length === 0) { | ||
| return { | ||
|
|
@@ -330,9 +348,11 @@ export const modelCommand: SlashCommand = { | |
| }; | ||
| } | ||
|
|
||
| const availableModels = selector.authType | ||
| ? config.getAvailableModelsForAuthType(selector.authType) | ||
| : config.getAllConfiguredModels(); | ||
| const availableModels = ( | ||
| selector.authType | ||
| ? config.getAvailableModelsForAuthType(selector.authType) | ||
| : config.getAllConfiguredModels() | ||
| ).filter((m) => !m.voiceOnly); | ||
| if (!availableModels.some((model) => model.id === selector.modelId)) { | ||
| return { | ||
| type: 'message', | ||
|
|
@@ -388,8 +408,9 @@ export const modelCommand: SlashCommand = { | |
| } | ||
| const parsed = parseAcpModelOption(modelName); | ||
| const targetAuthType = parsed.authType ?? authType; | ||
| const availableModels = | ||
| config.getAvailableModelsForAuthType(targetAuthType); | ||
| const availableModels = config | ||
| .getAvailableModelsForAuthType(targetAuthType) | ||
| .filter((m) => !m.fastOnly && !m.voiceOnly); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Apply the same filter in return config.getAvailableModels()
.filter((m) => !m.fastOnly && !m.voiceOnly)
.map((m) => m.id);— qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复。 |
||
| if (!availableModels.some((model) => model.id === parsed.modelId)) { | ||
| return { | ||
| type: 'message', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -228,7 +228,9 @@ export function ModelDialog({ | |
| (m) => | ||
| !m.isRuntimeModel && | ||
| (m.authType !== AuthType.QWEN_OAUTH || | ||
| authType === AuthType.QWEN_OAUTH), | ||
| authType === AuthType.QWEN_OAUTH) && | ||
| (isFastModelMode || !m.fastOnly) && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] A model configured with both Consider adding validation in — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 已修复。在
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The filter conditions added here ( — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同意 ModelDialog 缺少直接的测试覆盖。不过 ModelDialog 的过滤逻辑非常简单(两个布尔条件),且与 modelCommand 的测试逻辑一致。后续可以补充组件级测试。 |
||
| (isVoiceModelMode || !m.voiceOnly), | ||
| ); | ||
|
|
||
| // Group registry models by authType | ||
|
|
@@ -293,7 +295,7 @@ export function ModelDialog({ | |
| } | ||
|
|
||
| return result; | ||
| }, [authType, config]); | ||
| }, [authType, config, isFastModelMode, isVoiceModelMode]); | ||
|
|
||
| const MODEL_OPTIONS = useMemo( | ||
| () => | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1120,3 +1120,57 @@ describe('getProtocolForAuthType', () => { | |||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| describe('fastOnly and voiceOnly flags', () => { | ||||||||||||||||||||||||||||||||||||||||
| it('should propagate fastOnly flag to AvailableModel', () => { | ||||||||||||||||||||||||||||||||||||||||
| const config: ModelProvidersConfig = { | ||||||||||||||||||||||||||||||||||||||||
| openai: { | ||||||||||||||||||||||||||||||||||||||||
| protocol: Protocol.OPENAI, | ||||||||||||||||||||||||||||||||||||||||
| models: [ | ||||||||||||||||||||||||||||||||||||||||
| { id: 'gpt-4o', name: 'GPT-4o' }, | ||||||||||||||||||||||||||||||||||||||||
| { id: 'gpt-4o-mini', name: 'GPT-4o Mini', fastOnly: true }, | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| const registry = new ModelRegistry(config); | ||||||||||||||||||||||||||||||||||||||||
| const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); | ||||||||||||||||||||||||||||||||||||||||
| expect(models.find((m) => m.id === 'gpt-4o')?.fastOnly).toBeUndefined(); | ||||||||||||||||||||||||||||||||||||||||
| expect(models.find((m) => m.id === 'gpt-4o-mini')?.fastOnly).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('should propagate voiceOnly flag to AvailableModel', () => { | ||||||||||||||||||||||||||||||||||||||||
| const config: ModelProvidersConfig = { | ||||||||||||||||||||||||||||||||||||||||
| openai: { | ||||||||||||||||||||||||||||||||||||||||
| protocol: Protocol.OPENAI, | ||||||||||||||||||||||||||||||||||||||||
| models: [ | ||||||||||||||||||||||||||||||||||||||||
| { id: 'gpt-4o', name: 'GPT-4o' }, | ||||||||||||||||||||||||||||||||||||||||
| { id: 'whisper-1', name: 'Whisper', voiceOnly: true }, | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| const registry = new ModelRegistry(config); | ||||||||||||||||||||||||||||||||||||||||
| const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); | ||||||||||||||||||||||||||||||||||||||||
| expect(models.find((m) => m.id === 'gpt-4o')?.voiceOnly).toBeUndefined(); | ||||||||||||||||||||||||||||||||||||||||
| expect(models.find((m) => m.id === 'whisper-1')?.voiceOnly).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('should warn when both fastOnly and voiceOnly are set', () => { | ||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This test is named "should warn when both fastOnly and voiceOnly are set" but never actually verifies the warning. There is no spy on
Suggested change
— qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 好建议。不过 debugLogger 是模块级私有变量,spy 需要 vi.mock 整个模块,引入较多测试基础设施。当前测试验证了构造不会 throw 且 flag 正确传播,warn 的实际触发由代码覆盖保证。后续可以在测试基础设施完善后补充。 |
||||||||||||||||||||||||||||||||||||||||
| const config: ModelProvidersConfig = { | ||||||||||||||||||||||||||||||||||||||||
| openai: { | ||||||||||||||||||||||||||||||||||||||||
| protocol: Protocol.OPENAI, | ||||||||||||||||||||||||||||||||||||||||
| models: [ | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| id: 'unreachable-model', | ||||||||||||||||||||||||||||||||||||||||
| fastOnly: true, | ||||||||||||||||||||||||||||||||||||||||
| voiceOnly: true, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| const registry = new ModelRegistry(config); | ||||||||||||||||||||||||||||||||||||||||
| const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); | ||||||||||||||||||||||||||||||||||||||||
| expect(models).toHaveLength(1); | ||||||||||||||||||||||||||||||||||||||||
| expect(models[0].fastOnly).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
| expect(models[0].voiceOnly).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -151,6 +151,8 @@ export class ModelRegistry { | |
| modalities: model.generationConfig.modalities, | ||
| baseUrl: model.baseUrl, | ||
| envKey: model.envKey, | ||
| fastOnly: model.fastOnly, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] These new fields are propagated to Apply the same — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arena 和 ACP 不过滤是有意为之 — 它们是面向高级用户/外部集成的接口,展示所有模型是合理的。已在 PR 描述中说明。 |
||
| voiceOnly: model.voiceOnly, | ||
| })); | ||
| } | ||
|
|
||
|
|
@@ -264,6 +266,11 @@ export class ModelRegistry { | |
| `Model config in authType '${authType}' missing required field: id`, | ||
| ); | ||
| } | ||
| if (config.fastOnly && config.voiceOnly) { | ||
| debugLogger.warn( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Consider using — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同意这个观察。不过 debugLogger 是这个代码库现有的校验告警模式(参见 modelRegistry 里的其他 warn 调用),保持一致性。如果后续需要提升可见度可以单独改进日志基础设施。 |
||
| `Model "${config.id}" in authType "${authType}" has both fastOnly and voiceOnly set. It will be unreachable in all model selectors.`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,10 @@ export interface ModelConfig { | |
| capabilities?: ModelCapabilities; | ||
| /** Generation configuration (sampling parameters) */ | ||
| generationConfig?: ModelGenerationConfig; | ||
| /** When true, this model only appears in the fast model selector, not the main model list */ | ||
| fastOnly?: boolean; | ||
| /** When true, this model only appears in the voice model selector, not the main model list */ | ||
| voiceOnly?: boolean; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The visibility flags are defined here as opt-in booleans, but the filtering policy is not enforced at the data-source layer. At least 7 call sites beyond the ones patched in this PR remain unfiltered (ACP ×2, Arena ×2, — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 认同这是个合理的 API 设计建议,但这属于后续优化的范畴。当前实现保持最小改动原则 — 仅在直接面向用户的 UI 路径上过滤,暂不改动底层 API 签名。已添加 JSDoc 注释的 TODO 记录。 |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -117,6 +121,11 @@ export interface AvailableModel { | |
| baseUrl?: string; | ||
| envKey?: string; | ||
|
|
||
| /** When true, this model only appears in the fast model selector */ | ||
| fastOnly?: boolean; | ||
| /** When true, this model only appears in the voice model selector */ | ||
| voiceOnly?: boolean; | ||
|
|
||
| /** Whether this is a runtime model (not from modelProviders) */ | ||
| isRuntimeModel?: boolean; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
--voicehandler (modelCommand.ts~line 243) uses a distinct filter —.filter((m) => !m.fastOnly)— but the new test suite covers only the normal/modeland--fastpaths. Two--voicescenarios are untested:--voice <voiceOnly-model>should succeed (positive case)--voice <fastOnly-model>should be rejected (negative case)This was the exact path flagged as a prior bug (missing
!m.fastOnlyfilter, fixed in R1). Without regression tests, a future change could silently reintroduce it.— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已在 f0c48a9 中补充了 --voice 路径的测试:voiceOnly 模型在 --voice 选择器中可见、fastOnly 模型被 --voice 选择器拒绝。