Skip to content

feat(grpc): add vLLM multimodal support and split pipeline into fetch + preprocess - #497

Merged
CatherineSue merged 12 commits into
mainfrom
chang/mm-5
Feb 22, 2026
Merged

CatherineSue merged 12 commits into
mainfrom
chang/mm-5

Conversation

@CatherineSue

@CatherineSue CatherineSue commented Feb 21, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

The gRPC multimodal pipeline only supported SGLang. Adding vLLM support revealed a fundamental architecture issue: SGLang and vLLM have completely different expectations for multimodal data.

SGLang requires:

  • Preprocessed pixel tensors (normalized, tiled float32 arrays)
  • Expanded token IDs (placeholder tokens replaced with per-tile token sequences)
  • Model-specific metadata tensors (aspect_ratios, image_grid_thw, etc.)
  • A dedicated im_token_id for its pad_input_tokens function

vLLM requires:

  • Raw image bytes (JPEG/PNG) — it runs its own vision preprocessor internally
  • Original (unexpanded) token IDs — it handles placeholder expansion itself
  • No pixel tensors or model-specific metadata needed

The old process_multimodal() ran the full SGLang-specific pipeline (fetch + preprocess + expand) in ChatPreparationStage, before the backend type was known (backend is determined at ClientAcquisitionStage). This meant:

  1. vLLM received SGLang-specific preprocessed data it didn't need (wasted CPU)
  2. An original_token_ids workaround was needed to send unexpanded tokens to vLLM
  3. No clean way to send raw image bytes to vLLM

Solution

Split the multimodal pipeline into two phases, with the backend-agnostic work in Phase 1 and backend-specific work in Phase 2:

Phase 1 — ChatPreparationStage (backend-agnostic):

  • fetch_images(): Uses the multimodal tracker to extract image URLs from chat messages and fetch them concurrently as raw ImageFrames
  • Token IDs stay unexpanded
  • Fetched images stored on ProcessedMessages.multimodal_images as Vec<Arc<ImageFrame>>
  • No model registry dependency — the tracker only needs a MediaConnector

Phase 2 — ChatRequestBuildingStage (backend-specific):

  • process_for_backend() routes based on builder_client.is_sglang():
    • SGLang path → preprocess_for_sglang(): load model config, run vision preprocessor (pixel normalization, tiling), compute prompt replacements, expand placeholder tokens, build full MultimodalData with pixel tensors + metadata
    • vLLM path → build_vllm_multimodal_data(): extract raw JPEG/PNG bytes from ImageFrame, build minimal MultimodalData with only image_data

This also introduces MultimodalData and TensorBytes as backend-agnostic types with into_sglang_proto() and into_vllm_proto() converters.

TrackerConfig removal: The old TrackerConfig required resolving the model spec (via ModelRegistry) at tracker construction to obtain placeholder token strings (for ConversationSegment tracking) and modality limits (for early rejection via ModalityLimit). Both are dead in the new architecture — token expansion operates directly on token_ids in Phase 2, and ConversationSegment/PlaceholderMap bookkeeping is no longer needed. Removing TrackerConfig eliminates the model registry dependency from Phase 1, so whether a model has token expansion configs (e.g. llava, phi3v, llama4_vision) is irrelevant when all Phase 1 does is download image bytes.

Changes

  • Add TensorBytes, MultimodalData types with into_sglang_proto() / into_vllm_proto() converters in proto_wrapper.rs
  • Add MultimodalInputs message to vLLM proto and engine client
  • Split process_multimodal() into fetch_images(), preprocess_for_sglang(), build_vllm_multimodal_data(), process_for_backend()
  • Change ProcessedMessages.multimodal_inputs → multimodal_images: Option<Vec<Arc<ImageFrame>>>
  • Move backend-specific multimodal processing from ChatPreparationStage to ChatRequestBuildingStage
  • Remove original_token_ids workaround from PreparationOutput
  • Fix UintTensor/UintVec serialization: was casting u32→i64 (8 bytes) with dtype "uint32" (4 bytes)
  • Add non-contiguous pixel tensor fallback in build_multimodal_data
  • Replace unwrap() with proper error handling in request building stage
  • Remove TrackerConfig, ConversationSegment, PlaceholderHandle, PlaceholderMap, DEFAULT_PLACEHOLDERS, and ModalityLimit error variant
  • Simplify AsyncMultiModalTracker::new() to only require MediaConnector (no model registry needed)
  • Decouple Phase 1 image fetching from model registry — registry only needed in Phase 2

Test Plan

  • cargo test -p smg -- grpc::multimodal — all 9 tests pass
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • E2E: Send image chat request to vLLM backend → model receives raw bytes, describes image correctly
  • E2E: Send image chat request to SGLang backend → full preprocessing + token expansion, model works as before
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated

Summary by CodeRabbit

  • New Features

    • Backend-agnostic multimodal image support with staged image fetching and backend-specific preprocessing.
  • Changes

    • New public MultimodalData and TensorBytes types with converters for supported backends.
    • Processed messages now carry multimodal images (field renamed) and request builders accept optional multimodal data.
    • Multimodal tracker API simplified; placeholder/token-tracking exports removed.
  • Notes

    • Multimodal support disabled in the Go bindings; protobuf gained an optional multimodal field.

@github-actions github-actions Bot added grpc gRPC client and router changes model-gateway Model gateway crate changes labels Feb 21, 2026
@coderabbitai

coderabbitai Bot commented Feb 21, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces backend-agnostic multimodal types and a two‑phase multimodal pipeline in the model gateway, adds mm_inputs to vLLM proto and threads multimodal through vllm client, and disables multimodal propagation in the Go bindings by passing None.

Changes

Cohort / File(s) Summary
Go bindings
bindings/golang/src/client.rs, bindings/golang/src/policy.rs
Stop passing multimodal inputs into GenerateRequest (pass None) — Go bindings no longer propagate multimodal inputs.
gRPC proto & vLLM client
grpc_client/proto/vllm_engine.proto, grpc_client/src/vllm_engine.rs
Add MultimodalInputs message and GenerateRequest.mm_inputs; vLLM client builders accept and thread optional mm_inputs into requests (tests updated to set mm_inputs: None where appropriate).
Model gateway: proto wrapper & re-exports
model_gateway/src/routers/grpc/proto_wrapper.rs, model_gateway/src/routers/grpc/mod.rs
Introduce MultimodalData and TensorBytes types with conversion helpers (into_sglang_proto / into_vllm_proto) and re-export them; replace public multimodal_inputs surface with multimodal_images: Option<Vec<Arc<ImageFrame>>>.
Multimodal pipeline refactor
model_gateway/src/routers/grpc/multimodal.rs, model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs, model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs
Implement Phase 1 image fetching (fetch_images) returning image frames and Phase 2 backend-specific processing (preprocess_for_sglang / process_for_backend) that produces MultimodalData/TensorBytes; propagate new flow into preparation and request-building stages with new error branches and tokenizer checks.
Router client and harmony adjustments
model_gateway/src/routers/grpc/client.rs, model_gateway/src/routers/grpc/harmony/stages/request_building.rs
Change build_chat_request to accept Option<MultimodalData> and convert to backend protos at call sites; Harmony vLLM path calls vllm builder with None for multimodal inputs.
ProcessedMessages / utils updates
model_gateway/src/routers/grpc/utils.rs
Rename ProcessedMessages.multimodal_inputs → multimodal_images and update all construction paths to use multimodal_images (including early returns).
Multimodal crate API & tracker changes
multimodal/src/lib.rs, multimodal/src/tracker.rs, multimodal/src/types.rs, multimodal/src/error.rs, multimodal/tests/multimodal_tracker_test.rs
Simplify tracker: remove TrackerConfig, placeholders, ConversationSegment, Placeholder* types and related logic; AsyncMultiModalTracker::new now takes only connector and focuses on image fetch queueing; tests updated.
Misc: builders & tests
grpc_client/..., other small files
Set mm_inputs to None in plain/response-derived request builders and update tests to include mm_inputs: None.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ModelGateway
    participant MultimodalModule
    participant BackendBuilder
    participant gRPC
    Client->>ModelGateway: Send chat request (may include images)
    ModelGateway->>MultimodalModule: fetch_images(messages)  -- Phase 1
    MultimodalModule-->>ModelGateway: Vec<Arc<ImageFrame>>
    alt SGLang backend
        ModelGateway->>MultimodalModule: preprocess_for_sglang(images, token_ids)
        MultimodalModule-->>ModelGateway: (expanded_token_ids, MultimodalData)
    else non-SGLang backend
        ModelGateway->>MultimodalModule: process_for_backend(images, is_sglang=false)
        MultimodalModule-->>ModelGateway: (token_ids, MultimodalData)
    end
    ModelGateway->>BackendBuilder: build_chat_request(token_ids, MultimodalData)
    BackendBuilder->>gRPC: build proto request (mm_inputs set via conversion)
    gRPC-->>BackendBuilder: proto GenerateRequest
    BackendBuilder-->>Client: stream/response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

protocols

Suggested reviewers

  • slin1237
  • key4ng
  • tonyluj

Poem

🐰 I fetched the frames with nimble paws,
Phase one caught pixels without a pause,
Phase two crafts data for each backend's need,
Requests now carry the right-shaped feed,
Go sits quiet — the rabbit hops with glee.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: adding vLLM multimodal support and refactoring the pipeline into fetch and preprocess phases, which directly corresponds to the core modifications in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chang/mm-5

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

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @CatherineSue, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the gRPC multimodal pipeline by introducing support for vLLM backends and decoupling the image processing workflow. Previously, the system was tightly coupled to SGLang's preprocessing requirements, leading to inefficiencies when interacting with vLLM. The changes streamline the process by fetching raw images first, then applying backend-specific preprocessing and token expansion only when the target backend is identified, thereby optimizing resource usage and improving compatibility.

Highlights

  • vLLM Multimodal Support: Added native multimodal support for vLLM backends, allowing raw image bytes to be sent directly to vLLM, which handles internal image preprocessing.
  • Decoupled Multimodal Pipeline: Refactored the multimodal processing pipeline into two distinct phases: an initial backend-agnostic image fetching phase and a subsequent backend-specific preprocessing and token expansion phase. This avoids unnecessary SGLang-specific processing for vLLM.
  • Backend-Agnostic Multimodal Data Structures: Introduced new MultimodalData and TensorBytes structs in proto_wrapper.rs to represent multimodal inputs in a backend-agnostic manner, along with conversion methods (into_sglang_proto() and into_vllm_proto()).
  • Removed original_token_ids Workaround: Eliminated the need for the original_token_ids workaround by ensuring token expansion only occurs when necessary (for SGLang) and after the backend type is known.
  • Improved Error Handling: Replaced unwrap() calls with proper error handling in the request building stage for robustness.
Changelog
  • bindings/golang/src/client.rs
    • Disabled multimodal input support for Go bindings in sgl_client_chat_completion_stream.
  • bindings/golang/src/policy.rs
    • Disabled multimodal input support for Go bindings in sgl_multi_client_chat_completion_stream.
  • grpc_client/proto/vllm_engine.proto
    • Added a new MultimodalInputs message to define raw image byte input for vLLM.
    • Included an optional mm_inputs field of type MultimodalInputs in the GenerateRequest message.
  • grpc_client/src/vllm_engine.rs
    • Modified build_generate_request_from_chat to accept an Option<proto::MultimodalInputs> parameter.
    • Passed the mm_inputs parameter to the proto::GenerateRequest constructor.
    • Ensured mm_inputs is set to None in other GenerateRequest construction paths where multimodal data is not applicable.
  • model_gateway/src/routers/grpc/client.rs
    • Removed the import for sglang_proto::MultimodalInputs.
    • Updated build_chat_request to accept Option<MultimodalData>.
    • Added logic to convert the backend-agnostic MultimodalData into either SGLang or vLLM specific proto formats before building the request.
  • model_gateway/src/routers/grpc/harmony/stages/request_building.rs
    • Explicitly set multimodal inputs to None when building requests for the Harmony pipeline.
  • model_gateway/src/routers/grpc/mod.rs
    • Added Arc and ImageFrame imports for image handling.
    • Removed the import of sglang_proto::MultimodalInputs.
    • Re-exported MultimodalData and TensorBytes from proto_wrapper.
    • Updated the ProcessedMessages struct to use multimodal_images: Option<Vec<Arc<ImageFrame>>> instead of multimodal_inputs.
  • model_gateway/src/routers/grpc/multimodal.rs
    • Removed the import of sglang_proto.
    • Modified MultimodalOutput to store multimodal_data: super::MultimodalData instead of proto_mm_inputs.
    • Refactored the process_multimodal function into fetch_images for backend-agnostic image retrieval.
    • Introduced preprocess_for_sglang to handle SGLang-specific image preprocessing and token expansion.
    • Added build_vllm_multimodal_data to create minimal multimodal data for vLLM (raw bytes only).
    • Implemented process_for_backend to conditionally apply SGLang or vLLM specific multimodal processing.
    • Updated expand_tokens to accept a reference to token_ids.
    • Renamed build_proto_multimodal_inputs to build_multimodal_data and updated its return type to super::MultimodalData.
    • Renamed model_specific_to_tensor_data to model_specific_to_tensor_bytes and updated its return type to super::TensorBytes.
  • model_gateway/src/routers/grpc/proto_wrapper.rs
    • Added HashMap import.
    • Defined new structs MultimodalData and TensorBytes for backend-agnostic multimodal data representation.
    • Implemented into_sglang_proto() and into_vllm_proto() methods for MultimodalData to convert to backend-specific protobuf formats.
  • model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs
    • Changed the multimodal_inputs field to multimodal_images to store raw image frames.
    • Updated the multimodal processing step to call multimodal::fetch_images for initial image retrieval, deferring preprocessing.
    • Removed token expansion logic from this stage.
  • model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs
    • Imported the multimodal module.
    • Introduced logic to perform backend-specific multimodal processing using multimodal::process_for_backend.
    • Updated the build_chat_request call to pass the dynamically processed token_ids and multimodal_data.
  • model_gateway/src/routers/grpc/utils.rs
    • Updated the ProcessedMessages struct initialization to use multimodal_images: None instead of multimodal_inputs: None.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@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 vLLM multimodal support and refactors the gRPC multimodal pipeline. A high-severity Server-Side Request Forgery (SSRF) vulnerability was identified due to a lack of URL validation in the image fetching phase, which should be addressed in a dedicated security PR. A critical data serialization bug was found where u32 values are incorrectly serialized as 64-bit integers, leading to potential data corruption; explicit endianness conversion is recommended. Additionally, there's an opportunity to reduce code duplication for improved maintainability.

Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated
Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated

@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: 6ffe53b0b0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated

@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.

Caution

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

⚠️ Outside diff range comments (4)
bindings/golang/src/client.rs (1)

199-207: ⚠️ Potential issue | 🟠 Major

Don’t silently drop multimodal inputs.

Passing None here will ignore images if the request includes them. Please return an explicit “multimodal not supported in Go bindings” error when multimodal content is present (e.g., via processed_messages.multimodal_images or inspecting chat_request.messages) to avoid silent data loss.

🔧 Proposed fix
     // Build GenerateRequest
     let request_id = format!("chatcmpl-{}", Uuid::new_v4());
+    if processed_messages.multimodal_images.is_some() {
+        set_error_message(error_out, "Multimodal inputs are not supported in Go bindings");
+        return SglErrorCode::InvalidArgument;
+    }
     let proto_request = match client.build_generate_request_from_chat(
         request_id.clone(),
         &chat_request,
         processed_messages.text,
         token_ids,
         None, // multimodal not supported in golang bindings
         tool_constraint,
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindings/golang/src/client.rs` around lines 199 - 207, Before calling
client.build_generate_request_from_chat, detect whether the incoming request
contains multimodal content (e.g., check processed_messages.multimodal_images
and inspect chat_request.messages for image/attachment entries) and if any
multimodal content exists return an explicit error like "multimodal not
supported in Go bindings" instead of proceeding with the call that passes None;
modify the code around the request construction (the call to
build_generate_request_from_chat and the variables request_id,
processed_messages) to perform this check and early-return the error so images
are not silently dropped.
bindings/golang/src/policy.rs (1)

547-555: ⚠️ Potential issue | 🟠 Major

Return an error when multimodal inputs are provided.

This path also drops multimodal inputs silently. Please add an explicit “multimodal not supported” error when the request includes images to prevent confusing output.

🔧 Proposed fix
     // Build GenerateRequest
     let request_id = format!("chatcmpl-{}", Uuid::new_v4());
+    if processed_messages.multimodal_images.is_some() {
+        set_error_message(error_out, "Multimodal inputs are not supported in Go bindings");
+        return SglErrorCode::InvalidArgument;
+    }
     let proto_request = match client.build_generate_request_from_chat(
         request_id.clone(),
         &chat_request,
         processed_messages.text,
         token_ids,
         None, // multimodal not supported in golang bindings
         tool_constraint,
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindings/golang/src/policy.rs` around lines 547 - 555, The code currently
calls client.build_generate_request_from_chat and passes None for multimodal,
silently dropping images; change it to detect multimodal inputs in
processed_messages (e.g., check processed_messages.images or any
image/multimodal field) before building the request and return an explicit error
like "multimodal not supported" instead of proceeding. Update the code path
around build_generate_request_from_chat (where request_id and proto_request are
created) to return Err(...) / propagate an error when images are present so
callers receive a clear failure rather than silently losing multimodal data.
model_gateway/src/routers/grpc/multimodal.rs (1)

527-568: ⚠️ Potential issue | 🟠 Major

Fix uint32 tensor serialization width mismatch.
UintTensor and UintVec store Vec<u32> (confirmed via type definitions), but are serialized using (*v as i64).to_le_bytes() (8 bytes) while declaring dtype: "uint32" (4 bytes). This mismatch corrupts tensor payloads for backends expecting 4‑byte elements. Serialize using the native width by calling v.to_le_bytes() directly.

🛠️ Suggested fix
         ModelSpecificValue::UintTensor { data, shape } => Some(super::TensorBytes {
             data: data
                 .iter()
-                .flat_map(|v| (*v as i64).to_le_bytes())
+                .flat_map(|v| v.to_le_bytes())
                 .collect(),
             shape: shape.iter().map(|&d| d as u32).collect(),
             dtype: "uint32".to_string(),
         }),
         ModelSpecificValue::UintVec(v) => Some(super::TensorBytes {
             data: v
                 .iter()
-                .flat_map(|val| (*val as i64).to_le_bytes())
+                .flat_map(|val| val.to_le_bytes())
                 .collect(),
             shape: vec![v.len() as u32],
             dtype: "uint32".to_string(),
         }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/multimodal.rs` around lines 527 - 568, The
serialization for uint32 tensors in model_specific_to_tensor_bytes is using (*v
as i64).to_le_bytes() which writes 8-byte values while dtype is "uint32"; update
the ModelSpecificValue::UintTensor and ModelSpecificValue::UintVec branches to
serialize each u32 with v.to_le_bytes() (native 4-byte little-endian) and keep
dtype "uint32" and shape logic unchanged so payload width matches the declared
dtype.
model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs (1)

120-128: 🧹 Nitpick | 🔵 Trivial

Add a short TODO documenting the temporary 400‑mapping for multimodal fetch errors.

All fetch failures currently map to bad_request; a brief note keeps the planned 4xx/5xx split visible until error taxonomy improves.

📝 Suggested inline note
-                        return Err(error::bad_request(
+                        // TODO: Multimodal fetch errors currently map to 400;
+                        // refine to 4xx/5xx once multimodal error taxonomy is available.
+                        return Err(error::bad_request(

Based on learnings: In model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs, multimodal processing failures now return 400 Bad Request due to the generic anyhow::Error lacking distinguished error types; implement explicit error categorization and, until then, document the current behavior and anticipated extension.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs` around
lines 120 - 128, Add a short TODO comment right above the error! log and
Err(error::bad_request(...)) return in ChatPreparationStage::execute explaining
that multimodal image fetch failures are currently mapped to 400 Bad Request
because the underlying anyhow::Error lacks discriminated error types, and note
the intended future change to split client (4xx) vs server (5xx) errors once
explicit error taxonomy is implemented; reference the multimodal processing
branch (the error! call and the error::bad_request(...) return) so reviewers can
find and later replace the temporary mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@bindings/golang/src/client.rs`:
- Around line 199-207: Before calling client.build_generate_request_from_chat,
detect whether the incoming request contains multimodal content (e.g., check
processed_messages.multimodal_images and inspect chat_request.messages for
image/attachment entries) and if any multimodal content exists return an
explicit error like "multimodal not supported in Go bindings" instead of
proceeding with the call that passes None; modify the code around the request
construction (the call to build_generate_request_from_chat and the variables
request_id, processed_messages) to perform this check and early-return the error
so images are not silently dropped.

In `@bindings/golang/src/policy.rs`:
- Around line 547-555: The code currently calls
client.build_generate_request_from_chat and passes None for multimodal, silently
dropping images; change it to detect multimodal inputs in processed_messages
(e.g., check processed_messages.images or any image/multimodal field) before
building the request and return an explicit error like "multimodal not
supported" instead of proceeding. Update the code path around
build_generate_request_from_chat (where request_id and proto_request are
created) to return Err(...) / propagate an error when images are present so
callers receive a clear failure rather than silently losing multimodal data.

In `@model_gateway/src/routers/grpc/multimodal.rs`:
- Around line 527-568: The serialization for uint32 tensors in
model_specific_to_tensor_bytes is using (*v as i64).to_le_bytes() which writes
8-byte values while dtype is "uint32"; update the ModelSpecificValue::UintTensor
and ModelSpecificValue::UintVec branches to serialize each u32 with
v.to_le_bytes() (native 4-byte little-endian) and keep dtype "uint32" and shape
logic unchanged so payload width matches the declared dtype.

In `@model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs`:
- Around line 120-128: Add a short TODO comment right above the error! log and
Err(error::bad_request(...)) return in ChatPreparationStage::execute explaining
that multimodal image fetch failures are currently mapped to 400 Bad Request
because the underlying anyhow::Error lacks discriminated error types, and note
the intended future change to split client (4xx) vs server (5xx) errors once
explicit error taxonomy is implemented; reference the multimodal processing
branch (the error! call and the error::bad_request(...) return) so reviewers can
find and later replace the temporary mapping.

@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: 9b51a186ac

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@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: bcb5cca89e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated
@mergify

mergify Bot commented Feb 21, 2026

Copy link
Copy Markdown
Contributor

Hi @CatherineSue, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Feb 21, 2026
@github-actions github-actions Bot added tests Test changes multimodal Multimodal crate changes labels Feb 21, 2026
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Feb 21, 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: 2251d120d7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/grpc/multimodal.rs

@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 the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/routers/grpc/regular/stages/chat/request_building.rs`:
- Around line 108-113: Add a short inline comment documenting the fallback
behavior where tokenizer_source is set: explain that
ctx.components.tokenizer_registry.get_by_name(model_id) may return None and in
that case tokenizer_source is intentionally set to an empty string via
unwrap_or_default(), so downstream code should expect an empty source when no
tokenizer is registered for model_id; reference tokenizer_source,
tokenizer_registry.get_by_name, model_id and unwrap_or_default in the comment.

@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.

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/multimodal.rs (1)

426-427: ⚠️ Potential issue | 🟡 Minor

Minor: as u32 cast could mask invalid negative token IDs.

If PromptReplacement.tokens ever contained a negative value (e.g., due to upstream bug), *&t as u32 would silently wrap to a large u32. Consider using try_into() with an error, or document that negative token IDs are invalid.

🛡️ Optional defensive fix
-            expanded.extend(repl.tokens.iter().map(|&t| t as u32));
+            expanded.extend(repl.tokens.iter().map(|&t| {
+                debug_assert!(t >= 0, "Negative token ID in prompt replacement");
+                t as u32
+            }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/multimodal.rs` around lines 426 - 427, The
current expansion call expanded.extend(repl.tokens.iter().map(|&t| t as u32))
silently wraps negative PromptReplacement.tokens into large u32 values; change
the conversion to check for negativity using TryInto (or an explicit check) on
each token from repl.tokens in the function that builds expanded (e.g., where
PromptReplacement.tokens is iterated) and return or propagate an error if a
token is negative (or otherwise invalid) instead of using as u32 so invalid
token IDs are caught and handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@model_gateway/src/routers/grpc/multimodal.rs`:
- Around line 426-427: The current expansion call
expanded.extend(repl.tokens.iter().map(|&t| t as u32)) silently wraps negative
PromptReplacement.tokens into large u32 values; change the conversion to check
for negativity using TryInto (or an explicit check) on each token from
repl.tokens in the function that builds expanded (e.g., where
PromptReplacement.tokens is iterated) and return or propagate an error if a
token is negative (or otherwise invalid) instead of using as u32 so invalid
token IDs are caught and handled.

@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: f081e3b3db

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs
Comment thread multimodal/src/tracker.rs
@mergify

This comment was marked as resolved.

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Feb 22, 2026
…dalData

Introduce MultimodalData wrapper in proto_wrapper.rs to decouple the
multimodal pipeline from backend-specific proto types. Each backend
converts via into_sglang_proto() / into_vllm_proto() (consuming self
to avoid expensive clones).

For vLLM, raw image bytes are sent via the new proto MultimodalInputs
message. vLLM handles preprocessing internally while preserving our
pre-expanded token IDs.

Signed-off-by: Chang Su <chang.s.su@oracle.com>

# Conflicts:
#	model_gateway/src/routers/grpc/multimodal.rs
vLLM handles its own multimodal placeholder expansion internally.
The router was sending already-expanded token IDs (40→183 tokens),
causing vLLM to fail with "Failed to apply prompt replacement"
because placeholder tokens were already replaced.

Store original (unexpanded) token IDs in PreparationOutput and use
them when building vLLM requests, while SGLang continues to receive
expanded tokens.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…sing

Split the monolithic process_multimodal() into two phases:
- Phase 1 (preparation stage): fetch images via tracker, enforce modality
  limits, store raw ImageFrames on ProcessedMessages
- Phase 2 (request building stage): backend-specific processing where
  SGLang gets full pixel preprocessing + token expansion, while vLLM
  gets raw image bytes only (handles preprocessing internally)

This removes the original_token_ids workaround and avoids wasting CPU
on SGLang-specific preprocessing when the backend is vLLM.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…ed main

Adapt the cherry-picked multimodal gRPC code to the stricter clippy
configuration on main: inline format args, replace #[allow] with #[cfg(test)],
use exhaustive match arms, restore async fn for prepare_chat.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Guard against as_slice()/as_slice_memory_order() returning None for
non-contiguous ndarray tensors. Falls back to element-wise iteration
instead of silently producing empty bytes via unwrap_or_default().

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Signed-off-by: Chang Su <chang.s.su@oracle.com>
- Fix UintTensor/UintVec serialization: was casting u32 to i64 (8 bytes)
  while declaring dtype "uint32" (4 bytes), causing byte-width mismatch.
  Now serializes as native u32 little-endian bytes.
- Import MultimodalData and TensorBytes at module top to eliminate
  repeated super:: references.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…eview feedback

- Extract duplicated config+spec lookup into resolve_model_spec()
- Use crate:: imports instead of super:: for MultimodalData/TensorBytes
- Add TODO for 4xx/5xx error distinction in multimodal fetch
- Add TODO for token expansion vs routing policy interaction

Signed-off-by: Chang Su <chang.s.su@oracle.com>
…g from model registry

TrackerConfig required resolving the model spec (via ModelRegistry) at
tracker construction time to obtain two pieces of information:

1. placeholder_tokens — the model-specific placeholder token string
   (e.g. "<image>", "<|image|>") used to build ConversationSegment
   tracking and PlaceholderMap/PlaceholderHandle bookkeeping within the
   tracker.

2. modality_limits — per-modality item count limits from the model spec,
   enforced via the ModalityLimit error variant.

Both are dead in the current architecture:

- ConversationSegment tracking (text-position-aware placeholder
  insertion) was designed for reconstructing prompt text with
  placeholders, but token expansion now operates directly on token_ids
  in Phase 2 (preprocess_for_sglang). The tracker never needs to track
  text positions or know the placeholder token string.

- Modality limits were never consumed downstream and only rejected
  requests early. If limit enforcement is needed, it belongs at the
  model spec level in Phase 2 where the model is already resolved.

The practical problem: TrackerConfig forced ChatPreparationStage (Phase
1) to resolve the model spec via ModelRegistry just to fetch images.
This created an unnecessary dependency — whether a model has token
expansion configs (like llava, phi3v, llama4_vision) should not matter
when all Phase 1 does is download image bytes. By removing
TrackerConfig, Phase 1 only needs a MediaConnector, and the ModelRegistry
dependency is confined to Phase 2 in grpc/multimodal.rs where it
actually belongs.

Also removes the now-dead types: ConversationSegment, PlaceholderHandle,
PlaceholderMap, DEFAULT_PLACEHOLDERS, and the ModalityLimit error
variant.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Signed-off-by: Chang Su <chang.s.su@oracle.com>
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Feb 22, 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: f8623447d9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…eline

Remove the early-return rejection for multimodal input on the TRT-LLM
backend. TRT-LLM can accept pre-tokenized (unexpanded) token IDs plus
raw image data — its LLM API will decode and re-process internally.

This unblocks multimodal request flow for TRT-LLM in the chat pipeline.

Signed-off-by: Chang Su <changsu@nvidia.com>
Signed-off-by: Chang Su <chang.s.su@oracle.com>
The tokenizer_source lookup used get_by_name() only, so requests
identifying a model by UUID would silently fall back to an empty
source path, causing multimodal preprocessing to read configs from
the wrong directory. Fall back to get_by_id() when name lookup fails.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
@CatherineSue
CatherineSue merged commit 25ccd3d into main Feb 22, 2026
21 of 24 checks passed
@CatherineSue
CatherineSue deleted the chang/mm-5 branch February 22, 2026 01:22
CatherineSue added a commit that referenced this pull request Mar 1, 2026
…ration.rs

PR #497 split multimodal processing into Phase 1 (fetch in
preparation.rs) and Phase 2 (preprocess in request_building.rs) because
vLLM needed raw image bytes. Now that vLLM uses the same preprocessed
path as SGLang, the split is no longer justified. Collapse back to the
clean single-phase pattern from PR #495 (3a05e6b).

multimodal.rs:
- Add process_multimodal() as single async entry point combining
  fetch → preprocess → expand tokens → build MultimodalData.
- Inline resolve_model_spec() and preprocess() into
  process_multimodal() since both were single-use helpers. This also
  avoids computing placeholder_token twice.
- Collect mm_hashes inside build_multimodal_data() in the same pass
  as image_data (single iteration over images).
- Remove fetch_images(), process_for_backend(),
  build_raw_multimodal_data(), and the is_trtllm/is_vllm parameters
  that were added for the two-phase split.

preparation.rs:
- Replace Phase 1 fetch-only block with full process_multimodal()
  call. Store expanded token_ids and MultimodalData directly.
- Add tokenizer_source empty-string guard with a clear error message
  instead of letting it bubble up as a confusing file-not-found from
  get_or_load_config().

request_building.rs:
- Remove entire Phase 2 multimodal block (~55 lines).
- Use take() instead of as_ref() on ctx.state.preparation since
  request_building is the last consumer (worker_selection already ran).
  This eliminates all clones on token_ids, text, multimodal_data, and
  tool_constraints — previously cloning MultimodalData copied megabytes
  of pixel data unnecessarily.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
CatherineSue added a commit that referenced this pull request Mar 1, 2026
…ration.rs

PR #497 split multimodal processing into Phase 1 (fetch in
preparation.rs) and Phase 2 (preprocess in request_building.rs) because
vLLM needed raw image bytes. Now that vLLM uses the same preprocessed
path as SGLang, the split is no longer justified. Collapse back to the
clean single-phase pattern from PR #495 (3a05e6b).

multimodal.rs:
- Add process_multimodal() as single async entry point combining
  fetch → preprocess → expand tokens → build MultimodalData.
- Inline resolve_model_spec() and preprocess() into
  process_multimodal() since both were single-use helpers. This also
  avoids computing placeholder_token twice.
- Collect mm_hashes inside build_multimodal_data() in the same pass
  as image_data (single iteration over images).
- Remove fetch_images(), process_for_backend(),
  build_raw_multimodal_data(), and the is_trtllm/is_vllm parameters
  that were added for the two-phase split.

preparation.rs:
- Replace Phase 1 fetch-only block with full process_multimodal()
  call. Store expanded token_ids and MultimodalData directly.
- Add tokenizer_source empty-string guard with a clear error message
  instead of letting it bubble up as a confusing file-not-found from
  get_or_load_config().

request_building.rs:
- Remove entire Phase 2 multimodal block (~55 lines).
- Use take() instead of as_ref() on ctx.state.preparation since
  request_building is the last consumer (worker_selection already ran).
  This eliminates all clones on token_ids, text, multimodal_data, and
  tool_constraints — previously cloning MultimodalData copied megabytes
  of pixel data unnecessarily.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grpc gRPC client and router changes model-gateway Model gateway crate changes multimodal Multimodal crate changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant