관리자 사용량 근거를 정교화

This commit is contained in:
Yun Chan 2026-08-29 23:59:17 +09:00
parent ccdcfcd2f5
commit 707dba4f8f
10 changed files with 1136 additions and 209 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import unittest
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from fastapi import HTTPException
@ -53,6 +54,11 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
self.fetch_calls = 0
async def fetchrow(self, query, *args, **kwargs):
case.assertIn("LOWER(BTRIM(COALESCE(llm_provider, ''))) = 'e2e'", query)
case.assertIn("LOWER(BTRIM(COALESCE(model, ''))) = 'fake-client'", query)
case.assertIn("COALESCE(tokens_in, 0) = 1", query)
case.assertIn("COALESCE(tokens_out, 0) = 1", query)
case.assertIn("COALESCE(cost_usd, 0) = 0", query)
return {
"total_turns": 2,
"metered_turns": 1,
@ -65,28 +71,15 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
async def fetch(self, query, *args, **kwargs):
self.fetch_calls += 1
if self.fetch_calls == 2:
case.assertIn("date_trunc('day', created_at)", query)
return [
{
"day": "2026-06-28",
"turns": 1,
"token_metered_turns": 1,
"token_unmetered_turns": 0,
"tokens_in": 11,
"tokens_out": 13,
"cost_usd": 0.0042,
"unpriced_turns": 0,
"unpriced_tokens_in": 0,
"unpriced_tokens_out": 0,
"provider": "claude_cli",
"model": "gateway-default",
}
]
case.assertIn("unpriced_tokens_in", query)
case.assertEqual(self.fetch_calls, 1)
case.assertIn("date_trunc('day', created_at AT TIME ZONE 'UTC')", query)
case.assertNotIn("ORDER BY created_at, id", query)
case.assertIn("COUNT(*)::bigint AS matching_turns", query)
case.assertIn("GROUP BY 1, 2, 3, 4, 5, 6", query)
case.assertNotIn("LIMIT 12", query)
return [
{
"day": "2026-06-28",
"provider": "claude_cli",
"model": "gateway-default",
"turns": 1,
@ -95,6 +88,7 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
"tokens_in": 11,
"tokens_out": 13,
"cost_usd": 0.0042,
"matching_turns": 1,
"unpriced_turns": 0,
"unpriced_tokens_in": 0,
"unpriced_tokens_out": 0,
@ -116,6 +110,8 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(usage.token_unmetered_turns, 0)
self.assertEqual(usage.by_provider[0].provider, "claude_cli")
self.assertEqual(usage.by_provider[0].cost_basis, "provider_estimate")
self.assertEqual(usage.cost_basis, "provider_estimate")
self.assertEqual(usage.daily_cost[0].cost_basis, "provider_estimate")
self.assertEqual(usage.recorded_cost_usd, 0.0042)
self.assertEqual(usage.estimated_cost_usd, 0)
self.assertTrue(usage.evaluator_cache.enabled)
@ -125,6 +121,8 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(usage.daily_cost[0].cost_usd, 0.0042)
async def test_usage_from_database_backfills_agy_zero_cost_with_reference_rate(self) -> None:
case = self
class Conn:
def __init__(self) -> None:
self.fetch_calls = 0
@ -142,22 +140,20 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
async def fetch(self, query, *args, **kwargs):
self.fetch_calls += 1
row = {
"provider": "agy_cli",
"model": "gemini-3.6-flash-high",
"turns": 5,
"token_metered_turns": 5,
"token_unmetered_turns": 0,
"tokens_in": 35_703,
"tokens_out": 1_129,
"cost_usd": 0,
"unpriced_turns": 5,
"unpriced_tokens_in": 35_703,
"unpriced_tokens_out": 1_129,
}
if self.fetch_calls == 2:
return [{"day": "2026-07-31", **row}]
return [row]
case.assertEqual(self.fetch_calls, 1)
token_pairs = [(7_141, 226)] * 4 + [(7_139, 225)]
return [
{
"day": "2026-07-31",
"provider": "agy_cli",
"model": "gemini-3.6-flash-high",
"tokens_in": tokens_in,
"tokens_out": tokens_out,
"cost_usd": 0,
"matching_turns": 1,
}
for tokens_in, tokens_out in token_pairs
]
with patch.object(admin_routes, "acquire", return_value=_Acquire(Conn())):
usage = await admin_routes._usage_from_database(window_days=30)
@ -168,9 +164,308 @@ class AdminOpsTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(usage.daily_cost[0].cost_usd, 0.062022)
breakdown = usage.by_provider[0]
self.assertEqual(breakdown.cost_usd, 0.062022)
self.assertEqual(breakdown.cost_basis, "reference_rate")
self.assertEqual(breakdown.cost_basis, "reference_upper_bound")
self.assertIn("Gemini 3.6 Flash", breakdown.rate_label or "")
async def test_usage_from_database_prices_gemini_37_on_its_usage_date(self) -> None:
class Conn:
async def fetchrow(self, query, *args, **kwargs):
return {
"total_turns": 21,
"metered_turns": 21,
"token_metered_turns": 21,
"token_unmetered_turns": 0,
"tokens_in": 115_950,
"tokens_out": 4_407,
"cost_usd": 0,
}
async def fetch(self, query, *args, **kwargs):
token_pairs = [(5_521, 210)] * 20 + [(5_530, 207)]
return [
{
"day": "2026-08-28",
"provider": "agy_cli",
"model": "gemini-3.7-flash-high",
"tokens_in": tokens_in,
"tokens_out": tokens_out,
"cost_usd": 0,
"matching_turns": 1,
}
for tokens_in, tokens_out in token_pairs
]
with patch.object(admin_routes, "acquire", return_value=_Acquire(Conn())):
usage = await admin_routes._usage_from_database(window_days=30)
self.assertEqual(usage.cost_usd, 0.103489)
self.assertEqual(usage.recorded_cost_usd, 0)
self.assertEqual(usage.estimated_cost_usd, 0.103489)
self.assertEqual(usage.daily_cost[0].cost_usd, 0.103489)
self.assertEqual(usage.cost_basis, "reference_upper_bound")
self.assertEqual(usage.daily_cost[0].cost_basis, "reference_upper_bound")
breakdown = usage.by_provider[0]
self.assertEqual(breakdown.cost_usd, 0.103489)
self.assertEqual(breakdown.cost_basis, "reference_upper_bound")
self.assertIn("Gemini 3.7 Flash", breakdown.rate_label or "")
async def test_usage_from_database_keeps_effective_rates_across_date_boundary(self) -> None:
class Conn:
async def fetchrow(self, query, *args, **kwargs):
return {
"total_turns": 2,
"metered_turns": 2,
"token_metered_turns": 2,
"token_unmetered_turns": 0,
"tokens_in": 2_000_000,
"tokens_out": 2_000_000,
"cost_usd": 0,
"matching_turns": 1,
}
async def fetch(self, query, *args, **kwargs):
base = {
"provider": "agy_cli",
"model": "gemini-3.6-flash-high",
"tokens_in": 1_000_000,
"tokens_out": 1_000_000,
"cost_usd": 0,
"matching_turns": 1,
}
return [
{"day": "2026-08-12", **base},
{"day": "2026-08-13", **base},
]
with patch.object(admin_routes, "acquire", return_value=_Acquire(Conn())):
usage = await admin_routes._usage_from_database(window_days=30)
self.assertEqual(usage.cost_usd, 13.5)
self.assertEqual(usage.estimated_cost_usd, 13.5)
self.assertEqual([item.cost_usd for item in usage.daily_cost], [9.0, 4.5])
self.assertEqual(len(usage.by_provider), 1)
self.assertEqual(usage.by_provider[0].cost_usd, 13.5)
self.assertIn("기간별", usage.by_provider[0].rate_label or "")
async def test_usage_from_database_prices_request_tiers_before_aggregation(self) -> None:
class Conn:
async def fetchrow(self, query, *args, **kwargs):
return {
"total_turns": 2,
"metered_turns": 2,
"token_metered_turns": 2,
"token_unmetered_turns": 0,
"tokens_in": 300_000,
"tokens_out": 20_000,
"cost_usd": 0,
}
async def fetch(self, query, *args, **kwargs):
return [
{
"day": "2026-08-28",
"provider": "agy_cli",
"model": "gemini-3.1-pro-high",
"tokens_in": 150_000,
"tokens_out": 10_000,
"cost_usd": 0,
"matching_turns": 2,
},
]
with patch.object(admin_routes, "acquire", return_value=_Acquire(Conn())):
database_usage = await admin_routes._usage_from_database(window_days=30)
observed_at = datetime(2026, 8, 28, 12, tzinfo=timezone.utc).timestamp()
runtime_turns = [
SimpleNamespace(
speaker="client",
created_at=observed_at,
llm_provider="agy_cli",
model="gemini-3.1-pro-high",
tokens_in=150_000,
tokens_out=10_000,
cost_usd=0,
)
for _ in range(2)
]
with (
patch.object(
admin_routes.store,
"list",
return_value=[SimpleNamespace(turns=runtime_turns)],
),
patch.object(admin_routes.time, "time", return_value=observed_at + 1),
):
runtime_usage = admin_routes._usage_from_runtime_store(window_days=30)
self.assertEqual(database_usage.cost_usd, 0.84)
self.assertEqual(database_usage.by_provider[0].cost_usd, 0.84)
self.assertEqual(database_usage.daily_cost[0].cost_usd, 0.84)
self.assertEqual(runtime_usage.cost_usd, database_usage.cost_usd)
self.assertEqual(runtime_usage.by_provider[0].cost_usd, 0.84)
async def test_usage_from_database_marks_mixed_pricing_as_partial(self) -> None:
class Conn:
async def fetchrow(self, query, *args, **kwargs):
return {
"total_turns": 2,
"metered_turns": 2,
"token_metered_turns": 2,
"token_unmetered_turns": 0,
"tokens_in": 2_000_000,
"tokens_out": 2_000_000,
"cost_usd": 0,
"matching_turns": 1,
}
async def fetch(self, query, *args, **kwargs):
base = {
"provider": "agy_cli",
"model": "gemini-3.7-flash-high",
"tokens_in": 1_000_000,
"tokens_out": 1_000_000,
"cost_usd": 0,
"matching_turns": 1,
}
return [
{"day": "2026-08-12", **base},
{"day": "2026-08-13", **base},
]
with patch.object(admin_routes, "acquire", return_value=_Acquire(Conn())):
usage = await admin_routes._usage_from_database(window_days=30)
self.assertEqual(usage.cost_usd, 4.5)
self.assertEqual(usage.cost_basis, "partial_upper_bound")
self.assertEqual(usage.budget.cost_basis, "partial_upper_bound")
self.assertEqual(usage.by_provider[0].cost_basis, "partial_upper_bound")
self.assertEqual(
usage.by_provider[0].rate_label,
"일부 호출 미산정 · 산정된 부분도 상한 추정",
)
self.assertEqual(usage.daily_cost[0].cost_basis, "unavailable")
self.assertEqual(usage.daily_cost[1].cost_basis, "reference_upper_bound")
def test_cost_basis_preserves_missing_and_upper_bound_dimensions(self) -> None:
self.assertEqual(
admin_routes._aggregate_cost_basis(
["reference_upper_bound", "unavailable"]
),
"partial_upper_bound",
)
self.assertEqual(
admin_routes._aggregate_cost_basis(["reference_rate", "unavailable"]),
"partial",
)
self.assertEqual(
admin_routes._aggregate_cost_basis(
["provider_reported", "reference_upper_bound"]
),
"reference_upper_bound",
)
def test_budget_is_indeterminate_when_cost_is_not_bounded_both_ways(self) -> None:
with patch.object(admin_routes.settings, "admin_usage_budget_usd", 10.0):
partial = admin_routes._usage_budget(4.5, "partial")
partial_upper = admin_routes._usage_budget(
4.5, "partial_upper_bound"
)
safe_upper = admin_routes._usage_budget(7.5, "reference_upper_bound")
self.assertEqual(partial.status, "indeterminate")
self.assertEqual(partial.remaining_usd, 5.5)
self.assertEqual(partial_upper.status, "indeterminate")
self.assertIsNone(partial_upper.remaining_usd)
self.assertEqual(safe_upper.status, "ok")
self.assertEqual(safe_upper.remaining_usd, 2.5)
def test_synthetic_e2e_signature_is_not_reportable_usage(self) -> None:
self.assertFalse(admin_routes._is_reportable_usage("e2e", "fake-client", 1, 1, 0))
self.assertFalse(
admin_routes._is_reportable_usage(" E2E ", " FAKE-CLIENT ", 1, 1, 0)
)
self.assertTrue(admin_routes._is_reportable_usage("e2e", "fake-client", 2, 1, 0))
self.assertTrue(admin_routes._is_reportable_usage("e2e", "real-client", 1, 1, 0))
self.assertTrue(admin_routes._is_reportable_usage("agy_cli", "fake-client", 1, 1, 0))
def test_stored_reference_cost_does_not_claim_a_recomputed_rate(self) -> None:
breakdown = admin_routes._usage_breakdown(
provider="agy_cli",
model="gemini-3.6-flash-high",
turns=1,
token_metered_turns=1,
token_unmetered_turns=0,
tokens_in=1_000_000,
tokens_out=0,
stored_cost_usd=1.5,
unpriced_tokens_in=0,
unpriced_tokens_out=0,
priced_at="2026-08-28",
)
self.assertEqual(breakdown.cost_usd, 1.5)
self.assertEqual(breakdown.cost_basis, "reference_rate")
self.assertEqual(breakdown.rate_label, "호출 시점에 저장된 참조단가 추정값")
self.assertIsNone(breakdown.rate_source_url)
def test_legacy_reference_fallback_is_labeled_as_an_upper_bound(self) -> None:
breakdown = admin_routes._usage_breakdown(
provider="agy_cli",
model="gemini-3.7-flash-high",
turns=1,
token_metered_turns=1,
token_unmetered_turns=0,
tokens_in=115_950,
tokens_out=4_407,
stored_cost_usd=0,
unpriced_tokens_in=115_950,
unpriced_tokens_out=4_407,
priced_at="2026-08-28",
)
self.assertEqual(breakdown.cost_basis, "reference_upper_bound")
self.assertIn("캐시 미보존", breakdown.rate_label or "")
def test_runtime_usage_prices_by_turn_date_and_excludes_synthetic_provider(self) -> None:
observed_at = datetime(2026, 8, 28, 12, tzinfo=timezone.utc).timestamp()
turns = [
SimpleNamespace(
speaker="client",
created_at=observed_at,
llm_provider="agy_cli",
model="gemini-3.7-flash-high",
tokens_in=115_950,
tokens_out=4_407,
cost_usd=0,
),
SimpleNamespace(
speaker="client",
created_at=observed_at,
llm_provider="e2e",
model="fake-client",
tokens_in=1,
tokens_out=1,
cost_usd=0,
),
]
with (
patch.object(
admin_routes.store,
"list",
return_value=[SimpleNamespace(turns=turns)],
),
patch.object(admin_routes.time, "time", return_value=observed_at + 1),
):
usage = admin_routes._usage_from_runtime_store(window_days=30)
self.assertEqual(usage.total_turns, 1)
self.assertEqual(usage.metered_turns, 1)
self.assertEqual(usage.tokens_in, 115_950)
self.assertEqual(usage.cost_usd, 0.103489)
self.assertEqual(len(usage.by_provider), 1)
self.assertEqual(usage.by_provider[0].model, "gemini-3.7-flash-high")
async def test_uptime_from_database_aggregates_health_samples(self) -> None:
now = datetime.now(timezone.utc)