fix(accounting): isolate a spend log row the insert statement rejects

The raw SpendLogs insert validates columns and JSON before Postgres sees the
row, so a row that fails there is now bisected out and dropped alone while its
charge is kept, instead of failing and replaying the whole flush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Joshua Valluru 2026-11-04 16:01:37 -08:00
parent d7815814eb
commit da1f285196
2 changed files with 38 additions and 2 deletions

View file

@ -9174,7 +9174,7 @@ async def _keep_charges_of_dropped_logs(repo: SpendLogsRepository, rows: Sequenc
try:
await repo.insert_with_deliveries(charged)
except Exception as e: # noqa: BLE001 # the request log is already lost; keep the flush going for other rows
if not (PrismaDBExceptionHandler.is_rejected_row_error(e) or isinstance(e, UnicodeEncodeError)):
if not (PrismaDBExceptionHandler.is_rejected_row_error(e) or isinstance(e, (UnicodeEncodeError, ValueError))):
raise
spend_log_error(
"Spend tracking - could not keep the charges of %d dropped spend log rows. error=%s",
@ -9215,7 +9215,7 @@ async def _create_spend_logs_with_poison_isolation(
await repo.insert_with_deliveries(rows)
return failure_budget
except Exception as e:
if not (PrismaDBExceptionHandler.is_rejected_row_error(e) or isinstance(e, UnicodeEncodeError)):
if not (PrismaDBExceptionHandler.is_rejected_row_error(e) or isinstance(e, (UnicodeEncodeError, ValueError))):
raise
if PrismaDBExceptionHandler.is_database_service_unavailable_error(e):
raise

View file

@ -437,3 +437,39 @@ async def test_a_rejected_charge_rescue_is_logged_and_the_flush_continues():
rows = [{"request_id": "rejected", "accounting_payload": {"spend": 1.0}}]
assert await _create_spend_logs_with_poison_isolation(repo, rows, failure_budget=10) == 9
class _StatementValidatingRepository:
def __init__(self):
self.written = []
self.charges_kept = []
async def insert_with_deliveries(self, rows):
from litellm.repositories.spend_log_insert import spend_log_insert_statement
if all(row.get("accounting_log_disabled") for row in rows):
self.charges_kept.extend(row["request_id"] for row in rows)
return len(rows)
spend_log_insert_statement(rows)
self.written.extend(row["request_id"] for row in rows)
return len(rows)
@pytest.mark.asyncio
async def test_a_row_the_insert_statement_rejects_is_dropped_alone_and_keeps_its_charge():
from litellm.proxy.utils import _create_spend_logs_with_poison_isolation
repo = _StatementValidatingRepository()
rows = [
{"request_id": "good", "call_type": "acompletion", "messages": "[]"},
{
"request_id": "unparseable",
"call_type": "acompletion",
"messages": "not json",
"accounting_payload": {"spend": 1.0},
},
]
assert await _create_spend_logs_with_poison_isolation(repo, rows, failure_budget=10) < 10
assert repo.written == ["good"]
assert repo.charges_kept == ["unparseable"]