fix: use dedicated model names and settings

This commit is contained in:
mingholy.lmh
2025-09-23 21:57:49 +08:00
parent e4d16adf7b
commit 490c36caeb
11 changed files with 129 additions and 36 deletions

View File

@@ -69,7 +69,11 @@ const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join(
); );
// A more flexible type for test data that allows arbitrary properties. // A more flexible type for test data that allows arbitrary properties.
type TestSettings = Settings & { [key: string]: unknown }; type TestSettings = Settings & {
[key: string]: unknown;
nested?: { [key: string]: unknown };
nestedObj?: { [key: string]: unknown };
};
vi.mock('fs', async (importOriginal) => { vi.mock('fs', async (importOriginal) => {
// Get all the functions from the real 'fs' module // Get all the functions from the real 'fs' module
@@ -137,6 +141,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -197,6 +204,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -260,6 +270,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -320,6 +333,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -385,6 +401,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -477,6 +496,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -562,6 +584,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -691,6 +716,9 @@ describe('Settings Loading and Merging', () => {
'/system/dir', '/system/dir',
], ],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -1431,6 +1459,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -1516,7 +1547,11 @@ describe('Settings Loading and Merging', () => {
'workspace_endpoint_from_env/api', 'workspace_endpoint_from_env/api',
); );
expect( expect(
(settings.workspace.settings as TestSettings)['nested']['value'], (
(settings.workspace.settings as TestSettings).nested as {
[key: string]: unknown;
}
)['value'],
).toBe('workspace_endpoint_from_env'); ).toBe('workspace_endpoint_from_env');
expect((settings.merged as TestSettings)['endpoint']).toBe( expect((settings.merged as TestSettings)['endpoint']).toBe(
'workspace_endpoint_from_env/api', 'workspace_endpoint_from_env/api',
@@ -1766,19 +1801,39 @@ describe('Settings Loading and Merging', () => {
).toBeUndefined(); ).toBeUndefined();
expect( expect(
(settings.user.settings as TestSettings)['nestedObj']['nestedNull'], (
(settings.user.settings as TestSettings).nestedObj as {
[key: string]: unknown;
}
)['nestedNull'],
).toBeNull(); ).toBeNull();
expect( expect(
(settings.user.settings as TestSettings)['nestedObj']['nestedBool'], (
(settings.user.settings as TestSettings).nestedObj as {
[key: string]: unknown;
}
)['nestedBool'],
).toBe(true); ).toBe(true);
expect( expect(
(settings.user.settings as TestSettings)['nestedObj']['nestedNum'], (
(settings.user.settings as TestSettings).nestedObj as {
[key: string]: unknown;
}
)['nestedNum'],
).toBe(0); ).toBe(0);
expect( expect(
(settings.user.settings as TestSettings)['nestedObj']['nestedString'], (
(settings.user.settings as TestSettings).nestedObj as {
[key: string]: unknown;
}
)['nestedString'],
).toBe('literal'); ).toBe('literal');
expect( expect(
(settings.user.settings as TestSettings)['nestedObj']['anotherEnv'], (
(settings.user.settings as TestSettings).nestedObj as {
[key: string]: unknown;
}
)['anotherEnv'],
).toBe('env_string_nested_value'); ).toBe('env_string_nested_value');
delete process.env['MY_ENV_STRING']; delete process.env['MY_ENV_STRING'];
@@ -1864,6 +1919,9 @@ describe('Settings Loading and Merging', () => {
advanced: { advanced: {
excludedEnvVars: [], excludedEnvVars: [],
}, },
experimental: {},
contentGenerator: {},
systemPromptMappings: {},
extensions: { extensions: {
disabled: [], disabled: [],
workspacesWithMigrationNudge: [], workspacesWithMigrationNudge: [],
@@ -2336,14 +2394,14 @@ describe('Settings Loading and Merging', () => {
vimMode: false, vimMode: false,
}, },
model: { model: {
maxSessionTurns: 0, maxSessionTurns: -1,
}, },
context: { context: {
includeDirectories: [], includeDirectories: [],
}, },
security: { security: {
folderTrust: { folderTrust: {
enabled: null, enabled: false,
}, },
}, },
}; };
@@ -2352,9 +2410,9 @@ describe('Settings Loading and Merging', () => {
expect(v1Settings).toEqual({ expect(v1Settings).toEqual({
vimMode: false, vimMode: false,
maxSessionTurns: 0, maxSessionTurns: -1,
includeDirectories: [], includeDirectories: [],
folderTrust: null, folderTrust: false,
}); });
}); });

View File

@@ -396,6 +396,24 @@ function mergeSettings(
]), ]),
], ],
}, },
experimental: {
...(systemDefaults.experimental || {}),
...(user.experimental || {}),
...(safeWorkspaceWithoutFolderTrust.experimental || {}),
...(system.experimental || {}),
},
contentGenerator: {
...(systemDefaults.contentGenerator || {}),
...(user.contentGenerator || {}),
...(safeWorkspaceWithoutFolderTrust.contentGenerator || {}),
...(system.contentGenerator || {}),
},
systemPromptMappings: {
...(systemDefaults.systemPromptMappings || {}),
...(user.systemPromptMappings || {}),
...(safeWorkspaceWithoutFolderTrust.systemPromptMappings || {}),
...(system.systemPromptMappings || {}),
},
extensions: { extensions: {
...(systemDefaults.extensions || {}), ...(systemDefaults.extensions || {}),
...(user.extensions || {}), ...(user.extensions || {}),

View File

@@ -746,7 +746,7 @@ export const SETTINGS_SCHEMA = {
label: 'Vision Model Preview', label: 'Vision Model Preview',
category: 'Experimental', category: 'Experimental',
requiresRestart: false, requiresRestart: false,
default: false, default: true,
description: description:
'Enable vision model support and auto-switching functionality. When disabled, vision models like qwen-vl-max-latest will be hidden and auto-switching will not occur.', 'Enable vision model support and auto-switching functionality. When disabled, vision models like qwen-vl-max-latest will be hidden and auto-switching will not occur.',
showInDialog: true, showInDialog: true,

View File

@@ -670,7 +670,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (!contentGeneratorConfig) return []; if (!contentGeneratorConfig) return [];
const visionModelPreviewEnabled = const visionModelPreviewEnabled =
settings.merged.experimental?.visionModelPreview ?? false; settings.merged.experimental?.visionModelPreview ?? true;
switch (contentGeneratorConfig.authType) { switch (contentGeneratorConfig.authType) {
case AuthType.QWEN_OAUTH: case AuthType.QWEN_OAUTH:
@@ -759,7 +759,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
setModelSwitchedFromQuotaError, setModelSwitchedFromQuotaError,
refreshStatic, refreshStatic,
() => cancelHandlerRef.current(), () => cancelHandlerRef.current(),
settings.merged.experimental?.visionModelPreview ?? false, settings.merged.experimental?.visionModelPreview ?? true,
handleVisionSwitchRequired, handleVisionSwitchRequired,
); );

View File

@@ -89,7 +89,7 @@ export const useGeminiStream = (
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>, setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
onEditorClose: () => void, onEditorClose: () => void,
onCancelSubmit: () => void, onCancelSubmit: () => void,
visionModelPreviewEnabled: boolean = false, visionModelPreviewEnabled: boolean,
onVisionSwitchRequired?: (query: PartListUnion) => Promise<{ onVisionSwitchRequired?: (query: PartListUnion) => Promise<{
modelOverride?: string; modelOverride?: string;
persistSessionModel?: string; persistSessionModel?: string;

View File

@@ -41,7 +41,7 @@ describe('useVisionAutoSwitch helpers', () => {
const result = shouldOfferVisionSwitch( const result = shouldOfferVisionSwitch(
parts, parts,
AuthType.QWEN_OAUTH, AuthType.QWEN_OAUTH,
'qwen-vl-max-latest', 'vision-model',
true, true,
); );
expect(result).toBe(false); expect(result).toBe(false);
@@ -140,7 +140,7 @@ describe('useVisionAutoSwitch helpers', () => {
const result = shouldOfferVisionSwitch( const result = shouldOfferVisionSwitch(
parts, parts,
AuthType.QWEN_OAUTH, AuthType.QWEN_OAUTH,
'qwen-vl-max-latest', 'vision-model',
true, true,
); );
expect(result).toBe(false); expect(result).toBe(false);
@@ -314,7 +314,7 @@ describe('useVisionAutoSwitch hook', () => {
const config = createMockConfig(AuthType.QWEN_OAUTH, initialModel); const config = createMockConfig(AuthType.QWEN_OAUTH, initialModel);
const onVisionSwitchRequired = vi const onVisionSwitchRequired = vi
.fn() .fn()
.mockResolvedValue({ modelOverride: 'qwen-vl-max-latest' }); .mockResolvedValue({ modelOverride: 'coder-model' });
const { result } = renderHook(() => const { result } = renderHook(() =>
useVisionAutoSwitch(config, addItem as any, true, onVisionSwitchRequired), useVisionAutoSwitch(config, addItem as any, true, onVisionSwitchRequired),
); );
@@ -329,7 +329,7 @@ describe('useVisionAutoSwitch hook', () => {
}); });
expect(res).toEqual({ shouldProceed: true, originalModel: initialModel }); expect(res).toEqual({ shouldProceed: true, originalModel: initialModel });
expect(config.setModel).toHaveBeenCalledWith('qwen-vl-max-latest', { expect(config.setModel).toHaveBeenCalledWith('coder-model', {
reason: 'vision_auto_switch', reason: 'vision_auto_switch',
context: 'User-prompted vision switch (one-time override)', context: 'User-prompted vision switch (one-time override)',
}); });
@@ -348,7 +348,7 @@ describe('useVisionAutoSwitch hook', () => {
const config = createMockConfig(AuthType.QWEN_OAUTH, 'qwen3-coder-plus'); const config = createMockConfig(AuthType.QWEN_OAUTH, 'qwen3-coder-plus');
const onVisionSwitchRequired = vi const onVisionSwitchRequired = vi
.fn() .fn()
.mockResolvedValue({ persistSessionModel: 'qwen-vl-max-latest' }); .mockResolvedValue({ persistSessionModel: 'coder-model' });
const { result } = renderHook(() => const { result } = renderHook(() =>
useVisionAutoSwitch(config, addItem as any, true, onVisionSwitchRequired), useVisionAutoSwitch(config, addItem as any, true, onVisionSwitchRequired),
); );
@@ -363,7 +363,7 @@ describe('useVisionAutoSwitch hook', () => {
}); });
expect(res).toEqual({ shouldProceed: true }); expect(res).toEqual({ shouldProceed: true });
expect(config.setModel).toHaveBeenCalledWith('qwen-vl-max-latest', { expect(config.setModel).toHaveBeenCalledWith('coder-model', {
reason: 'vision_auto_switch', reason: 'vision_auto_switch',
context: 'User-prompted vision switch (session persistent)', context: 'User-prompted vision switch (session persistent)',
}); });
@@ -373,9 +373,7 @@ describe('useVisionAutoSwitch hook', () => {
result.current.restoreOriginalModel(); result.current.restoreOriginalModel();
}); });
// Last call should still be the persisted model set // Last call should still be the persisted model set
expect((config.setModel as any).mock.calls.pop()?.[0]).toBe( expect((config.setModel as any).mock.calls.pop()?.[0]).toBe('coder-model');
'qwen-vl-max-latest',
);
}); });
it('returns shouldProceed=true when dialog returns no special flags', async () => { it('returns shouldProceed=true when dialog returns no special flags', async () => {
@@ -507,7 +505,7 @@ describe('useVisionAutoSwitch hook', () => {
it('does not switch in YOLO mode when already using vision model', async () => { it('does not switch in YOLO mode when already using vision model', async () => {
const config = createMockConfig( const config = createMockConfig(
AuthType.QWEN_OAUTH, AuthType.QWEN_OAUTH,
'qwen-vl-max-latest', 'vision-model',
ApprovalMode.YOLO, ApprovalMode.YOLO,
); );
const onVisionSwitchRequired = vi.fn(); const onVisionSwitchRequired = vi.fn();
@@ -709,7 +707,7 @@ describe('useVisionAutoSwitch hook', () => {
expect(switchResult.shouldProceed).toBe(true); expect(switchResult.shouldProceed).toBe(true);
expect(switchResult.originalModel).toBe('qwen3-coder-plus'); expect(switchResult.originalModel).toBe('qwen3-coder-plus');
expect(config.setModel).toHaveBeenCalledWith('qwen-vl-max-latest', { expect(config.setModel).toHaveBeenCalledWith('vision-model', {
reason: 'vision_auto_switch', reason: 'vision_auto_switch',
context: 'Default VLM switch mode: once (one-time override)', context: 'Default VLM switch mode: once (one-time override)',
}); });
@@ -745,7 +743,7 @@ describe('useVisionAutoSwitch hook', () => {
expect(switchResult.shouldProceed).toBe(true); expect(switchResult.shouldProceed).toBe(true);
expect(switchResult.originalModel).toBeUndefined(); // No original model for session switch expect(switchResult.originalModel).toBeUndefined(); // No original model for session switch
expect(config.setModel).toHaveBeenCalledWith('qwen-vl-max-latest', { expect(config.setModel).toHaveBeenCalledWith('vision-model', {
reason: 'vision_auto_switch', reason: 'vision_auto_switch',
context: 'Default VLM switch mode: session (session persistent)', context: 'Default VLM switch mode: session (session persistent)',
}); });
@@ -794,7 +792,7 @@ describe('useVisionAutoSwitch hook', () => {
); );
const onVisionSwitchRequired = vi const onVisionSwitchRequired = vi
.fn() .fn()
.mockResolvedValue({ modelOverride: 'qwen-vl-max-latest' }); .mockResolvedValue({ modelOverride: 'vision-model' });
const { result } = renderHook(() => const { result } = renderHook(() =>
useVisionAutoSwitch( useVisionAutoSwitch(
config, config,

View File

@@ -121,7 +121,7 @@ export function shouldOfferVisionSwitch(
parts: PartListUnion, parts: PartListUnion,
authType: AuthType, authType: AuthType,
currentModel: string, currentModel: string,
visionModelPreviewEnabled: boolean = false, visionModelPreviewEnabled: boolean = true,
): boolean { ): boolean {
// Only trigger for qwen-oauth // Only trigger for qwen-oauth
if (authType !== AuthType.QWEN_OAUTH) { if (authType !== AuthType.QWEN_OAUTH) {
@@ -198,7 +198,7 @@ export interface VisionSwitchHandlingResult {
export function useVisionAutoSwitch( export function useVisionAutoSwitch(
config: Config, config: Config,
addItem: UseHistoryManagerReturn['addItem'], addItem: UseHistoryManagerReturn['addItem'],
visionModelPreviewEnabled: boolean = false, visionModelPreviewEnabled: boolean = true,
onVisionSwitchRequired?: (query: PartListUnion) => Promise<{ onVisionSwitchRequired?: (query: PartListUnion) => Promise<{
modelOverride?: string; modelOverride?: string;
persistSessionModel?: string; persistSessionModel?: string;

View File

@@ -10,8 +10,8 @@ export type AvailableModel = {
isVision?: boolean; isVision?: boolean;
}; };
export const MAINLINE_VLM = 'qwen-vl-max-latest'; export const MAINLINE_VLM = 'vision-model';
export const MAINLINE_CODER = 'qwen3-coder-plus'; export const MAINLINE_CODER = 'coder-model';
export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
{ id: MAINLINE_CODER, label: MAINLINE_CODER }, { id: MAINLINE_CODER, label: MAINLINE_CODER },

View File

@@ -4,11 +4,10 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
export const DEFAULT_QWEN_MODEL = 'qwen3-coder-plus'; export const DEFAULT_QWEN_MODEL = 'coder-model';
// We do not have a fallback model for now, but note it here anyway. export const DEFAULT_QWEN_FLASH_MODEL = 'coder-model';
export const DEFAULT_QWEN_FLASH_MODEL = 'qwen3-coder-flash';
export const DEFAULT_GEMINI_MODEL = 'qwen3-coder-plus'; export const DEFAULT_GEMINI_MODEL = 'coder-model';
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash'; export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite'; export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';

View File

@@ -820,6 +820,14 @@ function getToolCallExamples(model?: string): string {
if (/qwen[^-]*-vl/i.test(model)) { if (/qwen[^-]*-vl/i.test(model)) {
return qwenVlToolCallExamples; return qwenVlToolCallExamples;
} }
// Match coder-model pattern (same as qwen3-coder)
if (/coder-model/i.test(model)) {
return qwenCoderToolCallExamples;
}
// Match vision-model pattern (same as qwen3-vl)
if (/vision-model/i.test(model)) {
return qwenVlToolCallExamples;
}
} }
return generalToolCallExamples; return generalToolCallExamples;

View File

@@ -111,6 +111,9 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [
// Commercial Qwen3-Coder-Flash: 1M token context // Commercial Qwen3-Coder-Flash: 1M token context
[/^qwen3-coder-flash(-.*)?$/, LIMITS['1m']], // catches "qwen3-coder-flash" and date variants [/^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']],
// Commercial Qwen3-Max-Preview: 256K token context // Commercial Qwen3-Max-Preview: 256K token context
[/^qwen3-max-preview(-.*)?$/, LIMITS['256k']], // catches "qwen3-max-preview" and date variants [/^qwen3-max-preview(-.*)?$/, LIMITS['256k']], // catches "qwen3-max-preview" and date variants
@@ -134,6 +137,9 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [
// Qwen Vision Models // Qwen Vision Models
[/^qwen-vl-max.*$/, LIMITS['128k']], [/^qwen-vl-max.*$/, LIMITS['128k']],
// Generic vision-model: same as qwen-vl-max (128K token context)
[/^vision-model$/, LIMITS['128k']],
// ------------------- // -------------------
// ByteDance Seed-OSS (512K) // ByteDance Seed-OSS (512K)
// ------------------- // -------------------
@@ -169,12 +175,18 @@ const OUTPUT_PATTERNS: Array<[RegExp, TokenCount]> = [
// Qwen3-Coder-Plus: 65,536 max output tokens // Qwen3-Coder-Plus: 65,536 max output tokens
[/^qwen3-coder-plus(-.*)?$/, LIMITS['64k']], [/^qwen3-coder-plus(-.*)?$/, LIMITS['64k']],
// Generic coder-model: same as qwen3-coder-plus (64K max output tokens)
[/^coder-model$/, LIMITS['64k']],
// Qwen3-Max-Preview: 65,536 max output tokens // Qwen3-Max-Preview: 65,536 max output tokens
[/^qwen3-max-preview(-.*)?$/, LIMITS['64k']], [/^qwen3-max-preview(-.*)?$/, LIMITS['64k']],
// Qwen-VL-Max-Latest: 8,192 max output tokens // Qwen-VL-Max-Latest: 8,192 max output tokens
[/^qwen-vl-max-latest$/, LIMITS['8k']], [/^qwen-vl-max-latest$/, LIMITS['8k']],
// Generic vision-model: same as qwen-vl-max-latest (8K max output tokens)
[/^vision-model$/, LIMITS['8k']],
// Qwen3-VL-Plus: 8,192 max output tokens // Qwen3-VL-Plus: 8,192 max output tokens
[/^qwen3-vl-plus$/, LIMITS['8k']], [/^qwen3-vl-plus$/, LIMITS['8k']],
]; ];