litellm/tests/e2e/coverage_registry
Yassin Kortam 2fd7929dc3 test(e2e): cover tool-permission, tool-policy and llm-as-a-judge guardrails
Adds live coverage for three guardrails litellm implements itself, each driven
against a real chat completion and asserting on the permitted/denied tool or the
judge's verdict rather than a bare status code.

tool_permission is registered allow-list style (one allow rule,
default_action=deny, on_disallowed_action=block): an unlisted tool is rejected
pre-call with the denied tool named, and the listed tool survives the guardrail
and is called by the model. tool_policy is exercised through a key-scoped
blocked-tool override set with POST /v1/tool/policy: the blocked tool is
rejected pre-call while a sibling tool on the same key and the same guardrail
still reaches the model, so the block is attributable to the policy rather than
to the guardrail refusing all tool use. llm_as_a_judge scores the model's answer
against a French-only criterion; an English answer is rejected with 422 and the
failing verdict, and a French one comes back to the caller.

The judge test does not stop at the response. That guardrail fails open on any
internal error, and a fail-open returns an ordinary 200 that is identical to an
approval: same status, same body, and the applied-guardrails header still names
it. So both halves also read the guardrail's own run log at
/guardrails/usage/logs, where an approval is recorded as `passed`, an
intervention as `blocked`, and a fail-open as `flagged`. Without that leg the
accept half would pass just as happily against a build where adjudication never
ran.

Also corrects the llm_as_a_judge registry row. It asked for a pre_call block,
which the guardrail cannot do: it supports post_call only and the proxy rejects
registering it at pre_call outright, so the row could never go green as written.
Retargeted to post_call, which is what the guardrail actually enforces.

The judge runs on openai/gpt-4.1 rather than the suite's usual gpt-5.5 because
the guardrail hardcodes temperature=0 on its judge call, gpt-5.5 accepts only
the default temperature, and the resulting error is swallowed into a fail-open,
so gpt-5.5 can never adjudicate anything.

Covers guardrail.tool_permission.pre_call.blocks,
guardrail.tool_permission.pre_call.allows, guardrail.tool_policy.pre_call.blocks
and guardrail.llm_as_a_judge.post_call.blocks.

The guardrail params, the tool-policy override bodies and the chat helper live
in a suite-local module so the shared harness is untouched.
2026-07-27 15:54:40 -07:00
..
__init__.py test(e2e): add coverage registry and collector (#32304) 2026-07-07 15:51:20 -04:00
collector.py ci: gate tests/e2e on zero basedpyright errors in pre-commit and lint CI 2026-07-11 10:25:22 -07:00
guardrail.yaml test(e2e): cover tool-permission, tool-policy and llm-as-a-judge guardrails 2026-07-27 15:54:40 -07:00
llm_claude_code_compat.yaml fix(e2e/claude_code): unblock stage collection, align proxy env names, register compat models (#33433) 2026-07-16 11:05:31 -07:00
llm_conversational.yaml test(e2e): cover /openai chat passthrough cost logging (#34470) 2026-07-25 17:54:28 +00:00
llm_nonconversational.yaml test(e2e): cover /v1/images/edits (#34476) 2026-07-25 10:38:07 -07:00
logging.yaml test(e2e): datadog log delivery for streamed routes, read back from the real datadog api (#33566) 2026-07-16 19:37:07 -07:00
mcp.yaml test(e2e): cover MCP access-group tool selection at key creation (#34480) 2026-07-24 16:18:40 -07:00
mgmt.yaml fix(e2e): stop tests from breaking the shared proxy for every suite after them (#34664) 2026-07-25 23:12:55 +00:00
other.yaml test(e2e): invoke a real published a2a agent and assert it replies 2026-07-21 22:33:07 +00:00
quota_management.yaml test(true_rabbit): cover passthrough headers, batch assume-role, gemini, vllm, bedrock guardrails, batch rate-limit mapping (#33843) 2026-07-20 16:15:55 -07:00
README.md test(e2e): harness fixes for stage job green (skips + router/UI/budget) (#33634) 2026-07-16 20:30:30 -07:00
registry.py ci: gate tests/e2e on zero basedpyright errors in pre-commit and lint CI 2026-07-11 10:25:22 -07:00
reliability.yaml test(e2e): add weekly session-anomaly load test against real providers 2026-07-21 14:54:02 -07:00
schema.py test(e2e): cover /v1/images/edits (#34476) 2026-07-25 10:38:07 -07:00
test_collector.py test: emit e2e coverage lines for loki (#32513) 2026-07-08 11:06:56 -07:00

e2e coverage registry

This directory is the denominator for e2e test coverage: the set of behaviors we want covered, one row per behavior, checked into the repo so coverage is a number we can track instead of a guess. It implements the plan in the "E2E Coverage Tracking" note; the naming grammar lives in tests/e2e/CLAUDE.md.

The model

A cell is one customer-noticeable behavior a single e2e test can assert pass/fail on, for example llm.chat_completions.bedrock_converse.tool_use.stream.works. Cells are grouped module > feature > test, with LLM cells split into Core LLMs and Non-Core LLMs for dashboarding. Each cell carries a tier (P0/P1/P2), a source, and a fail_before_fix flag.

The rows live in per-prefix YAML files (llm_*.yaml, mgmt.yaml, mcp.yaml, reliability.yaml, quota_management.yaml, logging.yaml, guardrail.yaml, other.yaml) and validate against the discriminated union in schema.py, so an LLM row cannot carry a guardrail field and vice versa. llm rows with subject_endpoint of chat_completions, messages, or responses roll up to Core LLMs; all other LLM endpoints roll up to Non-Core LLMs. LLM endpoint, route, and capability values are typed in schema.py, so new taxonomy values require an explicit schema change. logging and guardrail are two id-prefixes that roll up into the single Logging & Guardrails dashboard module.

A test declares what it covers with a marker:

@pytest.mark.covers("llm.chat_completions.openai.tool_use.stream.works")
def test_openai_streaming_tool_calls(self) -> None:
    ...

The number

collector.py diffs the registry against those markers and reports coverage per module. It is static: a collect-only pass reads the markers, so it runs no test and needs no live proxy. Whether a covered cell currently passes or fails is a separate, live concern.

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector

Use --format loki after the e2e pytest run in the same Kubernetes job/pod to print structured stdout lines for Loki:

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector --format loki --strict

This emits exactly one COVERAGE_TOTAL line and one COVERAGE_MODULE line per module in MODULE_ORDER, in that order. Loki uses log-safe module= labels from LOKI_MODULE_LABELS (core_llms, management_ui, etc.) so existing JSON and Prometheus consumers keep their human-readable module names unchanged.

The headline is overall coverage. The collector also lists markers that point at ids not in the registry, so a typo or an unenumerated behavior surfaces instead of being silently dropped.

Use strict mode in CI once existing draft markers are reconciled:

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector --strict

Strict mode exits non-zero on @pytest.mark.covers(...) ids that are not checked into the registry. Add --fail-on-collection-errors when the job should also fail on pytest collection errors.

Status: this is a draft for review

The cells were enumerated from the codebase and the tiers are a first proposal. Known things to settle before treating the set as final:

  • tiers are proposed, not signed off; 125 P0 is a lot to prove fail-before-fix, so P0 may want tightening
  • a few cells need a support check or a prune (for example llm.embeddings.anthropic.* and reliability.perf.throughput.under_slo)
  • auth is covered in two places (other.auth.* and the mgmt authz assertions); the boundary needs a decision, and the auth cluster may deserve promotion to its own module
  • the P2 "niche" cells each stand in for a large tail of integrations/providers by design, so the denominator is deliberately P0-weighted rather than a full inventory