Commit graph

252 commits

Author SHA1 Message Date
Mateo Wang
b6dd3d932c
Merge pull request #42019 from BerriAI/litellm_master_key_boot_enforcement
feat(proxy)!: refuse to start with an unset, empty, or publicly known master key
2026-09-19 19:04:02 -07:00
Yassin Kortam
755f5c535d
Merge pull request #41994 from BerriAI/litellm_redis_spend_requeue_safety 2026-09-19 18:19:57 -07:00
ryan-crabbe-berri
99f99cfb46 fix(proxy): stop the boot when the requested master key migration fails, unless allow_requests_on_db_unavailable tolerates the outage 2026-09-19 18:10:52 -07:00
ryan-crabbe-berri
a6c51ba3de fix(proxy): never treat plaintext that base64-decodes to nothing as a ciphertext during the master key migration
A string such as "*" or "..." has no base64 characters, so it decoded to no bytes and read as an empty plaintext under any key. The migration would have counted it and overwritten it with a ciphertext of the empty string. Also read from the writer database instead of a read replica, report a database error during the migration instead of crashing the boot, skip columns the connected schema lacks across every schema on the search path, cap the JSON walk depth for the recursion detector, and move the boot wiring into one tested function.
2026-09-19 17:53:17 -07:00
ryan-crabbe-berri
38d776bd2b feat(proxy): re-encrypt stored secrets at boot from LITELLM_MIGRATE_FROM_MASTER_KEY so an unsafe key can be replaced while the proxy refuses to start
Rotating through POST /key/regenerate needs a running proxy, which a refused boot does not have. The refusal now counts the stored values that decrypt under the unsafe key. When there are none it only asks for a new key. When there are some it also asks for LITELLM_MIGRATE_FROM_MASTER_KEY, and the next boot with a safe key re-encrypts them and logs that the variable can be deleted. Leaving the variable set afterwards is a no-op with one notice.
2026-09-19 17:32:58 -07:00
yassin
e49e6bc660 fix(proxy): stop re-sending un-resendable spend batches from the Redis buffer 2026-09-19 19:54:11 +00:00
Tin Chi Lo
ed40241d26 fix(proxy): estimate auto-router baseline costs from durable cache history 2026-09-19 12:44:47 -07:00
Mateo Wang
f6d4766ebe
Merge pull request #41878 from BerriAI/litellm_requeue_daily_spend_without_redis_buffer
fix(proxy): requeue daily spend rows when the commit fails without the Redis buffer
2026-09-18 17:27:36 -07:00
mateo-berri
c6c8aed3f8 fix(proxy): drop only the daily spend batch whose failure cannot be re-sent, requeue the unsent ones 2026-09-18 16:22:04 -07:00
mateo-berri
5db2a0c885 test(proxy): type the sqlstate test parameters 2026-09-18 16:05:42 -07:00
mateo-berri
b3cf45e9f2 fix(proxy): drop daily spend batches that cannot be re-sent safely instead of requeueing them 2026-09-18 15:58:24 -07:00
mateo-berri
3edbf60e9c fix(proxy): requeue the daily tag rollup on commit failure without the Redis buffer 2026-09-18 14:58:34 -07:00
ryan-crabbe-berri
a5f6ce7bb1 Merge remote-tracking branch 'origin/main' into litellm_lit_3269_project_spend_tracking
# Conflicts:
#	tests/test_litellm/proxy/db/test_db_spend_update_writer.py
2026-09-18 14:22:07 -07:00
ryan
b6f4ad190e refactor(proxy): freeze the member spend arrays and budget link to stay within the type discipline budget
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:17 -07:00
ryan
2d61fa66b1 fix(proxy): lock teams in sorted team id order and keep existing member budgets on re-add
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:17 -07:00
ryan
499c334fce fix(proxy): lock each team in sorted order before the member spend upsert
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:17 -07:00
ryan
60077e90aa fix(proxy): take the team advisory lock before the member spend flush
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:17 -07:00
ryan
efef6ab684 fix(proxy): write team member spend as one roster checked upsert statement
Replaces the per team advisory lock and Pydantic roster parse in the spend flush
with a single INSERT ... ON CONFLICT statement that checks the stored roster in
SQL, so malformed roster JSON cannot fail the whole flush and large batches no
longer issue two queries per team inside the fixed transaction deadline

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:16 -07:00
ryan
6dce85c728 fix(proxy): skip recreating membership rows for members removed before a spend flush
Take the team advisory lock in the spend flush transaction and read the roster
through it, so a delayed flush after /team/member_delete cannot recreate the
deleted LiteLLM_TeamMembership row. TEAM_ADVISORY_LOCK_SQL moves to
team_repository so the spend writer can import it without a circular import

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:16 -07:00
ryan
de4b520153 fix(proxy): track team member spend when the member has no budget
add_new_member only wrote a LiteLLM_TeamMembership row when a budget id
resolved, and the spend writer used update_many so a missing row failed
silently. Members without a budget therefore never accrued per-member spend.

The membership row is now always upserted (budget_id NULL when no budget
applies) and the spend write is an upsert so members added before this fix
start accruing on their next request.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:54:16 -07:00
mateo-berri
72a14246a6 fix(proxy): requeue daily spend rows when the commit fails without the Redis buffer
With the Redis transaction buffer off, each daily spend queue (user, team, org,
end user, agent) was drained into a dict and handed to the bulk upsert. When
the upsert raised after its retries, the drained dict was discarded and the
exception escaped update_spend, so those rows never reached the daily rollup
tables and /user/daily/activity stayed short forever while /spend/logs had
every request.

Each daily queue now flushes through one helper that puts the uncommitted
remainder back on the queue for the next tick and moves on to the next table,
the same shape the window-spend step already used.
2026-09-18 13:50:56 -07:00
ryan-crabbe-berri
89289d4f77 fix(proxy): surface a failed project spend enqueue at error level
Some checks are pending
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
The call-site guard kept a project enqueue failure from skipping the sibling
spend writes, but logged it at debug only, so a dropped project charge was
invisible on a default log level. Log it through spend_log_error inside the
helper and re-raise, the way the org and agent helpers already do.
2026-09-17 16:26:26 -07:00
ryan-crabbe-berri
e4778cd2d1 test(proxy): drive the real spend queue in the project enqueue isolation test
Substitutes a queue that rejects project items instead of replacing writer
methods with mocks, so the test asserts the sibling key, team and tag spend
actually landed in the queue rather than that a mock was awaited.
2026-09-17 16:24:51 -07:00
ryan
70164dabd2 fix(proxy): isolate project spend enqueue failures from sibling spend writes
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 23:20:10 +00:00
ryan
bde5593523 Merge remote-tracking branch 'origin/main' into litellm_lit_3269_project_spend_tracking
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	litellm/proxy/common_utils/reset_budget_job.py
2026-09-17 22:29:16 +00:00
yassin
acd4f0eb04 fix(spend_tracking): attribute router-rejected requests to the model group provider
A request for a configured model group that the router rejects before picking a deployment (all deployments in cooldown, no healthy deployment) never gets a custom_llm_provider in its logging kwargs. The spend log payload persisted an empty provider, the daily spend tables carried it through, and the Admin UI Usage page rendered those requests under unknown even though every model in the group has a provider

get_logging_payload now takes the proxy router and, when the logged provider is missing, infers it from the model group's deployments. It only attributes when every deployment in the group resolves to the same provider; mixed groups, unknown groups and a missing router leave the value empty as before. Explicitly logged providers keep precedence

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 23:10:47 +00:00
Yassin Kortam
f62f140f75
Merge pull request #41403 from BerriAI/litellm_key_total_spend
feat(proxy): expose lifetime total_spend on virtual keys
2026-09-16 14:49:02 -07:00
Yassin Kortam
95abc9fb0b
Merge pull request #41330 from BerriAI/litellm_team_model_max_budget_v2
feat(team): team-level model_max_budget with key-level overrides
2026-09-16 14:48:29 -07:00
yassin
6b7cafe92b refactor(proxy): share one typed increment for key spend and total_spend writes
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 09:58:36 +00:00
yassin
6cf35ed71b feat(proxy): expose lifetime total_spend on virtual keys
Adds a persistent total_spend column to LiteLLM_VerificationToken and LiteLLM_DeletedVerificationToken, incremented in the same write as spend and left alone by budget resets. Surfaces it on /key/info, /key/list and the Admin UI Virtual Keys table and key detail view

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 09:42:20 +00:00
ryan
b20f1422eb fix(proxy): carry project_id through key metadata enrichment and drop docstrings
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 02:28:38 +00:00
ryan
1a749d84bd fix(proxy): track project spend and enforce project budgets additively
Project-scoped keys never wrote spend to LiteLLM_ProjectTable, so
/project/info stayed at 0 and project budgets could not block. Wire the
PROJECT entity through the spend queue, redis buffer, and db writer,
reserve and increment a spend:project counter, reseed it from the
project row, reset project spend in the budget cascade, and read the
live counter in the project max budget check. Team member budgets keep
gating project-scoped keys alongside the project budget

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 01:56:00 +00:00
Yassin Kortam
67cb0bc089
Merge pull request #41313 from BerriAI/litellm_model_activity_response_time
feat(ui): show average response time per model in usage model activity
2026-09-15 18:43:48 -07:00
yassin
260ff5f491 feat(team): team-level model_max_budget with key-level overrides
A team can now carry a per-model budget map that every key on the team
inherits. A key's own model_max_budget entry for the same model takes
precedence, so it is gated on and billed to the key alone.

Backend: NewTeamRequest/UpdateTeamRequest accept model_max_budget (validated
like the key-level field, enterprise gated); the value is hydrated onto
UserAPIKeyAuth via the token view, TeamGrants and the carried budget state;
_check_team_model_budget enforces it in the centralized common checks; the
limiter meters spend under team_model_spend:<team>:<model>:<duration> and
skips the team counter when the key overrides; /team/update lets only a
proxy admin raise, re-window or drop a cap; /team/info exposes usage.
The Anthropic context-management compaction summary subrequest runs the
same team gate. Both fallback token-view SQL definitions project the column.

UI: team create and edit forms reuse the key-level ModelMaxBudgetEditor,
premium gated, sending {} to clear and omitting unchanged fields.

A key entry overrides the team cap only when it spend-gates the model
(non-negative max_budget); a row that only carries tpm/rpm limits or a
negative cap leaves the team cap in force.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 00:40:58 +00:00
Yassin Kortam
5071d96d16
Merge pull request #41255 from BerriAI/litellm_org_member_spend_tracking
fix(proxy): track per-member organization spend
2026-09-15 17:25:40 -07:00
yassin
5b0fa89056 feat(ui): show average response time per model in usage model activity
Roll request_duration_ms of successful, non-internal requests into the
daily spend tables as total_response_time_ms plus timed_requests, expose
both through the daily activity endpoints, and derive the average in the
Usage -> Model Activity view of the Admin UI

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-15 22:16:12 +00:00
yassin
2505fcf95e test(proxy): type the fixture parameters of the org member redis test
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-15 20:05:23 +00:00
yassin
264305de23 fix(proxy): keep access-group raw SQL writes on the writer while writer_unavailable is stale
A stale RoutingPrismaWrapper.writer_unavailable flag made WriterPinnedClient hand back the routed wrapper, where query_raw is classified as a read, so the access-group UPDATE statements behind /key/regenerate, /key/generate with access_group_ids and model rename/delete went to the read replica and failed with SQLSTATE 25006. Route those raw statements through the underlying writer regardless of the flag; a raw SQL write has no replica fallback. WriterPinnedClient keeps yielding to the replica for degraded reads.

The model sync's backing-row count stays on the writer too: it runs right after the row delete/update on the writer and a lagging replica could still report the removed row, which would leave the group naming a model nobody serves.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-15 19:55:07 +00:00
yassin
c9ccda210e test(proxy): declare org member spend test bindings Final
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-15 19:49:58 +00:00
yassin
fb00567e4c fix(proxy): track per-member organization spend so the Organizations UI shows member spend
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-15 18:38:07 +00:00
yassin
c6a36fb8b6 fix(health): skip background health check DB writes when the latest-row read fails
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 6d2c4899b0)
2026-09-14 23:03:30 +00:00
devin-ai-integration[bot]
56045503db
fix(proxy): bound tool and guardrail index create_many by the spend-log statement budgets (#40561)
* fix(proxy): bound tool and guardrail index create_many by the spend-log statement budgets

One flush drains up to MAX_LOGS_PER_INTERVAL source transactions or logs, but a
transaction fans out to one LiteLLM_SpendLogToolIndex row per tool and a log
to one LiteLLM_SpendLogGuardrailIndex row per guardrail, so the index
create_many payload was unbounded. Both index writes now go through
spend_log_write_batches(SPEND_LOG_WRITE_BATCH_MAX_BYTES, SPEND_LOG_WRITE_BATCH_MAX_ROWS).
The tool index write moves out of the rollup batch_() so the split reduces
the query-engine payload; replayed index rows are no-ops under
skip_duplicates, and the daily rollup upserts stay in one transaction

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

* test(proxy): pin the row budget in the index fan-out tests

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

---------

Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 19:00:47 -07:00
Mateo Wang
d72ae3b9a2
Merge pull request #39895 from BerriAI/litellm_deflake_20260905
test: deflake redis semantic cache sys.modules leak, LangSmith init loop patch, wall-clock stagger assertion, and zombie grandchild check in the fake prisma cli
2026-09-12 15:20:26 -07:00
devin-ai-integration[bot]
1c61c2606e
perf(proxy): one MGET and one pipeline for post-call spend counters, no team/user/org refetch on the response path (#40841)
* perf(auth): prefetch user, team, membership, org and project in one MGET, one query and one pipeline

Auth read each object with its own Redis GET and, on a miss, its own DB
query, then the admission spend counters with one GET each. The prefetch
warms every entry the checks read with one MGET, one raw query for the
Redis misses and one pipeline write, and a per-request batch serves the
spend counter reads from one MGET. The per-object getters stay the
readers and the fallback, so enforcement does not depend on the prefetch

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

* refactor(auth): keep prefetch and spend batch collections immutable

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

* perf(auth): let the cold spend-counter reseed reuse the admission MGET instead of one GET per counter

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

* perf(auth): prefetch referenced auth objects only after the key's model access check passes

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

* test(auth): give the prefetch-ordering test's patches their test-quality reasons

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

* test(auth): move the real-Postgres prefetch join test to the proxy_behavior shard

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

* fix(auth): read NULL nested permission and budget lists as [] in the prefetch join

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

* perf(proxy): batch post-call spend counter reads and carry budget state through the request

Post-call warm checks, reservation reads and reconcile reads for one request now go through a task-local spend counter batch: one MGET answers every counter, successful increments write their result back into the batch so no second Redis read follows, and invalidation forgets the key. RedisCache.async_increment sends INCRBYFLOAT and its TTL command in one pipeline round trip.

Auth pins frozen team, user and org budget snapshots on UserAPIKeyAuth, the pre-call setup writes them into the request metadata, and Prometheus reads them back instead of calling get_key_object, get_team_object, get_user_object and get_org_object on the response path. The getters stay as the fallback for requests that carried nothing (custom auth, unauthenticated routes, skipped checks).

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

* perf(proxy): reconcile the budget reservation and the post-call warm checks from one MGET and one pipeline

A scope opened inside an open spend counter batch binds into it instead of starting its own, so the reservation reconcile and the post-call warm checks share the request's single MGET. The reconcile reads every reserved counter concurrently, sends the consistent adjustments in one INCRBYFLOAT+EXPIRE pipeline and settles a flushed or reseeded counter on its own afterwards, keeping the pre-call resize fail-closed. PendingSpendIncrement moves to spend_counter_batch so budget_reservation can build a pipeline without importing a private name

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

* chore(proxy): drop the dataclass import left behind by the PendingSpendIncrement move

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

* fix(types): import Self from typing_extensions so the proxy imports on Python 3.10

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

* test(proxy): use a neutral organization alias in the carried budget state tests

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

* test(proxy): cover recorded and forgotten spend counter values in the request batch

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

* test(caching): assert async_set_cache_pipeline_with_ttls keeps per-entry TTLs

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

* refactor(proxy): type the reservation entry carried through reconcile adjustments

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

* fix(auth): map the model table's aliases column to model_aliases in the prefetch join and read user memberships the way get_user_object does

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 16:05:50 +00:00
mateo
441f863abb Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_deflake_20260905
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	tests/test_litellm/proxy/db/test_pgbouncer.py
2026-09-12 09:18:42 +00:00
devin-ai-integration[bot]
98f6c14a76
test(pgbouncer): stop the never-listens replacement test flaking under CI load (#40830) 2026-09-11 19:32:56 -07:00
devin-ai-integration[bot]
b5bf09d22d
fix(db): carry DATABASE_SSLMODE/DATABASE_SSLROOTCERT into the assembled writer and reader URLs (#40815)
* fix(db): carry DATABASE_SSLMODE/DATABASE_SSLROOTCERT into the assembled writer and reader URLs

The componentized gateway supervisor starts the in-container PgBouncer from the
DATABASE_URL assembled out of the discrete DATABASE_* vars before config.yaml is
read, so an IAM URL had no way to request verified TLS: PgBouncer dialed the
server with server_tls_sslmode = prefer (no SNI, no verification) and public
RDS endpoints rejected the handshake. Two new env vars, exposed by the chart as
database.writer.sslMode / sslRootCert, are appended as libpq sslmode/sslrootcert
to every writer and reader URL the settings assemble (never to a pinned URL),
then translated for Prisma as before. Token refresh now also carries Prisma's
sslmode/sslcert/sslaccept over into the re-minted URL

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

* fix(db): keep TLS params on the CLI password URL and the initial IAM reader mint

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

* fix(db): treat DATABASE_SSLROOTCERT on its own as verify-full and cover collector and migrations TLS env

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

* test(db): type the reader mint TLS test double and drop its mutable capture

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-12 02:14:39 +00:00
ryan-crabbe-berri
e72d1cfa8d
Merge pull request #40572 from dclarksymmetry/fix/spend-counter-reseed-double-count
fix(proxy): prevent spend counter double counting
2026-09-11 18:14:04 -07:00
tin-berri
f22f9bc461
feat(auto-router): show routed model and savings in Claude Code and Codex (#40330) 2026-09-11 12:52:42 -07:00
mateo
da4052dcd7 test: give the fake pooler a readiness budget that survives a loaded CI worker
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-11 10:18:54 +00:00