fix(proxy): stop treating upstream model body field as a LiteLLM model on auth-enforced pass-through routes - #33710
Conversation
…l on auth-enforced pass-through routes An auth: true user-defined pass-through endpoint runs full virtual-key auth, and get_model_from_request unconditionally extracted the request body model field, so key/team/user/project model allowlist checks rejected requests whose model only exists upstream (key_model_access_denied), even when the key was explicitly granted the route via allowed_passthrough_routes. The pass-through route registry moves to a leaf module (route_registry.py) that the auth layer can import without re-entering the pass_through_endpoints -> user_api_key_auth -> auth_utils import cycle. get_model_from_request now returns None for routes registered as user-defined pass-through endpoints (exact and subpath), which skips model allowlist and per-model budget enforcement on those routes while key auth, allowed_passthrough_routes, and spend/budget checks stay intact. Built-in provider passthrough routes (/vertex_ai, /gemini, ...) keep model enforcement. Resolves LIT-4299
|
|
Greptile SummaryThis PR fixes a bug where auth-enforced user-defined pass-through routes (
Confidence Score: 5/5Safe to merge. The fix is surgical and backward-compatible: new parameters all have None defaults, every existing call site is unaffected, and all other auth controls (key validity, allowed_passthrough_routes, model allowlist on managed routes, spend tracking) remain fully enforced. The change threads the existing request object into get_model_from_request and uses a function-level attribute marker — set at route-registration time by the server — to detect pass-through dispatch. The identity-check (is True, not truthiness) correctly handles Mock objects in tests. Keying off the FastAPI-resolved endpoint rather than the request path handles the path-collision edge case correctly. Tests cover the core behavior, the collision case, and callers with no request object, each verified by mutation. No logic is removed from any other auth gate. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/types/passthrough_endpoints/pass_through_endpoints.py | Adds the LITELLM_PASS_THROUGH_ENDPOINT_MARKER constant that bridges create_pass_through_route and auth logic; clean addition with no side-effects. |
| litellm/proxy/pass_through_endpoints/pass_through_endpoints.py | Stamps LITELLM_PASS_THROUGH_ENDPOINT_MARKER onto the returned endpoint function exactly once, after the closure is built, so the attribute persists across requests. |
| litellm/proxy/auth/auth_utils.py | Adds _request_dispatched_to_pass_through_endpoint with a strict is True identity check, and threads request through get_model_from_request with a safe None default for backward compatibility. |
| litellm/proxy/auth/auth_checks.py | Threads the existing request parameter into get_model_from_request; request was already present in common_checks so the change is a one-liner with no signature impact. |
| litellm/proxy/auth/user_api_key_auth.py | Threads request into _get_model_from_request_context so per-model budget checks also skip model extraction for pass-through dispatched requests. |
| tests/test_litellm/proxy/auth/test_auth_utils.py | Adds three focused unit tests covering: pass-through dispatch suppresses model, builtin dispatch enforces model, and no-request caller still extracts model. |
| tests/test_litellm/proxy/auth/test_auth_checks.py | Adds an async integration test that exercises common_checks end-to-end: pass-through dispatch accepts upstream-only model, builtin dispatch rejects the same model against the team allowlist. |
Reviews (2): Last reviewed commit: "fix(proxy): key pass-through model-acces..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Note on the red osv-scan check: it flags mcp 1.26.0 in uv.lock (GHSA-hvrp-rf83-w775, GHSA-jpw9-pfvf-9f58, GHSA-vj7q-gjh5-988w; fixed in 1.27.2/1.28.1). This PR does not touch uv.lock or pyproject.toml, and the same check fails identically on every currently open PR against litellm_internal_staging, so it is base-branch drift. The dependency bump belongs in its own PR on the base branch |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
…oint, not the request path Addresses a model-authorization bypass: the first version decided whether to skip model-allowlist extraction by matching the request path against the pass-through route registry. That ignored the HTTP method and, more importantly, whether the request was actually dispatched to a pass-through handler. A custom pass-through whose path collides with a built-in route (e.g. /v1/chat/completions, or an include_subpath prefix of one) still writes a registry entry even though FastAPI serves the built-in handler, so a normal request to that route had its model checks skipped and could reach a model outside the key/team/user/project allowlist. The skip is now keyed off the FastAPI-resolved endpoint. create_pass_through_route tags its handler with LITELLM_PASS_THROUGH_ENDPOINT_MARKER, and get_model_from_request returns None only when request.scope["endpoint"] carries that marker. Because routing runs before auth dependencies, this reflects the handler that actually serves the request: on a collision the built-in handler is dispatched and carries no marker, so model enforcement stays on. This also removes the need for the separate route_registry module, so that extraction is reverted. Regression tests cover a pass-through-dispatched request (model suppressed), a built-in-dispatched request on the same path (model still enforced), and the no-request budget path. Resolves LIT-4299
|
@greptileai please review the current head 5296b05 |
|
CI note: the one red job besides osv-scan is |
adb1ffb
into
litellm_internal_staging
|
available to be tested in 1.94.0.rc1 |
Relevant issues
Linear ticket
Resolves LIT-4299
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy (config-registered pass-through endpoint with
auth: trueforwarding to the real OpenAI chat completions API, Postgres-backed proxy, real LLM calls costing real $). Config:Key minted with a model allowlist that does not contain the upstream model, plus access to the route:
Before (captured with this PR's model-extraction skip reverted, i.e. auth behavior identical to base 4d33964): the pass-through request is rejected because the upstream-only
modelbody field is checked against the key's LiteLLM model allowlist{ "error": { "message": "key not allowed to access model. This key can only access models=['gpt-5-mini']. Tried to access gpt-5-nano", "type": "key_model_access_denied", "param": "model", "code": "403" } }After (captured at 783454e): the same curl is forwarded to OpenAI and returns a real completion
{ "model": "gpt-5-nano-2025-08-07", "content": "PASSTHROUGH-OK", "usage_total_tokens": 284 }Every other auth control stays enforced (same fixed proxy, same run):
Independent e2e verification (Devin)
An independent Devin session verified the PR end to end on a fresh VM: two live proxies against the same Postgres and real OpenAI upstream, PR branch (783454e) on :4000 and the base merge-base (4d33964) on :4001, driven with one restricted virtual key. All contract items passed: the base control returned key_model_access_denied for gpt-5-nano on /my-custom-endpoint, the PR branch returned a real 200 completion for the identical request, and every negative control held (bogus key 401, key without allowed_passthrough_routes 403, model allowlist still enforced on /v1/chat/completions)
The recording was captured at 783454e. The follow-up commit 5296b05 rebuilt the internal mechanism (keying the skip off the dispatched FastAPI endpoint instead of the request path) to close a path-collision bypass; the observable behavior for every case shown here is identical, and the new collision case is covered by unit tests with mutation checks
Full terminal recording of the flow:
Base branch (pre-fix) denies the upstream-only model:
PR branch forwards the identical request and returns a real completion:
Negative controls (401 bogus key, 403 route, 403 model on managed route, 200 allowed model):
Full-resolution video: https://raw.githubusercontent.com/yassin-berriai/litellm-pr-media/main/pr-33710/rec.mp4
Type
🐛 Bug Fix
Changes
A user-defined pass-through endpoint configured with
auth: trueruns full virtual-key auth, andget_model_from_requestunconditionally extracted themodelfield from the request body, so the key/team/user/project model allowlist checks rejected any request whosemodelvalue only exists at the upstream (key_model_access_denied), even when the key was explicitly granted the route viaallowed_passthrough_routes. Pass-through bodies are forwarded verbatim to the configured target, so that field names an upstream model, not a LiteLLM-managed oneThe skip is keyed off the FastAPI-resolved endpoint, not the request path.
create_pass_through_routetags its handler with a marker attribute, andget_model_from_requestreturnsNoneonly whenrequest.scope["endpoint"]carries that marker. Because routing runs before auth dependencies, this reflects the handler that actually serves the request, so it is correct with respect to the HTTP method and to path collisions: a custom pass-through path that overlaps a built-in route (for example/v1/chat/completions, or aninclude_subpathprefix of one) resolves to the built-in handler, which carries no marker, so model enforcement stays on for that request. That single choke point coverscommon_checks, the key-levelcan_key_call_modelpath, and per-model budget checks, while key auth,allowed_passthrough_routesenforcement, and spend/budget tracking are untouched. Built-in provider passthrough routes (/vertex_ai,/gemini, ...) are separate handlers and keep model enforcementRegression tests:
get_model_from_requestreturnsNonewhen the request was dispatched to a pass-through handler, still extracts and enforces the model when the same path resolves to a built-in handler (the collision case), and still extracts the model on the no-request budget path (tests/test_litellm/proxy/auth/test_auth_utils.py);common_checksaccepts an upstream-onlymodelon a pass-through-dispatched request while still rejecting the same body when a built-in handler is dispatched (tests/test_litellm/proxy/auth/test_auth_checks.py). Each fails without the fix (verified by mutation)Final Attestation