From a54319da431bf5e69c7bbb2c1eaf565113fb7fef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:06:44 +0000 Subject: [PATCH] fix(fireworks_ai): honor cached_tokens in cost calculator The fireworks_ai cost calculator billed 100% of prompt tokens at the full input rate, ignoring usage.prompt_tokens_details.cached_tokens and the model's cache_read_input_token_cost. Split the prompt cost so cached tokens are billed at the cache-read rate, falling back to the full input rate when no cache-read price is set. Fixes #32496 --- litellm/llms/fireworks_ai/cost_calculator.py | 13 ++++- tests/local_testing/test_completion_cost.py | 61 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/litellm/llms/fireworks_ai/cost_calculator.py b/litellm/llms/fireworks_ai/cost_calculator.py index ed936f6233a..ad5bb91bbc0 100644 --- a/litellm/llms/fireworks_ai/cost_calculator.py +++ b/litellm/llms/fireworks_ai/cost_calculator.py @@ -2,7 +2,7 @@ For calculating cost of fireworks ai serverless inference models. """ -from typing import Tuple +from typing import Optional, Tuple from litellm.constants import ( FIREWORKS_AI_4_B, @@ -75,8 +75,17 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: model_info = get_model_info(model=base_model, custom_llm_provider="fireworks_ai") ## CALCULATE INPUT COST + input_cost_per_token: float = model_info["input_cost_per_token"] + cache_read_cost: Optional[float] = model_info.get("cache_read_input_token_cost") - prompt_cost: float = usage["prompt_tokens"] * model_info["input_cost_per_token"] + cached_tokens: int = 0 + if usage.prompt_tokens_details: + cached_tokens = usage.prompt_tokens_details.cached_tokens or 0 + + non_cached_tokens: int = max(usage["prompt_tokens"] - cached_tokens, 0) + prompt_cost: float = non_cached_tokens * input_cost_per_token + cached_tokens * ( + cache_read_cost if cache_read_cost is not None else input_cost_per_token + ) ## CALCULATE OUTPUT COST completion_cost = usage["completion_tokens"] * model_info["output_cost_per_token"] diff --git a/tests/local_testing/test_completion_cost.py b/tests/local_testing/test_completion_cost.py index cf0c645615d..0ac155d6de1 100644 --- a/tests/local_testing/test_completion_cost.py +++ b/tests/local_testing/test_completion_cost.py @@ -1225,6 +1225,67 @@ def test_completion_cost_fireworks_ai(model): assert cost > 0 +def test_fireworks_ai_cost_per_token_honors_cached_tokens(): + """ + Regression for https://github.com/BerriAI/litellm/issues/32496 + + The fireworks_ai cost calculator billed 100% of prompt tokens at the full + input rate, ignoring usage.prompt_tokens_details.cached_tokens and the + model's cache_read_input_token_cost. Cached tokens must be billed at the + cache-read rate. + """ + from litellm.llms.fireworks_ai.cost_calculator import cost_per_token + from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + model = "accounts/fireworks/models/glm-5p2" + input_cost = litellm.model_cost["fireworks_ai/" + model]["input_cost_per_token"] + cache_read_cost = litellm.model_cost["fireworks_ai/" + model]["cache_read_input_token_cost"] + assert cache_read_cost < input_cost + + usage = Usage( + prompt_tokens=8174, + completion_tokens=200, + total_tokens=8374, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=8165), + ) + + prompt_cost, _ = cost_per_token(model=model, usage=usage) + + expected = (8174 - 8165) * input_cost + 8165 * cache_read_cost + assert prompt_cost == pytest.approx(expected) + # must be far below the old "everything at full input rate" behavior + assert prompt_cost < usage.prompt_tokens * input_cost + + +def test_fireworks_ai_cost_per_token_no_cache_read_price_falls_back(): + """ + When a fireworks model has no cache_read_input_token_cost, cached tokens + fall back to the full input rate so behavior is unchanged for those models. + """ + from litellm.llms.fireworks_ai.cost_calculator import cost_per_token + from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + model = "accounts/fireworks/models/deepseek-v3p1" + assert litellm.model_cost["fireworks_ai/" + model].get("cache_read_input_token_cost") is None + input_cost = litellm.model_cost["fireworks_ai/" + model]["input_cost_per_token"] + + usage = Usage( + prompt_tokens=1000, + completion_tokens=50, + total_tokens=1050, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=800), + ) + + prompt_cost, _ = cost_per_token(model=model, usage=usage) + assert prompt_cost == pytest.approx(1000 * input_cost) + + def test_cost_azure_openai_prompt_caching(): from litellm.utils import Choices, Message, ModelResponse, Usage from litellm.types.utils import (