관리자 증거와 계정 흐름을 정돈
This commit is contained in:
parent
21bbe3f98d
commit
34aff65cb0
9 changed files with 1100 additions and 193 deletions
|
|
@ -45,6 +45,15 @@ interface AdminUsersResponse {
|
|||
users: AdminManagedUser[];
|
||||
}
|
||||
|
||||
type UsageCostBasis =
|
||||
| "provider_estimate"
|
||||
| "provider_reported"
|
||||
| "reference_rate"
|
||||
| "reference_upper_bound"
|
||||
| "partial"
|
||||
| "partial_upper_bound"
|
||||
| "unavailable";
|
||||
|
||||
interface AdminUsageBreakdown {
|
||||
provider: string;
|
||||
model: string;
|
||||
|
|
@ -54,6 +63,11 @@ interface AdminUsageBreakdown {
|
|||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
recorded_cost_usd?: number;
|
||||
estimated_cost_usd?: number;
|
||||
cost_basis?: UsageCostBasis;
|
||||
rate_label?: string | null;
|
||||
rate_source_url?: string | null;
|
||||
}
|
||||
|
||||
interface AdminUsageDailyCost {
|
||||
|
|
@ -62,13 +76,15 @@ interface AdminUsageDailyCost {
|
|||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
cost_basis?: UsageCostBasis;
|
||||
}
|
||||
|
||||
interface AdminUsageBudget {
|
||||
limit_usd: number;
|
||||
used_ratio: number;
|
||||
remaining_usd: number | null;
|
||||
status: "disabled" | "ok" | "warn" | "exceeded";
|
||||
status: "disabled" | "ok" | "warn" | "exceeded" | "indeterminate";
|
||||
cost_basis?: UsageCostBasis;
|
||||
}
|
||||
|
||||
interface AdminUsageEvaluatorCache {
|
||||
|
|
@ -94,6 +110,9 @@ interface AdminUsageResponse {
|
|||
tokens_in: number;
|
||||
tokens_out: number;
|
||||
cost_usd: number;
|
||||
recorded_cost_usd?: number;
|
||||
estimated_cost_usd?: number;
|
||||
cost_basis?: UsageCostBasis;
|
||||
budget: AdminUsageBudget;
|
||||
evaluator_cache?: AdminUsageEvaluatorCache;
|
||||
by_provider: AdminUsageBreakdown[];
|
||||
|
|
@ -927,20 +946,22 @@ test.describe("admin route guards", () => {
|
|||
durable: true,
|
||||
generated_at: 1_783_990_800,
|
||||
window_days: 30,
|
||||
total_turns: 37,
|
||||
metered_turns: 37,
|
||||
token_metered_turns: 35,
|
||||
total_turns: 53,
|
||||
metered_turns: 53,
|
||||
token_metered_turns: 51,
|
||||
token_unmetered_turns: 2,
|
||||
tokens_in: 160_703,
|
||||
tokens_out: 19_629,
|
||||
cost_usd: 7.023222,
|
||||
tokens_in: 240_950,
|
||||
tokens_out: 22_907,
|
||||
cost_usd: 7.064689,
|
||||
recorded_cost_usd: 6.9612,
|
||||
estimated_cost_usd: 0.062022,
|
||||
estimated_cost_usd: 0.103489,
|
||||
cost_basis: "reference_upper_bound",
|
||||
budget: {
|
||||
limit_usd: 20,
|
||||
used_ratio: 0.3512,
|
||||
remaining_usd: 12.976778,
|
||||
used_ratio: 0.3532,
|
||||
remaining_usd: 12.935311,
|
||||
status: "ok",
|
||||
cost_basis: "reference_upper_bound",
|
||||
},
|
||||
evaluator_cache: {
|
||||
enabled: true,
|
||||
|
|
@ -981,24 +1002,45 @@ test.describe("admin route guards", () => {
|
|||
},
|
||||
{
|
||||
provider: "agy_cli",
|
||||
model: "gemini-3.6-flash-high",
|
||||
turns: 5,
|
||||
token_metered_turns: 5,
|
||||
model: "gemini-3.7-flash-high",
|
||||
turns: 21,
|
||||
token_metered_turns: 21,
|
||||
token_unmetered_turns: 0,
|
||||
tokens_in: 35_703,
|
||||
tokens_out: 1_129,
|
||||
cost_usd: 0.062022,
|
||||
tokens_in: 115_950,
|
||||
tokens_out: 4_407,
|
||||
cost_usd: 0.103489,
|
||||
recorded_cost_usd: 0,
|
||||
estimated_cost_usd: 0.062022,
|
||||
cost_basis: "reference_rate",
|
||||
estimated_cost_usd: 0.103489,
|
||||
cost_basis: "reference_upper_bound",
|
||||
rate_label:
|
||||
"Google Gemini 3.6 Flash 표준 단가 · 입력 $1.50/M · 캐시 $0.15/M · 출력 $7.50/M",
|
||||
"Google Gemini 3.7 Flash 프로모션 표준 단가(2026-12-31까지) · 입력 $0.75/M · 캐시 $0.075/M · 출력 $3.75/M",
|
||||
},
|
||||
],
|
||||
daily_cost: [
|
||||
{ day: "2026-07-13", turns: 8, tokens_in: 32_000, tokens_out: 4_800, cost_usd: 1.42 },
|
||||
{ day: "2026-07-14", turns: 10, tokens_in: 41_000, tokens_out: 6_100, cost_usd: 2.08 },
|
||||
{ day: "2026-07-15", turns: 17, tokens_in: 87_703, tokens_out: 8_729, cost_usd: 3.183222 },
|
||||
{
|
||||
day: "2026-07-13",
|
||||
turns: 8,
|
||||
tokens_in: 32_000,
|
||||
tokens_out: 4_800,
|
||||
cost_usd: 1.42,
|
||||
cost_basis: "provider_reported",
|
||||
},
|
||||
{
|
||||
day: "2026-07-14",
|
||||
turns: 10,
|
||||
tokens_in: 41_000,
|
||||
tokens_out: 6_100,
|
||||
cost_usd: 2.08,
|
||||
cost_basis: "provider_reported",
|
||||
},
|
||||
{
|
||||
day: "2026-07-15",
|
||||
turns: 35,
|
||||
tokens_in: 167_950,
|
||||
tokens_out: 12_007,
|
||||
cost_usd: 3.564689,
|
||||
cost_basis: "reference_upper_bound",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -1014,17 +1056,21 @@ test.describe("admin route guards", () => {
|
|||
);
|
||||
await expect(page.getByText("운영 DB 원장").first()).toBeVisible();
|
||||
// 2026-07-27 D7: 합계 금액은 화면에 소수 2자리로 표시하고 원본 정밀도는 title 로 옮겼다.
|
||||
await expect(page.locator(".aic-ledger")).toContainText("$7.02");
|
||||
await expect(page.locator(".aic-ledger b").first()).toHaveAttribute("title", /7\.023222/);
|
||||
await expect(page.locator(".aic-budget")).toContainText("94.6%");
|
||||
await expect(page.locator(".aic-ledger")).toContainText("≤$7.06");
|
||||
await expect(page.locator(".aic-ledger b").first()).toHaveAttribute("title", /≤\$7\.064689/);
|
||||
await expect(page.locator(".aic-budget")).toContainText("잔여 ≥$12.94 · ≤35.3% 사용");
|
||||
await expect(page.locator(".aic-table")).toContainText("gpt-5-mini");
|
||||
await expect(page.locator(".aic-table")).toContainText("gemini-3.6-flash-high");
|
||||
await expect(page.locator(".aic-table")).toContainText("참조단가");
|
||||
await expect(page.locator(".aic-table")).toContainText("gemini-3.7-flash-high");
|
||||
await expect(page.locator(".aic-table")).toContainText("참조 상한");
|
||||
await expect(page.locator(".aic-table")).toContainText("≤$0.10");
|
||||
await expect(page.locator(".aic-table")).toContainText("≤$0.0049");
|
||||
await expect(page.locator(".aic-table")).toContainText("SDK 추정");
|
||||
await expect(page.locator(".aic-table")).toContainText("미계량");
|
||||
await expect(page.locator(".aic-table")).toContainText("$0.06");
|
||||
await expect(page.locator(".aic-table")).toContainText("0.9%");
|
||||
await expect(page.locator(".aic-ledger")).toContainText("기록 $6.96 · 참조 $0.06");
|
||||
await expect(page.locator(".aic-table")).toContainText("$0.10");
|
||||
await expect(page.locator(".aic-table")).toContainText("$0.0049");
|
||||
await expect(page.locator(".aic-table tbody tr").first().locator("td").nth(6)).toHaveText("—");
|
||||
await expect(page.locator(".aic-ledger")).toContainText("기록 $6.96 · 참조 ≤$0.10 · 참조 상한");
|
||||
await expect(page.locator(".aic-chart__day").last()).toContainText("≤$3.56");
|
||||
await expect(page.locator(".aic-cache-score")).toContainText("80%");
|
||||
await expect(page.getByLabel("AI 기본 모델")).toHaveValue("gateway-default");
|
||||
await expect(page.getByLabel("AI 엔진 공급자").locator("option")).toHaveCount(6);
|
||||
|
|
@ -1072,6 +1118,95 @@ test.describe("admin route guards", () => {
|
|||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("shows partial AI cost without inventing a per-call average", async ({ page }) => {
|
||||
await mockAdminSession(
|
||||
page,
|
||||
{
|
||||
user_id: "partial-cost-admin",
|
||||
email: "partial-cost-admin@twentyoz.kr",
|
||||
display_name: "Partial Cost Admin",
|
||||
role: "admin",
|
||||
admin_access: true,
|
||||
super_admin: true,
|
||||
onboarding_completed_at: 1_782_900_000,
|
||||
},
|
||||
{
|
||||
adminUsage: {
|
||||
source: "database",
|
||||
durable: true,
|
||||
generated_at: 1_783_990_800,
|
||||
window_days: 30,
|
||||
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: 4.5,
|
||||
recorded_cost_usd: 0,
|
||||
estimated_cost_usd: 4.5,
|
||||
cost_basis: "partial",
|
||||
budget: {
|
||||
limit_usd: 20,
|
||||
used_ratio: 0.225,
|
||||
remaining_usd: 15.5,
|
||||
status: "indeterminate",
|
||||
cost_basis: "partial",
|
||||
},
|
||||
evaluator_cache: {
|
||||
enabled: false,
|
||||
entries: 0,
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
stores: 0,
|
||||
evictions: 0,
|
||||
requests: 0,
|
||||
hit_rate: 0,
|
||||
},
|
||||
by_provider: [
|
||||
{
|
||||
provider: "agy_cli",
|
||||
model: "gemini-3.7-flash-high",
|
||||
turns: 2,
|
||||
token_metered_turns: 2,
|
||||
token_unmetered_turns: 0,
|
||||
tokens_in: 2_000_000,
|
||||
tokens_out: 2_000_000,
|
||||
cost_usd: 4.5,
|
||||
recorded_cost_usd: 0,
|
||||
estimated_cost_usd: 4.5,
|
||||
cost_basis: "partial",
|
||||
rate_label: "일부 호출 미산정 · 표시액은 산정 가능분 합계",
|
||||
},
|
||||
],
|
||||
daily_cost: [
|
||||
{
|
||||
day: "2026-08-13",
|
||||
turns: 2,
|
||||
tokens_in: 2_000_000,
|
||||
tokens_out: 2_000_000,
|
||||
cost_usd: 4.5,
|
||||
cost_basis: "partial",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto("/admin/ai");
|
||||
|
||||
const row = page.locator(".aic-table tbody tr").filter({ hasText: "gemini-3.7-flash-high" });
|
||||
await expect(row).toContainText("$4.50+");
|
||||
await expect(row).toContainText("일부 산정");
|
||||
await expect(row.locator("td").nth(5)).toHaveText("—");
|
||||
await expect(row.locator("td").nth(6)).toHaveText("—");
|
||||
await expect(page.locator(".aic-ledger b").first()).toHaveText("$4.50+");
|
||||
await expect(page.locator(".aic-ledger article").last().locator("b")).toHaveText("—");
|
||||
await expect(page.locator(".aic-budget")).toContainText("예산 미확정");
|
||||
await expect(page.locator(".aic-budget")).toContainText("잔여 ≤$15.50 · ≥22.5% 사용");
|
||||
await expect(page.locator(".aic-chart__value")).toHaveText("$4.50+");
|
||||
});
|
||||
|
||||
test("keeps the boot diagnostic overlay off a rendered AI operations page", async ({ page }) => {
|
||||
await mockAdminSession(page, {
|
||||
user_id: "ai-watchdog-admin",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue