관리자 사용량 근거를 정교화
This commit is contained in:
parent
ccdcfcd2f5
commit
707dba4f8f
10 changed files with 1136 additions and 209 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue