Q72: usage views show provider-reported tokens, not characters
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
05e296bd5d
commit
90b7306aee
6 files changed
+57
-22
No files matched your search
@@ -122,7 +122,8 @@ export class SnapshotService {
|
||||
select id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256,
|
||||
response_text, state, error_message, input_char_count, output_char_count,
|
||||
execution_mode, policy_decision, external_response_id, stream_event_count,
|
||||
cancelled_at, created_at, completed_at
|
||||
cancelled_at, created_at, completed_at, agent_id, task_run_id,
|
||||
input_tokens, output_tokens, cached_input_tokens, reasoning_tokens, cost_cents
|
||||
from provider_requests order by created_at desc limit 100
|
||||
`)).map((row) => camel(row) as unknown as AppSnapshot["providerRequests"][number]),
|
||||
providerRateLimits,
|
||||
|
||||
@@ -32,6 +32,16 @@ export const zh: Record<string, string> = {
|
||||
"superseded": "已被取代",
|
||||
"stalled": "已停滞",
|
||||
"Resume": "恢复",
|
||||
"Tokens": "Token",
|
||||
"not reported": "未报告",
|
||||
"{input} in · {output} out": "输入 {input} · 输出 {output}",
|
||||
"Cost": "费用",
|
||||
"unpriced": "未定价",
|
||||
"{n} tokens this week": "本周 {n} token",
|
||||
"{n} tokens this week · {count} calls without usage": "本周 {n} token · {count} 次调用未报告用量",
|
||||
"of which cached": "其中缓存",
|
||||
"of which reasoning": "其中推理",
|
||||
"Calls without usage": "未报告用量的调用",
|
||||
"Prices (US$ per million tokens)": "价格(美元 / 百万 token)",
|
||||
"Cached input": "缓存输入",
|
||||
"Unpriced: spend is not counted": "未定价:不计入花费",
|
||||
|
||||
@@ -106,7 +106,7 @@ function RunDetail({ run, tasks, agents, providers, providerRequests, actions }:
|
||||
tasks: TaskRecord[];
|
||||
agents: AgentRecord[];
|
||||
providers: ProviderRecord[];
|
||||
providerRequests: Array<{ id: string; orgId: string; providerId: string; model: string; state: string; responseText: string | null; inputCharCount: number; outputCharCount: number; createdAt: string }>;
|
||||
providerRequests: Array<{ id: string; orgId: string; providerId: string; model: string; state: string; responseText: string | null; inputCharCount: number; outputCharCount: number; createdAt: string; inputTokens: number | null; outputTokens: number | null; costCents: number | null }>;
|
||||
}) {
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
@@ -169,6 +169,8 @@ function RunDetail({ run, tasks, agents, providers, providerRequests, actions }:
|
||||
<MetaLabel>{t("State")}</MetaLabel><div>{request.state}</div>
|
||||
<MetaLabel>{t("Input chars")}</MetaLabel><div className="num">{request.inputCharCount.toLocaleString()}</div>
|
||||
<MetaLabel>{t("Output chars")}</MetaLabel><div className="num">{request.outputCharCount.toLocaleString()}</div>
|
||||
<MetaLabel>{t("Tokens")}</MetaLabel><div className="num">{request.inputTokens === null ? t("not reported") : t("{input} in · {output} out", { input: request.inputTokens.toLocaleString(), output: (request.outputTokens ?? 0).toLocaleString() })}</div>
|
||||
<MetaLabel>{t("Cost")}</MetaLabel><div className="num">{request.costCents === null ? t("unpriced") : `$${(request.costCents / 100).toFixed(4)}`}</div>
|
||||
</div>
|
||||
{request.responseText && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
|
||||
@@ -120,28 +120,34 @@ export function SuccessRateChart({ taskRuns }: { taskRuns: TaskRunRecord[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CostTrendChart({ providerRequests }: { providerRequests: Array<{ createdAt: string; inputCharCount: number; outputCharCount: number }> }) {
|
||||
/** Daily model tokens as the providers reported them; calls without reported usage are counted, not guessed. */
|
||||
export function CostTrendChart({ providerRequests }: { providerRequests: Array<{ createdAt: string; inputTokens: number | null; outputTokens: number | null }> }) {
|
||||
const buckets = last7DayBuckets(providerRequests.map((r) => r.createdAt));
|
||||
const charBuckets = buckets.map((b, i) => {
|
||||
const tokenBuckets = buckets.map((b, i) => {
|
||||
const dayStart = dayStartMs(i);
|
||||
const dayEnd = dayStart + 86400000;
|
||||
const chars = providerRequests
|
||||
const tokens = providerRequests
|
||||
.filter((r) => { const t = new Date(r.createdAt).getTime(); return t >= dayStart && t < dayEnd; })
|
||||
.reduce((s, r) => s + r.inputCharCount + r.outputCharCount, 0);
|
||||
return { ...b, chars };
|
||||
.reduce((s, r) => s + (r.inputTokens ?? 0) + (r.outputTokens ?? 0), 0);
|
||||
return { ...b, tokens };
|
||||
});
|
||||
const totalChars = charBuckets.reduce((s, b) => s + b.chars, 0);
|
||||
const max = Math.max(1, ...charBuckets.map((b) => b.chars));
|
||||
const coords = charBuckets.map((b, i) => ({
|
||||
const totalTokens = tokenBuckets.reduce((s, b) => s + b.tokens, 0);
|
||||
// Same seven calendar days as the chart.
|
||||
const unreported = providerRequests.filter((r) => r.inputTokens === null && new Date(r.createdAt).getTime() >= dayStartMs(0)).length;
|
||||
const max = Math.max(1, ...tokenBuckets.map((b) => b.tokens));
|
||||
const coords = tokenBuckets.map((b, i) => ({
|
||||
x: (i / 6) * (CHART_W - 10) + 5,
|
||||
y: CHART_H - 20 - (b.chars / max) * (CHART_H - 30),
|
||||
y: CHART_H - 20 - (b.tokens / max) * (CHART_H - 30),
|
||||
b,
|
||||
}));
|
||||
const points = coords.map((c) => `${c.x},${c.y}`).join(" ");
|
||||
const [hover, setHover] = useState<number | null>(null);
|
||||
const readout = hover !== null && charBuckets[hover]
|
||||
? `${charBuckets[hover]!.label} · ${kchars(charBuckets[hover]!.chars)}`
|
||||
: t("{n} this week", { n: kchars(totalChars) });
|
||||
const week = unreported > 0
|
||||
? t("{n} tokens this week · {count} calls without usage", { n: compact(totalTokens), count: unreported })
|
||||
: t("{n} tokens this week", { n: compact(totalTokens) });
|
||||
const readout = hover !== null && tokenBuckets[hover]
|
||||
? `${tokenBuckets[hover]!.label} · ${compact(tokenBuckets[hover]!.tokens)}`
|
||||
: week;
|
||||
|
||||
return (
|
||||
<ChartCard title={t("Token usage")} readout={readout}>
|
||||
@@ -159,9 +165,10 @@ export function CostTrendChart({ providerRequests }: { providerRequests: Array<{
|
||||
);
|
||||
}
|
||||
|
||||
function kchars(chars: number): string {
|
||||
if (chars >= 1000) return `${(chars / 1000).toFixed(chars >= 10000 ? 0 : 1)}k`;
|
||||
return String(chars);
|
||||
function compact(count: number): string {
|
||||
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
return String(count);
|
||||
}
|
||||
|
||||
const weekdayFormat = new Intl.DateTimeFormat(undefined, { weekday: "short" });
|
||||
|
||||
@@ -17,8 +17,12 @@ export function BillingPanel({
|
||||
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));
|
||||
const totalInputChars = monthRequests.reduce((s, r) => s + r.inputCharCount, 0);
|
||||
const totalOutputChars = monthRequests.reduce((s, r) => s + r.outputCharCount, 0);
|
||||
const sum = (pick: (request: ProviderRequestRecord) => number | null) => monthRequests.reduce((s, r) => s + (pick(r) ?? 0), 0);
|
||||
const inputTokens = sum((r) => r.inputTokens);
|
||||
const cachedTokens = sum((r) => r.cachedInputTokens);
|
||||
const outputTokens = sum((r) => r.outputTokens);
|
||||
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);
|
||||
|
||||
@@ -37,9 +41,11 @@ export function BillingPanel({
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
<div className="card-label" style={{ marginBottom: 8 }}>{t("Token usage (30d)")}</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "6px 14px", fontSize: 12 }}>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Input chars")}</span><span className="num">{totalInputChars.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Output chars")}</span><span className="num">{totalOutputChars.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Total")}</span><span className="num">{(totalInputChars + totalOutputChars).toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Input")}</span><span className="num">{inputTokens.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("of which cached")}</span><span className="num">{cachedTokens.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("Output")}</span><span className="num">{outputTokens.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--text-3)" }}>{t("of which reasoning")}</span><span className="num">{reasoningTokens.toLocaleString()}</span>
|
||||
{unreported > 0 && <><span style={{ color: "var(--text-3)" }}>{t("Calls without usage")}</span><span className="num">{unreported}</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card" style={{ padding: 14 }}>
|
||||
|
||||
@@ -35,6 +35,15 @@ export interface ProviderRequestRecord {
|
||||
cancelledAt: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
agentId: string | null;
|
||||
taskRunId: string | null;
|
||||
/** Provider-reported token usage; null when the provider reported none. */
|
||||
inputTokens: number | null;
|
||||
outputTokens: number | null;
|
||||
cachedInputTokens: number | null;
|
||||
reasoningTokens: number | null;
|
||||
/** Null when the provider has no prices or reported no usage. */
|
||||
costCents: number | null;
|
||||
}
|
||||
|
||||
export interface ProviderStreamRendererEvent {
|
||||
|
||||
Reference in new issue
Block a user