Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ import type { Config } from '@qwen-code/qwen-code-core';
import type { LoadedSettings } from '../config/settings.js';
import type { CliArgs } from '../config/config.js';
import {
AuthType,
SessionEndReason,
MCPServerConfig,
SessionService,
Expand Down Expand Up @@ -4021,6 +4022,14 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
baseUrl: 'https://api.example.com',
isRuntimeModel: false,
},
{
id: 'qwen-image-2.0',
label: 'Qwen Image 2.0',
authType: 'qwen',
baseUrl: 'https://api.example.com',
imageOnly: true,
isRuntimeModel: false,
},
]),
getToolRegistry: vi
.fn()
Expand Down Expand Up @@ -4064,6 +4073,26 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(
preflight.cells.find((c) => c.kind === 'tool_registry')?.status,
).toBe('ok');
expect(preflight.cells.find((c) => c.kind === 'providers')).toMatchObject({
status: 'ok',
detail: { count: 1, providers: ['qwen'] },
});

vi.mocked(mockConfig.getAllConfiguredModels).mockReturnValue([
{
id: 'qwen-image-2.0',
label: 'Qwen Image 2.0',
authType: AuthType.QWEN_OAUTH,
imageOnly: true,
},
]);
const imageOnlyPreflight = (await agent.extMethod(
SERVE_STATUS_EXT_METHODS.workspacePreflight,
{},
)) as { cells: Array<{ kind: string; status: string }> };
expect(
imageOnlyPreflight.cells.find((c) => c.kind === 'providers')?.status,
).toBe('error');

mockConnectionState.resolve();
await agentPromise;
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5855,7 +5855,9 @@ class QwenAgent implements Agent {

private buildProvidersPreflightCell(config: Config): ServePreflightCell {
try {
const models = config.getAllConfiguredModels();
const models = config
.getAllConfiguredModels()
.filter((model) => !model.imageOnly);
const authType = config.getAuthType?.();
if (models.length === 0) {
// `authType` set but zero models = the next `POST /session` will
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,24 @@ describe('loadCliConfig', () => {
);
});

it('should propagate the image model selection', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();

await loadCliConfig(
{
imageModel: 'openai:qwen-image-2.0\0https://images.example.com/api/v1',
},
argv,
);

expect(mockConfigConstructorParams).toHaveBeenCalledWith(
expect.objectContaining({
imageModel: 'openai:qwen-image-2.0\0https://images.example.com/api/v1',
}),
);
});

it('places session-injected (ACP/IDE) MCP servers at the top precedence tier', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2277,6 +2277,7 @@ export async function loadCliConfig(
webSearch:
bareMode || safeMode ? undefined : resolveWebSearchSettings(settings),
visionModel: settings.visionModel || undefined,
imageModel: settings.imageModel || undefined,
visionBridgeTimeoutMs: settings.visionBridgeTimeoutMs,
Comment on lines 2279 to 2281

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] Settings files are not guaranteed to be schema-validated on this load path, so a truthy non-string imageModel is forwarded into parsing code that calls string methods and crashes CLI initialization. Type-check this value before passing it to Config (and treat invalid values as unset or a configuration error).

— Codex GPT-5 via Qwen Code /review

modelFallbacks: resolveModelFallbacks(
argv.fallbackModel,
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('SettingsSchema', () => {
'advanced',
'plansDirectory',
'voiceModel',
'imageModel',
];

expectedSettings.forEach((setting) => {
Expand Down Expand Up @@ -179,6 +180,16 @@ describe('SettingsSchema', () => {
expect(voiceModel.showInDialog).toBe(false);
});

it('should define the image model setting', () => {
const imageModel = getSettingsSchema().imageModel;

expect(imageModel.type).toBe('string');
expect(imageModel.category).toBe('Model');
expect(imageModel.default).toBe('');
expect(imageModel.requiresRestart).toBe(false);
expect(imageModel.showInDialog).toBe(false);
});

it('should define the built-in Explore model setting', () => {
const exploreModel =
getSettingsSchema().agents.properties.builtin.properties.exploreModel;
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,17 @@ const SETTINGS_SCHEMA = {
showInDialog: true,
},

imageModel: {
type: 'string',
label: 'Image Model',
category: 'Model',
requiresRestart: false,
Comment on lines +1312 to +1316

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 declares direct imageModel edits as live-reloadable, but the settings reload paths do not call Config.setImageModel. Editing settings can therefore change the persisted value without changing the running tool registry. Either mark this setting as requiring restart or wire every live settings update through setImageModel and refresh active tool declarations.

— Codex GPT-5 via Qwen Code /review

default: '',
description:
'Model used by the built-in image_gen tool. Set with /model --image. The selected model must be marked imageOnly in modelProviders.',
showInDialog: false,
},

visionBridgeTimeoutMs: {
type: 'integer',
label: 'Vision Bridge Timeout (ms)',
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export default {
'toolDisplayName.ExitWorktree': 'toolDisplayName.ExitWorktree',
'toolDisplayName.Workflow': 'toolDisplayName.Workflow',
'toolDisplayName.ReadMcpResource': 'toolDisplayName.ReadMcpResource',
'toolDisplayName.ImageGen': 'toolDisplayName.ImageGen',
// ============================================================================
// Help / UI Components
// ============================================================================
Expand Down Expand Up @@ -1490,6 +1491,8 @@ export default {
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, [model-id] to switch immediately).',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).':
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --image for the image generation model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).':
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --image for the image generation model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).',
"Inline one-shot override isn't supported in this mode — run '/model {{model}}' first, then send your prompt.":
"Inline one-shot override isn't supported in this mode — run '/model {{model}}' first, then send your prompt.",
"Inline one-shot override can't switch providers. '{{model}}' belongs to a different provider — run '/model {{model}}' first, then send your prompt.":
Expand All @@ -1503,17 +1506,23 @@ export default {
'Set the model for voice transcription',
'Set the image-capable model used to transcribe images for a text-only main model':
'Set the image-capable model used to transcribe images for a text-only main model',
'Set the model used to generate images':
'Set the model used to generate images',
'Persist the model selection to the project settings (workspace scope)':
'Persist the model selection to the project settings (workspace scope)',
'Persist the model selection to the user settings (global scope)':
'Persist the model selection to the user settings (global scope)',
'Select Fast Model': 'Select Fast Model',
'Select Vision Model': 'Select Vision Model',
'Select Image Model': 'Select Image Model',
'Select Voice Model': 'Select Voice Model',
'Vision Model': 'Vision Model',
'Image Model': 'Image Model',
'Voice Model': 'Voice Model',
'Selected voice model is unavailable.':
'Selected voice model is unavailable.',
'Selected image model is unavailable.':
'Selected image model is unavailable.',
"Voice model '{{model}}' is configured more than once. Remove duplicate model ids before selecting it for voice transcription.":
"Voice model '{{model}}' is configured more than once. Remove duplicate model ids before selecting it for voice transcription.",
'Voice dictation: {{status}} (mode: {{mode}}, {{modelText}}).':
Expand Down Expand Up @@ -1761,8 +1770,16 @@ export default {
'Current voice model: {{voiceModel}}\nUse "/model --voice <model-id>" to set voice model.',
'Current vision model: {{visionModel}}\nUse "/model --vision <model-id>" to set the vision bridge model.':
'Current vision model: {{visionModel}}\nUse "/model --vision <model-id>" to set the vision bridge model.',
'Current image model: {{imageModel}}\nUse "/model --image <model-id>" to set the image generation model.':
'Current image model: {{imageModel}}\nUse "/model --image <model-id>" to set the image generation model.',
"Voice model '{{modelName}}' is ambiguous. Configure a unique model id before using /model --voice.":
"Voice model '{{modelName}}' is ambiguous. Configure a unique model id before using /model --voice.",
"Image model '{{modelName}}' matches multiple configured endpoints. Run /model --image without an argument and choose the exact endpoint.":
"Image model '{{modelName}}' matches multiple configured endpoints. Run /model --image without an argument and choose the exact endpoint.",
"Image model '{{modelName}}' must declare a valid HTTPS baseUrl and credential environment variable.":
"Image model '{{modelName}}' must declare a valid HTTPS baseUrl and credential environment variable.",
"'{{model}}' must declare a valid HTTPS baseUrl and credential environment variable.":
"'{{model}}' must declare a valid HTTPS baseUrl and credential environment variable.",
none: 'none',
unknown: 'unknown',
// ============================================================================
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export default {
'toolDisplayName.ExitWorktree': '退出 Worktree',
'toolDisplayName.Workflow': '工作流程',
'toolDisplayName.ReadMcpResource': '讀取 MCP 資源',
'toolDisplayName.ImageGen': '圖像生成',

'↑ to manage attachments': '↑ 管理附件',
'← → select, Delete to remove, ↓ to exit': '← → 選擇,Delete 刪除,↓ 退出',
Expand Down Expand Up @@ -1316,6 +1317,8 @@ export default {
'切換此會話的模型(--fast 可設置建議模型,--voice 可設置語音轉寫模型,[model-id] 可立即切換)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).':
'切換此會話的模型(--fast 建議模型,--voice 語音轉寫模型,--vision 視覺橋接模型,--project 持久化到專案設定,--global 持久化到使用者設定,[model-id] 立即切換,或用 [model-id] [prompt] 在另一個模型上執行一次性提示;內聯提示按原文發送,不展開 @file)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --image for the image generation model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).':
'切換此會話的模型(--fast 建議模型,--voice 語音轉寫模型,--vision 視覺橋接模型,--image 圖像生成模型,--project 持久化到專案設定,--global 持久化到使用者設定,[model-id] 立即切換,或用 [model-id] [prompt] 在另一個模型上執行一次性提示;內聯提示按原文發送,不展開 @file)',
"Inline one-shot override isn't supported in this mode — run '/model {{model}}' first, then send your prompt.":
"此模式不支援內聯一次性覆寫——請先執行 '/model {{model}}',再發送你的提示。",
"Inline one-shot override can't switch providers. '{{model}}' belongs to a different provider — run '/model {{model}}' first, then send your prompt.":
Expand All @@ -1328,16 +1331,20 @@ export default {
'Set the model for voice transcription': '設定語音轉寫模型',
'Set the image-capable model used to transcribe images for a text-only main model':
'設定用於為純文字主模型轉寫圖像的圖像能力模型',
'Set the model used to generate images': '設置用於生成圖像的模型',
'Persist the model selection to the project settings (workspace scope)':
'將模型選擇持久化到專案設定(工作區)',
'Persist the model selection to the user settings (global scope)':
'將模型選擇持久化到使用者設定(全域)',
'Select Fast Model': '選擇快速模型',
'Select Vision Model': '選擇視覺模型',
'Select Image Model': '選擇圖像模型',
'Select Voice Model': '選擇語音模型',
'Vision Model': '視覺模型',
'Image Model': '圖像模型',
'Voice Model': '語音模型',
'Selected voice model is unavailable.': '所選語音模型不可用。',
'Selected image model is unavailable.': '所選圖像模型不可用。',
"Voice model '{{model}}' is configured more than once. Remove duplicate model ids before selecting it for voice transcription.":
"語音模型 '{{model}}' 被配置了多次。請先移除重複的模型 ID,再將其選為語音轉寫模型。",
'Voice dictation: {{status}} (mode: {{mode}}, {{modelText}}).':
Expand Down Expand Up @@ -1543,8 +1550,16 @@ export default {
'當前語音模型:{{voiceModel}}\n使用 "/model --voice <model-id>" 設置語音模型。',
'Current vision model: {{visionModel}}\nUse "/model --vision <model-id>" to set the vision bridge model.':
'當前視覺模型:{{visionModel}}\n使用 "/model --vision <model-id>" 設置視覺橋接模型。',
'Current image model: {{imageModel}}\nUse "/model --image <model-id>" to set the image generation model.':
'當前圖像模型:{{imageModel}}\n使用 "/model --image <model-id>" 設置圖像生成模型。',
"Voice model '{{modelName}}' is ambiguous. Configure a unique model id before using /model --voice.":
"語音模型 '{{modelName}}' 不唯一。請先配置唯一的模型 ID,再使用 /model --voice。",
"Image model '{{modelName}}' matches multiple configured endpoints. Run /model --image without an argument and choose the exact endpoint.":
"圖像模型 '{{modelName}}' 匹配了多個已配置的端點。請執行 /model --image(不帶參數)並選擇確切的端點。",
"Image model '{{modelName}}' must declare a valid HTTPS baseUrl and credential environment variable.":
"圖像模型 '{{modelName}}' 必須宣告有效的 HTTPS baseUrl 和憑據環境變數。",
"'{{model}}' must declare a valid HTTPS baseUrl and credential environment variable.":
"'{{model}}' 必須宣告有效的 HTTPS baseUrl 和憑據環境變數。",
none: '無',
unknown: '未知',
'Manage folder trust settings': '管理檔案夾信任設置',
Expand Down
Loading
Loading