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": "已被取代",
|
"superseded": "已被取代",
|
||||||
"stalled": "已停滞",
|
"stalled": "已停滞",
|
||||||
"Resume": "恢复",
|
"Resume": "恢复",
|
||||||
|
"Spent": "已花费",
|
||||||
"Tokens": "Token",
|
"Tokens": "Token",
|
||||||
"not reported": "未报告",
|
"not reported": "未报告",
|
||||||
"{input} in · {output} out": "输入 {input} · 输出 {output}",
|
"{input} in · {output} out": "输入 {input} · 输出 {output}",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Avatar, Pill } from "../../shared/ui";
|
import { Avatar, Pill } from "../../shared/ui";
|
||||||
import type { AgentRecord, HiringPolicyRecord, ProviderRecord, ProviderRequestRecord } from "../../../../shared/domain";
|
import type { AgentRecord, HiringPolicyRecord, ProviderRecord, ProviderRequestRecord } from "../../../../shared/domain";
|
||||||
import { money } from "../types";
|
import { money } from "../types";
|
||||||
|
import { buildAgentSpend } from "./billing-helpers";
|
||||||
import { t } from "../../i18n/i18n";
|
import { t } from "../../i18n/i18n";
|
||||||
|
|
||||||
export function BillingPanel({
|
export function BillingPanel({
|
||||||
@@ -24,7 +25,7 @@ export function BillingPanel({
|
|||||||
const reasoningTokens = sum((r) => r.reasoningTokens);
|
const reasoningTokens = sum((r) => r.reasoningTokens);
|
||||||
const unreported = monthRequests.filter((r) => r.inputTokens === null).length;
|
const unreported = monthRequests.filter((r) => r.inputTokens === null).length;
|
||||||
const failedRequests = monthRequests.filter((r) => r.state === "failed");
|
const failedRequests = monthRequests.filter((r) => r.state === "failed");
|
||||||
const agentSpend = buildAgentSpend(agents, providers, monthRequests);
|
const agentSpend = buildAgentSpend(agents, monthRequests);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -62,7 +63,7 @@ export function BillingPanel({
|
|||||||
<div className="section-label" style={{ marginBottom: 10 }}>{t("Cost by agent")}</div>
|
<div className="section-label" style={{ marginBottom: 10 }}>{t("Cost by agent")}</div>
|
||||||
<div className="card" style={{ padding: 0, overflow: "hidden", marginBottom: 20 }}>
|
<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 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>
|
</div>
|
||||||
{agentSpend.map((row, i) => (
|
{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)" }}>
|
<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>
|
||||||
<div className="num mono">{money(row.agent.budgetCents)}</div>
|
<div className="num mono">{money(row.agent.budgetCents)}</div>
|
||||||
<div className="num mono">{row.requestCount}</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.tokens.toLocaleString()}</div>
|
||||||
<div className="num mono" style={{ fontSize: 12 }}>{row.outputChars.toLocaleString()}</div>
|
<div className="num mono" style={{ fontSize: 12 }}>${(row.spentCents / 100).toFixed(2)}</div>
|
||||||
</div>
|
</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 {
|
function withinDays(iso: string, days: number): boolean {
|
||||||
return Date.now() - new Date(iso).getTime() < days * 24 * 60 * 60 * 1000;
|
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