Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
BEGIN;

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyUserSpend_group_key" ON "LiteLLM_DailyUserSpend" ("user_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyTeamSpend_group_key" ON "LiteLLM_DailyTeamSpend" ("team_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyOrganizationSpend_group_key" ON "LiteLLM_DailyOrganizationSpend" ("organization_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyEndUserSpend_group_key" ON "LiteLLM_DailyEndUserSpend" ("end_user_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyAgentSpend_group_key" ON "LiteLLM_DailyAgentSpend" ("agent_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_DailyTagSpend_group_key" ON "LiteLLM_DailyTagSpend" ("tag", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint", "model_group");

DROP INDEX IF EXISTS "LiteLLM_DailyUserSpend_user_id_date_api_key_model_custom_ll_key";

DROP INDEX IF EXISTS "LiteLLM_DailyTeamSpend_team_id_date_api_key_model_custom_ll_key";

DROP INDEX IF EXISTS "LiteLLM_DailyOrganizationSpend_organization_id_date_api_key_key";

DROP INDEX IF EXISTS "LiteLLM_DailyEndUserSpend_end_user_id_date_api_key_model_cu_key";

DROP INDEX IF EXISTS "LiteLLM_DailyAgentSpend_agent_id_date_api_key_model_custom__key";

DROP INDEX IF EXISTS "LiteLLM_DailyTagSpend_tag_date_api_key_model_custom_llm_pro_key";

COMMIT;
12 changes: 6 additions & 6 deletions litellm-proxy-extras/litellm_proxy_extras/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ model LiteLLM_DailyUserSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyUserSpend_group_key")
@@index([date])
@@index([user_id, date])
@@index([api_key])
Expand Down Expand Up @@ -942,7 +942,7 @@ model LiteLLM_DailyOrganizationSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyOrganizationSpend_group_key")
@@index([date])
@@index([organization_id, date])
@@index([api_key])
Expand Down Expand Up @@ -979,7 +979,7 @@ model LiteLLM_DailyEndUserSpend {
timed_requests BigInt @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([end_user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([end_user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyEndUserSpend_group_key")
@@index([date])
@@index([end_user_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1016,7 +1016,7 @@ model LiteLLM_DailyAgentSpend {
timed_requests BigInt @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([agent_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([agent_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyAgentSpend_group_key")
@@index([date])
@@index([agent_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1055,7 +1055,7 @@ model LiteLLM_DailyTeamSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([team_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([team_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyTeamSpend_group_key")
@@index([date])
@@index([team_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1094,7 +1094,7 @@ model LiteLLM_DailyTagSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([tag, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([tag, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyTagSpend_group_key")
@@index([date])
@@index([tag, date])
@@index([api_key])
Expand Down
8 changes: 8 additions & 0 deletions litellm-proxy-extras/migration_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ When the guard fires:

> **AI AGENTS / ASSISTANTS:** Do **not** automatically re-run the command with `--allow-destructive`. If the guard fires while you are driving the runbook for a human, stop, show them the error, and wait for their explicit confirmation before passing the flag. Auto-passing `--allow-destructive` is the exact failure mode this guard exists to prevent.

## Daily spend model-group identity cutover

`20260922000000_daily_spend_model_group_identity` separates daily usage by the requested public model group. Schedule a maintenance window: the six replacement unique indexes scan existing daily spend tables and block writes while building

Drain incoming requests and all in-memory and shared Redis spend buffers, then stop every old proxy and spend collector before applying this migration. Start only upgraded writers after it completes. Old writers require the removed conflict target and cannot run alongside the expanded identity. The migration changes indexes only; previously merged attribution and legacy NULL groups remain as recorded

PTU flat charges retain their deployment/day identity across public-model renames. New inference rows include the requested group in their identity. Rolling back requires stopping writers and reconciling group-separated rows before restoring the old unique indexes; simply starting an older image is unsupported

## Common Fixes

**Missing testing module:**
Expand Down
12 changes: 9 additions & 3 deletions litellm/proxy/db/daily_spend_bulk_upsert.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ def daily_spend_entity_ids(payload: Mapping[str, object], entity: DailySpendEnti
# The unique constraint's columns after the entity id, in constraint order. A NULL can
# never match itself in a unique index, so every one of these is normalized to '': the
# conflict target has to be NULL-free or the row is re-inserted on every single flush.
_KEY_COLUMNS: Final = ("date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name", "endpoint")
_KEY_COLUMNS: Final = (
"date",
"api_key",
"model",
"custom_llm_provider",
"mcp_namespaced_tool_name",
"endpoint",
"model_group",
)

_COUNTER_COLUMNS: Final = (
"prompt_tokens",
Expand Down Expand Up @@ -164,7 +172,6 @@ def _row_params(
return (
str(uuid.uuid4()),
*key,
None if transaction.get("model_group") is None else _as_text(transaction.get("model_group")),
*(_as_int(transaction.get(column)) for column in _COUNTER_COLUMNS),
*(_as_float(transaction.get(column)) for column in _SPEND_COLUMNS),
*((None if request_id is None else _as_text(request_id),) if table.carries_request_id else ()),
Expand All @@ -176,7 +183,6 @@ def _insert_columns(table: DailySpendTable) -> tuple[str, ...]:
"id",
table.entity_id_column,
*_KEY_COLUMNS,
"model_group",
*_COUNTER_COLUMNS,
*_SPEND_COLUMNS,
*(("request_id",) if table.carries_request_id else ()),
Expand Down
25 changes: 10 additions & 15 deletions litellm/proxy/db/db_spend_update_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from litellm.proxy.db.daily_spend_bulk_upsert import (
DAILY_SPEND_TABLES,
build_bulk_upsert,
conflict_key,
daily_spend_entity_ids,
merge_by_conflict_key,
)
Expand Down Expand Up @@ -2811,7 +2812,7 @@ async def add_spend_log_transaction_to_daily_user_transaction(
"""
Add a spend log transaction to the `daily_spend_update_queue`

Key = @@unique([user_id, date, api_key, model, custom_llm_provider]) )
Key matches the daily user spend database identity, including the requested model group.

If key exists, update the transaction with the new spend and usage
"""
Expand All @@ -2825,9 +2826,8 @@ async def add_spend_log_transaction_to_daily_user_transaction(
if base_daily_transaction is None:
return

endpoint_str: Final = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{payload['user']}_{base_daily_transaction['date']}_{payload['api_key']}_{payload['model']}_{payload['custom_llm_provider']}_{endpoint_str}"
daily_transaction: Final = DailyUserSpendTransaction(user_id=payload["user"], **base_daily_transaction)
daily_transaction_key: Final = json.dumps(conflict_key(DAILY_SPEND_TABLES["user"], daily_transaction))
await self.daily_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})

async def add_spend_log_transaction_to_daily_team_transaction(
Expand All @@ -2848,9 +2848,8 @@ async def add_spend_log_transaction_to_daily_team_transaction(
verbose_proxy_logger.debug("team_id is None for request. Skipping incrementing team spend.")
return

endpoint_str: Final = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{payload['team_id']}_{base_daily_transaction['date']}_{payload['api_key']}_{payload['model']}_{payload['custom_llm_provider']}_{endpoint_str}"
daily_transaction: Final = DailyTeamSpendTransaction(team_id=payload["team_id"], **base_daily_transaction)
daily_transaction_key: Final = json.dumps(conflict_key(DAILY_SPEND_TABLES["team"], daily_transaction))
await self.daily_team_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})

async def add_spend_log_transaction_to_daily_org_transaction(
Expand Down Expand Up @@ -2881,9 +2880,8 @@ async def add_spend_log_transaction_to_daily_org_transaction(
if base_daily_transaction is None:
return

endpoint_str: Final = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{org_id}_{base_daily_transaction['date']}_{payload_with_org['api_key']}_{payload_with_org['model']}_{payload_with_org['custom_llm_provider']}_{endpoint_str}"
daily_transaction: Final = DailyOrganizationSpendTransaction(organization_id=org_id, **base_daily_transaction)
daily_transaction_key: Final = json.dumps(conflict_key(DAILY_SPEND_TABLES["org"], daily_transaction))
await self.daily_org_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})

async def add_spend_log_transaction_to_daily_end_user_transaction(
Expand Down Expand Up @@ -2914,9 +2912,8 @@ async def add_spend_log_transaction_to_daily_end_user_transaction(
if base_daily_transaction is None:
return

endpoint_str: Final = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{end_user_id}_{base_daily_transaction['date']}_{payload_with_end_user_id['api_key']}_{payload_with_end_user_id['model']}_{payload_with_end_user_id['custom_llm_provider']}_{endpoint_str}"
daily_transaction: Final = DailyEndUserSpendTransaction(end_user_id=end_user_id, **base_daily_transaction)
daily_transaction_key: Final = json.dumps(conflict_key(DAILY_SPEND_TABLES["end_user"], daily_transaction))
await self.daily_end_user_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})

async def add_spend_log_transaction_to_daily_agent_transaction(
Expand All @@ -2941,9 +2938,8 @@ async def add_spend_log_transaction_to_daily_agent_transaction(
)
if base_daily_transaction is None:
return
endpoint_str: Final = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{payload['agent_id']}_{base_daily_transaction['date']}_{payload_with_agent_id['api_key']}_{payload_with_agent_id['model']}_{payload_with_agent_id['custom_llm_provider']}_{endpoint_str}"
daily_transaction: Final = DailyAgentSpendTransaction(agent_id=payload["agent_id"], **base_daily_transaction)
daily_transaction_key: Final = json.dumps(conflict_key(DAILY_SPEND_TABLES["agent"], daily_transaction))
await self.daily_agent_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})

async def add_spend_log_transaction_to_daily_tag_transaction(
Expand All @@ -2968,10 +2964,9 @@ async def add_spend_log_transaction_to_daily_tag_transaction(
for tag in request_tags:
if tag is None:
continue
endpoint_str = base_daily_transaction.get("endpoint") or ""
daily_transaction_key = f"{tag}_{base_daily_transaction['date']}_{payload['api_key']}_{payload['model']}_{payload['custom_llm_provider']}_{endpoint_str}"
daily_transaction = DailyTagSpendTransaction(
tag=tag, **base_daily_transaction, request_id=payload["request_id"]
)

await self.daily_tag_spend_update_queue.add_update(update={daily_transaction_key: daily_transaction})
await self.daily_tag_spend_update_queue.add_update(
update={json.dumps(conflict_key(DAILY_SPEND_TABLES["tag"], daily_transaction)): daily_transaction}
)
12 changes: 6 additions & 6 deletions litellm/proxy/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ model LiteLLM_DailyUserSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyUserSpend_group_key")
@@index([date])
@@index([user_id, date])
@@index([api_key])
Expand Down Expand Up @@ -942,7 +942,7 @@ model LiteLLM_DailyOrganizationSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyOrganizationSpend_group_key")
@@index([date])
@@index([organization_id, date])
@@index([api_key])
Expand Down Expand Up @@ -979,7 +979,7 @@ model LiteLLM_DailyEndUserSpend {
timed_requests BigInt @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([end_user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([end_user_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyEndUserSpend_group_key")
@@index([date])
@@index([end_user_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1016,7 +1016,7 @@ model LiteLLM_DailyAgentSpend {
timed_requests BigInt @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([agent_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([agent_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyAgentSpend_group_key")
@@index([date])
@@index([agent_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1055,7 +1055,7 @@ model LiteLLM_DailyTeamSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([team_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([team_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyTeamSpend_group_key")
@@index([date])
@@index([team_id, date])
@@index([api_key])
Expand Down Expand Up @@ -1094,7 +1094,7 @@ model LiteLLM_DailyTagSpend {
created_at DateTime @default(now())
updated_at DateTime @updatedAt

@@unique([tag, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint])
@@unique([tag, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name, endpoint, model_group], map: "LiteLLM_DailyTagSpend_group_key")
@@index([date])
@@index([tag, date])
@@index([api_key])
Expand Down
45 changes: 21 additions & 24 deletions litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
``[ptu_effective_from, ptu_effective_to)`` window (a window opening at 23:00
charges one hour that day). The amount is written to ``LiteLLM_DailyTeamSpend``
under a sentinel api_key so the rows are distinguishable from per-request rows
and share the existing unique constraint.
and keep their identity across display-name changes.
"""

import asyncio
Expand All @@ -19,6 +19,7 @@
from datetime import date, datetime, time, timedelta, timezone
from types import MappingProxyType
from typing import TYPE_CHECKING, Final
from uuid import NAMESPACE_URL, uuid5

from litellm._logging import verbose_proxy_logger
from litellm.constants import (
Expand All @@ -30,6 +31,7 @@
PTU_SENTINEL_API_KEY,
)
from litellm.litellm_core_utils.ptu_pricing import ptu_terms
from litellm.proxy.db.routing_prisma_wrapper import WriterPinnedClient, writer_wrapper
from litellm.proxy.spend_tracking.ptu_feature_flag import is_ptu_cost_attribution_enabled
from litellm.repositories.model_repository import ModelRepository
from litellm.repositories.prisma_protocols import TableActions
Expand Down Expand Up @@ -242,32 +244,27 @@ async def _upsert_ptu_daily_row(
date_str: str,
flat_cost: float,
) -> None:
"""Idempotent upsert of a sentinel-api_key row on LiteLLM_DailyTeamSpend.

``model`` holds the deployment id because it is part of the table's unique key and a
rename must not move the row. ``model_group`` carries the operator-facing name, which
is outside the key and is what the usage views display.
"""
where: Final = {
"team_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint": {
"team_id": team_id,
"date": date_str,
"api_key": PTU_SENTINEL_API_KEY,
"model": model_id,
"custom_llm_provider": "",
"mcp_namespaced_tool_name": "",
"endpoint": "",
}
"""Upsert one deployment/day charge, retaining legacy IDs and a mutable display name."""
natural_key: Final = {
"team_id": team_id,
"date": date_str,
"api_key": PTU_SENTINEL_API_KEY,
"model": model_id,
}
table: Final = _DailyTeamSpendRepository(WriterPinnedClient(writer_wrapper(prisma_client.db))).table
existing: Final = await table.find_first(where=natural_key)
row_id: Final = (
existing.id
if existing is not None
else str(uuid5(NAMESPACE_URL, "litellm:ptu:" + json.dumps(tuple(natural_key.values()), separators=(",", ":"))))
)
now: Final = datetime.now(timezone.utc)
await _daily_team_spend_table(prisma_client).upsert(
where=where,
await table.upsert(
where={"id": row_id},
data={
"create": {
"team_id": team_id,
"date": date_str,
"api_key": PTU_SENTINEL_API_KEY,
"model": model_id,
**natural_key,
"id": row_id,
"model_group": model_name,
"custom_llm_provider": "",
"mcp_namespaced_tool_name": "",
Expand All @@ -291,7 +288,7 @@ async def _upsert_charge_with_retry(
) -> bool:
"""Write one charge, retrying transient failures. Returns False once attempts are spent.

The upsert is idempotent on the sentinel unique key, so a retry can only rewrite the
The upsert is idempotent on the stable row ID, so a retry can only rewrite the
same amount for the same day. Retrying in-run matters because the scheduled job moves
on to the next date: a write lost here is a day of PTU cost that no later run replays.
"""
Expand Down
Loading
Loading