Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,31 @@ moshcode cost --json # for a script
⚠ no rate for gpt-5.6-sol — tokens counted, cost omitted.
```

Under the table, **`burn` is the slope**: the same windows for every engine —
last 1 min, 15 min, 1 hour, 4 hours, 8 hours, and the report window — each with
what the requests inside it cost, that cost per hour of window, and the runs
behind it. `cost` says what the day cost; `burn` says whether the next hour will
cost the same, which is the number that decides whether to kill something.
From a box running a herd of claude sessions, `moshcode cost --all --since 8h`:

```
burn cost rate runs
last 1 min $1.32~ $79.16/h 2
last 15 min $38.34~ $153.35/h 8
last 1 hour $222.03~ $222.03/h 11
last 4 hours $966.27~ $241.57/h 19
last 8 hours $1786.84~ $223.35/h 45
window (8h) $1786.84~ $223.33/h 45
$3.72 a minute over the window, on average
```

The rows come from the same per-request records the table does, priced the
same way, so a claude herd and a codex herd are comparable on one screen. A
window longer than `--since` is left out rather than shown short. Codex logs
running totals, so a turn is the difference between two of them; aider stamps
only the start of a run, so its whole run lands there. `--json` carries the rows
as `burn`, each with its cost split by engine.

**`view` is a link — click it and the PR opens in your browser.** The cost table
is where you notice a session that cost $300, and the next thing you want is
the thing it produced, which lives on GitHub rather than on this machine. The
Expand All @@ -408,7 +433,7 @@ click. `moshcode cost --json` always carries `pr` and `prs` in full.

| engine | where the number comes from |
|---|---|
| claude | per-message `usage` in `~/.claude/projects/**/*.jsonl` |
| claude | per-message `usage` in `~/.claude/projects/**/*.jsonl`; a session's subagents and workflow agents (`<session>/subagents/…`) fold into its row |
| codex | cumulative `token_count` events in `~/.codex/sessions/…` |
| opencode, privacycode | the per-message `cost` each one computed itself |
| aider | the running session total it prints into `.aider.chat.history.md` |
Expand Down
6 changes: 4 additions & 2 deletions src/cli-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const CORE_CLI_COMMANDS = [
name: "cost",
group: "runtime",
description: "what each session is spending, read from the engines' own logs",
synopsis: [["moshcode cost [name] [--all] [--since 6h] [--json]", "session, engine, model, tokens, cost"]],
synopsis: [["moshcode cost [name] [--all] [--since 6h] [--json]", "session, engine, model, tokens, cost, and the burn by window"]],
flags: [
["--all", "every engine session on disk, herd or not", ""],
["--since <dur>", "how far back to look (30m, 6h, 3d)", "24h"],
Expand All @@ -168,7 +168,9 @@ export const CORE_CLI_COMMANDS = [
+ "`~` was worked out from published rates and is what the tokens WOULD cost on the api; unmarked "
+ "figures are the engine's own arithmetic. models with no rate show tokens and no cost — add yours "
+ "to ~/.moshcode/pricing.json. gemini, kimi, deepseek and openagents log nothing readable, "
+ "so they report no cost rather than zero.",
+ "so they report no cost rather than zero. under the table, `burn` is the same windows for every "
+ "engine — last 1 min, 15 min, 1 hour, 4 hours, 8 hours and the report window — with what the "
+ "requests inside each cost, that cost per hour, and the runs behind it; --json carries them as `burn`.",
},
// Everyone who has used a coding agent's own `/usage` types that word first,
// and tokens-and-spend is exactly what `cost` already answers.
Expand Down
33 changes: 32 additions & 1 deletion src/cost-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// Nothing is sampled, nothing is proxied, and a number the engine itself
// computed is never overwritten by our arithmetic.
import {
DEFAULT_WINDOW_MS, UNCOSTED_ENGINES, attributeRuns, engineRuns,
DEFAULT_WINDOW_MS, UNCOSTED_ENGINES, attributeRuns, burn, engineRuns,
formatTokens, formatUsd, totals,
} from "./cost.mjs";
import { pricingFile } from "./cost-pricing.mjs";
Expand Down Expand Up @@ -114,6 +114,29 @@ function renderRuns(runs, { indent = " " } = {}) {
);
}

/**
* The burn block: the same windows for every engine, under whatever table was
* just printed and over the same runs. The table answers "what did it cost" at
* one horizon; this is the slope, which is the number that decides whether to
* kill something. Printed only when something in it could be priced — a block
* of dashes says nothing the footer's warning does not.
*/
export function renderBurn(rows, { indent = " " } = {}) {
if (!rows.some((r) => r.cost != null)) return "";
const body = table(
rows.map((r) => [
bone(r.label),
costCell(r.cost, r.costSource),
r.perHour == null ? ash("—") : dim(`${formatUsd(r.perHour)}/h`),
dim(String(r.runs)),
]),
{ columns: ["burn", "cost", "rate", "runs"], header: true, indent: indent.length },
);
const window = rows.find((r) => r.key === "window");
if (window?.perMinute == null) return body;
return `${body}\n${indent}${dim(`${formatUsd(window.perMinute)} a minute over the window, on average`)}`;
}

/**
* Gather everything once: the roster, the engine runs in the window, and the
* attribution between them. Returned whole so `--json`, the table, and the
Expand Down Expand Up @@ -157,6 +180,9 @@ export async function costCommand(argv = [], { write = console.log } = {}) {
const asJson = argv.includes("--json");
const all = argv.includes("--all");
const since = Date.now() - parseWindow(flagValue(argv, "--since"));
// The last burn row is the report window, named the way it was asked for.
const windowLabel = `window (${flagValue(argv, "--since") || "24h"})`;
const burnOver = (runs) => burn(runs, { since, windowLabel });
const engineFlag = flagValue(argv, "--engine");
const engines = engineFlag ? engineFlag.split(",").map((s) => s.trim()).filter(Boolean) : null;
const watch = argv.includes("--watch");
Expand All @@ -182,6 +208,7 @@ export async function costCommand(argv = [], { write = console.log } = {}) {
if (asJson) {
write(JSON.stringify({
since,
burn: burnOver([...rows.flatMap((r) => r.runs), ...(name ? [] : report.unattributed)]),
sessions: rows.map(({ name: n, engine, cwd, state, models, usage, cost, costSource, unpriced, pr, prs, runs }) => ({
name: n, engine, cwd, state, models, usage, cost, costSource, unpriced, pr, prs,
runs: runs.map((r) => ({ id: r.id, model: r.model, usage: r.usage, cost: r.cost, costSource: r.costSource, start: r.start, end: r.end, pr: r.pr ?? null })),
Expand All @@ -201,6 +228,8 @@ export async function costCommand(argv = [], { write = console.log } = {}) {
return EXIT.matched;
}
write(renderRuns(runs));
const pace = renderBurn(burnOver(runs));
if (pace) { write(""); write(pace); }
// The runs ARE the rows here, so they are what the total totals. And the
// "not tied to a herd session" note would be describing the whole table
// back at itself, so it stays off.
Expand All @@ -215,6 +244,8 @@ export async function costCommand(argv = [], { write = console.log } = {}) {
}

write(renderCost(rows));
const pace = renderBurn(burnOver(rows.flatMap((r) => r.runs)));
if (pace) { write(""); write(pace); }
footer({ ...report, rows }, write);
if (UNCOSTED_ENGINES.some((e) => rows.some((r) => r.engine === e))) {
write(info(`${UNCOSTED_ENGINES.join(", ")} keep no usage log moshcode can read — those rows show no cost, not zero cost.`));
Expand Down
17 changes: 14 additions & 3 deletions src/cost-pricing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,22 @@ import { homedir } from "node:os";
* below, which is Anthropic's published relationship (and the only vendor whose
* cache pricing this file claims to know).
*
* Anthropic rates as published 2026-06; Sonnet 5's introductory $2/$10 runs
* through 2026-08-31 and is deliberately not encoded — an intro rate that
* expires silently would make this table wrong on a date nobody is watching.
* Anthropic rates as published 2026-06 (Fable 5.1 / Mythos 5.1 added 2026-09);
* Sonnet 5's introductory $2/$10 runs through 2026-08-31 and is deliberately
* not encoded — an intro rate that expires silently would make this table
* wrong on a date nobody is watching.
*/
export const PRICING = {
// Claude Fable 5.1 reads the cache at $0.25/MTok — 0.025× of input, a quarter
// of what CACHE_MULTIPLIERS would derive — so it carries its own `cacheRead`.
// It also needs its own entry: without one the prefix match in rateFor()
// lands on `claude-fable-5` and prices every cache read at four times the
// real rate, and on a long agent session cache reads are most of the tokens,
// so most of the estimate is wrong. Whether Mythos 5.1 shares the read rate
// was open at launch; it inherits Fable's here and ~/.moshcode/pricing.json
// overrides it.
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25 },
"claude-mythos-5-1": { input: 10, output: 50, cacheRead: 0.25 },
"claude-fable-5": { input: 10, output: 50 },
"claude-mythos-5": { input: 10, output: 50 },
"claude-opus-5": { input: 5, output: 25 },
Expand Down
Loading
Loading