Oregami
Repositories/oxedyne/daimond

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
32import { readFileSync } from 'fs';
33
34const ok = [], bad = [];
35const check = (name, pass, detail) => {
36 (pass ? ok : bad).push(name);
37 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
38};
39const near = (a, b, eps = 1e-9) => Math.abs(a - b) < eps;
40const 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.
52const BREAK = (() => {
53 const i = process.argv.indexOf('--break');
54 return i > 0 ? String(process.argv[i + 1] || '') : '';
55})();
56const 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};
72if (BREAK && !BREAKS[BREAK]) die(`no break called "${BREAK}"`);
73if (BREAK) console.log(`BREAK ${BREAK}: ${BREAKS[BREAK].what}\n`);
74
75// ── Load the modules under a stub window ───────────────────────────
76
77function 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.
93function 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
103const win = {};
104const store = memStore();
105loadModule('js/pricing.js', { window: win });
106loadModule('js/ledger.js', { window: win, localStorage: store });
107
108const P = win.DaimondPricing;
109const L = win.DaimondLedger;
110const 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 ─────────────────────────────────────────────────────────
634console.log(`\n${ok.length} ok, ${bad.length} failed`);
635if (bad.length) console.log('FAILED: ' + bad.join(', '));
636if (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}
641if (bad.length) process.exit(1);
642console.log('pricing, ledger and balance-notice core verified');