fix(cost-tracking): bill web searches reported only in server_side_tool_usage_details

This commit is contained in:
mateo-berri 2026-08-11 19:24:53 -07:00
parent 6bce073520
commit d96f76ca66
2 changed files with 50 additions and 0 deletions

View file

@ -2,6 +2,7 @@
Helper utilities for tracking the cost of built-in tools.
"""
from collections.abc import Mapping
from typing import Any, Final, Literal
import litellm
@ -23,6 +24,14 @@ from litellm.types.utils import (
)
def _usage_reports_server_side_web_search_calls(usage: Usage) -> bool:
details: Final = getattr(usage, "server_side_tool_usage_details", None)
if not isinstance(details, Mapping):
return False
calls: Final = details.get("web_search_calls")
return isinstance(calls, int) and calls > 0
class StandardBuiltInToolCostTracking:
"""
Helper class for tracking the cost of built-in tools
@ -351,6 +360,10 @@ class StandardBuiltInToolCostTracking:
# and _handle_web_search_cost() is never called.
if hasattr(usage, "server_tool_use") and _get_web_search_requests(usage.server_tool_use) is not None:
return True
# xAI reports usage.server_side_tool_usage_details.web_search_calls; a searched
# answer with no url_citation annotations has no other chat-path signal
if _usage_reports_server_side_web_search_calls(usage):
return True
return False
elif isinstance(response_object, ResponsesAPIResponse):
# response api explicitly includes web_search_call in the output
@ -370,6 +383,8 @@ class StandardBuiltInToolCostTracking:
)
):
return True
if _usage_reports_server_side_web_search_calls(usage):
return True
return False

View file

@ -632,3 +632,38 @@ def test_response_includes_output_type_reads_dict_output_items():
assert not StandardBuiltInToolCostTracking.response_includes_output_type(
response_object=response, output_type="file_search_call"
)
def test_web_search_gate_reads_server_side_tool_usage_details_without_citations():
"""
Regression: xAI chat responses bridged from the Responses API only carry
usage.server_side_tool_usage_details; a searched answer with no url_citation
annotations must still be billed for its web search calls.
"""
from litellm.llms.xai.cost_calculator import _DEFAULT_WEB_SEARCH_COST_PER_CALL
from litellm.types.utils import Usage
usage = Usage(
prompt_tokens=10,
completion_tokens=20,
total_tokens=30,
server_side_tool_usage_details={"web_search_calls": 3},
)
response = ModelResponse(model="xai/grok-4.5")
assert StandardBuiltInToolCostTracking.response_object_includes_web_search_call(
response_object=response, usage=usage
)
assert not StandardBuiltInToolCostTracking.response_object_includes_web_search_call(
response_object=response,
usage=Usage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
)
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
model="xai/grok-4.5",
response_object=response,
usage=usage,
custom_llm_provider="xai",
standard_built_in_tools_params=None,
)
assert cost == 3 * _DEFAULT_WEB_SEARCH_COST_PER_CALL