Commit graph

39506 commits

Author SHA1 Message Date
Tin Chi Lo
3d9c4cbcb7 chore: gitignore ui/litellm-dashboard/out (Next.js build output) 2026-06-10 12:00:35 -07:00
Tin Chi Lo
8b0c33a585 feat(ui/mcp): card-grid MCP servers list, frontend-only
Card slice of #28917 with env-vars wiring stripped; no networking or migration
2026-06-10 11:49:12 -07:00
Sameer Kankute
37d6047884 fix(mcp): clear allowed_tools and tool overrides on MCP server edit (#29411)
* fix(mcp): clear allowed_tools and tool overrides on MCP server edit

Send empty arrays/objects from the dashboard instead of null, coerce legacy
null payloads before Prisma, and stop auto-selecting all tools when the
stored allowlist is empty.

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

* style(mcp): simplify CRUD panel value ternary per review

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

* fix(mcp): enforce empty tool allowlist when cleared in dashboard

Set mcp_info.tool_allowlist_enforced on UI save so [] blocks all tools
while legacy servers with default [] remain unrestricted.

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

* Fix legacy MCP tool allowlist edit state

* test(mcp): pin allowlist fields on mock server in tools test

MagicMock auto-attributes are truthy and trigger server_applies_tool_allowlist
after the empty-allowlist enforcement change.

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

* fix(mcp): avoid locking legacy servers on quick edit save

Only set tool_allowlist_enforced when already enforced or the user
selected tools; skip allowlist fields on save for unrestricted servers;
do not auto-select all tools when editing legacy servers before load.

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

* fix(mcp): type mcp_info base for allowlist flag read

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

* fix(mcp): use MCPInfo type for tool_allowlist_enforced in edit save

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

* Update ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx

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

* Remove unused MCP allowlist variable

* Fix MCP legacy tool state display

* Fix legacy MCP tool allowlist saves

* fix(mcp): enforce allowlist when create flow deselects all tools

Track explicit allowlist interaction in the create form so deselecting
every tool persists tool_allowlist_enforced=true. Previously an empty
selection sent the flag as false with allowed_tools=[], which the proxy
treats as allow-all, contradicting the UI's 0 tools enabled state. This
mirrors the existing edit-flow handling.

* fix(mcp): enforce disallowed_tools on REST listing and keep restored tool selection on legacy edit

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-10 10:34:02 -07:00
Tin Chi Lo
c89c33648a test(mcp): drop backported tests for features absent from v1.88.0
The #29809 and #29960 cherry-picks landed onto v1.88.0, which predates the
per-user env-var rotation feature (#28917) and the MCP upstream-auth exception
module (#28356). Git's conflict resolution bundled those intermediate-commit
tests into the picked test files, where they fail against this base because the
code under test does not exist here (encrypt_value_helper, the env-var rotation
helper, and litellm.proxy._experimental.mcp_server.exceptions.MCPUpstreamAuthError).

Remove the leaked tests; the genuine regression tests added by #29809 (OBO
refresh-on-expiry) and #29960 (admin-only include_disabled_tools) stay and pass.
2026-06-10 09:20:06 -07:00
tin-berri
0956ad6b4e fix(mcp): allow team access-group grants in OAuth authorize/token access check (#30041)
* fix(mcp): honor team access-group grants in OAuth authorize/token access check

* test(mcp): mock build_effective_auth_contexts in non-admin authorize tests for isolation
2026-06-09 17:31:55 -07:00
tin-berri
5da72fc8a0 fix(ui/mcp): reset OAuth state on create-server modal close so a prior server's token no longer leaks into the next add-server session (#30000)
* fix(ui/mcp): reset OAuth hook state on modal close so a prior server's token no longer leaks into the next add-server session

* fix(ui/mcp): clear in-flight OAuth guard on reset and reset form/tools on modal close so nothing leaks on a parent-driven dismiss
2026-06-09 17:31:55 -07:00
tin-berri
bafad619ab fix(mcp): load MCP tool configuration tools via the OBO/passthrough-aware GET path (#29960)
* fix(ui): load MCP tool configuration tools via the OBO/passthrough-aware GET path

* fix(mcp): admin-only include_disabled_tools so the settings UI shows toggled-off tools

* fix(ui): repopulate MCP server edit form when server data loads after mount (OAuth return)

* fix(ui): persist MCP OAuth token on save and return to the Settings tab after authorize

* fix(ui): scope MCP OAuth callback to the initiating form so create and edit flows don't cross-talk

* fix(ui): derive OAuth-return Settings tab via lazy state init instead of setState-in-effect

* Fix MCP OAuth edit token handling

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-09 17:31:55 -07:00
tin-berri
07dffa855e changing expires_in default to use actual slack return details (#29951) 2026-06-09 17:26:59 -07:00
tin-berri
969c54a99c fix(mcp): let non-creator users OAuth into OBO-mode MCP servers from the Tools page (#29867)
* fix(ui): let non-creator users OAuth into OBO-mode MCP servers from the Tools page

* fix(ui): clear OBO Tools-tab one-shot on navigate-back and gate on credential-status errors
2026-06-09 17:26:59 -07:00
tin-berri
96fb9d1bde fix(ui): stop MCP playground tool calls from sending twice (#29821) 2026-06-09 17:23:44 -07:00
tin-berri
dc032bcc07 fix(ui): persist Tools-tab MCP OAuth token to DB (#29809) 2026-06-09 17:23:44 -07:00
tin-berri
83d33800bb fix(ui): route MCP playground auth by oauth2 mode instead of token_url (#29714)
Interactive PKCE and OBO servers were mislabeled as M2M, so passthrough never showed the Authorize gate; classify by oauth2_flow + delegate_auth_to_upstream instead.
2026-06-09 17:19:07 -07:00
tin-berri
23e3f73d8b fix(helm): Enable Backend Deployment to mount Gateway config.yaml (#29605)
* change deployment configs to include a litellm.cache for litellm-backend pod mirroring litellm-gateway pod

* omit backend annotations block when config and podAnnotations are both empty

* reuse gateway config/configmap for backend instead of separate backend config

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Tin Chi Lo <tin@Tins-MBP.localdomain>
Co-authored-by: Tin Chi Lo <tin@Tins-MacBook-Pro.local>
2026-06-09 17:15:29 -07:00
Sameer Kankute
bde16ff5b7
test(proxy): patch utils.get_server_root_path in passthrough auth tests
After removing get_server_root_path from pass_through_endpoints, route
and JWT tests must mock litellm.proxy.utils where normalization reads it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 18:45:10 -07:00
Sameer Kankute
3d00874438
fix(proxy): match passthrough registry routes bare-to-bare with SERVER_ROOT_PATH
After #28547, get_request_route strips the deployment prefix while registry
lookup still re-inflated stored paths via SERVER_ROOT_PATH, causing 404s
under paths like /llmproxy/ml. Compare normalized bare routes in both
is_registered_pass_through_route and get_registered_pass_through_route.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 18:44:58 -07:00
Mateo Wang
67428bdb1c
Merge pull request #29639 from BerriAI/litellm_ghsa_q775_defaults_rc188
fix(key_generate): harden GHSA-q775 session-token exemption against default_key_generate_params (1.88 rc)
2026-06-03 19:27:27 -07:00
mateo-berri
c5d214cf81
fix(key_generate): harden GHSA-q775 session-token exemption against default_key_generate_params
Capture _requested_team_id before the default_key_generate_params loop runs and
key the UI/CLI session-token budget-ceiling exemption off it, instead of the
post-defaults data.team_id. On an install that sets
default_key_generate_params.team_id, a session token requesting a personal key
(no explicit team_id) would otherwise have data.team_id auto-filled, flipping
is_ui_session_team_key on and bypassing the delegated-authority ceiling -- the
exact escalation GHSA-q775 closed. Mirrors the existing pre-defaults capture of
_requested_max_budget. Adds a regression test.

https://claude.ai/code/session_01RT583b1khYC3wjLrQ5hT5h
(cherry picked from commit efeb101ec6)
2026-06-04 01:18:15 +00:00
Mateo Wang
c2a0a6a2ee
Merge pull request #29637 from BerriAI/litellm_cherrypick_1_88_0_rc3
chore(release): patch v1.88.0-rc.1 with #29612 (session-token budget-ceiling exemption)
2026-06-03 17:16:33 -07:00
yuneng-jiang
26e9689c73
fix(key_generate): exempt UI/CLI session tokens from the budget ceiling for team keys (#29612)
Non-admin users creating a team key through the UI were rejected with
"max_budget cannot exceed the caller's own max_budget (0.25)". The request is
authenticated by a UI/CLI session token whose max_budget is the per-session chat
spend cap (max_ui_session_budget, default $0.25), and the delegated-authority
budget ceiling (GHSA-q775-qw9r-2r4g) treated that cap as a delegation limit.

Skip the ceiling only when a session token creates a team key (data.team_id set);
that key's spend is bounded by the team budget at request time. Personal keys and
every other non-admin caller keep the ceiling, so a session token cannot mint an
arbitrary-budget personal key.

(cherry picked from commit 97ba7e1a30)
2026-06-04 00:06:34 +00:00
Mateo Wang
0aea62bf50
Merge pull request #29632 from BerriAI/litellm_cherrypick_1_88_0_rc2
chore(release): patch v1.88.0-rc.1 with four staged fixes
2026-06-03 16:25:44 -07:00
Mateo Wang
ed04563db5
fix: passthrough endpoints duplicate logs (#29598)
* fix duplicate cost callbacks for anthropic streaming pass-through

Two bugs caused _PROXY_track_cost_callback to see stream=True +
complete_streaming_response=None on every streaming pass-through request,
making the dedup guard in dispatch_success_handlers permanently inactive:

1. pass_through_endpoints.py created the Logging object with stream=False
   for all requests. _is_assembled_stream_success short-circuits on
   self.stream is not True, so has_dispatched_final_stream_success was
   never set and any second dispatch went through unchecked.
   Fix: set logging_obj.stream = True after stream detection.

2. _create_anthropic_response_logging_payload set complete_streaming_response
   inside the try block after litellm.completion_cost(), so a pricing error
   caused an early return without setting it on model_call_details.
   Fix: set complete_streaming_response before the try block.

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

* fix stream

* add stream to logging obj

* test(pass_through): give mock logging object a real model_call_details dict

The anthropic passthrough logging payload now records the assembled
response on model_call_details before cost calculation, which requires
model_call_details to support item assignment. In production it is always
a dict; the existing unit test stubbed the logging object with a bare Mock
whose attribute is not subscriptable, so the new assignment raised
TypeError. Use a real dict to match the production logging object.

* test(pass_through): cover streaming logging-obj stream flag

The streaming branch of pass_through_request that marks the logging object
as streaming (logging_obj.stream and model_call_details["stream"]) had no
unit coverage, so the patch coverage gate flagged it. Add a regression test
that drives a streaming pass-through request through pass_through_request and
asserts the logging object is flagged as a stream before dispatch.

* test(pass_through): cover SSE-response stream flag fallback branch

The auto-detected streaming branch of pass_through_request (when a request
that was not flagged as streaming returns a text/event-stream response) sets
logging_obj.stream and model_call_details["stream"] but had no unit coverage,
so the codecov patch gate failed at 60%. Drive a non-streaming pass-through
request whose upstream response is SSE through pass_through_request and assert
the logging object is flagged as a stream before dispatch.

* fix(pass_through): gate complete_streaming_response on stream flag

perform_redaction only scrubs complete_streaming_response when
model_call_details["stream"] is True. Setting it unconditionally for
non-streaming Anthropic pass-through responses left the assembled
response unredacted in model_call_details, which is handed to logging
callbacks as kwargs when message logging is disabled. Only record it for
actual streaming responses so redaction always applies.

---------

Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 2bbdbfa5c3)
2026-06-03 22:54:06 +00:00
Mateo Wang
2110b41a16
fix(vertex): strip output_config.effort for Vertex Claude models that reject it (Haiku 4.5) (#29585)
* fix(vertex): strip output_config.effort for models that reject it

Haiku 4.5 on Vertex AI does not support output_config.effort and 400s with
"output_config.effort: Extra inputs are not permitted". PR #27074 emptied
VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS so effort would forward for Opus/Sonnet
4.6+, but that made the strip unconditional across every Vertex Anthropic
model, including ones that don't support it. Claude Code injects effort into
its default Messages payload, so `claude --model claude-haiku-4.5` started
failing.

Make the sanitizer model-aware: drop output_config.effort for models that
don't advertise output_config support (or any reasoning effort level) while
forwarding it for those that do. The fix covers both the chat-completion and
Messages pass-through transformation paths since they share the helper.

* chore(vertex): log at debug when dropping unsupported output_config.effort

Operators pointing an unregistered Vertex Claude alias that does support
effort would otherwise see it stripped with no signal. Debug level keeps it
out of normal logs since Claude Code sends effort on every request.

(cherry picked from commit cc55662e5f)
2026-06-03 22:54:06 +00:00
milan-berri
8373712086
fix(key_generate): allow team members to create keys on org-scoped teams (#29310)
* fix(key_generate): allow team members to create keys on org-scoped teams

When a virtual key is created for a team, enterprise logic inherits the
team's organization_id onto the key (add_team_organization_id). Since the
VERIA-55 org-IDOR fix, /key/generate then required the caller to be an
explicit LiteLLM_OrganizationMembership member of that org, returning
403 "Caller is not a member of organization_id=<uuid>". Admins normally
only add users to teams (not orgs), so self-serve key creation regressed
for any user on an org-scoped team (regression since v1.84.0-rc.1).

Skip the org-membership check when organization_id was inherited from the
key's team (organization_id == team_table.organization_id). Team-level
authorization already gates this path, so team membership is sufficient.
The membership check still runs when a caller assigns an organization_id
that did not come from the key's team, preserving the IDOR protection.

Adds regression tests covering both the team-inherited (allowed) and
foreign-org (still blocked) cases.

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

* test(key_generate): cover mismatched team org IDOR path on generate

Add test_generate_key_foreign_org_with_mismatched_team_still_enforces_membership
for the case where a team is present but request organization_id differs from
team_table.organization_id. Enterprise inheritance is no-op'd in the test so
the guard is exercised directly; membership validation must still run.

Addresses Greptile review on #29310.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit b11833c737)
2026-06-03 22:54:06 +00:00
Shivam Rawat
521ddc3b18
fix(proxy): resolve managed video model ids for auth (#29545)
* fix(proxy): resolve managed video model ids for auth

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

* test(proxy): cover character_id router model resolution

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit d45e9e4d56)
2026-06-03 22:54:06 +00:00
yuneng-jiang
5be0797d24
Merge pull request #29372 from BerriAI/litellm_internal_staging
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CodSpeed Benchmarks / benchmarks (push) Has been cancelled
Helm unit test / unit-test (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
GitHub Actions Security Analysis / zizmor (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 / key-generation (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
chore(ci): promote internal staging to main
2026-05-30 20:56:58 -07:00
yuneng-jiang
28c0d8579b
chore(deps): bump deps (#29373)
Some checks failed
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 / 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 / key-generation (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
* bump: version 0.1.41 → 0.1.42

* uv lock
2026-05-30 20:41:23 -07:00
yuneng-jiang
54ed5a4eb5
fix(e2e): tolerate trailing slash in SERVER_ROOT_PATH login redirect (#29369)
The Next.js admin UI is exported with trailingSlash: true, so the proxy
serves /ui/login at /ui/login/index.html and 308s /ui/login → /ui/login/.
The waitForURL predicate used endsWith("/ui/login"), which never matched
the canonicalized URL and timed out after 15s.

This was masked until the build artifacts were regenerated against the
AuthContext fix: the prior bundles still hit the racy redirect path that
fired before proxyBaseUrl was populated, producing /ui/login (no prefix,
no proxy round-trip, no trailing slash) which fortuitously satisfied the
predicate. The first PR to ship the corrected bundle exposed the
assertion bug.

Switch the predicate to includes("/ui/login"); the prefix assertion below
still validates the SERVER_ROOT_PATH preservation that is the actual
contract under test.
2026-05-30 20:00:33 -07:00
michelligabriele
80cf50dedb
fix(v3 limiter): cap no-max_tokens TPM floor at smallest configured limit (#28805) 2026-05-30 19:36:04 -07:00
yuneng-jiang
8b16b61114
chore: update Next.js build artifacts (2026-05-31 02:04 UTC, node v20.20.2) (#29366) 2026-05-30 19:25:42 -07:00
michelligabriele
117136cccc
fix(openai-moderation): wire streaming flags through to unified dispatcher (#27324) 2026-05-30 19:22:25 -07:00
yuneng-jiang
f0ebfb2a1b
fix(ui): break logout redirect loop across origins (#29360)
When the user has visited both the dev UI (e.g. localhost:3000) and the
proxy UI (e.g. localhost:4000) in the same tab, logging out from the dev
origin produced an infinite logout/login redirect.

The proxy-side LoginPage's "is the user still authenticated?" check
was reading getCookie("token"), which falls back to sessionStorage when
document.cookie has no token. The cross-origin clearTokenCookies() call
from the dev origin can clear cookies on the shared hostname, but cannot
reach sessionStorage on the proxy origin (sessionStorage is per-origin),
so the fallback returned a stale token and LoginPage interpreted the
user as logged in, redirecting back to the dev origin. Dev origin then
saw no cookie and redirected to LoginPage, repeating ~20x per second.

This change introduces getCookieFromDocument(), a cookie-only read with
no sessionStorage fallback, and uses it in LoginPage's already-logged-in
check. The HttpOnly-reverse-proxy defense from PR #23532 is unaffected:
storeLoginToken still writes both the JS cookie at /ui and the
sessionStorage backup, and getCookie still falls back for callers that
want the full read path.
2026-05-30 19:02:29 -07:00
ryan-crabbe-berri
a9cc6ed68c
test(e2e): cover PROXY_LOGOUT_URL redirect on Logout (#29080)
* test(e2e): cover PROXY_LOGOUT_URL redirect on Logout

Env-gated spec mirroring the existing serverRootPathRedirect pattern:
when the proxy is booted with PROXY_LOGOUT_URL set, clicking Logout in
the navbar must navigate to that external URL. The standard run_e2e.sh
exports an empty value so the rest of the suite is unaffected; this
spec self-skips unless the env var is populated.

* test(e2e): run PROXY_LOGOUT_URL spec in the suite + harden logout assertions

Boot the e2e proxy with PROXY_LOGOUT_URL set (job-level env in CircleCI and
run_e2e.sh) so proxyLogoutUrl.spec.ts actually runs instead of self-skipping.
Nothing else in the suite performs a logout, so this only affects the behavior
under test.

Harden the spec to verify the logout flow rather than a URL substring:
- wait for /sso/get/ui_settings before clicking so logoutUrl is populated
  (otherwise window.location.href = "" silently reloads same-origin)
- assert a token cookie exists first, and is cleared after logout
- locate the dropdown via getByRole instead of internal antd CSS classes
- stub the external destination and assert on URL origin + path prefix

* test(e2e): assert exact PROXY_LOGOUT_URL on logout redirect

Replace the origin + startsWith(pathname) checks with a single normalized
href comparison. With PROXY_LOGOUT_URL=https://www.example.com the path was
"/", so startsWith("/") matched any path and left path/query/hash
unchecked. Comparing normalized hrefs pins scheme, host, port, path, query
and hash while still tolerating the browser's trailing-slash/default-port
normalization.
2026-05-30 18:19:04 -07:00
yuneng-jiang
7d1bd9d9f4
fix(reset_budget): write only {spend, budget_reset_at} and stop pre-zeroing counter (#29358)
* 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 17:48:16 -07:00
ryan-crabbe-berri
90b5104475
feat(mcp/auth): additive key access-group grants + opt-in member assignment (#29313)
* fix(mcp): make key.access_group_ids grants additive over team ceiling

A key whose unified access_group_ids grant a private MCP server was
having that grant intersected against its team's MCP ceiling, so a key
in a team scoped to other servers (or with no own scope) lost the
granted server entirely. Resolve access_group_ids once as ungated
additive grants and union them on top of the key/team ceiling instead
of folding them into the key scope that gets intersected.

* test(mcp): align key access-group tests with additive-grant model

The previous commit moved key.access_group_ids resolution out of the
intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the
ungated additive grant path (_get_key_access_group_mcp_server_extras),
unioned on top of the team ceiling. Five tests from #28890/#29195 still
asserted the old gated / in-key-scope contract and failed:

- _get_allowed_mcp_servers_for_key now returns the object_permission
  ceiling only and never resolves access_group_ids; two tests now assert
  the group resolver is not called from that path (with and without an
  object_permission present).
- The extras path is ungated, so a group whose assigned_team_ids /
  assigned_key_ids exclude the caller still contributes its servers.
- The end-to-end test asserts the grant surfaces via the extras path
  rather than the base key path.
- Dropped test_key_access_group_ids_empty_returns_no_extras; the empty
  case is already covered by the extras family's no-groups test.

* feat(auth): gate member access-group assignment on keys behind opt-in

Non-admin team members could attach access_group_ids to keys they create
or update, letting them self-grant resources (MCP servers/models) the team
admin never intended. Add an opt-in KEY_ACCESS_GROUP_ASSIGNMENT team-member
permission (default-deny) enforced at /key/generate and /key/update; proxy
and team admins bypass. Surfaces automatically as a checkbox in the team
Member Permissions UI.

* fix(auth): gate access-group assignment on /key/regenerate too

RegenerateKeyRequest inherits access_group_ids and prepare_key_update_data
persists it, so a non-admin key owner could self-grant access groups by
regenerating. Apply the same opt-in member gate using the existing key's
team.

* test(auth): cover member access-group gate and additive MCP grants

Add unit tests for enforce_member_can_assign_access_groups (deny without
opt-in, allow with opt-in, and proxy-admin / team-admin / non-team-key
bypasses) and for _get_key_access_group_mcp_server_extras (no-auth and
no-resolved-servers return empty, resolved ids are expanded, errors
degrade to no grants).
2026-05-30 17:35:31 -07:00
Shivam Rawat
dc4f5b12ef
fix(proxy): enforce allowed_passthrough_routes for auth=true pass-thr… (#29256)
* fix(proxy): enforce allowed_passthrough_routes for auth=true pass-through

Pass-through endpoints with auth=true were injected into openai_routes,
so teams with openai_routes access bypassed per-team allowed_passthrough_routes.
Gate auth-enforced pass-through at JWT, virtual-key, and non-admin route checks.

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

* fix(proxy): clarify JWT passthrough denial

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

* fix(proxy): make pass-through auth checks method-aware

Prevent allowlist bypass when the same path is registered with different auth settings per HTTP method.

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

* Fix passthrough route auth checks

* fix(proxy): reject unregistered pass-through HTTP methods

Enforce method-aware JWT checks and return 405 when stale FastAPI routes accept requests outside the current pass-through registry.

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

* fix(proxy): remove duplicate request_method in JWT team lookup

Fixes SyntaxError on proxy startup caused by passing request_method twice to find_team_with_model_access.

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

* Fix passthrough route auth enforcement

* fix(proxy): raise passthrough-specific 403 directly in virtual-key path

* fix(proxy): load team for RBAC role-claim JWT passthrough gating

* Revert "chore(tests): migrate Bedrock CI to AWS account 941277531214 (#28728)" (#29326)

This reverts the Bedrock CI account migration (#28728). The original account
(888602223428) was put under an AWS security restriction after a leaked key
and has since been reactivated, while the replacement account (941277531214)
lacks access to several models the suites exercise (legacy Bedrock Claude 3
models, Cohere, Nova Canvas image gen, Bedrock batch inference, and flagship
Opus). Pointing CI back at the reactivated account restores that coverage.

This is the exact inverse of #28728: all hardcoded 941277531214 references go
back to 888602223428 (provisioned/imported-model ARNs, AgentCore runtime ARNs
and their suffixes, batch execution role ARN, and the example proxy config),
the S3 buckets revert to litellm-proxy and load-testing-oct, the guardrail IDs
revert to wf0hkdb5x07f and ff6ujrregl1q, the SageMaker endpoint and Knowledge
Base revert to their original ids, and the live-call tests go back to the
legacy model strings. The grid_spec fail_reason workaround for the unentitled
Opus cells is dropped while keeping the unrelated bedrock_effort_ceiling field
added after the migration.

The CircleCI AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars still point at
941277531214 and must be set to the reactivated account's fresh credentials
separately via the CircleCI API; AWS_REGION_NAME stays us-west-2.

(cherry picked from commit f11c12d157)

* fix(proxy): scope pass-through 405 to registry routes; grant rerank passthrough in rpm tests

The auth=true pass-through 405 guard fired for mapped provider routes
(e.g. /assemblyai/*) that are not in the in-memory registry, since
get_registered_pass_through_route returns None for them while
is_registered_pass_through_route matches via mapped_pass_through_routes.
Only raise 405 when the path is registered but the request method is not
allowed, so mapped provider pass-throughs fall through to the default
target params as before.

The rpm-limit pass-through tests register /v1/rerank with auth=true but
gave their keys no allowed_passthrough_routes, so the new default-deny
returned 403 before the rate limiter ran (non-deterministically,
depending on registry insertion order). Grant the keys explicit
passthrough access so the tests exercise rate limiting under the new
auth model.

* fix(proxy): guard request method lookup against scopes without a method

Starlette's Request.method property reads scope["method"] and raises
KeyError when the scope omits it (e.g. minimally-constructed test
requests). getattr only swallows AttributeError, so the new
_get_request_method helper propagated the KeyError up through
user_api_key_auth and surfaced as a ProxyException. Catch KeyError
(and AttributeError) and fall back to None.

* test(passthrough): pin SERVER_ROOT_PATH in unregistered-method test

test_custom_proxy.py sets os.environ['SERVER_ROOT_PATH'] = '/my-custom-path'
at module import with no cleanup. When that module is collected into the same
xdist worker as this test, the leaked root path is prepended to registered
pass-through paths, so is_registered_pass_through_route misses '/test/path'
and the handler returns 404 instead of the expected 405 (order-dependent).
Pin SERVER_ROOT_PATH to '' so the test is deterministic.

* test(passthrough): restore regression coverage for non-auth-enforced pass-through via llm_api_routes

* fix(proxy): record auth flag in pass-through registry for allowlist enforcement

Auth-enforced pass-through detection inferred enforcement from the FastAPI
dependency stored at registration time. The management create and update
endpoints register routes with dependencies=None even though auth defaults to
true, so is_auth_enforced_pass_through_route treated those DB-created routes as
unenforced. A key allowed for llm_api_routes could then call a management-created
auth-enabled pass-through route without matching allowed_passthrough_routes.

Store the auth setting on each registry entry and read it directly when deciding
whether the allowlist applies, instead of deriving it from dependency metadata.

* fix(proxy): include bool in pass-through registry value type for auth flag

The auth flag stored in _registered_pass_through_routes is a bool, which
was not part of the registry value Union, so mypy rejected the dict literal.
Add bool to the Union and narrow route_methods to a list before the
membership check so the in-operator stays valid.

* fix(proxy): preserve stored auth flag on pass-through endpoint update

model_dump(exclude_none=True) re-included the auth=True default whenever
a partial update omitted auth, silently flipping an existing auth=false
pass-through to auth-enforced and 403ing every team/key without
allowed_passthrough_routes. Merge only explicitly set fields via
exclude_unset so omitted fields keep their stored value.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-05-30 17:07:24 -07:00
Sameer Kankute
ba2699740c
feat(pass_through): extend passthrough_managed_object_ids to Azure (#29160)
* feat(pass_through): extend passthrough_managed_object_ids to Azure

Adds managed ID minting/resolution for Azure passthrough endpoints
(/azure/...) alongside the existing OpenAI passthrough support.

Key changes:
- pass_through_endpoints.py: detect azure/azure_ai custom_llm_provider
  (string or enum) to set _is_managed_id_provider and _managed_id_provider;
  both INPUT and OUTPUT rewrite blocks now fire for Azure.
- llm_passthrough_endpoints.py: forward custom_llm_provider into
  create_pass_through_route so it reaches pass_through_request (was None).
- managed_id_rewriter.py: extend _PASSTHROUGH_PREFIX_RE and _canonical_path
  to strip /azure/openai prefix and add /v1/ for Azure paths that omit it;
  add ("azure", method, path) entries to BUILTIN_OUTPUT_ID_FIELD_MAP for
  files and batches endpoints.
- managed_id_codec.py / types/utils.py: supporting codec and enum constant.
- proxy_server.py: register llm_passthrough_router before batches_router to
  prevent route collision for /openai_passthrough/* paths.

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

* fix(pass_through): remove unused imports for ruff F401

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

* fix(pass_through): satisfy mypy for optional parsed_body

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

* fix(pass_through): compute query params string after managed-ID rewrite

Move requested_query_params_str computation to after the managed-ID
input rewrite block so logging_url reflects the rewritten raw-provider
query params actually sent upstream, instead of the original
managed IDs.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Add support for managed ids for passthrough responses api

* Add support for list batches and list files

* style: run Black on passthrough managed ID files

Fix CI formatting for managed_id_rewriter.py and pass_through_endpoints.py.

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

* fix(passthrough): parse json file_object and implement before-cursor pagination

- Parse row.file_object via json.loads when Prisma returns it as a string;
  mirrors openai_files_endpoints/common_utils.py so list responses keep all
  stored detail fields (status, timestamps, etc.).
- Implement the previously-parsed-but-unused 'before' cursor for list
  pagination by flipping fetch order to ascending with a 'gt' bound on
  created_at, then reversing rows so the response stays newest-first.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Remove logger

* refactor: split list_passthrough_ids_from_db to fix PLR0915

Extract pagination, fetch, and serialization helpers so the main list
function stays under the statement limit without changing behavior.

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

* fix: scope passthrough managed ID dedup and list by provider

Validate embedded provider before reusing deduped file/object rows so
OpenAI and Azure cannot share the same managed ID for an identical raw ID.
Filter list responses to rows whose managed IDs decode to the current
provider, with over-fetch scanning when needed.

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

* fix(managed_id_rewriter): cap pagination trim at effective limit

When raw_limit > 100, fetch_limit is capped at 101 (one extra row to
detect has_more), but trimming with rows[:raw_limit] failed to drop
the sentinel row. Use the capped effective limit instead.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix: cross-provider object collision and fail-closed list error handling

Greptile P1: move provider check before access check in _mint_or_reuse_object
so a cross-provider raw ID collision (OpenAI and Azure share the same batch_
ID) falls through to mint a new provider-scoped row instead of raising 404.

Veria-ai medium: _fetch_provider_scoped_list_rows now always returns
(page, has_more) — DB errors break out of the scan loop and return matched
rows so far. list_passthrough_ids_from_db never returns None for a recognised
list route, so the caller can never fall through to the upstream provider.

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

* fix: namespace passthrough model_object_id by provider to prevent unique violation

Store model_object_id as 'passthrough:{provider}:{raw_id}' instead of the
bare raw ID so OpenAI and Azure can each own a row for the same raw batch ID
without hitting the @unique constraint. Dedup lookup uses the same namespaced
key so it is implicitly provider-scoped and the _managed_id_matches_provider
check is no longer needed on the object path.

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

* fix: gate list interception on managed_files hook like input/output rewrites

Without the hook no managed IDs are minted so the DB is empty. Intercepting
GET /v1/files without the hook returned an empty list and hid the caller's
real upstream files/batches. Matches the guard used by the input and output
rewrite blocks.

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

* fix: set has_more=True when scan cap is hit with a full final DB batch

When max_scans (20) is exhausted and the last DB page was full-sized,
there are almost certainly more rows beyond the scan window.  Track
last_batch_full across iterations so the scan_cap_hit condition sets
has_more=True in that case, preventing silent pagination truncation in
high-mixed-provider pools.

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

* fix(managed_id_rewriter): scope pagination cursor lookup to caller-owned rows

Prevent a cross-tenant timing oracle by constraining the after/before
cursor row lookup to the caller's owner_filter, and cover the real Azure
responses path form (no /v1/) in tests.

* fix(managed_id_rewriter): align passthrough list metadata with direct GET

Persist upstream file metadata when minting a managed file ID and rewrite
nested batch file IDs before snapshotting the object, so DB-served file/batch
list responses return the same fields and managed IDs as a direct endpoint
GET.

* fix(managed_id_rewriter): degrade to raw id on cross-owner object collision

The OUTPUT (mint) path of _mint_or_reuse_object raised HTTPException(404)
when a dedup hit on the namespaced model_object_id belonged to a different
owner, converting a successful upstream batch/response creation into a 404
for the caller. Two upstream accounts under one provider name can issue the
same raw id, so this is reachable in multi-tenant deployments.

Return the caller's raw id unmanaged instead: the upstream create already
succeeded, a new managed row can't be minted (model_object_id is @unique),
and reusing the other owner's managed id would later fail the access check.

* fix(managed_id_rewriter): scope list cursor by provider and cap body-rewrite recursion depth

* perf(managed_id_rewriter): push batch list provider scope to DB and anchor canonical-path prefix

Object (batch) list rows store model_object_id as passthrough:{provider}:{raw},
so the provider filter is now applied at the indexed DB column, collapsing the
application-layer multi-scan to a single query for that table. File rows keep
the decode-based scan since they have no provider column.

Anchor the canonical-path prefix regex at a path boundary so routes such as
/openai_realtime/... are no longer mis-stripped.

* fix(managed_id_rewriter): refresh stored batch snapshot on reuse

The dedup-reuse path in _mint_or_reuse_object returned the existing managed id
without updating the stored file_object, so DB-served list responses kept the
creation-time snapshot and showed null output_file_id/error_file_id even after
the batch completed. Refresh the snapshot when an owned row is reused so the
list reflects the batch's latest state.

* fix(managed_id_rewriter): deny cross-owner object access on retrieve/cancel/delete

Returning the raw id when can_access_resource fails only made sense for create
responses, where the caller's own upstream create succeeded under a raw id that a
different owner already holds. On retrieve/cancel/delete the caller reaches that
branch only by supplying another tenant's raw id (which bypasses the managed-id
input gate), so echoing the upstream object back leaked it cross-tenant. Restrict
the raw fallback to create routes and return 404 otherwise.

* fix(managed_id_rewriter): deny cross-owner file access on retrieve/delete

_mint_or_reuse_file scoped the raw file dedup lookup to the current caller, so a
raw file-... id belonging to another tenant was never found and the OUTPUT path
minted a fresh managed id for that same upstream file under the caller. A raw id
only reaches this path by skipping the managed-id input gate (raw provider ids
are opt-out), so a different-owner row means the caller is touching someone
else's file. Look up flat_model_file_ids globally and run can_access_resource;
deny with 404 on retrieve/delete and leave the raw id unmanaged on create, which
mirrors the cross-owner handling already in _mint_or_reuse_object.

* fix(managed_id_rewriter): deterministic provider-scoped file dedup

Replace the unscoped find_first in _mint_or_reuse_file with a find_many
ordered by created_at and an application-layer provider filter. The file
table has no provider column, so a raw file id shared across OpenAI and
Azure could map to one row per provider; find_first then picked a row
non-deterministically and, on a provider mismatch, minted a fresh managed
row on every call, accumulating duplicates. Selecting the oldest matching
same-provider row the caller can access keeps reuse stable and prevents
duplicate rows while preserving the cross-tenant deny/leave-raw behaviour.

* refactor(pass_through): scope passthrough managed IDs on the explicit provider

Move the openai/azure detection out of pass_through_request into
resolve_passthrough_managed_id_provider in llms/base_llm/managed_resources,
and key managed-ID rewriting on the forwarded custom_llm_provider rather than
the upstream URL's EndpointType. The helper documents why azure and azure_ai
collapse to one "azure" scope (they share the same Azure OpenAI files/batches
surface, so an ID minted on one must resolve on the other) and returns None for
any other provider so a third-party OpenAI-compatible endpoint never triggers
managed-ID minting.

Add TestManagedIdProviderScope covering the azure_ai -> azure collapse and the
non-openai/azure exclusion.

* test(log_db_metrics): assert sanitized event_metadata contract

test_log_db_metrics_success still asserted the legacy event_metadata
shape (function_name/function_kwargs/function_args), which #28909
intentionally removed so that live Prisma clients, OTel spans, and
secrets never land on a service-log span. The decorator now emits only
a sanitized payload: None when no table_name is present, and
{"table_name": ...} when it is. Update the test to verify both branches
of that contract.

* fix(managed_id_rewriter): page provider-scoped file list by offset

The file list scan advanced its cursor with a strict created_at boundary.
When several rows shared a created_at timestamp and a non-matching provider
row sat on the page boundary, the next query skipped the remaining rows at
that timestamp, dropping matching files from the response. Page by a stable
offset over a total order (created_at plus the unique id column) so tied
rows are never skipped or repeated.

* fix(managed_id_rewriter): push file-list provider scope to the DB

The file-list helper had no provider column to query, so it scanned the
table and filtered by decoding each managed ID in the application layer,
capped at 20 pages. For an admin with a large mixed-provider file pool
that cap could truncate a page.

Mint now writes a _passthrough_provider:{provider} marker into
flat_model_file_ids, giving the file table the same DB-queryable provider
scope object rows already get from the namespaced model_object_id. The
list helper pushes the scope into the query so a single round-trip serves
the page. The scan loop, the offset paging, and the cap are gone, so pages
can no longer truncate, leak the other provider, or skip rows that share a
created_at timestamp.

* fix(managed_id_rewriter): deny raw provider IDs that map to another tenant's managed resource

Clients only ever receive managed IDs on passthrough, so a raw file/batch/response ID for another tenant's managed object can only be recovered by decoding that tenant's managed ID. Raw IDs were forwarded upstream untouched (deliberate opt-out), which on a retrieve/cancel/delete executed upstream before the response-side ownership check ran, leaking a cross-tenant action.

Guard raw provider IDs on the input path: when a raw file-/batch_/resp_ ID resolves to a managed row the caller cannot access, return 404 before forwarding. Genuinely unmanaged raw IDs (no DB row) and IDs the caller owns are left untouched, preserving the opt-out.

* test(managed_id_rewriter): cover azure_ai and pre-versioned azure passthrough paths

* fix(managed_id_rewriter): fall back to raw id when persistence fails

A DB persistence failure after a successful upstream create left the
client holding a minted managed ID with no backing row, so every later
resolve returned 404 and the resource was permanently unreachable. Mint
the managed ID only when the row is stored; on persistence failure return
the raw provider id, matching the no-persistence-available fallback, so
the freshly-created resource stays reachable.

* fix(managed_id_rewriter): log only rewritten query param keys

* fix(managed_id_rewriter): use compound (created_at, id) list cursor boundary

A timestamp-only lt/gt cursor boundary skips list rows that share the
cursor row's created_at across a page boundary, silently dropping them.
Compare the unique id (the secondary sort key) alongside created_at so
the page walk stays complete when timestamps tie.

* fix(managed_id_rewriter): converge concurrent object creates on one managed id

* fix(managed_id_rewriter): bound raw-id guard DB lookups per request

The INPUT guard fired one DB lookup for every file-/batch_/resp_ prefixed
string in the path, query, and body. The file-id guard is an unindexed
array-containment scan over LiteLLM_ManagedFileTable, so an authenticated
caller could amplify a single passthrough request into thousands of
full-table scans by packing a body with id-shaped strings.

De-dupe raw ids within a request and cap the distinct guard lookups,
failing closed with 400 instead of skipping the guard. Legitimate callers
hold managed ids (resolved via an indexed unified_*_id lookup, not the
guard), so the cap only trips under abuse.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-05-30 16:30:10 -07:00
Shivam Rawat
7ca796beb1
fix(proxy): restrict vector store index create/delete to proxy admins (#29202)
* fix(proxy): restrict vector store index create/delete to proxy admins

Prevent non-admin API keys from registering indexes via POST /v1/indexes or deleting Azure AI Search indexes through managed pass-through routes.

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

* fix(proxy): tighten vector store index lifecycle checks

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 15:10:21 -07:00
Shivam Rawat
4c3efe9c7c
fix(guardrails): return HTTP 400 for litellm content filter blocks (#28418)
* fix(guardrails): return HTTP 400 for litellm content filter blocks

Align litellm_content_filter hard rejects with the standard guardrail block status code so clients receive 400 instead of 403.

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

* fix(guardrails): return HTTP 400 for custom code guardrail blocks

Pre-call custom code guardrail blocks now raise HTTPException(400) instead of using the passthrough ModifyResponseException path that returned a synthetic 200 response.

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

* fix(guardrails): preserve custom code passthrough blocks

Keep standalone custom code guardrail blocks on the passthrough contract while covering policy pipeline block handling for passthrough-style guardrail interventions.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 14:28:46 -07:00
Mateo Wang
152b1177e5
test(reasoning-effort-grid): cover Claude Opus 4.8 across provider routes (#29327)
* test(logging): align DB metrics event_metadata assertions with safe redaction

PR #28909 hardened log_db_metrics to emit a minimal, non-sensitive
event_metadata (only table_name when present, otherwise None) instead of
dumping function_name, function_kwargs, and function_args onto the span. The
test in test_log_db_redis_services was not updated and still asserted
"function_name" in event_metadata, which raised TypeError (argument of type
'NoneType' is not iterable) and turned the logging_testing CI job red on
litellm_internal_staging.

Update test_log_db_metrics_success to assert event_metadata is None when no
table_name is passed, and add test_log_db_metrics_event_metadata_is_safe as a
regression guard verifying that only the table name surfaces and that sensitive
kwargs (tokens, prisma client) are never dumped.

* test(bedrock): self-heal opus-4-7 grid cells when unentitled on CI

The bedrock-claude-opus-4-7 converse cells are unentitled on the Bedrock CI
account, so they were marked xfail. xfail keeps reporting them as expected
failures even after access is granted, so the wire translation never gets
verified again. Now the cell makes the call and skips only when Bedrock
replies "is not available for this account"; the moment the model is
entitled the same cells run their full assertions with no edit.

A focused unit test pins the tolerance predicate so any other failure still
surfaces loudly and the available path still runs the assertions.

* test(reasoning-effort-grid): add claude-opus-4-8 across provider routes

Adds claude-opus-4-8 to the anthropic, azure, vertex and bedrock-converse
routes (275 cells total) so the reasoning-effort wire translation is
covered for the new model. The bedrock opus-4-8 and opus-4-7 cells reuse
the self-heal path: they run the call and skip only on Bedrock's "is not
available for this account" reply, then assert in full once the model is
entitled. The azure and vertex opus-4-8 cells stay xfail until a Foundry
deployment exists and Vertex availability is confirmed. The shared
xhigh+max capability set is renamed to _CAPS_XHIGH_MAX now that more than
one model uses it.
2026-05-30 14:12:57 -07:00
yuneng-jiang
94a043efb2
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.
2026-05-30 14:04:22 -07:00
Mateo Wang
bfbb5d2375
fix(ci): make litellm_internal_staging green (logging test + Bedrock Opus 4.7 self-heal) (#29344)
* test(logging): align DB metrics event_metadata assertions with safe redaction

PR #28909 hardened log_db_metrics to emit a minimal, non-sensitive
event_metadata (only table_name when present, otherwise None) instead of
dumping function_name, function_kwargs, and function_args onto the span. The
test in test_log_db_redis_services was not updated and still asserted
"function_name" in event_metadata, which raised TypeError (argument of type
'NoneType' is not iterable) and turned the logging_testing CI job red on
litellm_internal_staging.

Update test_log_db_metrics_success to assert event_metadata is None when no
table_name is passed, and add test_log_db_metrics_event_metadata_is_safe as a
regression guard verifying that only the table name surfaces and that sensitive
kwargs (tokens, prisma client) are never dumped.

* test(bedrock): self-heal opus-4-7 grid cells when unentitled on CI

The bedrock-claude-opus-4-7 converse cells are unentitled on the Bedrock CI
account, so they were marked xfail. xfail keeps reporting them as expected
failures even after access is granted, so the wire translation never gets
verified again. Now the cell makes the call and skips only when Bedrock
replies "is not available for this account"; the moment the model is
entitled the same cells run their full assertions with no edit.

A focused unit test pins the tolerance predicate so any other failure still
surfaces loudly and the available path still runs the assertions.
2026-05-30 13:57:18 -07:00
Shivam Rawat
2d4c13c00f
fix(mcp): preserve omitted fields on PUT /v1/mcp/server partial updates (#29253)
* fix(mcp): preserve omitted fields on PUT /v1/mcp/server partial updates

Use model_dump(exclude_unset=True) for updates so schema defaults (transport=sse,
allow_all_keys=false, etc.) are not written when callers omit them. Serialize
JSON fields from the filtered dict and only force is_byok on create.

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

* fix(mcp): allow explicit alias=None on MCP server partial updates

Snapshot caller-provided fields before normalization so omitted alias is not written while an intentional alias=None still clears the stored value.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 12:03:22 -07:00
Shivam Rawat
ace3c65ab3
fix(mcp): preserve source_url in GET /v1/mcp/server list responses (#29249)
* fix(mcp): preserve source_url in GET /v1/mcp/server list responses

The list endpoint builds responses from the in-memory registry, but
source_url was dropped during the DB-to-registry roundtrip even though
GET /v1/mcp/server/{id} returned it correctly from the database.

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

* fix(tests/mcp): set source_url on MagicMock table records

MagicMock auto-creates source_url as a mock object, which fails MCPServer
Pydantic validation after source_url was wired through build_mcp_server_from_table.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 12:01:22 -07:00
Mateo Wang
f11c12d157
Revert "chore(tests): migrate Bedrock CI to AWS account 941277531214 (#28728)" (#29326)
This reverts the Bedrock CI account migration (#28728). The original account
(888602223428) was put under an AWS security restriction after a leaked key
and has since been reactivated, while the replacement account (941277531214)
lacks access to several models the suites exercise (legacy Bedrock Claude 3
models, Cohere, Nova Canvas image gen, Bedrock batch inference, and flagship
Opus). Pointing CI back at the reactivated account restores that coverage.

This is the exact inverse of #28728: all hardcoded 941277531214 references go
back to 888602223428 (provisioned/imported-model ARNs, AgentCore runtime ARNs
and their suffixes, batch execution role ARN, and the example proxy config),
the S3 buckets revert to litellm-proxy and load-testing-oct, the guardrail IDs
revert to wf0hkdb5x07f and ff6ujrregl1q, the SageMaker endpoint and Knowledge
Base revert to their original ids, and the live-call tests go back to the
legacy model strings. The grid_spec fail_reason workaround for the unentitled
Opus cells is dropped while keeping the unrelated bedrock_effort_ceiling field
added after the migration.

The CircleCI AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars still point at
941277531214 and must be set to the reactivated account's fresh credentials
separately via the CircleCI API; AWS_REGION_NAME stays us-west-2.
2026-05-30 11:26:24 -07:00
Shivam Rawat
967fed1fa1
feat(enterprise): add RESEND_FROM_EMAIL for self-hosted Resend sends (#28830)
Allow self-hosted installs to override the default LiteLLM sender address
via RESEND_FROM_EMAIL, matching SendGrid's SENDGRID_SENDER_EMAIL pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 10:09:05 -07:00
Sameer Kankute
4cc3dd7aad
feat(context_management): compact_20260112 polyfill for non-Anthropic providers (#28868)
* feat(anthropic/messages): in-gateway context_management polyfill for non-Anthropic providers

- Add `context_management/` module with `clear_tool_uses_20250919` editor
  dispatched before chat-completions translation on `/v1/messages`
- Hard-protect most-recently completed tool_result from being cleared
- Attach `context_management.applied_edits` to both non-streaming and
  streaming (final `message_delta`) responses
- Bedrock Converse: forward `context_management`; filter to
  `compact_20260112`-only edits with `compact-2026-01-12` beta header
- token_counter: guard Anthropic-format tools (no `function` key) to
  prevent AttributeError during polyfill token counting
- Streaming: handle empty-choices usage-only trailing chunks
- Skip polyfill when `litellm.drop_params = True`

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

* fix(bedrock): pop None context_management before sending to Bedrock Converse

If context_management is forwarded as None (e.g. when mapping returns
None for an invalid format), _filter_context_management_for_bedrock_converse
previously returned early without removing the key, leaving
"context_management": null in the request and causing a validation
error. Pop the key when the value is not a dict.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(bedrock/converse): pop None context_management; extract helpers to fix PLR0915

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

* fix(anthropic/messages): check per-request drop_params alongside global

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

* fix(anthropic/messages): preserve drop_params for downstream and respect explicit False

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix: lazy debug logging in clear_tool_uses; remove unused context_management constants

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic/messages): guard context_management polyfill with try/except

Wrap apply_context_management() in a try/except so any failure (e.g.
litellm.token_counter raising on an unknown tokenizer or unexpected
message format) is logged but does not crash the underlying LLM
request. The polyfill is a best-effort additive feature; on failure we
forward the original messages without applied edits.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(token_counter): guard None input_schema in Anthropic tool fallback

Use `or {}` instead of `.get(..., {})` so explicit null parameters do not
raise AttributeError when formatting function definitions for token counting.

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

* fix: minimize context_management polyfill threading

- Use None (not empty list) for polyfill_applied_edits when context
  management isn't requested, so semantics of 'feature not requested'
  vs 'feature requested but no edits applied' are distinct.
- In the streaming iterator, only pass applied_edits to the per-chunk
  translator on the final (finish_reason) chunk; intermediate chunks
  ignore it anyway, and this makes intent explicit on both sync and
  async paths.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(context_management): align tool_use counts and normalize list spec

- _count_tool_uses now requires a string id, matching _collect_tool_use_ids_in_order so the tool_uses trigger can't fire on blocks that aren't clearable.
- apply_context_management dispatcher now accepts the OpenAI list form and normalizes it via AnthropicConfig.map_openai_context_management_to_anthropic, so the polyfill path no longer silently no-ops on list input.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* feat(context_management): add compact_20260112 polyfill for non-Anthropic providers

Implements an in-gateway compaction polyfill that summarizes long conversations
using a configurable model when `compact_20260112` is requested for non-Anthropic
targets (e.g. OpenAI, Gemini), matching Anthropic's context management beta
behaviour for those providers.

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

* fix(compact): skip tool_result-only user turns; bedrock: elif for context_management

- compact_20260112 Phase D: when keeping the last user turn after a full
  summary, skip role=user turns whose content is exclusively tool_result
  blocks. Such turns translate to OpenAI tool-role messages with no
  preceding assistant tool_calls (those got summarized away), which
  non-Anthropic providers reject. Fall back to a synthetic continuation
  prompt if no eligible user question exists, so the downstream call
  always has a non-empty user message.
- bedrock converse: chain the context_management param as elif so it
  follows the same if/elif pattern as the surrounding thinking/
  reasoning_effort checks.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic): post-compaction question selection, system type, sync stream merge

- compact.py: select last user question from effective_messages (post-compaction slice) instead of raw messages, so prior summarized turns aren't reintroduced
- handler.py: widen _prepare_completion_kwargs system parameter type to Union[str, List[Dict]] matching PolyfillResult.system
- streaming_iterator.py: mirror async hold-and-merge logic in sync __next__ so context_management is attached to the final merged message_delta when stop_reason and usage arrive in separate chunks

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic/messages): apply context_management on sync path; clear held stop_reason chunk in async iterator

- Sync `anthropic_messages_handler` was silently dropping the
  `context_management` kwarg via `ANTHROPIC_ONLY_REQUEST_KEYS` after the
  polyfill was moved into the async handler. Bridge to the async
  dispatcher with `run_async_function` so `litellm.messages.create()`
  callers keep working (regressed e.g. `clear_tool_uses_20250919`).
- In the streaming iterator's `__anext__` `StopIteration` handler, clear
  `self.holding_stop_reason_chunk` after capturing it (matches `__next__`)
  so a subsequent call doesn't re-emit the same chunk.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(bugfixes): bedrock None context_mgmt; stream per-instance queue; sync polyfill; trailing-chunk passthrough

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic): silently drop trailing chunks after usage; remove dead _polyfill_result key

- streaming_iterator: in sync __next__, after the usage chunk has been
  merged and emitted, silently consume any trailing provider events
  via 'continue' instead of forwarding them through the queue. Trailing
  chunks would translate to content_block_delta or message_delta and
  violate Anthropic SSE ordering after the final message_delta. The
  async __anext__ already drops these via 'if not self.queued_usage_chunk:'
  gating, so this aligns sync and async behavior.

- handler: drop unused '_polyfill_result' from ANTHROPIC_ONLY_REQUEST_KEYS.
  PolyfillResult is passed as an explicit arg to the adapter methods, never
  through extra_kwargs, so the entry was dead code.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* refactor(anthropic): extract usage-merge helper; guard empty slice-only compaction result

- Extract the duplicated hold-and-merge usage logic from the sync __next__ and
  async __anext__ paths into a shared _merge_usage_into_held_stop_reason_chunk
  helper so the subtle cache-token / context_management attachment lives in
  exactly one place.
- In the compact_20260112 slice-only path, fall back to _select_last_user_question
  when _strip_compaction_blocks produces an empty list (e.g. messages ending on
  an assistant turn whose only content was the compaction block) so the
  downstream API never receives an empty messages array.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* refactor(anthropic/context_management): streaming iterator compaction fixes and compact polyfill improvements

- Extract usage-merge helper; guard empty slice-only compaction result
- Silently drop trailing chunks after usage; remove dead _polyfill_result key
- Fix bedrock None context_mgmt; stream per-instance queue; sync polyfill; trailing-chunk passthrough
- Apply context_management on sync path; clear held stop_reason chunk in async iterator
- Fix post-compaction question selection, system type, sync stream merge
- Skip tool_result-only user turns; bedrock: elif for context_management
- Add streaming iterator compaction test suite

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

* revert(html): restore flat *.html naming in _experimental/out

Reverses the accidental rename from *.html → */index.html introduced in
15ea941fbe. All 35 files moved back to their original flat paths so the
directory structure matches litellm_internal_staging.

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

* revert(config): restore proxy_server_config.yaml to litellm_internal_staging

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

* Fix: skip client compaction pre-processing when compact_20260112 polyfill will run

The _prepare_context_managed_request helper unconditionally applied
apply_client_compaction_block_history before invoking the polyfill. When
the request also configured a compact_20260112 spec, that pre-processing
consumed the client-sent compaction block and collapsed the message history
to just the latest user question, starving the polyfill of conversation
context. The polyfill's own Phase A (_slice_around_compaction_block)
already handles client compaction blocks correctly and inspects the full
post-compaction tail for the token-threshold check, so the pre-processing
is both redundant and destructive in this case.

Now the pre-processing only runs when no compact_20260112 polyfill spec
will execute (no spec, drop_params on, or only non-compact edits like
clear_tool_uses_20250919).

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic): plug compaction-block leak + iteration-usage gaps in streaming adapter

- handler: when polyfill_will_run skipped client-history pre-processing
  and the polyfill ultimately returned None (best-effort swallow on
  unexpected error), apply the slice-only fallback before returning so
  Anthropic-specific 'compaction' content blocks don't leak to non-
  Anthropic backends that would reject them.
- streaming_iterator: precompute will_merge_into_held so we don't pass
  applied_edits into the translator when the resulting processed_chunk
  will be discarded by the held stop-reason merge path.
- streaming_iterator: augment processed_chunk with iterations usage in
  the holding_chunk branch (sync and async) for parity with the other
  emission branches; ensures usage.iterations is attached on the rare
  message_delta-reaches-holding_chunk path.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic): correct streaming usage iteration + translate tools for token counting

- streaming_iterator: skip the trailing "message" iteration entry in the
  final message_delta when the held stop_reason chunk carries placeholder
  zero usage (no separate usage chunk arrived). Reporting zero tokens was
  misleading and inconsistent with the non-streaming path which always
  has real usage data.
- streaming_iterator: drop two redundant type checks inside branches
  that are already guarded by an outer message_delta type check.
- compact._count_effective_tokens: translate Anthropic-shaped tools
  (input_schema) to OpenAI shape before passing to litellm.token_counter
  so threshold checks aren't skewed by tokenizer paths that expect the
  OpenAI tool wrapper.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix lint

* fix(anthropic): plug content drop, compaction SSE shape, and compaction leak

- Sync streaming __next__ no longer drops a buffered holding_chunk when
  the usage-merge path has already fired. Restoring the prior unconditional
  flush behavior preserves provider-emitted content (the SSE-ordering nit
  of a trailing content delta is preferable to silent content loss).
- compaction content_block_start now carries the full block shape
  ({"type": "compaction", "content": ""}) to match the text-block
  pattern and Anthropic's native streaming shape, so clients that key off
  content_block_start see the field.
- apply_compact_20260112 now slices around / strips compaction blocks
  before the opt-in gate check. Previously, when summary_model was not
  configured the editor returned the raw messages, leaking Anthropic-only
  compaction content blocks to non-Anthropic providers that reject them.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic): resolve mypy types in context management polyfill

Use AppliedEdit and CompactionBlock consistently in the dispatcher and streaming adapter.

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

* fix(anthropic): flush held content chunk in async streaming path

Mirror the sync __next__ behavior: always flush a buffered
holding_chunk after the stream ends, even when usage was already
merged + emitted. Previously the async __anext__ kept the flush
inside the 'if not self.queued_usage_chunk:' guard, silently
dropping the last content delta on the proxy's primary path.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic adapter): correct sync streaming, surface polyfill failures, decouple sync path from proxy router

- translate_completion_output_params_streaming: add is_async flag so the
  sync handler returns Iterator[bytes] instead of an unusable
  AsyncIterator. Async callers keep the existing behavior via the
  default is_async=True.
- _run_polyfill_if_enabled: when the polyfill crashes and the spec
  requested non-compact edits (e.g. clear_tool_uses_20250919), raise an
  AnthropicContextManagementError instead of silently returning None so
  those edits are not dropped without an error surface. The
  compaction-block-slicing safety net remains for compact-only specs.
- anthropic_messages_handler (sync): stop auto-attaching the proxy
  llm_router. run_async_function bridges to a new thread's event loop;
  reusing the proxy's loop-bound httpx clients there causes
  'Event loop is closed' errors. The summary editor falls back to
  litellm.acompletion when llm_router is None.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix: address bug detection findings in token counter and streaming iterator

- token_counter: guard against non-dict 'function' field in tool dicts
  and skip tools missing a name to avoid emitting 'type None = ...' which
  would produce inaccurate token counts.
- streaming_iterator: change sync __next__ generic-error path to raise
  StopIteration (was StopAsyncIteration), so sync iteration cleanly stops.
- streaming_iterator: centralize context_management attachment so the
  held-stop_reason direct-flush path defensively re-attaches applied_edits
  to match the merge path's guarantee.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix lint

* fix: correct COMPACT_MIN_TRIGGER_TOKENS to 50_000

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix lint

* Fix lint

* Fix lint

* fix(compact): reduce to last user question when summary_model not configured but prior compaction block exists

Aligns the summary_model_not_configured path with the under-threshold and
client-compaction-block paths, which both reduce post-compaction messages
to just the latest user question so the downstream provider doesn't get
the summary on system prefix AND the full post-compaction history.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact): forward caller system prompt to summary model call

The default summarization instructions reference "the initial task above"
and "the raw history above", but the system prompt that holds that task
was not being forwarded to the summary model. The summary call now
prepends an OpenAI-shaped system message translated from the original
Anthropic-shaped system (str or content-block list) so the summarizer
has the agent role and initial task in scope.

* fix(compact_20260112): set default max_tokens and merge prompt when last turn is user

- Set COMPACT_SUMMARY_MAX_TOKENS default for the summary call so providers
  like Anthropic (which require max_tokens) don't silently fail and degrade
  to summary_call_failed.
- When the trailing translated message is already a user turn, merge the
  summarization prompt into it instead of appending a second user turn.
  Avoids consecutive role=user messages that strict providers reject.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic adapter): move current_content_block_start to __init__

Move the default TextBlock dict from a class-level attribute to __init__ so
concurrent stream instances don't share the same mutable dict. The class-level
default could be mutated in-place via tool_block['name'] = original_name in
_should_start_new_content_block, leaking state across streams. This mirrors
the existing fix already applied to chunk_queue.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact_20260112): surface error states + strip tool_result blocks in last user question

applied_edits_for_response() now includes compact_20260112 edits that
carry an error field (summary_model_not_configured, summary_call_failed,
summary_extraction_failed) so clients and operators can see why
compaction was requested but not applied.

_select_last_user_question() now strips tool_result blocks from mixed
[tool_result, text] turns rather than passing them through as-is. After
compaction the paired tool_use assistant turn no longer exists, so
forwarding tool_result blocks translates to orphaned role=tool messages
on non-Anthropic providers and produces a 400.

* fix(compact_20260112): carry prior compaction summary into Phase C summary call

When a request already contains a compaction block, Phase A slices
`effective_messages` to the turns since that block. Previously Phase C
passed the original `system` to the summary model, so multi-round
compaction silently dropped accumulated history each time the polyfill
fired. Pass `augmented_system` (original system + prior summary
prefix) so the summary model can produce a comprehensive summary that
incorporates both the prior round's context and the current slice.
`summarized_system` for the downstream call stays built from the
original `system` + new `summary_text`.

* refactor: delegate handler spec normalization to dispatcher

_normalize_spec_edits in adapters/handler.py duplicated the spec-shape
normalization already implemented by _normalize_spec in
context_management/dispatcher.py. The two could drift: a change in one
(e.g. supporting a new spec shape) without the other would cause the
handler's polyfill_will_run prediction to disagree with the
dispatcher's actual behavior, breaking the client-history pre-processing
skip.

Have the handler delegate to the dispatcher's _normalize_spec while
keeping handler-specific concerns (drop_params short-circuit, swallow
mapping exceptions) at the wrapper level.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact_20260112): surface warning-only applied edits in response

`applied_edits_for_response()` previously hid `compact_20260112` edits when
they had only warnings (no compaction block, no error). This dropped
diagnostically important warnings such as
`unsupported_trigger_type_X_using_input_tokens` and
`pause_after_compaction_ignored` whenever the conversation was under the
trigger threshold. Operators now see these warnings in the response.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix: address two low-severity context_management edge cases

- streaming_iterator: keep `sent_content_block_finish` in sync with the
  compaction block's emitted start/delta/stop lifecycle and reset it when
  the next text block's start is queued.
- bedrock _map_context_management_param: match dispatcher `_normalize_spec`
  behavior — only run the OpenAI→Anthropic mapper on list inputs; pass
  dict inputs through unchanged so already-Anthropic-format values aren't
  silently dropped.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact_20260112): use beta-header constant; require type discriminator; skip sync bridge when idle

- bedrock: replace hardcoded "compact-2026-01-12" beta string with
  ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value in both
  Converse (_filter_context_management_for_bedrock_converse) and Invoke
  (anthropic_claude3) compact-edit handlers.
- types: mark the "type" discriminator as Required[...] on the new
  CompactionBlock and UsageIteration TypedDicts so the discriminator
  is not silently optional under total=False.
- adapters/handler: short-circuit the sync /v1/messages adapter path
  before spawning the run_async_function worker-thread event loop when
  the request has no context_management spec and no client-sent
  compaction block in the message history.

Test plan:
- uv run pytest tests/test_litellm/llms/anthropic/experimental_pass_through/     tests/test_litellm/llms/bedrock/test_converse_context_management.py -q
  (370 + 10 = 380 passed)
- uv run pytest tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py     tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py     -k compact (3 passed)

* fix(compact_20260112): include system prompt tokens in threshold check

The threshold check in Phase B previously counted only message tokens and
the compaction-block content, omitting the system prompt entirely. When
the system carried a prior compaction summary (via _augment_system_with_summary)
or was otherwise large, the threshold could fire later than intended,
allowing the conversation to exceed the model's context window before
compaction activated.

_count_effective_tokens now also counts the (augmented) system prompt
text. The caller passes compaction_block=None when augmented_system
already includes the prior summary, to avoid double-counting.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix SSE ordering and compaction state machine bugs in AnthropicStreamWrapper

- Suppress holding_chunk flush after final message_delta has been emitted
  (queued_usage_chunk == True) so a trailing content_block_delta cannot
  follow message_delta, which strict Anthropic SDK clients may reject.
  When usage has not yet been merged, flush the holding_chunk *before*
  the held stop_reason chunk so SSE ordering remains correct.

- Replace _queue_compaction_block_events with _next_compaction_event,
  emitting the compaction start/delta/stop events one at a time. The
  state machine flags (sent_content_block_finish) and content block
  index now advance atomically with the terminal stop event actually
  being returned to the caller, eliminating the transient inconsistent
  state where flags say the block is finished while its stop event is
  still buffered.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact_20260112): enforce parent key/team allowlist on summary model

The compact_20260112 polyfill summary subrequest used llm_router.acompletion
directly, bypassing the proxy auth checks that gate model access for the
parent key/team. A caller whose key/team was not authorized for the
configured context_management_summary_model could still cause the proxy to
invoke that model and return its output as a compaction block.

Pull the parent's UserAPIKeyAuth out of litellm_metadata in the handler,
thread it through the dispatcher into apply_compact_20260112, and gate the
summary call on _can_object_call_model for both key-level and team-level
allowlists. Failures land as applied_edits[0].error =
summary_model_access_denied without raising. SDK callers (no UserAPIKeyAuth)
remain unaffected.

* fix(compact_20260112): distinguish access-denied from transient errors; greedy summary regex

- _check_summary_model_access now catches ProxyException explicitly for access
  denials and logs unexpected exceptions separately. Both still fail closed,
  but operators can now tell a denied key/team apart from a router internal
  raising during the check.
- _SUMMARY_TAG_RE switches from non-greedy to greedy so a stray </summary>
  inside the model's summary content no longer silently truncates the
  captured text.

* fix(compact_20260112): type object_type as Literal for mypy

* fix(compact_20260112): attribute summary subcall spend to parent key/team

The compact_20260112 polyfill summary subrequest propagated metadata via
the Anthropic-shape `metadata` parameter, which only carries `user_id`.
The proxy auth fields used for spend attribution (`user_api_key`,
`user_api_key_team_id`, `litellm_call_id`, ...) live in
`data["litellm_metadata"]`. As a result, summary subcalls landed on the
router with an empty propagated metadata and the resulting tokens were
not attributed to the caller's key/team budget.

Rename the polyfill chain's spend-propagation parameter to
`litellm_metadata` and pull it from `kwargs["litellm_metadata"]` in
both the async and sync handlers, so the post-call hooks see the parent
key/team and bill the summary tokens accordingly. Add an
`_extract_proxy_litellm_metadata` helper and refactor
`_extract_user_api_key_auth` to use it.

* chore(anthropic adapters): remove unused _extract_user_api_key_auth helper

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* chore(compact_20260112): non-greedy summary regex; use COMPACT_EDIT_TYPE in bedrock filter

- Make _SUMMARY_TAG_RE non-greedy so a response with multiple <summary>
  blocks captures only the first complete block.
- Replace the hardcoded 'compact_20260112' literal in
  _filter_context_management_for_bedrock_converse with the shared
  COMPACT_EDIT_TYPE constant.

* fix: bug fixes from PR review

- streaming_iterator: don't set sent_content_block_finish during compaction
  block lifecycle; that flag tracks the regular text/tool_use/thinking block
  state machine, conflating the two leaks bad state to introspection paths.
- compact._call_summary_model: send propagated proxy auth/spend-attribution
  fields as 'litellm_metadata' instead of 'metadata' so the router's post-call
  hooks attribute summary tokens to the caller's key/team budget.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(anthropic-streaming): insert content_block_stop between held delta and final message_delta

When the stream exhausts with both `holding_chunk` (a content_block_delta)
and `holding_stop_reason_chunk` (a message_delta) buffered, the after-loop
cleanup previously emitted them back-to-back, producing the invalid
Anthropic SSE sequence `content_block_delta -> message_delta`. Insert a
`content_block_stop` between them in both the sync `__next__` and async
`__anext__` paths so the emitted ordering remains
`content_block_delta -> content_block_stop -> message_delta`.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(compact_20260112): propagate allowed_model_region to summary subrequest

The router enforces region restrictions by reading allowed_model_region
from top-level request kwargs (Router._common_checks_available_deployment),
but the compact_20260112 summary subrequest only forwarded litellm_metadata.
A region-restricted caller could trigger compaction and have their conversation
summarized by a deployment outside the permitted region.

Extract allowed_model_region from user_api_key_auth and pass it through
_call_summary_model as a top-level kwarg so the router applies the same
region constraints the parent request would.

* fix(anthropic adapter): emit content_block_stop before held message_delta in drain paths

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* feat(context_management): configurable summary max_tokens; surface ignored knobs

- compact_20260112: read summary max_tokens from general_settings
  (context_management_summary_max_tokens) so operators can fit the
  chosen summary model's output budget; falls back to the compiled
  default for missing or invalid values.

- clear_tool_uses_20250919: log unsupported knobs at warning level
  (was debug, which silently dropped misconfiguration) and surface
  them as warnings on the AppliedEdit so clients see what was ignored.

* fix(compact_20260112): bound _call_summary_model with timeout

A slow or unresponsive summary model previously hung the parent
/v1/messages request with no escape hatch. Pass a 60s timeout on the
litellm.acompletion / llm_router.acompletion subrequest; on timeout the
existing summary_call_failed path forwards the request without
compaction rather than blocking indefinitely.

* fix(compact_20260112): preserve post-compaction tail on slice-only path

When a prior compaction block is present and the request is under threshold,
the polyfill was reducing downstream messages to just the latest user
question. The prior summary only covers turns before the compaction block,
so dropping the post-compaction tail silently lost recent context — a
multi-turn conversation that stayed below the threshold would arrive at the
model with no memory of any turn after the prior compaction.

Forward the already-stripped post-compaction tail unchanged on both the
under-threshold path and apply_client_compaction_block_history. Fall
back to _select_last_user_question only when the strip leaves nothing
for the downstream call to answer.

* fix(compact_20260112): enforce user/project/team-member model scopes on summary subrequest

The local gate previously only checked the parent key's and team's
allowed-model lists. A caller restricted by a personal user, project,
or per-team-member allowed_models scope could still trigger the
configured summary model and receive its <summary> output as a
compaction block, because llm_router.acompletion bypasses the proxy
common_checks path. Extend _check_summary_model_access to also load
the user_object, project_object, and team_membership and run the
matching allowlist check at each scope before invoking the summary
model.

* fix(compact_20260112): enforce summary model per-model budget and propagate budget metadata

* fix(compact_20260112): forward post-compaction tail when summary model unconfigured

* fix(anthropic endpoints): run failure hook on 500-level context management errors

* fix(compact_20260112): enforce summary model rate limit before summary call

* fix(compact_20260112): propagate end-user/project budget scope to summary call

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 09:20:05 -07:00
Yassin Kortam
3be3c1dea1
feat(otel): add team_metadata, http.route, and model names to inference spans (#29319)
Some checks are pending
Unit Tests: Proxy DB Operations / endpoints-and-responses (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 / 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
2026-05-30 00:54:30 -07:00
ryan-crabbe-berri
37e6e2da1c
test(e2e): assert internal-user navbar identity is scoped to that user (#29077)
* test(e2e): assert internal-user navbar identity is scoped to that user

The existing login.spec.ts only checks the admin's navbar identity.
This adds the symmetric check for the internal user — verifying the
account button + dropdown surface the internal user's email, id, and
role, and that no admin-scoped values leak through.

* test(e2e): harden navbar identity test per review feedback

Locate the user dropdown panel by a data-testid on the popupRender div
instead of Ant Design internal + Tailwind class names, so styling
refactors no longer risk breaking the identity-scoping assertions.
Source the seeded user emails/ids from shared constants (match seed.sql)
instead of hardcoding them inline.
2026-05-30 00:29:30 -07:00
ryan-crabbe-berri
892838963c
test(e2e): cover Internal User create-key flow when in no teams (#29083)
* test(e2e): cover Internal User create-key flow when in no teams

The seeded e2e-internal-user is in two teams, so the "no team" branch
of the Create Key modal — where the team dropdown must render empty —
was unreachable. Seeds a noteam@test.local user and adds a spec that
logs in fresh, opens the modal, and asserts the dropdown has zero
options.

* test(e2e): harden no-team dropdown assertion + add with-teams counterpart

Replace the one-shot count() check with a settled-empty assertion: wait for
the dropdown's loaded "No teams found" state before asserting zero options,
so the test can't pass on a transient empty frame while the team-options
request is still in flight.

Add internalUserWithTeams.spec.ts as the differential partner; it logs in as
the seeded e2e-internal-user (two team memberships) and asserts the dropdown
lists exactly those teams. Without it, the no-team spec's zero-options
assertion would still pass against a regression that empties the dropdown for
every user.
2026-05-30 00:26:28 -07:00
yuneng-jiang
12d29a38a7
tests(proxy_server): surface current behavior in tests (#29309)
* test(proxy/proxy_server): pin forwarding routes (PR2) (#28887)

* test(proxy): pin proxy_server.py forwarding-route behavior

PR2 of the proxy_server.py behavior-pinning project: fills the 12
forwarding-route test files added by the harness PR with happy + error
pins for all 52 LLM-facing routes (models, chat/completions, completions,
embeddings, moderations, audio, assistants, threads, utils, model-info,
model-metrics, queue). Every happy-path test asserts the full response
dict via normalize() so the gate enforces real shape pinning rather
than status codes.

* test(proxy): drop task-plumbing comments from PR2 test files

* test(proxy): tighten PR2 error-path status-code pins

Apply the same review feedback Greptile gave on PR1 (#28856) and PR3
(#28850) to PR2's forwarding-route tests:

- Replace permissive `>= 400` / `in (X, Y)` status assertions with the
  exact 500/405 the handler actually returns, so a regression that
  silently shifts the code now fails the pin.
- Add a body-presence check alongside each tightened status assertion
  to satisfy _pin_check.py's no-status-only rule.

---------

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

* test(proxy): pin proxy_server.py non-route surface behavior (PR1) (#28856)

* test(proxy): pin proxy_server.py non-route surface behavior (PR1)

Fills the 7 PR1 placeholder files under tests/test_litellm/proxy/proxy_server/
with behavior pins for the non-route surface of proxy_server.py:
lifecycle/init/shutdown, ProxyConfig class methods, DB-overlay config scrubbers,
spend counters, background-health helpers, OpenAPI customization, exception
handlers, and streaming-generator helpers.

233 tests cover 101 pin-list symbols (1+ happy + 1+ error each). New-tests-only
coverage on litellm/proxy/proxy_server.py: 32.80% line / 20.91% branch (PR1
gate: 25% line / 18% branch). Full directory runs in ~22s with -n 4.

Plan: https://www.notion.so/Plan-Pin-proxy_server-py-behavior-2026-05-25-36c43b8acdab81ee845fd5365128a2fc

* test(proxy): address Greptile review comments on test_lifecycle.py

- test_initialize_signature_is_async_with_expected_params: hard-code
  expected_param_count so a signature change actually trips the gate
  (previously both sides of the comparison were len(sig.parameters)).
- test_check_request_disconnection_invalid_when_connected_times_out:
  patch asyncio.sleep so the test no longer spins for ~1.2 s of real
  wall-clock; timeout lowered to 0.05 s.

---------

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

* test(proxy/proxy_server): pin control-plane routes (PR3) (#28850)

* test(proxy/proxy_server): pin misc routes (PR3, partial)

Adds happy + error tests for the misc control-plane routes:
GET /, /routes, /adaptive_router/state, /get_logo_url,
/get_image, /get_favicon.

Also gitignores .pin_list.txt (used by the pin gate).

* test(proxy/proxy_server): pin login/SSO routes (PR3, partial)

Adds happy + error tests for the 5 login/SSO control-plane routes:
GET /fallback/login, POST /login, POST /v2/login, POST /v3/login,
POST /v3/login/exchange. Mocks authenticate_user and
create_ui_token_object at their imported location.

* test(proxy/proxy_server): pin onboarding routes (PR3, partial)

Adds happy + error tests for the 2 onboarding control-plane routes:
GET /onboarding/get_token, POST /onboarding/claim_token. Wires a
MagicMock async context manager for prisma_client.db.tx() and
signs the onboarding JWT with the patched master_key.

* test(proxy/proxy_server): pin model_cost_map reload routes (PR3, partial)

Adds happy + error tests for the 5 model-cost-map control-plane routes:
POST /reload/model_cost_map, POST|DELETE|GET
/schedule/model_cost_map_reload(/status), GET /model/cost_map/source.
Attaches litellm_config to mock_prisma per-test (the table is not in
the default _PRISMA_TABLES fixture).

* test(proxy/proxy_server): pin anthropic_beta_headers reload routes (PR3, partial)

Adds happy + error tests for the 4 anthropic-beta-headers control-plane
routes: POST /reload/anthropic_beta_headers, POST|DELETE|GET
/schedule/anthropic_beta_headers_reload(/status). Stubs
db.litellm_config (not in default _PRISMA_TABLES) and monkeypatches
reload_beta_headers_config so no network calls fire.

* test(proxy/proxy_server): pin invitation routes (PR3, partial)

Adds happy + error tests for the 4 invitation control-plane routes:
POST /invitation/new, GET /invitation/info, POST /invitation/update,
POST /invitation/delete. Patches _user_has_admin_privileges /
_user_has_admin_view to avoid extensive get_user_object mocking.

* test(proxy/proxy_server): pin config CRUD routes (PR3, partial)

Adds happy + error tests for the 8 config-CRUD control-plane routes:
POST /config/update, POST|GET /config/field/update|info, GET /config/list,
POST /config/field/delete, POST /config/callback/delete,
GET /get/config/callbacks, GET /config/yaml. Attaches litellm_config
to mock_prisma per-test.

* test(proxy/proxy_server): tighten pin assertions per review

- test_routes_misc.py: `b"" in response.content` is trivially true;
  replace with `len(response.content) > 0` so an empty 405 body trips
  the gate.
- test_routes_login_sso.py: `len(response.content) >= 0` is trivially
  true; tighten to `> 0`.
- test_routes_anthropic_beta.py: replace brittle string-literal checks
  on the serialized JSON (`'"interval_hours": 12' in payload`) with
  `json.loads` + dict access so the assertion survives any serializer
  spacing.
- test_routes_config.py: `assert status_code in (404, 500)` was too
  permissive; the handler re-raises HTTPException(404) verbatim, so
  pin 404 strictly.

---------

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 23:17:24 -07:00