Repository navigation
feat(guardrails): backport the decision model guardrail to stable/1.104.x (#45663) - #45900
Conversation
…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)
There was a problem hiding this comment.
Devin Review found 1 potential issue.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| @staticmethod | ||
| def get_config_model() -> type[DecisionModelGuardrailConfigModel] | None: | ||
| return DecisionModelGuardrailConfigModel |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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 |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
PR overviewThis 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 |
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:
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 backports feat(guardrails): backport the decision model guardrail to rc/1.106.0 (#45663) #45888 and feat(guardrails): backport the decision model guardrail to rc/1.105.0 (#45663) #45894User Flow
Before: on stable/1.104.x 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): backport the decision model guardrail to rc/1.105.0 (#45663)(1804758), applied withgit cherry-pick -x#45894 is #45663 (merge commit 5f9dd38) adapted to an rc that predates
litellm/decisions/call.py,GuardrailReadOnlyDetails.tsx,LiteLLMBaseModel,@/lib/decisionModelsand 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 listtests/test_litellm/proxy/guardrails/guardrail_hooks, sotest_decision_model.pylives there_lazy_openapi_snapshot.jsonandschema.d.tsmatch a regeneration 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/test_litellm/proxy/guardrails,tests/unit/decisions,tests/unit/proxy/decisions_endpoints,tests/unit/typesandtest_llm_judge.py: 5289 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