diff --git a/litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py b/litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py index 221b1ae6eab..361fee8f1ad 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py +++ b/litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py @@ -50,33 +50,40 @@ class StandardBuiltInToolCostTracking: """ standard_built_in_tools_params = standard_built_in_tools_params or {} - # Handle web search - if StandardBuiltInToolCostTracking.response_object_includes_web_search_call( - response_object=response_object, usage=usage - ): - return StandardBuiltInToolCostTracking._handle_web_search_cost( + web_search_cost = ( + StandardBuiltInToolCostTracking._handle_web_search_cost( model=model, custom_llm_provider=custom_llm_provider, usage=usage, standard_built_in_tools_params=standard_built_in_tools_params, response_object=response_object, ) + if StandardBuiltInToolCostTracking.response_object_includes_web_search_call( + response_object=response_object, usage=usage + ) + else 0.0 + ) - # Handle file search - if StandardBuiltInToolCostTracking.response_object_includes_file_search_call(response_object=response_object): - return StandardBuiltInToolCostTracking._handle_file_search_cost( + file_search_cost = ( + StandardBuiltInToolCostTracking._handle_file_search_cost( model=model, custom_llm_provider=custom_llm_provider, standard_built_in_tools_params=standard_built_in_tools_params, ) + if StandardBuiltInToolCostTracking.response_object_includes_file_search_call( + response_object=response_object + ) + else 0.0 + ) - # Handle Azure assistant features - return StandardBuiltInToolCostTracking._handle_azure_assistant_costs( + azure_assistant_cost = StandardBuiltInToolCostTracking._handle_azure_assistant_costs( model=model, custom_llm_provider=custom_llm_provider, standard_built_in_tools_params=standard_built_in_tools_params, ) + return web_search_cost + file_search_cost + azure_assistant_cost + @staticmethod def _handle_web_search_cost( model: str, @@ -428,12 +435,11 @@ class StandardBuiltInToolCostTracking: Returns: True if the ResponsesAPIResponse includes one of the specified output types, False otherwise. """ - output = response_object.output - for output_item in output: - _output_type: Optional[str] = getattr(output_item, "type", None) - if _output_type == output_type: - return True - return False + return any( + (output_item.get("type") if isinstance(output_item, dict) else getattr(output_item, "type", None)) + == output_type + for output_item in response_object.output + ) @staticmethod def _safe_get_model_info(model: str, custom_llm_provider: Optional[str] = None) -> Optional[ModelInfo]: diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py index 24fd3c94ee3..2c3794771f8 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py @@ -604,3 +604,45 @@ def test_web_search_provider_prefix_fallback_does_not_misprice_non_gemini_model( # Note: File search integration test removed due to complex annotation detection logic # The unit tests in test_azure_assistant_cost_tracking.py provide comprehensive coverage + +def test_web_search_and_file_search_costs_are_both_billed(local_model_cost_map): + """ + A response that used both web search and file search must be billed for both. + + Regression for the bug where get_cost_for_built_in_tools returned after the first + matching tool category, silently dropping the second tool's cost. + """ + from litellm.constants import OPENAI_FILE_SEARCH_COST_PER_1K_CALLS + from litellm.types.llms.openai import ResponsesAPIResponse + + model = "gpt-4o-search-preview" + response = ResponsesAPIResponse( + id="resp_123", + created_at=1234567890, + model=model, + object="response", + output=[ + {"type": "web_search_call", "id": "ws_1", "status": "completed"}, + {"type": "file_search_call", "id": "fs_1", "status": "completed"}, + ], + ) + standard_built_in_tools_params = StandardBuiltInToolsParams( + web_search_options=WebSearchOptions(search_context_size="high"), + file_search=FileSearchTool(type="file_search"), + ) + + cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( + model=model, + response_object=response, + usage=None, + custom_llm_provider="openai", + standard_built_in_tools_params=standard_built_in_tools_params, + ) + + expected_web_search_cost = litellm.get_model_info(model)["search_context_cost_per_query"][ + "search_context_size_high" + ] + expected_file_search_cost = OPENAI_FILE_SEARCH_COST_PER_1K_CALLS + assert expected_web_search_cost > 0 + assert expected_file_search_cost > 0 + assert cost == pytest.approx(expected_web_search_cost + expected_file_search_cost)