mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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.
This commit is contained in:
parent
b9bff0998c
commit
face423e26
2 changed files with 73 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue