Repository navigation
feat(guardrails): backport the decision model guardrail to rc/1.105.0 (#45663) - #45894
Conversation
…#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)
There was a problem hiding this comment.
Devin Review found 3 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| 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 |
There was a problem hiding this comment.
🔴 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
Was this helpful? React with 👍 or 👎 to provide feedback.
| setDecisionChecks([]); | ||
| setDecisionProvider(null); |
There was a problem hiding this comment.
🟡 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
| setDecisionChecks([]); | |
| setDecisionProvider(null); | |
| setDecisionChecks([]); | |
| form.setValue("decision_model", undefined); | |
| setDecisionProvider(null); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| return trimDecisionChecks(checks).flatMap((check) => { | ||
| const result = scores.results[check.name]; | ||
| return result && scores.asked[check.name] === check.instructions ? [{ check, result }] : []; |
There was a problem hiding this comment.
🟡 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
Was this helpful? React with 👍 or 👎 to provide feedback.
…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>
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:
How it solves it:
decision_modelguardrail provider, its create form, the Test section, and the Decision Model section in the Guardrail Garden, as in feat(guardrails): add decision model guardrail with Admin UI flow #45663 and its rc/1.106.0 backport feat(guardrails): backport the decision model guardrail to rc/1.106.0 (#45663) #45888User Flow
Before: on rc/1.105.0 an admin who wants Jev to catch prompt injection has no way to set it up
After: the admin adds a decision model as a guardrail and injection attempts get a 400
jev-latest, add their yes/no questions with Block or Log only and a threshold, and try them in the Test sectionPicks
feat(guardrails): add decision model guardrail with Admin UI flow(merge commit 5f9dd38), applied withgit cherry-pick -xrc/1.105.0 predates several things #45663 builds on, so the pick is adapted where they are missing
litellm/decisions/call.pydoes not exist on rc/1.105.0, sosupported_decisions_providers()lives inlitellm/decisions/main.pyand lists rc's own decisions providers. The guardrail endpoints and the unit test import it from thereGuardrailReadOnlyDetails.tsx(feat(guardrails): per-mode stream_scope with bedrock stream and pass-through fixes #43801) does not exist, so the read-only decision model section isdecision_model/DecisionModelDetails.tsx, rendered byguardrail_info.tsxafter Default OnLiteLLMBaseModeldoes not exist, soDecisionModelCheckextendsLiteLLMPydanticObjectBase@/lib/decisionModels(feat(ui): let admins find, call, and test decision models from the Admin UI #45654) does not exist, so the decision mode check is inlined indecisionModelQuestion.tstypesafe.png, and the integration test finds the Jev model by name instead of by logoadd_guardrail_form.tsxandnetworking.tsxconflicted on main-only logging_only and import lines, resolved by keeping rc/1.105.0's lines plus feat(guardrails): add decision model guardrail with Admin UI flow #45663's additionsGET /v2/guardrails/listand guardrail info are built withBaseLitellmParams.model_validateinstead ofBaseLitellmParams(**...), which keeps rc's type-check budget with the two new typed fields. Same result for the same input_lazy_openapi_snapshot.jsonandschema.d.tswere regenerated on this branchTesting
openai/gpt-6-lunaas the decider and apre_callBlock question about instruction overrides:/v1/chat/completions,/v1/responsesand/v1/messageseach returned 400 "Violated decision model guardrail policy" for an injection prompt and 200 for a clean promptGET /v2/guardrails/listreturns the guardrail with itsdecision_modeland checks, andGET /guardrails/ui/add_guardrail_settingsreturnsdecision_model_providers(perplexity, typesafe, openrouter, cloudflare, strands_decider, openai)tests/unit/proxy/guardrails,tests/unit/types,tests/unit/decisionsandtest_llm_judge.py: 5522 passedLinear ticket
Resolves LIT-9443
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
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 moreType
🆕 New Feature
Caveats (if any)
azure_ai, Databricks orhosted_vllmdecisions provider, so the Microsoft and Databricks Decision Model cards in the Guardrail Garden show the "No decision models yet" note on this branch/model/new, and the decision model picker shows no TypeSafe logo next to itBehavior changes
Removed / renamed
Silent regressions to watch
GET /v2/guardrails/listand guardrail info return three more nulllitellm_paramskeys for every guardrail (decision_model,max_input_chars,max_concurrent_decision_calls), andGET /guardrails/ui/add_guardrail_settingsreturns a newdecision_model_providerslistmax_input_charsandmax_concurrent_decision_callsare 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 loadMigration for existing customers
Final Attestation