dsh-all-usage 1.1.18 → 1.2.0
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.
- package/CHANGELOG.md +453 -394
- package/LICENSE +21 -21
- package/README.en.md +270 -0
- package/README.md +142 -279
- package/assets/model-icons/ai2-color.svg +1 -0
- package/assets/model-icons/ai21-brand-color.svg +1 -0
- package/assets/model-icons/aionlabs.svg +1 -0
- package/assets/model-icons/alephalpha.svg +1 -0
- package/assets/model-icons/baai.svg +1 -0
- package/assets/model-icons/baichuan-color.svg +1 -0
- package/assets/model-icons/baidu-color.svg +1 -0
- package/assets/model-icons/bytedance-color.svg +1 -0
- package/assets/model-icons/cohere-color.svg +1 -0
- package/assets/model-icons/hunyuan-color.svg +1 -0
- package/assets/model-icons/ibm.svg +1 -0
- package/assets/model-icons/iflytekcloud-color.svg +1 -0
- package/assets/model-icons/internlm-color.svg +1 -0
- package/assets/model-icons/kling-color.svg +1 -0
- package/assets/model-icons/liquid.svg +1 -0
- package/assets/model-icons/longcat-color.svg +1 -0
- package/assets/model-icons/manifest.json +739 -213
- package/assets/model-icons/microsoft-color.svg +1 -0
- package/assets/model-icons/mistral-color.svg +1 -0
- package/assets/model-icons/nousresearch.svg +1 -0
- package/assets/model-icons/nova-color.svg +1 -0
- package/assets/model-icons/nvidia-color.svg +1 -0
- package/assets/model-icons/perplexity-color.svg +1 -0
- package/assets/model-icons/sensenova-color.svg +1 -0
- package/assets/model-icons/skywork-color.svg +1 -0
- package/assets/model-icons/snowflake-color.svg +1 -0
- package/assets/model-icons/stepfun-color.svg +1 -0
- package/assets/model-icons/tencent-color.svg +1 -0
- package/assets/model-icons/tii-color.svg +1 -0
- package/assets/model-icons/upstage-color.svg +1 -0
- package/assets/model-icons/voyage-color.svg +1 -0
- package/assets/model-icons/xiaomimimo.svg +1 -0
- package/assets/model-icons/yandex.svg +1 -0
- package/assets/readme-architecture-en.svg +95 -0
- package/assets/readme-architecture-zh.svg +95 -0
- package/assets/readme-sync-en.svg +81 -0
- package/assets/readme-sync-zh.svg +81 -0
- package/assets/screenshots/en/01-overview.png +0 -0
- package/assets/screenshots/en/02-heatmap.png +0 -0
- package/assets/screenshots/en/03-model-usage.png +0 -0
- package/assets/screenshots/en/04-request-logs.png +0 -0
- package/assets/screenshots/en/05-cost-settings.png +0 -0
- package/assets/screenshots/zh/01-overview.png +0 -0
- package/assets/screenshots/zh/02-heatmap.png +0 -0
- package/assets/screenshots/zh/03-model-usage.png +0 -0
- package/assets/screenshots/zh/04-request-logs.png +0 -0
- package/assets/screenshots/zh/05-cost-settings.png +0 -0
- package/cordis.patch.yml +5 -5
- package/lib/aggregation.js +1842 -1112
- package/lib/client.js +1 -1
- package/lib/http.js +417 -385
- package/lib/icons.js +40 -0
- package/lib/ledger.js +1000 -531
- package/lib/plugin.js +347 -327
- package/lib/pricing-runtime.js +615 -551
- package/lib/pricing.js +1463 -1412
- package/lib/records-cache.js +113 -0
- package/lib/session-sync.js +976 -841
- package/lib/usage-core.js +33 -10
- package/lib/version-check.js +133 -133
- package/package.json +7 -3
- package/screenshots.json +10 -3
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
package/lib/pricing-runtime.js
CHANGED
|
@@ -1,551 +1,615 @@
|
|
|
1
|
-
import { COST_SCHEMA_VERSION, builtinTemporalProfileFor, calculateCost, fetchHolidayCalendar, fetchModelsDevCatalog, normalizeCostSnapshot, normalizeModelId, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
|
|
2
|
-
import { billingInstantOf } from './usage-core.js'
|
|
3
|
-
|
|
4
|
-
const LEDGER_VERSION = 3
|
|
5
|
-
// One price table row per usage identity; the cap keeps the panel payload bounded.
|
|
6
|
-
const MAX_PRICING_ROWS = 500
|
|
7
|
-
// The holiday arrangement changes at most once a year (and only for the next
|
|
8
|
-
// one); a day-long cache keeps the panel from re-fetching on every open.
|
|
9
|
-
const HOLIDAY_CALENDAR_TTL_MS = 24 * 60 * 60 * 1000
|
|
10
|
-
|
|
11
|
-
export function createPricingRuntime(host) {
|
|
12
|
-
const { state, markStatsChanged } = host
|
|
13
|
-
const { storage } = host.services
|
|
14
|
-
const {
|
|
15
|
-
coerceIdentity,
|
|
16
|
-
dateKeys,
|
|
17
|
-
ensureDay,
|
|
18
|
-
ensureDayModel,
|
|
19
|
-
ensureDayWs,
|
|
20
|
-
ensureModel,
|
|
21
|
-
ensureWs,
|
|
22
|
-
addCostAggregateDirection,
|
|
23
|
-
adjustQueryCost,
|
|
24
|
-
resolveCurrentPricing,
|
|
25
|
-
} = host.aggregation
|
|
26
|
-
const persistLedgerRecord = (...args) => host.ledger.persistLedgerRecord(...args)
|
|
27
|
-
const drainLedgerWrites = (...args) => host.ledger.drainLedgerWrites(...args)
|
|
28
|
-
const nextLedgerRevision = (...args) => host.ledger.nextLedgerRevision(...args)
|
|
29
|
-
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
return
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
state.
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
state.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
if (costSnapshotEquivalent(oldCost, next))
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
1
|
+
import { COST_SCHEMA_VERSION, builtinTemporalProfileFor, calculateCost, fetchHolidayCalendar, fetchModelsDevCatalog, normalizeCostSnapshot, normalizeModelId, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
|
|
2
|
+
import { billingInstantOf, contextTimeIsFresh, contextTimeKey, validEventTime } from './usage-core.js'
|
|
3
|
+
|
|
4
|
+
const LEDGER_VERSION = 3
|
|
5
|
+
// One price table row per usage identity; the cap keeps the panel payload bounded.
|
|
6
|
+
const MAX_PRICING_ROWS = 500
|
|
7
|
+
// The holiday arrangement changes at most once a year (and only for the next
|
|
8
|
+
// one); a day-long cache keeps the panel from re-fetching on every open.
|
|
9
|
+
const HOLIDAY_CALENDAR_TTL_MS = 24 * 60 * 60 * 1000
|
|
10
|
+
|
|
11
|
+
export function createPricingRuntime(host) {
|
|
12
|
+
const { state, markStatsChanged } = host
|
|
13
|
+
const { storage } = host.services
|
|
14
|
+
const {
|
|
15
|
+
coerceIdentity,
|
|
16
|
+
dateKeys,
|
|
17
|
+
ensureDay,
|
|
18
|
+
ensureDayModel,
|
|
19
|
+
ensureDayWs,
|
|
20
|
+
ensureModel,
|
|
21
|
+
ensureWs,
|
|
22
|
+
addCostAggregateDirection,
|
|
23
|
+
adjustQueryCost,
|
|
24
|
+
resolveCurrentPricing,
|
|
25
|
+
} = host.aggregation
|
|
26
|
+
const persistLedgerRecord = (...args) => host.ledger.persistLedgerRecord(...args)
|
|
27
|
+
const drainLedgerWrites = (...args) => host.ledger.drainLedgerWrites(...args)
|
|
28
|
+
const nextLedgerRevision = (...args) => host.ledger.nextLedgerRevision(...args)
|
|
29
|
+
let pricingMutation = Promise.resolve()
|
|
30
|
+
function serializePricingMutation(task) {
|
|
31
|
+
const next = pricingMutation.then(task, task)
|
|
32
|
+
pricingMutation = next.catch(() => {})
|
|
33
|
+
return next
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function adjustCostOnly(item, cost, direction) {
|
|
37
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
38
|
+
const dates = item && typeof item.date === 'string' && typeof item.dateUtc === 'string' ? { local: item.date, utc: item.dateUtc } : dateKeys(item.time)
|
|
39
|
+
const targets = []
|
|
40
|
+
const seen = new Set()
|
|
41
|
+
for (const target of [
|
|
42
|
+
ensureModel(identity).cost,
|
|
43
|
+
state.totals.cost,
|
|
44
|
+
ensureWs(item.wsId).cost,
|
|
45
|
+
ensureDay(state.byDay, dates.local).cost,
|
|
46
|
+
ensureDay(state.byDayUtc, dates.utc).cost,
|
|
47
|
+
ensureDayWs(ensureDay(state.byDay, dates.local), item.wsId).cost,
|
|
48
|
+
ensureDayWs(ensureDay(state.byDayUtc, dates.utc), item.wsId).cost,
|
|
49
|
+
ensureDayModel(ensureDay(state.byDay, dates.local), identity).cost,
|
|
50
|
+
ensureDayModel(ensureDay(state.byDayUtc, dates.utc), identity).cost,
|
|
51
|
+
]) {
|
|
52
|
+
if (seen.has(target)) continue
|
|
53
|
+
seen.add(target)
|
|
54
|
+
targets.push(target)
|
|
55
|
+
}
|
|
56
|
+
for (const target of targets) addCostAggregateDirection(target, cost, direction)
|
|
57
|
+
// The session digest is a target too: a later removeSession() unwinds from it.
|
|
58
|
+
if (host.aggregation !== undefined && typeof host.aggregation.adjustSessionCostOnly === 'function') host.aggregation.adjustSessionCostOnly(item, cost, direction)
|
|
59
|
+
}
|
|
60
|
+
function usedPricingModels(tierSchedules = null, temporalSchedules = null, detailed = false) {
|
|
61
|
+
const rows = []
|
|
62
|
+
const seen = new Set()
|
|
63
|
+
const scheduleIds = new Map()
|
|
64
|
+
const scheduleIdFor = (tiers) => {
|
|
65
|
+
if (!Array.isArray(tierSchedules) || tiers.length === 0) return null
|
|
66
|
+
const key = JSON.stringify(tiers)
|
|
67
|
+
const existing = scheduleIds.get(key)
|
|
68
|
+
if (existing !== undefined) return existing
|
|
69
|
+
const id = 'tier-' + tierSchedules.length
|
|
70
|
+
scheduleIds.set(key, id)
|
|
71
|
+
tierSchedules.push({ id, tiers: tiers.map((tier) => ({ ...tier })) })
|
|
72
|
+
return id
|
|
73
|
+
}
|
|
74
|
+
// Peak/off-peak plans are shared by every route priced from the same entry,
|
|
75
|
+
// so they travel once per distinct plan exactly like tier schedules do.
|
|
76
|
+
const temporalIds = new Map()
|
|
77
|
+
const temporalScheduleIdFor = (profile) => {
|
|
78
|
+
if (!Array.isArray(temporalSchedules) || profile === null || typeof profile !== 'object') return null
|
|
79
|
+
const policies = Array.isArray(profile.policies) ? profile.policies : []
|
|
80
|
+
if (policies.length === 0) return null
|
|
81
|
+
// Every attached policy carries the hash of its own content, so the plan
|
|
82
|
+
// identity is a cheap join instead of a full serialization per row
|
|
83
|
+
// (falling back to JSON when a hash is missing).
|
|
84
|
+
const key = policies.every((policy) => typeof policy.policyHash === 'string' && policy.policyHash !== '')
|
|
85
|
+
? policies.map((policy) => policy.policyId + ':' + policy.policyHash).join('|')
|
|
86
|
+
: JSON.stringify(policies)
|
|
87
|
+
const existing = temporalIds.get(key)
|
|
88
|
+
if (existing !== undefined) return existing
|
|
89
|
+
const id = 'temporal-' + temporalSchedules.length
|
|
90
|
+
temporalIds.set(key, id)
|
|
91
|
+
temporalSchedules.push({ id, policies: policies.map((policy) => ({ ...policy })) })
|
|
92
|
+
return id
|
|
93
|
+
}
|
|
94
|
+
const providerKey = (value) => (typeof value === 'string' ? value.trim().toLowerCase() : '')
|
|
95
|
+
// Route identity keys are JSON tuples; rebuilding the exact tuple keeps a
|
|
96
|
+
// synthetic row on the same resolution-cache entry a real usage row uses.
|
|
97
|
+
const parseIdentityKey = (identityKey) => {
|
|
98
|
+
if (typeof identityKey !== 'string' || identityKey === '') return null
|
|
99
|
+
let parsed = null
|
|
100
|
+
try { parsed = JSON.parse(identityKey) } catch (err) { return null }
|
|
101
|
+
if (!Array.isArray(parsed) || parsed.length !== 4) return null
|
|
102
|
+
const textOrNull = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null)
|
|
103
|
+
return { provider: textOrNull(parsed[0]), requestedModel: textOrNull(parsed[1]), actualModel: textOrNull(parsed[2]), legacyLabel: textOrNull(parsed[3]) }
|
|
104
|
+
}
|
|
105
|
+
// The price edit target of a row: the entry that is actually in effect. A
|
|
106
|
+
// priced row edits its resolved entry, an unpriced row gets a new
|
|
107
|
+
// provider-agnostic manual entry keyed by its own model id.
|
|
108
|
+
const basisKeyFor = (identity, resolved) => {
|
|
109
|
+
const priced = resolved.status === 'priced' && typeof resolved.pricingModel === 'string' && resolved.pricingModel !== ''
|
|
110
|
+
const modelId = priced ? resolved.pricingModel : (identity.actualModel || identity.requestedModel || resolved.pricingModel || identity.label || '')
|
|
111
|
+
const provider = priced ? resolved.providerId || '' : ''
|
|
112
|
+
return providerKey(provider) + '\u0000' + normalizeModelId(modelId)
|
|
113
|
+
}
|
|
114
|
+
const coveredBases = new Set()
|
|
115
|
+
const rowOf = (identity, resolved, options = {}) => {
|
|
116
|
+
const tiers = Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
117
|
+
const tierScheduleId = resolved.tiered === true ? scheduleIdFor(tiers) : null
|
|
118
|
+
const temporalScheduleId = detailed ? temporalScheduleIdFor(resolved.temporalProfile) : null
|
|
119
|
+
coveredBases.add(basisKeyFor(identity, resolved))
|
|
120
|
+
return {
|
|
121
|
+
identityKey: typeof options.identityKey === 'string' ? options.identityKey : identity.identityKey,
|
|
122
|
+
provider: identity.provider,
|
|
123
|
+
requestedModel: identity.requestedModel,
|
|
124
|
+
actualModel: identity.actualModel,
|
|
125
|
+
model: typeof options.model === 'string' && options.model !== '' ? options.model : identity.label,
|
|
126
|
+
status: resolved.status,
|
|
127
|
+
reason: resolved.reason || '',
|
|
128
|
+
pricingModel: resolved.pricingModel || null,
|
|
129
|
+
providerId: resolved.providerId || null,
|
|
130
|
+
source: resolved.source || 'none',
|
|
131
|
+
currency: resolved.currency || 'USD',
|
|
132
|
+
rates: resolved.rates || null,
|
|
133
|
+
tiered: resolved.tiered === true,
|
|
134
|
+
tierCount: tiers.length,
|
|
135
|
+
...(tierScheduleId === null ? {} : { tierScheduleId }),
|
|
136
|
+
tieredInvalid: resolved.tieredInvalid === true,
|
|
137
|
+
inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
|
|
138
|
+
multiplier: resolved.multiplier || '1',
|
|
139
|
+
temporalExplicit: resolved.temporalExplicit === true,
|
|
140
|
+
temporalBuiltin: resolved.temporalBuiltin === true,
|
|
141
|
+
temporalConfigInvalid: resolved.temporalConfigInvalid === true,
|
|
142
|
+
...(detailed ? { usageBacked: options.usageBacked === true } : {}),
|
|
143
|
+
...(temporalScheduleId === null ? {} : { temporalScheduleId }),
|
|
144
|
+
...(resolved.temporalRoute !== undefined && resolved.temporalRoute !== null ? { temporalRoute: resolved.temporalRoute } : {}),
|
|
145
|
+
...(resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) && resolved.temporalProfile.policies.length > 0 ? { temporalPolicyId: String(resolved.temporalProfile.policies[0].policyId || ''), temporalTimezone: resolved.temporalProfile.policies[0].timezone === 'UTC' ? 'UTC' : null } : {}),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const item of state.usageByStep.values()) {
|
|
149
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
150
|
+
if (seen.has(identity.identityKey)) continue
|
|
151
|
+
seen.add(identity.identityKey)
|
|
152
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { usageBacked: true }))
|
|
153
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
154
|
+
}
|
|
155
|
+
if (detailed) {
|
|
156
|
+
// Configured rows the ledger never saw: an override or an
|
|
157
|
+
// identity-scoped mapping would otherwise become invisible and
|
|
158
|
+
// un-editable in the merged price table.
|
|
159
|
+
for (const mapping of state.pricingState.mappings) {
|
|
160
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
161
|
+
const identityKey = typeof mapping.identityKey === 'string' ? mapping.identityKey.trim() : ''
|
|
162
|
+
if (identityKey === '' || seen.has(identityKey)) continue
|
|
163
|
+
seen.add(identityKey)
|
|
164
|
+
const parsed = parseIdentityKey(identityKey)
|
|
165
|
+
const provider = parsed !== null ? parsed.provider : (mapping.provider === '' ? null : mapping.provider)
|
|
166
|
+
const model = parsed !== null ? (parsed.actualModel || parsed.requestedModel || parsed.legacyLabel) : (mapping.model !== '' ? mapping.model : (mapping.catalogModelId === '' ? null : mapping.catalogModelId))
|
|
167
|
+
const identity = {
|
|
168
|
+
identityKey,
|
|
169
|
+
provider,
|
|
170
|
+
requestedModel: parsed !== null ? parsed.requestedModel : model,
|
|
171
|
+
actualModel: parsed !== null ? parsed.actualModel : null,
|
|
172
|
+
label: (provider === null ? '' : provider + ' / ') + (model === null || model === '' ? identityKey : model),
|
|
173
|
+
legacy: parsed !== null && parsed.legacyLabel !== null && parsed.provider === null && parsed.requestedModel === null && parsed.actualModel === null,
|
|
174
|
+
}
|
|
175
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey, model: identity.label, usageBacked: false }))
|
|
176
|
+
}
|
|
177
|
+
for (const entry of state.pricingState.overrides) {
|
|
178
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
179
|
+
const key = providerKey(entry.providerId) + '\u0000' + normalizeModelId(entry.modelId)
|
|
180
|
+
if (coveredBases.has(key)) continue
|
|
181
|
+
coveredBases.add(key)
|
|
182
|
+
// A non-JSON key: it can never collide with a real route key (those are
|
|
183
|
+
// always JSON tuples), so a configured row cannot poison the shared
|
|
184
|
+
// resolution cache the way a constant key would.
|
|
185
|
+
const identity = {
|
|
186
|
+
identityKey: 'override:' + key,
|
|
187
|
+
provider: entry.providerId === '' ? null : entry.providerId,
|
|
188
|
+
requestedModel: entry.modelId,
|
|
189
|
+
actualModel: null,
|
|
190
|
+
label: entry.displayName || entry.modelId,
|
|
191
|
+
legacy: false,
|
|
192
|
+
}
|
|
193
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey: '', model: entry.displayName || entry.modelId, usageBacked: false }))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
rows.sort((left, right) => {
|
|
197
|
+
if (left.usageBacked !== right.usageBacked) return left.usageBacked === true ? -1 : 1
|
|
198
|
+
return String(left.model).localeCompare(String(right.model))
|
|
199
|
+
})
|
|
200
|
+
return rows
|
|
201
|
+
}
|
|
202
|
+
function pricingModelSearch(query, limit = 20) {
|
|
203
|
+
const raw = typeof query === 'string' ? query.trim().toLowerCase() : ''
|
|
204
|
+
if (raw === '') return []
|
|
205
|
+
const normalized = raw.replace(/\s+/g, ' ')
|
|
206
|
+
const selected = new Map()
|
|
207
|
+
for (const entry of state.pricingState.catalogEntries) {
|
|
208
|
+
const official = officialProviderIds(entry.modelId)
|
|
209
|
+
if (!official.has(String(entry.providerId || '').toLowerCase())) continue
|
|
210
|
+
const modelId = entry.modelId.toLowerCase()
|
|
211
|
+
const displayName = String(entry.displayName || '').toLowerCase()
|
|
212
|
+
if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
|
|
213
|
+
const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
|
|
214
|
+
const previous = selected.get(entry.modelId)
|
|
215
|
+
// The mapping column previews the target price before saving, so the
|
|
216
|
+
// search result must carry the rates and the built-in peak/off-peak
|
|
217
|
+
// capability (never the whole catalog entry).
|
|
218
|
+
if (previous === undefined || score < previous.score) selected.set(entry.modelId, {
|
|
219
|
+
value: entry.modelId,
|
|
220
|
+
label: entry.displayName,
|
|
221
|
+
providerId: entry.providerId,
|
|
222
|
+
providerName: entry.providerName || '',
|
|
223
|
+
tiered: entry.tiered === true,
|
|
224
|
+
tierCount: Array.isArray(entry.tiers) ? entry.tiers.length : 0,
|
|
225
|
+
rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
|
|
226
|
+
builtinTemporal: builtinTemporalProfileFor(entry.modelId) !== null,
|
|
227
|
+
score,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
return Array.from(selected.values()).sort((a, b) => a.score - b.score || a.value.localeCompare(b.value)).slice(0, Math.max(1, Math.min(50, Number.isInteger(limit) ? limit : 20))).map(({ score, ...entry }) => entry)
|
|
231
|
+
}
|
|
232
|
+
// The route list only changes when the ledger or the pricing state changes, and
|
|
233
|
+
// building it walks every usage row (~65k here) plus the resolution of each
|
|
234
|
+
// route. The cache keeps the last revision's result, so the per-refresh pricing
|
|
235
|
+
// snapshot and the detail table stop paying that walk.
|
|
236
|
+
let usedModelsCache = { key: '', rows: null }
|
|
237
|
+
function usedPricingModelsCached(tierSchedules, temporalSchedules, detailed) {
|
|
238
|
+
const key = String(state.dataRevision) + ':' + String(state.pricingRevision) + (detailed ? ':d' : ':s')
|
|
239
|
+
if (usedModelsCache.rows !== null && usedModelsCache.key === key) {
|
|
240
|
+
// The schedules travel with the rows; a cached result hands back its own
|
|
241
|
+
// copies so a caller cannot mutate them.
|
|
242
|
+
if (detailed) {
|
|
243
|
+
tierSchedules.push(...usedModelsCache.tierSchedules.map((entry) => ({ id: entry.id, tiers: entry.tiers.map((tier) => ({ ...tier })) })))
|
|
244
|
+
temporalSchedules.push(...usedModelsCache.temporalSchedules.map((entry) => ({ id: entry.id, policies: entry.policies.map((policy) => ({ ...policy })) })))
|
|
245
|
+
}
|
|
246
|
+
return usedModelsCache.rows
|
|
247
|
+
}
|
|
248
|
+
const rows = usedPricingModels(tierSchedules, temporalSchedules, detailed)
|
|
249
|
+
usedModelsCache = {
|
|
250
|
+
key,
|
|
251
|
+
rows,
|
|
252
|
+
tierSchedules: detailed ? tierSchedules.map((entry) => ({ id: entry.id, tiers: entry.tiers.map((tier) => ({ ...tier })) })) : [],
|
|
253
|
+
temporalSchedules: detailed ? temporalSchedules.map((entry) => ({ id: entry.id, policies: entry.policies.map((policy) => ({ ...policy })) })) : [],
|
|
254
|
+
}
|
|
255
|
+
return rows
|
|
256
|
+
}
|
|
257
|
+
function pricingSnapshot(options = {}) {
|
|
258
|
+
const pricingStateSnapshot = state.pricingState
|
|
259
|
+
const detailed = options.detailed !== false
|
|
260
|
+
const tierSchedules = detailed ? [] : null
|
|
261
|
+
const temporalSchedules = detailed ? [] : null
|
|
262
|
+
const snapshot = {
|
|
263
|
+
schemaVersion: pricingStateSnapshot.schemaVersion,
|
|
264
|
+
source: { ...pricingStateSnapshot.source },
|
|
265
|
+
sync: { ...pricingStateSnapshot.sync },
|
|
266
|
+
catalogModelCount: pricingStateSnapshot.catalogEntries.length,
|
|
267
|
+
overrideCount: pricingStateSnapshot.overrides.length,
|
|
268
|
+
mappingCount: pricingStateSnapshot.mappings.length,
|
|
269
|
+
configured: pricingStateSnapshot.catalogEntries.length > 0 || pricingStateSnapshot.overrides.length > 0 || pricingStateSnapshot.mappings.length > 0,
|
|
270
|
+
usedModels: usedPricingModelsCached(tierSchedules, temporalSchedules, detailed),
|
|
271
|
+
cost: serializeCostAggregate(state.totals.cost),
|
|
272
|
+
}
|
|
273
|
+
if (detailed) {
|
|
274
|
+
snapshot.config = {
|
|
275
|
+
sync: { ...pricingStateSnapshot.sync },
|
|
276
|
+
mappings: pricingStateSnapshot.mappings.map((mapping) => ({ ...mapping })),
|
|
277
|
+
// providerId and the peak/off-peak plan must round-trip: dropping
|
|
278
|
+
// providerId would turn a provider-scoped manual price into a
|
|
279
|
+
// provider-agnostic one on the next save (and would stop it from
|
|
280
|
+
// being found as a mapping target).
|
|
281
|
+
overrides: pricingStateSnapshot.overrides.map((entry) => ({
|
|
282
|
+
...entry,
|
|
283
|
+
...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}),
|
|
284
|
+
...(entry.temporalPricing === undefined ? {} : { temporalPricing: JSON.parse(JSON.stringify(entry.temporalPricing)) }),
|
|
285
|
+
})),
|
|
286
|
+
}
|
|
287
|
+
snapshot.tierSchedules = tierSchedules
|
|
288
|
+
snapshot.temporalSchedules = temporalSchedules
|
|
289
|
+
}
|
|
290
|
+
return snapshot
|
|
291
|
+
}
|
|
292
|
+
async function loadPricing() {
|
|
293
|
+
if (storage === undefined || storage.backend === undefined || typeof storage.backend.get !== 'function') return
|
|
294
|
+
try {
|
|
295
|
+
const backend = storage.backend.get('json')
|
|
296
|
+
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
297
|
+
const unit = await backend.kv.open({ name: 'all_usage_pricing', version: 0, tables: [], hasGlobal: true })
|
|
298
|
+
if (state.disposed) { await unit.close().catch(() => {}); return }
|
|
299
|
+
state.pricingUnit = unit
|
|
300
|
+
const snapshot = await unit.loadAll()
|
|
301
|
+
const global = snapshot && snapshot.global
|
|
302
|
+
const raw = global && typeof global === 'object' && global.pricing !== undefined ? global.pricing : global
|
|
303
|
+
state.pricingState = normalizePricingState(raw)
|
|
304
|
+
state.pricingResolutionCache.clear()
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error('[all-usage] pricing catalog unavailable:', err)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function persistPricing() {
|
|
310
|
+
if (state.disposed) return state.pricingWriteChain
|
|
311
|
+
const payload = { pricing: serializePricingState(state.pricingState) }
|
|
312
|
+
state.pricingWriteChain = state.pricingWriteChain.then(async () => {
|
|
313
|
+
if (state.pricingUnit === null || state.pricingUnit === undefined) return
|
|
314
|
+
await state.pricingUnit.setGlobal(payload)
|
|
315
|
+
})
|
|
316
|
+
state.pricingWriteChain = state.pricingWriteChain.catch((err) => {
|
|
317
|
+
console.error('[all-usage] pricing persist failed:', err)
|
|
318
|
+
})
|
|
319
|
+
return state.pricingWriteChain
|
|
320
|
+
}
|
|
321
|
+
function syncPricing(force = false) {
|
|
322
|
+
return serializePricingMutation(() => syncPricingOnce(force))
|
|
323
|
+
}
|
|
324
|
+
async function syncPricingOnce(force) {
|
|
325
|
+
await Promise.all([state.pricingReady, state.ledgerReady])
|
|
326
|
+
if (state.pricingSyncInFlight) return { ok: false, message: 'pricing-sync-in-progress', pricing: pricingSnapshot() }
|
|
327
|
+
const now = Date.now()
|
|
328
|
+
if (!force && state.pricingState.sync.lastSuccessAt > 0 && now - state.pricingState.sync.lastSuccessAt < state.pricingState.sync.intervalMs) return { ok: true, skipped: true, pricing: pricingSnapshot() }
|
|
329
|
+
state.pricingSyncInFlight = true
|
|
330
|
+
state.pricingState.sync.lastAttemptAt = now
|
|
331
|
+
try {
|
|
332
|
+
const result = await fetchModelsDevCatalog()
|
|
333
|
+
if (!result.ok) {
|
|
334
|
+
state.pricingState.source.lastError = result.error
|
|
335
|
+
state.pricingState.sync.lastError = result.error
|
|
336
|
+
// A failed attempt changes only sync health, not any configured price,
|
|
337
|
+
// so it must not invalidate the query snapshot/records caches.
|
|
338
|
+
markStatsChanged('sync-health')
|
|
339
|
+
await persistPricing()
|
|
340
|
+
return { ok: false, message: result.error, pricing: pricingSnapshot() }
|
|
341
|
+
}
|
|
342
|
+
const previousHash = state.pricingState.source && state.pricingState.source.catalogHash !== undefined ? String(state.pricingState.source.catalogHash) : ''
|
|
343
|
+
state.pricingState = normalizePricingState({
|
|
344
|
+
...serializePricingState(state.pricingState),
|
|
345
|
+
source: { url: result.catalog.sourceUrl, fetchedAt: result.catalog.fetchedAt, catalogHash: result.catalog.catalogHash, lastError: '' },
|
|
346
|
+
sync: { ...state.pricingState.sync, lastSuccessAt: result.catalog.fetchedAt, lastError: '' },
|
|
347
|
+
catalogEntries: result.catalog.entries,
|
|
348
|
+
})
|
|
349
|
+
state.pricingResolutionCache.clear()
|
|
350
|
+
const reconciliation = await reconcileUsageCosts({ backfill: true })
|
|
351
|
+
const backfill = reconciliation.backfill
|
|
352
|
+
const temporal = reconciliation.temporal
|
|
353
|
+
// Only catalog changes, newly priced usage, or temporal snapshot
|
|
354
|
+
// reconciliation can alter computed costs; a successful sync with an
|
|
355
|
+
// unchanged catalog must not invalidate the query snapshot, scoped caches,
|
|
356
|
+
// or records cursors. The sync-health bump still rebuilds the snapshot
|
|
357
|
+
// cache so lastSuccessAt is not stale on the next full response.
|
|
358
|
+
if (String(result.catalog.catalogHash || '') !== previousHash || backfill.priced > 0 || temporal.reconciled > 0) markStatsChanged('pricing')
|
|
359
|
+
else markStatsChanged('sync-health')
|
|
360
|
+
await persistPricing()
|
|
361
|
+
await drainLedgerWrites()
|
|
362
|
+
return { ok: true, skipped: false, backfill, pricing: pricingSnapshot() }
|
|
363
|
+
} finally {
|
|
364
|
+
state.pricingSyncInFlight = false
|
|
365
|
+
schedulePricingSync()
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// A cost rewrite must update both the cubes and its durable ledger item.
|
|
369
|
+
const usageIndexCache = new WeakMap()
|
|
370
|
+
function usageIndexFor(record) {
|
|
371
|
+
if (record === undefined || !Array.isArray(record.usage)) return new Map()
|
|
372
|
+
let index = usageIndexCache.get(record)
|
|
373
|
+
if (index === undefined || index.size !== record.usage.length) {
|
|
374
|
+
index = new Map()
|
|
375
|
+
for (const stored of record.usage) if (stored !== null && typeof stored === 'object') index.set(stored.key, stored)
|
|
376
|
+
usageIndexCache.set(record, index)
|
|
377
|
+
}
|
|
378
|
+
return index
|
|
379
|
+
}
|
|
380
|
+
/** Visit one session at a time, reading its record only when it is needed. */
|
|
381
|
+
async function visitPricingRows(visitor, includeEvicted = false) {
|
|
382
|
+
const visitors = Array.isArray(visitor) ? visitor : [visitor]
|
|
383
|
+
const generation = state.aggregationGeneration
|
|
384
|
+
const sessions = new Set()
|
|
385
|
+
for (const item of state.usageByStep.values()) if (typeof item.sid === 'string') sessions.add(item.sid)
|
|
386
|
+
if (includeEvicted && state.perSession instanceof Map) for (const sid of state.perSession.keys()) sessions.add(sid)
|
|
387
|
+
for (const sid of sessions) {
|
|
388
|
+
const task = async () => {
|
|
389
|
+
const releaseRecord = host.ledger?.pinRecord?.(sid)
|
|
390
|
+
try {
|
|
391
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
392
|
+
const record = host.ledger !== undefined && typeof host.ledger.recordFor === 'function'
|
|
393
|
+
? await host.ledger.recordFor(sid)
|
|
394
|
+
: state.ledgerRecords.get(sid)
|
|
395
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
396
|
+
const digest = state.perSession instanceof Map ? state.perSession.get(sid) : undefined
|
|
397
|
+
const durableRecord = record !== undefined && record !== null && record.__durableRecord !== undefined ? record.__durableRecord : record
|
|
398
|
+
const storedItems = usageIndexFor(durableRecord)
|
|
399
|
+
const residentItems = record === durableRecord ? storedItems : usageIndexFor(record)
|
|
400
|
+
const keys = includeEvicted && digest !== undefined
|
|
401
|
+
? Array.from(digest.keys.keys())
|
|
402
|
+
: Array.from(state.usageByStep.values()).filter((item) => item.sid === sid).map((item) => item.key)
|
|
403
|
+
let changed = false
|
|
404
|
+
let published = false
|
|
405
|
+
for (const key of keys) {
|
|
406
|
+
const stored = storedItems.get(key)
|
|
407
|
+
let item = state.usageByStep.get(key)
|
|
408
|
+
if (item === undefined && includeEvicted && stored !== undefined && record !== undefined) {
|
|
409
|
+
item = host.ledger.adoptableRow(host.aggregation.usageRowOf(record, stored, 'ledger-reuse'))
|
|
410
|
+
}
|
|
411
|
+
if (item === undefined) continue
|
|
412
|
+
for (const applyPricing of visitors) {
|
|
413
|
+
if (applyPricing(item) !== true) continue
|
|
414
|
+
// Queries must observe the revision as soon as costs change, including
|
|
415
|
+
// the await while the new durable record is being persisted.
|
|
416
|
+
if (!published) { markStatsChanged('pricing'); published = true }
|
|
417
|
+
if (stored !== undefined) {
|
|
418
|
+
stored.cost = item.cost
|
|
419
|
+
const residentItem = residentItems.get(key)
|
|
420
|
+
if (residentItem !== undefined) residentItem.cost = item.cost
|
|
421
|
+
changed = true
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (changed && durableRecord !== undefined) {
|
|
426
|
+
durableRecord.version = LEDGER_VERSION
|
|
427
|
+
durableRecord.updatedAt = nextLedgerRevision()
|
|
428
|
+
if (record !== durableRecord) { record.version = durableRecord.version; record.updatedAt = durableRecord.updatedAt }
|
|
429
|
+
// Keep upgrade evidence until a complete source-log rebuild clears it.
|
|
430
|
+
const stillMixed = durableRecord.turns.some((turn) => turn && turn.workspaceId !== durableRecord.workspaceId) || durableRecord.usage.some((item) => item && item.workspaceId !== durableRecord.workspaceId)
|
|
431
|
+
durableRecord.needsUpgrade = stillMixed || durableRecord.rebuildRequired !== undefined
|
|
432
|
+
await persistLedgerRecord(durableRecord)
|
|
433
|
+
}
|
|
434
|
+
} finally {
|
|
435
|
+
releaseRecord?.()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (host.sessionSync !== undefined && typeof host.sessionSync.enqueue === 'function') await host.sessionSync.enqueue(sid, task)
|
|
439
|
+
else await task()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function backfillCostVisitor(stats) {
|
|
443
|
+
return (item) => {
|
|
444
|
+
const oldCost = normalizeCostSnapshot(item.cost)
|
|
445
|
+
if (oldCost !== null && oldCost.pricingMode === 'official-model' && oldCost.status === 'priced') return false
|
|
446
|
+
stats.considered += 1
|
|
447
|
+
const billing = billingInstantOf(item, oldCost)
|
|
448
|
+
const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId), billing.at, billing.source)
|
|
449
|
+
const temporalFailClosed = next.status === 'unsupported' && typeof next.reason === 'string' && next.reason.startsWith('temporal-')
|
|
450
|
+
if (next.status !== 'priced' && !temporalFailClosed) return false
|
|
451
|
+
if (costSnapshotEquivalent(oldCost, next)) return false
|
|
452
|
+
if (oldCost !== null) {
|
|
453
|
+
adjustCostOnly(item, oldCost, -1)
|
|
454
|
+
adjustQueryCost(item, oldCost, -1)
|
|
455
|
+
}
|
|
456
|
+
item.cost = next
|
|
457
|
+
adjustCostOnly(item, next, 1)
|
|
458
|
+
adjustQueryCost(item, next, 1)
|
|
459
|
+
stats.priced += 1
|
|
460
|
+
return true
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function pricingRemaining() {
|
|
464
|
+
return state.totals.cost.unpricedCalls + state.totals.cost.ambiguousCalls + state.totals.cost.unsupportedCalls
|
|
465
|
+
}
|
|
466
|
+
async function backfillUnpricedCosts() {
|
|
467
|
+
const stats = { considered: 0, priced: 0 }
|
|
468
|
+
await visitPricingRows(backfillCostVisitor(stats), true)
|
|
469
|
+
return { ...stats, remaining: pricingRemaining() }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function costSnapshotEquivalent(left, right) {
|
|
473
|
+
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false
|
|
474
|
+
if (left.schemaVersion !== right.schemaVersion || left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingHoliday !== right.pricingHoliday || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
|
|
475
|
+
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite']) {
|
|
476
|
+
if (left.breakdown !== null && left.breakdown !== undefined && right.breakdown !== null && right.breakdown !== undefined) {
|
|
477
|
+
if (left.breakdown[key] !== right.breakdown[key]) return false
|
|
478
|
+
}
|
|
479
|
+
if (left.rates[key] !== right.rates[key] || right.rates[key] === undefined) return false
|
|
480
|
+
}
|
|
481
|
+
const leftTier = left.selectedTier || null
|
|
482
|
+
const rightTier = right.selectedTier || null
|
|
483
|
+
if (leftTier === null !== (rightTier === null)) return false
|
|
484
|
+
if (leftTier !== null && (leftTier.type !== rightTier.type || leftTier.size !== rightTier.size)) return false
|
|
485
|
+
return true
|
|
486
|
+
}
|
|
487
|
+
function snapshotPolicyMatches(rawCost, resolved) {
|
|
488
|
+
const exemptReason = rawCost.temporalExemptReason
|
|
489
|
+
const route = resolved.temporalRoute
|
|
490
|
+
if (exemptReason === 'route-not-official') return route !== 'official' && route !== 'mapped'
|
|
491
|
+
if (exemptReason === 'no-temporal-profile' && resolved.temporalProfile === undefined && resolved.temporalConfigInvalid !== true) return true
|
|
492
|
+
if (typeof rawCost.pricingPolicyId !== 'string' || typeof rawCost.pricingPolicyHash !== 'string') return false
|
|
493
|
+
const policies = resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) ? resolved.temporalProfile.policies : []
|
|
494
|
+
if (!policies.some((policy) => policy.policyId === rawCost.pricingPolicyId && policy.policyHash === rawCost.pricingPolicyHash)) return false
|
|
495
|
+
const plan = temporalPlanFor(resolved, rawCost.pricingAt)
|
|
496
|
+
if (plan.status !== 'applied') return false
|
|
497
|
+
return plan.policyHash === rawCost.pricingPolicyHash && plan.band === rawCost.pricingBand
|
|
498
|
+
}
|
|
499
|
+
function temporalCostVisitor(stats, force) {
|
|
500
|
+
return (item) => {
|
|
501
|
+
const rawCost = item.cost
|
|
502
|
+
if (rawCost === null || rawCost === undefined || typeof rawCost !== 'object' || rawCost.pricingMode !== 'official-model') return false
|
|
503
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
504
|
+
const resolved = resolveCurrentPricing(identity)
|
|
505
|
+
const route = resolved.temporalRoute
|
|
506
|
+
if (route !== 'official' && route !== 'mapped') return false
|
|
507
|
+
const legacyShape = rawCost.schemaVersion !== COST_SCHEMA_VERSION || rawCost.pricingTimeSource === null || rawCost.pricingTimeSource === undefined || rawCost.pricingTimeSource === 'legacy-unknown'
|
|
508
|
+
const contextTimes = state.sessionContextTimes.get(item.sid)
|
|
509
|
+
const ownStamp = contextTimes !== undefined && Number.isFinite(rawCost.pricingAt)
|
|
510
|
+
&& contextTimes.get(contextTimeKey(item.turn, item.step)) === rawCost.pricingAt
|
|
511
|
+
const staleInstant = rawCost.pricingTimeSource === 'request-context' && Number.isFinite(rawCost.pricingAt) && validEventTime(item.time)
|
|
512
|
+
&& !ownStamp && !contextTimeIsFresh(rawCost.pricingAt, item.time)
|
|
513
|
+
if (!legacyShape && !force && !staleInstant) {
|
|
514
|
+
if (rawCost.status === 'priced' && snapshotPolicyMatches(rawCost, resolved)) return false
|
|
515
|
+
if (rawCost.status !== 'priced') return false
|
|
516
|
+
}
|
|
517
|
+
stats.considered += 1
|
|
518
|
+
const oldCost = normalizeCostSnapshot(rawCost)
|
|
519
|
+
const billing = billingInstantOf(item, oldCost, !staleInstant)
|
|
520
|
+
const next = calculateCost(item.values, resolved, billing.at, billing.source)
|
|
521
|
+
if (costSnapshotEquivalent(oldCost, next) && rawCost.schemaVersion === next.schemaVersion) return false
|
|
522
|
+
if (oldCost !== null) {
|
|
523
|
+
adjustCostOnly(item, oldCost, -1)
|
|
524
|
+
adjustQueryCost(item, oldCost, -1)
|
|
525
|
+
}
|
|
526
|
+
item.cost = next
|
|
527
|
+
adjustCostOnly(item, next, 1)
|
|
528
|
+
adjustQueryCost(item, next, 1)
|
|
529
|
+
stats.reconciled += 1
|
|
530
|
+
return true
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** Preserve event-time snapshots on catalog refresh; reprice all rows only on demand. */
|
|
534
|
+
async function reconcileTemporalPricing(options = {}) {
|
|
535
|
+
const stats = { considered: 0, reconciled: 0 }
|
|
536
|
+
await visitPricingRows(temporalCostVisitor(stats, options.force === true), true)
|
|
537
|
+
return stats
|
|
538
|
+
}
|
|
539
|
+
async function reconcileUsageCosts(options = {}) {
|
|
540
|
+
const backfill = { considered: 0, priced: 0 }
|
|
541
|
+
const temporal = { considered: 0, reconciled: 0 }
|
|
542
|
+
const visitors = []
|
|
543
|
+
if (options.backfill === true) visitors.push(backfillCostVisitor(backfill))
|
|
544
|
+
if (options.temporal !== false) visitors.push(temporalCostVisitor(temporal, options.force === true))
|
|
545
|
+
await visitPricingRows(visitors, true)
|
|
546
|
+
return { backfill: { ...backfill, remaining: pricingRemaining() }, temporal }
|
|
547
|
+
}
|
|
548
|
+
function updatePricingState(raw, backfill, repriceTemporal = false) {
|
|
549
|
+
return serializePricingMutation(() => updatePricingStateOnce(raw, backfill, repriceTemporal))
|
|
550
|
+
}
|
|
551
|
+
async function updatePricingStateOnce(raw, backfill, repriceTemporal) {
|
|
552
|
+
const input = raw && typeof raw === 'object' && raw.pricing && typeof raw.pricing === 'object' ? raw.pricing : raw
|
|
553
|
+
const current = serializePricingState(state.pricingState)
|
|
554
|
+
const merged = { ...current, ...(input && typeof input === 'object' ? input : {}) }
|
|
555
|
+
if (input && typeof input === 'object' && input.sync && typeof input.sync === 'object' && !Array.isArray(input.sync)) merged.sync = { ...current.sync, ...input.sync }
|
|
556
|
+
state.pricingState = normalizePricingState(merged)
|
|
557
|
+
state.pricingResolutionCache.clear()
|
|
558
|
+
const reconciliation = await reconcileUsageCosts({ backfill: backfill === true, force: repriceTemporal === true })
|
|
559
|
+
const result = reconciliation.backfill
|
|
560
|
+
const temporal = reconciliation.temporal
|
|
561
|
+
markStatsChanged('pricing')
|
|
562
|
+
schedulePricingSync()
|
|
563
|
+
return { ...result, temporalReconciled: temporal.reconciled }
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Fetch one year of the Chinese holiday arrangement for the editor. Nothing is
|
|
567
|
+
* persisted here: the panel freezes the returned dates into a pricing policy,
|
|
568
|
+
* which is what makes later reconciliation reproducible.
|
|
569
|
+
*/
|
|
570
|
+
async function loadHolidayCalendar(year) {
|
|
571
|
+
const cache = state.holidayCache instanceof Map ? state.holidayCache : (state.holidayCache = new Map())
|
|
572
|
+
const key = String(Number(year))
|
|
573
|
+
const cached = cache.get(key)
|
|
574
|
+
if (cached !== undefined && Date.now() - cached.fetchedAt < HOLIDAY_CALENDAR_TTL_MS) return { ok: true, cached: true, ...cached }
|
|
575
|
+
const result = await fetchHolidayCalendar(year)
|
|
576
|
+
if (!result.ok) return result
|
|
577
|
+
const entry = {
|
|
578
|
+
year: result.calendar.year,
|
|
579
|
+
dates: result.calendar.dates,
|
|
580
|
+
paperUrl: result.calendar.paperUrl,
|
|
581
|
+
sourceUrl: result.calendar.sourceUrl,
|
|
582
|
+
fetchedAt: result.calendar.fetchedAt,
|
|
583
|
+
}
|
|
584
|
+
cache.set(key, entry)
|
|
585
|
+
return { ok: true, cached: false, ...entry }
|
|
586
|
+
}
|
|
587
|
+
function schedulePricingSync() {
|
|
588
|
+
if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
|
|
589
|
+
if (state.disposed || state.pricingState.sync.autoEnabled !== true) return
|
|
590
|
+
const elapsed = state.pricingState.sync.lastSuccessAt > 0 ? Date.now() - state.pricingState.sync.lastSuccessAt : state.pricingState.sync.intervalMs
|
|
591
|
+
const delay = Math.max(0, state.pricingState.sync.intervalMs - elapsed)
|
|
592
|
+
state.pricingSyncTimer = setTimeout(() => {
|
|
593
|
+
state.pricingSyncTimer = null
|
|
594
|
+
void syncPricing(true)
|
|
595
|
+
}, delay)
|
|
596
|
+
if (state.pricingSyncTimer && typeof state.pricingSyncTimer.unref === 'function') state.pricingSyncTimer.unref()
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
adjustCostOnly,
|
|
602
|
+
usedPricingModels,
|
|
603
|
+
pricingModelSearch,
|
|
604
|
+
pricingSnapshot,
|
|
605
|
+
loadPricing,
|
|
606
|
+
persistPricing,
|
|
607
|
+
syncPricing,
|
|
608
|
+
backfillUnpricedCosts,
|
|
609
|
+
reconcileTemporalPricing,
|
|
610
|
+
reconcileUsageCosts,
|
|
611
|
+
updatePricingState,
|
|
612
|
+
schedulePricingSync,
|
|
613
|
+
loadHolidayCalendar
|
|
614
|
+
}
|
|
615
|
+
}
|