Skip to content

feat(guardrails): backport the decision model guardrail to stable/1.104.x (#45663) - #45900

Merged
yucheng-berri merged 1 commit into
stable/1.104.xfrom
litellm_backport_45663_stable_1_104_x
Oct 11, 2026
Merged

yucheng-berri merged 1 commit into
stable/1.104.xfrom
litellm_backport_45663_stable_1_104_x

Conversation

@yucheng-berri

@yucheng-berri yucheng-berri commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

This backports #45663 onto stable/1.104.x, so the decision model guardrail and its Admin UI flow ship in the next 1.104 patch

Problem this solves:

  • stable/1.104.x has no guardrail that can use a decision model like Jev to block attacks

How it solves it:

User Flow

Before: on stable/1.104.x an admin who wants Jev to catch prompt injection has no way to set it up

  1. They open http://localhost:4000/ui/?page=guardrails and click Add New Guardrail
  2. The provider list has no Decision Model option, and the Guardrail Garden has no Decision Model section

After: the admin adds a decision model as a guardrail and injection attempts get a 400

  1. They pick provider "Decision Model", or a card under Decision Model in the Guardrail Garden
  2. They pick a decisions model such as jev-latest, add their yes/no questions with Block or Log only and a threshold, and try them in the Test section
  3. A request whose text trips a Block question gets HTTP 400 "Violated decision model guardrail policy", while a normal prompt gets its answer

Picks

#45894 is #45663 (merge commit 5f9dd38) adapted to an rc that predates litellm/decisions/call.py, GuardrailReadOnlyDetails.tsx, LiteLLMBaseModel, @/lib/decisionModels and the TypeSafe Add Model provider. stable/1.104.x predates them too and has the same decisions providers, so those adaptations carry over unchanged. See #45894 for the list

  • The only conflict was the unit test's location: stable/1.104.x keeps the guardrail hook tests in tests/test_litellm/proxy/guardrails/guardrail_hooks, so test_decision_model.py lives there
  • _lazy_openapi_snapshot.json and schema.d.ts match a regeneration on this branch

Testing

  • Live proxy from this branch with 2 workers, openai/gpt-6-luna as the decider and a pre_call Block question about instruction overrides: /v1/chat/completions, /v1/responses and /v1/messages each returned 400 "Violated decision model guardrail policy" for an injection prompt and 200 for a clean prompt
  • GET /v2/guardrails/list returns the guardrail with its decision_model and checks, and GET /guardrails/ui/add_guardrail_settings returns decision_model_providers (perplexity, typesafe, openrouter, cloudflare, strands_decider, openai)
  • tests/test_litellm/proxy/guardrails, tests/unit/decisions, tests/unit/proxy/decisions_endpoints, tests/unit/types and test_llm_judge.py: 5289 passed
  • The changed dashboard test files, for the form, the decision model components, guardrail info and the Guardrail Garden: 96 passed
  • ruff_strict, type_discipline, test_quality and type_check gates: OK against stable/1.104.x

Linear ticket

Resolves LIT-9443

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

Type

🆕 New Feature

Caveats (if any)

  • Same caveats as feat(guardrails): add decision model guardrail with Admin UI flow #45663
  • stable/1.104.x has no azure_ai, Databricks or hosted_vllm decisions provider, so the Microsoft and Databricks Decision Model cards in the Guardrail Garden show the "No decision models yet" note on this branch
  • stable/1.104.x's Add Model page has no TypeSafe provider, so a Jev model is added through config.yaml or /model/new, and the decision model picker shows no TypeSafe logo next to it

Behavior changes

Removed / renamed

  • None

Silent regressions to watch

  • Same as feat(guardrails): add decision model guardrail with Admin UI flow #45663: GET /v2/guardrails/list and guardrail info return three more null litellm_params keys for every guardrail (decision_model, max_input_chars, max_concurrent_decision_calls), and GET /guardrails/ui/add_guardrail_settings returns a new decision_model_providers list
  • max_input_chars and max_concurrent_decision_calls are typed as positive integers on every guardrail's params, so a custom guardrail that reuses those names with a zero, negative or non-number value fails to load
  • The Guardrail Garden shows a new Decision Model section between LiteLLM Content Filter and Partner Guardrails

Migration for existing customers

  • None required

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Devin Review

…04.x (#45663)

Cherry-pick of the rc/1.105.0 backport of #45663 (#45894), which already
adapts #45663 to branches that predate litellm/decisions/call.py,
GuardrailReadOnlyDetails.tsx, LiteLLMBaseModel, @/lib/decisionModels and
the TypeSafe Add Model provider. stable/1.104.x has the same decisions
providers as rc/1.105.0.

- The decision model unit test lives in tests/test_litellm/proxy/guardrails/
  guardrail_hooks, where stable/1.104.x keeps the guardrail hook tests
- The OpenAPI snapshot and schema.d.ts match a regeneration on this branch

(cherry picked from commit 1804758)
(cherry picked from commit 5f9dd38)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +140 to +142
@staticmethod
def get_config_model() -> type[DecisionModelGuardrailConfigModel] | None:
return DecisionModelGuardrailConfigModel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Saved decision checks cannot be edited

When an admin edits a decision-model guardrail, checks renders as an empty text input instead of its saved predicates. The generic editor submits typed changes as a string, so admins cannot update saved checks.

Learn more

The provider settings endpoint exposes the fields returned by get_config_model(), including the tuple of predicate checks in DecisionModelGuardrailConfigModel. The settings editor renders these fields through GuardrailProviderFields/guardrails/_components/guardrail_provider_fields.tsx:337-388). The text renderer displays tuple values as empty strings. Editing the field produces a string, which handleGuardrailUpdate/guardrails/_components/guardrail_info.tsx:398-447) forwards instead of structured checks.

Example: An admin opens a saved guardrail to change one predicate's threshold. The checks input appears empty and typing into it cannot produce a valid list of predicates.

Recommended fix: Add a dedicated checks editor to the settings view, initialize it from saved checks, and submit validated structured checks. Exclude the checks tuple from the generic provider form.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@yucheng-berri
yucheng-berri merged commit 19180f3 into stable/1.104.x Oct 11, 2026
11 of 12 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_backport_45663_stable_1_104_x branch October 11, 2026 00:42
async def _call_decisions(self, text: str, questions: Sequence[Mapping[str, object]]) -> object:
router: Final = self._router_provider()
if judge_target(router, self.decision_model).via == "router" and router is not None:
return await router.adecisions( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType,reportUnknownArgumentType] # factory_function-generated member

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Decision scans bypass caller budgets

An authenticated caller can repeatedly submit prompts that this guardrail blocks, consume the configured decision provider's quota, and keep their key/team spend unchanged. Both dispatch paths omit caller spend metadata, so _should_track_cost_callback skips these guardrail adecisions calls; _log also supplies no guardrail_cost, leaving the rejected parent request without the scan charges. (raw.githubusercontent.com)

Attribute and budget the scans using sanitized request identity, or aggregate their costs into the parent guardrail record, including on blocked requests. Check or reserve the applicable budget before dispatching additional scans.

async def _screen_text(
self, text: str, questions: Sequence[Mapping[str, object]], semaphore: asyncio.Semaphore
) -> tuple[tuple[OpenAIDecisionResponse, ...], tuple[Exception, ...]]:
results: Final = await asyncio.gather(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Long prompts monopolize the shared scan queue

An authenticated caller can stall other users' requests using the same guardrail by submitting a long message: gather schedules every chunk up front on the shared semaphore, while max_input_chars only limits individual chunk size. In a source-method reproduction with defaults, a 10 MiB text queued 477 model calls ahead of a later short request; the configured timeout applies to individual calls rather than the total queue wait. (raw.githubusercontent.com)

Bound total per-request scan work and queue depth, and schedule chunks fairly across requests with an end-to-end deadline.

@veria-ai

veria-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR backports the decision-model guardrail to stable/1.104.x, including prompt scanning through a configured decision provider and chunked processing of long messages.

Two issues remain open, with none addressed so far. Authenticated callers can consume decision-provider quota through blocked prompts without those scan costs counting toward their key or team budgets. Long prompts can also monopolize the shared scan queue and delay other users’ requests, leaving both cost-accounting and service-availability concerns unresolved.

Open issues (2)

Fixed/addressed: 0 · PR risk: 6/10

@AlpianPPLG AlpianPPLG left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants