fix(spend): drop non-finite Responses and OpenRouter costs and the breakdown of an unpriceable request

This commit is contained in:
mateo-berri 2026-10-28 15:16:12 -07:00
parent f149dea9de
commit f49b20161f
11 changed files with 115 additions and 18 deletions

View file

@ -951,9 +951,7 @@ def single_log_line(value: object) -> str:
def finite_reported_cost(value: object) -> float | None:
cost: Final = finite_cost(value)
if cost is None and isinstance(value, float):
verbose_logger.warning(
"Ignoring a non-finite provider-reported cost %s, the cost map prices the request", value
)
verbose_logger.warning("Ignoring a non-finite provider-reported cost %s", value)
return cost

View file

@ -2198,6 +2198,7 @@ class Logging(LiteLLMLoggingBaseClass):
verbose_logger.warning(
"Recording a non-finite response_cost as 0 for litellm_call_id=%s", single_log_line(self.litellm_call_id)
)
self.cost_breakdown = None
return 0.0
def uncached_price(self, result: object) -> UncachedPrice:
@ -7465,7 +7466,7 @@ def get_standard_logging_object_payload(
verbose_logger.warning(
"Logging a non-finite response_cost=%s as 0 for litellm_call_id=%s",
numeric_response_cost,
logging_obj.litellm_call_id,
single_log_line(logging_obj.litellm_call_id),
)
llm_response_cost: Final[float] = priced_response_cost or 0.0
guardrail_cost: Final = guardrail_information_cost(metadata.get("standard_logging_guardrail_information"))

View file

@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Final, cast
import httpx
from pydantic import ConfigDict, TypeAdapter
from litellm.litellm_core_utils.core_helpers import finite_cost
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.chat.transformation import BaseLLMException
@ -225,8 +226,11 @@ class OpenrouterConfig(OpenAIGPTConfig):
additional_headers: Final = cast( # cast-ok: preserve mapping operations on response metadata
dict[str, object], hidden_params["additional_headers"]
)
if isinstance(response_cost, str | int | float):
additional_headers["llm_provider-x-litellm-response-cost"] = float(response_cost)
reported_cost: Final = (
finite_cost(float(response_cost)) if isinstance(response_cost, str | int | float) else None
)
if reported_cost is not None:
additional_headers["llm_provider-x-litellm-response-cost"] = reported_cost
except Exception:
# If we can't extract cost, continue without it - don't fail the response
pass

View file

@ -34,7 +34,7 @@ from litellm.constants import (
DB_SPEND_UPDATE_JOB_NAME,
INTERNAL_CALL_ORIGIN_METADATA_KEY,
)
from litellm.litellm_core_utils.core_helpers import finite_cost
from litellm.litellm_core_utils.core_helpers import finite_cost, single_log_line
from litellm.litellm_core_utils.litellm_logging import coerce_model_access_groups
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
from litellm.proxy._types import (
@ -653,7 +653,7 @@ class DBSpendUpdateWriter:
verbose_proxy_logger.warning(
"Spend tracking - recording a non-finite response_cost=%s as 0 for request_id=%s",
response_cost,
payload["request_id"],
single_log_line(payload["request_id"]),
)
payload["spend"] = billable_cost or 0.0
if isinstance(payload["startTime"], datetime):

View file

@ -111,7 +111,7 @@ _DECODED_JSON: Final = TypeAdapter(object)
def _zero_for_non_finite_constant(constant: str) -> float:
verbose_proxy_logger.warning(
"Redis spend buffer - reading a %s spend an older version parked as 0, the spend it held is lost", constant
"Redis spend buffer - reading a parked %s spend as 0, the spend it held is lost", constant
)
return 0.0
@ -208,7 +208,7 @@ class RedisUpdateBuffer:
if transactions is None or len(transactions) == 0:
return
list_of_transactions: Final = [safe_dumps(transactions, non_finite_as_null=True)]
list_of_transactions: Final = [safe_dumps(transactions)]
if self.redis_cache is None:
return
try:
@ -369,7 +369,7 @@ class RedisUpdateBuffer:
rpush_list.append(
RedisPipelineRpushOperation(
key=redis_key,
values=[safe_dumps(transactions, non_finite_as_null=True)],
values=[safe_dumps(transactions)],
)
)
service_types.append(service_type)
@ -559,7 +559,7 @@ class RedisUpdateBuffer:
)
rpush_list: Final = tuple(
RedisPipelineRpushOperation(key=redis_key, values=(safe_dumps(transactions, non_finite_as_null=True),))
RedisPipelineRpushOperation(key=redis_key, values=(safe_dumps(transactions),))
for transactions, redis_key in restore_configs
if transactions
)

View file

@ -1,4 +1,5 @@
import builtins
import math
from collections.abc import Iterable, Mapping
from enum import Enum
from os import PathLike
@ -82,6 +83,7 @@ from typing_extensions import (
override,
)
from litellm._logging import verbose_logger
from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject, LiteLLMBaseModel
from litellm.types.responses.main import (
CustomToolCallOutputItem,
@ -1440,9 +1442,11 @@ class ResponseAPIUsage(BaseLiteLLMOpenAIResponseObject):
@classmethod
def parse_cost(cls, v: object) -> object:
"""Normalise cost: accept either a float or a dict with a ``total_cost`` key."""
if isinstance(v, dict):
return v.get("total_cost")
return v
cost: Final = v.get("total_cost") if isinstance(v, dict) else v
if isinstance(cost, float) and not math.isfinite(cost):
verbose_logger.warning("Dropping a non-finite provider usage cost")
return None
return cost
model_config = ConfigDict(extra="allow")

View file

@ -2062,7 +2062,7 @@ class Usage(SafeAttributeModel, CompletionUsage):
if cost is None:
del self.cost
elif isinstance(cost, float) and not math.isfinite(cost):
verbose_logger.warning("Dropping a non-finite provider usage cost, the cost map prices the request")
verbose_logger.warning("Dropping a non-finite provider usage cost")
del self.cost
else:
self.cost = cost

View file

@ -6135,17 +6135,33 @@ def test_get_standard_logging_object_payload_logs_a_non_finite_response_cost_as_
def test_an_overflowing_custom_price_records_a_zero_response_cost(caplog: pytest.LogCaptureFixture) -> None:
messages: Final = [{"role": "user", "content": "hello world"}]
start_time: Final = datetime.datetime.now()
logging_obj: Final = Logging(
model="gpt-4o",
messages=messages,
stream=False,
call_type="completion",
start_time=start_time,
litellm_call_id="overflow\nforged log line",
function_id="overflow",
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
response: Final = litellm.completion(
model="gpt-4o",
mock_response="hi there",
messages=[{"role": "user", "content": "hello world"}],
messages=messages,
input_cost_per_token=1e308,
output_cost_per_token=1e308,
litellm_call_id="overflow\nforged log line",
litellm_logging_obj=logging_obj,
)
logging_obj._process_hidden_params_and_response_cost(
response, start_time=start_time, end_time=datetime.datetime.now()
)
assert response._hidden_params["response_cost"] == 0.0
payload: Final = logging_obj.model_call_details["standard_logging_object"]
assert json.loads(json.dumps(payload, allow_nan=False, default=str))["response_cost"] == 0.0
warnings: Final = [r.getMessage() for r in caplog.records if "non-finite response_cost" in r.getMessage()]
assert warnings == ["Recording a non-finite response_cost as 0 for litellm_call_id=overflowforged log line"]

View file

@ -1,6 +1,7 @@
import json
from typing import Final
from unittest.mock import MagicMock
import httpx
import pytest
@ -464,6 +465,34 @@ def test_openrouter_cost_tracking_non_streaming():
)
@pytest.mark.parametrize("cost", [float("nan"), float("inf")])
def test_openrouter_chat_leaves_a_non_finite_provider_cost_out_of_the_response_headers(cost: float) -> None:
body: Final = {
"id": "gen-123",
"object": "chat.completion",
"created": 1234567890,
"model": "openai/gpt-5.6-luna",
"choices": [{"message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop", "index": 0}],
"usage": {"prompt_tokens": 10, "completion_tokens": 1, "total_tokens": 11, "cost": cost},
}
result: Final = OpenrouterConfig().transform_response(
model="openrouter/openai/gpt-5.6-luna",
raw_response=httpx.Response(
200, content=json.dumps(body).encode(), headers={"content-type": "application/json"}
),
model_response=litellm.ModelResponse(),
logging_obj=MagicMock(),
request_data={},
messages=[{"role": "user", "content": "Reply with ok"}],
optional_params={},
litellm_params={},
encoding=None,
)
assert "llm_provider-x-litellm-response-cost" not in result._hidden_params.get("additional_headers", {})
def test_openrouter_cost_tracking_streaming():
"""
Test OpenRouter cost tracking for streaming completions.

View file

@ -9,6 +9,8 @@ reasoning.encrypted_content for multi-turn stateless workflows.
Related issue: https://github.com/BerriAI/litellm/issues/22189
"""
import json
from typing import Final
from unittest.mock import MagicMock
import httpx
@ -157,6 +159,29 @@ def test_openrouter_responses_returns_provider_reported_cost(cost):
assert get_response_cost_from_hidden_params(response.hidden_params) == cost
@pytest.mark.parametrize("cost", [float("nan"), float("inf")])
def test_openrouter_responses_drops_a_non_finite_provider_cost(cost: float) -> None:
body: Final = {
"id": "resp_test",
"created_at": 1,
"model": "unmapped-model",
"object": "response",
"output": [],
"status": "completed",
"usage": {"input_tokens": 2, "output_tokens": 3, "total_tokens": 5, "cost": cost},
}
response: Final = OpenRouterResponsesAPIConfig().transform_response_api_response(
model="openrouter/unmapped-model",
raw_response=httpx.Response(
200, content=json.dumps(body).encode(), headers={"content-type": "application/json"}
),
logging_obj=MagicMock(),
)
assert get_response_cost_from_hidden_params(response.hidden_params) is None
assert json.loads(json.dumps(response.model_dump(), allow_nan=False, default=str))["usage"]["cost"] is None
@pytest.mark.parametrize(
"event_type",
["response.completed", "response.incomplete", "response.failed"],

View file

@ -824,6 +824,10 @@ class _ListRedis:
def __init__(self) -> None:
self.rows: list[str] = []
async def async_rpush(self, key: str, values: list[str]) -> int:
self.rows.extend(values)
return len(self.rows)
async def async_rpush_and_trim(self, key: str, values: list[str], max_len: int) -> int:
self.rows.extend(values)
pushed_len = len(self.rows)
@ -852,6 +856,22 @@ async def test_store_spend_logs_in_redis_drops_oldest_rows_past_the_cap():
assert await buffer.get_spend_logs_from_redis_buffer(limit=10) == ()
@pytest.mark.asyncio
@pytest.mark.parametrize("spend", [float("nan"), float("inf")])
async def test_a_non_finite_daily_spend_buffered_in_redis_drains_as_zero(spend: float):
buffer: Final = RedisUpdateBuffer(redis_cache=_ListRedis())
queue: Final = DailySpendUpdateQueue()
await queue.add_update(json.loads(_parked_daily_batch(spend)))
await queue.add_update(json.loads(_parked_daily_batch(0.0042)))
await buffer.store_in_memory_daily_tag_spend_updates_in_redis(queue)
drained: Final = await buffer.get_all_daily_tag_spend_update_transactions_from_redis_buffer()
assert drained is not None
assert drained[PARKED_DAILY_KEY]["spend"] == 0.0
assert drained[PARKED_DAILY_KEY]["api_requests"] == 2
@pytest.mark.asyncio
async def test_a_spend_log_row_parked_with_a_nan_spend_is_read_back_with_zero_spend():
buffer: Final = RedisUpdateBuffer(redis_cache=_ListRedis())