Merge pull request #35140 from BerriAI/litellm_fix_file_content_placeholder_cost

fix(cost): stop token-pricing the placeholder input on file content calls
This commit is contained in:
Mateo Wang 2026-08-06 03:04:36 -07:00 committed by GitHub
commit 972c0d0b04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 4 deletions

View file

@ -1399,10 +1399,7 @@ class Logging(LiteLLMLoggingBaseClass):
litellm_params=(self.litellm_params if hasattr(self, "litellm_params") else None)
)
prompt = "" # use for tts cost calc
_input: Final = 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)
@ -1461,6 +1458,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) -> ModelResponse | None:
"""
Native Google :generateContent bodies report token usage under

View file

@ -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()