From f02e095ddb21063b4d6c1135c59b919c418b4947 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:11:53 +0000 Subject: [PATCH] fix(cost): stop token-pricing the placeholder input on file content calls --- litellm/litellm_core_utils/litellm_logging.py | 18 ++++-- .../test_litellm_logging.py | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 83d6fcc0bee..aad4ad1f582 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1411,10 +1411,7 @@ class Logging(LiteLLMLoggingBaseClass): litellm_params=(self.litellm_params if hasattr(self, "litellm_params") else None) ) - prompt = "" # use for tts cost calc - _input = self.model_call_details.get("input", None) - if _input is not None and isinstance(_input, str): - prompt = _input + prompt = self._prompt_for_cost_calculation() if cache_hit is None: cache_hit = self.model_call_details.get("cache_hit", False) @@ -1473,6 +1470,19 @@ class Logging(LiteLLMLoggingBaseClass): return None + def _prompt_for_cost_calculation(self) -> str: + """ + The raw input string is only priced directly for text-to-speech, which bills per character. + Every other call type gets its billable units from the response usage object, and call types + that carry no usage at all (file content retrieval, and anything else `function_setup` cannot + build messages for) only have the ``"default-message-value"`` placeholder here, so passing the + input along would token-price that placeholder. + """ + if self.call_type not in (CallTypes.speech.value, CallTypes.aspeech.value): + return "" + _input = self.model_call_details.get("input", None) + return _input if isinstance(_input, str) else "" + def _generate_content_result_as_model_response(self, result: object) -> Optional[ModelResponse]: """ Native Google :generateContent bodies report token usage under diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index edc257f4c3f..4a9200aaf7c 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -11,6 +11,9 @@ sys.path.insert( import time +import httpx +from openai._legacy_response import HttpxBinaryResponseContent + import litellm from litellm.constants import SENTRY_DENYLIST, SENTRY_PII_DENYLIST from litellm.integrations.custom_logger import CustomLogger @@ -1771,6 +1774,60 @@ def test_response_cost_calculator_does_not_transform_non_generate_content_dict() assert not cost +def _file_content_logging_obj(call_type: str) -> LitellmLogging: + logging_obj = LitellmLogging( + model="gemini-3-flash-preview", + messages="default-message-value", + stream=False, + call_type=call_type, + start_time=time.time(), + litellm_call_id=f"file-content-{call_type}", + function_id=f"file-content-{call_type}", + ) + logging_obj.model_call_details["custom_llm_provider"] = "vertex_ai" + logging_obj.model_call_details["input"] = "default-message-value" + logging_obj.optional_params = {} + return logging_obj + + +@pytest.mark.parametrize("call_type", ["afile_content", "file_content"]) +def test_file_content_call_is_not_billed(call_type): + """ + Regression for #35130: file content retrieval has no token usage, but ``function_setup`` + stores the ``"default-message-value"`` placeholder as the logged input, which the cost + calculator then token-priced, billing every call at exactly 3 * input_cost_per_token. + """ + result = HttpxBinaryResponseContent(httpx.Response(status_code=200, content=b"file contents")) + + cost = _file_content_logging_obj(call_type)._response_cost_calculator(result=result) + + assert cost == 0.0 + + +@pytest.mark.parametrize("call_type", ["aspeech", "speech"]) +def test_speech_call_is_still_priced_from_input_characters(call_type): + """tts bills per input character, so speech call types must keep passing the input along.""" + logging_obj = LitellmLogging( + model="tts-1", + messages="the quick brown fox jumped over the lazy dogs", + stream=False, + call_type=call_type, + start_time=time.time(), + litellm_call_id=f"speech-{call_type}", + function_id=f"speech-{call_type}", + ) + logging_obj.model_call_details["custom_llm_provider"] = "openai" + logging_obj.model_call_details["input"] = "the quick brown fox jumped over the lazy dogs" + logging_obj.optional_params = {} + + result = HttpxBinaryResponseContent(httpx.Response(status_code=200, content=b"audio bytes")) + + cost = logging_obj._response_cost_calculator(result=result) + + assert cost is not None + assert cost > 0 + + def test_sentry_event_scrubber_initialization(monkeypatch): # Step 1: Create a fake sentry_sdk.scrubber module mock_event_scrubber_instance = MagicMock()