Demo budget 402 errors propagated through all document load routes

Previously a demo user who hit their spending limit would see a generic 500 when trying to open a document - the budget error was being caught and swallowed inside the storage layer. This PR surfaces a typed `HTTP 402` with `code: "demo_budget_exceeded"` through every document-facing route, and retunes the demo cost model so only LLM calls actually count against the budget.

chat-uiinfrastructure

The storage layer change is the key move. downloadFile and getSignedUrl in storage.ts previously caught all errors and returned null. Now isDemoBudgetError(err) re-throws before the catch swallows it, so routes can intercept it specifically. documents.ts wraps downloadFile and getSignedUrl calls in try/catch blocks with a shared sendDemoBudgetError() helper that emits HTTP 402 + code: "demo_budget_exceeded". The /docx route also drops its 302 redirect in favor of proxying raw bytes, which was necessary to catch errors at the route level rather than losing them in a redirect chain.

The downloads.ts route gets matching treatment, and the frontend hooks useFetchDocxBytes and useFetchSingleDoc now parse code from the JSON body and re-throw with the budget-specific message so the UI can show a recoverable state.

The cost model retuning (c4a955e2) is a separate concern bundled in the same PR. The LLM reserve drops from $0.75 to $0.10, and non-LLM services (Vault, Legal, Skills, Matters) all default to $0.00. LLM charges now settle from usage.cost when available, with a fallback path that calls /llm/v1/models, caches pricing for 5 minutes, and uses token pricing from the catalog if no explicit cost is returned. A new UploadTooLargeError class caps uploads at 100 MB with a typed 413 response.

The final polish commit (57ba208c) stops forwarding raw error messages to the browser. The 402 body becomes a fixed string with actionable instructions; 500 responses get opaque code fields rather than internal exception text.

So what Worth a look if you're building any kind of quota or rate-limit error that needs to reach the UI reliably through multiple storage abstraction layers. The `isDemoBudgetError` + re-throw + `code` field pattern is clean and reusable. Skip the cost-tuning numbers - they're specific to Case.dev's pricing - but the reserve-then-settle shape for LLM billing is worth understanding if you're designing similar metering.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

4 commits from CaseMark/mikeoss-casedotdev, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
5e899345 Surface demo budget errors for document loads kveton 2026-05-04 ↗ GitHub
c4a955e2 Tune demo usage metering kveton 2026-05-04 ↗ GitHub
57ba208c Harden document load error responses kveton 2026-05-04 ↗ GitHub
56e7f380 Merge pull request #7 from CaseMark/codex/demo-budget-document-errors Scott Kveton 2026-05-04 ↗ GitHub
Surface demo budget errors for document loads

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-33.md from inside the repo you want the changes in.

⬇ Download capture-thread-33.md