From 0af09747ea1ecf71cb345e15884610d2c7a9c59c Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Wed, 15 Jul 2026 11:24:06 +0800 Subject: [PATCH] fix: use seconds for Replicate runtime pricing --- litellm/cost_calculator.py | 14 +++++++++----- tests/test_litellm/test_cost_calculator.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index a40a8e1389c..91362b8bf6f 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -699,12 +699,12 @@ def get_replicate_completion_pricing(completion_response: dict, total_time=0.0): a100_80gb_price_per_second_public = ( DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND # assume all calls sent to A100 80GB for now ) - if total_time == 0.0: # total time is in ms + if total_time == 0.0: # total time is in seconds start_time = completion_response.get("created", time.time()) - end_time = getattr(completion_response, "ended", time.time()) + end_time = completion_response.get("ended", time.time()) total_time = end_time - start_time - return a100_80gb_price_per_second_public * total_time / 1000 + return a100_80gb_price_per_second_public * total_time def has_hidden_params(obj: Any) -> bool: @@ -1164,6 +1164,7 @@ def completion_cost( - For un-mapped Replicate models, the cost is calculated based on the total time used for the request. """ try: + replicate_total_time_seconds = total_time or 0.0 call_type = _infer_call_type(call_type, completion_response) or "completion" if ( @@ -1283,7 +1284,10 @@ def completion_cost( prompt_tokens_details = _usage.get("prompt_tokens_details") or {} cache_read_input_tokens = prompt_tokens_details.get("cached_tokens", 0) - total_time = getattr(completion_response, "_response_ms", 0) + response_time_ms: float = getattr(completion_response, "_response_ms", 0) or 0 + total_time = response_time_ms + if replicate_total_time_seconds == 0.0: + replicate_total_time_seconds = response_time_ms / 1000 hidden_params = getattr(completion_response, "_hidden_params", None) if hidden_params is not None: @@ -1521,7 +1525,7 @@ def completion_cost( # see https://replicate.com/pricing elif (model in litellm.replicate_models or "replicate" in model) and model not in litellm.model_cost: # for unmapped replicate model, default to replicate's time tracking logic - return get_replicate_completion_pricing(completion_response, total_time) # type: ignore + return get_replicate_completion_pricing(completion_response, replicate_total_time_seconds) # type: ignore if model is None: raise ValueError( diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 9636db4f4cd..52aafcdaf7b 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -11,10 +11,12 @@ sys.path.insert( from pydantic import BaseModel import litellm +from litellm.constants import DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND from litellm.cost_calculator import ( RealtimeAPITokenUsageProcessor, completion_cost, cost_per_token, + get_replicate_completion_pricing, handle_realtime_stream_cost_calculation, response_cost_calculator, ) @@ -23,6 +25,26 @@ from litellm.types.utils import ModelResponse, PromptTokensDetailsWrapper, Usage from litellm.utils import TranscriptionResponse +def test_unmapped_replicate_cost_with_explicit_seconds(): + cost = completion_cost(model="replicate/unmapped-runtime", total_time=2.0) + assert cost == pytest.approx(DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND * 2.0) + + +def test_unmapped_replicate_cost_with_timestamp_fallback(): + response = {"created": 10.0, "ended": 12.0} + cost = get_replicate_completion_pricing(response) + assert cost == pytest.approx(DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND * 2.0) + + +def test_unmapped_replicate_cost_converts_response_ms_to_seconds(): + model = "replicate/unmapped-runtime" + response = ModelResponse(model=model) + response._response_ms = 2000.0 + + cost = completion_cost(model=model, completion_response=response) + assert cost == pytest.approx(DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND * 2.0) + + def test_cost_per_token_duplicate_openai_prefix_matches_model_cost(monkeypatch): """ Router/proxy configs may use deployment ids like openai/openai/. Cost lookup must