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:
1 parent
90b7306aee
commit
844607eae9
4 files changed
+45
-26
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user