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:
Kerry Lu 2026-10-31 09:04:50 -07:00 • committed by GitHub
parent b2647116ca
commit 3cb2e60de0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 277 additions and 12 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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": {

View file

@ -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"}))

View file

@ -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))