From 73a2dc929358f013e121a023af1878bc225baeb8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:57:49 -0700 Subject: [PATCH] fix(interactions): fail a cross-replica delete when its pre-delete fetch fails so the creating poll keeps the bill --- litellm/interactions/background_cost_polling.py | 11 ++++++----- .../interactions/test_background_cost_polling.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/litellm/interactions/background_cost_polling.py b/litellm/interactions/background_cost_polling.py index ce0774d6b8c..a28e66d52d8 100644 --- a/litellm/interactions/background_cost_polling.py +++ b/litellm/interactions/background_cost_polling.py @@ -593,14 +593,15 @@ async def maybe_settle_background_interaction_before_delete( api_base=api_base if isinstance(api_base, str) else None, store=settlement_store, ) - response: Final = await _fetch_before_delete(context, fetch_interaction) - if response is None: + try: + response: Final = await fetch_interaction(context) + except Exception: verbose_logger.debug( - "Leaving background interaction %s to the poll that created it: this process could not fetch it with " - "the delete's credentials, so the delete is about to fail the same way", + "Failing the delete of background interaction %s: this process could not fetch it with the delete's " + "credentials, so the poll that created it keeps the bill", interaction_id, ) - return None + raise return await _settle_before_delete(context, response) diff --git a/tests/test_litellm/interactions/test_background_cost_polling.py b/tests/test_litellm/interactions/test_background_cost_polling.py index df80acfd7e6..22c697e04bc 100644 --- a/tests/test_litellm/interactions/test_background_cost_polling.py +++ b/tests/test_litellm/interactions/test_background_cost_polling.py @@ -696,23 +696,24 @@ async def test_delete_on_another_replica_releases_the_create_reservation(): @pytest.mark.asyncio -async def test_delete_on_another_replica_leaves_an_unfetchable_create_to_its_own_poll(): +async def test_delete_on_another_replica_fails_when_it_cannot_fetch_and_leaves_the_bill_to_the_creating_poll(): """ The settling replica fetches with the delete's credentials, never the create's, so a fetch it cannot make (a key only the deployment carries) - says nothing about the interaction: the delete is about to fail the same - way, and the poll on the creating replica still owns the bill. + says nothing about the interaction. Deleting anyway would strand the bill + behind a deleted interaction, so the delete fails with the fetch's error + and the poll on the creating replica still owns the bill. """ store = InMemoryBackgroundSettlementStore() logging_obj = _logging_obj(litellm_params={"metadata": _create_metadata()}) await _create_on_a_replica_that_then_dies(logging_obj, store) fetch, _ = _fetch_sequence(RuntimeError("Google API key is required")) - outcome = await maybe_settle_background_interaction_before_delete( - interaction_id="interactions/bg-abc", delete_kwargs={}, fetch_interaction=fetch, store=store - ) + with pytest.raises(RuntimeError, match="Google API key is required"): + await maybe_settle_background_interaction_before_delete( + interaction_id="interactions/bg-abc", delete_kwargs={}, fetch_interaction=fetch, store=store + ) - assert outcome is None assert await store.is_claimed("interactions/bg-abc") is False poll_fetch, _ = _fetch_sequence(_response("completed", with_usage=True)) await asyncio.wait_for(_register_poll(logging_obj, poll_fetch=poll_fetch, store=store), timeout=5)