fix(proxy): bill a failure after a logged stream success at zero tokens
Some checks failed
Cost map pins recheck / rerun-cost-map-guard (push) Has been cancelled
LiteLLM Rust DB / db-queries (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

When a post-call hook raises after a stream's success log went out, the
failure row lifted no usage and fell back to the success's token counts, so
the daily usage tables counted the stream's tokens twice. Lift a zero usage
and zero cost while the stream success bills the current attempt.
This commit is contained in:
mateo-berri 2026-11-06 23:25:05 -08:00
parent 77f1f81c64
commit 8099eef246
3 changed files with 20 additions and 5 deletions

View file

@ -1819,7 +1819,7 @@ def _failure_fields_to_lift(
)
_billed: Final = provider_billed and not (_success_already_logged or _stream_success_bills_this_attempt)
_usage_to_lift: Final = (
None
(Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), 0.0)
if _stream_success_bills_this_attempt
else _failure_usage_to_lift(
model_call_details=_model_call_details,

View file

@ -28,6 +28,7 @@ from litellm.llms.anthropic.messages.native.streaming_iterator import (
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload
from litellm.proxy.utils import ProxyLogging, _failure_fields_to_lift
from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.utils import Usage
@ -782,9 +783,11 @@ async def test_chat_stream_callback_error_after_its_success_dispatch_leaves_the_
pass
await asyncio.sleep(0)
lifted: Final = _failure_fields_to_lift(request_data, provider_billed=True)
failure_row: Final = get_logging_payload(
dict(_failure_fields_to_lift(request_data, provider_billed=True)), None, datetime.now(), datetime.now()
)
assert events == ["success_dispatched"]
assert (lifted.get("combined_usage_object"), lifted.get("response_cost")) == (None, None)
assert (failure_row["prompt_tokens"], failure_row["completion_tokens"], failure_row["spend"]) == (0, 0, 0.0)
@pytest.mark.asyncio

View file

@ -2868,6 +2868,7 @@ async def test_continued_stream_sharing_the_proxy_logging_object_bills_the_cut_t
async def test_a_failure_after_a_continued_stream_success_bills_nothing_the_success_row_already_billed(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter
) -> None:
from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload
from litellm.proxy.utils import _failure_fields_to_lift
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
@ -2880,7 +2881,12 @@ async def test_a_failure_after_a_continued_stream_success_bills_nothing_the_succ
(success,) = recorder.successes
assert success["cost_breakdown"]["additional_costs"]["mid_stream_partial_cost"] > 0
late_failure: Final = _failure_fields_to_lift({"litellm_logging_obj": shared_logging}, provider_billed=True)
assert (late_failure.get("combined_usage_object"), late_failure.get("response_cost")) == (None, None)
late_failure_row: Final = get_logging_payload(dict(late_failure), None, datetime.now(), datetime.now())
assert (late_failure_row["prompt_tokens"], late_failure_row["completion_tokens"], late_failure_row["spend"]) == (
0,
0,
0.0,
)
_AGENTIC_FIRST_CALL_USAGE: Final = {"prompt_tokens": 50, "completion_tokens": 7, "total_tokens": 57}
@ -3286,6 +3292,7 @@ def _backup_responses_cost() -> float:
async def test_responses_fallback_sharing_the_proxy_logging_object_bills_the_cut_partial_once(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, failed_usage: Mapping[str, int] | None
) -> None:
from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload
from litellm.proxy.utils import _failure_fields_to_lift
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
@ -3318,7 +3325,12 @@ async def test_responses_fallback_sharing_the_proxy_logging_object_bills_the_cut
success["response_cost"],
)
late_failure: Final = _failure_fields_to_lift({"litellm_logging_obj": shared_logging}, provider_billed=True)
assert (late_failure.get("combined_usage_object"), late_failure.get("response_cost")) == (None, None)
late_failure_row: Final = get_logging_payload(dict(late_failure), None, datetime.now(), datetime.now())
assert (late_failure_row["prompt_tokens"], late_failure_row["completion_tokens"], late_failure_row["spend"]) == (
0,
0,
0.0,
)
@pytest.mark.asyncio