From 59121223930e5b440b71d41e044da5c20f4a535e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:14:49 +0000 Subject: [PATCH] fix(proxy): price a billed failure row at the deployment's long-context tier like the success path (#47647) Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Backport of #47647 to stable/1.105.x. Cherry-picked from 70ece3501f5be4d5ed20e99958f2bc37183878a8 (main). --- litellm/proxy/utils.py | 80 ++++++++++++------- ..._utils_model_creation_and_error_logging.py | 29 +++++++ 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 06cf37676a8..4448cc285b8 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -82,7 +82,7 @@ from litellm.proxy.common_utils.openai_error_payload import ( from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error from litellm.types.guardrails import GuardrailEventHooks from litellm.types.proxy.model_listing import ModelInfoResponse -from litellm.types.utils import MCP_GUARDRAIL_CALL_TYPES, CallTypes, CallTypesLiteral, CostPerToken, ModelInfo, Usage +from litellm.types.utils import MCP_GUARDRAIL_CALL_TYPES, CallTypes, CallTypesLiteral, ModelInfo, Usage try: from litellm_enterprise.enterprise_callbacks.send_emails.base_email import ( @@ -146,6 +146,7 @@ from litellm.litellm_core_utils.core_helpers import ( is_expected_client_error, ) from litellm.litellm_core_utils.litellm_logging import Logging, deployment_pricing_model_info +from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads from litellm.litellm_core_utils.served_output_texts import ( @@ -1108,19 +1109,38 @@ def _billed_earlier_attempts( ) -def _deployment_cost_per_token(deployment_id: str | None, model: str) -> CostPerToken | None: - """The pricing the router registered for this deployment under its ``model_info.id``, read - through ``deployment_pricing_model_info`` exactly as the success path does; None means the - public cost map. The id is ``Logging.get_router_model_id()`` (the logging object's own - ``litellm_params``), never the flattened ``model_call_details`` copy a request body key can - overwrite.""" - model_info: Final = deployment_pricing_model_info(model_id=deployment_id, deployment_model=model) - if model_info is None: +def _deployment_priced_input_cost( + model: str, custom_llm_provider: str | None, prompt_tokens: int, deployment_pricing: ModelInfo +) -> float: + """Input price for the estimated prompt at the deployment's registered rates, read through + ``deployment_pricing_model_info`` and priced by ``generic_cost_per_token`` the way the success + path prices them, so a long-context tier the deployment declares (``input_cost_per_token_above_200k_tokens``) + is honoured instead of flattened to the standard rate. An entry it cannot price keeps the row at $0.""" + usage: Final = Usage(prompt_tokens=prompt_tokens, completion_tokens=0, total_tokens=prompt_tokens) + try: + prompt_cost, _ = generic_cost_per_token( + model=model, + usage=usage, + custom_llm_provider=custom_llm_provider or str(deployment_pricing.get("litellm_provider") or ""), + model_info=deployment_pricing, + ) + except Exception as exc: # noqa: BLE001 # a malformed registered entry must not drop the failure row + verbose_proxy_logger.debug("failure spend estimate: could not price deployment of %s: %s", model, exc) + return 0.0 + return prompt_cost + + +def _priced_input_cost(pricing_model: str, custom_llm_provider: str | None, prompt_tokens: int) -> float | None: + try: + estimated_input_cost, _ = litellm.cost_per_token( + model=pricing_model, + custom_llm_provider=custom_llm_provider, + prompt_tokens=prompt_tokens, + ) + except Exception as exc: # noqa: BLE001 # cost_per_token raises BadRequestError for an unmapped model and the row must still be written + verbose_proxy_logger.debug("failure spend estimate: could not price %s: %s", pricing_model, exc) return None - return CostPerToken( - input_cost_per_token=model_info["input_cost_per_token"] or 0.0, - output_cost_per_token=model_info["output_cost_per_token"] or 0.0, - ) + return estimated_input_cost def _estimated_input_cost( @@ -1129,24 +1149,22 @@ def _estimated_input_cost( deployment_id: str | None, base_model: str | None, ) -> float: - """Input price for the estimated prompt: the deployment's registered rate when it has one, else - the cost map's rate for ``base_model`` or the deployment model (an Azure deployment name is not - in the cost map, its ``base_model`` is). An unpriceable model keeps the row at $0 rather than - dropping the failure row.""" - try: - model: Final = str(model_call_details.get("model") or "") - estimated_input_cost, _ = litellm.cost_per_token( - model=base_model or model, - custom_llm_provider=str(model_call_details.get("custom_llm_provider") or "") or None, - prompt_tokens=prompt_tokens, - custom_cost_per_token=_deployment_cost_per_token(deployment_id, model), - ) - except Exception as exc: # noqa: BLE001 # cost_per_token raises BadRequestError for an unmapped model and the row must still be written - verbose_proxy_logger.debug( - "failure spend estimate: could not price %s: %s", model_call_details.get("model"), exc - ) - return 0.0 - return estimated_input_cost + """Input price for the estimated prompt: the deployment's registered rates when it has any (the id + is ``Logging.get_router_model_id()``, never the flattened ``model_call_details`` copy a request body + key can overwrite), else the cost map's rate for ``base_model``, else the deployment model, the same + candidate order ``completion_cost`` walks (an Azure deployment name is not in the cost map, its + ``base_model`` is; a ``base_model`` the cost map does not know falls through to the deployment + model). A model no candidate can price keeps the row at $0 rather than dropping the failure row.""" + model: Final = str(model_call_details.get("model") or "") + custom_llm_provider: Final = str(model_call_details.get("custom_llm_provider") or "") or None + deployment_pricing: Final = deployment_pricing_model_info(model_id=deployment_id, deployment_model=model) + if deployment_pricing is not None: + return _deployment_priced_input_cost(model, custom_llm_provider, prompt_tokens, deployment_pricing) + pricing_models: Final = (base_model, model) if base_model is not None else (model,) + candidate_costs: Final = ( + _priced_input_cost(pricing_model, custom_llm_provider, prompt_tokens) for pricing_model in pricing_models + ) + return next((cost for cost in candidate_costs if cost is not None), 0.0) _EMPTY_LIFT: Final = MappingProxyType({}) diff --git a/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py b/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py index 3f7484d6e58..1379cef7bef 100644 --- a/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py +++ b/tests/unit/proxy/test_proxy_utils_model_creation_and_error_logging.py @@ -2135,6 +2135,35 @@ def test_a_timed_out_request_is_priced_at_the_rate_the_router_registered_for_the assert response_cost == pytest.approx(usage.prompt_tokens * 0.25) +@pytest.mark.parametrize(("prompt_tokens", "expected_rate"), [(1_000, 1e-7), (250_000, 5e-7)]) +def test_a_timed_out_request_is_priced_at_the_deployments_long_context_tier(monkeypatch, prompt_tokens, expected_rate): + """A deployment that declares ``input_cost_per_token_above_200k_tokens`` next to its standard rate + bills a 250k-token prompt at the tier on the success path; the failure row must not flatten it to + the standard rate.""" + from litellm.proxy.utils import _estimated_input_cost + + monkeypatch.setitem( + litellm.model_cost, + "dep-lit-f9-tiered", + { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 1e-7, + "input_cost_per_token_above_200k_tokens": 5e-7, + "output_cost_per_token": 4e-7, + }, + ) + + response_cost = _estimated_input_cost( + model_call_details={"model": FAILURE_USAGE_MODEL, "custom_llm_provider": "openai"}, + prompt_tokens=prompt_tokens, + deployment_id="dep-lit-f9-tiered", + base_model=None, + ) + + assert response_cost == pytest.approx(prompt_tokens * expected_rate) + + def test_every_earlier_timed_out_attempt_is_billed_at_its_own_deployment_rate(monkeypatch): """The router leaves one RetryAttemptRecord per failed attempt in metadata.previous_models, the final attempt included; the earlier Timeout ones are billed (each at the deployment that served