litellm/litellm/proxy/db
devin-ai-integration[bot] ae01882535
feat(proxy): offload spend tracking to a pod-local collector sidecar (#40545)
* feat(proxy): offload spend tracking to a pod-local spend worker sidecar

py-spy on the gateway showed the post-response _PROXY_track_cost_callback,
spend-log and DBSpendUpdateWriter work running on the inference workers'
event loop, so a DB or Redis stall backed up the request path.

When LITELLM_SPEND_WORKER_ENABLED=true, _ProxyDBLogger serializes one compact
typed SpendEvent per success and hands it to a SpendEventProducer that ships
it over a unix socket (default) or loopback-only TCP to a sidecar started as
`python -m gateway.spend_worker`. The sidecar runs the unchanged
_ProxyDBLogger pipeline against the pod's PgBouncer (pooled_database_url).
When the sidecar is unreachable, the buffer is full, or the gateway shuts
down with events still queued or in flight, the producer applies
LITELLM_SPEND_WORKER_ON_UNAVAILABLE (fallback in-process, or drop). The
sidecar half-closes producers on SIGTERM and drains, the producer treats
EOF as unavailable, and the gateway flushes buffered spend counters on
shutdown. The sidecar honors LITELLM_LOG so its writes are visible in its
own process log.

Helm: both charts gain an opt-in spend-worker sidecar container sharing an
emptyDir socket dir, and the componentized chart's HPA uses a
ContainerResource CPU metric scoped to the gateway container so sidecar
CPU does not drive inference scaling.

* feat(terraform): opt-in spend-worker sidecar for the AWS and GCP gateway stacks

Adds spend_worker_* inputs to both modules. On ECS Fargate the sidecar is a second, non-essential container in the gateway task; on Cloud Run it is a second container in the gateway service. Both listen on loopback TCP, share the gateway's DB/Redis/secret env, and set LITELLM_JOB_ROLE=spend_worker. Disabled by default. Plan-only tests cover both, and the terraform CI workflow now runs the gcp module too

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

* test(proxy): retrieve a completed batch in the in-process spend path test

The base now defers cost tracking for batches that are still in flight, so an in_progress batch never reaches update_database

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

* refactor(proxy): rename the spend worker sidecar to collector

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

* fix(proxy): run the collector from the installed litellm package and finish in-flight fallbacks on shutdown

The sidecar command becomes python -m litellm.proxy.collector so the classic image, whose runtime
stage copies only the installed package, can run it. The module now assembles DATABASE_URL and the
pod-local pgbouncer URL itself, replacing gateway/collector.py

The componentized collector sidecar inherits gateway.volumeMounts so custom CA mounts reach it.
SpendEventProducer shields an in-progress fallback from the writer task cancellation so close()
no longer loses an event already handed to the in-process pipeline

Helpers used across modules (address_argument, should_store_prompts_and_responses_in_spend_logs,
flush_spend_counters_on_shutdown) become public so the change adds no reportPrivateUsage errors

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

* ci(terraform): drop the gcp job duplicated by the aws/gcp matrix

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

* fix(collector): keep metrics env off the classic sidecar and reject shared loopback ports

The classic chart no longer hands PROMETHEUS_METRICS_PORT and the billing metrics env to the collector container, and gives it the same /.npm scratch mount as the proxy on a read-only root. AWS and GCP now refuse a plan where the spend collector and the metrics sidecar bind the same loopback port. A regression test drives a sidecar crash mid-stream on asyncio and uvloop and checks no event is billed by both the sidecar and the in-process fallback; the producer docstring spells out why a failed drain() cannot double count

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

* style(proxy): format pooled_database_url after the pgbouncer rebase

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

* fix(proxy): keep the cache-hit preset key and survive dead producers on collector drain

Cache hits updated the logging object after the early return, so the offloaded spend event carried
preset_cache_key=None and the collector re-hashed reconstructed kwargs. Also guard write_eof() against
producer transports uvloop already closed so one dead connection cannot abort the drain

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

* fix(terraform): keep the gcp collector port off the metrics sidecar health port

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

* fix(proxy): collector connects to Postgres directly under IAM or Entra token auth

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

* fix(proxy): mark the collector's DATABASE_URL as pooled when it uses the pod's pgbouncer

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-10 17:14:13 -07:00
..
db_transaction_queue fix(spend_logs): keep partition DDL transactions alive for their statement timeout (#40098) 2026-09-07 11:38:53 -07:00
autorouter_session_rollup.py feat(ui): itemize auto-router classification spend (#40168) 2026-09-07 16:29:19 -07:00
base_client.py refactor(lint): apply every safe ruff autofix and zero 28 strict-rule budgets 2026-08-01 15:43:29 -07:00
budget_window_spend_writer.py fix(proxy): keep persisted spend another pod has not incremented in the window seed 2026-08-31 15:49:03 -07:00
check_migration.py fix(proxy): kill the whole prisma process group when a boot migration command times out (#39509) 2026-09-08 10:06:53 -07:00
create_views.py refactor(types): replace Any with real types across 178 backend files 2026-08-27 10:14:58 +00:00
daily_spend_bulk_upsert.py feat(spend): report prompt caching savings as total and gateway-attributed (#38134) 2026-08-28 00:19:06 -07:00
db_lookup_gate.py fix(proxy): bound concurrent key and spend-counter DB lookups to stop prisma pool thrash (#40387) 2026-09-10 05:44:30 +00:00
db_spend_update_writer.py Merge pull request #39556 from BerriAI/litellm_fix_spend_log_flush_event_loop_binding 2026-09-08 15:59:11 -07:00
db_url_settings.py feat(proxy): let the in-container pgbouncer follow rotating RDS IAM and Azure Entra tokens (#40623) 2026-09-10 16:18:00 -07:00
dynamo_db.py chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
exception_handler.py fix(proxy): recreate the Prisma client when the writer session turns read-only (#40610) 2026-09-10 13:53:42 -07:00
gateway_request_tracking.py perf(proxy): collapse per-worker SGR upserts into one statement per flush (#40362) 2026-09-09 10:48:06 -07:00
health_check_latest.py fix(proxy): dedup latest health checks in SQL and gate the DB save per window 2026-09-03 11:33:44 +02:00
log_db_metrics.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
pgbouncer.py feat(proxy): offload spend tracking to a pod-local collector sidecar (#40545) 2026-09-10 17:14:13 -07:00
prisma_client.py fix(proxy): default max_idle_connection_lifetime on componentized DB URLs (#40285) 2026-09-08 15:54:31 -07:00
proxy_worker_heartbeat.py fix(proxy): count worker heartbeats on the primary so replica lag cannot undercount 2026-08-14 17:23:06 -07:00
query_engine_reaper.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
routing_prisma_wrapper.py fix(db): let the writer pin yield to the replica while the writer is degraded 2026-08-29 11:44:45 -07:00
shadow_eval_funnel.py feat(shadow_eval): measure both arms' cost so a job reports what the router would have saved (#38631) 2026-08-28 15:13:19 -07:00
spend_counter_reseed.py fix(proxy): bound concurrent key and spend-counter DB lookups to stop prisma pool thrash (#40387) 2026-09-10 05:44:30 +00:00
spend_log_batching.py fix(spend): bound each spend-log write statement by row count as well as bytes (#37758) 2026-08-21 09:49:51 -07:00
spend_log_tool_index.py chore(typing): clear 1.6k basedpyright Any errors across 56 files 2026-08-11 06:47:39 -07:00
token_auth.py fix(proxy): honor DATABASE_DISABLE_PREPARED_STATEMENTS in componentized entrypoints (#38363) 2026-08-26 10:39:04 -07:00
tool_registry_writer.py merge(litellm_internal_staging): resolve typing sweep overlap, keep best-of-both types 2026-08-29 06:48:36 -07:00