From c2afb07f93b47e982ae57724bbf94100c1aefd4e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 07:02:00 +0000 Subject: [PATCH] fix(spend): keep free response reads and cancels off the creation spend row (#51110) * fix(spend): track self-heal for test_with_id_security_off_reads_and_cancels_add_no_rows_and_no_replayed_turns Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(spend): preserve response turn rows when id security is disabled Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/db/db_spend_update_writer.py | 7 +++ .../test_responses_stream_spend_row_id.py | 6 +++ .../proxy/db/test_db_spend_update_writer.py | 51 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index fd47b2fbb6b..12267120965 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -771,6 +771,13 @@ class DBSpendUpdateWriter: llm_router=llm_router, ) billable_cost: Final = _billable_cost(response_cost, payload) + if ( + proxy_settings.get("disable_responses_id_security") is True + and payload["call_type"] in {"aget_responses", "acancel_responses"} + and payload["status"] == "success" + and not billable_cost + ): + return True payload["spend"] = billable_cost or 0.0 payload["startTime"] = _iso_timestamp(payload["startTime"]) payload["endTime"] = _iso_timestamp(payload["endTime"]) diff --git a/tests/integration/spend/test_responses_stream_spend_row_id.py b/tests/integration/spend/test_responses_stream_spend_row_id.py index 58ed9f7785f..c13ef59fee3 100644 --- a/tests/integration/spend/test_responses_stream_spend_row_id.py +++ b/tests/integration/spend/test_responses_stream_spend_row_id.py @@ -448,6 +448,12 @@ def test_with_id_security_off_reads_and_cancels_add_no_rows_and_no_replayed_turn assert [string_value(JSON_OBJECT.validate_json(read.content)["id"]) for read in reads] == [first_id] * 3 later_id: Final = _created_response_id(insecure, key, {"model": native, "input": "a later request"}) assert _request_ids_for_key(insecure, key, 2) == [first_id, later_id] + (first_turn,) = insecure.rows( + 'SELECT request_id, proxy_server_request, messages, call_type FROM "LiteLLM_SpendLogs" WHERE request_id=%s', + (first_id,), + ) + assert first_turn["call_type"] == "aresponses", first_turn + assert object_value(first_turn["proxy_server_request"])["input"] == first_prompt, first_turn wire.drain() _terminal_response_id( diff --git a/tests/unit/proxy/db/test_db_spend_update_writer.py b/tests/unit/proxy/db/test_db_spend_update_writer.py index 8bf7cd36a8c..36aeacd8476 100644 --- a/tests/unit/proxy/db/test_db_spend_update_writer.py +++ b/tests/unit/proxy/db/test_db_spend_update_writer.py @@ -5890,6 +5890,57 @@ async def test_request_error_counts_survive_the_ledger_path_switch(monkeypatch: ): 1} +@pytest.mark.asyncio +@pytest.mark.parametrize("call_type", ["aresponses", "aget_responses", "acancel_responses"]) +@pytest.mark.parametrize( + ("security_disabled", "status", "cost", "keep_read"), + [ + (False, "success", 0.0, True), + (True, "success", 0.0, False), + (True, "success", None, False), + (True, "success", 0.23, True), + (True, "failure", 0.0, True), + ], +) +async def test_unsecured_free_response_reads_leave_the_creation_spend_row_alone( + monkeypatch: pytest.MonkeyPatch, + call_type: str, + security_disabled: bool, + status: str, + cost: float | None, + keep_read: bool, +) -> None: + from litellm.proxy import proxy_server + + writer: Final = DBSpendUpdateWriter() + prisma: Final = _tool_usage_prisma() + prisma.get_request_status.return_value = status + monkeypatch.setattr(proxy_server, "prisma_client", prisma) + monkeypatch.setattr(proxy_server, "disable_spend_logs", False) + monkeypatch.setitem(proxy_server.general_settings, "disable_responses_id_security", security_disabled) + now: Final = datetime(2026, 11, 11, tzinfo=timezone.utc) + assert await writer.update_database( + token="test-token", + user_id="test-user", + end_user_id=None, + team_id=None, + org_id=None, + kwargs={ + "model": "response-model", + "call_type": call_type, + "litellm_call_id": "read-call", + "litellm_params": {"metadata": {"user_api_key": "test-token", "status": status}}, + }, + completion_response={"id": "resp-shared"}, + start_time=now, + end_time=now, + response_cost=cost, + ) + await drain_logging_tasks() + expected: Final = [call_type] if call_type == "aresponses" or keep_read else [] + assert [row["call_type"] for row in prisma.spend_log_transactions] == expected + + class _SessionTotalsRedis: def __init__(self) -> None: self.values: dict[str, float] = {} # mutable-ok: stands in for Redis state