Commit graph

41067 commits

Author SHA1 Message Date
Yassin Kortam
08fa25042c
test(e2e): rename Gateway to ProxyClient and expose it as a session-scoped fixture (#33750)
The shared proxy wrapper in tests/e2e/e2e_gateway.py was misnamed: Gateway is
not a gateway server, it is the client every suite uses to talk to the proxy
(keys, models, chat/embed/ocr, spend read-backs, poll helpers). Rename the
module to proxy_client.py and the class to ProxyClient, with build_gateway
becoming build_proxy_client and the GatewayProvider protocol becoming
ProxyClientProvider. The .gateway attribute suites held is now .proxy. Only
identifiers changed; prose and string literals that use the word gateway for the
proxy-server concept were left alone.

Each suite previously built its own instance through a per-suite build_client()
that called build_gateway() inside, duplicating the proxy wiring across suites.
There is now one session-scoped proxy fixture in tests/e2e/conftest.py; every
suite's client fixture depends on it and injects it, so the wiring lives in one
place. claude_code keeps building its own client directly since it has its own
harness and does not use the shared fixtures.

Behavior is unchanged: shared transport, data-plane/control-plane split routing,
poll budget, typed request/response models, and resource cleanup all go through
the same object.
2026-07-18 18:41:18 +00:00
devin-ai-integration[bot]
6d5f24fe0b
docs(litellm-rust): add provider coding standards (#33833)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
2026-07-18 18:38:57 +00:00
yuneng-jiang
377d54e694
refactor(ui): migrate policy attachments table onto shared DataTable (#33827)
* refactor(ui): migrate policy attachments table onto shared DataTable

* refactor(ui): pass a specific success message to the attachment copy action
2026-07-18 11:37:39 -07:00
Yassin Kortam
e18966625d
feat(mcp): add ID-JAG (identity assertion authorization grant) support for MCP egress (#31516)
* feat(mcp): add ID-JAG egress auth as a v2 outbound-credentials arm

Adds the oauth2_id_jag MCP egress auth mode (draft-ietf-oauth-identity-assertion-authz-grant,
shipped by Okta as "AI agent token exchange") as a first-class arm of the v2
outbound_credentials resolver rather than a standalone v1 handler.

ID-JAG is a two-leg flow: an RFC 8693 token exchange swaps the caller's id_token for an
ID-JAG assertion at the IdP org authorization server, then an RFC 7523 jwt-bearer grant
presents that assertion to the MCP's resource authorization server for the access token
used to call the upstream. The gateway authenticates to both endpoints with a private-key
JWT client_assertion, falling back to client_secret when no key is configured.

The mode is modeled as IdJagConfig in the AuthConfig discriminated union, with client auth
as a ClientAuth tagged union (private_key_jwt or client_secret) so required fields are
enforced at construction and illegal states are unrepresentable. A new token_endpoint
collaborator performs the authenticated OAuth token-endpoint call and caches the result
with per-key single-flight; the resolver's _id_jag arm runs the two legs and returns an
httpx.Auth or a typed CredError. A missing caller identity token fails closed
(precondition_required), so an ID-JAG server never falls back to a static credential. The
v1->v2 adapter maps oauth2_id_jag servers onto IdJagConfig and the existing live v2 path
resolves them, so no standalone handler, has_id_jag_config flag, or resolve_mcp_auth
precedence branch is needed.

The ID-JAG client_private_key is encrypted at rest alongside client_secret.

* fix(mcp): sort token_endpoint imports to satisfy the I001 budget gate

* fix(mcp): give token_endpoint pyright suppressions reasons for the LIT004 budget

The freshly-merged base ratcheted the LIT004 ceiling down, so the six
unexplained pyright suppressions in token_endpoint.py went over budget.
Annotate each with why the boundary is untyped (litellm http handler and
InMemoryCache are untyped; response.json() is validated by
_TokenEndpointResponse in fetch) so the gate counts them as explained.

* fix(mcp): enforce ID-JAG exchange over caller auth overrides and redact token endpoint from client errors

For oauth2_id_jag servers the v2 resolver mints the upstream assertion from the caller's identity token; a caller-supplied x-mcp-auth / x-mcp-<alias>-authorization override or a conflicting injected Authorization must not disable that exchange and forward an arbitrary bearer, so IdJagConfig now joins authorization_code and token_exchange as a resolver-owned mode that keeps the v2 spec and ignores the override.

The token endpoint error branches previously returned the configured endpoint URL in the client-visible 503 detail. The endpoint now stays in server-side logs and clients get a generic token-exchange failure.

* fix(mcp): bind the ID-JAG token cache to the exchange config and map token endpoint network errors to typed CredErrors

* fix(mcp): fail closed when an oauth2_id_jag server is half-configured instead of deferring to v1 static credentials

* fix(mcp): evict the cached ID-JAG bearer on an upstream 401 so the retry re-exchanges

* fix(mcp): map an unsignable client assertion to a typed misconfigured error instead of an unhandled 500

* fix(mcp): redact credential fields from the server-registry debug dump
2026-07-18 11:36:25 -07:00
ryan-crabbe-berri
6f4f4f69df
refactor(ui): consolidate Add/Edit credential modals into one CredentialModal (#32572)
* refactor(ui): consolidate Add/Edit credential modals into one CredentialModal

AddCredentialModal and EditCredentialModal were ~90% identical: the same
provider select, ProviderSpecificFields, and submit/filter logic, differing
only in title, button text, edit-mode prefill, and the disabled credential
name. Replace both with a single CredentialModal driven by a mode: 'add' |
'edit' prop, and point the two call sites in credentials.tsx at it.

Removes ~120 lines of duplication and drops the no-explicit-any and
no-restricted-imports baselines. The two per-file tests merge into one
CredentialModal.test.tsx covering both modes (add: editable empty name;
edit: prefilled, disabled name; provider fields render).

* refactor(ui): derive credential name disabled state from mode, not data

The disabled flag on the credential name field was tied to whether
existingCredential?.credential_name is truthy, an artifact of the old
EditCredentialModal. Drive it from the isEdit flag like the rest of the
component so mode='add' with a stray existingCredential can't disable the
field and mode='edit' with an empty name can't leave it editable. Behavior
is unchanged for real call sites; adds a regression test for the edit-with-
empty-name case.

* refactor(ui): prefill credential form declaratively instead of via useEffect

The edit-mode form was seeded with an imperative form.setFieldsValue inside
a useEffect that also set React state (setSelectedProvider), an antd anti-
pattern carried over from the old EditCredentialModal. Both call sites mount
the modal fresh with existingCredential already present (conditional && plus
destroyOnHidden), so there is no 'prop arrives after mount' case to handle.

Replace it with antd's declarative initialValues on the Form and a lazy
useState initializer for the provider. Removes the effect, its
react-hooks/set-state-in-effect suppression and exhaustive-deps warning, and
one any cast; behavior is unchanged (edit now shows the real provider on
first paint instead of flashing the default). Existing tests cover prefill
and the disabled name field.
2026-07-18 18:24:03 +00:00
tin-berri
703327a544
Merge pull request #33768 from BerriAI/litellm_mcp_dcr_config_client_persist
fix(mcp): persist config.yaml DCR clients in a server-scoped store so refresh survives token expiry
2026-07-18 11:04:41 -07:00
devin-ai-integration[bot]
4a297dd611
fix(otel): restore proxy-level error.* attributes on v2 failure spans (LIT-4179) (#33664)
* fix(otel): restore proxy-level error.* attributes on v2 failure spans (LIT-4179)

* refactor(otel): narrow v2 failure hook return type to drop fastapi import (LIT-4179)

---------

Co-authored-by: yucheng-berri <yucheng@berri.ai>
2026-07-18 10:52:27 -07:00
devin-ai-integration[bot]
010b20072d
fix(router): enforce context-window pre-call checks for Responses API input (#33706)
* fix(router): enforce context-window pre-call checks for Responses API input

* test(router): cover _count_pre_call_check_tokens across API surfaces

* fix(router): count Responses instructions and skip pre-call token count when no input

* fix(router): forward Responses input into deployment selection for context-window checks

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-18 10:26:48 -07:00
tin-berri
3ba5266ab3
Merge pull request #33581 from BerriAI/litellm_lit4478_anthropic_auto_cache_ui
feat(ui): configure Anthropic automatic prompt caching from the Admin UI
2026-07-17 23:15:58 -07:00
devin-ai-integration[bot]
b3d05bd10b
feat(fireworks_ai): map litellm session id to x-session-affinity header for prompt caching (#33717)
* feat(fireworks_ai): map litellm session id to x-session-affinity header for prompt caching

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

* fix(proxy): normalize cached usage in spend logs

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

* fix(fireworks_ai): initialize chat config base class

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

* fix(fireworks_ai): normalize cached usage for spend logs

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

* fix(fireworks_ai): cover cached usage normalization

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

* fix(proxy): normalize cached usage in spend logs

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

* test(fireworks_ai): cover session id precedence

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

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 21:33:34 -07:00
devin-ai-integration[bot]
07e07e6e2b
fix(vertex_ai): exclude Gemini Google Search grounding tokens from input token billing (#33742)
* fix(vertex_ai): exclude Google Search grounding tokens from Gemini input token billing

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

* test(proxy): stub get_configured_token_limits on mocked routers

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 21:17:49 -07:00
yucheng-berri
f759c75466
feat: add Straiker guardrail integration (#33781)
* feat: add Straiker guardrail integration

Implements LLM security guardrails via Straiker with prompt and response inspection, multi-mode execution (pre_call, post_call), and configurable blocking or redaction of flagged content across providers, streaming, images, and tool calls.

* fix(guardrails): harden straiker source attribution and error-path consistency

Use the operator-configured source for Straiker application attribution instead of a caller-supplied agent_id metadata value, so a caller cannot spoof which application a detection is attributed to. Make _fail reuse _block so a post_call error raises ModifyResponseException like a deliberate post_call block rather than GuardrailRaisedException, and type the blocking helper as NoReturn so the type checker enforces that execution never falls through the BLOCKED branch. Serialize the webhook payload once and send it as raw content to avoid re-serializing on the size check and on every retry.

* fix(guardrails): read straiker config and metadata from all supported shapes

Handle a dict optional_params in _get_config_value so nested guardrail
settings loaded from YAML or the DB (timeout, unreachable_fallback, and
the rest) are applied instead of silently falling back to defaults;
previously only attribute-style access was supported. Build the webhook
metadata bag from the merged metadata so client tags stored under
litellm_metadata on routes like /v1/messages reach Straiker the same way
identity and application fields already do, and widen the internal-key
skip prefix to user_api so proxy-injected budget values are not
forwarded.

* fix(guardrails): fail safe on straiker interventions without redactions

Block instead of passing content through when Straiker returns
GUARDRAIL_INTERVENED without replacement texts, so a positive
intervention verdict can never silently forward the original flagged
content. Fix the streamed-request detection to read the request body
from proxy_server_request.body, where the proxy stores it, instead of a
top-level body key that is never populated; the previous fallback was
dead, so a streamed response whose stream flag was not lifted to the top
level would have been redacted rather than blocked while buffering
replayed the original chunks.

* revert(guardrails): restore straiker caller agent_id application attribution

Restore the original behavior where a request-scoped agent_id in metadata
sets the Straiker application source, falling back to the configured
source. This is the integration's intended per-application attribution;
litellm already resolves a key-owned agent_id ahead of any caller-supplied
value, so a configured key cannot be spoofed.

* revert(guardrails): restore straiker webhook metadata scoping

Restore the original behavior where the Straiker webhook metadata bag is
built from request-scoped metadata only. Forwarding litellm_metadata was
a scope change to what the integration sends to Straiker; keep the
author's intended scoping.

* fix(guardrails): keep proxy key material out of straiker webhook metadata

Widen the internal-key skip prefix from user_api_key_ to user_api so the
proxy-injected user_api_key hash and user_api_end_user_max_budget are not
copied into the Straiker webhook metadata bag. The narrower prefix missed
the bare user_api_key name, leaking the hashed key to the vendor. Keeps
the request-scoped metadata source unchanged.

---------

Co-authored-by: cs-mehta <chandra@straiker.ai>
2026-07-18 03:31:29 +00:00
devin-ai-integration[bot]
93afde8605
feat(proxy): add x-litellm-model-name response header with deployment model string (#33698)
The proxy already returns x-litellm-model-id (the deployment id) and x-litellm-model-group (the requested model-group alias), but never surfaces the concrete underlying model that served the request; the router rewrites the response model field to the group alias, so callers had no way to read the actual deployment model like anthropic/claude-haiku-4-5. Expose it as x-litellm-model-name, sourced from the deployment recorded in litellm_params metadata.

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 20:29:42 -07:00
tin-berri
3829fa3014
Merge pull request #33796 from BerriAI/litellm_fireworks_glm5p2_cache_read
fix(fireworks_ai): correct glm-5p2 prompt-cache read price to $0.14/1M
2026-07-17 20:13:04 -07:00
devin-ai-integration[bot]
8536e3b80e
fix(proxy): source /v1/models token limits from the cost map instead of Router.get_model_group_info (#33721)
* fix(proxy): source /v1/models token limits from cost map instead of Router.get_model_group_info

Resolves the per-model get_model_group_info fan-out on GET /v1/models
(and /models) that pegged the event loop on wildcard listings (#33636).
create_model_info_response now reads max_input_tokens/max_output_tokens
from litellm.get_model_info (the static cost map) rather than the router,
which aggregated and deepcopied every deployment in a group per listed
model.

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

* test(proxy): inject model-info lookup into create_model_info_response for deterministic coverage

Inject the cost-map lookup (defaulting to litellm.get_model_info) so the
except and max_output_tokens branches are exercised deterministically and
the token-limit tests no longer hardcode mutable cost-map values.

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

* feat(proxy): surface custom deployment token limits on /v1/models via cheap index lookup

Add Router.get_configured_token_limits, an O(1) model-name index lookup that
reads a concrete deployment's configured max_input_tokens/max_output_tokens
without triggering pattern matching or deep copies. create_model_info_response
layers this over the cost map so custom deployments absent from the cost map
still surface their limits, and admin-configured limits override cost-map
defaults, while wildcard-expanded names stay on the fast path.

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

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 20:04:18 -07:00
ryan-crabbe-berri
dbb5b813c1
test(e2e): budget reset diagonal for team, org, user, and #32005 team-member keys (#33771)
* test(e2e): budget reset diagonal for team, org, user, and #32005 team-member keys

Adds E2E-7/8/10/11 from the budget-level x key-kind coverage matrix: each budget
level serves traffic again after its budget_duration window elapses, walking the
same ladder as the enforcement diagonal. New registry rows and tests cover the
team, organization, and internal-user reset rungs, plus the #32005 interplay
where a team-member key frozen by its owner's user budget comes back when the
user's window renews; the bare-key and per-team-member rungs already had coverage

Each case isolates the cap to one entity, drives spend to a budget_exceeded
block, then polls past the window until a call succeeds, holding every refusal
as a budget block so a reset that no-ops (stays blocked forever) or crashes
(leaks a 5xx) fails the test. budget_duration becomes an optional param on the
budget_client create_team / create_user / create_org helpers

* test(e2e): fold the reset diagonal into test_budget_reset_e2e.py and address greptile nits

Move the team / org / user / #32005 reset cases out of the standalone
test_budget_reset_diagonal_e2e.py and into test_budget_reset_e2e.py, absorbing
the pre-existing bare-key reset into the same TestBudgetResetDiagonal spec class
so the whole reset ladder reads as one file (mirroring how the enforcement
diagonal lives in test_budget_enforcement_e2e.py) and the drive/poll helpers are
defined once instead of duplicated across reset files.

Greptile nits: bound the drive phase to under one window (12 attempts x 2s < 30s)
so a block is observed before the reset job can fire, and replace the bare assert
in the poll loop with a pytest.fail that prints the HTTP status, so a provider 429
or a crashed reset path is distinguishable from a budget block at a glance.

* test(e2e): trim reset diagonal docstrings back to the file's original style

* test(e2e): inline single-use drive-loop bounds

* test(e2e): cut the reset module docstring to one line

* test(e2e): make the org reset test wait for a scheduled window (bugbot)

/organization/new stores budget_duration without scheduling budget_reset_at, so
the reset job's NULL catch-up branch zeroes org spend on its first 5-10s tick;
the org reset test could pass off that catch-up instead of a real window roll
(tracked as LIT-4570). The test now reads the org's budget_id and polls
/budget/info until budget_reset_at is scheduled before driving spend, so the
recovery it observes can only come from a genuine window expiry. Verified live:
the org case now runs ~33s (a full window) instead of beating the rescheduler
2026-07-18 03:02:22 +00:00
devin-ai-integration[bot]
9b0a424000
fix(proxy): derive session id from Anthropic metadata.user_id for session affinity (#33723)
* fix(router): resolve Anthropic metadata session affinity

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

* fix(proxy): derive Anthropic session affinity metadata

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

* fix(proxy): support Anthropic metadata session objects

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

* fix(proxy): normalize Anthropic metadata user object

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 19:53:15 -07:00
yuneng-jiang
c8b36dc1d4
test(pricing): pin the realtime mode assertion to the bundled cost map (#33806)
test_get_model_info_reports_realtime_mode resolved gpt-realtime-mini through
litellm.get_model_info, which reads the cost map litellm fetches at import from
raw.githubusercontent.com/BerriAI/litellm/main. The mode=realtime retag from
#33728 is in this repo's json and its bundled backup but has not reached main
yet, so the test failed whenever the fetch succeeded and passed whenever the
runner was rate limited and litellm fell back to the backup, flapping the
Unit Tests: MCP, Secrets, Containers & Misc job on unrelated PRs

Resolve the lookup against the bundled backup instead, the way
tests/test_litellm/test_cost_calculator.py already does: force
LITELLM_LOCAL_MODEL_COST_MAP, rebind litellm.model_cost, and clear the
get_model_info lru cache before asserting so a remote-backed entry cached
earlier in the same worker cannot leak through, then clear it again afterwards
so no locally-backed entry outlives the test
2026-07-18 02:52:55 +00:00
Tin Chi Lo
99b85a3f2c fix(mcp): persist config.yaml DCR clients in a server-scoped store
Config.yaml-declared OAuth2 MCP servers using Dynamic Client Registration have no LiteLLM_MCPServerTable row, so the DCR persist path called update_mcp_server, which returns None for a missing row, then update_server(None), which dereferenced .approval_status and raised AttributeError. The exception was swallowed to a warning while /register still returned 200, so the minted client was never stored and every access-token expiry forced a full re-authorization

Persist the acquired DCR client (client_id, client_secret, token_endpoint_auth_method, redirect_uris, encrypted at rest) in a dedicated LiteLLM_MCPServerOAuthClient store keyed by server_id when the server has no row, overlay it onto the in-memory config server so the refresh_token grant can authenticate within the process, and rehydrate it when the registry syncs from the database (which runs after the DB connects, unlike config load) so restarts and other pods pick it up. The store is encrypted at rest and is re-encrypted by the master-key rotation path alongside the server rows, through a shared helper so the two sites cannot diverge. The DB-backed server path is unchanged, and guarding the None return removes the swallowed-crash footgun

Resolves the config.yaml DCR persistence regression introduced in v1.92.0 by #31912
2026-07-17 19:42:32 -07:00
Tin Chi Lo
47ba9e7612 fix(proxy): propagate the caching flag across workers via the safe-override allowlist
enable_anthropic_prompt_caching and anthropic_prompt_caching_ttl are set as live
litellm attributes on the worker that handles the UI save, exactly like
budget_exceeded_throttle_percentage, but they were missing from
LITELLM_SETTINGS_SAFE_DB_OVERRIDES, so a peer worker's config reload merged the DB
value without applying it to the live attribute and stayed stale.

Add both to the allowlist so they behave like the sibling field, and add
test_general_settings_ui_fields_are_db_overridable so the UI registry and the
override allowlist cannot drift again (the exact omission that caused this), plus
a regression test that the flag flips on a simulated peer-worker reload.
2026-07-17 19:38:42 -07:00
yuneng-jiang
6288f84977
Merge branch 'litellm_internal_staging' into litellm_fireworks_glm5p2_cache_read 2026-07-17 19:32:28 -07:00
yuneng-jiang
40e914cfa7
build(deps): bump mcp lock to 1.28.1 to clear image-scan findings (#33803)
* build(deps): bump mcp lock to 1.28.1 to clear image-scan findings

* build(deps): require mcp>=1.28.1
2026-07-17 19:28:21 -07:00
devin-ai-integration[bot]
a40206992e
fix(passthrough): stop classifying plain 'predict'/'search' paths as Vertex (#33658)
Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 19:20:00 -07:00
yuneng-jiang
c4ecdce7a2
chore: remove accidentally committed dist tarball and ignore dist/ (#33805)
dist/litellm-1.79.1.tar.gz (a 64-byte build artifact) was committed by
mistake. Release CI wipes dist/ before building, so it never affected
published artifacts, but it doesn't belong in version control. Add dist/
to .gitignore to prevent a repeat.
2026-07-18 02:15:23 +00:00
mubashir1osmani
13ecf55cd0
test(e2e): skip flaky OpenAI GPT cells; raise multi-window max_tokens (#33799)
OpenAI GPT-5.6 Claude Code cells burn minutes on CLI timeouts under the
full stage suite; gate them behind COMPAT_OPENAI_GPT_CELLS=1 like Mantle.
Multi-window budget e2e used max_tokens=1 which gpt-5.5 rejects mid-message
2026-07-17 19:07:18 -07:00
ryan-crabbe-berri
6a26a3aee7
test(e2e): a user's max_budget follows the person across personal and team keys (#33762) 2026-07-17 18:53:09 -07:00
ryan-crabbe-berri
0e03795013
test(e2e): a member's team budget cuts off only that member's key (#33718)
* test(e2e): a member's team budget cuts off only that member's key

* test(e2e): drop the float-formatted cap string from the member budget assert
2026-07-17 18:50:54 -07:00
yuneng-jiang
a4c9571181
test(proxy): make streaming-cancel mocks awaitable for the disconnect slot release (#33802)
PR #33736 made the shielded streaming cleanup await
proxy_logging_obj._arelease_max_parallel_requests_on_disconnect on the
client-disconnect path. The four streaming cancel and disconnect tests in
test_budget_reservation.py drive the generator with a bare MagicMock as
proxy_logging_obj, so the cleanup crashed with TypeError: object MagicMock
can't be used in 'await' expression, breaking proxy-infra CI on every PR

Give the mocks an AsyncMock for the release method and assert it is awaited
exactly once on each disconnect path, pinning the single-owner slot release
contract that PR #33736 introduced without test coverage
2026-07-18 01:50:20 +00:00
yuneng-jiang
967d934484
build(deps): allow redisvl, pypdf, and openapi-core on Python 3.14 (#33801)
Remove the python_version < '3.14' environment markers from redisvl,
pypdf, and openapi-core now that all three install and import cleanly
on 3.14. The relock is marker-only: no package version changed for any
Python branch, and the locked versions (redisvl 0.4.1, pypdf 6.13.3,
openapi-core 0.22.0) now serve 3.14 as well. semantic-router and
aurelio-sdk stay gated because every published release caps
python_requires below 3.14
2026-07-17 18:38:46 -07:00
ryan-crabbe-berri
577dd3b707
fix(ui): stop credential edit from persisting the masked api key (#33797)
Editing an existing LLM credential and changing only the api_base also
overwrote the stored api_key with its masked display value (e.g. sk****IA).
The edit form pre-fills fields from the credential the backend returns, whose
secrets come back masked, and the update handler sent every field straight
back; the endpoint then encrypted and stored the asterisks over the real key.

Run credential_values through stripMaskedSecrets before the PATCH so masked
placeholders are never sent, mirroring the guard the model edit form already
uses. The isMaskedSecret / stripMaskedSecrets helpers move out of
model_info_view into a shared utils module so both call sites share one
implementation.

Add a Playwright e2e that seeds a credential, edits only the api base in the
LLM Credentials tab, and asserts the outgoing PATCH no longer carries the
masked api_key while the new base persists.
2026-07-17 18:25:24 -07:00
yuneng-jiang
c725017ef9
chore(guardrails): remove docstring from singulr module for consistency (#33800) 2026-07-18 01:12:11 +00:00
Tin Chi Lo
d966122249 fix(fireworks_ai): correct glm-5p2 prompt-cache read price to $0.14/1M
glm-5p2 (and its fireworks_ai/glm-5p2 alias) carried cache_read_input_token_cost
of 2.6e-07, the GLM 5.1 rate; the entry was seeded from the wrong row. Fireworks'
standard serverless rate for GLM 5.2 is $0.14/1M = 1.4e-07, so every prompt-cache
hit was billed at nearly double the real rate.

Corrects the value in both the canonical map and the bundled backup. The existing
fireworks cost-calculator test now reads the cached rate from the map instead of
hardcoding it, so it tracks the shipped value.
2026-07-17 17:57:36 -07:00
yuneng-jiang
f3d20153b3
build(rust): raise pyo3 to 0.29 so the native bridge compiles on Python 3.14 (#33798)
pyo3 0.23.5 hard-caps the interpreter at Python 3.13, so building the
native bridge against a 3.14 interpreter aborts inside pyo3-ffi's build
script before anything links. This raises pyo3 and pyo3-async-runtimes
to 0.29 (currently the newest line, and the range starting at 0.26 that
supports 3.14) and migrates the three call sites whose APIs were renamed
across that range: Python::with_gil is now Python::attach and
Python::allow_threads is now Python::detach. On a GIL-enabled interpreter
those are pure renames with identical semantics, so behavior on 3.10
through 3.13 is unchanged

Verified by compiling the native module for cp313 and cp314 and driving
it directly on both interpreters: gil_stats reports exactly one GIL
release per sync OCR call and the async path completes, matching the
0.23.5 baseline. cargo fmt, clippy, and the workspace tests pass on both
3.13 and 3.14 with the lockfile locked, and the lock churn is confined to
the pyo3 crates

Part of #26343; addresses the pyo3 build failure reported in #33116
2026-07-18 00:40:15 +00:00
yuneng-jiang
b94311481e
fix(ui): migrate tag deletion to shared DeleteResourceModal (#33795)
The tag delete action moved into a Base UI dropdown menu when the tags
table was migrated onto the shared DataTable. That menu is modal by
default and holds a pointer-events lock on the page while it opens and
closes, which left the hand-rolled inline confirmation modal unclickable,
so deleting a tag stopped working

Replace the inline modal with the shared DeleteResourceModal, which
renders through an antd Modal portal that manages its own pointer-events
and z-index, matching every other table's delete flow. Add a deleting
loading state so the confirm button reflects progress and cannot be
double-clicked

Cover the wiring with a regression test that drives the delete flow
through the shared modal and asserts tagDeleteCall runs with the tag name
2026-07-17 17:33:36 -07:00
yuneng-jiang
966ff65fec
fix(anthropic): emit message_start once in Responses stream adapter (#32667) (#33793)
* fix(anthropic): emit message_start once in Responses stream adapter

* test(anthropic): cover response.created message_start guard branch

Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Napuh <55241721+Napuh@users.noreply.github.com>
2026-07-17 17:26:33 -07:00
yuneng-jiang
04a5ebb94d
chore(ci): merge oss branch (#33784)
* fix(embeddings): accept encoding_format='float' for vertex_ai/gemini embeddings (#33617)

OpenAI SDKs (and litellm's own client since ~1.84) send
encoding_format='float' by default, but the vertex embedding config only
supports ['dimensions'], so get_optional_params_embeddings raised
UnsupportedParamsError at the provider default value. Any
OpenAI-compatible client talking to a litellm proxy with vertex
embedding models got a 400 unless the operator set proxy-wide
drop_params: true.

Float lists are exactly what the vertex API returns, so the param is a
no-op: pop it before validation. Other values (e.g. 'base64') keep the
existing unsupported-param behavior (dropped with drop_params, raise
otherwise).

Fixes #33173

Co-authored-by: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(guardrails): add Singulr guardrail integration for LiteLLM gateway (#31302)

* singulr guardrail support for litellm gateway

* Update litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py

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

* fix comments

* improvement

* fix: resolve review comments and implement requested improvements

* fix:Guardrail bypass through uninspected messages

* fix:tool text scanning

* fix: Legacy function definitions bypass scanning by adding indirect message scaning

* chore: remove unintended basedpyright budget file

* fix:Response schema bypasses guardrail scanning (response_format.json_schema)

* chore: restore basedpyright-code-budget.json and update lint baselines

Restores the file deleted in c698b88686 to match upstream litellm_internal_staging.
Regenerates basedpyright and ruff-strict budget baselines via make lint-budget-update.

* fix: scan system messages as indirect prompt injection in Singulr guardrail

* chore: restore lint budget files to upstream baseline

* fix: resolve ruff UP006 and I001 violations in singulr guardrail

* Update litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py

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

* resolve review comments on Singulr guardrail

* fix: scan tool call results as indirect prompt injection in Singulr guardrail

* Apply suggestion from @greptile-apps[bot]

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

* minor

* formating fix

* refactor: shift extraction logic to singulr side

* refactor:keep precall hook only

* fix:formatting

* fix:linting

* improve config description

* Trigger CI

* fix

* fix:field description

* fix:errors due to change in field names

* style: apply ruff line-wrap formatting to singulr guardrail

* fix:exception

* fix:formatting

* fix playground

* improved

* Update litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py

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

* Update litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py

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

* fix

* fix ci issues

* remove uv.lock from pr

* fix

* fix:resolved comments

* chore: trigger CI

* remove uv.lock

* fix

* fix linting

* fix linting

* fix linting

* remove doc strings

* remove test fixes

* chore: retrigger CI

* change in singulr api contract

* remove some ut

* send litellm call_id to singulr

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: aniket-kardile <aniket.kardile@singulr.ai>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* Fix non-conformant UUIDv7 generation in native Opik integration (#31294)

create_uuid7() encoded the timestamp in units of 16 seconds instead of
milliseconds, so the top 48 bits came out ~4096x the real unix-ms. Opik's
backend validates the embedded UUIDv7 timestamp on ingestion (OPIK-7067);
the bad encoding decoded to ~year 2201 and every trace/span batch was
rejected with HTTP 400.

Rewrite create_uuid7() to be RFC 9562 conformant (top 48 bits = unix-ms),
using the standard library only so no new dependency is added. Add unit
tests covering UUIDv7 validity and millisecond timestamp encoding.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(proxy): expose uvicorn concurrency limit (#33077)

Expose uvicorn's limit_concurrency as a --limit_concurrency CLI flag and
LIMIT_CONCURRENCY environment variable. Uvicorn counts both active tasks and
accepted connections and returns HTTP 503 once the configured limit is reached.

Reject non-positive limits at CLI parse time and only add the setting to the
uvicorn startup arguments. Because idle connections also consume capacity,
deployments should use upstream connection/header timeouts and per-client
connection limits.

* test: reorder test_utils tail to keep the daily merge conflict-free (#33788)

The daily OSS branch and litellm_internal_staging each appended an
independent test block at the very end of tests/test_litellm/test_utils.py,
so merging the two collides on that shared end-of-file position even though
the additions are unrelated (this branch adds the vertex embedding
encoding-format tests; staging adds the per-model prompt-cache-minimum
tests). Moving this branch's new TestVertexEmbeddingEncodingFormat class
above test_gemini_image_models_do_not_support_reasoning, which both branches
share, gives the two additions different anchors, so git applies both
without a conflict and without pulling staging into this branch. Pure
reorder; no test bodies change

---------

Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com>
Co-authored-by: madan-singulr <150280287+madan-singulr@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: aniket-kardile <aniket.kardile@singulr.ai>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: Aliaksandr Kuzmik <98702584+alexkuzmik@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Salva Madrid <50212436+salvamadrid@users.noreply.github.com>
2026-07-17 23:22:13 +00:00
Yassin Kortam
89c87ae59a
test(e2e): mcp suite for key-without-access denial (#33752)
Add an e2e suite at tests/e2e/mcp/ that proves MCP authorization over the
api_key auth family. An admin registers an upstream MCP server through the
management API (POST /v1/mcp/server, persisted in the DB and picked up without
a restart) and queues its deletion. Two keys are created against that one
server: one granted access through object_permission.mcp_servers and one with
no MCP grant. The permitted key is a live control proving the upstream is
reachable and the tool is callable, so a denial on the ungranted key is an
authorization decision rather than a dead server. The denied key then sees
none of the server's tools on tools/list and is refused a tools/call with a
403 access_denied.

A deterministic self-hosted FastMCP upstream (add/multiply over
streamable-http) is added to the e2e compose stack so the suite runs offline
with a known tool set. KeyGenerateBody gains an optional typed
object_permission so the shared gateway can create a key with an MCP grant.
2026-07-17 16:04:43 -07:00
tin-berri
c5b4456401
Merge pull request #33153 from BerriAI/litellm_mcp_aggregate_outcomes
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
feat(mcp): per-server outcomes for aggregate tools/list and truthful single-server REST statuses
2026-07-17 14:25:55 -07:00
Yassin Kortam
45273f1943
refactor(e2e): remove bob_the_builder; drive remediation from a Grafana alert (provisioned outside the repo) (#33749) 2026-07-17 14:23:21 -07:00
Yassin Kortam
62207ac057
test(e2e): user budget across keys and team member budget isolation (#33745) 2026-07-17 14:22:32 -07:00
Yassin Kortam
71e0251341
refactor(e2e): replace bespoke result reporter with standard JUnit report (#33758)
* refactor(e2e): replace bespoke result reporter with standard JUnit report

tests/e2e/e2e_result_reporter.py hand-rolled a per-test logfmt emitter that
reimplemented outcome mapping, logfmt escaping, and node-id parsing to print one
E2E_RESULT line per finished test. Outcome, duration, and node id are all things
a standard pytest reporter already produces, so the only genuinely custom data is
the covers marker ids and the normalized package label

Delete the module and emit a standard pytest JUnit XML report (--junitxml)
instead, carrying the two custom signals as user_properties (JUnit <property>
entries) attached at collection time in pytest_collection_modifyitems, so they
land on every test on every outcome including skips and setup errors. The small
package/covers extraction lives in junit_properties.py and is unit tested plus
checked end to end against a real JUnit artifact in test_junit_properties.py

Shipping the JUnit report to Loki is a thin infra-side transform, documented in
grafana/status_history_panels.md

* chore(e2e): remove grafana status history panels doc and junit properties e2e test

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-17 20:53:22 +00:00
Tin Chi Lo
cf08c07fbb fix(mcp): key every caller-visible listing surface by the display prefix, never canonical names
Outcome keys in the tools/list _meta, the spend-log outcome and count maps, and the REST error
messages now all use get_server_prefix (alias, or the short prefix when that mode is enabled), the
same naming the caller already sees on tool names. Keying them by canonical server_name let an
authenticated caller enumerate internal server names and their health or auth state that the alias
and short-prefix schemes deliberately hide (Veria finding). One helper decides the key for every
surface; exception messages reaching the multi-server REST error list are mapped to their fault tag
with the display prefix instead of relaying exception text carrying canonical names. Server-side
logs keep the real names
2026-07-17 13:31:21 -07:00
Yassin Kortam
442fdc181e
docs(tests/e2e): align docs with the hard-fail-on-dead-proxy contract and scope the no-unit-tests rule (#33755)
The e2e docs claimed `e2e`-marked tests skip when no proxy answers the
liveness probe, but the harness has always hard-failed: conftest.py's
pytest_runtest_setup calls pytest.fail, its module docstring states
"hard failures only ... never skip", and logging/conftest.py forbids
skipping outright. Align the docs to the code so the single most
important contract reads the same everywhere; a dead proxy turns a run
red instead of being silently skipped and mistaken for a pass. The
per-suite conftest docstrings that described the shared hook as a
"proxy liveness skip" are corrected to "liveness gate" for the same
reason.

Also scope the no-unit-tests hard rule to what it means: never
substitute a unit test for e2e feature coverage, while explicitly
allowing tests that cover the harness itself (e.g.
coverage_registry/test_collector.py), which carry no e2e marker and
run whether or not a proxy is up.

No product code and no harness logic changed.

Resolves LIT-4554
2026-07-17 12:56:10 -07:00
Yassin Kortam
ad65cad820
test(e2e): delete unreferenced Grafana panel docs (#33743)
tests/e2e/grafana/status_history_panels.md was prose describing Loki/Grafana
status-history panels and LogQL queries. Nothing in the tree imports, reads, or
links to it; the e2e suite only emits the E2E_RESULT lines those panels consume
(tests/e2e/conftest.py, tests/e2e/e2e_result_reporter.py) and never depends on
this file. Dashboards drift when versioned as prose in the repo, so remove it;
if we want them versioned it should be dashboard-as-code in the observability
repo, not markdown here.
2026-07-17 12:29:20 -07:00
Yassin Kortam
ae92e511f1
fix(proxy): bill partial streamed spend when the client disconnects mid-stream (#33736)
* fix(proxy): bill partial streamed spend when the client disconnects mid-stream

* fix(router): guard FallbackStreamWrapper chunks alias for non-CSW streams

* fix(proxy): await disconnect billing dispatch instead of unrooted create_task

* fix(proxy): make disconnect slot release single-owner to avoid double release

* fix(proxy): use union syntax for disconnect cleanup params (UP045 budget)
2026-07-17 12:24:31 -07:00
Tin Chi Lo
4e5f488452 feat(ui): tighten the Prompt Caching descriptions
The toggle and ttl descriptions were a wall of text, with a panel intro that
mostly repeated the toggle description. Drop the intro and cut both descriptions
to one or two lines, keeping a one-clause note that the cache is shared across
callers on the same upstream credentials.
2026-07-17 12:16:09 -07:00
Tin Chi Lo
5de0340986 Merge origin/litellm_internal_staging into litellm_mcp_aggregate_outcomes
Conflict in _list_mcp_tools: staging (#33612) moved toolset-grant expansion into the shared
permission primitives and removed the _merge_toolset_permissions call; resolution applies that
removal to this branch's AggregateToolListing structure
2026-07-17 11:52:51 -07:00
ryan-crabbe-berri
7015bd2ea1
test(e2e): assert an org budget block is a 429 naming the organization (#33638)
* test(e2e): assert bare-key budget refusal is 429 and /key/info spend reaches the cap

* test(e2e): keep the bare-key budget assertion to the 429 refusal shape

* test(e2e): assert a team's max_budget blocks every key on the team

* test(e2e): focus the team budget case on the 429 blocking behavior

* test(e2e): assert an org budget block is a 429 naming the organization
2026-07-17 11:48:18 -07:00
yuneng-jiang
f9a217e45b
feat(router): add router plugin reference catalog (#33746) 2026-07-17 18:46:20 +00:00
Tin Chi Lo
73cbbdd51d feat(ui): move Anthropic prompt caching to its own Router Settings tab
Rather than mixing the flag and its ttl into the generic General settings table
(which also surfaced the confusing Not Set / In Config / In DB provenance badges),
give prompt caching a dedicated tab with a purpose-built toggle and ttl dropdown.

Each registry field gains an optional tab, surfaced as ConfigList.field_tab, so
the General tab renders the ungrouped fields and the caching fields render on
their own tab. The update, persist and reset endpoints are unchanged.
2026-07-17 11:38:24 -07:00