Skip to content

feat(guardrails): backport the decision model guardrail to rc/1.105.0 (#45663) - #45894

Merged
yucheng-berri merged 1 commit into
rc/1.105.0from
litellm_backport_45663_rc_1_105_0
Oct 11, 2026
Merged

yucheng-berri merged 1 commit into
rc/1.105.0from
litellm_backport_45663_rc_1_105_0

Conversation

@yucheng-berri

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

Copy link
Copy Markdown
Contributor

TLDR

This backports #45663 onto rc/1.105.0, so the decision model guardrail and its Admin UI flow ship in the 1.105.0 rc

Problem this solves:

  • rc/1.105.0 has no guardrail that can use a decision model like Jev to block attacks

How it solves it:

User Flow

Before: on rc/1.105.0 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

rc/1.105.0 predates several things #45663 builds on, so the pick is adapted where they are missing

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/unit/proxy/guardrails, tests/unit/types, tests/unit/decisions and test_llm_judge.py: 5522 passed
  • The dashboard tests for the form, the decision model components, guardrail info and the Guardrail Garden: 132 passed, including two new guardrail info tests for the decision model section
  • ruff_strict, type_discipline, test_quality and type_check gates: OK against rc/1.105.0

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
  • rc/1.105.0 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
  • rc/1.105.0'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

…#45663)

Cherry-pick of #45663 adapted to rc/1.105.0, which predates
litellm/decisions/call.py, GuardrailReadOnlyDetails.tsx, LiteLLMBaseModel,
@/lib/decisionModels and the TypeSafe Add Model provider.

- supported_decisions_providers() lists rc's DECISIONS_ENDPOINTS in
  litellm/decisions/main.py
- The read-only decision model section moves to
  decision_model/DecisionModelDetails.tsx, rendered by guardrail_info.tsx
- DecisionModelCheck extends LiteLLMPydanticObjectBase
- isDecisionMode is inlined and the TypeSafe garden card bundles typesafe.png
- The OpenAPI snapshot and schema.d.ts were regenerated on this branch
- Masked guardrail params are built with BaseLitellmParams.model_validate,
  which keeps rc's type-check budget with the two new typed fields

(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 3 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +146 to +155
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
model=self.decision_model,
input=text,
questions=questions,
num_retries=0,
fallbacks=[],
timeout=self.timeout,
)
return await litellm.adecisions( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType,reportUnknownArgumentType] # SDK dispatch returns a broad union

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.

🔴 Team-scoped decision models fail to run

For a team-scoped decision model, _call_decisions resolves the deployment without the request's team ID. judge_target then selects the SDK, which cannot reach that proxy deployment, so guarded requests fail

Learn more

A team-scoped model group is only visible to the router when resolution includes its team ID. judge_target resolves with team_id=None here, even when the guarded request belongs to the model's team. It therefore chooses the SDK path, which does not carry the proxy deployment's configuration or credentials. The guarded request fails instead of being scored.

Example: A team deploys team-jev and configures it as its decision model. A request from that team passes through the guardrail, but resolution without its team ID selects litellm.adecisions instead of router.adecisions.

Recommended fix: Extract the authenticated team ID from the guarded request and pass it consistently to judge_target and router dispatch. Verify that routing and authorization use the same team context for both resolution and the actual decisions call

Devin Review


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

Comment on lines +397 to +398
setDecisionChecks([]);
setDecisionProvider(null);

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.

🟡 Previous decision model survives provider changes

When the user switches providers and returns, handleProviderChange clears checks but retains decision_model. The next Decision Model guardrail can silently use the previously selected deployment

Suggested change
setDecisionChecks([]);
setDecisionProvider(null);
setDecisionChecks([]);
form.setValue("decision_model", undefined);
setDecisionProvider(null);

Devin Review


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

Comment on lines +148 to +150
return trimDecisionChecks(checks).flatMap((check) => {
const result = scores.results[check.name];
return result && scores.asked[check.name] === check.instructions ? [{ check, result }] : [];

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.

🟡 Untested questions leave old runs marked passing

After a user adds a Block question, visibleTestResults retains scores for older questions without marking the run incomplete. decisionTestOverall can display Pass although the new question was never tested

Learn more

Test history compares current questions against those asked in an earlier run. When a new question is added, the existing answers remain visible but the new question has no result. decisionTestOverall/guardrails/_components/decision_model/decisionModelQuestion.ts:138-140) only examines visible answers, so it can label the incomplete run Pass.

Example: Run a clean test for invoice_policy and receive 0.1. Add a new blocking prompt_injection question. The old run still says Pass although it contains no prompt-injection score.

Recommended fix: Mark a historical run stale when any currently configured question lacks an answer from that run. Suppress the overall Pass badge until all current questions were scored, while preserving useful historical scores where appropriate

Devin Review


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

@yucheng-berri
yucheng-berri merged commit 495288a into rc/1.105.0 Oct 11, 2026
10 of 12 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_backport_45663_rc_1_105_0 branch October 11, 2026 00:38
devin-ai-integration Bot added a commit that referenced this pull request Oct 11, 2026
…ers to litellm.decisions.call

The decision model guardrail backport (#45894) landed on the line after this branch
forked and imports supported_decisions_providers from litellm.decisions.main, which
#45846 replaces here. Match main by exposing it from litellm.decisions.call and point
the guardrail endpoint and its test at that module. schema.d.ts and the lazy OpenAPI
snapshot are regenerated on the merged tree

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

1 participant