Commit Graph
1949 Commits
Author SHA1 Message Date
Yuneng Jiang 9deefc0f76 fix: align MCP broker endpoint access controls with existing auth patterns 2026-04-20 16:52:59 -07:00
9aee0da7d8 fix: /health/readiness 503 loop when DB is unreachable (#26134)
* fix: /health/readiness returns 503 when DB is unreachable due to handle_db_exception re-raising

handle_db_exception() re-raises the Prisma exception inside _db_health_readiness_check's
except block, which propagates out to health_readiness() and gets wrapped in a 503.
The health endpoint never reached the reconnect path and the service never recovered.

Fix:
- Remove handle_db_exception() call from _db_health_readiness_check — that helper is
  for API request handlers (allow_requests_on_db_unavailable flag), not health checks
- Replace raw disconnect()+connect() with attempt_db_reconnect(), which uses the proper
  lock, cooldown, escalation, and heavy-reconnect (recreate_prisma_client) machinery

* test: update health readiness tests for handle_db_exception removal

- Remove tests that expected handle_db_exception to re-raise (old buggy behaviour)
- Remove tests asserting disconnect()/connect() calls (replaced by attempt_db_reconnect)
- Add regression tests covering the 503 loop fix:
  - transport errors never raise (ClientNotConnectedError, httpx.ConnectError, etc.)
  - reconnect success path returns 'connected'
  - reconnect failure path returns 'disconnected' without raising
  - non-transport errors return 'disconnected', skip reconnect

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
2026-04-20 15:29:43 -07:00
386f334fee Prompt Compression - add it to the proxy (#25729)
* refactor: new agentic loop event hook

simplifies how to create logic for tool based multi llm calls

* fix: compress - make it work on anthropic input as well

* fix(compress.py): working prompt compression for claude code

ensures claude code messages can run through proxy easily

* docs: add agentic loop hook guide

* docs: add agentic_loop_hook to sidebar

* fix: fix multiple arguments error

* fix: fix tool call loop for compression on streaming /v1/messages

* fix: fix linting errors

* fix: fix ci/cd errors

* feat(litellm_pre_call_utils.py): use claude code session for litellm session id

allows claude code logs to be stitched together, making it easy to know they were all part of the same conversation

* fix: suppress incorrect mypy warning rE: module

* revert: drop PR's changes to litellm/proxy/_experimental/out/

Restores the 34 HTML files under _experimental/out/ to their pre-PR
paths (X/index.html -> X.html). All renames are R100 (content
unchanged); no other files are touched.

* fix: address greptile review comments on PR #25729

- Skip ``kwargs["tools"] = []`` injection when compression is a no-op —
  Anthropic Messages rejects empty tool arrays on requests that did not
  originally declare tools.
- Move agentic-loop safety guards (fingerprint cycle / max depth) out of
  the per-callback try/except so they propagate instead of being swallowed
  by the generic exception handler. Extracted _check_agentic_loop_safety.
- Gate generic ``x-<vendor>-session-id`` capture behind the
  LITELLM_CAPTURE_VENDOR_SESSION_HEADERS env var (off by default) to
  preserve backwards compatibility; explicit x-litellm-* headers are
  unaffected.
- Fix monkeypatch target in pre-call-hook test to patch the actual
  module-level binding
  (litellm.integrations.compression_interception.handler.compress).
- Add regression tests for empty-tools skip and opt-in session capture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: drop LITELLM_CAPTURE_VENDOR_SESSION_HEADERS flag

Generic x-<vendor>-session-id header capture is a new feature and only
runs *after* the explicit x-litellm-trace-id / x-litellm-session-id
checks, so it does not change behavior for any existing caller that was
already using the LiteLLM headers — no backwards-incompatibility to gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(compress): replace input_type with CallTypes call_type

Drop the bespoke ``CompressionInputType`` literal and use the existing
``litellm.types.utils.CallTypes`` enum instead.  ``litellm.compress()``
now takes ``call_type: Union[CallTypes, str]`` (default
``CallTypes.completion``) — no new concept to learn, and the enum is
already the way the rest of the codebase talks about request shapes.

Supported values: ``completion`` / ``acompletion`` (OpenAI chat-completions
shape) and ``anthropic_messages`` (Anthropic structured content blocks).

Updated: compress(), the compression_interception handler, tests, docs,
and the two eval scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 15:08:00 -07:00
Ryan Crabbe c8b7c1bafa Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_feat-multi_threshold_budget_alerts 2026-04-18 15:26:02 -07:00
Ryan Crabbe eb6fd98611 Merge remote-tracking branch 'origin/main' into litellm_feat-multi_threshold_budget_alerts 2026-04-18 14:54:26 -07:00
ishaan-berriandGitHub ecff06df65 Merge pull request #26032 from BerriAI/litellm_mcp_pkce_fix_v2
fix(mcp): restore PKCE-triggering 401 when no stored per-user token exists
2026-04-18 14:52:31 -07:00
yuneng-jiangandGitHub e69051916e Merge pull request #25983 from BerriAI/litellm_yj_apr17
[Infra] Merge dev branch
2026-04-18 14:43:04 -07:00
Ishaan Jaffer b7813aad41 fix(mcp): restore PKCE-triggering 401 when no stored per-user token exists
Per-user OAuth MCP requests now only skip pre-emptive 401 when a stored token is available, preserving token-reuse behavior while restoring fast PKCE kickoff for first-time or missing-token users.
2026-04-18 14:04:22 -07:00
shivam dbf4f9637f Merge remote-tracking branch 'upstream/litellm_internal_staging' into litellm_project_rate_limiting 2026-04-18 13:26:24 -07:00
Yuneng Jiang f483f1e800 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_yj_apr17 2026-04-18 13:19:16 -07:00
Shivam RawatandGitHub f870095f3f Merge pull request #25991 from BerriAI/litellm_persist_default_router_end_budget
Litellm persist default router end budget.
2026-04-18 11:52:20 -07:00
ishaan-berriandGitHub d03c301c79 Merge pull request #25936 from BerriAI/litellm_health-check-reasoning-tokens
fix(proxy): prioritize reasoning health-check max token precedence
2026-04-18 11:35:04 -07:00
Yuneng Jiang 9c0b73e5f4 [Fix] should_create_missing_views returns False for reltuples=0 (falsy zero bug)
`should_create_missing_views()` had `and result[0]["reltuples"]` which is
falsy when reltuples=0. On a fresh empty PostgreSQL table, CREATE INDEX sets
reltuples=0, causing the guard to return False and skip view creation entirely.
Views like MonthlyGlobalSpendPerKey are never created, and the
/global/spend/logs endpoint returns 500.

Fix: change to `and result[0]["reltuples"] is not None` so reltuples=0
(empty table) and reltuples=-1 (unanalyzed table) both correctly return True.

Also harden test_vertex_ai.py to return None instead of crashing with
JSONDecodeError when the spend-logs endpoint returns a non-JSON 500 response,
and add unit tests covering all three reltuples branches (0, -1, positive).
2026-04-18 11:00:09 -07:00
Ryan Crabbe 48fb19b4fd fix: align test assertion with additive merge semantics
test_virtual_key_max_budget_alert_check_per_key_overrides_global asserted
override semantics but the implementation does additive merge. Renamed test
and updated assertion to match: per-key and global thresholds are unioned,
not replaced.
2026-04-17 23:46:40 -07:00
Yuneng Jiang e004876950 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/wonderful-bouman
# Conflicts:
#	tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py
2026-04-17 21:32:09 -07:00
shivamandClaude Sonnet 4.6 e8c33409ad test: add coverage for get_project_model_rpm/tpm_limit and check_complete_credentials
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 19:38:13 -07:00
shivam a449fc11a6 Merge branch 'litellm_internal_staging' into litellm_project_rate_limiting 2026-04-17 19:10:06 -07:00
shivam fad884e9a7 Merge branch 'litellm_internal_staging' into litellm_persist_default_router_end_budget 2026-04-17 19:06:32 -07:00
shivamandClaude Sonnet 4.6 6fd49f1da1 fix: enforce project-level model-specific rate limits in parallel_request_limiter_v3
Project-level model rpm/tpm limits stored in project_metadata were never
checked during rate limit enforcement — only model-level limits applied.

Adds _add_project_model_rate_limit_descriptor_from_metadata() to the v3
limiter (mirrors the existing team metadata path) and calls it in
async_pre_call_hook, creating a model_per_project descriptor keyed as
"{project_id}:{model}" with the project's configured limits.

Also extends get_model_rate_limit_from_metadata's Literal to accept
"project_metadata" and adds get_project_model_rpm/tpm_limit helpers.

Fixes: LIT-2317

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:17:24 -07:00
Ryan Crabbe 44eb2ea56e fix: address Greptile review — empty recipients guard, type annotation, task pre-filter
- Guard empty recipients in _handle_multi_threshold_max_budget_alert:
  log warning and skip instead of falling through to old path error loop
- Widen max_budget_alert_emails type to Dict[str, Union[str, List[str]]]
  to match _parse_email_list runtime behavior (accepts comma-separated strings)
- Pre-filter asyncio.create_task with min threshold check to avoid
  unnecessary task allocation on every request when spend is below
  all configured thresholds
2026-04-17 17:54:16 -07:00
bb9955beca [Fix] Budget reset job now resets implicitly-created end users with NULL budget_id
When litellm.max_end_user_budget_id is configured, implicitly-created end users
(via /chat/completions) have budget_id=NULL in the DB since the default budget
is only applied in-memory. The budget reset job filtered by budget_id, so these
users were never reset and eventually permanently blocked.

Fix: when the default budget is in the reset list, also query for and reset
end users with budget_id=NULL and spend > 0. This keeps the hot auth path
unchanged (no DB writes on every request).

Fixes #22019

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:49:33 -07:00
Ryan Crabbe 41a719a537 feat: add global fallback config, fix no-owner crash, improve email greeting
- Add `default_key_max_budget_alert_emails` litellm_settings config as
  global fallback for all virtual keys (per-key metadata takes priority)
- Fix crash when key has no user_id/user_email by passing recipient email
  to _get_email_params (same pattern as team soft budget path)
- Use owner email for greeting, falling back to key_alias or token
- Rename setting from default_max_budget_alert_emails to
  default_key_max_budget_alert_emails for clarity
2026-04-17 17:49:28 -07:00
Yuneng Jiang 11c3270cdc Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_yj_apr17
# Conflicts:
#	litellm/__init__.py
2026-04-17 17:36:40 -07:00
yuneng-jiangandGitHub b9f5be8956 Merge pull request #25922 from BerriAI/litellm_a2a_agent_acl
[Fix] Agent endpoint and routing permission checks
2026-04-17 17:19:59 -07:00
Ryan Crabbe 779a9fab8e feat: add configurable multi-threshold budget alerts for virtual keys
Users can set metadata.max_budget_alert_emails as a JSON map of threshold
percentages to email recipients on virtual keys. When configured, the email
handler loops over each threshold, checks per-threshold dedup cache, and
sends to the configured recipients (auto-including the key owner's email).

When no map is set, the existing single 80% threshold behavior is preserved
unchanged. Teams support is out of scope for this v0.
2026-04-17 17:12:35 -07:00
ishaan-berriandGitHub 1c128a86b8 Merge pull request #25256 from BerriAI/litellm_ishaan_april6
Litellm ishaan april6
2026-04-17 16:26:45 -07:00
Yuneng Jiang ee2cf0e6e8 fix: address three CI failures from recent security PR merges
- url_utils.py: narrow sockaddr[0] from str|int to str via a helper with a
  fail-closed isinstance check. Fixes the two mypy errors introduced by
  the SSRF hardening without masking unexpected stdlib behavior.

- key_management_endpoints.py: restore the documented team member_permissions
  path for /key/update. The cross-key admin check added to close the
  cross-org rewrite attack was over-broad: it rejected non-admin team
  members even when can_team_member_execute_key_management_endpoint had
  already validated their team membership and /key/update grant. Now skip
  the admin check when the key has a team_id and the change is non-budget
  (membership + permission already enforced above). Budget/spend changes
  still require team/org admin. The cross-org attack remains blocked:
  an outside org admin fails the earlier team membership check.

- test_logging_redaction_e2e_test.py: rename and rewrite two parametrized
  tests to assert that request-body turn_off_message_logging has no effect.
  Reflects the intentional removal of turn_off_message_logging from
  _supported_callback_params so the caller cannot override admin logging
  policy via the request body.

- test_key_management_endpoints.py: add two tests covering the restored
  team member permission path — one positive (non-budget update succeeds
  for a team member with /key/update grant), one negative (max_budget
  change still rejected without admin role).
2026-04-17 15:11:45 -07:00
Yuneng Jiang 9a8aa4fae4 [Feature] UI - Models: add api_base field for Anthropic provider form 2026-04-17 13:32:17 -07:00
Yuneng Jiang ad48af69aa [Feature] UI - Models: allow empty api_key for Anthropic provider (BYOK) 2026-04-17 13:32:17 -07:00
Yuneng Jiang d0f1706479 [Test] pre-call utils: regression coverage for BYOK x-api-key forwarding 2026-04-17 13:32:17 -07:00
Yuneng Jiang 66167f532e [Feature] UI - Settings: add forward_llm_provider_auth_headers toggle 2026-04-17 13:32:17 -07:00
Ishaan Jaffer e8461b5b97 style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
Ishaan Jaffer f31d4faa87 Merge origin/main into litellm_ishaan_april6 2026-04-17 12:36:51 -07:00
yuneng-jiangandGitHub 32714a4dc2 Merge pull request #25905 from stuxf/fix/metadata-security-controls
fix(proxy): read guardrail config from admin metadata, fix tag routing consistency
2026-04-17 12:16:28 -07:00
Sameer Kankute 76c69bb933 fix(proxy): avoid duplicate reasoning capability lookup
Compute supports_reasoning once per non-wildcard health-check resolution path and update the stale default-max-tokens test docstring.

Made-with: Cursor
2026-04-17 12:49:31 +05:30
Sameer Kankute d86c6a5b2f fix(proxy): prioritize reasoning health check token defaults
Apply reasoning-first precedence for background health-check max tokens, parse reasoning env as optional, and raise non-wildcard fallback max_tokens from 1 to 5 for better reliability.

Made-with: Cursor
2026-04-17 12:36:58 +05:30
user c7c3df2b02 fix(proxy): extend /key/update admin check to non-budget fields
Audit-B #2. _check_key_admin_access was gated on max_budget/spend
changes only, which meant a non-admin caller could blanket-rewrite
any OTHER field on any key (key_alias, models, tpm_limit, rpm_limit,
metadata, tags, allowed_routes, guardrails, blocked, duration,
permissions, auto_rotate, access_group_ids, object_permission, …)
as long as they avoided budget/spend. Example attack:

  POST /key/update {
    key: sk-victim-in-org-B,
    models: [],
    blocked: true,
    organization_id: org-A,
  }

The caller is org-admin of org-A, which satisfies the route gate;
the handler then wipes models and blocks the victim's key.

Policy after this fix:
- PROXY_ADMIN: always allowed.
- Key OWNER (matching user_id): allowed for non-budget fields;
  budget/spend changes still require team/org admin.
- Everyone else: must pass _check_key_admin_access (PROXY_ADMIN /
  key-owner / team-admin / org-admin of the key).

Regression test confirms a non-owner INTERNAL_USER cannot rewrite
key_alias/blocked on someone else's key; existing test covers the
owner-can-update-alias case; existing test covers
internal-user-cannot-modify-max-budget.
2026-04-17 00:41:00 +00:00
user 662d05531d fix(proxy): close three more org-boundary escape paths
Continuation of Veria E3NpkuAd / Audit-B hardening. All three are the
same anti-pattern PR #25904 already addressed for _user_is_org_admin
and /user/delete: route-level gate trusts a caller-supplied scope
field, handler operates on a different scope.

1. /user/update no longer silently creates a user when the target
   email doesn't exist. Pre-fix, an org admin could supply a fresh
   email + caller-chosen budget/models/metadata; the INSERT path
   created the user with no org attachment, bypassing /user/new's
   org/team authorization. Now require PROXY_ADMIN for the create
   branch; return 404 otherwise. Also fixes /user/bulk_update because
   it dispatches through the same _update_single_user_helper.

2. /team/bulk_member_add with all_users=true restricted to PROXY_ADMIN.
   The flag pulls every user in the database into the target team,
   ignoring org scope — any team admin could use it to capture every
   user across every org into their team.

3. /team/update now verifies destination-org admin rights. When the
   request carries an organization_id that differs from the team's
   current org, an org admin of the caller's current org could
   previously relocate the team into any other org (draining their
   resources, or capturing a team they once administered). Require
   PROXY_ADMIN or org-admin of the DESTINATION org for the relocation.

Regression tests for #1 and #3; #2 covered by the existing bulk_add
suite after the gate addition.
2026-04-17 00:36:28 +00:00
user 132063289f fix(proxy): strip root-level data['tags'] alongside metadata tags
Greptile P2. The admin-inject gate only removed tags from data['metadata']
and data['litellm_metadata'];  and the
policy engine read  directly, so a caller without
allow_client_tags could still drive tag-based policy decisions by moving
tags to the body root. Also strip the root key in the same branch.
2026-04-17 00:20:52 +00:00
user 8c0668f105 perf: batch target membership lookup in delete_user to avoid N+1
Greptile P1 on the /user/delete fix. Per CLAUDE.md 'No N+1 queries',
move the find_many inside the per-user loop to a single batched
fetch with {'user_id': {'in': data.user_ids}} before the loop, then
distribute to a per-user set in memory.
2026-04-17 00:17:57 +00:00
user 467166fdd7 fix(proxy): enforce per-target org authorization on /user/delete
Veria admin-queue finding E3NpkuAd, Audit-B #1. The route-level gate
accepts this call when the caller is PROXY_ADMIN or ORG_ADMIN of any
org named in request_data["organization_id"]/["organizations"]. The
handler processes data.user_ids without cross-checking whether those
users belong to the caller's administered orgs, so an org-admin of
org-A could delete users in org-B via:
  {"user_ids": ["victim_in_org_B"], "organization_id": "org-A"}

Add per-target authorization: org-admins may only delete users whose
entire org membership is within their admin scope; targets with any
org outside scope (or no org at all) require PROXY_ADMIN.

Regression test confirms an ORG_ADMIN call fails with 403 and no
cascade delete_many runs.
2026-04-17 00:11:00 +00:00
user b4e98d190a fix(proxy): close 6 more metadata/tag variant bypasses
Post-merge audit found 6 adjacent variants of the VERIA-28 class. All
fixed here with regression tests:

1. Strip widened from 3 named keys to the full user_api_key_* prefix.
   The proxy writes a dozen user_api_key_* fields (user_id, alias,
   spend, team_id, request_route, end_user_id, …) into
   data[_metadata_variable_name]; the 3-key strip left the rest
   exploitable for identity/spend forgery in audit logs and guardrails.

2. proxy_server_request['body'] snapshot moved to AFTER the strip.
   Was captured at line ~990 before the strip ran, so
   standard_logging_object, lago, and spend_tracking readers saw the
   attacker-forged payload even though the live data dict was clean.

3. get_tags_from_request_body (auth-time) now coerces JSON-string
   metadata via safe_json_loads. Previously crashed with
   AttributeError on string metadata (DoS; potential RBAC bypass if
   a caller swallowed the exception).

4. get_end_user_id_from_request_body coerces JSON-string
   metadata/litellm_metadata. Previously isinstance(dict) guard
   caused end-user budget attribution to be silently skipped when
   the caller sent metadata as a JSON string.

5. Four hand-rolled 'if data.get("metadata") is None: data["metadata"] = {}'
   blocks in proxy_server.py (7160, 7341, 7590, 11375) now guard on
   isinstance(dict). They crashed with TypeError when metadata was a
   JSON string (DoS).

6. _get_admin_metadata defensively guards with isinstance(dict);
   previously AttributeError'd on any leaked string metadata.

Also hoists the inline safe_json_loads import in _guardrail_modification_check
to module level per CLAUDE.md style.
2026-04-17 00:08:40 +00:00
user 76aa97f77b fix(proxy): close three variant metadata/tag injection paths
Close three variant bypasses adjacent to VERIA-28 found during post-fix
variant audit:

1. _guardrail_modification_check had the same isinstance(dict) bypass
   Veria-AI just flagged on the pre-call strip. A caller sending
   `{"metadata": "{…}"}` as a JSON-encoded string (multipart/form-data
   or extra_body) skipped the guard, got parsed to dict downstream, and
   reached guardrail logic with bypass flags intact. Coerce strings via
   safe_json_loads before evaluating.

2. The allow_client_tags strip only covered body metadata.tags and
   litellm_metadata.tags — caller-supplied tags arriving via the
   x-litellm-tags header or root-level data["tags"] bypassed it. Gate
   add_request_tag_to_metadata's result on the same flag.

3. requester_metadata was deepcopied BEFORE the strip, so attacker
   injections (user_api_key_metadata shadows, disallowed tags,
   _pipeline_managed_guardrails) persisted in the snapshot. The PANW
   guardrail (and any future consumer) trusting requester_metadata
   would see forged values. Move the deepcopy to after the strip.

Regression tests added for each.
2026-04-16 23:35:26 +00:00
user dc8e03b91f fix(proxy): expand _guardrail_modification_check to cover all bypass keys
Per VERIA-28's secondary recommendation. The existing check only gated
metadata.guardrails. User-supplied values for disable_global_guardrails
(plural and the original singular typo variant) and opted_out_global_guardrails
are already silently ignored by _get_admin_metadata at read time, but the
silent-ignore makes diagnosis confusing and relies on one specific read
site catching them.

Reject at auth time with a 403 when any of:
- guardrails list (existing)
- disable_global_guardrails (new)
- disable_global_guardrail (new — historical singular-key variant)
- opted_out_global_guardrails (new)

are present in metadata, litellm_metadata, or at the request root, and the
caller's team lacks can_modify_guardrails. Defense in depth: the strip at
the pre-call layer still runs; this check fails loudly one layer earlier
so operators see an explicit 403 rather than a silent-ignore.
2026-04-16 23:14:05 +00:00
user db19f24d69 fix(proxy): move metadata strip after JSON-string parse
Veria AI caught a bypass: metadata can arrive as a JSON string via
multipart/form-data or extra_body, and the existing strip block ran
before the string-to-dict parse. The isinstance(_user_meta, dict)
guard returned False on the string, the strip was skipped, and then
the parse turned the string into a dict — leaving user_api_key_metadata
/ user_api_key_team_metadata / _pipeline_managed_guardrails / tags
intact in the parsed dict.

Move the strip to run AFTER the parse and BEFORE the merge of
litellm_metadata into data[_metadata_variable_name], closing the bypass
for both raw-dict and string-encoded payloads.

Regression test: test_add_litellm_data_to_request_strips_string_encoded_admin_injection.
2026-04-16 23:00:52 +00:00
Yuneng Jiang 5df7c21c9a fix: extend x-pass- header protection to cover additional credential headers and add tests
- Move protected-headers set to module level as a frozenset
- Add x-api-key, x-goog-api-key to protected set (provider credential headers)
- Block x-amz- prefix to cover AWS SigV4 signing headers
- Normalize forwarded header names to lowercase on write
- Log at debug level when a protected header is skipped
- Add unit test covering protected-header drop and non-protected forwarding
2026-04-16 15:41:34 -07:00
user 8526628a8f test: update tag-merge tests for default-deny client tag policy
Two pre-existing tests codified the pre-fix behavior where any caller-
supplied metadata.tags would flow through to spend logs and routing:

- test_add_key_or_team_level_spend_logs_metadata_to_request exercised
  the request/key/team tag merge. Set allow_client_tags=True on the key
  metadata so the merge path is still tested under the new regime.

- test_create_file_with_nested_litellm_metadata asserted that
  litellm_metadata[tags] form-data propagated to the handler. Drop the
  tag field; the test still proves nested form-parser correctness via
  spend_logs_metadata and environment.
2026-04-16 22:40:43 +00:00
user 0e62addd94 fix(proxy): gate caller-supplied routing/budget tags behind allow_client_tags
VERIA-28 (High) follow-up: tag-based routing and tag budget enforcement
read metadata.tags directly from the request, letting an attacker reach
restricted tag-routed deployments or misattribute spend to a victim
team's tag.

Strip metadata.tags (and litellm_metadata.tags) at the pre-call boundary
unless the caller's key or team metadata opts in with
allow_client_tags=True. Default-deny: existing clients that need to pass
routing tags must have the flag set explicitly on their key or team.

Preserves the tag-routing feature for admins who trust their callers;
closes the injection path for everyone else.
2026-04-16 22:31:00 +00:00
user d0601692b8 fix(proxy): strip user_api_key_metadata injection slots from user input
Expand the pre-call metadata strip to also remove user_api_key_metadata
and user_api_key_team_metadata. The proxy writes these fields into
data[_metadata_variable_name] with admin-authoritative values, but only
into that one metadata key; the caller's value in the OTHER metadata
key (metadata vs litellm_metadata) would otherwise persist and be
picked up by _get_admin_metadata, letting a caller supply their own
'admin' config to disable guardrails, opt out of global policies, etc.

VERIA-28 (High): Security Policy and Guardrail Bypass via Unsanitized
Request Metadata.

Add regression test at the proxy boundary verifying the strip, and
extend the guardrail test to cover the post-strip admin-config path.
2026-04-16 21:48:36 +00:00
user 815a2bed1a test: add regression tests for cross-org admin escalation
Verify that an org admin of org-A cannot operate on org-B, and that
an admin of both orgs can operate on both.
2026-04-16 21:06:56 +00:00