Skip to content

feat(cost): Cost compare — fleet usage priced as any hosted model - #231

Closed
fish-not-phish wants to merge 3 commits into
MiaAI-Lab:mainfrom
fish-not-phish:feat/cost-compare
Closed

fish-not-phish wants to merge 3 commits into
MiaAI-Lab:mainfrom
fish-not-phish:feat/cost-compare

Conversation

@fish-not-phish

@fish-not-phish fish-not-phish commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

New Cost compare page (/cost, sidebar → "Cost compare", mobile Stats sheet, command
palette): prices the fleet's token counters at any hosted model — "what would our usage have
cost on Claude Opus, Sonnet, GPT-5.2, …?"

  • Any model, live: searchable dropdown over the community
    litellm price catalog
    (~2.9k chat models, prefix-ranked search). The last pick is remembered per browser.
  • Honest cost split: computed prefill × input rate, completions × output rate, cached
    prefill × the provider's cache-read rate when published (else the full input rate) — so the
    page also shows what the prefix cache saves in the selected period.
  • Same time ranges (24 h → all time) and the same per-Spark/port/model token counters as the
    Token totals page, so the two pages reconcile; All time uses lifetime totals.

How

  • GET /api/model-prices (server/prices/ModelPrices.js): fetches the litellm JSON, keeps
    chat-shaped entries with real per-token input/output costs, serves a slim catalog. Cached in
    memory for 24 h; the last good copy persists to config/model-prices.json (gitignored) and
    is served if the upstream fetch fails — stale prices beat an error, 502 only with no cache
    at all.
  • All pricing math lives in src/components/CostPage/costStats.ts — pure functions, no
    React, no I/O — so the page component stays presentation-only.

Testing

Per README.md §Contributions:

  • npm run typecheck — clean
  • npm run test:frontend — 99 files / 803 tests pass, including the new
    src/components/CostPage/costStats.test.ts and the updated MobileTabBar sheet test
  • Server: node --test server/__tests__/isolatedEnv.test.js server/prices/__tests__/ModelPrices.test.js — parser, disk-cache fallback and the
    no-cache 502 path covered; server/prices added to the test:server glob in
    package.json; the new MODEL_PRICES_JSON_PATH registered in isolatedEnv.js
  • Full server suite run on the branch: no new failures vs main (the deltas are the
    environment-dependent sudo/vulnhunt/llmlaunch checks, identical on pristine main)
  • Manual: served endpoint returns 2865 models; /cost renders in all four themes; the
    page keeps working with the network path down via the disk cache

Notes

  • No new runtime dependencies; the fetch uses global fetch (Node 18+) like
    server/collectors/HermesReleases.js.
  • The catalog refresh is lazy (on request, TTL-gated) — no timer, nothing to start/stop;
    server/index.js changes are two lines (import + one app.get).
  • The shared Spark model is untouched; costs aggregate the existing fleet-wide token rows.

Add a /cost page that prices the fleet's token counters at any model
from the community litellm price list, with a searchable catalog
dropdown (~2.9k models), input/output/cached cost split, per-day cost
chart, cache-savings and blended $/1M tiles.

- server/prices: GET /api/model-prices; 24 h memory cache, last-good
  copy persisted to config/model-prices.json as an offline fallback
- src/components/CostPage: page + pure cost math (costStats) w/ tests
- wire into sidebar, mobile sheet, command palette and routes
@MiaAI-Lab

Copy link
Copy Markdown
Owner

Reviewed at ff4cd86. It merges cleanly and typecheck, build and the full tests pass (server 1064/1065, frontend 803), and I found no injection or SSRF problem (the URL is a constant and the route sits behind the auth middleware), but it is not ported yet. (1) server/prices/ModelPrices.js:124-129 seeds the memory copy from the disk cache, so after the first failed refresh the next request serves that old catalog with stale:false for the whole TTL, hiding that prices are out of date; keep the disk copy separate and answer stale:true while refreshes fail. (2) A failed refresh is retried on every request with a 15 s timeout and no shared in-flight request, and CostPage.tsx:54-60 loads prices and usage together, so a LAN without internet leaves the page on 'Loading prices…'; add a short negative cache, one in-flight request, a shorter timeout and load prices independently of usage. (3) CostPage.tsx:293-294 divides an All-time cost by a fixed 30 days; reuse the tracked-days helper from Token totals. (4) The page calls raw.githubusercontent.com with no way to turn it off; please make it opt-in (setting or env), document it in the README, and add redirect: "error" and a body size cap. Leaving it open for the author.

(Review notes prepared with Claude Code on behalf of the maintainer.)

…tch, tracked-day average

Review items from MiaAI-Lab on MiaAI-Lab#231:

- ModelPrices: the disk fallback is no longer promoted into the fresh memory
  cache; it is served (and stays) stale:true while refreshes fail.
- ModelPrices: failed refreshes retry at most once per 60 s and share a single
  in-flight attempt; fetch timeout 15 s -> 5 s.
- ModelPrices: the raw.githubusercontent refresh is now opt-in via
  MODEL_PRICES_FETCH=1 (default off: the route serves the disk catalog and
  never phones out); the default fetch uses redirect:'error' and a 10 MB body cap.
- CostPage: prices load independently of usage, so a slow/dead price source
  no longer holds the whole page on a spinner; price failures get their own
  note with retry.
- CostPage: the All-time 'average per day' divides by tracked days
  (trackedBuckets, same rule as Token totals) instead of a fixed 30.
- README/.env.example: MODEL_PRICES_FETCH + MODEL_PRICES_JSON_PATH documented.
@fish-not-phish

fish-not-phish commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review! All four points are addressed in d95f735:

  1. Stale flag — the disk fallback is no longer promoted into the memory cache. It's a separate copy that answers stale: true for as long as refreshes keep failing; only a successful fetch resets to stale: false.
  2. Refresh resilience — failed refreshes are negative-cached for 60 s and share a single in-flight request, and the timeout drops to 5 s. The page also loads prices independently of usage now, so a network-less LAN gets the usage view plus a "price catalog could not be loaded — Retry" note instead of a spinner.
  3. All-time average — now divides by trackedBuckets(firstKey, "day", …), the same rule the Token totals page uses, instead of a fixed 30.
  4. Opt-in fetch — the GitHub refresh only runs with MODEL_PRICES_FETCH=1 (documented in README Environment variables and .env.example); while off, the route serves the disk cache and makes no requests. The fetch also uses redirect: "error" and enforces a 10 MB body cap (content-length and actual bytes).

Tests: five new cases in server/prices/__tests__/ModelPrices.test.js covering each of the above (stale persistence, retry window + in-flight dedupe, opt-out 502 hint, redirect/cap); npm run typecheck clean; frontend suite 802 passing — the one failure is a pre-existing timezone-dependent flake in benchShareCard that fails on pristine main too.

# Conflicts:
#	server/index.js
#	src/components/shell/AppSidebar.tsx
MiaAI-Lab pushed a commit that referenced this pull request Oct 19, 2026
Squash of #231 (ff4cd86, d95f735, ef69b33) by its author, with two changes on
top: the price fetch counts bytes as they stream and cancels past the 10 MB cap
(a body without Content-Length or a compressed one was buffered whole before the
cap applied), and a CHANGELOG entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@MiaAI-Lab

Copy link
Copy Markdown
Owner

Now on main as b361e06, squashed with your authorship (thank you). I re-checked the four points from the review at server/prices/ModelPrices.js and CostPage.tsx: the disk copy no longer masks staleness, failed refreshes are negative-cached with one request in flight, the All-time average uses the tracked-days helper, and the GitHub fetch is opt-in (MODEL_PRICES_FETCH=1, server/config.js:44) with redirect: "error". One change on top: the 10 MB cap was checked after res.text() had buffered the whole body, so a response without Content-Length (or a compressed one) got past it. fetchJsonCapped (ModelPrices.js:77) now counts bytes as they stream and cancels past the cap, with a test. I also added the CHANGELOG entry. Typecheck, build and the full tests pass.

(Review notes prepared with Claude Code on behalf of the maintainer.)

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.

2 participants