fix(guardrails): serve config guardrails from list and info endpoints without a DB and make their ids stable - #35259
Conversation
… without a DB and make their ids stable
Greptile SummaryThis PR makes config-defined guardrails available through list and info endpoints without Prisma and assigns deterministic IDs to guardrails lacking explicit IDs.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code failure identified. The database-optional paths preserve existing filtering, masking, source selection, and response conversion while allowing config guardrails to resolve from memory, and deterministic ID generation retains explicit IDs and assigns distinct repeatable IDs to duplicate names.
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_endpoints.py | Adds safe no-database fallbacks to list and info endpoints and exposes in-memory guardrail IDs in the v1 response. |
| litellm/proxy/guardrails/guardrail_registry.py | Replaces random implicit config guardrail IDs with deterministic UUIDv5 IDs and collision probing for duplicate names. |
| tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py | Adds focused tests for no-Prisma list and info behavior, unknown IDs, and v1 ID propagation. |
| tests/test_litellm/proxy/guardrails/test_guardrail_registry.py | Adds tests for restart-stable IDs, explicit-ID precedence, and deterministic duplicate-name handling. |
Reviews (1): Last reviewed commit: "fix(guardrails): serve config guardrails..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5ae1f15. Configure here.
TLDR
Problem this solves:
How it solves it:
Relevant issues
Fixes #35256
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
All legs use this config (guardrail defined only in config.yaml, no explicit guardrail_id, which is the normal case):
The no-DB legs boot the proxy through a wrapper that strips DATABASE_URL after dotenv loads (the repo .env would otherwise attach a DB):
Before (base ae242fd, no DB, port 51377)
/guardrails/undefined/infois the exact request the UI ends up making: v2 500s, the UI falls back to the v1 list, its rows have no id, and the row click fetches idundefined. The live in-memory id is unobtainable by any API consumer pre-fix, and the 500 fires before any lookup, so it is identical for every idAfter (this PR's head 5ae1f15, no DB, port 51919, boot 1)
After, restart stability (same head, no DB, port 51919, boot 2)
The id is byte-identical across boots, and the pre-restart id keeps resolving after the restart
After, with-DB sanity (same head, DATABASE_URL set, Postgres on localhost:5432, port 52466)
DB-defined guardrails keep their DB-generated ids and the list shape is unchanged; the config guardrail carries the same stable id with and without a DB
Type
🐛 Bug Fix
Changes
litellm/proxy/guardrails/guardrail_endpoints.py:/v2/guardrails/listandGET /guardrails/{id}plus/guardrails/{id}/infono longer raise 500 when prisma is not initialized; they treat the DB as empty and serve the in-memory config guardrails that both endpoints already knew how to merge. The info endpoint 404s only when neither source has the id._get_guardrails_list_response(the v1 list the UI falls back to) now includesguardrail_id; the ids are present on the config dicts becauseinitialize_guardrailwrites them back at startuplitellm/proxy/guardrails/guardrail_registry.py: a config guardrail without an explicitguardrail_idnow getsuuid5(CONFIG_GUARDRAIL_ID_NAMESPACE, guardrail_name)instead of a freshuuid4per process, so the id survives restarts and matches across replicas. An explicitguardrail_idin yaml still winsSafety analysis for the deterministic id:
Duplicate
guardrail_nameentries in config are legitimate today (load balancing across guardrail deployments with the same name, see_populate_router_guardrail_list). A plain name hash would make the second occurrence collide with the first and silently skip its initialization, so on collision the derivation walks deterministic seeds (name,name:1,name:2, ...) until a free id is found. Occurrences keep distinct ids that are stable across boots as long as the config keeps its order, and since the entries share a name a reorder only swaps ids between interchangeable deploymentsConsumers audited:
usage_tracking.pyaggregates daily usage by guardrail_id, so stable ids stop the per-restart fragmentation of usage rows for config guardrails (rows written under old random ids stay under those ids, a one-time discontinuity)._init_guardrails_in_db,sync_guardrail_from_db, andreconcile_db_guardrailsonly handle ids read from DB rows, which are DB-generated uuid4 values, and reconciliation never touches config-sourced entries, so there is no interaction with derived ids beyond a cryptographically negligible uuid collision. The mutation endpoints (PUT, PATCH, DELETE on /guardrails/{id}) check DB existence first and keep 404ing for config ids exactly as before. Thesource="db"callers ofinitialize_guardrailalways pass ids from DB rows, so the derived-id path is effectively config-only. No blocker foundQA runbook
config.yamland the no-DB wrapper asno_db_proxy.pylsof -nP -iTCP:51919 -sTCP:LISTENmust print nothingpython no_db_proxy.py config.yaml 51919curl -s -w "\nHTTP %{http_code}\n" http://localhost:51919/v2/guardrails/list -H "Authorization: Bearer sk-1234"and expect HTTP 200 listingtoolingwith aguardrail_idandguardrail_definition_locationconfigcurl -s http://localhost:51919/guardrails/list -H "Authorization: Bearer sk-1234" | jq '.guardrails[].guardrail_id'and expect the same id, not nullcurl -s -w "\nHTTP %{http_code}\n" http://localhost:51919/guardrails/<that id>/info -H "Authorization: Bearer sk-1234"and expect HTTP 200DATABASE_URLset (python litellm/proxy/proxy_cli.py --config config.yaml --port 52466), rerun step 4, and expect DB guardrails listed asdbalongside the config guardrail with the same id as the no-DB bootstoolingrow and expect its info panel instead of "Guardrail not found"Final Attestation
Note
Medium Risk
Deterministic config guardrail IDs change identity across upgrades (one-time usage/analytics discontinuity) and duplicate-name ordering affects which occurrence gets which id; read-path behavior for no-DB proxies is otherwise low risk.
Overview
Fixes Admin UI "Guardrail not found" and 500s on proxies without a database by making list/info endpoints work when Prisma is unset, and by returning stable IDs for config-defined guardrails.
No-DB / read APIs:
GET /v2/guardrails/listandGET /guardrails/{id}/info(andGET /guardrails/{id}) no longer fail with Prisma client not initialized. They treat the DB as empty and still merge config guardrails from the in-memory registry. Unknown IDs return 404 instead of 500. The v1GET /guardrails/listhelper now includesguardrail_idso UI fallback rows don’t request/guardrails/undefined/info.Stable config IDs: Config guardrails without an explicit
guardrail_idget a deterministicuuid5fromguardrail_name(withname:1,name:2, … for duplicate names) instead of a newuuid4per process, so IDs survive restarts and match across replicas. Explicit YAMLguardrail_idstill wins.Reviewed by Cursor Bugbot for commit 5ae1f15. Bugbot is set up for automated code reviews on this repo. Configure here.