From 28b5e4744333c8ad9f0f51a20fd54b20cc2cbf85 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:22:17 +0000 Subject: [PATCH] Sync ZGPU_PRICING with the published catalog, and guard it Checked every model in the docs catalog against the CLI. No model id is missing: all 14 published models already have a ZGPU_PRICING entry and are reachable from a command, and CHAT_MODELS covers all six text-generation models the catalog exposes for chat. The gap was not coverage, it was a price. gpt-oss-120b was repriced to $0.15 / $0.60 per 1M (docs commits "fixed pricing discrepancy" and "increased price for gpt-oss"); the CLI still had {0.03, 0.10}. Because savings are reported as baseline cost minus ZeroGPU cost, understating the ZeroGPU rate 5x on input and 6x on output made every gpt-oss-120b call report more saved than was actually saved. That is the one number in this table a user is asked to trust, so it is now correct. ZGPU_FALLBACK is unchanged: glm-5.2 at {1.10, 3.50} is still the priciest published rate, and the existing "never overstates savings" invariant still holds with gpt-oss at its true price. The real defect was that nothing tied this table to the catalog, so a docs reprice could sit unnoticed indefinitely. ZGPU_PRICING is now exported and pinned by two tests: one asserts every catalog model carries the published rate, the other asserts nothing is priced that the catalog does not list, so drift fails CI in both directions instead of quietly misreporting. Both were verified to fail when the stale rate is restored and when an unlisted model is added. Not added: zlm-v1-moderation-edge, referenced by the Claude Skill docs but absent from the model catalog, both OpenAPI enums, and any model page. It has no published price or endpoint contract to implement against, so adding it would mean inventing both. Co-Authored-By: Claude --- src/lib/savings.ts | 7 ++++--- tests/savings.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/lib/savings.ts b/src/lib/savings.ts index d5cab4b..8cf26b0 100644 --- a/src/lib/savings.ts +++ b/src/lib/savings.ts @@ -17,9 +17,10 @@ const CLAUDE_PRICING: Record = { const DEFAULT_BASELINE = "claude-opus-4-8"; // Actual ZeroGPU pricing per 1M tokens (in / out), per model. -// Source: https://docs.zerogpu.ai model catalog. -const ZGPU_PRICING: Record = { - "gpt-oss-120b": { in: 0.03, out: 0.1 }, +// Source: https://docs.zerogpu.ai model catalog. Exported so a test can pin +// this table to the published catalog — see tests/savings.test.ts. +export const ZGPU_PRICING: Record = { + "gpt-oss-120b": { in: 0.15, out: 0.6 }, "qwen3-30b-a3b-fp8": { in: 0.05, out: 0.3 }, "glm-5.2": { in: 1.1, out: 3.5 }, "deepseek-v4-flash": { in: 0.07, out: 0.14 }, diff --git a/tests/savings.test.ts b/tests/savings.test.ts index bdab8f8..f7347e8 100644 --- a/tests/savings.test.ts +++ b/tests/savings.test.ts @@ -12,6 +12,7 @@ import { savingsPath, formatNotice, formatReport, + ZGPU_PRICING, type SavingsState, } from "../src/lib/savings.js"; @@ -102,6 +103,46 @@ describe("computeCallSavings", () => { }); }); +describe("ZGPU_PRICING tracks the published model catalog", () => { + // Transcribed from https://docs.zerogpu.ai/docs/model-catalog ("At a glance"). + // This is the whole catalog, not a sample: if ZeroGPU publishes a new model or + // repriced an existing one, this table is what must change, and the two tests + // below then force src/lib/savings.ts to change with it. Without this guard the + // CLI silently keeps a stale rate and misreports savings — which is exactly how + // gpt-oss-120b sat at $0.03/$0.10 long after it was repriced to $0.15/$0.60. + const CATALOG: Record = { + "gpt-oss-120b": { in: 0.15, out: 0.6 }, + "qwen3-30b-a3b-fp8": { in: 0.05, out: 0.3 }, + "glm-5.2": { in: 1.1, out: 3.5 }, + "deepseek-v4-flash": { in: 0.07, out: 0.14 }, + "llama-3.1-8b-instruct-fast": { in: 0.02, out: 0.05 }, + "zlm-v2-iab-classify-edge-enriched": { in: 0.02, out: 0.05 }, + "zlm-v1-iab-classify-edge": { in: 0.02, out: 0.05 }, + "zlm-v1-iab-domain-classifier": { in: 0.02, out: 0.05 }, + "zlm-v1-followup-questions-edge": { in: 0.02, out: 0.05 }, + "gliner-multi-pii-v1": { in: 0.02, out: 0.05 }, + "gliner2-base-v1": { in: 0.02, out: 0.05 }, + "deberta-v3-small": { in: 0.02, out: 0.05 }, + "LFM2.5-1.2B-Thinking": { in: 0.02, out: 0.05 }, + "LFM2.5-1.2B-Instruct": { in: 0.02, out: 0.05 }, + }; + + it("prices every catalog model at the published rate", () => { + for (const [model, published] of Object.entries(CATALOG)) { + expect( + ZGPU_PRICING[model], + `${model}: ZGPU_PRICING disagrees with the published catalog`, + ).toEqual(published); + } + }); + + it("prices nothing that the catalog does not list", () => { + // Guards the other direction: a model retired from the catalog, or a typo'd + // id, would otherwise linger here and quietly price real calls. + expect(Object.keys(ZGPU_PRICING).sort()).toEqual(Object.keys(CATALOG).sort()); + }); +}); + describe("resolveBaselineModel", () => { it("defaults to claude-opus-4-8", () => { expect(resolveBaselineModel()).toBe("claude-opus-4-8");