Commit graph

38288 commits

Author SHA1 Message Date
yuneng-jiang
e4493edcdb
bump: version 1.84.3 → 1.84.4 (#29374)
* bump: version 1.84.3 → 1.84.4

* chore: update uv.lock for 1.84.4
2026-05-30 20:45:30 -07:00
yuneng-jiang
2e76be019a
Cherry-pick #29358 onto patch/v1.84.3 (#29363)
* fix(reset_budget): write only {spend, budget_reset_at} and stop pre-zeroing counter

ResetBudgetJob's batched update_data path shipped the full key/user/team
model on each reset. Prisma rejects object_permission_id and budget_limits
on the update input type, so any row carrying those fields detonated the
entire batch -- spend never reset, budget_reset_at never advanced. After
v1.84.0 started populating object_permission_id on UI-created keys, this
fires routinely.

_reset_budget_common also zeroed the cross-pod spend counter before the
DB write, so failed resets left enforcement reading 0 from the counter
while the DB still held the over-budget spend, admitting requests past
the cap until the counter naturally re-saturated from new reservations.

Switch the write to per-row narrow updates ({spend, budget_reset_at})
via db.batch_, and move the counter invalidation out of
_reset_budget_common so it only fires after the DB write commits. On
DB-write failure the counter is left untouched, enforcement continues
to block, and the next scheduler tick can retry without leaving a
bypass window.

Fixes #27730.

* fix(reset_budget): address Greptile review on #29358

- Strengthen the bypass-half regression test: replace the for-loop over
  call_args_list (vacuously true when empty) with assert_not_called(),
  so the test would actually flag a re-introduction of counter-zeroing
  via any code path.
- Add the same explanatory docstring on _write_user_reset_updates and
  _write_team_reset_updates that _write_key_reset_updates already has,
  so all three helpers point future maintainers at #27730.

* test(reset_budget): update test_proxy_budget_reset for new batch-write path

Same shape as the previous test_reset_budget_job.py update: keys/users/teams
now write through prisma.db.batch_().<table>.update, not update_data, so the
tests need a batcher mock and updated assertions. Adds:

- _wire_batcher_for_test helper that returns a list which accumulates per-row
  batch updates captured from prisma_client.db.batch_().
- _attrify helper that wraps dict fixtures so getattr(item, "token") works
  alongside the dict item-access the fake_reset_* mocks rely on. The new
  narrow-write helpers use getattr to pull out the row's id, and would
  silently skip plain dicts otherwise.
- Updates 3 partial_failure tests to assert against the batch-call list
  (rows by id, payload contains only {spend, budget_reset_at}) instead of
  update_data.assert_awaited_once + data_list inspection.
- Updates test_reset_budget_continues_other_categories_on_failure: only
  budget + enduser still flow through update_data; key/user/team go through
  the batch path now.
- Wires the batcher mock into 3 service_logger_*_success tests so commit()
  is actually awaitable and the success hook fires.

These tests were silently passing locally only because the editable install
in .venv pointed at the main repo, not the worktree — running pytest with
PYTHONPATH overridden to the worktree (matching CI) reproduces the failures.
2026-05-30 18:47:47 -07:00
Mateo Wang
a0bb8e6e51
Cherry-pick #29311 and #29343 onto patch/v1.84.3 (#29352)
* [internal copy of #29089] fix: duplicate claude code traces (#29311)

* refactor(proxy/auth): normalize Bearer prefix in safe-hash helper (#29343)

* refactor(proxy/auth): normalize Bearer prefix in safe-hash helper

UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading
"Bearer "/"bearer " prefix before its existing sk-/JWT classification, so
the helper produces the same hashed output regardless of whether the
caller stripped the Authorization header prefix or passed the header
value through unchanged.

* refactor(proxy/auth): make Bearer-prefix strip case-insensitive

Per RFC 7235 the HTTP authorization scheme token is case-insensitive.
Replace the two-prefix loop with a single case-insensitive check so the
helper normalizes "Bearer ", "bearer ", "BEARER ", and any mixed-case
variant before classifying the remainder as sk- or JWT. The contract
test gains coverage of "BEARER " and "BeArEr ".

* test(mcp): align auth-handler test expectations with safe-hash helper

The two MCP auth tests asserted that UserAPIKeyAuth(api_key="Bearer ...")
retained the raw header bytes on the api_key field. _safe_hash_litellm_api_key
now normalizes that input — stripping the Bearer prefix and hashing the
resulting sk- key — so the expectations move to the normalized form:
the bare token in the parametrize case, and hash_token("sk-...") in the
backward-compat assertion. This matches what the real auth flow produces
(the builder strips Bearer and the DB stores the hashed token), so the
mocks now line up with production rather than with the un-normalized
validator output.

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
2026-05-30 17:52:11 -07:00
yuneng-jiang
aab7ff639e
fix(docker): cherry-pick #28519 (restore npm to non_root builder) onto patch/v1.84.1 (#28973)
* fix(docker): restore npm to non_root builder image (#28519)

The non_root builder stage installs `nodejs` but not `npm`. Without `npm`
on PATH, prisma-python falls back to downloading a Node runtime via
nodeenv from nodejs.org, and that downloaded binary fails to load
`libatomic.so.1` — breaking `prisma generate` and the image build.

`npm` was dropped from this apk list in ca52e346b0. Restoring it lets
prisma-python use the system Node + npm, matching docker/Dockerfile
which already installs `npm` for the same reason.

* bump: version 1.84.2 → 1.84.3

* chore: uv lock after version bump 1.84.2 → 1.84.3
2026-05-27 09:46:39 -07:00
yuneng-jiang
5560f35279
chore(proxy): cherry-pick #28547 onto patch/v1.84.1 (#28967)
* chore(proxy): cherry-pick #28547 onto patch/v1.84.1

Backport of #28547 (`d480ffda3c`) onto the `patch/v1.84.1` branch.

Routes the remaining path-dependent call sites in auth, ACL, routing,
and audit-log decisions through `get_request_route(request)` so they
read from the ASGI `scope["path"]` instead of `request.url.path`. The
helper itself already exists on v1.84.1 (added by #27904 / #27878);
this PR extends the helper's usage to the additional sites listed
below.

Sites routed through get_request_route:
- _experimental/mcp_server/auth/user_api_key_auth_mcp.py
- management_endpoints/mcp_management_endpoints.py
- vector_store_endpoints/utils.py
- pass_through_endpoints/pass_through_endpoints.py
- auth/route_checks.py
- litellm_pre_call_utils.py
- spend_tracking/spend_management_endpoints.py
- common_utils/http_parsing_utils.py
- management_helpers/utils.py
- health_endpoints/_health_endpoints.py

Regression tests in tests/proxy_unit_tests/test_proxy_routes.py
construct a Request with scope["path"] set to a benign route and the
Host header crafted so url.path would resolve differently; each
site's decision is asserted against scope["path"].

Conflict resolution
-------------------

Two files conflicted because v1.84.1's base predates the
delegate_auth_to_upstream feature (#27834 — not on v1.84.1):

1. _experimental/mcp_server/auth/user_api_key_auth_mcp.py
   The cherry-pick brought in a `_target_servers_delegate_auth_to_upstream`
   elif branch in `process_mcp_request`. That branch is feature drift
   from #27834 and is irrelevant to the path-resolution change. Dropped
   the elif block; kept the get_request_route swap on the existing
   well-known/_target_servers_use_oauth2 call sites.

2. management_endpoints/mcp_management_endpoints.py
   The cherry-pick brought in the entire `_mcp_oauth_user_api_key_auth`
   function. That function does not exist on v1.84.1 (added by #27834);
   the #28547 change inside it is just a `request.url.path` →
   `get_request_route` swap. Dropped the function entirely.

The other 8 production files and the test file auto-merged cleanly
and contain only `request.url.path` → `get_request_route(request)`
swaps plus the lazy auth_utils import (no feature drift).

* bump: version 1.84.1 → 1.84.2

* chore: uv lock after version bump 1.84.1 → 1.84.2
2026-05-27 00:30:43 -07:00
Yuneng Jiang
6c9da6e799
chore: update uv.lock for 1.84.1 2026-05-20 19:01:25 -07:00
Yuneng Jiang
e9f5fc1dd8
bump: version 1.84.0 → 1.84.1 2026-05-20 19:00:45 -07:00
Sameer Kankute
5fb1f46183
fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns (#28324)
* fix(vertex_ai): omit function_call id on Vertex Gemini 3.5+ tool turns

Vertex AI rejects `id` on function_call/function_response parts; only Google AI Studio accepts it for Gemini 3.5+ strict tool matching.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(vertex_ai): forward custom_llm_provider in context caching

Pass custom_llm_provider through to _gemini_convert_messages_with_history
in the context caching path so Gemini 3.5+ tool-call `id` forwarding
behaves consistently between cached and non-cached completions on Google
AI Studio.

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
(cherry picked from commit fecf212d70)
2026-05-20 18:49:17 -07:00
milan-berri
4250713700
fix(spend_counter): seed Redis counter via SET NX to prevent cross-pod double-seed (#27854)
* fix(spend_counter): seed Redis counter via SET NX to prevent cross-pod double-seed

Symptom
-------
Customers on multi-pod deployments see team `spend` jump to ~2x (or N x
the pod count) shortly after a Redis cache miss / TTL expiry, triggering
spurious "Budget Crossed" alerts and blocked requests until the value is
manually reset.

Root cause
----------
`SpendCounterReseed.coalesced` warmed the primary spend counter by
calling `redis.async_increment(key, value=db_spend, refresh_ttl=True)`,
which lowers to Redis `INCRBYFLOAT`. That is additive, not idempotent.

The per-counter `asyncio.Lock` only coalesces seeders inside one
process. With N pods sharing one Redis, on a cold key (cold start, TTL
expiry, manual delete) every pod independently passes its lock + Redis
re-check, reads the same `db_spend`, and issues `INCRBYFLOAT db_spend`.
Final value: N x db_spend.

Fix
---
Use `redis.async_set_cache(key, value=db_spend, nx=True)` for the seed.
SET NX is atomic across pods: exactly one writer initializes the key;
losers read the winner's value via `async_get_cache`. This is the same
idiom already used by `coalesced_window` in the same file, so the two
seed paths are now consistent.

Per-request deltas continue to use `INCRBYFLOAT` (correct - additive
behaviour is what we want for increments, not for initial seed).

Verification
------------
Live two-process repro against the same Postgres + Redis (DB
spend = 506):

  Unpatched: 4/4 runs -> Redis counter = ~1012  (~2 x db_spend)
  Patched:  12/12 runs -> Redis counter = ~506

Unit tests (`test_proxy_server.py`):

- New `test_primary_spend_counter_redis_concurrent_seed_does_not_double_seed`
  patches `_get_lock` to return a fresh lock per caller (otherwise the
  per-process lock masks the race), races two `coalesced` calls, and
  asserts final = 506 with exactly one of two SET NX attempts winning.
- 4 existing tests updated for the new seed contract (SET NX for the
  seed, INCRBYFLOAT only for the per-request delta).
- Full `spend_counter or reseed or budget` slice: 22 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(spend_counter): make SET NX mock atomic so loser branch is exercised

Greptile flagged that `redis_set_cache` in
test_primary_spend_counter_redis_concurrent_seed_does_not_double_seed
placed `await asyncio.sleep(0)` AFTER the NX membership check. Both
concurrent tasks observed an empty `redis_store`, passed the guard, and
both returned True - so the loser branch (else: read back winner's value)
was never exercised.

Fix the mock to model real atomic Redis SET NX:

- Yield BEFORE the membership check so two concurrent callers interleave
  the way real SET NX does (first to resume runs check + write atomically
  and wins; second resumes after the key exists and loses).
- Track set_cache return values; assert sorted([loser, winner]) so we
  know exactly one task wins and one loses.
- Track async_get_cache calls that happen AFTER at least one SET NX has
  completed; assert at least one such read - that is the loser-path
  fallback (`current_value = float(cached)` when seeded is False).

Verified by temporarily reverting the mock to the old order: the test
now fails with `expected exactly one SET NX winner and one loser, got
[True, True]`, exactly the failure mode Greptile described.

No production code change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(spend_counter): mock async_set_cache to populate redis_store in concurrent read+write test

`test_concurrent_read_and_write_paths_share_one_db_query` mocks
`async_increment` to populate the in-memory `redis_store`, but did not
mock `async_set_cache`. After the SET-NX seed change in `coalesced()`,
the seed step writes via `async_set_cache(nx=True)` (default AsyncMock,
no `redis_store` write), so the simulated Redis stays empty after the
first reseed. The second `get_current_spend` then sees a clean Redis
miss, re-enters the DB read path, and the test fails with
`expected 1 DB query, got 2`.

Fix: add a `redis_set_cache` side_effect that updates `redis_store` on
`nx=True` (and rejects when the key already exists), matching the
pattern used by the four sibling tests fixed in this branch's first
commit. Pre-existing assertions are unchanged.

Full `tests/test_litellm/proxy/test_proxy_server.py`: 158 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 0fb710400f)
2026-05-20 18:42:35 -07:00
Sameer Kankute
00f7361f11
Day 0 support : Gemini 3.5 Flash (#28268)
* Add day 0 support for gemini 3.5 flash

* Fix pricing

* Fix greptile review

* Fix failing test

* Fix tests

* Fix: revert tool removing logic

* fix greptile and test

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
(cherry picked from commit 3c3d131f01)
2026-05-20 18:42:35 -07:00
yuneng-jiang
e1fc955464
Merge pull request #27909 from BerriAI/backport/27908-litellm_1.84.0rc2
Some checks failed
Unit Tests: Caching (Redis) / caching-redis (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
chore: backport #27908 to litellm_1.84.0rc2
2026-05-13 22:12:10 -07:00
Yuneng Jiang
fcd63b0137
uv lock 2026-05-13 22:10:15 -07:00
Yuneng Jiang
0f741fc12d
bump: version 0.4.71 → 0.4.72 2026-05-13 22:10:15 -07:00
yuneng-jiang
321d5763e4
Merge pull request #27904 from BerriAI/backport/27878-litellm_1.84.0rc2
fix: backport #27878 to litellm_1.84.0rc2
2026-05-13 21:43:12 -07:00
Yuneng Jiang
fe186651fa Merge remote-tracking branch 'origin/litellm_1.84.0rc2' into backport/27878-litellm_1.84.0rc2
# Conflicts:
#	tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py
2026-05-13 21:39:17 -07:00
yuneng-jiang
087003ea04
Merge pull request #27903 from BerriAI/backport/27892-litellm_1.84.0rc2
fix: backport #27892 to litellm_1.84.0rc2
2026-05-13 21:36:33 -07:00
Krrish Dholakia
d35d2a70f4 fix: harden /key/update authorization checks (#27878)
* fix: patch Host-header auth bypass in get_request_route

Starlette reconstructs request.url from the Host header. A malformed
Host like `localhost/?x=1` causes Starlette to build the full URL as
`http://localhost/?x=1/health`, which url-parses to path="/". Since "/"
is in LiteLLMRoutes.public_routes, all protected routes became reachable
without authentication.

Fix: read scope["path"] (set by uvicorn from the HTTP request line,
not derivable from headers) instead of request.url.path. Sub-path
deployments are handled via scope["app_root_path"] / scope["root_path"],
mirroring Starlette's own base_url construction logic.

Affected variants confirmed fixed:
  Host: localhost/?x=1
  Host: localhost:4000/?x=1
  Host: localhost/#test
  Host: localhost:4000/#test

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* style: reduce comments in route fix

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block credential fields in RAG ingest vector_store options

Credential fields (vertex_credentials, aws_access_key_id, api_key, etc.)
in ingest_options.vector_store are now rejected at the API boundary with
a 400 error. Credentials must be configured server-side.

Previously any authenticated user could supply a vertex_credentials dict
with type=external_account pointing credential_source.file at an
arbitrary path (e.g. /proc/1/environ) and token_url at an
attacker-controlled server. google-auth's identity_pool.Credentials
refresh() would read the file and POST its contents to the attacker.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block /key/update self-escalation by assigned users

Non-admin users who were assigned a key (created_by != caller) could
update any non-budget field — models, rpm_limit, guardrails, etc. —
without admin authorization, allowing privilege self-escalation.

Gate: only the key creator (created_by == caller) may edit their own
key without admin check; budget changes always require admin regardless
of creator status. All other callers must pass _check_key_admin_access.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block user-controlled api_base in RAG ingest vector_store options

A user-supplied api_base in ingest_options.vector_store caused the server
to forward its configured provider credentials (Gemini, OpenAI) to an
attacker-controlled endpoint via SSRF.

Add api_base to the blocked credential params set alongside api_key and
the existing credential fields.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: restrict /utils/transform_request to PROXY_ADMIN and apply body safety check

Any authenticated internal_user could POST arbitrary provider config
(aws_sts_endpoint, api_base, etc.) to /utils/transform_request and have
the server forward its credentials to an attacker-controlled endpoint.

- Gate the endpoint on PROXY_ADMIN role (403 for all other roles)
- Call is_request_body_safe() to reject banned params even for admins
- Convert ValueError from safety check to HTTP 400

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: apply banned-param check to /utils/transform_request

Without is_request_body_safe(), any authenticated user could pass
aws_sts_endpoint, api_base, or aws_web_identity_token to
/utils/transform_request and have the server forward its configured
provider credentials to an attacker-controlled endpoint during SDK
credential resolution.

Applies the same banned-param blocklist already used by LLM endpoints.
Endpoint remains accessible to all authenticated users.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: block SSRF via api_base in /prompts/test dotprompt YAML frontmatter

Any frontmatter key not in ["model","input","output"] flowed into
optional_params and was merged into the LLM call data dict, bypassing
is_request_body_safe. An attacker with any bearer key could set
api_base in YAML to redirect the outbound LLM request — including the
provider API key — to an attacker-controlled host.

Fix: call is_request_body_safe on the constructed data dict after
optional_params are merged, before invoking ProxyBaseLLMRequestProcessing.
ValueError from the banned-param check is surfaced as HTTP 400.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update litellm/proxy/rag_endpoints/endpoints.py

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* fix: coerce nested config strings before banned-param check

_NESTED_CONFIG_KEYS descent used isinstance(nested, dict) which silently
skipped litellm_embedding_config when delivered as a JSON string via
multipart/form-data. Banned params (api_base, aws_sts_endpoint, etc.)
nested inside the stringified value were invisible to is_request_body_safe.

_NESTED_METADATA_KEYS already used _coerce_metadata_to_dict which parses
JSON strings before checking. Apply the same coercion to _NESTED_CONFIG_KEYS.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: replace substring match with prefix match in is_llm_api_route

mapped_pass_through_routes used `_llm_passthrough_route in route` (substring)
so any admin-only path whose URL contained a provider name (openai, anthropic,
azure, bedrock, etc.) was misclassified as an LLM API route and bypassed the
admin gate in non_proxy_admin_allowed_routes_check.

Confirmed live: non-admin key could GET /credentials/by_name/openai (read
masked provider API key) and DELETE /credentials/openai (delete credential).

Fix: use exact match or startswith(prefix + "/") — the same pattern used
everywhere else in RouteChecks — so only routes that actually start with a
passthrough prefix are allowed through.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: stabilize PR #27878 test failures

- key_management_endpoints: extend can_skip_admin_check to team keys so
  team members with /key/update permission can update non-budget fields.
  can_team_member_execute_key_management_endpoint already validates team
  membership + permission and raises if unauthorized; reaching the admin
  check on a team key means the caller was authorized.

- test: set created_by on mock key in
  test_update_key_non_budget_fields_allowed_for_internal_user so
  caller_is_creator resolves correctly (MagicMock default ≠ user_id).

- auth_utils.get_request_route: guard against non-dict request.scope
  (e.g. MagicMock in unit tests) to prevent a MagicMock leaking into
  UserAPIKeyAuth.request_route and failing Pydantic validation.

- ci: assign test_multipart_bypass_repro.py to the proxy-runtime shard
  in test-unit-proxy-db.yml to satisfy the shard-coverage check.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(lint): add explicit str() cast in get_request_route for MyPy

scope.get() returns Any|None which MyPy cannot coerce to str implicitly.
Wrap both scope.get() calls in str() to satisfy the type checker.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: guard bare-/ root_path strip + make total_spend migration idempotent

auth_utils.get_request_route: when Starlette sets scope["app_root_path"]
to "/" (e.g. behind some middleware), the old stripping logic would
remove the leading slash from every path ("/team/new" → "team/new"),
breaking route matching and causing auth to misclassify protected routes.
Skip stripping when root_path is bare "/".

migration: add IF NOT EXISTS to total_spend ALTER TABLE so the migration
is safe to replay when a prior partial run already created the column.
Without this guard, prisma migrate deploy fails on CI DBs that were
partially migrated, causing all subsequent DB operations (including
/team/new) to 500.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: require creator still owns key for personal-key bypass in /key/update

caller_is_creator now requires both created_by == caller AND user_id ==
caller. Previously checking only created_by let a demoted admin who
originally created a key for another user continue editing non-budget
fields on it after reassignment, bypassing _check_key_admin_access.

Adds regression test: creator whose key was reassigned is blocked (403).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: extract auth checks to fix PLR0915 + broaden max_budget assertion

internal_user_endpoints._update_single_user_helper exceeded 50 statements
(PLR0915). Extract authorization checks into _check_user_update_authz helper
to bring statement count under the limit.

test_validate_max_budget: assert "negative" (substring of both the local
"cannot be negative" and the CI "non-negative finite number" messages) so
the test is stable regardless of which exact wording the function uses.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
2026-05-13 21:33:14 -07:00
Krrish Dholakia
b2c93b15e7
fix: block SSRF fields in RAG ingest vector_store config
aws_sts_endpoint, aws_web_identity_token, and aws_bedrock_runtime_endpoint
in ingest_options.vector_store were passed directly to the Bedrock ingestion
class, which reads them into boto3 STS client construction. Any authenticated
caller could redirect AssumeRole calls to an attacker-controlled server,
leaking the proxy's instance profile credentials.

Calls is_request_body_safe() on ingest_options["vector_store"] before
forwarding to litellm.aingest(). Same banned-params list and admin opt-in
escape hatch (allow_client_side_credentials) as the /chat/completions path.
ValueError from the safety check is caught and re-raised as HTTP 400.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 21:29:26 -07:00
Krrish Dholakia
fdb55ab65b
fix: block client-side pricing injection via request body
Authenticated clients could supply CustomPricingLiteLLMParams fields
(input_cost_per_token, output_cost_per_token, etc.) in the request body.
These were forwarded to register_model() in main.py, permanently mutating
the shared global litellm.model_cost dict for all users on the instance.

Adds all CustomPricingLiteLLMParams fields to _BANNED_REQUEST_BODY_PARAMS
so is_request_body_safe() rejects them before they reach completion().
New pricing fields added to CustomPricingLiteLLMParams are auto-covered.

Admin opt-in via allow_client_side_credentials or
configurable_clientside_auth_params still works as before.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 21:29:26 -07:00
yuneng-jiang
08ea016d8f
Merge pull request #27902 from BerriAI/litellm_/eager-euler-fd3639
chore(proxy): backport #27898 + #27801 to 1.84.0rc2
2026-05-13 21:14:25 -07:00
yuneng-jiang
3dd763d710
Merge pull request #27801 from stuxf/chore/get-instance-fn-runtime-s3-gate
chore(proxy): refuse remote-URL instance-fn loads outside config-file path

(cherry picked from commit e3e5209f51)
2026-05-13 20:57:24 -07:00
yuneng-jiang
974f5cd04f
Merge pull request #27898 from stuxf/chore/banned-params-extra-body-cover
chore(proxy): cover extra_body + azure_ad_token in banned-params check

(cherry picked from commit a6a9d8edf0)
2026-05-13 20:57:19 -07:00
yuneng-jiang
50030e3ab7
Merge pull request #27901 from BerriAI/yj_build_may13
[Infra] Build UI
2026-05-13 20:53:27 -07:00
Yuneng Jiang
02c5e1c0ec
chore: update Next.js build artifacts (2026-05-14 03:52 UTC, node v20.20.2) 2026-05-13 20:52:07 -07:00
yuneng-jiang
305875f97d
Merge pull request #27899 from BerriAI/backport-pr-27896-into-1.84.0rc2
fix(ui): fetch version + debug flag from /health/readiness/details (backport #27896)
2026-05-13 20:50:27 -07:00
Yuneng Jiang
51ed234e36
fix(ui): disable retries on readiness/details + cover token forwarding
Two small follow-ups on the readiness/details migration:

- Set `retry: false` on the query. The payload feeds a passive
  navbar tag and a debug banner; a 401 from an expired token
  shouldn't fan out into three retries against the proxy.
- Add navbar specs that assert the `accessToken` prop is forwarded
  into the hook (matches the DebugWarningBanner spec). Without
  this, the navbar could silently regress to passing `undefined`
  and the existing tests wouldn't catch it.
2026-05-13 20:36:38 -07:00
Yuneng Jiang
ab0267d9fd
fix(ui): fetch version + debug flag from /health/readiness/details
The proxy moved `litellm_version`, `is_detailed_debug`, and other
diagnostic fields off the public `/health/readiness` payload behind
an auth-gated `/health/readiness/details` endpoint. The navbar
version tag and the detailed-debug-mode banner stopped working
because they were still reading those fields from the unauthed
response, which no longer contains them.

Replace `useHealthReadiness` with a `useHealthReadinessDetails`
hook that takes an `accessToken` argument and sends a Bearer header
to the auth-gated endpoint. The hook stays disabled while
`accessToken` is falsy, so the navbar can keep rendering on the
public model hub (where the token is null) without triggering an
auth redirect or a 401-loop.
2026-05-13 20:36:37 -07:00
yuneng-jiang
be0526bac7
Merge pull request #27868 from BerriAI/backport-pr-27866-into-1.84.0rc2
fix(proxy): expose db status on public /health/readiness (backport #27866)
2026-05-13 13:41:24 -07:00
yuneng-jiang
be37cf782c fix(proxy): expose db status on public /health/readiness
Backport of #27866 onto litellm_1.84.0rc2.

External readiness probes consumed the legacy detailed payload's `db`
field to drive alerting and pod-rotation decisions. Stripping the body
to {"status": "healthy"} broke those probes silently — the HTTP code
still flipped to 503, but probes checking body.db == "connected"
treated the response as healthy.

Add `db` back to the unauthenticated payload. The rest of the diagnostic
fields (litellm_version, callbacks, cache, log_level) stay behind
/health/readiness/details so the recon-leak gate from #26912 holds.
Values match the legacy contract: "connected", "disconnected",
"Not connected". The 503-on-DB-disconnect behavior from LIT-2607 is
preserved.
2026-05-13 13:18:54 -07:00
yuneng-jiang
2b189be7f1
Merge pull request #27819 from BerriAI/litellm_backport_regenerate_guard_84rc2
[Fix] Backport /key/regenerate ownership-rebind + premium-gate guards (#27793)
2026-05-12 23:36:57 -07:00
yuneng-jiang
8b90a547ff
Merge pull request #27818 from BerriAI/litellm_backport_27812_to_1840rc2
[Fix] Lazy feature loading under SERVER_ROOT_PATH returns 404 (backport of #27812)
2026-05-12 23:35:59 -07:00
Yuneng Jiang
069469c7c0
chore(proxy): backport /key/regenerate ownership-rebind + premium-gate guards (#27793)
Backport of #27793 onto litellm_1.84.0rc2.

A non-admin caller could rebind their own key's user_id via /key/regenerate.
_execute_virtual_key_regeneration had org/team guards but no user_id guard,
and prepare_key_update_data did not strip the field — it survived
model_dump(exclude_unset=True) into the Prisma update. On the next request,
_return_user_api_key_auth_obj resolved the rebound user_id against
litellm_usertable and returned PROXY_ADMIN whenever the target row's
user_role was admin.

/key/update had the equivalent guard inline at _validate_update_key_data;
extract it to a shared helper _validate_caller_can_change_key_ownership and
call from both /key/update and _execute_virtual_key_regeneration.

Also tighten the premium gate that allowed the master-key rotation branch to
skip the enterprise check. The previous predicate was a field-presence test,
not an identity check. Verify the caller actually holds the master key via
_is_master_key before allowing the non-premium path.

Block explicit-null user_id and empty-string user_id as removal attempts;
both 403-reject for non-admin callers.
2026-05-12 23:18:36 -07:00
Yuneng Jiang
c14cab9082
Cache normalized SERVER_ROOT_PATH at middleware init
SERVER_ROOT_PATH is a process-startup env var. Read it once in
__init__ instead of calling get_server_root_path() + rstrip on every
request that arrives before all lazy features have loaded.
2026-05-12 23:15:19 -07:00
Yuneng Jiang
2675b507c6
Strip SERVER_ROOT_PATH before lazy-feature prefix match
LazyFeatureMiddleware compared the raw scope path against registered
prefixes (e.g. /policies), so requests under a server root path like
/api/v1/policies/... never matched, the feature never loaded, and the
endpoint returned 404. Strip the configured root path before matching,
normalizing trailing slashes and enforcing a component boundary so
/api does not falsely match /apiv2.
2026-05-12 23:15:19 -07:00
yuneng-jiang
037d4c505b
Merge pull request #27794 from BerriAI/litellm_/fervent-archimedes-7e86be
Some checks are pending
Unit Tests: Caching (Redis) / caching-redis (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
cherry-pick: reject bare str at file-input sinks (#27762) onto litellm_1.84.0rc2
2026-05-12 17:45:17 -07:00
Krrish Dholakia
590bc834c5
chore: reject bare str at file-input sinks to prevent local-file read (#27762)
Cherry-pick of #27762 onto litellm_1.84.0rc2.

* chore: reject bare str at file-input sinks to prevent local-file read (#27667)
* fix: use os.PathLike in ocr sink and check truthy reasoningSummary for bridge
  - ocr/main.py: widen Path check to os.PathLike for consistency with other sinks
  - main.py: bridge condition checks truthiness of reasoning_summary, not just None
* fix: remove unused pathlib.Path import in ocr/main.py

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
Co-authored-by: stuxf <70670632+stuxf@users.noreply.github.com>
2026-05-12 17:30:03 -07:00
yuneng-jiang
74bb5253dc
Merge pull request #27789 from BerriAI/litellm_/wizardly-sammet-94baac
fix(proxy): always merge caller-supplied tags into request metadata
2026-05-12 16:47:25 -07:00
Yuneng Jiang
cd9d31211a
docs(proxy): refresh stale comments referencing removed tag strip
The tag-strip block was removed in the parent commit but two surrounding
comments still referenced "tags without opt-in" and "runs AFTER the
strip". Update them to describe the remaining user_api_key_* and
_pipeline_managed_guardrails strip that the snapshot/merge ordering
actually protects against.
2026-05-12 16:34:42 -07:00
Yuneng Jiang
111ee52f23
fix(proxy): always merge caller-supplied tags into request metadata
Caller-supplied tags (`x-litellm-tags` header, body `tags`, `metadata.tags`)
were silently dropped unless the key/team had
`metadata.allow_client_tags: true` set. Restore the documented behavior:
tags from the request always flow into `metadata.tags` and union with any
admin-configured static tags from key/team/project metadata.

Removes the `allow_client_tags` opt-in flag from the pre-call pipeline.
The flag was only ever read here; it has no schema or endpoint footprint,
so leftover values in existing key metadata are inert.

Test cleanup mirrors the simplification: drop the three tests that
verified the strip-when-not-opted-in path, drop the `allow_client_tags`
fixture lines from the merge/union tests.
2026-05-12 16:34:42 -07:00
yuneng-jiang
4046cb1690
Merge pull request #27768 from milan-berri/litellm_cherry-pick-27383-onto-1.84.0rc2
cherry-pick: OpenAPI MCP extra_headers (#27383) onto litellm_1.84.0rc2
2026-05-12 13:49:31 -07:00
Cursor Agent
a1d1906025
fix(mcp): static headers win over forwarded headers in OpenAPI MCP
Match the existing MCP invariant in merge_mcp_headers and the managed MCP
path: operator-configured static headers always override caller-forwarded
headers on name conflict, with case-insensitive comparison so different
casing cannot bypass the precedence. _request_auth_header (BYOK) still
overrides Authorization last.

Addresses Veria review on PR #27383.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-05-12 23:33:37 +03:00
Milan
c31ead87d7
refactor(mcp): access has_client_credentials on MCPServer directly
Greptile: getattr default was redundant; property exists on MCPServer and
mcp_server is non-None inside the extra_headers forwarding block.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 23:33:37 +03:00
Milan
38bc921dd2
fix(mcp): forward extra_headers for OpenAPI MCP tools
OpenAPI-generated tools only applied static closure headers and BYOK
Authorization via ContextVar. Copy MCPServer.extra_headers from the
incoming MCP request into _request_extra_headers (set in server.py before
local tool dispatch), merge in openapi_to_mcp_generator via a small helper.

OAuth2 M2M: do not forward caller Authorization from raw_headers (same rule
as _prepare_mcp_server_headers for managed MCP).

Adds TestRequestExtraHeaders and clarifies mcp_server_manager registration
comment.

Fixes #26794

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 23:33:35 +03:00
yuneng-jiang
188875b22b
Merge pull request #27554 from BerriAI/litellm_/charming-sammet-8d77c3
Some checks failed
Unit Tests: Caching (Redis) / caching-redis (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / schema-migration (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
build(packaging): raise jinja2 floor to 3.1.6 (rc2 backport of #27552)
2026-05-09 22:10:31 -07:00
Yuneng Jiang
a5cc4ffbbc
build(packaging): raise jinja2 floor to 3.1.6
Our `uv.lock` already resolves jinja2 to 3.1.6, so Docker / CI installs
get that version. The `pyproject.toml` floor was lagging at 3.1.0,
which means downstream consumers using `--resolution=lowest-direct` or
older constraint files can land on 3.1.0-3.1.5 instead of the version
we actually test against.

Aligns the declared floor with the resolved version so external
installers see the same baseline our test matrix exercises.

`uv lock` diff is metadata-only (no resolved-version drift).
2026-05-09 14:00:10 -07:00
yuneng-jiang
5bb28032f0
Merge pull request #27545 from BerriAI/litellm_/charming-sammet-8d77c3
Some checks are pending
Unit Tests: Caching (Redis) / caching-redis (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
build(packaging): relax core runtime pins to ranges (rc2 backport of #27241)
2026-05-09 13:48:54 -07:00
Yuneng Jiang
82c8cb8adb
build(packaging): relax core runtime pins to ranges
Backport of #27241 onto litellm_1.84.0rc2.

The 12 entries in `[project.dependencies]` were exact `==` pins, a side
effect of the Poetry -> uv migration. This forces every downstream
package that lists litellm as a dependency to downgrade common runtime
libraries (openai, pydantic, aiohttp, click, jsonschema, ...) to the
exact versions we ship.

Switch to lower-bounded ranges with upper bounds where the upstream
package is pre-1.0 or has a known breaking-major-version policy.
Reproducibility for our Docker proxy and CI continues to come from
`uv.lock`, which is regenerated here as a metadata-only diff.

Conflict resolution vs upstream merge:
- The upstream merge commit also surfaced unrelated context entries
  (nvidia-riva-client, soundfile/stt-nvidia-riva extra) that exist in
  staging but not in rc2. Those are not part of #27241's intent and
  were dropped from the resolution; the rc2 uv.lock keeps its existing
  entry set, only the 12 specifier strings changed.
- `uv lock --check` passes (392 packages resolved, no drift).
2026-05-09 12:12:12 -07:00
yuneng-jiang
18c14d9f4c
Merge pull request #27539 from BerriAI/litellm_/budget-reservation-rc2-backport
fix(proxy): bound budget reservation per request (backport of #27509 to 1.84.0rc2)
2026-05-09 10:49:23 -07:00
Yuneng Jiang
d50c98d2fd
fix(proxy): gate image-gen reservation strictly on model mode
The previous detection treated any model with input_cost_per_image
or output_cost_per_image as image generation. Several chat and
embedding models carry those fields to price multimodal vision input,
not generated images:

- gemini-3.1-pro-preview (mode=chat) has output_cost_per_image=0.00012
  alongside input/output token pricing.
- azure/gpt-realtime-* (mode=chat) has input_cost_per_image=5e-6.
- amazon.titan-embed-image-v1 (mode=embedding) has
  input_cost_per_image=6e-5.

For these models the image-gen branch fired first and reserved a
fraction of a cent per request, short-circuiting the token-priced
path entirely. Long Gemini chats reserved 1 × $0.00012 instead of
the true token cost.

Gate strictly on mode in {"image_generation", "image_edit"}. All 197
real image_generation entries and all 31 image_edit entries
(Flux Kontext, Stability inpaint/outpaint, etc.) carry the right mode,
so the field-presence fallback was unnecessary.

Adds regression tests for the chat-model-with-image-cost-field case
and for image_edit reservation.
2026-05-09 10:29:55 -07:00
Yuneng Jiang
b3470154d4
fix(proxy): reserve per-image cost for image-generation requests
Image-generation routes (dall-e-3, flux, etc.) have no per-token output
cost so they fell through to the no-reservation read-time-only path.
Concurrent image requests against a depleted budget could all pass
common_checks (counter exactly at max_budget passes the strict-`>`
gate) and reach the provider before reconciliation caught up.

Add per-image reservation in _estimate_request_max_cost_for_model:
when the model has a per-image cost field, reserve `n × cost_per_image`
upfront. The atomic counter increment serializes concurrent admissions,
so the second request sees the post-first-reservation counter and
raises BudgetExceededError instead of silently leaking through.

Both `output_cost_per_image` and `input_cost_per_image` are honored —
naming is inconsistent across providers (OpenAI dall-e-3 uses
input_cost_per_image, aiml/dall-e-3 uses output_cost_per_image for
the same per-generated-image price).

Per-pixel pricing (DALL-E 2 size variants) and TTS/STT routes still
fall through to read-time enforcement; those are follow-ups.
2026-05-09 10:29:55 -07:00