From 157a677a31f02c9b2416e8fdc615eb1a452ac43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 7 Jun 2026 18:11:00 -0400 Subject: [PATCH] Expand billing panel with cost breakdown and budget utilization BillingPanel now shows: stat tiles (allocation, cap, requests, failures), token usage summary (input/output chars), budget utilization progress bar, per-agent cost breakdown table (budget, requests, I/O chars), and per-provider activity table. Replaced the minimal single-table view. --- .../src/renderer/src/pages/SettingsPage.tsx | 2 +- .../src/pages/settings/BillingPanel.tsx | 125 ++++++++++++++---- 2 files changed, 99 insertions(+), 28 deletions(-) diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 8081ab10..3a1f4b4d 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -133,7 +133,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) { roleGrants={roleGrants} /> )} - {tab === "billing" && } + {tab === "billing" && } {tab === "models" && } {tab === "providers" && ( sum + agent.budgetCents, 0); - const monthRequests = providerRequests.filter((request) => withinDays(request.createdAt, 30)); + const totalBudget = agents.reduce((sum, a) => sum + a.budgetCents, 0); + const totalCap = hiringPolicies.reduce((sum, p) => sum + p.monthlyBudgetLimitCents, 0); + const monthRequests = providerRequests.filter((r) => withinDays(r.createdAt, 30)); + const totalInputChars = monthRequests.reduce((s, r) => s + r.inputCharCount, 0); + const totalOutputChars = monthRequests.reduce((s, r) => s + r.outputCharCount, 0); + const failedRequests = monthRequests.filter((r) => r.state === "failed"); + const agentSpend = useMemo(() => buildAgentSpend(agents, providers, monthRequests), [agents, providers, monthRequests]); + return (

Billing

-

Allocations, provider activity, and budget windows.

-
-
Monthly allocation
-
{money(totalBudget)}
-
across {agents.length} {agents.length === 1 ? "agent" : "agents"}
+

Allocations, spend, and cost breakdown.

+ +
+ + 0 ? money(totalCap) : "—"} /> + + 0 ? "red" : undefined} />
-
Activity (30 days)
+
+
+
Token usage (30d)
+
+ Input chars{totalInputChars.toLocaleString()} + Output chars{totalOutputChars.toLocaleString()} + Total{(totalInputChars + totalOutputChars).toLocaleString()} +
+
+
+
Budget utilization
+
+
0 ? Math.min(100, Math.round((totalBudget / totalCap) * 100)) : 0}%`, height: "100%", background: "var(--accent)" }} /> +
+
+ {totalCap > 0 ? `${Math.round((totalBudget / totalCap) * 100)}% of cap` : "No budget cap set"} +
+
+
+ +
Cost by agent
+
+
+
Agent
Budget
Requests
Input
Output
+
+ {agentSpend.map((row, i) => ( +
+
+ + {row.agent.name} +
+
{money(row.agent.budgetCents)}
+
{row.requestCount}
+
{row.inputChars.toLocaleString()}
+
{row.outputChars.toLocaleString()}
+
+ ))} +
+ +
Provider activity (30d)
-
+
Provider
Requests
Errors
Agents
{providers.map((provider, index) => { - const requests = monthRequests.filter((request) => request.providerId === provider.id); - const errors = requests.filter((request) => request.state === "failed").length; - const boundAgents = agents.filter((agent) => agent.modelProviderId === provider.id).length; - const last = index === providers.length - 1; + const requests = monthRequests.filter((r) => r.providerId === provider.id); + const errors = requests.filter((r) => r.state === "failed").length; + const boundAgents = agents.filter((a) => a.modelProviderId === provider.id).length; return ( -
+
{provider.label}
- {provider.model && ( -
{provider.model}
- )} + {provider.model &&
{provider.model}
}
{requests.length}
{errors > 0 ? {errors} : 0}
@@ -59,6 +99,37 @@ export function BillingPanel({ ); } +function StatTile({ label, value, tone }: { label: string; value: string; tone?: "red" }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +interface AgentSpendRow { + agent: AgentRecord; + requestCount: number; + inputChars: number; + outputChars: number; +} + +function buildAgentSpend(agents: AgentRecord[], providers: ProviderRecord[], requests: ProviderRequestRecord[]): AgentSpendRow[] { + return agents.map((agent) => { + const agentRequests = requests.filter((r) => { + if (!agent.modelProviderId) return false; + return r.providerId === agent.modelProviderId; + }); + return { + agent, + requestCount: agentRequests.length, + inputChars: agentRequests.reduce((s, r) => s + r.inputCharCount, 0), + outputChars: agentRequests.reduce((s, r) => s + r.outputCharCount, 0), + }; + }).sort((a, b) => b.requestCount - a.requestCount); +} + function withinDays(iso: string, days: number): boolean { return Date.now() - new Date(iso).getTime() < days * 24 * 60 * 60 * 1000; }