Commit graph

43876 commits

Author SHA1 Message Date
devin-ai-integration[bot]
f54cd287a2
fix(bedrock): grant bedrock:CountTokens in OIDC session policy (#33145)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-05 14:55:46 -07:00
Yassin Kortam
9d58923065
fix(langfuse): stop a collected httpx handler from closing a shared client (#35981)
A cached HTTPHandler hands its raw httpx.Client out to consumers that keep it
for the process lifetime. When the shared client cache expires the entry on its
TTL or evicts it under the 200-entry cap, nothing references the handler, so it
is collected and its finalizer closed the client those consumers still hold.
Langfuse ingestion then failed silently on the SDK's background flush thread
until the process restarted.

A finalizer running proves only that nothing references the handler; it proves
nothing about the client. Both handlers now close the client during finalization
only when they built it and are still its sole referrer, so an unshared client is
still released promptly and a handed-out one is left alone. That keeps the
pooled-socket reclamation the finalizer was providing, which measures identical
to base over 2000 handler create-and-drop cycles.

Explicit close() stays, now gated on _owns_client so the wrapper never closes a
caller-injected client, and __aexit__ routes through it.

LangFuseLogger also keeps a reference to the handler whose client it hands the
SDK. Previously that handler was a local that went out of scope immediately,
leaving the client reachable only from the SDK. It still shares the cached
client, so no extra clients are created per logger.
2026-08-05 14:54:31 -07:00
Yassin Kortam
7a5d6a0548
ci: fail the build when a test file or Dockerfile is invoked by no job (#35991)
The absence of a CI signal is indistinguishable from a passing one, and
that shape has now produced several independent holes: whole test
directories no job runs, and shipped images no job builds. Nothing was
watching for either, so each was found by accident.

assert_ci_coverage.py enumerates every test_*.py under tests/ and every
Dockerfile in the repo, then credits only what the workflows and the
CircleCI config actually invoke. Paths filters and lint steps that merely
name a directory do not count as coverage, because crediting a mention is
the same mistake one level up. Anything neither invoked nor listed in
.github/ci-coverage-allowlist.yml with a written reason fails the job.

The guard found 275 uncovered test files and 6 unbuilt Dockerfiles. Fixed
here: the root Dockerfile, the primary published image, now gets an
image-scan leg that builds it and runs the offline migration check against
it, and 8 tests/test_litellm subdirectories join the shards that already
enumerate their siblings. Everything else is allowlisted per file, so a
new file cannot inherit an exemption, and the remaining decisions are
tracked rather than invisible.

The job reports but is not in branch protection, so it does not block
merges; promoting it is a separate change once the allowlist has survived
contact with a few pull requests.
2026-08-05 14:52:48 -07:00
Yassin Kortam
50c6a1f344
fix(ci): run every helm test suite, not just the first one per file (#35993)
* fix(ci): run every helm test suite, not just the first one per file

helm-unittest gained support for multiple suites in one test file in
v0.5.0; CI and the Makefile both pinned v0.4.4, the last release that
decodes a single YAML document per file. Any suite after a `---`
separator was parsed away and its assertions never ran, while the
summary still reported a clean pass.

Upgrading the pin to v0.8.2, the newest release that installs under the
pinned helm 3.11.1, brings the litellm-helm chart from 11 suites / 90
tests to 14 suites / 93 tests with no change to any test file. All the
recovered tests pass.

The run step now compares the number of declared `suite:` documents
against the number of suites the runner reports, so the same class of
silent skip fails the job loudly instead of passing quietly. The
Makefile target upgrades a stale local plugin instead of swallowing the
"already installed" error and leaving the developer on an old version.

* ci: install helm-unittest from a pinned, checksum-verified artifact

`helm plugin install <git url>` clones the plugin repo and executes its
install hook, which downloads the release tarball itself. The old
integrity step then checked the cloned repo's HEAD, which happens after
the hook has already run and never covers the binary that was actually
downloaded.

The plugin now comes from a full pinned release URL, verified against
the SHA-256 the project publishes in its helm-unittest-checksum.sha
sidecar, before anything is unpacked or run. Nothing remote executes
ahead of the check, and a re-published release asset fails the job
instead of installing silently.
2026-08-05 14:52:18 -07:00
Yassin Kortam
87dbb632b2
test(utils): pin the register_model replay test to the recorded half (#35994)
test_reapply_runtime_registrations_replays_register_model_overrides asserts that
a fetched catalog value survives the replay for a key an operator override does
not mention. Any Router still alive in the process re-asserts its own deployments
first, so a router serving openai/gpt-4o writes its model_info over that catalog
value and the assertion reads the router's number instead. Routers built by
earlier tests stay in the weak set until they are collected, which made the test
depend on collection timing and fail intermittently in shards that run the router
tests alongside it.

The live-router rebuild is covered in test_router_model_cost_isolation.py, so
this test now runs with the replay callback unset and exercises the recorded
registrations it is about.
2026-08-05 14:50:53 -07:00
Yassin Kortam
7984f4fa64
fix(logging): extend secret redaction to records litellm does not emit directly (#35977)
The redaction filter was attached to the handler shared by litellm's own
loggers, so it only covered records litellm emits. A litellm value can also
reach a log record through a dependency logging on its own logger, and those
records never pass through a litellm handler.

Attach the filter to each dependency logger that can carry one. The filter goes
on the emitting logger rather than on the root logger or a root handler, since
Logger.handle applies the emitting logger's filters before any handler runs, so
every downstream handler is covered regardless of who owns it.
2026-08-05 14:50:42 -07:00
Yassin Kortam
b8ef8508b5
fix(ci): make the env-key doc gate see bare get_secret and get_secret_str reads (#35996)
The gate required a litellm. prefix on get_secret and get_secret_str, so any
module importing either function directly bypassed it: 335 environment variables
read under litellm/ were invisible to it. The three patterns collapse into one
with the prefix optional, a negative lookbehind so attribute calls on unrelated
objects cannot match, and litellm.utils. accepted since four call sites reach
get_secret that way.

Widening the patterns alone would demand about 320 new rows in the central
reference table, most of them provider credentials that are already documented
on their own provider pages. So the gate now looks across every page of the docs
site rather than only that one table, which leaves 143 keys genuinely
undocumented instead of 322.
2026-08-05 14:49:55 -07:00
Abhimanyu Kapur
c76882b51b
fix(auto-router): stop the embedding model's context window from failing long requests (#35956)
* fix(auto-router): stop the embedding model's context window from failing long requests

The auto-router embeds the last user message to pick a model and sent it to the
embedding model unbounded. Embedding models carry 512 to 8k token windows while the
chat models they route to carry 200k+, so any prompt over the encoder's window failed
at the routing step with a 400 the destination model would never have raised.

Cut every doc to a character cap inside LiteLLMRouterEncoder, which is the one choke
point the auto-router, complexity-router, semantic guard and MCP tool filter all share.
Default 2000 chars, roughly 500 tokens, which fits even a 512-token self-hosted encoder,
overridable per deployment with auto_router_max_input_chars and globally with
DEFAULT_MAX_EMBEDDING_INPUT_CHARS.

Truncation alone cannot cover provider-side batch and byte limits, so any failure of
the route call now falls back to the auto-router's default model instead of propagating.
That path also fixes two latent bugs: a no-match left the auto-router alias in place as
the model name, which fails downstream with "Unmapped LLM provider" rather than reaching
default_model, and an empty route list raised IndexError.

Fixes #17869
Fixes #20277

* fix(auto-router): make the embedding input cap opt-in so guards still see whole prompts

Defaulting the cap inside the shared encoder truncated every consumer, not just the
auto-router. The semantic guard builds the same encoder, so its pre-call check would
have classified only the first 2000 characters while the full message still reached the
model, which a benign opener in front of an injection payload walks straight past. The
MCP tool filter and complexity router were silently narrowed the same way.

The encoder now defaults to sending docs whole and cuts only when a caller passes
max_input_chars. The auto-router is the only caller that does, so guard, MCP filter and
complexity-router behaviour is unchanged from before this branch.

DEFAULT_MAX_EMBEDDING_INPUT_CHARS becomes DEFAULT_AUTO_ROUTER_MAX_INPUT_CHARS, since it
is now specific to the auto-router, and drops its env override: the per-deployment
auto_router_max_input_chars already covers it, and every env var in constants.py has to
be documented, which is what broke the documentation and code-quality checks.

Also drops the added comments and the redundant type: ignore that review flagged.

* test(auto-router): cover the max_input_chars wiring from litellm_params

Nothing asserted that auto_router_max_input_chars on the deployment reaches the
AutoRouter that embeds prompts. Dropping the wiring left every test green while the cap
silently reverted to the default, so an operator with a 512-token embedding model could
not lower it and every long prompt would fall back to the default model instead of
being routed.

* test(auto-router): cover the populated route-choice list branch

The route layer can hand back a list, and picking its first element is where the
IndexError lived: the empty case was covered but the populated one was not, so the
branch that reads route_choice[0].name could be deleted with every test still green.
2026-08-05 14:47:40 -07:00
Devin AI
53ee9c8293 fix(anthropic): fall back when only some compaction iterations report thinking tokens
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-05 21:34:36 +00:00
Mateo Wang
6d604d27a6
fix(ci): make every remaining CI checkout shallow (#35997)
* fix(ci): make every remaining CI checkout shallow

PR #35982 only covered the lint and budget-ratchet jobs, so secret-scan
kept spending minutes fetching every branch inside its 5 minute timeout
and PRs kept getting cancelled. The UI lint and UI unit jobs carried the
same fetch-depth 0 checkout

secret-scan now checks out at depth 1, runs the hardcoded-secret pytest
without building the project environment, and lets the ggshield step
deepen history itself when a key is configured. UI lint resolves the
merge base through the API instead of local history. UI unit tests
compute the changed files the same way and feed them to vitest related,
because vitest --changed does a three-dot diff that silently selects
zero tests on a shallow clone

The daily branch creation workflows also did full checkouts, then
failed every run since persist-credentials: false left git push with no
credentials. They now create the ref through the GitHub API without a
checkout at all

* fix(ci): feed deleted UI files into vitest related selection

vitest --changed fed git's full change list to the related filter,
deletions included, so a deletion-only dashboard PR still selected the
tests importing the removed files. Keep that behavior by dropping the
diff filter and existence guard; vitest resolves nonexistent paths fine
and --passWithNoTests covers the nothing-related case
2026-08-05 14:32:17 -07:00
Yassin Kortam
c3a8962c00
fix(proxy): only treat a recoverable database outage as grounds to serve without one (#35864)
`is_database_connection_error` answered True for any `PrismaError` it did not
recognize, on the reasoning that an unclassified failure might be an outage and
the safer default was to keep serving. That default is inverted for faults that
never resolve. A query engine that is missing or version-skewed, a malformed
generated query, or a misused transaction all satisfied the predicate, so with
`allow_requests_on_db_unavailable` enabled the proxy would absorb one, boot
clean, and keep issuing fallback identities for as long as the process ran.

The predicate is now an allowlist: the httpx transport errors, prisma's
`EngineConnectionError`, and a `no_db_connection` ProxyException. That is what a
real outage produces, since the query engine is a local HTTP server and an
unreachable database surfaces as a transport failure against it, so the
high-availability path is unchanged. Anything unrecognized is now treated as
permanent and surfaces instead of being absorbed.

Deciding whether to serve without a database and deciding what to tell the
caller are different questions, so they no longer share a predicate.
`is_database_infrastructure_error` keeps the previous broad behavior and now
backs the reporting and recovery paths: service-unavailable classification, the
access-group endpoint's status mapping, and the health watchdog's reconnect
trigger. Their behavior is unchanged. Without that split, a permanently faulted
engine would have started reporting as an authentication failure, sending an
operator after a credential problem that does not exist.
2026-08-05 14:15:13 -07:00
Yassin Kortam
309e96c27b
fix(jina_ai): resolve the documented JINA_API_KEY as a fallback (#35992)
The Jina key fallback chain read JINA_AI_API_KEY three times in a row
before falling through to JINA_AI_TOKEN, so two of the four slots were
dead. Jina's own documentation publishes JINA_API_KEY, and litellm's
rerank validate_environment already tells users to set that name, but
nothing ever read it: a user who set only JINA_API_KEY got no key
resolved and Jina answered AUTH_MISSING_API_KEY.

Replace one of the repeats with JINA_API_KEY and drop the other.
JINA_AI_API_KEY stays first so no install that resolves a key today
changes which key it picks.
2026-08-05 14:13:42 -07:00
Devin AI
aadfa89ff6 Merge branch 'litellm_internal_staging' into litellm_fix_bedrock_adaptive_thinking_token_accounting
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-05 21:13:36 +00:00
Yassin Kortam
4562b539d8
fix(docker): fail the image build when the generated prisma engine paths drift off /opt/prisma (#35979)
The generated client bakes absolute query engine paths at build time, and
prisma-python scans them before it reads PRISMA_QUERY_ENGINE_BINARY. That
scan propagates EACCES rather than skipping a candidate, so a path baked
under the build user's HOME crashes client startup with a bare
PermissionError for every other uid, and the documented override cannot
recover from it.

Assert in the runtime stage that every baked query engine path sits under
the fixed, world-readable /opt/prisma bake, so a regression in the
generate step breaks the build instead of shipping an image that only
starts under the uid that built it. Adds an image-level test that runs
the same resolution as an arbitrary non-root uid.
2026-08-05 14:12:28 -07:00
mateo-berri
bee787b4b5 fix(guardrails): scan /v1/messages tool traffic
Guardrails silently skipped three surfaces on the Anthropic Messages
path, so an agent loop driven by /v1/messages ran unguarded:

- The Anthropic input translation never walked tool_result blocks, so
  content returned by a local tool (a curl, a file read, an MCP call)
  reached the model unscanned in both the string and list content
  shapes, images inside a tool_result included.
- tool_permission only understood ModelResponse, so an Anthropic
  non-streaming response or a raw SSE stream carrying tool_use blocks
  passed through with no rule ever evaluated.
- ContentFilterGuardrail scanned inputs["texts"] but never
  inputs["tool_calls"], so the arguments a model proposes for a tool
  call went unchecked.

Tool call arguments are parsed as JSON before filtering so a MASK
action rewrites the value and leaves the payload valid JSON; non-JSON
arguments fall back to scanning the raw string. Denied tool_use blocks
are dropped from the Anthropic content array and replaced with a text
block, and stop_reason resets to end_turn when nothing tool-shaped
survives.
2026-08-05 14:11:27 -07:00
Miles Adkins
431f61b4f7 fix(fireworks_ai): prefer native values silently on extras conflicts
Align with the API gateway translation: instead of raising BadRequestError
on alias or competing-constraint conflicts, the explicit Fireworks-native
param wins and the NIM/vLLM extra is dropped with a debug log. Covers
truncate_prompt_tokens vs prompt_truncate_len, chat_template_kwargs
enable_thinking vs reasoning_effort/thinking, guided_* vs response_format
(including response_format nested in an explicit extra_body, which the
previous conflict check missed), and multiple guided_* params (priority
order json, grammar, choice). Malformed non-object chat_template_kwargs
is also dropped with a log instead of raising.
2026-08-05 16:02:47 -05:00
Miles Adkins
6d80d05099 fix(fireworks_ai): align extras translation with the API gateway matrix
min_tokens is accepted natively by the Fireworks API (verified live), so
stop stripping it and let it pass through extra_body. Add the NIM-specific
include_reasoning and nvext keys to the strip set. enable_thinking=true
now omits reasoning_effort (model default) instead of forcing medium,
matching the gateway translation and preserving default-off models'
behavior; enable_thinking=false still maps to none.
2026-08-05 15:56:33 -05:00
Yassin Kortam
8ec562f279
fix(ai21): resolve the documented AI21_API_KEY instead of a misspelled name (#35985)
get_api_key resolved the ai21 key from AI211_API_KEY, with a doubled 1. Every other
ai21 code path reads AI21_API_KEY, including the validate_environment branches that
report it as the missing one, so the name a user is told to set was ignored here.

No user path reaches this branch today, since every provider-resolution site rewrites
custom_llm_provider to ai21_chat and sets the key from a correctly spelled read first,
so this is a correctness fix rather than a bug fix. It is worth making because the
env-var documentation gate reads this call site: leaving the misspelling in place would
require a row for AI211_API_KEY in the environment variables reference table, which
would turn a typo into public API
2026-08-05 13:53:13 -07:00
Devin AI
af2246c5b8 fix(anthropic,bedrock): report provider thinking tokens instead of classifying them as text
Resolves LIT-5244

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-05 20:47:05 +00:00
Mateo Wang
df831b807f
chore(typing): replace Any seams with real types across responses, proxy, and provider adapters (#35809)
* chore(typing): replace Any seams with real types across responses, proxy, and provider adapters

Replace Any-typed payload dicts, record shapes, and provider request/response
seams with TypedDicts, Protocols, and precise annotations in the ten litellm/
files carrying the highest combined basedpyright reportAny + reportExplicitAny
counts. No behavior changes.

Adds a regression test covering the managed-id list path so a prisma client
missing the managed tables keeps returning a fail-closed empty page.

* fix(passthrough): walk scalar request bodies through managed-id rewrite again

The top-level dispatch in rewrite_body_ids only handled dict and list
bodies, so a truthy scalar JSON body (bare string, number, bool) hit
dict.items() and raised AttributeError where the merge base passed it
through, and a bare managed-ID string body lost resolution. Restore the
base behavior by dispatching through _walk, widen the implementation to
object with a catch-all overload, and pin both paths with regression
tests
2026-08-05 13:45:21 -07:00
Yassin Kortam
267adf709a
fix(bedrock): sign Bedrock managed-file S3 requests with S3SigV4Auth (#35983)
S3 rebuilds the canonical request from the wire path with single
percent-encoding, which botocore models as S3SigV4Auth. Generic SigV4Auth
quotes the already encoded path a second time, so an object key holding any
character that percent-encodes was signed over %2520 while the request
carried %20, and S3 answered 403 SignatureDoesNotMatch.

The S3 logger was corrected in #35726; the Bedrock managed-files upload and
retrieval paths copied that same pre-fix pattern and were left behind, so a
configured bucket prefix with a space 403s every file upload and every file
content read.
2026-08-05 13:43:30 -07:00
Yassin Kortam
de00655363
fix(migrations): keep the toolchain heal from raising on an unreadable nodeenv cache (#35986)
heal_incomplete_nodeenv_cache() stats a $HOME-derived path with a bare
Path.is_dir(). pathlib only swallows ENOENT-shaped errnos, so a cache
directory the process cannot search raises PermissionError instead of
answering False. Images bake that cache under the build user's home, whose
mode is 0700, so a container started under any other uid dies there before
the Prisma CLI is ever invoked, and the migration never runs.

Tolerate OSError while inspecting the cache, matching the guard
nodeenv_cache_dir() already carries, so an unreachable cache means there is
nothing to heal rather than a crash. This restores the never-raises
contract ensure_prisma_toolchain() documents.
2026-08-05 13:38:53 -07:00
Yassin Kortam
7bd3a5e6ab
fix(docker): bake the componentized prisma engines at /opt/prisma so any uid can start (#35989)
The gateway and backend images generated the prisma client under
HOME=/home/nonroot, so the engine paths baked into the client sat inside a
directory the base image ships at mode 0700. Only uid 65532 can search it,
and prisma resolves those baked paths eagerly with an existence check that
propagates EACCES, so a container started under any other uid dies with a
PermissionError out of pathlib before the PRISMA_QUERY_ENGINE_BINARY
override is ever read. A chart that sets runAsUser, a docker run --user, or
an OpenShift namespace assigning an arbitrary uid all produce that shape,
and the gateway is the request-serving component, so the proxy does not
serve at all.

Bake to /opt/prisma instead, the fixed world-readable path the other three
images already use, and assert at build time that every baked path lands
there. chmod a+rX rather than a+r because prisma executes the engine to
check it can run on this machine. The runtime PRISMA_BINARY_CACHE_DIR pin
keeps the CLI wrapper's own resolution pointing at the bake rather than at
a /home/nonroot/.cache that no longer exists.
2026-08-05 13:38:46 -07:00
ryan-crabbe-berri
2dc49a913c
refactor(ui): replace hand-rolled query-param routing with nuqs (#35871)
* refactor(ui): replace hand-rolled query-param routing with nuqs

The dashboard carried five copies of the same pushState-based detail
routing hook plus a shared navigateWithParams helper, each with its own
plumbing test and a copy-pasted reactive useSearchParams mock in
component tests. nuqs provides the same shallow history-API routing
behind useQueryState/useQueryStates, so the key, team and org hooks are
deleted in favor of inline useQueryState at their single consumers,
while the models and logs hooks keep their interfaces but drop their
hand-rolled internals. Component tests now mount NuqsTestingAdapter
(via renderWithProviders or locally) instead of patching window.history,
and URL assertions go through onUrlUpdate spies that can additionally
distinguish push from replace, which the old window.location checks
could not

* test(ui): assert browser back closes the log drawer after in-drawer selection

Greptile flagged that the nuqs port of the switching-logs test stopped
at asserting emitted push and replace modes. The test now replays those
recorded modes against a history stack and performs the back step, so a
regression to push-on-select or broken URL-derived drawer state fails
the test instead of passing silently
2026-08-05 13:29:58 -07:00
mateo-berri
3728f3ea62 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_daily_any_cleanup_08_04_2026 2026-08-05 13:14:37 -07:00
Miles Adkins
599283584f feat(fireworks_ai): drop reasoning_effort=auto to the model default
Fireworks rejects reasoning_effort="auto" (accepted set: low, medium,
high, xhigh, max, none, adaptive), so OpenAI-compatible clients sending
it 400. Omitting the param means model default on Fireworks, which is
exactly what auto means on OpenAI's side, so skip it in
map_openai_params instead of forwarding.
2026-08-05 15:09:16 -05:00
tin-berri
32deaff015
feat(spend): rebuild the auto-router benchmarks backend as a per-session rollup (#35910)
Folds every successful auto-routed request into LiteLLM_AutoRouterSession with one
conditional upsert at spend-write time, classifying each turn (same model, first
visit, return to tier, out of order) against the row's own columns so nothing is
read before the write. The upsert's placeholders and argument tuple both derive
from the transaction dataclass's own field order, so the SQL and the call site
cannot drift apart. GET /auto_router/benchmarks aggregates the rollup, grouped
by the full (router, type) identity, and never scans LiteLLM_SpendLogs. A turn's
cache interaction is derived once from its usage record (savings.py owns the
extraction; compute_savings_spend derives cache reads from usage_object itself),
hits are counted order-independently so the overall hit rate matches its covered
denominator, caller-chosen session ids are bounded before entering the primary
key, and a poisoned statement drops only its own session's remaining turns.
Return misses inside the recorded TTL are named for what the telemetry shows
(within_ttl) rather than a presumed cause, since a provider can evict early.
Savings ride each router's derived baseline by default, so the response carries
no deployment-wide baseline label. Rollup retention has its own
maximum_autorouter_session_retention_period setting, pattern-identical to the
spend-logs knob and running in the same cleanup job on its own cutoff. Every
drain trigger sizes the queues through one owner and the enqueue honors
disable_spend_logs beside the tool-usage queue it mirrors.
2026-08-05 20:06:32 +00:00
Abhimanyu Kapur
bea65b6dcc
fix(autorouter): match CJK keyword_tier_rules that regex word boundaries miss (#35984)
* fix(autorouter): match CJK keyword_tier_rules that regex word boundaries miss

Single-word keywords were matched with a \b...\b regex. Every CJK character is a
regex word character and CJK is written without spaces, so \b never fires between
two of them and a rule like 发票 silently missed 我需要开发票, falling through to
complexity scoring instead of the configured tier.

Keywords containing CJK now match as plain substrings, the same way multi-word
phrases already did. The gate reads the keyword rather than the prompt, so a
keyword with no CJK in it keeps word boundary matching regardless of the script
the prompt is written in.

* fix(autorouter): cover Han extensions in planes 2 and 3, not just up to U+2FA1F

The supplementary range stopped at U+2FA1F, so Extension G and H ideographs kept
the word boundary path and stayed unmatchable. Both planes are dedicated to CJK
ideographs, so covering them whole also handles later extensions without chasing
each new block.
2026-08-05 20:02:54 +00:00
Mateo Wang
7c9c9ffbc7
fix(ci): fetch only head and merge-base in lint jobs instead of every branch (#35982) 2026-08-05 13:00:00 -07:00
Mateo Wang
c6dbf48944
Merge pull request #35916 from BerriAI/litellm_passthrough_live_credentials
fix(proxy): resolve pass-through credentials live from router deployments
2026-08-05 12:57:46 -07:00
Yassin Kortam
347798b80e
fix(router): keep custom model_info across a price data reload (#35491)
A price data reload replaced litellm.model_cost wholesale, discarding every
runtime registration: the deployment model_info the Router registers from
model_list, and pricing overrides passed to litellm.register_model. Custom
model groups lost max_input_tokens / max_output_tokens in /model_group/info,
and a deployment whose backend model is in the catalog silently reverted to
upstream values. Runtime registrations are now recorded and replayed on top of
the freshly fetched catalog.

Router._pre_call_checks resolved the per-deployment model name only after the
model-info lookup, so an unregistered model left it unset and the supported
params check ran against the bare model group name, raising "LLM Provider NOT
provided" out of deployment selection. The name is now resolved first, and an
unresolvable provider skips that check rather than failing the request.

Resolves LIT-4675
2026-08-05 19:56:50 +00:00
Yassin Kortam
7ac1085931
fix(auth): return 403 from the OAuth2 enterprise gate (#35838)
The enterprise gate on the OAuth2 auth path raised a bare `ValueError`,
which the terminal handler in auth_exception_handler.py converts to a 401.
Every sibling enterprise gate answers 403, including `_premium_user_check`
and the SSO gate. A 401 tells the client its credential was wrong and to
retry with a better one, and no credential can satisfy that while the
install is unlicensed, so it invites a retry loop that can never succeed.

It now raises a 403 `ProxyException` shaped like the SSO gate. Two response
fields move with it: the `Authentication Error, ` prefix goes away, since
the catch-all built that around `str(e)` and a `ProxyException` is re-raised
unmodified, and `param` becomes `premium_user`, naming the condition an
operator has to clear.

The gate's own text also gains the sentence break it was missing. The
message concatenated straight onto `CommonProxyErrors.not_premium_user`,
rendering as "premium usersYou must be a LiteLLM Enterprise user".
2026-08-05 12:53:56 -07:00
Yassin Kortam
4e8e4a7162
fix(docker): bake the pip image's prisma engines at a world-readable path (#35976)
The build_from_pip image ran a bare `prisma generate`, so prisma recorded
absolute engine paths under $HOME/.cache, which is /root/.cache in that
build. /root is mode 0700 on python:3.13-slim, so any runtime uid other
than 0 gets EACCES just traversing it and the proxy dies during prisma
client initialisation. That is exactly the shape a securityContext with
runAsUser produces.

Generate under a fixed /opt/prisma and chmod it a+rX, matching what the
shipped images already do, and pin PRISMA_BINARY_CACHE_DIR at runtime so
the client resolves the baked engines instead of looking under $HOME. A
build-time assertion fails the build if any recorded engine path lands
outside the pinned prefix, since the original breakage was silent at
build time and only surfaced as a runtime crash for non-root users.
2026-08-05 12:51:53 -07:00
Abhimanyu Kapur
b8df48cd7f
feat(auto-router): let operators replace the LLM classifier's system prompt (#35855)
* feat(auto-router): let operators replace the LLM classifier's system prompt

The complexity router's LLM classifier has always sent one built-in rubric, so the
router could only ever grade difficulty. Operators can now supply their own system
prompt, which replaces the rubric outright and repurposes the same tier machinery for
whatever taxonomy the prompt defines, data sensitivity being the obvious case.

Replacement is total: neither the rubric nor its closing line is appended, since both
describe grading difficulty over a "current message" and a prompt grading something
else is entitled to contradict them. That closing paragraph is also the classifier's
prompt-injection defense, so the config field and the dashboard editor both warn that
a replacement omitting it lets a caller ask for a tier and get it.

The heuristic fallback still scores complexity, which is meaningless for a repurposed
taxonomy, so classifier_fallback now chooses between the heuristic scorer and routing
straight to default_model. The default_model path bypasses tier pools, the adaptive
bandit, and escalation, because no tier was decided and the point of that fallback is
a known destination. It reports itself as default_model_fallback in the spend logs.

The dashboard's prompt editor prefills from a new
/auto_router/classifier/default_prompt endpoint rather than a copy of the rubric in
the frontend, and stores no override when the draft matches the default, so later
rubric improvements still reach every router that never customized it.

Tier names stay SIMPLE/MEDIUM/COMPLEX/REASONING; a custom prompt redefines what they
mean, not what they are called.

* fix(complexity-router): don't let the default_model classifier fallback bypass routing plugins

* fix(complexity-router): don't pin a session to the default model after a classifier failure

* fix(complexity-router): omit the tier from a default-model-fallback routing decision

The classifier never answered, so no tier was decided. The record reported the
tier whose pool happens to hold default_model, which reads in the spend log and
the UI as if the request was classified. Matches how default_fallback already
records a route that no tier produced.

* fix(proxy): allowlist /auto_router/ on the UI backend component

The new GET /auto_router/classifier/default_prompt is a UI-consumed management
route, so it belongs on the control plane. Without the prefix it was exposed by
neither component and test_gateway_plus_backend_covers_full_app failed.

* docs(ui): reword the classifier prompt disclaimer

Frames the closing paragraph as a strong recommendation rather than a
description of what gets dropped, names prompt injection explicitly, and
notes the tier names stay fixed regardless of their display names.

* fix(complexity-router): stop logging a fabricated tier on the plugin fallback path

The classifier-failed fallback resolves a tier so the routing-plugin pipeline has a
pool to filter, but nothing about the request produced that tier. The non-plugin
short-circuit already dropped it from the logged decision; the plugin path still
reported it, so a spend log claimed a classification the request never received.
Record the pool as a plugin-filtered-pool signal instead.

Also name the real problem when the resolved tier has no models at all: that raised
"No candidate models left after routing-plugin filtering" and sent operators hunting
for a policy plugin that never narrowed anything.
2026-08-05 19:48:11 +00:00
mateo-berri
2bd6af1b64 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_daily_any_cleanup_08_04_2026 2026-08-05 12:43:11 -07:00
Yassin Kortam
09dd167b5a
feat(sgr): make the gateway middleware the source of truth for successful requests (#35717)
SGR has had two independent definitions. The admin UI derived it from
SpendLogs, so it counted what litellm's logging callbacks observed and could
attribute and price. BillableRequestMetricsMiddleware counted what the proxy
actually answered at the ASGI edge, but only exported to OTLP for enterprise
metering. The two disagree by design in places, and the SpendLogs figure goes
quiet whenever spend logging is disabled or the callbacks are bypassed.

This adds LiteLLM_DailyGatewayRequests, written by the middleware, and points
the dashboard's Successful Requests tile at it.

Requests fold into an in-memory map at record time rather than going through a
queue like the spend path. A count is a pure aggregate, and every dimension of
the key is chosen by the proxy from a closed set: the date, the category, and a
route that the classifier maps to one of a fixed list of strings rather than
passing the raw path through. Nothing a caller sends can add a key, so the fold
and the table are bounded by (days x categories x routes) however much traffic
arrives; the spend queue blocks once full, which is not acceptable in the
response path. A scheduler job drains it on the existing batch interval, and a
failed flush merges its counts back so a database blip undercounts nothing.

The middleware previously returned early when no billing recorder was
injected, which is the unlicensed case. The new sink is not license-gated, so
that early return now requires both sinks to be absent. The billing recorder
keeps its 2xx-only gate; the sink takes every status so failed_requests is
real. The sink is not told which deployment served the request, unlike the
billing recorder. That id is a sha256 over litellm_params, credentials
included, so a caller who puts a credential in the request body mints a fresh
one per distinct value. No configuration is needed for that: api_base and
base_url are on _BANNED_REQUEST_BODY_PARAMS and need allow_client_side_
credentials, but api_key is not on that list, and both reach the same
_handle_clientside_credential branch. The read endpoint aggregates the
dimension away regardless, so the key is better off without it.

The new table carries no key, user or team dimension, so /gateway/daily/activity
is restricted to proxy admin roles and the per-key and per-model breakdowns
keep reading the daily spend tables. The old path is left running and marked
with TODOs.

A fetched result carries the range key it was fetched for, and the render
selects it only when that key matches the range on screen. Both the gateway
counts and the spend aggregate go through that rule: the request tiles read the
first and fall through to the second, so stamping only one of them would leave
the tile showing a superseded range by the other route.

The paginated pages behind that aggregate are reached through a failure flag,
so the flag is stamped too. A flag left over from the previous range would let
those pages through while a new range is in flight, which is the same defect
one fallback further down.
2026-08-05 12:40:47 -07:00
mateo-berri
18572fe86f Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_passthrough_live_credentials
# Conflicts:
#	basedpyright-code-budget.json
#	litellm/types/router.py
#	ruff-strict-budget.json
#	type-discipline-budget.json
2026-08-05 12:40:32 -07:00
mateo-berri
f2049a7d9b fix(proxy): narrow pass-through provider resolution to BadRequestError 2026-08-05 12:37:12 -07:00
Mateo Wang
332ec6c17a
Merge pull request #35926 from BerriAI/litellm_remove_types_ruff_exclusion
chore(lint): remove litellm/types from the ruff lint exclusion
2026-08-05 12:35:02 -07:00
mateo-berri
0a0c91483d Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_passthrough_live_credentials 2026-08-05 12:31:38 -07:00
mateo-berri
f7bdc10b21 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_passthrough_live_credentials
# Conflicts:
#	ruff-strict-budget.json
#	type-discipline-budget.json
2026-08-05 12:31:38 -07:00
Yassin Kortam
2a9843e649
fix(proxy): keep the connected DB client when a startup health check fails (#35837)
`_setup_prisma_client` ran `connect()`, then a `SELECT 1` health check, then
armed the DB health watchdog. Any failure fell into one handler that, with
`allow_requests_on_db_unavailable` set, swallowed the error and returned None,
which the caller assigns to the module-level `prisma_client`. A single
transient timeout on that health check therefore discarded a client that had
already connected, for the life of the process, and skipped the watchdog that
exists to reconnect it.

The watchdog now starts before the health check, and a swallowed post-connect
failure returns the connected client instead of None. A client whose
`connect()` failed is still discarded, and startup still hard-fails when
`allow_requests_on_db_unavailable` is not set.

The same check also misreported its own failure. `health_check()` labelled its
error `disconnect()`, a copy-paste from the real `disconnect()` below it, so
grepping the logs for the health check turned up nothing and read as "the check
never ran". Both it and the sibling `connect()` failure reported through
`print_verbose`, which reaches `verbose_proxy_logger.debug` and otherwise prints
only under the deprecated `litellm.set_verbose`, leaving a startup-blocking
database fault invisible at the verbosity operators actually run. Both now log
at warning under their own names. The proxy logger's handler carries the secret
redaction filter, so a connection string in the exception text is redacted
exactly as it was on the old print path.
2026-08-05 12:27:49 -07:00
Mateo Wang
54f83b2614
Merge pull request #35870 from BerriAI/litellm_reland_evicted_client_closer
fix(caching): re-land evicted LLM client closing (#35492) atop self-healing handlers
2026-08-05 12:22:12 -07:00
mateo-berri
6c76f5f9c6 chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down
Every ruff-strict rule that sat above its budget limit (FURB188, RUF022,
SIM118, UP007, UP032, UP037) is now at zero, LIT001 and LIT006 are back
under their ceilings, and the freed headroom is ratcheted out of
ruff-strict-budget.json, type-discipline-budget.json, and
basedpyright-code-budget.json so the gates take the fast path again
2026-08-05 12:18:13 -07:00
mateo-berri
83aca91dde fix(guardrails): allow litellm_content_filter to run on post_mcp_call
ContentFilterGuardrail implements apply_guardrail, which is everything the
generic post_mcp_call_hook machinery needs to scan an MCP tool result before
it reaches the model, but post_mcp_call was missing from
get_supported_event_hooks. _validate_event_hook rejects any mode outside that
list, so a config with `mode: post_mcp_call` failed proxy startup with
"Event hook GuardrailEventHooks.post_mcp_call is not in the supported event
hooks" instead of scanning tool output.

Declaring the hook makes the indirect-prompt-injection case enforceable: an
MCP fetch tool returns a page whose body carries "IGNORE ALL PREVIOUS
INSTRUCTIONS ...", and the gateway blocks the result rather than handing it
to the model.
2026-08-05 12:17:01 -07:00
mateo-berri
d259070fdf Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_daily_any_cleanup_08_04_2026 2026-08-05 12:13:11 -07:00
Yassin Kortam
0b8c58735d
fix(ci): make the env-key doc gate see get_secret_bool reads (#35833)
The gate only matched os.getenv(, litellm.get_secret( and
litellm.get_secret_str(, so a bare get_secret_bool("X") matched nothing and
the key bypassed the documentation requirement entirely. Add a fourth pattern
for get_secret_bool, with or without the litellm. prefix, and a negative
lookbehind so an unrelated receiver's .get_secret*( call is not mistaken for
an env var read.

Extraction and table parsing move into functions behind a __main__ guard so
the patterns can be unit tested; the script is still invoked exactly the same
way by CI.

This surfaces 13 keys the gate never checked, 8 of which have no reference
row yet.
2026-08-05 12:03:15 -07:00
tin-berri
d3d30353aa
refactor(ui): remove the three dashboard lint-budget violations added by #35893 (#35960)
PR #35929 zeroed the eslint budget headroom while #35893 added UI code in parallel, so staging went over budget by one complexity violation and two no-large-inline-object-arg violations, failing frontend-lint on every UI-touching PR until #35964 reverted the ratchet. This removes the three violations at the source so the budgets can ratchet back down: the submit-blocked-reason chain in add_auto_router_tab moves to a module-level helper, taking the component arrow from complexity 21 to 18, and the two four-property object literals in build_complexity_router_config.test.ts move into named variables. No behavior change; the touched suites pass (101 tests)
2026-08-05 11:51:24 -07:00
ryan-crabbe-berri
2792887e47
fix(proxy): give proxy_admin_viewer read parity with proxy_admin (#35851)
* fix(proxy): give proxy_admin_viewer read parity with proxy_admin

Route-level checks already default-allow management GETs for the viewer
role, but ~15 handlers compared user_role to PROXY_ADMIN only, dropping
viewers into regular-user scoping (/key/list, /user/info, /model/info,
guardrails, prompts, agents, memory, workflows, MCP catalog, coordination
redis settings, credential migration check, enterprise projects). Swap
those read paths to user_api_key_has_admin_view; write gates unchanged.

The dashboard now presents the viewer session as Admin for all gating
(effectiveSessionRole) so every page fetches with admin visibility, with
userRoleLabel/isViewOnly preserving the account-menu label and the
playground cost guard. The server remains the write authority.

* refactor(agents): remove side-effectful health_check param from GET /v1/agents

Addresses a security review finding on the admin viewer read parity change:
listing agents with health_check=true made the proxy issue a server-side GET
to every agent URL, so a read-scoped caller could trigger request fan-out
beyond their object permissions. The list endpoint is now a pure read for
every role.

Removes the query param, the URL probing helper and its timeouts, the
AgentHealthCheck httpx provider tag, and the dashboard's Health Check
toggle. Requests still passing health_check=true get the full list back
with the param ignored.

* fix(proxy): keep credential encryption check proxy_admin only

The residual scan behind GET /credentials/migrate-encryption/check loads
every model, credential, MCP, team, and verification-token row and runs a
decryption attempt on each stored value. Extending it to proxy_admin_viewer
let a read-only account repeatedly trigger deployment-wide scans, so the
route keeps its original full-admin gate.

* fix(agents): restore health_check, keep list fast path proxy_admin only

Restores the agent health_check feature exactly as before this PR: the
query param, the URL probing helper, the httpx provider tag, and the
dashboard toggle all return, so existing callers keep the filtering
contract. The viewer expansion is instead reverted at its source: the
GET /v1/agents admin fast path stays PROXY_ADMIN only, so a
proxy_admin_viewer goes through the object-permission scoped branch as
before and cannot fan out health checks beyond their allowlist. The
viewer read of a single agent stays viewer-inclusive since it has no
side effects.
2026-08-05 18:33:55 +00:00
Miles Adkins
0c0e1e8374 feat(fireworks_ai): translate NIM/vLLM extra params to Fireworks-native args
Requests migrated from NIM/vLLM servers carry extras that flow through the
extra_body passthrough verbatim, but the Fireworks chat completions API
either names them differently or does not accept them at all. Add
FireworksAIConfig.map_extra_body_params, invoked from the fireworks chat
dispatch, which renames truncate_prompt_tokens to prompt_truncate_len,
maps chat_template_kwargs.enable_thinking to reasoning_effort, converts
guided_json/guided_grammar/guided_choice to response_format, and drops
the remaining extras (min_tokens, stop_token_ids, skip_special_tokens,
guided_regex, etc.) with a debug log. Alias and competing-constraint
combinations raise BadRequestError. Unrecognized extras keep passing
through untouched, as do fireworks-native params like top_k.
2026-08-05 13:29:28 -05:00