Commit graph

81 commits

Author SHA1 Message Date
Tin Chi Lo
b92b7cc943 refactor(e2e): drop the unused conformance/prompts/resources foundation
Removes the auth_forwarder key-stamping relay (unused; raw httpx), the stub's
/conformance mount and its fixtures, MCP_STUB_CONFORMANCE_URL, the unused
prompt/resource client ops, and the unclaimed passes_official_conformance
registry cell. None of it was exercised by a test; the suite stays scoped to
the core happy paths against the deterministic stub.
2026-07-16 19:25:52 -07:00
Tin Chi Lo
644d1ec28f test(e2e): a stored per-user token lets the key in Authorization list and call tools
Positive companion to the Authorization challenge-guard: once a user has
authorized (per-user token stored via the x-litellm-api-key dance), a plain
Authorization: Bearer <key> session needs no dance and the gateway serves
tools using the stored token. Seeds the token via the dance, then lists and
calls over an Authorization-key session, and asserts the upstream received the
stored per-user token (not the key) with no key leak. Claims the previously
unclaimed mcp.{list_tools,call_tool}.bearer.succeeds cells. Green on stock.
2026-07-16 17:41:34 -07:00
Tin Chi Lo
4fba2f440f refactor(e2e): make the Authorization-header OAuth test a challenge-guard, not a full dance
The Authorization-header variant cannot complete the interactive round-trip:
once the host finishes the OAuth dance the SDK carries the minted upstream
token in Authorization, evicting the LiteLLM key the gateway needs to resolve
the per-user token. Assert the achievable, correct contract instead: a key in
Authorization on a gateway-managed oauth2 server gets the 401 challenge, not a
masked empty tool list. Full list-and-call stays on the x-litellm-api-key
variant. Red on stock main (masking); flips green with the mode-aware gateway
challenge fix.
2026-07-16 15:39:06 -07:00
Tin Chi Lo
5880135418 test(e2e): assert the server record round-trips in the ingress rejection tests 2026-07-16 13:03:24 -07:00
Tin Chi Lo
9b144f1311 test(e2e): run the list-and-call happy path through the interactive OAuth flow under both ingress headers 2026-07-16 12:49:25 -07:00
Tin Chi Lo
9fb7271ac6 refactor(e2e): name the interactive OAuth test as the list-and-call happy path it exercises 2026-07-16 12:49:25 -07:00
Tin Chi Lo
6c0d405ef4 refactor(e2e): scope the mcp suite to the core happy paths, keeping the conformance-ready foundation 2026-07-16 12:49:25 -07:00
Tin Chi Lo
c1e5ca785b test(e2e): run the official MCP conformance suite through the gateway and cover prompt/resource primitives 2026-07-16 12:49:25 -07:00
Tin Chi Lo
1b42f655b3 test(e2e): cover the interactive authorization_code MCP OAuth flow end to end 2026-07-16 12:49:25 -07:00
Tin Chi Lo
b1442d0398 refactor(e2e): unroll the auth-header matrix into explicitly named per-header tests 2026-07-16 12:49:25 -07:00
Tin Chi Lo
d32974c282 refactor(e2e): build wire headers and mint keys inline in the mcp test bodies 2026-07-16 12:49:25 -07:00
Tin Chi Lo
9ff988d7ef refactor(e2e): split the mcp gateway suite into feature-scoped specs with a parametrized auth-header matrix 2026-07-16 12:49:25 -07:00
Tin Chi Lo
ff3ef9d7e8 refactor(e2e): keep the concurrency test self-contained with local tuning values 2026-07-16 12:49:25 -07:00
Tin Chi Lo
23dffc7453 fix(e2e): use a plain union assignment instead of a PEP 695 type statement 2026-07-16 12:49:25 -07:00
Tin Chi Lo
565a87212c refactor(e2e): inline the concurrency burst in the test via a thread pool 2026-07-16 12:49:25 -07:00
Tin Chi Lo
d74cf13489 fix(e2e): restore uv.lock exclude-newer anchor and gate proxy startup on mcp-stub health 2026-07-16 12:49:25 -07:00
Tin Chi Lo
5edc486928 test(e2e): cover MCP gateway auth headers and per-server max concurrency 2026-07-16 12:49:08 -07:00
mubashir1osmani
ebdf0bbfd7
chore(e2e): establish litellm_e2e_staging integration line (#33502)
* chore(e2e): establish litellm_e2e_staging integration line

Long-lived berri branch for e2e suite recovery work (LIT-4479 through LIT-4486) before merge to litellm_internal_staging

* test(e2e): remove langfuse_otel logging e2e suite (#33558)

* test(e2e): remove langfuse_otel logging e2e suite

Removes the LIT-4483 dynamic per-team/key/org langfuse_otel logging e2e tests (tests/e2e/logging/test_langfuse_e2e.py, added in #32857). The shared logging_client harness and the langfuse coverage-registry cells are left in place; only the test module is removed. The otel and prometheus logging e2e suites are unaffected.

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

* test(e2e): drop orphaned langfuse coverage-registry cells

The three logging.langfuse.*.logs_spend P0 cells were only exercised by the deleted langfuse_otel e2e suite. Remove them so the coverage registry has no orphaned rows.

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

---------

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

* fix(e2e): log into the react admin ui in the management browser fixture (#33562)

The management ui_page fixture drove the old server-rendered login form: it clicked input[type="submit"] and treated wait_for_url("**/ui/**") as the done signal. /ui/ now serves the react (antd) dashboard whose submit is a <button type="submit">, so the click waited out the full 30s timeout and errored every browser test in the suite. wait_for_url also matched instantly because the login page already lives at /ui/, so on the fast path the fixture navigated before the auth cookie landed and got bounced back to login.

Click the antd submit button and wait for the token cookie loginCall sets on document.cookie, the real post-login signal.

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

* fix(e2e): make ui login readiness robust to httpOnly token cookies (#33564)

The login readiness check waited only on document.cookie including token=, which is empty when the token cookie is httpOnly. If the server ever sets it via a Set-Cookie header, the wait would spin to the 30s timeout and silently reproduce the original hang. Also accept the login form detaching (#username gone after the post-login redirect) so readiness holds regardless of how the cookie is delivered.

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

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Mubashir Osmani <mubashir@berri.ai>
2026-07-16 12:09:24 -07:00
mubashir1osmani
3f5ed5a9c8
fix(e2e/claude_code): unblock stage collection, align proxy env names, register compat models (#33433)
* refactor(e2e/claude_code): align proxy env names with the rest of tests/e2e

Every claude_code compat cell used to read its own `LITELLM_PROXY_BASE_URL` and `LITELLM_PROXY_API_KEY` and duplicate the same 12-line "missing env, hard fail" block. The rest of `tests/e2e/` reads `LITELLM_PROXY_URL` and `LITELLM_MASTER_KEY` from `e2e_config.py`, so anyone standing up a live proxy for one suite had to export a second spelling for claude_code, and every cell repeated the same boilerplate.

Centralize the resolution in `claude_code/_env.py`. `resolve_proxy()` prefers the suite-wide `LITELLM_PROXY_URL` / `LITELLM_MASTER_KEY` names and falls back to the legacy pair so existing CI wiring on stage keeps working during the roll-out. `require_proxy(compat_result)` is the one-liner cells call to bind `(base_url, api_key)` or hard-fail with a message that names both spellings.

55 cell files, `_basic_messaging.py`, and the driver's own unit-test fixture now go through the helper. `run_compat.sh` accepts either spelling and normalizes to the primary names before invoking pytest. `cron_vm/run_daily.sh` exports the primary names when launching pytest.

`_pr_gate_unit_tests/test_env_resolution.py` pins the resolution rules so a future edit cannot silently reintroduce the drift: primary names win on tie, legacy names still resolve when primary is unset, mixed URL-primary key-legacy still resolves, empty-string exports are treated as unset, `require_proxy` names both spellings in its error message.

Net diff: 71 files, +370/-1240.

* fix(e2e): anchor claude_code Bash pin at parents[1] so container run collects

`test_bash_tool_restrictions.py` derived `REPO_ROOT = Path(__file__).resolve().parents[4]` and then joined `tests/e2e/claude_code/<feature>`. That works locally, but the stage container mounts tests/e2e/ at /app/e2e/, so parents[4] resolves to filesystem root and the `_bash_cells()` assertion looks for `/tests/e2e/claude_code/tool_use` — a path that doesn't exist. Collection interrupts before any test runs, so the entire e2e suite appears broken.

Fix: `CLAUDE_CODE_DIR = Path(__file__).resolve().parents[1]` resolves to the sibling `claude_code/` dir in either layout, and the `relative_to(REPO_ROOT)` calls become `relative_to(CLAUDE_CODE_DIR)` so test IDs and error messages read the same.

Adds `test_claude_code_dir_anchor_is_layout_independent` as a regression pin: it checks the anchor lands on a directory named `claude_code` that contains this test file, which would fail under the old parents[4] anchor when run from /app/e2e/.

* feat(e2e/claude_code): register compat deployments via /model/new from a session fixture

Every compat cell hardcodes a virtual model name like `claude-sonnet-4-6` or `claude-sonnet-4-6-bedrock-invoke` and hits the proxy expecting it to be routable. On stage those live in the deployed model_list; locally the `docker-config.yaml` under tests/e2e/ only declares one of them, so anything past haiku 400s with `Invalid model name`.

`claude_code/test_config.yaml` is the ground-truth compat matrix config the deployment already uses. `_compat_models.py` loads it, normalizes the yaml keys pydantic would silently drop (vertex_ai_* → vertex_*), and selects the subset whose provider credentials are present in the environment. An autouse session fixture in `conftest.py` POSTs each selected deployment to `/model/new`, blocks until it is servable on the data plane, and tears them all down on session exit. Skips silently when the proxy env is unset so pure-unit runs stay hermetic.

`test_compat_models.py` pins the invariants that keep this safe. Every cell-referenced name must have a yaml entry (drift check catches a cell probing a name the fixture never registered); the yaml has no unused declarations; the fixture registers exactly 15 deployments (3 tiers × 5 provider surfaces); vertex_ai_* yaml keys populate the pydantic body's vertex_* fields (they got silently dropped historically); Azure needs both AZURE_FOUNDRY_* env vars; Bedrock lifts creds from the ambient AWS chain; Vertex needs both the yaml refs AND ambient GCP credentials.

* refactor(e2e/claude_code): inject env + runner instead of monkeypatching

`require_proxy` and `_basic_messaging.run_basic_messaging_cell` now take the env mapping (and the CLI runner) as constructor-style arguments with `os.environ` and `run_claude_models_parallel` as defaults. Tests exercise the branching by passing dicts and callables directly, so `monkeypatch.setenv` and `monkeypatch.setattr(_basic_messaging, "run_claude_models_parallel", ...)` are gone from every unit test in this refactor's blast radius.

`test_env_resolution.py` drops the `monkeypatch.setenv`/`delenv` fixtures and passes `env={...}` dicts to `require_proxy`. Added a new pinned check that a successful resolution leaves `compat_result` untouched, and split the "unset env" test into three explicit shapes (empty, primary-only, legacy-only) so a regression that swaps the precedence rule can no longer hide behind a single monkeypatched fixture.

`test_basic_messaging.py` (driver) replaces the `_install_fake_runner(monkeypatch, ...)` helper with `_make_fake_runner(...)` that returns a `(callable, captured_dict)` pair the test passes in via the helper's new `runner=` kwarg. Also drops the autouse `_proxy_env` fixture in favor of a module-level `_PROXY_ENV` dict each test wires through the helper's new `env=` kwarg. Added a regression pin that a missing-env call hard-fails without ever invoking the runner (so the guard order stays correct).

`test_run_daily_pytest_scrubs_env.py` updates its pin to assert the new suite-wide env spellings (`LITELLM_PROXY_URL` / `LITELLM_MASTER_KEY`) instead of the legacy `LITELLM_PROXY_BASE_URL` / `LITELLM_PROXY_API_KEY` that `run_daily.sh` used to export.

* handwrote rules
2026-07-16 11:05:31 -07:00
yucheng-berri
edc30ea515
test(e2e): datadog log delivery for successful chat, messages, and responses (LIT-4447) (#33415)
* test(e2e): datadog log delivery for successful chat, messages, and responses

Covers logging.datadog.success.exports_metric on all three routes: one
successful non-streaming call must reach the DataDog logs intake as exactly
one log event whose StandardLoggingPayload message carries the model group,
real token counts, and a response cost equal to the x-litellm-response-cost
header of the same response. Delivery is judged at the intake: the compose
stack gains a dd-sink service recording every batch the datadog callback
ships via the DD_BASE_URL testing override, and a typed reader replays it.

Writing these caught a live product bug: /v1/messages double-logs every
success (two byte-identical events per call), filed as LIT-4447; the messages
test tolerates byte-identical duplicates of the one event until it lands,
while a second differing event still fails

* test(e2e): address review findings on the datadog delivery suite

Consolidates the fresh-key first_ok helper into logging_client now that the
otel PR it mirrored has merged (both test files use the shared copy), moves
intake batch parsing into a helper so no path can leave the batch unbound,
and gives the sink's /health endpoint a truthful text/plain content type

* test(e2e): tolerate same-logical-event duplicates by call id, not byte identity

A clean LIT-4447 repro showed the duplicated payload is built twice and can
mint a fresh synthetic completion id per emission, arriving as two separate
intake POSTs with the same litellm_call_id and identical substantive fields.
Byte-identity was therefore a flaky criterion; duplicates now qualify only
when they share the call id, call type, model group, tokens, and cost, and a
second differing event still fails

* test(e2e): assert the scenario strictly; the messages test is the LIT-4447 regression pin

Per review direction the tests now assert exactly what the scenario promises:
exactly one DataDog log event per successful call, on every route. The
/v1/messages test therefore fails on current code against the known
double-log (LIT-4447) and is its regression pin; it goes green when the fix
lands. The duplicate-tolerance machinery is removed

* Simplify docstrings for DataDog log tests

Removed redundant phrasing about cost cross-checking in docstrings.

* Update test_datadog_log_e2e.py
2026-07-16 09:54:07 -07:00
Mateo Wang
906897bebf
Merge pull request #33473 from BerriAI/litellm_claude_code_passthrough 2026-07-15 20:22:15 -07:00
mateo-berri
c4fee0eafe test(e2e/claude_code): retry rate-limit-shaped CLI failures with backoff 2026-07-15 18:06:28 -07:00
devin-ai-integration[bot]
fac43df9b9
fix(complexity_router): return empty dict from _classifier_call_metadata when metadata is absent (#33452)
* fix(complexity_router): return empty dict from _classifier_call_metadata when metadata is absent

The LLM classifier reads request_kwargs.get("litellm_metadata"), but the proxy stores request metadata under "metadata", so this returned None. _classifier_call_metadata then passed None straight through to the classifier acompletion call, which assumes a dict and blows up with 'NoneType' object has no attribute 'update'; the router swallowed it and silently fell back to heuristic scoring, so the configured LLM classifier never ran. Returning an empty dict keeps the classifier call well-formed.

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

* test(e2e): cover complexity-router LLM classifier routes over the proxy

Add a live e2e regression for the complexity auto-router: a lexically simple but hard prompt ("Is P equal to NP?") is routed by the LLM classifier to the higher-tier anthropic backend, read back from the spend log's model. Before the metadata fix the classifier silently crashed and the router fell back to heuristic SIMPLE scoring on the openai backend, so this test fails pre-fix and passes post-fix.

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

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-15 17:46:00 -07:00
mateo-berri
79bfe2e996 docs(e2e/claude_code): document the known-red azure passthrough cell (anthropic-version dropped by the /azure fallback) 2026-07-15 17:25:56 -07:00
mateo-berri
ec4639fc0e test(claude_code): rename misleading REPO_ROOT to SUITE_ROOT in test_v0_layout 2026-07-15 16:23:26 -07:00
mateo-berri
95fe8687c8 test(e2e/claude_code): add passthrough matrix row for the big-3 clouds and Anthropic API
Adds a 'passthrough' feature row to the Claude Code compat matrix that
drives the real claude CLI in each cloud's native mode against
LiteLLM's passthrough routes (the LLM-gateway setup from
code.claude.com/docs/en/gateway) instead of the /v1/messages
translation layer:

- anthropic: ANTHROPIC_BASE_URL={proxy}/anthropic, forwarded verbatim
  to api.anthropic.com
- bedrock_invoke: CLAUDE_CODE_USE_BEDROCK=1 against {proxy}/bedrock;
  the router resolves the alias in /model/{alias}/invoke-with-response-stream
- vertex_ai: CLAUDE_CODE_USE_VERTEX=1 against {proxy}/vertex_ai/v1;
  alias, project, location and credentials resolve from the deployment,
  which now sets use_in_pass_through: true (and the canonical
  vertex_project/vertex_location param names) in test_config.yaml
- azure: CLAUDE_CODE_USE_FOUNDRY=1 against {proxy}/azure via the
  AZURE_API_BASE/AZURE_API_KEY fallback (documented in the cron env
  example)
- bedrock_converse: not_applicable; Claude Code has no Converse-wire
  client

The shared cell body lives in _passthrough.py with injectable runner
and env (no monkeypatching), unit-covered in
_driver_unit_tests/test_passthrough.py including pins on the per-mode
CLI env contracts captured from a real claude CLI (2.1.210) run
against a request-logging sink.
2026-07-15 16:22:02 -07:00
yucheng-berri
d6f498ff5c
test(e2e): failed request error span carries the full untruncated message and status (LIT-4179) (#33304)
* test(e2e): failed request error span carries the full untruncated message and status

Covers logging.otel.failure.exports_metric on chat_completions: a request that
fails at the provider (invalid upstream key deployment) must export one
complete trace whose gen-AI span carries the LIT-4179 error contract, declared
as one reviewable payload (EXPECTED_ERROR_SPAN_ATTRIBUTES) plus an untruncated
error.message proven by parsing the embedded provider error JSON back out of
the attribute. The root SERVER span must record the 401 the client received.
Adds STORE_MODEL_IN_DB to the compose stack so /model/new works locally, which
the suite's model-registering tests already assume

* test(e2e): clean failure diagnostics on the error-span contract per review

A truncated error.message with missing braces now fails with a readable
assertion instead of an unhandled ValueError, an unparseable embedded JSON
fails via pytest.fail with the truncation context, and the retry loop now
asserts the upstream provider failure was actually observed so a fresh-key
propagation deadline cannot masquerade as a trace-export failure

* test(e2e): pin the full error attribute set including the litellm.provider.error keys

The LIT-4179 fix restored error.message/code/stack_trace/llm_provider; a later
refactor (#32591) moved the litellm-specific keys under litellm.provider.error.*,
which the initial contract missed. The payload now pins error, error.type,
otel.status_code, litellm.provider.error.code=401, and
litellm.provider.error.llm_provider=anthropic exactly, plus non-empty
litellm.provider.error.stack_trace and the untruncated error.message

* test(e2e): author the error-span test docstring
2026-07-14 22:13:13 -07:00
yucheng-berri
817582e697
test(e2e): otel trace completeness on streaming chat, messages, and responses (LIT-3787) (#33234) 2026-07-14 20:23:54 -07:00
devin-ai-integration[bot]
56f4dbf60a
test(claude_code): move the Claude Code compatibility matrix under tests/e2e (#32548)
* test(claude_code): move the Claude Code compatibility matrix under tests/e2e

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

* ci(claude_code): drop the CircleCI compat PR gate; the matrix runs in the scheduled e2e suite instead

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

* ci: restore the upload-coverage job dropped by mistake with the compat gate

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

* fix(e2e/claude_code): print rate-limit summary on failed compat runs and fix stale run_daily.sh header comments

* test(claude_code): assert fine-grained tool streaming via input_json_delta instead of an event-count floor

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

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-07-14 19:19:03 -07:00
mateo-berri
a0c4e4684a test(e2e): scope virtual keys to the deployment under test 2026-07-14 17:12:15 -07:00
mateo-berri
04193649ee Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_e2e_bedrock_mid_system_cache 2026-07-14 15:33:19 -07:00
mateo-berri
0f28b1114e refactor(e2e): share anthropic cache-control shapes in endpoints_client 2026-07-14 15:11:34 -07:00
mateo-berri
6874271db4 docs(e2e): add cache_hit to the naming grammar assertion vocabulary 2026-07-14 15:00:31 -07:00
Mateo Wang
6ad2f85e0c
Merge pull request #32914 from BerriAI/litellm_e2e_key_rate_limit_coverage
test(e2e): cover key rpm/tpm rate limiting, window reset, and pacing headers
2026-07-14 14:12:28 -07:00
mateo-berri
a21669aaef refactor: make the code easier to read 2026-07-14 13:58:23 -07:00
mubashir1osmani
edd3bce0ec
fix(e2e): bound spend-log snapshots to a /spend/logs/v2 window (#33265)
The rate-limited batch spend test snapshotted unattributed rows via the
unpaginated /spend/logs whole-table read, which grows with the environment
(58MB on stage) and OOMKilled the e2e runner at its 512Mi limit on every
scheduled run. Gateway.spend_logs_window pages /spend/logs/v2 over an
explicit date window instead, and SpendLogsParams now rejects a filterless
read so the whole-table call cannot come back
2026-07-14 13:32:10 -07:00
yucheng-berri
6a213de9f4
test(e2e): otel trace completeness on /v1/messages (#33133)
* test(e2e): OTEL trace completeness on /v1/messages

Extends the LIT-3787 trace-completeness suite to the Anthropic-native route:
one successful non-streaming /v1/messages call must land at the destination as
ONE connected trace (root SERVER span + auth/db/cost children + gen-AI CLIENT
span, no dangling parents). Adds the raw /v1/messages sender to the logging
suite client.

* test(e2e): reuse the shared AnthropicMessagesBody per review

Drops the duplicate /v1/messages request model in favor of the one models.py
already provides (budget_client uses the same one), passes max_tokens at the
call site to match the sibling chat test, notes in the docstring why the
gen-AI span is named chat on this surface, and adopts the hardened read-back
signature

* test(e2e): author the messages trace test docstring

* test(e2e): declare the messages surface on the covers marker

* test(e2e): otel trace completeness on /v1/responses (#33134)

* test(e2e): OTEL trace completeness on /v1/responses

Extends the LIT-3787 trace-completeness suite to the OpenAI Responses API
route: one successful non-streaming /v1/responses call must land at the
destination as ONE connected trace. Adds the raw /v1/responses sender, a
CHEAP_OPENAI_MODEL config constant, and registers responses in the otel
registry cell's exercised_on.

* test(e2e): author the responses trace test docstring

* test(e2e): declare the responses and chat surfaces on the covers markers
2026-07-13 20:19:06 -07:00
Mateo Wang
ab87ebe26d
Merge branch 'litellm_internal_staging' into litellm_e2e_key_rate_limit_coverage 2026-07-13 19:44:57 -07:00
yucheng-berri
948a43cd64
test(e2e): otel trace completeness on /chat/completions (#33132)
* test(e2e): OTEL trace completeness on /chat/completions against a local Jaeger destination

Adds the logging-suite infrastructure for LIT-3787 trace-completeness coverage:
a jaeger service in the compose stack as the OTEL v2 destination (arize_phoenix
preset pointed at it via PHOENIX_COLLECTOR_HTTP_ENDPOINT, so gen-AI spans export
through a preset-owned provider - the code path where trace splits happen), a
typed Jaeger query read-back client, and the first test: one successful
non-streaming /chat/completions call exports ONE complete trace (root SERVER
span + auth/db/cost children + gen-AI CLIENT span, no dangling parents).

* test(e2e): harden the otel trace read-back per review

Jaeger reads now query server-side by the litellm.call_id span tag instead of
paging recent traces and filtering client-side; the compose stack's background
jobs alone can push a request trace past the page. A failed query hard-fails
instead of reading as an empty result, the settle predicate now also waits for
the prefix-matched db span the assertion demands, parent-chain walking follows
CHILD_OF references only, the zero-trace and split-trace failures get distinct
messages, jaeger gets a healthcheck so the depends_on condition is accurate,
and the chat docstring names the route the code actually asserts

* test(e2e): author the chat trace test docstring

* Update logging section in CLAUDE.md

Removed mention of OTEL trace-tree completeness from logging integration section.
2026-07-13 19:06:12 -07:00
mateo-berri
a76dd9bf8e test(e2e): cover model-aware mid-conversation system handling on Bedrock Invoke /v1/messages 2026-07-11 17:08:16 -07:00
mateo-berri
36dff0f63a test(e2e): source the ratelimit suite's model from E2E_CHEAP_ANTHROPIC_MODEL 2026-07-11 16:15:16 -07:00
mateo-berri
ca55fc2deb refactor(e2e): model the tpm spend loop's two outcomes as values 2026-07-11 16:15:16 -07:00
mateo-berri
9f55925c31 test(e2e): name the tpm window deadline's latency margin 2026-07-11 16:15:16 -07:00
mateo-berri
6f2435b54a test(e2e): fail the rpm reset test when the limiter resets early 2026-07-11 16:15:16 -07:00
mateo-berri
1ca1a9cd03 test(e2e): assert the tpm block at its exact token crossing instead of a call-count heuristic 2026-07-11 16:15:16 -07:00
mateo-berri
b9aef1b810 test(e2e): cover key rpm/tpm rate limiting, window reset, and pacing headers 2026-07-11 16:15:16 -07:00
mateo-berri
d37daf0b9f refactor(e2e): move budgets and spend_tracking suites under quota_management 2026-07-11 16:14:21 -07:00
mateo-berri
8225ddf2a4 fix(e2e): name the route the spend_calculate registry cell actually exercises 2026-07-11 16:13:48 -07:00
mateo-berri
320a55f01f refactor(e2e): bucket rate limits, budgets, and spend tracking under quota_management 2026-07-11 16:13:48 -07:00
yucheng-berri
69c5839cc0
fix(guardrails): filter Add-Guardrail mode dropdown per provider (#32712)
* fix(guardrails): filter Add-Guardrail mode dropdown per provider

The GET /guardrails/ui/add_guardrail_settings endpoint returned every
GuardrailEventHooks value in one flat supported_modes list, so the Admin
UI rendered pre_mcp_call as a selectable Mode for every guardrail. Saving
Content Filter or Tool Permission with pre_mcp_call then failed with a
400 because those guardrails' server-side supported_event_hooks list
excludes it.

Expose each guardrail's supported hooks as a get_supported_event_hooks
classmethod on CustomGuardrail (mirrors the existing get_config_model
pattern) and have the endpoint iterate guardrail_class_registry to build
a supported_modes_by_provider map. The UI Mode dropdown filters by that
map when the selected provider is known and falls back to the global
list otherwise. __init__ now sources its own supported_event_hooks list
from the classmethod so the two sides can't drift.

Also register BedrockGuardrail, ToolPermissionGuardrail, lakera,
lakera_v2, and presidio in guardrail_class_registry so they participate
in the map (they were previously only in guardrail_initializer_registry
and had no class-registry entry).

Behavior change: guardrails that previously had no supported_event_hooks
declared (aim, javelin, azure/text_moderation, cato_networks,
crowdstrike_aidr, headroom, hiddenlayer, lasso, noma, onyx,
prompt_security, qualifire, repelloai, zscaler_ai_guard, aporia_ai,
lakera_ai, lakera_ai_v2, mcp_jwt_signer, model_armor, presidio) now
validate the configured mode at instantiation. Existing configs where
the mode was silently a no-op will fail at proxy startup with a clear
validation error rather than running as a broken guardrail.

Resolves LIT-4226

* fix(guardrails): add LITELLM_STRICT_GUARDRAIL_MODES escape hatch, preserve current mode in edit form

Address Greptile P1 (startup break) and P2 (edit form UX):

LITELLM_STRICT_GUARDRAIL_MODES defaults to true (raise on unsupported
event_hook, unchanged behavior for the guardrails validated pre-PR).
Setting it to false logs a warning and continues, giving deployments an
opt-out while they fix configs that now surface as errors instead of
silently no-op'ing. Regression test covers both modes.

Edit form now surfaces the currently-saved mode even when it is not in
the filtered per-provider list, so a legacy row (e.g. content_filter
saved with pre_mcp_call before this fix) no longer disappears from the
dropdown; the option renders with a 'not supported by <provider>' note
so the user knows to pick another.

* fix(guardrails): correct audited hook lists, prune stale modes on provider switch, clean form lint

Audited every get_supported_event_hooks classmethod against the hooks
each guardrail's own tests exercise and its handler methods. Five were
too narrow and their tests caught it in CI: rubrik gains pre_call,
presidio gains during_call and pre_mcp_call, prompt_security, onyx and
qualifire gain during_call. The remaining classes match either their
original __init__ declarations or their exercised modes exactly.

Cursor review fixes: the Add form now drops selected modes the new
provider does not support when the user switches providers, so a
pre_mcp_call selection cannot ride along into a provider that rejects
it at save; the edit form handles list-shaped stored modes instead of
treating mode as always a string.

Extracted shared toModeArray and getSupportedModesForProvider helpers
into guardrail_info_helpers so both forms use one implementation, typed
the remaining any usages in both forms, removed nested ternaries, and
committed the ratcheted-down eslint metrics and pruned suppressions
2026-07-11 14:51:27 -07:00