Skip to content

feat(frontend): refuse media before routing for text-only models - #17874

Merged
biswapanda merged 2 commits into
mainfrom
bis/16251-frontend-reject-media
Nov 4, 2026
Merged

biswapanda merged 2 commits into
mainfrom
bis/16251-frontend-reject-media

Conversation

@biswapanda

@biswapanda biswapanda commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Text-only frontends can opt in to refusing media before routing or opening an SSE stream. --reject-media defaults to off; --reject-media-status-code accepts 400–599 and defaults to 415 so gateways can choose a failover status.

Key Changes

Approximately 331 added production lines, excluding tests, documentation, comments, and whitespace; no new dependencies.

  • Configure the policy through Rust, Python, CLI, and environment settings. Label refusals unsupported_modality independently of their HTTP status. This reuses the request counter family, with at most six additional series per model across three endpoints and unary/streaming requests.
  • Classify Chat Completions media, native Responses images (including file IDs, all supported roles, and function-call outputs), and native Anthropic images (including URL sources and nested tool results) before conversion can reject or discard them. Anthropic ingress inspection borrows media payloads and only runs when enabled.
  • Preserve shaped Anthropic 415/422 responses through main's request-ID middleware while extraction errors remain 400. OpenAI handler-rendered 422 errors, including backend client errors, also retain 422 with media rejection disabled; this is an intentional default-configuration behavior change. Anthropic engine lookup remains before conversion and token estimation when rejection is disabled. OpenAI readiness retains precedence.

Validation

Behavior Before review fixes After review fixes
Responses input_image with only file_id Conversion returned an error instead of the configured refusal Configured refusal before conversion
Responses system/developer/assistant images Conversion could discard media Configured refusal; no engine dispatch
Anthropic URL images and nested tool-result images Typed parsing/conversion returned 400 or dropped nested content Configured refusal from native ingress, including legacy text siblings
Anthropic default 415 after integrating main Main's middleware would rewrite shaped 415 to 400 Default 415 and configured 422/429 survive with request IDs and refusal metrics
Rejection disabled, unknown Anthropic model with conversion-invalid content Conversion ran before lookup 404 lookup precedence restored
Rejection disabled, OpenAI backend 422 Main rewrote the handler's shaped error to 400 HTTP regression pins 422 and sanitized message

All 44 HTTP service tests pass on the integrated revision (cargo test -p dynamo-llm --no-default-features --test http-service), including seven media regression tests. Removing the review fixes and retaining main's conflicting middleware produces six regression failures; restoring the fixes makes the full suite pass again (7.71 seconds). Changed-page documentation lint passes. The Fern CLI is unavailable locally, so Fern configuration/link checks were not rerun for this revision.

Initial implementation validation on 6969cca included 41 HTTP integration, 477 HTTP unit, 7 configuration unit, and 41 Python configuration tests, plus binding cargo check. That initial Python run used an existing native overlay. No rebuilt native Python roundtrip or live GPU deployment has been run for this update.

Issues and Ongoing Work

Pull Request Checklist
  • I used Dynamo's /pr-checklist skill.

Signed-off-by: Biswa Panda <biswa.panda@gmail.com>
@biswapanda
biswapanda requested a review from a team as a code owner November 2, 2026 21:58
@copy-pr-bot

copy-pr-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@biswapanda
biswapanda requested review from a team as code owners November 2, 2026 21:58
@github-actions github-actions Bot added feat documentation Improvements or additions to documentation frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` labels Nov 2, 2026
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

This change adds opt-in frontend rejection for requests containing image, video, or audio content. The rejection status is configurable. OpenAI-compatible and Anthropic routes return refusals before generation or model lookup and classify them as unsupported_modality.

Changes

Frontend media refusal

Layer / File(s) Summary
Configure and expose media refusal
lib/llm/src/frontend_config.rs, lib/runtime/src/config/environment_names.rs, components/src/dynamo/frontend/frontend_args.py, components/src/dynamo/frontend/main.py, components/src/dynamo/frontend/tests/*, lib/bindings/python/rust/llm/entrypoint.rs, lib/bindings/python/src/dynamo/_core.pyi, lib/llm/src/http/service/service_v2.rs, docs/fern/pages/reference/components/frontend-configuration.mdx
Adds enablement and status-code settings, defaults, and 400–599 validation. Passes the settings through frontend and Python entrypoints into service configuration. Adds configuration tests and documentation.
Classify unsupported modality failures
lib/llm/src/http/service/metrics.rs, lib/llm/src/http/service/disconnect.rs, lib/runtime/src/metrics/prometheus_names.rs, lib/bindings/python/src/dynamo/prometheus_names.py, lib/llm/src/http/service/openai.rs
Adds the UnsupportedModality error type and its metric label, log detail, terminal outcome, and delivered-failure classification.
Detect and reject media requests
lib/llm/src/preprocessor.rs, lib/llm/src/http/service/openai.rs, lib/llm/src/http/service/anthropic.rs, lib/llm/tests/http-service.rs
Detects image, video, and audio content across messages. OpenAI-compatible routes reject matching requests before generation; Anthropic rejects them before model lookup. Tests cover configured statuses, error bodies, metrics, malformed requests, and text-only requests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Severity of issue fixed: Medium

Merge Risk: Low · up to 6969c

Audio-bearing Responses requests do not get the refusal behavior described in the frontend documentation. This is a bounded compatibility gap rather than a blocker for supported Responses inputs.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: refusing media before routing for text-only models.
Linked Issues check Passed The PR implements the coding requirements in #16251. reject_media is opt-in and defaults to disabled. reject_media_status_code accepts 400–599 and defaults to 415. Media detection scans all chat m…
Out of Scope Changes check Passed The changed files support the media-rejection objective in #16251. Configuration, PyO3 bindings, environment names, frontend documentation, media detection, metrics, route handling, middleware behavio…
Description check Passed The description includes all required sections: Overview, Key Changes, Validation, Issues and Ongoing Work, and the pull request checklist. It provides detailed implementation scope, validation result…
Full details: Linked Issues check

Explanation

The PR implements the coding requirements in #16251. reject_media is opt-in and defaults to disabled. reject_media_status_code accepts 400–599 and defaults to 415. Media detection scans all chat messages, including history, and covers image, video, audio, media URLs, and inline input_audio. OpenAI, Responses, and Anthropic routes reject media before generation, routing, or engine lookup. The rejection uses ErrorType::UnsupportedModality and the unsupported_modality metric label. Integration and configuration tests cover the behavior.

Full details: Out of Scope Changes check

Explanation

The changed files support the media-rejection objective in #16251. Configuration, PyO3 bindings, environment names, frontend documentation, media detection, metrics, route handling, middleware behavior, and tests provide the requested opt-in setting, configurable status, early rejection, and distinct error classification. Constructor validation and Anthropic lookup and 422 handling support configuration and early route responses. No demonstrated change lacks a connection to this objective.

Full details: Description check

Explanation

The description includes all required sections: Overview, Key Changes, Validation, Issues and Ongoing Work, and the pull request checklist. It provides detailed implementation scope, validation results, limitations, linked issue information, and checklist completion.

Full details: Docstring Coverage

Explanation

Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 20 functions across 15 files. (2 skipped: 1 unsupported, 1 too large.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 7c195fd and 6969cca.

Files selected for processing: 17
  • components/src/dynamo/frontend/frontend_args.py
  • components/src/dynamo/frontend/main.py
  • components/src/dynamo/frontend/tests/test_frontend_reject_media_args.py
  • docs/fern/pages/reference/components/frontend-configuration.mdx
  • lib/bindings/python/rust/llm/entrypoint.rs
  • lib/bindings/python/src/dynamo/_core.pyi
  • lib/bindings/python/src/dynamo/prometheus_names.py
  • lib/llm/src/frontend_config.rs
  • lib/llm/src/http/service/anthropic.rs
  • lib/llm/src/http/service/disconnect.rs
  • lib/llm/src/http/service/metrics.rs
  • lib/llm/src/http/service/openai.rs
  • lib/llm/src/http/service/service_v2.rs
  • lib/llm/src/preprocessor.rs
  • lib/llm/tests/http-service.rs
  • lib/runtime/src/config/environment_names.rs
  • lib/runtime/src/metrics/prometheus_names.rs

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@biswapanda
biswapanda enabled auto-merge (squash) November 3, 2026 05:41

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

LGTM. One non-blocking P2 follow-up below for Responses input classification. Reviewed 6969cca; tests were not run locally.

Comment thread lib/llm/src/http/service/openai.rs Resolved

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

Summary: The PR adds an opt-in --reject-media gate that classifies deserialized user/tool content parts and refuses image/video/audio requests after validation and before engine lookup on chat, responses and /v1/messages, with a configurable 4xx/5xx status and an unsupported_modality metric. The chat path is correct and well tested. Two things need attention before merge: the single merge conflict sits on the Anthropic error middleware whose main version rewrites 415 to 400, and no test pins the default-415 Anthropic refusal, so a straightforward conflict resolution would silently break the documented status; and Anthropic URL-source images are rejected by conversion with 400 before the refusal runs.

Blocking

  • lib/llm/src/http/service/anthropic.rs:132 — PR intercepts only 422; main (#17367) rewrites 422 and 415 to 400, and this function is the one conflicting hunk → after rebase the default-415 /v1/messages refusal becomes 400 unless the pass-through is kept → add check_reject_media_anthropic(StatusCode::UNSUPPORTED_MEDIA_TYPE) (tests at http-service.rs:4313 only run 429 and 422) and keep shaped-body pass-through for 415 when resolving.

Non-blocking

  • anthropic.rs:570 — image blocks with source.type == "url" fail try_into (types.rs:303, "only base64 is supported") with 400 before media_refusal runs, so the configured gateway-failover status never fires for the most common Anthropic SDK shape (P2; Anthropic counterpart of jh-nv's Responses file_id point).
  • openai.rs:1063 — smart_json_error_middleware now passes any handler-rendered 422 through unconditionally, so a backend 422 forwarded via ForwardClientError reaches clients as 422 instead of 400 even with --reject-media off (P3, unannounced).
  • anthropic.rs:584 — engine lookup moved after conversion/validation unconditionally: with the flag off, unknown model + invalid body now returns 400 where main returned 404 (P3, unannounced; fine if intended).

Checked: diff read against merge base 7c195fd and git merge-tree against main (one conflict, anthropic_error_middleware); classification covers every non-text variant in dynamo-protocols 8.4.0 user/tool parts, text-only arrays and null assistant content pass; ordering readiness → validation → refusal → lookup on all three APIs; status range enforced in Rust new(), Python validate() and env parsing (invalid env warns, no panic); default off short-circuits; UNSUPPORTED_MODALITY mirrored in Rust and Python name files, exhaustive matches updated; refusal is a pre-stream HTTP error; one failure + one error mark per refusal. Tests not run; CI green on 6969cca.

Comment thread lib/llm/src/http/service/anthropic.rs Resolved
Comment thread lib/llm/src/http/service/anthropic.rs Resolved
Comment thread lib/llm/src/http/service/openai.rs Resolved
Comment thread lib/llm/src/http/service/anthropic.rs Resolved
Signed-off-by: Biswa Panda <biswa.panda@gmail.com>

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

Approving at 4aab4fe; this lifts my earlier request for changes.

Addressed

  • anthropic_error_middleware keeps main's request-id handling and passes shaped {"type":"error"} bodies through with the status untouched; test_reject_media_preserves_anthropic_default_415 pins the default.
  • Anthropic media is now classified on the raw body before try_into (anthropic/validation.rs::media_content_type), so URL-source images in user content and tool_result get the configured status; tests cover base64/url × top-level/nested.
  • Engine lookup restored before conversion; test_reject_media_disabled_preserves_anthropic_model_lookup_precedence pins 404 with the flag off.
  • The unconditional 422 pass-through in smart_json_error_middleware is documented as an intentional default change and tested.
  • Responses classification moved before conversion (responses_media_content_type), covering file_id images for every role and function_call_output, which also closes jh-nv's point.

Non-blocking

  • lib/llm/src/http/service/anthropic.rs:439 — Anthropic refusal now runs before typed validation (validate_anthropic_messages / tools / max_tokens), whereas chat and responses keep validation → refusal; media + invalid tools[0].type yields 415 instead of 400. Fine for gateway failover, just an inconsistency to be aware of (P3).
  • lib/llm/src/http/service/openai.rs:480 — Item::ComputerCallOutput screenshots are not classified and the converter skips them; computer-use is unsupported end to end, so follow-up at most (P3).

Checked: interdiff 6969cca → 4aab4fe (non-test changes confined to anthropic.rs, anthropic/validation.rs, openai.rs, docs; preprocessor.rs delta byte-identical); single refusal call site per handler, one failure record + one error mark per refusal; readiness still precedes refusal on chat/responses; no new unwrap/panic. The red rust-tests (.) is the unrelated http-transcriptions connection-error flake (upload_too_large_…), seen on other PRs today; rerunning it.

@biswapanda
biswapanda disabled auto-merge November 4, 2026 00:59
@biswapanda
biswapanda force-pushed the bis/16251-frontend-reject-media branch from 4aab4fe to 2fa369c Compare November 4, 2026 01:03
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

@biswapanda
biswapanda enabled auto-merge (squash) November 4, 2026 01:13
@biswapanda biswapanda self-assigned this Nov 4, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

✅ Dynamo PR CI passed — run 41751352487 (attempt 1) on 2fa369ccb5

Gate checks: ✅ backend-status-check · ✅ deploy-status-check · ✅ dynamo-status-check

Posted automatically by Devin for run 41751352487. Updated on every full-CI run of this PR.

@biswapanda
biswapanda merged commit a80115b into main Nov 4, 2026
161 of 165 checks passed
@biswapanda
biswapanda deleted the bis/16251-frontend-reject-media branch November 4, 2026 04:43
ishandhanani added a commit to ishandhanani/dynamo that referenced this pull request Nov 4, 2026
Absorbs the 95 upstream commits since 464ff29. Each upstream change lands in the crate that owns that code at the stack head; the stack's moves stay intact. Router-touching PRs and where they landed:

- ai-dynamo#17712 host-path routing modes for worker-selection plugins: &self + Send + Sync plugin traits and WorkerInputs::ACTIVE_REQUESTS (deprecated OCCUPANCY alias) in dynamo-worker-selection and dynamo-router-protocol; HostedWorkerPicker and HostedSelectionRequest in dynamo-worker-selection (hosted.rs), reporting SelectionFailure, which the kernel converts into KvSchedulerError; the InstanceSelector adapter in lib/llm (HostedInstanceSelector); the compat test's hosted coverage in kv-router's prelude test.
- ai-dynamo#17713 random/round-robin/least-loaded as builtin plugins: router_modes in dynamo-router-policies (renamed from dynamo-custom-policy-builtin) importing the prelude; RouterPluginRegistry::resolve_type in kv-router; the frontend's hosted path through dynamo_worker_selection::host.
- ai-dynamo#17899 1.7.0 removals: taken whole; the alias and the setter were gone at the head; power-of-two never arrives.
- ai-dynamo#17657 single-source fetch continuations: KvTransferSourceInterval and source_intervals in dynamo-kv-indexer, the bucketed summary in dynamo-kv-hints, the interval filter in the kernel's resolver; tests in the kernel.
- ai-dynamo#17856 KVCR hints in offline replay: mocker and lib/llm bodies; supports_kv_transfer_chain_retention on the flat config; its indexer-vs-config test in kv-router's core host tests.
- ai-dynamo#17794 vLLM partial-tail filter and ai-dynamo#17126 SGLang engine-owned multimodal pads: dynamo-kv-events zmq_wire (mm_exclusion.rs); the codec-plus-indexer tests in dynamo-kv-indexer; the engine_owned_mm_identity worker flag through the kernel's WorkerRequest, kv-router's indexer registry and run config, and the bindings.
- ai-dynamo#17653 SGLang 0.5.23: pad constant docs in dynamo-multimodal.
- ai-dynamo#15333, ai-dynamo#17874, ai-dynamo#17484, ai-dynamo#17688, ai-dynamo#17237, ai-dynamo#17894, ai-dynamo#17958: host-side changes merged in place (bindings, lib/llm, CI).

Lockfiles regenerated; no shim reintroduced.

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

Labels

documentation Improvements or additions to documentation feat frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CONTRIBUTION]: Text-only media rejection: configurable status, rejected at the frontend before streaming

3 participants