What
Metering exists; analysis doesn't. Every run records what it spent, but there is no way to see what a flow costs, what a month costs per model, what a subscription is actually saving, or what would happen to the bill if a flow moved to a different provider. Goal: a costs screen on the admin dashboard — graphs and data per flow/model/day, subscription-aware pricing, and a what-if that re-prices a flow under a different model. Today things like that are effectively untrackable.
What already works (don't redo this)
- Per-run metering, end to end. The runner emits an exit line with
tokens: { input, output, total, cost } (image/runner/run-job.mjs:121-122); token-budget.mjs accumulates turn_end usage and total is the billed total including cache reads/writes. The worker recovers it from a bounded log tail (run-history.mjs parseExitTokens) and buildRecord persists it on the run record.
- Pricing is pi-ai's, not ours.
cost comes from calculateCost over per-provider model tables (35 providers, tiered rates, Anthropic's 2× 1-hour cache-write rule). pi-dispatch holds no pricing table and computes nothing — keep it that way.
- Caps.
budget.mjs day/week/month job slots + the daily token counter (budget:t:*), all overlay-tunable.
- Surfaces. Cost renders in exactly two places: the RUN_DETAIL drill-in and the
/dispatch runs COST column. The dashboard LIST graphs nothing but three job-count meters and one token text line.
Gap 1 — cost is not attributable
The run record (run-history.mjs buildRecord, contract in INT-RUN-HISTORY-FILE-CONTRACT) carries flow and tokens but not model or provider — and not the cache-read/cache-write split that calculateCost prices. So "what did flow X on model Y cost this month" is not reconstructable from history, and no recorded run can be re-priced under different rates. Both are additive nullable fields, the same shape the chain fields and tokens itself took.
Gap 2 — metered and subscription runs are indistinguishable
Subscription-backed providers ship all-zero rate tables (kimi-coding, zai-coding-cn: cost: {input:0, output:0, ...}), so their runs record cost: 0 with real token counts — they look free, not prepaid. Meanwhile openai-codex (the ChatGPT backend) carries API-equivalent rates, so a subscription-covered run looks metered. The cost field silently mixes regimes with no marker. Subscriptions are the cheapest option most of the time right now — which makes this exactly the number that matters.
Gap 3 — subscriptions can't be modeled
Wanted: amortized effective cost. The operator declares each subscription — provider(s) it covers, monthly price, quota shape (token/request windows as far as the vendor states them) — in a small operator-authored file next to triggers.json. The costs screen then shows per subscription: quota burn-down for the current window, effective $/run given actual metered usage, and "this month at API rates would have been $X" as the comparison line. This must handle Claude, ChatGPT, Kimi, Grok, etc. — anything a rate-zeroed or API-equivalent-rated provider entry maps to. Note the credential path already forces the honest design: OAuth/subscription logins are refused at the env boundary (env-allowlist.mjs) — subscription providers arrive as API-key-shaped credentials with zero-rate tables, so an operator-side declaration is the only place the real price can come from.
Gap 4 — no aggregation, no graphs
There is no aggregation anywhere in admin/ — no per-flow totals, no group-by, nothing but per-row tables (listRuns clamps at 50). The only bar primitive is the block meter; there is no time-series primitive. Retention (PI_LOG_RETENTION_DAYS, default 30) bounds any series — say so on the screen rather than pretending at more history.
Gap 5 — no estimation, no what-if
The what-if that matters: "this flow, same token profile, on a different model/provider" — computable by re-pricing recorded token profiles (with the cache split from Gap 1) against pi-ai's rate tables, or against a declared subscription's amortization. For a flow with no history, expected usage has to be seeded: the flow's own measured median first, a coarse benchmark prior (published SWE-bench/DeepSWE-style tokens-per-task figures) only as a clearly labeled fallback. This repo has already been burned by unmeasured guesses (OQ-002, the "$0.5–$5 per job" non-requirement) — estimates must be visibly marked as estimates and never mix silently with metered numbers.
Where it lives
One COSTS view in the dashboard's view machine (a fifth view beside LIST/RUN_DETAIL/TRIGGER_DETAIL/LIVE_TAIL; c is a free key), data assembled in read-model.mjs (the dashboard is asserted fs-free), a matching pure renderer in render.mjs plus a /dispatch costs subcommand for the degraded path. A thin time-series/sparkline primitive belongs next to meter in panel.mjs/style.mjs — same thin-pure-module reasoning as the existing "no TUI framework" comment there.
Explicitly NOT proposed
- Live pricing fetches from vendor APIs — rates come from the pinned pi-ai tables; drift is a version-bump concern, not a runtime one.
- A database.
DES-RUN-HISTORY-FLAT-FILES-NO-DB stands; aggregation is a read-model fold over the sidecar files, bounded by retention.
- Auto-switching models based on cost. This screen informs the operator; it changes nothing.
Acceptance
Run records carry model, provider, and the cache-split token counts (additive, nullable); runs on zero-rate subscription providers are marked prepaid, never shown as free; a costs screen shows per-flow/per-model spend over the retention window, subscription burn-down and effective $/run from operator-declared subscriptions with the API-rate comparison, and a what-if re-pricing of a flow under another model or subscription; every estimated number is labeled as an estimate; no new network surface, no new deps beyond a pure drawing primitive, all fs/redis access stays in read-model.mjs.
What
Metering exists; analysis doesn't. Every run records what it spent, but there is no way to see what a flow costs, what a month costs per model, what a subscription is actually saving, or what would happen to the bill if a flow moved to a different provider. Goal: a costs screen on the admin dashboard — graphs and data per flow/model/day, subscription-aware pricing, and a what-if that re-prices a flow under a different model. Today things like that are effectively untrackable.
What already works (don't redo this)
tokens: { input, output, total, cost }(image/runner/run-job.mjs:121-122);token-budget.mjsaccumulatesturn_endusage andtotalis the billed total including cache reads/writes. The worker recovers it from a bounded log tail (run-history.mjsparseExitTokens) andbuildRecordpersists it on the run record.costcomes fromcalculateCostover per-provider model tables (35 providers, tiered rates, Anthropic's 2× 1-hour cache-write rule). pi-dispatch holds no pricing table and computes nothing — keep it that way.budget.mjsday/week/month job slots + the daily token counter (budget:t:*), all overlay-tunable./dispatch runsCOST column. The dashboard LIST graphs nothing but three job-count meters and one token text line.Gap 1 — cost is not attributable
The run record (
run-history.mjsbuildRecord, contract inINT-RUN-HISTORY-FILE-CONTRACT) carriesflowandtokensbut notmodelorprovider— and not the cache-read/cache-write split thatcalculateCostprices. So "what did flow X on model Y cost this month" is not reconstructable from history, and no recorded run can be re-priced under different rates. Both are additive nullable fields, the same shape the chain fields andtokensitself took.Gap 2 — metered and subscription runs are indistinguishable
Subscription-backed providers ship all-zero rate tables (
kimi-coding,zai-coding-cn:cost: {input:0, output:0, ...}), so their runs recordcost: 0with real token counts — they look free, not prepaid. Meanwhileopenai-codex(the ChatGPT backend) carries API-equivalent rates, so a subscription-covered run looks metered. Thecostfield silently mixes regimes with no marker. Subscriptions are the cheapest option most of the time right now — which makes this exactly the number that matters.Gap 3 — subscriptions can't be modeled
Wanted: amortized effective cost. The operator declares each subscription — provider(s) it covers, monthly price, quota shape (token/request windows as far as the vendor states them) — in a small operator-authored file next to
triggers.json. The costs screen then shows per subscription: quota burn-down for the current window, effective $/run given actual metered usage, and "this month at API rates would have been $X" as the comparison line. This must handle Claude, ChatGPT, Kimi, Grok, etc. — anything a rate-zeroed or API-equivalent-rated provider entry maps to. Note the credential path already forces the honest design: OAuth/subscription logins are refused at the env boundary (env-allowlist.mjs) — subscription providers arrive as API-key-shaped credentials with zero-rate tables, so an operator-side declaration is the only place the real price can come from.Gap 4 — no aggregation, no graphs
There is no aggregation anywhere in
admin/— no per-flow totals, no group-by, nothing but per-row tables (listRunsclamps at 50). The only bar primitive is the blockmeter; there is no time-series primitive. Retention (PI_LOG_RETENTION_DAYS, default 30) bounds any series — say so on the screen rather than pretending at more history.Gap 5 — no estimation, no what-if
The what-if that matters: "this flow, same token profile, on a different model/provider" — computable by re-pricing recorded token profiles (with the cache split from Gap 1) against pi-ai's rate tables, or against a declared subscription's amortization. For a flow with no history, expected usage has to be seeded: the flow's own measured median first, a coarse benchmark prior (published SWE-bench/DeepSWE-style tokens-per-task figures) only as a clearly labeled fallback. This repo has already been burned by unmeasured guesses (
OQ-002, the "$0.5–$5 per job" non-requirement) — estimates must be visibly marked as estimates and never mix silently with metered numbers.Where it lives
One COSTS view in the dashboard's view machine (a fifth view beside LIST/RUN_DETAIL/TRIGGER_DETAIL/LIVE_TAIL;
cis a free key), data assembled inread-model.mjs(the dashboard is asserted fs-free), a matching pure renderer inrender.mjsplus a/dispatch costssubcommand for the degraded path. A thin time-series/sparkline primitive belongs next tometerinpanel.mjs/style.mjs— same thin-pure-module reasoning as the existing "no TUI framework" comment there.Explicitly NOT proposed
DES-RUN-HISTORY-FLAT-FILES-NO-DBstands; aggregation is a read-model fold over the sidecar files, bounded by retention.Acceptance
Run records carry
model,provider, and the cache-split token counts (additive, nullable); runs on zero-rate subscription providers are marked prepaid, never shown as free; a costs screen shows per-flow/per-model spend over the retention window, subscription burn-down and effective $/run from operator-declared subscriptions with the API-rate comparison, and a what-if re-pricing of a flow under another model or subscription; every estimated number is labeled as an estimate; no new network surface, no new deps beyond a pure drawing primitive, all fs/redis access stays inread-model.mjs.