oxedyne/daimond/dev/verify_pricing.mjs
32.2 KiB, 1 run
created by r2519314175:599, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // verify_pricing.mjs — the cost pipeline's pure core, under Node. |
| 2 | // |
| 3 | // "This session" overstated real OpenRouter spend about six-fold, and not one |
| 4 | // part of that was a counting bug. Four separate things were wrong, and each is |
| 5 | // checked here, on the code as it is now, with the OLD behaviour reproduced |
| 6 | // beside it so the fix stays proven rather than merely asserted: |
| 7 | // |
| 8 | // 1. the rate table held direct-provider list prices 2.3x-4.5x above what a |
| 9 | // router actually charges; |
| 10 | // 2. cached prompt tokens were billed at the full input rate, because nothing |
| 11 | // ever supplied a cached count; |
| 12 | // 3. the resolver tested substrings BOTH ways and returned the first hit in |
| 13 | // object order, so `deepseek/deepseek-v3.2-exp` was billed at v3.1's rate; |
| 14 | // 4. the unknown-model fallback (1.00/3.00) caught most current router ids at |
| 15 | // roughly four times reality. |
| 16 | // |
| 17 | // And a fifth, found on 2026-08-20: the containment fallback in (3) was left |
| 18 | // supplying a CONTEXT WINDOW as well as a rate. `glm-5.3` normalises to `glm53`, |
| 19 | // which contains `glm5`, so it took the glm-5 entry -- 204,800 tokens where the |
| 20 | // generation it belongs to holds 1,048,576, and a price nobody published wearing |
| 21 | // no estimate mark. Containment cannot be tightened to tell that from a dated |
| 22 | // Anthropic id, which is checked below as a property rather than argued about. |
| 23 | // |
| 24 | // It also checks the ledger's new reported-cost path and the gateway's balance |
| 25 | // notice. Everything here is pure: no browser, no network, no gateway. The |
| 26 | // modules are IIFEs guarded on `window`, so they are evaluated in a sandbox with |
| 27 | // a stub -- the same trick verify_governor.mjs uses. |
| 28 | // |
| 29 | // node dev/verify_pricing.mjs --break nearwins # a neighbour's window again |
| 30 | // node dev/verify_pricing.mjs --break neartable # a borrowed rate called surveyed |
| 31 | // node dev/verify_pricing.mjs # and then, clean |
| 32 | import { readFileSync } from 'fs'; |
| 33 | |
| 34 | const ok = [], bad = []; |
| 35 | const check = (name, pass, detail) => { |
| 36 | (pass ? ok : bad).push(name); |
| 37 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 38 | }; |
| 39 | const near = (a, b, eps = 1e-9) => Math.abs(a - b) < eps; |
| 40 | const die = (why) => { console.error('ABORT: ' + why); process.exit(2); }; |
| 41 | |
| 42 | // ── The patches that prove the new checks can fail ───────────────── |
| 43 | // |
| 44 | // Each restores ONE half of the defect in the source text of `pricing.js` |
| 45 | // before it is evaluated, so the check is shown failing on the old behaviour |
| 46 | // rather than merely asserted against the new one. |
| 47 | // |
| 48 | // nearwins `contextWindow` back on `resolve`, so a neighbour's window is |
| 49 | // handed out as this model's -- the defect exactly as it shipped. |
| 50 | // neartable a borrowed rate counted as a surveyed one, so the figure is |
| 51 | // drawn without the estimate mark that says whose it is. |
| 52 | const BREAK = (() => { |
| 53 | const i = process.argv.indexOf('--break'); |
| 54 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 55 | })(); |
| 56 | const BREAKS = { |
| 57 | nearwins: { |
| 58 | what: 'contextWindow back on the containment resolver', |
| 59 | file: 'js/pricing.js', |
| 60 | edit: (src) => src.replace( |
| 61 | '\t\tvar entry = resolveExact(model);\n\t\treturn (entry && entry.ctx != null) ? entry.ctx : null;', |
| 62 | '\t\tvar entry = resolve(model);\n\t\treturn (entry && entry.ctx != null) ? entry.ctx : null;'), |
| 63 | }, |
| 64 | neartable: { |
| 65 | what: 'a borrowed rate counted as a surveyed one, so it wears no estimate mark', |
| 66 | file: 'js/pricing.js', |
| 67 | edit: (src) => src.replace( |
| 68 | "\t\treturn source === 'fallback' || source === 'near';", |
| 69 | "\t\treturn source === 'fallback';"), |
| 70 | }, |
| 71 | }; |
| 72 | if (BREAK && !BREAKS[BREAK]) die(`no break called "${BREAK}"`); |
| 73 | if (BREAK) console.log(`BREAK ${BREAK}: ${BREAKS[BREAK].what}\n`); |
| 74 | |
| 75 | // ── Load the modules under a stub window ─────────────────────────── |
| 76 | |
| 77 | function loadModule(rel, extra = {}) { |
| 78 | let src = readFileSync(new URL('../www/' + rel, import.meta.url), 'utf8'); |
| 79 | if (BREAKS[BREAK] && BREAKS[BREAK].file === rel) { |
| 80 | const out = BREAKS[BREAK].edit(src); |
| 81 | if (out === src) die(`the "${BREAK}" break no longer matches www/${rel}`); |
| 82 | src = out; |
| 83 | } |
| 84 | const names = ['window', ...Object.keys(extra)]; |
| 85 | const vals = [extra.window, ...Object.keys(extra).map(k => extra[k])]; |
| 86 | // eslint-disable-next-line no-new-func |
| 87 | new Function(...names, src)(...vals); |
| 88 | return extra.window; |
| 89 | } |
| 90 | |
| 91 | // A localStorage that lives in memory, so the ledger can be driven without a |
| 92 | // browser and without leaving anything behind. |
| 93 | function memStore() { |
| 94 | const map = new Map(); |
| 95 | return { |
| 96 | getItem: k => (map.has(k) ? map.get(k) : null), |
| 97 | setItem: (k, v) => { map.set(k, String(v)); }, |
| 98 | removeItem: k => { map.delete(k); }, |
| 99 | _map: map, |
| 100 | }; |
| 101 | } |
| 102 | |
| 103 | const win = {}; |
| 104 | const store = memStore(); |
| 105 | loadModule('js/pricing.js', { window: win }); |
| 106 | loadModule('js/ledger.js', { window: win, localStorage: store }); |
| 107 | |
| 108 | const P = win.DaimondPricing; |
| 109 | const L = win.DaimondLedger; |
| 110 | const C = P._core; |
| 111 | |
| 112 | // ── 1. The table holds routed prices, not list prices ────────────── |
| 113 | { |
| 114 | // Measured against openrouter.ai/api/v1/models on 2026-07-30. These are the |
| 115 | // figures the table was 2.3x-4.5x above. |
| 116 | const real = [ |
| 117 | ['z-ai/glm-5.2', 0.6153, 1.9338, 0.11427], |
| 118 | ['deepseek/deepseek-r1', 0.70, 2.50, null], |
| 119 | ['deepseek/deepseek-v4-pro', 0.435, 0.87, 0.003625], |
| 120 | ['openai/gpt-oss-120b', 0.037, 0.17, null], |
| 121 | ['meta-llama/llama-3.3-70b-instruct', 0.13, 0.40, null], |
| 122 | ['deepseek/deepseek-chat-v3.1', 0.25, 0.95, 0.13], |
| 123 | ['moonshotai/kimi-k2', 0.57, 2.30, null], |
| 124 | ['deepseek/deepseek-v3.2', 0.269, 0.40, 0.1345], |
| 125 | ['meta-llama/llama-4-scout', 0.10, 0.30, null], |
| 126 | ['meta-llama/llama-4-maverick', 0.20, 0.80, null], |
| 127 | ['qwen/qwen3-235b-a22b', 0.455, 1.82, null], |
| 128 | ]; |
| 129 | let wrong = []; |
| 130 | for (const [id, i, o, ca] of real) { |
| 131 | const r = P.rate(id); |
| 132 | if (!r) { wrong.push(id + ' unknown'); continue; } |
| 133 | if (!near(r.inUsdPerM, i) || !near(r.outUsdPerM, o)) { |
| 134 | wrong.push(`${id} ${r.inUsdPerM}/${r.outUsdPerM} vs ${i}/${o}`); |
| 135 | } |
| 136 | if (ca !== null && !near(r.cachedInUsdPerM, ca)) { |
| 137 | wrong.push(`${id} cached ${r.cachedInUsdPerM} vs ${ca}`); |
| 138 | } |
| 139 | } |
| 140 | check('the table matches the routed prices actually charged', wrong.length === 0, |
| 141 | wrong.join('; ')); |
| 142 | |
| 143 | // The old figures, and what they would have cost. This is the six-fold |
| 144 | // overstatement in one line, kept so nobody "tidies" the table back. |
| 145 | const OLD_GLM = { in: 1.40, out: 4.40 }; |
| 146 | const now = P.rate('z-ai/glm-5.2'); |
| 147 | check('glm-5.2 input is no longer more than double reality', |
| 148 | OLD_GLM.in / now.inUsdPerM > 2.2 && near(now.inUsdPerM, 0.6153), |
| 149 | `old ${OLD_GLM.in} vs now ${now.inUsdPerM}`); |
| 150 | check('glm-5.2 output is no longer more than double reality', |
| 151 | OLD_GLM.out / now.outUsdPerM > 2.2); |
| 152 | } |
| 153 | |
| 154 | // ── 2. A cached token costs less than a fresh one ────────────────── |
| 155 | { |
| 156 | const M = 1e6; |
| 157 | const fresh = P.priceFor('z-ai/glm-5.2', M, 0, 0); |
| 158 | const cached = P.priceFor('z-ai/glm-5.2', M, 0, M); |
| 159 | check('priceFor charges a cached prompt less than a fresh one', |
| 160 | cached.usd < fresh.usd, `${cached.usd} vs ${fresh.usd}`); |
| 161 | check('a wholly cached prompt costs the published cache rate', |
| 162 | near(cached.usd, 0.11427), 'got ' + cached.usd); |
| 163 | // The old call site hardcoded cachedTokens: 0, so this is exactly what the |
| 164 | // app was paying for -- an agentic loop's prompt is mostly cache. |
| 165 | check('billing a cached prompt as fresh overstates it by the rate ratio', |
| 166 | near(fresh.usd / cached.usd, 0.6153 / 0.11427, 1e-6), |
| 167 | 'ratio ' + (fresh.usd / cached.usd).toFixed(3)); |
| 168 | // A cached count larger than the prompt is a provider bug, not a discount. |
| 169 | check('cached tokens cannot exceed the prompt', |
| 170 | near(P.priceFor('z-ai/glm-5.2', 1000, 0, 999999).usd, |
| 171 | P.priceFor('z-ai/glm-5.2', 1000, 0, 1000).usd)); |
| 172 | // A model with no published cache rate bills cache at the input rate, which |
| 173 | // is the honest reading of "no discount published". |
| 174 | const noCache = P.rate('deepseek/deepseek-r1'); |
| 175 | check('a model with no published cache rate says so', noCache.cachedInUsdPerM === null); |
| 176 | check('and its cached tokens bill at the input rate', |
| 177 | near(P.priceFor('deepseek/deepseek-r1', 1e6, 0, 1e6).usd, 0.70)); |
| 178 | } |
| 179 | |
| 180 | // ── 3. The resolver cannot mis-map ───────────────────────────────── |
| 181 | // |
| 182 | // RED FIRST. This is the algorithm and the table as they shipped at seal 45, |
| 183 | // reproduced verbatim, so the defect is demonstrated rather than described. |
| 184 | { |
| 185 | const OLD_TABLE = { |
| 186 | 'glm-5.2': { in: 1.40, out: 4.40, alias: ['glm-5p2', 'glm5.2'] }, |
| 187 | 'deepseek-v3.1': { in: 0.60, out: 1.70, alias: ['deepseek-ai/deepseek-v3.1', 'deepseek-v3', 'deepseek-ai/deepseek-v3', 'deepseek-v3p1'] }, |
| 188 | 'deepseek-r1': { in: 3.00, out: 7.00, alias: ['deepseek-ai/deepseek-r1'] }, |
| 189 | 'deepseek-v3.2': { in: 0.26, out: 0.38, alias: ['deepseek-ai/deepseek-v3.2', 'deepseek-v3p2'] }, |
| 190 | 'kimi-k2': { in: 1.00, out: 3.00, alias: ['moonshotai/kimi-k2'] }, |
| 191 | }; |
| 192 | const OLD_INDEX = {}; |
| 193 | for (const id in OLD_TABLE) { |
| 194 | OLD_INDEX[C.norm(id)] = id; |
| 195 | for (const a of (OLD_TABLE[id].alias || [])) OLD_INDEX[C.norm(a)] = id; |
| 196 | } |
| 197 | // The shipped resolve: exact, then a two-way substring test in object order. |
| 198 | const oldResolve = (model) => { |
| 199 | const key = C.norm(model); |
| 200 | if (!key) return null; |
| 201 | if (OLD_INDEX[key]) return OLD_TABLE[OLD_INDEX[key]]; |
| 202 | for (const k in OLD_INDEX) { |
| 203 | if (key.indexOf(k) !== -1 || k.indexOf(key) !== -1) return OLD_TABLE[OLD_INDEX[k]]; |
| 204 | } |
| 205 | return null; |
| 206 | }; |
| 207 | |
| 208 | const oldHit = oldResolve('deepseek/deepseek-v3.2-exp'); |
| 209 | check('RED: the old resolver billed deepseek-v3.2-exp at v3.1\'s rate', |
| 210 | oldHit !== null && near(oldHit.in, 0.60), |
| 211 | 'old resolve gave in=' + (oldHit && oldHit.in)); |
| 212 | const nowHit = P.rate('deepseek/deepseek-v3.2-exp'); |
| 213 | check('GREEN: it now resolves to its own entry', |
| 214 | nowHit !== null && near(nowHit.inUsdPerM, 0.27) && near(nowHit.outUsdPerM, 0.41), |
| 215 | 'now in=' + (nowHit && nowHit.inUsdPerM)); |
| 216 | check('and the v3.2-exp rate is not the v3.2 rate either', |
| 217 | !near(nowHit.outUsdPerM, 0.40), 'out=' + nowHit.outUsdPerM); |
| 218 | |
| 219 | // The reverse direction was the other half of the fault: a bare id matched a |
| 220 | // LONGER table key. Shown on the old algorithm, then absent from the new one. |
| 221 | const oldBare = oldResolve('z-ai/glm-5'); |
| 222 | check('RED: the old resolver matched a bare glm-5 against glm-5.2', |
| 223 | oldBare !== null && near(oldBare.in, 1.40)); |
| 224 | check('GREEN: glm-5 now gets its own, dearer, rate', |
| 225 | near(P.rate('z-ai/glm-5').inUsdPerM, 0.95)); |
| 226 | |
| 227 | // The property, over the whole table: every canonical id and every alias |
| 228 | // resolves to its OWN entry, and no id resolves to an entry other than the |
| 229 | // owner of its longest matching key. |
| 230 | let selfFails = []; |
| 231 | for (const id in C.TABLE) { |
| 232 | if (C.resolve(id) !== C.TABLE[id]) selfFails.push(id); |
| 233 | for (const a of (C.TABLE[id].alias || [])) { |
| 234 | if (C.resolve(a) !== C.TABLE[id]) selfFails.push(a + ' → not ' + id); |
| 235 | } |
| 236 | } |
| 237 | check('every canonical id and alias resolves to itself', selfFails.length === 0, |
| 238 | selfFails.slice(0, 4).join('; ')); |
| 239 | |
| 240 | const longestOwner = (model) => { |
| 241 | const key = C.norm(model); |
| 242 | if (!key) return null; |
| 243 | if (C.INDEX[key]) return C.TABLE[C.INDEX[key]]; |
| 244 | let best = null; |
| 245 | for (const k of Object.keys(C.INDEX)) { |
| 246 | if (key.indexOf(k) !== -1 && (best === null || k.length > best.length)) best = k; |
| 247 | } |
| 248 | return best === null ? null : C.TABLE[C.INDEX[best]]; |
| 249 | }; |
| 250 | // A corpus of real router ids, table members and near-misses. |
| 251 | const corpus = [ |
| 252 | 'z-ai/glm-5.2', 'z-ai/glm-5.1', 'z-ai/glm-5', 'z-ai/glm-5-turbo', 'z-ai/glm-4.6', |
| 253 | 'z-ai/glm-4.7', 'z-ai/glm-4.7-flash', 'z-ai/glm-4.5-air', |
| 254 | 'deepseek/deepseek-chat', 'deepseek/deepseek-chat-v3.1', 'deepseek/deepseek-v3.1-terminus', |
| 255 | 'deepseek/deepseek-v3.2', 'deepseek/deepseek-v3.2-exp', 'deepseek/deepseek-r1', |
| 256 | 'deepseek/deepseek-r1-0528', 'deepseek/deepseek-v4-pro', 'deepseek/deepseek-v4-flash', |
| 257 | 'moonshotai/kimi-k2', 'moonshotai/kimi-k2-0905', 'moonshotai/kimi-k2.5', |
| 258 | 'moonshotai/kimi-k2.6', 'moonshotai/kimi-k2-thinking', 'moonshotai/kimi-k3', |
| 259 | 'openai/gpt-oss-120b', 'openai/gpt-oss-20b', 'openai/gpt-5.4', 'openai/gpt-5.2', |
| 260 | 'anthropic/claude-opus-5', 'anthropic/claude-sonnet-5', 'anthropic/claude-haiku-4.5', |
| 261 | 'google/gemini-3.1-pro-preview', 'google/gemini-2.5-flash', 'google/gemini-3.6-flash', |
| 262 | 'meta-llama/llama-3.3-70b-instruct', 'meta-llama/llama-4-scout', 'meta-llama/llama-4-maverick', |
| 263 | 'qwen/qwen3-235b-a22b', 'qwen/qwen3-coder', 'qwen/qwen3-max', 'qwen/qwen3.7-plus', |
| 264 | 'minimax/minimax-m2', 'minimax/minimax-m2.5', 'minimax/minimax-m3', |
| 265 | 'x-ai/grok-4.5', 'x-ai/grok-4.3', |
| 266 | 'accounts/fireworks/models/glm-5p2', 'accounts/fireworks/models/deepseek-v4-pro', |
| 267 | ]; |
| 268 | let propFails = []; |
| 269 | for (const id of corpus) { |
| 270 | if (C.resolve(id) !== longestOwner(id)) propFails.push(id); |
| 271 | } |
| 272 | check('no id resolves to anything but its longest-matching-key owner', |
| 273 | propFails.length === 0, propFails.slice(0, 5).join(', ')); |
| 274 | |
| 275 | // Prove the property test has teeth: the OLD algorithm, run over the NEW |
| 276 | // table, violates it. A check that cannot fail proves nothing. |
| 277 | const oldOverNew = (model) => { |
| 278 | const key = C.norm(model); |
| 279 | if (!key) return null; |
| 280 | if (C.INDEX[key]) return C.TABLE[C.INDEX[key]]; |
| 281 | for (const k in C.INDEX) { |
| 282 | if (key.indexOf(k) !== -1 || k.indexOf(key) !== -1) return C.TABLE[C.INDEX[k]]; |
| 283 | } |
| 284 | return null; |
| 285 | }; |
| 286 | let oldViolations = corpus.filter(id => oldOverNew(id) !== longestOwner(id)); |
| 287 | check('RED: the old algorithm violates that property on the new table', |
| 288 | oldViolations.length > 0, oldViolations.slice(0, 5).join(', ')); |
| 289 | } |
| 290 | |
| 291 | // ── 3b. A neighbour lends its rate and never its window ──────────── |
| 292 | // |
| 293 | // The remaining half of (3). Containment still places an id the table has never |
| 294 | // heard of, because a rate borrowed from the nearest relation beats the flat |
| 295 | // fallback and is visible in the spend readout either way. A WINDOW is not |
| 296 | // visible: a conversation clipped to a fifth of what the model would have held |
| 297 | // looks exactly like a conversation the model would not hold, so a borrowed |
| 298 | // window is withheld and the agent's own default assumption stands instead. |
| 299 | { |
| 300 | const NEAR = 'z-ai/glm-5.3'; // a real id the table does not carry |
| 301 | check('the case under test is real: glm-5.3 is placed only by containment', |
| 302 | C.resolveExact(NEAR) === null && C.resolve(NEAR) === C.TABLE['glm-5'], |
| 303 | 'exact=' + (C.resolveExact(NEAR) ? 'hit' : 'null')); |
| 304 | |
| 305 | // What it used to hand out, and what the generation it belongs to actually |
| 306 | // holds. The ratio is the clip, and it is here so nobody restores the |
| 307 | // fallback thinking it a tidy-up. |
| 308 | const borrowed = C.TABLE['glm-5'].ctx, sibling = C.TABLE['glm-5.2'].ctx; |
| 309 | check('RED: the borrowed window was a fifth of the sibling generation\'s', |
| 310 | sibling / borrowed > 4, `${borrowed} vs ${sibling}`); |
| 311 | check('a containment-only id now has NO context window', |
| 312 | P.contextWindow(NEAR, '') === null, |
| 313 | 'got ' + P.contextWindow(NEAR, '')); |
| 314 | check('and an exactly-known id keeps its own', |
| 315 | P.contextWindow('z-ai/glm-5.2', '') === 1048576); |
| 316 | |
| 317 | // The rate still comes, and says what it is. |
| 318 | const est = P.priceFor(NEAR, 1e6, 0, 0); |
| 319 | check('a containment-only id is still priced', est.usd > 0, 'usd ' + est.usd); |
| 320 | check('and the figure is the neighbour\'s, borrowed', |
| 321 | near(est.usd, C.TABLE['glm-5'].in), 'usd ' + est.usd); |
| 322 | check('and it is marked estimated, because nobody published it for this model', |
| 323 | est.estimated === true); |
| 324 | check('and the borrowing is named rather than passed off as surveyed', |
| 325 | est.source === 'near', 'source ' + est.source); |
| 326 | check('rate() names it the same way, so display and charge cannot disagree', |
| 327 | P.rate(NEAR).source === 'near' && P.rate('z-ai/glm-5.2').source === 'table', |
| 328 | P.rate(NEAR).source); |
| 329 | check('an exactly-known id is still not an estimate', |
| 330 | P.priceFor('z-ai/glm-5.2', 1e6, 0, 0).estimated === false); |
| 331 | // `near` and `fallback` are different states and stay so: one has a |
| 332 | // neighbour's number in it, the other has nobody's. |
| 333 | const none = P.priceFor('some-vendor/never-heard-of-it', 1e6, 0, 0); |
| 334 | check('a model with no neighbour at all still reads as fallback, not near', |
| 335 | none.source === 'fallback' && P.contextWindow('some-vendor/never-heard-of-it', '') === null); |
| 336 | |
| 337 | // A live quote outranks all of this: a provider that publishes a window for |
| 338 | // an id the table has never seen is the best answer there is. |
| 339 | win.DaimondModels = { |
| 340 | rateFor: (provider, model) => (provider === 'zai' && model === NEAR) |
| 341 | ? { inPerM: 0.9, outPerM: 2.8, cachedPerM: null, ctx: 1048576 } : null, |
| 342 | }; |
| 343 | check('a live window is used even where the table can only guess', |
| 344 | P.contextWindow(NEAR, 'zai') === 1048576, 'got ' + P.contextWindow(NEAR, 'zai')); |
| 345 | check('and a live quote for such an id is not an estimate', |
| 346 | P.priceFor(NEAR, 1e6, 0, 0, 'zai').estimated === false); |
| 347 | delete win.DaimondModels; |
| 348 | |
| 349 | // WHY THE FALLBACK IS NOT SIMPLY TIGHTENED, as a property rather than an |
| 350 | // opinion. The obvious repair -- refuse a match whose leftover starts with a |
| 351 | // digit, so `glm5` + `3` cannot borrow -- also refuses `claudeopus5` + |
| 352 | // `20251001`, which is the SAME model on a dated id and must match. `norm` |
| 353 | // has already dropped the separator that told them apart, so no rule reading |
| 354 | // the normalised string can. Anything that reddens this check has broken |
| 355 | // Anthropic's dated ids. |
| 356 | const leftover = (id) => { |
| 357 | const key = C.norm(id); |
| 358 | for (const k of C.KEYS) { |
| 359 | const at = key.indexOf(k); |
| 360 | if (at !== -1) return key.slice(0, at) + key.slice(at + k.length); |
| 361 | } |
| 362 | return null; |
| 363 | }; |
| 364 | const bump = leftover('z-ai/glm-5.3'); // a different model |
| 365 | const dated = leftover('anthropic/claude-opus-5-20251001'); // the same model, dated |
| 366 | check('a version bump and a date suffix are one shape after normalisation', |
| 367 | /^\d/.test(bump) && /^\d/.test(dated), `bump "${bump}" vs dated "${dated}"`); |
| 368 | check('and the dated id still reaches its own family', |
| 369 | C.resolve('anthropic/claude-opus-5-20251001') === C.TABLE['claude-opus-5']); |
| 370 | } |
| 371 | |
| 372 | // ── 4. The fallback is no longer four times reality ──────────────── |
| 373 | { |
| 374 | check('the fallback input rate is honest', near(P.fallback.inUsdPerM, 0.40), |
| 375 | 'got ' + P.fallback.inUsdPerM); |
| 376 | check('the fallback output rate is honest', near(P.fallback.outUsdPerM, 1.20)); |
| 377 | check('the fallback publishes a cache discount', P.fallback.cachedInUsdPerM < P.fallback.inUsdPerM); |
| 378 | // The old 1.00/3.00 against the median of what the table now holds. |
| 379 | const ins = Object.keys(C.TABLE).map(k => C.TABLE[k].in).sort((a, b) => a - b); |
| 380 | const median = ins[Math.floor(ins.length / 2)]; |
| 381 | check('and it is within a factor of three of the table median', |
| 382 | P.fallback.inUsdPerM / median < 3 && median / P.fallback.inUsdPerM < 3, |
| 383 | `fallback ${P.fallback.inUsdPerM} vs median ${median}`); |
| 384 | const unknown = P.priceFor('some-vendor/never-heard-of-it', 1e6, 1e6, 0); |
| 385 | check('an unknown model is marked estimated', unknown.estimated === true); |
| 386 | check('and a known one is not', P.priceFor('z-ai/glm-5.2', 1000, 100, 0).estimated === false); |
| 387 | check('the source is named', P.priceFor('z-ai/glm-5.2', 10, 1, 0).source === 'table' |
| 388 | && unknown.source === 'fallback'); |
| 389 | } |
| 390 | |
| 391 | // ── 5. A live rate from the provider beats the table ─────────────── |
| 392 | { |
| 393 | win.DaimondModels = { |
| 394 | rateFor: (provider, model) => (provider === 'openrouter' && model === 'z-ai/glm-5.2') |
| 395 | ? { inPerM: 0.5, outPerM: 1.5, cachedPerM: 0.05, ctx: 999 } : null, |
| 396 | }; |
| 397 | const live = P.priceFor('z-ai/glm-5.2', 1e6, 0, 0, 'openrouter'); |
| 398 | check('a live quote is used ahead of the table', near(live.usd, 0.5), 'got ' + live.usd); |
| 399 | check('a live quote is not called an estimate', live.estimated === false && live.source === 'live'); |
| 400 | check('a live context window wins too', P.contextWindow('z-ai/glm-5.2', 'openrouter') === 999); |
| 401 | check('without a provider the table still answers', |
| 402 | near(P.priceFor('z-ai/glm-5.2', 1e6, 0, 0).usd, 0.6153)); |
| 403 | check('an unknown provider falls back to the table', |
| 404 | near(P.priceFor('z-ai/glm-5.2', 1e6, 0, 0, 'groq').usd, 0.6153)); |
| 405 | delete win.DaimondModels; |
| 406 | } |
| 407 | |
| 408 | // ── 6. The ledger stores a reported cost verbatim ────────────────── |
| 409 | { |
| 410 | store._map.clear(); |
| 411 | const t0 = Date.now(); |
| 412 | // The end-to-end figure from the diagnosis: usage.cost 0.0021. |
| 413 | const rep = L.record({ ts: t0, model: 'z-ai/glm-5.2', promptTokens: 10240, |
| 414 | completionTokens: 128, cachedTokens: 9216, costUsd: 0.0021, provider: 'openrouter' }); |
| 415 | check('a reported cost is stored verbatim', rep.u === 0.0021, 'got ' + rep.u); |
| 416 | check('and flagged as reported', rep.r === 1); |
| 417 | check('and not marked estimated', !rep.e); |
| 418 | check('the provider is recorded', rep.pv === 'openrouter'); |
| 419 | // What the table would have said, so the gap is on the record. |
| 420 | const guess = P.priceFor('z-ai/glm-5.2', 10240, 128, 9216).usd; |
| 421 | check('the reported figure differs from the table\'s guess', !near(rep.u, guess), |
| 422 | `reported ${rep.u} vs priced ${guess.toFixed(6)}`); |
| 423 | |
| 424 | const est = L.record({ ts: t0 + 1000, model: 'z-ai/glm-5.2', promptTokens: 100, |
| 425 | completionTokens: 10, cachedTokens: 0, provider: 'openrouter' }); |
| 426 | check('no reported cost falls back to the table', est.r === undefined && est.u > 0); |
| 427 | check('and a fallback entry is not flagged reported', !est.r); |
| 428 | // A zero or nonsense reported cost is not a report. |
| 429 | const zero = L.record({ ts: t0 + 2000, model: 'z-ai/glm-5.2', promptTokens: 100, |
| 430 | completionTokens: 10, cachedTokens: 0, costUsd: 0, provider: 'openrouter' }); |
| 431 | check('a reported zero is treated as "did not say"', !zero.r && zero.u > 0); |
| 432 | const nan = L.record({ ts: t0 + 3000, model: 'z-ai/glm-5.2', promptTokens: 100, |
| 433 | completionTokens: 10, cachedTokens: 0, costUsd: NaN, provider: 'openrouter' }); |
| 434 | check('a NaN reported cost is ignored', !nan.r && nan.u > 0); |
| 435 | |
| 436 | const tot = L.totals(); |
| 437 | check('the session total separates reported from priced', |
| 438 | near(tot.session.reportedUsd, 0.0021) && tot.session.usd > tot.session.reportedUsd, |
| 439 | `reported ${tot.session.reportedUsd} of ${tot.session.usd}`); |
| 440 | } |
| 441 | |
| 442 | // ── 7. perProvider sums the right entries and only those ─────────── |
| 443 | { |
| 444 | store._map.clear(); |
| 445 | const base = Date.now() - 60 * 60 * 1000; // an hour ago |
| 446 | // Two on the provider we care about, after the base moment. |
| 447 | L.record({ ts: base + 1000, model: 'm', promptTokens: 1, completionTokens: 1, |
| 448 | costUsd: 0.10, provider: 'openrouter' }); |
| 449 | L.record({ ts: base + 2000, model: 'm', promptTokens: 1, completionTokens: 1, |
| 450 | costUsd: 0.25, provider: 'openrouter' }); |
| 451 | // A decoy on another provider. |
| 452 | L.record({ ts: base + 3000, model: 'm', promptTokens: 1, completionTokens: 1, |
| 453 | costUsd: 9.99, provider: 'groq' }); |
| 454 | // A decoy BEFORE the base moment: already accounted for in the user's figure. |
| 455 | L.record({ ts: base - 5000, model: 'm', promptTokens: 1, completionTokens: 1, |
| 456 | costUsd: 5.55, provider: 'openrouter' }); |
| 457 | // A decoy with no provider at all, as an entry written before `pv` existed. |
| 458 | L.record({ ts: base + 4000, model: 'm', promptTokens: 1, completionTokens: 1, |
| 459 | costUsd: 7.77 }); |
| 460 | |
| 461 | const rows = L.perProvider(base); |
| 462 | const byId = {}; |
| 463 | rows.forEach(r => { byId[r.provider] = r; }); |
| 464 | check('perProvider sums only the named provider, only after `since`', |
| 465 | near(byId.openrouter.usd, 0.35), 'got ' + (byId.openrouter && byId.openrouter.usd)); |
| 466 | check('a decoy on another provider is excluded', near(byId.groq.usd, 9.99)); |
| 467 | check('an entry with no provider groups under the empty id', |
| 468 | byId[''] !== undefined && near(byId[''].usd, 7.77)); |
| 469 | check('perProvider counts turns', byId.openrouter.turns === 2); |
| 470 | check('perProvider tracks the reported part', near(byId.openrouter.reportedUsd, 0.35)); |
| 471 | check('rows come back dearest first', rows[0].usd >= rows[rows.length - 1].usd); |
| 472 | check('an epoch-zero window sees everything', |
| 473 | near(L.perProvider(0).filter(r => r.provider === 'openrouter')[0].usd, 5.90)); |
| 474 | } |
| 475 | |
| 476 | // ── 8. Old entries still read — and their guesses are REPRICED ───── |
| 477 | { |
| 478 | // A store written before `pv` and `r` existed, priced by the old table |
| 479 | // whose guesses ran ~6x high. Every reader must tolerate the shape — and |
| 480 | // the first read must reprice the guesses under the corrected table |
| 481 | // (keeping the original in `u0`), because the stale figures were still |
| 482 | // inflating every total the user saw. |
| 483 | store._map.set('daimond-ledger', JSON.stringify([ |
| 484 | { t: Date.now() - 1000, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.5, e: false }, |
| 485 | { t: Date.now() - 500, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.25, e: true }, |
| 486 | ])); |
| 487 | // A fresh module life: repricing runs once per life, on the first read — |
| 488 | // which is what boot does. The long-lived L above has already spent its |
| 489 | // pass on earlier sections' stores. |
| 490 | const win8 = { }; |
| 491 | loadModule('js/pricing.js', { window: win8 }); |
| 492 | loadModule('js/ledger.js', { window: win8, localStorage: store }); |
| 493 | const L = win8.DaimondLedger; |
| 494 | const fair = P.priceFor('glm-5.2', 100, 10, 0, '').usd; |
| 495 | const tot = L.totals(); |
| 496 | check('an old store is repriced under the corrected table, not summed as guessed', |
| 497 | near(tot.session.usd, 2 * fair) && !near(tot.session.usd, 0.75), |
| 498 | 'session=' + tot.session.usd + ' fair=' + fair); |
| 499 | check('and reports nothing as reported', near(tot.session.reportedUsd, 0)); |
| 500 | const stored = JSON.parse(store._map.get('daimond-ledger')); |
| 501 | check('the original guess survives in u0 with the rp mark', |
| 502 | near(stored[0].u0, 0.5) && stored[0].rp === 1 && near(stored[1].u0, 0.25), |
| 503 | JSON.stringify(stored[0])); |
| 504 | check('perModel still works on old entries', L.perModel('month')[0].model === 'glm-5.2'); |
| 505 | check('perProvider groups old entries under the empty id', |
| 506 | L.perProvider(0)[0].provider === ''); |
| 507 | check('samples still project old entries', L.samples().length === 2); |
| 508 | } |
| 509 | |
| 510 | // ── 8b. A reported entry is never repriced ───────────────────────── |
| 511 | { |
| 512 | store._map.set('daimond-ledger', JSON.stringify([ |
| 513 | { t: Date.now() - 1000, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.0021, r: 1 }, |
| 514 | ])); |
| 515 | // A fresh module life, so the once-per-life guard does not skip the read. |
| 516 | const win2 = { }; |
| 517 | loadModule('js/pricing.js', { window: win2 }); |
| 518 | loadModule('js/ledger.js', { window: win2, localStorage: store }); |
| 519 | const t2 = win2.DaimondLedger.totals(); |
| 520 | check('a reported figure is money that moved — repricing never touches it', |
| 521 | near(t2.session.usd, 0.0021) && !JSON.parse(store._map.get('daimond-ledger'))[0].rp); |
| 522 | } |
| 523 | |
| 524 | // ── 8c. A session that stopped is not "this session" ─────────────── |
| 525 | { |
| 526 | const win3 = { }; |
| 527 | loadModule('js/pricing.js', { window: win3 }); |
| 528 | loadModule('js/ledger.js', { window: win3, localStorage: store }); |
| 529 | const L3 = win3.DaimondLedger; |
| 530 | // A tail that ended 16 minutes ago: last night's work, not this session. |
| 531 | store._map.set('daimond-ledger', JSON.stringify([ |
| 532 | { t: Date.now() - 20 * 60 * 1000, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.001, r: 1 }, |
| 533 | { t: Date.now() - 16 * 60 * 1000, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.001, r: 1 }, |
| 534 | ])); |
| 535 | const idle = L3.totals(); |
| 536 | check('a tail older than the session gap reads as NO current session', |
| 537 | near(idle.session.usd, 0) && idle.session.tokens === 0, |
| 538 | 'session=' + idle.session.usd); |
| 539 | check('the week still counts what the session no longer does', |
| 540 | near(idle.week.usd, 0.002)); |
| 541 | // A tail that ended five minutes ago is still this session. |
| 542 | store._map.set('daimond-ledger', JSON.stringify([ |
| 543 | { t: Date.now() - 5 * 60 * 1000, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.001, r: 1 }, |
| 544 | ])); |
| 545 | const live = L3.totals(); |
| 546 | check('a five-minute-old tail is still this session', near(live.session.usd, 0.001)); |
| 547 | } |
| 548 | |
| 549 | // ── 8d. What the reprice changed is readable, per period ─────────── |
| 550 | { |
| 551 | // The correction only earns trust if it can be shown. `repriced` answers |
| 552 | // what the touched entries cost now and what they were first guessed at, |
| 553 | // within the window the panel is showing, so the view can quote the figure |
| 554 | // the user remembers instead of dropping the total in silence. |
| 555 | const win4 = { }; |
| 556 | loadModule('js/pricing.js', { window: win4 }); |
| 557 | loadModule('js/ledger.js', { window: win4, localStorage: store }); |
| 558 | const L4 = win4.DaimondLedger; |
| 559 | const DAY = 86400000, now = Date.now(); |
| 560 | store._map.set('daimond-ledger', JSON.stringify([ |
| 561 | // Two already-repriced guesses inside the week. |
| 562 | { t: now - 2 * DAY, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.05, u0: 0.30, rp: 1 }, |
| 563 | { t: now - 3 * DAY, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.03, u0: 0.18, rp: 1 }, |
| 564 | // A third, older than the week but inside the month. |
| 565 | { t: now - 20 * DAY, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.01, u0: 0.06, rp: 1 }, |
| 566 | // A billed turn the reprice never touched. |
| 567 | { t: now - 1 * DAY, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.02, r: 1 }, |
| 568 | ])); |
| 569 | check('the ledger can say what the reprice changed', typeof L4.repriced === 'function'); |
| 570 | const m = (typeof L4.repriced === 'function') ? L4.repriced('month') : { turns: 0, usd: 0, was: 0 }; |
| 571 | const w = (typeof L4.repriced === 'function') ? L4.repriced('week') : { turns: 0, usd: 0, was: 0 }; |
| 572 | check('repriced() gives the corrected and the original total for the period', |
| 573 | m.turns === 3 && near(m.usd, 0.09) && near(m.was, 0.54), JSON.stringify(m)); |
| 574 | check('repriced() honours the period window', |
| 575 | w.turns === 2 && near(w.usd, 0.08) && near(w.was, 0.48), JSON.stringify(w)); |
| 576 | check('a billed turn is money that moved, so it is not part of the correction', |
| 577 | m.turns === 3 && !near(m.usd, 0.11), JSON.stringify(m)); |
| 578 | // A window holding nothing repriced answers with zeros, so the note the |
| 579 | // panel draws from this ages out on its own as the 90-day log retires them. |
| 580 | store._map.set('daimond-ledger', JSON.stringify([ |
| 581 | { t: now - 1 * DAY, m: 'glm-5.2', p: 100, c: 10, ca: 0, u: 0.02, r: 1 }, |
| 582 | ])); |
| 583 | const none = (typeof L4.repriced === 'function') ? L4.repriced('month') : null; |
| 584 | check('a period with no repriced turn reports nothing to explain', |
| 585 | !!none && none.turns === 0 && near(none.usd, 0) && near(none.was, 0), JSON.stringify(none)); |
| 586 | } |
| 587 | |
| 588 | // ── 9. The gateway's balance notice ──────────────────────────────── |
| 589 | { |
| 590 | // gateway.js needs rather more of a browser than the other two, so it is |
| 591 | // given only what `noteBalance` touches. Everything else in the file is |
| 592 | // declaration-only until called. |
| 593 | const events = []; |
| 594 | const gwin = { |
| 595 | dispatchEvent: (ev) => { events.push(ev); return true; }, |
| 596 | location: { href: 'https://example.test/', origin: 'https://example.test' }, |
| 597 | addEventListener: () => {}, |
| 598 | localStorage: store, |
| 599 | navigator: { language: 'en-AU', languages: ['en-AU'] }, |
| 600 | setTimeout: setTimeout, |
| 601 | }; |
| 602 | class FakeCustomEvent { |
| 603 | constructor(type, init) { this.type = type; this.detail = (init || {}).detail; } |
| 604 | } |
| 605 | const src = readFileSync(new URL('../www/js/gateway.js', import.meta.url), 'utf8'); |
| 606 | // eslint-disable-next-line no-new-func |
| 607 | new Function('window', 'localStorage', 'navigator', 'document', 'CustomEvent', 'Intl', src)( |
| 608 | gwin, store, gwin.navigator, { addEventListener: () => {} }, FakeCustomEvent, Intl); |
| 609 | const G = gwin.DaimondGateway; |
| 610 | check('gateway.js exposes noteBalance', typeof G.noteBalance === 'function'); |
| 611 | |
| 612 | G.noteBalance({ ok: true, credits_minor: 1234, currency: 'aud' }); |
| 613 | check('a numeric balance is taken', G.state().credits === 1234); |
| 614 | check('and the currency with it', G.state().currency === 'aud'); |
| 615 | check('and the event fires', events.length === 1 && events[0].type === 'daimond:credits' |
| 616 | && events[0].detail.credits === 1234); |
| 617 | |
| 618 | G.noteBalance({ ok: true }); |
| 619 | check('an absent balance changes nothing', G.state().credits === 1234 && events.length === 1); |
| 620 | G.noteBalance({ ok: true, credits_minor: null }); |
| 621 | check('a null balance changes nothing', G.state().credits === 1234 && events.length === 1); |
| 622 | G.noteBalance({ ok: true, credits_minor: '900' }); |
| 623 | check('a string balance changes nothing', G.state().credits === 1234 && events.length === 1); |
| 624 | G.noteBalance(null); |
| 625 | G.noteBalance(undefined); |
| 626 | G.noteBalance('nope'); |
| 627 | check('a non-object is ignored', G.state().credits === 1234 && events.length === 1); |
| 628 | G.noteBalance({ credits_minor: 0 }); |
| 629 | check('a real zero IS taken — spent out is not unknown', |
| 630 | G.state().credits === 0 && events.length === 2); |
| 631 | } |
| 632 | |
| 633 | // ── Result ───────────────────────────────────────────────────────── |
| 634 | console.log(`\n${ok.length} ok, ${bad.length} failed`); |
| 635 | if (bad.length) console.log('FAILED: ' + bad.join(', ')); |
| 636 | if (BREAK) { |
| 637 | if (bad.length) { console.log('the break was caught, as it should be'); process.exit(0); } |
| 638 | console.log('THE BREAK WAS NOT CAUGHT: this check proves nothing'); |
| 639 | process.exit(1); |
| 640 | } |
| 641 | if (bad.length) process.exit(1); |
| 642 | console.log('pricing, ledger and balance-notice core verified'); |