Skip to content

Costs view: notional cost of agent chats and subagents (REQ-COSTS-001) - #77

Merged
avrabe merged 1 commit into
mainfrom
feat/costs-view
Oct 29, 2026
Merged

avrabe merged 1 commit into
mainfrom
feat/costs-view

Conversation

@avrabe

@avrabe avrabe commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

A Costs view and GET /api/costs. They show what agent chats and subagents cost as notional list prices, not billed: list prices applied to the token counts in agent transcripts. The report comes from a workstation job in smithy that extracts and prices the transcripts.

The view

  • Banner: "notional list price, not billed", plus the report's age; stale after 26 h.
  • KPIs:
    • last 30 days, month to date, all history;
    • the share of subagent calls and dollars that ran on a cheaper tier than their caller;
    • the main / subagent / advisor split.
  • Breakdowns:
    • notional $ per day for the last 30 days, with the cheaper-tier part;
    • top sessions;
    • top repositories;
    • by model (tokens by type);
    • subagent spend by task category.
  • Unpriced requests: listed rather than counted as $0.

Ingest, and a deviation from the plan. The plan was a POST pushed with curl. But every POST already passes warte's bearer-token auth (REQ-API-002), and nothing issues tokens yet (#17). Opening this one route would break REQ-API-002 and let anyone on the LAN forge the view. So the report has two ways in, with the same checks on both:

  • [hub] costs_file (optional): the job copies its report there over ssh, the channel the scheduled jobs already use from launchd. warte re-reads the file when its mtime changes, and the mtime becomes received_at. The path is deployment config.
  • POST /api/costs: behind the existing auth, so it works once Feature: warte-sync skill (agents sync with warte on startup) #17 lands. It has an 8 MiB body limit on this route, because a report carries every session.

Privacy. The read API is unauthenticated (#46), so a report may hold numbers, model ids, repository names and categories only.

  • Every struct has deny_unknown_fields, and every string must match a short character class with no spaces.
  • A report with a task description, free text in a name, or a negative or non-finite amount gets a 400 (POST) or is rejected (file). Nothing from it is stored.
  • The rejection reason shown in the API and the view names the problem, never the value: serde's message is reduced to its kind and position.

Tests (tests/costs.rs, synthetic reports)

  • Before any report: report: null and an empty-state view.
  • Stored, served, rendered with the label; a push replaces the report; it survives a restart.
  • Rejected inputs: a description field, a prompt field, text in a name, a path as a repo, a negative amount, a missing label, a wrong schema, a bad window name. Nothing is stored for any of them.
  • A report over 2 MB is accepted.
  • POST without a token → 401.
  • The file is re-read on change; a file carrying a description is rejected without quoting it, and the view says so; a valid file is shown again.
  • The view escapes hostile names and flags a 3-day-old report as stale.

Other checks

  • The contract test covers /api/costs (in openapi.json, GET 200).
  • Playwright gets a Costs entry.
  • REQ-COSTS-001 (satisfies SC-006, v1.5.0), plus a CHANGELOG entry.

Negative controls (each restored by re-editing; afterwards the suite exits 0):

  • dropping deny_unknown_fields on Subagent: exit 101;
  • dropping the route's body limit: exit 101;
  • quoting the value in the rejection reason: exit 101.

Integration: the real extractor's report (about 0.9 MB) was served locally through costs_file. It passed validation and the view rendered; no figures are given here.

cargo fmt --check, cargo clippy --all-targets -D warnings and cargo test are green locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G5qpB7zvxLvdrvn5YRyj4R

…TS-001)

A workstation job (smithy pe-costs) extracts token counts from the agent
transcripts, prices them with a versioned list-price table and sends one
report. The Costs view shows, labelled "notional list price, not billed":
notional $ per day for the last 30 days, the top sessions, repositories and
models, subagent spend by task category, and the share of subagent calls and
dollars on a cheaper tier than the caller. GET /api/costs serves the report
with its age.

Ingest: [hub] costs_file, a file the job copies to the ground station over
ssh and warte re-reads when its mtime changes; and POST /api/costs behind the
existing bearer-token auth (usable once #17 issues tokens), with an 8 MiB body
limit on that route.

The read API is unauthenticated (#46), so a report with unknown fields
(a task description), free text in a name, or a bad amount is rejected and
nothing from it is stored. The reason shown never quotes the input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5qpB7zvxLvdrvn5YRyj4R
@avrabe
avrabe enabled auto-merge (squash) October 29, 2026 15:44
@avrabe
avrabe merged commit 4fcbe00 into main Oct 29, 2026
4 checks passed
@avrabe
avrabe deleted the feat/costs-view branch October 29, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant