Commit graph

858 commits

Author SHA1 Message Date
joshua-berri
49f56e8b23
refactor(proxy)!: drop the unused accounting event tables (#49923)
LiteLLM_AccountingEvent and LiteLLM_AccountingDelivery have had no reader since
the SpendLogs projection replaced them, so the migration drops them with their
enums. AccountingDeliveryState stays because LiteLLM_SpendLogDelivery uses it

The schema now spells the delivery_id default the way Postgres stores it, which
ends the drift Prisma reported against the ledger migration. The schema check
compares base types only, so an attribute-only change is no longer reported as
a type change

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:23:30 -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
Krrish Dholakia
c5da69cd13
feat(ui): show model deprecation banner on the models page (#50080)
* feat(ui): show model deprecation banner on the models page

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

* style(ui): format model deprecation banner

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

* fix(ui): refresh model deprecations when models change

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

* fix(ui): address review feedback on model deprecation banner

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

* test(ui): cover deprecation role gate and agent builder refresh

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

* fix(ui): separate deprecation banner toggle from alerts link

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

* fix(ui): dedupe and allow dismissing the model deprecation banner

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

* fix(ui): refresh table once and key banner entries on successor

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

* fix: quiet successor logs, count banner models, use local dates

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

* fix(ui): pin banner test date and bucket upcoming entries locally

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

* test: cover mixed-successor log level through the deprecations endpoint

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

* test(ui): move deprecation banner cases into integration tests

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

* fix(ui): keep deprecation banner when a refetch fails

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 18:50:14 -08:00
joshua-berri
906fa7ed28
feat(accounting): add opt-in SpendLogs ledger (#49856)
* feat(accounting): add opt-in SpendLogs ledger delivery

* fix(accounting): preserve missing organization daily spend parity

* fix(accounting): preserve ledger receipts when replaying migration

* fix(accounting): preserve callback windows and tune ledger batches

* fix(accounting): preserve window charges and bound ledger statements

* test(accounting): verify budget callback registration order

* test(accounting): verify captured charges after repeated startup

* feat(accounting): store durable payloads independently of spend logs

* fix(accounting): preserve first delivery and trace ledger writes

* fix(accounting): keep SDK imports independent of proxy tracing

* fix(accounting): honor log retention during batch takeover

* test(accounting): cover retained batch alerts and invalid settings

* test(accounting): verify downgrade command against pending work

* fix(accounting): drain retries without starving fresh deliveries

* test(accounting): cover delivery projection, metrics and window key reads

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

* test(accounting): read delivery metrics without the shared registry

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

* fix(accounting): charge a leftover $0 batch row when its claim fails

A failed batch claim buffers the retrieve's row and reports it charged. At flush the
row conflicted with the pre-upgrade $0 SpendLogs row, so its delivery was rolled back
and the charge never reached the spend tables. The flush now takes that row over in the
same transaction and keeps the delivery.

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

* fix(accounting): trace the batch takeover at flush as a SpendLogs update

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

* fix(accounting): keep local spend logs when SPEND_LOGS_URL has no export client

The export client is never created, so main always writes SpendLogs to the proxy's own
database even with SPEND_LOGS_URL set. The ledger skipped local rows whenever the URL was
set, which dropped those request logs. It now treats a flush as an export only when the
client exists, and batch claims write local rows as before.

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

* fix(accounting): keep every request's charge when its log row is shared, evicted or rejected

- Key non-batch deliveries on a per-request delivery_id so a reused client call id is still
  charged once per request, while buffer replays stay deduplicated
- Persist the redacted api_key instead of an opaque bearer token
- Keep the accounting payload when the spend-log queue evicts a row or Postgres rejects it
- Strip NUL from payloads and skip non-text tags so projection never parks a paid request

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

* fix(accounting): isolate a raw-SQL spend log row Postgres rejects instead of requeueing the batch

A rejected row on the ledger insert path raises RawQueryError, which the exact DataError check missed, so the whole batch was requeued every flush. Data and integrity SQLSTATE classes now bisect like before, while missing tables and deadlocks keep retrying

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

* fix(accounting): keep charging a request whose tag is too long for the daily tag index

A tag past Postgres's btree entry limit failed the DailyTagSpend upsert, and on the ledger path that rolled back the request's key, team and user charges with it. Such tags are now left out of the daily tag rollup only

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

* fix(accounting): store only guardrail billing fields in the accounting payload

The ledger payload copied whole guardrail records, including responses and match details, into LiteLLM_SpendLogDelivery even when spend logs were disabled. It now keeps the id, name, usage and cost fields the projection reads. NUL stripping reuses safe_json_structure instead of a recursive walk

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

* refactor(proxy): move spend payload stamping out of update_database

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

* test(proxy): restore the modules the import-layering guard evicts

The guard popped litellm.proxy.utils from sys.modules for good, so later tests on the same xdist worker patched a fresh module while calling functions bound to the old one, and spend queue tests failed depending on scheduling

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

* refactor(proxy): keep update_database's payload stamping on its own local

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

* refactor(accounting): type guardrail entries and use a public key redaction helper

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

* fix(accounting): keep charging requests whose end-user id is too long for an index

An oversized end-user id failed the SpendLogs insert with SQLSTATE 54000, which was requeued forever, and failed the end-user upsert in the delivery, which parked the key and team charge with it. Class 54 rows are now isolated like other rejected rows, and such ids are left out of the end-user rollups only

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

* refactor(proxy): type the spend log queue snapshot

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

* fix(accounting): leave out any projected row Postgres cannot index or store

Caller-supplied ids and counters, such as a long tag or end-user id or an out-of-range compression count, failed the delivery transaction and rolled back the request's key, team and user charges. One check in project() now drops only the row that cannot be stored, replacing the per-field cutoffs

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

* fix(accounting): keep the guardrail fields compression savings are counted from

Slimming the guardrail records dropped the provider and the tokens_saved figure that Headroom compression savings are read from, and dropped a bare-dict record entirely. The payload now keeps both while still leaving out guardrail content

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

* fix(accounting): cap projected counters so daily totals cannot overflow

Counters such as a caller-supplied compression count fit one by one but overflowed the bigint daily total once summed across requests, rolling back the key, team and user charges in the same transaction. Each counter is now capped at 2^40, far below any total that could overflow

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

* fix(accounting): let a buffered batch charge take over its zero-cost receipt

A batch charge queued after a failed lookup hit the $0 receipt an earlier poll left behind, and ON CONFLICT dropped it. The buffered insert now runs the same receipt takeover as a live retrieve

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

* fix(accounting): read a rescued batch row's spend from its accounting payload

Rows rescued from an overflowing queue keep only the accounting payload, so the buffered takeover never saw their positive spend and left the $0 receipt uncharged

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

* fix(accounting): read budget windows through the delivery transaction

Window seed and existing-row reads used a separate connection while the delivery transaction held one, so with a one-connection pool they waited on their own transaction until it timed out. Inside an enclosing transaction they now share its connection

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

* fix(accounting): retry a transient failure while keeping a dropped row's charge

The rescue swallowed every error, so a database blip right after a row was rejected lost the unpaid charge. Only a rejected row is still logged and skipped, anything else propagates so the flush retries

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

* test(accounting): give delivery projection payloads their charged entities

project() now reads entities only from accounting_entities, so the hand-built
delivery payloads carry them like real stored payloads do.

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

* refactor(accounting): own ledger rows by their payload and key batches on the batch id

Drop the accounting_path stamp and its SpendLogs column. A buffered row that
carries an accounting payload is ledger owned, one without it is legacy, and
spend counters get a plain windows_from_ledger flag. Window seeding tells
applied ledger rows apart by their delivery row.

A ledger batch is plain transaction A keyed by the batch id, so a repeated
completed retrieve conflicts and charges nothing. A $0 retrieve records no
delivery, a batch a legacy retrieve already charged is not charged again, and
the legacy batch path is unchanged from main apart from refusing a batch the
ledger already owns. Batch takeover and its delta bookkeeping are removed.

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

* test(accounting): drop a projection test duplicated while rebasing

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

* refactor(accounting): drop previous_payload now that deliveries are never replaced

Batch takeover was the only writer of previous_payload, so the worker applies
each delivery's payload as stored and the column leaves the migration.

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

* refactor(accounting): drop the ledger downgrade checklist script

The use_spend_logs_ledger flag is the rollback path, so a separate downgrade
gate is no longer needed.

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

* fix(accounting): name the window seed reads so their Prisma span resolves

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

* refactor(accounting): remove the unused accounting event types and protocols

types.py, protocols.py and measurements.py were left from the old event
scaffolding. Nothing outside the package imports them, so they go with their
tests.

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

* fix(accounting): park only deliveries whose own data fails

A delivery that keeps failing on a database outage, lock timeout or deadlock
stays pending and retries with capped backoff, so a recovered database applies
it. Only a data error or an invalid payload parks the isolated row.

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

* fix(accounting): isolate a spend log row the insert statement rejects

The raw SpendLogs insert validates columns and JSON before Postgres sees the
row, so a row that fails there is now bisected out and dropped alone while its
charge is kept, instead of failing and replaying the whole flush.

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

* fix(accounting): carry a job's model window on every completed poll in ledger mode

The ledger keeps the poll whose delivery claims the job, which need not be the
poll that won the limiter's in-memory charge. Every completed poll now carries
the job's window, so the delivery that lands always has it, while the live
model budget is still charged once.

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

* refactor(accounting): leave model-budget windows on the existing window writer

Model-budget windows are written by the model budget limiter rather than
derived from a request's spend, so they stay on main's window queue like live
budget counters do. This drops window capture from the accounting payload,
the deferred limiter registration and the job-window capture, and the ledger
keeps owning key and team budget windows.

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

* fix(accounting): keep a delivery pending when the database is unreachable

Prisma reports a can't reach database server outage as a DataError, so the
worker now checks for an unavailable database before treating a data error
as the delivery's own fault.

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

* fix(accounting): isolate only rows the raw insert cannot encode

Catching every ValueError also swallowed unexpected insert errors that main
re-raises. The raw insert now raises RejectedSpendLogRow for an unknown column
or non-JSON value, and only that is isolated as a rejected row.

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

* fix(accounting): store zero savings when caller data makes them uncomputable

A huge caller-supplied tokens_saved overflowed the savings math inside
build_accounting_payload, which dropped the request's charge in ledger mode.
Savings that cannot be computed are now recorded as zero and the charge lands.

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-04 17:49:15 -08:00
Kerry Lu
b080c59e78
fix(proxy): treat Postgres restart and shutdown errors as DB unavailable, not poison spend-log rows (#50132)
* fix(proxy): treat Postgres restart and shutdown errors as DB unavailable, not poison spend-log rows

Postgres refuses every new connection with SQLSTATE 57P03 while it starts up, shuts down or replays WAL, and prisma surfaces that as a base DataError with no code. The spend-log writer took it for a poison row, bisected the batch down to single rows and dropped each one. Spend logs now retry and requeue on 57P03 and on server-terminated sessions (57P01, 57P02, 57P05), tool and model usage rollups requeue when the write provably never reached the server, the tool registry requeues its rows, and auth answers 503 instead of 401 while the auth database restarts.

Co-authored-by: Dinu Arateanu <dinu.arateanu@rbinternational.com>

* fix(proxy): requeue tool discovery rows only when the write never reached Postgres, keep the auth fallback to 57P03

A terminated session (57P01, 57P02, 57P05) may have committed the tool upsert, and the upsert adds call_count on conflict, so replaying it counts the calls twice. The non-Redis tool discovery flush now uses the same never-sent rule as the rollups, which also stops a permanently rejected row (54000) from coming back every tick. One terminated session does not prove the database is down, so it no longer opens the allow_requests_on_db_unavailable fallback, it still answers 503

* fix(proxy): a full server (53300, P2024) also never took the write, so counters and tool rows are sent again

Postgres refuses the connection before any statement runs when it is out of slots, and a prisma pool timeout never reaches the server either, so those failures are as safe to replay as 57P03 and P1001

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dinu Arateanu <dinu.arateanu@rbinternational.com>
2026-11-04 17:39:30 -08:00
Mateo Wang
7a641e5172
fix(proxy): answer an upstream overload with 503 server_is_overloaded on OpenAI routes (#49993)
* fix(anthropic): answer a 503 as 529 overloaded_error on /v1/messages

Anthropic's error contract has no 503, so a client built against it (Claude Code) treats a 503 from /v1/messages as a plain 5xx and retries it ten times instead of taking its overload path. A Bedrock 503 now leaves /v1/messages and /v1/messages/count_tokens as 529 overloaded_error, for both a JSON answer and a stream that fails before its first byte.

An Anthropic 529 overloaded_error now maps to litellm.ServiceUnavailableError (503) instead of InternalServerError (500), so OpenAI-shaped routes answer it as 503, the status OpenAI documents for an overloaded engine, and router retry and allowed-fails policies key it as ServiceUnavailableError.

* test(proxy): expect overloaded_error on a 503 stream error after a keepalive ping

* fix(proxy): record the 529 the Anthropic routes answer and type the chat stream error frame

The messages route stamps the answered status on the exception before post_call_failure_hook runs, so the spend log row, the OTel server span, the failed-request metric, and alerting record the 529 the client saw instead of the deployment's 503, while the deployment failure metric and cooldown keep the deployment's own status. A chat completions stream error frame names the type its status stands for instead of the null .type ServiceUnavailableError carries. An Anthropic-shaped 503 envelope passes through as 529 overloaded_error through a validated TypedDict, and count_tokens answers every ProxyException as the Anthropic envelope. Five integration tests that pinned 503 on the messages route now expect 529

* test(proxy): expect the top-level Anthropic envelope from count_tokens errors

count_tokens now answers a ProxyException as a JSONResponse carrying the
Anthropic error envelope instead of raising HTTPException, which FastAPI
wrapped in {"detail": ...}. The three legacy error-format tests now read
the response body and check that no detail wrap is left.

* test(proxy): cover the 529 overloaded_error answer across routes, cooldown, and retry budgets

* fix(anthropic): keep Anthropic overloads as InternalServerError and answer 529 from the provider status

An upstream Anthropic 529 stays litellm.InternalServerError (status 500) so
InternalServerErrorRetries, InternalServerErrorAllowedFails, SDK handlers,
and the OpenAI-shaped routes behave as on main. The error now carries
provider_status_code, and the /v1/messages routes answer 529
overloaded_error from it.

* refactor(proxy): unwrap a mid-stream fallback error once instead of recursing for the provider status

* fix(anthropic): keep count_tokens' detail body, end keepalive streams with an Anthropic error event, and log any 529 as an overload

* test(integration): cover the audit round 3 cells and expect 529 on the count_tokens 503 cases

* fix(proxy): re-raise an unchanged stream HTTPException as itself and walk nested mid-stream fallbacks for the provider status

* fix(proxy): walk nested mid-stream fallbacks without recursion

* fix(anthropic): preserve 503s and answer upstream 529s on messages routes

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

* fix(anthropic): answer an upstream Anthropic 529 as 529 on count_tokens

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

* test(anthropic): use integration coverage for keepalive errors

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

* fix(proxy): answer an upstream overload with 503 server_is_overloaded on OpenAI routes

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

* fix(proxy): pass answered overload status to failure hooks

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

* fix(proxy): preserve Anthropic route exception handling

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

* refactor(proxy): bind the transformed failure without rebinding the parameter

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

* test(proxy): observe the overload failure hook through a real ProxyLogging

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-04 16:37:55 -08:00
yuneng-jiang
8fcb016dc6
test: make three flaky unit tests deterministic (#50114)
* test: make four flaky unit tests deterministic

The skill archive fixtures stamped the current time into zip bytes used as parametrize ids, so xdist workers collected different ids and errored the whole shard. They now use a fixed timestamp.

The usage-based v2 override test pinned no clock, so a minute boundary between rpm checks reset the counter. It now pins get_utc_datetime the way #49505 does.

The stalled-burst DB lookup test asserted a 2s wall-clock bound. It now asserts every lookup was waiting at once, which fails if lookups are serialized.

The long tool_search placement test asserted a 1s wall-clock bound. It keeps the placement assertion and drops the bound, as #49505 did for the json accumulator

* test(skills): drop the archive fixture timestamp change now that #50118 removed its test
2026-11-04 14:38:44 -08:00
Kerry Lu
d38c5143ae
test(skills): drop the archive rejection test whose zip-byte ids differ across xdist workers (#50118)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 22:12:55 +00:00
Yassin Kortam
732a50318b
feat(proxy): authenticate billing metrics with a cognito bearer token (#49852)
* feat(billing): support Cognito bearer auth for metrics

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

* fix(proxy): refetch billing token on 401 and require https

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

* fix(proxy): import Unpack from typing_extensions

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

* refactor(proxy): make billing metrics bearer only and drop mtls

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

* style(proxy): format billing metrics module

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-04 14:08:40 -08:00
zoveki
784754ca48
fix(azure)!: answer 401 when Entra rejects the deployment's credential (#49863)
* fix(azure): answer 401 when Entra rejects the deployment's credential

A ClientAuthenticationError from the Entra token provider (wrong, expired or rotated client secret) used to surface as a retryable 500 carrying the app's client id and Entra trace ids. Map it to a 401 AuthenticationError that keeps the AADSTS code and first sentence with GUIDs redacted, on both the direct token fetch and the provider handed to the OpenAI SDK client. Network failures stay 500.

* fix(azure): keep network failures to Entra a retryable 500

azure-identity wraps a transport failure in ClientAuthenticationError, so the
401 mapping now skips errors whose cause chain holds ServiceRequestError or
ServiceResponseError

* fix(azure): cover async token providers and Entra 5xx, and scope caller-sent Entra credentials

* fix(azure): map only AADSTS-coded Entra rejections to 401 and keep caller credential failures out of cooldown

An Entra ClientAuthenticationError now answers 401 only when its message carries an AADSTS code, its HTTP
status is not 429 or 5xx, and the code is not the AADSTS50196 throttle, so a chained credential's outage or
throttle stays a retryable error. Caller-sent Entra fields no longer change the deployment id; a 401 from a
value the caller sent skips the shared deployment's cooldown instead. The Azure AI Foundry agent token fetch
maps the same rejection, and the A2A endpoint answers it with HTTP 401.

* fix(router): count router default_litellm_params as configured Entra credentials when deciding cooldown

* fix(azure): keep transient Entra codes retryable and scope 401 handling to Entra rejections

ClientSecretCredential drops Entra's HTTP status, so throttling and transient
codes (AADSTS50033, 50196, 90006, 90024, 90033, 90055) now stay retryable
instead of mapping to 401.

The A2A endpoint answers 401 only for an AuthenticationError caused by an
Entra token rejection, and the router skips cooldown for a caller-sent Entra
field only on Azure-family deployments. The azure/ Decisions route and the
Bing grounding search tool now map an environment-credential rejection to the
same 401.

* fix(azure): map only known Entra credential rejections to 401 and cover the OIDC token exchange

Unlisted AADSTS codes keep the retryable 500, the router skips the 401 cooldown only when an Entra rejection caused it, and a federated token Entra rejects answers the same 401 on Azure chat, Responses and A2A agents

* fix(azure): answer 401 for Entra rejections on the /azure passthrough and token counting

The fallback hop skips cooldown when Entra rejected a credential the caller sent,
Prometheus counts a deployment's own Entra rejection as a deployment failure again,
and an OIDC exchange error keeps Entra's headers so Retry-After reaches the router.

* fix(azure): keep a no-op caller Entra field from skipping cooldown and redact the passthrough 401 message

A caller-sent Entra field now skips the deployment's cooldown only when it
differs from the deployment, its named credential, the router defaults, the
AZURE_* environment variables and the default scope. An empty value counts as
unset, so a deployment whose own secret is wrong still cools down when the
caller's field changes nothing about the token request.

The /azure passthrough's Entra 401 message goes through the same internal
hostname redaction as other client-facing errors.

* fix(router): compare a caller's Entra field with the value the deployment uses

A caller-sent Entra field skipped the cooldown only when it matched no
configured source, so a value equal to an environment default the deployment
overrides (a stale AZURE_CLIENT_SECRET, say) counted as the deployment's own
and let a caller's rejected credential cool down a working deployment.

The check now resolves the value the deployment would use without the caller,
in order from the router defaults, the deployment, its named credential, the
AZURE_* environment variables and the default scope, and skips the cooldown
only when the caller's value differs from it.

* fix(azure): follow the settings merge for empty Entra fields and keep non-rejection OIDC exchange errors unchanged

* fix(azure): count only the failed attempt's Entra rejection and ignore a non-callable token provider

* fix(azure): keep the streaming Entra rejection inside the failed attempt

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 20:44:55 +00:00
Meryem Sakin
4a7fb30b0a
fix(guardrails)!: check Anthropic and Responses request tools in tool_permission pre_call (#41011)
* fix(guardrails): check Anthropic-format request tools in tool_permission pre_call

The pre_call hook only collected tools shaped as {"type": "function", "function": {...}},
so flat tools from /v1/messages ({"name", "input_schema"}) and /v1/responses
({"type": "function", "name"}) were never checked and deny rules silently passed.

Reuse the shared anthropic_tool_names helper so every request tool name is checked,
and handle Anthropic tool_choice {"type": "tool"} in both block and rewrite modes.

Fixes #40583

* fix(guardrails): detect Responses API named tool_choice in tool_permission

Responses API named choices put the name at the top level ({"type": "function", "name": ...}),
so rewrite mode removed the denied tool but left a tool_choice pointing at it.

* test(guardrails): run tool_permission pre_call tests through real default_on gating

Build the guardrail as an always-on pre_call guardrail instead of patching should_run_guardrail.

* test(guardrails): type the new tool_permission pre_call test parameters

* fix(guardrails): keep the Responses API custom tool type distinct

Anthropic's user-defined tools carry type "custom", so pre_call mapped
that to "function" for tool_type rules. The Responses API has its own
custom tool type, and mapping it let a custom tool slip past a
tool_type deny rule when the default action is allow.

Normalize only for the formats where custom means a plain function, and
read tool names directly in the rewrite path, which never needed a type.

* test(guardrails): cover flat tools through Messages and Responses

Add blocked and permitted flat-tool cases through the running proxy, with
endpoint coverage and metadata for the current e2e harness

Require a guardrail policy verdict when checking denied tools so an
unrelated provider error cannot satisfy the regression test

Co-authored-by: Christopher Krah <christopher.krah@plexito.de>

* fix(guardrails): check namespace and nameless tools and relax tool_choice in tool permission rewrite

* fix(guardrails): check forced, grouped, loaded, and hosted tools by their real type

* fix(guardrails): drop references to removed tools and count loaded tools in tool permission rewrite

* fix(guardrails): check tools sent outside top-level tools in tool permission

* fix(guardrails): judge namespace members by their bare name as a fallback and keep rewrites valid for Gemini, Bedrock and hosted tools

A namespace member is checked by namespace__name first, and by its bare name when no rule names the joined one. Rewrite mode now prunes Gemini allowedFunctionNames and turns off forced calling when no function is left, drops a removed hosted tool from allowed_tools, reads Bedrock toolSpec and systemTool blocks by name, skips cachePoint markers, and leaves a tools field that is not a list for the provider to reject instead of failing with a 500

* fix(guardrails): let only name rules decide a namespace member's bare-name fallback

* fix(guardrails): judge malformed tool types, empty Gemini name lists and pass-through Anthropic tools like the endpoints do

A tool or tool_choice whose type is not a string now reads as untyped instead of crashing the guardrail with a 500, and a null type key is never read as a Gemini tool part. An empty allowedFunctionNames list no longer turns off the functions a rewrite kept. An Anthropic custom tool reads as a function by its input_schema on every route, so a pass-through route judges it the same as /v1/messages. A Responses WebSocket frame now forwards the extra_body tools a rewrite stripped

* fix(guardrails): judge a named hosted tool by its type and keep a nested chat custom tool custom

* fix(guardrails): keep judging Anthropic client tools and type-named server tools by their name

* fix(guardrails): judge a named tool by its name alone only when the name is its type's own

* fix(guardrails): judge the function a tool names beside a declaration list or a top-level name

* fix(guardrails): judge Gemini hosted keys whatever the tool type and a namespace's function

* fix(guardrails): judge legacy functions entries like tools and send Gemini a namespace's members

* fix(guardrails): judge a namespace member by every name a provider reads and send Gemini its schema converted

* fix(guardrails): judge a legacy functions entry as a function and relax a tool_choice in extra_body

* fix(gemini): keep a namespace whole when inlining its members would repeat a function name

* fix(guardrails): judge a namespace member by every bare name that shares its qualified name

* fix(vertex_ai): declare a repeated namespace member name once instead of sending namespace names to Gemini

* fix(guardrails): judge a tool_choice sent in extra_body like a top-level one

A forced name only extra_body.tool_choice carried was never checked, so block mode let a denied name through to the provider

* fix(guardrails): judge a Gemini namespace member returned by its bare name and force a named namespace's members

* test: keep formatter churn out of the tool_permission and Gemini test files

* fix(guardrails): judge extra_body.input tools, argument-aware alias rules, and exact forced Gemini functions

Tools loaded in extra_body.input are now checked and stripped like top-level input. A returned
namespace member is judged by the alias whose first name rule its arguments satisfy. A forced
Gemini name that matches a top-level function keeps that function. Alias grouping and Gemini
declaration dedupe now run in one pass instead of quadratic scans.

* fix(guardrails): judge and rewrite every tool field sent inside extra_body

extra_body is merged into the upstream body, so functions, function_call,
mcp_servers and toolConfig nested there reached the model unjudged. Treat
extra_body as a second request layer that is collected and rewritten the
same way as the top-level request.

* fix(guardrails): judge Gemini hosted keys by key and count only the tools a provider receives after extra_body

* fix(guardrails): relax a tool_choice that names a dropped namespace member by its bare name

Gemini and Anthropic chat send namespace members under their bare names, so rewrite mode now treats a dropped member's bare name, and an emptied namespace's own name, as removed when no kept tool still carries it

* fix(gemini,guardrails): keep a top-level function over a same-named namespace member and leave malformed tool values to the provider

Gemini and Vertex chat now declare a top-level function over a namespace member that shares its name, so a forced call gets the top-level schema. post_call judges a returned name a top-level tool carries as that tool, and pre_call no longer raises on an unhashable or non-string tool name, input item type, or function_call name.

* fix(guardrails): judge a returned namespace member named like a hosted MCP server as that member

* fix(gemini,guardrails): send a namespace as main did when another tool has a member's name

Gemini chat inlines a namespace's members only when no other tool, namespace
member, or legacy functions entry declares the same name, and otherwise sends the
namespace by its own name as main did. A tool_choice naming such a namespace
forces the namespace itself. The tool permission post_call reads top-level names
the way Gemini declares them and judges a call the Responses bridge tags with a
namespace as that member.

* fix(gemini): send colliding namespace members as namespace__name functions and judge them in post_call

When a namespace member shares a name with another tool, Gemini chat now gets that namespace's members as namespace__name functions instead of one function named after the namespace, so every function it can call is a name the rules judged. Declared names are counted once, and a forced member maps to each holder's qualified name.

post_call judges a returned namespace__name as that member, by its qualified or bare name the way pre_call does, so a bare-name allowlist or denylist applies to it.

* fix(gemini): keep generated member names unique and let a top-level function own its exact name

A namespace that listed a member twice, or a generated namespace__name that
repeated a top-level function, sent Gemini two declarations with one name, and
Gemini answered 400 where main answered 200. A name a namespace lists twice is
now one member, and a top-level function keeps its exact name.

post_call now judges a returned name a top-level function declares as that
function, never as the namespace member the name could also mean.

* fix(gemini): qualify namespace members by the namespace's top-level name

The guardrail judges a member as `name__member` from the namespace's top-level
name. Gemini built the name from `function.name` when the tool also carried one,
so a request could pass pre-call and reach Gemini under a name no rule judged.

* fix(vertex_ai): force only the namespace members Gemini is sent

A forced namespace or member on Gemini chat could still allow a generated name that a top-level function or an earlier namespace owns, so tool_choice forced an unrelated function. The forced names now come from the same collision checks the declarations use

* fix(vertex_ai): reject a forced namespace Gemini gets no member of, and resolve forced names in one pass

* fix(guardrails): judge a namespace member by a rule on its namespace's name

---------

Co-authored-by: Christopher Krah <christopher.krah@plexito.de>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 20:34:34 +00:00
tessellark
8ab0321816
fix(proxy): charge a video's provider-reported cost once, on the first poll that sees it completed (#49824)
* fix(proxy): charge a video's provider-reported cost once, on the first poll that sees it completed

A custom video provider can report a video's cost on the status poll that
sees it completed. Every poll of a video shared the video id as its spend
log request_id, so the paid poll's row was dropped as a duplicate and every
later poll of the finished video charged the cost again.

Video status rows now carry their own `_video_cost` request_id, and a poll
that reports a cost goes through the same settle-once claim batches use, so
the first poll that sees the video completed charges it and writes the row,
and every later poll charges nothing.

* fix(proxy): charge a video's provider-reported cost once on model, router and session budgets too

A video status poll gets the _video_cost row suffix only when it reports a cost, so a $0 poll keeps the bare video id and adds no row of its own, and the spend log writer claims a row by that suffix.

The batch charge-once marker now covers videos, keyed on the video id under {spend_key}:video:{id}, with the batch marker key unchanged. Model and router budgets use it, and per-session budgets now charge a repolled batch or video once and ignore a cost that is not a finite number

* fix(proxy): charge a finished video's own cost once, keyed by the id the client polled

Guardrail cost on a running poll no longer takes the charge-once claim, only a completed poll charges, the session marker TTL refreshes like the others, and two deployments whose provider ids match each keep their own charge.

* fix(proxy): charge repeat poll guardrails, key video charges by canonical id and survive redaction

* fix(proxy): keep video status polls in-process for the collector and give guardrail-only job polls an unclaimable row id

* fix(proxy): keep repeat batch polls free and group guardrail-only batch rows under their batch's trace

* fix(proxy): settle a finished job whose own cost is below its poll's guardrail cost under the job's charge row

* test(proxy): count charged_poll_cost as covered through the finished job guardrail poll tests

* fix(proxy): settle a finished batch that cost nothing itself on its first priced retrieve

* fix(proxy): charge no budget for a batch retrieve before its cost is final, and read the retrieved batch from before redaction

* fix(proxy): charge sessions a poll's guardrail-inclusive cost, count a job cost row's guardrail run, and keep batch_cost_is_final importable from litellm.batches

* fix(proxy): claim a job and charge the session budget in one Redis call

The session limiter claimed the job marker and then raised spend in a second Redis call. A failure between the two left the job claimed while its charge sat only in local memory, so later polls skipped it. One script now does both, and a refused script claims and charges locally

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 20:22:06 +00:00
Kerry Lu
b662f3f7a4
feat(decisions): add EmpirioLabs AI as a decisions provider (#49977)
* feat(decisions): add EmpirioLabs AI as a decisions provider

empiriolabs/aplomb-1 now works with litellm.decisions and litellm.systemone, the proxy /v1/decisions and /v1/systemone routes, the decision model guardrail and the Admin UI. Requests go to POST /v1/decisions on the EmpirioLabs AI API with the System One body, keyed by EMPIRIOLABS_API_KEY with an optional EMPIRIOLABS_API_BASE

Chat models keep routing through the existing OpenAI-compatible JSON provider, and a test pins that

* ci: run the EmpirioLabs AI decisions tests in the llms unit shard

* fix: align EmpirioLabs registration with CI gates

---------

Co-authored-by: Adam Dalloul <47503782+Adam-Dalloul@users.noreply.github.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-04 10:41:16 -08:00
joshua-berri
02b8da2f16
refactor: replace accounting event scaffolding with SpendLogs projection (#49843)
* refactor(accounting): replace event scaffolding with SpendLogs projection

* test: preserve spend timestamps without removed capture callback

* fix: guard accounting event removal across schema update paths

* refactor(accounting): retain schema and prepare durable payloads

* fix(accounting): preserve dates in serialized payloads

* fix(accounting): retain schema push compatibility

* fix(accounting): retain span registration for the legacy schema

* test(accounting): remove obsolete capture acknowledgement test

* fix(accounting): date every daily projection row the way the model rollup does

A space-separated startTime kept the whole timestamp as the daily key, while the model usage rollup took the calendar date. Every daily row now uses the same date helper

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

* chore(ui): regenerate dashboard API types after rebasing on main

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

* refactor(accounting): project only from the stored accounting payload

project() now reads entities only from the payload's accounting_entities, so
the raw SpendLogs fallback branches are gone. build_accounting_payload() builds
the metadata once instead of dumping it to JSON and parsing it back. Guardrail
usage dates use a tolerant UTC parse, so a malformed startTime drops only the
guardrail rows instead of failing the whole projection.

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-04 10:37:07 -08:00
Mateo Wang
80b18d165b
chore(proxy): use neutral example names for baggage tag mappings (#50011) 2026-11-04 14:49:55 +00:00
tin-berri
48bd9b01f6
feat(skills): upload local skill ZIPs from the dashboard (#50008) 2026-11-04 00:56:40 -08:00
shrey
640223719f
feat(mcp): add an opt-in per-server setting to skip MCP tool output validation (#49961) 2026-11-03 20:42:10 -08:00
tavren27
b9ad61d4f6
fix(agentic)!: surface a failed agentic follow-up call instead of the pre-loop tool call (#49227)
* fix(agentic): surface a failed agentic follow-up call instead of the pre-loop tool call

* fix(agentic): surface failed follow-ups from the OpenAI chat dispatcher and update pinned integration tests

* fix(agentic): end a capped agentic turn on chat and responses instead of a retried 500

* fix(openai): run agentic hooks after the first call's error handling so a follow-up error keeps its own type and message

* fix(agentic loop): end capped turns on every chat path and bill the first call when a follow-up fails

A turn refused at max_agentic_loops now drops the internal tool calls of every hook that claims calls in it, while a call the client declared survives. The hosted_vllm and openai chat paths now carry the loop depth, fingerprints and max_agentic_loops into the follow-up, so they stop at the cap instead of looping forever.

A failed follow-up call now records the first call's usage and cost on the request's failure log before the error reaches the client. A code interpreter sandbox that is missing or fails becomes the tool call's output for the model to handle, so the turn completes without a retry.

On native streaming /v1/messages a hook or follow-up failure now reaches the client as an Anthropic error event with the mapped type, and compresr declares its retrieve tool so the stream holds it back. Comments and docstrings the branch added against the repo rule are removed

* fix(agentic loop): bill retried attempts and clean up what failed or capped turns return

The router's retry loop now carries each failed attempt's paid first call into the request's billing, so a request whose follow-up keeps failing bills every first call on its failure row, and one that recovers bills the failed attempt's first call on its success row. A failure row no longer adds unpriced estimated tokens on top of attempts that were billed, so its tokens match its spend

A sandbox failure now answers the model with a fixed sentence and marks the code_interpreter_call as failed, without the sandbox's error text. Code interpreter on /v1/responses with Claude now runs its loop, because the bridged inner call no longer strips the hook's own markers

Dropped internal tool calls leave the kept calls numbered from 0, also on a replayed stream. A capped native /v1/messages stream now says the loop hit its limit and that max_agentic_loops raises it, instead of asking the client to retry

The agentic refusal and hook error logs no longer include the model or the exception text, and the agentic loop helpers take typed messages, params, and read-only fingerprints

* fix(agentic loop): keep a recovered retry's cost breakdown on the response it returns

A retried agentic request that recovers no longer adds the failed attempt's first call to its success row. The proxy keeps the follow-up's own row for that request id, so the added spend never showed up in the stored row, and that duplicate row is tracked separately. When every attempt fails, the failure row still bills each attempt's first call

Pricing a first call for the failure row no longer replaces the request's cost breakdown. Before, a recovered retry answered with x-litellm-response-cost-original, -input, and -output from the failed attempt's first call. Now its headers and logged breakdown match a request that succeeds on its first try

* fix(agentic): stop retrying a request whose agentic follow-up failed and bill every round it served

An error raised by an agentic follow-up after the first call was served now carries a typed mark, so the
router's retry loops, the mid-stream retry, and litellm's num_retries wrapper raise it as is instead of
rerunning the whole request. A failure of the first call itself still retries, and fallbacks still run.

The failure row bills every round the loop served before the failure, at any depth, and a later attempt
that succeeds bills what the failed attempts paid on its success row, with the cost breakdown and the
response cost headers adding up. The error response's x-litellm-response-cost reports the same amount
its failure row bills. Failure rows outside agentic hooks keep the prompt-token estimate they had before.

A refused tool call now matches by id among the refused ids, or by name among refused calls that carry
no id, so a name-only refusal next to an id refusal is no longer kept.

* fix(code_interpreter): delete a session sandbox whose code run failed so the next call starts a fresh one

* fix(code_interpreter): delete only the session sandbox that failed, never its replacement

* test(code_interpreter): type the session sandbox test helpers

* fix(proxy): report what a failed agentic follow-up's failure row bills on /v1/messages errors

* fix(agentic): keep the no-retry mark through error mapping and bill only failed follow-ups on success

An agentic loop error that is not a provider error, such as the streaming chat cap or a crash in a hook's
plan build, now keeps its no-retry mark when litellm maps it to an APIConnectionError, so the router no
longer reruns every paid round. The mark is stored on the exception, so a built-in error can carry it too.

A crash in a hook's plan build on /v1/messages and /v1/responses bills the first call on its failure row.

A recovered attempt adds a failed attempt's usage to its success row only when that attempt was a failed
agentic follow-up. Other failed attempts, such as a dropped stream, log their own row as on main.

A code interpreter sandbox that fails while running the code now says the code may have run but its
result is lost, instead of saying the code did not run.

* test(responses): bill a retried MCP request each attempt on its own row again

Round 4 stopped adding a non-agentic failed attempt to the success row, so this test goes back to main's assertions: the success row bills only its own rounds and the failed attempt's first round stays on its own failure row

* fix(spend): name a failed follow-up attempt the success record already bills

With the usage meter on, a failed agentic follow-up attempt gets its own charge-eligible record, and the request's success record also carries that attempt's usage and cost once the success row bills it. The success record now lists the attempt's record ids in included_attempt_ids, so a reader can tell the success record already covers it. Nothing changes when the meter is off

* fix(spend): count a billed failed follow-up's tokens on the success row

* fix(spend): bill a failed follow-up on a fallback served from cache

* fix(spend): show a billed failed follow-up in a cached fallback's cost breakdown

* fix(spend): keep a failed follow-up's billing state off the request-merged call details

* fix(spend): record the cost breakdown of a successful agentic follow-up

The chat agentic loop returned the follow-up response with the cost its own internal call had already stamped, so the request's logging reused that cost and never recorded cost_breakdown on the spend row. The loop now drops that stamped cost unless the follow-up was served from the cache, and the request prices the response itself

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 02:00:33 +00:00
Mateo Wang
d5ca88900c
fix(anthropic): answer an upstream Anthropic 529 as 529 overloaded_error on /v1/messages (#46063)
* fix(anthropic): answer a 503 as 529 overloaded_error on /v1/messages

Anthropic's error contract has no 503, so a client built against it (Claude Code) treats a 503 from /v1/messages as a plain 5xx and retries it ten times instead of taking its overload path. A Bedrock 503 now leaves /v1/messages and /v1/messages/count_tokens as 529 overloaded_error, for both a JSON answer and a stream that fails before its first byte.

An Anthropic 529 overloaded_error now maps to litellm.ServiceUnavailableError (503) instead of InternalServerError (500), so OpenAI-shaped routes answer it as 503, the status OpenAI documents for an overloaded engine, and router retry and allowed-fails policies key it as ServiceUnavailableError.

* test(proxy): expect overloaded_error on a 503 stream error after a keepalive ping

* fix(proxy): record the 529 the Anthropic routes answer and type the chat stream error frame

The messages route stamps the answered status on the exception before post_call_failure_hook runs, so the spend log row, the OTel server span, the failed-request metric, and alerting record the 529 the client saw instead of the deployment's 503, while the deployment failure metric and cooldown keep the deployment's own status. A chat completions stream error frame names the type its status stands for instead of the null .type ServiceUnavailableError carries. An Anthropic-shaped 503 envelope passes through as 529 overloaded_error through a validated TypedDict, and count_tokens answers every ProxyException as the Anthropic envelope. Five integration tests that pinned 503 on the messages route now expect 529

* test(proxy): expect the top-level Anthropic envelope from count_tokens errors

count_tokens now answers a ProxyException as a JSONResponse carrying the
Anthropic error envelope instead of raising HTTPException, which FastAPI
wrapped in {"detail": ...}. The three legacy error-format tests now read
the response body and check that no detail wrap is left.

* test(proxy): cover the 529 overloaded_error answer across routes, cooldown, and retry budgets

* fix(anthropic): keep Anthropic overloads as InternalServerError and answer 529 from the provider status

An upstream Anthropic 529 stays litellm.InternalServerError (status 500) so
InternalServerErrorRetries, InternalServerErrorAllowedFails, SDK handlers,
and the OpenAI-shaped routes behave as on main. The error now carries
provider_status_code, and the /v1/messages routes answer 529
overloaded_error from it.

* refactor(proxy): unwrap a mid-stream fallback error once instead of recursing for the provider status

* fix(anthropic): keep count_tokens' detail body, end keepalive streams with an Anthropic error event, and log any 529 as an overload

* test(integration): cover the audit round 3 cells and expect 529 on the count_tokens 503 cases

* fix(proxy): re-raise an unchanged stream HTTPException as itself and walk nested mid-stream fallbacks for the provider status

* fix(proxy): walk nested mid-stream fallbacks without recursion

* fix(anthropic): preserve 503s and answer upstream 529s on messages routes

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

* fix(anthropic): answer an upstream Anthropic 529 as 529 on count_tokens

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

* test(anthropic): use integration coverage for keepalive errors

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-03 17:37:09 -08:00
jesus-berri
0b39e5faf3
fix(ui): send only shown SSO fields when editing, and keep env-row SSO values behind a blank stored value (#49973)
* fix(ui): send only shown SSO fields when editing, and keep env-row SSO values behind a blank stored value

* fix(ui): clear every competing SSO provider when the admin switches provider

Only the detected provider's fields were nulled on a switch, so a second stored
provider (e.g. Microsoft behind Google) kept outranking the new one at sign-in
2026-11-04 01:13:26 +00:00
Mateo Wang
de7bb21887
fix(credentials): preserve stored info when patching credentials (#49919)
* fix(credentials): preserve stored info when patching credentials

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

* test(credentials): type patch test parameters

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-03 16:15:09 -08:00
mrinal-berri
e3b12fe703
feat(proxy): configure the request header buffer size (#49883)
* feat(proxy): configure the request header buffer size

* fix(proxy): preserve automatic HTTP parser selection
2026-11-03 16:11:46 -08: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
2e0d333de6
chore(blog): refresh the bundled blog posts backup with the latest 50 posts from the docs RSS feed (#49971)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 23:41:15 +00:00
yucheng-berri
ffbbb6366b
feat(proxy): add general_settings.fips_mode startup gate (#48600)
* feat(proxy): add LITELLM_FIPS_MODE startup gate

With LITELLM_FIPS_MODE=true the proxy refuses to start when TLS verification
is off, when Python's OpenSSL does not enforce FIPS, or when the startup
password migration fails. A value that is not true/false also refuses.

* feat(proxy): refuse FIPS mode when cryptography's OpenSSL is not FIPS

The stock cryptography wheel bundles its own non-FIPS OpenSSL, which serves
JWT, Fernet and AES-GCM even when Python's OpenSSL enforces FIPS.

* fix(proxy): refuse FIPS mode when cryptography's FIPS provider cannot hash

The stock cryptography wheel reads the host's FIPS config and reports FIPS on,
but its bundled OpenSSL cannot load the host's FIPS provider, so every
operation fails. Require a working SHA-256 alongside the FIPS flag.

* fix(proxy): refuse TLS verification off anywhere in FIPS mode

A model's own ssl_verify: false reached the HTTP client after the startup check
passed. get_ssl_verify now raises in FIPS mode whenever verification resolves
to off, which also covers the startup check.

* refactor(proxy): return FIPS boot refusals as values and harden the FIPS tests

* fix(proxy): stop refusing TLS verification off in FIPS mode

Skipping certificate verification is a security choice, not a FIPS one:
the TLS connection still only uses algorithms the FIPS provider allows.

* feat(fips): refuse a database used without FIPS mode, drop the password migration change

FIPS mode marks a new database with a fips_mode row in LiteLLM_Config and refuses one that already holds
LiteLLM rows without it. Without FIPS mode, the proxy refuses a database carrying the marker.

* test(fips): close the database test clients with aclose so the engine stops

* feat(fips): read FIPS mode from general_settings.fips_mode instead of LITELLM_FIPS_MODE

* refactor(fips): keep fips_mode out of the API-writable general settings, it is read from the config file only

* fix(fips): retry the startup marker and state reads once after a dropped connection

A dropped connection during the non-FIPS marker read used to count as an unmarked
database, so a blip at boot let a non-FIPS proxy start on a FIPS database. Both reads
now go through call_with_db_reconnect_retry, the reconnect-and-retry the other startup
reads use. A read that still fails keeps its old outcome.

* fix(fips): refuse a fips_mode key that has no value

fips_mode: os.environ/X with X unset, or a bare fips_mode: / null, resolved to None and
started without FIPS mode and without a message, while an empty string refused. A key
that is present now has to be a boolean; only a config without the key means off.

* refactor(fips): move the database check out of the boot gate

The boot gate keeps the setting and the two OpenSSL checks. The check that a
database matches the FIPS setting moves to its own PR, so a boot without the flag
runs no extra database read.

* test(fips): pin how a fips_mode string is read through enforce_fips_mode
2026-11-03 14:56:14 -08:00
norkelbyte
3b35ae340d
fix(rate-limit): estimate embedding image and audio data URIs as media in TPM pre-call checks (#49831)
* fix(rate-limit): estimate embedding image and audio data URIs as media in TPM pre-call checks

* fix(rate-limit): count embedding media as media only on models that embed it

A data URI sent to a text-only embedding model is read as text, so it keeps
the text estimate there. On models flagged supports_embedding_image_input,
image and audio use the chat media estimates and PDF and video count at most
the model's max_input_tokens. The same estimate now feeds project and key
limits, the usage-based, latency and cost routers' deployment tpm, and the
deployment itpm check.

* fix(proxy): count embedding media only where the backend reads it as media

Vertex AI, Gemini, Azure AI and Bedrock count a strict base64 data URI as
media per item. Cohere counts it as media only when the whole batch is
media, and Voyage, OCI and unknown models keep the text estimate. File
blocks carrying a data URI count like the bare URI.

The router context window pre-call check and the batch limiter now use the
same estimate, resolving each deployment or JSONL row model to its
backends. The cost map gains supports_embedding_image_input on
voyage/voyage-multimodal-3 and oci/cohere.embed-multilingual-image-v3.0.

* fix(router): hold each embedding media item to the context window on its own

A PDF or video data URI alone already reserves at most the model's
max_input_tokens, but the context window pre-call check then ran the
blanked input through the Responses transform and added a few framing
tokens, so a PDF that Vertex accepts got a 400. When every input item is
media, the check now returns the largest item's media estimate, since an
embedding request embeds each item separately

* fix(proxy): leave embedding media out of the context check and narrow Bedrock media scope

The router context window check now counts only the text of an embedding input, so Titan image lists no longer 400. Bedrock Marengo and Nova with a PDF keep the text estimate since they read those inputs as text or reject them, async_invoke Nova counts images as media, and the unused batch limiter change is dropped.

* fix(bedrock): bill Cohere embeddings by Bedrock's input token count header

* fix(embeddings): check data URI base64 in place and keep Bedrock Cohere text usage

The strict base64 check now scans the payload in place instead of decoding
and re-encoding a full copy. Bedrock's billed token header is used only for
image requests, so text-only Bedrock Cohere usage matches main. The deployment
itpm estimator change is reverted, since the router never passes embedding
input to that check.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 22:06:59 +00: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
Kerry Lu
c7a6657449
feat(cost-map): record the provider's recommended replacement as successor_model on deprecating rows and report it from /model/deprecations (#49789)
* feat(cost-map): record the provider's recommended replacement as successor_model on deprecating rows and report it from /model/deprecations

* fix(cost-map): pick the alias row's successor by its own shutdown date when the family has several

* chore(ui): regenerate schema.d.ts for the /model/deprecations successor_model wording

* fix(proxy): prefer a configured successor_model over the cost map's when deployments share a name, and keep the cost map's under a hand-set deprecation_date

* test(cost-map): allow successor_model in the cost map row schema

* feat(cost-map): infer successor_model as the next version in the same family for 136 dated rows with no published replacement

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-03 13:31:12 -08:00
joshua-berri
6c6594f1d0
fix(proxy): omit cost headers for unpriced responses (#49467)
* fix(proxy): omit cost headers for unpriced responses

* fix(proxy): preserve declared free response costs

* refactor(proxy): share the cost map pricing lookup for the response cost header

The header check now reuses the same raw entry, fill-missing and rule lookup that
cost model selection uses, so the two can't drift apart. It also counts every declared
rate field, so a free model priced only per request keeps its 0.0 header

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-03 12:54:08 -08:00
yuneng-jiang
200ecdb6e6
fix(proxy): defer optional Prisma imports in SSO settings (#49771) 2026-11-03 12:10:15 -08:00
shrey
0aa7079f04
fix(spend): read only the newest rows when paging sessions (LIT-8513) (#48886) 2026-11-03 10:51:09 -08:00
tessellark
1cc7da6274
fix(proxy): list slash-less wildcard groups like claude-* on /v1/models and /model/info (#49832)
* fix(proxy): list slash-less wildcard groups like claude-* on /v1/models and /model/info

* fix(proxy): expand team-owned wildcard rows on /model/info through their public name

* fix(proxy): keep listing wildcard names wider than their deployment and skip the Azure placeholder

A group like *gemini-3* over gemini/gemini-3* lists the catalog ids the name matches and routes back, as main did on /model/info, now on /v1/models too. Wildcard listings no longer list the Azure-LLM placeholder. Team-owned wildcard rows on /model/info go back to main's behavior.

* fix(proxy): keep Azure listings on slashed wildcards and list bare deployments through custom_llm_provider

get_provider_models returns main's list again, and only slash-less names skip the Azure-LLM catalog placeholder. A slash-less name over a bare wildcard deployment now lists the catalog of its custom_llm_provider.

* test(proxy): pin custom_llm_provider guards and slashed wider-name listing

* fix(proxy): list bare ids for wider names over a custom_llm_provider deployment

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 16:14:04 +00:00
paveln83
7bd342d7ea
fix(proxy): let a config license unlock enterprise settings and spell "environment" right in license errors (#49745)
* fix(proxy): spell "environment" right in the allowed_ips and AWS KMS V2 license errors

* fix(proxy): read general_settings.litellm_license before the allowed_ips and enforced_params gates

* fix(proxy): read the config license before callbacks and gates, keeping a valid env license

* fix(proxy): keep an invalid config license reported and sync license data with the license in use

A config license that fails no longer overwrites the license string when the env license is not valid either, so /health/license still reports has_license. A failed boot-time env license is not re-checked. Both license writes go through one helper that also refreshes premium_user_data, so /health/license and the seat limits follow the license in use.

* fix(proxy): keep the env license's boot result when a config license fails, without a second license check

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 09:56:08 +00:00
norkelbyte
864b98acec
fix(bedrock)!: fail a Nova Sonic realtime session whose Bedrock stream drops instead of closing 1000 (#49170)
* fix(bedrock): fail a Nova Sonic realtime session whose Bedrock stream drops instead of closing 1000

The Bedrock SDK ends the output stream the same way for a dropped connection as for a session that sent sessionEnd, so the handler treated a dropped stream as a clean finish. The client got a 1000 close with no error event and the router logged 200 OK with no retry

An output end now counts as clean only after the client left and the handler began closing the Bedrock input. An earlier end raises a 503 BedrockError, so the client gets an error event and a 1011 close, the failure handlers run, partial spend is still logged, and a retry of a committed session is still refused

* fix(bedrock): fail a dropped Nova Sonic session right away and log it as failed

When Bedrock's output side ends mid-session, by a drop or a modeled stream error, the client side no longer spends the close timeout sending close messages into the dead input stream, so the error reaches the app right after the drop. A session the app ends itself still sends Bedrock its close messages and closes the input

The replay refusal now leads with the committed cause, so the error event and the cut 1011 close reason both start with it. It stays a 400 and is tagged as a failure outside the selected deployment, so router retries and fallbacks of a dropped session no longer add extra failure counts

A clean realtime session now counts once toward its deployment's cooldown success counter, so cooldown reads the real failure rate instead of 100 percent. Realtime tpm and rpm counting is unchanged

The spend row of a session the provider ended now reads failure with its error information, while the events already forwarded are still billed. A session the app ended keeps logging success

* fix(bedrock): agree on a dropped realtime session's failure across loggers and the spend row

A realtime session's success event now reports status failure, with the same
error_information the spend row stores, when the session recorded a provider
failure in its metadata. Usage already streamed stays billed and the router
still counts one cooldown failure

The spend row builder now runs every row's error_information through
sanitize_error_information_for_spend_logs (DB size cap plus prompt redaction),
so the realtime failure rows get the same treatment as the proxy failure hook,
which no longer sanitizes on its own to avoid truncating twice

Drop the selection-failure tag on the committed-session refusal, since the
shared logging object already keeps a retried refusal out of cooldown, and pin
the router's realtime success counting with an OpenAI realtime deployment

* fix(bedrock): count realtime router successes only for sessions that logged one, and log first-turn drops

The router counts a realtime success only when the session's success handlers ran, so a refused handshake or an upstream close before any frame no longer offsets its failure. The success event's payload status stays success again, so each logger gets one failure record per dropped session, while the spend row still reads failure from the metadata the Bedrock handler records. A committed Bedrock session that fails before any logged event now gets one zero-cost Logs row. After an output failure the input stream is closed in the background, so the SDK body writer finishes without delaying the client's error. Every input send failure reports the same 503 stream-ended cause as an output drop

* fix(realtime): count a router success only for a session that ended cleanly

A realtime session the upstream closes with 1011 or 1006 after its first frame now marks itself as ended abnormally where that close is seen, in the shared relay and in the OpenAI Live handler, so the router no longer counts it as a cooldown success. The success callbacks still run, so the proxy keeps the budget and parallel-slot settlement it had

A Bedrock session that fails before Bedrock sends any usage now logs no success event, as on base. The spend-row failure fields go into a new metadata mapping instead of the request's own dict, validated with a pydantic TypeAdapter instead of an unchecked TypeIs guard. The handler tests drop a wall-clock bound and type their helpers

* fix(bedrock): log a dropped realtime session's usage from its own logging state

The failure event replaced the shared standard_logging_object while the
queued success dispatch was still running its callbacks, so loggers
received $0 and 0 tokens for a billed dropped session.

* fix(proxy): settle a rate-limit reservation once when a request logs a failure and a success

A dropped realtime session fires a failure event and then a success event for the same call. The v3 limiter refunded the TPM reservation on the failure and released it again on the success, so the key's TPM counter went negative. The success path now skips a reservation the failure already released and marks it settled for a later failure. The realtime handler test callbacks are typed

* fix(rate-limit): settle each token reservation once when a request logs both a failure and a success

A dropped realtime session logs a success for its usage and then a failure. After a success the
limiter now clears every reserved amount (TPM, PTU, ITPM, OTPM), so a later failure refunds nothing
twice. A new reservation written in the same context after a released one now starts clean, so
reserve, fail, reserve, succeed charges the second request's usage once instead of 78 TPM or
28 ITPM for 27 and 20 used

* fix(rate-limit): take a token hold before awaiting so concurrent success and failure callbacks settle it once

* fix(rate-limit): track the ITPM/OTPM hold's release apart from TPM so a nested pre-call keeps the request's project IO charge

* test(rate-limit): pin the split hold-release flags across nested MCP pre-calls

* fix(realtime): count a relay that fails on its own as an abnormal end

* test(realtime): pin the relay failure exit with a client frame the provider rejects

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 07:56:23 +00:00
Emerson Gomes
ecb8ef5ca8
feat(realtime)!: support latest OpenAI audio models (#35600)
* feat(realtime): support latest OpenAI audio models

* fix(transcription): type SDK stream duration metadata explicitly

* fix(realtime): preserve transport settings and translation duration

* refactor(realtime): reuse SDK client initialization

* fix(audio): bill finalized streams and authorize transcription models

* refactor(audio): keep transcription pricing selection together

* fix(realtime): reconcile model metadata and test contracts

* fix(realtime): preserve provider duration and model capabilities

* fix(realtime): keep transcription validation compatible with cost map guard

* fix(realtime): address transcription and billing review findings

* fix(realtime): preserve input usage and provider routing

* fix(realtime): bill retained output and route Azure aliases

* fix(realtime): reject malformed provider durations

* fix(realtime): preserve batch cache rates and Azure v1 routes

* fix(transcription): reject nonfinite provider durations

* refactor(realtime): align annotations with current lint rules

* fix(realtime): preserve translation deployment pricing and input usage

* test(transcription): verify synchronous stream finalization

* fix(realtime): retain missing translation input usage at close

* fix(transcription): reject streams that bypass output guardrails

* fix(realtime): reconcile audio protocols billing and current CI

* fix(realtime): trust confirmed formats and authorize default models

* fix(realtime): preserve absent models and reject unsupported guardrails

* fix(realtime): reject translation policy pipelines

* chore(ci): rerun transient infrastructure timing failure

* fix(realtime): reconcile audio protocols with current upstream

* fix(audio): validate provider responses at SDK boundaries

* fix(realtime): preserve minimum OpenAI SDK compatibility

* fix(realtime): preserve deferred Live session authorization

* fix(realtime): preserve upstream authentication and transcription behavior

* fix(audio): scope provider duration precedence to configured models

* fix(typing): avoid Final declarations inside transcription cost loop

* refactor(audio): reuse transcription mapping and duration resolution

* fix(realtime): enforce model budgets and preserve translation routing

* fix(audio): log streamed transcripts whose done event reports detected languages

* fix(audio): pass stream, languages and keywords through unchecked for non-OpenAI transcription providers

Providers without a streaming transcription handler got UnsupportedParamsError
for stream=True, languages and keywords, where the merge base forwarded them as
provider-specific params. Only openai and azure validate them now.

* fix(audio): bill streamed gpt-transcribe from the provider's reported duration

The stream path computed cost after logging had replaced the transcription's
duration usage, so it fell back to the measured file length while the
unstreamed call billed the provider's seconds.

* fix(transcription): log streamed transcription usage from the provider response

The stream logging path built the spend payload from the response after
its duration usage was transformed away, so whisper-1 with stream=true and
streamed gpt-transcribe logged an empty usage object. Bill and log from the
provider's TranscriptionResponse in both the sync and async paths.

* fix(realtime): keep main's routing, protocol and transport behavior around the new translation routes

- Send OpenAI realtime and translation client_secrets and calls through litellm's httpx client again, so ssl_verify and custom CA settings apply and upstream errors keep main's shape
- Use the input transcription model as the session model only for transcription sessions
- Drop the new /openai/v1 translation aliases and record each lazy feature's own routes, so the /openai pass-through and the MCP routes resolve as on main
- Let a configured Azure realtime_protocol win for every model, with GA as the default only when none is set
- Return x-litellm headers on streamed transcriptions and mark the request done
- Refuse translation sessions before connecting upstream when realtime guardrails apply or the provider is not OpenAI or Azure
- Accept json and text for gpt-transcribe, and drop other formats under drop_params instead of failing
- Bill translation sessions on output audio seconds only, at the single published rate
- Document the 201 SDP answer on the translation calls routes and the transcription 200 response

* fix(realtime): restore main behavior for guardrails, Azure transcription, and realtime clients

Streamed transcription runs unstreamed when a deployment post_call guardrail applies, so the
guardrail still sees the transcript. Azure transcription maps v1, latest and preview to the
default dated version on the deployments route for every model. The SDK client secret keeps
session.model ahead of model, realtime WebSockets connect to api_base again, realtime metadata
keeps only guardrails, and managed agents need an explicit transcription model. Reverts the two
unrelated streamed Responses error codes and the duplicate cached_tokens_details field, and the
gpt-transcribe format check only reads the request provider's cost map entry.

* fix(realtime): keep main's transcription, client secret and route behavior where the PR changed it

Transcription reaches every provider as on main again, sends stream only when asked, forwards language and
languages together, keeps form key order, bills provider seconds only when above zero, honors base_model for the
new flags, keeps main's Azure api_version on the proxy path, and streams carry chat's headers, an error event on a
mid-stream failure, and a spend row when the stream ends early. Translation cost reads both per-second rates from
one source and spend rows carry the billed seconds.

Realtime transcription WebSockets keep model in the upstream URL outside OpenAI's hosts, client secrets forward
explicit nulls, accept conversation and delay, and answer 400 on a malformed body, the /openai translation paths
are back to main, translation refuses only for guardrails realtime sessions run, nested aliases resolve and later
session.update models are authorized, translation client secrets run per-model auth, beta clients bridged to GA
get beta-shaped session events, and transcription sessions send the deployment model in both shapes.

* fix(realtime): address review findings on transcription billing, streaming and realtime auth

Per-second pricing replaces token billing only when the priced entry sets no token rates, translation rates fall back per key to the cost map, transcription cache keys include languages and keywords, a stream request a post_call guardrail forces unstreamed comes back as redacted SSE, Azure beta clients are bridged only for GA-only deployments, nested transcription models on the realtime WebSocket are checked against the key, and translation spend carries key and team metadata

* fix(azure): pick the GA realtime protocol for deployments whose model_info.base_model is GA-only

* fix(realtime): preserve prior merge resolutions after rebase

* fix(realtime): enforce translation policies and normalize Azure transcription URLs

* fix(cookbook): stop translation once output idles after the upload ends

* fix(realtime): bill translation on input audio and drop pre-read stream cost headers

Translation spend now uses the input audio seconds the session sent, priced
through input_cost_per_second, instead of the translated output duration.
A streamed transcription no longer returns cost headers before the stream is
read, since the cost is only known after the last event. Also removes an
unused realtime guardrail helper.

* fix(realtime): drop top-level session nulls, bill dropped transcription streams, and bridge beta response fields

Client secret, translation and transcription session bodies drop top-level nulls again, as main did, while nested nulls such as turn_detection still pass through. Transcription client secrets send the deployment model as session.model. A streamed transcription the client drops still runs its success callbacks, and the stream ends with data: [DONE] like OpenAI's. The beta to GA bridge maps max_response_output_tokens, speed and noise reduction, remaps response.create, and drops temperature under drop_params. The Entra token for realtime HTTP routes is fetched off the event loop

* test(e2e): cover realtime translation, live transcription, client secrets, and streamed transcription

* fix(realtime): close review gaps in translation billing, transcription streams, and Azure GA

Bill translation sessions from the deployment's own per-second rates, add per-second prices for
Azure gpt-4o transcribe models, keep stream-scoped guardrails seeing the caller's stream flag, map
mid-stream transcription errors to LiteLLM exceptions with a [DONE] frame, map GA responses back to
the beta shape for bridged beta clients, honor the minting key's guardrail opt-out on translation
calls, restore the Azure transcription session URL, and flag GA-only Azure realtime models

* fix(realtime): bill a translation rate the deployment omits from the session model

* test(e2e): check translated speech is not English instead of exact French wording

* fix(realtime): keep main's behavior for non-bridged backends, stream error frames, gateway URLs and Azure transcribe prices

Remap beta events to GA only for bridged beta clients and translation sessions, bridge beta clients on GA-pinned Azure deployments, log error-only upstream sessions as failures, flag dated Azure GA realtime models, drop the unpublished Azure per-second transcribe rates, log a mid-stream transcription failure once, keep main's SSE error frame outside transcription, drop unsupported transcription params under drop_params, keep the caller's Azure auth headers, keep a gateway /v1 path, let key opt-outs release string-callback guardrails on translation, and keep the cookbook waiting while a long upload is still sending

* fix(realtime): keep the proxy's realtime session gate off translation sessions

A key that opts out of a legacy callback guardrail now gets a working translation session, since the transcription-guardrail gate no longer sends a realtime session.update the translation upstream rejects

* fix(realtime): keep websocket model limits, bill measured transcription length, route transcription sessions on the allowed model

A WebSocket opened without a query model now authorizes as an empty model, transcription cost prefers the measured upload length over the provider's reported seconds, transcription sessions overwrite nested models with the authorized top-level model again, and a non-boolean transcription stream value answers 400 or is dropped under drop_params.

* fix(ci): copy the rust bridge extension into the openapi docs gate's base worktree

* fix(model-catalog): accept the realtime translation endpoints in the cost map

* fix(cookbook): close the translation session and read until session.closed

* fix(realtime): treat an empty transcription stream as unset and pass the cost-map guard

An empty `stream` form value runs the transcription unstreamed again, as
OpenAI does, instead of answering 400. The four translate cost-map entries
drop the translation routes from supported_endpoints until the main
branch's catalog parser accepts them, and the nested transcription model
lookup uses the router registry entry point.

* test(proxy): drop the mid-stream error frame pin that main's error shape change replaced

* fix(transcription): bill uploaded seconds when a stream fails mid-transcript

* fix(proxy): bill uploaded seconds when a streamed transcription breaks mid-stream

* fix(audio): keep transcription's positional arguments and redact streamed error details

- Move the new languages, keywords and stream options to the end of transcription's parameters, so a prompt or format passed by position keeps its meaning
- Redact internal hostnames, paths and tracebacks from a streamed transcription's mid-stream error event, as chat streams already do
- Move the CA-bundle check for the realtime HTTP routes, which serves real TLS on loopback, from the unit suite to the SDK integration suite

* fix(audio): stream the guarded transcript to SDK callers that asked for a stream

* fix(logging): keep a streamed transcript out of callbacks that turned off message logging

A streamed transcription's assembled response reached success callbacks with its text intact under turn_off_message_logging or a callback's message_logging=False, because the shared streaming redactor had no transcription branch. It now redacts the text, as the unstreamed call already does

* fix(proxy): stream a transcription the deployment streamed when the client sent no stream field

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 07:27:52 +00:00
tessellark
f4a2e2b3af
fix(anthropic): send an https PDF image_url to Claude as a document (#49708)
Some checks are pending
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* fix(anthropic): send an https PDF image_url to Claude as a document

A PDF link sent as image_url to Claude on Anthropic went out as an image
block and Anthropic answered 400. A link whose path ends in .pdf is now
downloaded like Vertex links are, and the served Content-Type picks the
document or image block, so CDN links that serve a PNG from a .pdf path
keep working. An explicit format of application/pdf sends a url document
with no download. PDF media types match regardless of case or parameters,
and count_tokens picks the same block as the request.

* fix(anthropic): download https PDF links off the event loop

The proxy downloaded an https link ending in .pdf synchronously while
building the Anthropic request, so on an async call every other request
on that worker waited for the download. Those links are now inlined
before the request is built, the same way http links already are, and
the /v1/messages guardrail write-back does the same. A non-string
format on an image_url is ignored the way it already was inside the
image_url mapping, instead of raising an AttributeError.

* fix(anthropic): send a PDF link the proxy cannot download as a link and keep guardrail url blocks as sent

* test(anthropic): expect the PNG media type the served bytes carry at a .jpg path

* fix(anthropic): bound the PDF link download and keep query-string PDF names

Give up on a .pdf link after NON_LLM_CONNECTION_TIMEOUT instead of the
client's 6000 s default, fall back to main's whole-URL extension check so
download?file=report.pdf still counts as a PDF, treat application/x-pdf as
a PDF, and set the url document's application/pdf format only for the
guardrail write-back so other /v1/messages providers send what main sends

* fix(anthropic): probe a .pdf link and send it to Claude as a url document

Instead of downloading a .pdf link and inlining it as base64, ask the host for
its first kilobyte and send the link as a url document when it serves a PDF.
Every other link goes out as the url image main sends, so robots.txt handling,
caching and the wire format stay as they are on main.

Also restore the guardrail adapter and the content type inference to main,
keep the client's url document blocks (title, context, citations,
cache_control) through the guardrail write-back, treat a PDF filename in the
query string as a PDF link, probe links concurrently on the sync path, cap the
async probe's read at the download limit, and catch only transport, URL and
SSRF failures in the probe

* fix(anthropic): read only the first kilobyte when probing a PDF link, and restore each url document from its own place

The probe now streams the response and keeps the first 1 KB, so a host that
ignores Range no longer fills memory with up to 20 parallel bodies. The
guardrail write-back now matches url documents inside a tool_result and outside
one separately, since the write-back moves tool results ahead of other content

* fix(anthropic): remember each PDF link probe's verdict and cap its total time, and skip the probe on Bedrock invoke

* fix(anthropic): keep a PDF probe's error status for a minute, not ten

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 07:05:15 +00:00
Devin Gupta
216041e243
feat(decisions): add Parallel AI web-grounded decisions (#49188)
* feat(decisions): add Parallel AI web-grounded decisions

* fix(decisions): name the Parallel model decision

* fix(parallel_ai): restrict environment credentials to trusted decision endpoints

* fix(decisions): keep Parallel evidence fields off other providers and handle missing confidence

Only fields a provider names reach the OpenAI response, so existing providers keep
their base shape. A malformed api_base answers 400 on both credential paths. The
complexity router classifier and the Playground handle a provider that omits
confidence. Regenerates the lazy OpenAPI snapshot and the dashboard API types

* chore(cost-map): declare the Decisions routes on parallel_ai/decision

The provider endpoint files are generated from the cost map, so the row must
name both routes for the generated System One flag to hold

* fix(decisions): report no classifier confidence when a provider omits it

A choice answer with no confidence is a valid classifier answer, so the
complexity router picks its tier and leaves classifier_confidence and the
confidence signal out instead of reporting a different metric in its place.

Also show the Parallel AI logo in the provider picker and add a Parallel
translation fixture captured from the live API for both Decisions routes.

* fix(decisions): keep a token-priced row on token billing when it also sets a query price

A plain input_cost_per_query bills flat only on a row with no token price, and input_cost_per_query_decisions always bills flat, so a custom Decisions row that already bills tokens costs the same as before this PR

* fix(decisions): bill a deployment's query price and accept Parallel bases that end in a route

A proxy request logs through an object built before the router picks the deployment, so a query price on litellm_params never reached Decisions billing. Parallel Search accepts a base ending in /v1/search and shares PARALLEL_AI_API_BASE with Decisions, so Decisions now accepts that base and one ending in /v1/decisions.

* fix(decisions): keep per-query billing and optional confidence on Parallel only

A query price on any other Decisions provider moved that provider off token
billing, and a choice or score answer with no confidence became a 200 with a
null for every provider. Both now apply to Parallel alone, so other providers
bill and fail as they do on main.

A Parallel deployment with explicit zero token rates and a query price read as
free, so every budget check was skipped while it billed per call. It now counts
as priced.

The dashboard key for Parallel is now PARALLEL_AI so a served bundle that
predates the provider still sends parallel_ai as the provider slug, and the
README table lists the provider.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 04:47:02 +00:00
devin-ai-integration[bot]
08b951a814
fix(vllm): list hosted_vllm wildcard models when the server has no api key (#40066)
* fix(proxy): expand wildcard models for providers with model info but no cost map entry

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

* fix(vllm): stop requiring an api key to list vllm models

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

* fix(proxy): preserve model ids during wildcard expansion

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

* fix(vllm): forward api key for model discovery

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

* fix(vllm): support bearer auth for model discovery

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

* fix(vllm): list hosted_vllm models from the server chat uses, with a bounded wait

The hosted_vllm listing now resolves HOSTED_VLLM_API_BASE and HOSTED_VLLM_API_KEY like hosted_vllm chat, instead of the /vllm pass-through's VLLM_API_BASE, and waits at most NON_LLM_CONNECTION_TIMEOUT rather than the completion timeout. vllm/* keeps listing nothing, since its chat runs in process. The router pass-through now forwards the deployment's api_key.

* fix(vllm): skip the placeholder key, honor deployment TLS and headers, and retry failed listings after a minute

* fix(discovery): cache only timed-out model listings, so a fast failure is tried again on the next call

* fix(proxy): list wildcard models off the event loop, so a hung provider no longer stalls other requests

* fix(utils): keep a concurrent call's model listing when another call times out

* fix(proxy): run model discovery on its own thread pool and keep credential lookups off the event loop

* fix(proxy): stop the file credential lookup at the first match before listing wildcard models

The fallback lookup now walks the allowed model names lazily, in the same
order as before, so a hung vLLM server is listed only when no configured
name serves the requested provider. The vLLM auth headers no longer
mutate the caller's headers dict.

* fix(proxy): try every configured wildcard route before listing any wildcard's models in the file credential lookup

* fix(proxy): answer model lists from cached listings first and share one listing per server

* test(vllm): give the self-signed cert fixture fixed validity dates

* fix(proxy): skip wildcard servers a key cannot reach on /v1/model/info

/v1/model/info listed every wildcard deployment and filtered by the key
afterwards, so a key limited to one model waited on every wildcard server and
sent listing calls to servers it cannot use. The listing now skips a wildcard
whose patterns match none of the key's models before it asks the server.

A key or team granted hosted_vllm/* with no deployment of that name now lists
no models, like openrouter/*, since chat answers 400 for every id the
environment server would list.

* fix(proxy): match a wildcard's own row pattern before asking its server

The /v1/model/info pre-filter matched a key's models against both the
deployment name and its litellm_params.model, so a key granted
hosted_vllm/* still asked a vllm-secure/* -> hosted_vllm/* server whose
rows it can never see. It now matches only the pattern expansion names
the rows with, picked by one shared helper.

* test(vllm): cover the vLLM listings and no-slash wildcard aliases the patch left unrun

Adds tests for hosted_vllm get_models with and without a server, vllm/*
never asking a configured server, and /model/info pre-filtering of
wildcard aliases whose name has no slash. Drops a test comment that
only repeated a method signature.

---------

Co-authored-by: jesus <jesus@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-11-03 04:13:28 +00:00
ryan-crabbe-berri
be50cbe9d8
fix(responses)!: authorize and decode comparison response IDs (#49402)
* test(responses): reproduce comparison response ID ownership and wire failures

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

* test(responses): guard stale retries and malformed comparison envelopes

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

* fix(responses): authorize and unwrap comparison response ids

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

* test(responses): require comparison IDs to follow response security policy

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

* test(responses): deny raw comparison response ids by default

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

* fix(responses): reject raw comparison response ids by default

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

* test(responses): enforce managed ownership with unmanaged ID opt-in

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-03 03:40:46 +00:00
zoveki
704e73d6fe
fix(cost-map): give the bare gpt-4o, gpt-4o-mini, gpt-4.1 and gpt-4.1-mini aliases their 2027-01-14 shutdown date (#49707)
* fix(cost-map): give the bare gpt-4o, gpt-4o-mini, gpt-4.1 and gpt-4.1-mini aliases their 2027-01-14 shutdown date

* fix(proxy): report a deployment's own provider retirement date before the bare model's

An Azure deployment declared as model gpt-4o with custom_llm_provider azure, or as
azure/<deployment> with base_model gpt-4o, now resolves azure/gpt-4o's date instead of
the bare OpenAI row's. The GPT-4 series alias test now reads both cost maps with typed rows

* fix(proxy): read Azure's retirement schedule for azure_ai deployments

An azure_ai deployment with no azure_ai row of its own now reports the azure/<model> date, and a cost-map row wins over the provider-less copy the router registers under the deployment's key, which carries the bare model's date.

* fix(proxy): let custom and registered base_model rows keep their deprecation date

Only the copy the router writes at startup, which has no provider and inherits the bare row's date, gives way to a later candidate

* fix(proxy): report a pinned snapshot's own earlier deprecation date over its base_model's

* fix(proxy): recognize the router's copied cost-map rows by the loaded catalog

* fix(proxy): read Azure's schedule for a provider-prefixed azure_ai base_model

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 02:58:39 +00:00
alberto
e63067a23b
fix(anthropic_endpoints)!: return Anthropic error envelopes on /v1/messages auth failures (#48466)
* fix: Return Anthropic error envelopes on Messages auth failures

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* fix: Answer Anthropic envelopes on Messages auth failures without relabelling unmapped statuses

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>

* fix(proxy): answer the Anthropic error envelope on the /anthropic pass-through

The pass-through relays upstream errors in Anthropic's shape, but the proxy's
own refusals there (bad key, model access, budget) still answered the OpenAI
body, so an Anthropic SDK pointed at /anthropic could not read them

* fix(proxy): answer the Anthropic envelope for 404, 405 and unhandled 500 errors on Messages routes

A disabled Claude Code gateway 404, a wrong-method 405 and an unhandled 500 on the
Messages and /anthropic/ routes kept FastAPI's detail body or the OpenAI shape. The
central HTTP and unhandled-exception handlers now pick the Anthropic envelope by route.
The dashboard playground toast titles Anthropic error types when the body has no code.

* fix(proxy): keep LiteLLM error types and Anthropic envelopes on Messages routes

Error bodies on the Messages routes now carry error.litellm_error_type when the
LiteLLM type differs from the Anthropic type, so clients and the Admin UI keep
causes like budget_exceeded. Unlisted 4xx statuses map to invalid_request_error
per Anthropic's errors page, model-list requests that send anthropic-version get
the Anthropic envelope, and the admission, decompression and request-size
middleware errors use it on these routes too.

* test(proxy): expect litellm_error_type in Messages integration tests and type middleware tests

* fix(proxy): keep handler error bodies unchanged and timeouts as api_error

Errors the Messages and count_tokens handlers already returned in the Anthropic
shape keep their original bodies, so litellm_error_type now appears only on
errors the proxy raises before the handler. A 408 stays api_error as before,
and the playground toast titles the new Anthropic types and ignores long
numbers in front of an embedded error

* fix(proxy): keep the base error types inside the Messages handler and scope toast titles to Anthropic bodies

The wider status table (402 billing_error, 409 conflict_error, 504 timeout_error,
other unlisted 4xx invalid_request_error) now applies only to errors the proxy
raises before the handler. Errors inside the Messages handler, streamed error
frames and the SDK streaming adapters keep the merge base's table.

The dashboard toast reads a status before embedded JSON and titles by Anthropic
type only when the body is an Anthropic error envelope, so other pages keep
their titles.

* test(anthropic): mark the new test variables Final

---------

Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-03 01:53:29 +00:00
jesus-berri
acd084ab38
fix(mcp): answer bare OPTIONS on MCP routes without running LiteLLM auth (#49497)
Every MCP route (/mcp, /<server>/mcp, /toolset/<name>/mcp, /mcp/proxy) ran LiteLLM auth before looking at the HTTP method, so a bare OPTIONS probe got the RFC 9728 401 OAuth challenge and clients that probe with OPTIONS looped on re-auth. The Streamable HTTP handler now returns 204 with Allow: GET, POST, DELETE, OPTIONS before auth runs. GET, POST and DELETE are unchanged.

Resolves LIT-10231
2026-11-02 16:16:29 -08:00
paveln83
b84493d9a7
fix(proxy): log each endpoint's own call type on failed audio, moderation, image, and rerank spend rows (#49684)
* fix(proxy): record the route's call type on failed speech spend rows

A failed /v1/audio/speech request never puts a logging object on the request
data, so the failure lift returned nothing and the spend row's call_type came
out empty (or whatever the client sent in the body). Without a logging object,
lift the call type the route already resolved, so a failure row records the
same call type a success row on that route records

* fix(proxy): keep the call type of a failure logged under its own route

An MCP tool listing that fails inside a Responses, chat, or messages call
is logged under /mcp/list_tools, so its spend row keeps list_mcp_tools
instead of taking the outer request's call type

* fix(proxy): take a failed row's call type from its endpoint, not a route path guess

* fix(proxy): log the endpoint call type on failed image generation and rerank rows

* fix(proxy): estimate failed-call ledger usage from the route's call type again

The failure usage snapshot now gets the same call type the spend row uses for
its estimate, so a failed Responses call counts input from the newest
compaction item again, as on main.

The endpoint call type tests now go through real routers whose provider
answers 400, instead of asserting on a mocked failure hook.

* test(proxy): type the failed spend row tests and fixture

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 23:57:11 +00:00
Mateo Wang
a5fe912ecf
fix(proxy): preserve DB settings during config reload (#49735)
* fix(proxy): preserve DB settings during config reload

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

* test(proxy): mark reload settings variable as final

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-02 23:55:10 +00:00
paveln83
7fe6b1de34
fix(proxy)!: ignore stored general_settings keys no settings API manages (#48967)
Apply database general settings only for managed keys, warn about ignored copies, and allow admins to delete stale stored keys

Move intended database-only settings into config.yaml before upgrading, including authentication and access restrictions
2026-11-02 14:58:46 -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
simonp9
250af8f027
fix(spend)!: save streamed /v1/responses spend rows under the encrypted resp_ id the client received (#49493)
* fix(responses): log the encrypted response id on streamed /v1/responses spend rows

With response id security on, the stream hook encrypts the resp_ id on every
frame, but logging had already received the SDK's own copy of the terminal
response, so the spend row, the standard logging payload and callbacks got the
managed id (or the chatcmpl id on a bridged stream) the client never saw. The
hook now records the exact id it advertises on the request's logging object,
and the logging layer stamps that id on the logged streamed response

* fix(responses): return the id the client sent on GET and cancel, and keep read rows off the create row's id

Every GET /v1/responses/{id} re-encrypted the response id, so a client that continued a bridged conversation with the
id a GET returned referenced a ciphertext no spend row was filed under. The follow-up lost turn 1 and opened a new
session. GET and cancel now answer with the exact id the client sent when it is an id this proxy issued and it decodes
to the response the provider returned. A create with previous_response_id still gets a fresh id for its own response,
and a raw provider id still gets a managed id

Because a read now carries the create's id, a GET or cancel spend row is filed as <id>_retrieve_<call id> or
<id>_cancel_<call id>. That keeps one row per read, as before, and a read row can never take the create row's
request_id, so the create row survives whichever row is written first and a follow-up replays turn 1 once. The
background settlement row keeps <id>_background_cost, and the cost poll's own status checks keep the response id

* fix(responses): log a fresh id for each read and cancel, echo the held id only to its owner

A GET or cancel of a stored response now logs a new encrypted id of its own,
so callbacks and spend rows never reuse the create's id, while the owner of
the id still gets back the exact id they sent. An admin reading someone
else's response gets an id bound to the admin, as before. The spend row id
suffix for reads and cancels is gone, so with response id security off the
rows and session history match the default branch. A streamed GET follows
the same rules.

* fix(responses): echo the sent id to every reader that passes the response access check

A GET, streamed GET or cancel of a response now answers with the id the caller sent whenever the caller passed
the access check, not only when the caller is the user or team the id was issued to. An admin, the master key
or a teammate who read someone else's streamed response got a fresh id back, and the session lookup could not
reach the first turn from it, so a bridged follow-up through that id lost the conversation history

Every read and cancel still logs a fresh id bound to the reader, never the create's id

* fix(responses): keep a read or cancel row in session history only when it brings new output

A GET, streamed GET or cancel sent with a session header joins the
create's session, and its stored response repeats the create's output.
The replay then sent the same assistant turn once per read.

Session history now keeps an aget_responses or acancel_responses row
only when its stored Responses output has an item id that no earlier
kept row carried. A polled background response keeps the first poll
that returns the finished output, since its queued create row has none,
and later polls of the same output are dropped. Every other row keeps
today's rule

* fix(responses): replay each history item once, echo the sent id on delete, keep background cost files apart

A create row now owns its output items in session history, and a read or cancel row adds only items no
create and no earlier read carried, in one linear pass, so a background response polled while it grows,
or a read that ends before its streamed create, replays the answer once and after its prompt

DELETE /v1/responses/{id} answers with the id the caller sent, on a copy, and logs a fresh encrypted id
like a read does

Azure Data Lake Storage per-event files and unbatched GCS objects are named after the event's spend row
id, so a streamed background create and its cost event, which now share the client's id, keep separate
files

The integration tests that decoded the managed id now match the exact id the client received

* fix(responses): keep main's answer for a raw-id delete and main's storage names outside background cost events

A DELETE by a raw provider id returns the provider's id as main did, and logging still records a fresh
encrypted id. A streamed GET matches the held id by upstream response id, like a plain GET

Only a background response's cost event gets the _background_cost suffix on its Azure Data Lake Storage
file and unbatched GCS object, so batch retrieves and events with no id keep main's names

An item a session read replays takes the newest read's copy at the first read's place, so an item a
poll caught half written replays finished

Tests pin the client's id over the router's continued response id, and the storage test no longer reads
the clock after the upload

* fix(spend): give background cost events their own OpenMeter and DynamoDB ids, replay id-less read items

A streamed background create and its later cost event share the client's
id, so OpenMeter dropped the second event and DynamoDB overwrote the
create's item. Both now key the cost event as <id>_background_cost, like
its spend row and storage file.

Session history again replays a read's output items that carry no id,
as main did.

* fix(integrations): keep OpenMeter and DynamoDB logging working without the proxy extras

* fix(integrations): give background cost events their own ClickHouse request id and DeepEval span id

A streamed background create and its later cost event now share the id the
client received, so ClickHouse and DeepEval saw both events under one id.
Route both through logged_event_key, the way the Azure storage, GCS,
OpenMeter and DynamoDB loggers already do, so the cost event's ClickHouse
request_id matches its spend row's `<id>_background_cost` and its DeepEval
span gets its own uuid. ClickHouse response_id stays the raw provider id.

* test(deepeval): annotate the background cost span test's locals with Final

* test(responses): run the DELETE wire test that pins the echoed client id

DELETE /v1/responses/{id} now answers with the id the caller sent, so the
skip that marked it as a known bug no longer applies. The test passes on
the /v1, /openai/v1 and bare routes.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 14:13:17 -08:00
tessellark
c720b23238
feat(proxy)!: honor model_info.max_input_tokens in the Codex catalog for stock models (#49701)
* feat(proxy): honor model_info.max_input_tokens in the Codex catalog for stock models

A Codex model catalog entry copied from Codex's stock entry (a stock slug, or an alias whose upstream is one) now takes the max_input_tokens every routable deployment of the group sets as its context_window, capped at the stock entry's max_context_window like Codex's own model_context_window. With nothing set, the stock window stays.

* feat(proxy): honor the smallest max_input_tokens a group's deployments set in the Codex catalog

A deployment without a limit counts as Codex's own window, so a limit on only some deployments can lower the window but never raise it

* fix(proxy): skip a zero or negative max_input_tokens when sizing a Codex catalog window

* test(proxy): stub the max_input_tokens group field and type the Codex window tests

* fix(proxy): skip a max_input_tokens a saved /model/info response copied from the cost map

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 13:27:58 -08:00
Prabal Pratap Singh (ZeroX)
b3c397e964
feat(hosted_vllm)!: keep vLLM's stop_reason and token_ids on chat choices and stream chunks (#48393)
* feat(hosted_vllm): keep vLLM's stop_reason and token_ids on chat choices

Non-streamed hosted_vllm chat completions moved vLLM's choice-level
stop_reason and token_ids into provider_specific_fields, so clients
written against vLLM could not find them where vLLM puts them. They are
now also set on the choice itself, and the copy in
provider_specific_fields stays for existing readers.

Fixes #47945

* feat(hosted_vllm): keep stop_reason and token_ids on streamed chunks and proxy stream output

Streamed chat finish chunks and streamed /v1/completions finish chunks now carry vLLM's stop_reason. Each stream chunk keeps the token_ids vLLM sent with it, including held-back empty chunks, and the proxy no longer strips those chunks as empty usage chunks or drops the fields in its fast serializer.

* fix(hosted_vllm): keep the upstream choice index on token-id-only stream chunks

* fix(hosted_vllm): keep vLLM choice fields for null, cache hits, n>1, and streamed completions

Response conversion keeps stop_reason and token_ids on the choice for any OpenAI compatible response, cache replays carry them, finish fields are recorded per choice index, text completion streams keep token_ids, and the proxy passes only these two keys through, nulls included.

* fix(hosted_vllm): send empty text on token-id chunks, keep fields on their own choice, and keep null stop_reason on slow-path streams

Token-id-only chunks now carry an empty content or text string, a finish chunk only takes stop_reason and token_ids from its own upstream choice, content chunks no longer repeat stop_reason, the slow stream serializer keeps a null stop_reason or token_ids, and fields cached under provider_specific_fields come back on the choice

* fix(hosted_vllm): keep stop_reason on the last content chunk, keep both fields in built stream responses, and key the cache on return_token_ids

* fix(hosted_vllm): keep another choice's final token_ids and stop_reason on their own stream chunk

With n above 1, a finish-only vLLM chunk for a choice other than the one the single finish chunk
carries was dropped or taken as choice 0's finish, losing that choice's last token_ids. It now goes
out as an id-only chunk on its own choice index.

* fix(hosted_vllm): send the first choice's finish chunk before another choice's later token-id chunks

* fix(streaming): keep each streamed text completion choice on its own index

* fix(streaming): finish each streamed text completion choice on its own index and null token_ids in redacted logs

* fix(proxy): drop token_ids from guarded streams, redact choice-level provider fields, and keep one message_delta per Anthropic stream

* fix(proxy): skip token-id-only chunks on guarded streams and put stop_reason on the final text chunk

* fix(proxy): drop token_ids from the logged and cached response of a guarded stream

* fix(proxy): drop token_ids under per-chunk stream checks and redact text-completion stream choices

* fix(caching): key return_token_ids and continuation_token sent in extra_body

* fix(streaming): drop token ids from streamed chunks while post_call_rules run

vLLM holds text back behind a possible stop string but sends its token ids at once, so an SDK stream sent ids
before litellm.post_call_rules checked their text. While those rules are set, streamed choices drop token_ids
and chunks that carry only ids are skipped, as the proxy already does ahead of a per-chunk check

* fix(streaming): keep vLLM fields when a stream's only choice index is not 0

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-02 13:26:29 -08:00