@subvalue/cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/PRE_RELEASE.md +7 -7
  2. package/README.md +7 -5
  3. package/dist/PRICING.md +15 -16
  4. package/dist/packages/cli/src/index.js +109 -20
  5. package/dist/packages/cli/src/protocol.js +13 -0
  6. package/dist/packages/cli/src/runtime.js +44 -14
  7. package/dist/packages/cli/src/server.js +306 -62
  8. package/dist/packages/core/src/calendar.js +41 -18
  9. package/dist/packages/core/src/coverage.js +12 -0
  10. package/dist/packages/core/src/demo.js +61 -9
  11. package/dist/packages/core/src/metadata.js +52 -9
  12. package/dist/packages/core/src/pricing/current.js +346 -297
  13. package/dist/packages/core/src/pricing/index.js +373 -73
  14. package/dist/packages/core/src/providers/claude/index.js +93 -21
  15. package/dist/packages/core/src/providers/codex/index.js +213 -46
  16. package/dist/packages/core/src/scanner.js +226 -46
  17. package/dist/packages/core/src/security.js +127 -34
  18. package/dist/packages/core/src/storage.js +215 -47
  19. package/dist/packages/core/src/subscriptions.js +146 -21
  20. package/dist/packages/core/src/summary.js +410 -59
  21. package/dist/packages/core/src/types.js +73 -18
  22. package/dist/packages/core/src/usage-json.js +166 -47
  23. package/dist/packages/receipt/src/index.js +392 -82
  24. package/dist/packages/ui/src/brand.js +43 -27
  25. package/dist/public/app.css +2303 -61
  26. package/dist/public/apps/web/src/landing.js +209 -8
  27. package/dist/public/apps/web/src/main.js +1143 -98
  28. package/dist/public/index.html +24 -1
  29. package/dist/public/landing.css +3286 -80
  30. package/dist/public/landing.html +21 -1
  31. package/dist/public/packages/core/src/calendar.js +41 -18
  32. package/dist/public/packages/core/src/coverage.js +12 -0
  33. package/dist/public/packages/core/src/subscriptions.js +146 -21
  34. package/dist/public/packages/receipt/src/index.js +392 -82
  35. package/dist/public/packages/ui/src/brand.js +43 -27
  36. package/dist/public/privacy.css +197 -9
  37. package/package.json +1 -1
@@ -1,64 +1,415 @@
1
-
2
- import {priceRecord,catalog, } from './pricing/index.js';
3
- import {fullCalendarMonths,shiftCalendarDays} from './calendar.js';
4
- export {fullCalendarMonths} from './calendar.js';
5
- const dayFormatter=new Intl.DateTimeFormat('en-CA',{year:'numeric',month:'2-digit',day:'2-digit'});
6
- function localDay(timestamp ) {const parts=dayFormatter.formatToParts(new Date(timestamp));return ['year','month','day'].map(type=>parts.find(part=>part.type===type) .value).join('-');}
7
-
8
- export function dateRange(preset ,now=new Date(),from ,to ,month ,anchor ) {
9
- const today=localDay(now.toISOString());
10
- if(anchor!==undefined){if((preset!=='7d'&&preset!=='30d')||!/^(?:19|20|21)\d{2}-\d{2}-\d{2}$/.test(anchor)||shiftCalendarDays(anchor,0)===null)throw new Error('Choose a valid period');now=new Date(anchor+'T12:00:00');}
11
- const end=new Date(now.getFullYear(),now.getMonth(),now.getDate()+1);let start=new Date(now.getFullYear(),now.getMonth(),now.getDate()-29);let label='Last 30 days';
12
- if(preset==='7d'){start=new Date(now.getFullYear(),now.getMonth(),now.getDate()-6);label='Last 7 days';}
13
- else if(preset==='month'){
14
- if(month!==undefined&&!/^(?:19|20|21)\d{2}-(?:0[1-9]|1[0-2])$/.test(month))throw new Error('Choose a valid month');
15
- const year=month?Number(month.slice(0,4)):now.getFullYear(),index=month?Number(month.slice(5,7))-1:now.getMonth();
16
- start=new Date(year,index,1);end.setTime(+new Date(year,index+1,1));label=start.toLocaleDateString('en-US',{month:'long',year:'numeric'});
1
+
2
+ import { priceRecord, catalog, } from './pricing/index.js';
3
+ import { fullCalendarMonths, shiftCalendarDays } from './calendar.js';
4
+ export { fullCalendarMonths } from './calendar.js';
5
+ const dayFormatter = new Intl.DateTimeFormat('en-CA', {
6
+ year: 'numeric',
7
+ month: '2-digit',
8
+ day: '2-digit',
9
+ });
10
+ function localDay(timestamp ) {
11
+ const parts = dayFormatter.formatToParts(new Date(timestamp));
12
+ return ['year', 'month', 'day']
13
+ .map((type) => parts.find((part) => part.type === type) .value)
14
+ .join('-');
15
+ }
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ export function dateRange(
26
+ preset ,
27
+ now = new Date(),
28
+ from ,
29
+ to ,
30
+ month ,
31
+ anchor ,
32
+ ) {
33
+ const today = localDay(now.toISOString());
34
+ if (anchor !== undefined) {
35
+ if (
36
+ (preset !== '7d' && preset !== '30d') ||
37
+ !/^(?:19|20|21)\d{2}-\d{2}-\d{2}$/.test(anchor) ||
38
+ shiftCalendarDays(anchor, 0) === null
39
+ )
40
+ throw new Error('Choose a valid period');
41
+ now = new Date(anchor + 'T12:00:00');
17
42
  }
18
- else if(preset==='custom'){
19
- if(!from||!to||!/^\d{4}-\d{2}-\d{2}$/.test(from)||!/^\d{4}-\d{2}-\d{2}$/.test(to))throw new Error('Choose a valid date range');
20
- start=new Date(from+'T00:00:00');const last=new Date(to+'T00:00:00');const matches=(date ,text )=>Number.isFinite(+date)&&date.getFullYear()===Number(text.slice(0,4))&&date.getMonth()+1===Number(text.slice(5,7))&&date.getDate()===Number(text.slice(8,10));if(!matches(start,from)||!matches(last,to)||start>last||+last-+start>36600*864e5)throw new Error('Choose a valid date range');end.setTime(+new Date(last.getFullYear(),last.getMonth(),last.getDate()+1));label=`${from} — ${to}`;
21
- }else if(preset!=='30d')throw new Error('Invalid period');
22
- const calendarFrom=localDay(start.toISOString()),calendarTo=localDay(new Date(+end-1).toISOString());
23
- if(anchor!==undefined&&anchor!==today)label=`${calendarFrom} — ${calendarTo}`;
24
- const range={from:start.toISOString(),until:end.toISOString(),label,preset,calendarFrom,calendarTo};
25
- return {...range,calendarMonths:fullCalendarMonths(range)};
43
+ const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
44
+ let start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29);
45
+ let label = 'Last 30 days';
46
+ if (preset === '7d') {
47
+ start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
48
+ label = 'Last 7 days';
49
+ } else if (preset === 'month') {
50
+ if (month !== undefined && !/^(?:19|20|21)\d{2}-(?:0[1-9]|1[0-2])$/.test(month))
51
+ throw new Error('Choose a valid month');
52
+ const year = month ? Number(month.slice(0, 4)) : now.getFullYear(),
53
+ index = month ? Number(month.slice(5, 7)) - 1 : now.getMonth();
54
+ start = new Date(year, index, 1);
55
+ end.setTime(+new Date(year, index + 1, 1));
56
+ label = start.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
57
+ } else if (preset === 'custom') {
58
+ if (!from || !to || !/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to))
59
+ throw new Error('Choose a valid date range');
60
+ start = new Date(from + 'T00:00:00');
61
+ const last = new Date(to + 'T00:00:00');
62
+ const matches = (date , text ) =>
63
+ Number.isFinite(+date) &&
64
+ date.getFullYear() === Number(text.slice(0, 4)) &&
65
+ date.getMonth() + 1 === Number(text.slice(5, 7)) &&
66
+ date.getDate() === Number(text.slice(8, 10));
67
+ if (
68
+ !matches(start, from) ||
69
+ !matches(last, to) ||
70
+ start > last ||
71
+ +last - +start > 36600 * 864e5
72
+ )
73
+ throw new Error('Choose a valid date range');
74
+ end.setTime(+new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1));
75
+ label = `${from} — ${to}`;
76
+ } else if (preset !== '30d') throw new Error('Invalid period');
77
+ const calendarFrom = localDay(start.toISOString()),
78
+ calendarTo = localDay(new Date(+end - 1).toISOString());
79
+ if (anchor !== undefined && anchor !== today) label = `${calendarFrom} — ${calendarTo}`;
80
+ const range = {
81
+ from: start.toISOString(),
82
+ until: end.toISOString(),
83
+ label,
84
+ preset,
85
+ calendarFrom,
86
+ calendarTo,
87
+ };
88
+ return { ...range, calendarMonths: fullCalendarMonths(range) };
89
+ }
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+ export function subscriptionForRange(monthly , range ) {
99
+ const months = fullCalendarMonths(range);
100
+ return months === null ? null : monthly * months;
26
101
  }
27
-
28
- export function subscriptionForRange(monthly ,range ) {
29
- const months=fullCalendarMonths(range);return months===null?null:monthly*months;
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+ function neutral(reason , subscription = null) {
165
+ return { subscription, value: null, roi: null, breakEven: null, outcome: 'neutral', reason };
30
166
  }
31
-
32
-
33
-
34
-
35
- function neutral(reason ,subscription =null) {return {subscription,value:null,roi:null,breakEven:null,outcome:'neutral',reason};}
36
- export function summarize(records ,statuses ,settings ,range ,c =catalog,demo=false) {
37
- const selected=records.filter(r=>r.timestamp&&r.timestamp>=range.from&&r.timestamp<range.until);const prices=new Map(selected.map(r=>[r.id,priceRecord(r,c)]));const dailyMap=new Map ();
38
- for(const r of selected){const date=localDay(r.timestamp );const d=dailyMap.get(date)??{date,codex:null,claude:null,hasUsage:true};const cost=prices.get(r.id) .usd;if(cost!==null)d[r.provider]=(d[r.provider]??0)+cost;dailyMap.set(date,d);}
39
- const daily=[...dailyMap.values()].sort((a,b)=>a.date.localeCompare(b.date));
40
- const providers=(['codex','claude'] ).map(provider=>{
41
- const rows=selected.filter(r=>r.provider===provider),all=records.filter(r=>r.provider===provider),source=statuses.find(s=>s.provider===provider);const issues ={};
42
- const costs=rows.map(r=>prices.get(r.id) );for(const p of costs)if(p.reason)issues[p.reason]=(issues[p.reason]??0)+1;
43
- const undated=all.filter(r=>!r.timestamp).length;if(undated)issues['undated-events']=undated;
44
- const priced=costs.filter(p=>p.usd!==null),subtotal=priced.length?priced.reduce((n,p)=>n+p.usd ,0):null;
45
- const historyPartial=!demo; // Local transcripts cannot prove that all historical requests were retained.
46
- const completePricing=rows.length>0&&priced.length===rows.length;const apiEquivalent=completePricing?subtotal:null;
47
- const billing=settings.billing[provider];const subscription=billing.mode==='SUBSCRIPTION'&&billing.monthly!==null?subscriptionForRange(billing.monthly,range):null;
48
- let comparison=neutral(billing.mode==='API'?'API usage':billing.mode==='MIXED'?'Mixed billing':billing.mode==='UNKNOWN'?'Billing not set':fullCalendarMonths(range)===null?'Choose a full month':subscription===null?'Subscription price not set':!completePricing?'Pricing unavailable':'Partial history',subscription);
49
- if(completePricing&&subscription!==null){const value=apiEquivalent -subscription;let accumulated=0;let breakEven =null;for(const d of daily){accumulated+=d[provider]??0;if(accumulated>=subscription){breakEven=d.date;break;}}comparison={subscription,value,roi:subscription>0?apiEquivalent /subscription:null,breakEven:historyPartial?null:breakEven,outcome:historyPartial?'neutral':value>0?'positive':value<0?'negative':'neutral',reason:historyPartial?'Observed usage · Partial history':null};}
50
- const sumTokens=(key ) =>rows.length&&rows.every(r=>r[key]!==null)?rows.reduce((total,r)=>total+r[key] ,0):null;
51
- const tokenBreakdown={input:sumTokens('input_tokens'),output:sumTokens('output_tokens'),cacheRead:sumTokens('cached_input_tokens')};
52
- const tokensKnown=rows.filter(r=>r.total_tokens!==null);const timestamps=all.map(r=>r.timestamp).filter((t) =>!!t).sort();
53
- const hasUsage=rows.some(r=>r.total_tokens!==0);const sourceIncomplete=source?.errors||source?.missingFiles||source?.diagnostics.malformed||source?.diagnostics.partialTail||source?.diagnostics.oversize||source?.diagnostics.unsupported;
54
- return {provider,detected:source?.detected??false,visible:hasUsage||settings.include[provider],status:rows.length?(hasUsage?'USAGE_FOUND':'NO_USAGE'):'NO_DATA',records:rows.length,importedRecords:all.length,tokens:tokensKnown.length?tokensKnown.reduce((n,r)=>n+r.total_tokens ,0):null,tokensPartial:tokensKnown.length!==rows.length,tokenBreakdown,apiEquivalent,knownSubtotal:subtotal,pricedRecords:priced.length,priceCoverage:rows.length?priced.length/rows.length:null,confidence:!rows.length?'NO DATA':rows.some(r=>r.quality==='INCOMPLETE')?'INCOMPLETE':sourceIncomplete||undated||rows.some(r=>r.quality==='LOW')?'LOW':rows.some(r=>r.quality==='MEDIUM')?'MEDIUM':'HIGH',historyPartial,comparison,mode:billing.mode,monthly:billing.monthly,first:timestamps[0]??null,last:timestamps.at(-1)??null,days:new Set(timestamps.map(t=>t.slice(0,10))).size,issues} ;
167
+ export function summarize(
168
+ records ,
169
+ statuses ,
170
+ settings ,
171
+ range ,
172
+ c = catalog,
173
+ demo = false,
174
+ ) {
175
+ const selected = records.filter(
176
+ (r) => r.timestamp && r.timestamp >= range.from && r.timestamp < range.until,
177
+ );
178
+ const prices = new Map(selected.map((r) => [r.id, priceRecord(r, c)]));
179
+ const dailyMap = new Map ();
180
+ for (const r of selected) {
181
+ const date = localDay(r.timestamp );
182
+ const d = dailyMap.get(date) ?? { date, codex: null, claude: null, hasUsage: true };
183
+ const cost = prices.get(r.id) .usd;
184
+ if (cost !== null) d[r.provider] = (d[r.provider] ?? 0) + cost;
185
+ dailyMap.set(date, d);
186
+ }
187
+ const daily = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
188
+ const providers = (['codex', 'claude'] ).map((provider) => {
189
+ const rows = selected.filter((r) => r.provider === provider),
190
+ all = records.filter((r) => r.provider === provider),
191
+ source = statuses.find((s) => s.provider === provider);
192
+ const issues = {};
193
+ const costs = rows.map((r) => prices.get(r.id) );
194
+ for (const p of costs) if (p.reason) issues[p.reason] = (issues[p.reason] ?? 0) + 1;
195
+ const undated = all.filter((r) => !r.timestamp).length;
196
+ if (undated) issues['undated-events'] = undated;
197
+ const priced = costs.filter((p) => p.usd !== null),
198
+ subtotal = priced.length ? priced.reduce((n, p) => n + p.usd , 0) : null;
199
+ const historyPartial = !demo; // Local transcripts cannot prove that all historical requests were retained.
200
+ const completePricing = rows.length > 0 && priced.length === rows.length;
201
+ const apiEquivalent = completePricing ? subtotal : null;
202
+ const billing = settings.billing[provider];
203
+ const subscription =
204
+ billing.mode === 'SUBSCRIPTION' && billing.monthly !== null
205
+ ? subscriptionForRange(billing.monthly, range)
206
+ : null;
207
+ let comparison = neutral(
208
+ billing.mode === 'NO_SUBSCRIPTION'
209
+ ? 'No subscription'
210
+ : billing.mode === 'API'
211
+ ? 'API usage'
212
+ : billing.mode === 'MIXED'
213
+ ? 'Mixed billing'
214
+ : billing.mode === 'UNKNOWN'
215
+ ? 'Billing not set'
216
+ : fullCalendarMonths(range) === null
217
+ ? 'Choose a full month'
218
+ : subscription === null
219
+ ? 'Subscription price not set'
220
+ : !completePricing
221
+ ? 'Pricing unavailable'
222
+ : 'Partial history',
223
+ subscription,
224
+ );
225
+ if (completePricing && subscription !== null) {
226
+ const value = apiEquivalent - subscription;
227
+ let accumulated = 0;
228
+ let breakEven = null;
229
+ for (const d of daily) {
230
+ accumulated += d[provider] ?? 0;
231
+ if (accumulated >= subscription) {
232
+ breakEven = d.date;
233
+ break;
234
+ }
235
+ }
236
+ comparison = {
237
+ subscription,
238
+ value,
239
+ roi: subscription > 0 ? apiEquivalent / subscription : null,
240
+ breakEven: historyPartial ? null : breakEven,
241
+ outcome: historyPartial
242
+ ? 'neutral'
243
+ : value > 0
244
+ ? 'positive'
245
+ : value < 0
246
+ ? 'negative'
247
+ : 'neutral',
248
+ reason: historyPartial ? 'Observed usage · Partial history' : null,
249
+ };
250
+ }
251
+ const sumTokens = (
252
+ key ,
253
+ ) =>
254
+ rows.length && rows.every((r) => r[key] !== null)
255
+ ? rows.reduce((total, r) => total + r[key] , 0)
256
+ : null;
257
+ const tokenBreakdown = {
258
+ input: sumTokens('input_tokens'),
259
+ output: sumTokens('output_tokens'),
260
+ cacheRead: sumTokens('cached_input_tokens'),
261
+ };
262
+ const tokensKnown = rows.filter((r) => r.total_tokens !== null);
263
+ const timestamps = all
264
+ .map((r) => r.timestamp)
265
+ .filter((t) => !!t)
266
+ .sort();
267
+ const hasUsage = rows.some((r) => r.total_tokens !== 0);
268
+ const sourceIncomplete =
269
+ source?.errors ||
270
+ source?.missingFiles ||
271
+ source?.diagnostics.malformed ||
272
+ source?.diagnostics.partialTail ||
273
+ source?.diagnostics.oversize ||
274
+ source?.diagnostics.unsupported;
275
+ return {
276
+ provider,
277
+ detected: source?.detected ?? false,
278
+ visible: hasUsage || settings.include[provider],
279
+ status: rows.length ? (hasUsage ? 'USAGE_FOUND' : 'NO_USAGE') : 'NO_DATA',
280
+ records: rows.length,
281
+ importedRecords: all.length,
282
+ tokens: tokensKnown.length ? tokensKnown.reduce((n, r) => n + r.total_tokens , 0) : null,
283
+ tokensPartial: tokensKnown.length !== rows.length,
284
+ tokenBreakdown,
285
+ apiEquivalent,
286
+ knownSubtotal: subtotal,
287
+ pricedRecords: priced.length,
288
+ priceCoverage: rows.length ? priced.length / rows.length : null,
289
+ confidence: !rows.length
290
+ ? 'NO DATA'
291
+ : rows.some((r) => r.quality === 'INCOMPLETE')
292
+ ? 'INCOMPLETE'
293
+ : sourceIncomplete || undated || rows.some((r) => r.quality === 'LOW')
294
+ ? 'LOW'
295
+ : rows.some((r) => r.quality === 'MEDIUM')
296
+ ? 'MEDIUM'
297
+ : 'HIGH',
298
+ historyPartial,
299
+ comparison,
300
+ mode: billing.mode,
301
+ monthly: billing.monthly,
302
+ first: timestamps[0] ?? null,
303
+ last: timestamps.at(-1) ?? null,
304
+ days: new Set(timestamps.map((t) => t.slice(0, 10))).size,
305
+ issues,
306
+ } ;
307
+ });
308
+ const active = providers.filter((p) => p.records > 0),
309
+ pricedRecords = active.reduce((n, p) => n + p.pricedRecords, 0),
310
+ count = selected.length;
311
+ const known = active.filter((p) => p.knownSubtotal !== null);
312
+ const knownSubtotal = known.length ? known.reduce((n, p) => n + p.knownSubtotal , 0) : null;
313
+ const apiEquivalent = count > 0 && pricedRecords === count ? knownSubtotal : null;
314
+ const subs = active.filter((p) => p.mode === 'SUBSCRIPTION');
315
+ let comparison = neutral(
316
+ active.some((p) => p.mode !== 'SUBSCRIPTION')
317
+ ? 'Subscription comparison unavailable'
318
+ : 'Partial history',
319
+ );
320
+ if (
321
+ subs.length &&
322
+ subs.length === active.length &&
323
+ subs.every((p) => p.comparison.value !== null)
324
+ ) {
325
+ const subscription = subs.reduce((n, p) => n + p.comparison.subscription , 0);
326
+ const value = apiEquivalent - subscription;
327
+ let accumulated = 0,
328
+ breakEven = null;
329
+ if (demo)
330
+ for (const d of daily) {
331
+ accumulated += (d.codex ?? 0) + (d.claude ?? 0);
332
+ if (accumulated >= subscription) {
333
+ breakEven = d.date;
334
+ break;
335
+ }
336
+ }
337
+ comparison = {
338
+ subscription,
339
+ value,
340
+ roi: subscription > 0 ? apiEquivalent / subscription : null,
341
+ breakEven,
342
+ outcome: demo ? (value > 0 ? 'positive' : value < 0 ? 'negative' : 'neutral') : 'neutral',
343
+ reason: demo ? null : 'Observed usage · Partial history',
344
+ };
345
+ } else if (
346
+ subs.length &&
347
+ subs.length === active.length &&
348
+ subs.every((p) => p.comparison.subscription !== null)
349
+ )
350
+ comparison.subscription = subs.reduce((n, p) => n + p.comparison.subscription , 0);
351
+ const confidence = active.length
352
+ ? ['INCOMPLETE', 'LOW', 'MEDIUM', 'HIGH'].find((q) => active.some((p) => p.confidence === q))
353
+ : 'NO DATA';
354
+ const groups = new Map ();
355
+ for (const r of selected) {
356
+ const key = r.provider + ':' + (r.model_raw ?? 'Unknown model');
357
+ let rows = groups.get(key);
358
+ if (!rows) {
359
+ rows = [];
360
+ groups.set(key, rows);
361
+ }
362
+ rows.push(r);
363
+ }
364
+ const models = [...groups.values()].map((rows) => {
365
+ const first = rows[0],
366
+ p = prices.get(first.id) ;
367
+ const priced = rows.filter((r) => prices.get(r.id) .usd !== null),
368
+ known = rows.filter((r) => r.total_tokens !== null);
369
+ const issues = {};
370
+ for (const r of rows) {
371
+ const reason = prices.get(r.id) .reason;
372
+ if (reason) issues[reason] = (issues[reason] ?? 0) + 1;
373
+ }
374
+ return {
375
+ provider: first.provider,
376
+ raw: first.model_raw ?? 'Unknown model',
377
+ canonical: p.canonical,
378
+ records: rows.length,
379
+ pricedRecords: priced.length,
380
+ tokens: known.length ? known.reduce((n, r) => n + r.total_tokens , 0) : null,
381
+ pricedTokens: priced.some((r) => r.total_tokens !== null)
382
+ ? priced.reduce((n, r) => n + (r.total_tokens ?? 0), 0)
383
+ : null,
384
+ knownSubtotal: priced.length ? priced.reduce((n, r) => n + prices.get(r.id) .usd , 0) : null,
385
+ mappingConfidence: p.mapping?.confidence ?? null,
386
+ mappingSource: p.mapping?.source ?? null,
387
+ issues,
388
+ };
55
389
  });
56
- const active=providers.filter(p=>p.records>0),pricedRecords=active.reduce((n,p)=>n+p.pricedRecords,0),count=selected.length;const known=active.filter(p=>p.knownSubtotal!==null);const knownSubtotal=known.length?known.reduce((n,p)=>n+p.knownSubtotal ,0):null;const apiEquivalent=count>0&&pricedRecords===count?knownSubtotal:null;
57
- const subs=active.filter(p=>p.mode==='SUBSCRIPTION');let comparison=neutral(active.some(p=>p.mode==='API'||p.mode==='MIXED'||p.mode==='UNKNOWN')?'Subscription comparison unavailable':'Partial history');
58
- if(subs.length&&subs.length===active.length&&subs.every(p=>p.comparison.value!==null)){const subscription=subs.reduce((n,p)=>n+p.comparison.subscription ,0);const value=apiEquivalent -subscription;let accumulated=0,breakEven =null;if(demo)for(const d of daily){accumulated+=(d.codex??0)+(d.claude??0);if(accumulated>=subscription){breakEven=d.date;break;}}comparison={subscription,value,roi:subscription>0?apiEquivalent /subscription:null,breakEven,outcome:demo?(value>0?'positive':value<0?'negative':'neutral'):'neutral',reason:demo?null:'Observed usage · Partial history'};}
59
- else if(subs.length&&subs.length===active.length&&subs.every(p=>p.comparison.subscription!==null))comparison.subscription=subs.reduce((n,p)=>n+p.comparison.subscription ,0);
60
- const confidence=active.length?['INCOMPLETE','LOW','MEDIUM','HIGH'].find(q=>active.some(p=>p.confidence===q)) :'NO DATA';
61
- const groups=new Map ();for(const r of selected){const key=r.provider+':'+(r.model_raw??'Unknown model');let rows=groups.get(key);if(!rows){rows=[];groups.set(key,rows);}rows.push(r);}
62
- const models=[...groups.values()].map(rows=>{const first=rows[0],p=prices.get(first.id) ;const priced=rows.filter(r=>prices.get(r.id) .usd!==null),known=rows.filter(r=>r.total_tokens!==null);const issues ={};for(const r of rows){const reason=prices.get(r.id) .reason;if(reason)issues[reason]=(issues[reason]??0)+1;}return {provider:first.provider,raw:first.model_raw??'Unknown model',canonical:p.canonical,records:rows.length,pricedRecords:priced.length,tokens:known.length?known.reduce((n,r)=>n+r.total_tokens ,0):null,pricedTokens:priced.some(r=>r.total_tokens!==null)?priced.reduce((n,r)=>n+(r.total_tokens??0),0):null,knownSubtotal:priced.length?priced.reduce((n,r)=>n+prices.get(r.id) .usd ,0):null,mappingConfidence:p.mapping?.confidence??null,mappingSource:p.mapping?.source??null,issues};});
63
- return {range,providers,total:{apiEquivalent,knownSubtotal,records:count,pricedRecords,priceCoverage:count?pricedRecords/count:null,comparison},daily,confidence,historyPartial:!demo,unresolvedModels:[...new Set(selected.filter(r=>!prices.get(r.id) .canonical).map(r=>r.model_raw??'Unknown model'))],models,pricingVersion:c.version,demo};
390
+ return {
391
+ range,
392
+ providers,
393
+ total: {
394
+ apiEquivalent,
395
+ knownSubtotal,
396
+ records: count,
397
+ pricedRecords,
398
+ priceCoverage: count ? pricedRecords / count : null,
399
+ comparison,
400
+ },
401
+ daily,
402
+ confidence,
403
+ historyPartial: !demo,
404
+ unresolvedModels: [
405
+ ...new Set(
406
+ selected
407
+ .filter((r) => !prices.get(r.id) .canonical)
408
+ .map((r) => r.model_raw ?? 'Unknown model'),
409
+ ),
410
+ ],
411
+ models,
412
+ pricingVersion: c.version,
413
+ demo,
414
+ };
64
415
  }
@@ -1,30 +1,85 @@
1
1
 
2
2
 
3
-
3
+ // API and MIXED remain readable for existing local settings.
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
4
24
 
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
14
33
 
15
34
 
16
35
 
17
-
18
-
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
19
46
 
20
47
 
21
-
22
-
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
23
57
 
24
58
 
25
-
59
+
60
+
61
+
26
62
 
27
-
63
+
28
64
 
29
- export const defaultSettings = () => ({onboarded:false,currency:'USD',include:{codex:false,claude:false},billing:{codex:{mode:'UNKNOWN',monthly:null},claude:{mode:'UNKNOWN',monthly:null}}});
30
- export const emptyDiagnostics = () => ({malformed:0,inherited:0,repeated:0,resets:0,oversize:0,unsupported:0,partialTail:false,first:null,last:null,records:0});
65
+ export const defaultSettings = () => ({
66
+ onboarded: false,
67
+ currency: 'USD',
68
+ include: { codex: false, claude: false },
69
+ billing: {
70
+ codex: { mode: 'UNKNOWN', monthly: null },
71
+ claude: { mode: 'UNKNOWN', monthly: null },
72
+ },
73
+ });
74
+ export const emptyDiagnostics = () => ({
75
+ malformed: 0,
76
+ inherited: 0,
77
+ repeated: 0,
78
+ resets: 0,
79
+ oversize: 0,
80
+ unsupported: 0,
81
+ partialTail: false,
82
+ first: null,
83
+ last: null,
84
+ records: 0,
85
+ });