Commit graph

4151 commits

Author SHA1 Message Date
Mateo Wang
52fc276f05
Merge pull request #32587 from BerriAI/litellm_fix_batch_model_access_hash_32580
fix(auth): resolve managed batch/file deployment model_id to model name for team access checks
2026-07-29 18:43:39 -07:00
ryan-crabbe-berri
581f5c319e
feat(cli): read base_url from persistent config file (#35015)
* feat(cli): read base_url from persistent config file

Adds a lite config command group (set/get/unset) backed by
~/.litellm/config.json so users no longer need to export
LITELLM_PROXY_URL in every shell session. Resolution order is
--base-url flag, then LITELLM_PROXY_URL, then the config file,
then the localhost default. A config-file base_url counts as an
explicit server choice for lite auth print-token, matching the
env var semantics it replaces.

* fix(cli): harden config persistence after review feedback

Rejects base_url values containing a query string or fragment,
including bare trailing ? or # which parse as empty but still
corrupt every joined request URL. Writes config.json and token.json
atomically through a shared write_private_json helper (0600 at
creation, fsync, os.replace) so an interrupted save can no longer
truncate the file or leave it world-readable. Warns on stderr when
an existing config file is invalid instead of silently ignoring it,
including invalid UTF-8. Resolves the eager --version flag through
the same env, config file, default chain as every other command,
and reads the config file once per invocation so base_url and
base_url_explicit always come from the same snapshot.

* fix(cli): resolve --version after option parsing

The eager --version callback ran before --base-url and --api-key were
parsed, so it could not see an explicitly named server. Combined with
the env fallback added for config-file support, that sent the resolved
API key to whichever server the config file pointed at even when the
user named a different one on the command line. Making the flag a
normal option and handling it in the group callback gives the version
request the same flag, env, config, default precedence as every other
command, and lets the stored-token lookup stay origin-checked.
2026-07-29 18:25:05 -07:00
Tin Chi Lo
7041f5768f fix(mcp): never write discovery results to the row, heal rows a release already stamped, and retry failed discovery with backoff
An interactive oauth2 MCP server created with explicit endpoint URLs and no issuer served
400 "authorization url is not configured" from /authorize about a minute after creation,
with the admin's endpoints intact in the row the whole time (#34985). Discovery wrote its
trust-on-first-use issuer into the same column an admin writes, so the next registry build
read the gateway's own output back as an admin pin, anchored the server to RFC 8414
section 3.3, and discarded the stored endpoint columns; one transient metadata fetch
failure then had nothing to serve, and the reload fast path pinned the broken entry until
an unrelated config write

The core of the fix is a deletion. The gateway no longer writes discovery results anywhere:
the OAuth columns and credentials.scopes carry admin intent alone, and everything discovery
learns lives on the in-memory registry entry, as the existing carry-forward already
assumes. With no gateway write there is no value whose provenance a later build can
misread, so the accidental anchoring cannot be expressed

Deleting the write cannot fix a row a released version already stamped, which still reads
as pinned, so a one-time startup heal clears those stamps. The signal is necessarily a
heuristic: updated_by records only the most recent writer and no audit trail says which
field it touched. A row is therefore healed only on the full signature of the defect, which
is discovery as the last writer plus an issuer plus at least one configured endpoint column
that anchoring is actively discarding; rows with an issuer but no configured endpoints are
left alone, since for them both paths resolve from the same upstream document. Every heal
logs the cleared value so an admin who pinned deliberately can re-pin, and the heal records
its own actor, which makes it idempotent

The reload fast path exempts servers missing an endpoint their flow needs, so failed
discovery retries on the normal reload cadence rather than waiting for a config write. Flow
requirements are read through effective_oauth2_flow, the column-first shape-fallback judge
every flow decision uses, so a legacy null-flow M2M row is classified exactly as the
request path classifies it instead of re-discovering forever; a dcr_bridge server with no
configured client needs its registration endpoint for the relay arm, and an entra_obo
server needs a scope, both of which discovery can supply. Retries back off per server,
doubling from one reload cadence to a fifteen-minute cap, so a permanently unresolvable
server cannot re-run the RFC 9728 to 8414 chain and re-log its warning every cycle forever

Deployments with store_model_in_db unset or false loaded MCP servers exactly once at
startup, leaving that retry with no driver, so they now refresh the registry on the same
reload interval. That job deliberately calls a reload-only entry point rather than the
startup composite, keeping the one-time oauth2_flow backfill and issuer heal out of a
recurring path

Losing the persisted trust-on-first-use issuer also means the issuer column no longer
changes underneath the OAuth token identity, so user tokens are purged only when an admin
actually edits the server

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-29 17:51:22 -07:00
Mateo Wang
8b03315ac6
Merge pull request #35028 from BerriAI/litellm_batch_provider_credentials
fix(proxy): resolve named credentials on provider-only batch and files calls
2026-07-29 11:59:09 -07:00
Mateo Wang
c56e657097
Merge pull request #34266 from BerriAI/litellm_team-model-allowlist-stale-qqg50q
fix(proxy): stop serving stale team model allowlist after /team/update
2026-07-29 11:00:38 -07:00
Mateo Wang
2348ccc977
Merge pull request #35107 from BerriAI/litellm_usage_public_model_names
fix(ui): show public model names in usage breakdowns
2026-07-29 10:42:13 -07:00
ryan-crabbe-berri
fdea50daa2
feat(ui): shareable log links via log_id query param on the logs page (#34879)
* feat(ui): shareable log links via log_id query param on the logs page

Clicking a log row now writes ?log_id=<request_id> to the URL, closing the
drawer removes it, and loading the logs page with ?log_id= opens the drawer
for that log. When the log is not in the loaded page, it is fetched by
request_id (the backend already drops the date window for id lookups), so
links keep working for logs of any age. Drawer open state derives from the
URL, mirroring the models page ?model= pattern.

* fix(ui): close the log drawer on browser back after opening via session id

Session opens now write ?session_id= to the URL instead of holding local
state, so back removes both params and the drawer closes (Greptile P1).
Session views become shareable links as a side effect. In-drawer log
switching now replaces the history entry instead of pushing, so back
always closes the drawer in one step rather than replaying every viewed
log.

* fix(proxy): scope /spend/logs/session/ui to the requesting user's visible logs

Non-admin callers now only receive session rows they could already see on
/spend/logs/ui: their own logs plus logs of teams where they hold the
spend-logs permission. Previously any authenticated user could read any
session's log metadata by id, which shareable ?session_id= links made
trivial to trigger. Admin views are unchanged. Also, clicking a log row
now clears a lingering ?session_id= from the URL so the drawer shows the
clicked log instead of a stale session (Greptile P1).
2026-07-29 17:01:09 +00:00
mateo-berri
d17387e2e1 fix(proxy): fall back on empty-string model_group in aggregated usage SQL 2026-07-29 09:59:34 -07:00
ryan-crabbe-berri
9b7a6b9b90
feat(ui): split failed requests into their own series on the cache dashboard (#34862)
* feat(ui): chart failed requests as their own series on the cache dashboard

Spend logs for failed requests are stored with an empty call_type, so the
Cache Hits vs API Requests chart lumped them into an Unknown bar that read
as normal LLM API traffic. The activity query now also returns a per-group
failed_rows count (status = 'failure') and the dashboard charts it as a
third stacked series, so failures are visibly separate from successful
requests and cache hits. The chart data transform moves into a pure
summarizeCacheActivity helper with unit tests; header stats keep their
existing semantics (cache hit ratio still counts failures in the
denominator).

* refactor(ui): move cache dashboard aggregation server-side with a typed response

The /global/activity/cache_hits endpoint previously returned raw per
(key, call_type, model) spend-log aggregates typed as LiteLLM_SpendLogs
(wrong), and the dashboard reduced them in the browser: grouping by
call_type, relabeling empty call_type as Unknown, and computing the stat
card totals. All of that now happens server-side. The SQL groups per
call_type and splits cache hits vs successful vs failed requests, a new
cache_activity module validates rows into Pydantic models and computes
totals plus the key-alias/model filter options, and the endpoint declares
a real response_model so schema.d.ts types it correctly. The dashboard
consumes it through a typed $api react-query hook (filters ride the
query key and are applied in SQL instead of the browser), the hand-rolled
summarizeCacheActivity transform and the adminGlobalCacheActivity fetch
helper are deleted, and the refresh button now actually refetches.

The endpoint is UI-internal (hidden from the public swagger), so the
response reshape is not a public API break.
2026-07-29 09:48:17 -07:00
ryan-crabbe-berri
40878a1ed5
fix(proxy): allow /key/update to identify the key by key_alias (#34851)
* fix(proxy): allow /key/update to identify the key by key_alias

* fix(ui): drop machine-dependent union-order churn from generated schema.d.ts
2026-07-29 09:48:08 -07:00
mateo-berri
802ed1c74f fix(ui): show public model names in usage breakdowns 2026-07-29 09:46:49 -07:00
Mateo Wang
e299d2b970
Merge pull request #35074 from BerriAI/litellm_daily_any_cleanup_07_29_2026
chore(typing): clear basedpyright Any errors in proxy management endpoints
2026-07-29 09:42:19 -07:00
ryan-crabbe-berri
25ebe5600a
fix(scim): stop provisioning nested group ids as internal users (#34997)
* fix(scim): stop provisioning nested group ids as internal users

POST/PUT/PATCH /scim/v2/Groups treated every member.value as a user id, so
with the default scim_upsert_user=true an unknown id was auto-created as an
internal user. Entra sends nested groups as members carrying "type": "Group",
which meant every nested group produced a phantom internal user whose id and
email were the group GUID, and those users counted toward licensed seats.

Group members are now classified before they are used: members typed "Group"
are skipped without a database hit, an id that names an existing team is
skipped too (Okta sends untyped ids through filtered paths, so the type alone
is not enough), and only ids that resolve to a user, or that resolve to
nothing at all, keep today's behavior. The user lookup runs before the team
lookup so a user whose id collides with a team id keeps syncing.

The type was previously dropped at parse time on POST/PUT because SCIMMember
had no such field, and on PATCH because the raw member dicts were reduced to
bare ids; both paths now share one resolver and one parser that preserves it.

Member removals no longer upsert: a remove of an id we do not know is an
idempotent no-op rather than a reason to create a user and immediately drop
it, and strict mode (scim_upsert_user=false) no longer rejects it. Removal of
an id that is on the roster but has no user row still cleans up membership.

Responses now state members are of type "User" instead of emitting a null,
and the advertised Group schema documents the members.type sub-attribute.

* fix(scim): harden group member classification after adversarial review

Removals now bypass classification and drop exactly the ids they name,
restoring cleanup of roster entries the old bug left behind. The
team-id fallback only applies to untyped members, so an explicit User
type always provisions even when the id collides with a team. Member
types are normalized before matching; a type other than User or Group
only skips when the id is not an existing user. Non-string type values
are tolerated as absent on every verb instead of failing validation.
Admitted member ids are deduped order-preserving, which also closes a
pre-existing duplicate-row hazard on group creation.

* fix(scim): only treat scim-managed teams as nested groups

A PR reviewer flagged that an untyped SCIM member whose id collides
with an admin-created team was silently skipped, suppressing that
user's provisioning. SCIM group writes (POST, PUT, and every PATCH)
now stamp the team with scim_managed metadata, and the typeless
team-id skip only applies to teams carrying that marker or the
scim_data blob older PUTs already wrote. Admin-created teams stay
unmarked, so a colliding untyped member provisions the user in
permissive mode and returns the standard unknown-user 400 in strict
mode. Teams SCIM touched before this change adopt the marker on their
next group write.
2026-07-29 09:38:17 -07:00
mateo-berri
095364fd04
test: cover the model_validate conversion sites flagged by codecov
Add regression tests for the db-fetch paths whose converted construction
lines were uncovered: the auth_checks getters (default end user budget, end
user, team membership, access group, team by alias, org by alias, object
permission, managed vector stores, project), get_all_team_memberships and
list_available_teams in team_endpoints, and the proxy admin user info
helper. Each test feeds a mocked prisma row through the real function and
asserts the validated model's fields, so a bad model_validate conversion on
any of these paths now fails a test instead of only dropping coverage.
2026-07-29 10:04:58 +00:00
mateo-berri
44e091aedb
chore(typing): clear basedpyright Any errors in proxy management endpoints
Convert pydantic table-model construction from Cls(**row.model_dump())
kwargs-unpacking to Cls.model_validate(...) across the management endpoint
hotspot files (team, key, internal user, scim, model management, spend
tracking, auth checks, proxy_server). Unpacking an untyped dict reports one
Any-typed argument per matched model field, so each converted site clears
10-35 diagnostics while running the exact same pydantic validation.
Conversions were limited to models verified to use pydantic's default
__init__; UserAPIKeyAuth and LiteLLM_VerificationTokenView keep their custom
kwargs-rewriting __init__ and are untouched. Two locally-verified helper
params move from Any to object.

Whole-tree basedpyright, measured against the branch point in the same
environment: reportAny 24,431 -> 22,741 (-1,690), reportArgumentType
2,189 -> 2,136 (-53), reportUnknownArgumentType 34,370 -> 34,067 (-303),
reportExplicitAny 7,285 -> 7,283 (-2); total 154,882 -> 152,834 (-2,048)
with no rule increasing anywhere and no per-file increases. No casts, no
suppressions, no behavior changes. Budgets ratcheted: basedpyright -2,048
across 4 rules, ruff ANN401 -2.
2026-07-29 09:16:18 +00:00
mateo-berri
47a9fabb5a fix(proxy): honor key-level model allowlist in provider-only credential resolution 2026-07-28 21:11:50 -07:00
mateo-berri
6d607ca3c2 fix(router): never resolve another team's deployment credentials for shared model names 2026-07-28 20:47:40 -07:00
Tin Chi Lo
1e04aee089 fix(proxy): reject model writes that corrupt an auto-router pseudo-model
An auto-router deployment's litellm_params.model (auto_router/...) is the
discriminator the router loads it by, but the model management endpoints
accepted any client-supplied value verbatim; a doubled or stripped prefix
made router init fail on the next load and ignore_invalid_deployments
silently dropped the deployment. Validate writes that supply
litellm_params.model at all three endpoints against the merged params and
reject incoherent values with an actionable 400. Classification is
extracted to router_utils/auto_router_model_naming.py so the Router
predicates and the validation share one source
2026-07-28 20:25:07 -07:00
tin-berri
1a6642ee2e
Merge pull request #34861 from BerriAI/litellm_lit4872_surface_reload_drop
fix(proxy): report when a model write does not survive the post-write reload
2026-07-28 20:18:02 -07:00
Mateo Wang
2bb297efa0
Merge pull request #34993 from BerriAI/claude/auto-til-blocked-cwalrj
fix(proxy): skip team model aliases that point at deleted deployments
2026-07-28 20:09:20 -07:00
Mateo Wang
c542e74b68
Merge pull request #34222 from BerriAI/litellm_jwt_v1_messages_team_route_1784693761
fix(jwt_auth): allow /v1/messages for JWT teams by default
2026-07-28 19:59:26 -07:00
mateo-berri
b592a37b8d test(proxy): cover stale-alias warning dedup and key-cache eviction 2026-07-28 19:39:30 -07:00
mateo-berri
17ce2c4e92 fix(proxy): resolve named credentials on provider-only batch and files calls 2026-07-28 18:57:15 -07:00
Tin Chi Lo
9f4e3c6009 fix(proxy): report when a model write does not survive the post-write reload
Every model-write endpoint returned 200 off the DB write alone; a model the
reload dropped (ignore_invalid_deployments, or a wholesale reload failure)
stayed invisible on every channel at once, which is how the registry-leak
defect went undiagnosed for three weeks. ProxyConfig.add_deployment and
clear_cache now return whether the reload pass completed, and each write
endpoint verifies the rows it wrote are live in this pod's router afterwards,
distinguishing a deliberately environment-inactive model via the same
predicate the Router's own gate uses. The access-group writers return the
mutated id set instead of discarding it
2026-07-28 18:52:06 -07:00
tin-berri
898c4e93bc
Merge pull request #34672 from BerriAI/litellm_lit4761_vertex_passthrough_stream
fix(vertex): decide rawPredict passthrough streaming from the request body
2026-07-28 17:40:58 -07:00
mubashir1osmani
7cd009caf7
fix(proxy): avoid DB outage during planned RDS IAM rotation (#34749)
* fix(proxy): warm rotate Prisma client for IAM refresh

* fix(proxy): drain Prisma operations during IAM rotation

* fix(proxy): bound the drain wait when retiring a replaced prisma engine

A replaced engine waited indefinitely for its drain tracker to empty.
Hung queries self-release via prisma's 30s default HTTP timeout, but a
transaction whose owner is hard-cancelled before commit/rollback leaks
its drain count forever, keeping the retired engine and its DB
connection pool alive indefinitely; at one rotation per 12 minutes such
engines accumulate. Cap the wait at 90 seconds, which exceeds every
legitimate operation bound (30s HTTP timeout, 60s max interactive
transaction timeout in this codebase), then kill the engine anyway.
Work killed at the deadline degrades to the pre-drain behavior and is
retried by the existing reconnect/backoff layers.

---------

Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-07-28 13:27:50 -07:00
mateo-berri
d409fec6de
fix(proxy): keep team model aliases while a surviving replica serves the deleted name
Scrub aliases on delete only when the deleted deployment's model_name no
longer resolves in the router. A legacy load-balanced team model can have
several deployment rows sharing one internal name; deleting one replica
must not remove aliases that still route to the survivors, in any team
2026-07-28 20:07:30 +00:00
mateo-berri
5e1d9705db
fix(proxy): skip team model aliases that point at deleted deployments
A team's model_aliases can map a public name like gpt-4 to the internal
routing key (model_name_{team_id}_{uuid}) of a team deployment that has
since been deleted, e.g. after replacing per-team duplicates with one
gateway-level model. The pre-call rewrite then sent every request to a
name the router cannot serve, failing with "no healthy deployments for
model_name_..." even though the requested name still resolves at the
gateway level. The rewrite is now skipped when the alias target has no
live deployment in the router

delete_model also skipped the team alias scan for internal-shaped names
on the assumption they can never be alias values, which is exactly the
shape legacy team model aliases have, so deleting a legacy team model
left the stale alias behind. The scan now always runs, and a public
name that still resolves to a live router deployment (e.g. a shared
gateway-level model group) stays in team.models so the delete does not
revoke the team's access to it
2026-07-28 19:45:00 +00:00
Tin Chi Lo
06a58efb2e feat(mcp): manual authorization-code delivery for headless MCP clients
The aggregate gateway DCR flow ends in a 303 to the client's loopback
redirect_uri. When the MCP client runs on a browserless machine (EC2,
SSH box, container) the user authorizes from a browser on another
machine, so the 303 dereferences the wrong loopback and the code never
reaches the client.

The connect banner now offers manual delivery for loopback clients: the
finish form posts delivery=manual and /authorize/complete renders the
callback URL on a no-store page instead of redirecting. The user pastes
it into the client (Claude Code v2.1.191+ accepts a pasted callback URL)
or fetches it from the client machine's terminal. Manual codes keep the
same sealing, PKCE binding, and single-use guard, with a 5 minute
expiry instead of 2 to survive the copy-paste hop; the used-code marker
TTL derives from the code's own remaining lifetime so the single-use
property holds for the full 5 minutes. The default redirect path is
unchanged.

Resolves LIT-4863
2026-07-28 11:23:37 -07:00
milan
e3559cf1b7 test(auth): cover managed batch/file team access denial end to end
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 15:39:50 +00:00
Devin AI
ddaee8df16 fix(auth): resolve managed batch/file deployment model_id to model name for team access checks 2026-07-28 15:35:19 +00:00
mateo-berri
c2f0014a63 fix(jwt_auth): grant only /v1/messages routes to JWT teams by default, not all anthropic_routes 2026-07-27 17:15:09 -07:00
tin-berri
8f86c87f8e
Merge pull request #34564 from BerriAI/litellm_fix_router_registry_leak_on_edit
fix(router): release the pre-routing strategy slot when a deployment is replaced or deleted
2026-07-27 16:46:40 -07:00
tin-berri
09856a40cd
Merge pull request #34586 from BerriAI/litellm_lit4795_headroom_anthropic
fix(guardrails): compress content-parts messages in headroom guardrail (Anthropic traffic)
2026-07-27 16:12:45 -07:00
tin-berri
9bb75d67af
Merge pull request #34675 from BerriAI/litellm_tool_spend_rollup
fix(proxy): roll up tool spend daily instead of scanning SpendLogs
2026-07-27 15:31:19 -07:00
yuneng-jiang
38ea85b4bb
Merge pull request #32583 from BerriAI/litellm_/redact-langsmith-api-key-c92cc3
fix(proxy): sanitize per-key callback config out of logged metadata
2026-07-27 15:25:03 -07:00
ryan-crabbe-berri
0171170fc7
fix(ui): validate default team values in Default User Settings (#34815)
* fix(ui): validate default team values in Default User Settings

The Default User Settings form accepted any free-text team id, and the
proxy persisted it without checking the team exists. New users were then
silently never added to the default team because the consume-time 404
from team_member_add was swallowed at debug level.

Backend: PATCH /update/internal_user_settings now rejects unknown and
duplicate team ids with a 400 naming them, before any persistence or
team budget side effects. Team-add failures in _add_user_to_team now log
at ERROR with user and team ids.

UI: DefaultUserSettings rewritten as a shadcn + react-hook-form + zod
form following the org-settings pattern. The team id free-text input is
replaced with a searchable server-backed team picker, so only existing
teams can be selected; zod blocks empty and duplicate rows. The shared
deriveErrorMessage helper now unwraps the HTTPException detail.error
shape so backend validation errors surface readably in toasts.

* fix(ui): restore read-only view with Edit Settings toggle on default user settings

Parity with the pre-migration form: the tab renders a read-only summary
of the saved defaults, Edit Settings opens the RHF form, Cancel discards
pending edits and returns to the summary, and a successful save returns
to the summary showing the new values. Model sentinel labels in the
summary are derived from ModelSelect's now-exported special values
instead of duplicating the strings.

* refactor(ui): rename MODEL_SELECT_SPECIAL_VALUES_ARRAY to MODEL_SENTINEL_OPTIONS

* fix(ui): move Edit Settings into the card header action slot
2026-07-27 15:05:51 -07:00
Shivam Rawat
300e710bc3 fix(router): release the pre-routing strategy slot when a deployment is replaced or deleted
Auto-router-family deployments live in two structures: the model_list, and a
pre-routing strategy registry keyed by (model_name, tags). Removing a deployment
dropped it from the model_list without releasing its registry slot, so the re-add
that follows hit the "already exists" guard in _register_pre_routing_strategy and
ignore_invalid_deployments swallowed it. The deployment came out and never went
back, while the DB row and the endpoint response both looked fine. Only a restart
healed it, and under multiple replicas each pod diverged into holding a different
subset of routers.

Removal now releases the (model_name, tags) slot from every strategy registry, in
both upsert_deployment and delete_deployment, guarded on the auto_router/ prefix so
removing a regular deployment cannot evict a router that merely shares its
model_name. Releasing from every registry rather than the first match is what makes
this correct for hybrids: registration is one-to-many, since a complexity router
configured with adaptive is also registered in adaptive_routers under the same key
by the deferred finalize pass. Releasing only the first match left that adaptive
strategy live, so a deleted or replaced alias stayed routable through it.

Adaptive post-call hooks are rebuilt whenever the adaptive registry changes, not
only at the end of set_model_list. The hook set is defined as exactly one hook per
registered adaptive router, so a released router stops recording turns instead of
holding a hook bound to a strategy nothing points at any more.

The swallowed upsert failure is logged at warning instead of debug, which is below
the default log level and left this failure with no observable signal anywhere.

delete_deployment resolves the outgoing deployment before popping it, and a
resolution failure no longer aborts the removal; previously an entry that failed
validation would have been left in the model_list permanently.

delete_model drops its blanket pop across all four registries. That predates this
change and over-evicts: it removes every tag variant registered under the name
while only one is being deleted, and nothing reloads on that path to restore the
survivors. delete_deployment now handles it correctly and tag-scoped, so the
endpoint-level eviction and its helper are removed rather than left to mask it.
2026-07-27 14:41:05 -07:00
Tin Chi Lo
2e12614a5b fix(proxy): stop retrying post-send ambiguous DB errors in every spend writer
Resolves LIT-4823. An adversarial review reproduced against real Postgres
that a batched increment upsert stalling past the prisma engine timeout
leaves its transaction open on the pooled connection; the retry draws the
same connection, its statements stack into the still-open transaction, and
one commit applies both increment sets while the writer reports success.
httpx.ReadTimeout is exactly that post-send case and every spend writer
retried it.

DB_RETRY_SAFE_ERROR_TYPES (ConnectError only, the failure that proves the
statements never reached the database) is now the single owner of what a
non-idempotent writer may retry. All seven entity and daily spend writer
retry arms and the tool usage flush consume it. DB_CONNECTION_ERROR_TYPES
is unchanged for the idempotent spend-log writer, whose create_many with
skip_duplicates may safely retry the full tuple.

The corruption was reproduced on update_daily_user_spend (seeded 10|100|1,
expected 11|110|2, observed 12|120|3); the new policy tests pin that a
ReadTimeout drops the batch loudly on the first attempt and a ConnectError
still retries.
2026-07-27 12:03:04 -07:00
yuneng-jiang
2b7e01bb7e
Merge pull request #34691 from BerriAI/litellm_/management-endpoint-standards-b1cd57
refactor(management): move the logs end-user filter onto /management/v1
2026-07-27 11:30:51 -07:00
Tin Chi Lo
33fadd70a3 fix(guardrails): compress content-parts messages in headroom guardrail
Anthropic-format requests translate to messages whose content is a list
of part dicts, which the headroom compression service's transforms
silently skip (they only rewrite string content), so compression never
applied to Anthropic client traffic while the guardrail still reported
itself as applied.

Flatten all-text part lists to plain strings for /v1/compress and
restore the original shapes from the response: untouched rows keep
their exact original parts, a rewritten row collapses to one part
carrying the last declared cache_control breakpoint (a breakpoint
caches the prefix ending at its part, so the last one and its TTL
still describe the merged row). Rows with any non-text part are never
flattened, since merging text across a non-text part would move a
later breakpoint to the other side of it; they pass through the
service untouched, matching its own behavior for non-string content.
Flattening and write-back use the shared content_text helpers that
compresr's breakpoint fix also uses.

Resolves LIT-4795

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:22:06 -07:00
tin-berri
10cd4288b6
Merge pull request #34660 from BerriAI/litellm_lit4804_compresr_cache_control
fix(guardrails): preserve cache_control breakpoints in compresr write-back
2026-07-27 11:17:51 -07:00
Yuneng Jiang
c3edf2402b
test(proxy): pin both branches of the validation exception handler
Same cause as the otel handler test: this file builds its request as a
SimpleNamespace carrying only `state`, and the validation handler now reads
`request.url.path` to pick an error contract, so the fake needs a url

While here, cover what the two existing tests do not. They only exercise the
proxy-wide 422, and the control plane's 400 problem document was reachable only
through the route test, which registers its own copy of the handler in a local
app rather than the real one. Two cases now pin the real handler directly: a
`/management/v1` path returns problem+json with a `detail` string, and paths that
merely resemble the prefix (`/management`, `/v1/management/foo`) keep the 422
shape their callers parse
2026-07-27 09:59:47 -07:00
Tin Chi Lo
1240c1a76d fix(proxy): close the adversarial-review findings on the tool spend rollup
Three fixes from an adversarial review of this branch, each at the owning
seam rather than the report site.

The flush retried DB_CONNECTION_ERROR_TYPES, which includes ReadTimeout.
A ReadTimeout is the committed-but-unacked case: the review reproduced the
engine abandoning the transaction open on the pooled connection, the retry
stacking its statements into it, and one commit applying both increment
sets while the flush reports success. The retry now covers only
ConnectError, the one failure that proves the statements never reached the
database; post-send failures drop the batch with an error log. The
docstring no longer claims an idempotency the pattern does not have. The
same hazard exists in the untouched daily spend writer and is left for its
own change.

get_tool_calls_from_response read choices[0] only, so a tool invoked in a
later choice of an n>1 response earned spend but never reached the rollup,
the index, or the registry. Choice scope is now an explicit parameter:
accounting passes include_all_choices=True because every choice costs
money; guardrails keep the primary-choice default because they rebuild the
primary assistant message. First multi-choice fixtures in the suite pin
both scopes.

maxBarSize=64 had been added to the shared BarChart unconditionally,
resizing every existing consumer. It is now a prop; only the tool spend
charts opt in. The legend flex-wrap changes stay global because clipping
overflow was a defect, not a preference.
2026-07-26 01:55:47 -07:00
Yuneng Jiang
cf127e16e8
fix(management): stop emitting a dead docs link in problem documents
The RFC 9457 `type` was `https://docs.litellm.ai/errors/<slug>`, copied from the
standard's own error example. That path is a 404 and there is no docs section
behind it, so every error body shipped a broken link

RFC 9457 only requires `type` to identify the problem type; it encourages, but
does not require, that dereferencing it yield documentation. An https URI makes a
promise we are not keeping, so use `urn:litellm:error:<slug>` instead, which
carries the same machine-readable identity with nothing to resolve. Switching to
an https base later is a contract change for anyone matching on `type`, so that
should wait for pages that actually exist

A test pins the identifier against regressing to an https docs URL, since the
existing assertion built the expected value from the same constant and would have
stayed green whatever it held
2026-07-26 00:16:51 -07:00
Yuneng Jiang
78e76fff4d
Merge branch 'litellm_internal_staging' into litellm_/management-endpoint-standards-b1cd57 2026-07-25 23:57:32 -07:00
Yuneng Jiang
cb78491482
refactor(management): move the logs end-user filter onto /management/v1
`/customer/aliases` shipped two days ago and has not been in a release, so its
wire contract is still free to change. This lands it on the control-plane
contract before that stops being true, since after a release the path, the param
names and the envelope would all need a permanent legacy adapter

The endpoint becomes `GET /management/v1/spend_logs/end_users`. It is a facet,
the distinct values one column takes over a filtered query on a resource, not an
entity collection; naming it after `customers` implied it listed the end-user
table when it actually reads spend logs, which is a different row set. Serving it
under the parent resource means its filters are the parent's filters, so the
dropdown offers exactly the values the logs table can show without two endpoints
having to keep agreeing on that

Contract changes: `size` becomes `page_size`, `search` becomes `q`, the window
moves from flat `start_date` / `end_date` to `filter[startTime][gte]` / `[lte]`,
and the body becomes `{data, meta, links}`. Unknown query params are now a 400
rather than being silently dropped, because an ignored filter over-returns data.
Errors are RFC 9457 problem documents on this prefix only; every other route
keeps the shape its callers already parse

`links` is what makes the rest deferrable. The dashboard hook follows the
server's `links.next` instead of computing `page + 1`, so moving this to cursor
pagination later changes the links and nothing the client does. That matters
because the inner scan is a sliding window, so offset paging can currently skip
or repeat an end user across pages; the fix is a follow-up, and the hypermedia
means it will not be a breaking one

Cursor mode, `sort`, `include`, ETag / `If-None-Match` and the generic `ListSpec`
framework are all deliberately out of scope here. They are additive or internal,
so none of them needs to beat the release
2026-07-25 23:57:25 -07:00
Tin Chi Lo
c8b0530c30 fix(proxy): roll up tool spend daily instead of scanning SpendLogs
GET /v1/tool/spend served the Cost Optimization card with two raw queries
over LiteLLM_SpendLogToolIndex x LiteLLM_SpendLogs on every dashboard load;
the totals query's driving scan was all of SpendLogs in the window. Both
per-request tables reach 1M+ rows at customer scale, so the card cost
O(traffic) per view and had to be capped at 30 days.

The index writer also mined proxy_server_request.tools, i.e. tools DECLARED
in the request body, attributing each request's full spend to tools that
never ran; and all non-MCP mining ran against payload fields that are '{}'
unless store_prompts_in_spend_logs is enabled, so non-MCP coverage silently
depended on a privacy setting.

Now the spend writer builds a ToolUsageTransaction at request time from
invoked tools only, resolved by the shared get_tool_calls_from_response
normalizer so every response surface (chat completions, Responses API,
Anthropic Messages) is covered; the tool registry's response arm delegates
to the same owner. Transactions queue beside the spend-log queue and the
flush job writes index rows plus a new LiteLLM_DailyToolSpend rollup
(date, tool_name PK) in one transaction, retrying connection errors with
backoff (a failed batch commits nothing, so the retry cannot double-count)
and dropping the batch with an error log on anything else.

The endpoint aggregates in SQL: by_tool is the top TOOL_SPEND_TOP_TOOLS
tools by spend via group_by and daily covers only those tools, so the
response is bounded by days x TOOL_SPEND_TOP_TOOLS regardless of range or
tool-name cardinality; the 30-day clamp is gone. total_spend is dropped
from the response; it was never rendered and its deduplicated semantics
are not computable from a rollup. Spend-log retention deliberately does
not touch the rollup, so tool spend history outlives per-request rows.
2026-07-25 21:52:58 -07:00
devin-ai-integration[bot]
24123269cc
fix(guardrails): resolve judge_model credentials via lazy Router lookup in llm_as_a_judge (#34509)
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* fix(guardrails): resolve judge_model credentials via Router in llm_as_a_judge

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

* fix(guardrails): wire llm_router into DB-backed judge guardrail init paths

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

* test(guardrails): assert patch endpoint forwards llm_router to sync

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

* refactor(guardrails): resolve judge Router lazily and fix wildcard/alias dispatch

Resolve the proxy Router at judge-call time via an injected provider instead of
capturing it at construction, so a DB-backed judge guardrail created before the
Router exists no longer captures None permanently. Select the Router path with
router.get_model_list(model_name=judge_model) so wildcard routes and
model_group_alias keys resolve, not just literal deployment names. Isolate the
judge call from user-traffic routing with num_retries=0 and fallbacks=[].

Revert the llm_router threading through the DB sync/reinit/create/approve/patch
paths since the lazy provider makes it unnecessary. Replace mocked-Router tests
with real Router coverage for plain deployments, model_group_alias, and wildcard
routes, plus lazy per-call resolution.

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

* fix(guardrails): harden judge verdict parsing and guard proxy import

Strip markdown fences and surrounding prose before json.loads so fencing-prone
judge models evaluate instead of failing open, guard the proxy_server import in
_default_router_provider so an unimportable proxy falls back to the SDK, and
snapshot/restore global callback lists in the DB-path judge registry tests

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

* fix(guardrails): reject non-object judge verdicts instead of failing open as success

* fix(guardrails): route hidden model_group_alias judge models through the Router

---------

Co-authored-by: milan <milan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng-berri <yucheng@berri.ai>
2026-07-25 20:16:43 -07:00
tin-berri
f7078e2e08
Merge pull request #34265 from BerriAI/litellm_lit4339_upstream_resource
feat(mcp): send RFC 8707 resource indicators on upstream OAuth legs
2026-07-25 18:53:45 -07:00