Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Settings are organized into categories. Most settings should be placed within th
| `general.voice.enabled` | boolean | Enable voice dictation in the prompt input. Also toggleable with the `/voice` command. Requires a transcription model (`voiceModel`) to be configured. | `false` |
| `general.voice.mode` | enum | How push-to-talk behaves: `"hold"` to talk while the key is held, or `"tap"` to start and tap (or pause) to stop and submit. | `"hold"` |
| `general.voice.language` | string | Preferred spoken language for voice transcription (e.g. `"english"`, `"chinese"`). Leave empty to auto-detect. | `""` |
| `general.voice.keytermsFile` | string | Path to a custom keyterms file (one term per line, `#` for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to `.qwen/voice-keyterms.txt` when present. Read only in trusted workspaces. Only applies to Qwen ASR models (`qwen3-asr-*`). | `""` |
| `general.voice.keytermsFile` | string | Path to a custom keyterms file (one term per line, `#` for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to `.qwen/voice-keyterms.txt` when present. Read only in trusted workspaces. Only applies to Qwen ASR models (`qwen3-asr-*` and the `qwen-audio-*` ASR family). | `""` |
| `general.voice.refineTranscript` | boolean | Clean up voice transcripts with the fast model before inserting them — removes filler words and fixes recognition errors while preserving meaning. Falls back to the raw transcript on failure, and is skipped when no fast model is configured. | `true` |
| `general.cleanupPeriodDays` | number | Days to retain `~/.qwen/file-history/` session backups used by `/rewind`. Backups older than this are removed by a background pass that runs at most once per day. `0` = minimum retention (~1 hour): keeps sessions touched in the last hour plus the currently active one. Changes take effect after restart. | `30` |
| `general.language` | enum | Language for the user interface. Use `"auto"` to detect from system settings, or a language code (e.g. `"zh-CN"`, `"fr"`). Custom codes can be added by placing JS locale files in `~/.qwen/locales/`. See [i18n](../features/language). Requires restart. | `"auto"` |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: false,
default: '',
description:
'Path to a custom keyterms file (one term per line, "#" for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to ".qwen/voice-keyterms.txt" when present. The file contents are sent to the ASR provider and it is read only in trusted workspaces. Only applies to Qwen ASR models (qwen3-asr-*).',
'Path to a custom keyterms file (one term per line, "#" for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to ".qwen/voice-keyterms.txt" when present. The file contents are sent to the ASR provider and it is read only in trusted workspaces. Only applies to Qwen ASR models (qwen3-asr-* and the qwen-audio-* ASR family).',

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] R1-1: [fails-closed] [regression] The keytermsFile description was updated here, but the generated packages/vscode-ide-companion/schemas/settings.schema.json was not regenerated — its line 105 still reads Only applies to Qwen ASR models (qwen3-asr-*).. This PR touches packages/cli and packages/web-shell, so CI classifies it into the full profile; the lint_and_static job then runs npm run generate:settings-schema followed by the "Check settings schema is up-to-date" step (.github/workflows/ci.yml:1171-1185), which exits 1 when the schema file is dirty — and regenerating rewrites exactly this description. The PR therefore fails that CI gate as committed, and until the artifact is regenerated, VS Code settings IntelliSense keeps showing the stale family list, contradicting the docs this same PR updates.

Witness:

$ npm run generate:settings-schema      (scratch tree at ef99e56625)
exit 0
$ git status --porcelain packages/vscode-ide-companion/schemas/settings.schema.json
 M packages/vscode-ide-companion/schemas/settings.schema.json
- "description": "...Only applies to Qwen ASR models (qwen3-asr-*).",
+ "description": "...Only applies to Qwen ASR models (qwen3-asr-* and the qwen-audio-* ASR family).",

Regenerate and commit the artifact — do not hand-edit the JSON:

npm run generate:settings-schema

Note that scripts/generate-settings-schema.ts copies setting.description verbatim from SETTINGS_SCHEMA and rewrites the whole file (and scripts/build.js re-runs the generator after CLI builds), so the fix must be a regeneration, not a hand-edit of the description line. Acceptance criterion: the CI freshness gate itself — after npm run generate:settings-schema, git status --porcelain packages/vscode-ide-companion/schemas/settings.schema.json must be empty; without the regenerated artifact committed, the "Check settings schema is up-to-date" step exits 1.

— qwen3.8-max via Qwen Code /review (v0.23.0)

showInDialog: false,
},
refineTranscript: {
Expand Down
20 changes: 19 additions & 1 deletion packages/cli/src/services/voice-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ export type VoiceTransport =
| 'dashscope-task-realtime'
| 'unsupported';

/** Map a model id to the ASR transport it uses, or 'unsupported'. */
/**
* Map a model id to the ASR transport it uses, or 'unsupported'.
*
* The `qwen-audio-<version>` family (Model Studio Token Plan, #10932) follows
* the same split as `qwen3-asr-flash`: `*-realtime` and `*-asr-flash-streaming`
* ids speak the OpenAI realtime WebSocket dialect, while bare and date-suffixed
* `*-asr-flash` ids use the batch chat/completions shape. Other ids in the
* family (filetrans, tts) are not dictation transports and stay unsupported.
*/
export function resolveVoiceTransport(model: string): VoiceTransport {
const id = model.toLowerCase();
if (/^qwen3-asr-flash-realtime(?:-|$)/.test(id)) {
Expand All @@ -22,6 +30,16 @@ export function resolveVoiceTransport(model: string): VoiceTransport {
if (/^qwen3-asr-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(id)) {
return 'qwen-asr-chat';
}
if (
/^qwen-audio-[\d.]+-(?:asr-flash-(?:realtime|streaming)|realtime)(?:-|$)/.test(
id,
)
) {
return 'qwen-asr-realtime';
}
if (/^qwen-audio-[\d.]+-asr-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(id)) {

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.

[Suggestion] R1-2: This diff makes the qwen-audio-*-asr-flash batch family selectable, but two recovery messages newly reachable for it still recommend only legacy ids: packages/cli/src/services/voice-transcriber.ts:843 ("Use qwen3-asr-flash for batch or choose a realtime voice model such as qwen3-asr-flash-realtime / fun-asr-realtime / paraformer-realtime-v2") and packages/cli/src/ui/hooks/use-voice-input.ts:335 ("switch voiceModel to qwen3-asr-flash for batch transcription"). A Token Plan user — the population issue 10932 enables — configures qwen-audio-3.0-asr-flash, which now passes the selection guard; if the endpoint then rejects it as model_not_supported, or a streaming qwen-audio model is selected without the native audio-capture module, the guidance sends them to qwen3-asr-flash — but the issue's own live probes show qwen3-asr-flash answering 404 model_not_found on the Token Plan gateway, so the user is pointed at a model their plan does not serve instead of their own family's batch variant, a second dead end.

Witness:

probe driving the real transcribeVoiceAudio() with a stubbed 400 model_not_supported response:
OBSERVED[qwen-audio-3.0-asr-flash]: This voice model cannot be used for batch
transcription. Use qwen3-asr-flash for batch or choose a realtime voice model such as
qwen3-asr-flash-realtime / fun-asr-realtime / paraformer-realtime-v2.

Extend both messages to also name the qwen-audio-*-asr-flash batch family — e.g. "Use qwen3-asr-flash or qwen-audio--asr-flash for batch ..." — or phrase the guidance generically ("a batch-capable ASR model").

— qwen3.8-max via Qwen Code /review (v0.23.0)

return 'qwen-asr-chat';
}
if (/^(fun-asr|paraformer).*realtime(?:-|$)/.test(id)) {
return 'dashscope-task-realtime';
}
Expand Down
65 changes: 65 additions & 0 deletions packages/cli/src/ui/voice/voice-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AuthType, type AvailableModel } from '@qwen-code/qwen-code-core';
import {
isSelectableVoiceModel,
isTranscribableVoiceModel,
resolveVoiceTransport,
} from './voice-model.js';

function model(overrides: Partial<AvailableModel>): AvailableModel {
Expand Down Expand Up @@ -51,4 +52,68 @@ describe('voice model guards', () => {
isSelectableVoiceModel(model({ id: 'qwen3-asr-flash-filetrans' })),
).toBe(false);
});

it('isSelectableVoiceModel accepts the qwen-audio Token Plan ASR family', () => {
expect(
isSelectableVoiceModel(model({ id: 'qwen-audio-3.0-asr-flash' })),
).toBe(true);
expect(
isSelectableVoiceModel(model({ id: 'qwen-audio-3.0-realtime-plus' })),
).toBe(true);
});
});

describe('resolveVoiceTransport', () => {
it('routes the qwen-audio Token Plan ASR family', () => {
// Bare and date-suffixed asr ids use the batch chat shape (#10932).
expect(resolveVoiceTransport('qwen-audio-3.0-asr-flash')).toBe(
'qwen-asr-chat',
);
expect(resolveVoiceTransport('qwen-audio-3.0-asr-flash-2026-09-01')).toBe(
'qwen-asr-chat',
);
// Streaming variants speak the OpenAI realtime WebSocket dialect.
expect(resolveVoiceTransport('qwen-audio-3.0-asr-flash-streaming')).toBe(
'qwen-asr-realtime',
);
expect(resolveVoiceTransport('qwen-audio-3.0-asr-flash-realtime')).toBe(
'qwen-asr-realtime',
);
expect(resolveVoiceTransport('qwen-audio-3.0-realtime-plus')).toBe(
'qwen-asr-realtime',
);
expect(resolveVoiceTransport('Qwen-Audio-3.0-ASR-Flash')).toBe(
'qwen-asr-chat',
);
});

it('keeps routing the legacy qwen3-asr and dashscope ids', () => {
expect(resolveVoiceTransport('qwen3-asr-flash')).toBe('qwen-asr-chat');
expect(resolveVoiceTransport('qwen3-asr-flash-2025-08-20')).toBe(
'qwen-asr-chat',
);
expect(resolveVoiceTransport('qwen3-asr-flash-realtime')).toBe(
'qwen-asr-realtime',
);
expect(resolveVoiceTransport('fun-asr-realtime')).toBe(
'dashscope-task-realtime',
);
expect(resolveVoiceTransport('paraformer-realtime-v2')).toBe(
'dashscope-task-realtime',
);
});

it('rejects non-ASR ids, including filetrans and tts in the family', () => {
expect(resolveVoiceTransport('gpt-4o')).toBe('unsupported');
expect(resolveVoiceTransport('custom:asr')).toBe('unsupported');
expect(resolveVoiceTransport('qwen3-asr-flash-filetrans')).toBe(
'unsupported',
);
expect(resolveVoiceTransport('qwen-audio-3.0-asr-flash-filetrans')).toBe(
'unsupported',
);
expect(resolveVoiceTransport('qwen-audio-3.0-tts-plus')).toBe(
'unsupported',
);
});
});
76 changes: 76 additions & 0 deletions packages/web-shell/client/voice/voiceModels.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import { extractVoiceModels, isVoiceModelId } from './voiceModels';

describe('isVoiceModelId', () => {
it('accepts the qwen-audio Token Plan ASR family (#10932)', () => {
expect(isVoiceModelId('qwen-audio-3.0-asr-flash')).toBe(true);
expect(isVoiceModelId('qwen-audio-3.0-asr-flash-2026-09-01')).toBe(true);
expect(isVoiceModelId('qwen-audio-3.0-asr-flash-streaming')).toBe(true);
expect(isVoiceModelId('qwen-audio-3.0-asr-flash-realtime')).toBe(true);
expect(isVoiceModelId('qwen-audio-3.0-realtime-plus')).toBe(true);
expect(isVoiceModelId('Qwen-Audio-3.0-ASR-Flash')).toBe(true);
});

it('keeps accepting the legacy ASR ids', () => {
expect(isVoiceModelId('qwen3-asr-flash')).toBe(true);
expect(isVoiceModelId('qwen3-asr-flash-2025-08-20')).toBe(true);
expect(isVoiceModelId('qwen3-asr-flash-realtime')).toBe(true);
expect(isVoiceModelId('fun-asr-realtime')).toBe(true);
expect(isVoiceModelId('paraformer-realtime-v2')).toBe(true);
});

it('rejects non-ASR ids, including filetrans and tts in the family', () => {
expect(isVoiceModelId('gpt-4o')).toBe(false);
expect(isVoiceModelId('qwen3-asr-flash-filetrans')).toBe(false);
expect(isVoiceModelId('qwen-audio-3.0-asr-flash-filetrans')).toBe(false);
expect(isVoiceModelId('qwen-audio-3.0-tts-plus')).toBe(false);
});
});

describe('extractVoiceModels', () => {
it('lists qwen-audio ASR ids from a providers status and dedupes', () => {
const status = {
providers: [
{
authType: 'USE_OPENAI',
models: [
{
baseModelId: 'qwen-audio-3.0-asr-flash',
name: 'Qwen Audio ASR',
baseUrl: 'https://token-plan.example/compatible-mode/v1',
contextLimit: 8000,
},
{
// Registered with an auth suffix; the base id is the voice id.
baseModelId: 'qwen-audio-3.0-asr-flash',
name: 'Qwen Audio ASR (duplicate provider)',
},
{ baseModelId: 'qwen-max', name: 'Qwen Max' },
{ baseModelId: 'qwen-audio-3.0-asr-flash-filetrans' },
{ isRuntime: true, baseModelId: 'qwen3-asr-flash-realtime' },
],
},
],
};
const options = extractVoiceModels(status);
expect(options).toHaveLength(1);
expect(options[0]).toEqual({
id: 'qwen-audio-3.0-asr-flash',
label: 'Qwen Audio ASR',
authType: 'USE_OPENAI',
baseUrl: 'https://token-plan.example/compatible-mode/v1',
contextWindow: 8000,
modalities: { audio: true },
});
});

it('returns an empty list for an undefined status', () => {
expect(extractVoiceModels(undefined)).toEqual([]);
});
});
4 changes: 4 additions & 0 deletions packages/web-shell/client/voice/voiceModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export function isVoiceModelId(id: string): boolean {
return (
/^qwen3-asr-flash-realtime(?:-|$)/.test(s) ||
/^qwen3-asr-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(s) ||
/^qwen-audio-[\d.]+-(?:asr-flash-(?:realtime|streaming)|realtime)(?:-|$)/.test(
s,
) ||
/^qwen-audio-[\d.]+-asr-flash(?:-\d{4}-\d{2}-\d{2})?$/.test(s) ||
/^(fun-asr|paraformer).*realtime(?:-|$)/.test(s)
);
}
Expand Down
Loading