Commit graph

183 commits

Author SHA1 Message Date
Kerry Lu
05c04d6261
perf(spend): narrow user and team spend log reads to the owner's api_keys so the (api_key, startTime) index is used (#51026)
* perf(spend): narrow user and team spend log reads to the owner's api_keys so the (api_key, startTime) index is used

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

* fix(spend): satisfy the type discipline gate for the api_key scope helper

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

* fix(spend): run the api_key lookup under the statement timeout, skip it for search and empty key lists

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-11-10 15:04:08 -08:00
giladk1221
9ef5542339
feat(search): add Microsoft Web IQ provider (#50883)
* feat(search): add Microsoft Web IQ provider

* fix(search): require HTTPS for Web IQ credentials

* fix(search): include Web IQ in the packaged endpoint catalog

* fix(search): register Microsoft Web IQ query pricing

* test(search): fix Web IQ exception assertion lint

* fix(search): type Web IQ response passthrough and search error headers

The type-check gate failed on 8 new unknown-type errors in the Web IQ
transformation. Build passthrough fields from a typed helper and validate
the response models directly, and type the base search error headers as
dict[str, str] so subclasses no longer inherit an unknown parameter.

* fix(search): raise Web IQ parse errors without widening the base header type

Typing the base search error headers as dict[str, str] made the shared
HTTP handler report one more argument-type error. Restore the base
signature and raise the same BaseLLMException directly from Web IQ.

---------

Co-authored-by: Gilad Katz <29776916+giladk1221@users.noreply.github.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-11-10 22:43:36 +00:00
joshua-berri
894552205b
feat(spend): report guardrail spend separately in daily activity (#51010)
* feat(spend): report guardrail spend separately in daily activity

Model and key spend in /user/daily/activity/aggregated include the cost of
guardrails like Bedrock guardrails, and nothing in the response says how much.
A customer that bills tenants per key and per model could not get model cost
alone, so tenants that pay AWS for their own guardrails were charged for them
twice

Daily spend rows now carry a guardrail_spend column next to spend, and every
breakdown in the daily activity responses exposes it, so model cost is
spend - guardrail_spend. spend keeps its meaning, so budgets still count
guardrail cost as before

The value comes from the request's cost_breakdown.guardrail_cost, capped at
the row's spend because an allowance discount or a repeat job poll can charge
less than the guardrail cost the breakdown records. The delivery ledger
freezes it into the accounting payload next to accounting_savings, since the
stored payload does not keep cost_breakdown, and the legacy daily queue writes
the same value

The migration only adds the column with a 0 default, so rows written before it
read 0

* fix(spend): count guardrail spend on rows billed without a cost breakdown

A request blocked before the LLM call is billed for its guardrails straight from
guardrail_information, so it carries no cost_breakdown and its guardrail_spend
read 0. Both writers now price guardrail_spend from guardrail_information with
guardrail_information_cost, the same function success and failure rows use to add
guardrail cost to spend

Also fills guardrail_spend in the client usage view built from request logs,
qualifies the field description for rows recorded before it existed, and adds
the lock timeout the migration checks require

* fix(ui): avoid a nested ternary when reading guardrail entries from request logs

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
2026-11-10 13:02:26 -08:00
mrinal-berri
9ba551b871
feat(proxy): let teams use their own allowed S3 buckets for batch files (#50571)
* feat(proxy): let teams send allowed S3 buckets for batch files and read them back

A team's metadata can now list allowed_s3_buckets. Keys on that team may send one
of those buckets as s3_bucket_name on POST /v1/files and as s3_output_bucket_name on
POST /v1/batches, and the upload and batch-create handlers check the bucket against
the team list plus general_settings.allowed_client_side_s3_buckets. The early auth
body check still rejects bucket fields on every other route and method

Reads of managed files now trust the bucket LiteLLM recorded in the stored s3:// uri
through an internal stored_s3_bucket_name param, so content, retrieve, delete and the
batch cost job can read files written to a request-chosen bucket. Callers can never
send that param, and files in the model bucket resolve exactly as before

* test(proxy): assert outcomes in team bucket tests and type the managed content read

Gives the admit-only tests a real assertion, has the refresh test check what the caller gets back, and narrows the provider file id before reading its bucket so the type gate stays at its base count

* fix(proxy): keep team S3 bucket lists proxy-admin only and read prefixed request buckets back

A team admin allowed to edit team metadata could add any bucket to the team's allowed_s3_buckets, so the key now sits with the other proxy-controlled team metadata keys. The stored bucket for a managed S3 file now keeps any prefix that comes before LiteLLM's own path, so a file uploaded to a request bucket like team-bucket/tenant reads, refreshes and deletes from that prefix

* fix(proxy): keep the request's approved S3 bucket when merging deployment credentials

Model-routed uploads and batch creates merge the deployment's credentials into the request, and that merge let a deployment's s3_bucket_name or s3_output_bucket_name replace the bucket the request had already been approved for, so Bedrock wrote to the deployment bucket. The merge now keeps any bucket the request carries and fills it from the deployment only when the request has none

* fix(proxy): keep the request bucket only on upload and batch create, not on reads

The previous commit let a request's S3 bucket win over the deployment's on every credential merge, which undid #47496 on reads, where the deployment's bucket bounds which buckets a model-routed file id may point at. Deployment credentials win again by default, and only model-routed and provider-only upload and batch create opt in to keep the bucket the request was approved for

* fix(proxy): only proxy admins can set a team's allowed_s3_buckets

Org admins and users allowed to create teams could put any bucket on a team's allowed_s3_buckets through /team/new or a team update, which grants that team the proxy's AWS access to the bucket. The key now goes through the same proxy-admin-only metadata check as the batch limits, so only a proxy admin can add, change or remove it, and resending the stored value still passes
2026-11-10 11:06:17 -08:00
Kerry Lu
c1abd490b2
feat(customers): reject unknown fields on /customer/new and /customer/update instead of dropping them (#50965)
* feat(customers): reject unknown fields on customer create and update instead of dropping them

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

* fix(customers): derive the budget hint from the budget request fields and fix the create example

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-11-10 18:08:21 +00:00
Mateo Wang
ac08eb5663
fix: make scheduled key, user, and team spend resets idempotent (#50298)
* fix: make scheduled spend resets idempotent

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

* test: cover reset roundoff with shared Redis workers

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

* test: isolate reset workers and type raw adapters

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

* fix: keep updated_at moving on zero-spend budget resets

* fix: never leave spend below zero after a budget reset

* fix(proxy): keep the stored budget reset time when budget_duration is resent unchanged

Key, user, and team updates recomputed budget_reset_at whenever the body carried budget_duration, and the Admin UI key form sends it on every save. Under the guarded reset, a resend during a sweep skipped that period's reset, and a resend after the boundary skipped it on main too. An unchanged duration now keeps the stored reset time

* fix(proxy): keep a resent budget reset time only once it is due

A save that resends the same budget_duration keeps the stored reset time
only when that time has already passed, so the scheduled sweep still
resets the period it is due for. A row that is not due yet realigns its
reset time on save, the same as before.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-09 22:21:43 -08:00
Kerry Lu
64d84e557e
feat(budgets): add per-model tpm and rpm limit maps to budgets and customers (#50797)
* feat(budgets): add per-model tpm and rpm limit maps to budgets and customers

Budgets gain model_tpm_limit and model_rpm_limit (model name to whole number, 0 or more).
/budget/new, /budget/update, /budget/info and /customer/new carry them, and the auth
path loads a customer's maps onto UserAPIKeyAuth as end_user_model_tpm_limit and
end_user_model_rpm_limit so the limiter can enforce them in a follow-up.

* fix(budgets): keep org, team and project per-model limits in metadata; lock_timeout and type gate fixes

Organizations, teams and projects store model_tpm_limit / model_rpm_limit in their own metadata,
which the limiter reads, so the shared budget allowlist leaves those two fields to customers.
Migration starts with SET LOCAL lock_timeout. Mapping annotations and rebind-ok reasons satisfy
the type discipline gate. Customer info tests expect the two new budget keys.

* fix(organizations): keep stripping budget_id and flattening nested per-model maps on update

The update path pops every budget field from the organization row again, so a caller's budget_id
never changes the attached budget, and a nested litellm_budget_table still flattens the two maps
into org metadata. Drops the explanatory comment on CUSTOMER_ONLY_BUDGET_FIELDS.

* fix(types): keep basedpyright and ruff strict counts flat for the new budget fields

Reads the per-model maps off the end-user params with membership checks instead of .get on an
untyped dict, writes the inline customer budget through the client's typed jsonify_object, and
builds budget rows in handle_budget_for_entity with model_validate so the two Mapping fields never
receive object-typed kwargs.

* chore: drop unrelated reformatting from project endpoints

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-11-09 22:15:18 -08:00
kenneth-berri
7ccc0e0fff
feat(usage): split customer spend by the key's current team (#50764)
* feat: add customer spend breakdown and filtering by current team

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

* fix: recover legacy customer team spend and tolerate lookup outages

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

* perf: resolve customer team keys once per daily activity query

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

* fix: paginate customer team activity in SQL

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-11-09 19:35:31 -08:00
Mateo Wang
71a9fcb56a
fix(sso): read the SSO client secret before the refresh-token DB check (#50773)
* fix(sso): read the SSO client secret before the refresh-token DB check so a concurrent env sync cannot split the client id from its secret

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

* test(sso): type the secret-race test and read the provider from the IdP boundary mock

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-11-09 18:54:15 -08:00
yujonglee
523b3efea2
refactor(model-cost): move the cost map inputs, fetch and register_model overlay into Rust (#50501)
* refactor(model-cost): move the cost map inputs, fetch and register_model overlay into Rust

* fix(model-cost): keep main's compat helpers pure and move network tests to integration

ModelCostMapReloaded.model_cost_map now returns the alias-sharing table, so an alias registration after adopting a reload still reprices the owner. _finalize_model_cost_map reads a caller's map without size limits and installs nothing, through the new NativeModelCostMap.finalize. The unit tests that started a local server or a child interpreter now live in tests/integration/sdk/test_model_cost_fetch_sdk.py, and run_isolated gets direct tests for joining its blocking pool and leaving the fork gate alone.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 02:37:35 +00:00
jesus-berri
c76a95fc7e
feat(proxy): let a key's logging entry reference a proxy credential (#50572)
* feat(proxy): let a key's logging entry reference a proxy credential

A virtual key's metadata.logging entry can name a proxy credential with
litellm_credential_name. At request time the credential's values, which config
credential_list resolves through the secret manager at boot, are merged under the
entry's callback_vars, with the entry's own values winning. An entry whose
credential is not defined is dropped, so it never falls back to the proxy's own
callback environment.

Only proxy admins may change the logging entries of a key when the stored or
requested list holds a reference (403), and a reference to an undefined
credential is a 400. The check runs on key generate, update, team bulk update,
regenerate and /user/new, and compares decrypted entries so re-saving metadata
read back from /key/info is not a change.

Resolves LIT-10061

* fix(proxy): drop a key logging entry whose referenced credential holds no logging values

* fix(proxy): only require newly added credential references to exist on key writes

* fix(proxy): log a dropped credential reference as an error so /key/health reports it

* fix(proxy): gate credential references on bulk-created user keys

* fix(proxy): keep failing requests on keys with non-object logging metadata

A key whose stored metadata.logging is not a list of objects failed every
request with a 500 before this branch. The credential reference rewrite
turned those values into no entries, so the request quietly succeeded
without the key's logging. Raise a TypeError for those shapes again so the
request path behaves as it did on main, while empty values and objects with
unknown fields still go through the existing skip.

* test(proxy): audit cells for credential references across surfaces, v1, dropped entries and malformed logging

The malformed logging cell expects the base behavior that 7ac4ab1356
restored, a 500 for a key whose stored metadata.logging is not a list of
objects and a 200 for an empty list

* fix(proxy): keep a 500 for teams with non-object logging metadata

On main, a team whose stored metadata.logging held non-object items failed
chat requests with a 500, because AddTeamCallback(**item) raised a TypeError.
This branch switched to AddTeamCallback.model_validate, whose before-validator
raises AttributeError on those items, and the proxy maps AttributeError to a
400 invalid_request_error. Raise a TypeError for non-mapping items before
validation so the status matches main again, as the key path already does.
2026-11-09 16:02:46 -08:00
zoveki
abe3b11f7a
fix(proxy): show committed spend in the remaining budget gauges (#49199)
* fix(proxy): report live key and team spend to budget gauges within the key cache TTL

* fix(prometheus): carry live spend for the user, org and customer budget gauges too

The remaining budget gauges for users, orgs and customers read the same cached rows as the key and
team gauges, so they read high for about a minute after a charge. Auth now carries the live counter
for each budgeted entity in one snapshot that only the gauges read, floored at the spend auth loaded.
The x-litellm-key-spend header and logging spend fields keep their current values.

* fix(prometheus): show committed spend in the remaining budget gauges

Reads twin counters that take only charged cost, so a worst-case budget hold
never shows as spent and a request that finishes last cannot set a gauge back.
The twins exist only while the Prometheus logger is registered, and every path
that resets a spend counter drops them. Failed requests now set the org gauge
from the carried org row and committed spend, not the cached org row.

* fix(prometheus): count a charge the cached customer row already holds once

* fix(prometheus): keep committed spend only for budgeted entities and drop it on spend resets

Auth now reads committed spend through the admission MGET and names the counters a charge
may move, so the master key and unbudgeted entities get no twin. Organization spend edits
and the global spend reset drop their twins, and a long request no longer reads as charged.

* fix(prometheus): read the committed org spend on failed requests without the auth object

* test(prometheus): cover a failed request with no carried org

* fix(prometheus): move the committed counters for charges with no auth on the row

A billed background response is charged from its creator's stored attribution, after the request's auth is gone. Its charge moved the live counters but not the committed ones, so every gauge read one charge behind. It now moves the committed counters that already exist, and its own gauge adds its cost to them.

* fix(prometheus): keep the gauges right for pass-through rows, overlapping charges and slow DB spend writes

Pass-through rows carry the auth object, so a cold committed counter gets seeded there too. The gauges read a
charge still waiting in the post-call pipeline as not counted, whatever other charges reached the counter. Committed
counters outlive two DB spend writes. Cold seeds read the DB once per worker, and the global spend reset reads keys
and teams in bounded pages.

* fix(prometheus): count a charged failure's own cost in the org gauge

A stream cut mid-response still charges for what it delivered, and that charge is usually still queued when the failure logs. The failure path passed a cost of 0, so the org gauge left that charge out until the worker's next request

* fix(prometheus): skip twins for window-named ids, drop twins on delete, keep auth out of PostHog

* fix(prometheus): keep the auth object off pass-through rows and drop user twins on every user delete

Pass-through rows now carry only the committed spend in litellm_params, so loggers that dump the row's metadata never get the auth object. Every user delete path, SCIM and bulk delete included, drops the deleted users' committed spend twins

* fix(prometheus): keep committed spend values out of logger dumps of pass-through rows

CommittedBudgetSpend fields are repr=False, so Athina's payload, Sentry breadcrumbs and debug logs that str() a pass-through row's litellm_params show no spend values

* fix(prometheus): keep the gauge fallback on a slow committed read and refresh committed twin TTLs on cluster

The success-path committed read now gets half the per-request budget, so a slow Redis falls back to stored spend before the outer timeout cancels the gauges. Post-call charges refresh a committed twin's TTL on Redis Cluster too, so a twin never expires while its charges are still on their way to the DB. Also applies cargo fmt to the PR's Rust files

* fix(proxy): key the global reset's committed-spend page queries so the Prisma span check reads them

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-09 20:05:37 +00:00
quietjules
001126cd1c
feat(proxy): add OpenSearch index pass-through with per-key read/write grants (#50573)
* feat(proxy): add OpenSearch index pass-through with per-key read/write grants

Adds /opensearch/<index>/<api> for LiteLLM vector store indexes on OpenSearch
stores. A key or team needs read or write on the index in
allowed_vector_store_indexes, and the proxy signs each call with the vector
store's AWS identity, so callers never hold AWS credentials

The route maps the LiteLLM index name to the store's provider index, refuses
any path, query parameter, or body field that could reach another index, and
sends the exact signed bytes upstream with only the signed headers

* fix(proxy): let OpenSearch _bulk and _msearch NDJSON bodies through auth

opensearch-py sends _bulk and _msearch NDJSON with content-type application/json, and the
auth-time body read parsed it as one JSON document and answered 400 before the route ran.
Auth now skips parsing OpenSearch NDJSON bodies the same way it skips Azure Speech audio.

* feat(proxy): accept a _bulk _index naming the path index and allow named search pipelines

A _bulk action line may name the index in the URL path, and LiteLLM rewrites it to the
real index, which is what opensearch-py's helpers.bulk sends. The search_pipeline query
param now goes through so hybrid search works, while an inline pipeline stays refused.

Also types the type-checking Router stub with _router_init's parameters again, which
make lint's tests/e2e check needs since the Rust core change.

* fix(opensearch): answer 401 when the AWS credential source cannot read its token file

* refactor(proxy): check OpenSearch request bodies without recursive helpers

* fix(opensearch): answer 401 when AWS credentials fail to refresh at signing time

* fix(opensearch): keep auth off OpenSearch bodies and widen the read list to index checks

Auth no longer parses any OpenSearch request body, so a document field named model, api_base or base_url is written
instead of answering 401 or 403. GET and HEAD on the index and HEAD on _doc and _source are reads, each operation
checks the exact grant the list names, an index whose store name two stores share answers 400, and an _msearch
header that names the path index is sent to the mapped index

* fix(opensearch): need read for writes that return documents and keep document fields out of LiteLLM params

* fix(opensearch): keep documents out of LiteLLM hooks, keep main's credential statuses, make _msearch admin only, and bill only searches

* fix(proxy): bill only an exact /opensearch/<index>/_search path

A document read such as /opensearch/docs/_doc/_search ended in /_search and was counted as a billable search. The check now matches the index segment and _search exactly

* fix(proxy): only proxy admins set vector store index grants, and bill no OpenSearch document call

An index grant now opens document writes through /opensearch, so key generate, update, bulk update,
regenerate, team new and update, /user/new, and bulk user creation answer 403 when a non-admin
changes allowed_vector_store_indexes, top-level or under metadata. Resending the stored grants is not a change

An /opensearch path is matched before the LLM route suffixes, so a document id such as search or
embeddings never makes a document call count as a billable request

* fix(proxy): treat an empty index grant list from a non-admin as a change

An explicit empty top-level allowed_vector_store_indexes passed the admin-only check as unchanged and then
replaced the stored grants. It now counts as a change, so a non-admin gets 403. The new grant tests also
carry parameter types

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-09 18:54:20 +00:00
paveln83
141a580951
fix(proxy)!: price expanded wildcard /model/info rows as the model their calls route to (#49128)
* fix(proxy): price expanded wildcard /model/info rows as the model their calls route to

* fix(proxy): read wildcard routing through the router registry entry points

* fix(proxy): price a wildcard row only when the router sends its name there

Use the router's own route candidates for the listed name, so an alias, a routing group, a more specific pattern, or a team's own deployment keeps a wildcard row unpriced

* fix(proxy): show no price on a wildcard row whose calls go to another deployment

* fix(proxy): keep the listed price on catch-all wildcard rows and match the edit path to the read path

* fix(proxy): price a wildcard row aliased to a sibling name as the alias target

* fix(proxy): price wildcard rows through the caller's alias renames and show nothing for rows another deployment takes

* fix(proxy): keep same-model wildcard row prices, skip deleted team aliases, use the caller's team

* fix(proxy): keep live team aliases when skipping deleted team alias targets

* fix(proxy): show no wildcard row price when the same-model deployment serving it sets custom rates

* fix(model_info): recognize a replayed wildcard row on a worker that has not loaded the wildcard

* test(proxy): feed wildcard discovery real provider data in model info tests

* fix(proxy): price a wildcard row another deployment serves as the model its calls bill

A row whose calls reach another deployment now shows that deployment's model
and limits, and goes blank only when those deployments bill more than one model
or declare their own rate. A name no route serves keeps its own price, and a
replayed row stores none of the metadata it showed.

Drop the router's per-call scan of every deployment for a team key, since the
team and name indexes already cover those names.

* fix(proxy): price a wildcard row aliased to an aliased group as the group its calls reach

An alias target that is itself aliased onward listed the onward group's deployments too, so the row showed no price while calls billed the first target's model. The billed model now comes only from the route candidates

* fix(proxy): price a wildcard row billed by another deployment without the wildcard's base_model

A row aliased to another deployment priced as that deployment's model, but the lookup still swapped in the source wildcard's base_model, so the row showed a rate its calls never bill

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-08 19:08:22 +00:00
dingdangmao
c61ec51108
fix(key/info): return 400 instead of 500 when no key is provided (#43577)
* fix(key/info): return 400 instead of 500 when no key is provided

When GET /key/info is called without a ?key= query parameter and the
authenticated principal has no virtual key (e.g. an admin-UI session
token whose UserAPIKeyAuth.api_key is None), info_key_fn passed
hashed_key=None to Prisma find_unique(where={"token": None}), which
raised MissingRequiredValueError and surfaced as an unhandled 500.

Add an explicit None/empty guard that raises a clean 400 ProxyException
before the DB lookup, and add a regression test asserting that prisma
find_unique is never called with token=None.

Fixes #43571

Signed-off-by: apex-mochen <2756823972@qq.com>

* test: remove redundant assertion comment

* fix(key/info): remove redundant None check to satisfy basedpyright delta gate

After if not key: raise, key is narrowed to str. The subsequent
if key is not None: guard is always true and triggers basedpyright
reportUnnecessaryComparison, which fails the delta gate.

Collapse to a single hash_token_if_needed(token=key) call.

Signed-off-by: apex-mochen <2756823972@qq.com>

---------

Signed-off-by: apex-mochen <2756823972@qq.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-11-08 02:49:34 +00:00
yuneng-jiang
c024a4d2e3
test(proxy): delete 28 dead skipped tests in test_key_generate_prisma.py (#50550)
57 of the 62 tests in this file are hard-skipped because they need a live
Prisma DB, so they never run. This removes the 20 whose behavior a running
integration or e2e test already asserts, and the 8 that assert nothing
meaningful or can never pass. The imports and the one helper only they used
go with them. The other 29 skipped tests stay as a list of behavior still to
port into tests/integration.
2026-11-07 16:54:23 -08:00
Kerry Lu
51b49973c9
fix(proxy): hold DB_RECONCILE_LOCK around credential create, update and delete (#50551)
* fix(proxy): hold MODEL_RECONCILE_LOCK around credential create, update and delete

The config reload snapshots the credential table, awaits config rows, then prunes and
upserts litellm.credential_list from that snapshot. A credential write landing inside
that window was undone in memory until the next reload. The three endpoints now take
the same lock the reload holds.

Fixes #50273

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

* fix(proxy): take MODEL_RECONCILE_LOCK before the credential update reads its row

Two PATCHes queued behind a reload read the same old row and the second
overwrote the first. The relay now keeps enough bytes to spot a split
after marker, and the new unit tests are fully typed.

* test(credentials): annotate the reconcile lock test locals with Final

* refactor(proxy): rename MODEL_RECONCILE_LOCK to DB_RECONCILE_LOCK

The lock gates every db-to-memory reconcile and now the credential writes too, so
the model-only name undersold what it protects.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-07 16:02:44 -08:00
devin-ai-integration[bot]
5d66f2d8ff
fix(usage): keep spend when metadata lookups hit database failures (#50253)
* fix(usage): tolerate entity metadata database failures

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

* fix(usage): keep spend when metadata lookups hit database failures

Treat transport errors and the query engine crash on a closed connection as database
failures in the key, user and guardrail lookups of the aggregated, keys, users,
model_top_keys and cache_leakage_keys routes, and keep prisma out of module-level imports
so a proxy install without prisma still imports the usage endpoints.

* fix(usage): fall back only on database outages, on every daily activity route

Data-layer Prisma errors such as a rejected query or a missing table return 500 again, a key whose details failed to load reports key_exists null instead of false, and the non-aggregated daily activity routes keep spend when the key or entity metadata lookup hits an outage

* fix(proxy): skip the key metadata lookup when the daily rows carry no keys

* fix(usage): keep the guardrail breakdown fallback for any Prisma error

A missing guardrail usage table or column made the Usage call return 500 after the outage-only narrowing. The guardrail lookup now falls back on outages and on any Prisma error, as it did before this PR

* fix(usage): keep spend when the team, organization, customer, or agent name lookup drops

---------

Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-06 14:28:42 -08:00
Mateo Wang
3f38730d2c
refactor(typing): clear about 1,100 basedpyright Any errors across 303 files (#49986)
* refactor(typing): clear basedpyright Any errors (wave 1)

* refactor(typing): clear basedpyright Any errors (wave 2)

* refactor(typing): clear basedpyright Any errors (wave 3)

* refactor(typing): clear basedpyright Any errors (wave 4)

* refactor(typing): clear basedpyright Any errors (wave 5)

* refactor(typing): clear basedpyright Any errors (wave 6)

* fix(typing): keep the cached-plan fallback's direct query_first calls so span naming resolves

* fix(logging): keep callbacks running when litellm_params is a params model

should_run_callback and the sync success and failure handlers read litellm_params through any object with a get method again, as they did before this branch, so a logging hook that returns a GenericLiteLLMParams no longer stops sync logging for that request

Adds behavior tests for the changed lines codecov listed as not run

* fix: read the regional uplift for every data residency again and reuse one dict TypeAdapter

The typed lookup only read the eu and us multipliers, so a deployment that set regional_processing_uplift_multiplier_fedramp stopped applying it. /model/update and the model write paths now share one module-level TypeAdapter instead of building one per call.

* fix: bill regional uplift multipliers float() accepts, bytes included

* fix: raise the in operator's own error from one shared json_contains, and name the bad uplift type
2026-11-06 13:31:15 -08:00
joshua-berri
de0b389205
feat(router): add the Switchyard stage router as an auto router strategy (#50407)
* feat(router): add Switchyard Stage Router strategy

* feat(router): decision-model classifier and safer fallbacks for the Switchyard stage router

Moves to nemo-switchyard 0.4.0a1, which adds decision-model classifiers and makes
ModelCall.respond and fail async. The classifier is now a tagged union of an LLM
judge and a decision model, and decision calls go through the router's adecisions
so any decisions provider works. Algorithm misbehavior and library errors route to
the capable model instead of failing the request, and the session id plus client
session headers are always passed so capable holds apply without a classifier

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

* fix(router): keep the Switchyard router inside the type-check gate

The gate's environment did not install the optional switchyard extra, so every
libsy symbol counted as Unknown. nemo-switchyard joins proxy-dev like the other
optional runtime packages, and imports go through switchyard_rust.libsy, which
ships py.typed. Step handling moves into helpers so no Final is rebound in the
loop, and the dashboard API types are regenerated for the new routing fields

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

* fix(router): route Switchyard on every endpoint and reject unsafe router saves

The router now passes each request's call type to routing strategies, and
Switchyard converts the request to chat messages with the existing converter for
that call type before translating it. /v1/messages turns with tool or thinking
blocks used to fall back to the capable model, and now route on their signals.
The Anthropic converter can keep tool_result.is_error on an analysis copy, so
Switchyard sees tool failures there, while requests sent to models never carry it

Saves of a strategy router that lists itself now fail with a 400 instead of a
500 with a dead row, the capable and efficient models must differ, judge and
decision calls are not retried before failing open, the Switchyard routing
fields are classified for redaction, and the edit dialog returns the saved
router instead of the stale one

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

* fix(router): catch self-routing on patch and rename, and trim Switchyard tests

The self-routing check now reads the stored params through the decrypting
helper and runs when a patch carries no litellm_params, so a PATCH that points
a router at itself or renames it onto one of its targets returns 400 instead of
a 500 with a dead row. The Switchyard tests share one transcript builder and
router fixture, and reformatting noise in unrelated test files is reverted

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

* refactor(ui): make Switchyard a strategy in the existing auto router form

Switchyard is now a card in the existing strategy picker. Picking it swaps the
tier and classifier fields for the Switchyard fields, and the shared name, team
and save button create it through one submit branch, the same flow as the other
strategies. The separate add form, the extracted picker and the hidden-form
swap are gone, and the edit dialog keeps its own Switchyard form

The judge's session_affinity and message_hash_fallback settings are removed,
since Switchyard rejects both for the stage router

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

* fix(ui): accept partial tool semantics and show server errors on model edit

The Switchyard tool semantics field rejected mappings that list only some
categories, although the server accepts them. Model PATCH errors were
reduced to a generic message, so it now goes through the shared API client
like model create and surfaces the server text

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

* feat(ui): show the routing strategy picker up front in the auto router form

Every strategy was hidden behind an Advanced configuration link, so the
quick setup read as the only option and never said it creates a heuristic
router. The picker now always shows with Heuristics as the labelled
default, the quick setup sits under it, and picking another strategy opens
that strategy prefilled with the chosen models

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

* fix(ui): show the server's reason when a model settings or API key update fails

Both screens replaced the rejection with a fixed message, so a 400 such as
a router routing to itself read only as a generic failure

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

* feat(router): route Switchyard on the Rust router core

main replaced the Python router core with the Rust one, which left
Switchyard routers registered nowhere. Register them with the other
pre-routing strategies, hand each strategy the endpoint the request came
through, and evict Switchyard routers on reload and delete like the rest

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

* fix(router): scope Switchyard sessions per API key and fix review gaps in the Switchyard form

Switchyard session state is keyed by a hash of the caller's key hash and session id, and client
session headers no longer reach Switchyard unscoped. The add form carries the quick setup models
into Switchyard, sends the team and access groups, clears a stale tool semantics error, and the
schema reads API-saved configs with nulls and structured decision instructions

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

* test(router): make the Switchyard test folder a package

* fix(router): read the Switchyard session key hash only from the proxy's own metadata

* fix(proxy): keep nested nulls as null when masking deployment params

* fix(router): send Switchyard judge calls with the caller's own metadata bucket so tag routing applies

* fix(router): put Switchyard judge metadata in the bucket the judge endpoint reads

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:07:57 -08:00
Kerry Lu
d4f338ef1e
feat(tags): reset a tag's spend through /tag/update (#50323)
* feat(tags): reset a tag's spend through /tag/update

Co-authored-by: Arnold Galovics <arnold_galovics@docktape.com>

* test(tags): assert spend update response contract

* fix(tags): seed spend counter after evicting tag cache

* refactor(spend): expose shared counter reset helper

* fix(spend): sanitize counter key in spend counter warning log

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Arnold Galovics <arnold_galovics@docktape.com>
2026-11-06 10:41:22 -08:00
yucheng-berri
4086d5ccaa
feat(proxy): hash UI passwords with PBKDF2 in FIPS mode (#49874)
* feat(proxy): hash UI passwords with PBKDF2 in FIPS mode

A FIPS OpenSSL refuses scrypt, so every password write fails there and
nobody can sign in with a password. When OpenSSL enforces FIPS, passwords
now use PBKDF2-HMAC-SHA256 at 600k iterations with a salt from OpenSSL's
DRBG, stored as "pbkdf2:" plus base64 of salt and key. Any other row is
refused with one error log and left as it is, and the boot migration does
nothing, because FIPS mode is for new deployments.

PBKDF2 at 600k takes about five times as long as scrypt, so in FIPS mode the
five request paths run it on one thread instead of the event loop. A single
thread keeps a login burst from using up a CPU quota, which is what stalls
the loop with asyncio.to_thread. A login that queued for that thread asks the
throttle again before it answers, so a burst of guesses can't get past a block
that the failures ahead of it just set.

The breached password check is off in FIPS mode, because it runs the
password through SHA-1.

A standard OpenSSL reads False at every new branch, so standard mode keeps
scrypt, inline, on the same rows.

Resolves LIT-8425

* fix(proxy): take FIPS logins one at a time and recheck the throttle first

A login that waited for the KDF thread asked the throttle again only after
deriving, so a burst for one user kept that thread busy long after the user
was blocked. Logins now wait for their turn on a lock and ask the throttle
when it comes, so a blocked login costs no PBKDF2 and only one login at a time
holds a place in the KDF queue. Password changes and invite claims no longer
wait behind a whole burst of logins.

* fix(proxy): keep PBKDF2 rows on a standard boot and warn when FIPS skips the breach check

A standard boot treated a pbkdf2: row as plaintext and hashed its text, so the
real password stopped working and the old row text signed in. The migration now
leaves a row of that exact shape alone. Any other pbkdf2: text still migrates.

FIPS mode now logs a warning when it skips a breach check that is turned on.

The one-thread test no longer depends on timing, and the new test variables are
annotated Final.

* fix(proxy): keep the breached password check on in FIPS mode

* fix(proxy): choose the password KDF from fips_mode instead of the host OpenSSL

* test(proxy): fake the scrypt probe in the FIPS fixture now that the gate checks scrypt
2026-11-06 01:31:59 -08:00
yujonglee
b1b236826a
feat(redis): serve the proxy's common Redis call with Rust stores (#50170)
* feat(redis): route the router, rate limiters and auth through the native store families

* style(redis): satisfy the python lint gates

Format the rate limit store, sort its __all__ and give the routing read batch and two tests concrete list and dict types.

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

* fix(router): give the registry's default usage store a memory-only handle

RouterUsageStore now takes a handle, so the default passed None and any usage read before RustCore swapped in its store raised AttributeError.

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

* test(redis): build stores from native handles and wait for router tasks

Tests still used the pre-refactor store constructors (Store(None), handle= factories, NativeIncrement, tuple scopes). The router outcome tests counted 20 loop ticks, too few now that the allowed-fails increment resolves on the native runtime, so settle() waits for the scheduled tasks instead.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 06:01:11 +00:00
jesus-berri
4f06ca217f
feat(team): let default_team_params attach access_group_ids to every new team (#49908)
* feat(team): let default_team_params attach access_group_ids to every new team

* fix(types): keep default access_group_ids as Sequence for the type discipline gate

* fix(settings): chunk the default access group existence lookup

* test(team): prove default access groups reach the saved team row and type the settings fixtures

* fix(team): check default access groups on the writer so a fresh group is not rejected behind a read replica
2026-11-05 19:58:51 -08:00
mrinal-berri
fb2a1450dd
fix(users)!: validate custom IDs and reject concurrent duplicate creation (#49748)
* fix(users): validate custom IDs and reject concurrent duplicate creation

* fix(users): return precise conflicts without retrying them

* fix(users): preserve retries for existing-user upserts

* fix(scim): preserve opaque IDs during user provisioning

* fix(sso): preserve identity provider user IDs
2026-11-05 17:07:19 -08:00
Nate Armstrong
067f93340f
refactor(router): delete the python router core, the rust core is the only core (#50178) 2026-11-05 10:40:46 -08:00
joshua-berri
a12a6cd154
feat(accounting)!: make the SpendLogs ledger the default accounting path (#49872)
* fix(accounting): keep one undecodable delivery from blocking the ledger

A request whose metadata carried a 401-digit tokens_saved stored that number in
its delivery payload. The query engine could not decode it when the worker
claimed the batch, so every later delivery on the proxy stayed pending

The worker now claims the payload as text and decodes each row in Python. A row
that still cannot be decoded is parked alone while the rest of the batch
applies. The payload builder also clamps integers to the JSON-safe range, so
caller-supplied counts can no longer reach the engine at that size

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

* fix(accounting): take spend locks in one order so the ledger cannot deadlock

A ledger delivery updates many spend tables in one transaction, so it can
deadlock with any writer that locks the same rows in another order. It took the
team advisory lock after the user and team rows, while team roster writers take
it first

The delivery now takes every team advisory lock before any row, then writes
tables in one canonical order with ids sorted. It also runs under the rollup
lock_timeout, applies 500 deliveries per batch by default, and spreads retries
with jitter

The writers that could still collide with it now follow the same order. The
budget reset cascade, model allowlist rename, global spend reset, bulk user
update, org delete and the delete-by-list paths lock their rows in key order
first, and access group endpoints update teams and keys in one sorted pass

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

* test(accounting): prove a ledger delivery waits instead of deadlocking

Real Postgres tests hold the first lock of a team roster change, an access group
update and a budget reset cascade, start a delivery, and let the other writer
finish. The delivery must wait and then charge once. Before the lock order fix
each case ends in a Postgres deadlock

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

* feat(accounting)!: make the SpendLogs ledger the default accounting path

use_spend_logs_ledger now defaults to true, so every request is charged through
LiteLLM_SpendLogDelivery. Setting it to false rolls back to the in-memory and
Redis spend queues, which stay in place. Tests that cover those legacy writers
pin the setting off, and the rest check the queued charge the ledger stores

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

* test(proxy): let the mocked client open a transaction for key and team deletes

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

* fix(proxy): take team locks first when a model rename rewrites allowlists

A rename locked team rows before user rows while a roster change holds the team
advisory lock and a user row, so the two could deadlock. The rename now takes the
advisory lock for every team that lists the model, sorted, before any row lock.

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

* fix(accounting): drain a backlog in one worker run, one batch per transaction

One run applied a single fresh batch, so at 500 per batch charges fell behind any
load above that rate. The worker now keeps claiming fresh batches while they come
back full, for up to five seconds, and each batch still commits on its own.

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

* fix(accounting): queue only the charge when a request's spend log is suppressed

With logging off for a request, the ledger still queued the full row, so a failed
flush or a shutdown could leave its prompt and response in Redis. The queue now
holds only the fields the charge needs.

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

* style(proxy): format the rename team listing query

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

* fix(accounting): remove expired delivery receipts as fast as the worker applies them

The worker now applies several batches per run but still removed one cleanup batch
of applied receipts, so the table could outgrow its retention window. Cleanup now
repeats while each pass removes a full batch, for up to five seconds.

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

* fix(proxy): retry a model rename when a team starts listing the model mid-lock

The team advisory locks come from a snapshot, so a team that gained the old name
before its row lock was locked without its advisory lock. That attempt now commits
without writing and the next one locks the fresh set of teams.

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

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 09:14:10 -08:00
ishaan-j-berri
639625d679
fix(usage): restore per-user spend breakdown on the user aggregated route (#49464)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-05 01:57:23 -08:00
Nate Armstrong
9752869d84
feat(router): add the Rust router core with the host as its only boundary (#49513)
Adds the litellm-router-core crate, its step-driven PyO3 bridge
(RouterCoreHandle / RouterCall) and RustCore (litellm/router_rust/),
served behind LITELLM_RUST through the ROUTER route rule. The host
call-out is the only boundary: every attempt goes through Host::invoke,
there is no native executor path, and RustCore never builds, wraps or
forwards to PythonCore, so router_python_core.py stays deletable.
Attempt preparation lives in litellm/router_attempt.py so it outlives
router_python_core.py.

RustCore matches PythonCore's behavior, including its known bugs (fixed
in follow-up PRs). Routing decisions (strategy picks, routing groups,
PTU, pre-routing, pre-call checks, fallback walk, health routing, access
groups, weights, cooldowns, allowed fails, usage) live in the crate;
work that needs live Python objects (request shaping, endpoints,
streaming, model info facts, callbacks, alerting, prompt management)
lives in the host-side litellm/router_rust/ package (steps/, invoke/,
surface/) driven by Rust steps.

The crate owns the registry. It runs the deployment build loop (secret
references, ids, organization fan-out, install order) and asks the host
for what needs Python, one deployment at a time so errors surface where
Python raised them: secrets, validate, provider and added steps. It
holds the model list, the index maps and the pattern routers with the
Python core's history rules (host_index.rs), and answers the registry
lookups (lookup.rs). The Python views of that state in
litellm/router_rust/surface/ (model_list, index(), names(), patterns())
read the handle, and every write to them becomes a changeset. Model
info, deployment model info, configured limits and model group info are
merged in the crate (model_info.rs, group_info.rs) from catalog rows and
facts the host answers on a model_info step asked at query time, so
price reloads and register_model stay visible. The usage cache key
format is defined once, in the crate.

The crate records each attempt's outcome itself rather than through
litellm callbacks. A streamed attempt is deferred: it counts its
request when the stream opens, and the host's stream wrappers report
the success with the final usage, or a failure after the first chunk,
when the stream ends, as the Python core's callbacks do.

tests/unit/test_router/core characterizes PythonCore's public behavior
and runs every test on both cores, with nothing pending; only the names
of the Python core's own outcome callbacks are asserted on Python alone.

litellm.Router is now final: subclassing it raises TypeError at the class statement, and the
machinery that kept subclass overrides working is gone. The Rust core never honoured subclass
overrides, so they would have silently stopped running there. Custom routing goes through
register_plugin(), settings through apply() and outcomes through observe(). This is a breaking
change for anyone subclassing Router.

Fuzz parity fixes on top of the rebase: the Rust core keeps num_retries,
max_fallbacks, allowed_fails, retry_after, timeout, the tag settings,
routing_strategy_args and the fallback lists as given (the crate reads
them as numbers, update_settings casts as Python does); a deprecated
`model_list = [...]` write reaches the Rust core; a failed set_model_list
is all or nothing on both cores (the deployments served before the call
are put back and the error raised); config_deployments() is a generator and `del` of a plain
attribute works on the Rust core. Least-busy pick reservations from
main now work on the Rust core: its selectors share one reservation
store, routed calls reserve and free their unstarted picks, and
select() and batch cancel do not reserve. On the Python core a dropped
upsert takes its name back off deployment_names, and
get_deployment_order reads every numeric spelling as one int so mixed
orders no longer raise TypeError.

Rulings on the remaining differences, the Python core changed where it
was the odd one out: sync picks and picks by deployment id apply the
team filter as async picks do, so another team's deployment is refused;
the fallback lists and default_litellm_params in RouterSettings are
copies, so an in-place edit is ignored and settings change only through
apply(); one health record with an unreadable timestamp drops out alone;
an empty fallback entry is skipped; a bool cooldown_time is refused at
construction, a bool or string cooldown_time on a deployment or a call
falls back to the router default, and a failure status that no HTTP
status can be is recorded as 500; an infinite or NaN cost and a whole
number wider than 64 bits (ptu_shares) are refused on both cores with
the same error (litellm/router_utils/deployment_numbers.py); and a
/v1/messages stream whose provider iterator raises cools the deployment
and retries like a non-stream call, with the Rust core waiting for that
cooldown before its retry picks.

Differences the proxy parity fuzz and its regression tests found on the
Rust core: a file upload or a batch on an uploaded file falls back only
within its own model group, as on the Python core, since the file lives
with one provider (the host sends same_group_fallbacks_only and the walk
skips targets in other groups); Decisions calls drop deployments whose
provider has no Decisions config, and deployments that refuse the audio
or videos a call carries, unless that would leave none, as the Python
core does, while the retry count still reads the whole group; a file
upload that fails on one order level is created on the next level's
deployments rather than the first level again; and a response that
cannot be converted (a text_completion body without a message) reports
only its own traceback, so a rate limit handled by the attempt before it
no longer turns the error into a 429, and both cores return 500.
2026-11-04 21:47:58 -08:00
ishaan-j-berri
d2b2e76374
feat(proxy): add model deprecation alerts with daily requests by key (#50022)
* feat(ui): show recent model deprecations in notification bell

* feat(ui): simplify deprecation request trends and add model identity
2026-11-05 04:03:42 +00:00
Kerry Lu
2d0c720425
fix(proxy): cancel the Admin UI's raw spend-log reads in Postgres after 30s (#49965)
* fix(proxy): cancel the Admin UI's raw spend-log reads in Postgres after 30s

Prisma's HTTP client gives up on a statement after 30s, so the Logs, Caching,
Cost Optimization and spend report pages already fail at 30s, but Postgres kept
running the scan. Every raw LiteLLM_SpendLogs read behind those pages now runs
under SET LOCAL statement_timeout = 30000 through one shared helper, which
/team/spend/by_user also uses instead of its own copy

* fix(proxy): keep the spend report's 503 and time out the session totals read

The grouped Logs page's session totals query now runs under the same
statement timeout, /global/spend/report re-raises the helper's 503 instead
of turning it into a 400, and the hand-written prisma fakes in the endpoint
tests expose read_db so every guarded route is exercised

* fix(proxy): keep the Logs page when Postgres cancels its session totals read

A cancelled session totals read degrades like any other failed enrichment
read instead of failing the whole page. The span-name test treats the shared
statement-timeout helper as a forwarder, and the spend report test no longer
depends on a license in the environment

* test(proxy): /global/spend/report keeps the 503 hint when Postgres cancels the read

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 16:07:00 -08:00
Kerry Lu
391c8013c9
fix(proxy): cancel /team/spend/by_user in Postgres after 30s (#49922)
* fix(proxy): let the Admin UI cancel /team/spend/by_user at 60s with an opt-in statement timeout

GET /team/spend/by_user takes an optional timeout_seconds (1 to 600). When set, the raw
LiteLLM_SpendLogs aggregation runs under SET LOCAL statement_timeout and Postgres cancels it,
and the route returns 503 instead of letting the query run for hours. Callers that omit the
param are unchanged. The Admin UI sends timeout_seconds=60 and the Spend Per User Within Team
card shows the server's error instead of an empty table

* fix(proxy): cancel /team/spend/by_user in Postgres after 30s

Prisma's HTTP client already gives up on the statement at 30s, so the
query now runs under SET LOCAL statement_timeout = 30000 for every caller
and the opt-in timeout_seconds param is gone. The Admin UI card shows the
request error instead of the empty-state text.

* fix(proxy): keep /team/spend/by_user on the read replica inside its timeout transaction

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 14:03:28 -08:00
jesus-berri
6db7001c05
feat(ui): show user names instead of raw ids on detail pages (#47181)
* feat: show user names in access group details

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

* feat: show user names on project, org, model and prompt details

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

* fix(ui): look up user names by exact id

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

* fix(ui): fall back to user list for org admins

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

* fix(ui): fall back when /v2/user/info returns a different user id

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

* fix(proxy): let org admins read their org members in /v2/user/info

Org admins could list a user through /user/list but got a 404 from
/v2/user/info for that same user, so the dashboard fell back to a
substring /user/list lookup that could miss the user. /v2/user/info
now allows an org admin to read users in organizations they administer,
and the dashboard's single-user lookup calls only that endpoint.

Expected 4xx responses from /v2/user/info no longer log a traceback.

* fix(proxy): drop unbounded IN from org admin check in /v2/user/info

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 13:41:20 -08:00
Etienne Chabert
523b87ec71
fix(proxy): charge budget windows when the cached key or team expired before the request ended (#47608)
* fix(proxy): charge budget windows when the cached key or team expired before the request ended

The post-call spend update reads a key's or team's budget_limits from
user_api_key_cache and returned early on a miss, skipping both the window
counter increment and the LiteLLM_BudgetWindowSpend row. The cached object
expires after user_api_key_cache_ttl (60s by default), so any request that ran
longer than that without another request refreshing the entry was counted in
the key's lifetime spend but never in its budget windows. With budget
reservation disabled nothing else charges the windows, so keys kept running
past their window caps

On a miss the update now reads budget_limits straight from the key or team row.
It does not re-cache the object, since a write from the spend path could race a
/key/delete and put a just-deleted key back into the auth cache

* test(proxy): assert the budget window fallback never re-caches the key or team

* fix(proxy): read a missing key or team row once per cache TTL in the budget window fallback

The master key and UI session tokens are logged under names no key row
carries, so every one of their requests missed the auth cache and read the
database. A missing row is now remembered for the cache TTL in this process
only. A failed read is not remembered, and a row that exists is read fresh on
every miss so a key update is never served stale.

* fix(proxy): skip the budget window row read for a key or team with no windows

The fallback remembered only a missing row, so a key or team that exists but
holds no budget_limits was read from the database on every request that
outlived the key cache. It now remembers both cases under one cache key built
in user_api_key_cache.py, for the same TTL as the auth cache

* fix(proxy): keep no-window markers in their own cache and clear them when an update adds windows

Markers for keys and teams with no budget windows lived in the shared 200-entry auth cache, so a burst of late costs could evict cached teams and users. They now live in a dedicated in-memory cache on UserApiKeyCache, reached by local and broadcast evictions. /key/update and /team/update now evict the marker on every worker, so a late cost after an update charges the windows the update added.

* test(proxy): give the spend counter tests the proxy's real key cache and type the window spend helper

* fix(proxy): keep a row read that an update outran from restoring the no-window marker

* fix(proxy): clear finished no-window reads and evict a marker-shaped user id from both caches

* fix(proxy): apply other spend counters before a window row read and let no-window notes follow the key cache settings

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 14:41:59 -08:00
tavren27
a4de720144
fix(count_tokens): count grok-4-1, grok-4.3 and DeepSeek V4.1 tool requests the way their hosts bill them (#49074)
* fix(count_tokens): count grok-4-1, grok-4.3 and DeepSeek V4.1 tool requests like their chat templates

Render grok-4-1, grok-4.3 and DeepSeek V4.1 tool schemas the way each chat
template does, and add a per-deployment token_count_tool_framing next to
token_count_framing for the hidden tool preamble a host bills. The Azure AI
Foundry grok-4-1, xAI grok-4.3 and Fireworks DeepSeek V4.1 cost map rows carry
the measured framing, overridable in model_info

* fix(count_tokens): count Azure grok-4-1 Responses requests with their own framing and use the tool framing in place of the base framing

* fix(count_tokens): keep operator framing on tool and Responses counts, frame bridged Responses like chat, follow base_model for tool rendering, count grok tool_choice

* fix(token_counter): count Azure Responses strict default, skip false strict and built-in-only tools, per-kind operator framing

* fix(token_counter): count an explicit null strict like false on Azure Responses

* fix(count_tokens): follow the chat completions bridge flag and Azure's strict default on azure/ deployments

Keep Azure grok-4-1 chat tool counts at or over the bill by raising its tool framing to 302

* fix(token_counter): keep strict false for non-grok templates, coerce the bridge flag, frame DeepSeek API counts

* fix(proxy): frame Responses counts for the api_base router defaults send inference to

* fix(proxy): keep the api_base helper's name so main's /v1/messages count caller resolves

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 12:37:35 -08:00
Nate Armstrong
87d91d78b7
refactor(router): route the router family through RoutingContext and close the outside surface (#49118)
Strategies, pre-call checks, cooldown and fallback helpers, and the auto, complexity and quality
sub-routers now take a RoutingContext instead of the Router, and read the router only through
registry(), settings(), health() and the context's forwarding methods. Router-free helpers move
from router_utils to litellm_core_utils, outside callers of router state switch to entry points,
and the surface allowlist shrinks to the endpoint methods plus the entry points
2026-10-30 19:18:54 -07:00
yujonglee
ef9385c712
refactor(proxy): inject gateway activity through a typed repository (#49116)
* refactor(proxy): inject gateway activity through a typed repository

* ci: keep dependency tests in their existing shard

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-30 16:25:11 -07:00
jesus-berri
e8f572b39b
feat(sso): create the team_id_jwt_field team on UI SSO login when team_id_upsert is set (#49256)
* feat(sso): create the team_id_jwt_field team on UI SSO login when team_id_upsert is set

* fix(sso): keep the upserted team_id_jwt_field team out of result.team_ids so restricted_sso_group and the access-token group fallback are unchanged

* fix(sso): only upsert the team_id_jwt_field team on UI SSO login when enable_jwt_auth is on

The API JWT path only reaches team_id_upsert inside the enable_jwt_auth branch, so a deployment
with a leftover litellm_jwtauth block and JWT auth off never created teams. Gate the UI SSO upsert
the same way

* fix(sso): read enable_jwt_auth through the proxy_server module so the basedpyright gate stays at its ceiling
2026-10-30 14:39:25 -07:00
tin-berri
35d3a1784b
fix(proxy): preserve access windows when editing models (#49316) 2026-10-30 21:27:23 +00:00
Maxime Boudier
b20acb4869
fix(proxy)!: persist sent model_info fields on POST /model/update (#46902)
* fix(proxy): persist sent model_info fields on POST /model/update

POST /model/update rebuilt litellm_params from the request but dropped the
request's model_info, so changing base_model (or mode, tier, etc.) on an
existing DB model was silently ignored. Clients that only use this endpoint,
like the Terraform provider, could never fix a deployment's base_model
without deleting and recreating it.

Persist the model_info fields the caller actually sent, on top of the stored
ones. ModelInfo defaults never overwrite stored values, server-owned, team,
PTU and pricing fields stay on their dedicated write paths, and values echoed
from a /model/info response are not pinned.

* fix(proxy): keep echoed and blank model_info values out of POST /model/update writes

Blank strings from typed clients no longer overwrite stored values. Values echoed from
/model/info (the stored base model's cost map entry and discovered limits) are not
pinned, and an echoed catalog value resets a differing stored override like PATCH does.
direct_access and access_via_team_ids stay display-only. The tests stop patching the
auth check class and give the team lookup a real row instead.

* fix(proxy): lock the model row and skip unchanged model_info on POST /model/update

* fix(proxy): treat only keyed /model/update bodies as echoes and write the stored model_info as saved

A typed model_info value that matched the cost map, or the previous base model's entry,
was dropped and could erase a stored override. Echo detection now follows PATCH, only a
body carrying the /model/info key counts, compared against the base model the row has
after the write. The locked write patches the stored JSON instead of dumping the hydrated
Deployment, so litellm_params pricing is no longer copied into model_info. Tests run the
real Router with discovery over a mock upstream

* docs(terraform): note that model_info updates now take effect and overwrite values set outside Terraform

* test: expect POST /model/update to save the cache point flag, and wrap long test lines

test_x06 pinned the old contract, where POST /model/update answered 200 and ignored model_info.
This PR makes POST save model_info the way PATCH does, so x06 now asserts what its PATCH sibling
x01 asserts, the stored flag flips to true and cache points come back on.

* fix(proxy): answer 400 when POST /model/update gets an access_groups that is not a list of strings

* fix(proxy): read discovered model info through the router registry entry point

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 12:10:13 -07:00
devin-ai-integration[bot]
e752528ac8
feat(keys): allow assigning a project to an unassigned key on /key/update (#41700)
* feat(keys): allow assigning a project to an unassigned key on /key/update

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

* chore(keys): suppress LIT002 on project assignment error payloads

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

* chore(keys): format project assignment error detail

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

* test(keys): assert accepted project assignment flows into update payload

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

* chore(keys): drop stale LIT002 suppressions removed by main

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

* test(keys): mock writer_db team lookup used by check_db_only path

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

* fix(keys): write project assignment only while key row is still unassigned

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

* chore(keys): satisfy type gate on guarded project assignment write

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

* fix(keys): default soft-budget update path to unguarded write

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

* fix(keys): run guarded project assignment in one writer transaction

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

* fix(keys): include object permission link in assignment guard

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

* refactor(keys): return permission-linked payload without mutating the update dict

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

* chore(keys): fix type errors and stale noqa after main merge

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

* chore(keys): use Sequence in assignment guard model annotation

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

* chore(keys): type update fields as a mapping in the key update path

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

* chore(keys): place cast reason on the cast call

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

* fix(keys): honor explicit null max_budget when assigning a project

* test(keys): assert explicit null max_budget flows into update payload

* fix(keys): match NULL models column in guarded project assignment

* fix(keys): authorize caller before project lookup on /key/update

* fix(keys): drop explicit null object_permission before project assignment write

---------

Co-authored-by: jesus <jesus@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-30 11:53:06 -07:00
Etienne Chabert
372c096388
fix(proxy): evict a deleted key from every worker's auth cache (#47609)
* fix(proxy): evict a deleted key from every worker's auth cache

/key/delete dropped the key object only from the in-memory cache of the worker
that served the call. Auth serves key objects cache-first, so every other
worker, and the Redis copy when enable_redis_auth_cache is on, kept
authenticating the deleted key until user_api_key_cache_ttl expired. /key/update
already evicts and broadcasts through evict_and_broadcast, delete now does the
same for the tokens it deletes

* fix(proxy): evict only the deleted rows and broadcast them in one batch

Scope the cross-worker eviction on /key/delete to the rows the caller was allowed to delete and send it through delete_cache_key_objects, so a caller-supplied name can no longer drop another team's cached entries on every worker

* refactor(proxy): drop the comment above the delete eviction call

* fix(proxy): run the usage cache eviction beside the auth eviction so a hung Redis costs one wait

* test(proxy): wait on the broadcast instead of a sleep and type the peer eviction tests

* test(proxy): wait for the peer to apply the broadcast instead of polling its cache

* test(proxy): cover hashed keys and aliases when deleting a key warmed on a peer

* fix(key_management): evict every requested key on delete, including keys whose row is already gone

A batch delete that names a live key and a key whose row was removed outside the API evicted only the live key, so the gone key kept authenticating from every worker's cache. Every requested key hash is evicted now, while raw non-key cache keys are still left alone

* test(key_management): split the gone-row eviction assertion into one comprehension per worker

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 10:20:00 -07:00
Mateo Wang
cbfb1abdb3
fix(proxy)!: label shared-handler errors the way the route catch-alls do (#39555)
* fix(proxy): name shared-handler errors by the status they answer

handle_exception_on_proxy pinned type=internal_server_error on every
exception it wrapped, so a management route rejecting a caller's own
argument answered {"type":"internal_server_error","code":"400"} and an
SDK branching on the type read its own bad request as a gateway outage.

Derive the type from the status the response actually carries, keeping
a type the exception already names.

* test(proxy): pin a 500 for exceptions whose status is None

* fix(proxy): keep internal_server_error for timeouts and 5xx, redact hosts in shared handler errors

Provider 5xx and timeouts keep internal_server_error so retry loops still retry them,
the shared handler redacts internal hosts from raw exception messages, and /key/list
routes its HTTPException through the shared handler instead of its own copy of the
old label.

* fix(proxy): label shared-handler errors like the chat route, pick the status before the type, redact only gateway failures

* fix(proxy): pass shared-handler error messages through as on main

Redacting timeout and 5xx messages changed what every shared-handler route
returns, which relabeling the error type does not need, so messages go back
to the verbatim text main sends.

* fix(proxy): redact only the status-less failure main never sent

* fix(proxy): answer a generic 500 for a status-less failure and expect auth_error on the /user/new 403

A failure with no status now answers "Internal server error" instead of its own text, since that text can
name a guardrail's private host and pattern redaction misses cluster DNS names and IPv6 addresses. The
/user/new integration test now expects the auth_error type main's route catch-alls give a 403 the proxy raises

* fix(proxy): answer only a 4xx or 5xx carried status and fall back to the route default otherwise

A numeric-string status outside 400 to 599 made /v1/messages, images, rerank and the OpenAI pass-through answer HTTP 200 with an error body or drop the connection, and non-ASCII digits or a 5000-digit string slipped through isdecimal. error_status_code now accepts an int or a three-digit ASCII string in 400 to 599, the range the chat route already enforces

* refactor(proxy): parse a three-digit string status without recursing
2026-10-30 06:58:05 -07:00
zoveki
87fec5467f
fix(sso): keep CLI SSO and Claude Code gateway team lists in the user record's order (#49033)
* fix(sso): bind CLI SSO and Claude Code gateway logins to the first team on the user record

* fix(proxy): keep SCIM and JWT team sync in record order, and look up SSO groups once

SCIM PATCH and JWT team sync saved a user's teams from a set, so the first
team on the record, the one a Claude Code gateway sign-in binds, could change
on any sync. Both now keep the record's order and append new teams in the
order the IdP sent them.

SSO group sync now drops claim groups that are not LiteLLM teams with one
lookup before adding the missing teams one at a time, so a claim full of
non-team groups no longer costs one failed add per group on every sign-in.

* test(sso): expect the gateway team dropdown in the user record's order

* fix(sso): read team existence from the writer, keep new-user and SCIM replace team order

* fix(scim): keep the teams a user already has in record order on PATCH and PUT

* fix(scim): keep record team order when a SCIM POST matches an existing email

* fix(scim): join new teams in request order so a failed roster write keeps it

* fix(scim): let a groups replace win over later group ops in a PATCH again

Applying group adds and removes after a replace made a PATCH that deactivates a user
and adds a team that does not exist answer 404 and leave the user active, where it
answered 200 before. Keep the record's team order, but ignore group ops after the last
replace as the base did.

* test(sso): type the CLI team detail row helper

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: rj-cairn <349020452+rj-cairn@users.noreply.github.com>
2026-10-30 05:07:00 -07:00
devin-ai-integration[bot]
0070ce23b3
fix(router)!: apply content_policy_fallbacks when router settings update at runtime (#45428)
* fix(router): apply content_policy_fallbacks when router settings update at runtime

* test(router): annotate the fallback regression test's locals as Final

* fix(proxy): validate router_settings.content_policy_fallbacks on /config/update like its siblings

* test(proxy): annotate the content_policy_fallbacks config update test locals as Final

* fix(proxy): keep litellm_settings content policy fallbacks, accept dict targets, skip invalid stored rules

A runtime content_policy_fallbacks rule now layers over the litellm_settings rules instead of replacing
them, so models the runtime rules do not name keep their file fallbacks. Fallback targets may be
deployment dicts as well as model names. A stored rule that fails validation is logged and skipped at
sync, so the other router settings still apply. Per-user token discovery now reads every fallback key
from key and team router_settings.

* fix(proxy): validate content policy fallback rules one by one and keep a config-owned list alone

An invalid stored rule is now skipped on its own instead of dropping every runtime rule, a router_settings list from the config file stays the router's only rules across DB syncs, and GET /fallback names the model of a dict target.

* fix(proxy): answer an error for a string-valued fallback rule on GET /fallback instead of its characters

* fix(proxy): keep config-file content-policy rules as Router init accepted them and log a skipped rule once per change

* fix(proxy): log a skipped content-policy rule whenever the saved rules change

* fix(proxy): keep general fallbacks when no content-policy rule is saved, name a model-less target's model, and take only model names in key rules

* fix(proxy): reject an empty dict target in a key's content-policy rule

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 04:27:11 -07:00
devin-ai-integration[bot]
b8283d8b61
feat(usage): show subscription-covered providers and account fees separately (#44772)
* feat(usage): show subscription-covered providers and account fees separately

* fix(usage): write only the named fee columns and sync the root schema

The fee update now writes just the columns the request names, so two admins changing
different fields at once both land, and the dashboard edit form sends only what changed.
The root schema.prisma carries the new table like the two copies, and a billing period
anchored in the calendar's last month ends on December 31, 9999 instead of failing.

* fix(usage): register the subscription table span and keep provider logic under llms

* fix(subscription_accounts): register the fee routes as management routes and reject an empty account id

* fix(usage): run the new tests in CI and close subscription card review gaps

Adds the subscription test file to its unit shard, sets the migration lock timeout, rejects infinite fees, keeps the card from rewriting login files, lists each deployment once without aliases, shows the provider badge on entity views, and asks before removing a fee

* fix(usage): resolve named-credential ChatGPT accounts and keep admin viewers read-only on the fee card

* fix(usage): count a subscription fee only for accounts behind a current deployment

Usage listed a fee row for an account no deployment uses while another ChatGPT
deployment existed and dropped it when none did. It now lists only accounts behind a
current deployment in both cases. Adds a real proxy and Postgres integration test for
the fee API and for ChatGPT Responses rows on a deployment's own login, and names the
card's multi-component test with the integration suffix.

* fix(usage): keep a stored subscription fee listed after its account's deployments are removed

The usage route lists every account behind a subscription deployment plus every account carrying a stored fee, and reads the fees on every proxy, so a fee stays visible, counted, and editable for past ranges after its last deployment goes. The integration test now polls every day the calls spanned, so a UTC midnight crossing no longer fails it.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 03:58:36 -07:00
tessellark
080e643edc
fix(proxy): apply new model rows on every worker as soon as they are written (#49034)
* fix(proxy): apply published model rows on every worker before the debounced resync

Config sync messages now carry the ids of model rows a create, update, or upsert wrote. Every worker that receives one loads those rows by id right away, including while its debounced full resync is still waiting, so a group added one deployment at a time is complete on every worker as soon as the write returns. Deletes, bulk writes, and messages from older pods carry no ids and keep the full resync path

* fix(proxy): load published model rows from the primary under the reconcile lock

Read the by-id rows through the writer so a lagging read replica cannot put an old row back, and read them inside MODEL_RECONCILE_LOCK so a concurrent delete or reload stays the last word. Look the ids up in bounded chunks, reload DB credentials only when a row names one this worker lacks, and leave auto-router rows to the debounced full resync. Add an integration test that boots two proxies and needs the peer to fall back to a just-added backup within 4 s.

* test(proxy): cover a published model row that names no credential

* fix(proxy): keep the unknown model name read-through on the replica and outside the lock

Only the by-id load that follows a published write reads the primary inside
the reconcile lock. A request for a model name a worker does not know reads
through the replica routing before taking the lock again, as on main, so
unknown names put no load on the primary and do not wait behind a reload.

* fix(proxy): keep access group edits from dropping staying deployments and batch published config ids

* fix(proxy): read access group rows from the primary so quick edits are not lost to a lagging replica

* fix(proxy): install ordinary rows queued with an auto router and check access group edits before writing

A batch of published model ids that held one auto router skipped every row in it, so ordinary
rows written at the same moment waited for the full reload. The by-id load now leaves only the
auto router rows to that reload.

An access group update now reads every deployment from the primary, answers 404 for an unknown
group and 400 for an unknown model id or name before it writes anything, then removes the group
from the deployments that leave before it adds it to the ones that join, so a failed write leaves
the group narrower than asked, never wider.

* fix(proxy): write an access group update in one transaction after a fresh read

The update now checks model names first, reads every deployment from the
primary, and commits every join and leave in one batch right after that
read, so a failed write changes nothing and a field another admin set
during validation is kept. Other workers hear about the rows only after
the commit.

* fix(proxy): lock the model rows while an access group update reads and rewrites them

* test(proxy): run the model resync tests on the real installer and a real router

The by-id resync tests now install rows through ProxyConfig's real installer and credential loader on a real Router, faking only the database, and the config sync test row model is frozen.

* fix(proxy): lock only the rows an access group update touches and answer 404 before 400

An access group update locked every model row, so a lock on any unrelated
row stalled it for 30 s and then failed it with a 500, and unrelated model
edits on other proxies waited behind it. It now takes a per-group advisory
lock, locks only the rows that carry the group or that it selects, and
reads them again once locked. The group existence check runs before the
model name check again, so a missing group answers 404 as on main.

* fix(proxy): never wait on a locked row in an access group update

The update now takes its row locks with NOWAIT. When another edit holds one
of the group's rows, it rolls back, holding nothing, and tries again for about
10 s before answering 409. Holding rows while waiting deadlocked against team
deletes and let a concurrent model edit undo the update's change. The
per-group lock also moves to the two-key advisory space, apart from team ids.

* fix(proxy): retry access group row locks for 30 s with SKIP LOCKED and lock only the rows model_ids select

* fix(proxy): take the access group lock with pg_try_advisory_xact_lock and retry while another edit holds it

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-30 03:19:25 -07:00
norkelbyte
e0778bc9dd
refactor(types): replace Any with proven types in 21 files (#48921)
* refactor(types): replace Any with proven types in 35 files

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

* fix(types): keep anthropic chat transformation json path untyped

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

* fix(types): accept null gemini vector store fields and load encryption TypedDicts on python 3.10

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

* fix(types): validate Python client user responses and type delete_user as the count it returns

* fix(types): keep loose payloads working under the sweep's validators

An off-schema Anthropic stream event is now skipped instead of ending the
guardrail's text and stop_reason scan, and MAI image usage accepts the loose
numbers it did before. New dict and list adapters hide their input from
validation errors, RerankResponse indexing keeps its field types, and the
delete_user test mocks the int /user/delete returns.

* fix(types): make the changed DynamoAI fields ReadOnly and skip non-object A2A stream items

* refactor(types): tighten round 2 parse sites and keep payloads out of errors

Parse Anthropic SSE events with validate_json and ignore extra fields, validate admin team and key rows per row with hidden inputs, type rerank get and __getitem__ per key, and move the Phoenix prompt version response to a frozen BaseModel

* fix(types): keep MAI image calls alive on unreadable usage counts

An unreadable MAI usage count now counts as absent instead of failing a
billed image call, and the provider's usage fields pass through as sent.
The Anthropic stream scan warnings no longer log raw model output, and the
off-schema skip has tests that fail on the old scan.

* fix(types): count non-object MAI usage as absent and parse Anthropic SSE with json.loads

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-29 18:43:24 -07:00
Mateo Wang
636e707089
fix(proxy)!: stop answering with the literal string "None" as error type and param on the proxy-server, auth, spend, and key health routes (#39540)
* fix(proxy): stop answering with the literal string "None" on the proxy-server, auth, and health routes

The error objects these routes return defaulted `type` and `param` to the
four-character string "None", so a client's error handler matched no known
OpenAI type and fell into its generic branch.

Route them through the shared openai_error_payload helpers instead, so `type`
comes from the carried type or the HTTP status and `param` serializes as JSON
null.

* fix(proxy): stop answering with the literal string "None" on the spend and management routes

The error objects these routes return defaulted `type` and `param` to the
four-character string "None", so a client's error handler matched no known
OpenAI type and fell into its generic branch.

Route them through the shared openai_error_payload helpers instead, so `type`
comes from the carried type or the HTTP status and `param` serializes as JSON
null.

* test(proxy): pin the mid-stream failure frame and the DB outage body to a JSON null param

* fix(proxy): type upstream timeouts as timeout_error and update integration asserts for the null param

A 408 with no carried type now answers timeout_error instead of invalid_request_error, so a client branching on type keeps retrying it. Three integration suites still compared auth rejections against param="None".

* fix(proxy): keep a 408 on main's invalid_request_error type and pin the transcription file cap's 400

* fix(proxy): type a route failure by the status it answers with after the tag-routing and no-deployment remap

* fix(proxy): keep auth_error on proxy-raised 401/403 route failures and ban the string None as an error type or param

* fix(proxy): type a hook's 401 or 403 on completions, moderations and transcription by its status, as chat does

Only a status the proxy remapped into 401 or 403, such as a tag-routing denial, keeps auth_error.

* test(proxy): build the transcription file-cap test on a real router so it holds after the router refactor
2026-10-29 18:20:52 -07:00
Nate Armstrong
ead221995e
refactor(router): move router callers onto the entry points and invert the proxy imports (#48847)
Code outside the router now reaches it only through the entry points
(registry, settings, health, select, apply, observe, register_plugin, retire)
or the endpoint methods; the outside-member allowlist shrinks from 139 to 45.
litellm/router.py no longer imports litellm.proxy: it asks the installed
RouterHost (litellm/router_host.py) for vertex pass-through credentials,
member auto-router authorization and auto-router prompt compression, and the
proxy installs its host on import. Call sites where a model name can be
missing keep the old answer instead of reaching an entry point with None.
2026-10-29 17:26:33 +00:00