Skip to content

turnpike 0.3.0

Choose a tag to compare

@wilbeibi wilbeibi released this 19 Aug 21:59
· 30 commits to main since this release
461729e

Prices carry time. prices.json used to be a snapshot of now, so every refresh repriced all of history — DeepSeek's August increase (~2.5x blended on a real workload) would have under-reported new calls until models.dev caught up, then retroactively inflated a month of old ones the instant it did. The table is now a price history: each call is priced at the rates in force when it was made.

"deepseek-v4-pro": {
  "input_per_m": 0.435, "output_per_m": 0.87,
  "pinned": true,            // upstream lists one of the two rates; don't clobber this
  "revisions": [{
    "effective_from": "2026-08-16T16:00:00Z",
    "input_per_m": 1.32, "output_per_m": 3.96,
    "off_peak": { "utc": ["10:00-01:00", "04:00-06:00"], "multiplier": 0.5 }
  }]
}

prices pull appends a dated revision rather than replacing what's there, stamped with the pull time — if you know the real date, edit effective_from and the next pull leaves it alone. Providers that bill by the clock get a recurring UTC off_peak window. Base rates always hold the undiscounted price, so a window you forget to write overcharges you on paper rather than hiding spend — the same bias as turnpike's refusal to report a confident $0.

That makes prices.json the only record of your price history, so it's treated as your data rather than a cache: a pull keeps the previous copy at prices.json.bak and swaps the new one in by rename, and a table it can't parse is an error that leaves the file untouched. A revision only has to state what changed — anything it omits it inherits from the base entry, so a hand-written revision can't quietly delete pricing it didn't mention.

Pre-existing tables have neither field, so they load and price identically.

turnpike stats --by-tool. Every call now records how its identity was learned, so --by-tool can group each one under its single best identity — the declared x-turnpike-client header when the caller sends one, otherwise the observed calling process, falling back to the User-Agent. One column per call instead of three you have to reconcile by hand. The new column forward-migrates onto existing databases; old rows keep the historical client-then-process order.

check has a fourth exit code. 3 is unknown — nothing is broken, turnpike just can't vouch for the number yet (no calls recorded, or some have no price). Previously that collapsed into 2 alongside a typo'd --budget and corrupt data, which meant "go investigate" and "wait for a price table" were the same signal:

turnpike check --budget 50/day -q
case $? in
  0) ;;                              # under budget
  1) ntfy send "LLM budget blown" ;; # at/over — the actionable alert
  3) ;;                              # unknown — no data yet, or a price gap; not an error
  *) exit $? ;;                      # 2: something's actually broken
esac

If you branch on check's exit code, this is the one thing to look at when upgrading.

Install: cargo install turnpike (crates.io) or cargo install --path . from a clone.

Full changelog: v0.2.0...v0.3.0