Q74: billing shows money spent against allocations, not an allocation ratio
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
844607eae9
commit
daea9797d4
7 files changed
+38
-4
No files matched your search
@@ -8,6 +8,7 @@ type Row = Record<string, unknown>;
|
||||
type ProviderUsageWindowRow = Pick<
|
||||
AppSnapshot["providerRateLimits"][number],
|
||||
"orgId" | "providerId" | "windowStart" | "windowEnd" | "requestCount" | "inputCharCount" | "outputCharCount"
|
||||
| "costCents" | "inputTokens" | "outputTokens" | "requestsWithoutUsage" | "unpricedRequests"
|
||||
>;
|
||||
|
||||
function all<T>(statement: StatementSync): T[] {
|
||||
@@ -224,6 +225,11 @@ function buildProviderRateLimits(
|
||||
outputCharCount,
|
||||
remainingRequestCount,
|
||||
exhausted: remainingRequestCount === 0,
|
||||
costCents: usage?.costCents ?? 0,
|
||||
inputTokens: usage?.inputTokens ?? 0,
|
||||
outputTokens: usage?.outputTokens ?? 0,
|
||||
requestsWithoutUsage: usage?.requestsWithoutUsage ?? 0,
|
||||
unpricedRequests: usage?.unpricedRequests ?? 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -32,6 +32,10 @@ export const zh: Record<string, string> = {
|
||||
"superseded": "已被取代",
|
||||
"stalled": "已停滞",
|
||||
"Resume": "恢复",
|
||||
"Spent this month": "本月已花费",
|
||||
"${spent} of ${allocated} allocated to agents": "已花 ${spent} / 分配给 Agent 的 ${allocated}",
|
||||
"{count} calls not priced": "{count} 次调用未计价",
|
||||
"Allocations use {percent}% of department caps": "分配额度占部门上限的 {percent}%",
|
||||
"Spent": "已花费",
|
||||
"Tokens": "Token",
|
||||
"not reported": "未报告",
|
||||
|
||||
@@ -67,6 +67,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
catalog={snapshot.providerCatalog}
|
||||
workspaces={workspaces}
|
||||
providerRequests={providerRequests}
|
||||
providerRateLimits={providerRateLimits}
|
||||
hiringPolicies={hiringPolicies}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ProviderRateLimitStateRecord } from "../../../../shared/provider-runtime-domain";
|
||||
import type {
|
||||
AgentRecord,
|
||||
HiringPolicyRecord,
|
||||
@@ -19,6 +20,7 @@ export function AdvancedPanel({
|
||||
catalog,
|
||||
workspaces,
|
||||
providerRequests,
|
||||
providerRateLimits,
|
||||
hiringPolicies,
|
||||
actions,
|
||||
}: {
|
||||
@@ -28,6 +30,7 @@ export function AdvancedPanel({
|
||||
catalog: ProviderCatalogEntry[];
|
||||
workspaces: WorkspaceRecord[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||
hiringPolicies: HiringPolicyRecord[];
|
||||
actions: SnapshotActions;
|
||||
}) {
|
||||
@@ -50,7 +53,7 @@ export function AdvancedPanel({
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 28 }}>
|
||||
<BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} hiringPolicies={hiringPolicies} />
|
||||
<BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} providerRateLimits={providerRateLimits} hiringPolicies={hiringPolicies} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,19 +2,25 @@ 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 type { ProviderRateLimitStateRecord } from "../../../../shared/provider-runtime-domain";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
export function BillingPanel({
|
||||
agents,
|
||||
providers,
|
||||
providerRequests,
|
||||
providerRateLimits,
|
||||
hiringPolicies,
|
||||
}: {
|
||||
agents: AgentRecord[];
|
||||
providers: ProviderRecord[];
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||
hiringPolicies: HiringPolicyRecord[];
|
||||
}) {
|
||||
// Month to date, from the provider totals: every call counts, not only the ones loaded into the snapshot.
|
||||
const spentCents = providerRateLimits.reduce((s, usage) => s + usage.costCents, 0);
|
||||
const unpricedCalls = providerRateLimits.reduce((s, usage) => s + usage.unpricedRequests + usage.requestsWithoutUsage, 0);
|
||||
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));
|
||||
@@ -50,12 +56,16 @@ export function BillingPanel({
|
||||
</div>
|
||||
</div>
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
<div className="card-label" style={{ marginBottom: 8 }}>{t("Budget utilization")}</div>
|
||||
<div className="card-label" style={{ marginBottom: 8 }}>{t("Spent this month")}</div>
|
||||
<div style={{ height: 6, background: "var(--panel-3)", borderRadius: 3, overflow: "hidden", marginBottom: 8 }}>
|
||||
<div style={{ width: `${totalCap > 0 ? Math.min(100, Math.round((totalBudget / totalCap) * 100)) : 0}%`, height: "100%", background: "var(--accent)" }} />
|
||||
<div style={{ width: `${totalBudget > 0 ? Math.min(100, Math.round((spentCents / totalBudget) * 100)) : 0}%`, height: "100%", background: "var(--accent)" }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>
|
||||
{totalCap > 0 ? t("{percent}% of cap", { percent: Math.round((totalBudget / totalCap) * 100) }) : t("No budget cap set")}
|
||||
{t("${spent} of ${allocated} allocated to agents", { spent: (spentCents / 100).toFixed(2), allocated: (totalBudget / 100).toFixed(2) })}
|
||||
{unpricedCalls > 0 && <> · {t("{count} calls not priced", { count: unpricedCalls })}</>}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 4 }}>
|
||||
{totalCap > 0 ? t("Allocations use {percent}% of department caps", { percent: Math.round((totalBudget / totalCap) * 100) }) : t("No budget cap set")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,12 @@ export interface ProviderRateLimitStateRecord {
|
||||
outputCharCount: number;
|
||||
remainingRequestCount: number;
|
||||
exhausted: boolean;
|
||||
/** This month's provider-reported spend and tokens; calls without usage or prices are counted, not guessed. */
|
||||
costCents: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
requestsWithoutUsage: number;
|
||||
unpricedRequests: number;
|
||||
}
|
||||
|
||||
export interface ProviderObservedRateLimitState {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ProviderRequestStore } from "../../src/main/services/provider-request-store";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
import { PROVIDER_EXECUTION_MODE } from "../../src/shared/domain";
|
||||
|
||||
describe("usage settlement", () => {
|
||||
@@ -39,6 +40,9 @@ describe("usage settlement", () => {
|
||||
`).get(provider.id)).toEqual({
|
||||
request_count: 2, input_tokens: 1_000_000, output_tokens: 100_000, cached_input_tokens: 400_000, cost_cents: 342, requests_without_usage: 1, unpriced_requests: 0,
|
||||
});
|
||||
// The month's spend reaches the renderer from these totals, not from the capped list of recent requests.
|
||||
const limits = new SnapshotService(connection.db).load().providerRateLimits.find((item) => item.providerId === provider.id);
|
||||
expect(limits).toMatchObject({ costCents: 342, inputTokens: 1_000_000, outputTokens: 100_000, requestsWithoutUsage: 1, unpricedRequests: 0 });
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user