관리자 사용량 근거를 정교화
This commit is contained in:
parent
ccdcfcd2f5
commit
707dba4f8f
10 changed files with 1136 additions and 209 deletions
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
import time
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Literal, cast
|
||||
from typing import Annotated, Iterable, Literal, cast
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
|
@ -50,11 +50,14 @@ router = APIRouter(prefix="/admin", tags=["admin"])
|
|||
|
||||
AdminPrincipal = Annotated[Principal, Depends(require_admin_access())]
|
||||
HealthStatus = Literal["ok", "degraded", "down"]
|
||||
UsageBudgetStatus = Literal["disabled", "ok", "warn", "exceeded"]
|
||||
UsageBudgetStatus = Literal["disabled", "ok", "warn", "exceeded", "indeterminate"]
|
||||
UsageCostBasis = Literal[
|
||||
"provider_estimate",
|
||||
"provider_reported",
|
||||
"reference_rate",
|
||||
"reference_upper_bound",
|
||||
"partial",
|
||||
"partial_upper_bound",
|
||||
"unavailable",
|
||||
]
|
||||
TicketCategory = Literal[
|
||||
|
|
@ -69,8 +72,21 @@ TicketPriority = Literal["low", "normal", "high", "urgent"]
|
|||
TicketStatus = Literal["open", "triaged", "in_progress", "resolved", "closed"]
|
||||
NotificationDeliveryStatus = Literal["queued", "sending", "sent", "failed", "skipped"]
|
||||
|
||||
METERED_CLIENT_TURN_FILTER_SQL = """
|
||||
SYNTHETIC_USAGE_SIGNATURES = frozenset({("e2e", "fake-client", 1, 1, 0.0)})
|
||||
|
||||
REPORTABLE_CLIENT_TURN_FILTER_SQL = """
|
||||
speaker = 'client'
|
||||
AND NOT (
|
||||
LOWER(BTRIM(COALESCE(llm_provider, ''))) = 'e2e'
|
||||
AND LOWER(BTRIM(COALESCE(model, ''))) = 'fake-client'
|
||||
AND COALESCE(tokens_in, 0) = 1
|
||||
AND COALESCE(tokens_out, 0) = 1
|
||||
AND COALESCE(cost_usd, 0) = 0
|
||||
)
|
||||
"""
|
||||
|
||||
METERED_CLIENT_TURN_FILTER_SQL = f"""
|
||||
{REPORTABLE_CLIENT_TURN_FILTER_SQL}
|
||||
AND (
|
||||
llm_provider IS NOT NULL OR model IS NOT NULL
|
||||
OR tokens_in IS NOT NULL OR tokens_out IS NOT NULL
|
||||
|
|
@ -78,28 +94,6 @@ METERED_CLIENT_TURN_FILTER_SQL = """
|
|||
)
|
||||
"""
|
||||
|
||||
USAGE_AGGREGATE_COLUMNS_SQL = """
|
||||
COUNT(*) AS turns,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(tokens_in, 0) > 0 OR COALESCE(tokens_out, 0) > 0
|
||||
) AS token_metered_turns,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(tokens_in, 0) <= 0 AND COALESCE(tokens_out, 0) <= 0
|
||||
) AS token_unmetered_turns,
|
||||
COALESCE(SUM(tokens_in), 0)::bigint AS tokens_in,
|
||||
COALESCE(SUM(tokens_out), 0)::bigint AS tokens_out,
|
||||
COALESCE(SUM(cost_usd), 0)::numeric AS cost_usd,
|
||||
COUNT(*) FILTER (WHERE COALESCE(cost_usd, 0) <= 0) AS unpriced_turns,
|
||||
COALESCE(
|
||||
SUM(tokens_in) FILTER (WHERE COALESCE(cost_usd, 0) <= 0),
|
||||
0
|
||||
)::bigint AS unpriced_tokens_in,
|
||||
COALESCE(
|
||||
SUM(tokens_out) FILTER (WHERE COALESCE(cost_usd, 0) <= 0),
|
||||
0
|
||||
)::bigint AS unpriced_tokens_out
|
||||
"""
|
||||
|
||||
SUPPORT_TICKET_DETAIL_FROM_SQL = """
|
||||
SELECT
|
||||
t.id,
|
||||
|
|
@ -187,6 +181,7 @@ class AdminUsageDailyCost(BaseModel):
|
|||
tokens_in: int
|
||||
tokens_out: int
|
||||
cost_usd: float
|
||||
cost_basis: UsageCostBasis = "provider_reported"
|
||||
|
||||
|
||||
class AdminUsageBudget(BaseModel):
|
||||
|
|
@ -194,6 +189,7 @@ class AdminUsageBudget(BaseModel):
|
|||
used_ratio: float
|
||||
remaining_usd: float | None
|
||||
status: UsageBudgetStatus
|
||||
cost_basis: UsageCostBasis = "provider_reported"
|
||||
|
||||
|
||||
class AdminUsageEvaluatorCache(BaseModel):
|
||||
|
|
@ -221,6 +217,7 @@ class AdminUsageResponse(BaseModel):
|
|||
cost_usd: float
|
||||
recorded_cost_usd: float = 0.0
|
||||
estimated_cost_usd: float = 0.0
|
||||
cost_basis: UsageCostBasis = "provider_reported"
|
||||
budget: AdminUsageBudget
|
||||
evaluator_cache: AdminUsageEvaluatorCache
|
||||
by_provider: list[AdminUsageBreakdown]
|
||||
|
|
@ -458,6 +455,23 @@ def _safe_usage_int(value: object) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def _is_reportable_usage(
|
||||
provider: str,
|
||||
model: str,
|
||||
tokens_in: int,
|
||||
tokens_out: int,
|
||||
cost_usd: float,
|
||||
) -> bool:
|
||||
signature = (
|
||||
provider.strip().lower(),
|
||||
model.strip().lower(),
|
||||
max(0, tokens_in),
|
||||
max(0, tokens_out),
|
||||
max(0.0, cost_usd),
|
||||
)
|
||||
return signature not in SYNTHETIC_USAGE_SIGNATURES
|
||||
|
||||
|
||||
def _usage_breakdown(
|
||||
*,
|
||||
provider: str,
|
||||
|
|
@ -470,31 +484,41 @@ def _usage_breakdown(
|
|||
stored_cost_usd: float,
|
||||
unpriced_tokens_in: int,
|
||||
unpriced_tokens_out: int,
|
||||
priced_at: datetime | int | float | str,
|
||||
) -> AdminUsageBreakdown:
|
||||
"""저장된 공급자 비용 추정치와 공식 참조단가를 한 원장 행으로 정규화한다."""
|
||||
|
||||
reference_basis = provider_uses_reference_cost(provider)
|
||||
fallback = estimate_reference_cost(
|
||||
provider=provider,
|
||||
model=model,
|
||||
tokens_in=unpriced_tokens_in,
|
||||
tokens_out=unpriced_tokens_out,
|
||||
priced_at=priced_at,
|
||||
)
|
||||
rate_info = fallback or estimate_reference_cost(
|
||||
provider=provider,
|
||||
model=model,
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
)
|
||||
rate_info = fallback
|
||||
if rate_info is None and not (reference_basis and stored_cost_usd > 0):
|
||||
rate_info = estimate_reference_cost(
|
||||
provider=provider,
|
||||
model=model,
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
priced_at=priced_at,
|
||||
)
|
||||
fallback_cost = fallback.cost_usd if fallback is not None else 0.0
|
||||
effective_cost = max(0.0, stored_cost_usd) + fallback_cost
|
||||
reference_basis = provider_uses_reference_cost(provider)
|
||||
|
||||
if reference_basis:
|
||||
recorded_cost = 0.0
|
||||
estimated_cost = effective_cost
|
||||
basis: UsageCostBasis = (
|
||||
"reference_rate" if rate_info is not None or effective_cost > 0 else "unavailable"
|
||||
)
|
||||
if fallback is not None and stored_cost_usd <= 0:
|
||||
basis: UsageCostBasis = "reference_upper_bound"
|
||||
else:
|
||||
basis = (
|
||||
"reference_rate"
|
||||
if rate_info is not None or effective_cost > 0
|
||||
else "unavailable"
|
||||
)
|
||||
else:
|
||||
recorded_cost = max(0.0, stored_cost_usd)
|
||||
estimated_cost = fallback_cost
|
||||
|
|
@ -505,6 +529,17 @@ def _usage_breakdown(
|
|||
else:
|
||||
basis = "unavailable"
|
||||
|
||||
rate_label = rate_info.rate_label if rate_info is not None else None
|
||||
rate_source_url = rate_info.source_url if rate_info is not None else None
|
||||
if reference_basis and stored_cost_usd > 0:
|
||||
if fallback is not None:
|
||||
rate_label = "호출 시점 저장 추정값 + 미저장분 공식 참조단가 합산"
|
||||
else:
|
||||
rate_label = "호출 시점에 저장된 참조단가 추정값"
|
||||
rate_source_url = None
|
||||
elif fallback is not None:
|
||||
rate_label = f"{fallback.rate_label} · 캐시 미보존 과거행은 전체 입력 기준"
|
||||
|
||||
return AdminUsageBreakdown(
|
||||
provider=provider,
|
||||
model=model,
|
||||
|
|
@ -513,16 +548,120 @@ def _usage_breakdown(
|
|||
token_unmetered_turns=max(0, token_unmetered_turns),
|
||||
tokens_in=max(0, tokens_in),
|
||||
tokens_out=max(0, tokens_out),
|
||||
cost_usd=round(effective_cost, 6),
|
||||
recorded_cost_usd=round(recorded_cost, 6),
|
||||
estimated_cost_usd=round(estimated_cost, 6),
|
||||
cost_usd=effective_cost,
|
||||
recorded_cost_usd=recorded_cost,
|
||||
estimated_cost_usd=estimated_cost,
|
||||
cost_basis=basis,
|
||||
rate_label=rate_info.rate_label if rate_info is not None else None,
|
||||
rate_source_url=rate_info.source_url if rate_info is not None else None,
|
||||
rate_label=rate_label,
|
||||
rate_source_url=rate_source_url,
|
||||
)
|
||||
|
||||
|
||||
def _usage_budget(cost_usd: float) -> AdminUsageBudget:
|
||||
def _merge_usage_breakdowns(
|
||||
breakdowns: Iterable[AdminUsageBreakdown],
|
||||
) -> list[AdminUsageBreakdown]:
|
||||
grouped: dict[tuple[str, str], list[AdminUsageBreakdown]] = {}
|
||||
for item in breakdowns:
|
||||
grouped.setdefault((item.provider, item.model), []).append(item)
|
||||
|
||||
merged: list[AdminUsageBreakdown] = []
|
||||
for (provider, model), items in grouped.items():
|
||||
basis = _aggregate_cost_basis(item.cost_basis for item in items)
|
||||
labels = {item.rate_label for item in items if item.rate_label}
|
||||
source_urls = {item.rate_source_url for item in items if item.rate_source_url}
|
||||
rate_label = next(iter(labels)) if len(labels) == 1 else None
|
||||
if basis == "partial":
|
||||
rate_label = "일부 호출 미산정 · 표시액은 산정 가능분 합계"
|
||||
elif basis == "partial_upper_bound":
|
||||
rate_label = "일부 호출 미산정 · 산정된 부분도 상한 추정"
|
||||
elif len(labels) > 1:
|
||||
rate_label = (
|
||||
"기간별 공식 참조단가 상한 합산"
|
||||
if basis == "reference_upper_bound"
|
||||
else "기간별 공식 참조단가 합산"
|
||||
)
|
||||
merged.append(
|
||||
AdminUsageBreakdown(
|
||||
provider=provider,
|
||||
model=model,
|
||||
turns=sum(item.turns for item in items),
|
||||
token_metered_turns=sum(item.token_metered_turns for item in items),
|
||||
token_unmetered_turns=sum(item.token_unmetered_turns for item in items),
|
||||
tokens_in=sum(item.tokens_in for item in items),
|
||||
tokens_out=sum(item.tokens_out for item in items),
|
||||
cost_usd=round(sum(item.cost_usd for item in items), 6),
|
||||
recorded_cost_usd=round(
|
||||
sum(item.recorded_cost_usd for item in items), 6
|
||||
),
|
||||
estimated_cost_usd=round(
|
||||
sum(item.estimated_cost_usd for item in items), 6
|
||||
),
|
||||
cost_basis=cast(UsageCostBasis, basis),
|
||||
rate_label=rate_label,
|
||||
rate_source_url=(
|
||||
next(iter(source_urls)) if len(source_urls) == 1 else None
|
||||
),
|
||||
)
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
def _aggregate_cost_basis(
|
||||
bases: Iterable[UsageCostBasis],
|
||||
) -> UsageCostBasis:
|
||||
basis_set = set(bases)
|
||||
if not basis_set:
|
||||
return "provider_reported"
|
||||
|
||||
has_missing = bool(
|
||||
basis_set & {"unavailable", "partial", "partial_upper_bound"}
|
||||
)
|
||||
has_upper_bound = bool(
|
||||
basis_set & {"reference_upper_bound", "partial_upper_bound"}
|
||||
)
|
||||
has_known_amount = basis_set != {"unavailable"}
|
||||
if has_missing:
|
||||
if not has_known_amount:
|
||||
return "unavailable"
|
||||
return "partial_upper_bound" if has_upper_bound else "partial"
|
||||
if has_upper_bound:
|
||||
return "reference_upper_bound"
|
||||
for candidate in (
|
||||
"provider_estimate",
|
||||
"provider_reported",
|
||||
"reference_rate",
|
||||
):
|
||||
if candidate in basis_set:
|
||||
return cast(UsageCostBasis, candidate)
|
||||
return "unavailable"
|
||||
|
||||
|
||||
def _scale_usage_breakdown(
|
||||
breakdown: AdminUsageBreakdown,
|
||||
multiplier: int,
|
||||
) -> AdminUsageBreakdown:
|
||||
count = max(0, multiplier)
|
||||
return AdminUsageBreakdown(
|
||||
provider=breakdown.provider,
|
||||
model=breakdown.model,
|
||||
turns=breakdown.turns * count,
|
||||
token_metered_turns=breakdown.token_metered_turns * count,
|
||||
token_unmetered_turns=breakdown.token_unmetered_turns * count,
|
||||
tokens_in=breakdown.tokens_in * count,
|
||||
tokens_out=breakdown.tokens_out * count,
|
||||
cost_usd=breakdown.cost_usd * count,
|
||||
recorded_cost_usd=breakdown.recorded_cost_usd * count,
|
||||
estimated_cost_usd=breakdown.estimated_cost_usd * count,
|
||||
cost_basis=breakdown.cost_basis,
|
||||
rate_label=breakdown.rate_label,
|
||||
rate_source_url=breakdown.rate_source_url,
|
||||
)
|
||||
|
||||
|
||||
def _usage_budget(
|
||||
cost_usd: float,
|
||||
cost_basis: UsageCostBasis,
|
||||
) -> AdminUsageBudget:
|
||||
limit = max(0.0, float(settings.admin_usage_budget_usd or 0.0))
|
||||
if limit <= 0:
|
||||
return AdminUsageBudget(
|
||||
|
|
@ -530,18 +669,35 @@ def _usage_budget(cost_usd: float) -> AdminUsageBudget:
|
|||
used_ratio=0.0,
|
||||
remaining_usd=None,
|
||||
status="disabled",
|
||||
cost_basis=cost_basis,
|
||||
)
|
||||
used_ratio = max(0.0, cost_usd / limit)
|
||||
status_value: UsageBudgetStatus = "ok"
|
||||
if used_ratio >= 1.0:
|
||||
remaining_usd: float | None = round(max(0.0, limit - cost_usd), 6)
|
||||
status_value: UsageBudgetStatus
|
||||
if cost_basis in {"partial_upper_bound", "unavailable"}:
|
||||
status_value = "indeterminate"
|
||||
remaining_usd = None
|
||||
elif cost_basis == "partial":
|
||||
if used_ratio >= 1.0:
|
||||
status_value = "exceeded"
|
||||
elif used_ratio >= 0.8:
|
||||
status_value = "warn"
|
||||
else:
|
||||
status_value = "indeterminate"
|
||||
elif cost_basis == "reference_upper_bound":
|
||||
status_value = "ok" if used_ratio < 0.8 else "indeterminate"
|
||||
elif used_ratio >= 1.0:
|
||||
status_value = "exceeded"
|
||||
elif used_ratio >= 0.8:
|
||||
status_value = "warn"
|
||||
else:
|
||||
status_value = "ok"
|
||||
return AdminUsageBudget(
|
||||
limit_usd=round(limit, 6),
|
||||
used_ratio=round(used_ratio, 4),
|
||||
remaining_usd=round(max(0.0, limit - cost_usd), 6),
|
||||
remaining_usd=remaining_usd,
|
||||
status=status_value,
|
||||
cost_basis=cost_basis,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -628,7 +784,7 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
total_row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE speaker = 'client') AS total_turns,
|
||||
COUNT(*) FILTER (WHERE {REPORTABLE_CLIENT_TURN_FILTER_SQL}) AS total_turns,
|
||||
COUNT(*) FILTER (WHERE {METERED_CLIENT_TURN_FILTER_SQL}) AS metered_turns,
|
||||
COUNT(*) FILTER (
|
||||
WHERE {METERED_CLIENT_TURN_FILTER_SQL}
|
||||
|
|
@ -639,58 +795,69 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
AND COALESCE(tokens_in, 0) <= 0
|
||||
AND COALESCE(tokens_out, 0) <= 0
|
||||
) AS token_unmetered_turns,
|
||||
COALESCE(SUM(tokens_in) FILTER (WHERE speaker = 'client'), 0)::bigint AS tokens_in,
|
||||
COALESCE(SUM(tokens_out) FILTER (WHERE speaker = 'client'), 0)::bigint AS tokens_out,
|
||||
COALESCE(SUM(cost_usd) FILTER (WHERE speaker = 'client'), 0)::numeric AS cost_usd
|
||||
COALESCE(
|
||||
SUM(tokens_in) FILTER (WHERE {REPORTABLE_CLIENT_TURN_FILTER_SQL}), 0
|
||||
)::bigint AS tokens_in,
|
||||
COALESCE(
|
||||
SUM(tokens_out) FILTER (WHERE {REPORTABLE_CLIENT_TURN_FILTER_SQL}), 0
|
||||
)::bigint AS tokens_out,
|
||||
COALESCE(
|
||||
SUM(cost_usd) FILTER (WHERE {REPORTABLE_CLIENT_TURN_FILTER_SQL}), 0
|
||||
)::numeric AS cost_usd
|
||||
FROM app.turns
|
||||
WHERE created_at >= now() - ($1::int * interval '1 day')
|
||||
AND speaker = 'client'
|
||||
""",
|
||||
window_days,
|
||||
)
|
||||
rows = await conn.fetch(
|
||||
usage_rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT
|
||||
to_char(
|
||||
date_trunc('day', created_at AT TIME ZONE 'UTC'),
|
||||
'YYYY-MM-DD'
|
||||
) AS day,
|
||||
COALESCE(llm_provider, 'unknown') AS provider,
|
||||
COALESCE(model, 'unknown') AS model,
|
||||
{USAGE_AGGREGATE_COLUMNS_SQL}
|
||||
COALESCE(tokens_in, 0)::bigint AS tokens_in,
|
||||
COALESCE(tokens_out, 0)::bigint AS tokens_out,
|
||||
COALESCE(cost_usd, 0)::numeric AS cost_usd,
|
||||
COUNT(*)::bigint AS matching_turns
|
||||
FROM app.turns
|
||||
WHERE created_at >= now() - ($1::int * interval '1 day')
|
||||
AND {METERED_CLIENT_TURN_FILTER_SQL}
|
||||
GROUP BY 1, 2
|
||||
""",
|
||||
window_days,
|
||||
)
|
||||
daily_rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT
|
||||
to_char(date_trunc('day', created_at), 'YYYY-MM-DD') AS day,
|
||||
COALESCE(llm_provider, 'unknown') AS provider,
|
||||
COALESCE(model, 'unknown') AS model,
|
||||
{USAGE_AGGREGATE_COLUMNS_SQL}
|
||||
FROM app.turns
|
||||
WHERE created_at >= now() - ($1::int * interval '1 day')
|
||||
AND {METERED_CLIENT_TURN_FILTER_SQL}
|
||||
GROUP BY 1, 2, 3
|
||||
ORDER BY 1, 2, 3
|
||||
GROUP BY 1, 2, 3, 4, 5, 6
|
||||
""",
|
||||
window_days,
|
||||
)
|
||||
|
||||
all_breakdowns = [
|
||||
_usage_breakdown(
|
||||
provider=str(row["provider"] or "unknown"),
|
||||
model=str(row["model"] or "unknown"),
|
||||
turns=_safe_usage_int(row["turns"]),
|
||||
token_metered_turns=_safe_usage_int(row["token_metered_turns"]),
|
||||
token_unmetered_turns=_safe_usage_int(row["token_unmetered_turns"]),
|
||||
tokens_in=_safe_usage_int(row["tokens_in"]),
|
||||
tokens_out=_safe_usage_int(row["tokens_out"]),
|
||||
stored_cost_usd=_decimal_to_float(row["cost_usd"]),
|
||||
unpriced_tokens_in=_safe_usage_int(row["unpriced_tokens_in"]),
|
||||
unpriced_tokens_out=_safe_usage_int(row["unpriced_tokens_out"]),
|
||||
# 요청별 입력 크기로 단가 구간이 갈리는 모델이 있어 동일 계량 signature만 묶는다.
|
||||
dated_breakdowns: list[AdminUsageBreakdown] = []
|
||||
for row in usage_rows:
|
||||
tokens_in = _safe_usage_int(row["tokens_in"])
|
||||
tokens_out = _safe_usage_int(row["tokens_out"])
|
||||
stored_cost_usd = _decimal_to_float(row["cost_usd"])
|
||||
is_token_metered = tokens_in > 0 or tokens_out > 0
|
||||
matching_turns = max(1, _safe_usage_int(row["matching_turns"]))
|
||||
dated_breakdowns.append(
|
||||
_scale_usage_breakdown(
|
||||
_usage_breakdown(
|
||||
provider=str(row["provider"] or "unknown"),
|
||||
model=str(row["model"] or "unknown"),
|
||||
turns=1,
|
||||
token_metered_turns=1 if is_token_metered else 0,
|
||||
token_unmetered_turns=0 if is_token_metered else 1,
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
stored_cost_usd=stored_cost_usd,
|
||||
unpriced_tokens_in=tokens_in if stored_cost_usd <= 0 else 0,
|
||||
unpriced_tokens_out=tokens_out if stored_cost_usd <= 0 else 0,
|
||||
priced_at=str(row["day"]),
|
||||
),
|
||||
matching_turns,
|
||||
)
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
all_breakdowns = _merge_usage_breakdowns(dated_breakdowns)
|
||||
all_breakdowns.sort(
|
||||
key=lambda item: (
|
||||
-item.cost_usd,
|
||||
|
|
@ -700,33 +867,32 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
item.model,
|
||||
)
|
||||
)
|
||||
recorded_cost = round(sum(item.recorded_cost_usd for item in all_breakdowns), 6)
|
||||
estimated_cost = round(sum(item.estimated_cost_usd for item in all_breakdowns), 6)
|
||||
recorded_cost = round(sum(item.recorded_cost_usd for item in dated_breakdowns), 6)
|
||||
estimated_cost = round(sum(item.estimated_cost_usd for item in dated_breakdowns), 6)
|
||||
total_cost = round(recorded_cost + estimated_cost, 6)
|
||||
total_cost_basis = _aggregate_cost_basis(
|
||||
item.cost_basis for item in dated_breakdowns
|
||||
)
|
||||
|
||||
daily_buckets: dict[str, dict[str, int | float]] = {}
|
||||
for row in daily_rows:
|
||||
breakdown = _usage_breakdown(
|
||||
provider=str(row["provider"] or "unknown"),
|
||||
model=str(row["model"] or "unknown"),
|
||||
turns=_safe_usage_int(row["turns"]),
|
||||
token_metered_turns=_safe_usage_int(row["token_metered_turns"]),
|
||||
token_unmetered_turns=_safe_usage_int(row["token_unmetered_turns"]),
|
||||
tokens_in=_safe_usage_int(row["tokens_in"]),
|
||||
tokens_out=_safe_usage_int(row["tokens_out"]),
|
||||
stored_cost_usd=_decimal_to_float(row["cost_usd"]),
|
||||
unpriced_tokens_in=_safe_usage_int(row["unpriced_tokens_in"]),
|
||||
unpriced_tokens_out=_safe_usage_int(row["unpriced_tokens_out"]),
|
||||
)
|
||||
daily_buckets: dict[str, dict[str, int | float | list[UsageCostBasis]]] = {}
|
||||
for row, breakdown in zip(usage_rows, dated_breakdowns, strict=True):
|
||||
day = str(row["day"])
|
||||
bucket = daily_buckets.setdefault(
|
||||
day,
|
||||
{"turns": 0, "tokens_in": 0, "tokens_out": 0, "cost_usd": 0.0},
|
||||
{
|
||||
"turns": 0,
|
||||
"tokens_in": 0,
|
||||
"tokens_out": 0,
|
||||
"cost_usd": 0.0,
|
||||
"cost_bases": [],
|
||||
},
|
||||
)
|
||||
bucket["turns"] = int(bucket["turns"]) + breakdown.turns
|
||||
bucket["tokens_in"] = int(bucket["tokens_in"]) + breakdown.tokens_in
|
||||
bucket["tokens_out"] = int(bucket["tokens_out"]) + breakdown.tokens_out
|
||||
bucket["cost_usd"] = float(bucket["cost_usd"]) + breakdown.cost_usd
|
||||
cost_bases = cast(list[UsageCostBasis], bucket["cost_bases"])
|
||||
cost_bases.append(breakdown.cost_basis)
|
||||
|
||||
return AdminUsageResponse(
|
||||
source="database",
|
||||
|
|
@ -746,7 +912,8 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
cost_usd=total_cost,
|
||||
recorded_cost_usd=recorded_cost,
|
||||
estimated_cost_usd=estimated_cost,
|
||||
budget=_usage_budget(total_cost),
|
||||
cost_basis=total_cost_basis,
|
||||
budget=_usage_budget(total_cost, total_cost_basis),
|
||||
evaluator_cache=_usage_evaluator_cache(),
|
||||
by_provider=all_breakdowns[:12],
|
||||
daily_cost=[
|
||||
|
|
@ -756,6 +923,9 @@ async def _usage_from_database(window_days: int) -> AdminUsageResponse:
|
|||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
cost_usd=round(float(values["cost_usd"]), 6),
|
||||
cost_basis=_aggregate_cost_basis(
|
||||
cast(list[UsageCostBasis], values["cost_bases"])
|
||||
),
|
||||
)
|
||||
for day, values in sorted(daily_buckets.items())
|
||||
],
|
||||
|
|
@ -1038,8 +1208,8 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
cost_usd = 0.0
|
||||
recorded_cost_usd = 0.0
|
||||
estimated_cost_usd = 0.0
|
||||
buckets: dict[tuple[str, str], dict[str, int | float]] = {}
|
||||
daily_buckets: dict[str, dict[str, int | float]] = {}
|
||||
turn_breakdowns: list[AdminUsageBreakdown] = []
|
||||
daily_buckets: dict[str, dict[str, int | float | list[UsageCostBasis]]] = {}
|
||||
|
||||
for sess in store.list():
|
||||
for turn in getattr(sess, "turns", []) or []:
|
||||
|
|
@ -1048,12 +1218,20 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
created_at = float(getattr(turn, "created_at", 0.0) or 0.0)
|
||||
if created_at < window_start:
|
||||
continue
|
||||
total_turns += 1
|
||||
provider = str(getattr(turn, "llm_provider", None) or "unknown")
|
||||
model = str(getattr(turn, "model", None) or "unknown")
|
||||
turn_tokens_in = _safe_usage_int(getattr(turn, "tokens_in", 0))
|
||||
turn_tokens_out = _safe_usage_int(getattr(turn, "tokens_out", 0))
|
||||
turn_cost = _decimal_to_float(getattr(turn, "cost_usd", 0.0))
|
||||
if not _is_reportable_usage(
|
||||
provider,
|
||||
model,
|
||||
turn_tokens_in,
|
||||
turn_tokens_out,
|
||||
turn_cost,
|
||||
):
|
||||
continue
|
||||
total_turns += 1
|
||||
is_metered = (
|
||||
provider != "unknown"
|
||||
or model != "unknown"
|
||||
|
|
@ -1082,43 +1260,22 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
stored_cost_usd=turn_cost,
|
||||
unpriced_tokens_in=turn_tokens_in if turn_cost <= 0 else 0,
|
||||
unpriced_tokens_out=turn_tokens_out if turn_cost <= 0 else 0,
|
||||
priced_at=created_at,
|
||||
)
|
||||
turn_breakdowns.append(turn_breakdown)
|
||||
cost_usd += turn_breakdown.cost_usd
|
||||
recorded_cost_usd += turn_breakdown.recorded_cost_usd
|
||||
estimated_cost_usd += turn_breakdown.estimated_cost_usd
|
||||
key = (provider, model)
|
||||
bucket = buckets.setdefault(
|
||||
key,
|
||||
{
|
||||
"turns": 0,
|
||||
"token_metered_turns": 0,
|
||||
"token_unmetered_turns": 0,
|
||||
"tokens_in": 0,
|
||||
"tokens_out": 0,
|
||||
"stored_cost_usd": 0.0,
|
||||
"unpriced_tokens_in": 0,
|
||||
"unpriced_tokens_out": 0,
|
||||
},
|
||||
)
|
||||
bucket["turns"] = int(bucket["turns"]) + 1
|
||||
if is_token_metered:
|
||||
bucket["token_metered_turns"] = int(bucket["token_metered_turns"]) + 1
|
||||
else:
|
||||
bucket["token_unmetered_turns"] = int(bucket["token_unmetered_turns"]) + 1
|
||||
bucket["tokens_in"] = int(bucket["tokens_in"]) + turn_tokens_in
|
||||
bucket["tokens_out"] = int(bucket["tokens_out"]) + turn_tokens_out
|
||||
bucket["stored_cost_usd"] = float(bucket["stored_cost_usd"]) + turn_cost
|
||||
if turn_cost <= 0:
|
||||
bucket["unpriced_tokens_in"] = (
|
||||
int(bucket["unpriced_tokens_in"]) + turn_tokens_in
|
||||
)
|
||||
bucket["unpriced_tokens_out"] = (
|
||||
int(bucket["unpriced_tokens_out"]) + turn_tokens_out
|
||||
)
|
||||
day = datetime.fromtimestamp(created_at, timezone.utc).strftime("%Y-%m-%d")
|
||||
daily_bucket = daily_buckets.setdefault(
|
||||
day,
|
||||
{"turns": 0, "tokens_in": 0, "tokens_out": 0, "cost_usd": 0.0},
|
||||
{
|
||||
"turns": 0,
|
||||
"tokens_in": 0,
|
||||
"tokens_out": 0,
|
||||
"cost_usd": 0.0,
|
||||
"cost_bases": [],
|
||||
},
|
||||
)
|
||||
daily_bucket["turns"] = int(daily_bucket["turns"]) + 1
|
||||
daily_bucket["tokens_in"] = int(daily_bucket["tokens_in"]) + turn_tokens_in
|
||||
|
|
@ -1126,22 +1283,12 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
daily_bucket["cost_usd"] = (
|
||||
float(daily_bucket["cost_usd"]) + turn_breakdown.cost_usd
|
||||
)
|
||||
daily_cost_bases = cast(
|
||||
list[UsageCostBasis], daily_bucket["cost_bases"]
|
||||
)
|
||||
daily_cost_bases.append(turn_breakdown.cost_basis)
|
||||
|
||||
by_provider = [
|
||||
_usage_breakdown(
|
||||
provider=provider,
|
||||
model=model,
|
||||
turns=int(values["turns"]),
|
||||
token_metered_turns=int(values["token_metered_turns"]),
|
||||
token_unmetered_turns=int(values["token_unmetered_turns"]),
|
||||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
stored_cost_usd=float(values["stored_cost_usd"]),
|
||||
unpriced_tokens_in=int(values["unpriced_tokens_in"]),
|
||||
unpriced_tokens_out=int(values["unpriced_tokens_out"]),
|
||||
)
|
||||
for (provider, model), values in buckets.items()
|
||||
]
|
||||
by_provider = _merge_usage_breakdowns(turn_breakdowns)
|
||||
by_provider.sort(
|
||||
key=lambda item: (
|
||||
-item.cost_usd,
|
||||
|
|
@ -1153,6 +1300,9 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
)
|
||||
|
||||
total_cost = round(cost_usd, 6)
|
||||
total_cost_basis = _aggregate_cost_basis(
|
||||
item.cost_basis for item in turn_breakdowns
|
||||
)
|
||||
return AdminUsageResponse(
|
||||
source="server_session_registry",
|
||||
durable=False,
|
||||
|
|
@ -1167,7 +1317,8 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
cost_usd=total_cost,
|
||||
recorded_cost_usd=round(recorded_cost_usd, 6),
|
||||
estimated_cost_usd=round(estimated_cost_usd, 6),
|
||||
budget=_usage_budget(total_cost),
|
||||
cost_basis=total_cost_basis,
|
||||
budget=_usage_budget(total_cost, total_cost_basis),
|
||||
evaluator_cache=_usage_evaluator_cache(),
|
||||
by_provider=by_provider[:12],
|
||||
daily_cost=[
|
||||
|
|
@ -1177,6 +1328,9 @@ def _usage_from_runtime_store(window_days: int) -> AdminUsageResponse:
|
|||
tokens_in=int(values["tokens_in"]),
|
||||
tokens_out=int(values["tokens_out"]),
|
||||
cost_usd=round(float(values["cost_usd"]), 6),
|
||||
cost_basis=_aggregate_cost_basis(
|
||||
cast(list[UsageCostBasis], values["cost_bases"])
|
||||
),
|
||||
)
|
||||
for day, values in sorted(daily_buckets.items())
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue