feat(messages): route Azure Anthropic /messages through Rust behind rust:true - #33616
Conversation
…ust:true Adds an opt-in Rust path for non-streaming Azure Anthropic Messages. A deployment sets rust: true in litellm_params to route litellm.messages() and the proxy /v1/messages endpoint through the native Rust bridge; a missing flag or rust: false keeps the existing Python path, and non-Azure providers, streaming, an unavailable bridge, or a None result all fall back to Python. Rust-backed responses carry an x-litellm-rust: true response header so callers can see which path served the request. Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
…oc check Mirrors the existing LITELLM_USE_RUST_OCR entry; the flag is an internal rollout toggle that is intentionally not in the public environment settings docs yet. Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Greptile SummaryThis PR adds an opt-in Rust path for Azure Anthropic
Confidence Score: 5/5Safe to merge: the Rust path is fully opt-in per deployment, any bridge failure falls back to the existing Python implementation, and all previously flagged issues are resolved. The change introduces no new default behavior — every request not explicitly flagged with
|
| Filename | Overview |
|---|---|
| litellm/llms/custom_httpx/llm_http_handler.py | Inserts Rust gate and fake-stream helper before the existing Python HTTP call; extracts _finalize_anthropic_messages_response to avoid duplicating the post-call logic across the Rust and Python paths. The placement relative to pre_call logging and agentic-hook detection is correct. |
| litellm/rust_bridge/messages.py | Thin Python wrapper for the Rust messages bridge; previous reviewer concerns about dead rust_messages_enabled() and a missing enabled field are fully resolved — the gate is now purely per-deployment via litellm_params.get("rust"), matching the PR's opt-in design. |
| litellm/rust_bridge/ocr.py | Fixed the OCR-enabled side-effect flagged in the previous review: _rust_ocr_enabled is now only updated when configuring_ocr or not configuring_messages, so callers who only set the messages bridge no longer inadvertently toggle OCR. |
| litellm-rust/crates/core/src/providers/azure_ai/messages/transformation.rs | Implements Azure-specific transforms: URL normalization, x-api-key auth, cache_control.scope stripping, and system-role message folding. URL normalization uses split_once("/anthropic") which can spuriously match the second // in the URL scheme when a resource hostname starts with "anthropic". |
| litellm-rust/crates/ai-gateway/src/messages/handler.rs | Buffered HTTP executor: applies per-request timeout override on top of the shared OnceLock reqwest client and truncates upstream error bodies to MESSAGES_ERROR_BODY_MAX_CHARS before crossing the host boundary. Logic is straightforward and correct. |
| litellm-rust/crates/ai-gateway/src/messages/prepare.rs | Resolves provider config, injects auth only when the header is absent (correctly avoids duplicating Python-set headers), sets default headers idempotently, then deserializes and transforms the typed request. Correct and safe. |
| litellm-rust/crates/core/src/messages/types.rs | Well-typed end-to-end request/response structs with #[serde(flatten)] extra for forward-compatibility with new Anthropic parameters. Named fields cover the full documented parameter set; unknown fields are preserved and round-tripped. |
| litellm-rust/crates/python-bridge/src/lib.rs | Adds PyO3 messages and amessages adapters mirroring the existing ocr/aocr pattern; sync path releases the GIL, async path uses future_into_py. Both validate that body is a JSON object before dispatch. |
| tests/test_litellm/rust_bridge/test_messages.py | All tests use injected mock bridges; no real network calls. Covers gate on/off, Azure-only restriction, argument forwarding, timeout conversion, bridge-exception fallback, streaming eligibility, SSE event shape, and bridge unavailability. |
| litellm/rust_bridge/timeouts.py | Extracts _timeout_to_seconds from ocr.py into a shared utility; OCR callers updated to import from here. Clean refactor with no logic changes. |
Reviews (2): Last reviewed commit: "fix(messages): fold system-role messages..." | Re-trigger Greptile
| def rust_messages_enabled() -> bool: | ||
| return _STATE.enabled | ||
|
|
There was a problem hiding this comment.
rust_messages_enabled() is never consulted in the routing gate
rust_messages_enabled() returns _STATE.enabled, which is initialized from the LITELLM_USE_RUST_MESSAGES environment variable and updated by use_litellm_rust. However, _maybe_rust_anthropic_messages only checks litellm_params.get("rust") is not True — it never calls rust_messages_enabled(). This means setting LITELLM_USE_RUST_MESSAGES=0 or calling use_litellm_rust(False) has no effect on the messages path, contrary to what the OCR path does (rust_ocr_enabled() is checked in ocr/main.py before every call). The function is effectively dead code and the global disable toggle is silently broken for messages.
|
|
||
|
|
||
| def load_rust_amessages() -> RustAmessages | None: | ||
| if _STATE.amessages is not None: | ||
| return _STATE.amessages | ||
| from litellm.rust_bridge import get_native_bridge | ||
|
|
||
| native_bridge = get_native_bridge() | ||
| if native_bridge is None: | ||
| return None | ||
| return cast(RustAmessages, getattr(native_bridge, "amessages", None)) | ||
|
|
||
|
|
There was a problem hiding this comment.
load_rust_messages / load_rust_amessages ignore _STATE.enabled
Both loader functions return a bridge implementation without checking _STATE.enabled. If a caller injects a bridge via set_rust_messages(enabled=False, messages=bridge), load_rust_messages() will still return bridge because only the _STATE.messages is not None guard is evaluated. The enabled field is stored and exported as rust_messages_enabled() but never enforced in the load path — the OCR bridge has the same architectural shape but the OCR load path is gated externally in ocr/main.py. The messages path has no equivalent external gate, so the enabled field is a no-op in the current design.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…al toggle use_litellm_rust only mutates the OCR enabled flag when configuring OCR (or called with no bridge kwargs, preserving the legacy contract), so configuring only the messages bridge no longer flips OCR state. Remove the vestigial global enabled/env state from the messages bridge. Routing is controlled per deployment by rust:true in the shared handler gate, so the messages module never consulted the global toggle; drop it rather than leave a no-op switch. Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Demo: live gateway proofRan the proxy locally on Results
Automated: |
… file and type the request/response contract Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
… via buffered fake-stream Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
… back to Python on Rust bridge errors Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Updated demo: Claude Code making real edits through the gateway (Rust route)This supersedes the earlier curl-only demo. Claude Code is pointed at the local gateway ( What the recording shows The config has One honest caveat: Claude Code also fires a small-fast-model title-generation call that uses structured outputs, which this Azure workspace is not entitled for, so that single side call returns 400. It does not affect the coding turns or the edits, and both the Python and Rust deployments reject that exact request identically |
|
@greptileai please re-review. Since the last review this PR split the Anthropic Messages config into its own provider file, typed the request/response contract end to end (no bare |
…itellm_rust_messages_azure
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
This reverts commit c86d861.
Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
Screenshots / Proof of Fix
End-to-end against a live proxy on
localhost:4000hitting the real Azure Anthropic API (no mocks), captured at commitee027e9c5bTwo deployments of the same model differ only in the
rustflagThe
rust: truedeployment returns a real 200 and carries thex-litellm-rust: truemarker headerThe
rust: falsetwin returns a real 200 with no marker header, i.e. it stayed on the existing Python pathBeyond curl, Claude Code was pointed at the gateway and made real code edits through the Rust route across two models (
sonnet-4-5-rustfixed a bug,sonnet-4-6-rustadded a function). The recorded walkthrough and a written test report with screenshots are shared with the requester rather than embedded hereType
🆕 New Feature
Changes
This adds an opt-in Rust path for Azure Anthropic Messages, mirroring the existing OCR Rust bridge as closely as possible
Rollout is per-deployment and off by default. A request routes through Rust only when the provider is
azure_aiand the deployment setsrust: trueinlitellm_params. Every other case falls back to the existing Python implementation: a missingrustflag,rust: false, a non-Azure provider, a native bridge that is not built, or a bridge that returnsNone. The gate also wraps the bridge call so that any Rust bridge exception falls back to Python, which keeps the public status and error body identical to the Python path (previously a Rust upstream error surfaced as a 500 with an OCR-flavored message). Because the gate lives inside the sharedanthropic_messageshandler and only shortcuts the outbound network call, all Python hooks, interceptors, provider resolution, logging, and mock/short-circuit behavior are preservedWhen Rust serves a request the response carries an
x-litellm-rust: trueheader (set through_hidden_params.additional_headers) so a caller can tell which path handled itStreaming now also routes through Rust for eligible requests. Python prepares the normal Anthropic request, Rust performs the upstream call with
streamremoved, and Python re-emits the full response as Anthropic SSE throughFakeAnthropicMessagesStreamIteratorwhile still exposingx-litellm-rust: true. This is buffered Rust-handled streaming, not incremental Rust SSE transport, and it keeps the existing event shape (message_start,content_block_*,message_delta,message_stop)The work follows the three-crate Rust layout.
litellm-coreholds the pure transforms, now split so the generic Anthropic Messages config lives in its own provider file (providers/anthropic/messages) and the Azure config composes it (providers/azure_ai/messages) adding/anthropic/v1/messagesURL normalization,x-api-keyauth, andcache_controlscopestripping. The request and response contract is typed end to end (AnthropicMessagesRequest/AnthropicMessagesResponsewith typedsystem,messages, andcontentblocks) instead of a bareserde_json::Value, covering the full documented Anthropic Messages parameter set.litellm-ai-gatewayowns the buffered network execution with a reused reqwest client, connect and full-request timeouts, and bounded, sanitized upstream error bodies that never log payloads or secrets.litellm-python-bridgeexposes thin PyO3messagesandamessagesadaptersThe Azure transform also folds a
role: "system"chat message into the top-levelsystemfield. Some agent clients (e.g. Claude Code) send an extrarole: "system"message that Anthropic and Azure both reject; folding it intosystemis spec-compliant and lets those clients complete real turns through the gatewayChanges
Files of note: the Python gate and buffered-stream helper in
litellm/llms/custom_httpx/llm_http_handler.py, the bridge wrapper inlitellm/rust_bridge/messages.py, the core transforms underlitellm-rust/crates/core/src/providers/{anthropic,azure_ai}/messages, and the gateway I/O underlitellm-rust/crates/ai-gateway/src/messagesTests cover both layers.
cargo test --workspaceexercises the core transforms (supported-parameter coverage, request shape, response normalization, cache-control scope stripping, and the system-role fold), the gateway request/auth/error handling, and the bridge marshaling.tests/test_litellm/rust_bridge/test_messages.pycovers the gate on and off, the Azure-only restriction, sync and async argument forwarding, timeout conversion, the header metadata, buffered SSE events, and each fallback including the new bridge-exception fallback. The existing OCR bridge tests still passFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/f57af0b79ee8472287d328e6dd984593
Requested by: @ishaan-berri