mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(proxy): report /cost/estimate rates from the call that billed them
The estimate looked the reported per-token rates up a second time, with the provider this endpoint resolved rather than the one completion_cost infers. The provider decides whether a token tier threshold is inclusive, so an unrouted xai model sitting exactly on 200k billed at the tier rate and reported the base rate, half of it. completion_cost now hands back the rates its own lines were billed at, and the endpoint reports those. Claude-Session: https://claude.ai/code/session_01RLKy5DMi3XCBUJ37WzfNi1
This commit is contained in:
parent
ba91588b15
commit
36f3ca95d8
7 changed files with 195 additions and 52 deletions
|
|
@ -25,6 +25,7 @@ from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import
|
||||||
TranscriptionUsageObjectTransformation,
|
TranscriptionUsageObjectTransformation,
|
||||||
)
|
)
|
||||||
from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
||||||
|
BilledTokenRates,
|
||||||
CostCalculatorUtils,
|
CostCalculatorUtils,
|
||||||
_generic_cost_per_character,
|
_generic_cost_per_character,
|
||||||
_get_regional_uplift_multiplier,
|
_get_regional_uplift_multiplier,
|
||||||
|
|
@ -1122,6 +1123,7 @@ def _store_cost_breakdown_in_logging_obj(
|
||||||
service_tier: str | None = None,
|
service_tier: str | None = None,
|
||||||
data_residency: str | None = None,
|
data_residency: str | None = None,
|
||||||
vertex_location: str | None = None,
|
vertex_location: str | None = None,
|
||||||
|
billed_token_rates: BilledTokenRates | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Helper function to store cost breakdown in the logging object.
|
Helper function to store cost breakdown in the logging object.
|
||||||
|
|
@ -1166,6 +1168,7 @@ def _store_cost_breakdown_in_logging_obj(
|
||||||
service_tier=service_tier,
|
service_tier=service_tier,
|
||||||
data_residency=data_residency,
|
data_residency=data_residency,
|
||||||
vertex_location=vertex_location,
|
vertex_location=vertex_location,
|
||||||
|
billed_token_rates=billed_token_rates,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as breakdown_error:
|
except Exception as breakdown_error:
|
||||||
|
|
@ -1735,6 +1738,7 @@ def completion_cost(
|
||||||
_reasoning_cost: float | None = None
|
_reasoning_cost: float | None = None
|
||||||
_cache_read_cost: float | None = None
|
_cache_read_cost: float | None = None
|
||||||
_cache_creation_cost: float | None = None
|
_cache_creation_cost: float | None = None
|
||||||
|
_billed_token_rates: BilledTokenRates | None = None
|
||||||
if cost_per_token_usage_object is not None and model:
|
if cost_per_token_usage_object is not None and model:
|
||||||
_breakdown_provider: str | None = (
|
_breakdown_provider: str | None = (
|
||||||
custom_llm_provider if isinstance(custom_llm_provider, str) else None
|
custom_llm_provider if isinstance(custom_llm_provider, str) else None
|
||||||
|
|
@ -1751,6 +1755,7 @@ def completion_cost(
|
||||||
_reasoning_cost = _token_type_breakdown.reasoning_cost
|
_reasoning_cost = _token_type_breakdown.reasoning_cost
|
||||||
_cache_read_cost = _token_type_breakdown.cache_read_cost
|
_cache_read_cost = _token_type_breakdown.cache_read_cost
|
||||||
_cache_creation_cost = _token_type_breakdown.cache_creation_cost
|
_cache_creation_cost = _token_type_breakdown.cache_creation_cost
|
||||||
|
_billed_token_rates = _token_type_breakdown.rates
|
||||||
_store_cost_breakdown_in_logging_obj(
|
_store_cost_breakdown_in_logging_obj(
|
||||||
litellm_logging_obj=litellm_logging_obj,
|
litellm_logging_obj=litellm_logging_obj,
|
||||||
prompt_tokens_cost_usd_dollar=prompt_tokens_cost_usd_dollar,
|
prompt_tokens_cost_usd_dollar=prompt_tokens_cost_usd_dollar,
|
||||||
|
|
@ -1770,6 +1775,7 @@ def completion_cost(
|
||||||
service_tier=service_tier,
|
service_tier=service_tier,
|
||||||
data_residency=data_residency,
|
data_residency=data_residency,
|
||||||
vertex_location=vertex_location,
|
vertex_location=vertex_location,
|
||||||
|
billed_token_rates=_billed_token_rates,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _final_cost
|
return _final_cost
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ if TYPE_CHECKING:
|
||||||
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
||||||
|
|
||||||
from litellm.integrations.otel.logger import OpenTelemetryV2
|
from litellm.integrations.otel.logger import OpenTelemetryV2
|
||||||
|
from litellm.litellm_core_utils.llm_cost_calc.utils import BilledTokenRates
|
||||||
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
|
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
|
||||||
try:
|
try:
|
||||||
from litellm_enterprise.enterprise_callbacks.callback_controls import (
|
from litellm_enterprise.enterprise_callbacks.callback_controls import (
|
||||||
|
|
@ -581,6 +582,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
||||||
|
|
||||||
# Initialize cost breakdown field
|
# Initialize cost breakdown field
|
||||||
self.cost_breakdown: CostBreakdown | None = None
|
self.cost_breakdown: CostBreakdown | None = None
|
||||||
|
self.billed_token_rates: BilledTokenRates | None = None
|
||||||
|
|
||||||
# Init Caching related details
|
# Init Caching related details
|
||||||
self.caching_details: CachingDetails | None = None
|
self.caching_details: CachingDetails | None = None
|
||||||
|
|
@ -1585,6 +1587,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
||||||
service_tier: str | None = None,
|
service_tier: str | None = None,
|
||||||
data_residency: str | None = None,
|
data_residency: str | None = None,
|
||||||
vertex_location: str | None = None,
|
vertex_location: str | None = None,
|
||||||
|
billed_token_rates: "BilledTokenRates | None" = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Helper method to store cost breakdown in the logging object.
|
Helper method to store cost breakdown in the logging object.
|
||||||
|
|
@ -1604,8 +1607,10 @@ class Logging(LiteLLMLoggingBaseClass):
|
||||||
service_tier: Tier the costs above were priced on, already resolved
|
service_tier: Tier the costs above were priced on, already resolved
|
||||||
data_residency: Region uplift the costs above were priced on, already resolved
|
data_residency: Region uplift the costs above were priced on, already resolved
|
||||||
vertex_location: Vertex AI location the costs above were priced on, already resolved
|
vertex_location: Vertex AI location the costs above were priced on, already resolved
|
||||||
|
billed_token_rates: Per-token rates the costs above were billed at, already resolved
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.billed_token_rates = billed_token_rates
|
||||||
self.cost_breakdown = CostBreakdown(
|
self.cost_breakdown = CostBreakdown(
|
||||||
input_cost=input_cost,
|
input_cost=input_cost,
|
||||||
output_cost=output_cost,
|
output_cost=output_cost,
|
||||||
|
|
|
||||||
|
|
@ -1302,34 +1302,6 @@ def _coerce_token_count(value: object) -> int:
|
||||||
return value if isinstance(value, int) and value > 0 else 0
|
return value if isinstance(value, int) and value > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class TokenTypeCostBreakdown:
|
|
||||||
reasoning_cost: float
|
|
||||||
cache_read_cost: float
|
|
||||||
cache_creation_cost: float
|
|
||||||
|
|
||||||
|
|
||||||
def _reasoning_token_count(usage: Usage) -> int:
|
|
||||||
parsed: Final = (
|
|
||||||
parse_completion_tokens_details(usage)["reasoning_tokens"] if usage.completion_tokens_details is not None else 0
|
|
||||||
)
|
|
||||||
return parsed or _coerce_token_count(getattr(usage, "reasoning_tokens", 0))
|
|
||||||
|
|
||||||
|
|
||||||
def _cache_token_counts(usage: Usage) -> tuple[int, int, CacheCreationTokenDetails | None]:
|
|
||||||
"""(cache read tokens, cache creation tokens, cache creation details): read from prompt_tokens_details
|
|
||||||
first, then the private top-level counters the Usage constructor mirrors cache tokens onto for
|
|
||||||
providers/callers that bypass the details."""
|
|
||||||
parsed: Final = parse_prompt_tokens_details(usage) if usage.prompt_tokens_details is not None else None
|
|
||||||
parsed_read: Final = parsed["cache_hit_tokens"] if parsed is not None else 0
|
|
||||||
parsed_creation: Final = parsed["cache_creation_tokens"] if parsed is not None else 0
|
|
||||||
return (
|
|
||||||
parsed_read or _coerce_token_count(getattr(usage, "_cache_read_input_tokens", 0)),
|
|
||||||
parsed_creation or _coerce_token_count(getattr(usage, "_cache_creation_input_tokens", 0)),
|
|
||||||
parsed["cache_creation_token_details"] if parsed is not None else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class BilledTokenRates:
|
class BilledTokenRates:
|
||||||
"""Per-token rates one request's usage bills at, after token tiers, off-peak windows and the
|
"""Per-token rates one request's usage bills at, after token tiers, off-peak windows and the
|
||||||
|
|
@ -1355,6 +1327,37 @@ class BilledTokenRates:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TokenTypeCostBreakdown:
|
||||||
|
reasoning_cost: float
|
||||||
|
cache_read_cost: float
|
||||||
|
cache_creation_cost: float
|
||||||
|
rates: BilledTokenRates | None = None
|
||||||
|
"""Rates these lines were billed at, so a caller reporting both cannot resolve them a second,
|
||||||
|
differently-argued way. None when the model's pricing could not be resolved."""
|
||||||
|
|
||||||
|
|
||||||
|
def _reasoning_token_count(usage: Usage) -> int:
|
||||||
|
parsed: Final = (
|
||||||
|
parse_completion_tokens_details(usage)["reasoning_tokens"] if usage.completion_tokens_details is not None else 0
|
||||||
|
)
|
||||||
|
return parsed or _coerce_token_count(getattr(usage, "reasoning_tokens", 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_token_counts(usage: Usage) -> tuple[int, int, CacheCreationTokenDetails | None]:
|
||||||
|
"""(cache read tokens, cache creation tokens, cache creation details): read from prompt_tokens_details
|
||||||
|
first, then the private top-level counters the Usage constructor mirrors cache tokens onto for
|
||||||
|
providers/callers that bypass the details."""
|
||||||
|
parsed: Final = parse_prompt_tokens_details(usage) if usage.prompt_tokens_details is not None else None
|
||||||
|
parsed_read: Final = parsed["cache_hit_tokens"] if parsed is not None else 0
|
||||||
|
parsed_creation: Final = parsed["cache_creation_tokens"] if parsed is not None else 0
|
||||||
|
return (
|
||||||
|
parsed_read or _coerce_token_count(getattr(usage, "_cache_read_input_tokens", 0)),
|
||||||
|
parsed_creation or _coerce_token_count(getattr(usage, "_cache_creation_input_tokens", 0)),
|
||||||
|
parsed["cache_creation_token_details"] if parsed is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _custom_pricing_rates(custom_cost_per_token: CostPerToken) -> BilledTokenRates:
|
def _custom_pricing_rates(custom_cost_per_token: CostPerToken) -> BilledTokenRates:
|
||||||
"""Flat custom pricing has no tiers, uplifts or reasoning rate: cache tokens bill at the configured
|
"""Flat custom pricing has no tiers, uplifts or reasoning rate: cache tokens bill at the configured
|
||||||
cache rates (else the input rate) and reasoning at the output rate, as _cost_per_token_custom_pricing_helper does."""
|
cache rates (else the input rate) and reasoning at the output rate, as _cost_per_token_custom_pricing_helper does."""
|
||||||
|
|
@ -1497,6 +1500,7 @@ def get_token_type_cost_breakdown(
|
||||||
reasoning_cost=float(_reasoning_token_count(usage)) * rates.output_cost_per_reasoning_token,
|
reasoning_cost=float(_reasoning_token_count(usage)) * rates.output_cost_per_reasoning_token,
|
||||||
cache_read_cost=float(cache_read_tokens) * rates.cache_read_input_token_cost,
|
cache_read_cost=float(cache_read_tokens) * rates.cache_read_input_token_cost,
|
||||||
cache_creation_cost=cache_creation_cost,
|
cache_creation_cost=cache_creation_cost,
|
||||||
|
rates=rates,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import litellm
|
||||||
from litellm._internal_context import current_billing_time, pinned_billing_time
|
from litellm._internal_context import current_billing_time, pinned_billing_time
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.cost_calculator import completion_cost
|
from litellm.cost_calculator import completion_cost
|
||||||
from litellm.litellm_core_utils.llm_cost_calc.utils import get_billed_token_rates
|
|
||||||
from litellm.proxy._types import (
|
from litellm.proxy._types import (
|
||||||
CommonProxyErrors,
|
CommonProxyErrors,
|
||||||
CostEstimateRequest,
|
CostEstimateRequest,
|
||||||
|
|
@ -85,9 +84,9 @@ def _extract_custom_pricing(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _lookup_model_info(model: str) -> ModelInfo | None:
|
def _lookup_model_info(model: str, custom_llm_provider: str | None = None) -> ModelInfo | None:
|
||||||
try:
|
try:
|
||||||
return litellm.get_model_info(model=model)
|
return litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -122,7 +121,7 @@ def _resolve_model_for_cost_lookup(model: str) -> ResolvedCostModel:
|
||||||
if resolved_model:
|
if resolved_model:
|
||||||
verbose_proxy_logger.debug("Resolved model '%s' to '%s' from router", model, resolved_model)
|
verbose_proxy_logger.debug("Resolved model '%s' to '%s' from router", model, resolved_model)
|
||||||
custom_cost_per_token: Final = _extract_custom_pricing(
|
custom_cost_per_token: Final = _extract_custom_pricing(
|
||||||
litellm_params, model_info, _lookup_model_info(str(resolved_model))
|
litellm_params, model_info, _lookup_model_info(str(resolved_model), provider)
|
||||||
)
|
)
|
||||||
return ResolvedCostModel(str(resolved_model), provider, custom_cost_per_token)
|
return ResolvedCostModel(str(resolved_model), provider, custom_cost_per_token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -632,10 +631,9 @@ async def estimate_cost(
|
||||||
function_id="cost-estimate",
|
function_id="cost-estimate",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The totals, the per-token-type lines and the reported rates each resolve pricing on their
|
# Pinning one moment keeps an off-peak window that opens mid-quote from pricing the totals on
|
||||||
# own path. Pinning one moment keeps an off-peak window that opens mid-quote from splitting them.
|
# one side of it and the reported rates on the other.
|
||||||
billed_at: Final = current_billing_time()
|
with pinned_billing_time(current_billing_time()):
|
||||||
with pinned_billing_time(billed_at):
|
|
||||||
# Use completion_cost which handles all the logic including margins/discounts
|
# Use completion_cost which handles all the logic including margins/discounts
|
||||||
try:
|
try:
|
||||||
cost_per_request: Final = completion_cost(
|
cost_per_request: Final = completion_cost(
|
||||||
|
|
@ -653,19 +651,16 @@ async def estimate_cost(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
rates: Final = get_billed_token_rates(
|
# The rates come back from the pricing call itself rather than a second lookup, so they are the
|
||||||
model=resolved_model,
|
# ones the cost lines above billed at even when completion_cost infers a provider this endpoint
|
||||||
custom_llm_provider=resolved_provider,
|
# never resolved (an unrouted "xai/grok-4" prices on xai's inclusive tier thresholds; a lookup
|
||||||
usage=usage,
|
# here without that provider would report the sub-200k rate for a line billed above it).
|
||||||
custom_cost_per_token=resolved.custom_cost_per_token,
|
rates: Final = litellm_logging_obj.billed_token_rates
|
||||||
current_time=billed_at,
|
|
||||||
)
|
|
||||||
|
|
||||||
per_request: Final = _cost_lines(cost_per_request, litellm_logging_obj.cost_breakdown)
|
per_request: Final = _cost_lines(cost_per_request, litellm_logging_obj.cost_breakdown)
|
||||||
daily: Final = per_request.times(request.num_requests_per_day)
|
daily: Final = per_request.times(request.num_requests_per_day)
|
||||||
monthly: Final = per_request.times(request.num_requests_per_month)
|
monthly: Final = per_request.times(request.num_requests_per_month)
|
||||||
|
|
||||||
model_info: Final = _lookup_model_info(resolved_model)
|
model_info: Final = _lookup_model_info(resolved_model, resolved_provider)
|
||||||
mapped_provider: Final = model_info.get("litellm_provider") if model_info is not None else None
|
mapped_provider: Final = model_info.get("litellm_provider") if model_info is not None else None
|
||||||
custom_llm_provider: Final = mapped_provider if mapped_provider is not None else resolved_provider
|
custom_llm_provider: Final = mapped_provider if mapped_provider is not None else resolved_provider
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
||||||
CostCalculatorUtils,
|
CostCalculatorUtils,
|
||||||
PromptTokensDetailsResult,
|
PromptTokensDetailsResult,
|
||||||
TokenRates,
|
TokenRates,
|
||||||
TokenTypeCostBreakdown,
|
|
||||||
_calculate_input_cost,
|
_calculate_input_cost,
|
||||||
_get_token_base_cost,
|
_get_token_base_cost,
|
||||||
_is_off_peak,
|
_is_off_peak,
|
||||||
|
|
@ -4023,6 +4022,49 @@ def test_a_pinned_billing_time_prices_the_totals_and_the_reported_rates_at_one_m
|
||||||
assert peak_completion_cost == pytest.approx(500 * peak_rates.output_cost_per_token)
|
assert peak_completion_cost == pytest.approx(500 * peak_rates.output_cost_per_token)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_token_type_breakdown_carries_the_rates_it_billed_at(monkeypatch):
|
||||||
|
"""Callers that report both the lines and the rates read the rates off the breakdown rather than
|
||||||
|
resolving them a second time, so the breakdown has to hand back exactly what it billed at."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"xai/tiered-model",
|
||||||
|
{
|
||||||
|
"input_cost_per_token": 3e-6,
|
||||||
|
"output_cost_per_token": 15e-6,
|
||||||
|
"cache_read_input_token_cost": 3e-7,
|
||||||
|
"input_cost_per_token_above_200k_tokens": 6e-6,
|
||||||
|
"output_cost_per_token_above_200k_tokens": 3e-5,
|
||||||
|
"cache_read_input_token_cost_above_200k_tokens": 6e-7,
|
||||||
|
"litellm_provider": "xai",
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
usage = Usage(
|
||||||
|
prompt_tokens=200_000,
|
||||||
|
completion_tokens=1_000,
|
||||||
|
total_tokens=201_000,
|
||||||
|
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=100_000),
|
||||||
|
)
|
||||||
|
|
||||||
|
breakdown = get_token_type_cost_breakdown(model="xai/tiered-model", custom_llm_provider="xai", usage=usage)
|
||||||
|
|
||||||
|
assert breakdown.rates == get_billed_token_rates(
|
||||||
|
model="xai/tiered-model", custom_llm_provider="xai", usage=usage
|
||||||
|
)
|
||||||
|
assert breakdown.rates.cache_read_input_token_cost == pytest.approx(6e-7)
|
||||||
|
assert breakdown.cache_read_cost == pytest.approx(100_000 * breakdown.rates.cache_read_input_token_cost)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_token_type_breakdown_reports_no_rates_for_an_unpriced_model():
|
||||||
|
usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
|
||||||
|
|
||||||
|
breakdown = get_token_type_cost_breakdown(
|
||||||
|
model="no-such-model-anywhere", custom_llm_provider="openai", usage=usage
|
||||||
|
)
|
||||||
|
|
||||||
|
assert breakdown.rates is None
|
||||||
|
|
||||||
|
|
||||||
def test_billed_token_rates_are_none_for_an_unpriced_model():
|
def test_billed_token_rates_are_none_for_an_unpriced_model():
|
||||||
usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
|
usage = Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15)
|
||||||
|
|
||||||
|
|
@ -4036,9 +4078,7 @@ def test_token_type_cost_breakdown_zero_without_special_tokens(_local_model_cost
|
||||||
model="gpt-4o", custom_llm_provider="openai", usage=usage
|
model="gpt-4o", custom_llm_provider="openai", usage=usage
|
||||||
)
|
)
|
||||||
|
|
||||||
assert breakdown == TokenTypeCostBreakdown(
|
assert (breakdown.reasoning_cost, breakdown.cache_read_cost, breakdown.cache_creation_cost) == (0.0, 0.0, 0.0)
|
||||||
reasoning_cost=0.0, cache_read_cost=0.0, cache_creation_cost=0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -4110,9 +4150,7 @@ def test_token_type_cost_breakdown_handles_unknown_model_gracefully():
|
||||||
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=5),
|
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=5),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert breakdown == TokenTypeCostBreakdown(
|
assert (breakdown.reasoning_cost, breakdown.cache_read_cost, breakdown.cache_creation_cost) == (0.0, 0.0, 0.0)
|
||||||
reasoning_cost=0.0, cache_read_cost=0.0, cache_creation_cost=0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_token_type_cost_breakdown_applies_regional_uplift(_local_model_cost_map):
|
def test_token_type_cost_breakdown_applies_regional_uplift(_local_model_cost_map):
|
||||||
|
|
|
||||||
|
|
@ -1139,6 +1139,45 @@ class TestEstimateCostCacheAndReasoningTokens:
|
||||||
assert response.output_cost_per_request == pytest.approx(OUTPUT_TOKENS * response.output_cost_per_token)
|
assert response.output_cost_per_request == pytest.approx(OUTPUT_TOKENS * response.output_cost_per_token)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_an_unrouted_model_reports_the_rates_of_the_provider_the_calculator_inferred(self, monkeypatch):
|
||||||
|
"""The cost calculator infers a provider this endpoint never resolved, and the provider decides
|
||||||
|
whether a tier threshold is inclusive. xai bills a request sitting exactly on the 200k threshold
|
||||||
|
at the tier rate, so the reported rates have to be the tier's rather than the sub-tier base."""
|
||||||
|
an_xai_model = "xai/tiered-model"
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
an_xai_model,
|
||||||
|
{
|
||||||
|
"input_cost_per_token": 3e-6,
|
||||||
|
"output_cost_per_token": 15e-6,
|
||||||
|
"cache_read_input_token_cost": 3e-7,
|
||||||
|
"input_cost_per_token_above_200k_tokens": 6e-6,
|
||||||
|
"output_cost_per_token_above_200k_tokens": 3e-5,
|
||||||
|
"cache_read_input_token_cost_above_200k_tokens": 6e-7,
|
||||||
|
"litellm_provider": "xai",
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await _estimate(
|
||||||
|
None,
|
||||||
|
model=an_xai_model,
|
||||||
|
input_tokens=200_000,
|
||||||
|
cache_read_input_tokens=100_000,
|
||||||
|
output_tokens=1_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.input_cost_per_token == pytest.approx(6e-6)
|
||||||
|
assert response.output_cost_per_token == pytest.approx(3e-5)
|
||||||
|
assert response.cache_read_input_token_cost == pytest.approx(6e-7)
|
||||||
|
assert response.cache_read_cost_per_request == pytest.approx(100_000 * response.cache_read_input_token_cost)
|
||||||
|
assert response.input_cost_per_request == pytest.approx(
|
||||||
|
100_000 * response.input_cost_per_token + response.cache_read_cost_per_request
|
||||||
|
)
|
||||||
|
assert response.output_cost_per_request == pytest.approx(1_000 * response.output_cost_per_token)
|
||||||
|
|
||||||
|
|
||||||
class TestCostEstimateRequestTokenSubsets:
|
class TestCostEstimateRequestTokenSubsets:
|
||||||
def test_cache_tokens_beyond_the_input_tokens_are_rejected(self):
|
def test_cache_tokens_beyond_the_input_tokens_are_rejected(self):
|
||||||
with pytest.raises(ValidationError, match="cannot exceed input_tokens"):
|
with pytest.raises(ValidationError, match="cannot exceed input_tokens"):
|
||||||
|
|
|
||||||
|
|
@ -3736,6 +3736,62 @@ def test_completion_cost_logs_reasoning_and_cache_breakdown(_local_model_cost_ma
|
||||||
assert logging_obj.cost_breakdown["cache_read_cost"] == pytest.approx(100 * 3e-08)
|
assert logging_obj.cost_breakdown["cache_read_cost"] == pytest.approx(100 * 3e-08)
|
||||||
|
|
||||||
|
|
||||||
|
def test_completion_cost_logs_the_rates_it_billed_at(monkeypatch):
|
||||||
|
"""A caller reporting the cost lines beside their per-token rates reads both off this one call.
|
||||||
|
completion_cost infers the provider, and xai's inclusive tier thresholds put a request sitting
|
||||||
|
exactly on 200k at the tier rate, which a lookup made without that inferred provider would miss.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||||
|
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"xai/tiered-model",
|
||||||
|
{
|
||||||
|
"input_cost_per_token": 3e-6,
|
||||||
|
"output_cost_per_token": 15e-6,
|
||||||
|
"cache_read_input_token_cost": 3e-7,
|
||||||
|
"input_cost_per_token_above_200k_tokens": 6e-6,
|
||||||
|
"output_cost_per_token_above_200k_tokens": 3e-5,
|
||||||
|
"cache_read_input_token_cost_above_200k_tokens": 6e-7,
|
||||||
|
"litellm_provider": "xai",
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logging_obj = Logging(
|
||||||
|
model="xai/tiered-model",
|
||||||
|
messages=[{"role": "user", "content": "Hello"}],
|
||||||
|
stream=False,
|
||||||
|
call_type="completion",
|
||||||
|
start_time=datetime.now(),
|
||||||
|
litellm_call_id="billed-rates",
|
||||||
|
function_id="f",
|
||||||
|
)
|
||||||
|
usage = Usage(
|
||||||
|
prompt_tokens=200_000,
|
||||||
|
completion_tokens=1_000,
|
||||||
|
total_tokens=201_000,
|
||||||
|
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=100_000),
|
||||||
|
)
|
||||||
|
|
||||||
|
litellm.completion_cost(
|
||||||
|
completion_response=ModelResponse(model="xai/tiered-model", usage=usage),
|
||||||
|
model="xai/tiered-model",
|
||||||
|
custom_llm_provider=None,
|
||||||
|
litellm_logging_obj=logging_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
rates = logging_obj.billed_token_rates
|
||||||
|
assert rates is not None
|
||||||
|
assert rates.input_cost_per_token == pytest.approx(6e-6)
|
||||||
|
assert rates.cache_read_input_token_cost == pytest.approx(6e-7)
|
||||||
|
assert logging_obj.cost_breakdown["cache_read_cost"] == pytest.approx(
|
||||||
|
100_000 * rates.cache_read_input_token_cost
|
||||||
|
)
|
||||||
|
assert logging_obj.cost_breakdown["output_cost"] == pytest.approx(1_000 * rates.output_cost_per_token)
|
||||||
|
|
||||||
|
|
||||||
def test_completion_cost_logs_cache_and_reasoning_breakdown_for_custom_pricing():
|
def test_completion_cost_logs_cache_and_reasoning_breakdown_for_custom_pricing():
|
||||||
"""
|
"""
|
||||||
A custom-priced deployment bills cache tokens at its custom cache rates, but the
|
A custom-priced deployment bills cache tokens at its custom cache rates, but the
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue