Commit graph

286 commits

Author SHA1 Message Date
mateo-berri
d924226b01 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit_5870_passthrough_e2e_pins_wt 2026-08-20 17:33:35 -07:00
mateo-berri
94239d281f test(e2e): name the pinned GitHub issue in each passthrough test docstring
The passthrough tests and their coverage registry rows pointed at the internal
ticket id, which does not resolve for anyone following a link from
status.litellm.ai. Each test docstring and registry rationale now names the
GitHub issue it pins: #36086 for the two prefix routing cases, #36087 for the
file list cursors, #36523 for streamed Responses cost, and #36646 for
embeddings spend.
2026-08-20 17:33:33 -07:00
Mateo Wang
d40aea865d
Merge pull request #37679 from mubashir1osmani/litellm_lit5890_replay_key_params_form
test(e2e): pin query params and multipart form fields as replay match-key identity
2026-08-20 16:16:33 -07:00
Mateo Wang
8f68bc6579
Merge pull request #37607 from BerriAI/litellm_lit_5869_cost_e2e_pins
test(e2e): pin prompt-cache, service-tier, and cost-header billing as permanent regressions
2026-08-20 16:15:08 -07:00
ryan-crabbe-berri
21e9632713
test: add six ruff rules that catch tests which cannot fail (#37709)
`assert False` inside a `try:` raises AssertionError, which the `except
Exception` right below it catches, so several tests reported green no matter
what the code did. `pytest.fail` raises Failed, a BaseException, and escapes.

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

Selects B011, B015, B018, PT015, PLR0133 and PLW0127 in ruff-tests.toml
alongside F821, with all 50 existing violations fixed, so no budget file or
ratchet is needed. CI already runs this config over tests/.
2026-08-20 14:21:26 -07:00
Mateo Wang
fc3b160fb5
Merge pull request #37565 from BerriAI/litellm_lit_5745_provider_edge_replay
feat(e2e): move record/replay to the provider edge (LIT-5745)
2026-08-20 13:50:04 -07:00
mubashir1osmani
80cb65502c test(e2e): pin query params and multipart form fields as replay match-key identity
The canonical request already folds params and form into the digest, but
nothing asserted it, so dropping either from canonicalize() left all 92
fixture tests green. Two GETs differing only in query string, or two
uploads differing only in a form field, would share a replay pool and
FIFO-pop each other's recorded response.

Resolves LIT-5890
2026-08-20 15:59:34 -04:00
mateo-berri
975a6806c3 test(e2e): request reasoning explicitly on the reasoning-cost assertions
The two tests that assert on reasoning cost read reasoning_tokens off the
response and required it to be nonzero, without ever asking the model to
reason. Both now send reasoning_effort, so the assertion rests on a
parameter the test sets rather than on the model's default behavior.

The cache-breakdown test sends it on its prime call too: OpenAI's prefix
cache keys on the reasoning setting as well as the tokens, so priming at
a different effort never produces a read.
2026-08-20 04:11:08 -07:00
mateo-berri
69278ae37b docs(e2e): correct the passthrough-stream registry row's uncovered reason 2026-08-20 03:25:31 -07:00
mateo-berri
e11399fc56 test(e2e): pin the unflagged Vertex cache test to us-east5 2026-08-20 03:13:14 -07:00
mateo-berri
aa8e7278e3 test(e2e): drop the passthrough streaming-cost test, it needs a config flag
The final streaming usage frame only carries usage.cost when the proxy runs
with litellm_settings.include_cost_in_streaming_usage: true, and that flag is
readable only off the module-level litellm setting. There is no header, key,
or management route that turns it on per request, so a test cannot ask the
shared e2e proxy for it, and the proxy's config does not live in this repo.

The registry row stays as an uncovered gap with the reason recorded, rather
than being deleted, so the behavior is still on the list of things we want
covered once the gateway config is reachable.

The StreamOptions model, ChatBody.stream_options, Usage.cost, and
AnthropicMessagesResponse.id existed only for that test, so they go with it.
2026-08-20 03:08:05 -07:00
mateo-berri
8b7c801d61 test(e2e): pin openai_passthrough routing, cost logging, and file list isolation
Five e2e tests over routes a customer drives through the gateway, each one
pinning a fix that currently has no live coverage.

The dedicated /openai_passthrough prefix used to be swallowed by the
provider-scoped /{provider}/v1/files and /{provider}/v1/batches routes, which
bound "openai_passthrough" as a provider name and failed inside the gateway
before ever reaching OpenAI. Two tests now upload a file and list batches
through that prefix and assert OpenAI's own objects come back.

Streamed /openai_passthrough/v1/responses and /openai_passthrough/v1/embeddings
are relayed to OpenAI but still have to be costed, since the customer budgets
against this traffic. Both used to land a row the gateway could not use: the
streamed responses call logged a zero-cost row under a random id, and
embeddings wrote no row at all. Each test now reconciles the logged spend and
token counts against the response the caller was actually served.

GET /v1/files narrowed its data to the caller's own rows but left first_id and
last_id addressing the shared provider account's page, handing any caller raw
provider file ids belonging to other tenants. The new test asserts both cursors
address rows in the page the caller can see.

ResourceManager.defer now accepts any callable rather than one returning None,
so a delete that answers with a response model can be deferred as-is.
2026-08-20 03:03:35 -07:00
mateo-berri
155ef8cb11 test(e2e): require consecutive cache reads before the reminder turn 2026-08-20 03:03:08 -07:00
mateo-berri
fc98bab180 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into fix-anthropic-mid-conversation-system-cache 2026-08-20 02:55:51 -07:00
mateo-berri
fe7ada15e2 fix: keep tool_use and tool_result adjacent when converting mid-conversation system turns
On models without supports_mid_conversation_system, a system entry between
an assistant tool_use turn and the user tool_result turn became a user turn
in that position and the provider rejected the request ("tool_use ids were
found without tool_result blocks immediately after"). That run of entries
now goes right after the tool_result turn, where consecutive user turns
merge upstream. The converted turn also carries only role and content, as
the hoist did, so an entry with extra keys no longer 400s with "Extra
inputs are not permitted".

The e2e cache priming re-sends the identical first turn until its own cache
entry reads back before the reminder turn goes out, since Vertex can take a
few seconds to serve a freshly written entry.
2026-08-20 02:43:10 -07:00
mateo-berri
ef6af5c615 test(e2e): accept both model-not-found phrasings on a shared proxy
/audio/transcriptions answers a model-less request with one of two 400s
depending on whether any wildcard deployment is registered at the time, and
every suite shares one proxy, so run order decided which message came back.
The assertion pinned only the no-wildcard wording, so it went red whenever
the model-access-group suite had registered its wildcards first. It now
accepts either message and still holds the error to naming the model

Verified against a live proxy in both states: with a wildcard registered
(the message CI was seeing) and with none (the message the assertion
expected), the suite passes 3/3 either way
2026-08-20 02:03:27 -07:00
mateo-berri
bcb6a6eaab test(e2e): pin prompt-cache, service-tier, and cost-header billing
Seven live e2e tests covering cost-tracking regressions that currently ship
unnoticed: cache-write tokens billed at the cache-creation rate (#34046),
per-component cost_breakdown on the spend row (#31686), cache reads billed at
the cache-read discount on streamed calls (#34812), cache tokens surviving the
anthropic-messages to Responses bridge (#34957), priority-tier rates applied to
input, output and reasoning (#35923, #35925), the per-component response cost
headers summing to the total (#36965), and cost injected into the final usage
frame of an /openai passthrough stream (#36503).

Every test registers its own deployment with a distinct custom rate per
component, so a component billed at the wrong rate cannot pass. The shared
helpers in cost_rows.py encode the one thing the two surfaces disagree on: the
spend row's input_cost is gross of cache while the response's cost-input header
is net of it.
2026-08-20 01:50:01 -07:00
Mateo Wang
47a7e1742e
Merge pull request #37539 from BerriAI/litellm_batch_enqueued_token_limit
feat(proxy): enqueued-token rate limiting for batches with refund on completion and cancellation
2026-08-19 22:22:05 -07:00
ryan-crabbe-berri
0b374541bb
refactor(ui): migrate the last antd components off antd onto shadcn (#37569)
Converts the remaining dashboard components that still imported antd: admin panel, agents, MCP toolsets, policies, prompts, bulk user edit, create user, plugin settings, teams, add model, auto router, cloudzero export, BYOK credentials, credential modal, onboarding link, create key and routing groups.

Primitives map onto the house shadcn set: Typography onto semantic tags, Select onto ui/select, SearchSelect or MultiSelect, Input onto ui/input, Tooltip onto SimpleTooltip, Card, Table, Tabs, Switch, Checkbox, Radio, Tag onto Badge, Divider onto Separator, Spin onto UiLoadingSpinner, Modal onto Dialog, message onto toast, and Space, Row, Col, Flex and Layout onto flex containers.
2026-08-20 03:01:07 +00:00
mateo-berri
d5ac49588a Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_batch_enqueued_token_limit 2026-08-19 19:31:46 -07:00
mateo-berri
367dd537b9 feat(e2e): move record/replay to the provider edge (LIT-5745)
Replaces the test-side fixture transport with an in-process provider-edge
HTTP server the proxy's deployments point their api_base at. Record forwards
provider calls verbatim and writes them to the bundle; replay answers them
from the bundle with zero provider calls while key auth, routing, cost
calculation, and spend-log writes still execute against the live proxy and
database. Drift comes back as HTTP 599 naming the computed and closest
recorded keys. Request headers are never stored and responses are kept
byte-identical between modes from the proxy's side of the socket.
2026-08-19 18:39:15 -07:00
ryan-crabbe-berri
74b279bc44
fix(auth): resolve bare model names against wildcard deployments in model access groups (#37492)
* fix(auth): resolve bare model names against wildcard deployments in model access groups

* test(e2e): cover model access group permission checks on keys and teams
2026-08-19 15:33:29 -07:00
Mateo Wang
7e247c1477
Merge pull request #37525 from BerriAI/litellm_lit_5741_replay_match_keys
feat(e2e): canonical content-based match keys for record-and-replay
2026-08-19 15:27:16 -07:00
mateo-berri
7a6a677b72 feat(proxy): enqueued-token rate limiting for batches with refund on completion and cancellation 2026-08-19 15:09:05 -07:00
mateo-berri
975e79dcef fix(e2e): make concurrent replay consumption race-free
Greptile flagged that lazy per-slug pool initialization could double-build
under concurrent replay calls, splitting consumption across a discarded
pool. Pools are now built once at ReplaySource construction and per-key
consumption is a single atomic deque pop, with a barrier-synchronized
regression test that fails 10/10 under the lazy-init mutant
2026-08-19 14:55:53 -07:00
mateo-berri
125587d286 feat(e2e): canonical content-based match keys for record-and-replay
Replay previously matched interactions by transport verb and path in
recorded order, so a request whose body drifted from the recording
silently replayed the stale response, and reordering two independent
calls broke replay even though both were recorded. Match keys are now
canonical: fixture_canonical.py strips volatile headers and credential
fields, replaces unique markers, generated ids, uuids, and timestamps
with fixed placeholders, sorts object keys, and hashes what remains, so
a key is stable across runs and machines while any real content drift
is a hard ReplayMiss naming the computed key, the closest recorded key
with its file, and a content diff, with no fallthrough to a live call.
Matching is order-independent across distinct keys and FIFO within one
key. Recording now also redacts credential body and form fields (not
just auth headers) so provider keys never land in bundles.

Resolves LIT-5741
2026-08-19 14:33:22 -07:00
Mateo Wang
59c7e7a17d
Merge pull request #37360 from BerriAI/litellm_lit_5729_e2e_record_replay_seam
feat(e2e): add record/replay transport seam and fixture bundle format
2026-08-19 14:28:03 -07:00
mateo-berri
2e1d407711 test(e2e): pin the tag-routing denial to its actual cause
The strict-denial pin only asserted a 401, so any unrelated 401 (a bad key, a deleted key) would have kept it green while tag routing silently broke. The harness now keeps the 401 response body, the way it already does for 429s, and the pin asserts the tag-routing denial message.
2026-08-18 20:49:12 -07:00
Mateo Wang
1de398d9aa
Merge pull request #37366 from BerriAI/litellm_e2e_auto_router_regression_pins
fix(router): honor key-level tag filtering in pre-routing and pin auto-router e2e regressions
2026-08-18 20:06:49 -07:00
yuneng-jiang
f27d88bd13
test(ui): repoint the e2e locators at the post-antd form controls (#37421)
* test(ui): repoint the e2e locators at the post-antd form controls

Nine Playwright specs went red after the tremor and antd removals, none of
them because the product broke. The specs selected on markup those libraries
owned: tremor's TextInput stamped data-testid="base-input", the antd toast
facade rendered .ant-notification, and the team member modal's email field was
an .ant-select. Removing the libraries deleted those hooks silently.

Repoint each onto a user-facing locator that survives the next migration:
getByLabel for the key name, the MCP tool argument and the two cache pricing
fields, getByRole("combobox") for the team member email search, and the toast
container for the add-model success message.

The pricing fields needed a source change to be reachable at all. antd's
Form.Item used to assign the field name as the input id and tie the label to
it; the react-hook-form rewrite renders FieldLabel with no htmlFor and lets
FormField generate an opaque control id, so both cache inputs lost their
accessible name and could only be told apart by placeholder, which they share.
Pin the id back to the field name and point the label at it.

* fix(ui): let FormField own the pricing field label instead of hand-rendering one

The previous commit gave the cache cost inputs an accessible name by rendering
a FieldLabel with htmlFor next to the FormField. FieldLabel forwards Label's
props, and Label only accepts children, so next build failed type checking.

FormField already renders a label wired to the control id it generates, and
FormField.test.tsx covers that association, so passing label through is both
type-safe and less markup. The read-only branch keeps its plain FieldLabel,
which has no control to point at.
2026-08-19 02:41:05 +00:00
mateo-berri
47f3cf804e fix(router): honor request-level tag filtering in pre-routing strategy selection
Key and team router_settings set enable_tag_filtering on the request kwargs,
and get_deployments_for_tag already treats that as authoritative, but
_select_pre_routing_strategy only consulted the router-wide flag, so tagged
auto-router markers still captured untagged requests from keys that enabled
filtering. The e2e auto-router module now enables tag filtering through
key-level router_settings instead of flipping /config/update module-wide,
which was denying concurrently running tagged requests from other suites on
the shared per-build CI proxy.
2026-08-18 16:19:43 -07:00
mateo-berri
d4db4b1379 test(e2e): pin marker alias connection params staying off the routed tier 2026-08-18 15:14:05 -07:00
mateo-berri
96cee087be test(e2e): pin auto-router tag-split, alias pricing, heuristic scope, and Responses routing regressions 2026-08-18 14:49:12 -07:00
mateo-berri
6bf535bb8f feat(e2e): fail passed replays that leave recorded interactions unconsumed 2026-08-18 14:34:23 -07:00
mateo-berri
2adf8aa581 feat(e2e): add record/replay transport seam and fixture bundle format
E2E_FIXTURE_MODE selects the transport every e2e client is built on: live
(default, unchanged behavior), record (pass through to the live proxy while
writing every interaction to a fixture bundle), or replay (serve every
interaction from the bundle with no proxy and no provider spend). Both new
transports fulfil the existing Transport protocol, so no test changes shape.

A bundle is a directory with a manifest (record timestamp, harness version,
format version) and one JSON file per interaction, grouped per test in call
order. Replay against a manifest older than seven days hard-fails at
collection time naming the bundle age. Record always wipes and never reads
the previous bundle, refusing to wipe a directory that is not a bundle.
Auth header values are redacted on write; uploads store a sha256 digest.
unique_marker() becomes deterministic per test in record/replay modes so a
replay run regenerates exactly the requests the record run sent.

Content-based match keys, streaming chunk fidelity, and provider-scoping are
follow-ups (LIT-5741, LIT-5742, LIT-5745).
2026-08-18 14:08:00 -07:00
Yassin Kortam
d5b91b94d3
test(e2e): replay a real tool-search assistant turn back to Bedrock Invoke (#36856)
The tool_search x bedrock_invoke cell only ever probed the first turn, so
nothing in the suite has sent a server_tool_use block back to a provider.
Every turn of a real Claude Code session after the first carries the
server_tool_use and tool_search_tool_result blocks the previous turn
produced, and that path was uncovered.

Adds probe_tool_search_multiturn, which takes the real assistant turn
back, answers any client-side tool_use with the id the model actually
emitted, and replays the whole thing as history with the tools still
declared. The assertion refuses to go green unless both server-tool
blocks made it into the replayed history, so a first turn truncated at
max_tokens reads as a failure instead of a vacuous pass.

The replay assertion's red paths never run in a green cell, so they get
markerless harness tests of their own alongside the existing
_builder_unit_tests tree.

No production code.
2026-08-17 11:59:26 -07:00
Yuneng Jiang
de2b220c36
test(e2e/ui): assert the log drawer chevrons by their lucide classes
The log details drawer moved off Ant Design in 03d2b16bc, so its section
header renders lucide ChevronUp/ChevronDown rather than antd's UpOutlined
and DownOutlined. The collapse test still waited on .anticon-up and
.anticon-down, which no longer exist anywhere under view_logs, so it
failed on every run and burned all three attempts identically.

Point the three assertions at .lucide-chevron-up and .lucide-chevron-down,
matching how the dashboard's other suites address lucide icons.
2026-08-15 17:50:40 -07:00
yuneng-jiang
91aee78e78
Merge pull request #37065 from BerriAI/litellm_/nice-wilson-9fbed6
test(e2e): assert provider error shape instead of pinned prose
2026-08-15 17:15:30 -07:00
Yuneng Jiang
481ab07ca7
test(e2e): skip the bedrock web search cell the stack cannot provision
This cell needs the websearch_interception callback and a declared search
backend, both listed in its own module docstring. The ephemeral e2e stack
ships neither, so the request falls through to the bedrock transformation
and takes the by-design 400 that tells you to enable interception.

The cell has never been green here: the error path merged about an hour and
a half before the cell did, and the last full suite to pass predates the
cell entirely. Skip it with the reason recorded so the run reports honestly
instead of carrying a permanent red, and unskip once the stack ships the
config the docstring already spells out.
2026-08-15 16:09:02 -07:00
Yuneng Jiang
bf93483b95
test(e2e): assert provider error shape instead of pinned prose
Both providers reworded the error strings these two cells pinned, so the
suite went red without any behavior changing. Anthropic's auth error is now
"API key is invalid." rather than "invalid x-api-key", and OpenAI rejects an
empty upload with "This model does not support the format you provided.",
which names neither "file" nor "audio".

Assert the durable shape instead. The otel cell pins the machine-readable
authentication_error type plus a non-empty message, and the embedded JSON
still has to parse, which is what proves the attribute survived untruncated.
The transcription cell pins that the 400 relays the provider's own rejection
and is typed as a client input error, so a regression that swallows the
provider reason or returns a 500 still fails.
2026-08-15 16:08:52 -07:00
Yuneng Jiang
edc1d65558
test(e2e/ui): address the migrated dashboard controls by role, not antd classes
The playground, logs drawer and AI Hub modal moved off antd, so the specs
that reached for .ant-select, .ant-drawer-content, .ant-modal and
.ant-radio-button-wrapper no longer match anything and time out.

Address the same controls through their accessible role and name instead,
which holds across the component library swap and reads closer to what a
user does.
2026-08-15 15:37:09 -07:00
Shifat Islam Santo
1b5e50727c fix: reuse block builder for lint budget, assert unflagged cache e2e 2026-08-14 14:40:10 -05:00
Shifat Islam Santo
658c67c152 fix: preserve prompt cache for mid-conversation system on unflagged Claude models 2026-08-14 14:19:48 -05:00
Yuneng Jiang
693683c013
Merge branch 'litellm_internal_staging' into litellm_shadcn_modelhub_0814 2026-08-14 09:59:57 -07:00
Yuneng Jiang
afff1b08fa
refactor(ui): move the shared dropdowns and selectors onto shadcn primitives
Rebuilds the thirteen form-free components under common_components on
the in-repo shadcn layer, so they inherit the dashboard's design tokens
instead of styling themselves through Ant Design and Tremor.

SearchSelect and the three dropdowns that wrap it now forward an
optional input id, so an antd Form.Item label still resolves to its
control. The e2e steps that reached into antd's Select and Modal
internals now go through the test id, role and data-slot.
2026-08-14 06:09:10 -07:00
Yuneng Jiang
3a537cce4d
refactor(ui): move the model hub and model select onto shadcn primitives
Rebuilds public_model_hub, MakeSkillPublicForm, ModelSelect and the
guardrail LogViewer on the in-repo shadcn layer, so they inherit the
dashboard's design tokens instead of styling themselves through Ant
Design and Tremor.

Public prop signatures are unchanged, so no caller moves. The two
teams e2e steps that reached into antd's Select internals now drive
the combobox through its test id, role and data-slot instead.
2026-08-14 04:48:49 -07:00
Yuneng Jiang
ed01f7316b
test(e2e): assert the model allow-list permits, not only denies
Every case in TestAccessControl asserted that something was refused. A gateway
that denied the allow-listed model too would have passed all of them, so the
suite could not tell "denied correctly" from "broken outright".

Adds the positive half: a key allow-listed for gemini-2.5-flash can call it and
gets back a real completion rather than a 200-wrapped error.

Also tightens the unknown-model case. It accepted any valid JSON, so a bare
"{}" or even "null" satisfied it. It now requires the OpenAI-shaped error
envelope with a message a client can actually surface, parsed through a typed
model instead of json.loads.
2026-08-13 13:37:31 -07: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
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
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