Commit graph

45730 commits

Author SHA1 Message Date
devin-ai-integration[bot]
22e8b45c68
feat(proxy): add maximum_health_check_retention_period to bound the health-check table (#37681)
* feat(proxy): add health check retention cleanup

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

* test(proxy): drop redundant health-check assertion

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

* fix(proxy): share cleanup budget across retention groups

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

* refactor(proxy): clarify cleanup group deadlines

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 16:01:16 -07:00
devin-ai-integration[bot]
3ea1c16b0d
fix(auth): cache team member default budget as a typed model (#37695)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 16:00:57 -07:00
devin-ai-integration[bot]
a030b33188
fix(scim): fail group sync when a member add or user creation fails (LIT-5105) (#37688)
* fix(scim): fail group sync when a member add or user creation fails

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

* style(scim): apply ruff format

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 16:00:37 -07:00
devin-ai-integration[bot]
387a948263
fix(scim): keep the matched user_id on POST /Users email match (#37701)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 16:00:08 -07:00
mateo
df00c334d1 fix(proxy): reload the unpriced-model toggle regardless of supported_db_objects
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 22:53:34 +00:00
mateo-berri
307ca1bcc7 fix(redis): say when an async client drops an unusable connect func
A caller-supplied redis_connect_func has no way to run on an async
connection, so log it instead of dropping it in silence.
2026-08-20 15:53:00 -07:00
mateo-berri
3c44f8d926 fix(ui): surface toggle failures on the block-unpriced-models setting
The hook swallowed errors into the console, so an admin flipping the switch without
STORE_MODEL_IN_DB saw nothing happen and got no reason why. Adds the missing hook tests.
2026-08-20 15:47:13 -07:00
mateo-berri
bfe54eb013 docs(redis): say why async paths cannot reuse redis_connect_func
The AUTH exchange it runs is the blocking client API, so on an async
connection send_command and read_response hand back coroutines nobody
awaits and the connect fails outright.
2026-08-20 15:46:03 -07:00
mateo-berri
cba4fa403d fix(redis): keep Azure AD and GCP IAM auth on URL and pool clients
REDIS_URL-based async clients and every async connection pool dropped the
managed-identity credential the caller configured, so they connected
unauthenticated against an auth-enforcing Redis. The conversion from
redis_connect_func to a CredentialProvider now happens once, before any
branch, and covers the url, sentinel, cluster, and pool paths alike.

Also adds credential_provider to the cluster kwargs allowlist, which
silently filtered it out.
2026-08-20 15:42:48 -07:00
Bisma Nawaz
909ab23b89 test: annotate parametrized traffic-type test inputs 2026-08-21 03:42:34 +05:00
mateo-berri
a3b6762788 fix(streaming): price partial-stream spend rows at the real model and keep prompt and cache fields
A streaming chat completion that ends early (client disconnect, or the proxy
cutting the stream at LITELLM_MAX_STREAMING_DURATION_SECONDS) wrote a spend log
row with spend 0.0, prompt_tokens 0 on the proxy-cut path, and no cache fields
in usage_object. The proxy restamps chunk.model in place to the client-facing
alias, so the partial response rebuilt from those chunks priced the unmapped
alias and came out at 0. The failure path also rebuilt usage without the
request messages, so prompt tokens counted to 0, and a cut stream never sees
the final usage event that normally zero-fills the cache fields.

Restamp the rebuilt partial response with the wrapper's real model before cost
calculation on both the disconnect and the failure paths, pass the request
messages when rebuilding usage on the failure path, and zero-fill missing
cache usage fields the way completed streams already do.
2026-08-20 15:39:08 -07:00
mateo-berri
3672fa9fb5 fix(proxy): log block_requests_for_models_without_pricing updates lazily
The eager f-strings tripped tests/test_litellm/test_logging.py::test_logging_calls_do_not_build_their_message_eagerly.
2026-08-20 15:38:27 -07:00
mateo-berri
2b2d6d7aad fix(proxy): apply DB-persisted safe litellm settings on every worker's config reload
Peer workers previously kept their startup value for block_requests_for_models_without_pricing
until a restart, so a toggle from the UI only took effect on the worker that served the request.
2026-08-20 15:35:20 -07:00
Mateo Wang
1d7f675e52
Merge pull request #37663 from BerriAI/litellm_azure_postgres_entra_auth
feat(proxy): authenticate to Azure Postgres with Microsoft Entra ID tokens
2026-08-20 15:35:05 -07:00
Devin AI
f3896c0527 test(fal_ai): use monkeypatch for the gpt-image-2 cost map fixture
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 22:27:01 +00:00
Devin AI
60706d5f89 feat(fal_ai): add gpt-image-2 image generation support
Route fal.ai's openai/gpt-image-2 endpoints through a dedicated transformation that maps OpenAI image params (n, size, quality, output_format) into fal's schema, and register the model in the cost map.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 22:18:34 +00:00
mateo-berri
5c89490124 Merge litellm_internal_staging into litellm_fix_responses_bridge_incomplete_500 2026-08-20 15:16:14 -07:00
tin-berri
cb4eb82249
feat(ui): per-model reasoning effort in the complexity tier editor (#37673)
* feat(ui): per-model reasoning effort in the complexity tier editor

* feat(ui): gate the effort control on model group reasoning support
2026-08-20 15:10:35 -07:00
Bisma Nawaz
d317c5621f fix: map Gemini ON_DEMAND_FLEX traffic type to flex service tier 2026-08-21 02:56:23 +05:00
mateo
eb8d402187 test(proxy): cover a registry model priced only via tiered_pricing
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 21:55:43 +00:00
tin-berri
2dcd453860
feat(shadow_eval)!: gate the per-key budget on dollar spend instead of turns (#37555) 2026-08-20 14:55:21 -07:00
tin-berri
60e03bedcf
fix(ui): surface the paginated fallback on Cost Optimization (#37659)
* fix(ui): surface the paginated fallback on Cost Optimization

The page streamed its fallback silently: useDailyActivityRange dropped
the hook's progress and cancel fields and CacheLeakageCard only showed
a loading state while empty. Extract the Usage page's fetch banner into
a shared PaginationStatusAlerts component, render it above the tabs,
and note on the cache leakage tables when pages are still arriving.

* fix(ui): gate the cache leakage streaming note on isFetchingMore only

loading also covers a fresh aggregated request over the previous
range's rows, where pagination copy mislabels stale data. Drop the
redundant component comment flagged against the repo comment policy.
2026-08-20 14:55:00 -07:00
mateo-berri
ab79b8dcb6 fix: count tiered_pricing as a cost mapping when blocking unpriced models 2026-08-20 14:52:37 -07:00
Mateo Wang
d556fac56b
Merge pull request #37112 from mubashir1osmani/litellm_add_perplexity_agent_api_models
feat(perplexity): add Agent API third-party models
2026-08-20 14:49:12 -07:00
yuneng-jiang
9432f40145
bump: litellm-enterprise 0.1.57 -> 0.1.58, litellm-proxy-extras 0.4.87 -> 0.4.88 (#37717) 2026-08-20 14:45:40 -07:00
mateo
6bb677d30f fix(model-costs): correct gpt-5.6 input token cap
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 21:44:32 +00:00
yucheng-berri
abdde94ad5
fix(ptu): refuse an incomplete config.yaml reservation the way the endpoints do (#37703)
* fix(ptu): refuse an incomplete config.yaml reservation the way the endpoints do

POST /model/new answers 400 when PTU fields are set without a team_id, a
ptu_effective_from, or the count and rate together. config.yaml ran none of
those checks, so the same deployment loaded and served, billing per token
while accruing no flat cost, with nothing logged.

The rule moves into litellm_core_utils.ptu_pricing so both paths state it
once. Registration refuses such a deployment and names it, and the proxy's
ignore_invalid_deployments keeps that to the one entry. Only enforced while
PTU cost attribution is enabled, so a proxy that never opted in is unchanged.

* refactor(ptu): build the refusal message in the module that owns the rule

router.py raised a message it composed itself, which put proxy-facing
wording on the shared SDK surface. ptu_config_error now takes the
deployment name and returns the whole sentence; the endpoints still ask
without a name and their 400 bodies are unchanged.
2026-08-20 14:43:06 -07:00
mateo-berri
5301872093 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_block_unpriced_models 2026-08-20 14:42:03 -07:00
Yassin Kortam
996693f1eb
fix(a2a): accept the whole JSON-RPC id union the spec defines (#37704)
JSON-RPC 2.0 types `id` as string, integer or null, but
LiteLLMSendMessageResponse annotated it as a bare required `str`. Pydantic v2
dropped v1's int-to-str coercion, so an upstream agent echoing an integer id was
rejected outright, and a null id, which section 5 requires for an error that
cannot be correlated to a request, was rejected too. Both surfaced as -32603 with
a pydantic ValidationError in the message: five distinct 500s on
/a2a/{agent_id}, across message/send and tasks/get.

Everything around the model already handled the full union: the endpoint reads
the id off the body as Any, its helpers are typed `str | int | None`, the error
builder takes `object`, and the streaming path passes the id through untouched.
The response model was the only narrowing left.

Backfilling an id the agent omitted keeps the caller's type too, since JSON-RPC
requires the response id to equal the request id and a caller that sent 7 cannot
correlate a response carrying "7".

`bool` is excluded from the integer half even though it subclasses `int`, so a
boolean id is stringified rather than relayed as 1 or 0, where it would collide
with a real integer id another in-flight request may be using.
2026-08-20 14:41:22 -07:00
yuneng-jiang
4af66657f9
feat(ci): freeze the conftest save/restore inventory so it can only shrink (#37621)
* feat(ci): freeze the conftest save/restore inventory so it can only shrink

* fix(ci): resolve the named constant a conftest save loop iterates

* fix(ci): match the snapshot shape instead of a list of blessed dict names

* feat(ci): fail a branch that clears TQ violations without lowering the ceiling

A limit that only ever falls is not the same as one that falls when it can.
Clearing violations and leaving the ceiling above the new count let the same
violations return later under a limit nobody moved, so the gate now fails on
that and names `make lint-budget-update` as the fix. It needs both head below
base and head below limit, so headroom already in the base is never blamed on
the branch that happens to run next.

Drops the seeded-rule exemption from the ratchet along with it. Its stated
reason was that the base tree predates a rule introduced on this branch, but
base counts are measured with the current checker, so such a rule is counted at
the base too and its grandfathered total was never at risk of reading as fixed.
Removing the exemption is what lets a newly seeded rule ratchet like the six
that came before it.

The base scan is skipped when the branch touches neither the test tree nor the
checker, since neither count can have moved.
2026-08-20 21:39:59 +00:00
mateo-berri
7f539e388c Merge branch 'litellm_block_unpriced_models' of https://github.com/BerriAI/litellm into litellm_block_unpriced_models
# Conflicts:
#	litellm/proxy/auth/auth_checks.py
#	tests/test_litellm/proxy/auth/test_auth_checks.py
2026-08-20 14:36:58 -07:00
yuneng-jiang
648c6e7dc5
feat(ci): assert .github/workflows holds only workflows, correctly named (#37616)
* feat(ci): assert .github/workflows holds only workflows, correctly named

* style(tests): annotate the hygiene test module's names with Final

* fix(ci): report a .yaml workflow as a naming finding, not a stray

GitHub reads .yml and .yaml alike, so WF001 telling you to move a valid
.yaml workflow to .github/scripts/ was wrong advice. WF001 now covers only
files that are not workflows at all, and the .yml spelling this directory
keeps moves to WF004, which says to rename rather than relocate.

WF001 also never looked into subdirectories, since GitHub does not read
them either; the message now says so. The directory is injected rather
than read off a module constant, so the cases are testable without
monkeypatching.
2026-08-20 21:36:26 +00:00
mateo-berri
2996a18fa9 fix(model_prices): document 262k input limit on fireworks qwen3p8-max 2026-08-20 14:33:04 -07:00
mateo-berri
21891b4483 fix(proxy): count explicit zero prices on any billed metric as configured pricing 2026-08-20 14:32:54 -07:00
yuneng-jiang
cde134488c
test(ci): reject coverage-allowlist entries that no longer match a file (#37608)
* test(ci): reject coverage-allowlist entries that no longer match a file

* fix(ci): match a dockerfile allowlist entry the way the census exempts one
2026-08-20 14:25:28 -07:00
yuneng-jiang
8a18e24faa
test: merge three stranded twins into the files that shadow them (#37600)
* test: merge three stranded twins into the files that shadow them

The second mirror's last four files each share a filename with a live test, so
the previous commit could not move them. Three of the four turn out to be plain
additions: their classes collide with nothing in the live file, so the tests are
extra coverage that has sat unrun rather than a competing version of anything.

Appending them takes the three files from 156 collected tests to 196, and all
196 pass. The 40 recovered are 13 OCI cases covering key normalization,
credential validation, complete-URL building and image-url transformation, 15
management-endpoint cases covering empty-value handling and the premium check,
and 12 DeepSeek thinking-parameter cases.

One assertion had to change. test_map_reasoning_effort_none_does_not_enable_thinking
asserted that reasoning_effort='none' leaves no thinking key, while the handler
maps it to {'type': 'disabled'} on purpose, documented in map_openai_params as
the OpenAI-style way to ask for thinking off. The test's stated intent holds,
since disabled does not enable anything, so it now asserts the disabled mapping
instead of the key's absence. Two imports moved to module scope for the
appended code, and no live test was touched.

test_discoverable_endpoints.py is the one left. Its twin grew from 1268 lines
to 9434, 25 of its assertions fail against today's code, and only 5 of its 19
tests have no counterpart, so deciding what survives that rewrite is a
judgement about the endpoints rather than a merge. The allowlist now holds
exactly that file and that reasoning.

* test(oci): stop the OCI suite reading credentials from the environment

validate_environment falls back to os.environ for every OCI credential and only
defaults the region when OCI_REGION is unset, so on a machine with OCI
configured the missing-credential test finds credentials it never passed and the
default-region test builds a URL for the ambient region. The suite then passes
or fails depending on who runs it.

A fixture drops the seven OCI variables for the four classes this branch added
and for TestOCIChatConfig, which had the same dependency before any of this and
fails the same way: with OCI_USER and friends exported, two of its cases fail on
origin/litellm_internal_staging today.

  clean env:        83 passed
  ambient OCI env:  83 passed

Same numbers either way, where the pre-existing file gave 68 passed / 2 failed
under the second.
2026-08-20 14:25:23 -07:00
yuneng-jiang
861140b755
perf(ci): measure unit-shard coverage with the sys.monitoring core (#37589)
Coverage is the single biggest time lever on the unit shards: the legacy
no-coverage workflow ran the same directory in about 5 minutes against 11 to 13
with coverage on. coverage.py's sys.monitoring backend (PEP 669) is the cheapest
core it ships, and it is not in use here today.

It has to be asked for explicitly. coverage 7.14 only defaults to sysmon from
Python 3.14 (`SYSMON_DEFAULT = CPYTHON and PYVERSION >= (3, 14)`) and these
shards pin 3.12, so without `COVERAGE_CORE` they get the slow tracer.

The audit left open whether sysmon survives turning on branch coverage. It does
not, at this Python. coverage gates branch measurement under sysmon on
`branch_right_left`, which needs newer than 3.14.0a5; on 3.12 it refuses and
falls back to the default core with a `no-sysmon` warning. Verified directly
against Python 3.12.13 with coverage 7.14.0:

    $ COVERAGE_CORE=sysmon python -m coverage run --branch --source=. run.py
    CoverageWarning: Can't use core=sysmon: sys.monitoring can't measure
    branches in this version, using default core (no-sysmon)

So this speedup and `branch = true` are mutually exclusive until the runners
move to 3.14. Nothing here turns branch coverage on, so the two never collide
in this change, but whoever does turn it on is choosing to give this back.
2026-08-20 14:25:19 -07:00
mateo-berri
1c1b3c5f17 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_model_registry_consolidated_20260820_wt 2026-08-20 14:21:31 -07:00
ryan-crabbe-berri
21e9632713
test: add six ruff rules that catch tests which cannot fail (#37709)
`assert False` inside a `try:` raises AssertionError, which the `except
Exception` right below it catches, so several tests reported green no matter
what the code did. `pytest.fail` raises Failed, a BaseException, and escapes.

A bare `a == b` statement is evaluated and discarded. Nine of those sat in
tests, and one was comparing against a model name the router never produces.

Selects B011, B015, B018, PT015, PLR0133 and PLW0127 in ruff-tests.toml
alongside F821, with all 50 existing violations fixed, so no budget file or
ratchet is needed. CI already runs this config over tests/.
2026-08-20 14:21:26 -07:00
72004
67d16a499d Type the batch-retire helpers and rename test helper to avoid shadowing existing _completed_batch 2026-08-21 02:13:11 +05:00
Yassin Kortam
2f23cf5701
fix(mcp): normalize auth schemes so MCP egress emits exactly one prefix (#37668)
MCP egress prefixed the configured scheme unconditionally, but callers legitimately supply
both a bare token (from a stored credential) and an already-schemed value (passed through
from the caller's x-mcp-auth or Authorization header). The second shape produced
Authorization: Bearer Bearer <jwt>, which upstream servers reject as a malformed token. It
presented intermittently because a resolved stored credential arrives via extra_headers and
overwrites the doubled header, so only users without one always failed.

strip_auth_scheme drops one leading scheme before the header is rebuilt. It matches the
scheme case-insensitively per RFC 7235 and requires a credential behind it, so both a token
that merely begins with the scheme text and a scheme with nothing behind it are left intact.
MCPAuth.authorization stays verbatim because that auth type means the caller owns the whole
header value.

For MCPAuth.basic the normalization has to happen in update_auth_value rather than at
header-build time: to_basic_auth has already encoded the whole "Basic <credentials>" string
by then, so no prefix is left to find. A schemed value whose remainder decodes is already
encoded and is reused; one that does not decode is the bare pair with the scheme written in
front of it, and is encoded rather than forwarded as an invalid header.

The same doubling reached OpenAPI-backed servers through _format_byok_openapi_auth_header. A
non-BYOK server short-circuits _resolve_byok_mcp_auth_header, so that formatter also receives
the deprecated global x-mcp-auth, which is already a complete header value.
2026-08-20 14:11:38 -07:00
Yassin Kortam
f3639a6fb3
fix(mcp): let a salt-key-orphaned OAuth credential be replaced by re-authorization (#37672)
store_user_oauth_credential refused to overwrite any existing row that did not
decode as an OAuth2 payload, which conflated two states: a live BYOK secret that
reads back as plaintext, and ciphertext written under a LITELLM_SALT_KEY the proxy
no longer holds. The second is unrecoverable by any caller, so refusing preserved
nothing and instead wedged the user out of the OAuth flow permanently, since
re-authorizing is their only recovery.

The guard now raises only when the existing value is genuinely readable. An
undecryptable row is logged and replaced by the newly authorized token.

Both read paths were equally silent: get_user_oauth_credential and
list_user_oauth_credentials (which backs the bulk prefetch) each dropped an
undecryptable row indistinguishably from "user never authorized", so an operator
saw an upstream 401 and no hint that a credential had failed to decrypt. Both now
warn with the user and server ids, never the stored value.
2026-08-20 14:11:17 -07:00
72004
f8b31f493a fix: don't retire a completed batch from cost recovery while output_file_id is still lagging 2026-08-21 02:05:05 +05:00
mateo
9c29e11ad4 Merge branch 'litellm_block_unpriced_models' of https://git-manager.devin.ai/proxy/github.com/BerriAI/litellm into litellm_block_unpriced_models 2026-08-20 20:59:46 +00:00
mateo
c551a5c44a fix(proxy): treat explicit zero non-token prices as priced
A deployment that overrides any cost_per field, including at zero, now counts as priced so it is not blocked as unpriced

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 20:59:38 +00:00
mateo-berri
50a346da1c fix(model_prices): restore supports_vision on Mistral Small 4.0 entries 2026-08-20 13:54:03 -07:00
mateo-berri
abc6ebfb33 fix(responses_bridge): map incomplete responses to finish_reason length instead of 500 2026-08-20 13:50:40 -07:00
mateo-berri
affe2b4529 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_azure_postgres_entra_auth 2026-08-20 13:50:19 -07:00
Mateo Wang
fc3b160fb5
Merge pull request #37565 from BerriAI/litellm_lit_5745_provider_edge_replay
feat(e2e): move record/replay to the provider edge (LIT-5745)
2026-08-20 13:50:04 -07:00
github-actions[bot]
fc042a299a fix(azure): prefer workload identity over managed identity
AKS workload identity injects AZURE_CLIENT_ID, AZURE_TENANT_ID, and
AZURE_FEDERATED_TOKEN_FILE into the pod, and never a client secret.
Reading that bare client id as a managed identity sent the pod to IMDS,
which has no identity attached to it, so the token request failed and the
federated token was never exchanged.

AZURE_FEDERATED_TOKEN_FILE now wins over the bare client id and infers
DefaultAzureCredential, whose chain reaches WorkloadIdentityCredential
before ManagedIdentityCredential. DefaultAzureCredential passes
AZURE_CLIENT_ID to both legs, so a plain user-assigned managed identity
still reaches the same identity it does today.

This is the credential path Azure recommends for passwordless Postgres on
AKS, and it also fixes the Azure OpenAI token provider, which infers its
credential the same way.
2026-08-20 13:45:56 -07:00