diff --git a/litellm/integrations/dotprompt/prompt_manager.py b/litellm/integrations/dotprompt/prompt_manager.py index a0d5be71392..fd0b17ba746 100644 --- a/litellm/integrations/dotprompt/prompt_manager.py +++ b/litellm/integrations/dotprompt/prompt_manager.py @@ -149,7 +149,6 @@ class PromptManager: ) self.prompts[template_id] = template except Exception: - # Optional: print(f"Error loading prompt from JSON: {template_id}") pass def _load_prompt_file(self, file_path: str | Path, prompt_id: str) -> PromptTemplate: 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 9a2c4e244fb..9250b92e268 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 @@ -7,7 +7,9 @@ from typing import Any, Final, Literal import litellm from litellm.constants import OPENAI_FILE_SEARCH_COST_PER_1K_CALLS -from litellm.litellm_core_utils.llm_cost_calc.utils import get_web_search_requests +from litellm.litellm_core_utils.llm_cost_calc.utils import ( + get_web_search_requests_from_usage, +) from litellm.types.llms.openai import ( FileSearchTool, ResponsesAPIResponse, @@ -368,7 +370,7 @@ class StandardBuiltInToolCostTracking: get_anthropic_web_search_requests_from_response, ) - if usage is not None and (get_web_search_requests(getattr(usage, "server_tool_use", None)) is not None): + if usage is not None and (get_web_search_requests_from_usage(usage) is not None): return usage web_search_requests: Final = get_anthropic_web_search_requests_from_response(response_object) if web_search_requests is None: @@ -416,7 +418,7 @@ class StandardBuiltInToolCostTracking: # Anthropic Claude (direct API and Vertex AI) uses server_tool_use.web_search_requests. # Without this check, Claude ModelResponse always falls through to return False # 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: + if get_web_search_requests_from_usage(usage) 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 @@ -429,16 +431,12 @@ class StandardBuiltInToolCostTracking: response_object=response_object, output_type="web_search_call" ) elif usage is not None: - if ( - hasattr(usage, "server_tool_use") - and get_web_search_requests(usage.server_tool_use) is not None - or ( - hasattr(usage, "prompt_tokens_details") - and usage.prompt_tokens_details is not None - and isinstance(usage.prompt_tokens_details, PromptTokensDetailsWrapper) - and hasattr(usage.prompt_tokens_details, "web_search_requests") - and usage.prompt_tokens_details.web_search_requests is not None - ) + if get_web_search_requests_from_usage(usage) is not None or ( + hasattr(usage, "prompt_tokens_details") + and usage.prompt_tokens_details is not None + and isinstance(usage.prompt_tokens_details, PromptTokensDetailsWrapper) + and hasattr(usage.prompt_tokens_details, "web_search_requests") + and usage.prompt_tokens_details.web_search_requests is not None ): return True if _usage_reports_server_side_web_search_calls(usage): diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 9d782cf7a4d..bdbaee00c19 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -92,6 +92,16 @@ def get_web_search_requests(server_tool_use: Any) -> int | None: return getattr(server_tool_use, "web_search_requests", None) +def get_web_search_requests_from_usage(usage: Usage) -> int | None: + """Read ``web_search_requests`` from a ``Usage``'s ``server_tool_use``. + + ``Usage`` deletes unset optional fields from ``__dict__`` (see + ``SafeAttributeModel``), so direct attribute access can raise + ``AttributeError``; ``getattr`` with a default is required here. + """ + return get_web_search_requests(getattr(usage, "server_tool_use", None)) + + def _is_above_128k(tokens: float) -> bool: if tokens > 128000: return True diff --git a/litellm/llms/anthropic/cost_calculation.py b/litellm/llms/anthropic/cost_calculation.py index ec6c480efcc..95615b8e748 100644 --- a/litellm/llms/anthropic/cost_calculation.py +++ b/litellm/llms/anthropic/cost_calculation.py @@ -10,7 +10,7 @@ from pydantic import BaseModel, ValidationError from litellm.litellm_core_utils.llm_cost_calc.utils import ( generic_cost_per_token, get_provider_specific_geo_multiplier, - get_web_search_requests, + get_web_search_requests_from_usage, ) if TYPE_CHECKING: @@ -104,7 +104,7 @@ def get_cost_for_anthropic_web_search( if usage is None: return 0.0 - web_search_requests: Final = get_web_search_requests(getattr(usage, "server_tool_use", None)) + web_search_requests: Final = get_web_search_requests_from_usage(usage) if web_search_requests is None: return 0.0 diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index d7b527824ea..3597b8c329e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1358,12 +1358,10 @@ class LiteLLMAnthropicMessagesAdapter: @classmethod def _get_web_search_request_count(cls, usage: Usage) -> int: from litellm.litellm_core_utils.llm_cost_calc.utils import ( - get_web_search_requests, + get_web_search_requests_from_usage, ) - from_server_tool_use: Final = cls._positive_int( - get_web_search_requests(getattr(usage, "server_tool_use", None)) - ) + from_server_tool_use: Final = cls._positive_int(get_web_search_requests_from_usage(usage)) if from_server_tool_use > 0: return from_server_tool_use return cls._first_positive_prompt_tokens_detail_value(usage, ("web_search_requests",)) diff --git a/litellm/llms/gemini/cost_calculator.py b/litellm/llms/gemini/cost_calculator.py index 52285af1f5f..b82103b0ff8 100644 --- a/litellm/llms/gemini/cost_calculator.py +++ b/litellm/llms/gemini/cost_calculator.py @@ -39,7 +39,9 @@ def cost_per_web_search_request(usage: "Usage", model_info: "ModelInfo") -> floa ``model_info`` when available, falling back to $0.035 for models not yet updated in the pricing JSON. """ - from litellm.litellm_core_utils.llm_cost_calc.utils import get_web_search_requests + from litellm.litellm_core_utils.llm_cost_calc.utils import ( + get_web_search_requests_from_usage, + ) from litellm.types.utils import PromptTokensDetailsWrapper _DEFAULT_COST: Final = 35e-3 @@ -57,7 +59,7 @@ def cost_per_web_search_request(usage: "Usage", model_info: "ModelInfo") -> floa ) else None ) - requests_from_server_tool_use: Final = get_web_search_requests(getattr(usage, "server_tool_use", None)) + requests_from_server_tool_use: Final = get_web_search_requests_from_usage(usage) number_of_web_search_requests: Final = requests_from_prompt_details or requests_from_server_tool_use or 0 billing_mode: Final = model_info.get("web_search_billing_unit") or "per_prompt" diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 375e1117371..47554913419 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -2661,7 +2661,7 @@ async def test_run_direct_health_check_drops_only_the_rejected_kwarg(monkeypatch rejected argument alongside working ones would probe deployments the operator opted out.""" import litellm.proxy.proxy_server as proxy_server - seen: list = [] + seen: list[tuple[dict[str, str] | None, bool]] = [] async def fake_perform_health_check( model_list, diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking_dict_safety.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking_dict_safety.py index 3a0a3574539..78bf9292ef5 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking_dict_safety.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking_dict_safety.py @@ -10,8 +10,8 @@ import pytest from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, - get_web_search_requests, ) +from litellm.litellm_core_utils.llm_cost_calc.utils import get_web_search_requests from litellm.types.utils import ModelResponse, ServerToolUse, Usage diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index b690b3448ec..5fc4a361e78 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -17,7 +17,12 @@ from litellm.anthropic_interface import messages from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler -from litellm.types.utils import Delta, ModelResponse, StreamingChoices +from litellm.types.utils import ( + Delta, + ModelResponse, + StandardLoggingPayloadErrorInformation, + StreamingChoices, +) def test_anthropic_experimental_pass_through_messages_handler(): @@ -1292,7 +1297,7 @@ class TestMessagesStreamingSuccessLogging: class _FailureCapture(CustomLogger): def __init__(self): super().__init__() - self.error_information: List[Dict[str, Any]] = [] + self.error_information: list[StandardLoggingPayloadErrorInformation] = [] async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time): payload = kwargs.get("standard_logging_object") or {} diff --git a/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py b/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py index 27115ffe241..44b8bb3c9a2 100644 --- a/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py +++ b/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py @@ -8,10 +8,8 @@ See https://github.com/BerriAI/litellm/issues/26153. import pytest -from litellm.llms.anthropic.cost_calculation import ( - get_cost_for_anthropic_web_search, - get_web_search_requests, -) +from litellm.litellm_core_utils.llm_cost_calc.utils import get_web_search_requests +from litellm.llms.anthropic.cost_calculation import get_cost_for_anthropic_web_search from litellm.types.utils import ModelInfo, ServerToolUse