Skip to content

Weekly per-student model budget (PrincetonCourses tiers) - #34

Open
aikhanjum wants to merge 1 commit into
feat/usage-loggingfrom
feat/weekly-usage-limit
Open

aikhanjum wants to merge 1 commit into
feat/usage-loggingfrom
feat/weekly-usage-limit

Conversation

@aikhanjum

Copy link
Copy Markdown
Collaborator

Stacked on #33, which adds the per-turn cost this budget counts. Review #33 first. Once it merges, this PR will retarget to main.

Per Ibraheem: the limit is in dollars, copies PrincetonCourses' limit, and uses a weekly window only.

The limit
PrincetonCourses' ask gateway (tiger-junction/apps/ask-gateway/app/usage_tracker.py) gives each student:

  • Tier 1: $3.00 on any model, Opus 5 included
  • Tier 2: another $1.00, Sonnet 5 only. Opus requests step down automatically.
  • Exhausted: after $4.00, no more answers until the window frees up

PrincetonCourses uses fixed 8-hour windows. PI uses one rolling 7-day window, so each turn's cost stops counting exactly a week after it was spent.

⚠️ This is $4 per week, where PrincetonCourses allows $4 per 8 hours, so PI is about 21x tighter per week. If that wasn't the intent, it's a config change. The amounts are Worker vars in wrangler.jsonc (USAGE_TIER1_USD, USAGE_TIER2_USD), with no code change needed.

How it works

  • Ledger: each student's desk object (u-<netid>-desk) gets a pi_usage SQLite table (at, micros, chat). After every turn, the chat records its cost from Log token usage and cost for every chat turn #33 there. That includes failed or stopped turns, since they were still billed.
  • Pruning: spend older than 7 days is deleted on the same write a turn already causes. No alarms, so nothing wakes the desk on a timer.
  • Before each turn, the chat asks the desk for the student's status:
    • Tier 2 with Opus selected: the turn is answered by Sonnet 5 instead.
    • Exhausted: the turn is answered by a stand-in model (notice-model.ts) that streams the limit note ("You've used this week's PI allowance. It frees up Thu, Oct 15, 3:10 PM.") and never calls Anthropic. No MCP connections open, and nothing is billed.
  • If the desk can't answer, the turn runs anyway and the error is logged. The budget is for cost control, so an outage there shouldn't take PI down.
  • The chat page shows a note when a student is on Sonnet-only or out of budget, with the time it frees up in Eastern time.

Files

  • src/shared/usage-limit.ts (new): budget defaults, budgetFrom(env), usageStatus (rolling window, tier, when the next tier frees up), note text
  • src/server/notice-model.ts (new): the zero-cost stand-in model
  • src/server/pi.ts: budget check and step-down in beforeTurn, recording in logTurnUsage, desk RPCs usageRecord / usageStatus
  • src/client/pages/ChatPage.tsx: the note
  • wrangler.jsonc: USAGE_TIER1_USD: "3", USAGE_TIER2_USD: "1"
  • tests/usage-limit.test.ts: tiers, rolling window edges, reset times, env parsing, Eastern time formatting, and the stand-in model streaming the note with zero usage

Testing

  • npm run typecheck, npm test (52 passing) and npm run build all pass.
  • I checked in Think 0.16's source that getModel() runs before beforeTurn, and that a model, maxSteps and activeTools returned from beforeTurn are what streamText gets.
  • The desk's SQLite ledger only runs inside the Workers runtime, so it isn't unit tested. Please run one signed-in turn on a preview and check that a pi_usage row lands.

🤖 Generated with Claude Code

Copies PrincetonCourses' ask-gateway tiers in dollars: $3 on any model,
then Sonnet 5 only for $1 more, then PI stops answering until enough
spend ages out of a rolling 7-day window. Spend is recorded per turn on
the student's desk object and pruned on write, so nothing wakes the
desk on a timer. An exhausted student gets the limit note from a
stand-in model that never calls Anthropic. Amounts are Worker vars.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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