diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 8331c68fe24..a836dcc3ce6 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -2293,6 +2293,29 @@ def response_cost_calculator( if provider_response_cost is not None and not custom_pricing: return provider_response_cost + if ( + isinstance(response_object, ModelResponse) + and custom_llm_provider == "openai" + and litellm_logging_obj is not None + and litellm_logging_obj.stream is False + and not custom_pricing + and base_model is None + ): + reported_usage_cost: Final[object] = getattr(getattr(response_object, "usage", None), "cost", None) + if ( + isinstance(reported_usage_cost, (int, float)) + and not isinstance(reported_usage_cost, bool) + and math.isfinite(reported_usage_cost) + and reported_usage_cost >= 0 + ): + discounted_usage_cost: Final = _apply_cost_discount( + base_cost=float(reported_usage_cost), + custom_llm_provider=custom_llm_provider, + model=model, + region_name=region_name, + )[0] + return _apply_cost_margin(discounted_usage_cost, custom_llm_provider)[0] + response_cost = completion_cost( completion_response=response_object, model=model, diff --git a/tests/unit/test_cost_calculator.py b/tests/unit/test_cost_calculator.py index 27b25c1f98e..bbec9b111ae 100644 --- a/tests/unit/test_cost_calculator.py +++ b/tests/unit/test_cost_calculator.py @@ -10522,3 +10522,213 @@ 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) + + +@pytest.mark.parametrize("async_mode", [False, True]) +@pytest.mark.parametrize("reported_cost", [0.0, 0.037]) +@respx.mock +async def test_openai_completion_records_reported_usage_cost( + monkeypatch: pytest.MonkeyPatch, async_mode: bool, reported_cost: float +) -> None: + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + respx.post("https://reported-cost.example/v1/chat/completions").mock( + return_value=httpx.Response( + 200, + json={ + "id": "chatcmpl-upstream-cost", + "object": "chat.completion", + "created": 1, + "model": "reported-cost-model", + "choices": [ + {"index": 0, "message": {"role": "assistant", "content": "hello"}, "finish_reason": "stop"} + ], + "usage": {"prompt_tokens": 100, "completion_tokens": 20, "total_tokens": 120, "cost": reported_cost}, + }, + ) + ) + request: Final = { + "model": "openai/reported-cost-model", + "api_base": "https://reported-cost.example/v1", + "api_key": "sk-unit-test", + "messages": [{"role": "user", "content": "hello"}], + } + response: Final = await litellm.acompletion(**request) if async_mode else litellm.completion(**request) + assert response._hidden_params["response_cost"] == pytest.approx(reported_cost) + + +@pytest.mark.parametrize("reported_cost", [0.0, 0.037]) +@pytest.mark.parametrize("has_catalog_price", [False, True]) +def test_openai_reported_usage_cost_reaches_spend_log( + monkeypatch: pytest.MonkeyPatch, reported_cost: float, has_catalog_price: bool +) -> None: + model: Final = "openai/reported-usage-cost-fixture" + if has_catalog_price: + monkeypatch.setitem( + litellm.model_cost, + model, + {"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 2e-6, "output_cost_per_token": 6e-6}, + ) + started: Final = datetime.datetime(2026, 1, 1) + logging_obj: Final = Logging( + model=model, + messages=[{"role": "user", "content": "hello"}], + stream=False, + call_type="completion", + start_time=started, + litellm_call_id="reported-usage-cost", + function_id="reported-usage-cost", + ) + logging_obj.update_from_kwargs(kwargs={}, optional_params={}, litellm_params={"custom_llm_provider": "openai"}) + logging_obj.model_call_details["custom_llm_provider"] = "openai" + response: Final = convert_to_model_response_object( + response_object={ + "id": "chatcmpl-reported-cost", + "model": model, + "choices": [{"index": 0, "message": {"role": "assistant", "content": "hello"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 100, "completion_tokens": 20, "total_tokens": 120, "cost": reported_cost}, + }, + model_response_object=ModelResponse(), + ) + logging_obj._process_hidden_params_and_response_cost(response, started, started) + assert logging_obj.model_call_details["response_cost"] == pytest.approx(reported_cost) + assert logging_obj.model_call_details["standard_logging_object"]["response_cost"] == pytest.approx(reported_cost) + + +@pytest.mark.parametrize("pricing_policy", ["automatic", "custom", "base", "cache"]) +@pytest.mark.parametrize("reported_cost", [None, -0.037, float("nan"), float("inf"), 0.037]) +def test_openai_reported_usage_cost_respects_pricing_policy( + monkeypatch: pytest.MonkeyPatch, + pricing_policy: str, + reported_cost: float | None, +) -> None: + model: Final = "openai/reported-usage-pricing-fixture" + base_model: Final = "openai/reported-usage-base-fixture" + input_rate: Final = 2e-6 + output_rate: Final = 6e-6 + monkeypatch.setitem( + litellm.model_cost, + model, + { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": input_rate, + "output_cost_per_token": output_rate, + }, + ) + monkeypatch.setitem( + litellm.model_cost, + base_model, + { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": input_rate * 2, + "output_cost_per_token": output_rate * 2, + }, + ) + response: Final = ModelResponse( + model=model, usage=Usage(prompt_tokens=100, completion_tokens=20, cost=reported_cost) + ) + cost: Final = response_cost_calculator( + response_object=response, + model=model, + custom_llm_provider="openai", + call_type="completion", + optional_params={}, + custom_pricing=pricing_policy == "custom", + base_model=base_model if pricing_policy == "base" else None, + cache_hit=pricing_policy == "cache", + litellm_logging_obj=Logging( + model=model, + messages=[], + stream=False, + call_type="completion", + start_time=datetime.datetime(2026, 1, 1), + litellm_call_id="reported-cost-pricing-policy", + function_id="reported-cost-pricing-policy", + ), + ) + token_cost: Final = (100 * input_rate + 20 * output_rate) * (2 if pricing_policy == "base" else 1) + expected: Final = ( + 0.0 + if pricing_policy == "cache" + else reported_cost + if pricing_policy == "automatic" and reported_cost == 0.037 + else token_cost + ) + assert cost == pytest.approx(expected) + + +@pytest.mark.parametrize("reported_cost", [0.0, 0.037]) +@pytest.mark.parametrize("discount", [0.0, 0.2]) +def test_openai_reported_usage_cost_applies_discount_before_margin( + monkeypatch: pytest.MonkeyPatch, reported_cost: float, discount: float +) -> None: + margin: Final = 0.1 + fixed_amount: Final = 0.001 + monkeypatch.setattr(litellm, "cost_discount_config", {"openai": discount}) + monkeypatch.setattr(litellm, "cost_margin_config", {"global": {"percentage": margin, "fixed_amount": fixed_amount}}) + response: Final = ModelResponse( + model="openai/reported-cost-with-margin", + usage=Usage(prompt_tokens=100, completion_tokens=20, cost=reported_cost), + ) + cost: Final = response_cost_calculator( + response_object=response, + model="openai/reported-cost-with-margin", + custom_llm_provider="openai", + call_type="completion", + optional_params={}, + litellm_logging_obj=Logging( + model="openai/reported-cost-with-margin", + messages=[], + stream=False, + call_type="completion", + start_time=datetime.datetime(2026, 1, 1), + litellm_call_id="reported-cost-margin", + function_id="reported-cost-margin", + ), + ) + assert cost == pytest.approx(reported_cost * (1 - discount) * (1 + margin) + fixed_amount) + + +@pytest.mark.parametrize("stream_context", ["absent", "streaming", "unspecified", "non_streaming"]) +def test_openai_reported_usage_cost_requires_non_streaming_context( + monkeypatch: pytest.MonkeyPatch, stream_context: str +) -> None: + model: Final = "openai/reported-usage-stream-context" + input_rate: Final = 1e-6 + output_rate: Final = 2e-6 + reported_cost: Final = 0.037 + monkeypatch.setitem( + litellm.model_cost, + model, + { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": input_rate, + "output_cost_per_token": output_rate, + }, + ) + logging_obj: Final = ( + None + if stream_context == "absent" + else Logging( + model=model, + messages=[], + stream=False if stream_context == "non_streaming" else True if stream_context == "streaming" else None, + call_type="completion", + start_time=datetime.datetime(2026, 1, 1), + litellm_call_id="reported-cost-stream-context", + function_id="reported-cost-stream-context", + ) + ) + response: Final = ModelResponse(model=model, usage=Usage(prompt_tokens=10, completion_tokens=5, cost=reported_cost)) + cost: Final = response_cost_calculator( + response_object=response, + model=model, + custom_llm_provider="openai", + call_type="completion", + optional_params={}, + litellm_logging_obj=logging_obj, + ) + expected: Final = reported_cost if stream_context == "non_streaming" else 10 * input_rate + 5 * output_rate + assert cost == pytest.approx(expected)