hsa-app/internal/classify/pricing.go
Jean-Michel Tremblay 5c21f131fd Add AI auto-fill, tally, recent views, and cheaper-by-default classification
Features (see spec.md v2):
- Wire receipt classification into the upload flow; cheap model (Haiku 4.5)
  is now the default, shown as a footnote with per-scan cost in cents.
- Skip-AI toggle to enter fields by hand.
- Duplicate-transaction warning: live check on date+amount, gated submit.
- Tally tab: person x year totals with margins and grand total.
- Recent uploads / recent receipts tabs with paging and file serving.
- People reconcile on startup: merge stray partial names (e.g. "Jude" ->
  "Jude Tremblay"), reassigning receipts; idempotent seeding.
- scripts/build.sh builds the binary; scripts/run.sh builds and runs with .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:04:42 -04:00

47 lines
2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package classify
// Pricing for receipt classification. Token counts come straight from each API
// response (exact, free); per-token prices are not available from any Anthropic
// API, so they live here as constants keyed by exact model id. A model id is
// priced once and never re-priced — Anthropic ships price changes as new ids — so
// this table only needs a new row when we adopt a new model (i.e. when we'd be
// changing CLASSIFY_MODEL anyway). An unknown id yields ok=false, and callers show
// the token counts without a (wrong) dollar figure rather than guessing.
// Usage is the token accounting from one Messages API response.
type Usage struct {
InputTokens int
OutputTokens int
CacheReadTokens int
CacheCreationTokens int
}
// rate holds a model's price in cents per one million tokens.
type rate struct {
inPerM float64 // cents per 1M input tokens
outPerM float64 // cents per 1M output tokens
}
// modelRates is the hand-maintained price table (cents per 1M tokens).
// $1/1M == 100 cents/1M.
var modelRates = map[string]rate{
"claude-haiku-4-5-20251001": {inPerM: 100, outPerM: 500},
"claude-haiku-4-5": {inPerM: 100, outPerM: 500},
"claude-sonnet-4-6": {inPerM: 300, outPerM: 1500},
"claude-opus-4-8": {inPerM: 500, outPerM: 2500},
"claude-opus-4-7": {inPerM: 500, outPerM: 2500},
}
// CostCents returns the cost of a classification call in cents, and whether the
// model's price is known. Cache tokens are billed at the input rate (a slight
// over-estimate — cache reads actually bill at ~0.1× input — which is fine for a
// "what did this scan cost" readout). Returns ok=false for an unpriced model.
func CostCents(model string, u Usage) (float64, bool) {
r, ok := modelRates[model]
if !ok {
return 0, false
}
inTokens := float64(u.InputTokens + u.CacheReadTokens + u.CacheCreationTokens)
cents := inTokens*r.inPerM/1_000_000 + float64(u.OutputTokens)*r.outPerM/1_000_000
return cents, true
}