Skip to content

feat(guardrails): return structured guardrail_response on /apply_guardrail for success and blocked responses - #29384

Open
jainashu37 wants to merge 1 commit into
BerriAI:litellm_oss_branchfrom
jainashu37:feat/apply-guardrail-response-on-success-and-block
Open

feat(guardrails): return structured guardrail_response on /apply_guardrail for success and blocked responses#29384
jainashu37 wants to merge 1 commit into
BerriAI:litellm_oss_branchfrom
jainashu37:feat/apply-guardrail-response-on-success-and-block

Conversation

@jainashu37

@jainashu37 jainashu37 commented May 31, 2026

Copy link
Copy Markdown

Relevant issues

Related to #28970 (same endpoint, independent and self-contained change).

Type

New Feature

Changes

The /apply_guardrail endpoint only returned response_text. This change surfaces the full guardrail result on both the success path and the blocked path.

  1. Add a new optional field guardrail_response to ApplyGuardrailResponse.

  2. On success (HTTP 200), the endpoint populates guardrail_response from the guardrail's own logged result. Guardrails write their result (standard_logging_guardrail_information) into the request_data dict in their finally block, so the endpoint reads it back from the same dict via _collect_guardrail_info_from_data.

  3. On a block (HTTP 403), _enrich_guardrail_block_exception raises a ProxyException with provider_specific_fields set to the structured classification, so the failure response carries the same guardrail detail as success, instead of a stringified dict inside the message field.

This is provider agnostic. It works for any CustomGuardrail (litellm_content_filter, Bedrock, and others) because they all write to the shared standard_logging_guardrail_information container.

Pre-Submission checklist

  • Added testing in tests/test_litellm/ (mocked unit tests for the helpers and the new model field)
  • My PR passes unit tests on make test-unit
  • My PR scope is isolated. It solves one specific problem
  • Requested a @greptileai review (will do right after opening)

Screenshots / Proof of Fix

Success (HTTP 200):

{
  "response_text": "What is the capital of India",
  "guardrail_response": [
    {
      "guardrail_name": "content-safety-multi",
      "guardrail_status": "success",
      "guardrail_provider": "litellm_content_filter",
      "guardrail_response": []
    }
  ]
}

Blocked (HTTP 403):

{
  "error": {
    "message": "Content blocked: denied_medical_advice category keyword 'medicine' detected (severity: high)",
    "code": "403",
    "provider_specific_fields": {
      "guardrail_response": [
        {
          "guardrail_name": "content-safety-multi",
          "guardrail_status": "guardrail_intervened",
          "guardrail_response": [
            { "category": "denied_medical_advice", "keyword": "medicine", "severity": "high", "action": "BLOCK" }
          ]
        }
      ]
    }
  }
}

@CLAassistant

CLAassistant commented May 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codspeed-hq

codspeed-hq Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Congrats! CodSpeed is installed 🎉

🆕 16 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces structured guardrail results on the /apply_guardrail endpoint for both the success (HTTP 200) and blocked (HTTP 403) paths. Previously the endpoint only returned response_text; callers had no structured access to the guardrail's classification.

  • Success path: request_data is initialised before the try block and passed into apply_guardrail; after the guardrail's finally block writes standard_logging_guardrail_information into the dict, _collect_guardrail_info_from_data reads it back and populates ApplyGuardrailResponse.guardrail_response.
  • Blocked path: _enrich_guardrail_block_exception wraps HTTPException blocks into a ProxyException with provider_specific_fields carrying the same structured list, so the 403 JSON body contains guardrail_response rather than a stringified dict.
  • Model change: ApplyGuardrailResponse gains an optional guardrail_response: Optional[List[Dict[str, Any]]] field (defaults to None, fully backwards-compatible).

Confidence Score: 5/5

Safe to merge — the change is additive, the endpoint returns an enriched response on both paths, and all existing behaviour is preserved when no guardrail info is available.

The previously flagged success-path gap (guardrail_response never populated on HTTP 200) is correctly resolved by moving request_data before the try block and calling _collect_guardrail_info_from_data after apply_guardrail returns. The failure path correctly converts HTTPException to ProxyException with provider_specific_fields, which the existing exception handler serialises at the right HTTP status code. The model addition is backwards-compatible. No new regressions are introduced.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_endpoints.py Adds _collect_guardrail_info_from_data and _enrich_guardrail_block_exception helpers; moves request_data before the try block; populates guardrail_response on both the 200 and 403 paths correctly.
litellm/types/guardrails.py Adds optional guardrail_response field to ApplyGuardrailResponse; backwards-compatible addition with None default.
tests/test_litellm/proxy/guardrails/test_apply_guardrail_response.py New unit tests for both helpers and the model field; all mocked, no real network calls.

Reviews (2): Last reviewed commit: "feat(guardrails): return structured guar..." | Re-trigger Greptile

Comment on lines +2324 to +2330
return ProxyException(
message=message if isinstance(message, str) else str(message),
type=ProxyErrorTypes.internal_server_error,
param="None",
code=e.status_code,
provider_specific_fields={"guardrail_response": info},
)

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.

P2 ProxyErrorTypes.internal_server_error is semantically wrong for a guardrail block. The HTTP status code (e.status_code, typically 403) is preserved correctly in code, but the type field in the serialized error body will read "internal_server_error", which is misleading for clients that decode it. A more descriptive type (e.g. bad_request_error, or ideally a dedicated guardrail_blocked value) should be used here.

Suggested change
return ProxyException(
message=message if isinstance(message, str) else str(message),
type=ProxyErrorTypes.internal_server_error,
param="None",
code=e.status_code,
provider_specific_fields={"guardrail_response": info},
)
return ProxyException(
message=message if isinstance(message, str) else str(message),
type=ProxyErrorTypes.bad_request_error,
param="None",
code=e.status_code,
provider_specific_fields={"guardrail_response": info},
)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +2286 to +2290
for key in ("metadata", "litellm_metadata"):
container = data.get(key) or {}
found = container.get("standard_logging_guardrail_information")
if found:
entries.extend(found if isinstance(found, list) else [found])

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.

P2 Possible duplicate entries when both metadata namespaces are populated

The loop iterates ("metadata", "litellm_metadata") and appends entries from each independently. If a guardrail (or a pre-call hook) writes standard_logging_guardrail_information into both data["metadata"] and data["litellm_metadata"], the same guardrail run will appear twice in the collected list. A deduplication step or a clear policy on which namespace takes precedence would prevent callers from seeing doubled results.

@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/guardrails/guardrail_endpoints.py 96.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jainashu37
jainashu37 force-pushed the feat/apply-guardrail-response-on-success-and-block branch from a65a5a1 to 3d5dbd5 Compare June 1, 2026 05:47
@jainashu37
jainashu37 changed the base branch from main to litellm_oss_branch June 1, 2026 05:47
…drail (success + block)

The /apply_guardrail endpoint only returned response_text. This surfaces the
full guardrail result on both paths:

- Add guardrail_response: Optional[List[Dict[str, Any]]] to ApplyGuardrailResponse.
- On success (HTTP 200), populate it from the guardrail's own logged result
  (standard_logging_guardrail_information, written into request_data in the
  guardrail's finally block) via _collect_guardrail_info_from_data.
- On a block (HTTP 403), _enrich_guardrail_block_exception raises a ProxyException
  with provider_specific_fields={"guardrail_response": ...} so the failure
  response carries the same structured classification as success, instead of a
  stringified dict in message.

Provider agnostic: works for any CustomGuardrail (litellm_content_filter,
Bedrock, etc.) because they all write to the shared
standard_logging_guardrail_information container.

Adds mocked unit tests for the helpers and the new model field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jainashu37
jainashu37 force-pushed the feat/apply-guardrail-response-on-success-and-block branch from 3d5dbd5 to 2b18e2b Compare June 1, 2026 05:50
@jainashu37

Copy link
Copy Markdown
Author

@greptileai please re-review. The success-path gap from the previous review is fixed: the endpoint now populates guardrail_response itself via _collect_guardrail_info_from_data on HTTP 200 (no custom hook required), and the 403 path still returns the structured classification via provider_specific_fields.

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.

2 participants