From 844607eae9f5c813b4055c76aa4762fcfcfc8298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 12:29:26 -0400 Subject: [PATCH] Q73: agent cost counts each call once, for the agent that made it Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/i18n/zh.ts | 1 + .../src/pages/settings/BillingPanel.tsx | 31 +++---------------- .../src/pages/settings/billing-helpers.ts | 21 +++++++++++++ tests/unit/billing-helpers.test.ts | 18 +++++++++++ 4 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 src/renderer/src/pages/settings/billing-helpers.ts create mode 100644 tests/unit/billing-helpers.test.ts diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 2f7dcc68..0ccf3b34 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -32,6 +32,7 @@ export const zh: Record = { "superseded": "已被取代", "stalled": "已停滞", "Resume": "恢复", + "Spent": "已花费", "Tokens": "Token", "not reported": "未报告", "{input} in · {output} out": "输入 {input} · 输出 {output}", diff --git a/src/renderer/src/pages/settings/BillingPanel.tsx b/src/renderer/src/pages/settings/BillingPanel.tsx index d01a3eb6..349181f4 100644 --- a/src/renderer/src/pages/settings/BillingPanel.tsx +++ b/src/renderer/src/pages/settings/BillingPanel.tsx @@ -1,6 +1,7 @@ import { Avatar, Pill } from "../../shared/ui"; import type { AgentRecord, HiringPolicyRecord, ProviderRecord, ProviderRequestRecord } from "../../../../shared/domain"; import { money } from "../types"; +import { buildAgentSpend } from "./billing-helpers"; import { t } from "../../i18n/i18n"; export function BillingPanel({ @@ -24,7 +25,7 @@ export function BillingPanel({ const reasoningTokens = sum((r) => r.reasoningTokens); const unreported = monthRequests.filter((r) => r.inputTokens === null).length; const failedRequests = monthRequests.filter((r) => r.state === "failed"); - const agentSpend = buildAgentSpend(agents, providers, monthRequests); + const agentSpend = buildAgentSpend(agents, monthRequests); return (
@@ -62,7 +63,7 @@ export function BillingPanel({
{t("Cost by agent")}
-
{t("Agent")}
{t("Budget")}
{t("Requests")}
{t("Input")}
{t("Output")}
+
{t("Agent")}
{t("Budget")}
{t("Requests")}
{t("Tokens")}
{t("Spent")}
{agentSpend.map((row, i) => (
@@ -72,8 +73,8 @@ export function BillingPanel({
{money(row.agent.budgetCents)}
{row.requestCount}
-
{row.inputChars.toLocaleString()}
-
{row.outputChars.toLocaleString()}
+
{row.tokens.toLocaleString()}
+
${(row.spentCents / 100).toFixed(2)}
))}
@@ -113,28 +114,6 @@ function StatTile({ label, value, tone }: { label: string; value: string; tone?: ); } -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; } diff --git a/src/renderer/src/pages/settings/billing-helpers.ts b/src/renderer/src/pages/settings/billing-helpers.ts new file mode 100644 index 00000000..401d95f4 --- /dev/null +++ b/src/renderer/src/pages/settings/billing-helpers.ts @@ -0,0 +1,21 @@ +import type { AgentRecord, ProviderRequestRecord } from "../../../../shared/domain"; + +export interface AgentSpendRow { + agent: AgentRecord; + requestCount: number; + tokens: number; + spentCents: number; +} + +/** Each call counts once, for the agent that made it, whatever provider the agent uses now. */ +export function buildAgentSpend(agents: AgentRecord[], requests: ProviderRequestRecord[]): AgentSpendRow[] { + return agents.map((agent) => { + const agentRequests = requests.filter((r) => r.agentId === agent.id); + return { + agent, + requestCount: agentRequests.length, + tokens: agentRequests.reduce((s, r) => s + (r.inputTokens ?? 0) + (r.outputTokens ?? 0), 0), + spentCents: agentRequests.reduce((s, r) => s + (r.costCents ?? 0), 0), + }; + }).sort((a, b) => b.spentCents - a.spentCents || b.requestCount - a.requestCount); +} diff --git a/tests/unit/billing-helpers.test.ts b/tests/unit/billing-helpers.test.ts new file mode 100644 index 00000000..be420053 --- /dev/null +++ b/tests/unit/billing-helpers.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import { buildAgentSpend } from "../../src/renderer/src/pages/settings/billing-helpers"; +import type { AgentRecord, ProviderRequestRecord } from "../../src/shared/domain"; + +const agent = (id: string, modelProviderId: string) => ({ id, name: id, modelProviderId, budgetCents: 1000 }) as AgentRecord; +const call = (agentId: string | null, providerId: string, costCents: number | null) => + ({ agentId, providerId, inputTokens: 100, outputTokens: 50, costCents }) as ProviderRequestRecord; + +describe("billing attribution", () => { + test("each call counts once for the agent that made it, not for everyone on the same provider", () => { + const rows = buildAgentSpend([agent("ada", "prv_a"), agent("bo", "prv_a"), agent("cy", "prv_b")], [ + call("ada", "prv_a", 10), call("ada", "prv_a", 5), call("bo", "prv_a", 1), call("cy", "prv_a", 2), call(null, "prv_a", 99), + ]); + expect(rows.map((row) => [row.agent.id, row.requestCount, row.tokens, row.spentCents])).toEqual([ + ["ada", 2, 300, 15], ["cy", 1, 150, 2], ["bo", 1, 150, 1], + ]); + }); +});