fix(anthropic): report real token usage on blocked responses - #31217
Conversation
Greptile SummaryThis PR fixes synthetic guardrail-blocked responses in the
Confidence Score: 5/5Safe to merge — the change is isolated to blocked-response synthesis paths and falls back gracefully to zero usage on any error. The fix is well-scoped: it only touches the ModifyResponseException handler branches, which are off the happy path. Both new helpers are pure functions with a blanket exception handler, so the worst case is the same zero-usage behavior as before. Tests are mock-based, cover the main wiring, system-prompt inclusion, and the error fallback. No changes to auth, routing, or database access. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/anthropic_endpoints/endpoints.py | Adds _get_blocked_response_usage helper and wires it into the ModifyResponseException handler; imports AnthropicUsage for the return type; correctly includes the top-level system field in input token counting. |
| litellm/proxy/proxy_server.py | Adds module-level _blocked_response_usage helper and updates all three ModifyResponseException handler sites (chat_completion streaming + non-streaming, completion streaming + non-streaming) to use real token counts; removes the two hard-coded zero-usage assignments. |
| tests/test_litellm/proxy/anthropic_endpoints/test_endpoints.py | Adds TestBlockedResponseUsage class with five properly-mocked tests covering nonzero counts, output wiring, system-prompt inclusion, fallback, and an integration smoke test through the full handler. |
| tests/test_litellm/proxy/test_blocked_response_usage.py | New file with three mock-based tests covering the _blocked_response_usage helper for messages+tools, text-prompt, and error-fallback paths. |
Reviews (3): Last reviewed commit: "fix(proxy): report real token usage on O..." | Re-trigger Greptile
4134c87 to
b56e20c
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
9ded5cf to
5666852
Compare
5666852 to
6014334
Compare
|
@greptile-apps review |
|
Thanks for this thoughtful fix for blocked-response token counting — the test coverage looks thorough and Greptile gave it a 5/5! A couple of items to address before this can be merged:\n\n1. CI lint is failing — there is a check showing FAILURE. Could you take a look and fix the lint error so all checks are green?\n2. Proof of fix — the PR body describes the change well but it would be great to see some concrete evidence that the fix works end-to-end (e.g. test output showing non-zero token counts returned for a blocked response, or a before/after log snippet). Even a short output snippet would do!\n\nOnce those are addressed this looks close to ready. Thanks! |
6014334 to
1a557d1
Compare
Thanks for the review! Fixed the lint issues, and I'll add an end-to-end example soon. |
1a557d1 to
8532357
Compare
@Sameerlite Thanks again for the review. I've updated the PR to add correct token counting for PR description has been updated with example responses. |
Sameerlite
left a comment
There was a problem hiding this comment.
The proper fix would be:
- Add an original_response optional field to ModifyResponseException
- Pass the LLM response into it when raising post-call
- In the handler that catches ModifyResponseException, use e.original_response.usage directly instead of re-counting tokens
The token counter approach is a workaround for the symptom (zero usage) rather than fixing the root cause (usage being discarded)
26ac40c to
cca71a0
Compare
8532357 to
881bded
Compare
|
Thanks! Addressed both: 1. CI / lint & conflicts — rebased onto the latest 2. Proof of fix — concrete before/after for a guardrail-blocked response (the exact Backed by mock-based unit tests (8 passing) covering chat (messages+tools), text completion (prompt), the Anthropic top-level |
When a guardrail blocks a post-call response, the synthetic violation response reported hard-coded zero usage, discarding the token usage the upstream call had already consumed. Fix the root cause rather than re-counting tokens: - Add an optional `original_response` field to ModifyResponseException. - The unified guardrail's post-call success hook attaches the blocked LLM response to the exception. - The /v1/messages and OpenAI-format (/v1/chat/completions, /v1/completions) block handlers report `original_response.usage` directly. Pre-call blocks never invoked the LLM, so usage is zero. Mock-based tests cover the helper (returns original usage / zero), the success hook attaching original_response, and the endpoint reporting it end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
881bded to
370308c
Compare
|
@Sameerlite good call — you're right that re-counting was treating the symptom. Reworked it to fix the root cause per your suggestion:
Tests updated to mock-based coverage: the helper returns the original response's usage (and zero when absent), the success hook attaches |
When a guardrail blocks a post-call response, the synthetic violation response reported hard-coded zero usage, discarding the token usage the upstream call had already consumed. Fix the root cause rather than re-counting tokens: - Add an optional `original_response` field to ModifyResponseException. - The unified guardrail's post-call success hook attaches the blocked LLM response to the exception. - The /v1/messages and OpenAI-format (/v1/chat/completions, /v1/completions) block handlers report `original_response.usage` directly. Pre-call blocks never invoked the LLM, so usage is zero. Mock-based tests cover the helper (returns original usage / zero), the success hook attaching original_response, and the endpoint reporting it end-to-end. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(prometheus): bound per-request budget metric emission with a timeout (#31632) * fix(prometheus): bound per-request budget metric emission with a timeout Wrap the per-request budget-metric gather in asyncio.wait_for so a slow Redis or DB lookup cannot consume the whole LoggingWorker watchdog and get the success-logging event cancelled. On timeout the emission is skipped in isolation; budget gauges are still refreshed by the periodic cron. The timeout is configurable via PROMETHEUS_BUDGET_METRICS_PER_REQUEST_TIMEOUT and defaults to 5.0 seconds, falling back to the default on an invalid value instead of raising * fix(prometheus): reject non-finite and non-positive budget-metrics timeout env float() accepts 0, negatives, nan and inf, which bypass the fallback: a value <= 0 makes asyncio.wait_for time out immediately and skip every per-request emission, and inf reintroduces the unbounded wait the timeout was meant to bound. Validate the parsed value is finite and greater than zero before using it, otherwise fall back to the default * fix: report the blocked LLM response's real token usage (#31217) When a guardrail blocks a post-call response, the synthetic violation response reported hard-coded zero usage, discarding the token usage the upstream call had already consumed. Fix the root cause rather than re-counting tokens: - Add an optional `original_response` field to ModifyResponseException. - The unified guardrail's post-call success hook attaches the blocked LLM response to the exception. - The /v1/messages and OpenAI-format (/v1/chat/completions, /v1/completions) block handlers report `original_response.usage` directly. Pre-call blocks never invoked the LLM, so usage is zero. Mock-based tests cover the helper (returns original usage / zero), the success hook attaching original_response, and the endpoint reporting it end-to-end. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(guardrails): buffer + cleanly terminate streamed responses on block (#31389) Streaming moderation improvements for the unified guardrail post-call streaming iterator hook: - streaming_buffer_until_moderated: withhold all chunks until end-of-stream moderation passes, then release the original response (clean) or only the block message (blocked) -- the original content is never delivered on a block. Snapshot chunks with a shallow list() copy (end-of-stream builds a separate assembled response; chunks aren't mutated in place). - Clean Anthropic SSE on block: synthesize a well-formed termination sequence instead of a bare data: {"error": ...} blob that truncates the stream. Provider-specific synthesis lives in AnthropicMessagesHandler via build_block_sse_chunks (format-agnostic routing stays in the hook). - Mid-stream blocks continue the in-progress message (close open content block, append block message, terminate) rather than emitting a second message_start, which clients reject. Standalone envelope only when no chunks were sent (buffered path). - ModifyResponseException imported under TYPE_CHECKING + locally at runtime to avoid a module-level cyclic import. Adds regression tests for buffering (content withheld on block) and mid-stream continuation (single message_start). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: report real usage on streaming blocks, disable buffered mode for content-rewriting guardrails - _standalone_block_chunks and _block_continuation_chunks now read real token usage from ModifyResponseException.original_response instead of hardcoding zero, matching the non-streaming _blocked_response_usage path. Shared helper moved to guardrail_translation/utils.py. - streaming_buffer_until_moderated is now forced off when the guardrail has mask_response_content=True, since buffered replay releases the withheld original chunks verbatim -- unsafe for a guardrail that rewrites content (e.g. PII masking). - Fix inverted streaming-flag precedence comment. * style: ruff format after greploop fixes * fix: handle Anthropic streaming guardrail blocks * fix(responses): check terminal event type for streaming guardrail end-of-stream detection _check_streaming_has_ended assumed responses_so_far held ModelResponse objects with .choices, but for the Responses API the accumulated chunks are raw SSE event dicts, causing an AttributeError on every call * fix: preserve Anthropic blocked stream usage --------- Co-authored-by: FERNANDO IZAR <fizar@me.com> Co-authored-by: Joseph Barker <156112794+seph-barker@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
When a guardrail blocks a post-call response, the synthetic violation response reported hard-coded zero usage, discarding the token usage the upstream call had already consumed. Fix the root cause rather than re-counting tokens: - Add an optional `original_response` field to ModifyResponseException. - The unified guardrail's post-call success hook attaches the blocked LLM response to the exception. - The /v1/messages and OpenAI-format (/v1/chat/completions, /v1/completions) block handlers report `original_response.usage` directly. Pre-call blocks never invoked the LLM, so usage is zero. Mock-based tests cover the helper (returns original usage / zero), the success hook attaching original_response, and the endpoint reporting it end-to-end. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The
ModifyResponseExceptionhandler in the/v1/messagesendpoint synthesizes a "blocked" response reporting zero input and output tokens, even though the request consumed real input tokens and the synthetic block message carries real content. Callers relying on usage (billing, quotas, metrics) under-count every blocked response.Compute
input_tokensfrom the original request messages (carried on the exception'srequest_data) andoutput_tokensfrom the block message text vialitellm.token_counter. Counting is best-effort and falls back to zero on failure so a blocked response is always returned. The streaming synthesis path reuses the same response object, so both paths are fixed by one change.Adds tests asserting nonzero, correct counts and graceful fallback.
Example responses, showing correct token counts.
From a request to
/v1/chat/completionswith a moderated response:From a request to
/v1/messages: