Commit graph

42855 commits

Author SHA1 Message Date
Yuneng Jiang
dfdafbf89b
fix(ui): keep the tools panel mounted so a tool's expanded detail survives
antd's Collapse kept the panel mounted once opened, so a tool a user had
expanded stayed expanded after closing and reopening Tools. Base UI renders
only the open branch, so the migration silently reset every ToolItem.

The regression test passes against the antd original, fails against the
migration without keepMounted, and passes with it.
2026-08-13 12:50:56 -07:00
Yuneng Jiang
b4092f476f
test(ui): find the section copy button by role instead of the antd icon
InputCard and OutputCard located SectionHeader's copy button by querying for a
descendant with aria-label="copy", which is the antd CopyOutlined icon. That
selector reaches into SectionHeader's internals, so migrating it off antd left
copyButton undefined and failed four tests.

getByRole("button", { name: /copy/i }) is green against both the antd and the
shadcn SectionHeader, verified by running these two files against each.
2026-08-13 12:47:45 -07:00
Yuneng Jiang
bc9005c149
refactor(ui): migrate SectionHeader and ToolsSection to shadcn 2026-08-13 10:04:38 -07:00
Yuneng Jiang
eb23dc2e81
test(ui): pin SectionHeader and ToolsSection behaviour before migration 2026-08-13 09:37:43 -07:00
yuneng-jiang
9d069f21dc
refactor(ui): migrate guardrails content tables to shared DataTable (#36708)
* test(ui): characterize guardrails content tables

* refactor(ui): migrate guardrails content tables to shared DataTable

* test(ui): verify shared guardrails table integration
2026-08-13 09:21:00 -07:00
yuneng-jiang
b8241d6664
refactor(ui): migrate guardrails monitor table to shared DataTable (#36709)
* test(ui): characterize guardrails monitor table

* refactor(ui): migrate guardrails monitor table to shared DataTable
2026-08-13 09:20:48 -07:00
yuneng-jiang
bfa34e2b10
refactor(ui): migrate usage tables to shared DataTable (#36707)
* test(ui): decouple usage table test from antd

* refactor(ui): migrate usage tables to shared DataTable

* test(ui): preserve data utility exports in usage tests
2026-08-13 09:20:45 -07:00
yucheng-berri
09889e1986
fix(langfuse): coerce header-sourced mask and trace-update steering values (#36740)
langfuse_* request headers land in metadata as strings, but the trace path reads
mask_input/mask_output with a bare truthiness check and iterates update_trace_keys
directly. A header saying mask_input: false redacted the payload it was asked to
keep, and update_trace_keys was walked one character at a time so every requested
key silently failed to match
2026-08-13 00:44:26 -07:00
yuneng-jiang
a7397b2459
fix(helm): render nodeSelector on the migrations job (#36747)
The template rendered affinity and tolerations but never nodeSelector, so a
values file that pinned the chart to a node pool got the gateway and every
subchart placed correctly while the migration Job silently fell through to
whatever the cluster's default pool was.

That is worse than an outright failure. On EKS Auto Mode the default pool hands
out 3 GiB nodes and the migration container needs roughly 3.6 GB, so the Job
was OOM-killed on a pool it was never meant to run on, while the values file
that would have placed it on a large enough node looked correct.

The new test fails against the old template with "unknown path
spec.template.spec.nodeSelector".
2026-08-13 00:41:36 -07:00
yucheng-berri
6f84c468d4
fix(guardrails): scan and re-emit raw Anthropic SSE streams in the bedrock post-call hook (#36598)
* fix(guardrails): scan and re-emit raw Anthropic SSE streams in the bedrock post-call hook

* fix(guardrails): keep upstream id and model on a blocked Anthropic stream

* fix(guardrails): deliver a blocked Anthropic stream as an error frame

* fix(guardrails): deliver an unscannable Anthropic stream as an error frame

* fix(guardrails): emit the guardrail block detail as JSON in the stream error frame

* fix(guardrails): deliver an Anthropic block through the shared block-SSE builder

* fix(guardrails): keep the shared SSE assembler behavior-identical for existing callers

* fix(guardrails): keep the stream error message a string and drop an unreachable branch

* chore(guardrails): drop a comment that repeated its own docstring

* fix(guardrails): let bedrock service failures keep their status instead of framing them as blocks

* fix(guardrails): key the streamed block decision on status, not detail shape

InvokeGuardrailChecks details a Mapping on its 500 for an unparseable response,
so a detail-shape test read that outage as a policy block and framed it as a 200
guardrail_error. Both block sites raise 400, so gate on the status too.

* refactor(guardrails): narrow the SSE error-frame helper to the input it actually takes

Both callers pass a string, so the Mapping overload and its json.dumps branch
were unreachable. Folds the block branch's narrative comment into the rebind
suppressions that already carry a reason.
2026-08-13 07:23:49 +00:00
yuneng-jiang
3d76dfc72e
refactor(ui): migrate search and user controls to shadcn (#36694)
* test(ui): characterize shared migration surfaces

* refactor(ui): migrate search and user controls

* fix(ui): restore search tool clear action
2026-08-13 06:43:30 +00:00
yuneng-jiang
fd00b98f64
refactor(ui): migrate guardrails-monitor, projects, logs to shadcn (#34606)
* test(ui): pin behaviour of guardrails-monitor, projects and logs components before migration

Adds role- and text-based characterisation tests for EvaluationSettingsModal,
GuardrailDetail and AuditLogDrawer, which had none, and moves the remaining
antd-specific assertions (.ant-spin, the icon role of an antd Spin indicator)
onto library-neutral ARIA queries. Also covers the enterprise banner on the
deleted keys and deleted teams pages, which no test reached.

All of these pass against the current antd and Tremor components.

* refactor(ui): migrate guardrails-monitor, projects and logs to shadcn

Replaces antd and Tremor with installed shadcn primitives across the files
these three routes exclusively own. Markup only, except where noted below.

Deletes AntDLoadingSpinner, an antd-only primitive living in the shadcn ui/
folder, and moves its single call site onto ui/ui-loading-spinner.

Two behaviour notes. The logs tab handler previously mapped every tab past
the first to "audit logs", so the audit panel kept polling while Deleted
Keys or Deleted Teams was on screen; each tab now reports its own value and
panels stay mounted via keepMounted. The evaluation settings dialog is
bounded to the viewport and scrolls internally, which the antd Modal got
from being top-anchored on a scrolling page.

The tests added in the previous commit pass unedited against these
components.
2026-08-13 06:43:12 +00:00
yuneng-jiang
4df421e058
refactor(ui): migrate guardrail and duration controls to shadcn (#36693)
* test(ui): characterize shared migration surfaces

* refactor(ui): migrate guardrail and duration controls

* fix(ui): preserve duration select callback shape

* fix(ui): narrow duration selection value
2026-08-13 06:42:42 +00:00
yuneng-jiang
e619106306
refactor(ui): migrate team detail controls to shadcn (#36695)
* test(ui): characterize shared migration surfaces

* refactor(ui): migrate team detail controls
2026-08-13 06:41:28 +00:00
Mateo Wang
73e555a3e3
test(interactions): follow Google spec drift replacing Turn with typed steps (#36730)
* test(interactions): follow Google spec drift replacing Turn with typed steps

* test(interactions): send step and content-list input to the live Gemini API
2026-08-12 23:32:57 -07:00
yucheng-berri
d86336a7c6
fix(langfuse): emit otel trace version and release on the keys langfuse v4 reads (#36702)
* fix(langfuse): emit otel trace version and release on the keys langfuse v4 reads

The langfuse_otel exporter wrote version to langfuse.generation.version and
langfuse.trace.version, and release to langfuse.trace.release. Langfuse v4
recognizes neither, so both landed in the generic span attribute bag and every
trace reported version and release as null. v4 has a single langfuse.version
key, lifted to the trace when it sits on the root span, plus langfuse.release.

Also routes the otel v2 preset's per-request headers through the shared builder
so key-scoped and team-scoped exports carry x-langfuse-ingestion-version like
the other three exporter paths already do.

* fix(langfuse): give trace_version precedence over version on the shared v4 key

Matches the documented contract in docs/observability/langfuse_integration.md
and the legacy langfuse SDK callback, which both treat trace_version as the
authoritative trace version with version as its fallback.
2026-08-12 20:39:58 -07:00
Mateo Wang
c7f5527870
Merge pull request #36726 from BerriAI/litellm_claude_md_remove_at_prefix
docs(claude): drop the @ prefix from the PR template path
2026-08-12 20:31:59 -07:00
mateo
de4765ae88 docs(claude): drop the @ prefix from the PR template path
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-13 03:20:15 +00:00
devin-ai-integration[bot]
3864e12415
fix(spend): stop losing spend log rows when a flush is cancelled (#34826) 2026-08-12 20:10:50 -07:00
Mateo Wang
fdd72b5b23
Merge pull request #36627 from BerriAI/litellm_fix_autorouter_untagged_hijack
Some checks failed
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Proxy Legacy Tests / auth-and-jwt (push) Waiting to run
Unit Tests: Proxy Legacy Tests / key-generation (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-config (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-response-and-misc (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server-extras (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-token-counter (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-user-auth-and-spend (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-utils (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
fix(router): let untagged requests bypass a tagged pre-routing strategy on shared model names
2026-08-12 19:48:00 -07:00
Mateo Wang
6038dc6891
Merge pull request #36664 from BerriAI/litellm_groq_model_registry_audit
fix(model_prices): sync the Groq registry with Groq's docs
2026-08-12 19:37:39 -07:00
Mateo Wang
6209b8928d
Merge pull request #36698 from BerriAI/litellm_bedrock_gpt56_long_context
fix(bedrock_mantle): 1M context window and long-context pricing for GPT-5.6 Sol/Terra/Luna
2026-08-12 18:49:45 -07:00
mateo-berri
feff5ae34c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_autorouter_untagged_hijack
# Conflicts:
#	litellm/router.py
2026-08-13 00:47:52 +00:00
mateo-berri
ce3d30a434 fix(router): read no tags, instead of raising, from a non-dict metadata bucket
Pre-routing now reads the request's tags on every request with a registered
strategy, including the single-strategy case that used to short-circuit before
looking at tags. Metadata is request-controlled, so a caller that sends
`litellm_metadata` (or `tags`) as a string or any other non-dict shape crashed
tag lookup with an AttributeError instead of routing untagged.
2026-08-13 00:45:49 +00:00
Mateo Wang
3b5317c40d
Merge pull request #36626 from BerriAI/litellm_fix_autorouter_alias_forwarding
fix(router): forward auto-router alias params from the marker entry, not the first same-name deployment
2026-08-12 16:55:12 -07:00
Yassin Kortam
50ff555d79
fix(ui): show and edit key-level router settings on a virtual key (#36674)
The virtual-key edit view never rendered the router settings stored on a
key, so fallbacks configured at creation could not be verified or changed
afterwards. The key info panel now summarises them and the edit view
embeds the router settings accordion.

The accordion is a fixed-field editor, so its value is merged over the
stored object instead of replacing it. Routing fields it cannot render,
such as tag_routing_prefix or model_group_retry_policy, survive an
unrelated edit, while a field it does own that the admin emptied still
goes out as null so the clear reaches the server. Emptying every field
sends {}, which the proxy reads as no key-level override, so the key
falls back to its team and global routing rather than being pinned to a
blob of nulls.
2026-08-12 16:47:45 -07:00
mateo
9f1129ae19 Merge branch 'litellm_internal_staging' into litellm_fix_autorouter_untagged_hijack
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 22:57:01 +00:00
mateo
b55e6cb2f7 Merge branch 'litellm_internal_staging' into litellm_fix_autorouter_alias_forwarding
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 22:55:55 +00:00
Mateo Wang
964f0755ee
Merge pull request #36495 from BerriAI/litellm_no_redis_warning_banner 2026-08-12 15:55:47 -07:00
Yassin Kortam
5bc523a09f
feat(terraform/aws): make VPC, Aurora, and Redis optional (#36676)
Adds vpc_id/public_subnet_ids/private_subnet_ids to deploy into existing
networking, plus create_database/database_url and create_redis/redis_url to
use existing data stores or none at all. Defaults keep today's module-owned
behavior.

A container reads a secret by ARN, which gives Terraform no edge to the
_version that writes the value. The managed-Aurora path hid that behind the
cluster create; the bring-your-own path has nothing slow in between, so the
migration, the bootstrap, and both services now depend on the versions
explicitly.

Supplied private subnets must cover two AZs whenever Aurora or ElastiCache is
module-created, since both subnet groups require it, and a check block warns
when a Redis-less stack can run more than one gateway process, because per-key
rate limits are counted per process without Redis.
2026-08-12 15:27:13 -07:00
devin-ai-integration[bot]
e52ae039f2
feat(model_prices): add NVIDIA Nemotron 3.5 Lightning on OpenRouter and DeepInfra (#36696)
* feat(model_prices): add NVIDIA Nemotron 3.5 Lightning on OpenRouter and DeepInfra

Nemotron 3.5 Lightning shipped 2026-08-11 with public per-token pricing on
OpenRouter and DeepInfra at $0.05/M in and $0.20/M out. Without cost map
entries both ids raise "This model isn't mapped yet" and log at zero spend.

* fix(model_prices): stop asserting an output cap for Nemotron 3.5 Lightning

262144 is the native context window, not the output budget, and neither
OpenRouter nor DeepInfra publishes an output cap. Keeps max_input_tokens at the
native 256K window: 1M needs VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 on a self-hosted
deployment, so it is not what these hosted endpoints serve.

* chore(tests): drop the Nemotron 3.5 Lightning metadata test

Requested on the review thread: the cost map entries stand on their own.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 21:48:02 +00:00
mateo
b6cdb27f44 test(bedrock_mantle): pin GPT-5.6 1M context window and long-context rates
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 21:32:06 +00:00
tin-berri
1911269ddf
fix(router): never price a strategy-router alias (#36691)
* fix: never price a strategy-router alias

A strategy-router alias (auto_router/complexity_router/<name>) is never the
deployment that gets called or billed, but custom pricing configured on it was
being treated as real pricing in two places:

- registered in litellm.model_cost under the alias deployment id, so an
  explicit zero made _is_cost_explicitly_configured() report the group as a
  genuinely free model and every budget check was skipped, while the request
  routed to a paid deployment and accrued real spend
- copied onto request_kwargs by the alias-params merge, so the routed
  deployment got re-registered at the alias price and the request billed 0.0

Both are fixed at the writer, so config, /model/new and price-map reload all
take the same path

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

* chore: annotate filtered cost-map copy for the mutable-collection gate

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 14:26:30 -07:00
mateo
ef305fe4ab fix(bedrock_mantle): 1M context window and long-context pricing for GPT-5.6 Sol/Terra/Luna
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-12 21:20:10 +00:00
Mateo Wang
c642e8400d
Merge pull request #36465 from BerriAI/litellm_e2e_claude_code_cron_vm 2026-08-12 14:17:48 -07:00
yuneng-jiang
32535987e8
fix(proxy): serialize model reconciles so concurrent model writes stop evicting each other (#36687)
* fix(proxy): serialize model reconciles so concurrent writes stop evicting each other

A model write is a read-modify-write of the shared `llm_router` global: read the
db into a snapshot, then make the router match that snapshot. Nothing serialized
it, so two of them interleaving was not a lost update but an eviction --
_delete_deployment removes every live deployment absent from the snapshot it was
handed, so the request holding the older snapshot reconciles the newer request's
model straight back out of the router. The row survives in the db, which is what
makes it easy to miss: the pod simply stops serving a model it was told to serve
until some later reload happens to put it back.

clear_cache compounds it. It deletes every db model from the router before
reloading them, so for the width of that reload the pod serves none of them --
and any concurrent write sampling the router in that window sees the hole.

Fix is one lock (MODEL_RECONCILE_LOCK) held across both, so each reconcile reads
the db and applies it atomically and no stale snapshot can evict a newer model.
clear_cache holds it across wipe+reload and calls the already-locked
_add_deployment_locked, since asyncio.Lock is not reentrant and routing back
through the public add_deployment would deadlock the pod's whole model-write
path.

The verdict needed the same treatment. raise_if_reload_degraded_serving compared
a desired-set read during the reload against a router snapshot taken after it,
so a neighbouring reconcile's in-flight wipe was reported to the caller as
collateral damage from its own reload -- a 500 on a create that had in fact
succeeded. Reconciles now return a ReconcileOutcome carrying both the desired set
and the post-reconcile serving state, captured before the lock is released, and
the verdict judges against that. Omitting live_after keeps the old live re-read,
which stays correct for the no-reconcile-ran case.

Found by running the e2e suite with pytest-xdist at 8 workers: three unrelated
tests failed together on "Previously served model id(s) [...] are also no longer
being served by this pod", which is this. Serial runs concurrent enough to hit it
are rare, which is why 78 minutes of sequential e2e never surfaced it -- but any
customer provisioning models in parallel (terraform, CI) is in exactly this race.

test_reconciles_serialize_so_no_stale_snapshot_can_evict fails with 5 == 1
without the lock.

* fix(tests): return a ReconcileOutcome from the PTU test's add_deployment mock

test_ptu_model_settings.py stubs proxy_config.add_deployment with
AsyncMock(return_value=None). Now that add_deployment returns a
ReconcileOutcome, add_new_model reads .still_desired off that None and
the two PTU gate tests fail with "'NoneType' object has no attribute
'still_desired'".

Return ReconcileOutcome(still_desired=None, live_after=None), matching
the other reconcile mocks. Both fields None means no reconcile state was
captured, so the serving verdict falls back to reading the router live,
which is what the test's mock_router already drives — the PTU assertions
are unchanged.

Two sibling test files were updated for this in the parent commit; this
one was missed because the local env cannot collect four modules under
tests/test_litellm/proxy (prisma generate artifacts), so the full shard
only ran in CI.

Also applies ruff format to proxy_server.py: the new add_deployment
wrapper's single call fits on one line under the project's line length.

* fix(proxy): lock the delete evictions and stop clear_cache wiping deployments

Two follow-ups to MODEL_RECONCILE_LOCK, both found by review.

1. delete_model and delete_team_models evict from llm_router directly,
   outside the lock. The db row is gone by then, but a reconcile that
   snapshotted the db BEFORE the delete still lists that id as desired and
   upserts the deployment straight back, so the pod keeps serving a model
   the database no longer has until some later reconcile notices. Taking
   the lock orders the eviction after any in-flight reconcile's re-add.
   Both new tests fail without the lock ("did not wait for
   MODEL_RECONCILE_LOCK") and pass with it.

2. clear_cache no longer wipes deployments. It used to delete_deployment()
   every db model before the reload restored them, which left the router
   serving ZERO db models for the entire width of the reload -- every
   inference request landing in that window fell into a real hole, and
   serializing reconciles made the aggregate outage additive rather than
   overlapping. The wipe was also redundant: _delete_deployment evicts
   exactly the ids the db no longer lists, and upsert_deployment
   pops-and-re-adds a deployment whose params changed while no-opping one
   that did not, so the reconcile converges to the same state on its own.
   Every mutation is visible to that comparison (blocked, and updated_at
   for premium, are written into model_info).

   The auto-router pops are NOT redundant and stay: they are keyed by
   model_name, which no deployment-id reconcile touches.

The new tests patch their own lock rather than contending the module-level
one: asyncio.Lock binds to the event loop of its first contended acquire
and raises on every other loop after that, which would poison the next
asyncio test in the process. The proxy has a single event loop for its
lifetime so this is test-only, but it is a trap worth naming for whoever
writes the next concurrency test here.

* fix(proxy): scope the clear_cache wipe to auto-router deployments

Review caught a regression in the previous commit. Dropping the wipe
entirely stranded every db-backed auto-router on the pod.

The strategy registries (auto_routers, complexity_routers,
adaptive_routers, quality_routers) are keyed by model_name, which no
deployment-id reconcile touches, so clear_cache pops them and relies on
the reload to rebuild them. But the rebuild only happens on the ADD path:
Router.upsert_deployment returns early when a deployment is unchanged and
never reaches add_deployment -> _add_deployment ->
init_auto_router_deployment, which is what repopulates them. With the wipe
gone the deployment was always unchanged, so the pop was permanent: ANY
unrelated model write -- a team admin patching one team-owned model --
left every db-backed auto, complexity, adaptive and quality router
unroutable across tenants until a restart.

Restore the wipe for exactly the auto_router/* db deployments, whose
strategy entries are the ones being popped. Deleting them forces upsert
down the add path so both the deployment and its strategy entry come back.
Ordinary db models stay un-wiped, which is the point of the previous
commit: wiping them un-served every db model for the width of the reload,
and the reconcile converges without it.

test_clear_cache_wipes_auto_routers_but_leaves_ordinary_db_models pins
both halves against each other, since fixing either one naively breaks the
other. Both clear_cache tests fail with the pop-without-delete version.

* refactor(clear_cache): fold auto-router wipe into the classification pass

The auto-router scoping added in 5deddfd introduced two new mutable-collection
constructions, pushing LIT002 five over its budget ceiling.

Rather than suppress, do the work in the single pass that already walks
current_models: detect and delete the auto_router/* db deployments while
classifying, accumulating names into a set that replaces the old
db_router_deployments comprehension. Net-zero LIT002, same behaviour.

Comment updated to describe where the wipe actually happens now.
2026-08-12 13:42:26 -07:00
yuneng-jiang
32a5259407
test(e2e-ui): verify UI mutations against the API instead of trusting the toast (#36632)
* test(e2e-ui): cover the Playground, Logs and Usage manual-QA flows

These three pages carried no e2e coverage, so the manual QA checklist was the
only thing standing behind them.

Playground: sends a chat from the UI for both configured models, and for both
virtual-key sources (the logged-in session, and a key pasted into the panel).
This is the only spec that drives the dashboard's own LLM call path rather than
an admin CRUD endpoint.

Logs: a request the proxy actually served appears in the table, its drawer
expands to the real request and response bodies, both copy to the clipboard,
the Input card collapses, the JSON view exposes Request/Response, and the End
User filter narrows the table to one customer.

Usage: traffic billed to a virtual key reaches Top Virtual Keys, the card
toggles between table and chart, and the key opens its key-info panel.

Router settings: the existing spec proved the UI can record a fallback; the new
one proves the fallback is honoured, by pointing a model at an unreachable
upstream and asserting the reply comes back anyway. It asserts the un-fallen-back
call fails first, so a quietly-working primary cannot fake a pass.

Supporting changes:

- helpers/traffic.ts generates the traffic these pages render, rather than
  seeding rows no code produced. Its two wait helpers exist because the Logs
  and Usage pages read different stores: spend logs are flushed on a timer, and
  the Usage page reads a background rollup *and* fetches once on mount, so
  waiting on the DOM there can never converge.
- helpers/playground.ts holds the playground controls, now shared with the
  fallback spec. Everything is scoped to the visible copy of the config panel,
  which is rendered twice for the docked and collapsed layouts.
- run_e2e.sh gains E2E_KEEP_ALIVE=1, which brings the stack up and blocks so a
  spec can be re-run against it without paying for a UI rebuild each iteration.

Verified with the full suite on a fresh stack: 89 passed, 0 failed, 5 skipped.

* test(e2e-ui): cover listing and calling MCP tools

Covers the two MCP manual-QA items the create-only spec cannot reach:
opening a server's tool list, and calling a tool and seeing its result.

Both need an MCP server that actually answers, so this points at DeepWiki's
public MCP server -- Streamable HTTP, auth None, so there is no credential to
hold and nothing to leak from a public repo. The call is made by the proxy,
not the browser; nothing in the e2e chart restricts that egress.

The external dependency is real and is left visible: an upstream outage turns
these red rather than auto-skipping, because a spec that skips itself on
connection trouble also skips when the proxy's MCP client is what broke.
E2E_SKIP_EXTERNAL_MCP=1 is the explicit opt-out.

Not yet executed against a live stack.

* test(e2e-ui): verify key mutations round-trip instead of trusting the toast

The recurring customer report is a form that says "Saved!" and then either
no-ops or clobbers an unrelated field. A toast-only assertion passes in both
cases, and outside three specs that is all this suite checks.

Adds helpers/roundTrip.ts, factoring out the idiom clearCustomPricing,
credentials and routerSettings already use: capture the outgoing request body,
then read the resource back through the management API.

Applies it to the keys spec:
  - create: the key is readable from /key/list and owns a team_id, rather than
    trusting a table row rendered from the create response the UI already held
  - update limits: TPM/RPM are on the wire AND persisted, and the key's models
    and team are unchanged -- bumping one field wiping another is the reported
    failure mode (PR #34452), not a hypothetical
  - delete: the key is gone from /key/list, not merely toasted as deleted
  - regenerate: the stored token actually changed

/key/list shape is per KeyListResponseObject in litellm/proxy/_types.py.

Not yet executed: ports 4000/8090 are held by a parallel run.

* test(e2e-ui): let the local harness run on non-default ports

Two checkouts cannot run run_e2e.sh at the same time: it hardcodes 4000/5432/
8090, so the second aborts on "port 4000 is in use" and the only way forward is
to stop someone else's stack.

PROXY_PORT / POSTGRES_PORT / MOCK_LLM_PORT now override those, defaulting to the
historical values so an unset environment behaves exactly as before -- CI, the
CircleCI job and the chart's sidecar all keep working untouched.

Two details that would otherwise make a relocated stack fail confusingly:
  - the suite resolves its target from E2E_UI_BASE_URL, which defaults to :4000
    independently, so the run would build and boot correctly and then test
    whatever was on the default port. run_e2e.sh now derives it.
  - the mock server binds its port in server.py, so moving it needs MOCK_LLM_PORT
    there too. Its HOST stays loopback-only: 127.0.0.1:8090 from inside the
    proxy's own pod is the contract the e2e chart's sidecar is written against.

* test(e2e-ui): cover MCP server edit and delete, verified via the API

mcpServers.spec.ts only ever creates a server, and creation is the one MCP
operation nobody has complained about. The reports are all on the other side:
an alias rename that needs three or four saves to take, a delete that needs two
attempts. Both produce a success toast on the failing attempt, so a toast-only
assertion cannot tell them from working software.

Rename asserts the new alias and the target server_id are on the PUT, then
polls /v1/mcp/server until the stored alias matches -- one save has to be
enough. Delete asserts the server is really gone from the list.

Points at an unreachable URL: these exercise litellm's persistence, never the
upstream, so a live MCP server would add a network dependency for nothing.
mcpTools.spec.ts is where a real upstream is needed.

Both pass against a local stack, as do the mcpTools specs from 5e189e9b1a.
Neither reproduced the reported failures on this build -- they guard, they did
not catch.

* test(e2e-ui): verify team create, invite and delete against the API

Three team mutations stopped at a toast, and one of those toasts is matched as
loosely as /success/i -- almost any notification satisfied it.

  - create: the team is readable from /team/list and kept the models chosen in
    the modal, rather than trusting the UI's own "Team created"
  - invite: the invited address really appears in members_with_roles, which is
    the point of the flow
  - delete: the team is gone from /team/list. The existing assertion was that
    the row vanished, which is the client dropping it from local state and
    happens whether or not the delete reached the database.

Shapes read off a live proxy: /team/list is a bare array; /team/info nests the
record under team_info. All 6 tests pass locally.

* test(e2e-ui): verify team-admin member and key mutations against the API

The team-admin flows stopped at a success toast. A member add that lands on
the wrong team, a remove that takes out the wrong row, and a key that comes
back unscoped all produce the same toast as the working case, so the existing
assertions could not tell them apart.

Each mutation now pins what went on the wire and reads the result back:
member add/remove assert team_id and the member identifier on the request,
then poll /team/info's roster; the team key asserts team_id on /key/generate
and reads /key/list back to confirm the key is owned by the admin's own team
rather than orphaned.

* test(e2e-ui): verify model add and limit edits against the stored deployment

The Models specs checked the rendered result: the TPM/RPM edit asserted the
new numbers were visible in view mode, and the two add flows asserted a row
showed up in the table. Both render from state the UI already holds, so a save
the backend dropped and a save it kept look the same.

Each mutation now pins the request and reads the deployment back. The limits
edit also asserts the fields it did not touch -- upstream model and team
ownership -- are unchanged, because handleModelUpdate rebuilds and PATCHes the
whole litellm_params blob, which is how an unrelated field gets clobbered by a
save that reports success.

The two add flows assert model_name, the routed model and custom_llm_provider
on the wire and in storage; a deployment that loses its provider looks correct
in the table and is unroutable.

The Team-BYOK test is unchanged -- it is skipped without a license, so any
change to it would be unverified.

* test(e2e-ui): delete the MCP servers these specs create

MCP servers outlive the test that made them, the MCP page contacts every
server it lists, and most of the ones these specs create point at an
unreachable host. They accumulate, and each one makes navigateToPage's
networkidle wait a little slower to settle.

Measured on a local stack: with eleven leaked servers the whole MCP suite
failed on a 30s navigation timeout, including specs that leaked nothing.
Deleting the leftovers made all five pass. With per-test cleanup added, a
run from a clean slate leaves zero behind and takes 20s instead of 1m24s.

mcpServers.spec.ts carried a note that no teardown was needed because the
runner brings up a fresh database each time. That holds for CI and is why
this went unnoticed; it does not hold for a local stack that is reused.

* test(e2e-ui): say why the team-model setup call failed

The setup that creates a team-scoped model asserted a bare `ok()`, so a
failure read "expected true, received false" and pointed at the UI. The call
is enterprise-gated -- creating a model with model_info.team_id returns 403
without LITELLM_LICENSE -- and that is invisible from the old message. It now
carries the status and body, which names the cause immediately.

The api_base also pointed at the mock's default port rather than the one the
harness started; nothing in the test calls the model, but the two should not
disagree.

* test(e2e-ui): add a model through the UI and serve traffic with it

Every existing Add Model test stops at "the row appears in the table",
which a deployment that cannot serve a single request also does. The
manual-QA item this replaces is the whole loop: fill the form, pass Test
Connect, add it, confirm it works.

The new test ends by calling the model it just created. That is the only
assertion that rules out a dropped api_base, a mangled provider prefix,
or a name the router never registers -- all of which look identical in
the UI.

No provider credential is involved. OpenAI-Compatible is the provider
whose form exposes API Base, so the deployment points at the harness's
own mock LLM. The mock speaks the OpenAI wire format, so Test Connect
performs a real completion against a real endpoint and really succeeds.

Also adds teardown for the deployment it creates. A local run throws its
database away, but the deployed stack does not, and a leaked deployment
shows up in every later Models table and /v2/model/info readback.

Both new assertions were mutation-tested: pointing the traffic poll at a
name that was never created fails the test, and the wire assertion fails
when the typed name is not what reaches /model/new.

* test(e2e-ui): print the proxy log when the proxy dies on its own

In E2E_KEEP_ALIVE=1 mode the harness blocks until the proxy pid goes away, then
printed a bare "Proxy exited." and fell straight into cleanup, which rm -f's the
log. The proxy has now exited by itself twice, minutes after a run had finished,
leaving nothing to look at. Both startup failure paths already tail -n 100 the log
before giving up, so this was the one death that stayed silent

Dump the same 100 lines before exiting. A normal Ctrl-C teardown still deletes the
log and prints nothing, which is why INT and TERM now exit instead of running
cleanup and falling back into the wait loop: under the single trap a SIGTERM
deleted the log, resumed the loop, and would then report "tail: no such file",
besides running cleanup twice

* test(e2e-ui): split the log-drawer copy assertions off the expand test

The copy assertions need `navigator.clipboard`, which the browser only
exposes in a secure context. Locally the suite runs against
http://127.0.0.1 and localhost is trustworthy, so it is there. In CI the
run pod is pointed at a plain-HTTP cluster DNS name, where it is
undefined -- and InputCard.handleCopy calls writeText unguarded, so the
click throws before MessageManager.success and no toast ever renders.
That failed all three attempts of litellm-e2e-ui build 10.

Measured rather than inferred: on http://127.0.0.1:4100
isSecureContext/typeof navigator.clipboard are true/"object", and on a
DNS name resolving to that same address they are false/"undefined",
which reproduces the CI failure exactly.

Splitting keeps the drawer-rendering coverage running everywhere and
confines the skip to the part the browser has actually switched off. The
copy assertions still run in full wherever the origin is trustworthy.

The underlying product behaviour is left alone deliberately: any
deployment served over plain HTTP on a hostname has a copy button that
throws and gives no feedback, and that deserves its own fix rather than
being papered over from a test.

* test(e2e-ui): cut the added comments back to what the code cannot say itself

Greptile flagged the helper commentary, and it was right: CLAUDE.md says not to
write comments unless they explain very complex business logic, and much of what
was added here narrated ordinary test setup and motivation instead.

Trims 310 comment lines across the 14 files this branch touched. Kept only the
notes that record something unrecoverable from the code: why the request
listener is armed before the click, why a locator walks up the DOM, why an
assertion exists beyond the toast. Pre-existing comments are left alone.

No behaviour change. The only non-comment hunk is a prettier reformat.
2026-08-12 13:39:58 -07:00
yucheng-berri
0e9da56f89
fix(batches): strip NUL bytes from passthrough batch tags before the managed object write (#36688)
PostgreSQL rejects NUL in jsonb with 22P05, and the tags go into the managed
object's CREATE payload, so one poisoned tag aborts the whole row insert rather
than just that column. With no LiteLLM_ManagedObjectTable row, CheckBatchCost
never discovers the batch, so a batch that really ran and billed at the provider
produces no spend at all. The create-time write is fire and forget, so nothing
retries it.

This regressed in #36468, which started passing request_tags and
persist_attribution from the Anthropic passthrough; before that no
caller-supplied string reached the column.

Sanitize in the shared helper that builds the value, matching how
spend_tracking_utils already handles LiteLLM_SpendLogs.request_tags. Both the
Anthropic and the Vertex passthrough build tags through that one helper, so this
covers both. Rename it to _sanitized_str_tuple since it no longer merely
coerces.
2026-08-12 13:31:51 -07:00
ryan-crabbe-berri
2d12a3ea41
fix(proxy): expand config-defined model access groups when resolving team models for /v2/model/info (#34211)
* fix(proxy): expand config-defined model access groups when resolving team models for /v2/model/info

Teams whose only model grant is a config-defined access group (a model_info.access_groups
name listed in team.models) got an empty /v2/model/info?include_team_models=true result.
_add_team_models_to_all_models passed each team.models entry straight to
llm_router.get_model_list(model_name=...), which never matches an access-group name, so
the group's member deployments were dropped. Runtime auth and /v1/models were unaffected
because they expand team.models through get_team_models first.

Resolve team.models through the same get_team_models resolver before iterating, reusing the
exact path runtime auth and /v1/models trust so the two can't drift again. The get_model_names
and get_model_access_groups accessors are hoisted above the team loop so they run once.

* fix(proxy): keep a literal model whose name collides with an access-group name in listings

A grant string that names both a deployed model and a config access group grants
BOTH at runtime (_check_model_access_helper unions them), but the listing resolver
dropped the literal and substituted the group members, hiding a callable model from
/v1/models and /v2/model/info. Keep the literal when it is also a deployed model so
listings match runtime access exactly. Pure-group names (no collision) are still
replaced by their members. Also rewrites _get_models_from_access_groups to build
its result without mutating the input list.

Addresses the Greptile P1 on this PR.

* fix(proxy): type proxy_model_list param as Sequence to satisfy LIT001 budget
2026-08-12 12:54:36 -07:00
yuneng-jiang
dcb789f3ed
refactor(ui): migrate policy impact popover to shadcn (#36653)
* test(ui): characterize policy impact popover

* refactor(ui): migrate policy impact popover to shadcn

* test(ui): type policy impact mocks
2026-08-12 12:42:21 -07:00
yuneng-jiang
806564b14c
refactor(ui): migrate models-and-endpoints to shadcn (#36648)
* test(ui): characterize models and endpoints components

* refactor(ui): migrate models and endpoints to shadcn

* fix(ui): preserve numeric input boundaries

* fix(ui): label models numeric controls

* fix(ui): preserve comma-containing utterances

* fix(ui): deduplicate pasted router utterances

* refactor(ui): keep utterance dedup immutable

* fix(ui): keep model refresh action on tab row
2026-08-12 12:42:17 -07:00
yuneng-jiang
5a617d808a
refactor(ui): migrate team settings to shadcn (#36641)
* test(ui): characterize default team settings

* refactor(ui): migrate teams settings to shadcn

* chore(ui): prune teams lint suppression

* test(ui): preserve teams settings contracts
2026-08-12 12:41:44 -07:00
yuneng-jiang
17c20a5793
refactor(ui): migrate prompts to shadcn (#36643)
* test(ui): characterize prompt editor controls

* refactor(ui): migrate prompts to shadcn

* fix(ui): preserve prompts interaction contracts

* fix(ui): restore prompts history contracts

* fix(ui): preserve prompts escape layering
2026-08-12 12:41:25 -07:00
yuneng-jiang
d329951999
refactor(ui): migrate users dashboard to shadcn (#36642)
* test(ui): characterize users dashboard behavior

* refactor(ui): migrate users dashboard to shadcn

* fix(ui): preserve users tab state
2026-08-12 12:41:10 -07:00
yuneng-jiang
4445eb71f6
refactor(ui): migrate admin-panel to shadcn (#36635)
* test(ui): characterize admin settings components

* refactor(ui): migrate admin-panel to shadcn

* fix(ui): restore compatible page grouping

* test(ui): cover legacy page grouping runtimes

* test(ui): restore admin settings rendering contracts
2026-08-12 12:40:49 -07:00
yuneng-jiang
7d12f21e31
refactor(ui): migrate cost-tracking to shadcn (#36631)
* refactor(ui): migrate cost-tracking helpers to shadcn

* fix(ui): restore export menu keyboard navigation
2026-08-12 12:40:12 -07:00
yuneng-jiang
2b9e3db6b0
refactor(ui): migrate cost-optimization to shadcn (#36629) 2026-08-12 12:40:04 -07:00
Yassin Kortam
eefbe2eb18
fix(proxy): log requests rejected for an unparsable body in spend logs (#36673)
A request whose body never parses is rejected in auth, before the endpoint
runs, so nothing downstream fires the failure hook that writes the spend log
row Request Logs reads. The caller sees a 400 that leaves no trace.

Auth now records that rejection through the same post_call_failure_hook the
endpoints use, keyed to the caller it already authenticated. Logging is
best-effort: a logging failure is swallowed so the 400 the caller sees is
unchanged. The path where the key is also rejected is left alone, since the
auth failure handler already logs that request.
2026-08-12 12:37:15 -07:00
Yassin Kortam
a01b421ce9
fix(mcp): bound MCP client requests with a session read timeout (#36675)
An upstream that ends its response stream without a JSON-RPC reply leaves the
request pending forever. Tool discovery then only ended when an outer cancel
scope killed it, which logged a cancelled list_tools, ignored the timeout the
operator configured, and reported no tools to the client. Prompts and resources
had no outer guard at all.

Give the client session a read timeout so every request it sends is bounded,
including initialize. The SDK reports its own elapsed timeout as an McpError
carrying an HTTP status code in the field that otherwise holds JSON-RPC error
codes, and it relays an upstream's JSON-RPC error through that same class and
field, so the code alone cannot separate the two: an upstream answering with
application code 408 would be blamed on the gateway as a 504. Translate the
SDK's timeout into a TimeoutError in the module that configures the timeout,
matching on the elapsed timeout in the exception's context chain rather than on
the number, so the listing taxonomy never has to read a JSON-RPC code as an HTTP
status and every caller gets the same signal.

The bare cancellation warning is replaced by a line naming the server and the
budget that elapsed, and quiet_on_error does not demote it.
2026-08-12 12:36:24 -07:00
Yassin Kortam
258fe3e4ba
fix(passthrough): carry the budget reservation into request metadata (#36592)
A successful pass-through request left its pre-call budget reservation in
the shared Redis spend counter. `_init_kwargs_for_pass_through_endpoint`
built the request metadata from the sanitized key fields only, so
`_PROXY_track_cost_callback` resolved `budget_reservation = None` and
`increment_spend_counters` added the actual cost on top of a reservation
nobody released. The counter drifted above real spend on every request
until the key falsely tripped BudgetExceededError, while the Postgres
spend stayed far below the limit. The failure path was unaffected because
it releases `user_api_key_dict.budget_reservation` directly.

The reservation is now set alongside the other internal keys, after the
client-supplied metadata merge, so a request body cannot forge one that
names arbitrary counter keys.
2026-08-12 12:34:13 -07:00