fix(anthropic_endpoints): return Anthropic type:error envelope for /v1/messages errors - #39037
Conversation
…1/messages errors
Greptile SummaryThis PR changes route-handled
Confidence Score: 4/5The PR is not yet safe to merge because the previously reported unconditional compatibility break remains and requires a user-controlled transition mechanism. The current route still changes every handled Files Needing Attention: litellm/proxy/anthropic_endpoints/endpoints.py
|
| Filename | Overview |
|---|---|
| litellm/anthropic_interface/exceptions/exceptions.py | Extends the Anthropic error detail type to represent optional read-only provider-specific fields. |
| litellm/proxy/anthropic_endpoints/endpoints.py | Converts route-handled failures into Anthropic envelopes while preserving headers, status mapping, failure hooks, and error-span handling. |
| tests/test_litellm/proxy/anthropic_endpoints/test_endpoints.py | Updates unit coverage to assert Anthropic envelopes and status-to-error-type mapping. |
Reviews (2): Last reviewed commit: "fix(anthropic_endpoints): return Anthrop..." | 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 562664f. Configure here.
TLDR
Problem this solves:
/v1/messagescame back in OpenAI's error envelopetypeandparamwere the literal string"None"{"type":"error","error":{...}}per Anthropic's docsHow it solves it:
invalid_request_error, 429 ->rate_limit_error, ...)error.provider_specific_fieldsrequest_idechoes the client'sx-request-id, matching the existing context-management error pathx-litellm-call-id, ...) and OTel server-span error stamping are preservedUser Flow
Before: a developer whose Anthropic-format app hits a guardrail block gets an OpenAI-shaped error with
"None"strings, so their Anthropic error handling can't classify it{"model": "claude-sonnet-5", "messages": [...]}and the message trips the admin's content guardrail{"error": {"message": "Content blocked: keyword 'kumquat' detected", "type": "None", "param": "None", "code": "400"}}{"type": "error", "error": {"type": ..., "message": ...}}shape api.anthropic.com returns, and the error type reads as the string"None"type: "error"marker and no usable error type, so the block surfaces as an unclassified failureAfter: the same request comes back exactly in Anthropic's documented error format
{"type": "error", "error": {"type": "invalid_request_error", "message": "Content blocked: keyword 'kumquat' detected", "provider_specific_fields": {...}}, "request_id": "req-..."}error.provider_specific_fieldsRelevant issues
Linear ticket
Resolves LIT-6468
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: proxy run from the worktree with
python litellm/proxy/proxy_cli.py --config config.yaml --port <port>(before leg on 35768 at the merge base, after leg on 20535 at this PR's tip), real Anthropic API key, master keysk-1234custom_guardrail.pyraisesHTTPException(400, detail={"error": "Content blocked: keyword 'kumquat' detected", "keyword": "kumquat", "guardrail": "keyword-block"})fromasync_post_call_success_hookwhen the request mentions kumquat, and the same fromasync_pre_call_hookfor durianReference: what api.anthropic.com itself returns for a 400 (the target shape)
Before (81c8c93)
/v1/messages post_call guardrail block (non-streaming)
curl -sS -w '\nHTTP %{http_code}\n' http://localhost:35768/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -H 'x-request-id: req-lit6468-demo' -d '{"model":"claude-sonnet-5","max_tokens":100,"messages":[{"role":"user","content":"Say the word kumquat back to me"}]}'"type":"None","param":"None", norequest_id:/v1/messages pre_call guardrail block with stream: true
curl -sS -w '\nHTTP %{http_code}\n' http://localhost:35768/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -d '{"model":"claude-sonnet-5","max_tokens":50,"stream":true,"messages":[{"role":"user","content":"Tell me about durian fruit"}]}'/v1/messages invalid model
curl -sS -w '\nHTTP %{http_code}\n' http://localhost:35768/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -d '{"model":"no-such-model","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'/v1/messages happy path
curl -sS -w '\nHTTP %{http_code}\n' http://localhost:35768/v1/messages -H 'Authorization: Bearer sk-1234' -H 'content-type: application/json' -d '{"model":"claude-sonnet-5","max_tokens":50,"messages":[{"role":"user","content":"Reply with exactly: hello from QA"}]}'Anthropic python SDK
anthropic.Anthropic(base_url="http://localhost:35768", api_key="sk-1234")thenclient.messages.create(...)with the kumquat messageBadRequestErrorwhose body is the OpenAI envelope with the"None"strings:/v1/chat/completions and /v1/responses (OpenAI-format surfaces)
http://localhost:35768/v1/chat/completions(non-streaming andstream:true) andhttp://localhost:35768/v1/responsesAfter (562664f)
/v1/messages post_call guardrail block (non-streaming)
request_idechoing the client'sx-request-id, guardrail detail preserved:/v1/messages pre_call guardrail block with stream: true
/v1/messages invalid model
x-litellm-call-id: 244ef570-e175-482f-ad76-6e5b778e4914,x-litellm-version: 1.100.0/v1/messages happy path
Anthropic python SDK
BadRequestError(the SDK keys its error classes on the status code), body now the documented envelope:/v1/chat/completions and /v1/responses (OpenAI-format surfaces)
diff), so the OpenAI-format surfaces are untouchedType
🐛 Bug Fix
Caveats (if any)
Medium
/v1/messageserrors must adaptLow
paramis no longer surfaced on this route's errors; Anthropic's envelope has no slot for it/v1/messages/count_tokenserrors are unchanged (FastAPI{"detail": ...}wrapping, out of scope)/v1/messagesimplementation still emits its own OpenAI-ish envelopeTest expectation changes in
tests/test_litellm/proxy/anthropic_endpoints/test_endpoints.py, all because the route now returns an Anthropic-envelopeJSONResponseinstead of re-raisingProxyException:TestProxyExceptionPassthrough->TestProxyExceptionAnthropicEnvelope:pytest.raises(ProxyException)flips to asserting the returned 400 envelope (and a new 429 ->rate_limit_errorcase)TestHttpExceptionDictDetail: same flip; still proves the LIT-6466 clean message and the dict detail, now undererror.provider_specific_fieldsTestFailureHookRequestData: same flip to a returned 500 envelope; the hook-data assertions it exists for are unchangedFinal 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
562664f passes /live-pr-risk
Note
Medium Risk
This is an intentional wire-format change on a primary API surface; status and messages stay the same but clients that parsed the old OpenAI envelope on
/v1/messagesmust adapt.Overview
/v1/messageserror responses now use Anthropic’s documented{"type":"error","error":{...}}shape instead of bubblingProxyExceptionthrough the global OpenAI-style handler (which produced"type":"None"/"param":"None").A new
_anthropic_error_json_responsehelper builds the envelope viaAnthropicExceptionMapping, preserves status codes and LiteLLM headers, stamps OTel server spans like the global handler, echoesx-request-idasrequest_id, and maps HTTP status to Anthropic error types (e.g. 400 →invalid_request_error, 429 →rate_limit_error). Guardrail and other dict-detail failures still attach their payload undererror.provider_specific_fields, now typed onAnthropicErrorDetail.ProxyException,HTTPException, and generic failures inanthropic_responsereturn thisJSONResponserather than re-raising. Tests were updated to assert the Anthropic envelope instead ofpytest.raises(ProxyException).Reviewed by Cursor Bugbot for commit 562664f. Bugbot is set up for automated code reviews on this repo. Configure here.