Commit graph

252 commits

Author SHA1 Message Date
Yassin Kortam
c16c2cd6ee
fix(proxy): mark session/SSO/SAML cookies Secure behind a TLS-terminating reverse proxy (#39391)
* fix(proxy): mark session/SSO/SAML cookies Secure behind a TLS-terminating reverse proxy

litellm only sees a plain-HTTP hop when TLS terminates at a reverse proxy,
so cookie Secure attributes previously derived from (or defaulted without
regard to) the literal request scheme could be dropped in production. The
token session cookie set by every login path never carried Secure/HttpOnly/
SameSite at all.

Adds IPAddressUtils.is_request_https, a single trust-aware resolver used by
every cookie-setting call site: PROXY_BASE_URL, then X-Forwarded-Proto only
from a configured trusted proxy (general_settings.use_x_forwarded_for +
mcp_trusted_proxy_ranges), then the literal scheme. An unconfigured or
untrusted caller cannot spoof the header to force Secure on.

Resolves LIT-6748

* fix(proxy): make the shared session-cookie helper public, type new test helpers

set_session_token_cookie is imported across modules (ui_sso.py ->
proxy_server.py), so the leading underscore was misleading and breached
basedpyright's reportPrivateUsage budget with zero headroom. Also adds
missing parameter/return type annotations to the new test helper functions
per repo convention.
2026-09-02 16:42:29 -07:00
Yassin Kortam
bc2370f3fd
test(proxy): verify NO_DOCS/NO_REDOC/NO_OPENAPI restrict every doc surface (#39378)
* test(proxy): verify NO_DOCS/NO_REDOC/NO_OPENAPI gate every doc surface

Confirms /docs, /redoc and /openapi.json are reachable by default and
each 404 with no schema in the body when NO_DOCS/NO_REDOC/NO_OPENAPI are
set, and that disabling them does not affect other routes.

* test(proxy): pin the production docs_url construction to the real helpers

Addresses Greptile 4/5 feedback: the disabled-path tests built a fresh
FastAPI() app, which could miss drift in the actual
FastAPI(docs_url=..., ...) call in proxy_server.py. This pins that
exact call site against _get_docs_url/_get_redoc_url/_get_openapi_url.
2026-09-02 16:06:04 -07:00
Yassin Kortam
6c5fb0ef6f
fix(proxy): build coordination Redis from REDIS_* env vars unconditionally (#39410)
Coordination Redis (spend counters, budget-window enforcement, and the
reset_spend cache-eviction broadcast) previously only attached when a
deployment set general_settings.coordination_redis or
litellm_settings.cache. Bare REDIS_HOST/REDIS_PORT env vars alone did
nothing, so a multi-replica proxy with no cache block got no cross-pod
coordination at all: a key reset on one pod never cleared another
pod's stale budget enforcement.

The inferred Redis is pinged before being adopted, and a malformed
REDIS_CLUSTER_NODES/REDIS_SENTINEL_NODES value is tolerated too: env
vars can be set for an unrelated reason with nothing reachable there,
and guessing wrong must not turn a previously harmless in-memory-only
proxy into one that fails to boot or raises on its next cache write.
2026-09-02 15:21:15 -07:00
Yassin Kortam
25991fe78a
feat(auth): enforce configurable password policy and SSO-only login (#39381)
Adds a configurable password-strength policy (default: min 12 chars,
upper/lower/number/special, all individually toggleable, floored at 8
so a misconfigured minimum cannot disable the length check, and
unicode-aware so an accented letter cannot satisfy the special-
character requirement) enforced on every path that sets a local
user's password: /user/update, /user/bulk_update, and the invitation
onboarding claim flow.

Adds general_settings.disable_password_login_when_sso_enabled, which
rejects username/password login on /login, /v2/login and /v3/login
(including the UI_USERNAME/UI_PASSWORD admin fallback) once ANY
configured SSO provider is FULLY ready: every companion secret/
endpoint an OAuth provider needs, checked independently per provider
so a stray leftover client id for an unused provider can't mask a
different, fully configured one; and for SAML, the optional
python3-saml runtime being importable, checked without letting a
fully-missing package's ModuleNotFoundError take down password login
itself. SSO becomes the enforced boundary for interactive UI access
without an incomplete, mixed, or half-installed SSO setup locking
every admin out or breaking login outright. Master-key API access is
untouched, and unsetting the setting plus a restart restores password
login as the documented recovery path.
2026-09-02 14:28:13 -07:00
devin-ai-integration[bot]
846900320e
feat(alerting): slack alerts for per-user daily/monthly spend thresholds and spend anomaly detection (#38438)
* feat(alerting): slack alerts for per-user daily/monthly spend thresholds and spend anomaly detection

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

* test(alerting): use specific ValidationError matches in config rejection test

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

* fix(proxy): tolerate mocked slack alerting args when scheduling user spend scan

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

* fix(alerting): reject non-finite values in user spend alert settings

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-01 15:09:03 -07:00
ryan-crabbe-berri
abfb6adc2b refactor(proxy): bound the budget window seed by time instead of request ids
The one-time seed for a budget window row subtracted the batch's own
LiteLLM_SpendLogs rows by request_id, and request_id is the client's
x-litellm-call-id whenever the response carries no id of its own. Carrying
that set through the queue meant an unbounded, client-controlled aggregate
that the commit-failure requeue kept alive across retries.

Every log row at or after a batch's earliest start is owed by an increment
that still reaches the row, so summing only rows before it needs nothing
from the request. That drops request_ids end to end and closes the
cross-pod double count the id list could not see.
2026-08-29 17:13:26 -07:00
ryan-crabbe-berri
8463cb901e Merge remote-tracking branch 'origin/litellm_window_spend_writer' into litellm_window_spend_reader 2026-08-29 12:29:14 -07:00
ryan-crabbe-berri
2fac72392a Merge remote-tracking branch 'origin/litellm_window_spend_schema' into litellm_window_spend_writer 2026-08-29 12:27:58 -07:00
ryan-crabbe-berri
e8994e8ce1 Merge branch 'litellm_window_spend_writer' into litellm_window_spend_reader 2026-08-29 11:49:42 -07:00
ryan-crabbe-berri
4e22a5ef5a Merge branch 'litellm_window_spend_schema' into litellm_window_spend_writer 2026-08-29 11:47:37 -07:00
mateo-berri
6d3e687ce4 fix(db): let the writer pin yield to the replica while the writer is degraded 2026-08-29 11:44:45 -07:00
mateo-berri
a8c36e8307 Merge remote-tracking branch 'origin/litellm_internal_staging' into devin_ai_fix_model_new_read_replica_lag_38556 2026-08-29 11:29:07 -07:00
ryan-crabbe-berri
7745fe887f Merge branch 'litellm_window_spend_writer' into litellm_window_spend_reader 2026-08-29 11:06:05 -07:00
ryan-crabbe-berri
041cae8280 fix(proxy): bound the window spend seed exclusion to the batch's own start time
request_id can be chosen by the client through x-litellm-call-id, so an
unbounded NOT (request_id = ANY(batch)) let a replayed old id drop that id's
historical LiteLLM_SpendLogs row from the one-time seed while its increment
still landed. The increment now carries the request start, the batch keeps
the earliest one, and the seed only excludes ids whose startTime is at or
after it.
2026-08-29 11:05:57 -07:00
mateo-berri
acb621c35b Merge remote-tracking branch 'origin/litellm_internal_staging' into devin_ai_fix_model_new_read_replica_lag_38556 2026-08-29 10:54:46 -07:00
ryan-crabbe-berri
9beb5ead4d fix(proxy): keep the exact model= DB predicate within the type-discipline budget
The where clause now uses the exact name string directly and skips the DB query when the typed search cannot occur in that name, so no new mutable literals are added (LIT002 gate).
2026-08-29 10:40:39 -07:00
ryan-crabbe-berri
3e99ee8d0e fix(proxy): scope the DB-side model search by the exact model= filter
With model=<group>&search=<term>, the router list was narrowed to the group but the DB query only matched the substring, so other groups' rows leaked into the page and total_count.
2026-08-29 10:32:29 -07:00
ryan-crabbe-berri
76e7bd41f4 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fallback_access_group_check 2026-08-27 17:29:08 -07:00
Devin AI
57d8ae9d17 Merge remote-tracking branch 'origin/litellm_internal_staging' into devin_ai_fix_model_new_read_replica_lag_38556 2026-08-27 22:59:54 +00:00
Devin AI
63762b8ee0 fix(proxy): pin model reconcile read to the writer DB
The router reload triggered by /model/new read the model table through
the read replica, so a lagging replica made the reload miss the just
committed row and fail the request with a 500 even though the write was
durable. Fixes #38556

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-27 22:31:58 +00:00
ryan-crabbe-berri
3ea501430b fix(router): authorize config-level fallback targets against the calling key
Router fallbacks configured in router_settings were attempted without
re-checking whether the calling key could use the fallback model, so a key
limited to one access group was served by any model listed as a fallback
for something it could call. Auth only validated the requested model and
fallbacks sent in the request body.

Add a fallback_access_check predicate to Router, consulted before every
cross-model-group fallback attempt; rejected targets are skipped and the
primary's own error is raised when none remain. The proxy injects a check
that runs the same key, team and project model access checks the requested
model goes through.
2026-08-27 14:08:51 -07:00
Devin AI
09ff5bf6cd Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_deflake_20260821 2026-08-27 09:17:37 +00:00
Mateo Wang
aedaf4d0b0
Merge pull request #38434 from BerriAI/litellm_propagate_prompt_deletes
fix(prompts): propagate prompt deletes to every worker and pod
2026-08-26 21:03:00 -07:00
mateo-berri
4fd7b9946f fix(prompts): keep prompts created mid-sync out of the deleted-row sweep 2026-08-26 18:22:32 -07:00
mateo-berri
26b7bc3583 fix(prompts): propagate prompt deletes to every worker and pod 2026-08-26 18:01:08 -07:00
tin-berri
1df25e26cf
revert(proxy): remove router_model_name from auto-routed response bodies (#38429)
Reverts #37725. The field existed so SDK callers that cannot read
`x-litellm-model-id` could tell which tier an auto-router picked, and the
framework that motivated it was LangChain. `@langchain/openai` builds
`additional_kwargs` and `response_metadata` from fixed key allowlists and drops
unknown fields at both the chunk top level and inside `delta`, so no
proxy-side placement of a namespaced key can reach a LangChain caller.

The complexity router's existing `return_raw_model_name` already covers that
case: it puts the resolved model in the standard `model` field, which
LangChain does propagate (`model_name` is on its metadata allowlist), and the
proxy honors it on both the streaming and non-streaming paths.

Keeps the unrelated cleanup from #37725 that dropped the redundant
function-local `ProxyBaseLLMRequestProcessing` import shadowing the
module-level one in `async_data_generator`.

`TestModelGroupAliasReachesPreRoutingStrategies` asserted on the marker as a
proof of strategy dispatch; the surviving `response.model == "gemini-flash"`
assertion already proves it.
2026-08-26 17:58:30 -07:00
Mateo Wang
53a607e088
Merge pull request #38411 from BerriAI/litellm_fix_prompt_patch_sync
fix(prompts): propagate PATCHed prompt templates to every worker and pod
2026-08-26 17:20:27 -07:00
mateo-berri
5461bb3b48 fix(prompts): sync only the newest row when environments share a versioned prompt id 2026-08-26 17:00:24 -07:00
mateo-berri
6df307fef8 fix(prompts): validate a prompt replacement before swapping and isolate per-row sync failures 2026-08-26 15:49:31 -07:00
mateo-berri
fd751a5023 fix(proxy): key lazy openapi stubs off registered features, not sys.modules 2026-08-26 15:18:55 -07:00
mateo-berri
5130bafda8 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_prompt_patch_sync 2026-08-26 15:18:11 -07:00
mateo-berri
b687fe2b50 fix(prompts): propagate PATCHed prompt templates to every worker and pod 2026-08-26 15:03:32 -07:00
mateo-berri
3b9f6ee2aa fix(proxy): apply empty DB router_settings lists only where yaml sets no value 2026-08-26 14:37:29 -07:00
mateo-berri
6d1b295dae fix(proxy): stop empty DB router_settings lists from clobbering yaml fallbacks 2026-08-26 13:58:00 -07:00
mateo-berri
7965cfdd2b fix(proxy): keep listing a keyless user's own models when their user row is missing
Keys minted by /key/generate get no LiteLLM_UserTable row, so /v2/model/info?user_models_only=true for such a user hit the new None guard and returned 400 where the merge base returned the user's own models. Skip the team-model merge for a missing row instead of raising, since a user with no row belongs to no team
2026-08-25 16:51:03 -07:00
mateo-berri
4582496c8a Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_decrease_anys_opus5_round2
# Conflicts:
#	basedpyright-code-budget.json
#	litellm/proxy/auth/user_api_key_auth.py
#	litellm/proxy/management_endpoints/team_endpoints.py
#	litellm/proxy/management_helpers/utils.py
#	ruff-strict-budget.json
#	tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py
#	type-discipline-budget.json
2026-08-25 16:17:29 -07:00
Yassin Kortam
104fe73113
fix(dashboard): don't show a stale provider prompt-cache chip on a response-cache hit (#37951)
* fix(dashboard): don't show a stale provider prompt-cache chip on a response-cache hit

The playground's non-streaming chat completion and responses paths replayed a cache hit's original usage payload verbatim, so ResponseMetrics kept rendering the provider's prompt-cache-write/read chips using token counts from the original request. Detect the hit via the x-litellm-cache-key response header and render a Response Cache indicator instead.

* fix(dashboard): expose x-litellm-cache-key through CORS for the playground cache-hit indicator
2026-08-25 13:07:09 -07:00
Yassin Kortam
1d695a714b
fix(proxy): reset a stuck team member's budget (#37971)
* fix(proxy): reset a stuck team member's budget

A per-team-member budget check reads a cross-pod spend counter that
nothing ever invalidates. Once a member exceeds their per-member
budget, resetting the key's spend, raising the user's or the team's
own budget, or issuing a new key all leave the member stuck, because
none of them touch this counter or its cached membership object.

Add POST /team/{team_id}/member/{user_id}/reset_spend to reset a
member's tracked spend, and invalidate the same cached state from
/team/member_update when it raises a member's own budget, so that
path also takes effect immediately instead of waiting on the
membership cache's TTL. Name the entity in the check's error message
so a stuck member is diagnosable from the 429 body alone.

* fix(proxy): close reset-vs-floor-read race and surface double Redis write failure on member spend reset

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

* fix(proxy): broadcast spend reset as a SET so the handler's self-delivered message cannot erase the reset guard

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

* fix(proxy): omit null fields from the invalidation message so plain evictions keep the old wire format

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-08-25 09:50:09 -07:00
mateo-berri
9dabd72f2d refactor(repositories): type prisma table access with one generic protocol
Every repository handed its `.table` back untyped, so a dozen modules had
each grown a private `_PrismaTableActions` Protocol to paper over it. They
had drifted: some declared `update` as returning the row, others the row or
None, and none agreed on whether `find_many` was covariant

Replace all of them with a single `TableActions[RowT_co]` in
`litellm/repositories/prisma_protocols.py`, keyed to the prisma row each
repository is bound to. Query inputs stay `Mapping[str, object]` so callers
keep passing plain dicts, and `find_many` returns `Sequence` so the row type
stays covariant

Typing the nullable returns honestly surfaced paths that were already
crashing. A team admin could never edit or delete a memory entry owned by
their team: the write-auth check fed a raw prisma row to a helper that
expects the domain model, so `members_with_roles` arrived as plain dicts and
the request died as a 500 instead of applying the edit. Non-admin members hit
the same 500 in place of the 403 they were owed, so refusal and breakage were
indistinguishable. `/v2/model/info?user_models_only=true` dereferenced a
missing user row rather than returning the 400 the route already had, three
team routes dereferenced a team deleted between the read and the write, and
the agent registry dereferenced a missing agent instead of naming it

basedpyright drops 2,132 errors, 1,454 of them reportAny and 73
reportExplicitAny. The dashboard's generated types pick up `string[]` where
they had `unknown[]` for a team's members, admins and models
2026-08-25 12:14:17 +00:00
Devin AI
3cd49b9e0c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_deflake_20260821 2026-08-23 09:23:30 +00:00
yuneng-jiang
6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* test: drop the cwd-relative sys.path.insert calls from the test suite

TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.

Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.

Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.

* test: drop the duplicate imports the sys.path sweep exposed to F811

* test(pre-call-utils): restore the os import the new bedrock tests need
2026-08-22 09:25:58 -07:00
ryan-crabbe-berri
91599aef69 test: say whether a match= pattern is a regex or a literal (ruff RUF043) 2026-08-21 16:25:33 -07:00
ryan-crabbe-berri
ed02a121dd
Merge pull request #37878 from BerriAI/litellm_ruff_no_duplicate_definitions
test: enforce F811 so a duplicate definition cannot silently replace the first
2026-08-21 12:49:43 -07:00
ryan-crabbe-berri
e9d40a8f73 test: enforce F811 so a duplicate definition cannot silently replace the first
A name bound twice keeps only the second binding. In `tests/` that is nearly
always a repeated import, harmless but misleading, and the same rule is what
catches the cases that are not harmless: a local that shadows an import the
module still calls, and a second `def test_x` that quietly replaces the first.

311 of the 344 sites were repeated imports and came out with ruff's own fix.
The remaining 33 needed a decision. Four modules imported a name they never
used because a local definition below already shadowed it. Two comprehensions
bound `call` over `unittest.mock.call`, which those modules import and use.
One test rebound the two module handles its nested reload closure had captured.
One class attribute shadowed an unused `status` import.

The load-test fixtures move to a conftest, which is how pytest is meant to share
them, so the test module no longer imports three fixture names it never calls.
The nine `prisma_client` parameters keep a narrow `noqa`: pytest resolves that
fixture by name before the body runs, so the parameter never shadows anything.
2026-08-21 12:06:19 -07:00
tin-berri
f9a8c96b82
feat(proxy): add router_model_name to auto-routed response bodies (#37725)
The auto-routed model group was only reachable through the
x-litellm-model-id response header. SDK and framework callers that do not
expose response headers had no way to read it, and under streaming there
was no body surface at all.

The response body `model` field is deliberately restamped back to the
client-requested alias on both paths, which is correct OpenAI semantics,
so this adds a separate namespaced `router_model_name` key instead of
redefining `model`. The key is written on non-streaming bodies and on
every SSE chunk, including the streaming fast path, and is emitted only
when an auto-routing strategy actually selected the deployment.

After a mid-stream fallback moves the request off the group the router
picked, the key is omitted rather than continuing to claim the original
tier. The router marker already supports per-chunk fallback signals via
`x-litellm-attempted-fallbacks` headers; this wires that signal into
the gate so no stale tier is claimed after a fallback fires.

Also removes a redundant function-local import in the streaming
generator that shadowed the module-level one for the whole function.
2026-08-21 10:12:09 -07:00
Devin AI
ee7203281b fix(ptu): take the router as an argument instead of the proxy module global
The rollup read litellm.proxy.proxy_server.llm_router out of sys.modules, so a run
priced and swept whatever deployments anything else in the process had left on that
module. Under xdist the shard's module-to-worker assignment varies per run, which made
three rollup tests fail or pass on the same commit depending on ordering.

Callers now hand the router in, and the proxy's scheduled job passes its own.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-21 12:39:57 +00:00
ryan-crabbe-berri
b76def0e5d
test: require a match= on broad pytest.raises, and drop duplicate parametrize cases (#37769)
`pytest.raises(Exception)` with no `match=` passes on any error that broad. A
TypeError from a refactor, a botched fixture, an import that moved: all of them
read as the rejection the test claims to police, so the test goes green for the
wrong reason and stays green after the behaviour it guards is gone.

PT011 closes that gap for the 317 sites B017 could not reach, because B017 only
fires on a single-statement body with no `as e` binding. Each pattern here is the
message the code actually raised, recorded by running the sites under a plugin
that logged the concrete type and text per call site, so the assertions describe
observed behaviour rather than a guess. Where a site raises more than one message
across its parametrize cases, the pattern is an alternation of what was seen;
where the exception carries an empty `str()` and puts the text on `.message`, the
site keeps a narrow `noqa` with the reason.

PT014 removes four parametrize cases that were listed twice. The duplicate re-runs
an assertion that already passed, and it usually marks a case someone meant to
vary and forgot to edit.
2026-08-20 20:24:49 -07:00
mateo-berri
f8a23aab09 fix: gate guardrail read-through to active rows and serialize it with the reload reconcile 2026-08-19 01:46:33 -07:00
mateo-berri
790d645d34 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_replica_registry_read_through
# Conflicts:
#	tests/test_litellm/proxy/test_proxy_server.py
2026-08-19 00:48:03 -07:00
mateo-berri
afeed48a70 fix(proxy): serialize read-through with reloads, gate db object types
The model resync now mutates the router under MODEL_RECONCILE_LOCK, and the
agent resync shares the new AGENT_RECONCILE_LOCK with the periodic agent
reload, so a reconcile built from a pre-write DB snapshot can no longer evict
or duplicate what a read-through just registered. Every resync checks
should_load_db_object for its object type, keeping read-through consistent
with what the replica is configured to load, and the a2a raise sites tag
ProxyModelNotFoundError as non-retryable so an agent miss no longer burns the
model resync budget.
2026-08-18 22:43:44 -07:00