litellm - 💡(How to fix) Fix [Bug] /v1/responses doesn't persist guardrail_information in LiteLLM_SpendLogs (Guardrails Monitor undercounts blocks)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When a guardrail blocks a /v1/responses request, the block is enforced correctly (HTTP error returned), but metadata.guardrail_information in LiteLLM_SpendLogs is null. The same guardrail with the same prompt on /v1/chat/completions persists the full guardrail_information object correctly.

Net effect: Guardrails Monitor undercounts blocked requests for any client using the Responses API (e.g. Codex, which defaults to wire_api=responses).

Error Message

When a guardrail blocks a /v1/responses request, the block is enforced correctly (HTTP error returned), but metadata.guardrail_information in LiteLLM_SpendLogs is null. The same guardrail with the same prompt on /v1/chat/completions persists the full guardrail_information object correctly.

/v1/chat/completions → HTTP error + full guardrail_information in SpendLogs (correct)

/v1/responses → HTTP error + guardrail_information = null in SpendLogs (bug)

Metadata propagation gap in the Responses API path. Local traceback for the block goes through: The Chat Completions path likely populates metadata.guardrail_information in the post-block error / spend-log write handler. The Responses API path needs the equivalent — either in response_api_endpoints/endpoints.py, or by sharing the spend-log-writing logic between both paths so guardrail metadata is attached identically.

Root Cause

OpenAI Codex CLI (and other agentic clients) default to wire_api=responses. For these clients, LiteLLM's guardrails enforce correctly but become invisible to monitoring/audit dashboards — making per-team violation reporting, billing for guardrail usage, and Guardrails Monitor stats systematically under-count.

Code Example

# /v1/chat/completions  → HTTP error + full guardrail_information in SpendLogs (correct)
curl -X POST http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "any-model",
    "messages": [{"role": "user", "content": "<phrase that triggers guardrail>"}]
  }'

# /v1/responses  → HTTP error + guardrail_information = null in SpendLogs (bug)
curl -X POST http://localhost:4000/v1/responses \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "any-model",
    "input": "<phrase that triggers guardrail>",
    "stream": false
  }'

---

SELECT request_id, call_type, status,
       metadata ? 'guardrail_information' AS has_gi,
       metadata->'guardrail_information' AS guardrail_information
FROM "LiteLLM_SpendLogs"
WHERE request_id IN ('<chat-call-id>', '<responses-call-id>');

---

request_id (chat):       e79ab1aa-5991-44f4-957e-2707b182d58a
request_id (responses):  7359791c-a0a7-459c-91ab-9d617a0776ad

---

=== chat completions ===
status   = failure
has_gi   = true
gi       = [{"duration": 0.319685, "guardrail_mode": "pre_call",
             "guardrail_name": "cex-input-scan",
             "guardrail_status": "guardrail_failed_to_respond",
             "guardrail_response": "deny", ...}]

=== responses ===
status   = failure
has_gi   = true
gi       = null

---

litellm/proxy/response_api_endpoints/endpoints.py
litellm/proxy/common_request_processing.py
litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py
litellm/llms/openai/responses/guardrail_translation/handler.py
RAW_BUFFERClick to expand / collapse

Summary

When a guardrail blocks a /v1/responses request, the block is enforced correctly (HTTP error returned), but metadata.guardrail_information in LiteLLM_SpendLogs is null. The same guardrail with the same prompt on /v1/chat/completions persists the full guardrail_information object correctly.

Net effect: Guardrails Monitor undercounts blocked requests for any client using the Responses API (e.g. Codex, which defaults to wire_api=responses).

Environment

  • LiteLLM versions confirmed affected: v1.82.6 AND v1.86.0 (4 minor releases apart, still broken)
  • Deployment: LiteLLM proxy + PostgreSQL
  • Guardrail provider — bug reproduced on both:
    • litellm_content_filter (built-in)
    • custom_code (Custom Code Guardrail / YAML sandbox)
  • Strongly suggests the issue is in unified_guardrail or the Responses API spend-log write path, not provider-specific

Reproduction

Set up any pre_call guardrail that blocks on a specific phrase. Send the same prompt to both endpoints:

# /v1/chat/completions  → HTTP error + full guardrail_information in SpendLogs (correct)
curl -X POST http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "any-model",
    "messages": [{"role": "user", "content": "<phrase that triggers guardrail>"}]
  }'

# /v1/responses  → HTTP error + guardrail_information = null in SpendLogs (bug)
curl -X POST http://localhost:4000/v1/responses \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "any-model",
    "input": "<phrase that triggers guardrail>",
    "stream": false
  }'

Then query LiteLLM_SpendLogs:

SELECT request_id, call_type, status,
       metadata ? 'guardrail_information' AS has_gi,
       metadata->'guardrail_information' AS guardrail_information
FROM "LiteLLM_SpendLogs"
WHERE request_id IN ('<chat-call-id>', '<responses-call-id>');

Expected vs Actual

Endpointstatusmetadata.guardrail_information
/v1/chat/completionsfailureFull array: [{guardrail_name, guardrail_status, guardrail_response, duration, ...}]
/v1/responsesfailurenull

Downstream consequences for every /v1/responses block:

  • No row inserted into LiteLLM_SpendLogGuardrailIndex
  • LiteLLM_DailyGuardrailMetrics.blocked_count not incremented
  • Block does not appear in the Guardrails Monitor UI

Real reproduction output (v1.86.0, custom_code provider)

Same pre_call guardrail calling an external scanner, same prompt:

request_id (chat):       e79ab1aa-5991-44f4-957e-2707b182d58a
request_id (responses):  7359791c-a0a7-459c-91ab-9d617a0776ad
=== chat completions ===
status   = failure
has_gi   = true
gi       = [{"duration": 0.319685, "guardrail_mode": "pre_call",
             "guardrail_name": "cex-input-scan",
             "guardrail_status": "guardrail_failed_to_respond",
             "guardrail_response": "deny", ...}]

=== responses ===
status   = failure
has_gi   = true
gi       = null

The has_gi=true (key exists) but gi=null (value null) is suggestive: something on the Responses path is creating the metadata.guardrail_information key but writing null instead of the actual guardrail data.

Suspected Cause

Metadata propagation gap in the Responses API path. Local traceback for the block goes through:

litellm/proxy/response_api_endpoints/endpoints.py
litellm/proxy/common_request_processing.py
litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py
litellm/llms/openai/responses/guardrail_translation/handler.py

The guardrail hook raises HTTPException correctly, but the final SpendLog write path for Responses API doesn't carry the guardrail intervention metadata into metadata.guardrail_information. The Chat Completions path does this correctly.

Why this matters

OpenAI Codex CLI (and other agentic clients) default to wire_api=responses. For these clients, LiteLLM's guardrails enforce correctly but become invisible to monitoring/audit dashboards — making per-team violation reporting, billing for guardrail usage, and Guardrails Monitor stats systematically under-count.

Possible fix area

The Chat Completions path likely populates metadata.guardrail_information in the post-block error / spend-log write handler. The Responses API path needs the equivalent — either in response_api_endpoints/endpoints.py, or by sharing the spend-log-writing logic between both paths so guardrail metadata is attached identically.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - 💡(How to fix) Fix [Bug] /v1/responses doesn't persist guardrail_information in LiteLLM_SpendLogs (Guardrails Monitor undercounts blocks)