fix(cost): never re-bill a usage.cost litellm stamped on a stream as a provider report

stamp_billed_usage_cost marks the response when the stream builder or the Responses stream stamps litellm's own bill into usage.cost, and completion_cost drops such a usage.cost before pricing, so a second pricing pass (MCP auto-execute rounds, uncached_price) does not add the margin twice. The xAI and Perplexity reported-cost tests go through the central calculator, with the token math falling back to an all-output split for an unmapped model.
This commit is contained in:
devin-ai-integration[bot] 2026-10-25 02:46:17 +00:00
parent b4ee6ef4f3
commit e0ee962054
8 changed files with 56 additions and 14 deletions

View file

@ -25,7 +25,7 @@ from litellm.constants import (
from litellm.litellm_core_utils.core_helpers import as_str_mapping, provider_reported_usage_cost
from litellm.litellm_core_utils.cost_discount import resolve_cost_discount
from litellm.litellm_core_utils.decisions_pricing import decisions_pricing_fields, is_decisions_call_type
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, usage_cost_stamped_by_litellm
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
StandardBuiltInToolCostTracking,
)
@ -1705,7 +1705,7 @@ def completion_cost(
provider_usage_object: Final = get_usage_object(completion_response=completion_response)
cost_per_token_usage_object: Final[Usage | None] = (
_without_provider_stated_cost(provider_usage_object)
if custom_pricing or base_model is not None
if custom_pricing or base_model is not None or usage_cost_stamped_by_litellm(completion_response)
else provider_usage_object
)
rerank_billed_units: RerankBilledUnits | None = None

View file

@ -134,3 +134,17 @@ def get_or_create_hidden_params(obj: object) -> MutableMapping[str, object]:
if hidden_params_mapping is not None:
return hidden_params_mapping
raise TypeError(f"unsupported hidden params storage: {type(hidden_params).__name__}")
USAGE_COST_STAMPED_BY_LITELLM: Final = "usage_cost_stamped_by_litellm"
def stamp_billed_usage_cost(obj: object, usage: object, cost: float) -> None:
"""usage.cost becomes what litellm billed, marked so the calculator never re-bills it as a provider's report."""
setattr(usage, "cost", cost)
set_hidden_param(obj, USAGE_COST_STAMPED_BY_LITELLM, True)
def usage_cost_stamped_by_litellm(obj: object) -> bool:
hidden_params: Final = get_hidden_params(obj)
return hidden_params is not None and hidden_params.get(USAGE_COST_STAMPED_BY_LITELLM) is True

View file

@ -13,7 +13,7 @@ from typing_extensions import ReadOnly, Required
import litellm
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import chunk_usage, provider_reported_usage_cost
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params, stamp_billed_usage_cost
from litellm.litellm_core_utils.prompt_templates.common_utils import (
get_content_from_model_response, # pyright: ignore[reportUnknownVariableType] # response param accepts an untyped dict
)
@ -1416,7 +1416,7 @@ def _stamp_streaming_usage_cost(usage: Usage, response: ModelResponse, logging_o
computed_cost: Final = logging_obj.response_cost_calculator(result=response)
if not isinstance(computed_cost, (int, float)) or computed_cost <= 0:
return
setattr(usage, "cost", computed_cost)
stamp_billed_usage_cost(response, usage, computed_cost)
_set_hidden_response_cost(response, float(computed_cost))

View file

@ -41,6 +41,7 @@ from litellm.exceptions import MidStreamFallbackError, RateLimitError
from litellm.litellm_core_utils.asyncify import run_async_function
from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.exception_mapping_utils import with_deployment_api_base
from litellm.litellm_core_utils.hidden_params import stamp_billed_usage_cost
from litellm.litellm_core_utils.internal_call_metadata import without_budget_reservation
from litellm.litellm_core_utils.litellm_logging import BilledRoundsSpend
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
@ -1757,7 +1758,7 @@ def stamp_responses_usage_cost(
except Exception:
return
if isinstance(cost, (int, float)) and cost > 0:
setattr(usage_obj, "cost", cost)
stamp_billed_usage_cost(response_obj, usage_obj, cost)
def opening_item_payload(payload: Mapping[str, object]) -> Mapping[str, object]:

View file

@ -228,7 +228,9 @@ class TestPerplexityCostCalculator:
usage = Usage(prompt_tokens=1000, completion_tokens=200, total_tokens=1200)
usage.cost = {"total_cost": 0.00501}
prompt_cost, completion_cost = perplexity_cost_per_token(model=self.OFF_PEAK_MODEL, usage=usage)
prompt_cost, completion_cost = billed_cost_per_token(
model=self.OFF_PEAK_MODEL, custom_llm_provider="perplexity", usage_object=usage
)
assert prompt_cost == 0.0
assert completion_cost == 0.00501
assert math.isclose(prompt_cost + completion_cost, 0.00501, rel_tol=1e-10)
assert prompt_cost > 0.0 and completion_cost > 0.0

View file

@ -11,7 +11,7 @@ from unittest.mock import MagicMock, Mock
import httpx
from litellm.llms.xai.cost_calculator import cost_per_token
from litellm.cost_calculator import cost_per_token
from litellm.llms.xai.responses.transformation import XAIResponsesAPIConfig
from litellm.responses.utils import ResponseAPILoggingUtils
from litellm.types.llms.openai import (
@ -498,7 +498,7 @@ class TestXAIResponsesReportedCost:
assert usage.cost == 0.0037756
chat_usage = ResponseAPILoggingUtils.transform_response_api_usage_to_chat_usage(usage)
assert cost_per_token(model="grok-4-latest", usage=chat_usage) == (0.0, 0.0037756)
assert cost_per_token(model="grok-4-latest", custom_llm_provider="xai", usage_object=chat_usage) == (0.0, 0.0037756)
def test_streamed_reported_cost_reaches_the_cost_calculator(self):
event = XAIResponsesAPIConfig().transform_streaming_response(
@ -520,7 +520,7 @@ class TestXAIResponsesReportedCost:
assert isinstance(event, ResponseCompletedEvent)
chat_usage = ResponseAPILoggingUtils.transform_response_api_usage_to_chat_usage(event.response.usage)
assert cost_per_token(model="grok-4-latest", usage=chat_usage) == (0.0, 0.0037756)
assert cost_per_token(model="grok-4-latest", custom_llm_provider="xai", usage_object=chat_usage) == (0.0, 0.0037756)
def test_usage_without_a_reported_cost_is_left_alone(self):
usage = self._transformed_usage({"input_tokens": 100, "output_tokens": 200, "total_tokens": 300})

View file

@ -13,7 +13,7 @@ from litellm.llms.xai.chat.transformation import (
XAIChatCompletionStreamingHandler,
XAIChatConfig,
)
from litellm.llms.xai.cost_calculator import cost_per_token
from litellm.cost_calculator import cost_per_token
from litellm.types.utils import (
CompletionTokensDetailsWrapper,
ModelResponse,
@ -433,7 +433,7 @@ class TestXAIReportedCost:
)
assert usage.cost == 0.0037756
assert cost_per_token(model="grok-4-latest", usage=usage) == (0.0, 0.0037756)
assert cost_per_token(model="grok-4-latest", custom_llm_provider="xai", usage_object=usage) == (0.0, 0.0037756)
def test_usage_without_a_reported_cost_is_left_alone(self):
usage = self._transformed_usage({"prompt_tokens": 100, "completion_tokens": 200, "total_tokens": 300})
@ -481,7 +481,7 @@ class TestXAIReportedCost:
assembled = litellm.stream_chunk_builder(chunks=[parsed])
assert assembled.usage.cost == 0.0037756
assert cost_per_token(model="grok-4-latest", usage=assembled.usage) == (
assert cost_per_token(model="grok-4-latest", custom_llm_provider="xai", usage_object=assembled.usage) == (
0.0,
0.0037756,
)

View file

@ -10641,6 +10641,31 @@ def test_provider_reported_usage_cost_takes_discount_then_margin(monkeypatch: py
assert _bill_reported_cost(stream, {}, {}) == pytest.approx(0.037 * 0.8 * 1.5 + 0.001)
def test_a_usage_cost_litellm_stamped_on_a_stream_is_not_billed_again_as_a_provider_report(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, _REPORTED_COST_FIXTURE_RATES)
monkeypatch.setattr(litellm, "cost_margin_config", {"global": {"fixed_amount": 0.002}})
logging_obj: Final = _reported_cost_logging(True, {})
token_math_plus_margin: Final = 100 * 1e-6 + 20 * 3e-6 + 0.002
response: Final[object] = litellm.stream_chunk_builder(
[
_reported_cost_stream_chunk("hi", None, None),
_reported_cost_stream_chunk(None, "stop", None),
_reported_cost_stream_chunk(None, None, Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120)),
],
messages=[{"role": "user", "content": "hi"}],
logging_obj=logging_obj,
)
assert isinstance(response, ModelResponse)
assert response.usage is not None and response.usage.cost == pytest.approx(token_math_plus_margin)
assert logging_obj.response_cost_calculator(result=response, ignore_stamped_cost=True) == pytest.approx(
token_math_plus_margin
)
def test_provider_reported_usage_cost_splits_input_and_output_by_the_token_math_ratio(
monkeypatch: pytest.MonkeyPatch,
) -> None: