fix(passthrough,streaming): close Anthropic pass-through + streaming cost-capture gaps - #31023
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR closes several cost-attribution gaps in Anthropic pass-through and streaming paths:
Confidence Score: 4/5The change is generally safe — all new error handling is defensive (try/except with fallbacks, never masking the original error), and the core streaming and pass-through logging paths are not restructured, only augmented with recovery logic. The fixes are well-scoped and the error-handling additions are best-effort (they never raise or disrupt the original flow). The main open question is the absence of any test coverage or before/after evidence for the seven described fix paths; without that it's hard to be certain the recovery logic fires correctly and doesn't introduce subtle double-logging for partial-usage + failure events on the same request. anthropic_passthrough_logging_handler.py and streaming_handler.py carry the most new logic (usage-only fallback path, _log_partial_usage_on_stream_error) and have no accompanying tests in this PR.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/litellm_logging.py | Adds get_model_from_passthrough_model_group to recover provider/model from the model_group metadata for pass-through requests priced at $0 due to model=unknown; integrates it into the central cost calculator. |
| litellm/litellm_core_utils/streaming_handler.py | Adds _log_partial_usage_on_stream_error for mid-flight stream failures and wraps stream_chunk_builder in try/except (sync and async) to fall back to calculate_total_usage when assembly raises; calls partial-usage logging from timeout and exception handlers. |
| litellm/proxy/auth/auth_checks.py | Enriches BudgetExceededError with key_alias and masked key_name in the error message; BudgetExceededError.init already accepts a message kwarg, so the change is well-formed. |
| litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py | Wraps _build_complete_streaming_response in try/except and adds _build_usage_only_response_from_chunks to recover cost from raw SSE events when full assembly fails; records resolved model and response_cost into model_call_details up-front. |
| litellm/proxy/pass_through_endpoints/llm_provider_handlers/base_passthrough_logging_handler.py | Adds model recovery and up-front model_call_details recording to the base handler's cost-calculation and streaming-assembly paths; mirrors the Anthropic-specific changes for all pass-through providers. |
| litellm/proxy/pass_through_endpoints/streaming_handler.py | Single-line fix: adds errors=replace to the UTF-8 decode of raw SSE bytes so a stream cut mid-multibyte sequence doesn't discard all usage events already received. |
Reviews (2): Last reviewed commit: "fix(passthrough,streaming): close Anthro..." | Re-trigger Greptile
|
|
a81dff9 to
0bdeed2
Compare
…cost-capture gaps Streaming and pass-through requests could be logged with $0 cost, an "unknown" model, or dropped from SpendLogs entirely while the provider still billed every token. This consolidates fixes for the distinct leak paths into one change, plus a key-identification improvement for budget errors. - Recover "<provider>/<model>" from the inferred pass-through model_group in the central cost calculator and the per-provider handlers, so cost is priced instead of recorded as $0. Record the resolved model on the logging object up-front so a later failure cannot leave model="unknown". - Treat a stream_chunk_builder raise the same as a None result in the core CustomStreamWrapper (sync + async) and the Anthropic pass-through handler. Large/agentic streams (tool-use, thinking, web-search) can make assembly re-raise from inside the success path, which otherwise escaped and dropped the request from SpendLogs. - Add a usage-only fallback for Anthropic streaming pass-through: when full assembly fails, rebuild usage from the message_start / message_delta SSE events via AnthropicConfig.calculate_usage (the same path the success case uses) so cache, web-search and geo tokens are priced correctly. - Log partial usage when a stream dies mid-flight (client disconnect / request timeout / mid-stream error) instead of recording zero usage. - Decode buffered pass-through bytes with errors="replace" so a stream cut mid-multibyte-sequence still logs the usage already received. - Record response_cost into model_call_details on the pass-through success path (it is read from there, not from kwargs). - Identify the key (alias + masked key) in the virtual-key budget error so operators don't have to reverse-map spend back to a key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0bdeed2 to
7c5ed7a
Compare
|
Thanks for the contribution! A couple of things to help get this ready:
Also kicking off a fresh Greptile review on this. |
Summary
Streaming and pass-through requests could be logged with
$0cost,model="unknown", or dropped from SpendLogs entirely — while the upstream provider still billed every token. This consolidates fixes for the distinct leak paths into one change, plus a key-identification improvement for budget errors.Scenarios fixed (and why each is needed)
Each item is a concrete path where spend was silently lost, which breaks cost attribution, budget enforcement, and chargeback.
Pass-through request logged with
model="unknown"→ priced at $0./anthropic/v1/messages,/openai/...etc. reach logging before the model is resolved, socompletion_cost()cannot price the request.→ Recover
<provider>/<model>from the inferredmodel_groupin the central cost calculator and the per-provider handlers, and record the resolved model on the logging object up-front so a later failure can't leavemodel="unknown".stream_chunk_builderraises on large / agentic streams → the whole request is dropped.Tool-use / thinking / web-search streams can make assembly re-raise (as
APIError) from inside the success handler /except StopIteration, so it escapes__next__/__anext__and the Anthropic pass-through handler. The request never reaches SpendLogs even though every token was billed.→ Catch the raise in the core
CustomStreamWrapper(sync and async) and in the Anthropic handler, and treat it the same as aNoneresult.Usage-only fallback undercount.
When full assembly fails, rebuild usage from the
message_start/message_deltaSSE events viaAnthropicConfig.calculate_usage— the same path the success case uses — so prompt tokens are cache-inclusive andcache_read/cache_write/web_search/ geo tokens are priced correctly. The previous fallback (Usage(...)+setattr) left those fields unset, so they were priced at $0.Stream dies mid-flight → zero usage logged.
Client disconnect / request timeout / mid-stream provider error: success-only logging records nothing, though the provider billed the tokens generated before the failure.
→
_log_partial_usage_on_stream_error()assembles partial usage from the chunks already received and logs it via the normal success path, called from the failure/timeout handlers. Best-effort: it never raises and never masks the original streaming error.A stream cut mid-multibyte-sequence dropped the whole request.
b"".join(raw_bytes).decode("utf-8")raised, discarding all usage events already received.→ decode with
errors="replace".Streaming pass-through spend not persisted.
The pass-through success path reads spend from
model_call_details["response_cost"], not fromkwargs, so streaming pass-through logged $0 even when the cost was computed.→ record
response_cost(and the resolved model) intomodel_call_details.Opaque virtual-key budget errors.
BudgetExceededErrorfor a virtual key didn't name the key, forcing operators to reverse-map a spend figure back to a key during an incident.→ include the key alias + masked key (which carries the last 4 chars) in the error message.
Repro (headline case — items 1–3)
POST /anthropic/v1/messagesrequest that exercises tool-use or a long agentic turn (enough thatstream_chunk_buildercan't reassemble it) — or disconnect the client mid-stream.$0cost /model="unknown", or missing entirely.🤖 Generated with Claude Code