fix(cost): keep the upstream's usage.cost beside litellm's bill and stamp a zero operator price on the stream frame

This commit is contained in:
devin-ai-integration[bot] 2026-10-30 02:48:17 +00:00
parent 84042736c3
commit d2b17a9d26
5 changed files with 105 additions and 61 deletions

View file

@ -1020,32 +1020,41 @@ def chunk_usage(chunk: object) -> object:
PROVIDERS_BILLED_FROM_THEIR_OWN_USAGE_FIELDS: Final = frozenset({"xai", "perplexity", "opencode_go"})
LITELLM_BILLED_USAGE_COST: Final = "litellm_billed_usage_cost"
UPSTREAM_USAGE_COST: Final = "upstream_usage_cost"
def mark_usage_cost_as_litellm_bill(response: object, bill: float) -> None:
"""Records litellm's bill as the response's ``response_cost`` and notes that ``usage.cost`` now holds that bill,
so ``billed_usage_cost`` does not read it back as the upstream's charge on a later pricing pass."""
def _numeric(value: object) -> float | None:
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
def stamp_litellm_bill_on_usage(response: object, usage: object, bill: float) -> None:
"""Writes litellm's bill into ``usage.cost`` and the hidden ``response_cost`` cache, keeping the upstream's own
``usage.cost`` under ``upstream_usage_cost`` so a later pricing pass still bills the upstream's charge instead of
reading litellm's bill back as if the upstream had reported it."""
hidden: Final[object] = getattr(response, "_hidden_params", None)
if isinstance(hidden, dict):
hidden_params: Final = cast(dict[str, object], hidden) # cast-ok: hidden params ride on the response as a dict
hidden_params.update({"response_cost": bill, LITELLM_BILLED_USAGE_COST: bill})
def _usage_cost_is_litellm_bill(response: object) -> bool:
hidden: Final = as_str_mapping(getattr(response, "_hidden_params", None))
bill: Final[object] = None if hidden is None else hidden.get(LITELLM_BILLED_USAGE_COST)
usage_cost: Final[object] = getattr(_usage_field(response, "usage"), "cost", None)
return isinstance(bill, (int, float)) and not isinstance(bill, bool) and bill == usage_cost
upstream: Final = (
hidden_params[UPSTREAM_USAGE_COST]
if UPSTREAM_USAGE_COST in hidden_params
else _numeric(getattr(usage, "cost", None))
)
hidden_params.update({"response_cost": bill, UPSTREAM_USAGE_COST: upstream})
setattr(usage, "cost", bill)
def billed_usage_cost(response: object, custom_llm_provider: str | None) -> float | None:
"""The charge an OpenAI-compatible upstream reports in ``usage.cost``, skipped for providers billed from their own
usage fields and once litellm rewrote ``usage.cost`` to its own bill (the stream builder and the Responses terminal
event do)."""
if custom_llm_provider in PROVIDERS_BILLED_FROM_THEIR_OWN_USAGE_FIELDS or _usage_cost_is_litellm_bill(response):
usage fields. Once litellm wrote its own bill into ``usage.cost`` (the stream builder and the Responses terminal
event do), the upstream's figure is read from ``upstream_usage_cost`` instead."""
if custom_llm_provider in PROVIDERS_BILLED_FROM_THEIR_OWN_USAGE_FIELDS:
return None
cost: Final = provider_reported_usage_cost(_usage_field(response, "usage"), custom_llm_provider)
hidden: Final = as_str_mapping(getattr(response, "_hidden_params", None))
cost: Final = (
_numeric(hidden[UPSTREAM_USAGE_COST])
if hidden is not None and UPSTREAM_USAGE_COST in hidden
else provider_reported_usage_cost(_usage_field(response, "usage"), custom_llm_provider)
)
return cost if cost is not None and math.isfinite(cost) and cost >= 0 else None

View file

@ -15,8 +15,8 @@ from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import (
PROVIDERS_BILLED_FROM_THEIR_OWN_USAGE_FIELDS,
chunk_usage,
mark_usage_cost_as_litellm_bill,
provider_reported_usage_cost,
stamp_litellm_bill_on_usage,
)
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import BEDROCK_TRACE_FIELDS, object_items, object_mapping
@ -1425,11 +1425,10 @@ def _stamp_streaming_usage_cost(usage: Usage, response: ModelResponse, logging_o
):
return
computed_cost: Final = logging_obj.response_cost_calculator(result=response)
if not isinstance(computed_cost, (int, float)) or computed_cost <= 0:
if isinstance(computed_cost, bool) or not isinstance(computed_cost, (int, float)) or computed_cost < 0:
return
setattr(usage, "cost", computed_cost)
_set_hidden_response_cost(response, float(computed_cost))
mark_usage_cost_as_litellm_bill(response, float(computed_cost))
if computed_cost > 0 or isinstance(getattr(usage, "cost", None), (int, float)):
stamp_litellm_bill_on_usage(response, usage, float(computed_cost))
def _field_of(value: object, name: str) -> object:

View file

@ -40,10 +40,10 @@ from litellm.constants import (
from litellm.exceptions import MidStreamFallbackError, RateLimitError
from litellm.litellm_core_utils.asyncify import run_async_function
from litellm.litellm_core_utils.core_helpers import (
mark_usage_cost_as_litellm_bill,
process_response_headers,
provider_reported_usage_cost,
set_response_cost_in_hidden_params,
stamp_litellm_bill_on_usage,
)
from litellm.litellm_core_utils.exception_mapping_utils import with_deployment_api_base
from litellm.litellm_core_utils.internal_call_metadata import (
@ -1870,8 +1870,7 @@ def stamp_responses_usage_cost(
if isinstance(bill, bool) or not isinstance(bill, (int, float)):
return
if bill > 0 or isinstance(getattr(usage_obj, "cost", None), (int, float)):
setattr(usage_obj, "cost", float(bill))
mark_usage_cost_as_litellm_bill(response_obj, float(bill))
stamp_litellm_bill_on_usage(response_obj, usage_obj, float(bill))
def opening_item_payload(payload: Mapping[str, object]) -> Mapping[str, object]:

View file

@ -69,9 +69,7 @@ def test_preserve_upstream_non_openai_attributes_keeps_scalar_and_structured_val
scalar_attribute="scalar",
structured_attribute={"items": ["value"]},
)
model_response: Final = ModelResponseStream(
choices=[StreamingChoices(index=0, delta=Delta(content="hello"))]
)
model_response: Final = ModelResponseStream(choices=[StreamingChoices(index=0, delta=Delta(content="hello"))])
preserve_upstream_non_openai_attributes(model_response, original_chunk)
@ -262,9 +260,7 @@ class TestMapFinishReasonAnthropic:
("content_filtered", "content_filter"),
],
)
def test_anthropic_finish_reasons(
self, provider_reason: str, expected: str
) -> None:
def test_anthropic_finish_reasons(self, provider_reason: str, expected: str) -> None:
assert map_finish_reason(provider_reason) == expected
def test_refusal(self):
@ -336,9 +332,7 @@ class TestMapFinishReasonZhipu:
class TestMapFinishReasonOpenAIPassthrough:
@pytest.mark.parametrize(
"reason", ["stop", "length", "tool_calls", "function_call", "content_filter"]
)
@pytest.mark.parametrize("reason", ["stop", "length", "tool_calls", "function_call", "content_filter"])
def test_openai_values_pass_through(self, reason):
assert map_finish_reason(reason) == reason
@ -349,9 +343,7 @@ class TestMapFinishReasonGenericError:
assert map_finish_reason("error") == "stop"
def test_lowercase_error_does_not_warn(self, mocker):
warn = mocker.patch(
"litellm.litellm_core_utils.core_helpers.verbose_logger.warning"
)
warn = mocker.patch("litellm.litellm_core_utils.core_helpers.verbose_logger.warning")
assert map_finish_reason("error") == "stop"
warn.assert_not_called()
@ -369,8 +361,7 @@ class TestFinishReasonMapOutputsAreValid:
"""Every value in _FINISH_REASON_MAP must be a valid OpenAI finish reason."""
for provider_reason, openai_reason in _FINISH_REASON_MAP.items():
assert openai_reason in VALID_OPENAI_FINISH_REASONS, (
f"Mapped value '{openai_reason}' (from '{provider_reason}') "
f"is not a valid OpenAI finish reason"
f"Mapped value '{openai_reason}' (from '{provider_reason}') is not a valid OpenAI finish reason"
)
@ -380,28 +371,18 @@ class TestRedactNestedMatchAndRegexKeys:
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{"type": "NAME", "match": "secret-name", "action": "BLOCKED"}
]
},
"wordPolicy": {
"customWords": [{"match": "badword", "action": "BLOCKED"}]
"piiEntities": [{"type": "NAME", "match": "secret-name", "action": "BLOCKED"}]
},
"wordPolicy": {"customWords": [{"match": "badword", "action": "BLOCKED"}]},
}
],
"regex": "should-redact-key-named-regex",
}
out = redact_nested_match_and_regex_keys(payload)
assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][
"match"
] == "[REDACTED]"
assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == (
"[REDACTED]"
)
assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0]["match"] == "[REDACTED]"
assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == ("[REDACTED]")
assert out["regex"] == "[REDACTED]"
assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][
0
]["match"] == "secret-name"
assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0]["match"] == "secret-name"
def test_passes_through_none_and_str(self):
assert redact_nested_match_and_regex_keys(None) is None
@ -564,13 +545,17 @@ class TestIsExpectedClientError:
assert is_expected_client_error(over_budget) is True
litellm_limit = RateLimitError(
message="key over rpm", llm_provider="anthropic", model="claude-haiku-4-5",
message="key over rpm",
llm_provider="anthropic",
model="claude-haiku-4-5",
category=RateLimitErrorCategory.LITELLM_RATE_LIMIT,
)
assert is_expected_client_error(litellm_limit) is True
vendor_limit = RateLimitError(
message="rate limited upstream", llm_provider="anthropic", model="claude-haiku-4-5",
message="rate limited upstream",
llm_provider="anthropic",
model="claude-haiku-4-5",
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
)
assert is_expected_client_error(vendor_limit) is False
@ -808,7 +793,7 @@ class TestReceiveStampSurvivesASecondMetadataBucket:
def test_billed_usage_cost_bills_any_chat_upstream_without_its_own_reader() -> None:
from types import SimpleNamespace
from litellm.litellm_core_utils.core_helpers import billed_usage_cost, mark_usage_cost_as_litellm_bill
from litellm.litellm_core_utils.core_helpers import billed_usage_cost
from litellm.types.utils import ModelResponse, Usage
usage = Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120, cost=0.037)
@ -817,8 +802,20 @@ def test_billed_usage_cost_bills_any_chat_upstream_without_its_own_reader() -> N
assert billed_usage_cost(chat, "openrouter") == 0.037
assert billed_usage_cost(chat, "xai") is None
assert billed_usage_cost(SimpleNamespace(usage=usage), "openai") == 0.037
priced_by_litellm = ModelResponse(usage=usage)
priced_by_litellm._hidden_params["response_cost"] = 0.037
assert billed_usage_cost(priced_by_litellm, "openai") == 0.037
mark_usage_cost_as_litellm_bill(priced_by_litellm, 0.037)
assert billed_usage_cost(priced_by_litellm, "openai") is None
def test_billed_usage_cost_keeps_the_upstream_charge_after_litellm_wrote_its_bill_into_usage_cost() -> None:
from litellm.litellm_core_utils.core_helpers import billed_usage_cost, stamp_litellm_bill_on_usage
from litellm.types.utils import ModelResponse, Usage
reported = ModelResponse(usage=Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120, cost=0.037))
stamp_litellm_bill_on_usage(reported, reported.usage, 0.0407)
stamp_litellm_bill_on_usage(reported, reported.usage, 0.0407)
assert reported.usage.cost == 0.0407
assert reported._hidden_params["response_cost"] == 0.0407
assert billed_usage_cost(reported, "openai") == 0.037
unreported = ModelResponse(usage=Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120))
stamp_litellm_bill_on_usage(unreported, unreported.usage, 0.0015)
assert unreported.usage.cost == 0.0015
assert billed_usage_cost(unreported, "openai") is None

View file

@ -5347,6 +5347,46 @@ def test_stream_chunk_builder_keeps_provider_reported_usage_cost():
assert response._hidden_params["response_cost"] == pytest.approx(0.5)
def test_completion_cost_on_a_stamped_stream_still_bills_the_reported_charge():
usage_chunk: Final = _stream_builder_text_chunk("gpt-4o", "")
usage_chunk.usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15, cost=0.5)
chunks: Final = [
_stream_builder_text_chunk("gpt-4o", "Hello ", finish_reason="stop"),
usage_chunk,
]
response: Final = litellm.stream_chunk_builder(
chunks=chunks, messages=[{"role": "user", "content": "hi"}], logging_obj=_stream_builder_logging_obj()
)
assert response is not None
assert litellm.completion_cost(completion_response=response) == pytest.approx(0.5)
def test_stream_chunk_builder_stamps_a_zero_operator_price_over_a_reported_cost():
logging_obj: Final = _stream_builder_logging_obj()
logging_obj.litellm_params = {
**logging_obj.litellm_params,
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
}
logging_obj.custom_cost_per_token = {"input_cost_per_token": 0.0, "output_cost_per_token": 0.0}
usage_chunk: Final = _stream_builder_text_chunk("gpt-4o", "")
usage_chunk.usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15, cost=0.5)
chunks: Final = [
_stream_builder_text_chunk("gpt-4o", "Hello ", finish_reason="stop"),
usage_chunk,
]
response: Final = litellm.stream_chunk_builder(
chunks=chunks, messages=[{"role": "user", "content": "hi"}], logging_obj=logging_obj
)
assert response is not None
assert getattr(response.usage, "cost", None) == 0.0
assert response._hidden_params["response_cost"] == 0.0
def test_stream_chunk_builder_prices_alias_from_openai_sdk_usage_chunk():
from openai.types.completion_usage import CompletionUsage