Commit graph

123 commits

Author SHA1 Message Date
Mateo Wang
2e06d195b2
Merge pull request #39857 from BerriAI/litellm_e2e_reliability_module_cells
test(e2e): cover the reliability retry, cooldown, fallback, and routing-strategy cells
2026-09-15 11:38:51 -07:00
yuneng-jiang
15bd8b0e4a
Merge pull request #40892 from BerriAI/litellm_jwt_management_callers
test: bind management E2E callers and isolate JWT actors
2026-09-14 20:30:52 -07:00
Joshua Valluru
94e4dd725f fix(mcp): require admission for delegated OAuth 2026-09-12 16:41:23 -07:00
Yuneng Jiang
c8bb54993e
test: enforce isolated actors and stop OIDC process groups 2026-09-12 13:49:49 -07:00
Yuneng Jiang
88de192dcf
test: bind management E2E callers and isolate JWT actors 2026-09-12 13:29:04 -07:00
mateo-berri
036a380fa0 chore: merge litellm_internal_staging into litellm_e2e_reliability_module_cells
Some checks failed
ai-gateway image / ai-gateway release image (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
2026-09-12 13:05:02 -07:00
yuneng-jiang
1be89d28b8
Merge pull request #39990 from BerriAI/litellm_e2e_jwt_harness
test(e2e): reusable JWT fixtures and management lifecycle coverage
2026-09-11 21:18:59 -07:00
mateo-berri
9375719feb Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_e2e_memory_regression_failing_requests
# Conflicts:
#	tests/e2e/CLAUDE.md
#	tests/e2e/models.py
2026-09-11 19:31:11 -07:00
Yuneng Jiang
a706dbbb7d
chore: merge current staging into JWT E2E infrastructure 2026-09-11 17:13:52 -07:00
Yuneng Jiang
4dd6af5722
chore: merge latest staging into JWT E2E foundation 2026-09-11 16:46:20 -07:00
Yuneng Jiang
9a80bf2ad4
test(e2e): harden JWT fixtures and cover management lifecycles 2026-09-11 16:36:12 -07:00
Kerry Lu
1270ecb781 test(load): drive /v1/messages alongside /chat/completions in the Redis chaos test
The Anthropic Messages route reaches the same Redis touchpoints and cost-tracking
callback through its own request path, so a failure-path regression there would not
surface from chat completions alone. Each simulated user now picks one endpoint round
robin and stays on it, and the per-endpoint split is asserted and reported so a run
that silently drove only one route fails instead of passing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-11 12:28:02 -07:00
Kerry Lu
1699f2d6dc Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_e2e_redis_timeout
# Conflicts:
#	uv.lock
2026-09-11 11:38:22 -07:00
Kerry Lu
33ec56ed75 test(e2e): rewrite the Redis timeout test as a locust chaos load test
The sequential version sent one request at a time, so a Redis outage never
reached the concurrency where the failed-tracking alert body actually grows.
This drives the proxy with locust against one model group of three mock
deployments, two failing at order 1 and one serving at order 2, so every
request spends its retries on the failing pair and lands on the serving
deployment through the order-based fallback. Two phases, a healthy baseline
and a CLIENT PAUSE WRITE window, and every request must succeed in both.

Latency, RSS and CPU are reported as p50/p90/p99 per phase rather than
asserted on: RSS and CPU come from psutil on the proxy's process tree, since
a multi-worker proxy serves /metrics from the prometheus multiprocess
collector and that drops the process collector's series. Thresholds stay open
until weekly runs give real baselines.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 21:55:23 -07:00
mateo-berri
dcf8228a9d test(e2e): memory regression test for failing requests on the release gate 2026-09-10 17:18:20 -07:00
Kerry Lu
1213d7d399 test(e2e): cover /embeddings and assert memory and fallbacks in the Redis timeout test
Add an embeddings case with its own closed-port primary and mock backup (the fallback map in
the gateway config gains the pair; LiteLLMParamsBody.mock_response accepts the list an embedding
mock needs). Assert from /metrics that the proxy's resident memory grows by no more than 200 MB
across each case where the process collector reports it (Linux), that the router counted a
successful fallback for every request, and that every spend row is a success.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZDULyJPp17ZFiJenRxs2T
2026-09-09 17:17:33 -07:00
ryan-crabbe-berri
2085a37b82 test(e2e): issue the JWT suite's tokens from a real Keycloak realm
The suite used to mint its own RS256 tokens from a stand-in issuer, which
could only ever prove the proxy agreed with the tests: the claims were
whatever the tests chose to sign. Every JWT bug worth catching lives in the
shape of what an identity provider really emits, so the suite now runs
against Keycloak (realm in idp_realm.json), provisions a group and a user per
test through its admin API, and signs in through the direct-access grant.

That changes what the tokens look like: sub is Keycloak's opaque user uuid
rather than a friendly name, groups arrives from a protocol mapper, aud is
the IdP's own audience, and the JWKS carries an encryption key beside the
signing key so the proxy has to select on kid. The expiry case now takes a
one-second token from a second client in the realm and waits for it to lapse
instead of forging a stale exp.

The proxy config the suite needs is unchanged. CI runs it against a Keycloak
deployed beside the ephemeral stack, which lives in the releaser repo.

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-09 16:56:14 -07:00
mateo-berri
5db99ca7ba test(e2e): register the poller batch cost join as its own uncovered cell
The retrieve writer and the CheckBatchCost poller are two different spend
writers, and the in-run failed batch only proves the first. Split the
key_attribution batch cell in two: retrieve_batch_cost_joins_retrieving_key,
which test_terminal_batch_cost_row_joins_the_retrieving_key claims, and
poller_batch_cost_joins_creating_key, which no test claims yet and so shows
up on the coverage dashboard as a P1 gap instead of hiding behind the
retrieve leg. The rationale records why one run cannot hand the poller a
completed batch on a stack that boots a fresh Postgres per build.
2026-09-09 16:23:45 -07:00
mateo-berri
654afb5477 fix(e2e): assert the batch cost join on an in-run failed batch instead of a cross-run baton
The batch list is served from LiteLLM_ManagedObjectTable whenever the managed
files hook is loaded, and the Buildkite e2e stacks bundle a fresh Postgres per
build, so a prior run's marker batch is never listed and the baton could only
ever pass vacuously. Each run now creates a batch OpenAI fails at validation
within seconds, retrieves it by its raw provider id with the same key until it
is failed, and asserts the {provider_batch_id}_batch_cost row that retrieve
writes joins the key's token hash and alias
2026-09-09 16:14:07 -07:00
Kerry Lu
939ac04a93 test(e2e): cover /v1/responses in the Redis timeout test and fail the primary for real
The Responses path returns a mock for any mock_response string, so the InternalServerError
sentinel never failed there. Point the primary deployment's api_base at a closed port instead,
which fails every endpoint the same way, then parametrize the test over /chat/completions and
/v1/responses and register both on the coverage cell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZDULyJPp17ZFiJenRxs2T
2026-09-09 16:03:36 -07:00
mateo-berri
07a0ca1074 fix(e2e): bill the batch cost baton through a raw-id retrieve and drive the callback replay writer
The completed marker batch is now retrieved by its raw provider id with this
run's key, so the proxy prices it inline and the {provider_batch_id}_batch_cost
row must join that key's token and alias. The CheckBatchCost poller only bills
batches it created in the same database, which a stack booted fresh per run
never holds for a completed marker, so the old unified-id assertion had no row
to find. Every run also replays one callback log through
POST /v1/rust_control_plane/logs, the third spend writer, and asserts its row
joins the key like the eight request paths
2026-09-09 15:54:11 -07:00
Kerry Lu
02351da51f test(e2e): register the Redis timeout cell at tier P1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZDULyJPp17ZFiJenRxs2T
2026-09-09 15:51:35 -07:00
Kerry Lu
058ff8c63c test(e2e): keep answering while every Redis command times out
Add tests/e2e/router/test_redis_timeout_e2e.py against a proxy booted from
tests/e2e/gateway/redis_timeout_ci_config.yml: a real Redis with socket_timeout 0.001, so every
command times out and the circuit breaker opens, plus a primary deployment that always fails and
falls back to a healthy one, so every request carries retry breadcrumbs into cost tracking. The
test drives twenty chat requests through the proxy and asserts each answers within ten seconds,
the last third is no slower than the first, /health/liveliness stays fast, and every request
still reaches the spend log.

Gate it behind the redis_timeout marker and E2E_REDIS_TIMEOUT, exclude it from the per-PR
e2e-changed selector, register the reliability.circuit_breaker.redis_timeout.stays_responsive
cell, and run it as its own job in the weekly load anomaly workflow with a Postgres and Valkey
service. Against a v1.100.0 proxy the run wedges the worker: requests time out and liveliness
stops answering (LIT-6780).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZDULyJPp17ZFiJenRxs2T
2026-09-09 15:43:50 -07:00
mateo-berri
3020a13e24 test(e2e): every spend row a virtual key writes joins its token across all write paths
One aliased key owned by a user with an email drives chat, queued chat,
messages, responses, embeddings, the Gemini passthrough, a batch file upload,
and a batch create against a live proxy. Each row must carry api_key equal to
the key's LiteLLM_VerificationToken.token and the alias in metadata, and
/spend/logs?api_key= and /user/daily/activity must report the key with its
alias and email. Health-check rows must keep the literal service-account key,
and the batch cost row for a completed marker batch must join the key that
created it. A re-hashed api_key (the v1.99.0 regression fixed by #39568 and
#39572) now fails the Buildkite e2e stage naming the write path

Resolves MAT-180
2026-09-09 14:34:12 -07:00
Yuneng Jiang
9d90a54491
revert(model-management): roll back #40047
This reverts commit e8e3172d7d

Restore the previous model update and router cost registration behavior while pricing compatibility is investigated
2026-09-09 00:06:26 -07:00
yuneng-jiang
e8e3172d7d
fix(model-management): honor an explicit null as a clear on model update (#40047)
* fix(model-management): honor an explicit null as a clear on model update

PATCH /model/{model_id}/update merged the patch with exclude_none and then
popped explicit nulls only for the mirrored pricing fields, so a null sent for
max_input_tokens, mode, supports_vision or any other key was dropped and a value
pinned by an earlier save could never be removed.

The route now follows JSON Merge Patch over both blobs: a key absent from the
body is unchanged, a key sent as null is removed from the stored row, and a key
sent with a value is set. Ownership and identity keys keep ignoring a null, as
do the fields the stored models require, since clearing one writes a row no
reload can rebuild. Mirrored pricing keys still clear from both blobs.

Clearing a price also needed the router to stop merging a deployment's cost-map
entry onto its previous registration, which left the old rate in place and kept
billing at a price the deployment no longer carried.

Adds a create, read, partial-update, clear, enforce, delete lifecycle e2e that
reads back on every replica, and a harness helper for that read-back.

* fix(router): keep a deployment id that names a real model from evicting its catalog entry

Deployments are keyed into litellm.model_cost alongside the built-in catalog, so
evicting a deployment's stale entry by id could take a real model's entry with it:
registering a deployment whose model_info.id is "gpt-4o" stripped that model's
pricing, context window and capability flags process-wide, for every other
deployment of it, until the next price-map reload.

Only evict an entry this registration owns. A colliding id keeps the previous
merge, which pollutes the catalog entry rather than emptying it.

Also pins the Admin UI round trip: the model edit form echoes the whole /model/info
row back on save, and that read reports every key the deployment never stored as an
explicit null, so the clear path has to leave those keys alone.

* fix(router): decide cost-map eviction by what this registrar created

The previous guard read a catalog entry off `litellm_provider`, so a deployment
that declares its own provider in model_info was treated as one and kept billing
at a price it no longer carried. It also only held for a single registration: a
second one under a colliding id saw the id the first merge left behind and
evicted the catalog entry anyway.

Track the cost-map keys this registrar creates instead. A key it created is
evicted before re-registration; one it did not is left to merge, which is what a
deployment id colliding with a catalog model name needs.

Also folds the required-fields comment into the docstring that already gives the
reason.

* fix(router): release a deployment's cost-map key when it is deleted

The ownership ledger only grew. A deleted deployment kept its claim, so if a
later catalog refresh started publishing a model under that same name, the next
registration would treat the catalog entry as the deployment's own and evict it.

Deleting a deployment now gives the key back, which also stops the ledger
growing for the life of the process.

* fix(router): hold a cost-map key while another live router still serves it

The claim is process-wide but the release was per-deletion, so with two routers
serving one deployment id, the first deletion put the survivor back on merging
and the price it had just cleared would keep billing.

Release the key only once no live router still serves that id.

* fix(router): register a router in the live set when it gains a deployment

_live_routers was only joined when a router was constructed with a model_list,
but a router built empty is populated through add_deployment, and the empty
branch exists for exactly that. Such a router was invisible to the live-router
scan, so deleting the deployment from another router released the shared
cost-map key while it was still serving that id.

Joining the set where a deployment enters the list covers every path, and it
also lets a price reload rebuild what a dynamically built router serves.

* fix(e2e): read the stored model row from the control plane, not each gateway

The lifecycle suite polled /model/info on every URL in PROXY_REPLICA_URLS. Those
URLs are the stack's gateways, and gateway/routes/allowlist.py trims them to the
LLM data-plane surface, so /model/info answers only on the backend and 404s on
every replica. All five tests failed at their first read-back in CI while passing
against a monolith, where one process serves both planes.

The stored row has one answer behind it, so it is read through the shared
transport, which routes control-plane paths to the backend. What every gateway
must agree on is which models it serves, so the create and delete steps poll
/v1/models per replica instead, a route the gateway does serve.
read_back_everywhere now rejects a control-plane path outright rather than
timing out on it.

Two things surfaced behind that. /public/ was missing from the transport's
control-plane prefixes, so model_cost_map() was routed to a gateway and 404'd,
and the billing steps needed a data-plane wait: a PATCH lands on the backend and
each gateway picks it up on its own config reload, measured here at 12-24s, so
they now drive calls until the new rate reaches the spend row and let the
deadline fail them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1S92J8gSxxKVe1JBzxWBF

* test(models): keep polling outcomes immutable and document shared ownership

* test: validate opaque stream IDs and hide log-reader credentials

* test: isolate auto-router scenarios and clean partial setup

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 06:10:18 +00:00
yuneng-jiang
36bd7f1138
fix(mcp): honor an explicit null on toolset update, cover MCP lifecycle e2e (#40022)
* fix(mcp): honor an explicit null on toolset update, cover MCP lifecycle e2e

PUT /v1/mcp/toolset dumped its payload with exclude_none, so a field sent as
null looked exactly like one the caller left out and the stored value
survived. An admin could not clear a toolset's description: the save reported
success and the old text came straight back. It now dumps with exclude_unset,
so absent keeps and null clears, which is what PUT /v1/mcp/server already did.
A null tools list clears the selection to empty, and a null toolset_name is
ignored because a toolset always has a name.

Adds create, read, partial-update, clear and delete e2e coverage for MCP
servers and toolsets, with every read-back polled on every replica so an edit
that lands on one replica and not another fails the test, plus an enforcement
test proving a key granted a toolset lists exactly that toolset's tools
against the real Datadog upstream.

* fix(e2e): refuse a read-back that no replica serves

A read-back over an empty replica mapping satisfied every predicate and
returned as if it had converged, so it would have asserted nothing and
passed. No wiring can produce that today, since the replica list always
falls back to at least one URL, but a helper whose whole job is proving a
write reached every replica should not have a shape that passes vacuously.

* fix(mcp): keep a null tools list a no-op on toolset update

Treating a null tools list as a clear meant an existing client that sends
tools=null during a partial update, meaning "leave the selection alone",
silently lost every tool the toolset grants. That is a permission surface,
so the quiet version of it is the worst version.

A toolset always has a tool list, the same way it always has a name, so a
null on either is now a no-op. Emptying the selection is an explicit [],
which cannot be confused with a field the caller left out, and which is
what the dashboard already sends.

* fix(e2e): keep MCP admin routes on the data plane

/v1/mcp/* is a lazily mounted feature, so a gateway registers it on the first
matching request, which happens after the startup route trim that drops
management endpoints. Routing it to the control plane therefore sent every MCP
call to the one backend process: the new lifecycle read-backs proved a single
process rather than every replica, and mcp_client's await_registered barrier
waited on a registry that does not serve the tools/list call it guards, so the
existing MCP suites polled a gateway that had not synced yet until poll_timeout

Verified against a two-gateway split stack (backend on 4001, gateways on 4010
and 4011, one postgres): both gateways answer /v1/mcp/server and /v1/mcp/toolset,
and each served 6 server reads and 7 toolset reads over the run

* fix(e2e): grant the toolset by the tool's own name, not the wire name

tools/list serves a tool as <prefix><tool_name>, but a toolset grants by the
tool's own name: resolve_toolset_permissions reads toolset.tools[].tool_name
straight through, and the prefix is added on the way out. The test built the
toolset from the names tools/list reported, so the grant matched nothing, the
scoped key listed no tools, and await_tools ran out its whole poll_timeout
before failing

Measure the prefix off search_datadog_logs, whose own name is known, rather than
guessing it from the alias, since the proxy can be configured to prefix with a
short server id instead. The expectation compared against tools/list stays in
wire names; only what the toolset stores crosses back

* test(mcp): build immutable lifecycle updates and replica results

* test: validate opaque stream IDs and hide log-reader credentials

* test: isolate auto-router scenarios and clean partial setup

* test: honor Datadog search rate-limit reset headers

* test: share the Datadog read-back deadline across retries

* test: preserve captured MCP toolset update fields
2026-09-08 22:50:13 -07:00
Yuneng Jiang
4044cd2f0c
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/guardrail-automation-testing-3ecd3d 2026-09-07 11:38:59 -07:00
yuneng-jiang
4b3355bdc6
test(e2e): prove the virtual key lifecycle on every gateway replica (#40023)
* test(e2e): prove the virtual key lifecycle on every replica

Walks one virtual key through create, read, partial update, clear, enforce
and delete against a live proxy and database, reading every write back on
every gateway replica.

The management suite already had single write-then-read tests for keys, but
none of them proved that a partial /key/update leaves the untouched fields
alone, that an explicit null clears a field, or that a write is visible on
more than the one gateway that took it.

Adds read_back_everywhere to the shared ProxyClient: it polls a GET path on
every URL in PROXY_REPLICA_URLS until each replica's parsed body satisfies
the caller's predicate, and fails naming the replica that never converged.
The CLEAR sentinel in the e2e models makes an explicit JSON null expressible
in a body the transport otherwise strips of None fields.

Documents /key/update's merge patch semantics on the endpoint docstring.

* test(e2e): prove key revocation and field preservation on every replica

Applies the findings from an adversarial review of the first commit.

The delete step only checked that chat was refused on the gateway that took
the write, so it would have passed while a sibling gateway kept serving the
deleted key. It now serves one call from every replica first, so each has the
key cached and the delete has something to revoke everywhere, then polls every
replica for the refusal.

The file also carried its own poll loop that tested the deadline before
attempting, so it gave up one attempt early and skipped the attempt landing
exactly on the deadline. It now shares the harness helper, which is generic
over the polled value rather than over a parsed body, so the same loop covers
both the info read-back and the chat refusal.

The model the enforcement step registers now carries a unique marker in its
alias, matching every other deployment this suite creates, so concurrent runs
never share one model group.

The docstring sentence claimed an explicit null clears any field. It does not:
the metadata-backed fields merge into stored metadata, where a null is a silent
no-op, and only the key's own columns clear. Regenerating the dashboard types
picks up the corrected text.

* fix(e2e): delete a deployment that never becomes servable

Registering a model posts /model/new and then waits for every replica to list
it. When that wait timed out the deployment already existed in the database but
its id had never been returned, so no caller could delete it and the row
outlived the run. It is now deleted before the failure propagates.

Found by review on the key lifecycle suite, whose module fixture registers a
deployment this way, but every caller of the shared helper had the same
exposure.

* docs(e2e): drop the duplicated notes from the lifecycle docstrings

The delete method restated what the warm-up helper already explains, and the
module restated the merge patch rule that the endpoint and the request model
both document.
2026-09-07 11:30:46 -07:00
Yuneng Jiang
5561d2e476
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/guardrail-automation-testing-3ecd3d 2026-09-07 10:08:15 -07:00
Mateo Wang
50f54b9c7e
Merge pull request #40014 from BerriAI/litellm_lit_7036_retry_policy_400s
fix(router): skip the refusing deployment when retrying a non-transient error
2026-09-07 09:38:43 -07:00
mateo-berri
d664ca139e chore(router): suppress the retry-skip kwargs writes and correct the filter docstring
The two writes that hand the skip list to the next attempt now carry a
`# rebind-ok` reason, which is the sanctioned escape hatch for an unavoidable
parameter mutation and matches how `log_retry` already writes into the same
kwargs dict a few lines above

`get_excluded_filtered_deployments`'s docstring said returning the unfiltered
list would re-include the deployment that just failed. The retry skip does
exactly that on purpose, so the docstring now says each caller decides what an
empty result means

The reliability registry cell the new e2e test claims is marked
`fail_before_fix: proven`: the same config returns 400 at the merge base and
200 off a sibling deployment at the tip
2026-09-06 00:53:24 -07:00
Yuneng Jiang
6b493cb61e
test(guardrails): pin the presidio spend-log record and the UI's masked-entity persistence
The presidio suite proved masking happened by reading the served answer, and the
UI suite proved the wizard's "Select All & Mask" produced a row. Neither checked
the thing an operator actually looks at afterwards: the audit trail.

Adds an e2e test asserting the spend log carries the pre_call guardrail record
for a masked request: status, provider, per-entity masked counts, and the
detected-entity list the dashboard's guardrail panel renders its scores from.
It keys off the x-litellm-applied-guardrails response header rather than masked
text in the answer, because whether the model echoes the prompt back is a model
decision, not a guardrail one. Both a mutation that stops writing the record and
one that empties the entity list turn it red.

Two records land on one log, pre_call and post_call, so the assertion selects on
mode as well as name; picking by name alone could hand it the empty post_call
record depending on write order.

On the UI side, the Presidio wizard test now reads the stored guardrail back and
asserts every persisted entity carries the MASK action. A row appearing in the
table did not prove the entity selection survived the save, so a wizard that
persisted an empty pii_entities_config would have passed.

SpendLogRow gains a typed metadata field. guardrail_response is left as object
because each provider writes its own shape there (presidio a list of entities,
bedrock an assessment object, a failed run the exception string); a union narrow
enough to be useful would fail to parse the others and break every suite that
reads a spend log. The caller validates the shape it expects with a TypeAdapter.

run_e2e.sh now pins PROXY_BASE_URL to the stack's own origin. The proxy builds
its post-login redirect from that variable when it is set, so a value inherited
from a developer's .env sent the browser off the relocated stack and the suite's
login step timed out on every port but 4000.
2026-09-06 00:43:13 -07:00
mateo-berri
116f88b023 fix(e2e-changed): keep the gate off suites the stack cannot run
The selector picked up two suites that can never pass in this stack, so
editing either one turned the check permanently red: the presidio masking
suite calls pytest.fail without an analyzer and anonymizer that up.sh
never starts, and the pipecat audio suite skips itself at import time
unless the NLTK punkt_tab data is present, which nothing installs.

tests/e2e/coverage_registry/test_collector.py had the same problem for a
different reason. Its nested pytest.main autoloads pytest-retry from the
ci group the workflow installs and dies with "INTERNALERROR: no option
named 'filtered_exceptions'", so the collect-only pass now disables that
plugin. The plugin's entry point is pytest-retry, not retry, so the same
one-word fix lands on mutmut's pytest_add_cli_args, where "-p no:retry"
was disabling nothing.

Two smaller holes in the harness: a canary argument the shell never
expanded used to select nothing and let the gate pass green, and a secret
that cannot be represented in both bash and dotenv was rejected without
naming the key.
2026-09-05 21:03:50 -07:00
ryan-crabbe-berri
90ac77e58e test(e2e): add JWT issuer harness and first live JWT auth tests
Add tests/e2e/jwt_issuer.py, a test-only RS256 issuer that serves a JWKS
document and signs arbitrary claims over an open loopback-only /token
endpoint, so e2e tests can mint tokens without ever holding a signing key.
One key per process keeps the proxy's cached JWKS valid for the whole run.

Add the first five live JWT auth tests in tests/e2e/other: a valid token for
an existing team is accepted and its spend row carries the claimed team and
user, a tampered signature and an expired token are refused with 401, a
token naming a team that does not exist is refused with 403, and a plain
sk- virtual key keeps working with enable_jwt_auth on. Harness unit tests
cover the issuer itself. Team and user create/delete land on the shared
ProxyClient (warn-only teardown, /user/delete typed as the int it returns)
instead of a fourth per-suite copy.

Document the issuer command, the E2E_JWT_ISSUER_PORT convention (default
4190), JWT_PUBLIC_KEY_URL and the litellm_jwtauth config block in
tests/e2e/CONTRIBUTING.md, and register the new cells in other.yaml.
2026-09-05 17:44:02 -07:00
mateo-berri
038a7c6ed7 test(e2e): widen the cooldown propagation window to 15s and trim the registry rows to the surface the cells drive
Replicas re-read cooldowns from Redis at most every 10s
(default_redis_batch_cache_expiry), so the 12s window left 2s of slack;
it is now 15s and the benched phase runs from 15s to 26s after the trip.

The reliability rows the new cells cover claimed exercised_on messages
too, but every cell drives /v1/chat/completions, so they now claim
chat_completions only. RouterSettingsOverride.timeout and
RouterCurrentValues.routing_strategy had no reader and are gone.
2026-09-05 03:07:56 -07:00
mateo-berri
6de9087f03 test(e2e): gate the prompt-cache cell behind an opt-in marker and take ten shuffle picks
The prompt-cache affinity test needs the prompt_caching pre-call check on the
proxy, which the CI stack does not carry until project-releaser #223 lands, so
it now sits behind a prompt_caching_stack marker that is deselected unless
E2E_PROMPT_CACHING_STACK is set, the same shape as managed_files. The
per-directory deselection hooks for weekly and managed_files move into the
parent conftest as one OPT_IN_MARKERS table, so the collector counts a gated
cell only where its env var is set (31/36 today, 32/36 with #223).

The simple-shuffle cell asked for three picks, which a shuffle ignoring the
weights passes one time in eight; it now asks for ten.
2026-09-05 02:10:06 -07:00
mateo-berri
f022b5eda8 test(e2e/batches): assert Bedrock batch cancel and list in the lifecycle
Bedrock batch cancel (StopModelInvocationJob) and the managed list view
both work through the proxy since LIT-4774, but the batches e2e still
gated them off and the coverage registry claimed no cell for either.
Flip can_cancel/can_list for the Bedrock provider, assert cancel the
same way the OpenAI leg does, add the two registry cells the gates
select, and update COVERAGE.md
2026-09-04 18:30:55 -07:00
ryan-crabbe-berri
f7accc4e29 test(e2e): drop the two mgmt registry cells no shared-proxy test can cover
mgmt.cache_settings.update.happy_path and
mgmt.config_override.hashicorp_vault.happy_path were the last two uncovered
Management/UI cells, and neither can be covered against the shared proxy the
e2e suites run on. Both routes reconfigure the whole process rather than a
resource the test owns.

/cache/settings persists whatever it receives into a row that outranks the YAML
cache_params and is re-applied on a timer, so a partial write downgrades a TLS
cluster to a plaintext standalone node and every later Redis call hangs. That is
what took out 60 of 72 tests on 2026-07-25 and got the original test removed in
PR #34664.

/config_overrides/hashicorp_vault has the same shape: a POST sets the HCP_VAULT_*
env vars, swaps litellm.secret_manager_client process-wide, and writes a row the
config-reload poll re-applies, so every os.environ/ lookup on the pod resolves
against the test's Vault until the DELETE lands. Its constructor also never
dials Vault, so a POST to a bogus address still returns 200 and a smoke test
built on it would pass for the wrong reason.

Keeping rows we have decided not to cover only inflates the denominator, so drop
them and record the reasoning where someone would go to write the test. Filing
the isolated-proxy harness they both need separately; the cells come back with
it.

Management/UI goes 75/77 to 75/75, headline 402/544 to 402/542.
2026-08-31 19:33:04 -07:00
ryan-crabbe-berri
e263c09e4f test(e2e): cover model access group budgets against a live proxy
Four cases in tests/e2e/quota_management/budgets, driving real OpenAI calls
through a group whose shared pool is drained to exhaustion: the spender key
stays blocked, a key that spent nothing of its own is blocked by the same
pool, a sibling group with no budget keeps serving, and the budget read
reports the spend drawn against the group.

Adds set/get/delete access group budget to BudgetClient and the four
matching rows to the coverage registry.
2026-08-29 15:46:49 -07:00
yucheng-berri
f0fadb7f99
test(e2e): add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure) (#38552)
* test: add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure)

Five new live e2e scenarios raising Logging & Guardrails registry coverage:
s3_v2 success and failure objects read back from the real S3 bucket,
gcs_bucket success record read back through the GCS JSON API (with
nextPageToken pagination and per-request bearer minting), team-scoped
Langfuse callback delivery with non-team isolation, and DataDog failure
event delivery queried by indexed model_group. datadog_reader gains
query-based variants of the marker search; the langfuse cell is a new
registry row. Bucket readers settle past a full flush interval so a
late duplicate cannot hide from the exactly-one assertions

* test: cover clock-skew day prefix in gcs read-back and retry team callback propagation

* test: key the s3 failure read-back on the provider error, not payload absence

* chore: rerun ci

* chore: rerun ci after config sync

* chore: rerun ci with pr lane env

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* test: add guardrail e2e coverage (presidio masking, bedrock post and during call, moderation on messages) (#38553)

* test: add guardrail e2e coverage (presidio masking, bedrock post/during, moderation on messages)

* test: require the phone placeholder positively in the presidio masking predicate

* test: count only the 400 verdict body as a bedrock post_call block

* test(e2e): exempt the guardrail config echo from the post_call leak assertion

* test(e2e): pin the fail-closed contract for an unknown guardrail name (skipped, product gap)

* test(e2e): tolerate the readiness 503 from a transient db blip in the callback-config probes
2026-08-29 09:43:44 -07:00
mateo-berri
2a15bc4fde test(e2e): cover Together reasoning_effort=none, json_schema, and cache-read pricing 2026-08-28 12:49:09 -07:00
Mateo Wang
c81ceba431
Merge pull request #38232 from BerriAI/litellm_e2e_bedrock_customer_matrix
test(e2e): cover the Bedrock provider-feature cells customers run
2026-08-26 09:34:07 -07:00
mateo-berri
a543b0348c test(e2e): cover Together AI reasoning, tool calls, template kwargs, and cost through a live proxy 2026-08-25 18:12:33 -07:00
mateo-berri
4b5e3db890 test(e2e): cover the Bedrock provider-feature cells customers run
Adds live e2e coverage for the Bedrock combinations behind recent customer
incidents: llm_provider-* response-header forwarding on /chat/completions
(nonstream and stream), regional us.anthropic.* inference-profile ids over
the invoke route, and the Admin UI Test Connection probe for a
responses-mode Bedrock Mantle deployment. Registers the matching cells in
the coverage registry and publishes the provider x feature matrix table in
its README.
2026-08-25 10:15:29 -07:00
mateo-berri
b9e5eec28c test(e2e): pin require_managed_files enforcement behind a marker-gated stack phase 2026-08-24 10:59:13 -07:00
Mateo Wang
a59e6115ba
Merge pull request #37903 from BerriAI/litellm_lit_5902_ws_passthrough_e2e_pin
test(e2e): pin the openai websocket passthrough prefixes
2026-08-21 18:25:19 -07:00
mateo-berri
601d6ff2c8 [e2e] Pin the OpenAI websocket passthrough prefixes
The websocket routes under /openai_passthrough and /openai had no e2e
coverage, so nothing catches the regression from issue #36088, where both
prefixes carried HTTP routes only and refused every upgrade with a 403
before a socket ever existed.

Two tests cover it. The realtime one opens /openai_passthrough/v1/realtime
and asserts OpenAI's own session.created frame comes back, which proves the
route is registered and relayed upstream. The responses one asserts
/openai/v1/responses accepts the upgrade, since a responses.connect socket
waits for the client to speak first and has no opening frame to check.

A refused upgrade is an HTTP response rather than a close frame, so both
assert on the handshake. ws_base_url moves into e2e_config now that a
second suite needs it
2026-08-21 17:26:18 -07:00
mateo-berri
73e1863d97 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit_5730_batches_completion_e2e 2026-08-21 17:08:24 -07:00
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