Skip to content

feat(multimodal): support Qwen3 ASR and Omni - #1905

Merged
slin1237 merged 5 commits into
mainfrom
hongtaoc/qwen3-asr-omni
Jul 13, 2026
Merged

feat(multimodal): support Qwen3 ASR and Omni#1905
slin1237 merged 5 commits into
mainfrom
hongtaoc/qwen3-asr-omni

Conversation

@chenht2022

@chenht2022 chenht2022 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Description

Problem

SMG's multimodal pipeline supports image and video inputs, but does not provide first-class audio preprocessing or model contracts. As a result, Qwen3-ASR and the Qwen3-Omni Thinker cannot be served through the existing multimodal request path.

Several shared stages, including media planning, placeholder expansion, batching, hashing, and TokenSpeed transport, also assume vision-oriented inputs and need to handle multiple modalities consistently.

Solution

Add audio as a first-class multimodal input and generalize the shared preprocessing and transport pipeline across image, video, and audio.

The implementation adds audio decoding and log-Mel preprocessing, registers Qwen3-ASR and Qwen3-Omni processors, and preserves authored media ordering through batching and itemized TokenSpeed transport.

Qwen3-Omni support is limited to the Thinker with text output (return_audio=false). Talker, Code2Wav, audio-in-video, timestamps, generated audio, and audio EPD are out of scope.

Changes

  • Add audio decoding with an FFmpeg fallback.
  • Add Qwen3-compatible log-Mel preprocessing.
  • Add Qwen3-ASR and Qwen3-Omni processor specifications and prompt replacements.
  • Add Qwen3-Omni-specific vision preprocessing.
  • Generalize media planning, placeholder expansion, hashing, batching, and field layout handling across modalities.
  • Preserve mixed-modality ordering during preprocessing and request assembly.
  • Support itemized image, video, and audio transport to regular TokenSpeed workers.
  • Add focused multimodal, tokenizer, protocol, and routing tests.

Test Plan

The following commands pass:

  • cargo test -p llm-multimodal --lib
  • cargo test -p smg --lib routers::grpc::multimodal
  • cargo test -p smg --lib routers::grpc::router::tests
  • cargo test -p llm-tokenizer --test qwen2_vocab_merges
  • cargo test -p openai-protocol
  • cargo check -p smg
  • cargo fmt --all -- --check

Summary by CodeRabbit

  • New Features

    • Added end-to-end audio support in multimodal requests (including audio URL/data-url/inline audio) with audio decoding and preprocessing.
    • Added Qwen3-ASR transcription support with language handling and response formatting.
    • Added Qwen3-Omni support for combined image/video/audio requests.
    • Enhanced tokenizer auto-detection for Qwen2-style vocab+merges bundles.
    • Added video sampling metadata and per-frame resizing/budget controls.
  • Bug Fixes

    • Strengthened multimodal request validation and ensured audio/image/video placeholder/anchor consistency.
    • Improved multimodal routing and encoder input construction accuracy, especially for video sampling.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds end-to-end audio multimodal support, Qwen3 audio preprocessing, media-plan-driven gateway processing, Qwen3-ASR transcription routing, TokenSpeed audio handling, Qwen3-Omni video processing, and Qwen2 vocabulary/merges tokenizer loading.

Changes

Multimodal audio and encoder contracts

Layer / File(s) Summary
Audio decoding and DSP
crates/multimodal/src/audio/*, crates/multimodal/Cargo.toml
Adds Symphonia/FFmpeg decoding, mono conversion, limits, timeouts, resampling, and mel-filterbank processing.
Audio media and preprocessing
crates/multimodal/src/{types.rs,media.rs,tracker.rs}, crates/multimodal/src/audio/*
Adds audio sources/clips, fetching and tracking, processor registries, and Qwen3 audio encoder inputs.
Shared encoder contracts and model specs
crates/multimodal/src/{encoder_inputs.rs,error.rs,lib.rs}, registry/*, vision/*
Moves encoder-input types into shared modules and adds Qwen3 ASR/Omni specs plus video sampling metadata and budgets.

Protocol, tokenizer, and servicer integration

Layer / File(s) Summary
Audio API and TokenSpeed support
crates/protocols/*, grpc_servicer/..., crates/grpc_client/proto/*
Adds audio content types and output controls, recognizes audio modalities, preserves side-tensor dtypes, and updates encoder item offset handling.
Qwen2 tokenizer loading
crates/tokenizer/src/*, crates/tokenizer/tests/*
Loads Qwen2 vocab.json/merges.txt layouts, validates added-token IDs, and adds Qwen3-ASR parity coverage.

Gateway multimodal pipeline

Layer / File(s) Summary
Media planning and validation
model_gateway/src/routers/grpc/multimodal/{detect.rs,plan.rs}, regular/stages/*
Builds ordered media plans, resolves modality-specific placeholders, validates rendered anchors, and routes chat/messages processing through plans.
Multimodal processing and assembly
model_gateway/src/routers/grpc/multimodal/{mod.rs,process.rs,assemble.rs,config.rs}
Refactors intermediates into typed media batches and prompt bindings, preprocesses modalities concurrently, and assembles backend-specific outputs.
TokenSpeed routing and formatting
model_gateway/src/routers/grpc/{epd_encode.rs,common/stages/worker_selection.rs,multimodal/transport.rs}, utils/*
Updates encode routing hashes, worker selection, dtype precedence, and modality-aware prompt formatting.

ASR routing and output validation

Layer / File(s) Summary
Qwen3-ASR transcription endpoint
model_gateway/src/routers/grpc/router.rs, mod.rs, pd_router.rs, harmony/builder.rs
Adds Qwen3-ASR transcription request construction/parsing and rejects unsupported audio or non-text output paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GrpcRouter
    participant MediaPlan
    participant AsyncMultiModalTracker
    participant MediaConnector
    participant AudioProcessorRegistry
    participant TokenSpeedAssembly

    Client->>GrpcRouter: Submit chat request with audio content
    GrpcRouter->>MediaPlan: Build ordered media plan
    MediaPlan-->>GrpcRouter: Media parts and modality counts
    GrpcRouter->>AsyncMultiModalTracker: Enqueue audio
    AsyncMultiModalTracker->>MediaConnector: Fetch and decode audio
    MediaConnector-->>AsyncMultiModalTracker: AudioClip
    GrpcRouter->>AudioProcessorRegistry: Create model-specific processor
    AudioProcessorRegistry-->>GrpcRouter: Qwen3AudioProcessor
    GrpcRouter->>TokenSpeedAssembly: Assemble audio encoder inputs
    TokenSpeedAssembly-->>Client: Multimodal response
Loading
sequenceDiagram
    participant Client
    participant GrpcRouter
    participant Qwen3AsrRequestBuilder
    participant ChatPipeline
    participant OutputParser

    Client->>GrpcRouter: Submit audio transcription request
    GrpcRouter->>Qwen3AsrRequestBuilder: Build ASR chat request
    Qwen3AsrRequestBuilder-->>GrpcRouter: ChatCompletionRequest
    GrpcRouter->>ChatPipeline: Execute chat request
    ChatPipeline-->>GrpcRouter: Generated response
    GrpcRouter->>OutputParser: Parse transcription text
    OutputParser-->>Client: JSON or text transcription
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: key4ng, slin1237, catherinesue, gongwei-130, xinyuezhang369

Poem

A rabbit hears the waveforms hum,
Through mel-bright paths the samples run.
Audio joins the multimodal stream,
ASR turns sound to text and dream.
Plans and tokens hop in tune—
New ears bloom beneath the moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: adding Qwen3 ASR and Qwen3 Omni multimodal support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hongtaoc/qwen3-asr-omni

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added tokenizer Tokenizer related changes dependencies Dependency updates grpc gRPC client and router changes tests Test changes multimodal Multimodal crate changes protocols Protocols crate changes model-gateway Model gateway crate changes labels Jul 12, 2026
Signed-off-by: chenht2022 <chenht2022@gmail.com>
@chenht2022
chenht2022 force-pushed the hongtaoc/qwen3-asr-omni branch from af1adc9 to 5602d7a Compare July 12, 2026 18:38

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive audio preprocessing and decoding support to the multimodal pipeline, specifically targeting Qwen3-ASR and Qwen3-Omni models. It adds audio decoding via Symphonia with an FFmpeg fallback, implements a Whisper-compatible log-mel frontend using RustFFT, and refactors the gateway and gRPC servicers to handle multiple modalities concurrently. The review feedback focuses on performance optimizations: specifically, reusing the interleaved vector allocation outside the decoding loop in decode.rs, and pre-planning the FFT once before the batch loop in qwen3_audio.rs to avoid redundant, expensive planning operations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +124 to +125
let mut mono = Vec::new();
loop {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid allocating a new vector on every iteration of the decoding loop, declare interleaved outside the loop.

Suggested change
let mut mono = Vec::new();
loop {
let mut mono = Vec::new();
let mut interleaved = Vec::new();
loop {
References
  1. Avoid heap allocations (such as allocating a new Vec) in hot or periodic paths.

Comment thread crates/multimodal/src/audio/decode.rs Outdated
"failed to reserve {additional_samples} decoded audio samples: {error}"
))
})?;
let mut interleaved = vec![0.0_f32; interleaved_samples];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reuse the interleaved vector allocated outside the loop by resizing it instead of re-allocating on every packet.

Suggested change
let mut interleaved = vec![0.0_f32; interleaved_samples];
interleaved.resize(interleaved_samples, 0.0);
References
  1. Avoid heap allocations (such as allocating a new Vec) in hot or periodic paths.

}

let batch_size = waveforms.len();
let feature_values = batch_size

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Planning the FFT is an expensive operation. To avoid re-planning it for every single audio clip in the batch, initialize the FftPlanner and plan the forward FFT once before the loop, then pass it to whisper_log_mel.

        let batch_size = waveforms.len();
        let mut planner = FftPlanner::<f32>::new();
        let fft = planner.plan_fft_forward(self.params.n_fft);

.min(max_frames);
let mut padded = waveform;
padded.resize(max_samples, self.params.padding_value);
let features = whisper_log_mel(&padded, max_frames, &self.params)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass the pre-planned FFT to whisper_log_mel.

Suggested change
let features = whisper_log_mel(&padded, max_frames, &self.params)?;
let features = whisper_log_mel(&padded, max_frames, &self.params, &fft)?;

Comment on lines +333 to +337
fn whisper_log_mel(
samples: &[f32],
frame_count: usize,
params: &Qwen3AudioParams,
) -> Result<Array2<f32>, TransformError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the signature of whisper_log_mel to accept the pre-planned FFT.

Suggested change
fn whisper_log_mel(
samples: &[f32],
frame_count: usize,
params: &Qwen3AudioParams,
) -> Result<Array2<f32>, TransformError> {
fn whisper_log_mel(
samples: &[f32],
frame_count: usize,
params: &Qwen3AudioParams,
fft: &Arc<dyn rustfft::Fft<f32>>,
) -> Result<Array2<f32>, TransformError> {

Comment on lines +343 to +344
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(params.n_fft);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove the local FFT planning since it is now passed as an argument.

error!(
function = "MessagePreparationStage::execute",
"Multimodal content detected but multimodal components not initialized"
model = %model_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: This returns error::internal_error (HTTP 500), but the identical call in chat/preparation.rs returns error::bad_request (HTTP 400). Since prepare_placeholder_tokens can fail due to user-facing reasons (unsupported modality for the model, modality limit exceeded, placeholder token missing from vocab), the Messages API path should also surface this as a 400 so callers get an actionable error instead of a confusing 500.

Suggested change
model = %model_id,
error::bad_request(
"invalid_multimodal_request",
format!("Invalid multimodal request: {e}"),
)

Signed-off-by: chenht2022 <chenht2022@gmail.com>
@chenht2022
chenht2022 marked this pull request as ready for review July 12, 2026 19:01
@lightseek-bot lightseek-bot added the priority:high High priority label Jul 12, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70d6b34dc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +225 to +227
let feature_length = original_samples
.div_ceil(self.params.hop_length)
.min(max_frames);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Floor per-audio frame counts instead of rounding up

When a request batches multiple audio clips and a shorter clip's sample length is not an exact multiple of hop_length, this rounds its valid feature frames up while max_frames (and the Whisper STFT path described above) use floor division. That marks one padded frame as real and can inflate audio_feature_lengths/prompt token counts at chunk boundaries (for example around n_window * 2 frames), so the expanded audio placeholders no longer match the actual valid encoder output. Use the same floor calculation as max_frames for each item.

Useful? React with 👍 / 👎.

Comment on lines +120 to +123
error::internal_error(
"multimodal_placeholder_resolution_failed",
format!("Failed to resolve multimodal placeholder token: {e}"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return a client error for invalid Messages media

For Messages API requests that contain media but target a non-multimodal model, an unsupported modality, or too many media items, prepare_placeholder_tokens returns an input-validation error; mapping it to internal_error turns those client mistakes into 500s instead of 4xx responses. The chat path maps the same preparation failure to bad_request, so this should do the same to avoid false server errors/retries for invalid multimodal Messages requests.

Useful? React with 👍 / 👎.

Signed-off-by: chenht2022 <chenht2022@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
model_gateway/src/routers/grpc/utils/chat_utils.rs (1)

220-316: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Thread a placeholder into the None path The Go bindings still call process_chat_messages(..., None), so any multimodal ChatCompletionRequest sent through those entrypoints now fails instead of rendering a prompt. Pass the configured placeholder tokens there too, or keep the wrapper tolerant when no placeholders are supplied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model_gateway/src/routers/grpc/utils/chat_utils.rs` around lines 220 - 316,
Ensure the Go-binding entrypoints calling process_chat_messages no longer pass
None for multimodal requests: thread the configured PlaceholderTokens through to
transform_content_field. If those callers cannot provide tokens, update the None
path to preserve rendering compatibility instead of returning a
missing-placeholder error, while retaining placeholder substitution when tokens
are configured.
crates/multimodal/src/vision/processors/qwen_vl_base.rs (1)

544-611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doc comment on smart_resize_video no longer reflects the dual-mode (TotalVolume/PerFrame) budget logic.

The doc comment states the pixel budget is unconditionally "applied to the full sampled video volume (T * H * W)", but the body now branches on self.config.video_resize_mode, applying the budget per-frame for QwenVideoResizeMode::PerFrame. Worth updating so future readers don't assume single-mode behavior.

📝 Proposed doc update
     /// Smart resize for Qwen3-style video processors.
     ///
-    /// Unlike image resize, the pixel budget is applied to the full sampled
-    /// video volume (`T * H * W`), matching HuggingFace's Qwen3 video
-    /// processor.
+    /// Unlike image resize, the pixel budget is applied at video granularity.
+    /// With `QwenVideoResizeMode::TotalVolume` the budget covers the full
+    /// sampled volume (`T * H * W`), matching HuggingFace's Qwen3 video
+    /// processor. With `QwenVideoResizeMode::PerFrame` the budget applies to
+    /// a single frame's `H * W`, independent of frame count.
     pub fn smart_resize_video(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/src/vision/processors/qwen_vl_base.rs` around lines 544 -
611, Update the doc comment for smart_resize_video to describe both
video_resize_mode behaviors: TotalVolume applies the pixel budget to the padded
full video volume, while PerFrame applies it independently to a single frame.
Remove the unconditional claim that the budget always uses T * H * W.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/multimodal/Cargo.toml`:
- Around line 32-33: Update the symphonia dependency declaration in Cargo.toml
to remove features = ["all"] and enable only the explicit feature(s) required
for the existing WAV handling path. Preserve FFmpeg as the fallback and do not
add unrelated codec or container features.

In `@crates/multimodal/src/audio/transforms.rs`:
- Around line 66-152: The audio test suite lacks coverage for the
bandlimited_resample path. Add a golden-vector test using a non-16 kHz input
clip that exercises bandlimited_resample, and assert the complete expected
output and length to detect kernel/indexing or output-length regressions.

In `@crates/multimodal/src/media.rs`:
- Around line 348-374: Update fetch_http_audio and the matching HTTP media
fetcher to enforce the configured maximum payload size before buffering: reject
responses whose Content-Length exceeds the limit and stream body chunks while
tracking cumulative bytes, returning the existing appropriate error once the
limit is crossed. Replace unbounded resp.bytes() buffering with the bounded
collection path, while preserving normal decoding for responses within the
limit.

In `@crates/multimodal/src/vision/processors/qwen3_omni_vision.rs`:
- Around line 81-95: Remove the duplicate
from_shared_preprocessor_config_for_video implementation and delegate video
shared-config handling to from_preprocessor_config. Update
with_video_preprocessor_config to use that delegation directly, preserving the
existing image limit fallbacks, video defaults, and other with_limits arguments.
- Around line 143-150: Extract the duplicated six-field structural override
check from has_structural_overrides and
Qwen2VLProcessor::with_preprocessor_config into one shared helper on the common
configuration or processor-base type. Update both call sites to use that helper,
preserving the current fields and behavior so future additions require changing
only one implementation.

In `@crates/multimodal/src/vision/processors/qwen3_vl.rs`:
- Around line 91-93: Update Qwen3VLProcessor initialization in new, with_config,
and from_preprocessor_config so video_min_pixels and video_max_pixels use
dedicated video defaults or configuration overrides rather than
DEFAULT_MIN_PIXELS and DEFAULT_MAX_PIXELS. Keep image bounds assigned only to
the image fields, and preserve QwenVLProcessorBase::smart_resize_video reading
the independent video limits.

---

Outside diff comments:
In `@crates/multimodal/src/vision/processors/qwen_vl_base.rs`:
- Around line 544-611: Update the doc comment for smart_resize_video to describe
both video_resize_mode behaviors: TotalVolume applies the pixel budget to the
padded full video volume, while PerFrame applies it independently to a single
frame. Remove the unconditional claim that the budget always uses T * H * W.

In `@model_gateway/src/routers/grpc/utils/chat_utils.rs`:
- Around line 220-316: Ensure the Go-binding entrypoints calling
process_chat_messages no longer pass None for multimodal requests: thread the
configured PlaceholderTokens through to transform_content_field. If those
callers cannot provide tokens, update the None path to preserve rendering
compatibility instead of returning a missing-placeholder error, while retaining
placeholder substitution when tokens are configured.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a31903a-22c4-4088-aaa1-4193f3843f9b

📥 Commits

Reviewing files that changed from the base of the PR and between 517eb32 and 70d6b34.

📒 Files selected for processing (66)
  • crates/grpc_client/proto/tokenspeed_encoder.proto
  • crates/grpc_client/proto/tokenspeed_scheduler.proto
  • crates/multimodal/Cargo.toml
  • crates/multimodal/src/audio/decode.rs
  • crates/multimodal/src/audio/mod.rs
  • crates/multimodal/src/audio/processor.rs
  • crates/multimodal/src/audio/processors/mod.rs
  • crates/multimodal/src/audio/processors/qwen3_audio.rs
  • crates/multimodal/src/audio/transforms.rs
  • crates/multimodal/src/encoder_inputs.rs
  • crates/multimodal/src/error.rs
  • crates/multimodal/src/hasher.rs
  • crates/multimodal/src/lib.rs
  • crates/multimodal/src/media.rs
  • crates/multimodal/src/registry/kimi_k25.rs
  • crates/multimodal/src/registry/llama4.rs
  • crates/multimodal/src/registry/llava.rs
  • crates/multimodal/src/registry/mod.rs
  • crates/multimodal/src/registry/phi3_v.rs
  • crates/multimodal/src/registry/qwen3_asr.rs
  • crates/multimodal/src/registry/qwen3_omni.rs
  • crates/multimodal/src/registry/qwen3_vl.rs
  • crates/multimodal/src/registry/qwen_vl.rs
  • crates/multimodal/src/registry/traits.rs
  • crates/multimodal/src/tracker.rs
  • crates/multimodal/src/types.rs
  • crates/multimodal/src/vision/mod.rs
  • crates/multimodal/src/vision/processor.rs
  • crates/multimodal/src/vision/processors/kimi_k25.rs
  • crates/multimodal/src/vision/processors/llava.rs
  • crates/multimodal/src/vision/processors/mod.rs
  • crates/multimodal/src/vision/processors/qwen2_vl.rs
  • crates/multimodal/src/vision/processors/qwen3_omni_vision.rs
  • crates/multimodal/src/vision/processors/qwen3_vl.rs
  • crates/multimodal/src/vision/processors/qwen_vl_base.rs
  • crates/multimodal/src/vision/transforms.rs
  • crates/multimodal/tests/multimodal_tracker_test.rs
  • crates/protocols/src/chat.rs
  • crates/protocols/src/common.rs
  • crates/tokenizer/src/factory.rs
  • crates/tokenizer/src/huggingface.rs
  • crates/tokenizer/tests/qwen2_vocab_merges.rs
  • crates/tokenizer/tests/qwen3_asr_bpe_parity.rs
  • grpc_servicer/smg_grpc_servicer/tokenspeed/encoder_servicer.py
  • grpc_servicer/smg_grpc_servicer/tokenspeed/servicer.py
  • model_gateway/src/routers/grpc/common/stages/worker_selection.rs
  • model_gateway/src/routers/grpc/epd_encode.rs
  • model_gateway/src/routers/grpc/harmony/builder.rs
  • model_gateway/src/routers/grpc/mod.rs
  • model_gateway/src/routers/grpc/multimodal/assemble.rs
  • model_gateway/src/routers/grpc/multimodal/config.rs
  • model_gateway/src/routers/grpc/multimodal/detect.rs
  • model_gateway/src/routers/grpc/multimodal/mod.rs
  • model_gateway/src/routers/grpc/multimodal/plan.rs
  • model_gateway/src/routers/grpc/multimodal/process.rs
  • model_gateway/src/routers/grpc/multimodal/transport.rs
  • model_gateway/src/routers/grpc/pd_router.rs
  • model_gateway/src/routers/grpc/proto_wrapper.rs
  • model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs
  • model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs
  • model_gateway/src/routers/grpc/router.rs
  • model_gateway/src/routers/grpc/utils/chat_utils.rs
  • model_gateway/src/routers/grpc/utils/message_utils.rs
  • model_gateway/src/routers/grpc/utils/mod.rs
💤 Files with no reviewable changes (2)
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs
  • model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs

Comment on lines +32 to +33
rustfft = "6.4"
symphonia = { version = "0.6", default-features = false, features = ["all"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Determine which Symphonia codecs/formats are actually exercised before FFmpeg fallback.
rg -n 'symphonia' crates/multimodal/src/audio/decode.rs -A5
rg -n 'Codec|CodecType|register_all|get_codecs|get_formats|probe' crates/multimodal/src/audio/decode.rs

Repository: lightseekorg/smg

Length of output: 4955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Cargo.toml excerpt\n'
sed -n '1,80p' crates/multimodal/Cargo.toml

printf '\n## Symphonia decode implementation\n'
sed -n '84,170p' crates/multimodal/src/audio/decode.rs

printf '\n## Audio tests\n'
sed -n '470,540p' crates/multimodal/src/audio/decode.rs

Repository: lightseekorg/smg

Length of output: 7805


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
p = Path("crates/multimodal/src/audio/decode.rs")
text = p.read_text()
# print only lines with Symphonia hint/codecs references and tests names for quick scan
for i, line in enumerate(text.splitlines(), 1):
    if "get_probe()" in line or "get_codecs()" in line or "Hint" in line or "TrackType" in line or "wav_i16_mono" in line or "symphonia_decodes_wav_to_mono_f32" in line:
        print(f"{i}: {line}")
PY

Repository: lightseekorg/smg

Length of output: 704


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("crates/multimodal/src/audio/decode.rs")
for start, end in [(84,170), (470,540)]:
    print(f"\n### lines {start}-{end}")
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4}: {line}")
PY

Repository: lightseekorg/smg

Length of output: 6750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Audio dispatch and suffix helpers\n'
sed -n '1,220p' crates/multimodal/src/audio/decode.rs

printf '\n## Search for ffmpeg/symphonia backend selection\n'
rg -n 'audio_temp_suffix|SMG_AUDIO_DECODE_BACKEND|ffmpeg fallback|decode_audio_with_ffmpeg|decode_audio_with_symphonia' crates/multimodal/src/audio/decode.rs

Repository: lightseekorg/smg

Length of output: 9407


Narrow Symphonia's feature set features = ["all"] pulls in every Symphonia codec/container reader, but this path only has WAV coverage and FFmpeg already handles the fallback path. If Symphonia only needs a smaller in-process subset, list those features explicitly instead of enabling everything.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/Cargo.toml` around lines 32 - 33, Update the symphonia
dependency declaration in Cargo.toml to remove features = ["all"] and enable
only the explicit feature(s) required for the existing WAV handling path.
Preserve FFmpeg as the fallback and do not add unrelated codec or container
features.

Comment on lines +66 to +152
/// Match torchaudio's default `functional.resample`: band-limited sinc
/// interpolation with a Hann window, filter width 6, and rolloff 0.99.
pub(super) fn bandlimited_resample(
samples: &[f32],
src_sample_rate: usize,
dst_sample_rate: usize,
) -> Result<Vec<f32>, TransformError> {
const LOWPASS_FILTER_WIDTH: f32 = 6.0;
const ROLLOFF: f32 = 0.99;

if src_sample_rate == 0 || dst_sample_rate == 0 {
return Err(TransformError::ShapeError(
"audio resampling rates must be positive".to_string(),
));
}
if samples.is_empty() || src_sample_rate == dst_sample_rate {
return Ok(samples.to_vec());
}

let gcd = greatest_common_divisor(src_sample_rate, dst_sample_rate);
let orig_freq = src_sample_rate / gcd;
let new_freq = dst_sample_rate / gcd;
let base_freq = orig_freq.min(new_freq) as f32 * ROLLOFF;
let width = (LOWPASS_FILTER_WIDTH * orig_freq as f32 / base_freq).ceil() as usize;
let kernel_len = width
.checked_mul(2)
.and_then(|value| value.checked_add(orig_freq))
.ok_or_else(|| TransformError::ShapeError("audio resample kernel is too large".into()))?;
let kernel_values = new_freq
.checked_mul(kernel_len)
.ok_or_else(|| TransformError::ShapeError("audio resample kernel size overflow".into()))?;
let mut kernels = Vec::new();
kernels.try_reserve_exact(kernel_values).map_err(|error| {
TransformError::ShapeError(format!("failed to allocate audio resample kernel: {error}"))
})?;

let orig_freq_f32 = orig_freq as f32;
let new_freq_f32 = new_freq as f32;
let scale = base_freq / orig_freq_f32;
for phase in 0..new_freq {
for kernel_index in 0..kernel_len {
let idx = (kernel_index as f32 - width as f32) / orig_freq_f32;
let mut t = (idx - phase as f32 / new_freq_f32) * base_freq;
t = t.clamp(-LOWPASS_FILTER_WIDTH, LOWPASS_FILTER_WIDTH);
let window = (t * PI_F32 / LOWPASS_FILTER_WIDTH / 2.0).cos().powi(2);
let radians = t * PI_F32;
let sinc = if radians == 0.0 {
1.0
} else {
radians.sin() / radians
};
kernels.push(sinc * window * scale);
}
}

let target_len_u128 = (samples.len() as u128 * new_freq as u128).div_ceil(orig_freq as u128);
let target_len = usize::try_from(target_len_u128).map_err(|_| {
TransformError::ShapeError("resampled audio length exceeds usize".to_string())
})?;
let mut output = Vec::new();
output.try_reserve_exact(target_len).map_err(|error| {
TransformError::ShapeError(format!("failed to allocate resampled audio: {error}"))
})?;

for block in 0..samples.len().div_ceil(orig_freq) {
let input_start = block * orig_freq;
for phase in 0..new_freq {
if output.len() == target_len {
return Ok(output);
}
let kernel = &kernels[phase * kernel_len..(phase + 1) * kernel_len];
let mut value = 0.0_f32;
for (kernel_index, &coefficient) in kernel.iter().enumerate() {
let padded_index = input_start + kernel_index;
if padded_index >= width {
let sample_index = padded_index - width;
if let Some(&sample) = samples.get(sample_index) {
value = sample.mul_add(coefficient, value);
}
}
}
output.push(value);
}
}

Ok(output)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

torchaudio functional.resample sinc interpolation output length formula

💡 Result:

In torchaudio.functional.resample, the output length is calculated based on the ratio of the new frequency to the original frequency. Specifically, if the input waveform has a length of T, the output length is defined by the formula: Output Length = ceil(T * new_freq / orig_freq) This follows the standard definition of resampling in digital signal processing, where the number of samples scales linearly with the sampling rate ratio. The implementation uses bandlimited sinc interpolation, which treats the signal as a continuous function and samples it at the new rate, resulting in this length conversion [1][2][3].

Citations:


🏁 Script executed:

git ls-files crates/multimodal/src/audio/transforms.rs crates/multimodal | sed -n '1,120p'
rg -n "bandlimited_resample|resample|16k|16000|qwen3_audio" crates/multimodal -g '*.{rs,toml}'
python3 - <<'PY'
from pathlib import Path
p = Path('crates/multimodal/src/audio/transforms.rs')
print(p.exists(), p.stat().st_size if p.exists() else None)
PY

Repository: lightseekorg/smg

Length of output: 8797


🏁 Script executed:

sed -n '1,220p' crates/multimodal/src/audio/processors/qwen3_audio.rs
printf '\n---\n'
sed -n '220,640p' crates/multimodal/src/audio/processors/qwen3_audio.rs
printf '\n---\n'
sed -n '1,220p' crates/multimodal/src/audio/transforms.rs

Repository: lightseekorg/smg

Length of output: 27557


Add a test for the resampling path. The current audio tests only cover 16 kHz input, so bandlimited_resample never runs. A golden-vector case for a non-16 kHz clip would catch kernel/indexing or output-length drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/src/audio/transforms.rs` around lines 66 - 152, The audio
test suite lacks coverage for the bandlimited_resample path. Add a golden-vector
test using a non-16 kHz input clip that exercises bandlimited_resample, and
assert the complete expected output and length to detect kernel/indexing or
output-length regressions.

Comment thread crates/multimodal/src/media.rs
Comment thread crates/multimodal/src/vision/processors/qwen3_omni_vision.rs
Comment on lines +143 to +150
fn has_structural_overrides(config: &PreProcessorConfig) -> bool {
config.patch_size.is_some()
|| config.merge_size.is_some()
|| config.min_pixels.is_some()
|| config.max_pixels.is_some()
|| config.temporal_patch_size.is_some()
|| config.size.is_some()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

has_structural_overrides duplicates the same override check in qwen2_vl.rs.

Qwen2VLProcessor::with_preprocessor_config (in qwen2_vl.rs) inlines an identical 6-field override check. Consider hoisting this into a shared helper (e.g. on PreProcessorConfig or QwenVLProcessorBase) to avoid drift if a new structural field is added later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/src/vision/processors/qwen3_omni_vision.rs` around lines
143 - 150, Extract the duplicated six-field structural override check from
has_structural_overrides and Qwen2VLProcessor::with_preprocessor_config into one
shared helper on the common configuration or processor-base type. Update both
call sites to use that helper, preserving the current fields and behavior so
future additions require changing only one implementation.

Comment on lines +91 to +93
video_min_pixels: DEFAULT_MIN_PIXELS,
video_max_pixels: DEFAULT_MAX_PIXELS,
video_resize_mode: QwenVideoResizeMode::TotalVolume,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and map the processor implementations.
git ls-files | rg 'crates/.*/qwen3_vl\.rs$|qwen_vl_base\.rs$|qwen3_omni.*\.rs$|process\.rs$'

echo '--- qwen3_vl.rs outline ---'
ast-grep outline crates/multimodal/src/vision/processors/qwen3_vl.rs --view expanded || true

echo '--- qwen_vl_base.rs outline ---'
ast-grep outline crates/multimodal/src/vision/processors/qwen_vl_base.rs --view expanded || true

echo '--- qwen3_omni files outline ---'
for f in $(git ls-files | rg 'qwen3_omni.*\.rs$'); do
  echo "FILE: $f"
  ast-grep outline "$f" --view expanded || true
done

Repository: lightseekorg/smg

Length of output: 10958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact constructors and video preprocessing logic with line numbers.
sed -n '1,240p' crates/multimodal/src/vision/processors/qwen3_vl.rs
echo '---'
sed -n '1,260p' crates/multimodal/src/vision/processors/qwen_vl_base.rs

Repository: lightseekorg/smg

Length of output: 15239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the video-related constructors and any dedicated video config paths.
sed -n '1,240p' crates/multimodal/src/vision/processors/qwen3_vl.rs
echo '---'
sed -n '1,260p' crates/multimodal/src/vision/processors/qwen_vl_base.rs
echo '---'
for f in $(git ls-files | rg 'qwen3_omni.*\.rs$'); do
  echo "FILE: $f"
  sed -n '1,260p' "$f"
  echo '---'
done

Repository: lightseekorg/smg

Length of output: 33308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the exact lines and the downstream usage of the fields.
nl -ba crates/multimodal/src/vision/processors/qwen3_vl.rs | sed -n '1,260p'
echo '---'
nl -ba crates/multimodal/src/vision/processors/qwen_vl_base.rs | sed -n '1,320p'
echo '---'
rg -n "video_min_pixels|video_max_pixels|video_resize_mode|from_video_preprocessor_config|with_video_preprocessor_config|smart_resize_video|preprocess_video" crates/multimodal/src/vision/processors

Repository: lightseekorg/smg

Length of output: 194


🌐 Web query:

Qwen3-VL video_min_pixels video_max_pixels smart_resize_video Qwen3 VL config

💡 Result:

In Qwen3-VL, video processing parameters are used to manage the input resolution and token budget for video data during training and inference [1][2][3]. Key Parameters: - video_max_pixels: Defines the maximum total pixel count allowed for an entire video [3]. For a video with T frames, height H, and width W, the product T × H × W must not exceed this value [3]. Common default configurations or examples include 1664 * 28 * 28 or 1024 * 28 * 28 [1][2]. - video_min_pixels: Sets the minimum allowable pixel count for the video, similarly calculated as the total budget across all frames [3]. Examples include 256 * 28 * 28 [1][2]. - smart_resize_video: This refers to a processing logic (often implemented in supporting utilities or custom pipelines like those found in multimodal wrappers) that handles the resizing and patchification of video frames to fit the model's patch-based vision encoder requirements [4][5]. It ensures that input video frames are resized and structured correctly—maintaining spatial-temporal integrity—before being processed by the vision transformer [4][5]. Configuration Context: These parameters are typically managed within DataArguments in training scripts or through the processor's configuration dictionary [1][2][3]. Because Qwen3-VL processes video as a sequence of patches, setting these limits is critical for managing GPU memory consumption and preventing Out-Of-Memory (OOM) errors [6][3]. If the total number of tokens (derived from pixels and patches) exceeds model capacity, performance or stability may be compromised [3]. For detailed implementation logic, developers are often directed to the qwen_vl_utils/vision_process.py utility or equivalent processor definitions [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the video planning and preprocessing code paths that consume the video config.
sed -n '360,620p' crates/multimodal/src/vision/processors/qwen_vl_base.rs
echo '---'
sed -n '1200,1305p' crates/multimodal/src/vision/processors/qwen_vl_base.rs
echo '---'
sed -n '630,690p' crates/multimodal/src/vision/processors/qwen3_vl.rs
echo '---'
sed -n '230,290p' crates/multimodal/src/vision/processors/qwen3_vl.rs

Repository: lightseekorg/smg

Length of output: 18013


Keep Qwen3-VL video limits independent from image limits
QwenVLProcessorBase::smart_resize_video reads video_min_pixels/video_max_pixels separately, but Qwen3VLProcessor always copies the image bounds into those fields in new, with_config, and from_preprocessor_config (lines 91-93, 116-118, 141-149). That removes video-specific budgeting for this processor; add a video-specific defaults/override path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/src/vision/processors/qwen3_vl.rs` around lines 91 - 93,
Update Qwen3VLProcessor initialization in new, with_config, and
from_preprocessor_config so video_min_pixels and video_max_pixels use dedicated
video defaults or configuration overrides rather than DEFAULT_MIN_PIXELS and
DEFAULT_MAX_PIXELS. Keep image bounds assigned only to the image fields, and
preserve QwenVLProcessorBase::smart_resize_video reading the independent video
limits.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a5af02f3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +149 to +150
messages.push(ChatMessage::System {
content: MessageContent::Text(prompt.to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize Qwen3-ASR prompt text before templating

When /v1/audio/transcriptions callers pass prompt, this copies that user-controlled text directly into a Qwen3-ASR system turn. The Qwen3-ASR prompt format uses ChatML controls and the <asr_text> delimiter, and upstream vLLM strips those tokens for this builder to prevent reconstruction attacks (see https://docs.vllm.ai/en/stable/api/vllm/model_executor/models/qwen3_asr/#_sanitize_transcription_user_text), so a prompt containing <|im_end|> or <asr_text> can change the rendered prompt/assistant prefix and corrupt or steer the parsed transcription. Sanitize the prompt before inserting it.

Useful? React with 👍 / 👎.

vLLM 0.25 eagerly imports torchcodec, which dlopens FFmpeg shared libraries at import; the e2e runner image ships none, so every vLLM worker died importing vllm. Install distro FFmpeg in the vLLM setup and add an import canary so a missing-lib regression fails at install rather than mid-test.

Signed-off-by: lightseek-bot <243258330+lightseek-bot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/ci_install_vllm.sh`:
- Around line 58-63: Update the dependency installation flow in
scripts/ci_install_vllm.sh to explicitly install a Torch-compatible torchcodec
before the existing `python3 -c` import canary, or raise the vLLM lower bound to
a release whose metadata requires torchcodec. Ensure the canary continues
validating imports of torch, torchcodec, and vllm on a clean runner.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: faf774db-c744-47c3-9163-19cfa746c6ae

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5af02 and 08e4d4f.

📒 Files selected for processing (1)
  • scripts/ci_install_vllm.sh

Comment on lines +58 to +63
# Import canary: fail here (not mid-e2e) if vLLM's eager torchcodec import
# can't find the FFmpeg shared libs installed above (torch first so its
# bundled CUDA libraries are loaded)
python3 -c "import torch, torchcodec, vllm"
echo "vllm/torchcodec import canary OK"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from importlib.metadata import requires

deps = requires("vllm") or []
assert any(dep.lower().startswith("torchcodec") for dep in deps), (
    "Resolved vLLM does not declare torchcodec; install it explicitly."
)
import torchcodec
print("torchcodec dependency/import verified")
PY

Repository: lightseekorg/smg

Length of output: 1085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the script
cat -n scripts/ci_install_vllm.sh | sed -n '1,140p'

Repository: lightseekorg/smg

Length of output: 5555


🌐 Web query:

vllm 0.22.1 PyPI JSON requires_dist torchcodec

💡 Result:

vLLM version 0.22.1 does not strictly require torchcodec as a core dependency for installation, although it is utilized as an optional backend for video decoding features [1]. While vLLM has integrated torchcodec to replace older backends like decord for video processing [2][1], it is generally managed as an optional or platform-specific component rather than a hard requirement for all installations [3][4]. For instance, scripts exist to facilitate its installation specifically for ROCm compatibility [3], and it has been removed from certain audio-related dependency lists in related forks [4]. PyPI metadata for vllm 0.22.1 lists numerous dependencies [5], but torchcodec is not mandated for basic LLM inference functionality [6]. Users requiring video decoding features should ensure their environment meets the requirements for torchcodec, which may include manual installation depending on the platform [3][2].

Citations:


🌐 Web query:

vllm 0.24.0 PyPI JSON requires_dist torchcodec

💡 Result:

In vLLM version 0.24.0, torchcodec is utilized as an optional video decoding backend [1]. While torchcodec is integrated into the framework to support efficient video decoding [2][1], it is not listed as a mandatory dependency in the core PyPI package installation requirements (install_requires) [3]. The project previously moved to adopt torchcodec as a replacement for decord [4], but it is treated as an optional component that users may choose to install for specific functionality. Consequently, you will not find torchcodec in the requires_dist field of the vLLM 0.24.0 PyPI metadata [3]. Because torchcodec itself lacks a formal dependency constraint on PyTorch in its own PyPI metadata [5], it is typically managed as an optional environment dependency rather than a core package requirement [3]. Documentation and utility functions within vLLM, such as vllm.utils.import_utils.check_torchcodec_available, are provided to handle cases where this optional package is missing or not installed [6].

Citations:


Install torchcodec before the import canary. The current vllm>=0.22.1 pin still allows releases whose metadata does not pull in torchcodec, so this can fail on a clean runner with ModuleNotFoundError. Install a Torch-compatible torchcodec explicitly, or raise the lower bound to a release that includes it in requires_dist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci_install_vllm.sh` around lines 58 - 63, Update the dependency
installation flow in scripts/ci_install_vllm.sh to explicitly install a
Torch-compatible torchcodec before the existing `python3 -c` import canary, or
raise the vLLM lower bound to a release whose metadata requires torchcodec.
Ensure the canary continues validating imports of torch, torchcodec, and vllm on
a clean runner.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08e4d4f8af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +145 to +151
Modality::Video => Self::replacements(
metadata,
preprocessed,
modality,
"video_token_id",
VIDEO_PAD_TOKEN,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror Qwen3-VL video placeholder layout

For Qwen3-Omni video inputs, this falls through to the generic replacement helper, which emits one flat run of <|video_pad|> tokens and leaves structural_prefix at 0. The Qwen3-VL spec handles the same video placeholder contract by using video_grid_thw to split multi-frame clips into per-frame vision blocks and by including the template-owned leading <|vision_start|> in the reported range (crates/multimodal/src/registry/qwen3_vl.rs:250-272), because the backend M-RoPE pass scans that range for frame markers. With a normal Omni video where video_grid_thw[0] > 1 (and even for backends that need the leading marker on single-frame videos), the placeholder metadata lacks those markers, so video position encoding can fail or be assigned to the wrong range; the Omni video branch should mirror the Qwen3-VL video replacement path.

Useful? React with 👍 / 👎.

Signed-off-by: chenht2022 <chenht2022@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
model_gateway/src/routers/grpc/utils/chat_utils.rs (1)

252-269: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Convert media-only content to an empty string when anchors are absent.

With placeholder_tokens == None, Line 253 skips every media part. For media-only input, both vectors remain empty and Line 266 leaves the original parts array in a String template instead of omitting media.

Proposed fix
             let mut media_parts: Vec<String> = Vec::new();
             let mut text_parts: Vec<String> = Vec::new();
+            let mut has_supported_media = false;
             for part in content_array {
@@
                     Some(type_name @ ("image_url" | "video_url" | "audio_url" | "input_audio")) => {
                         let modality = modality_for_chat_part(type_name).ok_or_else(|| {
                             format!("unsupported media content part type: {type_name}")
                         })?;
+                        has_supported_media = true;
                         let Some(tokens) = placeholder_tokens else {
                             continue;
                         };
@@
-            if !media_parts.is_empty() || !text_parts.is_empty() {
+            if has_supported_media || !media_parts.is_empty() || !text_parts.is_empty() {
                 let ordered: Vec<String> = media_parts.into_iter().chain(text_parts).collect();
                 *content_value = Value::String(ordered.join("\n"));
             }

Add a media-only None regression test expecting "".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model_gateway/src/routers/grpc/utils/chat_utils.rs` around lines 252 - 269,
Update the content conversion logic around placeholder_tokens and the
media_parts/text_parts aggregation so media-only input with placeholder_tokens
== None produces Value::String("") rather than preserving the original parts
array; retain existing placeholder handling when tokens are available and add a
regression test covering the media-only None case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@model_gateway/src/routers/grpc/router.rs`:
- Around line 100-107: Replace the iterative loop in sanitize_qwen3_asr_prompt
with a linear-time, stack-based single-pass sanitizer that removes all
chat/control tokens and ASR_TEXT_TAG occurrences without repeatedly rescanning
or reallocating the full prompt. Preserve the current sanitization result for
nested and repeated token inputs.

---

Outside diff comments:
In `@model_gateway/src/routers/grpc/utils/chat_utils.rs`:
- Around line 252-269: Update the content conversion logic around
placeholder_tokens and the media_parts/text_parts aggregation so media-only
input with placeholder_tokens == None produces Value::String("") rather than
preserving the original parts array; retain existing placeholder handling when
tokens are available and add a regression test covering the media-only None
case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2f3d7e4-9dc8-4ef0-b7b4-cc9cc66d8fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 08e4d4f and d56bd75.

📒 Files selected for processing (9)
  • crates/multimodal/src/audio/decode.rs
  • crates/multimodal/src/audio/processors/qwen3_audio.rs
  • crates/multimodal/src/error.rs
  • crates/multimodal/src/media.rs
  • crates/multimodal/src/vision/processors/qwen3_omni_vision.rs
  • crates/multimodal/src/vision/processors/qwen_vl_base.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
  • model_gateway/src/routers/grpc/router.rs
  • model_gateway/src/routers/grpc/utils/chat_utils.rs

Comment on lines +100 to +107
fn sanitize_qwen3_asr_prompt(mut text: String) -> String {
loop {
let sanitized = strip_chatml_like_tokens(&text).replace(ASR_TEXT_TAG, "");
if sanitized == text {
return text;
}
text = sanitized;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make prompt-control sanitization linear.

This repeatedly rescans and reallocates the complete prompt. Nested input such as "<|a".repeat(n) + "|>".repeat(n) removes one layer per pass, yielding O(n²) work on the request task. Use a stack-based single-pass sanitizer, or reject/cap pathological nesting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model_gateway/src/routers/grpc/router.rs` around lines 100 - 107, Replace the
iterative loop in sanitize_qwen3_asr_prompt with a linear-time, stack-based
single-pass sanitizer that removes all chat/control tokens and ASR_TEXT_TAG
occurrences without repeatedly rescanning or reallocating the full prompt.
Preserve the current sanitization result for nested and repeated token inputs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d56bd7597f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +285 to +286
let data = data.trim();
let decoded = BASE64_STANDARD.decode(data)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the audio input limit on data URLs

When chat input_audio or the transcription adapter supplies audio, it reaches this data-URL path and decodes the entire base64 payload without applying audio_max_input_bytes(), unlike HTTP audio URLs which use collect_http_body_with_limit. A large inline audio request therefore allocates the base64 string plus decoded bytes and then enters the decoder before any input-size rejection, making the advertised audio input limit ineffective for the main inline-audio path.

Useful? React with 👍 / 👎.

Comment on lines +141 to +144
|| ["model", "model_path", "tokenizer", "tokenizer_path"]
.iter()
.filter_map(|key| metadata.spec.labels.get(*key))
.any(|value| is_qwen3_asr_identifier(value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include model_type when recognizing Qwen3-ASR workers

For a Qwen3-ASR worker served under an alias or local path that does not literally contain qwen3-asr, worker discovery still reports the reliable model_type=qwen3_asr label, but this check only inspects path-like labels. In that deployment /v1/audio/transcriptions is rejected as unsupported before the chat pipeline runs, even though the registry and processor would handle the model; include model_type (and similar config labels) in this predicate.

Useful? React with 👍 / 👎.

@slin1237
slin1237 merged commit 4219188 into main Jul 13, 2026
53 of 55 checks passed
@slin1237
slin1237 deleted the hongtaoc/qwen3-asr-omni branch July 13, 2026 06:13
slin1237 added a commit that referenced this pull request Jul 13, 2026
Add an e2e test for POST /v1/audio/transcriptions against a TokenSpeed
Qwen3-ASR worker, driven through the standard api_client (OpenAI SDK) /
model fixtures like the other chat_completions e2e tests. Covers a
whole-file transcription, the text response_format, and 400 rejection of
an unsupported language. Runs in the existing e2e-1gpu-chat (tokenspeed)
lane via the engine marker.

Refs #1905

Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates grpc gRPC client and router changes model-gateway Model gateway crate changes multimodal Multimodal crate changes priority:high High priority protocols Protocols crate changes tests Test changes tokenizer Tokenizer related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants