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>
This commit is contained in:
devin-ai-integration[bot] 2026-11-11 07:02:00 +00:00 • committed by GitHub
parent 606490a52d
commit c2afb07f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 64 additions and 0 deletions

View file

@ -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"])

View file

@ -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(

View file

@ -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