litellm/tests/e2e/coverage_registry
Meryem Sakin 4a7fb30b0a
fix(guardrails)!: check Anthropic and Responses request tools in tool_permission pre_call (#41011)
* fix(guardrails): check Anthropic-format request tools in tool_permission pre_call

The pre_call hook only collected tools shaped as {"type": "function", "function": {...}},
so flat tools from /v1/messages ({"name", "input_schema"}) and /v1/responses
({"type": "function", "name"}) were never checked and deny rules silently passed.

Reuse the shared anthropic_tool_names helper so every request tool name is checked,
and handle Anthropic tool_choice {"type": "tool"} in both block and rewrite modes.

Fixes #40583

* fix(guardrails): detect Responses API named tool_choice in tool_permission

Responses API named choices put the name at the top level ({"type": "function", "name": ...}),
so rewrite mode removed the denied tool but left a tool_choice pointing at it.

* test(guardrails): run tool_permission pre_call tests through real default_on gating

Build the guardrail as an always-on pre_call guardrail instead of patching should_run_guardrail.

* test(guardrails): type the new tool_permission pre_call test parameters

* fix(guardrails): keep the Responses API custom tool type distinct

Anthropic's user-defined tools carry type "custom", so pre_call mapped
that to "function" for tool_type rules. The Responses API has its own
custom tool type, and mapping it let a custom tool slip past a
tool_type deny rule when the default action is allow.

Normalize only for the formats where custom means a plain function, and
read tool names directly in the rewrite path, which never needed a type.

* test(guardrails): cover flat tools through Messages and Responses

Add blocked and permitted flat-tool cases through the running proxy, with
endpoint coverage and metadata for the current e2e harness

Require a guardrail policy verdict when checking denied tools so an
unrelated provider error cannot satisfy the regression test

Co-authored-by: Christopher Krah <christopher.krah@plexito.de>

* fix(guardrails): check namespace and nameless tools and relax tool_choice in tool permission rewrite

* fix(guardrails): check forced, grouped, loaded, and hosted tools by their real type

* fix(guardrails): drop references to removed tools and count loaded tools in tool permission rewrite

* fix(guardrails): check tools sent outside top-level tools in tool permission

* fix(guardrails): judge namespace members by their bare name as a fallback and keep rewrites valid for Gemini, Bedrock and hosted tools

A namespace member is checked by namespace__name first, and by its bare name when no rule names the joined one. Rewrite mode now prunes Gemini allowedFunctionNames and turns off forced calling when no function is left, drops a removed hosted tool from allowed_tools, reads Bedrock toolSpec and systemTool blocks by name, skips cachePoint markers, and leaves a tools field that is not a list for the provider to reject instead of failing with a 500

* fix(guardrails): let only name rules decide a namespace member's bare-name fallback

* fix(guardrails): judge malformed tool types, empty Gemini name lists and pass-through Anthropic tools like the endpoints do

A tool or tool_choice whose type is not a string now reads as untyped instead of crashing the guardrail with a 500, and a null type key is never read as a Gemini tool part. An empty allowedFunctionNames list no longer turns off the functions a rewrite kept. An Anthropic custom tool reads as a function by its input_schema on every route, so a pass-through route judges it the same as /v1/messages. A Responses WebSocket frame now forwards the extra_body tools a rewrite stripped

* fix(guardrails): judge a named hosted tool by its type and keep a nested chat custom tool custom

* fix(guardrails): keep judging Anthropic client tools and type-named server tools by their name

* fix(guardrails): judge a named tool by its name alone only when the name is its type's own

* fix(guardrails): judge the function a tool names beside a declaration list or a top-level name

* fix(guardrails): judge Gemini hosted keys whatever the tool type and a namespace's function

* fix(guardrails): judge legacy functions entries like tools and send Gemini a namespace's members

* fix(guardrails): judge a namespace member by every name a provider reads and send Gemini its schema converted

* fix(guardrails): judge a legacy functions entry as a function and relax a tool_choice in extra_body

* fix(gemini): keep a namespace whole when inlining its members would repeat a function name

* fix(guardrails): judge a namespace member by every bare name that shares its qualified name

* fix(vertex_ai): declare a repeated namespace member name once instead of sending namespace names to Gemini

* fix(guardrails): judge a tool_choice sent in extra_body like a top-level one

A forced name only extra_body.tool_choice carried was never checked, so block mode let a denied name through to the provider

* fix(guardrails): judge a Gemini namespace member returned by its bare name and force a named namespace's members

* test: keep formatter churn out of the tool_permission and Gemini test files

* fix(guardrails): judge extra_body.input tools, argument-aware alias rules, and exact forced Gemini functions

Tools loaded in extra_body.input are now checked and stripped like top-level input. A returned
namespace member is judged by the alias whose first name rule its arguments satisfy. A forced
Gemini name that matches a top-level function keeps that function. Alias grouping and Gemini
declaration dedupe now run in one pass instead of quadratic scans.

* fix(guardrails): judge and rewrite every tool field sent inside extra_body

extra_body is merged into the upstream body, so functions, function_call,
mcp_servers and toolConfig nested there reached the model unjudged. Treat
extra_body as a second request layer that is collected and rewritten the
same way as the top-level request.

* fix(guardrails): judge Gemini hosted keys by key and count only the tools a provider receives after extra_body

* fix(guardrails): relax a tool_choice that names a dropped namespace member by its bare name

Gemini and Anthropic chat send namespace members under their bare names, so rewrite mode now treats a dropped member's bare name, and an emptied namespace's own name, as removed when no kept tool still carries it

* fix(gemini,guardrails): keep a top-level function over a same-named namespace member and leave malformed tool values to the provider

Gemini and Vertex chat now declare a top-level function over a namespace member that shares its name, so a forced call gets the top-level schema. post_call judges a returned name a top-level tool carries as that tool, and pre_call no longer raises on an unhashable or non-string tool name, input item type, or function_call name.

* fix(guardrails): judge a returned namespace member named like a hosted MCP server as that member

* fix(gemini,guardrails): send a namespace as main did when another tool has a member's name

Gemini chat inlines a namespace's members only when no other tool, namespace
member, or legacy functions entry declares the same name, and otherwise sends the
namespace by its own name as main did. A tool_choice naming such a namespace
forces the namespace itself. The tool permission post_call reads top-level names
the way Gemini declares them and judges a call the Responses bridge tags with a
namespace as that member.

* fix(gemini): send colliding namespace members as namespace__name functions and judge them in post_call

When a namespace member shares a name with another tool, Gemini chat now gets that namespace's members as namespace__name functions instead of one function named after the namespace, so every function it can call is a name the rules judged. Declared names are counted once, and a forced member maps to each holder's qualified name.

post_call judges a returned namespace__name as that member, by its qualified or bare name the way pre_call does, so a bare-name allowlist or denylist applies to it.

* fix(gemini): keep generated member names unique and let a top-level function own its exact name

A namespace that listed a member twice, or a generated namespace__name that
repeated a top-level function, sent Gemini two declarations with one name, and
Gemini answered 400 where main answered 200. A name a namespace lists twice is
now one member, and a top-level function keeps its exact name.

post_call now judges a returned name a top-level function declares as that
function, never as the namespace member the name could also mean.

* fix(gemini): qualify namespace members by the namespace's top-level name

The guardrail judges a member as `name__member` from the namespace's top-level
name. Gemini built the name from `function.name` when the tool also carried one,
so a request could pass pre-call and reach Gemini under a name no rule judged.

* fix(vertex_ai): force only the namespace members Gemini is sent

A forced namespace or member on Gemini chat could still allow a generated name that a top-level function or an earlier namespace owns, so tool_choice forced an unrelated function. The forced names now come from the same collision checks the declarations use

* fix(vertex_ai): reject a forced namespace Gemini gets no member of, and resolve forced names in one pass

* fix(guardrails): judge a namespace member by a rule on its namespace's name

---------

Co-authored-by: Christopher Krah <christopher.krah@plexito.de>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-04 20:34:34 +00:00
..
__init__.py chore: consolidate CLAUDE.md into AGENTS.md 2026-09-19 02:30:35 +00:00
collector.py fix(e2e-changed): keep the gate off suites the stack cannot run 2026-09-05 21:03:50 -07:00
guardrail.yaml fix(guardrails)!: check Anthropic and Responses request tools in tool_permission pre_call (#41011) 2026-11-04 20:34:34 +00:00
llm_claude_code_compat.yaml test(e2e): replay a real tool-search assistant turn back to Bedrock Invoke (#36856) 2026-08-17 11:59:26 -07:00
llm_conversational.yaml test: move Sail, Weave and Presidio e2e tests to integration tests (#49533) 2026-10-31 18:31:01 -07:00
llm_nonconversational.yaml feat(realtime)!: support latest OpenAI audio models (#35600) 2026-11-03 07:27:52 +00:00
logging.yaml fix(langfuse): log Responses API token usage instead of zeros (#42212) 2026-10-10 16:42:59 -07:00
management_cases.py test: enforce isolated actors and stop OIDC process groups 2026-09-12 13:49:49 -07:00
mcp.yaml refactor(mcp): drop the experimental label from the MCP server and client paths (#48513) 2026-10-27 17:47:50 +00:00
mgmt.yaml test(e2e): repair fallback management test for config-owned fallbacks (#47023) 2026-10-17 01:27:39 +00:00
other.yaml test(a2a): replace the third-party agent e2e test with an integration test (#49541) 2026-10-31 18:59:04 -07:00
quota_management.yaml test(e2e): retire the quiet_stack lock and batch the claude_code model registration (#49984) 2026-11-04 09:55:53 -08:00
README.md chore: consolidate CLAUDE.md into AGENTS.md 2026-09-19 02:30:35 +00: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): assert the sibling-replica cooldown through the router (#44706) 2026-10-06 02:12:57 +00:00
schema.py feat(realtime)!: support latest OpenAI audio models (#35600) 2026-11-03 07:27:52 +00: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/AGENTS.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.

A skipped test asserts nothing, so its markers do not count. A cell is covered only when at least one test pytest would actually run declares it; a cell claimed by both a live test and a skipped one stays covered. Skip state comes from pytest's own evaluator, so skip and skipif resolve exactly as they do in the e2e run, which also means a skipif on an absent credential makes that cell uncovered in the environments where the test cannot run. Cells left uncovered this way are listed under the headline (and counted by litellm_e2e_coverage_skipped_markers) so an unskipped-pending gap is visible rather than inflating the number. The one skip the collector cannot see is pytest.skip() called from inside a test body, since it does not exist until the test runs.

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.

Provider x feature matrix: customer-run Bedrock combinations

The provider and feature combinations customers actually run get explicit cells, expanded here as incidents surface new ones. The current Bedrock set, seeded from a customer's production shape (regional us.anthropic.* inference-profile ids over both chat routes, provider response headers for AWS-side correlation, and the Test Connection probe for a responses-mode Bedrock Mantle deployment):

Cell Feature Covering test
llm.chat_completions.bedrock_converse.basic.nonstream.works regional us. id, Converse llm_translation/test_chat_completions_regression_e2e.py
llm.chat_completions.bedrock_converse.basic.stream.works regional us. id, Converse stream llm_translation/test_chat_completions_regression_e2e.py
llm.chat_completions.bedrock_invoke.basic.nonstream.works regional us. id, Invoke llm_translation/test_bedrock_provider_matrix_e2e.py
llm.chat_completions.bedrock_invoke.basic.stream.works regional us. id, Invoke stream llm_translation/test_bedrock_provider_matrix_e2e.py
llm.chat_completions.bedrock_converse.response_headers.nonstream.works llm_provider-* headers llm_translation/test_bedrock_provider_matrix_e2e.py
llm.chat_completions.bedrock_converse.response_headers.stream.works llm_provider-* headers, stream llm_translation/test_bedrock_provider_matrix_e2e.py
mgmt.model.test_connection.happy_path Test Connection, Bedrock Mantle management/test_model_test_connection_e2e.py

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