Local task cost quick start
Track what each agent task costs — locally.
Measurement stands alone. Routing and anonymized outcome contribution are separate, optional steps — the @hokusai/costs package never calls Hokusai and never reads HOKUSAI_API_KEY.
Give it usage events (or Claude Code / Codex session files you name explicitly) and it returns a typed report with per-task, per-model, per-harness breakdowns. Charges and token-equivalent estimates stay labeled separately. Missing values stay null, never $0.
Install
The version is pinned exactly so your output matches the pricing revision in the examples below.
npm install @hokusai/costs@0.5.0Two-line quick start
Pass a harness that your code already emits usage events from — measureTask subscribes for the run and returns a typed cost report alongside your run’s result.
import { measureTask } from "@hokusai/costs"
const { result, cost } = await measureTask({ events: harness }, () => harness.run(task))Supported emitters: an on/off or addListener/removeListener EventEmitter, an EventTarget, or a sync/async iterable. The default event name is "usage".
When you need usage events
Automatic capture works only from usage events you pass, or from Claude Code / Codex session files you name explicitly. With neither available, call recordUsage yourself. If nothing was captured, the report returns coverage.status: "unavailable", totalUsd: null, and a no_usage_captured diagnostic. Hokusai never guesses.
Record manually
Use createTaskCostTracker when your harness owns the loop. Each recordUsage call takes a model id, a usage object (the fromAnthropicUsage / fromOpenAIUsage helpers map provider payloads), an optional provider-reported charge, and an optional stable event id so retries dedupe.
import {
createTaskCostTracker,
fromAnthropicUsage,
fromOpenAIUsage,
} from "@hokusai/costs"
const tracker = createTaskCostTracker({
taskId: "task-local-demo",
harness: "native",
})
// A known model with a provider-reported charge.
tracker.recordUsage({
model: "claude-sonnet-5",
usage: fromAnthropicUsage({ input_tokens: 4000, output_tokens: 900 }),
providerCostUsd: 0.0135,
costBasis: "per_token_api",
})
// A known model priced locally from the built-in table (an estimate).
tracker.recordUsage({
model: "gpt-5",
usage: fromOpenAIUsage({ prompt_tokens: 2000, completion_tokens: 300 }),
})
// An unknown model: cost stays null, never 0.
tracker.recordUsage({
model: "some-internal-model",
usage: { inputTokens: 1000, outputTokens: 200 },
})
const report = tracker.summary()
if (report.totalUsd !== null) throw new Error("unknown model must null the total")
if (!(report.estimatedUsd > 0)) throw new Error("expected a priced lower bound")
if (report.coverage.unknownModels[0] !== "some-internal-model") {
throw new Error("expected the unknown model to be called out")
}
Claude Code and Codex session files
If the agent is running inside Claude Code or Codex, name the session files explicitly: a non-empty list of session ids, the paths (or roots) to look in, and an inclusive time window. Session-file capture is Node-only; file paths and prompt text never enter the report (counts and model ids only).
Claude Code
const CLAUDE_CODE_SESSION_ID = "your-claude-code-session-id"
const CLAUDE_CODE_SESSION_PATH = "/absolute/path/to/your-claude-code-session.jsonl"
import { measureTask } from "@hokusai/costs"
const { cost } = await measureTask(
{
taskId: "task-claude-code-demo",
sessionFiles: {
harness: "claude-code",
sessionIds: [CLAUDE_CODE_SESSION_ID],
paths: [CLAUDE_CODE_SESSION_PATH],
window: {
startedAt: "2026-01-01T00:00:00Z",
endedAt: "2026-01-01T01:00:00Z",
},
},
},
() => "task-ran",
)
if (cost.source.sessionFileCount !== 1) {
throw new Error("expected one session file")
}
Codex
const CODEX_SESSION_ID = "your-codex-session-id"
const CODEX_SESSION_ROOT = "/absolute/path/to/your/codex/sessions"
import { measureTask } from "@hokusai/costs"
const { cost } = await measureTask(
{
taskId: "task-codex-demo",
sessionFiles: {
harness: "codex",
sessionIds: [CODEX_SESSION_ID],
roots: [CODEX_SESSION_ROOT],
window: {
startedAt: "2026-01-01T00:00:00Z",
endedAt: "2026-01-01T01:00:00Z",
},
},
},
() => "task-ran",
)
// coverage.status is "complete" | "partial" | "unavailable" | "known_zero".
// With no matching session files it is "unavailable" and totalUsd is null —
// Hokusai never guesses.
if (cost.coverage.status === "unavailable" && cost.totalUsd !== null) {
throw new Error("unavailable coverage must not carry a total")
}
Query your local ledger
Pass a ledger to measureTask or the tracker, and accepted records are persisted (the tracker needs an explicit await tracker.flush(); measureTask flushes for you). Ledger files are opened with 0600 permissions. Query by task, time window, or any combination of model, harness, backend, and source.
import { mkdtempSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import {
createTaskCostTracker,
fromAnthropicUsage,
fromOpenAIUsage,
} from "@hokusai/costs"
import {
createNodeFileLedgerStorage,
defaultLedgerHash,
openTaskCostLedger,
} from "@hokusai/costs/ledger/node"
const dir = mkdtempSync(join(tmpdir(), "hokusai-costs-ledger-"))
const ledger = await openTaskCostLedger({
storage: createNodeFileLedgerStorage({ path: join(dir, "task-cost.v1.jsonl") }),
hashFn: defaultLedgerHash,
})
const tracker = createTaskCostTracker({
taskId: "task-ledger-demo",
harness: "claude-code",
ledger,
})
tracker.recordUsage({
model: "claude-sonnet-5",
usage: fromAnthropicUsage({
input_tokens: 10000,
output_tokens: 2000,
cache_read_input_tokens: 1000,
}),
providerCostUsd: 0.045,
costBasis: "per_token_api",
})
tracker.recordUsage({
model: "gpt-5",
harness: "codex",
usage: fromOpenAIUsage({ prompt_tokens: 4000, completion_tokens: 500 }),
})
tracker.recordUsage({
model: "some-internal-model",
usage: { inputTokens: 500, outputTokens: 100 },
})
await tracker.flush()
const summary = ledger.summarizeTask("task-ledger-demo")
const q = ledger.query({
taskId: "task-ledger-demo",
groupBy: ["model", "backend", "source"],
})
// `summary.byModel`, `byHarness`, `byBackend`, `bySource` ride alongside
// the raw `q.groups` so hosts can pick the shape that matches their UI.
console.log(JSON.stringify({ summary, groups: q.groups }, null, 2))
Sample output
What the ledger query above returns when the three recordUsage calls run against the released package. Amounts shown here are converted from the ledger’s integer meteredMicros / tokenEquivalentMicros into USD for display.
| Task | Model | Backend | Source | Basis | Amount (USD) |
|---|---|---|---|---|---|
| task-ledger-demo | claude-sonnet-5 | anthropic | provider_reported | charge | $0.0450 |
| task-ledger-demo | gpt-5 | openai | local_estimate | estimate | ≈ $0.0100 |
| task-ledger-demo | some-internal-model | unknown | none | missing | — |
Charges, estimates, and missing values
- Charge. A provider-reported amount in USD. Shown in the report as
actualUsdand in the ledger asmeteredMicros. - Token-equivalent estimate. Priced locally from a dated built-in table (or your own overrides) and reported as
estimatedUsd. Under a Claude Code or Codex subscription (costBasis: "subscription"), the dollar figure is what the usage would cost on per-token APIs — not a bill from your subscription provider. - Missing. An unknown model, an unreported counter, or an unpriceable usage shape stays
null, never$0.totalUsdisnullunless every captured event resolved; the priced subtotal is a lower bound andcoverage.unknownModelsnames what was unpriceable.
Charges and estimates are never summed into one unlabeled bill. Tiered, negotiated, and batch pricing are not modeled.
What stays local
Kept
- Token counts and USD amounts.
- Model ids, harness names, and other enum values.
- Timestamps and task / session / turn / event ids you supply.
Never kept
- Prompts and transcript text.
- File paths, directory names, and git branches.
- Credentials, account ids, and user identifiers.
Behavior: no network calls, HOKUSAI_API_KEY is not read, and nothing is written to disk unless you open a ledger. Ledger files live at the path you give and are created with mode 0600.
Optional: share with Hokusai
Nothing is submitted by this package. If you later choose to route through Hokusai or contribute an anonymized outcome row, that is a separate, explicit step and you pass a total yourself — a null total stays absent, never 0. Hokusai’s routing model recommends; your harness still runs the model and owns the execution.