Skip to content

fix(cost): preserve Anthropic server_tool_use web search usage in cost tracking#31355

Merged
mateo-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_anthropic_web_search_cost
Jun 27, 2026
Merged

fix(cost): preserve Anthropic server_tool_use web search usage in cost tracking#31355
mateo-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_anthropic_web_search_cost

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Anthropic /v1/messages responses report built-in web search usage under usage.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-shape Usage drops server_tool_use, and separately AnthropicResponse.model_validate(...).model_dump(...) stripped the field before cost tracking ever saw it

Linear ticket

N/A

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays 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/messages web search API, no mocks. The model actually runs server-side web searches (both responses carry real web_search_result citations to Wikipedia, CBS, and others) and Anthropic reports usage.server_tool_use.web_search_requests. The per-request spend is read straight off the x-litellm-response-cost-original response header, so the number shown is exactly what the proxy attributes to the call

Setup is identical for before and after. claude-sonnet-4-5 is used because its cost map entry carries the web search per-query price (search_context_size_medium = $0.01):

# config.yaml
model_list:
  - model_name: claude-sonnet-4-5-websearch
    litellm_params:
      model: anthropic/claude-sonnet-4-5
      api_key: os.environ/ANTHROPIC_API_KEY

general_settings:
  master_key: sk-1234
export ANTHROPIC_API_KEY=sk-ant-...        # real key, billed for real searches
litellm --config config.yaml --port 4000

One non-streaming /v1/messages request whose prompt forces a web_search call, so Anthropic returns usage.server_tool_use.web_search_requests:

curl -sS -D - -o /tmp/body.json \
  -X POST http://localhost:4000/v1/messages \
  -H 'content-type: application/json' \
  -H 'x-api-key: sk-1234' \
  -H 'anthropic-version: 2023-06-01' \
  -d '{
        "model": "claude-sonnet-4-5-websearch",
        "max_tokens": 1024,
        "tools": [{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}],
        "messages": [{"role": "user", "content": "Use web_search to find who won the 2022 FIFA World Cup, then answer in one sentence. You must call web_search."}]
      }' | grep -i '^x-litellm-response-cost-original'

jq -r '.usage.server_tool_use, (.content[] | select(.type=="text").text)' /tmp/body.json
# {"web_search_requests": 1, "web_fetch_requests": 0}
# Argentina won the 2022 FIFA World Cup in Qatar, defeating France in the final

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 reported web_search_request is billed

Before (on litellm_internal_staging, without this PR)

The search runs and usage.server_tool_use.web_search_requests is 1, but the reconstructed Usage on the sync cost path drops server_tool_use, so the web search fee is computed as $0 and the spend equals token cost only

x-litellm-response-cost-original: 0.03735

usage: input_tokens=11850  output_tokens=120  server_tool_use.web_search_requests=1
token-only cost = 11850 * $3/M + 120 * $15/M = 0.03735
web search fee billed = 0.03735 - 0.03735 = $0.00   <- dropped

After (with this PR)

Same request, same real search. The fallback recovers web_search_requests from the raw Anthropic response, so the $0.01 per-search fee is added on top of token cost

x-litellm-response-cost-original: 0.047005

usage: input_tokens=11850  output_tokens=97  server_tool_use.web_search_requests=1
token-only cost = 11850 * $3/M + 97 * $15/M = 0.037005
web search fee billed = 0.047005 - 0.037005 = $0.01   <- now included

Type

🐛 Bug Fix

Changes

AnthropicResponseUsageBlock now sets model_config = ConfigDict(extra="allow") so that AnthropicResponse.model_validate(...).model_dump(...) keeps server_tool_use instead of silently dropping it on the logging fallback

StandardBuiltInToolCostTracking reads the web search count straight off the raw Anthropic response dict when the reconstructed Usage lacks server_tool_use. The raw dict is parsed by get_anthropic_web_search_requests_from_response in litellm/llms/anthropic/cost_calculation.py, a small typed Pydantic probe rather than ad-hoc dict access, so the Anthropic-specific parsing lives under llms/ and no new Any/Unknown leaks into the type budget. Detection happens in response_object_includes_web_search_call, and _usage_with_anthropic_web_search synthesizes a ServerToolUse so the Anthropic web-search calculator sees the correct count without mutating the caller's Usage; it copies an existing Usage via model_copy, or builds a fresh one when the cost path is handed only the raw dict

The fallback only activates for a raw Anthropic /v1/messages dict that carries an integer usage.server_tool_use.web_search_requests. Every other response shape keeps its existing behavior, Usage objects that already expose server_tool_use are returned untouched, and a reported count of zero prices the call at $0 rather than the default medium tier

Regression 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 as 0.0, and server_tool_use stripped by model_dump) and pass after, asserting the exact per-query fee times the request count, that the passed-in Usage is not mutated, that a dict-only path with no parallel Usage is still priced per request, and that a zero count charges nothing


Note

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/messages when built-in web search runs: usage.server_tool_use.web_search_requests was dropped before built-in tool cost ran, so spend often omitted the per-search fee.

AnthropicResponseUsageBlock now allows extra fields (extra="allow") so AnthropicResponse.model_validate(...).model_dump() keeps server_tool_use on 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 in response_object_includes_web_search_call recognizes those dicts, and _handle_web_search_cost builds a non-mutating Usage (via model_copy) when the OpenAI-shaped Usage lacks the field—so pricing uses per-request count (including zero requests → $0) rather than a default medium tier when usage is missing or incomplete.

Regression tests cover dict-only usage, dropped server_tool_use on an existing Usage, zero requests, and preserved fields after model_dump.

Reviewed by Cursor Bugbot for commit d282dd2. Bugbot is set up for automated code reviews on this repo. Configure here.

…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

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/anthropic/cost_calculation.py 88.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mateo-berri mateo-berri marked this pull request as ready for review June 25, 2026 22:45
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two related bugs that caused the Anthropic built-in web search fee to be silently dropped from cost tracking: AnthropicResponseUsageBlock now uses extra="allow" so model_validate/model_dump preserves server_tool_use, and the cost tracker gains a raw-dict fallback that reads usage.server_tool_use.web_search_requests directly from the Anthropic response when the reconstructed Usage has already lost it.

  • AnthropicResponseUsageBlock gains ConfigDict(extra="allow"), fixing the logging fallback path where model_dump stripped the field before cost tracking ran.
  • get_anthropic_web_search_requests_from_response (in llms/anthropic/) uses typed Pydantic probe models to extract the web search count from a raw dict; _usage_with_anthropic_web_search synthesizes a copied Usage carrying ServerToolUse without mutating the caller's object.
  • Four new regression tests cover the raw-dict fallback with and without a parallel Usage, zero-request correctness, and round-trip preservation of server_tool_use through model_validate/model_dump.

Confidence Score: 5/5

Safe 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 llms/anthropic/, and model_copy used to avoid mutating callers' Usage. The fallback only activates for raw Anthropic dicts that carry usage.server_tool_use.web_search_requests, leaving every other response shape on its existing path.

No files require special attention.

Important Files Changed

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

Comment thread litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py Outdated
Comment thread litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py
@mateo-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai

…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.
@mateo-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py Outdated
…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.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@mateo-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mateo-berri mateo-berri requested a review from tin-berri June 27, 2026 01:58
@mateo-berri mateo-berri merged commit ef66620 into litellm_internal_staging Jun 27, 2026
126 checks passed
@mateo-berri mateo-berri deleted the litellm_anthropic_web_search_cost branch June 27, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants