Skip to content

chore: sync upstream 2026-10-03 - #293

Merged
shudonglin merged 255 commits into
litellm_internal_stagingfrom
chore/sync-upstream-2026-10-03
Oct 4, 2026
Merged

shudonglin merged 255 commits into
litellm_internal_stagingfrom
chore/sync-upstream-2026-10-03

Conversation

@shudonglin

@shudonglin shudonglin commented Oct 3, 2026 •

Copy link
Copy Markdown

Full -X theirs sync of https://github.com/BerriAI/litellm main (now at HEAD, 252 commits since litellm_internal_staging).

2026-10-04 refresh of this sync PR: 71 more upstream commits had landed since the 2026-10-03 merge (which was green but not yet mergeable at the start of today's run). Re-merged upstream/main into this branch and reconciled the new conflicts and merge corruptions:

Conflict resolutions (resolved toward upstream, per the fork's own policy on modify/delete conflicts):

  • litellm/tracing/decode.py, litellm/tracing/store.py, tests/test_litellm/tracing/test_decode.py: deleted, following upstream's own "tracing development seed" commit (feat(traces): tracing development seed BerriAI/litellm#44363), which replaced the whole Python-side OTLP decode/storage path with a direct Rust call (no intermediate Python decode step).

Fork patches re-applied after today's merge wiped or broke them:

  • osv-scanner.toml: restored the RUSTSEC-2024-0436 and RUSTSEC-2023-0071 IgnoredVulns entries (dropped again by this sync, same as yesterday's).
  • litellm/router_utils/fallback_event_handlers.py: restored the RouterErrors/RouterRateLimitError/RouterRateLimitErrorBasic/_get_deployment_order imports the merge silently dropped from this file's import block.
  • litellm/proxy/lens/analysis.py: restored from upstream/main verbatim. Yesterday's fix deleted a dead nested decide closure in favor of the module-level _decide helper trio; today's further upstream commits flipped which copy is live, leaving both the now-dead trio and an orphaned = applied unpack referencing a variable that no longer exists. Upstream's current file has already done the same dead-code cleanup, so restored verbatim rather than hand-patching.
  • litellm/proxy/lens/inference.py: restored the attempts=min(64, max(8, job.settings.concurrency * 4)) scaled retry budget on the reserve_budget CAS loop, dropped when upstream wrapped it in @asynccontextmanager with settle-on-failure handling this sync.
  • Dockerfile, backend/Dockerfile, docker/Dockerfile.database, docker/Dockerfile.non_root: re-pinned the runtime stage's FROM $LITELLM_RUNTIME_IMAGE back to a literal digest matching its ARG default (reverted to the vanilla $VAR form by this sync).

Dropped/retired (superseded by upstream, not fork patches anymore):

  • litellm/constants.py's OTLP_MAX_SPANS + the span.rs "keep first N spans" truncation patch: upstream's own new DecodeLimits/Budget system (litellm-rust/crates/traces/src/otlp/limits.rs) ships its own test suite asserting every limit dimension fails fast and uniformly on overflow (environment_decode_limits_are_used_and_invalid_values_fail, configurable_decode_limits_apply_to_both_wire_formats). Restoring yesterday's "truncate gracefully" patch broke those 2 upstream tests, so reverted to upstream's Err(Error::TooLarge) and deleted the fork's own now-superseded regression test. The underlying OTel-Collector-default-batch-size concern is still real but is now an operator-config fix (OTLP_MAX_SPANS>=8192), not a code patch.
  • deploy/lens/Dockerfile: upstream itself rewrote this off the bare python:3.12-slim tag onto the same wolfi-base ARGs the main Dockerfile uses, already digest-pinned at the ARG default.

Verification before pushing:

  • python3 scripts/verify_fork_patches.py: OK, all 234 current-assertion patches present, all literal digest pins current.
  • cargo check --workspace and cargo test -p litellm-traces --test otlp (166 passed) in litellm-rust/.
  • Targeted ruff check --select F821,F401 across litellm/ and litellm-rust/: clean (no new undefined-name/unused-import regressions).
  • pytest on the touched areas: lens/analysis, lens/inference, fallback_event_handlers (142 passed), router tests (1861 passed), tracing tests (92+ passed), proxy routes/realtime/http-parsing (212 passed).

Follow-up push after the first CI run on this head: fixed a ruff-format violation in litellm/harness/sandbox/local.py (a nearby upstream line shifted this sync), and quarantined a new fork-infra gap: upstream's brand-new roi-database test shard (BerriAI#44426) hit the same un-registered-Codecov-repo gap as the existing Lens coverage upload, so added continue-on-error: true to its own coverage-upload step, same fix already applied there.

Second follow-up: the merged head also breached all 3 lint-budget gates documented in REPO_RECURRING_FIXES (ruff-strict, type-discipline, basedpyright), each confirmed as upstream's own growth (not fork hunks) via a base-vs-head diff and raised per that section's procedure, with two calling out pre-existing drift already present before this sync touched anything: ruff-strict C901 306->308, TID251 1035->1037; type-discipline LIT001 22174->22233, LIT010 16398->17885 (base already 1171 over before this sync); basedpyright reportUnusedFunction 136->141, reportUnusedVariable 137->201 (base already 58 over before this sync). All three gates re-run clean locally after the raise.

🤖 Generated with Claude Code

https://claude.ai/code/session_011vvp2gsj31HM4iGNhKKvk6


Generated by Claude Code

shrey-berri and others added 30 commits October 1, 2026 09:23
…ration (BerriAI#43996)

* test(proxy): migrate DB and Redis backed proxy tests into tests/integration

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

* test(proxy): drop a type suppression comment from the key metadata integration test

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

* test(proxy): scope integration test cleanup to owned rows and wait for backend stats flush

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

* test(integration): seed NULL cache_hit and bound recovery reads from below

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(lens): bill worker analysis through virtual keys

* fix(lens): pin the verified worker image and add setup proof

* fix(lens): preserve network checks and redact billed analysis logs

* test(lens): preserve legacy worker result submission during upgrade

* fix(lens): enforce trusted worker IPs and restore coverage uploads

* docs(lens): explain trusted proxy requirements for worker allowlists

* fix(lens): yield to worker disconnects after the synthetic body
…sts/unit/proxy (BerriAI#43998)

* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy

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

* test(proxy): stub HIBP through respx by disabling the aiohttp transport

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

* test(proxy): share the httpx transport fixture across proxy unit tests

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

* test(proxy): restore proxy globals without a missing-value sentinel

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

* test(proxy): package moved dirs and stub the login breach check at the HTTP boundary

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

* test(proxy): isolate the mcp server manager per test

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(ci): add used_client_oauth_token to the GCS pub/sub spend-log golden

BerriAI#43063 stamps used_client_oauth_token into spend-log metadata, so
test_async_gcs_pub_sub_v1 failed on main with an extra metadata key

* test(ui): give the auto-router threshold save wait room for the availability debounce

BerriAI#42625 keeps Save disabled while a 300ms-debounced availability check runs.
This test waits for Save right after the change, so the whole debounce lands
inside waitFor's 1s default and it times out under CI load. It is the
recurring UI Unit Tests failure on main since BerriAI#42625 landed

* test(e2e): expect no pricing tier on bills for streamed calls OpenAI served at default

BerriAI#42870 added both the rule that a served default or standard tier bills at
base pricing and records no service_tier, and streamed tests expecting the
row to record 'default'. They have failed on every scheduled litellm-e2e run
since. The tests now map the served tier to the pricing basis the bill must
record and check input is billed at that basis's rate; the messages case
registers custom rates so the rate check has something to compare against

* test(e2e-ui): wait for the call-id search before hovering the logs row

The row the spec hovers is already on the unfiltered first page, so it was
found before the search request returned. The search response then
re-rendered the table under the mouse, and the Base UI tooltip never opened.
Reproduced with Playwright against a local proxy: hovering right after the
fill never shows the tooltip, hovering after the search response shows the
call id every time

* test(e2e): run the Together structured-output case on the hybrid Qwen with reasoning off

The case picked the cheapest Together row flagged supports_response_schema.
DeepSeek-V4-Flash-0731 hit its cost-map deprecation date on 2026-09-29, so the
pick moved to GLM-5.3-Flash, a reasoning-only model that spends the 1024-token
budget thinking and returns content=None. Qwen3.5-9B is the pinned hybrid model
the reasoning_effort=none case already exercises, and Together lists it with
structured output support

* test(integration): read the agent 365 guardrail status by its own name in spend logs

The MCP shard runs under xdist against one database, and a sibling file creates a
default_on pre_mcp_call content filter there. The owned proxy reloads DB guardrails, so
that filter's 'success' entry could land first in guardrail_information and the test
read it instead of the agent 365 verdict

* test(unit): ignore asyncio's leaked-task records in the budget limiter push-failure log check

gc.collect() inside the caplog window can collect a pending task an earlier test left
on a closed loop, and asyncio logs 'Task was destroyed but it is pending' into this
test's records. The check still counts every LiteLLM logger, and unretrieved task
exceptions on this loop still go through the asserted exception handler

* test(e2e-ui): fill the create-tag fields inside the dialog

BerriAI#42949 added 'Filter by tag name' and 'Filter by description' inputs to the Tag
Management page, so page-wide getByLabel('Tag Name') and getByLabel('Description')
match two elements and Playwright's strict mode fails the create step

* test(integration): run integration proxies with the CI license

Multi-worker proxies start each uvicorn worker in a fresh process, so every
worker reads the license from its environment. Forward LITELLM_LICENSE into the
proxy and test runner environments

* ci: save GitHub Actions caches only from main and bump codecov-action to 5.5.5

Every pull request saved its own uv, maturin, Rust and Prisma caches, about
4.5 GB per PR, so the repository's 10 GB cache budget evicted main's entries
within minutes. Pull request jobs then missed every cache, downloaded all
dependencies from PyPI and hit the install step timeouts. Pull requests now
restore only, and main keeps the caches warm for them. test-linting and
check-ui-api-types run only on pull requests and keep saving

codecov-action 5.5.4 imports its signing key from the deleted codecovsecurity
keybase account, so every upload failed signature verification. 5.5.5 reads it
from codecovsecops; the key ID matches the one signing the current CLI

* test(unit): join the session-minting thread before collecting the handler

asyncio.to_thread resumes the test as soon as the worker sets its result,
while the pool thread can still hold the work item and through it the
handler. gc.collect() then cannot finalize the handler and the session stays
open. A pool that shuts down before the test continues drops that reference

* test(integration): relaunch owned proxies that lose their port, expire idle gateway connections early

owned_proxy_process released its reserved port and the proxy bound it only
after full startup, so another xdist worker or an outgoing connection could
take it first and the proxy exited with 'address already in use'. The launch
now retries on a fresh port when that happens and stops every failed attempt.

uvicorn closes idle keep-alive connections after 5 seconds and httpx expired
them at the same 5 seconds, so a request sent right at that mark could reuse a
socket the server was closing and get 'Connection reset by peer'. Gateway
clients now drop idle connections after 2 seconds

* ci(circleci): give the base SDK wheel build the same 30 minute no-output window as the Windows build

The release profile builds with fat LTO and one codegen unit, so the final
link of litellm-cache-s3 runs silently for minutes. Successful builds take
711 to 749 seconds, right at the default 10 minute no-output limit, and about
30% of recent runs were killed there

* test(integration): model the budget-reset database outage as 10 seconds instead of 5 refused connections

The proxy retries the database about every 30 seconds and each retry opens
roughly one connection, so a 5-connection outage took 3 to 4 retries to clear
and recovery landed between 60 and 90 seconds, straddling the test's 80 second
reset window. A fixed 10 second outage still refuses the immediate reconnect
and recovers on the next retry

* ci: move the unit-test uv cache split into a composite action

check_workflow_startup_safety sums every setup step's timeout, so the save and
restore variants each counted 5 minutes although only one runs. One composite
step keeps the setup ceiling at 35 minutes

* test(unit): point tiktoken at the bundled cache for every unit test

The rust_bridge tokenizer tests loaded o200k_base before any test in their
xdist worker had imported default_encoding, so tiktoken fell back to the
temp cache and tried to download under pytest-socket. Move the session
fixture from litellm_core_utils/conftest.py to the root unit conftest.

* test(integration): answer model discovery probes in the hosted_vllm wire tests

The router's periodic upstream model info refresh sends GET /v1/models to
hosted_vllm deployments, so a wire server that is live during a refresh
sees an extra request. Answer the probe with an empty model list and leave
it out of the provider-call assertions, matching the responses bridge
tests.
…ails tests into tests/unit/proxy (BerriAI#44003)

* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy

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

* test(proxy): stub HIBP through respx by disabling the aiohttp transport

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

* test(proxy): share the httpx transport fixture across proxy unit tests

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

* test(proxy): restore proxy globals without a missing-value sentinel

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

* test(proxy): package moved dirs and stub the login breach check at the HTTP boundary

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

* test(proxy): isolate the mcp server manager per test

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

* test(proxy): move management_endpoints, management_helpers and guardrails tests into tests/unit/proxy

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

* test(proxy): reuse the shared httpx transport fixture in moved proxy tests

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

* test(proxy): stub outbound HTTP and package moved test dirs

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

* test(proxy): restore the config server hostname in the mcp resolution test

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

* test(proxy): pin the completion tokenizer model in the straiker screening test

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ew (BerriAI#43972)

* feat(ui): redesign agent trace run view with chat-style detail pane

Tree with connector lines, typed icon tiles and provider logos, hover
cards with timing, and Input/Output sections rendered as message cards.

* fix(tracing): show text for block-list message content and split normalizers per convention

OpenAI responses-style content (reasoning + text blocks) rendered as raw
JSON in the trace view. Keep the text blocks and drop opaque reasoning.

Move each convention into litellm/tracing/normalizers with an ordered
registry so new frameworks plug in without touching OTLP decoding.

* fix(tracing): keep long message histories as valid JSON and parse function_call blocks

* feat(ui): open agent traces in a resizable side drawer with a devtool-style tree

Clicking a run opens it in a drawer over the list instead of a full page.
j/k and the header arrows switch runs, Esc closes. The tree gets dashed
connectors, per-span waterfall bars, mono tool names and real provider
logos. AI messages with reasoning/function_call blocks render as text.

* fix(trace-ui): address review: valid JSON trimming, drawer keys, reduced motion, narrow screens

* feat(tracing): serve span content in a standard LiteLLM UI format

GET /v1/traces/{trace_id}/spans/{span_id} now also returns input_ui and
output_ui, a tagged union of messages, fields or text built server side by
litellm/tracing/ui_format.py. The trace UI renders from those fields and only
falls back to client-side parsing when talking to an older proxy. The raw
input and output strings are unchanged, and so is storage

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

* fix(tracing): fall back to an elision marker when shortened messages still exceed the size limit

* fix(tracing): keep both messages when tool_calls are oversized and keep failed-tool styling

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
BerriAI#43748)

* feat(s3_v2): add s3_partition_granularity option for hourly S3 folders

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

* test(integration): cover s3 v2 partition granularity across surfaces, settings and chaos

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

* test(integration): cover previous_response_id history rebuilt from an hourly cold storage object

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

* fix(s3_v2): reuse the cold storage key only when s3_v2 owns cold storage

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

* test(s3_v2): cover hour rollover, postgres outage, in-flight switches, key/team vars and real S3 layout

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

* chore(liccheck): authorize libfaketime, the GPLv2 dev-only clock the s3 rollover integration test preloads

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

* test(s3_v2): wait for the rejected-request cell's payloads by id, not by line count

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

* test(s3_v2): declare the postgres outage cell's models in config and trip the relay on burst ids

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

* test(s3_v2): drop the libfaketime hour rollover cell and its dev dependency

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

* test(e2e): deselect the s3_v2 live e2e on the stage-mirror stack

The stage-mirror config enables no s3_v2 callback, so every test in test_s3_log_e2e.py fails its readiness check there. The file keeps running in the Buildkite e2e lane, which configures s3_v2

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

* test(s3_v2): declare the sink outage burst models in config

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

* refactor(s3_v2): read cold storage metadata without an empty dict default

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

* test(integration): wait for the proxy to reconnect before the postgres outage recovery request

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

---------

Co-authored-by: mrinal <mrinal@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng <yucheng@berri.ai>
* chore(lens): remove deployment screenshots

* refactor(lens)!: rename internal engine package and API

* fix(lens): pin worker image for renamed API

* test(lens): cover fresh and populated rename migrations

* fix(lens): protect db-push upgrades and restore routing and CI

* fix(lens): resolve migration tables across schemas and include database driver
…s/unit/proxy (BerriAI#44006)

* test(proxy): move utils, agent_endpoints and endpoint tests into tests/unit/proxy

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

* test(proxy): package moved unit test directories

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

* test(proxy): exclude proxy-db-owned files from the misc target

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

* test(proxy): drop the redundant fixture docstrings in the proxy conftest

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…/unit/proxy (BerriAI#44012)

* test(proxy): move proxy_server, _experimental and db tests into tests/unit/proxy

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

* test(proxy): keep tuple identity in proxy state restore and fix misc target paths

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ils and root proxy tests into tests/unit/proxy (BerriAI#44015)

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…roxy by glob (BerriAI#44018)

* test(proxy): delete the legacy proxy test tree and serve the redirect test from loopback

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

* test(proxy): exercise the shard check directly for unit_selection-owned children

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

* test(proxy): serve the redirect test from respx instead of a socket

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

* test(proxy): credit shard ownership only to unit flags wired in gha

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

* test(proxy): implement the wired-flag shard crediting the tests assert

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

* test(proxy): split the root proxy test files into their own unit shard

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

* test(proxy): point the rate-limit skip reason at the usage-based-routing-v2 RPM tests

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

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.16.2 to 6.19.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](py-pdf/pypdf@6.16.2...6.19.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

The pytest-postgresql based proxy tests moved to tests/integration on the real
Postgres harness in BerriAI#43996, so nothing loads the plugin anymore. uv.lock is
edited by hand to drop the package and its now orphaned mirakuru and port-for
deps; uv lock --check passes and a full relock resolves the same package set
…rriAI#44055)

* docs(proxy): point mcp_server test references at tests/unit/proxy

The legacy tests/test_litellm/proxy tree was removed in BerriAI#44018. Repoint the
mcp_server AGENTS.md mirror path, swap its auth example for a module that still
exists, and drop the utils.py comment block that named the old test path

* docs(proxy): fix remaining mcp_server legacy test path and note import-time env reads

Repoint the second tests/test_litellm reference in the mcp_server AGENTS.md
Tests section and move the import-time env guidance there from the removed
utils.py comment
…4044)

* feat(ui): bucket model leaderboard usage by day or week

* test(ui): cover daily buckets in model leaderboard series

* feat(ui): add daily/weekly toggle and per-bucket total to model leaderboard chart

* test(ui): cover the daily/weekly toggle on the model leaderboard

* feat(model-insights): add gateway-wide daily totals to the response type

* feat(model-insights): return per-day totals across every model, not just the top ranked ones

* test(model-insights): daily totals include models outside the top ranking

* chore(model-insights): regenerate lazy openapi snapshot for daily totals

* chore(ui): regenerate api types for model insights daily totals

* fix(ui): compute leaderboard bucket totals from gateway-wide daily totals

* fix(ui): show the gateway total, not the top-ten subtotal, in the leaderboard tooltip

* test(ui): cover gateway-wide bucket totals on the model leaderboard

* fix(model-insights): type daily totals as an immutable tuple

* fix(model-insights): build daily totals without new mutable collections
* chore(lint): remove the LIT002 mutable-construction rule

Drop LIT002 from scripts/check_type_discipline.py along with its helpers,
its budget entry, its unit tests, and the AGENTS.md and gate docstring
mentions. `# mutable-ok` now only suppresses LIT001, so the markers that
only existed to silence LIT002 became LIT013 stale suppressions and are
removed. The files whose layout depended on those trailing comments are
reformatted with ruff format.

Every other LIT rule count is unchanged and the ASTs of all touched
litellm/ files match main apart from one docstring.

* chore(lint): keep the leftover mutable-ok markers for a follow-up

Restore the ~1.4k `# mutable-ok` markers stripped in the previous commit
so this PR only touches the checker, its tests, the budget, and docs.
Those markers no longer suppress anything, so `# mutable-ok` is exempt
from LIT013 until a follow-up strips them.

* Revert "chore(lint): keep the leftover mutable-ok markers for a follow-up"

This reverts commit c35bc0b.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Price-Sync: litellm-providers

Co-authored-by: berriai-litellm-provider-info-sync[bot] <328147090+berriai-litellm-provider-info-sync[bot]@users.noreply.github.com>
…rriAI#43967)

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rriAI#44058)

Sail now rejects completion_window "flex" on synchronous requests with a
400 saying flex is only for background responses or Batch work. The chat
flex case and the responses flex case have failed on every scheduled
litellm-e2e run in builds 337, 340 and 341. The chat cases keep balanced and
auto, and the responses case sends a caller metadata.completion_window of
balanced, so both still prove the window reaches Sail and the bill uses
that window's distinct rates
… tracebacks (BerriAI#44062)

* ci(circleci): test Redis behavior against local Redis and print short tracebacks

The redis_caching_unit_tests job ran three legacy files against the shared remote Redis. The DualCache and
batch-read logic that never needed a server now lives in tests/unit/caching/test_dual_cache.py with a mocked
RedisCache, and the behavior that does need one (the increment-with-floor Lua script, read-through, deletes,
batch reads) moved to tests/integration, which starts a local Redis. test_returned_settings only read
REDIS_PORT and is replaced by a unit test of Router.get_settings

CircleCI pytest runs now use --tb=short so failure output stays readable in the test results tab

* ci(integration): print short tracebacks from run.py and allow Redis in the sdk shard
…rriAI#44064)

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nd fix registry metadata (BerriAI#43907)

* fix(cost-map): add nebius qwen3.8-27b and correct nebius context limits

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

* fix(cost-map): add and correct provider deprecation dates for deepseek, gemini and azure models

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

* fix(cost-map): add perplexity, openrouter and voyage models and correct gemini, nebius and perplexity metadata

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>
… tool (BerriAI#43892)

* feat(tool-policies): show the user who owns the key that discovered a tool

GET /v1/tool/list and GET /v1/tool/{tool_name} resolve the discovering key's owner from the verification token and user tables at response time and return it as a nullable user field. The Tool Policies page adds a User column that shows alias, then email, then ID, with the same cell the Virtual Keys page uses. Keys without an owner, deleted owners, and rows without a key hash show no user, and a database failure in the owner lookup keeps the tools listed with user null

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

* fix(tool-policies): bound the owner lookup with chunked membership queries

The key-by-token and user-by-id lookups behind the tool rows' user field
put every distinct key hash into one IN list. BaseRepository gains
find_many_in, which runs the repository's chunked membership query and
converts the rows like find_many does, and the owner lookup uses it

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

* test(tool-policies): cover the owner column across the tool routes and the dashboard

Integration cells for the direct, detail and filtered tool routes, owners without alias or email, deleted owners and keys, keyless and unknown-key historical rows, more keys than one membership chunk, repeated reads, two-worker reads during discovery and a failed owner lookup. A Playwright cell drives the bundled Tool Policies page against the live proxy and follows the owner link

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

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…iAI#44052)

* feat(proxy): gzip buffered responses for clients that accept it

Large JSON reads like /user/daily/activity/aggregated shipped tens of MB
uncompressed. Compress single-message bodies of 500B or more when the
client's Accept-Encoding allows gzip (q-values and the wildcard honored).
Streamed and etagged responses pass through untouched, every negotiable
response carries Vary: Accept-Encoding, and bodies of 1MB or more are
compressed in a worker thread

* fix(proxy): skip partial and no-transform responses in gzip and always release the held start

The gzip gate now also skips 206 Partial Content and Cache-Control: no-transform,
since compressing either breaks byte ranges or ignores an explicit ban on transforms.
A response start without a headers key no longer raises, and a start the app never
follows with a body message is forwarded when the app returns instead of being dropped.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…gs (BerriAI#44057)

The usage card hid actual and baseline spend unless every older session
could be rebuilt from SpendLogs within two seconds, which on a real
gateway it never was. Each complexity router's actual spend is now its
rollup spend and its baseline is spend plus recorded savings, for old
and new requests alike, so the benchmarks and session endpoints never
scan SpendLogs. Adaptive and quality routers record no savings baseline
and stay out of the compared totals; savings_estimated_classifier_cost
is kept and covers the same compared requests
…44035)

* refactor(proxy): inject tracing receiver and access context

* refactor(proxy): own tracing resources through FastAPI lifespan

* test(proxy): pass tracing dependency in Lens lifecycle

* refactor(proxy): stop tracing logger cooperatively

* refactor(proxy): derive tracing permissions in one place

* refactor(proxy): compose application lifespan state

* refactor(proxy): give Lens tracing storage directly

* refactor(tracing): name shared ClickHouse storage explicitly

* refactor(tracing): extract shared ClickHouse storage crate

* test(proxy): isolate db push timeout from Lens safety check

* fix(tracing): drain spend retries during shutdown
* refactor: separate OTLP HTTP decoding from trace codec

* feat: complete trace ingestion and read paths

* fix: encode OTLP protobuf errors in Rust

* fix: raise OTLP body limit to 16 MiB

* test: cover OTLP auth body parsing boundary

* refactor: parse OTLP media type into enum

* fix: enforce OTLP body size at HTTP boundary

* perf: preserve shared OTLP metadata across ingestion

* bench: compare owned and shared trace resource fanout

* refactor: extract shared storage and Python conversion caches

* refactor: keep shared storage owned by traces

* test: keep trace loopback coverage in Rust

* test(proxy): adapt trace coverage to injected access context

* fix(tracing): satisfy stacked branch lint checks

* refactor(tracing): use immutable ingestion payloads

* fix(tracing): declare native error encoder export

* test(proxy): resolve trace access through dependency

* fix(tracing): align merged normalizer types and bridge tests

* fix(tracing): address ingestion and diagnostic review findings

* fix(proxy): preserve body parsing for partial request scopes

* test(proxy): use valid HTTP scopes in request fixtures

* test(proxy): complete auth request flow scopes
…he Azure retired models page (BerriAI#44455)

Price-Sync: litellm-providers

Co-authored-by: berriai-litellm-provider-info-sync[bot] <328147090+berriai-litellm-provider-info-sync[bot]@users.noreply.github.com>
…ic Messages API (BerriAI#44419)

* fix(health): probe Bedrock Mantle Claude deployments over the Anthropic Messages API

Bedrock Mantle serves Claude ids only on /anthropic/v1/messages, but health
checks probed every chat-mode deployment over /v1/chat/completions, so a
bedrock_mantle Claude deployment showed unhealthy while real /v1/messages
traffic to it succeeded

Add an anthropic_messages health check mode and make it the default for
bedrock_mantle Claude models. An explicit model_info.mode still wins, and
/health/test_connection and the Add Model form accept the new mode

* fix(health): resolve the test connection mode from the deployment when the request omits it

The Admin UI model page sent the mode /model/info had filled in from the cost
map back as the probe mode, so Test Connection on a Bedrock Mantle Claude
deployment still went over chat completions. The page now forwards only the
row's id, and /health/test_connection resolves a missing mode the way /health
does: the stored model_info.mode, then the mode the provider requires, then the
cost map.

* fix(health): resolve an omitted ahealth_check mode the way the proxy does

* fix(health): test connection honors a stored mode only for the stored model and rejects a non-string mode

A request that selects a stored deployment and sends a different litellm_params.model now resolves the probe mode from that model instead of the stored model_info.mode. A litellm_params.mode that is not a string answers 400 instead of 500. The Bedrock Mantle rule that Claude models are probed over the Messages API moves into the provider package.

* fix(health): shape test connection probe params for the model the request probes

A request that selects a stored deployment by id and overrides the model
resolved its probe mode from the overridden model but still injected
max_tokens from the stored mode, so an embedding override of an
anthropic_messages deployment failed with a Mistral 422 extra_forbidden

* fix(health): report an early ahealth_check failure as itself, not as a missing mode

With the mode resolved automatically when the caller omits it, a failure
before that resolution (no model, a non-string model, a provider that does
not resolve) was wrapped as "Missing mode", a hint that pointed at the wrong
fix and dropped raw_request_typed_dict from the result. Every failure now
returns the same shape.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
BerriAI#44462)

* fix(tests): match the OS bind error in the owned-proxy port-race retry

* test(integration): keep the port-race predicate pure so its unit tests stay in-process

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
…sources (BerriAI#44448)

* feat(anthropic): workload identity federation and pluggable identity sources

Backend half of BerriAI#38818 (internal copy of the fork PR BerriAI#38013), rebuilt as one
commit on top of litellm_internal_staging without the dashboard changes.

Deployments on anthropic/ without a static api_key can exchange an OIDC
workload assertion for a short-lived sk-ant-oat01 token through a shared
RFC 7523 JWT-bearer engine. The assertion comes from a mounted token file,
an env token, a LiteLLM-signed issuer, or Keycloak, chosen per deployment,
per named credential, or through ANTHROPIC_IDENTITY_SOURCE. The federation
fields are server-owned: refused inline in request bodies and on
POST /model/new, proxy-admin only on credentials, and the token exchange
is pinned to api.anthropic.com unless LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS
adds a host. GET /credentials/{name}/jwks exports the public key set of a
LiteLLM-signed credential for the Claude Console.

The OpenAI federation trio from BerriAI#39613 rides along on the backend side with
the same server-owned handling.

Fixes BerriAI#28607
Resolves LIT-6107

Co-authored-by: derhornspieler <15236687+derhornspieler@users.noreply.github.com>

* fix(anthropic): let batch-result downloads mint from deployment params and accept host:port allowlist entries

The files handler enabled workload identity on batch-result downloads but never received the
deployment's litellm_params, so a deployment authenticating through a named credential could only
mint from process-wide env vars. It now threads litellm_params through to the auth header the way
the batch retrieve path already does.

LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS entries written as host:port were read by urlsplit as a scheme,
so the allowlist kept the raw entry while the exchange compared bare hostnames and refused the
gateway. Entries are now parsed as network locations whether or not they carry a scheme.

* fix(types): move the WIF kwargs key sets to a leaf module so the kwargs funnel imports without a cycle

* test(anthropic): pin case-insensitive matching of WIF exchange-host allowlist entries

* fix(anthropic): end workload identity federation errors without a period so the router suffix reads cleanly

* fix(proxy): decrypt stored litellm_params before the WIF write gate

* fix(proxy): hide WIF secret references from /health output

* fix(proxy): keep the proxy error shape on credential endpoint refusals

* fix(proxy): hide identity token file paths from /health output

* fix(anthropic): rename the federation workspace param so Bedrock's anthropic_workspace_id keeps working

The Bedrock Claude Platform route already reads anthropic_workspace_id from
optional_params, so banning that spelling as a server-owned federation
parameter broke a pre-existing client capability. The federation field is now
anthropic_federation_workspace_id (env ANTHROPIC_FEDERATION_WORKSPACE_ID),
which restores the base branch's behavior for Bedrock callers, drops the
Bedrock-specific hint from the refusal message, and deletes the unconditional
ban constant that no longer had a reader

* fix(auth): share one exchanged token across workers reading the same assertion

Anthropic accepts each identity assertion exactly once, so two uvicorn
workers reading the same token file both minting from it means the second
exchange is denied with jti_reused. Minted tokens now land in a per-user
0700 cache directory guarded by a file lock, so workers on the same host
reuse one exchange until the token expires or the assertion rotates. A 401
is only retried when the re-read assertion actually differs, and the denial
hint explains jti_reused. LITELLM_TOKEN_EXCHANGE_CACHE_DIR moves the cache
and an empty value disables it

* fix: keep anthropic federation from being shadowed or leaked

An empty or whitespace-only ANTHROPIC_API_KEY counted as set, so a federated
deployment sent an empty x-api-key on every call instead of minting a token.
Blank values now read as unset, and a real static key on a federated deployment
logs once that it outranks federation and nothing is being federated.

The exchange-host allowlist matched hostnames only, so a second process on
another port of an allowed host was trusted with the workload's identity token.
An entry that names a port now trusts that port alone, while a bare host still
trusts every port.

The shared token store exists so the workers reading one projected token file do
not each spend its single-use jti. A source that mints its own assertion per
exchange shares nothing with another worker, so it no longer writes a live token
to disk for a lookup that can never hit.

* fix: unlink a staged token file a failed write leaves behind

The 401 denial hint now also says federation ignores ANTHROPIC_WORKSPACE_ID, which the Bedrock Claude platform provider already reads.

* refactor: move anthropic jwks derivation behind a provider-owned tagged union

* fix: unlink the staged token file when its write fails at close

A buffered write only reaches the disk when the handle closes, so a full disk surfaces at close and left the staging file behind holding a usable token.

* fix(anthropic): close the staging descriptor before writing the shared token file

* fix(wif): judge federation writes by what they set, not what is stored

The admin gate read the stored deployment, so a team admin lost edit, delete
and Test Connection on any deployment carrying federation params. It now
returns early unless the submitted fields touch the federation surface, and a
Test Connection probe that points the deployment at its own api_base is still
refused, with the 403 no longer wrapped into a 500

The rest of the same review pass: POST /model/new refuses only a blocking
value of `blocked`, so a client that always sends `blocked: false` is not
turned away; a request body can no longer pick which federated identity to
mint as by naming a stored credential; an advisory refresh the executor
refuses disarms the entry instead of wedging the identity until the follower
timeout; the static-key shadow warning resolves its env fallback inside the
cache instead of once per request; credential writes drop nulls before
storing them; the token exchange validates the endpoint URL before reading an
assertion and keeps refusing redirects across a client heal; /health hides
every server-owned federation field from non-admins; and the async create_file
and create_batch paths say which setting is missing when the provider resolves
no URL

* fix(proxy): let a deployment write name a federated credential

reject_federated_credential_reference runs from is_request_body_safe, which
pre_db_read_auth_checks calls on every route, so it also fired on POST
/model/new, /model/update, /model/{id}/update and /health/test_connection. A
proxy admin could no longer attach a federated credential to a deployment over
the API or the Admin UI, leaving a static config.yaml entry as the only way to
configure the feature the rejection told the caller to go configure, and
_reject_non_admin_wif_write never got to make the call it exists to make.

is_request_body_safe now takes the route and skips only the credential-reference
check on the routes that reach can_user_make_model_call. Federation fields typed
inline into a body stay refused everywhere, and a call naming a federated
credential still cannot pick the identity it mints as.

* refactor(proxy): derive health display policy from the federation key sets

The health check module hand-copied the five workload identity fields whose
value is a credential, so a shared proxy surface named provider-specific
parameters and a newly added secret-bearing field would have gone on being
displayed until someone remembered both places

WIF_SECRET_BEARING_KEYS now sits beside the key sets it splits out of,
types/utils derives secret_bearing_wif_litellm_params from it, and the health
layer splats that tuple the same way it already splats the admin-only one

* fix(anthropic_wif): treat blank identity-source fields as unset

* test(proxy): classify the federation params in the credential slot registry

main's registry test (BerriAI#43298) now fails the build for any credential-named
deployment param without a classification. The five federation fields that
carry a token, a token file path, or a signing or client secret reference are
Unplanted, matching WIF_SECRET_BEARING_KEYS; the four remaining Keycloak
settings name a URL, a client id, an auth method, or a scope and are NotSecret

* fix(anthropic_wif): declare federation params as owned connection leaves and chart their metrics

Register the 18 Anthropic and 3 OpenAI federation params as frozen
ConnectionSettings leaves so the owned-kwarg registry, the kwargs funnel
and the request-body ban list read one declaration. Pass the deployment
api_base through to the count-tokens handler instead of a pre-suffixed
URL, which doubled the /count_tokens path on main's prompt-cache
predictor. Add the five litellm_anthropic_wif_* families to the
all-metrics Grafana dashboard.

* fix(credentials): gate PATCH on WIF fields resolved from model_id

The credential PATCH handler checked server-owned workload identity
federation fields only on the values the caller sent, while a body that
named a deployment through model_id had its credential values resolved
after that check. A non-admin could therefore copy a federated
deployment's WIF fields onto an ordinary credential. Resolve the incoming
values first and run the non-admin gate on them, matching the POST path

* fix(anthropic): count tokens with ANTHROPIC_AUTH_TOKEN through the shared auth header

Count-tokens walked its own credential ladder: a static key, else skip minting when
ANTHROPIC_AUTH_TOKEN is set, else mint a federated token. With only the auth token set it
forwarded nothing and the proxy silently fell back to its local tokenizer while chat on the
same deployment authenticated with that token. The handler now takes the auth header that
AnthropicModelInfo.aget_auth_header resolves, the same ladder chat, files, batches and skills
use, and merges the oauth beta a minted or consumer token carries with the token-counting beta

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: derhornspieler <15236687+derhornspieler@users.noreply.github.com>
Co-authored-by: mateo-berri <happymvw@gmail.com>
… provider and shared YAML deployment (BerriAI#44451)

* test(integration): exact four-part translation cases on a shared fake provider and shared YAML deployment

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

* test(integration): compare every non-transport provider header and check for late provider requests at session end

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

* test(integration): name TranslationTestCase fields after litellm and provider sides and drop regressions

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

* test(integration): prefix checked TranslationTestCase fields with expected_ and name the fake reply mock_provider_response

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

* docs(integration): name TranslationTestCase fields in the translation README

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

* test(integration): add a claude-opus-5-5 base case and deployment next to claude-sonnet-4-6

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

* test(integration): name translation cases <MODEL>_TEST_CASE and document the naming rule

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

* docs(integration): move translation test rules into tests/integration/translation

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

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…AI#44465)

* feat(roi): show matched people by default in contributor lists

* fix(roi): keep matched filter tabs readable on narrow screens

* fix(roi): retain spend-only users and support older browsers
…erriAI#44444)

* feat(enterprise): bundle LiteAdmin Slack worker with native gateway login

* fix(enterprise): preserve gateway prefixes during native Slack linking

* fix(enterprise): retain native Slack linking on the admin backend

* fix(enterprise): reuse shared native Slack connection services
…nsus (BerriAI#43556)

* fix(caching): count tool_call cache_control marks in the injection census

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

* fix(caching): remove cache census casts

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

* fix(caching): only skip injection on message or content marks

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

* fix(caching): skip injection on messages whose tool calls carry marks

Reverts 9f08d8a. A default 5m mark injected on assistant text lands
before the client's 1h tool_use mark, which Anthropic rejects with a 400
because a 1h breakpoint must not follow a 5m one. Keeping the full census
in the skip check leaves the client's tool_call breakpoint as the only
one on that message.

* fix(caching): count every client tool_call cache mark in the breakpoint census

The census gated tool_call marks on type function and dict shape, so a client mark on a call without a type or with a string cache_control slipped past the count and injection overflowed the 4 breakpoint cap. Count any non-None tool_call mark, keep the server tool exclusion, and add integration cells for the capped surfaces, the yaml stand-down, Bedrock and Gemini, and router affinity

* test(integration): hold the upstream so the worker kill lands mid-burst

* refactor(caching): reuse the transform's server tool lookup in the breakpoint census

The census now calls the same helper the Anthropic transform uses to decide
whether a marked tool call becomes a server tool block, so the two cannot
drift apart. The owned-proxy burst test waits up to 90 seconds for the burst
to reach the wire before it kills a worker

* refactor(anthropic): move the server tool rebuild check under llms/anthropic

* test(integration): audit the tool call mark census across chat, messages, responses, and chaos

Adds the /audit cells for the breakpoint census on assistant tool_calls marks: the
Responses stream bridge, the OpenAI and Anthropic SDK clients, in-process Pydantic
messages, response cache twins, request-level points, a provider 401 on a capped request,
malformed provider_specific_fields and tool_call ids, null or empty points, a points
update mid-burst, a proxy restart mid-burst, and a worker SIGKILL that picks the worker
holding the burst's upstream connections

* test(integration): close the SDK clients the cache census cells open

---------

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>
…44466)

Price-Sync: litellm-providers

Co-authored-by: berriai-litellm-provider-info-sync[bot] <328147090+berriai-litellm-provider-info-sync[bot]@users.noreply.github.com>
…riAI#44467)

* fix(lens): pin worker dependencies and support approved image digests

* fix(lens): include locked dependencies and release identity in build context

* fix(lens): select the dev worker package for SHA-tagged charts
* feat: seed Lens dev with configurable load profiles

* feat: run Lens UI live through the dev launcher

* fix: verify Lens UI startup before seeding

* fix(ui): render run timestamps on one compact line

The agent runs table printed the long locale form with timezone, which
wrapped to two lines per row. Use a fixed-width 24h form with
milliseconds that matches the timeline axis, keep the long form in the
hover title, and show the timezone once in the column header

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(ui): use the root query client for the Lens demo

Drop the demo's nested QueryClient. Cache keys are already partitioned by
scope, and the root client now skips retries on 4xx ApiErrors, which covers
the demo's not-in-demo and read-only rejections and live 4xx alike.

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

* chore: drop unused synthetic_spend reference from query help

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

* chore(traces): drop the deeplite fixtures and map every SDK to a logo

The deeplite captures predate the example repository and carried
synthetic spend rows, which leaked a fixture-only column into the
query help SQL and pinned tests to its shape. Replace them with the
SDK captures in the ClickHouse round trip and query API tests, and
derive the seed tenant lookup from the capture metadata

The runs table only knew the two Anthropic framework slugs. Register
the slugs the normalizer emits for LangChain, LangGraph, Deep Agents,
CrewAI, Google ADK, LlamaIndex, OpenAI Agents, Pydantic AI, Strands,
Vercel AI SDK, Codex, Cursor and Copilot, and fall back to the generic
agent glyph when a run has no known framework

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(ui): inject the Lens sample through data sources instead of demo checks

Components no longer ask whether they are in the demo. The traces source
carries live and handoff, navigation state comes from a URL or memory
route, and the preview action comes from context instead of onDemo props

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

* feat(ui): infinite scroll for the agent runs list

Replace the Load more button with a sentinel that fetches the next
cursor page as the list nears its end. Placeholder rows hold the tail
while more runs exist, and a failed page stops auto-loading until Retry.

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

* feat(ui): keep the whole Lens view in the URL

Lens navigation now lives entirely in query params through nuqs: the
sample session (demo=true, with a Demo data switch in the header), the
open run (trace, trace_ref), the selected step, view and detail section
(span, view, span_tab) and the list filters and range (q, agent, status,
hours). Any Lens view is a shareable link and the back button walks runs

RunView takes its selection injected: the drawer feeds it URL state and
the investigations evidence sheet keeps a local one, so a finding's
original run never writes step ids into the URL. Leaving the sample
session clears every Lens key except the tab so sample ids never point
at live data

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* style(traces): cargo fmt captures tests

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

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…erriAI#44470)

* test(ui): wait for step search value to settle in TraceDrawer test

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

* test(ui): dispatch search typing and nav keys deterministically in TraceDrawer test

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

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…erriAI#44473)

* feat(ui): extract trace drawer into a shared SidePanel that closes on outside press

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

* feat(ui): center Lens mode switch in a notch joined to the content card

Larger Traces/Investigations switch, a subtle dot when an investigation is running or queued, and a bigger Lens title with a docs link.

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

* fix(ui): stronger Lens frame border and header spacing

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

* fix(ui): align Lens notch fillet with the notch border

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

* fix(ui): centered Lens loading/error states and proxy JSON calls in dev

The dev server answered GET /lens with the Lens page HTML because the UI route shadowed the proxy fallback rewrite. JSON API requests now go to the proxy before page routes, and a non-JSON success body raises a readable ApiError.

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

* refactor(ui): theme-scale type and shape in trace views, aligned pane bars

Add local/no-arbitrary-design-value, scoped to TraceView and Lens, banning
arbitrary font size, tracking, leading, radius, border and CSS property
values. Map the Figma-export values onto the theme scale and replace hex
colors with info/destructive tokens.

Add PaneBar, a fixed-height bordered row, and build the step tree and span
detail headers from it so their borders line up across the split.

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

* fix(ui): restore AgentTracesSection emptied in e7c3092

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

* feat(ui): move Set up tracing into the empty runs state

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

* refactor(ui): let the runs table gate the setup CTA on an empty range

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

* feat(ui): mark Lens demo mode with a blue toggle and frame instead of a banner

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

* fix(ui): draw Lens notch corners with CSS borders and thicken the demo frame

The SVG corner strokes did not snap to the same device pixels as the tab and
card borders, leaving a visible offset at the join.

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

* refactor(ui): drop the redundant Lens timeline header

The status, run counts, truncated agent legend and range span all repeated the Live toggle, runs table and range picker

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

* fix(ui): keep drawer shortcuts out of open menus and use usehooks-ts for timers and observers

SidePanel J/K/Esc now yields to menus and listboxes, not just dialogs.
The step tree shortcut footer wraps instead of clipping in narrow columns.
Replace hand-rolled timeout, keydown, media query and ResizeObserver effects
with usehooks-ts, and drop routine doc comments.

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

* fix(ui): offer tracing setup when filters hide every run

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

* feat(ui): replace Lens runs filters with a single ProseMirror query box

Agent and status dropdowns are gone. One query box (react-prosemirror) takes
free text plus key:value clauses (-key:value, key:*glob*) over name, agent,
status, model, input and trace_id, with field and value autocomplete. The
editor emits after a 150ms pause so typing no longer re-renders the runs view
per keystroke, and the URL keeps only q.

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

* fix(ui): keep the open trace on screen while the next one loads

Switching to an unvisited trace remounted the panel and flashed a loading skeleton.
The drawer now keeps the previous trace visible, dimmed and inert, until the new one arrives.

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

* feat(ui): virtualize the Lens runs table and drop its status footer

The footer's run count only tracked how many pages had loaded and
"Updated just now" never changed, so it carried no signal. The zoom
clear button moves onto the timeline. Rows now render through
@tanstack/react-virtual so scrolling deep into a range stays cheap

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

* refactor(ui): load traces with Suspense and show the previous run via useDeferredValue

Replaces keepPreviousData with the React pattern for showing stale content while fresh content loads.
Load failures go through an error boundary that retries the query on reset.

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

* fix(ui): open investigation details from list rows instead of the edit dialog

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

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4476)

* fix(lens): document working preview setup before coordinated releases

* docs(lens): keep current setup guidance factual and preserve Helm options

* fix(lens): support explicit worker images on Compose 2
* feat(sdk): add run_tool_loop and arun_tool_loop helpers

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

* chore(tests): allow-list bounded tool-loop recursion in recursive detector

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

* fix(sdk): harden run_tool_loop per review

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

* chore(deps): keep uv.lock at revision 3

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

* chore(tests): authorize typing-extensions PSF-2.0 license

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>
* feat(lens): guide setup through the first investigation

* fix(lens): restore the onboarding reference visuals

* fix(lens): compact onboarding and animate gateway flow

* feat(lens): refine onboarding motion and linked examples

* feat(lens): turn the LED swarm into organized dot groups

* fix(lens): make the LED dot flow visibly animate

* feat(lens): refine the swarm scale palette and motion

* fix(lens): preserve investigations during activity refresh errors
* test(ci): fix four CircleCI regressions on main

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

* test(ci): stop reloading auth_checks in unit tests

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

* test(constants): cover CLI JWT expiry env parsing

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

* test(gateway): restore proxy lifespan after importing gateway.main in launch tests

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

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ERROR shows them (BerriAI#44202)

* fix(proxy-extras): log v1 migration failures at ERROR so LITELLM_LOG=ERROR shows them

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

* fix(proxy-extras): reuse litellm secret redaction and mask configured DB passwords exactly

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

* refactor(proxy-extras): name the password alternation in _redact_credentials

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

* style(proxy-extras): wrap v1 migration ERROR lines at 120 columns

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

* test(proxy-extras): integration cells for v1 migration ERROR logging and password redaction

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

* test(proxy-extras): integration cells for component DB env vars, JSON logs, migration Job and v2 resolver

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

* test(proxy-extras): give every v1 migration integration cell the 900s timeout

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

* test(proxy-extras): bind the recovery forwarder before migrating so the retry cannot race it

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

* test(proxy-extras): drop the slow P3005 integration cell and bound migration subprocesses

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

* fix(proxy-extras): keep command repr and tolerate non-sequence cmd in migration error logs

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

* test(proxy-extras): double the subprocess boundary in the cmd=None retry test

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

---------

Co-authored-by: jesus <jesus@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng <yucheng@berri.ai>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked two other candidates this run and ruled them out: the migration-lock rolling-deploy boot race in litellm-proxy-extras/litellm_proxy_extras/migration_lock.py (held_migration_lock uses a non-blocking advisory-lock try that simply yields False on contention rather than blocking a new replica's boot), and a possible managed-file id mix-up in enterprise/litellm_enterprise/proxy/hooks/managed_files.py's _iter_provider_file_id_pairs (iterates rows independently, so a shared provider file id across two accessible rows isn't actually a cross-tenant leak path here).

Extended reasoning...

This run's diff spans CI/build config, Dockerfiles, Rust trace-ingestion/ClickHouse crates, proxy/lens/harness Python, and Prisma migrations; two new CONFIRMED findings (a Dockerfile ARG-override regression and an unbounded ancestor/descendant graph walk in trace resolution) are being posted inline. Given those unresolved findings and the long open history of prior unaddressed inline threads on this PR, approval is not appropriate; this note only adds the two additional candidates investigated and ruled out in this pass so the author knows they were checked.

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Still open from earlier reviews (3):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.


FROM $LITELLM_RUNTIME_IMAGE AS runtime
ARG PROXY_EXTRAS_SOURCE
FROM cgr.dev/chainguard/wolfi-base@sha256:1d95114038f76513a9ace6fca107d5582b08c65981f81f61cb56bf7fd2ef216d AS runtime

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Operators who build this Dockerfile with --build-arg LITELLM_RUNTIME_IMAGE=<custom-image> silently get the hardcoded chainguard wolfi-base digest instead, losing the override that worked on the base branch. The runtime stage's FROM $LITELLM_RUNTIME_IMAGE AS runtime was replaced with a literal FROM cgr.dev/chainguard/wolfi-base@ sha256:1d95114038f76513a9ace6fca107d5582b08c65981f81f61cb56bf7fd2ef216d AS runtime, while the ARG LITELLM_RUNTIME_IMAGE=... declaration at line 5 is kept, still implying it is configurable. Fix: keep the runtime stage's FROM referencing $LITELLM_RUNTIME_IMAGE (as deploy/lens/Dockerfile:16 still does), or drop the now-unused ARG so the Dockerfile doesn't advertise a build-time override it ignores.

Why this was flagged

Trigger: an operator builds docker/Dockerfile.non_root with --build-arg LITELLM_RUNTIME_IMAGE=<internal-mirror-or-pinned-digest> to swap the runtime base image, a documented-by-ARG capability that worked at the pre-diff commit (confirmed: base commit 4a3f30a line 139 was FROM $LITELLM_RUNTIME_IMAGE AS runtime). After this diff, line 125 hardcodes FROM cgr.dev/chainguard/wolfi-base@ sha256:1d95...ef216d AS runtime, and grep confirms LITELLM_RUNTIME_IMAGE is no longer referenced anywhere else in the file. The build arg is silently ignored: no warning, no error, the image is built from the hardcoded digest regardless of what the caller passed. Base behavior: the same build arg actually changed the runtime base image.

Verification: normal (regression). docker/Dockerfile.non_root line 125 is now FROM cgr.dev/chainguard/wolfi-base@ sha256:1d95114038f76513a9ace6fca107d5582b08c65981f81f61cb56bf7fd2ef216d AS runtime, replacing the base branch's FROM $LITELLM_RUNTIME_IMAGE AS runtime. The ARG LITELLM_RUNTIME_IMAGE=... at line 5 is retained but referenced nowhere else, so --build-arg LITELLM_RUNTIME_IMAGE=<custom> is now silently ignored.

Comment on lines +88 to +99
pub(super) fn call_requests(&self, call: usize) -> Option<Requests<'a>> {
let wrappers = self.graph.ancestors(call).into_iter().filter(|ancestor| {
self.kind(*ancestor) == ObservationType::Llm
&& self
.graph
.descendants(*ancestor)
.into_iter()
.all(|descendant| {
self.graph.id(descendant) == self.graph.id(call)
|| self.kind(descendant) != ObservationType::Llm
})
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Reading a trace with a long parent-child span chain can make a single get_trace/get_trace_page call burn CPU for a very long time, a cost the base branch (no such endpoint) never had. call_requests (resolution.rs:88-99) calls graph.ancestors(call), then for every ancestor returned calls graph.descendants(*ancestor); neither Graph::ancestors nor Graph::descendants (graph.rs:56-85) memoize, so each call re-walks the chain. Invoked once per LLM leaf span, this makes resolution cost grow roughly quadratically to cubically with chain depth, up to MAX_GRAPH_SPANS=100,000 (span_batches.rs:12). Fix: precompute ancestor/descendant relationships once per Resolution so cost stays near-linear regardless of chain shape, covering both call_requests and the similar per-row ancestors() call in resolved_type (resolution.rs:210-213).

Why this was flagged

Trigger: a trace with many spans linked in one long parent-child chain (e.g. a looping agent emitting one span per iteration), up to MAX_GRAPH_SPANS=100,000 (span_batches.rs:12), the only cap enforced before resolve_trace runs. get_trace/get_trace_page (traces-clickhouse/src/reads.rs:293,355) call resolve_trace -> Resolution::new -> call_requests (resolution.rs:88-99): it computes graph.ancestors(call) then, for every ancestor, graph.descendants(*ancestor) (resolution.rs:90-93). Neither Graph::ancestors nor Graph::descendants (graph.rs:56-85) cache results. For chain depth D≈N this makes one call_requests cost O(N) ancestors x O(N) descendants-per-ancestor, invoked once per LLM leaf span, so total resolution cost approaches O(N^3). This synchronous CPU work runs inside the async handler with no yield point, stalling the worker thread servicing it.

Verification: graph.rs:56-65 (ancestors) and graph.rs:67-85 (descendants) each allocate a fresh HashSet+Vec keyed on string span_ids and re-walk from scratch, with no memoization. resolution.rs:88-99 call_requests computes ancestors(call), then calls descendants(*ancestor) for every returned ancestor, making ancestor-list cost O(N) and per-ancestor subtree walks sum to O(N²).

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I checked three other candidates raised this run and ruled them out: the liteadmin-builder Dockerfile stage's apk add without an explicit USER root (the wolfi-base image at that digest runs as root by default there, consistent with other stages' USER root being defensive rather than required, and the build succeeds in CI); the two new spend-logs index migrations (idempotent SQL, no conflict with already-applied migrations); and the auto-router daily-spend cleanup's date-string cutoff (handled by the existing Cutoff/_cutoff_cast split already used for other tables, not a type mismatch).

Extended reasoning...

This is a very large, long-running PR (15+ pushed versions) spanning Rust tracing/ClickHouse crates, proxy DB migrations, Docker/CI, and a new litellm.harness sandbox feature, with security-sensitive surface (ClickHouse RBAC provisioning, OTLP ingestion limits, secret handling in sandbox env) already flagged in prior rounds. This run surfaced two new CONFIRMED bugs (silent ClickHouse-tracing disablement on upgrade, and a Python 3.9 crash in the new migration-index default) that will post as inline comments. Numerous prior findings across many rounds remain only author-resolved (not independent sign-off), so an outstanding-objections concern persists; decision is defer, not approve.

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Still open from earlier reviews (5):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread litellm/tracing/config.py
Comment on lines +13 to +20
def is_clickhouse_tracing_enabled(settings: object) -> bool:
if not isinstance(settings, Mapping):
return False
typed_settings: Final = STORE_SETTINGS.validate_python(settings)
store: Final = typed_settings.get("store")
if not isinstance(store, Mapping):
return False
return STORE_SETTINGS.validate_python(store).get("type") == "clickhouse"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Operators who already enabled ClickHouse agent tracing with the previously-shipped config general_settings.tracing.store: clickhouse (a plain string, exactly what base docker/tracing-config.yaml had) get tracing silently disabled after upgrading, with no error or log line. is_clickhouse_tracing_enabled (config.py:13-20) now requires store to be a Mapping with type: clickhouse; a plain string fails isinstance(store, Mapping) and returns False. proxy_server.py:1589 feeds that into manage_tracing(enabled=False, ...) which just yields None (tracing_runtime.py:52-54) with no warning logged either place. POST /v1/traces then 503s with "Agent tracing is not enabled" (tracing_endpoints.py:38), which is misleading since tracing: is in fact set. …

Why this was flagged

…Fix: when store is a non-mapping truthy value (old string format), log a clear deprecation/error instead of silently treating it as disabled, or accept the legacy string form for backward compatibility.

Trigger: an existing proxy config has general_settings.tracing.store: clickhouse (string), the exact format base commit 4a3f30a shipped in docker/tracing-config.yaml, reached via ProxyStartupEvent at proxy_server.py:1583-1589. Bug: is_clickhouse_tracing_enabled (litellm/tracing/config.py:13-20) returns False for a non-Mapping store, so tracing_enabled is False. manage_tracing (litellm/proxy/tracing_runtime.py:46-54) takes the enabled=False branch, yields None, and logs nothing (the info log at line 64 is only on the enabled path). On base, general_settings.get("tracing") being merely truthy was enough to start TraceReceiver.from_env() (base proxy_server.py:11412-11429), so tracing worked. Now POST/GET /v1/traces returns 503 "Agent tracing is not enabled" (tracing_endpoints.py:38) with zero diagnostic indicating the config format changed.

Verification: normal (regression against base). The base commit 4a3f30a shipped general_settings.tracing.store: clickhouse as a plain string in docker/tracing-config.yaml, and base init_tracing enabled tracing precisely on that string: if not isinstance(settings, dict) or settings.get("store") != "clickhouse": return (base proxy_server.py:11413-11415), then logged "Agent tracing enabled (store=clickhouse)".

Comment on lines +1396 to +1400
def run_migration_job(
use_migrate: bool = False,
use_v2_resolver: bool = False,
setup: Callable[[bool, bool], bool] = setup_database,
build: Callable[[], bool] = build_request_log_indexes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Users running litellm-proxy-extras on Python 3.9 (the floor declared by requires-python = ">=3.9" in litellm-proxy-extras/pyproject.toml) get a crash in the new migration-index feature instead of it working. run_migration_job's defaults setup=setup_database and build=build_request_log_indexes (utils.py:1399-1400), and start_request_log_index_build's build=build_request_log_indexes (utils.py:1408), bind the bare class-body name to the raw staticmethod descriptor, not the function, since staticmethod only became directly callable in Python 3.10. Fix: make every sibling-staticmethod default a plain callable at all 3 sites, so run_migration_job() (migrations/run.py:61) and start_request_log_index_build() (prisma_client.py:1021) don't raise TypeError: 'staticmethod' object is not callable.

Why this was flagged

Trigger: litellm-proxy-extras installed and imported under CPython 3.9 (explicitly supported per pyproject.toml), then migrations/run.py:61 calls ProxyExtrasDBManager.run_migration_job(use_migrate=..., use_v2_resolver=...) with no setup/build override, or litellm/proxy/db/prisma_client.py:1021 calls ProxyExtrasDBManager.start_request_log_index_build() with no override. Both rely on utils.py:1399/1400/1408 defaults setup_database/build_request_log_indexes, which at class-body-execution time are staticmethod wrapper objects, not plain functions, since bare-name lookup in a class body bypasses get. Calling setup(...) or build() inside the function body then raises TypeError: 'staticmethod' object is not callable — a hard failure of the migration job's main entrypoint and the proxy's startup index-build thread; no test in this diff exercises the zero-argument call path.

Verification: setup_database (utils.py:1347-1348) and build_request_log_indexes (1380-1381) are @ staticmethod, so the names resolve to staticmethod descriptors in the class namespace.

Comment on lines +36 to +44
#[serde(untagged)]
pub enum TracingStoreSettings {
ClickHouse(ClickHouseStoreSettings),
}

#[derive(Clone, Default, Debug, Deserialize)]
#[serde(default)]
pub struct TracingSettings {
pub store: Option<TracingStoreSettings>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Operators upgrading the litellm-rust gateway with their existing general_settings.tracing.store: clickhouse config (a plain string, the exact format docker/tracing-config.yaml shipped pre-diff) now get the whole gateway process crash at startup instead of just tracing being affected. TracingStoreSettings (settings.rs:36-38) is an untagged enum with one variant, ClickHouseStoreSettings, which requires a mapping; a bare string fails to deserialize, confirmed by the new test tracing_settings_reject_string_store. Config::load (lib.rs:90-92) returns that error, and gateway/src/main.rs:47 does Config::load(settings.litellm_config)?, so parsing the whole config.yaml fails and the binary exits non-zero. Fix: do not let an unrecognized/legacy tracing.store shape fail the entire config load — accept the legacy string form or fall back to tracing: None with a logged warning.

Why this was flagged

Trigger: an operator runs the litellm-rust gateway against a config.yaml containing general_settings:\n tracing:\n store: clickhouse — the exact format this PR's own docker/tracing-config.yaml shipped before being rewritten to the object form. Before this diff, GeneralSettings had no tracing field at all, so that key landed in the catch-all additional_fields flatten bag and parsing always succeeded. After this diff, tracing: Option<TracingSettings> is strongly typed (settings.rs:58), and TracingStoreSettings (settings.rs:36-38) is #[serde(untagged)] with only a ClickHouse(ClickHouseStoreSettings) mapping variant, so a bare string fails deserialization — proven by the new test tracing_settings_reject_string_store. Config::load (lib.rs:90) propagates that Err, and gateway/src/main.rs:47 does Config::load(settings.litellm_config)?, so main() returns Err and the process exits immediately: the whole gateway never starts, not merely tracing being disabled.

Verification: normal — this change turns a booting gateway into a startup crash for the config format the repo itself shipped in the base branch. Base GeneralSettings had no tracing field, so store: clickhouse parsed via the flatten catch-all. After the diff, TracingStoreSettings (settings.rs:36-39), an untagged enum requiring a mapping, rejects the bare string, and gateway/src/main.rs:47 Config::load(...)? propagates that error, crashing the whole gateway at startup.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Still open from earlier reviews (7):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 litellm/proxy/_lazy_features.py:379 — pre-existing (security-relevant, in code this diff substantially rewrites): on the new split gateway/backend deployment, any single request for a path owned by a lazy feature that the split excludes (e.g. /guardrails/apply on the gateway, a backend-only admin surface per gateway/routes/allowlist.py) makes LazyFeatureMiddleware.call (_lazy_features.py:358-379) import and app.include_router() that feature's full router at runtime via _register_feature (line 484), undoing the one-time route trim gateway/main.py:69 / backend/main.py:58 already applied, and then serving the request. …

    Why this was flagged

    …Fix: have the middleware (or register_fn) check the active component's allowlist (gateway/routes/allowlist.py or backend/routes/allowlist.py) before registering, so lazy-loading can never reintroduce a route the split excluded.

    Trigger: an operator runs the split deployment (gateway.main:app / backend.main:app, new Dockerfiles/helm in this diff) and a caller sends any request whose path matches a LAZY_FEATURES prefix the other component owns, e.g. GET /guardrails/apply on the gateway. LazyFeatureMiddleware.call (_lazy_features.py:358-379) matches on LAZY_FEATURES only, not on GATEWAY_PATH_PREFIXES/BACKEND_PATH_PREFIXES, so it calls _force_load -> _register_feature (line 445-500) which app.include_router()s the admin router into the live gateway app, reversing gateway/main.py:69's startup-time route trim. The request is then dispatched to and served by that now-registered admin router on the gateway. _eager_route_wins (line ~413) does not stop this: the reserved anchor route was itself removed by the trim, so it treats every remaining route as 'ahead' and still proceeds to load.

    Verification: Severity: pre-existing (real and reachable, but the base branch fails identically by the same route — does not block).

    The behavior is real. proxy_server.py:20141 calls attach_lazy_features(app) on the single shared app object that both gateway/main.py and backend/main.py import (from litellm.proxy.proxy_server import app). With lazy routes enabled (default), that attaches LazyFeatureMiddleware to the ASGI stack.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline finding on deploy/lens/compose.yaml, I checked two other candidates raised this run. The new spend_logs index migrations (20260823000000 and 20260831120001) are deliberate no-op placeholders (SELECT 1) — actual index creation is deferred to request_log_indexes.py, so neither fails prisma migrate deploy for already-upgraded operators. And the baseline-accounting "missing spend log" path (litellm/proxy/db/baseline_accounting.py:580-587) returns "unavailable" and retries on the next flush rather than permanently dropping savings, so that understatement is transient, not permanent.

Extended reasoning...

This run's two ruled-out candidates were verified directly against the code: the migration SQL files are confirmed no-op placeholders with index building moved to request_log_indexes.py, and baseline_accounting.py's project() method explicitly returns "unavailable" on a missing spend log and relies on flush_baseline_accounting's retry-on-exception comment to re-attempt later, rather than silently and permanently under-crediting savings. These are informational only; the large outstanding thread of prior unresolved/author-only-resolved objections on this PR already means a human look remains warranted regardless.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (8):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread deploy/lens/compose.yaml
services:
lens-worker:
image: ${LENS_WORKER_IMAGE:-ghcr.io/berriai/litellm-lens-worker@sha256:40fdb82113dd4474cb6e833cf28552487d87c8baf61693a1c3fc2863b7968c6a}
image: ${LENS_WORKER_IMAGE:-${LITELLM_VERSION:+ghcr.io/berriai/litellm-lens-worker:v}${LITELLM_VERSION:-}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): operators who run docker compose -f deploy/lens/compose.yaml up without LENS_WORKER_IMAGE or LITELLM_VERSION set get a bare Docker "invalid reference format" error instead of the working pinned-digest image the base branch shipped. image: ${LENS_WORKER_IMAGE:-${LITELLM_VERSION:+ghcr.io/berriai/litellm-lens-worker:v}${LITELLM_VERSION:-}} resolves to an empty string when both vars are unset, unlike LITELLM_URL/LENS_WORKER_TOKEN on lines 5-6 which use the :? form with a clear message. Fix: give the image field the same :?Set LENS_WORKER_IMAGE... required-variable treatment (or restore a real default) so the failure matches the file's own convention instead of silently producing an empty image reference.

Why this was flagged

Trigger: a user downloads deploy/lens/compose.yaml directly and runs docker compose up without exporting LENS_WORKER_IMAGE or LITELLM_VERSION (e.g. missed the README step at deploy/lens/README.md requiring 'an explicit LENS_WORKER_IMAGE'). compose.yaml:3's default ${LITELLM_VERSION:+...}${LITELLM_VERSION:-} evaluates to '' when LITELLM_VERSION is unset, so image: "". Base branch had a working fallback: a pinned digest ghcr.io/berriai/litellm-lens-worker@ sha256:40fdb8.... Now the service fails with a cryptic 'invalid reference format' from Docker instead of either working or a clear ':?'-style required-var message, which lines 5-6 in the same file use for LITELLM_URL/LENS_WORKER_TOKEN.

Verification: On deploy/lens/compose.yaml:3, when both LENS_WORKER_IMAGE and LITELLM_VERSION are unset, the default expands to "", so image is empty and docker compose up fails with a bare "invalid reference format" error. This is inconsistent with lines 5-6, which use the :? form with explicit operator messages. The diff confirms the base shipped a concrete digest fallback that was removed.

@shudonglin
shudonglin merged commit 988536a into litellm_internal_staging Oct 4, 2026
138 checks passed
@shudonglin
shudonglin deleted the chore/sync-upstream-2026-10-03 branch October 4, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.