fix(cost): bill a provider reported usage.cost on every path unless the deployment is priced

cost_per_token bills a finite non-negative usage.cost from any provider, split across input and output by the ratio of the token math, unless the caller priced the call itself. completion_cost drops the reported cost when the deployment has custom pricing or a base_model, so operator rates win on streams too, where stream_chunk_builder used to stamp usage.cost as the final response_cost ahead of custom pricing and margins. The xAI and Perplexity calculators lose their private copies of the same rule.

Co-authored-by: AoHanBei <dongaohan21@gmail.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-25 02:09:17 +00:00
parent de5dc5b238
commit c200c2f9f8
10 changed files with 381 additions and 61 deletions

View file

@ -5,7 +5,7 @@ import math
import time
from collections.abc import Mapping, Sequence
from datetime import datetime
from functools import lru_cache
from functools import lru_cache, partial
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast
@ -22,7 +22,7 @@ from litellm.constants import (
OPENAI_LIVE_TERMINAL_RESPONSE_EVENT_TYPES,
REALTIME_METERED_USAGE_KEY,
)
from litellm.litellm_core_utils.core_helpers import as_str_mapping
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
@ -408,6 +408,19 @@ def _per_second_pricing_cost(
return resolved_cost_per_second * seconds, 0.0
def _billable_reported_cost(usage_object: Usage | None) -> float | None:
return _billable_header_cost(provider_reported_usage_cost(usage_object, None))
def _split_reported_cost(reported_cost: float, priced: tuple[float, float] | None) -> tuple[float, float]:
"""Providers report one total, so attribute it to input and output in the ratio the token math gives."""
priced_total: Final = (priced[0] + priced[1]) if priced is not None else 0.0
if priced is None or priced_total <= 0:
return 0.0, reported_cost
input_cost: Final = reported_cost * priced[0] / priced_total
return input_cost, reported_cost - input_cost
def cost_per_token(
model: str = "",
prompt_tokens: int = 0,
@ -444,6 +457,87 @@ def cost_per_token(
request_model: str | None = None, # original request model for router detection
custom_model_info: OCRPricing | None = None,
bedrock_model_info: ModelInfo | None = None,
) -> tuple[float, float]:
"""Cost in USD for prompt and completion tokens.
A numeric ``usage.cost`` the provider reported is the bill, split by the ratio of our own token math,
unless the caller priced the call itself with ``custom_cost_per_token`` or ``custom_cost_per_second``.
Without a reported cost the token math is the bill.
"""
priced_by_caller: Final = custom_cost_per_token is not None or custom_cost_per_second is not None
reported_cost: Final = None if priced_by_caller else _billable_reported_cost(usage_object)
rated: Final = partial(
_cost_per_token_from_rates,
model=model,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
response_time_ms=response_time_ms,
custom_llm_provider=custom_llm_provider,
region_name=region_name,
prompt_characters=prompt_characters,
completion_characters=completion_characters,
cache_creation_input_tokens=cache_creation_input_tokens,
cache_read_input_tokens=cache_read_input_tokens,
custom_cost_per_token=custom_cost_per_token,
custom_cost_per_second=custom_cost_per_second,
number_of_queries=number_of_queries,
usage_object=usage_object,
rerank_billed_units=rerank_billed_units,
call_type=call_type,
audio_transcription_file_duration=audio_transcription_file_duration,
service_tier=service_tier,
data_residency=data_residency,
vertex_location=vertex_location,
response=response,
request_model=request_model,
custom_model_info=custom_model_info,
bedrock_model_info=bedrock_model_info,
)
if reported_cost is None:
return rated()
try:
return _split_reported_cost(reported_cost, rated())
except Exception: # noqa: BLE001 # an unmapped model has no rates to split by, the reported total still bills
verbose_logger.debug("No rates for model=%s, billing the provider reported cost %s", model, reported_cost)
return _split_reported_cost(reported_cost, None)
def _cost_per_token_from_rates(
model: str = "",
prompt_tokens: int = 0,
completion_tokens: int = 0,
response_time_ms: float | None = 0.0,
custom_llm_provider: str | None = None,
region_name=None,
### CHARACTER PRICING ###
prompt_characters: int | None = None,
completion_characters: int | None = None,
### PROMPT CACHING PRICING ### - used for anthropic
cache_creation_input_tokens: int | None = 0,
cache_read_input_tokens: int | None = 0,
### CUSTOM PRICING ###
custom_cost_per_token: CostPerToken | None = None,
custom_cost_per_second: float | None = None,
### NUMBER OF QUERIES ###
number_of_queries: int | None = None,
### USAGE OBJECT ###
usage_object: Usage | None = None, # just read the usage object if provided
### BILLED UNITS ###
rerank_billed_units: RerankBilledUnits | None = None,
### CALL TYPE ###
call_type: CallTypesLiteral = "completion",
audio_transcription_file_duration: float = 0.0, # for audio transcription calls - the file time in seconds
### SERVICE TIER ###
service_tier: str | None = None, # for OpenAI service tier pricing
### DATA RESIDENCY ###
data_residency: str | None = None, # for OpenAI regional-processing uplift (e.g. "eu", "us")
### VERTEX LOCATION ###
vertex_location: str | None = None, # for Vertex AI regional-endpoint uplift (e.g. "us-east5", "global")
response: object | None = None,
### REQUEST MODEL ###
request_model: str | None = None, # original request model for router detection
custom_model_info: OCRPricing | None = None,
bedrock_model_info: ModelInfo | None = None,
) -> tuple[float, float]:
"""
Calculates the cost per token for a given model, prompt tokens, and completion tokens.
@ -1610,7 +1704,9 @@ def completion_cost(
audio_transcription_file_duration: float = 0.0
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 else provider_usage_object
_without_provider_stated_cost(provider_usage_object)
if custom_pricing or base_model is not None
else provider_usage_object
)
rerank_billed_units: RerankBilledUnits | None = None

View file

@ -1350,9 +1350,6 @@ def stream_chunk_builder_text_completion(
return TextCompletionResponse(**response)
_CALCULATOR_PRICED_REPORTED_COST_PROVIDERS: Final = frozenset({LlmProviders.XAI.value})
def _logged_provider(logging_obj: Optional["Logging"]) -> object:
if logging_obj is None:
return None
@ -1360,10 +1357,6 @@ def _logged_provider(logging_obj: Optional["Logging"]) -> object:
return provider
def _reported_cost_is_priced_by_calculator(logging_obj: Optional["Logging"]) -> bool:
return _logged_provider(logging_obj) in _CALCULATOR_PRICED_REPORTED_COST_PROVIDERS
def _stream_builder_response_cost(
response: ModelResponse, logging_obj: Optional["Logging"], chunks: Sequence[object]
) -> float | None:
@ -1374,9 +1367,6 @@ def _stream_builder_response_cost(
for chunk in reversed(chunks)
)
return next((cost for cost in reported if cost is not None), None)
usage_cost: Final = getattr(getattr(response, "usage", None), "cost", None)
if isinstance(usage_cost, (int, float)) and not _reported_cost_is_priced_by_calculator(logging_obj):
return float(usage_cost)
if logging_obj is not None:
return None
provider_hint: Final = response._hidden_params.get( # pyright: ignore[reportPrivateUsage] # no public accessor

View file

@ -24,22 +24,6 @@ def cost_per_token(model: str, usage: Usage, current_time: datetime | None = Non
Returns:
Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd
"""
## USE PRE-CALCULATED COST FROM PERPLEXITY IF AVAILABLE
## Perplexity returns accurate cost in usage.cost.total_cost including request fees.
## By the time it reaches here, ResponseAPIUsage.parse_cost has already flattened
## that dict down to a float, so both shapes must be accepted.
cost_info: Final = getattr(usage, "cost", None)
total_cost: float | None = None
if isinstance(cost_info, dict):
total_cost = cost_info.get("total_cost")
elif isinstance(cost_info, (int, float)) and not isinstance(cost_info, bool):
total_cost = float(cost_info)
if total_cost is not None:
# Return total cost as completion_cost (prompt_cost=0) since Perplexity
# doesn't break down by input/output in their cost object
return (0.0, float(total_cost))
## FALLBACK: Calculate cost manually if Perplexity doesn't provide it
## GET MODEL INFO
model_info: Final = get_model_info(model=model, custom_llm_provider="perplexity")

View file

@ -1,8 +1,8 @@
"""
Helper util for handling XAI-specific cost calculation
- Prefers the cost xAI reports on the response over recomputing it locally
- Uses the generic cost calculator which already handles tiered pricing correctly
- Handles XAI-specific reasoning token billing (billed as part of completion tokens)
- Skips the web search surcharge when xAI reported a total that already covers it
"""
import math
@ -49,19 +49,6 @@ def _cost_reported_by_xai(usage: "Usage") -> float | None:
return float(reported_cost)
def _split_reported_cost(model: str, usage: Usage, reported_cost: float) -> tuple[float, float]:
"""xAI reports one total, so attribute it to input and output in the ratio our own token math gives."""
try:
priced_input, priced_output = _token_math_cost_per_token(model, usage)
except Exception: # noqa: BLE001 # generic_cost_per_token raises bare Exception for unmapped models
return 0.0, reported_cost
priced_total: Final = priced_input + priced_output
if priced_total <= 0:
return 0.0, reported_cost
input_cost: Final = reported_cost * priced_input / priced_total
return input_cost, reported_cost - input_cost
def cost_per_token(model: str, usage: Usage) -> tuple[float, float]:
"""
Calculates the cost per token for a given XAI model, prompt tokens, and completion tokens.
@ -74,9 +61,6 @@ def cost_per_token(model: str, usage: Usage) -> tuple[float, float]:
Returns:
Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd
"""
reported_cost: Final = _cost_reported_by_xai(usage)
if reported_cost is not None:
return _split_reported_cost(model, usage, reported_cost)
return _token_math_cost_per_token(model, usage)

View file

@ -11878,7 +11878,89 @@ GPT_5_6_TERRA_MESSAGES_BRIDGED_STREAM: Final = GPT_5_6_TERRA_MESSAGES_BRIDGED_JS
}
)
# An OpenAI-compatible gateway that reports what it charged under usage.cost (#47533). The reported total is the bill,
# split across input and output by the ratio of the cost map rates (100 * 1e-6 : 20 * 3e-6), on plain and streamed
# replies alike. A deployment the operator priced keeps the operator's rates on both.
_GATEWAY_REPORTED_USAGE: Final = {"prompt_tokens": 100, "completion_tokens": 20, "total_tokens": 120, "cost": 0.037}
_GATEWAY_REPORTED_EXPECTED: Final = ExactExpected(
spend=0.037, input_cost=0.023125, output_cost=0.013875, prompt_tokens=100, completion_tokens=20
)
_GATEWAY_OPERATOR_PRICED: Final = Deployment(input_cost_per_token=2e-06, output_cost_per_token=6e-06)
_GATEWAY_OPERATOR_PRICED_EXPECTED: Final = ExactExpected(
spend=0.00032, input_cost=0.0002, output_cost=0.00012, prompt_tokens=100, completion_tokens=20
)
_GATEWAY_CHUNK: Final = {
"id": "chatcmpl-$REQUEST_ID",
"object": "chat.completion.chunk",
"created": 1,
"model": "gateway-reported-cost-fixture",
}
GATEWAY_REPORTED_COST: Final = CostTrackingTestCase(
name="gateway-reported-cost",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="gateway-reported-cost-fixture",
endpoint="/v1/chat/completions",
request={"model": "$MODEL", "messages": [{"role": "user", "content": "reported cost"}], "stream": False},
response=JsonResponse(
content_type="application/json",
body={
"id": "chatcmpl-$REQUEST_ID",
"object": "chat.completion",
"created": 1,
"model": "gateway-reported-cost-fixture",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
"usage": _GATEWAY_REPORTED_USAGE,
},
),
expected=_GATEWAY_REPORTED_EXPECTED,
)
GATEWAY_REPORTED_COST_STREAM: Final = CostTrackingTestCase(
name="gateway-reported-cost-stream",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="gateway-reported-cost-fixture",
endpoint="/v1/chat/completions",
request={
"model": "$MODEL",
"messages": [{"role": "user", "content": "reported cost"}],
"stream": True,
"stream_options": {"include_usage": True},
},
response=SseResponse(
content_type="text/event-stream",
frames=sse_frames(
{**_GATEWAY_CHUNK, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "ok"}}]},
{**_GATEWAY_CHUNK, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
{**_GATEWAY_CHUNK, "choices": [], "usage": _GATEWAY_REPORTED_USAGE},
done=True,
),
),
expected=_GATEWAY_REPORTED_EXPECTED,
)
GATEWAY_REPORTED_COST_OPERATOR_PRICED: Final = GATEWAY_REPORTED_COST.model_copy(
update={
"name": "gateway-reported-cost-operator_priced",
"deployment": _GATEWAY_OPERATOR_PRICED,
"expected": _GATEWAY_OPERATOR_PRICED_EXPECTED,
}
)
GATEWAY_REPORTED_COST_OPERATOR_PRICED_STREAM: Final = GATEWAY_REPORTED_COST_STREAM.model_copy(
update={
"name": "gateway-reported-cost-operator_priced-stream",
"deployment": _GATEWAY_OPERATOR_PRICED,
"expected": _GATEWAY_OPERATOR_PRICED_EXPECTED,
}
)
CASES: Final[tuple[CostTrackingTestCase, ...]] = (
GATEWAY_REPORTED_COST,
GATEWAY_REPORTED_COST_STREAM,
GATEWAY_REPORTED_COST_OPERATOR_PRICED,
GATEWAY_REPORTED_COST_OPERATOR_PRICED_STREAM,
GPT_5_6_SLASH_ALIAS,
GPT_5_6_SLASH_ALIAS_STREAM,
GPT_6_SOL_RESPONSES_DEPLOYMENT_JSON,
@ -12120,6 +12202,10 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
)
PARITY: Final[tuple[StreamParityTestCase, ...]] = (
StreamParityTestCase(plain=GATEWAY_REPORTED_COST, streamed=GATEWAY_REPORTED_COST_STREAM),
StreamParityTestCase(
plain=GATEWAY_REPORTED_COST_OPERATOR_PRICED, streamed=GATEWAY_REPORTED_COST_OPERATOR_PRICED_STREAM
),
StreamParityTestCase(plain=GPT_5_3_CODEX_INPUT_TEXT, streamed=GPT_5_3_CODEX_STREAM),
StreamParityTestCase(plain=GPT_5_3_CODEX_INPUT_TEXT, streamed=GPT_5_3_CODEX_STREAM_INCOMPLETE),
StreamParityTestCase(plain=GPT_5_3_CODEX_INPUT_TEXT, streamed=GPT_5_3_CODEX_STREAM_UNVALIDATED),

View file

@ -26,6 +26,12 @@ COST_MAP_ENTRIES: Final[dict[str, dict[str, JsonValue]]] = {
"input_cost_per_token_above_128k_tokens": 2.2e-07,
"output_cost_per_token_above_128k_tokens": 8.8e-07,
},
"gateway-reported-cost-fixture": {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 1e-06,
"output_cost_per_token": 3e-06,
},
"openrouter/anthropic/claude-sonnet-5": {
"litellm_provider": "openrouter",
"mode": "chat",

View file

@ -2401,14 +2401,14 @@ def test_perplexity_streaming_dict_cost_bills_through_its_own_calculator():
) == pytest.approx(0.00503)
def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
def test_openai_compatible_streaming_cost_is_the_reported_cost():
import litellm
from litellm.cost_calculator import (
get_response_cost_from_hidden_params,
response_cost_calculator,
)
model = "openai/streams-cost-in-nanodollars"
model = "openai/streams-with-a-reported-cost"
litellm.register_model(
{
model: {
@ -2423,7 +2423,7 @@ def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
id="chatcmpl-openai-compatible",
model=model,
choices=[],
usage=Usage(completion_tokens=5, prompt_tokens=10, total_tokens=15, cost=3_144_000),
usage=Usage(completion_tokens=5, prompt_tokens=10, total_tokens=15, cost=0.037),
)
CustomStreamWrapper._propagate_usage_cost_to_hidden_params(complete_response, "openai")
@ -2435,7 +2435,7 @@ def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
custom_llm_provider="openai",
call_type="completion",
optional_params={},
) == pytest.approx(2e-5)
) == pytest.approx(0.037)
def test_xai_streaming_reported_cost_still_takes_the_margin(monkeypatch):

View file

@ -15,6 +15,7 @@ import pytest
# Add the project root to Python path
import litellm
from litellm.cost_calculator import cost_per_token as billed_cost_per_token
from litellm.llms.perplexity.cost_calculator import (
cost_per_token as perplexity_cost_per_token,
)
@ -109,13 +110,13 @@ class TestPerplexityCostCalculator:
"total_cost": 0.008,
}
prompt_cost, completion_cost = perplexity_cost_per_token(model="sonar-pro", usage=usage)
prompt_cost, completion_cost = billed_cost_per_token(
model="perplexity/sonar-pro", custom_llm_provider="perplexity", usage_object=usage
)
# When Perplexity provides total_cost, we use it directly
# prompt_cost should be 0, completion_cost should be total_cost
assert prompt_cost == 0.0
assert completion_cost == 0.008
assert prompt_cost + completion_cost == 0.008
# When Perplexity provides total_cost, that total is the bill
assert math.isclose(prompt_cost + completion_cost, 0.008, rel_tol=1e-10)
assert prompt_cost > 0.0 and completion_cost > 0.0
def test_uses_perplexity_provided_cost_when_normalized_to_float(self):
"""
@ -135,10 +136,11 @@ class TestPerplexityCostCalculator:
usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150)
usage.cost = 0.008
prompt_cost, completion_cost = perplexity_cost_per_token(model="sonar-pro", usage=usage)
prompt_cost, completion_cost = billed_cost_per_token(
model="perplexity/sonar-pro", custom_llm_provider="perplexity", usage_object=usage
)
assert prompt_cost == 0.0
assert completion_cost == 0.008
assert math.isclose(prompt_cost + completion_cost, 0.008, rel_tol=1e-10)
OFF_PEAK_MODEL = "sonar-off-peak-test"
OFF_PEAK_WINDOW = "14:00-00:00"

View file

@ -6,6 +6,7 @@ import math
import os
import litellm
from litellm.cost_calculator import cost_per_token as billed_cost_per_token
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
StandardBuiltInToolCostTracking,
)
@ -115,7 +116,9 @@ class TestXAICostCalculator:
model="grok-4.7", usage=Usage(prompt_tokens=100, completion_tokens=200, total_tokens=300)
)
prompt_cost, completion_cost = cost_per_token(model="grok-4.7", usage=usage)
prompt_cost, completion_cost = billed_cost_per_token(
model="xai/grok-4.7", custom_llm_provider="xai", usage_object=usage
)
assert math.isclose(prompt_cost + completion_cost, 0.0037756, rel_tol=1e-10)
assert math.isclose(prompt_cost / completion_cost, priced_prompt / priced_completion, rel_tol=1e-10)
@ -165,7 +168,10 @@ class TestXAICostCalculator:
"""A reported zero is a real answer, not a missing value."""
usage = Usage(prompt_tokens=100, completion_tokens=200, total_tokens=300, cost=0.0)
assert cost_per_token(model="grok-4-latest", usage=usage) == (0.0, 0.0)
assert billed_cost_per_token(model="xai/grok-4-latest", custom_llm_provider="xai", usage_object=usage) == (
0.0,
0.0,
)
def test_custom_pricing_beats_the_reported_cost(self):
response = ModelResponse(

View file

@ -10522,3 +10522,169 @@ def test_bedrock_registered_cache_prices_preserve_dynamic_threshold_rates(
assert entry is not None
assert entry[1]["input_cost_per_token_above_300k_tokens"] == 0.03
assert entry[1][tier_field] == (tier_override if tier_override is not None else 0.007)
_REPORTED_USAGE: Final = {"prompt_tokens": 100, "completion_tokens": 20, "total_tokens": 120, "cost": 0.037}
_REPORTED_COST_FIXTURE_MODEL: Final = "openai/reported-cost-fixture"
_REPORTED_COST_FIXTURE_RATES: Final = {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 1e-6,
"output_cost_per_token": 3e-6,
}
def _reported_cost_logging(stream: bool, litellm_params: Mapping[str, object]) -> Logging:
logging_obj: Final = Logging(
model=_REPORTED_COST_FIXTURE_MODEL,
messages=[{"role": "user", "content": "hi"}],
stream=stream,
call_type="completion",
start_time=datetime.datetime(2026, 1, 1),
litellm_call_id="reported-cost",
function_id="reported-cost",
)
logging_obj.update_from_kwargs(
kwargs={}, optional_params={}, litellm_params={"custom_llm_provider": "openai", **litellm_params}
)
logging_obj.model_call_details["custom_llm_provider"] = "openai"
return logging_obj
def _reported_cost_stream_chunk(content: str | None, finish_reason: str | None, usage: Usage | None) -> object:
return litellm.ModelResponseStream(
id="chatcmpl-reported",
model="reported-cost-fixture",
created=1,
object="chat.completion.chunk",
choices=[litellm.types.utils.StreamingChoices(index=0, delta=litellm.types.utils.Delta(content=content), finish_reason=finish_reason)],
usage=usage,
)
def _bill_reported_cost(stream: bool, litellm_params: Mapping[str, object], hidden_params: Mapping[str, object]) -> float | None:
"""The spend the proxy logs for one upstream reply carrying usage.cost, streamed or not."""
logging_obj: Final = _reported_cost_logging(stream, litellm_params)
if stream:
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(**_REPORTED_USAGE)),
],
messages=[{"role": "user", "content": "hi"}],
logging_obj=logging_obj,
)
else:
response = ModelResponse(
id="chatcmpl-reported",
model="reported-cost-fixture",
choices=[Choices(index=0, message=Message(role="assistant", content="hi"), finish_reason="stop")],
usage=Usage(**_REPORTED_USAGE),
)
assert isinstance(response, ModelResponse)
response._hidden_params.update({"custom_llm_provider": "openai", **hidden_params})
return logging_obj.response_cost_calculator(result=response)
@pytest.mark.parametrize("stream", [False, True])
@pytest.mark.parametrize("mapped", [False, True])
def test_provider_reported_usage_cost_is_billed_on_stream_and_non_stream(
monkeypatch: pytest.MonkeyPatch, stream: bool, mapped: bool
) -> None:
"""Regression for #47533: a plain openai-compatible deployment billed usage.cost on streams and $0 on non-stream."""
if mapped:
monkeypatch.setitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, _REPORTED_COST_FIXTURE_RATES)
else:
monkeypatch.delitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, raising=False)
assert _bill_reported_cost(stream, {}, {}) == pytest.approx(0.037)
@pytest.mark.parametrize("stream", [False, True])
def test_deployment_pricing_beats_the_provider_reported_usage_cost(monkeypatch: pytest.MonkeyPatch, stream: bool) -> None:
"""Streams used to bill usage.cost even when the operator priced the deployment; both paths bill the operator's rates."""
deployment_id: Final = "reported-cost-deployment-priced-by-the-operator"
monkeypatch.setitem(
litellm.model_cost,
deployment_id,
{"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 2e-6, "output_cost_per_token": 6e-6},
)
litellm_params: Final = {
"input_cost_per_token": 2e-6,
"output_cost_per_token": 6e-6,
"model_info": {"id": deployment_id},
}
assert _bill_reported_cost(stream, litellm_params, {"model_id": deployment_id}) == pytest.approx(
100 * 2e-6 + 20 * 6e-6
)
@pytest.mark.parametrize("stream", [False, True])
def test_base_model_pricing_beats_the_provider_reported_usage_cost(monkeypatch: pytest.MonkeyPatch, stream: bool) -> None:
base_model: Final = "openai/reported-cost-base-model-fixture"
monkeypatch.setitem(
litellm.model_cost,
base_model,
{"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 5e-6, "output_cost_per_token": 7e-6},
)
assert _bill_reported_cost(stream, {"base_model": base_model}, {}) == pytest.approx(100 * 5e-6 + 20 * 7e-6)
@pytest.mark.parametrize("stream", [False, True])
def test_provider_reported_usage_cost_takes_discount_then_margin(monkeypatch: pytest.MonkeyPatch, stream: bool) -> None:
monkeypatch.setattr(litellm, "cost_discount_config", {"openai": 0.2})
monkeypatch.setattr(litellm, "cost_margin_config", {"global": {"percentage": 0.5, "fixed_amount": 0.001}})
assert _bill_reported_cost(stream, {}, {}) == pytest.approx(0.037 * 0.8 * 1.5 + 0.001)
def test_provider_reported_usage_cost_splits_input_and_output_by_the_token_math_ratio(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, _REPORTED_COST_FIXTURE_RATES)
usage: Final = Usage(**_REPORTED_USAGE)
prompt_cost, completion_cost = cost_per_token(
model=_REPORTED_COST_FIXTURE_MODEL, custom_llm_provider="openai", usage_object=usage
)
priced_prompt: Final = 100 * 1e-6
priced_completion: Final = 20 * 3e-6
assert prompt_cost + completion_cost == pytest.approx(0.037)
assert prompt_cost == pytest.approx(0.037 * priced_prompt / (priced_prompt + priced_completion))
def test_provider_reported_usage_cost_without_rates_is_all_output(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, raising=False)
assert cost_per_token(
model=_REPORTED_COST_FIXTURE_MODEL, custom_llm_provider="openai", usage_object=Usage(**_REPORTED_USAGE)
) == (0.0, 0.037)
@pytest.mark.parametrize("reported", [-0.037, float("nan"), float("inf"), True])
def test_unusable_provider_reported_usage_cost_falls_back_to_the_token_math(
monkeypatch: pytest.MonkeyPatch, reported: object
) -> None:
monkeypatch.setitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, _REPORTED_COST_FIXTURE_RATES)
usage: Final = Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120)
setattr(usage, "cost", reported)
assert cost_per_token(model=_REPORTED_COST_FIXTURE_MODEL, custom_llm_provider="openai", usage_object=usage) == (
pytest.approx(100 * 1e-6),
pytest.approx(20 * 3e-6),
)
def test_caller_supplied_custom_cost_per_token_beats_the_provider_reported_usage_cost() -> None:
assert cost_per_token(
model=_REPORTED_COST_FIXTURE_MODEL,
custom_llm_provider="openai",
prompt_tokens=100,
completion_tokens=20,
usage_object=Usage(**_REPORTED_USAGE),
custom_cost_per_token={"input_cost_per_token": 0.001, "output_cost_per_token": 0.002},
) == (pytest.approx(0.1), pytest.approx(0.04))