mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-01 07:03:02 +00:00
fix(cost): split a provider-reported total between input and output by the cost map rate ratio (#49357)
* fix(cost): split a provider-reported total between input and output by the cost map rate ratio Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(cost): expect the kyma reported-cost headers to follow the cost map rate split Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(cost): keep the kyma test file at its original formatting Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(cost): price the response-cost header split from the served model, not the request alias Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b2647116ca
commit
3cb2e60de0
6 changed files with 277 additions and 12 deletions
|
|
@ -1554,12 +1554,66 @@ def _breakdown_total(litellm_logging_obj: LitellmLoggingObject | None) -> float
|
|||
return float(total) if isinstance(total, (int, float)) and not isinstance(total, bool) else None
|
||||
|
||||
|
||||
def _cost_map_priced_input_output(
|
||||
model: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
usage: Usage | None,
|
||||
call_type: CallTypesLiteral | None,
|
||||
service_tier: str | None,
|
||||
data_residency: str | None,
|
||||
) -> tuple[float, float]:
|
||||
if not model or usage is None:
|
||||
return 0.0, 0.0
|
||||
try:
|
||||
return cost_per_token(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
usage_object=usage,
|
||||
call_type=call_type or "completion",
|
||||
service_tier=service_tier,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
except Exception as pricing_error: # noqa: BLE001 # cost_per_token raises plain Exception for an unmapped model
|
||||
verbose_logger.debug("No cost map price for reported-cost split of %s: %s", model, pricing_error)
|
||||
return 0.0, 0.0
|
||||
|
||||
|
||||
def split_reported_cost_by_cost_map_ratio(
|
||||
reported_total: float,
|
||||
model: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
usage: Usage | None,
|
||||
call_type: CallTypesLiteral | None,
|
||||
service_tier: str | None,
|
||||
data_residency: str | None,
|
||||
) -> tuple[float, float]:
|
||||
"""Apportions a provider-reported total between input and output in the ratio litellm's own cost map would
|
||||
price the same usage, so a per-input-token model books the whole total on input. Unmapped or zero-priced
|
||||
usage keeps the whole total on output."""
|
||||
priced_input, priced_output = _cost_map_priced_input_output(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
usage=usage,
|
||||
call_type=call_type,
|
||||
service_tier=service_tier,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
priced_total: Final = priced_input + priced_output
|
||||
if priced_total <= 0:
|
||||
return 0.0, reported_total
|
||||
input_cost: Final = reported_total * (priced_input / priced_total)
|
||||
return input_cost, reported_total - input_cost
|
||||
|
||||
|
||||
def _adjusted_provider_billed_cost(
|
||||
reported_cost: float,
|
||||
custom_llm_provider: str | None,
|
||||
pricing_provider: str | None,
|
||||
model: str | None,
|
||||
region_name: str | None,
|
||||
litellm_logging_obj: LitellmLoggingObject | None,
|
||||
usage: Usage | None,
|
||||
call_type: CallTypesLiteral | None,
|
||||
service_tier: str | None,
|
||||
data_residency: str | None,
|
||||
) -> float:
|
||||
|
|
@ -1569,10 +1623,19 @@ def _adjusted_provider_billed_cost(
|
|||
adjusted_cost, margin_percent, margin_fixed_amount, margin_total_amount = _apply_cost_margin(
|
||||
base_cost=discounted_cost, custom_llm_provider=custom_llm_provider
|
||||
)
|
||||
input_cost, output_cost = split_reported_cost_by_cost_map_ratio(
|
||||
reported_total=adjusted_cost,
|
||||
model=model,
|
||||
custom_llm_provider=pricing_provider,
|
||||
usage=usage,
|
||||
call_type=call_type,
|
||||
service_tier=service_tier,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
_store_cost_breakdown_in_logging_obj(
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
prompt_tokens_cost_usd_dollar=0.0,
|
||||
completion_tokens_cost_usd_dollar=adjusted_cost,
|
||||
prompt_tokens_cost_usd_dollar=input_cost,
|
||||
completion_tokens_cost_usd_dollar=output_cost,
|
||||
cost_for_built_in_tools_cost_usd_dollar=0.0,
|
||||
total_cost_usd_dollar=adjusted_cost,
|
||||
original_cost=reported_cost,
|
||||
|
|
@ -2080,9 +2143,16 @@ def completion_cost(
|
|||
return _adjusted_provider_billed_cost(
|
||||
reported_cost=reported_cost,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
pricing_provider=_pricing_provider_for_cost_candidate(
|
||||
candidate_provider=candidate_pricing_provider,
|
||||
request_provider=request_pricing_provider,
|
||||
current_provider=custom_llm_provider,
|
||||
),
|
||||
model=model_for_discount,
|
||||
region_name=region_name if isinstance(region_name, str) else None,
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
usage=cost_per_token_usage_object,
|
||||
call_type=call_type,
|
||||
service_tier=service_tier,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
|
|
@ -2387,10 +2457,28 @@ def response_cost_calculator(
|
|||
provider_response_cost: Final = get_response_cost_from_hidden_params(hidden_params)
|
||||
if provider_response_cost is not None and not custom_pricing:
|
||||
if _breakdown_total(litellm_logging_obj) != provider_response_cost:
|
||||
header_priced_model: Final = select_model_name_for_cost_calc(
|
||||
model=model,
|
||||
completion_response=response_object,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
custom_pricing=custom_pricing,
|
||||
base_model=base_model,
|
||||
router_model_id=router_model_id,
|
||||
region_name=region_name,
|
||||
)
|
||||
header_input_cost, header_output_cost = split_reported_cost_by_cost_map_ratio(
|
||||
reported_total=provider_response_cost,
|
||||
model=header_priced_model or model,
|
||||
custom_llm_provider=pricing_provider or custom_llm_provider,
|
||||
usage=get_usage_object(response_object),
|
||||
call_type=call_type,
|
||||
service_tier=service_tier,
|
||||
data_residency=data_residency,
|
||||
)
|
||||
_store_cost_breakdown_in_logging_obj(
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
prompt_tokens_cost_usd_dollar=0.0,
|
||||
completion_tokens_cost_usd_dollar=provider_response_cost,
|
||||
prompt_tokens_cost_usd_dollar=header_input_cost,
|
||||
completion_tokens_cost_usd_dollar=header_output_cost,
|
||||
cost_for_built_in_tools_cost_usd_dollar=0.0,
|
||||
total_cost_usd_dollar=provider_response_cost,
|
||||
original_cost=provider_response_cost,
|
||||
|
|
|
|||
|
|
@ -493,8 +493,8 @@ OPENROUTER_ANTHROPIC_CLAUDE_SONNET_5_PROVIDER_REPORTED_COST: Final = CostTrackin
|
|||
),
|
||||
expected=ExactExpected(
|
||||
spend=0.0421,
|
||||
input_cost=0.0,
|
||||
output_cost=0.0421,
|
||||
input_cost=0.0421 * (1840 * 3.2e-06) / (1840 * 3.2e-06 + 412 * 1.6e-05),
|
||||
output_cost=0.0421 * (412 * 1.6e-05) / (1840 * 3.2e-06 + 412 * 1.6e-05),
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
breakdown_persisted=False,
|
||||
|
|
|
|||
|
|
@ -7787,8 +7787,8 @@ GPT_5_6_UPSTREAM_COST_HEADER_BILLS_HEADER_VALUE: Final = CostTrackingTestCase(
|
|||
),
|
||||
expected=ExactExpected(
|
||||
spend=0.000413,
|
||||
input_cost=0.0,
|
||||
output_cost=0.000413,
|
||||
input_cost=0.000413 * (10 * 1.75e-06) / (10 * 1.75e-06 + 253 * 1.4e-05),
|
||||
output_cost=0.000413 * (253 * 1.4e-05) / (10 * 1.75e-06 + 253 * 1.4e-05),
|
||||
prompt_tokens=10,
|
||||
completion_tokens=253,
|
||||
breakdown_persisted=False,
|
||||
|
|
@ -11885,7 +11885,7 @@ GPT_5_6_TERRA_MESSAGES_BRIDGED_STREAM: Final = GPT_5_6_TERRA_MESSAGES_BRIDGED_JS
|
|||
# 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.0, output_cost=0.037, prompt_tokens=100, completion_tokens=20
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ COST_MAP_ENTRIES: Final[dict[str, dict[str, JsonValue]]] = {
|
|||
"openrouter/typesafe/jev-1.13": {
|
||||
"litellm_provider": "openrouter",
|
||||
"mode": "evaluation",
|
||||
"input_cost_per_token": 0.0,
|
||||
"input_cost_per_token": 4.2e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
},
|
||||
"perplexity/pplx-decider-v1-27b": {
|
||||
|
|
|
|||
|
|
@ -410,8 +410,12 @@ def test_cost_headers_show_the_discount_and_margin_taken_from_the_cost_kyma_repo
|
|||
assert float(headers["x-litellm-response-cost-discount-amount"] or "nan") == pytest.approx(0.004)
|
||||
assert float(headers["x-litellm-response-cost-margin-amount"] or "nan") == pytest.approx(0.0004)
|
||||
assert float(headers["x-litellm-response-cost"] or "nan") == pytest.approx(0.0044)
|
||||
assert float(headers["x-litellm-response-cost-input"] or "nan") == 0.0
|
||||
assert float(headers["x-litellm-response-cost-output"] or "nan") == pytest.approx(0.0044)
|
||||
row: Final = litellm.get_model_info("sonar", custom_llm_provider="kyma")
|
||||
priced_input: Final = 4 * row["input_cost_per_token"]
|
||||
priced_output: Final = 3 * row["output_cost_per_token"]
|
||||
input_share: Final = priced_input / (priced_input + priced_output)
|
||||
assert float(headers["x-litellm-response-cost-input"] or "nan") == pytest.approx(0.0044 * input_share)
|
||||
assert float(headers["x-litellm-response-cost-output"] or "nan") == pytest.approx(0.0044 * (1 - input_share))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thinking", ({"type": "enabled", "budget_tokens": 1024}, {"type": "adaptive"}))
|
||||
|
|
|
|||
|
|
@ -10935,3 +10935,176 @@ 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_TOTAL: Final = 0.037
|
||||
|
||||
|
||||
def _reported_cost_breakdown(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
model: str,
|
||||
call_type: Literal["completion", "decisions"],
|
||||
cost_map_row: Mapping[str, object] | None,
|
||||
) -> Mapping[str, object]:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
|
||||
if cost_map_row is not None:
|
||||
litellm.register_model(model_cost={model: dict(cost_map_row)})
|
||||
logging_obj: Final = Logging(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "reported cost"}],
|
||||
stream=False,
|
||||
call_type=call_type,
|
||||
start_time=datetime.datetime.now(),
|
||||
litellm_call_id="reported-cost-split",
|
||||
function_id="f",
|
||||
)
|
||||
response: Final = ModelResponse(
|
||||
model=model,
|
||||
choices=[Choices(message=Message(content="ok"))],
|
||||
usage=Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120, cost=_REPORTED_TOTAL),
|
||||
)
|
||||
spend: Final = completion_cost(
|
||||
completion_response=response,
|
||||
model=model,
|
||||
custom_llm_provider="openai",
|
||||
call_type=call_type,
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
assert spend == _REPORTED_TOTAL
|
||||
breakdown: Final = logging_obj.cost_breakdown
|
||||
assert breakdown is not None
|
||||
return breakdown
|
||||
|
||||
|
||||
def test_reported_total_is_split_between_input_and_output_by_the_cost_map_rate_ratio(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
breakdown: Final = _reported_cost_breakdown(
|
||||
monkeypatch,
|
||||
model="cc-reported-cost-both-rates",
|
||||
call_type="completion",
|
||||
cost_map_row={
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"input_cost_per_token": 1e-06,
|
||||
"output_cost_per_token": 3e-06,
|
||||
},
|
||||
)
|
||||
|
||||
input_share: Final = (100 * 1e-06) / (100 * 1e-06 + 20 * 3e-06)
|
||||
assert breakdown["input_cost"] == pytest.approx(_REPORTED_TOTAL * input_share)
|
||||
assert breakdown["output_cost"] == pytest.approx(_REPORTED_TOTAL * (1 - input_share))
|
||||
assert breakdown["input_cost"] + breakdown["output_cost"] == _REPORTED_TOTAL
|
||||
assert breakdown["total_cost"] == _REPORTED_TOTAL
|
||||
|
||||
|
||||
def test_reported_total_on_a_decisions_model_priced_per_input_token_lands_on_input(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
breakdown: Final = _reported_cost_breakdown(
|
||||
monkeypatch,
|
||||
model="cc-reported-cost-decisions",
|
||||
call_type="decisions",
|
||||
cost_map_row={
|
||||
"litellm_provider": "openai",
|
||||
"mode": "decisions",
|
||||
"input_cost_per_token": 4.2e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
assert breakdown["input_cost"] == _REPORTED_TOTAL
|
||||
assert breakdown["output_cost"] == 0.0
|
||||
|
||||
|
||||
def test_reported_total_without_a_cost_map_row_falls_back_to_output(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
breakdown: Final = _reported_cost_breakdown(
|
||||
monkeypatch, model="cc-reported-cost-unmapped", call_type="completion", cost_map_row=None
|
||||
)
|
||||
|
||||
assert breakdown["input_cost"] == 0.0
|
||||
assert breakdown["output_cost"] == _REPORTED_TOTAL
|
||||
|
||||
|
||||
def test_upstream_cost_header_total_is_split_by_the_cost_map_rate_ratio(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
|
||||
litellm.register_model(
|
||||
model_cost={
|
||||
"cc-header-cost-chat": {
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"input_cost_per_token": 0.001,
|
||||
"output_cost_per_token": 0.002,
|
||||
}
|
||||
}
|
||||
)
|
||||
logging_obj: Final = Logging(
|
||||
model="cc-header-cost-chat",
|
||||
messages=[{"role": "user", "content": "reported cost"}],
|
||||
stream=False,
|
||||
call_type="completion",
|
||||
start_time=datetime.datetime.now(),
|
||||
litellm_call_id="reported-cost-header-split",
|
||||
function_id="f",
|
||||
)
|
||||
|
||||
cost: Final = response_cost_calculator(
|
||||
response_object=_header_cost_chat_response({"llm_provider-x-litellm-response-cost": "0.25"}),
|
||||
model="cc-header-cost-chat",
|
||||
custom_llm_provider="openai",
|
||||
call_type="completion",
|
||||
optional_params={},
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert cost == 0.25
|
||||
breakdown: Final = logging_obj.cost_breakdown
|
||||
assert breakdown is not None
|
||||
input_share: Final = (10 * 0.001) / (10 * 0.001 + 20 * 0.002)
|
||||
assert breakdown["input_cost"] == pytest.approx(0.25 * input_share)
|
||||
assert breakdown["output_cost"] == pytest.approx(0.25 * (1 - input_share))
|
||||
assert breakdown["total_cost"] == 0.25
|
||||
|
||||
|
||||
def test_upstream_cost_header_total_on_an_unmapped_alias_is_split_by_the_served_model_rates(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
|
||||
litellm.register_model(
|
||||
model_cost={
|
||||
"cc-header-cost-chat": {
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"input_cost_per_token": 0.001,
|
||||
"output_cost_per_token": 0.002,
|
||||
}
|
||||
}
|
||||
)
|
||||
logging_obj: Final = Logging(
|
||||
model="team-alias-for-header-cost",
|
||||
messages=[{"role": "user", "content": "reported cost"}],
|
||||
stream=False,
|
||||
call_type="completion",
|
||||
start_time=datetime.datetime.now(),
|
||||
litellm_call_id="reported-cost-header-alias-split",
|
||||
function_id="f",
|
||||
)
|
||||
|
||||
cost: Final = response_cost_calculator(
|
||||
response_object=_header_cost_chat_response({"llm_provider-x-litellm-response-cost": "0.25"}),
|
||||
model="team-alias-for-header-cost",
|
||||
custom_llm_provider="openai",
|
||||
call_type="completion",
|
||||
optional_params={},
|
||||
litellm_logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert cost == 0.25
|
||||
breakdown: Final = logging_obj.cost_breakdown
|
||||
assert breakdown is not None
|
||||
input_share: Final = (10 * 0.001) / (10 * 0.001 + 20 * 0.002)
|
||||
assert breakdown["input_cost"] == pytest.approx(0.25 * input_share)
|
||||
assert breakdown["output_cost"] == pytest.approx(0.25 * (1 - input_share))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue