Skip to content

feat(router): add --mm-per-request-image-limit override for spec image limits - #2381

Merged
smg-project-bot merged 1 commit into
mainfrom
fix/mm-image-limit-override
Sep 1, 2026
Merged

smg-project-bot merged 1 commit into
mainfrom
fix/mm-image-limit-override

Conversation

@CatherineSue

Copy link
Copy Markdown
Member

Description

Problem

The router's multimodal model specs hardcode per-request modality limits — e.g. crates/multimodal/src/registry/qwen3_vl.rs sets Modality::Image → 10 — with no router-level override. A vLLM engine configured with --limit-mm-per-prompt.image 128 still gets every >10-image request rejected at the gateway: HTTP 400 invalid_multimodal_request: model spec qwen3_vl supports at most 10 image inputs; got 100. Live-hit on the moirai OMENative PD campaign (Qwen3-VL, multi-turn conversations legitimately accumulate 100+ images). The gateway should not silently contradict the engine's configured capability.

SMG_IMAGE_MAX_COUNT (#2153) can already raise the limit via env, but there was no first-class router flag plumbed through RouterConfig and the Python launcher.

Solution

Add --mm-per-request-image-limit N to the Rust binary and the Python launcher. When set, it replaces every model spec's built-in image limit (raising or lowering it), and takes precedence over SMG_IMAGE_MAX_COUNT. It never enables a modality a spec does not declare. Unset keeps today's spec-default behavior. The value must be >= 1: clap rejects 0 at parse time, and RouterConfig::validate() rejects it on the pyo3 path.

Enforcement stays at the single validation call site (prepare_placeholder_tokens in model_gateway/src/routers/grpc/multimodal/plan.rs, backed by check_media_counts in crates/multimodal/src/registry/traits.rs). The registry trait gains validate_media_request_with_limits, an object-safe variant taking a per-modality override map, so the mechanism generalizes to video/audio if a flag is ever needed there (env overrides already cover them).

Future work (deeper fix): workers should advertise their engine's --limit-mm-per-prompt via registration labels, and the router should take min(spec, engine) per worker instead of a deployment-wide flag.

Changes

  • crates/multimodal/src/registry/traits.rs: new validate_media_request_with_limits trait method (caller override map > env override > spec limit); validate_media_request delegates to it with no overrides. Unit test for both directions of the override plus unoverridden modalities.
  • model_gateway/src/config/{types,builder,validation}.rs: RouterConfig.mm_per_request_image_limit: Option<usize> + builder method + Some(0) rejected in validate_server_settings.
  • model_gateway/src/main.rs: --mm-per-request-image-limit CLI flag (range(1..)), plumbed into the builder; extended the multimodal config-plumbing guard test.
  • model_gateway/src/routers/grpc/multimodal/{config,plan}.rs, router.rs: MultimodalComponents carries modality_limit_overrides built from RouterConfig; the validation call site passes it through.
  • bindings/python/src/lib.rs: mm_per_request_image_limit appended at the pyo3 signature/struct tail, forwarded to the RouterConfig builder (validated by the existing router_config.validate() call in start()).
  • bindings/python/src/smg/router_args.py: dataclass field appended at the tail + --mm-per-request-image-limit argparse flag.
  • bindings/python/tests/test_arg_parser.py: parse test + frozen field-order list updated.

Test Plan

Ran locally:

  • cargo test -p llm-multimodal --lib traits:: — 8 passed (includes new caller_limit_override_replaces_spec_limit)
  • cargo test -p smg --bin smg multimodal_transport_flows_into_both_configs — passes (now also asserts the new flag reaches both RouterConfig and ServerConfig, and that clap rejects 0)
  • cargo test -p smg --lib config::validation::tests::zero_mm_per_request_image_limit_is_rejected — passes
  • cargo test -p smg --lib routers::grpc::multimodal — 53 passed
  • cargo check -p llm-multimodal / -p smg --tests / -p smg-python — clean
  • cargo clippy -p llm-multimodal -p smg -p smg-python --no-deps — no warnings
  • maturin develop + pytest bindings/python/tests/test_arg_parser.py — 58 passed, 1 skipped; also verified Router.from_args accepts the new field end-to-end through the pyo3 boundary
  • cargo +nightly fmt — clean

Honest note: the full cargo clippy --workspace --all-targets --all-features pre-commit hook needs system opencv (the opencv-video feature) which is not installed on this machine, so that exact invocation is deferred to CI; the touched crates were clippy-clean with default features.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes (needs system opencv; deferred to CI — touched crates clippy-clean locally)
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

…e limits

The multimodal model specs hardcode per-request modality limits (e.g.
qwen3_vl caps images at 10) with no router-level override. A vLLM engine
configured with --limit-mm-per-prompt.image 128 still gets every
>10-image request rejected at the gateway with HTTP 400
invalid_multimodal_request, so the gateway silently contradicts the
engine's configured capability.

Add a --mm-per-request-image-limit flag (Rust binary and Python
launcher) that replaces each spec's built-in image limit for all models,
taking precedence over the SMG_IMAGE_MAX_COUNT env override. The
registry trait gains validate_media_request_with_limits, an object-safe
variant taking per-modality caller overrides, and the router passes its
configured map through MultimodalComponents into the single validation
call site. Unset keeps today's spec-default behavior; zero is rejected
at clap parse time and in RouterConfig::validate.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@github-actions github-actions Bot added python-bindings Python bindings changes grpc gRPC client and router changes tests Test changes multimodal Multimodal crate changes model-gateway Model gateway crate changes labels Sep 1, 2026

/// [`Self::validate_media_request`] with caller-supplied per-modality limit
/// overrides; each replaces the spec limit and beats the env override.
fn validate_media_request_with_limits(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: The trait now has two validation entry points, and the only production caller (prepare_placeholder_tokens) uses the new one. Today no spec overrides validate_media_request, so this is fine — but the "obvious" method for a future spec author to override is validate_media_request, and such an override would be silently bypassed by the router, since the default validate_media_request_with_limits calls modality_limits/check_media_counts directly.

Cheap ways to keep that from rotting: note in the validate_media_request doc comment that specs must override validate_media_request_with_limits (not this one) because the gateway only calls the _with_limits variant, or drop the default body of validate_media_request and make it a non-trait helper so there's only one overridable hook.

model_registry: Arc::new(ModelRegistry::default()),
config_registry,
pixel_cache: pixel_cache_from_env(),
modality_limit_overrides: image_limit_override

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: This Option<usize> → HashMap<Modality, usize> mapping is the one link in the chain with no test. The PR covers the ends (traits.rs unit test for the override semantics, main.rs for CLI → RouterConfig, validation.rs for Some(0)), but nothing asserts that MultimodalComponents::new(reg, Some(128)) yields {Image: 128} and that None yields an empty map. A silent regression here (e.g. someone keying it on Modality::ImageEmbeds, or an unwrap_or_default() that swallows the value) would make the flag a no-op with every existing test still green.

MultimodalComponents::new builds a reqwest client and a MediaConnector, so a direct test is a bit heavy — extracting the two-line mapping into a small free function (fn image_limit_overrides(limit: Option<usize>) -> HashMap<Modality, usize>) would make it a two-case unit test.

)
parser.add_argument(
f"--{prefix}mm-per-request-image-limit",
type=int,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: The help text promises "Must be >= 1", but type=int accepts 0 and negatives, so the two entry points reject bad input at different places with different messages:

  • Rust CLI: clap range(1..) → clean parse-time error naming the flag.
  • Python launcher 0: accepted by argparse, accepted by the pyo3 constructor, and only rejected at Router.start() as Configuration validation failed: ....
  • Python launcher -1: OverflowError out of the pyo3 Option<usize> extraction at _Router(**args_dict).

Both do fail loudly, so nothing is silently wrong — but a small type= validator would give the Python path the same parse-time rejection as the Rust one:

def _positive_int(value: str) -> int:
    parsed = int(value)
    if parsed < 1:
        raise argparse.ArgumentTypeError("must be >= 1")
    return parsed

Comment thread model_gateway/src/main.rs
/// Per-request image-count limit applied to every model, replacing each
/// spec's built-in limit (e.g. to match the engine's `--limit-mm-per-prompt`).
#[arg(long, value_parser = clap::value_parser!(u64).range(1..), help_heading = "Multimodal")]
mm_per_request_image_limit: Option<u64>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: Naming is off-pattern for this help group. The two sibling flags under help_heading = "Multimodal" spell the prefix out (--multimodal-tensor-transport, --multimodal-shm-min-bytes), and the env override this flag supersedes is SMG_IMAGE_MAX_COUNT. --mm-per-request-image-limit introduces a third spelling (mm) for the same subsystem.

Worth settling now rather than later — the name is baked into RouterConfig's serde field, the pyo3 signature, and the frozen RouterArgs field-order list, so renaming after release is a breaking change on three surfaces. --multimodal-per-request-image-limit would match the neighbours; if the mm prefix is deliberate (it does echo vLLM's --limit-mm-per-prompt), a line in the doc comment saying so would stop the next person from "fixing" it.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: b4bfa2ba-ed61-436f-b701-e1279cfdc103

📥 Commits

Reviewing files that changed from the base of the PR and between cc495e6 and 0156cdc.

📒 Files selected for processing (11)
  • bindings/python/src/lib.rs
  • bindings/python/src/smg/router_args.py
  • bindings/python/tests/test_arg_parser.py
  • crates/multimodal/src/registry/traits.rs
  • model_gateway/src/config/builder.rs
  • model_gateway/src/config/types.rs
  • model_gateway/src/config/validation.rs
  • model_gateway/src/main.rs
  • model_gateway/src/routers/grpc/multimodal/config.rs
  • model_gateway/src/routers/grpc/multimodal/plan.rs
  • model_gateway/src/routers/grpc/router.rs

Included review availability: 9 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added an optional --mm-per-request-image-limit setting to limit images in each multimodal request.
    • Added support for configuring the image limit through the Python Router.
    • Configured limits override model-declared limits when provided.
  • Bug Fixes

    • Invalid values of 0 are now rejected; positive limits are accepted.
    • Requests using unsupported media types continue to be rejected.

Walkthrough

The change adds an optional per-request image-count limit to Python and server configuration. The limit is validated, propagated through multimodal router initialization, and applied during media request validation.

Changes

Multimodal image limits

Layer / File(s) Summary
Configuration surface
model_gateway/src/config/types.rs, model_gateway/src/config/builder.rs, model_gateway/src/main.rs, bindings/python/src/smg/router_args.py, bindings/python/src/lib.rs, bindings/python/tests/test_arg_parser.py
The Python and server interfaces accept --mm-per-request-image-limit. The value defaults to None, is stored in RouterConfig, and is propagated through router construction. Parsing and propagation tests cover configured and unset values.
Limit validation contract
model_gateway/src/config/validation.rs, crates/multimodal/src/registry/traits.rs, model_gateway/src/main.rs
Zero is rejected. The limit-aware media validator applies caller overrides before environment and declared limits. Tests cover raising limits, rejecting excessive counts, and preserving unoverridden modality limits.
Multimodal enforcement
model_gateway/src/routers/grpc/router.rs, model_gateway/src/routers/grpc/multimodal/config.rs, model_gateway/src/routers/grpc/multimodal/plan.rs
The configured image limit becomes a Modality::Image override. Placeholder-token preparation uses the override-aware validator.
Estimated code review effort: 3 (Moderate) ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonRouter
  participant RouterConfig
  participant GrpcRouter
  participant MultimodalComponents
  participant ModelProcessorSpec
  PythonRouter->>RouterConfig: Set mm_per_request_image_limit
  RouterConfig->>GrpcRouter: Pass configured image limit
  GrpcRouter->>MultimodalComponents: Initialize image override
  MultimodalComponents->>ModelProcessorSpec: Validate media request with limits
Loading

Suggested reviewers: key4ng, slin1237

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 11 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a router flag that overrides model-spec image limits.
Description check ✅ Passed The description directly explains the problem, solution, affected components, behavior, and test coverage for the image-limit override.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mm-image-limit-override

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


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

@smg-project-bot
smg-project-bot merged commit f48304a into main Sep 1, 2026
40 of 48 checks passed
@smg-project-bot
smg-project-bot deleted the fix/mm-image-limit-override branch September 1, 2026 19:57
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 python-bindings Python bindings changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants