Commit graph

49299 commits

Author SHA1 Message Date
ryan-crabbe-berri
43a02e2dbc fix(proxy): report the billed token rates in /cost/estimate
The rate fields reported base cost-map prices while the cost lines were billed at the token tier, off-peak window and regional multipliers the calculator picks for the request, so a line did not always equal tokens times its reported rate. get_billed_token_rates now resolves the rates once, the token-type breakdown and the endpoint both read from it, and a tiered-model test asserts every line equals its token count times the rate reported next to it

Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb
2026-09-07 16:53:41 -07:00
mateo-berri
5c23d296d3 fix(azure_ai): cost OCR relays per page so non-chat relays debit budgets 2026-09-07 16:51:56 -07:00
mateo-berri
614b151365 chore: merge origin/litellm_internal_staging into litellm_lit_7022_azure_ai_passthrough_config 2026-09-07 16:43:12 -07:00
mateo-berri
e01bb98960 merge: bring litellm_internal_staging into litellm_fix_agent_mcp_grants again
Staging moved by the auto-router classifier cost change (#40168) between the
first merge and its push; this merge picks it up so the PR merges cleanly
2026-09-07 16:36:55 -07:00
Yuneng Jiang
4ab5719ff9
test(batches): use immutable expectations with explicit test doubles 2026-09-07 16:36:27 -07:00
mateo-berri
5e4dec4b88 merge: bring litellm_internal_staging into litellm_fix_agent_mcp_grants
Take staging's test_bedrock_knowledgebase_hook.py, which drops the duplicate
embedding_executor parameter that turned the lint check red, and make the two
cross-module helpers this branch added public (raise_denied_scoped_mcp_access
and routes_through_gateway) so the private-usage budget stays at its base count
2026-09-07 16:35:40 -07:00
tin-berri
9d0c9b9382
feat(ui): itemize auto-router classification spend (#40168)
Resolves LIT-7141

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-07 16:29:19 -07:00
Yuneng Jiang
7bff9bf9a2
fix(batches): handle provider cancellation and file cleanup gaps 2026-09-07 16:22:32 -07:00
mateo-berri
b827375e60 Merge branch 'litellm_internal_staging' into litellm_lit_4116_drop_params_string_coerce 2026-09-07 16:22:26 -07:00
mateo-berri
1a9611d1a0 Merge branch 'litellm_internal_staging' into litellm_lit_4116_drop_params_string_coerce
Resolve the conflicts in utils.py, types/router.py, and the tests, and collapse the 56 per-provider isinstance(drop_params, bool) gates to bool(drop_params) now that get_optional_params normalizes the flag once at the top
2026-09-07 16:20:36 -07:00
Mateo Wang
bb8fe4a32f
Merge pull request #39826 from BerriAI/litellm_lit_6348_fireworks_responses_api
feat(fireworks_ai): add native Responses API config
2026-09-07 16:15:58 -07:00
tin-berri
cd681a573f
fix(mcp): encrypt stored static headers and stdio environment (#40164)
Encrypt secret maps at the shared persistence boundary, preserve plaintext API/runtime views, and extend rotation and migration scanning to legacy rows.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-07 16:03:06 -07:00
ryan-crabbe-berri
c84131b81a feat(proxy): price cache and reasoning tokens in /cost/estimate
POST /cost/estimate now accepts cache_read_input_tokens,
cache_creation_input_tokens and reasoning_tokens, bills them at the
model's cache and reasoning rates, and reports each share per request,
per day and per month next to the rates it used.

Custom-priced deployments also get cache and reasoning lines in the cost
breakdown now, so the estimate and the spend logs reconcile with their
totals instead of showing zero for those tokens.

Requested by a customer (Pylon #7365).

Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb
2026-09-07 16:00:20 -07:00
Yuneng Jiang
e03c7ccbab
ci: remove main branch source guard 2026-09-07 15:58:25 -07:00
yuneng-jiang
8af624b5f5
Merge pull request #40162 from BerriAI/litellm_default_branch_tooling
ci: follow the default branch in development tooling
2026-09-07 15:56:30 -07:00
mateo-berri
18aa52d5e1 chore: merge litellm_internal_staging into litellm_lit_6348_fireworks_responses_api 2026-09-07 15:43:08 -07:00
yuneng-jiang
1646901170
Merge pull request #40041 from BerriAI/litellm_presidio_ui_user_story_e2e
test(e2e/ui): automate the RC checklist's Presidio guardrail walk
2026-09-07 15:41:33 -07:00
yuneng-jiang
41e4f1a8c8
Merge pull request #40038 from BerriAI/litellm_/guardrail-automation-testing-3ecd3d
test(guardrails): pin the presidio spend-log record and the UI's masked-entity persistence
2026-09-07 15:39:51 -07:00
Yuneng Jiang
15ba07f20d
ci: avoid duplicate default branch fetches 2026-09-07 15:28:01 -07:00
ryan-crabbe-berri
38683643e0
Merge pull request #40170 from BerriAI/litellm_chatgpt_add_model_provider
feat(ui): list the ChatGPT subscription provider in the Add Model form
2026-09-07 15:21:58 -07:00
Mateo Wang
058d260509
Merge pull request #38914 from BerriAI/litellm_fix_skills_hook_import_side_effect
fix(proxy): register SkillsInjectionHook at proxy startup instead of import time
2026-09-07 15:18:54 -07:00
Mateo Wang
0e9e2c01f3
Merge pull request #38806 from BerriAI/litellm_fix_mcp_test_connection_oauth_bearer
fix(mcp): forward staged credentials on /mcp-rest/test/connection like /test/tools/list
2026-09-07 15:18:35 -07:00
Mateo Wang
d0dd3ce2d8
Merge pull request #38869 from BerriAI/litellm_fix_guardrail_route_call_types
fix(guardrails): resolve generateContent routes and async-first passthrough call types
2026-09-07 15:15:51 -07:00
Mateo Wang
9eaf15bcf9
Merge pull request #38842 from BerriAI/litellm_fix_responses_reasoning_drop_params
fix(responses): drop unsupported reasoning param for openai non-reasoning models
2026-09-07 15:14:13 -07:00
ryan-crabbe-berri
bb2db2d3f8 test(proxy): drop docstrings from the Add Model provider tests
Move the only guidance worth keeping into the parity assertion message so a
failing run tells the contributor to add a catalog entry instead of growing
the frozen unlisted set.

Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb
2026-09-07 15:12:41 -07:00
ryan-crabbe-berri
8e5a12057a feat(ui): list the ChatGPT subscription provider in the Add Model form
The Add Model provider dropdown is driven entirely by provider_create_fields.json,
and chatgpt had no entry there, so the documented ChatGPT subscription setup was
unreachable from the Admin UI. Add the entry plus the dashboard enum, slug, logo and
placeholder mappings so the provider can be selected and its cost-map models listed.

The entry carries no credential fields on purpose: the chatgpt backend ignores
api_key and api_base and signs in through the device-code auth file on the proxy
host, so any field here would be inert.

Add a parity test that every LlmProviders value is either listed for Add Model or
frozen in an explicit unlisted set, so a new backend provider cannot silently miss
the dropdown again.

Claude-Session: https://claude.ai/code/session_011Tn3657NkV6ojLqewL64Kb
2026-09-07 14:55:43 -07:00
tin-berri
c5ec2eedc1
fix(spend): price caching savings on the billed request basis (#40160)
Resolves LIT-7137

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-07 21:41:05 +00:00
Yuneng Jiang
a096dd615c
refactor(e2e): use immutable batch cleanup test expectations 2026-09-07 14:39:25 -07:00
Yuneng Jiang
e238d20fbd
ci: follow the default branch in development tooling 2026-09-07 14:34:45 -07:00
devin-ai-integration[bot]
038025ba5e
fix(guardrails): accept on_violation block and alert for mcp_security (#40155)
* fix(guardrails): accept on_violation block and alert for mcp_security

The MCP Security policy template sends on_violation: "block", but the shared
LitellmParams model only allowed the /v1/realtime values "warn" and
"end_session", so POST /guardrails returned 422 before the MCP guardrail was
initialized. Widen the literal to include the MCP actions, map every non-alert
value to MCP's default "block" at init, and regenerate the lazy OpenAPI
snapshot and dashboard API types

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(guardrails): restrict on_violation block/alert to mcp_security and keep legacy MCP mapping

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(guardrails): return 422 when PATCH sets an mcp_security-only on_violation on another guardrail

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 14:33:02 -07:00
Jean-Rémi Larcelet-Prost
f896df1b06
docs: fix stale file paths in ARCHITECTURE.md (#40157)
Several file references under proxy/management_helpers/ and other
paths no longer exist; the code moved to proxy/common_utils/,
proxy/db/db_transaction_queue/, litellm_enterprise/proxy/common_utils/,
proxy/hooks/litellm_skills/, and litellm_core_utils/.
2026-09-07 14:19:46 -07:00
tin-berri
7c1745cc71
feat(ui): make automatic auto-router setup discoverable and show what it configured (#40146)
* fix(ui): expand Detailed Configuration after automatic auto-router setup

* feat(ui): promote automatic auto-router setup to a callout banner

* test(ui): read tier chips through testing-library queries to stay in lint budget

* style(ui): drop explanatory comments per repo convention

* test(ui): reject unexpected automatic tier models
2026-09-07 13:55:27 -07:00
devin-ai-integration[bot]
b618c7ad86
fix(proxy): let authorized internal users open vector store details (#40150)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 13:10:09 -07:00
yuneng-jiang
6be7a1cbe0
Merge pull request #40149 from BerriAI/litellm_prime_main_gha
ci: prepare workflows for main default branch
2026-09-07 13:00:47 -07:00
yucheng-berri
e04e5d7113
fix(router): keep provider response headers on streaming chat completions (#40091)
* fix(router): keep provider response headers on streaming chat completions

The Router re-wraps a deployment's CustomStreamWrapper in FallbackStreamWrapper
(and its sync twin) so a mid-stream failure can fail over. Neither wrapper
forwarded `_response_headers`, so every streaming chat completion handed the
proxy's callbacks and its response-header builder a wrapper with no provider
headers, and a successful mid-stream fallback still published the failed
deployment's identity, `x-request-id` and rate limit counters.

Forward `_response_headers` into both wrappers, repoint the wrapper at the
deployment that served the stream once a fallback takes over, and rebuild the
proxy's response headers from that deployment while `create_response` still has
the first chunk buffered.

* fix(router): follow a nested fallback to the deployment that served the stream

A fallback the router picks is itself a fallback-aware wrapper, and it only
repoints at its own fallback once it yields, so reading its hidden params at
selection time named a deployment that produced no output. Re-read them when
the first fallback item arrives, which is still before the proxy commits
response headers.

Also addresses review feedback: the streaming header builder reads self.data
instead of taking a coarse request_data parameter, and the new test recorder
local is Final.

* test(router): cover the fallback header adoption helper directly

The router_code_coverage gate wants every router.py function named in a
router test, and this also pins the weak-reference behavior: a wrapper
collected mid-stream must not break the generator still draining it.

* refactor(proxy): take a read-only mapping for the model-id lookup

_get_model_id_from_response only reads its request payload, so a Mapping
says what it needs and the two metadata hops are narrowed instead of
assumed to be dicts.

* test: drop mutable recorder locals and routine comments from the new tests

An AsyncMock await_count and an asyncio.Event say the same thing as a
list and a dict that the test mutates.

* chore(router): justify the two rebinds in the fallback loops

Both are the one-shot re-read that follows a nested fallback, so they get
the repo's rebind-ok note like the rest of the file.
2026-09-07 12:59:57 -07:00
ryan-crabbe-berri
11702ae4e1
Merge pull request #38204 from davida-ps/litellm_prompt_security_file_modify_block
fix(guardrails): block Prompt Security file modifications
2026-09-07 12:53:57 -07:00
Yuneng Jiang
edeb93e727
ci: prepare workflows for main default branch 2026-09-07 12:50:17 -07:00
Yuneng Jiang
6bdf206cc5
fix(e2e): accept managed file deletion responses 2026-09-07 12:48:07 -07:00
devin-ai-integration[bot]
6908318c16
feat(keys): allow editing soft budget on existing keys (#39002)
* feat(keys): allow editing soft budget on existing keys

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(ui): extract KeyBudgetNumberField to keep key_edit_view under max-lines

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(ui): format keyEditFormValues with prettier

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(keys): cover soft budget validation and update adapter

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): reject non-finite soft budget values instead of clearing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(keys): assert soft budget validation returns None for valid values

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(keys): write soft budget and key row in one transaction

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yassin <yassin@berri.ai>
2026-09-07 19:37:44 +00:00
devin-ai-integration[bot]
e11a8c59ff
fix(ui): show inherited MCP servers on the internal user editor and flag access groups with no members (#40036)
* fix(ui): show inherited MCP servers on the internal-user editor and flag access groups with no members

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): consult the unfiltered access group registry before calling a group empty

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 19:34:43 +00:00
Yassin Kortam
192e38fa7b
feat(skills): semantic search over the LiteLLM-hosted skill registry (#39401)
* feat(skills): semantic search over the LiteLLM-hosted skill registry

Adds GET /v1/skills?query= (custom_llm_provider=litellm_proxy) and a
skill_search MCP virtual tool, ranking the caller's accessible skills by
semantic similarity, mirroring the A2A agent registry search (LIT-6309).

Also fixes a pre-existing bug where create_skill() dropped description and
instructions for the litellm_proxy provider, which left every LiteLLM-hosted
skill with no searchable text.

* fix(mcp): coerce skill_search top_k instead of raising 500 on malformed input

The MCP-REST skill_search dispatch validated raw tool arguments through a
pydantic model directly, so a non-numeric top_k raised a ValidationError
that the endpoint's catch-all turned into an HTTP 500. Mirrors the
agent_search branch's tolerant coerce_top_k handling instead.

* fix(skills): enforce key limits on search embeddings and bound the semantic index

Semantic search embeddings now run the same pre_call_hook the /embeddings
route runs, so key rate limits, budgets and guardrails apply before the
embedding model is called. The shared SemanticTextIndex caps cached vectors
and evicts the least recently searched entries, and each skill's embedded
text is capped so one skill cannot inflate the embedding batch

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(skills): surface proxy 429s from search embeddings instead of a 503

ProxyRateLimitError is also an OpenAIError, so the search engine was folding
a key rate limit into skill_search_unavailable. Proxy HTTPExceptions now
propagate so the caller gets the same 429 the /embeddings route returns

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(skills): import assert_never from typing_extensions for Python 3.10

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(skills): embed the request as the pre-call hooks returned it, not the original text

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(skills): keep the litellm_proxy provider check for GET /v1/skills?query= inside llms/

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(skills): move the GET /v1/skills?query= endpoint tests under tests/test_litellm/proxy

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 12:28:38 -07:00
Yuneng Jiang
a56c60e892
fix(e2e): wait for managed batch cancellation before deleting inputs 2026-09-07 12:27:51 -07:00
devin-ai-integration[bot]
c949843157
fix(ui): send empty vector_stores when the last team vector store is removed (#40144)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 12:24:04 -07:00
devin-ai-integration[bot]
cc287a7d8f
fix(ui): hide the Create Vector Store flow from non proxy admins (#40148)
* fix(ui): hide the Create Vector Store flow from non proxy admins

The vector stores page rendered the Create Vector Store tab, the
+ Add Vector Store button and a GET /credentials call for every role,
while the proxy only lets proxy admins call POST /vector_store/new and
GET /credentials. Internal users landed on the create form and got an
Only proxy admin error toast. Gate all three on isProxyAdminRole and
default everyone else to the Manage tab, matching the Indexes tab and
the Add Model gating.

Resolves LIT-7131

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): exclude view-only admin sessions from the vector store create flow

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-07 12:23:47 -07:00
Yuneng Jiang
6c21be6194
fix(e2e): clean up batch files reliably and expire Azure inputs 2026-09-07 12:15:06 -07:00
tin-berri
eea4c860f7
feat(ui): add key-scoped auto-router usage tab (#39999)
* feat(ui): add key-scoped auto-router usage tab

GET /auto_router/benchmarks takes an optional api_key filter, applied in the
rollup aggregate on the primary key's leading column. Proxy admins get a
separate Auto-router usage tab on key detail pages with spend, baseline,
savings, tier routing, cache metrics and the existing router selector

* fix(ui): share key analytics date range
2026-09-07 12:12:22 -07:00
Mateo Wang
e0c5bb990a
Merge pull request #39835 from BerriAI/litellm_cost_map_guard
feat(ci): add the cost map guard check
2026-09-07 12:11:41 -07:00
Mateo Wang
2cfe106f9f
Merge pull request #39660 from BerriAI/litellm_bedrock_passthrough_model_access
fix(proxy): enforce key and team model access on Bedrock passthrough routes (internal copy of #34244)
2026-09-07 12:11:15 -07:00
tin-berri
1ae3216120
fix(router): preserve default heuristic updates (#40007) 2026-09-07 12:05:43 -07:00
Roman D
55fe4a7894
feat(proxy): resolve root_path per request from a configured prefix list (SERVER_ROOT_PATHS) (#35935)
* feat(proxy): resolve root_path per request from SERVER_ROOT_PATHS

One deployment can encode exactly one client-visible URL path prefix
today: SERVER_ROOT_PATH is a scalar stamped onto the app at startup, so
a pod fronting several ingress prefixes 404s every prefix but one before
any handler runs, and MCP OAuth discovery can emit only one prefix's
URLs (RFC 9728 section 3 exact-match fails for the rest).

Add an opt-in outermost ASGI middleware that matches the request path
against a configured prefix list (SERVER_ROOT_PATHS, comma-separated) on
a segment boundary and sets scope["root_path"] for that request only.
Everything downstream is stock Starlette: route matching strips
root_path so routes stay registered root-relative, and request.base_url
re-includes it, so the discovery documents' resource and the 401
challenges' resource_metadata land under the prefix the client actually
called — with no discovery-builder changes.

LazyFeatureMiddleware now strips the scope root_path (falling back to
the cached SERVER_ROOT_PATH scalar) before feature prefix matching, so
lazily-registered routers — the MCP OAuth discovery router among them —
load under per-request prefixes.

Follow-up to the routing discussion on #35226; composes with, but does
not depend on, #35576.

* fix(proxy): import Sequence from collections.abc (ruff UP035 strict-budget gate)

* review(greptile): trim implementation commentary; fixture-own MCP registry state in tests

Addresses both P2s from the first Greptile pass:
- per_request_root_path_middleware.py (and the related _lazy_features /
  proxy_server comments) cut down to the constraints the code cannot
  express, per repo comment guidance
- the new discovery tests no longer clear/repopulate the shared MCP
  registry inline; a fixture snapshots it, hands the test an empty
  registry, and restores it afterwards so no state leaks between cases

* fix(lint): mutable-ok marker on the prefix accumulator (LIT002 type-discipline gate)

* fix(proxy): tie 401 challenges and get_custom_url to the per-request root_path

The per-request root_path middleware sets scope["root_path"] to the
prefix the client actually called, but the OAuth 401 challenges
(raise_user_oauth_challenge / raise_token_exchange_challenge) still
built their resource_metadata from SERVER_ROOT_PATH. On a pod fronting
several prefixes, the challenge advertised a discovery URL under a
different prefix than the discovery document served — the two
disagreed on where the resource metadata lives, and a strict RFC 9728
client refused the challenge. Route the challenges through a small
ContextVar the middleware populates so they read the same effective
root_path Starlette resolves the request under.

The same accessor fixes get_custom_url: when a request lives under a
SERVER_ROOT_PATHS-matched prefix, request.base_url already carries it,
so appending the SERVER_ROOT_PATH scalar on top produced e.g.
/tenant-a/legacy/sso/callback — a path that does not exist. Reading
the per-request prefix instead (and relying on join_paths's tail-dedup)
keeps SSO login/callback URLs under one prefix — the one the request
actually arrived on.

Fallback: outside a request (module-load-time UI URL builders,
background tasks) the ContextVar is unset and the accessor reads
SERVER_ROOT_PATH, matching get_server_root_path() so scalar-only
deployments are byte-identical.

* fix(mcp): challenge URL under per-request prefix must route, and mock parity

Two follow-ups to the review fix that made the 401 challenge use the
per-request root_path:

1. oauth_protected_resource_path must pick the URL structure that
   actually routes for the mechanism in use:
   - The scalar SERVER_ROOT_PATH deployment registers the well-known
     routes with the prefix INSERTED (via well_known_root_suffix at
     import time), matching RFC 8414 §3. The challenge URL must use the
     same insertion or a client fetching it 404s.
   - The per-request SERVER_ROOT_PATHS deployment can't register routes
     per prefix; PerRequestRootPathMiddleware strips the prefix from
     scope["path"] and the router matches the un-inserted route. The
     URL must place the prefix BEFORE .well-known so the strip leaves a
     matching path.
   The previous fix used the insertion form for both, which 404'd the
   discovery fetch on the per-request path — the discovery doc and the
   challenge would then disagree on where the resource metadata lives,
   the very failure the review flagged. End-to-end verified: the URL
   the challenge advertises routes and the doc's `resource` field
   equals the URL the client originally called (RFC 9728 §3).

2. get_request_root_path now delegates its fallback through
   get_server_root_path() instead of reading the env directly, so every
   existing `monkeypatch.setattr("litellm.proxy.utils.get_server_root_path"`
   test override keeps working. This unstubbed the mock on the /v2/login
   test that failed on the last CI run.

Plus the lint budget: annotate the local accumulator Final, tag the
scope["root_path"] rewrite as an intentional ASGI-contract mutation,
tag the reused `path`/`root_path` rebinds in LazyFeatureMiddleware, and
add reason strings to the two new PLC0415 lazy-import noqas.

* test(mcp): pin the reviewer's expected end-state — challenge URL routes, resource matches called URL

End-to-end regression test that mounts the discoverable router + the
per-request root_path middleware, hits an MCP endpoint that raises
raise_user_oauth_challenge, fetches the resource_metadata URL the
challenge advertises, and checks the returned document's `resource`
equals the URL the client originally called (RFC 9728 §3 exact match).

Covers /tenant-a, /tenant-b, and the unprefixed path on the same app so
a regression on any prefix — challenge URL 404s, or doc emits a
different prefix than the client called — fails at this test rather
than in a strict MCP client's discovery.

---------

Co-authored-by: gym-cmd <186399764+gym-cmd@users.noreply.github.com>
2026-09-07 11:54:59 -07:00