From face423e2690e3ce289d395ed1da892b17b7f1b7 Mon Sep 17 00:00:00 2001 From: nazsats Date: Sat, 22 Aug 2026 20:24:56 +0530 Subject: [PATCH 1/2] fix(cost): clamp negative completion tokens so cost cannot go negative generic_cost_per_token guards three of its four token paths with max(0, ...). The fourth - completion tokens with no modality breakdown, which is the common case - multiplied the raw value straight into the cost: else: # No breakdown at all, all tokens are text tokens text_tokens = usage.completion_tokens So a negative completion count produced a negative cost, while the same input on the prompt side was clamped: cost_per_token(model="gpt-4o", prompt_tokens=-100, completion_tokens=0) # (0.0, 0.0) cost_per_token(model="gpt-4o", prompt_tokens=0, completion_tokens=-100) # (0.0, -0.001) cost_per_token(model="gpt-4o", prompt_tokens=100, completion_tokens=-100) # (0.00025, -0.001) -> negative total These numbers feed spend tracking, where a negative cost does not merely report wrongly - it credits the budget. No provider is known to return a negative token count, and every internal path that subtracts already clamps, so this is hardening rather than a bug anyone is hitting today. It makes the fourth path agree with the other three. Four tests: the two failing cases, the already-clamped branch as a regression guard, and ordinary positive pricing to show the clamp changes nothing else. Without the patch the first two fail. --- .../litellm_core_utils/llm_cost_calc/utils.py | 8 ++- .../llm_cost_calc/test_llm_cost_calc_utils.py | 67 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 0a52e1d283e..cf78ffbeaae 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -947,8 +947,12 @@ def generic_cost_per_token( usage.completion_tokens - reasoning_tokens - audio_tokens - image_tokens - video_tokens, ) else: - # No breakdown at all, all tokens are text tokens - text_tokens = usage.completion_tokens + # No breakdown at all, all tokens are text tokens. + # Clamped for the same reason the branch above is: a negative count + # would otherwise multiply straight into a negative cost, and this + # is the only one of the four token paths in this module that did + # not guard against it. + text_tokens = max(0, usage.completion_tokens) is_text_tokens_total = True ## TEXT COST completion_cost = float(text_tokens) * completion_base_cost diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index c8c36032793..680769c9daf 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -3517,3 +3517,70 @@ def test_generic_cost_per_token_grok_46_long_context(_local_model_cost_map): ) assert prompt_cost == pytest.approx(200_000 * 4e-06 + 50_000 * 1e-06) assert completion_cost == pytest.approx(1_000 * 1.2e-05) + + +def test_negative_completion_tokens_do_not_produce_negative_cost(): + """A negative completion count must not yield a negative cost. + + generic_cost_per_token guards three of its four token paths with max(0, ...). + The fourth - completion tokens with no modality breakdown, which is the + common case - multiplied the raw value straight into the cost, so a negative + count produced a negative cost and a negative total. + + That matters because these numbers feed spend tracking: a negative cost does + not merely report wrongly, it credits the budget. + """ + usage = Usage(prompt_tokens=0, completion_tokens=-100, total_tokens=-100) + + prompt_cost, completion_cost = generic_cost_per_token( + model="gpt-4o", usage=usage, custom_llm_provider="openai" + ) + + assert completion_cost == 0.0 + assert prompt_cost + completion_cost >= 0.0 + + +def test_negative_completion_tokens_with_positive_prompt_keeps_total_positive(): + """The mixed case: a real prompt with a bad completion count. + + Before the fix this returned a negative total, so the request reduced + recorded spend instead of increasing it. + """ + usage = Usage(prompt_tokens=100, completion_tokens=-100, total_tokens=0) + + prompt_cost, completion_cost = generic_cost_per_token( + model="gpt-4o", usage=usage, custom_llm_provider="openai" + ) + + assert prompt_cost > 0.0 + assert completion_cost == 0.0 + assert prompt_cost + completion_cost > 0.0 + + +def test_negative_completion_tokens_with_breakdown_already_clamped(): + """The branch that already clamped must keep behaving as it did.""" + usage = Usage( + prompt_tokens=0, + completion_tokens=-100, + total_tokens=-100, + completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=5), + ) + + _, completion_cost = generic_cost_per_token( + model="gpt-4o", usage=usage, custom_llm_provider="openai" + ) + + assert completion_cost >= 0.0 + + +def test_positive_token_costs_are_unchanged(): + """The clamp must not alter ordinary pricing.""" + usage = Usage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500) + + prompt_cost, completion_cost = generic_cost_per_token( + model="gpt-4o", usage=usage, custom_llm_provider="openai" + ) + + model_info = litellm.get_model_info("gpt-4o") + assert prompt_cost == pytest.approx(1000 * model_info["input_cost_per_token"]) + assert completion_cost == pytest.approx(500 * model_info["output_cost_per_token"]) From e0de9cbab272edb0c087b280295810225836bb84 Mon Sep 17 00:00:00 2001 From: nazsats Date: Mon, 24 Aug 2026 13:41:20 +0530 Subject: [PATCH 2/2] Trim the clamp comment down to the reason for it --- litellm/litellm_core_utils/llm_cost_calc/utils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index cf78ffbeaae..3f86afb10eb 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -947,11 +947,8 @@ def generic_cost_per_token( usage.completion_tokens - reasoning_tokens - audio_tokens - image_tokens - video_tokens, ) else: - # No breakdown at all, all tokens are text tokens. - # Clamped for the same reason the branch above is: a negative count - # would otherwise multiply straight into a negative cost, and this - # is the only one of the four token paths in this module that did - # not guard against it. + # No breakdown at all, all tokens are text tokens. Clamped like + # the branch above, so a negative count cannot credit the budget. text_tokens = max(0, usage.completion_tokens) is_text_tokens_total = True ## TEXT COST