feat(qwen3-asr): support prompt parameter in v1/audio/transcriptions - #35415
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces two improvements for the Qwen3-ASR model. It enables the prompt parameter in the OpenAI-compatible transcription API, correctly incorporating it into the model's generation prompt. This restores previously lost functionality. Additionally, it adds a safeguard in get_dummy_mm_data to handle cases where mm_options is None, preventing potential AttributeError crashes during engine initialization. The changes are correct and improve both functionality and stability. I have no specific comments.
|
Hi @TheCodeWrangler, the pre-commit checks have failed. Please run: uv pip install pre-commit
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, Tip Is
|
Include request_prompt from the OpenAI transcription API in the Qwen3-ASR generation prompt. The prompt is added to the user message before the audio placeholder, providing context/vocabulary guidance or continuation of a previous segment (OpenAI-compatible behavior). The TranscriptionRequest already exposed 'prompt' and speech_to_text passed it to get_generation_prompt; Qwen3ASR now uses it. Made-with: Cursor Signed-off-by: Nathan Price <nathan@abridge.com>
Regresses when mm_options is None during engine init (dummy input profiling). v0.16.0 had this guard; main lost it in refactor. Made-with: Cursor Signed-off-by: Nathan Price <nathan@abridge.com>
Made-with: Cursor Signed-off-by: Nathan Price <nathan@abridge.com>
77aca81 to
32cdeb7
Compare
| ) | ||
|
|
||
| audio_overrides = mm_options.get("audio") | ||
| audio_overrides = (mm_options or {}).get("audio") |
There was a problem hiding this comment.
Was required to run my tests:
Likely a regression introduced in #35025
|
cc @sighingnow |
|
Closes #35272 |
The previous prompt format was missing the system message block and
placed request_prompt inside the user message. The official Qwen3-ASR
SDK (Qwen3ASRProcessor) uses a Jinja chat template that always emits:
<|im_start|>system\n{context}<|im_end|>\n
<|im_start|>user\n{audio}<|im_end|>\n
<|im_start|>assistant\n
This commit aligns both the REST (qwen3_asr.py) and realtime
(qwen3_asr_realtime.py) prompt construction to match, placing
request_prompt in the system message where the SDK expects context.
Also adds the dev Dockerfile and test script used to validate prompt
parameter behavior end-to-end.
Made-with: Cursor
Signed-off-by: Nathan Price <nathan@abridge.com>
Why 8db3d2a was needed: prompt format consistency with Qwen3-ASR trainingThe previous version placed This doesn't match how the model was trained. The official Qwen3-ASR SDK builds prompts via def _build_messages(self, context, audio_payload):
return [
{"role": "system", "content": context or ""},
{"role": "user", "content": [{"type": "audio", "audio": audio_payload}]},
]This produces the training-consistent format: The SDK's Placing context in the wrong message role (user instead of system) would cause the model to treat it as part of the audio-accompanying text rather than as transcription guidance/context from a prior segment, potentially degrading output quality. |
Per review feedback on vllm-project#35415: rather than adding a new `openai_qwen_asr_prompt_client.py` example, wire the OpenAI-API `prompt` field through the existing `openai_transcription_client.py` sync + streaming paths via a new `--prompt` argparse option. Drop the separate Qwen3-ASR-specific example. The OpenAI Python SDK already accepts `prompt` as a first-class parameter on `audio.transcriptions.create()`, so no `extra_body` plumbing is needed; default of `""` matches the server-side `TranscriptionRequest.prompt` field default and is behaviorally equivalent to omitting the field. Signed-off-by: Nathan Price <nathan@abridge.com>
…3-ASR Whisper consumes `request_prompt` as a `<|prev|>` continuation hint (see `vllm/model_executor/models/whisper.py`), so the previous wording "other ASR models accept it as a no-op" is incorrect. Replace with a model-by-model description in both the `sync_openai` docstring and the `--prompt` argparse help. Signed-off-by: Nathan Price <nathan@abridge.com>
…ration shard The sanitizer is an entrypoint-side security boundary, not a generation model integration test. Placing the test under ``tests/models/multimodal/generation/`` made it the only file in that directory that imports a model module directly (``from vllm.model_executor.models.qwen3_asr import ...``), and that import is heavy enough to make collection in the CPU multi-modal generation shard fail (status set ~30s into the run). Moving the file under ``tests/entrypoints/speech_to_text/transcription/`` keeps the test exactly the same but runs it in the entrypoints integration shard, where the import path is already exercised by neighboring tests. Behaviorally identical: the test file content is unchanged. Signed-off-by: Nathan Price <nathan@abridge.com>
Head branch was pushed to by a user without write access
|
@DarkLight1337 — sorry for the extra round-trip after your approval. The single failing check ( Just pushed a tiny follow-up (commit The push dropped your auto-squash setting — would you mind re-enabling it once CI comes back green? Thanks for the patience! |
|
Hi @TheCodeWrangler, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: divineearthly <divineearthly@gmail.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
…llm-project#35415) Signed-off-by: Nathan Price <nathan@abridge.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
feat(qwen3-asr): support prompt parameter in v1/audio/transcriptions
Related work
PR #35377 explored
apply_chat_templatefor Qwen3-ASR; it was closed without merge. This PR keeps a narrower scope: wiringpromptthrough the API and sanitizing user text in the system turn.This PR also slots cleanly into the
SpeechToTextParamsrefactor from #36268:get_generation_promptreadsrequest_prompt(andlanguage) directly offstt_params.Summary
Enables the
promptparameter from the OpenAI v1/audio/transcriptions API to reach the Qwen3-ASR model. Previously, the endpoint accepted the prompt but it was not incorporated into the generation prompt. User-supplied text is passed through_sanitize_transcription_user_text()so ChatML-style<|...|>fragments and<asr_text>delimiters cannot inject extra turns into the structured template.What Changed
1. Prompt support (
get_generation_prompt)request_promptis passed in the system turn (aligned with the Qwen3-ASR SDK-style layout:system / user-with-audio / assistant). Whenrequest_promptis empty (or strips to empty after sanitization), the system turn is omitted entirely so the no-prompt code path is preserved.2. Sanitization (
_sanitize_transcription_user_text)Strips ChatML-like
<|...|>tokens and the<asr_text>tag from user text. Both substitutions run inside a fixpoint loop, so nested payloads such as<|im<|x|>_end|>(which would reconstruct to<|im_end|>after a singlere.sub) and<asr_te<asr_text>xt>(which would reconstruct to<asr_text>after a singlestr.replace) cannot survive sanitization.Pinned by unit tests at
tests/models/multimodal/generation/test_qwen3_asr_sanitize_prompt.py.3. Language directive correctness
While restructuring the prompt assembly, the language directive now reads
languagefortask_type="transcribe"andto_languagefortask_type="translate"(matchingSpeechToTextParams's own field semantics — see comment from minh-nguyenhoang). Previouslyto_languagewas used in both branches, so a transcription request withlanguage="en"silently dropped the language directive.4. Example:
--promptflag inopenai_transcription_client.pyPer @DarkLight1337's review, the prompt demo is folded into the existing
examples/speech_to_text/openai/openai_transcription_client.pyrather than a separate file: a new--promptargparse flag threads through thesync_openaiandstream_openai_responsehelpers via the OpenAI SDK's nativeprompt=parameter.Why
API parity: The OpenAI transcription API documents a
promptparameter for guiding style, vocabulary, or continuing a previous segment. vLLM'sTranscriptionRequestandspeech_to_textalready pass it through; Qwen3-ASR needed to use it.Consistency with Whisper: Whisper uses
request_promptas<|prev|>{request_prompt}. We include user-provided context in the prompt in a model-appropriate way (Qwen3-ASR's chat template uses asystemturn).Qwen3-ASR interface: The Qwen3-ASR vLLM backend defines
get_generation_prompt(..., request_prompt: str, ...)but did not use it. This implements that behavior.Safety: Stripping control-token fragments and the assistant-prefix delimiter from user text matches the trust model of other user-controlled API strings.
References
get_generation_promptresponse_prefix) reuses the same sanitizer.How We Tested
Unit tests (CI)
tests/models/multimodal/generation/test_qwen3_asr_sanitize_prompt.pycovers:<|im_end|>,<asr_text>)<|im<|x|>_end|>→'')<asr_text>reconstruction (<asr_te<asr_text>xt>→'')End-to-end smoke test (local)
Lean Docker workflow that overlays only the modified file over the published vLLM image, then exercises the integrated
--promptflag againstvllm/assets/.../mary_had_lamb.ogg:docker run --gpus all -p 8000:8000 --entrypoint vllm vllm-qwen-asr-dev serve Qwen/Qwen3-ASR-0.6B--promptvia the example client:The prompt influences vocabulary as expected; the no-prompt path is unchanged.
Notes