fix(proxy): stop putting the literal string "None" in error payloads - #39521
Conversation
A blocked guardrail (and any other HTTP error the proxy converts) came back with "type": "None" and "param": "None", because the converters passed the string "None" as the getattr default instead of None. OpenAI types error.type as a required string and error.param as nullable, so type now falls back to the type its status code stands for and param serializes as JSON null. Covers the non-streaming body, the SSE error frame, the client-disconnect frame, and the unclassified-exception path, so every unified LLM endpoint and the anthropic endpoints return the same shape.
Greptile SummaryThe PR normalizes proxy error metadata so missing exception fields produce status-appropriate OpenAI error types and JSON-null parameters instead of the literal string
Confidence Score: 5/5The PR appears safe to merge with no blocking or independently actionable issues identified. The changed paths preserve valid exception-supplied string fields and integer status codes while replacing invalid stringified-null defaults with OpenAI-compatible fallback types and JSON null, with focused regression coverage across normal and streaming errors.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_request_processing.py | Centralizes error metadata normalization and applies it consistently across the affected proxy error serialization paths; no actionable defect identified. |
| tests/test_litellm/proxy/test_common_request_processing.py | Updates corrected wire-format expectations and adds focused regression tests covering guardrail, streaming, fallback, and field-preservation behavior. |
Reviews (1): Last reviewed commit: "fix(proxy): stop putting the literal str..." | Re-trigger Greptile
|
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 1b4d2e2. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
c67fe2d
into
litellm_internal_staging
… param The proxy's exception tails defaulted `type` and `param` to the four-character string "None", which is neither a known OpenAI error type nor the JSON null the nullable `param` field is typed as, so a client's error handler matched nothing and fell into its generic branch. Lifts the helpers PR BerriAI#39521 added for the unified LLM endpoints into litellm/proxy/common_utils/openai_error_payload.py and calls them from the file, rerank, image, realtime, anthropic, and pass-through route families, plus the shared handle_exception_on_proxy handler that the management, batches, fine-tuning, credential, SCIM, guardrail, and customer routes funnel through. The remaining families (proxy_server, auth, health, spend tracking, and management endpoints) follow in separate PRs so each slice stays QA'able on a live proxy.
TLDR
Problem this solves:
"type": "None"and"param": "None"as stringsHow it solves it:
getattrdefaults were the string"None", notNonetypenow falls back to the type its status code stands forparamnow serializes as JSONnullUser Flow
Before: a developer whose app routes chat through the gateway with a Bedrock guardrail turned on gets an error body whose
typeandparamare the literal stringNone, so their error handling matches no known type and retries a block that will never succeed{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "How do I brew the perfect cup of coffee?"}]}, a prompt their guardrail denies{"error": {"message": "Violated guardrail policy", "type": "None", "param": "None", "code": "400", ...}}error.typeagainst OpenAI's values (invalid_request_error,rate_limit_error, ...), matches none, and falls into its generic unknown-error branch, which retries the same blocked prompterror.paramto say which field was at fault and get the four-character stringNone, so the message they show says the problem is in a field called None"stream": true, and every one of them carries the same two"None"stringsBadRequestErrorwhoseerr.body["param"]is'None', a truthy string, soif err.body["param"]:takes the wrong branchAfter: the same block comes back with a real error type and a null param, so their handler recognizes it, shows the guardrail message, and stops retrying
{"error": {"message": "Violated guardrail policy", "type": "invalid_request_error", "param": null, "code": "400", ...}}invalid_request_error, surfaces the guardrail message to the user, and does not retryerror.param, get JSONnull, and skip the which-field branch entirely"stream": trueall carry the same real type and null paramBadRequestErrorwhoseerr.body["param"]isNone, soif err.body["param"]:takes the branch they meantRelevant issues
Linear ticket
Resolves LIT-6808
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<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 more@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
Shared setup, identical on both sides: one config with a real Bedrock guardrail that denies the topic "coffee", each proxy booted with 2 uvicorn workers on its own port, both against the same Postgres.
Before (658f506)
POST /v1/chat/completions
"None":POST /v1/chat/completions with "stream": true
"None"strings:POST /v1/responses
"None"strings:POST /v1/messages
"None"strings:OpenAI Python SDK
After (1b4d2e2)
POST /v1/chat/completions
POST /v1/chat/completions with "stream": true
POST /v1/responses
POST /v1/messages
OpenAI Python SDK
None:Notes from the run:
authentication_error, 403permission_error, 429rate_limit_errorprovider_specific_fieldsis untouched, so the guardrail detail still rides alongType
🐛 Bug Fix
Caveats (if any)
Low
type, not only guardrail blockstypeused to serialize as JSONnullinternal_server_error, LiteLLM's own value"None"as agetattrdefaultGET /v1/fileswith a badtarget_model_namesstill shows ittypehalf as LIT-6839request_idin spend logs looks related but is notFinal Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
1b4d2e2 passes /live-pr-risk
Note
Medium Risk
Changes shared proxy error-shaping for all routes; behavior is more correct for clients but any consumer that depended on the string
"None"will see differenttype/paramvalues.Overview
Fixes proxy error JSON that used the literal strings
"None"forerror.typeanderror.paramwhen exceptions did not carry those fields—most visible on guardrail blocks and generic failures across streaming and non-streaming routes.Centralized helpers in
common_request_processing.py(_openai_error_type,_openai_error_param,_error_status_code) now derive a real OpenAI-styletypefrom the HTTP status (e.g. 400 →invalid_request_error, 401 →authentication_error, 429 →rate_limit_error) while still honoringtype/paramset on the exception.paramserializes as JSONnullwhen absent.Those helpers replace the old
getattr(..., "None")defaults inproxy_exception_from_http_exception,sse_error_payload, client-disconnect responses,_handle_llm_api_exception, and streaming generator error paths so surfaces stay aligned withProxyException.to_dict(). Tests add LIT-6808 regressions and update expected SSE/non-streaming payloads.Reviewed by Cursor Bugbot for commit 1b4d2e2. Bugbot is set up for automated code reviews on this repo. Configure here.