Skip to content

Log token usage and cost for every chat turn - #33

Open
aikhanjum wants to merge 1 commit into
mainfrom
feat/usage-logging
Open

aikhanjum wants to merge 1 commit into
mainfrom
feat/usage-logging

Conversation

@aikhanjum

@aikhanjum aikhanjum commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

First step of the cost analysis from the Oct 8 meeting. Before we can say what PI costs at N users, we need real cost per chat instead of guesses. This PR logs it for every turn.

What gets logged
After each turn, the chat object writes one JSON line to Workers Logs (observability is already on in wrangler.jsonc):

{"event":"pi.usage","status":"completed","user":"3f9a1c0e7b2d4a55","chat":"a1b2c3d4-e5f6","model":"claude-opus-5",
 "steps":3,"input":2140,"cacheRead":0,"cacheWrite5m":0,"cacheWrite1h":0,"output":612,"reasoning":180,
 "webSearches":0,"costUsd":0.026,"ms":14210}
  • steps counts the model calls in the turn. Every tool call adds a step, and each step resends the system prompt, the tool definitions and the history, so the turn's bill is the sum over all of them.
  • Input is split into uncached, cache read, 5-minute cache write and 1-hour cache write, and each is priced at its own rate. Web searches add $0.01 each.
  • user is an HMAC of the NetID keyed with SESSION_SECRET. That lets us see how usage spreads across students without NetIDs in the logs.
  • ms is wall time for the turn. The chat object is billed for that whole time, so this is what turns into Durable Object GB-s.
  • An unpriced model logs costUsd: null with a warning, so it shows up as a gap and not as free.

Code

  • src/server/usage.ts (new) holds the price table (Opus 5, Sonnet 5, Opus 5.5, Sonnet 5.5) and the pure functions addStep, costUsd and pseudonym.
  • src/server/pi.ts resets the counter in beforeTurn, adds each step in onStepEnd, and logs once in onChatResponse or onChatError, whichever comes first. Logging is wrapped so it can never fail a turn.
  • tests/usage.test.ts checks the buckets, the price math against hand-computed values, the fallbacks, unpriced models and the pseudonyms.

Testing

  • npm run typecheck, npm test (45 passing) and npm run build all pass.
  • I fed a recorded-shape Anthropic stream through the real AI SDK streamText into these functions. The counts matched (1,000 uncached, 8,000 cache read, 1,500 + 500 cache write, 300 output, 2 searches), and the cost came out to $0.050875, the same as by hand.
  • Not yet seen on live traffic. After deploy, filter Workers Logs on event = "pi.usage".

Next
The analysis doc (cost at 100, 500, 1k and 3k users, and free-tier breakpoints) uses these numbers. After that comes the rolling per-student usage limit on the desk object.

🤖 Generated with Claude Code

Each turn now ends with one structured pi.usage log line: model, steps,
uncached input, cache reads and writes, output, reasoning, web searches,
estimated USD and wall time, with the student as a keyed pseudonym
instead of a NetID. This is the data for the PI cost analysis and the
basis for a per-student usage limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@aikhanjum

Copy link
Copy Markdown
Collaborator Author

Following up on #31, #32, #33, and #34 together to keep this in one place. The test, typecheck, and build checks pass on all four. Please review #33 before #34, since the weekly budget change is based on the usage logging branch. I would appreciate feedback on the set when convenient.

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