Q73: agent cost counts each call once, for the agent that made it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 12:29:26 -04:00
1 parent 90b7306aee
commit 844607eae9
4 files changed
+45 -26

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ export const zh: Record<string, string> = {
"superseded": "已被取代",
"stalled": "已停滞",
"Resume": "恢复",
"Spent": "已花费",
"Tokens": "Token",
"not reported": "未报告",
"{input} in · {output} out": "输入 {input} · 输出 {output}",
@@ -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 (
<div>
@@ -62,7 +63,7 @@ export function BillingPanel({
<div className="section-label" style={{ marginBottom: 10 }}>{t("Cost by agent")}</div>
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 20 }}>
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)" }}>
<div>{t("Agent")}</div><div>{t("Budget")}</div><div>{t("Requests")}</div><div>{t("Input")}</div><div>{t("Output")}</div>
<div>{t("Agent")}</div><div>{t("Budget")}</div><div>{t("Requests")}</div><div>{t("Tokens")}</div><div>{t("Spent")}</div>
</div>
{agentSpend.map((row, i) => (
<div key={row.agent.id} className="table-row" style={{ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)", borderBottom: i === agentSpend.length - 1 ? "none" : "1px solid var(--border-soft)" }}>
@@ -72,8 +73,8 @@ export function BillingPanel({
</div>
<div className="num mono">{money(row.agent.budgetCents)}</div>
<div className="num mono">{row.requestCount}</div>
<div className="num mono" style={{ fontSize: 12 }}>{row.inputChars.toLocaleString()}</div>
<div className="num mono" style={{ fontSize: 12 }}>{row.outputChars.toLocaleString()}</div>
<div className="num mono" style={{ fontSize: 12 }}>{row.tokens.toLocaleString()}</div>
<div className="num mono" style={{ fontSize: 12 }}>${(row.spentCents / 100).toFixed(2)}</div>
</div>
))}
</div>
@@ -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;
}
@@ -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);
}
+18
View File
@@ -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],
]);
});
});