You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Usage page prints $9,566.69 next to "Repo Coder" and $36.35 as "Est. billed" — every breakdown carries only the notional figure, so no agent has a charged cost #543
"What did this agent cost me?" On the Usage page today, the Repo Coder row reads $9,566.69 and the headline "Est. billed" stat reads $36.35. Both are on screen at once, they differ by 263x, and nothing on the page says which of the two the Repo Coder row belongs to.
It belongs to neither. The row is notional list-price value; the headline is charged money; and there is no per-agent charged figure anywhere in the payload.
Measured
usage_summary, production, 2026-08-13, range: "all" (identical to 30d — the whole ledger is inside 30 days):
totals.costMicros 9,621,218,575 $9,621.22 notional
totals.chargedCostMicros 36,352,782 $36.35 money
→ charged is 0.378% of notional
byAgent[0] Repo Coder 9,566,686,384 $9,566.69 99.43% of the notional total
byKind[0] engine 9,541,310,295 $9,541.31 99.17%, 449 calls
byPayer unknown 9,584,865,793 $9,584.87 99.62%
byok-api 36,336,533 $36.34
platform 16,249 $0.02
subscription (no bucket — zero rows)
So the one agent that dominates the page is 99.7% kind:engine at payer: unknown. The dollar figure printed beside its name is, for that agent, almost entirely not money — and the page renders it in the same $ format, in the same column, as every other row.
Mechanism
totals carries the charged figure. UsageBucket does not.
workers/api/src/lib/usage.ts:281-297 — the bucket type:
Same loop body, same row, same predicate available. byModel, byKind, byAgentand byPayer all go through bump(), so none of them carries it. aggregateAdminUsage has the same split — addInto (usage.ts:451-457) accumulates chargedMicros, but its per-bucket into() calls bump(), so the admin by-user and by-agent breakdowns are notional-only too.
The renderer then prints the notional figure with no qualifier. store/console/src/pages/Usage.tsx:164:
That one line renders the by-agent, by-model, by-kind and by-payer breakdowns. The "Payer not established" row therefore prints $9,584.87 in dollars — the row whose entire purpose is to say "this is not money".
The breakdowns were written before payer existed and were left as pure token/value rollups, which is exactly right for the "how busy is this agent" question they were originally for.
Composed, the page states a true total it cannot decompose. The owner's actual question is decomposition.
What to do, cheapest first
Accumulate it. Three lines: chargedCostMicros: 0 in emptyBucket (usage.ts:322), if (isCharged(r.payer)) b.chargedCostMicros += r.cost_micros || 0; in bump (usage.ts:324), and the field on UsageBucket. Every breakdown gets it at once, including byPayer and both admin aggregates, because they all share bump(). isCharged is already imported at usage.ts:11.
Render two columns, not one.Usage.tsx:164 shows usd(r.costMicros); add the charged figure beside it, and size the bar by charged when any row has one. A row where charged is $0.00 of $9,566.69 is the most informative thing the page could say, and today it is the one thing it cannot.
Do NOT drop the notional column. It is the only figure that reflects what a subscription-run Coder actually consumed; stats-sources.ts:321-329 already documents why filtering it to charged would show $0.00 for an agent that plainly did a great deal of work. Two numbers, labelled.
Alternatives considered and rejected
Filter the breakdowns to charged rows only. Rejected for the reason above, and for the same reason usageValue is deliberately unfiltered: the Repo Coder would render as $0.00 and read as idle.
Add a chargedByAgent array beside byAgent. Rejected — two arrays that must be joined by key, when one shared bump() already visits every bucket in one pass.
Compute it client-side from byPayer. Impossible: byPayer and byAgent are separate rollups of the same rows with no cross-product, so there is no way to get "charged, for this agent" out of them.
Acceptance criteria
GET /v1/usage returns chargedCostMicros on every bucket in byModel, byKind, byAgent, byPayer.
For the account measured above, byPayer returns {key:"unknown", costMicros: 9_584_865_793, chargedCostMicros: 0} and {key:"byok-api", costMicros: 36_336_533, chargedCostMicros: 36_336_533}.
sum(byAgent[].chargedCostMicros) === totals.chargedCostMicros — assert it in usage.test.ts over a fixture containing rows of all four payer states (byok-api, platform, subscription, NULL).
The console shows both figures per row, and a row with chargedCostMicros: 0 is visually distinguishable from one that was not measured.
aggregateAdminUsage's byUser / byAgent carry it too (same bump()), so the operator view stops having the same defect.
Regression risk
Breakdown sizes its bars by costMicros and falls back to tokens when every row is zero (Usage.tsx:151-154). Switching the bar metric to charged would make the Repo Coder — 99.4% of the account's consumption — render as a 2%-wide stub, which is a worse lie than the current one. Size the bar by notional, print both numbers. A snapshot test over the fixture above is what catches a later "simplification" to one column.
Related: #346 (which added the totals-level split), #526 (the same page cannot attribute per instance either), and the range-truncation defect in the charged figure filed separately.
The question and the two answers the page gives
"What did this agent cost me?" On the Usage page today, the Repo Coder row reads $9,566.69 and the headline "Est. billed" stat reads $36.35. Both are on screen at once, they differ by 263x, and nothing on the page says which of the two the Repo Coder row belongs to.
It belongs to neither. The row is notional list-price value; the headline is charged money; and there is no per-agent charged figure anywhere in the payload.
Measured
usage_summary, production, 2026-08-13,range: "all"(identical to30d— the whole ledger is inside 30 days):So the one agent that dominates the page is 99.7%
kind:engineatpayer: unknown. The dollar figure printed beside its name is, for that agent, almost entirely not money — and the page renders it in the same$format, in the same column, as every other row.Mechanism
totalscarries the charged figure.UsageBucketdoes not.workers/api/src/lib/usage.ts:281-297— the bucket type:usage.ts:324-334—bump()receives the whole row,payerincluded, and accumulates everything except the one field that answers the question:Six lines above, at
usage.ts:369, the totals loop does exactly the thing the buckets do not:Same loop body, same row, same predicate available.
byModel,byKind,byAgentandbyPayerall go throughbump(), so none of them carries it.aggregateAdminUsagehas the same split —addInto(usage.ts:451-457) accumulateschargedMicros, but its per-bucketinto()callsbump(), so the admin by-user and by-agent breakdowns are notional-only too.The renderer then prints the notional figure with no qualifier.
store/console/src/pages/Usage.tsx:164:That one line renders the by-agent, by-model, by-kind and by-payer breakdowns. The "Payer not established" row therefore prints
$9,584.87in dollars — the row whose entire purpose is to say "this is not money".Why it composed into a bug
Two individually-correct decisions:
totalsand stopped there. Correct as far as it went — it ended the "everything is a bill" reading of the headline.payerexisted and were left as pure token/value rollups, which is exactly right for the "how busy is this agent" question they were originally for.Composed, the page states a true total it cannot decompose. The owner's actual question is decomposition.
What to do, cheapest first
chargedCostMicros: 0inemptyBucket(usage.ts:322),if (isCharged(r.payer)) b.chargedCostMicros += r.cost_micros || 0;inbump(usage.ts:324), and the field onUsageBucket. Every breakdown gets it at once, includingbyPayerand both admin aggregates, because they all sharebump().isChargedis already imported atusage.ts:11.Usage.tsx:164showsusd(r.costMicros); add the charged figure beside it, and size the bar by charged when any row has one. A row where charged is$0.00of$9,566.69is the most informative thing the page could say, and today it is the one thing it cannot.stats-sources.ts:321-329already documents why filtering it to charged would show$0.00for an agent that plainly did a great deal of work. Two numbers, labelled.Alternatives considered and rejected
usageValueis deliberately unfiltered: the Repo Coder would render as$0.00and read as idle.chargedByAgentarray besidebyAgent. Rejected — two arrays that must be joined by key, when one sharedbump()already visits every bucket in one pass.byPayer. Impossible:byPayerandbyAgentare separate rollups of the same rows with no cross-product, so there is no way to get "charged, for this agent" out of them.Acceptance criteria
GET /v1/usagereturnschargedCostMicroson every bucket inbyModel,byKind,byAgent,byPayer.byPayerreturns{key:"unknown", costMicros: 9_584_865_793, chargedCostMicros: 0}and{key:"byok-api", costMicros: 36_336_533, chargedCostMicros: 36_336_533}.sum(byAgent[].chargedCostMicros) === totals.chargedCostMicros— assert it inusage.test.tsover a fixture containing rows of all four payer states (byok-api,platform,subscription,NULL).chargedCostMicros: 0is visually distinguishable from one that was not measured.aggregateAdminUsage'sbyUser/byAgentcarry it too (samebump()), so the operator view stops having the same defect.Regression risk
Breakdownsizes its bars bycostMicrosand falls back to tokens when every row is zero (Usage.tsx:151-154). Switching the bar metric to charged would make the Repo Coder — 99.4% of the account's consumption — render as a 2%-wide stub, which is a worse lie than the current one. Size the bar by notional, print both numbers. A snapshot test over the fixture above is what catches a later "simplification" to one column.Related: #346 (which added the totals-level split), #526 (the same page cannot attribute per instance either), and the range-truncation defect in the charged figure filed separately.