mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
refactor(proxy)!: drop the unused accounting event tables (#49923)
LiteLLM_AccountingEvent and LiteLLM_AccountingDelivery have had no reader since the SpendLogs projection replaced them, so the migration drops them with their enums. AccountingDeliveryState stays because LiteLLM_SpendLogDelivery uses it The schema now spells the delivery_id default the way Postgres stores it, which ends the drift Prisma reported against the ledger migration. The schema check compares base types only, so an attribute-only change is no longer reported as a type change Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a392b9e5f
commit
49f56e8b23
8 changed files with 100 additions and 204 deletions
|
|
@ -0,0 +1,5 @@
|
|||
SET LOCAL lock_timeout = '10s';
|
||||
DROP TABLE IF EXISTS "LiteLLM_AccountingDelivery";
|
||||
DROP TABLE IF EXISTS "LiteLLM_AccountingEvent";
|
||||
DROP TYPE IF EXISTS "AccountingDeliveryTarget";
|
||||
DROP TYPE IF EXISTS "AccountingEventKind";
|
||||
|
|
@ -2071,78 +2071,14 @@ model LiteLLM_TelemetryReport {
|
|||
@@index([created_at])
|
||||
}
|
||||
|
||||
enum AccountingEventKind {
|
||||
inbound_usage
|
||||
outbound_usage
|
||||
cost
|
||||
}
|
||||
|
||||
enum AccountingDeliveryState {
|
||||
pending
|
||||
applied
|
||||
parked
|
||||
}
|
||||
|
||||
enum AccountingDeliveryTarget {
|
||||
LiteLLM_VerificationToken
|
||||
LiteLLM_UserTable
|
||||
LiteLLM_TeamTable
|
||||
LiteLLM_TeamMembership
|
||||
LiteLLM_OrganizationTable
|
||||
LiteLLM_OrganizationMembership
|
||||
LiteLLM_EndUserTable
|
||||
LiteLLM_AgentsTable
|
||||
LiteLLM_TagTable
|
||||
LiteLLM_ProjectTable
|
||||
LiteLLM_ModelAccessGroupBudgetTable
|
||||
LiteLLM_BudgetTable
|
||||
LiteLLM_BudgetWindowSpend
|
||||
LiteLLM_DailyUserSpend
|
||||
LiteLLM_DailyTeamSpend
|
||||
LiteLLM_DailyOrganizationSpend
|
||||
LiteLLM_DailyEndUserSpend
|
||||
LiteLLM_DailyAgentSpend
|
||||
LiteLLM_DailyTagSpend
|
||||
LiteLLM_DailyModelUsage
|
||||
LiteLLM_DailyToolSpend
|
||||
LiteLLM_AutoRouterDailySpend
|
||||
}
|
||||
|
||||
// Durable accounting ledger. One row per usage or cost event; payload holds the
|
||||
// private event envelope versioned by schema_version. Usage rows retain request-scoped
|
||||
// pricing context; cost rows include resolved rates and the finalized pricing snapshot.
|
||||
model LiteLLM_AccountingEvent {
|
||||
event_id String @id
|
||||
kind AccountingEventKind
|
||||
source_event_id String?
|
||||
schema_version Int
|
||||
payload_hash String
|
||||
occurred_at DateTime
|
||||
recorded_at DateTime @default(now())
|
||||
payload Json
|
||||
|
||||
@@index([recorded_at])
|
||||
@@index([kind, source_event_id])
|
||||
}
|
||||
|
||||
// One row per (event, target table, version): pending work for the projector and,
|
||||
// once applied in the same transaction as the target write, the receipt that stops a retry.
|
||||
model LiteLLM_AccountingDelivery {
|
||||
event_id String
|
||||
target AccountingDeliveryTarget
|
||||
version Int
|
||||
state AccountingDeliveryState @default(pending)
|
||||
attempts Int @default(0)
|
||||
next_attempt_at DateTime @default(now())
|
||||
applied_at DateTime?
|
||||
last_error String?
|
||||
|
||||
@@id([event_id, target, version])
|
||||
@@index([target, state, next_attempt_at])
|
||||
}
|
||||
|
||||
model LiteLLM_SpendLogDelivery {
|
||||
delivery_id String @id @default(dbgenerated("gen_random_uuid()::text"))
|
||||
delivery_id String @id @default(dbgenerated("(gen_random_uuid())::text"))
|
||||
request_id String
|
||||
state AccountingDeliveryState @default(pending)
|
||||
accounting_payload Json
|
||||
|
|
|
|||
|
|
@ -338,8 +338,6 @@ _PRISMA_MODELS: Final[frozenset[str]] = frozenset(
|
|||
"LiteLLM_ToolTable",
|
||||
"LiteLLM_TelemetryReport",
|
||||
"LiteLLM_AccessGroupTable",
|
||||
"LiteLLM_AccountingDelivery",
|
||||
"LiteLLM_AccountingEvent",
|
||||
"LiteLLM_SpendLogDelivery",
|
||||
"LiteLLM_ClaudeCodePluginTable",
|
||||
"LiteLLM_ClaudeCodePluginUpload",
|
||||
|
|
|
|||
|
|
@ -2071,78 +2071,14 @@ model LiteLLM_TelemetryReport {
|
|||
@@index([created_at])
|
||||
}
|
||||
|
||||
enum AccountingEventKind {
|
||||
inbound_usage
|
||||
outbound_usage
|
||||
cost
|
||||
}
|
||||
|
||||
enum AccountingDeliveryState {
|
||||
pending
|
||||
applied
|
||||
parked
|
||||
}
|
||||
|
||||
enum AccountingDeliveryTarget {
|
||||
LiteLLM_VerificationToken
|
||||
LiteLLM_UserTable
|
||||
LiteLLM_TeamTable
|
||||
LiteLLM_TeamMembership
|
||||
LiteLLM_OrganizationTable
|
||||
LiteLLM_OrganizationMembership
|
||||
LiteLLM_EndUserTable
|
||||
LiteLLM_AgentsTable
|
||||
LiteLLM_TagTable
|
||||
LiteLLM_ProjectTable
|
||||
LiteLLM_ModelAccessGroupBudgetTable
|
||||
LiteLLM_BudgetTable
|
||||
LiteLLM_BudgetWindowSpend
|
||||
LiteLLM_DailyUserSpend
|
||||
LiteLLM_DailyTeamSpend
|
||||
LiteLLM_DailyOrganizationSpend
|
||||
LiteLLM_DailyEndUserSpend
|
||||
LiteLLM_DailyAgentSpend
|
||||
LiteLLM_DailyTagSpend
|
||||
LiteLLM_DailyModelUsage
|
||||
LiteLLM_DailyToolSpend
|
||||
LiteLLM_AutoRouterDailySpend
|
||||
}
|
||||
|
||||
// Durable accounting ledger. One row per usage or cost event; payload holds the
|
||||
// private event envelope versioned by schema_version. Usage rows retain request-scoped
|
||||
// pricing context; cost rows include resolved rates and the finalized pricing snapshot.
|
||||
model LiteLLM_AccountingEvent {
|
||||
event_id String @id
|
||||
kind AccountingEventKind
|
||||
source_event_id String?
|
||||
schema_version Int
|
||||
payload_hash String
|
||||
occurred_at DateTime
|
||||
recorded_at DateTime @default(now())
|
||||
payload Json
|
||||
|
||||
@@index([recorded_at])
|
||||
@@index([kind, source_event_id])
|
||||
}
|
||||
|
||||
// One row per (event, target table, version): pending work for the projector and,
|
||||
// once applied in the same transaction as the target write, the receipt that stops a retry.
|
||||
model LiteLLM_AccountingDelivery {
|
||||
event_id String
|
||||
target AccountingDeliveryTarget
|
||||
version Int
|
||||
state AccountingDeliveryState @default(pending)
|
||||
attempts Int @default(0)
|
||||
next_attempt_at DateTime @default(now())
|
||||
applied_at DateTime?
|
||||
last_error String?
|
||||
|
||||
@@id([event_id, target, version])
|
||||
@@index([target, state, next_attempt_at])
|
||||
}
|
||||
|
||||
model LiteLLM_SpendLogDelivery {
|
||||
delivery_id String @id @default(dbgenerated("gen_random_uuid()::text"))
|
||||
delivery_id String @id @default(dbgenerated("(gen_random_uuid())::text"))
|
||||
request_id String
|
||||
state AccountingDeliveryState @default(pending)
|
||||
accounting_payload Json
|
||||
|
|
|
|||
|
|
@ -2071,78 +2071,14 @@ model LiteLLM_TelemetryReport {
|
|||
@@index([created_at])
|
||||
}
|
||||
|
||||
enum AccountingEventKind {
|
||||
inbound_usage
|
||||
outbound_usage
|
||||
cost
|
||||
}
|
||||
|
||||
enum AccountingDeliveryState {
|
||||
pending
|
||||
applied
|
||||
parked
|
||||
}
|
||||
|
||||
enum AccountingDeliveryTarget {
|
||||
LiteLLM_VerificationToken
|
||||
LiteLLM_UserTable
|
||||
LiteLLM_TeamTable
|
||||
LiteLLM_TeamMembership
|
||||
LiteLLM_OrganizationTable
|
||||
LiteLLM_OrganizationMembership
|
||||
LiteLLM_EndUserTable
|
||||
LiteLLM_AgentsTable
|
||||
LiteLLM_TagTable
|
||||
LiteLLM_ProjectTable
|
||||
LiteLLM_ModelAccessGroupBudgetTable
|
||||
LiteLLM_BudgetTable
|
||||
LiteLLM_BudgetWindowSpend
|
||||
LiteLLM_DailyUserSpend
|
||||
LiteLLM_DailyTeamSpend
|
||||
LiteLLM_DailyOrganizationSpend
|
||||
LiteLLM_DailyEndUserSpend
|
||||
LiteLLM_DailyAgentSpend
|
||||
LiteLLM_DailyTagSpend
|
||||
LiteLLM_DailyModelUsage
|
||||
LiteLLM_DailyToolSpend
|
||||
LiteLLM_AutoRouterDailySpend
|
||||
}
|
||||
|
||||
// Durable accounting ledger. One row per usage or cost event; payload holds the
|
||||
// private event envelope versioned by schema_version. Usage rows retain request-scoped
|
||||
// pricing context; cost rows include resolved rates and the finalized pricing snapshot.
|
||||
model LiteLLM_AccountingEvent {
|
||||
event_id String @id
|
||||
kind AccountingEventKind
|
||||
source_event_id String?
|
||||
schema_version Int
|
||||
payload_hash String
|
||||
occurred_at DateTime
|
||||
recorded_at DateTime @default(now())
|
||||
payload Json
|
||||
|
||||
@@index([recorded_at])
|
||||
@@index([kind, source_event_id])
|
||||
}
|
||||
|
||||
// One row per (event, target table, version): pending work for the projector and,
|
||||
// once applied in the same transaction as the target write, the receipt that stops a retry.
|
||||
model LiteLLM_AccountingDelivery {
|
||||
event_id String
|
||||
target AccountingDeliveryTarget
|
||||
version Int
|
||||
state AccountingDeliveryState @default(pending)
|
||||
attempts Int @default(0)
|
||||
next_attempt_at DateTime @default(now())
|
||||
applied_at DateTime?
|
||||
last_error String?
|
||||
|
||||
@@id([event_id, target, version])
|
||||
@@index([target, state, next_attempt_at])
|
||||
}
|
||||
|
||||
model LiteLLM_SpendLogDelivery {
|
||||
delivery_id String @id @default(dbgenerated("gen_random_uuid()::text"))
|
||||
delivery_id String @id @default(dbgenerated("(gen_random_uuid())::text"))
|
||||
request_id String
|
||||
state AccountingDeliveryState @default(pending)
|
||||
accounting_payload Json
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from pydantic import TypeAdapter
|
|||
|
||||
Schema = Mapping[str, Mapping[str, str]]
|
||||
MODELS: Final = re.compile(r"^[ \t]*model\s+(\w+)\s*\{(.*?)^[ \t]*\}", re.MULTILINE | re.DOTALL)
|
||||
DROPPED_UNUSED_MODELS: Final = frozenset({"LiteLLM_AccountingDelivery", "LiteLLM_AccountingEvent"})
|
||||
|
||||
|
||||
def parse_model_fields(schema: str) -> Schema:
|
||||
|
|
@ -27,18 +28,18 @@ def _field_changes(model: str, field: str, old_type: str, new_fields: Mapping[st
|
|||
if field not in new_fields:
|
||||
yield f"Breaking: Field {model}.{field} was removed"
|
||||
return
|
||||
new_type: Final = new_fields[field]
|
||||
if old_type == new_type:
|
||||
return
|
||||
if "?" in old_type and "?" not in new_type:
|
||||
old_base: Final = old_type.split()[0]
|
||||
new_base: Final = new_fields[field].split()[0]
|
||||
if old_base.endswith("?") and not new_base.endswith("?"):
|
||||
yield f"Breaking: Field {model}.{field} changed from optional to required"
|
||||
if not old_type.startswith(new_type.split("?")[0]):
|
||||
yield f"Breaking: Field {model}.{field} changed type from {old_type} to {new_type}"
|
||||
if old_base.rstrip("?") != new_base.rstrip("?"):
|
||||
yield f"Breaking: Field {model}.{field} changed type from {old_base} to {new_base}"
|
||||
|
||||
|
||||
def _model_changes(model: str, old_fields: Mapping[str, str], new_schema: Schema) -> Iterator[str]:
|
||||
if model not in new_schema:
|
||||
yield f"Breaking: Model {model} was removed"
|
||||
if model not in DROPPED_UNUSED_MODELS:
|
||||
yield f"Breaking: Model {model} was removed"
|
||||
return
|
||||
for field, old_type in old_fields.items():
|
||||
yield from _field_changes(model, field, old_type, new_schema[model])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
_MIGRATIONS: Final = Path(__file__).parents[4] / "litellm-proxy-extras/litellm_proxy_extras/migrations"
|
||||
_OLD_LEDGER: Final = _MIGRATIONS / "20261027000000_add_accounting_ledger_tables/migration.sql"
|
||||
_SPEND_LOG_DELIVERY: Final = _MIGRATIONS / "20261103010000_spendlogs_accounting_delivery/migration.sql"
|
||||
_DROP: Final = _MIGRATIONS / "20261104010000_drop_accounting_event_tables/migration.sql"
|
||||
_DROPPED: Final = (
|
||||
"AccountingDeliveryTarget",
|
||||
"AccountingEventKind",
|
||||
"LiteLLM_AccountingDelivery",
|
||||
"LiteLLM_AccountingEvent",
|
||||
)
|
||||
|
||||
|
||||
def _apply(connection: psycopg.Connection[tuple[object, ...]], migration: Path) -> None:
|
||||
with connection.transaction():
|
||||
connection.execute(migration.read_bytes())
|
||||
|
||||
|
||||
def _present(connection: psycopg.Connection[tuple[object, ...]], names: tuple[str, ...]) -> tuple[object, ...]:
|
||||
return tuple(
|
||||
row[0]
|
||||
for row in connection.execute(
|
||||
"SELECT relname FROM pg_class WHERE relname = ANY(%s) "
|
||||
"UNION SELECT typname FROM pg_type WHERE typname = ANY(%s) ORDER BY 1",
|
||||
(list(names), list(names)),
|
||||
).fetchall()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("old_states", [(), ("applied", "pending", "parked")], ids=["empty", "with_rows"])
|
||||
def test_drop_removes_the_old_ledger_and_keeps_spend_log_deliveries(old_states: tuple[str, ...]) -> None:
|
||||
with psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) as connection:
|
||||
_apply(connection, _OLD_LEDGER)
|
||||
connection.execute('CREATE TABLE "LiteLLM_SpendLogs" (request_id text PRIMARY KEY)')
|
||||
_apply(connection, _SPEND_LOG_DELIVERY)
|
||||
for index, state in enumerate(old_states):
|
||||
connection.execute(
|
||||
'INSERT INTO "LiteLLM_AccountingEvent" (event_id, kind, schema_version, payload_hash, occurred_at, '
|
||||
"payload) VALUES (%s, 'cost', 1, 'hash', now(), '{}')",
|
||||
(f"event-{index}",),
|
||||
)
|
||||
connection.execute(
|
||||
'INSERT INTO "LiteLLM_AccountingDelivery" (event_id, target, version, state) '
|
||||
"VALUES (%s, 'LiteLLM_UserTable', 1, %s)",
|
||||
(f"event-{index}", state),
|
||||
)
|
||||
connection.execute(
|
||||
'INSERT INTO "LiteLLM_SpendLogDelivery" (request_id, state, accounting_payload, is_batch) '
|
||||
"VALUES ('request', 'pending', '{}', false)"
|
||||
)
|
||||
|
||||
_apply(connection, _DROP)
|
||||
_apply(connection, _DROP)
|
||||
|
||||
assert _present(connection, _DROPPED) == ()
|
||||
assert connection.execute('SELECT request_id, state::text FROM "LiteLLM_SpendLogDelivery"').fetchall() == [
|
||||
("request", "pending")
|
||||
]
|
||||
|
|
@ -12,6 +12,12 @@ def test_aaaaaschema_compatibility() -> None:
|
|||
assert check_breaking_changes(parse_model_fields(ORIGINAL), parse_model_fields(added)) == ()
|
||||
|
||||
|
||||
def test_changing_only_a_field_attribute_is_not_breaking() -> None:
|
||||
previous: Final = parse_model_fields('model Item {\n id String @id @default(dbgenerated("a()::text"))\n}\n')
|
||||
current: Final = parse_model_fields('model Item {\n id String @id @default(dbgenerated("(a())::text"))\n}\n')
|
||||
assert check_breaking_changes(previous, current) == ()
|
||||
|
||||
|
||||
def test_parse_model_fields_retains_types_and_attributes() -> None:
|
||||
assert parse_model_fields(ORIGINAL) == {"Item": {"id": "String @id", "label": "String?"}}
|
||||
|
||||
|
|
@ -40,3 +46,17 @@ def test_schema_compatibility_rejects_removed_models_fields_and_narrowed_types(
|
|||
schema: str, expected: tuple[str, ...]
|
||||
) -> None:
|
||||
assert check_breaking_changes(parse_model_fields(ORIGINAL), parse_model_fields(schema)) == expected
|
||||
|
||||
|
||||
def test_only_the_dropped_accounting_models_may_be_removed() -> None:
|
||||
accounting: Final = (
|
||||
"model LiteLLM_AccountingEvent {\n event_id String @id\n}\n"
|
||||
"model LiteLLM_AccountingDelivery {\n event_id String\n note String?\n}\n"
|
||||
)
|
||||
previous: Final = parse_model_fields(ORIGINAL + accounting)
|
||||
|
||||
assert check_breaking_changes(previous, parse_model_fields(ORIGINAL)) == ()
|
||||
assert check_breaking_changes(previous, parse_model_fields("")) == ("Breaking: Model Item was removed",)
|
||||
assert check_breaking_changes(
|
||||
previous, parse_model_fields(ORIGINAL + "model LiteLLM_AccountingDelivery {\n event_id String\n}\n")
|
||||
) == ("Breaking: Field LiteLLM_AccountingDelivery.note was removed",)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue