fix(cost): preserve Anthropic server_tool_use web search usage in cost tracking#31355
Conversation
…t tracking Anthropic /v1/messages responses report built-in web search usage under usage.server_tool_use.web_search_requests, but the sync cost path reconstructs an OpenAI-shape Usage that drops server_tool_use and validates the response through AnthropicResponse, which previously stripped the field. Either path could leave the web-search fee uncounted. AnthropicResponseUsageBlock now allows extra fields so model_validate/model_dump keeps server_tool_use, and the built-in tool cost tracker reads the web search count straight off the raw Anthropic response dict when the reconstructed Usage lacks it, synthesizing a ServerToolUse without mutating the caller's Usage.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes two related bugs that caused the Anthropic built-in web search fee to be silently dropped from cost tracking:
Confidence Score: 5/5Safe to merge — all changes are on the cost-tracking fallback path, existing behavior for non-Anthropic providers is unchanged, and zero-request cases return 0.0 correctly. The fix is narrow and well-tested: two root causes addressed with matching regression tests, provider-specific code correctly placed in No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py | Adds response_object forwarding to _handle_web_search_cost, plus _usage_with_anthropic_web_search to synthesize a Usage with server_tool_use from a raw Anthropic dict when the reconstructed Usage lacks it; also extends response_object_includes_web_search_call to detect the Anthropic raw-dict path. |
| litellm/llms/anthropic/cost_calculation.py | Adds typed Pydantic probe classes and get_anthropic_web_search_requests_from_response to safely extract usage.server_tool_use.web_search_requests from a raw Anthropic /v1/messages dict; provider-specific parsing correctly lives in llms/anthropic/. |
| litellm/types/llms/anthropic.py | Adds model_config = ConfigDict(extra="allow") to AnthropicResponseUsageBlock so model_validate/model_dump no longer silently drops server_tool_use and other extra usage fields returned by Anthropic. |
| tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py | Adds four regression tests: raw-dict fallback with and without a parallel Usage, zero-request correctness, and AnthropicResponseUsageBlock round-trip preservation of server_tool_use; all are mock-only with no network calls. |
Reviews (4): Last reviewed commit: "fix(cost): price Anthropic web search wh..." | Re-trigger Greptile
…fy strict-rule budget
…anthropic Relocate the raw /v1/messages web-search-count probe out of the shared built-in tool cost tracker into litellm/llms/anthropic/cost_calculation.py, next to get_cost_for_anthropic_web_search, so provider-specific response parsing lives under llms/. The core cost tracker now delegates to get_anthropic_web_search_requests_from_response and keeps only the generic Usage/ServerToolUse orchestration.
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing usage skips count pricing
- _usage_with_anthropic_web_search now synthesizes a Usage with server_tool_use from the raw Anthropic dict when no Usage is passed, so _handle_web_search_cost runs the per-request Anthropic path (including zero) instead of falling back to the medium-tier flat fee.
You can send follow-ups to the cloud agent here.
…ies the count response_object_includes_web_search_call enters the web search branch as soon as the raw Anthropic dict reports usage.server_tool_use.web_search_requests, but _usage_with_anthropic_web_search bailed when the caller did not also pass a Usage object. _handle_web_search_cost then skipped the per-request anthropic path and fell back to the flat search_context_size_medium tier, charging a fixed fee instead of per_query x count (or zero when the count is zero). Synthesize a Usage from the raw dict when no Usage is supplied so count-based pricing runs uniformly regardless of how the response reaches the tracker.
|
|
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
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 d282dd2. Configure here.
Relevant issues
Anthropic
/v1/messagesresponses report built-in web search usage underusage.server_tool_use.web_search_requests, but the spend record intermittently missed the web-search fee. On the sync cost path the response is the raw Anthropic dict while the reconstructed OpenAI-shapeUsagedropsserver_tool_use, and separatelyAnthropicResponse.model_validate(...).model_dump(...)stripped the field before cost tracking ever saw itLinear ticket
N/A
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Screenshots / Proof of Fix
Fully end to end against a live proxy hitting the real Anthropic
/v1/messagesweb search API, no mocks. The model actually runs server-side web searches (both responses carry realweb_search_resultcitations to Wikipedia, CBS, and others) and Anthropic reportsusage.server_tool_use.web_search_requests. The per-request spend is read straight off thex-litellm-response-cost-originalresponse header, so the number shown is exactly what the proxy attributes to the callSetup is identical for before and after.
claude-sonnet-4-5is used because its cost map entry carries the web search per-query price (search_context_size_medium = $0.01):One non-streaming
/v1/messagesrequest whose prompt forces aweb_searchcall, so Anthropic returnsusage.server_tool_use.web_search_requests:Output tokens differ slightly between the two calls since each is a fresh live generation, so each total is decomposed against that response's own
usage. Input tokens are identical at 11850 and the only thing that changes is whether the one reportedweb_search_requestis billedBefore (on
litellm_internal_staging, without this PR)The search runs and
usage.server_tool_use.web_search_requestsis1, but the reconstructedUsageon the sync cost path dropsserver_tool_use, so the web search fee is computed as$0and the spend equals token cost onlyAfter (with this PR)
Same request, same real search. The fallback recovers
web_search_requestsfrom the raw Anthropic response, so the $0.01 per-search fee is added on top of token costType
🐛 Bug Fix
Changes
AnthropicResponseUsageBlocknow setsmodel_config = ConfigDict(extra="allow")so thatAnthropicResponse.model_validate(...).model_dump(...)keepsserver_tool_useinstead of silently dropping it on the logging fallbackStandardBuiltInToolCostTrackingreads the web search count straight off the raw Anthropic response dict when the reconstructedUsagelacksserver_tool_use. The raw dict is parsed byget_anthropic_web_search_requests_from_responseinlitellm/llms/anthropic/cost_calculation.py, a small typed Pydantic probe rather than ad-hoc dict access, so the Anthropic-specific parsing lives underllms/and no newAny/Unknownleaks into the type budget. Detection happens inresponse_object_includes_web_search_call, and_usage_with_anthropic_web_searchsynthesizes aServerToolUseso the Anthropic web-search calculator sees the correct count without mutating the caller'sUsage; it copies an existingUsageviamodel_copy, or builds a fresh one when the cost path is handed only the raw dictThe fallback only activates for a raw Anthropic
/v1/messagesdict that carries an integerusage.server_tool_use.web_search_requests. Every other response shape keeps its existing behavior,Usageobjects that already exposeserver_tool_useare returned untouched, and a reported count of zero prices the call at$0rather than the default medium tierRegression coverage lives in
tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py. The tests fail before this change (fee computed as0.0, andserver_tool_usestripped bymodel_dump) and pass after, asserting the exact per-query fee times the request count, that the passed-inUsageis not mutated, that a dict-only path with no parallelUsageis still priced per request, and that a zero count charges nothingNote
Medium Risk
Changes only billing/cost attribution for Anthropic web search, but incorrect fees directly affect spend records; scope is narrow and covered by regression tests.
Overview
Fixes under-billing on Anthropic
/v1/messageswhen built-in web search runs:usage.server_tool_use.web_search_requestswas dropped before built-in tool cost ran, so spend often omitted the per-search fee.AnthropicResponseUsageBlocknow allows extra fields (extra="allow") soAnthropicResponse.model_validate(...).model_dump()keepsserver_tool_useon the logging path instead of stripping it.Built-in web search cost tracking now treats raw Anthropic response dicts as first-class: a typed helper reads
usage.server_tool_use.web_search_requests, detection inresponse_object_includes_web_search_callrecognizes those dicts, and_handle_web_search_costbuilds a non-mutatingUsage(viamodel_copy) when the OpenAI-shapedUsagelacks the field—so pricing uses per-request count (including zero requests → $0) rather than a default medium tier whenusageis missing or incomplete.Regression tests cover dict-only usage, dropped
server_tool_useon an existingUsage, zero requests, and preserved fields aftermodel_dump.Reviewed by Cursor Bugbot for commit d282dd2. Bugbot is set up for automated code reviews on this repo. Configure here.