Auth gate rejections are raised before add_litellm_data_to_request stamps the caller User-Agent and SpendLogsMetadata dropped the field, so failure spend logs and prometheus labels could not identify an abusive client. Stamp requester_ip_address and user_agent on the failure hook payload and carry user_agent through spend log metadata. Request scopes without a headers entry are tolerated.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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>
A zero computed decrement still fell back to an absolute spend: 0, so
spend flushed between the read and the commit of a zero-spend row was
erased the same way. The payload is now always
{"spend": {"decrement": spend_decrement}}, and a 0.0 decrement is a
no-op that preserves later spend.
Post-reset the admission spend counter was seeded with the in-memory
post-reset value, which misses increments that raced the reset write.
Invalidate instead: delete the in-memory and Redis counter keys so the
next get_current_spend read reseeds from the committed row.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The budget reset job read a row's spend, reset it in place, then wrote
spend: 0 (or decremented by max_budget under rollover) when committing.
Any spend the batch writer incremented into the row between the read and
the commit was erased while LiteLLM_DailyUserSpend kept it, so the daily
rollup permanently exceeded the counters.
Capture each row's spend before _reset_budget_common mutates it and write
a decrement of pre_spend - post_spend, which equals max_budget in the
rollover-over-cap case it replaces. Rows with no spend still get an
absolute spend: 0.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
_organization_max_budget_check returned early whenever org_max_budget
was <= 0, so an organization with an explicit max_budget of 0 was
treated as unlimited instead of zero allowance. Key, team, and user
budget checks already skip only on None; align organization budgets
with that convention.
validate_team_org_change had the same defect in a different shape: it
used a truthy check on the org's max_budget when validating a team
move, so an explicit 0 there silently skipped the guard too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POST /credentials let a duplicate name hit the unique index and handed back
Prisma's "Unique constraint failed" as a 500, so callers string-matched that
message to tell a caller mistake from a server fault. The unique violation now
maps to a 409 whose message names the PATCH route, two concurrent creates of
one name agree on it, and the detection lives in a repository helper the five
hand-rolled copies can move onto later
PATCH /credentials/{name} took a CredentialItem body, so the model_id the
Terraform adopt path sent was dropped. It now accepts UpdateCredentialItem and
shares the deployment lookup with create. Both handlers take the router as a
FastAPI dependency instead of reading the proxy global, which is what the
tests override
The router clamps a negative request_retry_count found in request metadata before counting a failure, and the proxy strips a client-supplied request_retry_count with the other router-reserved metadata fields. The rust OCR lifecycle test that trips the per-request cap now plants request_retry_count instead of attempted_retries, which the cap no longer reads since the previous commit