Skip to content

feat(gateway): add multimodal support to Messages API gRPC pipeline - #776

Merged
slin1237 merged 2 commits into
mainfrom
slin/messages-multimodal
Mar 17, 2026
Merged

slin1237 merged 2 commits into
mainfrom
slin/messages-multimodal

Conversation

@slin1237

@slin1237 slin1237 commented Mar 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Wire the existing multimodal processing infrastructure into the Messages API pipeline, enabling vision model usage through the Messages API (/v1/messages).

Closes #XXX

What changed

  • multimodal.rs: Add Messages API detection (has_multimodal_content_messages), extraction (extract_content_parts_messages), and processing entry point (process_multimodal_messages). Refactor process_multimodal to delegate to a shared process_multimodal_parts core so both chat and messages pipelines share identical fetch → preprocess → expand → intermediate logic.
  • messages/preparation.rs: Add Step 3.5 multimodal processing block mirroring chat/preparation.rs — detect multimodal content, resolve tokenizer source, process images, update token_ids with expanded tokens, store multimodal_intermediate on ProcessedMessages.
  • messages/request_building.rs: Assemble backend-specific multimodal data from the intermediate via assemble_multimodal_data() and pass it to build_messages_request() instead of None.

Why

Messages API requests containing images (base64 or URL via InputContentBlock::Image) were silently ignored — the pipeline passed None for multimodal data. This blocked vision model usage through the Messages API.

How

Reuse the entire existing multimodal stack. Only the top-of-funnel detection and extraction differ (InputMessage/InputContentBlock types vs ChatMessage/ContentPart types). Everything from MultimodalIntermediate downward — image preprocessing, token expansion, backend-specific assembly (sglang/vllm/trtllm), proto conversion — is shared via the new process_multimodal_parts function.

Key design decisions:

  • ImageSource::Base64 is converted to data URLs (data:{media_type};base64,{data}) for the media connector, matching how chat handles inline images
  • ImageSource::Url is passed through directly
  • Only InputContentBlock::Image triggers multimodal (not Document blocks — those need separate PDF processing)
  • Detection is scoped to Role::User messages only, matching Anthropic's API semantics

Test plan

  • cargo check -p smg — compiles clean with zero warnings
  • All 9 existing multimodal tests pass unchanged
  • Side-by-side comparison verified: messages pipeline matches chat pipeline pattern exactly for multimodal processing (component checks, tokenizer source resolution, error handling, intermediate storage, assembly)
  • Manual test: send Messages API request with base64 image to a vision model backend
  • Manual test: send Messages API request with image URL to a vision model backend

Summary by CodeRabbit

  • New Features

    • Multimodal support added to the Messages API: users can include images (URLs or Base64) alongside text and have them processed; token counts may be expanded accordingly.
  • Refactor

    • Unified multimodal pipeline across chat and messages for consistent media handling and intermediate payload propagation.
  • Tests

    • Updated tests to reflect the new unified media representation and message-path behavior.

image

Wire the existing multimodal processing infrastructure (image fetch,
preprocessing, token expansion, backend-specific assembly) into the
Messages API pipeline. The chat completion pipeline already supports
multimodal; this change adds the same capability for Messages API
requests.

What changed:
- multimodal.rs: add has_multimodal_content_messages() detection for
  InputContentBlock::Image, extract_content_parts_messages() to convert
  ImageSource::Base64/Url to ChatContentPart::ImageUrl (data URLs for
  base64, passthrough for URLs), and process_multimodal_messages() entry
  point. Refactor process_multimodal() to delegate to a shared
  process_multimodal_parts() core so both pipelines share the same
  fetch → preprocess → expand → intermediate logic.
- messages/preparation.rs: add Step 3.5 multimodal processing block
  mirroring chat/preparation.rs — detect multimodal content, resolve
  tokenizer source, call process_multimodal_messages(), update token_ids
  with expanded tokens, store multimodal_intermediate on
  ProcessedMessages. Remove clippy::unused_async expect since .await is
  now used.
- messages/request_building.rs: assemble backend-specific multimodal
  data from the intermediate (via assemble_multimodal_data) and pass it
  to build_messages_request() instead of None.

Why: Messages API requests containing images (base64 or URL) were
silently ignored — the pipeline passed None for multimodal data. This
blocked vision model usage through the Messages API.

How: Reuse the entire existing multimodal stack. Only the top-of-funnel
detection and extraction differ (InputMessage/InputContentBlock types vs
ChatMessage/ContentPart types). Everything from MultimodalIntermediate
downward — image preprocessing, token expansion, backend-specific
assembly (sglang/vllm/trtllm), proto conversion — is shared.

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@github-actions github-actions Bot added grpc gRPC client and router changes model-gateway Model gateway crate changes labels Mar 16, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 Messages API by introducing comprehensive multimodal support, allowing it to process image content for vision models. By leveraging and refactoring the existing multimodal infrastructure, the changes ensure that image-based requests are no longer silently ignored, thereby unlocking new capabilities for users interacting with the API. The core processing logic is now unified, promoting code reuse and maintainability across different API pipelines.

Highlights

  • Multimodal Support for Messages API: Enabled vision model usage through the Messages API (/v1/messages) by integrating existing multimodal processing infrastructure into its gRPC pipeline.
  • Shared Multimodal Processing Core: Refactored the multimodal processing logic into a shared core function (process_multimodal_parts) to be reused by both the chat completion and Messages API pipelines, ensuring consistent fetch, preprocess, expand, and intermediate logic.
  • Messages API Specific Multimodal Handling: Introduced new functions for Messages API to detect multimodal content (has_multimodal_content_messages), extract content parts (extract_content_parts_messages), and serve as an entry point for processing (process_multimodal_messages), adapting to InputMessage and InputContentBlock types.
  • Integration into Messages API Pipeline Stages: Integrated the new multimodal processing into the preparation.rs stage to detect content, resolve tokenizer sources, process images, expand tokens, and store intermediate data, and into request_building.rs to assemble and pass backend-specific multimodal data.
Changelog
  • model_gateway/src/routers/grpc/multimodal.rs
    • Updated module documentation to reflect support for both chat and messages pipelines.
    • Imported InputMessage, InputContent, InputContentBlock, ImageSource, and Role types for Messages API integration.
    • Added has_multimodal_content_messages to detect multimodal content within Messages API requests.
    • Implemented extract_content_parts_messages to convert Messages API image blocks into ChatContentParts, including base64 to data URL conversion.
    • Introduced process_multimodal_messages as the entry point for Messages API multimodal processing, delegating to a shared core.
    • Refactored the existing process_multimodal function to utilize the new shared process_multimodal_parts core.
    • Created process_multimodal_parts as a shared asynchronous function to handle the core multimodal processing chain.
  • model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
    • Imported the multimodal module and debug tracing macro.
    • Removed the #[expect(clippy::unused_async)] attribute from the prepare_messages function.
    • Integrated a new 'Step 3.5: Multimodal processing' block into the preparation stage.
    • Added logic to detect multimodal content using multimodal::has_multimodal_content_messages.
    • Implemented tokenizer source resolution for multimodal processing.
    • Called multimodal::process_multimodal_messages to execute image fetching, preprocessing, and token expansion.
    • Included error handling for multimodal processing failures.
    • Updated token_ids with expanded tokens and stored the multimodal_intermediate data.
    • Assigned the multimodal_intermediate to the processed_messages structure.
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs
    • Imported the assemble_multimodal_data function from the multimodal module.
    • Added logic to assemble backend-specific multimodal data from the multimodal_intermediate.
    • Passed the assembled multimodal_data to the builder_client.build_messages_request function.
Activity
  • Verified cargo check -p smg compiles clean with zero warnings.
  • Confirmed all 9 existing multimodal tests pass unchanged.
  • Performed side-by-side comparison to verify messages pipeline matches chat pipeline pattern for multimodal processing.
  • Planned manual test for sending Messages API request with base64 image to a vision model backend.
  • Planned manual test for sending Messages API request with image URL to a vision model backend.
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. ↩

@coderabbitai

coderabbitai Bot commented Mar 16, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

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

Adds Messages-API multimodal support and a unified media-content pipeline: detects multimodal content in InputMessage arrays, extracts MediaContentParts, exposes process_multimodal_messages, threads multimodal intermediate data through message preparation and request building, and renames ChatContentPart → MediaContentPart.

Changes

Cohort / File(s) Summary
Multimodal core & Messages entrypoints
model_gateway/src/routers/grpc/multimodal.rs
Adds has_multimodal_content_messages and process_multimodal_messages, introduces process_multimodal_parts shared core, adapts chat path to use MediaContentPart, and converts Image blocks to shared MediaContentPart.
Message preparation stage
model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
Detects multimodal content, resolves tokenizer source, calls process_multimodal_messages, stores multimodal_intermediate and expanded token IDs into processed messages, and surfaces multimodal-specific errors and logs.
Request building
model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs
Imports assemble_multimodal_data, computes multimodal_data from processed_messages.multimodal_intermediate, and passes it into build_messages_request.
Multimodal crate: API, types, tracker, tests
crates/multimodal/src/lib.rs, crates/multimodal/src/types.rs, crates/multimodal/src/tracker.rs, crates/multimodal/tests/multimodal_tracker_test.rs
Renames public enum ChatContentPart → MediaContentPart across exports, types, tracker signatures, and tests; updates imports and variant usages accordingly.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Prep as MessagePreparationStage
    participant Detector as MultimodalDetector
    participant Extractor as ContentExtractor
    participant Tokenizer as Tokenizer
    participant Processor as MultimodalProcessor
    participant Builder as RequestBuilder

    Client->>Prep: send InputMessage[] (may include Image/Text blocks)
    Prep->>Detector: has_multimodal_content_messages(messages)
    Detector-->>Prep: true/false

    alt multimodal present
        Prep->>Extractor: extract_content_parts_messages(messages)
        Extractor-->>Prep: Vec<MediaContentPart>

        Prep->>Tokenizer: resolve tokenizer & token_ids
        Tokenizer-->>Prep: tokenizer, token_ids

        Prep->>Processor: process_multimodal_messages(parts, model_id, tokenizer, token_ids, components, src)
        rect rgba(100,150,200,0.5)
            Processor->>Processor: fetch & preprocess media
            Processor->>Processor: expand token_ids / embed media
            Processor->>Processor: assemble multimodal_data & intermediate
        end
        Processor-->>Prep: MultimodalOutput (expanded_token_ids, intermediate)
        Prep->>Builder: build_messages_request(..., multimodal_data)
    else no multimodal
        Prep->>Builder: build_messages_request(..., None)
    end

    Builder-->>Client: gRPC request/response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • lightseekorg/smg#741 — Extends the Messages API pipeline that this change integrates into (preparation/request stages).
  • lightseekorg/smg#495 — Earlier multimodal pipeline changes to multimodal.rs (chat-focused) that this PR generalizes to Messages.
  • lightseekorg/smg#588 — Introduced multimodal intermediate/assembly flow that this PR reuses and threads through Messages preparation.

Suggested labels

multimodal

Suggested reviewers

  • CatherineSue
  • key4ng

Poem

🐰 I hopped through messages and found a frame,
Where pixels wink and tokens learn their name,
I stitched the parts, both image and the text,
A tiny rabbit cheered the multimodal next,
Hooray — now messages and media play the same game!

🚥 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 summarizes the main change: adding multimodal support to the Messages API gRPC pipeline, which is the core objective reflected in all file modifications.
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slin/messages-multimodal
📝 Coding Plan
  • Generate coding plan for human review comments

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

@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 effectively integrates multimodal support into the Messages API pipeline by reusing existing infrastructure from the chat pipeline. The refactoring to create a shared process_multimodal_parts function is a solid choice for maintainability, and the changes in the preparation and request-building stages are logical and consistent with the established patterns. I've provided a few suggestions for refactoring to enhance code clarity and align with idiomatic Rust practices, referencing relevant repository rules.

Comment on lines +225 to +235
messages.iter().any(|msg| {
if msg.role != Role::User {
return false;
}
match &msg.content {
InputContent::Blocks(blocks) => blocks
.iter()
.any(|block| matches!(block, InputContentBlock::Image(_))),
InputContent::String(_) => false,
}
})

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

The closure passed to any() can be simplified by combining the role check with the content check using a logical AND (&&). This makes the intent clearer and more concise.

    messages.iter().any(|msg| {
        msg.role == Role::User
            && match &msg.content {
                InputContent::Blocks(blocks) => blocks
                    .iter()
                    .any(|block| matches!(block, InputContentBlock::Image(_))),
                InputContent::String(_) => false,
            }
    })
References
  1. Prioritize code simplicity and clarity over micro-optimizations, especially when the performance gain is negligible for typical use cases (e.g., iterating over a small number of items).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is valid

Comment on lines +240 to +283
fn extract_content_parts_messages(messages: &[InputMessage]) -> Vec<ChatContentPart> {
let mut parts = Vec::new();

for msg in messages {
if msg.role != Role::User {
continue;
}
let blocks = match &msg.content {
InputContent::Blocks(blocks) => blocks,
InputContent::String(_) => continue,
};

for block in blocks {
match block {
InputContentBlock::Image(image_block) => match &image_block.source {
ImageSource::Base64 { media_type, data } => {
// Convert base64 to data URL for the media connector
let data_url = format!("data:{media_type};base64,{data}");
parts.push(ChatContentPart::ImageUrl {
url: data_url,
detail: None,
uuid: None,
});
}
ImageSource::Url { url } => {
parts.push(ChatContentPart::ImageUrl {
url: url.clone(),
detail: None,
uuid: None,
});
}
},
InputContentBlock::Text(text_block) => {
parts.push(ChatContentPart::Text {
text: text_block.text.clone(),
});
}
_ => {}
}
}
}

parts
}

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

This function can be refactored to use a more idiomatic functional style with iterators. This approach chains iterator methods like filter_map and flatten to process the messages, which can improve readability by reducing nesting and eliminating the need for a mutable parts vector.

fn extract_content_parts_messages(messages: &[InputMessage]) -> Vec<ChatContentPart> {
    messages
        .iter()
        .filter(|msg| msg.role == Role::User)
        .filter_map(|msg| match &msg.content {
            InputContent::Blocks(blocks) => Some(blocks.iter()),
            _ => None,
        })
        .flatten()
        .filter_map(|block| match block {
            InputContentBlock::Image(image_block) => match &image_block.source {
                ImageSource::Base64 { media_type, data } => {
                    let data_url = format!("data:{media_type};base64,{data}");
                    Some(ChatContentPart::ImageUrl {
                        url: data_url,
                        detail: None,
                        uuid: None,
                    })
                }
                ImageSource::Url { url } => Some(ChatContentPart::ImageUrl {
                    url: url.clone(),
                    detail: None,
                    uuid: None,
                }),
            },
            InputContentBlock::Text(text_block) => Some(ChatContentPart::Text {
                text: text_block.text.clone(),
            }),
            _ => None,
        })
        .collect()
}
References
  1. Prioritize code simplicity and clarity over micro-optimizations, especially when the performance gain is negligible for typical use cases (e.g., iterating over a small number of items).
  2. When using flat_map on an iterator from a match expression, prefer returning iterators over existing data (e.g., using .as_slice() or &[]) over creating and iterating on a temporary value (e.g., [].iter()) to prevent lifetime issues.

Comment on lines +126 to +156
match multimodal::process_multimodal_messages(
&request.messages,
model_id,
&*tokenizer,
token_ids,
mm_components,
&tokenizer_source,
)
.await
{
Ok(output) => {
debug!(
function = "MessagePreparationStage::execute",
expanded_tokens = output.expanded_token_ids.len(),
"Multimodal processing complete"
);
token_ids = output.expanded_token_ids;
multimodal_intermediate = Some(output.intermediate);
}
Err(e) => {
error!(
function = "MessagePreparationStage::execute",
error = %e,
"Multimodal processing failed"
);
return Err(error::bad_request(
"multimodal_processing_failed",
format!("Multimodal processing failed: {e}"),
));
}
}

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

This match block for error handling can be made more concise by using map_err and the ? operator. This is a common and idiomatic pattern in Rust for chaining operations that can fail.

                let output = multimodal::process_multimodal_messages(
                    &request.messages,
                    model_id,
                    &*tokenizer,
                    token_ids,
                    mm_components,
                    &tokenizer_source,
                )
                .await
                .map_err(|e| {
                    error!(
                        function = "MessagePreparationStage::execute",
                        error = %e,
                        "Multimodal processing failed"
                    );
                    error::bad_request(
                        "multimodal_processing_failed",
                        format!("Multimodal processing failed: {e}"),
                    )
                })?;

                debug!(
                    function = "MessagePreparationStage::execute",
                    expanded_tokens = output.expanded_token_ids.len(),
                    "Multimodal processing complete"
                );
                token_ids = output.expanded_token_ids;
                multimodal_intermediate = Some(output.intermediate);
References
  1. Prioritize code simplicity and clarity over micro-optimizations, especially when the performance gain is negligible for typical use cases (e.g., iterating over a small number of items).
  2. Refactor match statements to avoid duplication. When arms have common logic, use the match to return the differing value and perform the common logic once.
  3. Do not introduce panics in code that interacts with external systems if the upstream server does not handle the error. Instead, handle the error gracefully or propagate it appropriately.

@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/messages/preparation.rs`:
- Around line 126-154: The multimodal error is being logged and returned with
the full error string (which may include raw data URLs); update the handling
around multimodal::process_multimodal_messages in
MessagePreparationStage::execute so you never include the raw error text: log a
sanitized message (e.g., "Multimodal processing failed" plus an error kind or
truncated/safe indicator rather than %e) and return
error::bad_request("multimodal_processing_failed", "Multimodal processing
failed") or otherwise a redacted message instead of format!("Multimodal
processing failed: {e}"); apply the identical change to the analogous handler in
chat/preparation.rs to ensure no raw connector payloads are written to logs or
responses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b7183ae-c776-4289-8c8a-a38705269c68

📥 Commits

Reviewing files that changed from the base of the PR and between eae871e and 3017779.

📒 Files selected for processing (3)
  • model_gateway/src/routers/grpc/multimodal.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs

Comment on lines +126 to +154
match multimodal::process_multimodal_messages(
&request.messages,
model_id,
&*tokenizer,
token_ids,
mm_components,
&tokenizer_source,
)
.await
{
Ok(output) => {
debug!(
function = "MessagePreparationStage::execute",
expanded_tokens = output.expanded_token_ids.len(),
"Multimodal processing complete"
);
token_ids = output.expanded_token_ids;
multimodal_intermediate = Some(output.intermediate);
}
Err(e) => {
error!(
function = "MessagePreparationStage::execute",
error = %e,
"Multimodal processing failed"
);
return Err(error::bad_request(
"multimodal_processing_failed",
format!("Multimodal processing failed: {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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual code in the file specified
cat -n model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs | sed -n '100,160p'

Repository: lightseekorg/smg

Length of output: 3253


🏁 Script executed:

# Check the multimodal.rs file to understand how data: URLs are constructed
cat -n model_gateway/src/routers/grpc/multimodal.rs | sed -n '1,50p'

Repository: lightseekorg/smg

Length of output: 2524


🏁 Script executed:

# Search for process_multimodal_messages to understand the error handling
rg -n "process_multimodal_messages" --type rust -A 5

Repository: lightseekorg/smg

Length of output: 1231


🏁 Script executed:

# Check if there's a multimodal crate and understand error types
find . -name "multimodal.rs" -o -name "lib.rs" | grep -i multimodal | head -20

Repository: lightseekorg/smg

Length of output: 137


🏁 Script executed:

# Look for data: URL construction patterns
rg -n "data:" --type rust -B 2 -A 2 | head -40

Repository: lightseekorg/smg

Length of output: 2886


🏁 Script executed:

# Look at the multimodal message processing function
cat -n model_gateway/src/routers/grpc/multimodal.rs | sed -n '289,400p'

Repository: lightseekorg/smg

Length of output: 4361


🏁 Script executed:

# Search for inline base64 or data URL handling in multimodal
rg -n "inline.*base64|data.*base64" --type rust -B 3 -A 3

Repository: lightseekorg/smg

Length of output: 1993


🏁 Script executed:

# Check the multimodal crate to understand error propagation
cat -n crates/multimodal/src/lib.rs | head -100

Repository: lightseekorg/smg

Length of output: 1042


🏁 Script executed:

# Look for how InputContent with inline images is processed
rg -n "InputContent|ContentPart" model_gateway/src/routers/grpc/multimodal.rs -A 2 -B 2 | head -80

Repository: lightseekorg/smg

Length of output: 3450


🏁 Script executed:

# Search for error message construction patterns in the entire codebase for multimodal
rg -n "Multimodal processing failed" --type rust

Repository: lightseekorg/smg

Length of output: 613


🏁 Script executed:

# Check the chat/preparation.rs error handling for comparison
cat -n model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs | sed -n '115,135p'

Repository: lightseekorg/smg

Length of output: 1154


🏁 Script executed:

# Look for other error handling patterns in multimodal to see if there's already suppression elsewhere
rg -n "Err\(e\)|Err\(_e\)" model_gateway/src/routers/grpc/multimodal.rs -B 2 -A 4 | head -60

Repository: lightseekorg/smg

Length of output: 42


🏁 Script executed:

# Check if there are any tests that demonstrate the data URL leak concern
rg -n "data:image" --type rust -B 2 -A 2

Repository: lightseekorg/smg

Length of output: 480


🏁 Script executed:

# Check if there are error propagation tests showing actual error messages from multimodal
rg -n "process_multimodal" crates/multimodal -B 2 -A 10 | head -80

Repository: lightseekorg/smg

Length of output: 42


🏁 Script executed:

# Verify if MediaConnector errors could include the data: URL
cat -n crates/multimodal/src/media.rs | sed -n '130,160p'

Repository: lightseekorg/smg

Length of output: 1228


🏁 Script executed:

# Check if there are any existing error suppression patterns in the gateway for sensitive data
rg -n "Err\(_[a-z]\)" model_gateway/src/routers --type rust | head -20

Repository: lightseekorg/smg

Length of output: 42


🏁 Script executed:

# Check if there are any other places where Err(_) pattern is used to suppress sensitive data
rg -n "Err\(.*_.*\)" model_gateway/src/routers/grpc --type rust -B 1 -A 2 | head -40

Repository: lightseekorg/smg

Length of output: 3394


🏁 Script executed:

# Look at the multimodal tracker to see what errors it can produce
cat -n crates/multimodal/src/tracker.rs | head -100

Repository: lightseekorg/smg

Length of output: 3970


🏁 Script executed:

# Search for the full error chain in multimodal tracker finalize
rg -n "finalize" crates/multimodal/src/tracker.rs -A 15

Repository: lightseekorg/smg

Length of output: 1113


Redact multimodal errors before logging or returning them.

Inline base64 images are converted to data:{media_type};base64,{data} URLs (in multimodal.rs line 257) before multimodal processing. If the connector's error includes the failing data URL, both the server logs (error = %e) and the HTTP response (format!("Multimodal processing failed: {e}")) will leak the uploaded image payload. Keep the 400 Bad Request status, but redact the raw error message.

Proposed fix
                    Err(_e) => {
                         error!(
                             function = "MessagePreparationStage::execute",
+                            model = %model_id,
-                            error = %e,
                             "Multimodal processing failed"
                         );
                         return Err(error::bad_request(
                             "multimodal_processing_failed",
-                            format!("Multimodal processing failed: {e}"),
+                            "Multimodal processing failed",
                         ));
                     }

The same pattern exists in chat/preparation.rs lines 118–127 and should be fixed identically.

🤖 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/messages/preparation.rs` around
lines 126 - 154, The multimodal error is being logged and returned with the full
error string (which may include raw data URLs); update the handling around
multimodal::process_multimodal_messages in MessagePreparationStage::execute so
you never include the raw error text: log a sanitized message (e.g., "Multimodal
processing failed" plus an error kind or truncated/safe indicator rather than
%e) and return error::bad_request("multimodal_processing_failed", "Multimodal
processing failed") or otherwise a redacted message instead of
format!("Multimodal processing failed: {e}"); apply the identical change to the
analogous handler in chat/preparation.rs to ensure no raw connector payloads are
written to logs or responses.

@CatherineSue CatherineSue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

overall LGTM. not a fan of the overall design tho.


/// Extract multimodal content parts from Messages API input messages,
/// converting `InputContentBlock::Image` to multimodal crate `ChatContentPart`.
fn extract_content_parts_messages(messages: &[InputMessage]) -> Vec<ChatContentPart> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😂 Seems we don't want to do extraction directly on messages input so we transform it to ChatContentPart first?

components: &MultimodalComponents,
tokenizer_source: &str,
) -> Result<MultimodalOutput> {
let mut tracker = AsyncMultiModalTracker::new(components.media_connector.clone());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@github-actions github-actions Bot added the tests Test changes label Mar 17, 2026

@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/multimodal.rs`:
- Around line 238-283: In extract_content_parts_messages, add a brief comment
above the catch-all arm (the `_ => {}` in the match over InputContentBlock
inside extract_content_parts_messages) explaining that non-image/text block
types—e.g., InputContentBlock::Document, ToolUse, ToolResult, VideoUrl, etc.—are
intentionally ignored here because this pipeline only extracts multimodal Image
and Text parts for the media connector; reference the catch-all and the specific
handled arms (InputContentBlock::Image, InputContentBlock::Text) so future
maintainers understand the omission.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2200f36-cfcf-4e19-b2f7-e7f488ba323a

📥 Commits

Reviewing files that changed from the base of the PR and between 3017779 and e21f4da.

📒 Files selected for processing (5)
  • crates/multimodal/src/lib.rs
  • crates/multimodal/src/tracker.rs
  • crates/multimodal/src/types.rs
  • crates/multimodal/tests/multimodal_tracker_test.rs
  • model_gateway/src/routers/grpc/multimodal.rs

Comment on lines +238 to +283
/// Extract multimodal content parts from Messages API input messages,
/// converting `InputContentBlock::Image` to multimodal crate `MediaContentPart`.
fn extract_content_parts_messages(messages: &[InputMessage]) -> Vec<MediaContentPart> {
let mut parts = Vec::new();

for msg in messages {
if msg.role != Role::User {
continue;
}
let blocks = match &msg.content {
InputContent::Blocks(blocks) => blocks,
InputContent::String(_) => continue,
};

for block in blocks {
match block {
InputContentBlock::Image(image_block) => match &image_block.source {
ImageSource::Base64 { media_type, data } => {
// Convert base64 to data URL for the media connector
let data_url = format!("data:{media_type};base64,{data}");
parts.push(MediaContentPart::ImageUrl {
url: data_url,
detail: None,
uuid: None,
});
}
ImageSource::Url { url } => {
parts.push(MediaContentPart::ImageUrl {
url: url.clone(),
detail: None,
uuid: None,
});
}
},
InputContentBlock::Text(text_block) => {
parts.push(MediaContentPart::Text {
text: text_block.text.clone(),
});
}
_ => {}
}
}
}

parts
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add a brief comment explaining which block types are intentionally skipped.

The _ => {} catch-all at line 277 silently skips InputContentBlock::Document, ToolUse, ToolResult, etc. This is consistent with the existing pattern for VideoUrl in the chat pipeline, but a brief comment would improve clarity for future maintainers.

📝 Suggested comment
                 InputContentBlock::Text(text_block) => {
                     parts.push(MediaContentPart::Text {
                         text: text_block.text.clone(),
                     });
                 }
-                _ => {}
+                // Skip Document, ToolUse, ToolResult, etc. — only images and text are
+                // processed for multimodal; other block types pass through unchanged.
+                _ => {}
             }
🤖 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 238 - 283, In
extract_content_parts_messages, add a brief comment above the catch-all arm (the
`_ => {}` in the match over InputContentBlock inside
extract_content_parts_messages) explaining that non-image/text block types—e.g.,
InputContentBlock::Document, ToolUse, ToolResult, VideoUrl, etc.—are
intentionally ignored here because this pipeline only extracts multimodal Image
and Text parts for the media connector; reference the catch-all and the specific
handled arms (InputContentBlock::Image, InputContentBlock::Text) so future
maintainers understand the omission.

@slin1237
slin1237 force-pushed the slin/messages-multimodal branch from e21f4da to 97c9261 Compare March 17, 2026 02:01

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/multimodal/src/lib.rs`:
- Around line 13-15: The crate root removed the legacy export ChatContentPart
causing downstream breakage; restore a compatibility alias by re-exporting the
new type (MediaContentPart) under the old name (ChatContentPart) in lib.rs as a
deprecated shim so consumers keep working for one release—add a pub use or type
alias for ChatContentPart that points to MediaContentPart and mark it deprecated
in the documentation/comments.

In `@model_gateway/src/routers/grpc/multimodal.rs`:
- Around line 223-236: Add unit tests that cover the Messages-specific
multimodal helpers: write tests for has_multimodal_content_messages to assert
that images on non-User roles are ignored, that InputContentBlock variants that
are not images are skipped, and that image sources using ImageSource::Base64 are
normalized to a data: URI (verify the normalization helper that handles
ImageSource::Base64). Create focused cases constructing InputMessage instances
with Role::System/Assistant and Role::User, with InputContent::Blocks containing
InputContentBlock::Image with ImageSource::Base64 and non-image blocks, and
assert expected boolean results and normalized source strings via the multimodal
helper functions (e.g., has_multimodal_content_messages and the image-source
normalization function referenced in the same module).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17ddca3c-49a2-4988-9e08-2d1f996e691e

📥 Commits

Reviewing files that changed from the base of the PR and between e21f4da and 97c9261.

📒 Files selected for processing (5)
  • crates/multimodal/src/lib.rs
  • crates/multimodal/src/tracker.rs
  • crates/multimodal/src/types.rs
  • crates/multimodal/tests/multimodal_tracker_test.rs
  • model_gateway/src/routers/grpc/multimodal.rs

Comment on lines 13 to 15
pub use types::{
ChatContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
MediaContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
MultiModalData, MultiModalUUIDs, PlaceholderRange, PromptReplacement, TokenId, TrackedMedia,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Keep a compatibility export for the renamed public type.

Dropping ChatContentPart from the crate root will break downstream imports immediately. If llm-multimodal is consumed outside this workspace, please either keep a deprecated alias for one release or ship this behind an explicit breaking-version bump.

Possible compatibility shim
 pub use types::{
     MediaContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
     MultiModalData, MultiModalUUIDs, PlaceholderRange, PromptReplacement, TokenId, TrackedMedia,
 };
+#[deprecated(note = "renamed to MediaContentPart")]
+pub use types::MediaContentPart as ChatContentPart;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub use types::{
ChatContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
MediaContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
MultiModalData, MultiModalUUIDs, PlaceholderRange, PromptReplacement, TokenId, TrackedMedia,
pub use types::{
MediaContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
MultiModalData, MultiModalUUIDs, PlaceholderRange, PromptReplacement, TokenId, TrackedMedia,
};
#[deprecated(note = "renamed to MediaContentPart")]
pub use types::MediaContentPart as ChatContentPart;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/multimodal/src/lib.rs` around lines 13 - 15, The crate root removed
the legacy export ChatContentPart causing downstream breakage; restore a
compatibility alias by re-exporting the new type (MediaContentPart) under the
old name (ChatContentPart) in lib.rs as a deprecated shim so consumers keep
working for one release—add a pub use or type alias for ChatContentPart that
points to MediaContentPart and mark it deprecated in the documentation/comments.

Comment thread model_gateway/src/routers/grpc/multimodal.rs
@slin1237
slin1237 force-pushed the slin/messages-multimodal branch from 97c9261 to d7959a5 Compare March 17, 2026 04:31

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

♻️ Duplicate comments (2)
crates/multimodal/src/lib.rs (1)

13-16: ⚠️ Potential issue | 🟠 Major

Keep a deprecated ChatContentPart shim for one release.

This re-export rename makes llm_multimodal::ChatContentPart imports fail immediately. If llm-multimodal is still a published 1.x crate, keep a deprecated alias here (and ideally in types.rs for llm_multimodal::types::ChatContentPart) or ship this behind a major-version bump.

Possible compatibility shim
 pub use types::{
     MediaContentPart, FieldLayout, ImageDetail, ImageFrame, ImageSize, ImageSource, Modality,
     MultiModalData, MultiModalUUIDs, PlaceholderRange, PromptReplacement, TokenId, TrackedMedia,
 };
+#[deprecated(note = "renamed to MediaContentPart")]
+pub use types::MediaContentPart as ChatContentPart;

Run this to verify the crate’s publish/version status before deciding whether the shim can be skipped:

#!/bin/bash
set -euo pipefail

for file in $(fd Cargo.toml); do
  if rg -q '^\s*name\s*=\s*"llm-multimodal"' "$file"; then
    echo "== $file =="
    rg -n --no-heading '^\s*(name|version|publish)\s*=' "$file"
  fi
done

rg -n --no-heading '\bChatContentPart\b' --type rust

Expected results: if llm-multimodal is still published and remains on a non-breaking version line, this rename needs a compatibility shim or a major-version bump.

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

In `@crates/multimodal/src/lib.rs` around lines 13 - 16, Add a one-release
compatibility shim that preserves the old llm_multimodal::ChatContentPart name
by introducing a deprecated type alias to the new MediaContentPart;
specifically, add `pub type ChatContentPart = MediaContentPart;` with a
#[deprecated] annotation in the same module where MediaContentPart is defined
(types.rs) and re-export the alias from lib.rs alongside the existing pub use
list so imports resolving ChatContentPart continue to work for one release
before removing the alias in a future major bump.
model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs (1)

145-154: ⚠️ Potential issue | 🟠 Major

Redact multimodal errors before logging or returning them.

Line 148 logs %e and Line 153 echoes {e} back to the client. Inline images are normalized to data: URLs in model_gateway/src/routers/grpc/multimodal.rs, so connector failures here can leak the full base64 payload into both logs and the 400 body.

Safer error handling
-                    Err(e) => {
+                    Err(_e) => {
                         error!(
                             function = "MessagePreparationStage::execute",
-                            error = %e,
+                            model = %model_id,
                             "Multimodal processing failed"
                         );
                         return Err(error::bad_request(
                             "multimodal_processing_failed",
-                            format!("Multimodal processing failed: {e}"),
+                            "Multimodal processing failed",
                         ));
                     }

Based on learnings, all multimodal processing failures in this pipeline currently stay as 400 Bad Request for simplicity, so the fix here should redact the message without changing the status mapping.

🤖 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/messages/preparation.rs` around
lines 145 - 154, In MessagePreparationStage::execute, avoid logging or returning
the raw multimodal error (currently using %e in error! and {e} in
error::bad_request) because it may contain base64 data: URLs; instead
sanitize/redact the error before use—e.g., derive a safe message like
"multimodal_processing_failed" with non-sensitive details (error kind or a short
code) and log only that safe message plus a non-sensitive error type, and pass
the same redacted string into error::bad_request; reference
MessagePreparationStage::execute, the multimodal normalization in multimodal.rs,
and the error::bad_request call when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/multimodal/src/lib.rs`:
- Around line 13-16: Add a one-release compatibility shim that preserves the old
llm_multimodal::ChatContentPart name by introducing a deprecated type alias to
the new MediaContentPart; specifically, add `pub type ChatContentPart =
MediaContentPart;` with a #[deprecated] annotation in the same module where
MediaContentPart is defined (types.rs) and re-export the alias from lib.rs
alongside the existing pub use list so imports resolving ChatContentPart
continue to work for one release before removing the alias in a future major
bump.

In `@model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs`:
- Around line 145-154: In MessagePreparationStage::execute, avoid logging or
returning the raw multimodal error (currently using %e in error! and {e} in
error::bad_request) because it may contain base64 data: URLs; instead
sanitize/redact the error before use—e.g., derive a safe message like
"multimodal_processing_failed" with non-sensitive details (error kind or a short
code) and log only that safe message plus a non-sensitive error type, and pass
the same redacted string into error::bad_request; reference
MessagePreparationStage::execute, the multimodal normalization in multimodal.rs,
and the error::bad_request call when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ffac8cfe-8d6f-4e32-b8fa-afc222bca52f

📥 Commits

Reviewing files that changed from the base of the PR and between 97c9261 and d7959a5.

📒 Files selected for processing (7)
  • crates/multimodal/src/lib.rs
  • crates/multimodal/src/tracker.rs
  • crates/multimodal/src/types.rs
  • crates/multimodal/tests/multimodal_tracker_test.rs
  • model_gateway/src/routers/grpc/multimodal.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs
  • model_gateway/src/routers/grpc/regular/stages/messages/request_building.rs

Rename ChatContentPart to MediaContentPart in the llm-multimodal crate.
This type is API-agnostic — it represents normalized media content for
the multimodal tracker, not chat-specific content. The old name made it
look like the Messages API pipeline was converting to Chat types,
violating the first-class design principle.

The name is now consistent everywhere — MediaContentPart in the crate
and MediaContentPart in the gateway, no aliases needed.

What changed:
- crates/multimodal/src/types.rs: rename enum ChatContentPart → MediaContentPart
- crates/multimodal/src/tracker.rs: update push_part signature and match arms
- crates/multimodal/src/lib.rs: update re-export
- crates/multimodal/tests/multimodal_tracker_test.rs: update test usage
- model_gateway/src/routers/grpc/multimodal.rs: import MediaContentPart
  directly (no alias needed), update all construction sites and test assertions

Signed-off-by: Simon Lin <simonslin@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the slin/messages-multimodal branch from d7959a5 to 28055f7 Compare March 17, 2026 06:03
@slin1237
slin1237 merged commit 56f31bb into main Mar 17, 2026
12 of 13 checks passed
@slin1237
slin1237 deleted the slin/messages-multimodal branch March 17, 2026 06:06
slin1237 added a commit that referenced this pull request Mar 17, 2026
PR #776 added MediaContentPart to llm-multimodal but did not bump the
crate version. The crates.io release failed because model_gateway
imports MediaContentPart which does not exist in the published
llm-multimodal 1.3.0.

What changed:
- crates/multimodal/Cargo.toml: 1.3.0 → 1.4.0
- Cargo.toml: update workspace dep to 1.4.0

Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
smfirmin pushed a commit to smfirmin/smg that referenced this pull request Mar 20, 2026
smfirmin pushed a commit to smfirmin/smg that referenced this pull request Mar 20, 2026
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 tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants