mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
* feat(guardrails): add Alice by ActiveFence guardrail
Adds `guardrail: alice` — policy-based guardrails for prompts and model
responses, evaluated against ActiveFence's Alice.
What makes this different from the other providers: Alice evaluates against
policies configured per *application*, and a proxy typically fronts several of
them, so the application cannot be a static config value. It is named on the
LiteLLM virtual key instead:
curl $PROXY/key/generate -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"key_alias": "payments-bot",
"metadata": {"alice_app_id": "payments-bot"}}'
read via `CustomGuardrail._get_admin_metadata`, with `key_alias` as the
fallback. That helper is what makes it trustworthy: it reads whichever metadata
holder the proxy wrote the authenticated key's values into — which differs by
route — and the proxy strips caller-supplied `user_api_key_*` from both, so a
caller cannot point its own traffic at an application with laxer policies than
the one its key was issued for. A request whose key names no application is
refused rather than evaluated against a guess.
Implements `apply_guardrail` only, so pre_call, during_call, post_call and
streaming all come from UnifiedLLMGuardrails. Blocks with
GuardrailRaisedException; masks by substituting Alice's redacted text; a MASK
carrying no replacement blocks rather than passing the original through. A
verdict reporting `errors[]` is treated as a failure, not a pass — otherwise a
half-evaluated message would be allowed. `unreachable_fallback` (already on
LitellmParams) chooses fail-closed or fail-open on transport failure.
Config:
guardrails:
- guardrail_name: alice
litellm_params:
guardrail: alice
mode: [pre_call, post_call]
api_key: os.environ/ALICE_API_KEY
21 tests in tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice.py
cover registration, credential resolution, the app-id ladder including the
forged-metadata case, every verdict, and both unreachable policies.
No new LitellmParams field, so no schema.d.ts regeneration is needed.
* refactor(guardrails): post to Alice's LiteLLM endpoint and forward verbatim
Switches from `/v2/evaluate/message` — Alice's single-text endpoint — to
`/v2/evaluate/litellm`, which takes the hook's arguments as they arrive and
answers with a verdict.
That inverts where the work happens, and shrinks this plugin accordingly. It
now selects nothing and renames nothing: it posts `{input_type, inputs,
request_data}` and enforces `{verdict, categories, correlation_id, message,
replacements}`. Which parts of a conversation are worth evaluating, and how a
verdict is reached, are decided by Alice — so changing either is a change on
their side rather than a LiteLLM upgrade for every user.
The app-id resolution this plugin carried is gone with it. Alice reads the
application off the authenticated key's metadata itself, from the payload it is
handed, so the ladder here was duplicating a decision the far side already
makes. The security property is unchanged and still comes from the proxy
stripping caller-supplied `user_api_key_*` before a guardrail sees the request.
Masking is now positional — the far side chose which texts it was answering
for, so it says which by index. Only `texts` is written; a new
`structured_messages` object would make the chat translation layer skip the
`texts` write-back and silently drop the edits. A mask that lands nowhere
blocks rather than passing the original through.
`request_data` carries live Python objects (an OpenTelemetry span among them),
so `_json_safe` copies it into something serialisable by a mechanical rule
rather than a field list — a list drifts from what the far side needs, a rule
cannot. Serialising naively raises, and that error would read as "guardrail
unavailable" on every request.
26 tests, covering verbatim forwarding, each verdict, positional masking, the
`structured_messages` identity trap, both unreachable policies, and the
serialiser's handling of unserialisable values and cycles.
* fix(alice guardrail): satisfy lint and code-quality CI gates
- Bound _json_safe's recursion and register it in recursive_detector's
ignore list (it already caps depth and dedupes cycles by id, matching
the repo's established pattern for legitimate bounded recursion).
- Clear ruff-strict budget breaches: annotate __init__'s return type,
raise TypeError (not ValueError) for a bad response body, type
_json_safe's payload as object instead of Any, and file-scope-ignore
ANN401 for **kwargs (forwarding it as object broke the call into
CustomGuardrail.__init__, confirmed via basedpyright).
- Clear type-discipline budget breaches: suppress the construction/
annotation checks on one-shot HTTP payloads, the module-level
guardrail registries, and _json_safe's bounded accumulator; narrow
AliceVerdict's list fields to tuples and _evaluate's request_data to
Mapping[str, object] where nothing downstream mutates them.
* test(alice guardrail): assert the guardrail actually registers
The registration test called init_guardrails_v2 and asserted nothing, so it
passed whether or not the guardrail was ever registered — TQ001 in the
test-quality gate, and a fair catch: a test that cannot fail is not covering
the thing it names.
Now asserts exactly one AliceGuardrail lands in litellm.callbacks under the
configured name.
This surfaced only after the ruff-strict and type-discipline gates stopped
failing ahead of it; the lint job runs its gates in sequence, so an earlier
failure masks every later one.
* fix(alice guardrail): reach 100% patch coverage, drop the ActiveFence naming
Codecov flagged 10 uncovered lines, all of them error paths — which is where a
guardrail most needs covering, since each one decides whether traffic flows
unscreened.
Two of the ten turned out to be dead rather than untested, and are removed:
- `except GuardrailRaisedException: raise` in apply_guardrail. `_evaluate`
raises httpx errors, Timeout and TypeError, never that — so the clause could
never fire.
- the trailing `json.dumps` probe in `_json_safe`. Everything json.dumps
handles natively is caught by the isinstance branches above (a dict or list
subclass included), so anything reaching the bottom — bytes, datetime, an
OpenTelemetry span — cannot cross the wire regardless. It now says so and
returns None.
The rest are now tested: a timeout, 502/503/504 as unreachable, a 4xx as NOT
unreachable (a rejected credential is our misconfiguration, not an outage, and
must not fail open), a non-object response body, and a model whose model_dump
raises.
Also drops "by ActiveFence" throughout — the product is Alice — and points the
header at alice.io. `ui_friendly_name` is now "Alice", which is the key
guardrailLogoMap and the garden card look up, so all three moved together.
* fix(alice guardrail): strip caller credentials, widen unreachable detection, block partial MASK
Addresses PR review: request_data no longer forwards secret_fields.raw_headers or
the root api_key to Alice (the caller's Authorization token in the clear otherwise);
HTTP 500, malformed JSON, and a non-object body now route through the configured
unreachable_fallback instead of raising raw, so fail_open still fails open on those;
a MASK verdict with even one out-of-range replacement now blocks entirely instead of
silently letting the rest through unmasked. Also tightens request_data's type and
documents the known streaming-mask limitation on the class.
* fix(alice guardrail): strip credentials at any depth, stop filtering on texts
secret_fields/api_key/headers/provider_specific_header can appear nested
under proxy_server_request, metadata, litellm_metadata, and their
requester_metadata/body sub-paths in a real captured payload — a
top-level-only strip missed all of those. _json_safe now drops these keys
by name wherever they occur during serialization, so a new nesting path
can't reintroduce the leak.
apply_guardrail also stopped skipping the call whenever texts was empty,
even when tool_calls/images/structured_messages carried content — that
was the plugin making a selection decision Alice's design says belongs on
the far side. It now only skips when none of the selectable fields have
anything in them.
* fix(alice guardrail): route an undecodable response body through the fallback
`response.json()` raises UnicodeDecodeError when the body carries bytes that
are not valid UTF-8, and that escaped the except clause: UnicodeDecodeError is
a *sibling* of json.JSONDecodeError under ValueError, not a subclass of it, so
naming only JSONDecodeError left it uncaught. Both fallback modes surfaced a
raw decoding error instead of applying unreachable_fallback — which for a
fail_open deployment meant a hard failure where it had asked for an allow.
Named explicitly rather than widening to ValueError, so the clause still says
which three conditions it means. Tested under both policies.
|
||
|---|---|---|
| .. | ||
| a2a_protocol | ||
| anthropic_interface | ||
| batches | ||
| caching | ||
| completion_extras | ||
| compression | ||
| containers | ||
| endpoints | ||
| enterprise | ||
| expected_fine_tuning_api | ||
| expected_responses_api_request | ||
| experimental_mcp_client | ||
| fixtures/together_ai_sync | ||
| google_genai | ||
| images | ||
| integrations | ||
| interactions | ||
| litellm_core_utils | ||
| llms | ||
| models | ||
| ocr | ||
| passthrough | ||
| proxy | ||
| rag | ||
| realtime_api | ||
| repositories | ||
| rerank_api | ||
| responses | ||
| router_strategy | ||
| router_utils | ||
| rust_bridge | ||
| sandbox | ||
| secret_managers | ||
| test_router | ||
| types | ||
| vector_stores | ||
| videos | ||
| __init__.py | ||
| conftest.py | ||
| log.txt | ||
| readme.md | ||
| test_a2a_registry_lookup.py | ||
| test_acompletion_session_reuse_e2e.py | ||
| test_add_deployment_no_master_key.py | ||
| test_aembedding_session_reuse_e2e.py | ||
| test_anthropic_beta_headers_filtering.py | ||
| test_anthropic_skills_transformation.py | ||
| test_anthropic_sonnet_1hr_cache_pricing.py | ||
| test_assert_ci_coverage.py | ||
| test_assert_workflow_dir_hygiene.py | ||
| test_audio_transcription_rust_bridge.py | ||
| test_azure_ad_token_credential_resolution.py | ||
| test_azure_ai_grok_4_3_model_metadata.py | ||
| test_azure_audio_price_aliases.py | ||
| test_batch_completion_models_all_responses.py | ||
| test_bedrock_anthropic_1hr_cache_pricing.py | ||
| test_bedrock_batch_pricing.py | ||
| test_bedrock_extended_beta_models.py | ||
| test_bedrock_nemotron_super.py | ||
| test_bedrock_usgov_haiku_1hr_cache.py | ||
| test_bedrock_usgov_pricing.py | ||
| test_budget_ratchet_check.py | ||
| test_chat_ui_responses_session.py | ||
| test_check_licenses.py | ||
| test_check_migrations_no_data_rewrites.py | ||
| test_check_test_quality.py | ||
| test_check_type_discipline.py | ||
| test_circleci_path_filter.py | ||
| test_circleci_rust_toolchain.py | ||
| test_claude_fable_5_config.py | ||
| test_claude_haiku_4_5_config.py | ||
| test_claude_opus_4_6_config.py | ||
| test_claude_opus_4_8_config.py | ||
| test_claude_opus_5_config.py | ||
| test_claude_sonnet_4_6_config.py | ||
| test_claude_sonnet_5_config.py | ||
| test_cloudflare_workers_ai_model_metadata.py | ||
| test_command_r7b_pricing.py | ||
| test_completion_timeout_resolution.py | ||
| test_component_entrypoint.py | ||
| test_compression.py | ||
| test_conftest.py | ||
| test_conftest_isolation.py | ||
| test_constants.py | ||
| test_container_router.py | ||
| test_cost_calculation_log_level.py | ||
| test_cost_calculator.py | ||
| test_count_tokens_public_api.py | ||
| test_dashscope_image_generation.py | ||
| test_daybreak_model_metadata.py | ||
| test_deepseek_model_metadata.py | ||
| test_detect_changes.py | ||
| test_dockerfile_non_root.py | ||
| test_e2e_egress_sentinel.py | ||
| test_eager_tiktoken_load.py | ||
| test_env_key_doc_gate.py | ||
| test_exception_exports.py | ||
| test_exception_header_preservation.py | ||
| test_exception_mapping_request_attribute.py | ||
| test_filter_out_litellm_params.py | ||
| test_fireworks_serverless_model_costs.py | ||
| test_friendli_glm_5_3_flash_model_metadata.py | ||
| test_friendli_glm_5_3_model_metadata.py | ||
| test_gate_slot_lock.py | ||
| test_gemini_3_1_flash_lite_image_pricing.py | ||
| test_gemini_tts_native_audio_pricing.py | ||
| test_get_blog_posts.py | ||
| test_git_hooks.py | ||
| test_github_close_low_quality_prs.py | ||
| test_github_review_gate.py | ||
| test_github_triage_with_llm.py | ||
| test_github_triage_workflows.py | ||
| test_gpt_5_4_model_metadata.py | ||
| test_gpt_5_5_model_metadata.py | ||
| test_gpt_image_cost_calculator.py | ||
| test_gpt_realtime_mode.py | ||
| test_groq_streaming_encoding.py | ||
| test_guardrail_exception_status_codes.py | ||
| test_lazy_imports.py | ||
| test_litellm_params_reserved_keys.py | ||
| test_logging.py | ||
| test_lowest_latency_zero_tokens.py | ||
| test_main.py | ||
| test_main_module_header.py | ||
| test_mistral_medium_3_5_model_metadata.py | ||
| test_mistral_small_4_0_model_metadata.py | ||
| test_mistral_zai_glm_5_2_model_metadata.py | ||
| test_model_block_unblock.py | ||
| test_model_cost_aliases.py | ||
| test_model_param_helper.py | ||
| test_model_prices_schema.py | ||
| test_model_response_normalization.py | ||
| test_muse_spark_1_1_model_metadata.py | ||
| test_muse_spark_1_2_model_metadata.py | ||
| test_mutation_report.py | ||
| test_nested_drop_params.py | ||
| test_non_chat_routes_open_llm_spans.py | ||
| test_openai_embedding_encoding_format_default.py | ||
| test_pre_commit_lint.py | ||
| test_prisma_generate_if_needed.py | ||
| test_project_alias_tracking.py | ||
| test_project_tags_pydantic.py | ||
| test_proxy_auth.py | ||
| test_rag_openai_ingestion.py | ||
| test_rate_limit_error_unification.py | ||
| test_redact_string_in_error_paths.py | ||
| test_redis.py | ||
| test_register_model_custom_pricing.py | ||
| test_register_model_zero_cost_persistence.py | ||
| test_replicate_model_key_format.py | ||
| test_responses_api_bridge_non_stream.py | ||
| test_responses_id_security.py | ||
| test_responses_streaming_container_ownership.py | ||
| test_retrieve_batch_bedrock_dispatch.py | ||
| test_router.py | ||
| test_router_block_helpers.py | ||
| test_router_exception_redaction.py | ||
| test_router_google_genai.py | ||
| test_router_model_cost_isolation.py | ||
| test_router_order_fallback.py | ||
| test_router_per_deployment_num_retries.py | ||
| test_router_redis_init.py | ||
| test_router_retry_backoff_headers.py | ||
| test_router_retry_non_retryable_errors.py | ||
| test_router_retry_policy_update.py | ||
| test_router_silent_experiment.py | ||
| test_router_streaming_fallback_metadata.py | ||
| test_router_weighted_failover.py | ||
| test_ruff_strict_gate.py | ||
| test_sambanova_model_metadata.py | ||
| test_secret_redaction.py | ||
| test_select_ui_test_scope.py | ||
| test_service_logger.py | ||
| test_setup_wizard.py | ||
| test_shared_session_integration.py | ||
| test_ssl_verify_unit.py | ||
| test_stream_chunk_builder_annotations.py | ||
| test_stream_chunk_builder_citations.py | ||
| test_stream_chunk_builder_images.py | ||
| test_streaming_connection_cleanup.py | ||
| test_sync_together_ai_models.py | ||
| test_system_message_format_bug.py | ||
| test_test_quality_gate.py | ||
| test_thinking_enabled.py | ||
| test_together_ai_model_metadata.py | ||
| test_type_check_gate.py | ||
| test_type_discipline_gate.py | ||
| test_utils.py | ||
| test_utils_module_docstring.py | ||
| test_uuid_helper.py | ||
| test_vcr_safe_body_matcher.py | ||
| test_video_generation.py | ||
| test_with_dashboard_node.py | ||
| test_xai_grok_4_3_model_metadata.py | ||
| test_xai_responses_auto_routing.py | ||
Testing for litellm/
This directory 1:1 maps the the litellm/ directory, and can only contain mocked tests.
The point of this is to:
- Increase test coverage of
litellm/ - Make it easy for contributors to add tests for the
litellm/package and easily run tests without needing LLM API keys.
File name conventions
litellm/proxy/test_caching_routes.pymaps tolitellm/proxy/caching_routes.pytest_<filename>.pymaps tolitellm/<filename>.py