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.js
CHANGED
|
@@ -1,1412 +1,1463 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
const PRICING_SCHEMA_VERSION = 1
|
|
4
|
-
const COST_SCHEMA_VERSION = 2
|
|
5
|
-
const MODEL_CATALOG_URL = 'https://models.dev/api.json'
|
|
6
|
-
const DEFAULT_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000
|
|
7
|
-
const MAX_CATALOG_BYTES = 24 * 1024 * 1024
|
|
8
|
-
const MAX_PRICE_ENTRIES = 10000
|
|
9
|
-
// Hard candidate ceiling before the sorting-heavy selection: a pathological
|
|
10
|
-
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
11
|
-
// Hard candidate ceiling before any normalization-heavy sort: a pathological
|
|
12
|
-
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
13
|
-
const MAX_CATALOG_CANDIDATES = 30000
|
|
14
|
-
const MAX_OVERRIDES = 500
|
|
15
|
-
const MAX_MAPPINGS = 500
|
|
16
|
-
const MAX_CONTEXT_TIERS = 32
|
|
17
|
-
const MAX_TEMPORAL_RULES = 16
|
|
18
|
-
const MAX_TEMPORAL_WINDOWS = 16
|
|
19
|
-
const MAX_TEMPORAL_POLICIES = 8
|
|
20
|
-
const MAX_TEMPORAL_HOLIDAYS = 400
|
|
21
|
-
const TEMPORAL_TIMEZONE = 'UTC'
|
|
22
|
-
// Statutory-holiday dates are China Standard Time calendar days; a UTC policy
|
|
23
|
-
// band therefore runs from 16:00Z the day before to 16:00Z on the day itself.
|
|
24
|
-
const TEMPORAL_HOLIDAY_OFFSET_MS = 8 * 60 * 60 * 1000
|
|
25
|
-
// Public, no-key datasets of the State Council holiday arrangement (the file per
|
|
26
|
-
// year carries `days[].isOffDay`, and the official gov.cn notice in `papers`).
|
|
27
|
-
const HOLIDAY_CALENDAR_BASES = [
|
|
28
|
-
'https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/',
|
|
29
|
-
'https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/',
|
|
30
|
-
]
|
|
31
|
-
const MAX_HOLIDAY_CALENDAR_BYTES = 512 * 1024
|
|
32
|
-
const TEMPORAL_TIME_SOURCES = ['request-context', 'usage-event', 'legacy-unknown']
|
|
33
|
-
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
34
|
-
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
35
|
-
const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
|
|
36
|
-
const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']
|
|
37
|
-
const COST_ACCUMULATOR = Symbol('costAccumulator')
|
|
38
|
-
const COST_PARTS_CACHE = new WeakMap()
|
|
39
|
-
const OFFICIAL_PROVIDER_RULES = [
|
|
40
|
-
{ providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
|
|
41
|
-
{ providers: ['anthropic'], prefixes: ['claude-'] },
|
|
42
|
-
{ providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
|
|
43
|
-
{ providers: ['xai'], prefixes: ['grok-'] },
|
|
44
|
-
{ providers: ['deepseek', 'deepseek-official'], prefixes: ['deepseek-'] },
|
|
45
|
-
{ providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
|
|
46
|
-
{ providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
|
|
47
|
-
{ providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
|
|
48
|
-
{ providers: ['minimax'], prefixes: ['minimax-'] },
|
|
49
|
-
{ providers: ['mistral'], prefixes: ['mistral-', 'mixtral-'] },
|
|
50
|
-
{ providers: ['meta'], prefixes: ['llama-', 'meta-llama'] },
|
|
51
|
-
{ providers: ['cohere'], prefixes: ['command-'] },
|
|
52
|
-
{ providers: ['ai21'], prefixes: ['jamba-'] },
|
|
53
|
-
{ providers: ['baidu'], prefixes: ['ernie-'] },
|
|
54
|
-
]
|
|
55
|
-
const MAX_DECIMAL_DIGITS = 40
|
|
56
|
-
const MAX_DECIMAL_EXPONENT = 24
|
|
57
|
-
|
|
58
|
-
function isRecord(value) {
|
|
59
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function finiteNumber(value) {
|
|
63
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
64
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
65
|
-
const parsed = Number(value)
|
|
66
|
-
if (Number.isFinite(parsed)) return parsed
|
|
67
|
-
}
|
|
68
|
-
return null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function decimalParts(value) {
|
|
72
|
-
const raw = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
73
|
-
const match = raw.match(/^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/)
|
|
74
|
-
if (!match) return null
|
|
75
|
-
let digits = (match[1] || '') + (match[2] || '')
|
|
76
|
-
const exponent = match[3] ? Number(match[3]) : 0
|
|
77
|
-
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > MAX_DECIMAL_EXPONENT) return null
|
|
78
|
-
let scale = (match[2] || '').length - exponent
|
|
79
|
-
digits = digits.replace(/^0+(?=\d)/, '')
|
|
80
|
-
if (scale < 0) {
|
|
81
|
-
digits += '0'.repeat(-scale)
|
|
82
|
-
scale = 0
|
|
83
|
-
}
|
|
84
|
-
if (scale > digits.length) digits = '0'.repeat(scale - digits.length + 1) + digits
|
|
85
|
-
if (digits.length > MAX_DECIMAL_DIGITS) return null
|
|
86
|
-
while (scale > 0 && digits.length > 1 && digits.endsWith('0')) {
|
|
87
|
-
digits = digits.slice(0, -1)
|
|
88
|
-
scale -= 1
|
|
89
|
-
}
|
|
90
|
-
digits = digits.replace(/^0+(?=\d)/, '')
|
|
91
|
-
return { digits: BigInt(digits || '0'), scale }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function decimalText(value) {
|
|
95
|
-
const parts = decimalParts(value)
|
|
96
|
-
if (parts === null) return null
|
|
97
|
-
if (parts.digits === 0n) return '0'
|
|
98
|
-
const raw = parts.digits.toString()
|
|
99
|
-
if (parts.scale === 0) return raw
|
|
100
|
-
const padded = raw.padStart(parts.scale + 1, '0')
|
|
101
|
-
const split = padded.length - parts.scale
|
|
102
|
-
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizedDecimalParts(parts) {
|
|
106
|
-
let digits = parts && typeof parts.digits === 'bigint' ? parts.digits : 0n
|
|
107
|
-
let scale = parts && Number.isSafeInteger(parts.scale) && parts.scale >= 0 ? parts.scale : 0
|
|
108
|
-
if (digits < 0n) digits = 0n
|
|
109
|
-
while (scale > 0 && digits !== 0n && digits % 10n === 0n) {
|
|
110
|
-
digits /= 10n
|
|
111
|
-
scale -= 1
|
|
112
|
-
}
|
|
113
|
-
return { digits, scale }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function decimalAccumulatorText(value) {
|
|
117
|
-
const parts = normalizedDecimalParts(value)
|
|
118
|
-
if (parts.digits === 0n) return '0'
|
|
119
|
-
const raw = parts.digits.toString()
|
|
120
|
-
if (parts.scale === 0) return raw
|
|
121
|
-
const padded = raw.padStart(parts.scale + 1, '0')
|
|
122
|
-
const split = padded.length - parts.scale
|
|
123
|
-
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isCostAccumulator(value) {
|
|
127
|
-
return value !== null && typeof value === 'object' && value[COST_ACCUMULATOR] === true
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function createCostAccumulator(currency = 'USD') {
|
|
131
|
-
const result = { currency, input: { digits: 0n, scale: 0 }, output: { digits: 0n, scale: 0 }, cacheRead: { digits: 0n, scale: 0 }, cacheWrite: { digits: 0n, scale: 0 }, baseTotal: { digits: 0n, scale: 0 }, total: { digits: 0n, scale: 0 }, pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
132
|
-
Object.defineProperty(result, COST_ACCUMULATOR, { value: true })
|
|
133
|
-
return result
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function costAccumulatorParts(cost) {
|
|
137
|
-
if (isCostAccumulator(cost)) return cost
|
|
138
|
-
if (cost !== null && typeof cost === 'object') {
|
|
139
|
-
const cached = COST_PARTS_CACHE.get(cost)
|
|
140
|
-
if (cached !== undefined) return cached
|
|
141
|
-
}
|
|
142
|
-
const value = isRecord(cost) ? cost : {}
|
|
143
|
-
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
144
|
-
const breakdown = isRecord(value.breakdown) ? value.breakdown : {}
|
|
145
|
-
const result = {
|
|
146
|
-
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
147
|
-
status,
|
|
148
|
-
input: decimalParts(breakdown.input) || { digits: 0n, scale: 0 },
|
|
149
|
-
output: decimalParts(breakdown.output) || { digits: 0n, scale: 0 },
|
|
150
|
-
cacheRead: decimalParts(breakdown.cacheRead) || { digits: 0n, scale: 0 },
|
|
151
|
-
cacheWrite: decimalParts(breakdown.cacheWrite) || { digits: 0n, scale: 0 },
|
|
152
|
-
baseTotal: decimalParts(value.baseTotal) || { digits: 0n, scale: 0 },
|
|
153
|
-
total: decimalParts(value.total) || { digits: 0n, scale: 0 },
|
|
154
|
-
}
|
|
155
|
-
if (cost !== null && typeof cost === 'object') COST_PARTS_CACHE.set(cost, result)
|
|
156
|
-
return result
|
|
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
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
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
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return null
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
for (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
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
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return null
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
return {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
if (
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
const
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const PRICING_SCHEMA_VERSION = 1
|
|
4
|
+
const COST_SCHEMA_VERSION = 2
|
|
5
|
+
const MODEL_CATALOG_URL = 'https://models.dev/api.json'
|
|
6
|
+
const DEFAULT_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000
|
|
7
|
+
const MAX_CATALOG_BYTES = 24 * 1024 * 1024
|
|
8
|
+
const MAX_PRICE_ENTRIES = 10000
|
|
9
|
+
// Hard candidate ceiling before the sorting-heavy selection: a pathological
|
|
10
|
+
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
11
|
+
// Hard candidate ceiling before any normalization-heavy sort: a pathological
|
|
12
|
+
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
13
|
+
const MAX_CATALOG_CANDIDATES = 30000
|
|
14
|
+
const MAX_OVERRIDES = 500
|
|
15
|
+
const MAX_MAPPINGS = 500
|
|
16
|
+
const MAX_CONTEXT_TIERS = 32
|
|
17
|
+
const MAX_TEMPORAL_RULES = 16
|
|
18
|
+
const MAX_TEMPORAL_WINDOWS = 16
|
|
19
|
+
const MAX_TEMPORAL_POLICIES = 8
|
|
20
|
+
const MAX_TEMPORAL_HOLIDAYS = 400
|
|
21
|
+
const TEMPORAL_TIMEZONE = 'UTC'
|
|
22
|
+
// Statutory-holiday dates are China Standard Time calendar days; a UTC policy
|
|
23
|
+
// band therefore runs from 16:00Z the day before to 16:00Z on the day itself.
|
|
24
|
+
const TEMPORAL_HOLIDAY_OFFSET_MS = 8 * 60 * 60 * 1000
|
|
25
|
+
// Public, no-key datasets of the State Council holiday arrangement (the file per
|
|
26
|
+
// year carries `days[].isOffDay`, and the official gov.cn notice in `papers`).
|
|
27
|
+
const HOLIDAY_CALENDAR_BASES = [
|
|
28
|
+
'https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/',
|
|
29
|
+
'https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/',
|
|
30
|
+
]
|
|
31
|
+
const MAX_HOLIDAY_CALENDAR_BYTES = 512 * 1024
|
|
32
|
+
const TEMPORAL_TIME_SOURCES = ['request-context', 'usage-event', 'legacy-unknown']
|
|
33
|
+
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
34
|
+
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
35
|
+
const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
|
|
36
|
+
const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']
|
|
37
|
+
const COST_ACCUMULATOR = Symbol('costAccumulator')
|
|
38
|
+
const COST_PARTS_CACHE = new WeakMap()
|
|
39
|
+
const OFFICIAL_PROVIDER_RULES = [
|
|
40
|
+
{ providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
|
|
41
|
+
{ providers: ['anthropic'], prefixes: ['claude-'] },
|
|
42
|
+
{ providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
|
|
43
|
+
{ providers: ['xai'], prefixes: ['grok-'] },
|
|
44
|
+
{ providers: ['deepseek', 'deepseek-official'], prefixes: ['deepseek-'] },
|
|
45
|
+
{ providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
|
|
46
|
+
{ providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
|
|
47
|
+
{ providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
|
|
48
|
+
{ providers: ['minimax'], prefixes: ['minimax-'] },
|
|
49
|
+
{ providers: ['mistral'], prefixes: ['mistral-', 'mixtral-'] },
|
|
50
|
+
{ providers: ['meta'], prefixes: ['llama-', 'meta-llama'] },
|
|
51
|
+
{ providers: ['cohere'], prefixes: ['command-'] },
|
|
52
|
+
{ providers: ['ai21'], prefixes: ['jamba-'] },
|
|
53
|
+
{ providers: ['baidu'], prefixes: ['ernie-'] },
|
|
54
|
+
]
|
|
55
|
+
const MAX_DECIMAL_DIGITS = 40
|
|
56
|
+
const MAX_DECIMAL_EXPONENT = 24
|
|
57
|
+
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function finiteNumber(value) {
|
|
63
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
64
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
65
|
+
const parsed = Number(value)
|
|
66
|
+
if (Number.isFinite(parsed)) return parsed
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function decimalParts(value) {
|
|
72
|
+
const raw = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
73
|
+
const match = raw.match(/^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/)
|
|
74
|
+
if (!match) return null
|
|
75
|
+
let digits = (match[1] || '') + (match[2] || '')
|
|
76
|
+
const exponent = match[3] ? Number(match[3]) : 0
|
|
77
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > MAX_DECIMAL_EXPONENT) return null
|
|
78
|
+
let scale = (match[2] || '').length - exponent
|
|
79
|
+
digits = digits.replace(/^0+(?=\d)/, '')
|
|
80
|
+
if (scale < 0) {
|
|
81
|
+
digits += '0'.repeat(-scale)
|
|
82
|
+
scale = 0
|
|
83
|
+
}
|
|
84
|
+
if (scale > digits.length) digits = '0'.repeat(scale - digits.length + 1) + digits
|
|
85
|
+
if (digits.length > MAX_DECIMAL_DIGITS) return null
|
|
86
|
+
while (scale > 0 && digits.length > 1 && digits.endsWith('0')) {
|
|
87
|
+
digits = digits.slice(0, -1)
|
|
88
|
+
scale -= 1
|
|
89
|
+
}
|
|
90
|
+
digits = digits.replace(/^0+(?=\d)/, '')
|
|
91
|
+
return { digits: BigInt(digits || '0'), scale }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function decimalText(value) {
|
|
95
|
+
const parts = decimalParts(value)
|
|
96
|
+
if (parts === null) return null
|
|
97
|
+
if (parts.digits === 0n) return '0'
|
|
98
|
+
const raw = parts.digits.toString()
|
|
99
|
+
if (parts.scale === 0) return raw
|
|
100
|
+
const padded = raw.padStart(parts.scale + 1, '0')
|
|
101
|
+
const split = padded.length - parts.scale
|
|
102
|
+
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizedDecimalParts(parts) {
|
|
106
|
+
let digits = parts && typeof parts.digits === 'bigint' ? parts.digits : 0n
|
|
107
|
+
let scale = parts && Number.isSafeInteger(parts.scale) && parts.scale >= 0 ? parts.scale : 0
|
|
108
|
+
if (digits < 0n) digits = 0n
|
|
109
|
+
while (scale > 0 && digits !== 0n && digits % 10n === 0n) {
|
|
110
|
+
digits /= 10n
|
|
111
|
+
scale -= 1
|
|
112
|
+
}
|
|
113
|
+
return { digits, scale }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decimalAccumulatorText(value) {
|
|
117
|
+
const parts = normalizedDecimalParts(value)
|
|
118
|
+
if (parts.digits === 0n) return '0'
|
|
119
|
+
const raw = parts.digits.toString()
|
|
120
|
+
if (parts.scale === 0) return raw
|
|
121
|
+
const padded = raw.padStart(parts.scale + 1, '0')
|
|
122
|
+
const split = padded.length - parts.scale
|
|
123
|
+
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isCostAccumulator(value) {
|
|
127
|
+
return value !== null && typeof value === 'object' && value[COST_ACCUMULATOR] === true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createCostAccumulator(currency = 'USD') {
|
|
131
|
+
const result = { currency, input: { digits: 0n, scale: 0 }, output: { digits: 0n, scale: 0 }, cacheRead: { digits: 0n, scale: 0 }, cacheWrite: { digits: 0n, scale: 0 }, baseTotal: { digits: 0n, scale: 0 }, total: { digits: 0n, scale: 0 }, pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
132
|
+
Object.defineProperty(result, COST_ACCUMULATOR, { value: true })
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function costAccumulatorParts(cost) {
|
|
137
|
+
if (isCostAccumulator(cost)) return cost
|
|
138
|
+
if (cost !== null && typeof cost === 'object') {
|
|
139
|
+
const cached = COST_PARTS_CACHE.get(cost)
|
|
140
|
+
if (cached !== undefined) return cached
|
|
141
|
+
}
|
|
142
|
+
const value = isRecord(cost) ? cost : {}
|
|
143
|
+
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
144
|
+
const breakdown = isRecord(value.breakdown) ? value.breakdown : {}
|
|
145
|
+
const result = {
|
|
146
|
+
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
147
|
+
status,
|
|
148
|
+
input: decimalParts(breakdown.input) || { digits: 0n, scale: 0 },
|
|
149
|
+
output: decimalParts(breakdown.output) || { digits: 0n, scale: 0 },
|
|
150
|
+
cacheRead: decimalParts(breakdown.cacheRead) || { digits: 0n, scale: 0 },
|
|
151
|
+
cacheWrite: decimalParts(breakdown.cacheWrite) || { digits: 0n, scale: 0 },
|
|
152
|
+
baseTotal: decimalParts(value.baseTotal) || { digits: 0n, scale: 0 },
|
|
153
|
+
total: decimalParts(value.total) || { digits: 0n, scale: 0 },
|
|
154
|
+
}
|
|
155
|
+
if (cost !== null && typeof cost === 'object') COST_PARTS_CACHE.set(cost, result)
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Powers of ten as BigInt, built on demand. Aligning two decimal parts is the
|
|
160
|
+
// hottest step of a cold start (the fold adds six cost fields per usage row into
|
|
161
|
+
// several cubes), and it used to call `10n ** BigInt(k)` twice per addition.
|
|
162
|
+
const POW10 = [1n]
|
|
163
|
+
function pow10(exponent) {
|
|
164
|
+
let value = POW10[exponent]
|
|
165
|
+
if (value === undefined) {
|
|
166
|
+
for (let index = POW10.length; index <= exponent; index += 1) POW10.push(POW10[index - 1] * 10n)
|
|
167
|
+
value = POW10[exponent]
|
|
168
|
+
}
|
|
169
|
+
return value
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Both operands of an addition are already canonical in the common case (the
|
|
173
|
+
// parser strips trailing zeros, the accumulators store what this function wrote,
|
|
174
|
+
// and a fresh accumulator is 0), so the three normalizing passes per field are
|
|
175
|
+
// skipped: aligning a value that happens to carry trailing zeros still yields the
|
|
176
|
+
// same number, and the result is normalized below regardless.
|
|
177
|
+
function canonicalDecimalParts(parts) {
|
|
178
|
+
return parts !== null && typeof parts === 'object' && typeof parts.digits === 'bigint' && parts.digits >= 0n && Number.isSafeInteger(parts.scale) && parts.scale >= 0
|
|
179
|
+
? parts
|
|
180
|
+
: normalizedDecimalParts(parts)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function addDecimalAccumulator(target, field, value, direction) {
|
|
184
|
+
const left = canonicalDecimalParts(target[field])
|
|
185
|
+
const right = canonicalDecimalParts(value)
|
|
186
|
+
const scale = Math.max(left.scale, right.scale)
|
|
187
|
+
const leftDigits = left.scale === scale ? left.digits : left.digits * pow10(scale - left.scale)
|
|
188
|
+
const rightDigits = right.scale === scale ? right.digits : right.digits * pow10(scale - right.scale)
|
|
189
|
+
let digits = leftDigits + BigInt(direction) * rightDigits
|
|
190
|
+
if (digits < 0n) digits = 0n
|
|
191
|
+
target[field] = normalizedDecimalParts({ digits, scale })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function addCostAccumulator(target, cost, direction = 1) {
|
|
195
|
+
if (!isCostAccumulator(target)) return target
|
|
196
|
+
if (isCostAccumulator(cost)) {
|
|
197
|
+
for (const field of COST_FIELDS) addDecimalAccumulator(target, field, cost[field], direction)
|
|
198
|
+
target.pricedCalls = Math.max(0, target.pricedCalls + direction * cost.pricedCalls)
|
|
199
|
+
target.unpricedCalls = Math.max(0, target.unpricedCalls + direction * cost.unpricedCalls)
|
|
200
|
+
target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction * cost.ambiguousCalls)
|
|
201
|
+
target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction * cost.unsupportedCalls)
|
|
202
|
+
return target
|
|
203
|
+
}
|
|
204
|
+
const value = costAccumulatorParts(cost)
|
|
205
|
+
if (value.status === 'priced') {
|
|
206
|
+
for (const field of COST_FIELDS) addDecimalAccumulator(target, field, value[field], direction)
|
|
207
|
+
target.pricedCalls = Math.max(0, target.pricedCalls + direction)
|
|
208
|
+
} else if (value.status === 'ambiguous') target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction)
|
|
209
|
+
else if (value.status === 'unsupported') target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction)
|
|
210
|
+
else target.unpricedCalls = Math.max(0, target.unpricedCalls + direction)
|
|
211
|
+
return target
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function decimalAdd(left, right) {
|
|
215
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
216
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
217
|
+
const scale = Math.max(a.scale, b.scale)
|
|
218
|
+
const value = a.digits * pow10(scale - a.scale) + b.digits * pow10(scale - b.scale)
|
|
219
|
+
return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function decimalMultiply(left, right) {
|
|
223
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
224
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
225
|
+
return decimalText((a.digits * b.digits).toString() + ((a.scale + b.scale) > 0 ? 'e-' + (a.scale + b.scale) : '')) || '0'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function decimalSubtract(left, right) {
|
|
229
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
230
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
231
|
+
const scale = Math.max(a.scale, b.scale)
|
|
232
|
+
const value = a.digits * pow10(scale - a.scale) - b.digits * pow10(scale - b.scale)
|
|
233
|
+
if (value <= 0n) return '0'
|
|
234
|
+
return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function decimalGreaterThanZero(value) {
|
|
238
|
+
const parts = decimalParts(value)
|
|
239
|
+
return parts !== null && parts.digits > 0n
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function nonNegativeDecimal(value, fallback = '0') {
|
|
243
|
+
const parsed = decimalText(value)
|
|
244
|
+
if (parsed === null || !decimalParts(parsed) || decimalParts(parsed).digits < 0n) return fallback
|
|
245
|
+
return parsed
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeModelId(value) {
|
|
249
|
+
if (typeof value !== 'string') return ''
|
|
250
|
+
let normalized = value.trim()
|
|
251
|
+
const slash = normalized.lastIndexOf('/')
|
|
252
|
+
if (slash >= 0) normalized = normalized.slice(slash + 1)
|
|
253
|
+
normalized = normalized.split(':', 1)[0].trim().replace(/@/g, '-').toLowerCase()
|
|
254
|
+
normalized = normalized.replace(/\[1m\]$/i, '').trim()
|
|
255
|
+
return normalized
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function pushUnique(list, value) {
|
|
259
|
+
if (typeof value === 'string' && value !== '' && !list.includes(value)) list.push(value)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function stripKnownNamespace(value) {
|
|
263
|
+
const claude = value.lastIndexOf('claude-')
|
|
264
|
+
if (claude > 0) return value.slice(claude)
|
|
265
|
+
for (const marker of ['openai.', 'anthropic.', 'google.', 'moonshot.', 'moonshotai.', 'bedrock.', 'global.']) {
|
|
266
|
+
if (value.startsWith(marker)) return value.slice(marker.length)
|
|
267
|
+
}
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function stripClaudeDesktopPrefix(value) {
|
|
272
|
+
const markers = ['abab', 'ark-code', 'arctic', 'astron', 'codex', 'command-r', 'deepseek', 'doubao', 'ernie', 'gemini', 'gemma', 'glm', 'gpt', 'grok', 'hermes', 'hy3', 'hunyuan', 'jamba', 'kimi', 'lfm', 'llama', 'longcat', 'mercury', 'mimo', 'minimax', 'mistral', 'mixtral', 'moonshot', 'nemotron', 'nova-', 'openai', 'qianfan', 'qwen', 'seed-', 'solar', 'stepfun']
|
|
273
|
+
if (!value.startsWith('claude-')) return null
|
|
274
|
+
const rest = value.slice('claude-'.length)
|
|
275
|
+
return markers.some((marker) => rest.startsWith(marker)) ? rest : null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stripBedrockSuffix(value) {
|
|
279
|
+
const match = value.match(/^(.+)-v(\d+)$/)
|
|
280
|
+
return match ? match[1] : null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function stripDateSuffix(value) {
|
|
284
|
+
let match = value.match(/^(.+)-(\d{4}-\d{2}-\d{2})$/)
|
|
285
|
+
if (match) return match[1]
|
|
286
|
+
match = value.match(/^(.+)-(\d{8})$/)
|
|
287
|
+
if (match) return match[1]
|
|
288
|
+
match = value.match(/^(.+)-(\d{6})$/)
|
|
289
|
+
if (!match) return null
|
|
290
|
+
const month = Number(match[2].slice(2, 4))
|
|
291
|
+
const day = Number(match[2].slice(4, 6))
|
|
292
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= 31 ? match[1] : null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function stripReasoningSuffix(value) {
|
|
296
|
+
for (const suffix of ['-minimal', '-low', '-medium', '-high', '-xhigh']) {
|
|
297
|
+
if (value.endsWith(suffix) && value.length > suffix.length) return value.slice(0, -suffix.length)
|
|
298
|
+
}
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function shouldTryPrefix(value) {
|
|
303
|
+
const dashCount = (value.match(/-/g) || []).length
|
|
304
|
+
if (value.startsWith('claude-')) return dashCount >= 3
|
|
305
|
+
if (['o1', 'o3', 'o4', 'o5'].some((prefix) => value.startsWith(prefix))) return dashCount >= 1
|
|
306
|
+
return ['gpt-', 'gemini-', 'deepseek-', 'qwen-', 'glm-', 'kimi-', 'minimax-'].some((prefix) => value.startsWith(prefix)) && dashCount >= 2
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function modelPricingCandidates(value) {
|
|
310
|
+
const cleaned = normalizeModelId(value)
|
|
311
|
+
if (cleaned === '') return []
|
|
312
|
+
const candidates = []
|
|
313
|
+
const queue = [cleaned]
|
|
314
|
+
while (queue.length > 0) {
|
|
315
|
+
const candidate = queue.pop()
|
|
316
|
+
if (candidates.includes(candidate)) continue
|
|
317
|
+
pushUnique(candidates, candidate)
|
|
318
|
+
for (const next of [stripKnownNamespace(candidate), stripClaudeDesktopPrefix(candidate), stripBedrockSuffix(candidate), stripDateSuffix(candidate), stripReasoningSuffix(candidate)]) {
|
|
319
|
+
if (next !== null) queue.push(next)
|
|
320
|
+
}
|
|
321
|
+
if (candidate.startsWith('claude-') && candidate.includes('.')) queue.push(candidate.replace(/\./g, '-'))
|
|
322
|
+
}
|
|
323
|
+
return candidates
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizeProvider(value) {
|
|
327
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function officialProviderIds(modelId) {
|
|
331
|
+
const providers = new Set()
|
|
332
|
+
for (const candidate of modelPricingCandidates(modelId)) {
|
|
333
|
+
for (const rule of OFFICIAL_PROVIDER_RULES) if (rule.prefixes.some((prefix) => candidate.startsWith(prefix))) for (const provider of rule.providers) providers.add(provider)
|
|
334
|
+
}
|
|
335
|
+
return providers
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function identityModels(identity) {
|
|
339
|
+
const value = isRecord(identity) ? identity : {}
|
|
340
|
+
const result = []
|
|
341
|
+
for (const candidate of [value.actualModel, value.requestedModel, value.model, value.label]) {
|
|
342
|
+
const normalized = normalizeModelId(candidate)
|
|
343
|
+
if (normalized !== '' && !result.includes(normalized)) result.push(normalized)
|
|
344
|
+
}
|
|
345
|
+
return result
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function identityKeyOf(identity) {
|
|
349
|
+
if (!isRecord(identity)) return ''
|
|
350
|
+
const key = typeof identity.identityKey === 'string' && identity.identityKey.trim() !== '' ? identity.identityKey : identity.usageIdentityKey
|
|
351
|
+
return typeof key === 'string' ? key.trim() : ''
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function priceEntryKey(entry) {
|
|
355
|
+
return normalizeProvider(entry.providerId) + '\0' + normalizeModelId(entry.modelId)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function decimalRate(raw, keys, fallback = undefined) {
|
|
359
|
+
let value
|
|
360
|
+
for (const key of keys) {
|
|
361
|
+
if (raw && raw[key] !== undefined) {
|
|
362
|
+
value = raw[key]
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (value === undefined || value === null || value === '') value = fallback
|
|
367
|
+
return decimalText(value)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function temporalRatesOf(raw, fallback) {
|
|
371
|
+
const rates = isRecord(raw) ? raw : {}
|
|
372
|
+
const input = decimalRate(rates, ['input', 'inputPerMillion'], fallback && fallback.input)
|
|
373
|
+
const output = decimalRate(rates, ['output', 'outputPerMillion'], fallback && fallback.output)
|
|
374
|
+
const cacheRead = decimalRate(rates, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], fallback && fallback.cacheRead)
|
|
375
|
+
const cacheWrite = decimalRate(rates, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], fallback && fallback.cacheWrite)
|
|
376
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null) return null
|
|
377
|
+
const parts = [decimalParts(input), decimalParts(output), decimalParts(cacheRead), decimalParts(cacheWrite)]
|
|
378
|
+
if (parts.some((entry) => entry === null || entry.digits < 0n)) return null
|
|
379
|
+
return { input, output, cacheRead, cacheWrite }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizeTemporalWindows(raw) {
|
|
383
|
+
if (!Array.isArray(raw)) return null
|
|
384
|
+
const windows = []
|
|
385
|
+
for (const candidate of raw.slice(0, MAX_TEMPORAL_WINDOWS)) {
|
|
386
|
+
if (!isRecord(candidate)) return null
|
|
387
|
+
const startMinute = finiteNumber(candidate.startMinute)
|
|
388
|
+
const endMinute = finiteNumber(candidate.endMinute)
|
|
389
|
+
if (startMinute === null || endMinute === null || !Number.isInteger(startMinute) || !Number.isInteger(endMinute) || startMinute < 0 || startMinute >= 1440 || endMinute <= startMinute || endMinute > 1440) return null
|
|
390
|
+
windows.push({ startMinute, endMinute })
|
|
391
|
+
}
|
|
392
|
+
if (windows.length === 0) return null
|
|
393
|
+
// Half-open [start, end) windows that overlap would make the band ambiguous;
|
|
394
|
+
// deterministic order plus adjacency-only constraint keeps selection stable.
|
|
395
|
+
windows.sort((left, right) => left.startMinute - right.startMinute || left.endMinute - right.endMinute)
|
|
396
|
+
for (let index = 1; index < windows.length; index += 1) {
|
|
397
|
+
if (windows[index].startMinute < windows[index - 1].endMinute) return null
|
|
398
|
+
}
|
|
399
|
+
return windows
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeHolidayDate(value) {
|
|
403
|
+
if (typeof value !== 'string') return null
|
|
404
|
+
const text = value.trim()
|
|
405
|
+
const match = text.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/)
|
|
406
|
+
if (match === null) return null
|
|
407
|
+
const year = Number(match[1])
|
|
408
|
+
const month = Number(match[2])
|
|
409
|
+
const day = Number(match[3])
|
|
410
|
+
if (year < 2000 || year > 2100) return null
|
|
411
|
+
const date = new Date(Date.UTC(year, month - 1, day))
|
|
412
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null
|
|
413
|
+
return text
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Explicit statutory-holiday dates: sorted, de-duplicated, strictly shaped. */
|
|
417
|
+
function normalizeTemporalHolidays(raw) {
|
|
418
|
+
if (raw === undefined || raw === null) return []
|
|
419
|
+
if (!Array.isArray(raw)) return null
|
|
420
|
+
const seen = new Set()
|
|
421
|
+
const days = []
|
|
422
|
+
for (const candidate of raw) {
|
|
423
|
+
const text = normalizeHolidayDate(candidate)
|
|
424
|
+
if (text === null) return null
|
|
425
|
+
if (seen.has(text)) continue
|
|
426
|
+
seen.add(text)
|
|
427
|
+
days.push(text)
|
|
428
|
+
if (days.length > MAX_TEMPORAL_HOLIDAYS) return null
|
|
429
|
+
}
|
|
430
|
+
days.sort()
|
|
431
|
+
return days
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const TEMPORAL_HOLIDAY_SETS = new WeakMap()
|
|
435
|
+
/** Cached lookup set for one policy's holiday list. */
|
|
436
|
+
function holidaySetFor(policy) {
|
|
437
|
+
if (!isRecord(policy)) return null
|
|
438
|
+
const holidays = Array.isArray(policy.holidays) ? policy.holidays : null
|
|
439
|
+
if (holidays === null || holidays.length === 0) return null
|
|
440
|
+
let set = TEMPORAL_HOLIDAY_SETS.get(policy)
|
|
441
|
+
if (set === undefined) {
|
|
442
|
+
set = new Set(holidays)
|
|
443
|
+
TEMPORAL_HOLIDAY_SETS.set(policy, set)
|
|
444
|
+
}
|
|
445
|
+
return set
|
|
446
|
+
}
|
|
447
|
+
/** True when the instant falls on one of the policy's statutory holidays (CST day). */
|
|
448
|
+
function holidayCoversInstant(policy, atMs) {
|
|
449
|
+
const set = holidaySetFor(policy)
|
|
450
|
+
if (set === null || !Number.isFinite(atMs)) return false
|
|
451
|
+
const local = new Date(Math.trunc(atMs) + TEMPORAL_HOLIDAY_OFFSET_MS)
|
|
452
|
+
const text = String(local.getUTCFullYear()) + '-' + String(local.getUTCMonth() + 1).padStart(2, '0') + '-' + String(local.getUTCDate()).padStart(2, '0')
|
|
453
|
+
return set.has(text)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** One versioned temporal policy: effective-windowed, UTC-band rules. */
|
|
457
|
+
function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
|
|
458
|
+
if (!isRecord(raw)) return null
|
|
459
|
+
const policyId = typeof raw.policyId === 'string' && raw.policyId.trim() !== '' ? raw.policyId.trim().slice(0, 128) : ''
|
|
460
|
+
if (policyId === '') return null
|
|
461
|
+
const sourceUrl = typeof raw.sourceUrl === 'string' && raw.sourceUrl.trim() !== '' ? raw.sourceUrl.trim().slice(0, 512) : ''
|
|
462
|
+
const timezone = typeof raw.timezone === 'string' && raw.timezone.trim() !== '' ? raw.timezone.trim().toUpperCase() : TEMPORAL_TIMEZONE
|
|
463
|
+
// DeepSeek peak/off-peak windows are defined in UTC; any other timezone is
|
|
464
|
+
// rejected outright so band selection can never depend on the viewer's clock.
|
|
465
|
+
if (timezone !== TEMPORAL_TIMEZONE) return null
|
|
466
|
+
const effectiveFrom = raw.effectiveFrom === undefined || raw.effectiveFrom === null ? 0 : finiteNumber(raw.effectiveFrom)
|
|
467
|
+
const effectiveUntil = raw.effectiveUntil === undefined || raw.effectiveUntil === null ? null : finiteNumber(raw.effectiveUntil)
|
|
468
|
+
if (effectiveFrom === null || !Number.isSafeInteger(effectiveFrom) || effectiveFrom < 0) return null
|
|
469
|
+
if (effectiveUntil !== null && (!Number.isSafeInteger(effectiveUntil) || effectiveUntil <= effectiveFrom)) return null
|
|
470
|
+
let defaultPlan = null
|
|
471
|
+
if (raw.defaultPlan !== undefined && raw.defaultPlan !== null) {
|
|
472
|
+
if (!isRecord(raw.defaultPlan)) return null
|
|
473
|
+
const rates = temporalRatesOf(raw.defaultPlan.rates || raw.defaultPlan, entryDefaultRates)
|
|
474
|
+
if (rates === null) return null
|
|
475
|
+
defaultPlan = { id: typeof raw.defaultPlan.id === 'string' && raw.defaultPlan.id.trim() !== '' ? raw.defaultPlan.id.trim().slice(0, 64) : 'default', rates }
|
|
476
|
+
}
|
|
477
|
+
if (raw.rules !== undefined && !Array.isArray(raw.rules)) return null
|
|
478
|
+
if (!Array.isArray(raw.rules)) return null
|
|
479
|
+
if (raw.rules.length > MAX_TEMPORAL_RULES) return null
|
|
480
|
+
const rules = []
|
|
481
|
+
for (const rawRule of raw.rules) {
|
|
482
|
+
if (!isRecord(rawRule)) return null
|
|
483
|
+
const id = typeof rawRule.id === 'string' && rawRule.id.trim() !== '' ? rawRule.id.trim().slice(0, 64) : ''
|
|
484
|
+
if (id === '') return null
|
|
485
|
+
const weekdays = Array.from(new Set((Array.isArray(rawRule.weekdays) ? rawRule.weekdays : []).map(Number)))
|
|
486
|
+
if (weekdays.length === 0 || weekdays.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) return null
|
|
487
|
+
weekdays.sort((left, right) => left - right)
|
|
488
|
+
const windows = normalizeTemporalWindows(rawRule.windows)
|
|
489
|
+
if (windows === null) return null
|
|
490
|
+
const rates = temporalRatesOf(rawRule.rates || rawRule, defaultPlan ? defaultPlan.rates : entryDefaultRates)
|
|
491
|
+
if (rates === null) return null
|
|
492
|
+
rules.push({ id, weekdays, windows, rates })
|
|
493
|
+
}
|
|
494
|
+
if (rules.length === 0) return null
|
|
495
|
+
// Rules must never both claim the same weekday minute: a JSON array order
|
|
496
|
+
// that decides a price is nondeterministic and must be rejected instead.
|
|
497
|
+
const dayWindows = new Map()
|
|
498
|
+
for (const rule of rules) {
|
|
499
|
+
for (const day of rule.weekdays) {
|
|
500
|
+
let list = dayWindows.get(day)
|
|
501
|
+
if (list === undefined) { list = []; dayWindows.set(day, list) }
|
|
502
|
+
for (const window of rule.windows) list.push(window)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const list of dayWindows.values()) {
|
|
506
|
+
list.sort((left, right) => left.startMinute - right.startMinute || left.endMinute - right.endMinute)
|
|
507
|
+
for (let index = 1; index < list.length; index += 1) {
|
|
508
|
+
if (list[index].startMinute < list[index - 1].endMinute) return null
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const holidays = normalizeTemporalHolidays(raw.holidays)
|
|
512
|
+
if (holidays === null) return null
|
|
513
|
+
// Provenance of a fetched holiday list. Deliberately NOT part of the policy
|
|
514
|
+
// hash: where the dates came from does not change what they price, but it is
|
|
515
|
+
// kept next to them so a frozen list can be traced back.
|
|
516
|
+
const holidaysSource = typeof raw.holidaysSource === 'string' ? raw.holidaysSource.trim().slice(0, 512) : ''
|
|
517
|
+
return { policyId, sourceUrl, timezone, effectiveFrom, effectiveUntil, defaultPlan, holidays, holidaysSource, rules }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Temporal pricing plan: an ordered, non-overlapping archive of policies. */
|
|
521
|
+
function normalizeTemporalPricing(raw, entryDefaultRates = undefined) {
|
|
522
|
+
if (!isRecord(raw)) return null
|
|
523
|
+
const rawPolicies = Array.isArray(raw.policies) ? raw.policies : (typeof raw.policyId === 'string' ? [raw] : null)
|
|
524
|
+
if (rawPolicies === null || rawPolicies.length === 0 || rawPolicies.length > MAX_TEMPORAL_POLICIES) return null
|
|
525
|
+
const policies = []
|
|
526
|
+
for (const rawPolicy of rawPolicies) {
|
|
527
|
+
const policy = normalizeTemporalPolicy(rawPolicy, entryDefaultRates)
|
|
528
|
+
if (policy === null) return null
|
|
529
|
+
policies.push(policy)
|
|
530
|
+
}
|
|
531
|
+
policies.sort((left, right) => left.effectiveFrom - right.effectiveFrom || (left.policyId < right.policyId ? -1 : left.policyId > right.policyId ? 1 : 0))
|
|
532
|
+
// A policy whose window never ends must be the last one; successive windows
|
|
533
|
+
// may leave a gap (which fails closed) but must never overlap.
|
|
534
|
+
for (let index = 1; index < policies.length; index += 1) {
|
|
535
|
+
const previous = policies[index - 1]
|
|
536
|
+
if (previous.effectiveUntil === null || policies[index].effectiveFrom < previous.effectiveUntil) return null
|
|
537
|
+
}
|
|
538
|
+
return { policies }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const DEEPSEEK_TEMPORAL_POLICY_ID = 'deepseek-v4-2026-08-pricing'
|
|
542
|
+
const DEEPSEEK_TEMPORAL_POLICY_URL = 'https://api-docs.deepseek.com/quick_start/pricing/'
|
|
543
|
+
// DeepSeek announced peak/off-peak pricing effective 2026-08-16T16:00:00Z; the
|
|
544
|
+
// V4-Flash-Vision-Exp model itself became available on 2026-08-21. Before those
|
|
545
|
+
// instants there is no verifiable band archive, so usage fails closed instead
|
|
546
|
+
// of inheriting today's V4 rates.
|
|
547
|
+
const DEEPSEEK_TEMPORAL_EFFECTIVE_FROM = Date.UTC(2026, 7, 16, 16, 0, 0)
|
|
548
|
+
const DEEPSEEK_VISION_EFFECTIVE_FROM = Date.UTC(2026, 7, 21, 0, 0, 0)
|
|
549
|
+
// Built-in first-party profiles: each model carries its own explicit off-peak
|
|
550
|
+
// (inherited from the live catalog entry) and peak rates; the peak rates are
|
|
551
|
+
// stored as data, never derived as an automatic 'half price' rule.
|
|
552
|
+
// models.dev publishes `deepseek-flash` (DeepSeek V4.1 Flash) in the same
|
|
553
|
+
// `deepseek-flash` family and at the same standard rates as
|
|
554
|
+
// `deepseek-v4-flash`, so it shares that band; a variant that arrives with
|
|
555
|
+
// different standard rates still needs its own row.
|
|
556
|
+
const DEEPSEEK_TEMPORAL_MODELS = new Map([
|
|
557
|
+
['deepseek-v4-flash', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
558
|
+
['deepseek-flash', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
559
|
+
['deepseek-v4-flash-vision-exp', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_VISION_EFFECTIVE_FROM }],
|
|
560
|
+
['deepseek-v4-pro', { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
561
|
+
])
|
|
562
|
+
|
|
563
|
+
// DeepSeek's own peak table excludes Chinese public holidays (`周一至周五(不含中国
|
|
564
|
+
// 法定节假日)`), so the built-in plan has to carry the arrangement itself; otherwise
|
|
565
|
+
// holiday requests are charged at peak rates. The list below is the 2026 arrangement
|
|
566
|
+
// from the State Council notice. Later years ship with a plugin update, and any plan
|
|
567
|
+
// (a built-in copy or a custom one) can be extended by hand or fetched at runtime
|
|
568
|
+
// through POST /api/all-usage/pricing/holidays.
|
|
569
|
+
const CHINA_HOLIDAY_SOURCE_2026 = 'https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm'
|
|
570
|
+
const CHINA_HOLIDAYS_2026 = [
|
|
571
|
+
'2026-01-01', '2026-01-02', '2026-01-03', '2026-02-15', '2026-02-16', '2026-02-17',
|
|
572
|
+
'2026-02-18', '2026-02-19', '2026-02-20', '2026-02-21', '2026-02-22', '2026-02-23',
|
|
573
|
+
'2026-04-04', '2026-04-05', '2026-04-06', '2026-05-01', '2026-05-02', '2026-05-03',
|
|
574
|
+
'2026-05-04', '2026-05-05', '2026-06-19', '2026-06-20', '2026-06-21', '2026-09-25',
|
|
575
|
+
'2026-09-26', '2026-09-27', '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',
|
|
576
|
+
'2026-10-05', '2026-10-06', '2026-10-07',
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
export function builtinTemporalProfileFor(modelId) {
|
|
580
|
+
const spec = DEEPSEEK_TEMPORAL_MODELS.get(normalizeModelId(modelId))
|
|
581
|
+
if (spec === undefined) return null
|
|
582
|
+
return {
|
|
583
|
+
policies: [{
|
|
584
|
+
policyId: DEEPSEEK_TEMPORAL_POLICY_ID,
|
|
585
|
+
sourceUrl: DEEPSEEK_TEMPORAL_POLICY_URL,
|
|
586
|
+
timezone: TEMPORAL_TIMEZONE,
|
|
587
|
+
effectiveFrom: spec.effectiveFrom,
|
|
588
|
+
effectiveUntil: null,
|
|
589
|
+
defaultPlan: null,
|
|
590
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
591
|
+
holidaysSource: CHINA_HOLIDAY_SOURCE_2026 + ' · 2026 中国法定节假日安排',
|
|
592
|
+
rules: [{ id: 'peak', weekdays: [1, 2, 3, 4, 5], windows: [{ startMinute: 60, endMinute: 240 }, { startMinute: 360, endMinute: 600 }], rates: { input: spec.input, output: spec.output, cacheRead: spec.cacheRead, cacheWrite: spec.cacheWrite } }],
|
|
593
|
+
}],
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Policy covering an instant inside a profile's ordered policy archive. */
|
|
598
|
+
function temporalPolicyFor(profile, atMs) {
|
|
599
|
+
const policies = isRecord(profile) ? profile.policies : null
|
|
600
|
+
if (!Array.isArray(policies) || policies.length === 0 || !Number.isFinite(atMs)) return null
|
|
601
|
+
const at = Math.trunc(atMs)
|
|
602
|
+
for (const policy of policies) {
|
|
603
|
+
if (at < policy.effectiveFrom) continue
|
|
604
|
+
if (policy.effectiveUntil !== null && at >= policy.effectiveUntil) continue
|
|
605
|
+
return policy
|
|
606
|
+
}
|
|
607
|
+
return null
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** UTC half-open band: weekday in rule set AND minute within [start, end). */
|
|
611
|
+
function temporalBand(profile, atMs) {
|
|
612
|
+
if (!Number.isFinite(atMs) || atMs < 0) return null
|
|
613
|
+
const policy = temporalPolicyFor(profile, atMs)
|
|
614
|
+
if (policy === null) return null
|
|
615
|
+
// A statutory holiday is a full off-peak day: no rule can claim its instants.
|
|
616
|
+
if (holidayCoversInstant(policy, atMs)) return 'off-peak'
|
|
617
|
+
const matched = matchingTemporalRule(policy, atMs)
|
|
618
|
+
return matched === null ? 'off-peak' : 'peak'
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function matchingTemporalRule(policy, atMs) {
|
|
622
|
+
const date = new Date(Math.trunc(atMs))
|
|
623
|
+
const weekday = date.getUTCDay()
|
|
624
|
+
const minute = date.getUTCHours() * 60 + date.getUTCMinutes()
|
|
625
|
+
for (const rule of (policy && Array.isArray(policy.rules) ? policy.rules : [])) {
|
|
626
|
+
if (!(Array.isArray(rule.weekdays) ? rule.weekdays : []).includes(weekday)) continue
|
|
627
|
+
if ((Array.isArray(rule.windows) ? rule.windows : []).some((window) => minute >= window.startMinute && minute < window.endMinute)) return rule
|
|
628
|
+
}
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Deterministic hash of one policy: the policy content only, never the live
|
|
634
|
+
* catalog rates. A snapshot's policy hash is therefore stable across catalog
|
|
635
|
+
* refreshes, which keeps historical costs auditable (see the reconciliation
|
|
636
|
+
* rules); changing the policy itself yields a new hash.
|
|
637
|
+
*/
|
|
638
|
+
function temporalPolicyHash(policy) {
|
|
639
|
+
const stable = JSON.stringify([
|
|
640
|
+
policy.policyId,
|
|
641
|
+
policy.timezone,
|
|
642
|
+
policy.effectiveFrom,
|
|
643
|
+
policy.effectiveUntil,
|
|
644
|
+
policy.defaultPlan,
|
|
645
|
+
policy.holidays,
|
|
646
|
+
policy.rules,
|
|
647
|
+
])
|
|
648
|
+
return createHash('sha256').update(stable).digest('hex')
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Resolve one usage instant against a resolved pricing entry: which policy
|
|
653
|
+
* covers it, which band applies and which rates that band carries. Returns the
|
|
654
|
+
* stable signature that both calculateCost() and the snapshot-reuse check
|
|
655
|
+
* consume.
|
|
656
|
+
*/
|
|
657
|
+
function temporalPlanFor(resolved, pricingAtMs) {
|
|
658
|
+
if (isRecord(resolved) && resolved.temporalConfigInvalid === true) {
|
|
659
|
+
return { status: 'config-invalid', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'temporal-config-invalid' }
|
|
660
|
+
}
|
|
661
|
+
const profile = isRecord(resolved) && resolved.temporalProfile ? resolved.temporalProfile : null
|
|
662
|
+
if (profile === null) {
|
|
663
|
+
return { status: 'none', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'no-temporal-profile' }
|
|
664
|
+
}
|
|
665
|
+
const route = resolved.temporalRoute === 'official' || resolved.temporalRoute === 'mapped' ? resolved.temporalRoute : 'other'
|
|
666
|
+
if (route === 'other') return { status: 'other-route', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'route-not-official' }
|
|
667
|
+
if (!Number.isFinite(pricingAtMs) || pricingAtMs < 0) return { status: 'time-missing', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
668
|
+
const at = Math.trunc(pricingAtMs)
|
|
669
|
+
const policy = temporalPolicyFor(profile, at)
|
|
670
|
+
if (policy === null) return { status: 'history-gap', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
671
|
+
const common = { policyId: policy.policyId || null, policyHash: policy.policyHash || null, timezone: policy.timezone === TEMPORAL_TIMEZONE ? TEMPORAL_TIMEZONE : null }
|
|
672
|
+
// A statutory holiday is a full off-peak day; its audit flag is stored with
|
|
673
|
+
// the cost snapshot so a ledger row can explain why it was off-peak.
|
|
674
|
+
if (holidayCoversInstant(policy, at)) return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: true, exemptReason: null, ...common }
|
|
675
|
+
const rule = matchingTemporalRule(policy, at)
|
|
676
|
+
if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, holiday: false, exemptReason: null, ...common }
|
|
677
|
+
return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: false, exemptReason: null, ...common }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function normalizeContextTier(raw, fallbackRates, legacySize = null) {
|
|
681
|
+
if (!isRecord(raw)) return null
|
|
682
|
+
const descriptor = isRecord(raw.tier) ? raw.tier : raw
|
|
683
|
+
const type = legacySize === null ? (typeof descriptor.type === 'string' ? descriptor.type.trim().toLowerCase() : '') : 'context'
|
|
684
|
+
const sizeValue = legacySize === null ? descriptor.size : legacySize
|
|
685
|
+
const size = finiteNumber(sizeValue)
|
|
686
|
+
if (type !== 'context' || size === null || !Number.isSafeInteger(size) || size <= 0 || size > 1000000000) return null
|
|
687
|
+
const input = decimalRate(raw, ['input', 'inputPerMillion'], fallbackRates.input)
|
|
688
|
+
const output = decimalRate(raw, ['output', 'outputPerMillion'], fallbackRates.output)
|
|
689
|
+
const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], fallbackRates.cacheRead)
|
|
690
|
+
const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], fallbackRates.cacheWrite)
|
|
691
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null) return null
|
|
692
|
+
if (decimalParts(input).digits < 0n || decimalParts(output).digits < 0n || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
|
|
693
|
+
return { type: 'context', size, input, output, cacheRead, cacheWrite }
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
697
|
+
if (!isRecord(raw)) return null
|
|
698
|
+
const modelId = normalizeModelId(raw.modelId || raw.id)
|
|
699
|
+
if (modelId === '') return null
|
|
700
|
+
const input = decimalRate(raw, ['input', 'inputPerMillion'])
|
|
701
|
+
const output = decimalRate(raw, ['output', 'outputPerMillion'])
|
|
702
|
+
if (input === null || output === null || decimalParts(input).digits < 0n || decimalParts(output).digits < 0n) return null
|
|
703
|
+
const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], '0')
|
|
704
|
+
const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], '0')
|
|
705
|
+
if (cacheRead === null || cacheWrite === null || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
|
|
706
|
+
const fallbackRates = { input, output, cacheRead, cacheWrite }
|
|
707
|
+
const tiers = []
|
|
708
|
+
let tieredInvalid = false
|
|
709
|
+
const hasTierData = raw.tiers !== undefined || raw.context_over_200k !== undefined
|
|
710
|
+
if (raw.tiers !== undefined) {
|
|
711
|
+
if (!Array.isArray(raw.tiers)) tieredInvalid = true
|
|
712
|
+
else {
|
|
713
|
+
if (raw.tiers.length > MAX_CONTEXT_TIERS) tieredInvalid = true
|
|
714
|
+
for (const rawTier of raw.tiers.slice(0, MAX_CONTEXT_TIERS)) {
|
|
715
|
+
const tier = normalizeContextTier(rawTier, fallbackRates)
|
|
716
|
+
if (tier === null || tiers.some((existing) => existing.size === tier.size)) tieredInvalid = true
|
|
717
|
+
else tiers.push(tier)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (raw.context_over_200k !== undefined && raw.tiers === undefined) {
|
|
722
|
+
const tier = normalizeContextTier(raw.context_over_200k, fallbackRates, 200000)
|
|
723
|
+
if (tier === null) tieredInvalid = true
|
|
724
|
+
else tiers.push(tier)
|
|
725
|
+
}
|
|
726
|
+
tiers.sort((left, right) => left.size - right.size)
|
|
727
|
+
const tiered = raw.tiered !== undefined ? raw.tiered === true : hasTierData || tiers.length > 0
|
|
728
|
+
if (!tiered) {
|
|
729
|
+
tiers.length = 0
|
|
730
|
+
tieredInvalid = false
|
|
731
|
+
} else if (tiers.length === 0) tieredInvalid = true
|
|
732
|
+
const hasTemporalConfig = raw.temporalPricing !== undefined && raw.temporalPricing !== null
|
|
733
|
+
const temporalPricing = hasTemporalConfig ? normalizeTemporalPricing(raw.temporalPricing, fallbackRates) || undefined : undefined
|
|
734
|
+
// An explicit invalid config keeps its sentinel across the normalize →
|
|
735
|
+
// serialize → normalize cycle, so a restart cannot silently re-enable the
|
|
736
|
+
// built-in profile after a rejected temporalPricing edit.
|
|
737
|
+
const temporalPricingInvalid = hasTemporalConfig && temporalPricing === undefined ? true : raw.temporalPricingInvalid === true && temporalPricing === undefined
|
|
738
|
+
return {
|
|
739
|
+
providerId: typeof raw.providerId === 'string' ? raw.providerId.trim().toLowerCase() : '',
|
|
740
|
+
providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
|
|
741
|
+
modelId,
|
|
742
|
+
displayName: typeof raw.displayName === 'string' && raw.displayName.trim() !== '' ? raw.displayName.trim().slice(0, 200) : modelId,
|
|
743
|
+
currency: typeof raw.currency === 'string' && raw.currency.trim() !== '' ? raw.currency.trim().toUpperCase() : 'USD',
|
|
744
|
+
input,
|
|
745
|
+
output,
|
|
746
|
+
cacheRead,
|
|
747
|
+
cacheWrite,
|
|
748
|
+
source: raw.source === 'manual' ? 'manual' : sourceDefault,
|
|
749
|
+
tiered,
|
|
750
|
+
...(tiered ? { tiers, tieredInvalid } : {}),
|
|
751
|
+
reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
|
|
752
|
+
fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
|
|
753
|
+
...(temporalPricing === undefined ? {} : { temporalPricing }),
|
|
754
|
+
...(temporalPricingInvalid === true ? { temporalPricingInvalid: true } : {}),
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
|
|
759
|
+
if (!isRecord(raw)) return { ok: false, error: 'catalog-not-object' }
|
|
760
|
+
const entries = []
|
|
761
|
+
for (const [providerKey, provider] of Object.entries(raw)) {
|
|
762
|
+
if (!isRecord(provider) || !isRecord(provider.models)) continue
|
|
763
|
+
const providerId = typeof provider.id === 'string' && provider.id.trim() !== '' ? provider.id.trim() : providerKey
|
|
764
|
+
const providerName = typeof provider.name === 'string' ? provider.name : providerId
|
|
765
|
+
for (const [modelKey, model] of Object.entries(provider.models)) {
|
|
766
|
+
if (!isRecord(model)) continue
|
|
767
|
+
const entry = normalizePriceEntry({
|
|
768
|
+
providerId,
|
|
769
|
+
providerName,
|
|
770
|
+
modelId: typeof model.id === 'string' ? model.id : modelKey,
|
|
771
|
+
displayName: model.name,
|
|
772
|
+
currency: 'USD',
|
|
773
|
+
input: model.cost && model.cost.input,
|
|
774
|
+
output: model.cost && model.cost.output,
|
|
775
|
+
cacheRead: model.cost && model.cost.cache_read,
|
|
776
|
+
cacheWrite: model.cost && model.cost.cache_write,
|
|
777
|
+
tiers: model.cost && model.cost.tiers,
|
|
778
|
+
context_over_200k: model.cost && model.cost.context_over_200k,
|
|
779
|
+
tiered: model.cost && model.cost.tiers !== undefined || model.cost && model.cost.context_over_200k !== undefined,
|
|
780
|
+
reasoningRateAvailable: model.cost && model.cost.reasoning !== undefined,
|
|
781
|
+
source: 'models.dev',
|
|
782
|
+
fetchedAt,
|
|
783
|
+
})
|
|
784
|
+
if (entry === null || entry.currency !== 'USD') continue
|
|
785
|
+
// Duplicate normalized keys are NOT resolved here: they are kept and
|
|
786
|
+
// resolved deterministically after the stable sort below.
|
|
787
|
+
entries.push(entry)
|
|
788
|
+
if (entries.length > MAX_CATALOG_CANDIDATES) return { ok: false, error: 'catalog-exceeds-candidate-limit', candidateCount: entries.length }
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (entries.length === 0) return { ok: false, error: 'catalog-has-no-priced-models' }
|
|
792
|
+
// Sort by the stable key first: upstream object order must not decide which
|
|
793
|
+
// models survive a large catalog or which conflicting duplicate is kept.
|
|
794
|
+
// Code-unit comparison: default-locale localeCompare can report ties for
|
|
795
|
+
// distinct strings (e.g. composed/decomposed accents), which would let
|
|
796
|
+
// upstream enumeration order decide the surviving entry.
|
|
797
|
+
entries.sort((left, right) => { const ka = priceEntryKey(left); const kb = priceEntryKey(right); return ka < kb ? -1 : ka > kb ? 1 : 0 })
|
|
798
|
+
// Deterministic duplicate selection: within one normalized key, compare only
|
|
799
|
+
// the small conflict group by their full content (fetchedAt excluded), so
|
|
800
|
+
// ordering swaps cannot change the surviving price.
|
|
801
|
+
const stableText = (entry) => {
|
|
802
|
+
const copy = Object.assign({}, entry)
|
|
803
|
+
delete copy.fetchedAt
|
|
804
|
+
if (typeof copy.providerId === 'string') copy.providerId = copy.providerId.trim().toLowerCase()
|
|
805
|
+
return JSON.stringify(copy)
|
|
806
|
+
}
|
|
807
|
+
const ordered = []
|
|
808
|
+
{
|
|
809
|
+
let index = 0
|
|
810
|
+
while (index < entries.length) {
|
|
811
|
+
const key = priceEntryKey(entries[index])
|
|
812
|
+
let end = index + 1
|
|
813
|
+
while (end < entries.length && priceEntryKey(entries[end]) === key) end += 1
|
|
814
|
+
let group = end - index > 1 ? entries.slice(index, end).sort((left, right) => { const la = stableText(left); const lb = stableText(right); return la < lb ? -1 : la > lb ? 1 : 0 }) : entries.slice(index, end)
|
|
815
|
+
ordered.push(group[0])
|
|
816
|
+
index = end
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
ordered.length = Math.min(ordered.length, MAX_PRICE_ENTRIES)
|
|
820
|
+
// The fetchedAt stamp differs on every fetch; exclude it from the content
|
|
821
|
+
// hash so identical catalog contents hash identically across syncs.
|
|
822
|
+
const canonical = JSON.stringify(ordered.map((entry) => {
|
|
823
|
+
if (entry.fetchedAt === undefined) return entry
|
|
824
|
+
const { fetchedAt, ...stable } = entry
|
|
825
|
+
return stable
|
|
826
|
+
}))
|
|
827
|
+
return {
|
|
828
|
+
ok: true,
|
|
829
|
+
catalog: {
|
|
830
|
+
schemaVersion: PRICING_SCHEMA_VERSION,
|
|
831
|
+
sourceUrl: MODEL_CATALOG_URL,
|
|
832
|
+
fetchedAt,
|
|
833
|
+
catalogHash: createHash('sha256').update(canonical).digest('hex'),
|
|
834
|
+
entries: ordered,
|
|
835
|
+
},
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function normalizePricingState(raw) {
|
|
840
|
+
const value = isRecord(raw) ? raw : {}
|
|
841
|
+
const sync = isRecord(value.sync) ? value.sync : {}
|
|
842
|
+
const source = isRecord(value.source) ? value.source : {}
|
|
843
|
+
const normalizeEntries = (items, sourceDefault, limit) => (Array.isArray(items) ? items.slice(0, limit) : []).map((item) => normalizePriceEntry(item, sourceDefault)).filter((item) => item !== null)
|
|
844
|
+
const catalogEntries = normalizeEntries(value.catalogEntries || value.entries, 'models.dev', MAX_PRICE_ENTRIES)
|
|
845
|
+
const overrides = normalizeEntries(value.overrides, 'manual', MAX_OVERRIDES).map((entry) => ({ ...entry, source: 'manual' }))
|
|
846
|
+
const mappings = (Array.isArray(value.mappings) ? value.mappings : []).slice(0, MAX_MAPPINGS).map((mapping) => {
|
|
847
|
+
if (!isRecord(mapping)) return null
|
|
848
|
+
const provider = typeof mapping.provider === 'string' ? mapping.provider.trim() : ''
|
|
849
|
+
const model = normalizeModelId(mapping.model || mapping.modelId)
|
|
850
|
+
const catalogProviderId = typeof mapping.catalogProviderId === 'string' ? mapping.catalogProviderId.trim() : ''
|
|
851
|
+
const catalogModelId = normalizeModelId(mapping.catalogModelId || mapping.catalogModel)
|
|
852
|
+
const identityKey = (typeof mapping.identityKey === 'string' && mapping.identityKey.trim() !== '' ? mapping.identityKey : mapping.usageIdentityKey)
|
|
853
|
+
const normalizedIdentityKey = typeof identityKey === 'string' ? identityKey.trim().slice(0, 1024) : ''
|
|
854
|
+
if (provider === '' && model === '' && catalogProviderId === '' && catalogModelId === '' && normalizedIdentityKey === '') return null
|
|
855
|
+
return {
|
|
856
|
+
identityKey: normalizedIdentityKey,
|
|
857
|
+
provider,
|
|
858
|
+
model,
|
|
859
|
+
catalogProviderId,
|
|
860
|
+
catalogModelId,
|
|
861
|
+
inputTokenSemantics: INPUT_SEMANTICS.includes(mapping.inputTokenSemantics) ? mapping.inputTokenSemantics : 'fresh',
|
|
862
|
+
multiplier: decimalText(mapping.multiplier === undefined ? '1' : mapping.multiplier) || '1',
|
|
863
|
+
}
|
|
864
|
+
}).filter((mapping) => mapping !== null)
|
|
865
|
+
const providerAliases = {}
|
|
866
|
+
if (isRecord(value.providerAliases)) {
|
|
867
|
+
for (const [from, to] of Object.entries(value.providerAliases).slice(0, MAX_MAPPINGS)) {
|
|
868
|
+
if (typeof from === 'string' && typeof to === 'string' && from.trim() !== '' && to.trim() !== '') providerAliases[from.trim()] = to.trim()
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const normalized = {
|
|
872
|
+
schemaVersion: PRICING_SCHEMA_VERSION,
|
|
873
|
+
source: {
|
|
874
|
+
url: typeof source.url === 'string' && source.url !== '' ? source.url : MODEL_CATALOG_URL,
|
|
875
|
+
fetchedAt: Number.isFinite(source.fetchedAt) ? source.fetchedAt : 0,
|
|
876
|
+
catalogHash: typeof source.catalogHash === 'string' ? source.catalogHash.slice(0, 128) : '',
|
|
877
|
+
lastError: typeof source.lastError === 'string' ? source.lastError.slice(0, 500) : '',
|
|
878
|
+
},
|
|
879
|
+
sync: {
|
|
880
|
+
autoEnabled: sync.autoEnabled === true,
|
|
881
|
+
intervalMs: Number.isFinite(sync.intervalMs) && sync.intervalMs >= 60 * 60 * 1000 ? sync.intervalMs : DEFAULT_SYNC_INTERVAL_MS,
|
|
882
|
+
lastAttemptAt: Number.isFinite(sync.lastAttemptAt) ? sync.lastAttemptAt : 0,
|
|
883
|
+
lastSuccessAt: Number.isFinite(sync.lastSuccessAt) ? sync.lastSuccessAt : 0,
|
|
884
|
+
lastError: typeof sync.lastError === 'string' ? sync.lastError.slice(0, 500) : '',
|
|
885
|
+
},
|
|
886
|
+
catalogEntries,
|
|
887
|
+
overrides,
|
|
888
|
+
mappings,
|
|
889
|
+
providerAliases,
|
|
890
|
+
}
|
|
891
|
+
const entries = normalized.overrides.concat(normalized.catalogEntries)
|
|
892
|
+
const exactIndex = new Map()
|
|
893
|
+
for (const entry of entries) {
|
|
894
|
+
const list = exactIndex.get(entry.modelId) || []
|
|
895
|
+
list.push(entry)
|
|
896
|
+
exactIndex.set(entry.modelId, list)
|
|
897
|
+
}
|
|
898
|
+
Object.defineProperty(normalized, '_entries', { value: entries, enumerable: false })
|
|
899
|
+
Object.defineProperty(normalized, '_exactIndex', { value: exactIndex, enumerable: false })
|
|
900
|
+
Object.defineProperty(normalized, '_normalized', { value: true, enumerable: false })
|
|
901
|
+
return normalized
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function createEmptyPricingState() {
|
|
905
|
+
return normalizePricingState({})
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function serializePricingState(state) {
|
|
909
|
+
const normalized = normalizePricingState(state)
|
|
910
|
+
return {
|
|
911
|
+
schemaVersion: normalized.schemaVersion,
|
|
912
|
+
source: { ...normalized.source },
|
|
913
|
+
sync: { ...normalized.sync },
|
|
914
|
+
catalogEntries: normalized.catalogEntries.map((entry) => ({ ...entry })),
|
|
915
|
+
overrides: normalized.overrides.map((entry) => ({ ...entry })),
|
|
916
|
+
mappings: normalized.mappings.map((mapping) => ({ ...mapping })),
|
|
917
|
+
providerAliases: { ...normalized.providerAliases },
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function sameRates(left, right) {
|
|
922
|
+
if (left.input !== right.input || left.output !== right.output || left.cacheRead !== right.cacheRead || left.cacheWrite !== right.cacheWrite || left.currency !== right.currency || left.tiered !== right.tiered || left.tieredInvalid !== right.tieredInvalid || left.temporalPricingInvalid !== right.temporalPricingInvalid) return false
|
|
923
|
+
// Two entries with identical flat rates but different band plans are NOT the
|
|
924
|
+
// same price: the winner must never depend on enumeration order.
|
|
925
|
+
if (JSON.stringify(left.temporalPricing || null) !== JSON.stringify(right.temporalPricing || null)) return false
|
|
926
|
+
const leftTiers = Array.isArray(left.tiers) ? left.tiers : []
|
|
927
|
+
const rightTiers = Array.isArray(right.tiers) ? right.tiers : []
|
|
928
|
+
if (leftTiers.length !== rightTiers.length) return false
|
|
929
|
+
return leftTiers.every((leftTier, index) => {
|
|
930
|
+
const rightTier = rightTiers[index]
|
|
931
|
+
return rightTier && leftTier.type === rightTier.type && leftTier.size === rightTier.size && leftTier.input === rightTier.input && leftTier.output === rightTier.output && leftTier.cacheRead === rightTier.cacheRead && leftTier.cacheWrite === rightTier.cacheWrite
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function mappingMatches(mapping, identity) {
|
|
936
|
+
if (mapping.identityKey !== '') {
|
|
937
|
+
if (mapping.identityKey !== identityKeyOf(identity)) return false
|
|
938
|
+
return mapping.provider === '' || (isRecord(identity) && typeof identity.provider === 'string' && identity.provider.trim().toLowerCase() === mapping.provider.toLowerCase())
|
|
939
|
+
}
|
|
940
|
+
if (mapping.provider !== '' && (!isRecord(identity) || typeof identity.provider !== 'string' || identity.provider.trim().toLowerCase() !== mapping.provider.toLowerCase())) return false
|
|
941
|
+
const models = identityModels(identity)
|
|
942
|
+
return mapping.model !== '' && models.includes(mapping.model)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function findMappedEntry(mapping, state) {
|
|
946
|
+
if (mapping.catalogProviderId !== '' && mapping.catalogModelId !== '') {
|
|
947
|
+
return state.overrides.find((entry) => normalizeProvider(entry.providerId) === normalizeProvider(mapping.catalogProviderId) && entry.modelId === mapping.catalogModelId) || state.catalogEntries.find((entry) => normalizeProvider(entry.providerId) === normalizeProvider(mapping.catalogProviderId) && entry.modelId === mapping.catalogModelId) || null
|
|
948
|
+
}
|
|
949
|
+
if (mapping.catalogModelId !== '') {
|
|
950
|
+
const matches = state.overrides.concat(state.catalogEntries).filter((entry) => entry.modelId === mapping.catalogModelId)
|
|
951
|
+
return matches.length === 1 ? matches[0] : null
|
|
952
|
+
}
|
|
953
|
+
return null
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const DEEPSEEK_OFFICIAL_PROVIDERS = ['deepseek', 'deepseek-official']
|
|
957
|
+
|
|
958
|
+
function attachTemporalProfile(entry, mapped, identity) {
|
|
959
|
+
if (entry === null || entry === undefined || !isRecord(entry)) return null
|
|
960
|
+
// An explicit but invalid temporal config must never silently fall back to
|
|
961
|
+
// the built-in profile: the configuration error stays visible and the model
|
|
962
|
+
// fails closed instead of pricing with a plan the user did not write.
|
|
963
|
+
if (entry.temporalPricingInvalid === true) return { invalid: true }
|
|
964
|
+
let profile = isRecord(entry.temporalPricing) ? entry.temporalPricing : null
|
|
965
|
+
// The editor needs to tell a user-authored plan from the built-in DeepSeek
|
|
966
|
+
// table: only the former can be reset, the latter is re-attached on every
|
|
967
|
+
// resolution and must never be silently frozen into an override.
|
|
968
|
+
const explicit = profile !== null
|
|
969
|
+
let builtin = false
|
|
970
|
+
if (profile === null && DEEPSEEK_OFFICIAL_PROVIDERS.includes(normalizeProvider(entry.providerId))) {
|
|
971
|
+
profile = builtinTemporalProfileFor(entry.modelId)
|
|
972
|
+
builtin = profile !== null
|
|
973
|
+
}
|
|
974
|
+
if (profile === null) return null
|
|
975
|
+
const providerId = normalizeProvider(entry.providerId)
|
|
976
|
+
const identityProvider = normalizeProvider(identity && identity.provider)
|
|
977
|
+
// Safety boundary: the peak/off-peak table applies only when the DSH route
|
|
978
|
+
// is the first-party provider, or when the user explicitly mapped this route
|
|
979
|
+
// to the DeepSeek official entry. OpenRouter and other resellers never get
|
|
980
|
+
// the DeepSeek temporal band automatically.
|
|
981
|
+
const route = DEEPSEEK_OFFICIAL_PROVIDERS.includes(providerId) && DEEPSEEK_OFFICIAL_PROVIDERS.includes(identityProvider) ? 'official' : mapped !== undefined && mapped !== null ? 'mapped' : 'other'
|
|
982
|
+
return {
|
|
983
|
+
route,
|
|
984
|
+
explicit,
|
|
985
|
+
builtin,
|
|
986
|
+
profile: {
|
|
987
|
+
policies: profile.policies.map((policy) => ({ ...policy, policyHash: temporalPolicyHash(policy) })),
|
|
988
|
+
},
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function resolvePricing(identity, rawState) {
|
|
993
|
+
const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
|
|
994
|
+
const identityKey = identityKeyOf(identity)
|
|
995
|
+
const exactMapped = identityKey === '' ? undefined : state.mappings.find((mapping) => mapping.identityKey !== '' && mapping.identityKey === identityKey && mappingMatches(mapping, identity))
|
|
996
|
+
const mapped = exactMapped || state.mappings.find((mapping) => mapping.identityKey === '' && mappingMatches(mapping, identity))
|
|
997
|
+
const mappedEntry = mapped === undefined ? null : findMappedEntry(mapped, state)
|
|
998
|
+
if (mapped !== undefined && mappedEntry === null && (mapped.catalogModelId !== '' || mapped.catalogProviderId !== '')) {
|
|
999
|
+
return { status: 'unpriced', reason: 'mapping-target-not-found', pricingModel: mapped.catalogModelId || null, providerId: mapped.catalogProviderId || null }
|
|
1000
|
+
}
|
|
1001
|
+
const models = mappedEntry === null ? identityModels(identity) : [mappedEntry.modelId]
|
|
1002
|
+
const pool = state._entries || state.overrides.concat(state.catalogEntries)
|
|
1003
|
+
const exactIndex = state._exactIndex || new Map()
|
|
1004
|
+
const officialProviders = new Set(models.flatMap((model) => Array.from(officialProviderIds(model))))
|
|
1005
|
+
const choose = (entries, model, candidate, exact) => entries.filter((entry) => {
|
|
1006
|
+
if (mappedEntry !== null && entry !== mappedEntry) return false
|
|
1007
|
+
if (entry.currency !== 'USD') return false
|
|
1008
|
+
if (entry.source === 'manual') return true
|
|
1009
|
+
return officialProviders.has(normalizeProvider(entry.providerId))
|
|
1010
|
+
}).map((entry) => ({ entry, model, candidate, exact }))
|
|
1011
|
+
for (const model of models) {
|
|
1012
|
+
const manualMatches = []
|
|
1013
|
+
const officialMatches = []
|
|
1014
|
+
for (const [index, id] of modelPricingCandidates(model).entries()) {
|
|
1015
|
+
const exactMatches = choose(exactIndex.get(id) || [], model, id, true)
|
|
1016
|
+
manualMatches.push(...exactMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 })))
|
|
1017
|
+
officialMatches.push(...exactMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 })))
|
|
1018
|
+
if (shouldTryPrefix(id)) {
|
|
1019
|
+
const prefixMatches = choose(pool.filter((entry) => entry.modelId.startsWith(id + '-')), model, id, false)
|
|
1020
|
+
manualMatches.push(...prefixMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
|
|
1021
|
+
officialMatches.push(...prefixMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const matches = manualMatches.length > 0 ? manualMatches : officialMatches
|
|
1025
|
+
if (matches.length === 0) continue
|
|
1026
|
+
matches.sort((a, b) => a.rank - b.rank || a.entry.modelId.length - b.entry.modelId.length)
|
|
1027
|
+
const best = matches[0]
|
|
1028
|
+
const sameRank = matches.filter((match) => match.rank === best.rank)
|
|
1029
|
+
const unique = []
|
|
1030
|
+
for (const match of sameRank) if (!unique.some((entry) => sameRates(entry, match.entry))) unique.push(match.entry)
|
|
1031
|
+
if (unique.length > 1) return { status: 'ambiguous', reason: 'multiple-official-prices', pricingModel: best.entry.modelId, providerId: null, candidates: unique.map((entry) => ({ providerId: entry.providerId, modelId: entry.modelId })) }
|
|
1032
|
+
const entry = unique[0] || best.entry
|
|
1033
|
+
const tiered = entry.tiered === true
|
|
1034
|
+
const tiers = Array.isArray(entry.tiers) ? entry.tiers.map((tier) => ({ ...tier })) : []
|
|
1035
|
+
const tieredSupported = !tiered || (entry.tieredInvalid !== true && tiers.length > 0)
|
|
1036
|
+
return {
|
|
1037
|
+
status: tieredSupported ? 'priced' : 'unsupported',
|
|
1038
|
+
reason: tieredSupported ? '' : 'tiered-pricing-not-modeled',
|
|
1039
|
+
pricingModel: entry.modelId,
|
|
1040
|
+
providerId: entry.providerId || null,
|
|
1041
|
+
providerName: entry.providerName || null,
|
|
1042
|
+
displayName: entry.displayName,
|
|
1043
|
+
currency: entry.currency,
|
|
1044
|
+
rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
|
|
1045
|
+
source: entry.source,
|
|
1046
|
+
tiered,
|
|
1047
|
+
...(tiered ? { tiers, tieredInvalid: entry.tieredInvalid === true } : {}),
|
|
1048
|
+
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
1049
|
+
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
1050
|
+
multiplier: mapped ? mapped.multiplier : '1',
|
|
1051
|
+
...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route, temporalExplicit: temporal.explicit === true, temporalBuiltin: temporal.builtin === true } })(),
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const model = models[0] || null
|
|
1055
|
+
if (officialProviders.size === 0) return { status: 'unsupported', reason: 'official-provider-unknown', pricingModel: model, providerId: null }
|
|
1056
|
+
return { status: 'unpriced', reason: 'official-price-not-found', pricingModel: model, providerId: null }
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function tokenCount(value) {
|
|
1060
|
+
const parsed = finiteNumber(value)
|
|
1061
|
+
if (parsed === null || parsed <= 0) return 0n
|
|
1062
|
+
return BigInt(Math.trunc(parsed))
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function costPerMillion(tokens, rate) {
|
|
1066
|
+
const tokenValue = tokenCount(tokens)
|
|
1067
|
+
const rateParts = decimalParts(rate) || { digits: 0n, scale: 0 }
|
|
1068
|
+
const numerator = tokenValue * rateParts.digits
|
|
1069
|
+
const scale = rateParts.scale + 6
|
|
1070
|
+
return decimalText(numerator.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function contextTokenCount(input, cacheRead, cacheWrite, inputSemantics) {
|
|
1074
|
+
const values = inputSemantics === 'total' ? [input] : inputSemantics === 'legacy' ? [input, cacheWrite] : [input, cacheRead, cacheWrite]
|
|
1075
|
+
let total = 0
|
|
1076
|
+
for (const value of values) {
|
|
1077
|
+
const tokens = Math.max(0, Math.trunc(value))
|
|
1078
|
+
if (!Number.isFinite(tokens) || total > Number.MAX_SAFE_INTEGER - tokens) return Number.MAX_SAFE_INTEGER
|
|
1079
|
+
total += tokens
|
|
1080
|
+
}
|
|
1081
|
+
return total
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function validContextTier(tier, previousSize = 0) {
|
|
1085
|
+
if (!isRecord(tier) || tier.type !== 'context' || !Number.isSafeInteger(tier.size) || tier.size <= previousSize || tier.size > 1000000000) return false
|
|
1086
|
+
return RATE_KEYS.every((key) => decimalText(tier[key]) !== null)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function validContextTierSchedule(tiers) {
|
|
1090
|
+
if (!Array.isArray(tiers) || tiers.length === 0) return false
|
|
1091
|
+
let previousSize = 0
|
|
1092
|
+
for (const tier of tiers) {
|
|
1093
|
+
if (!validContextTier(tier, previousSize)) return false
|
|
1094
|
+
previousSize = tier.size
|
|
1095
|
+
}
|
|
1096
|
+
return true
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function selectContextTier(tiers, contextTokens) {
|
|
1100
|
+
let selected = null
|
|
1101
|
+
for (const tier of tiers) {
|
|
1102
|
+
// models.dev defines size as the point where the next band starts.
|
|
1103
|
+
if (contextTokens > tier.size) selected = tier
|
|
1104
|
+
else break
|
|
1105
|
+
}
|
|
1106
|
+
return selected
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource = 'usage-event') {
|
|
1110
|
+
const input = finiteNumber(values && values.input) || 0
|
|
1111
|
+
const output = finiteNumber(values && values.output) || 0
|
|
1112
|
+
const cacheRead = finiteNumber(values && values.cacheRead) || 0
|
|
1113
|
+
const cacheWrite = finiteNumber(values && values.cacheWrite) || 0
|
|
1114
|
+
const inputSemantics = INPUT_SEMANTICS.includes(resolved && resolved.inputTokenSemantics) ? resolved.inputTokenSemantics : 'fresh'
|
|
1115
|
+
const billableInput = inputSemantics === 'total' ? Math.max(0, input - cacheRead - cacheWrite) : inputSemantics === 'legacy' ? Math.max(0, input - cacheRead) : input
|
|
1116
|
+
const billableOutput = output
|
|
1117
|
+
const tiered = resolved && resolved.tiered === true
|
|
1118
|
+
const tiers = tiered && Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
1119
|
+
const tierScheduleValid = !tiered || validContextTierSchedule(tiers)
|
|
1120
|
+
const plan = temporalPlanFor(resolved, pricingAtMs)
|
|
1121
|
+
const pricingAt = Number.isFinite(pricingAtMs) && pricingAtMs >= 0 ? Math.trunc(pricingAtMs) : null
|
|
1122
|
+
const timeSource = TEMPORAL_TIME_SOURCES.includes(pricingTimeSource) ? pricingTimeSource : 'usage-event'
|
|
1123
|
+
let status = tierScheduleValid && resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : tiered ? 'unsupported' : 'unpriced'
|
|
1124
|
+
let reason = tiered && !tierScheduleValid ? 'tiered-pricing-not-modeled' : resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found'
|
|
1125
|
+
if (status === 'priced') {
|
|
1126
|
+
if (plan.status === 'config-invalid') {
|
|
1127
|
+
// The user submitted a temporal config that failed validation; never
|
|
1128
|
+
// silently price with the built-in profile or the static fallback.
|
|
1129
|
+
status = 'unsupported'
|
|
1130
|
+
reason = 'temporal-config-invalid'
|
|
1131
|
+
} else if (tiered && (plan.status === 'applied' || plan.status === 'history-gap' || plan.status === 'time-missing')) {
|
|
1132
|
+
// Band plans and context tiers are two independent pricing axes; a model
|
|
1133
|
+
// carrying both is not modeled and fails closed instead of guessing.
|
|
1134
|
+
status = 'unsupported'
|
|
1135
|
+
reason = 'temporal-tiered-unsupported'
|
|
1136
|
+
} else if (plan.status === 'history-gap') {
|
|
1137
|
+
// A profile exists but the usage instant falls outside its effective
|
|
1138
|
+
// window: never discount into an unverifiable period.
|
|
1139
|
+
status = 'unsupported'
|
|
1140
|
+
reason = 'temporal-price-history-unavailable'
|
|
1141
|
+
} else if (plan.status === 'time-missing') {
|
|
1142
|
+
status = 'unsupported'
|
|
1143
|
+
reason = 'temporal-time-unavailable'
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const base = {
|
|
1147
|
+
schemaVersion: COST_SCHEMA_VERSION,
|
|
1148
|
+
pricingMode: 'official-model',
|
|
1149
|
+
status,
|
|
1150
|
+
currency: resolved && resolved.currency ? resolved.currency : 'USD',
|
|
1151
|
+
source: resolved && resolved.source ? resolved.source : 'none',
|
|
1152
|
+
pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
|
|
1153
|
+
providerId: resolved && resolved.providerId ? resolved.providerId : null,
|
|
1154
|
+
inputTokenSemantics: inputSemantics,
|
|
1155
|
+
multiplier: decimalText(resolved && resolved.multiplier !== undefined ? resolved.multiplier : '1') || '1',
|
|
1156
|
+
billableInputTokens: Math.trunc(billableInput),
|
|
1157
|
+
billableOutputTokens: Math.trunc(billableOutput),
|
|
1158
|
+
pricingAt,
|
|
1159
|
+
pricingTimeSource: timeSource,
|
|
1160
|
+
pricingBand: plan.band,
|
|
1161
|
+
pricingHoliday: plan.holiday === true,
|
|
1162
|
+
pricingTimezone: plan.timezone,
|
|
1163
|
+
pricingPolicyId: plan.policyId,
|
|
1164
|
+
pricingPolicyHash: plan.policyHash,
|
|
1165
|
+
temporalApplicable: plan.status === 'applied',
|
|
1166
|
+
temporalExemptReason: plan.exemptReason,
|
|
1167
|
+
rates: resolved && resolved.rates ? { ...resolved.rates } : { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
1168
|
+
breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
1169
|
+
baseTotal: '0',
|
|
1170
|
+
total: '0',
|
|
1171
|
+
reason,
|
|
1172
|
+
tiered,
|
|
1173
|
+
reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
|
|
1174
|
+
}
|
|
1175
|
+
if (base.status !== 'priced') return base
|
|
1176
|
+
if (tiered) {
|
|
1177
|
+
const contextTokens = contextTokenCount(input, cacheRead, cacheWrite, inputSemantics)
|
|
1178
|
+
const selectedTier = selectContextTier(tiers, contextTokens)
|
|
1179
|
+
const selectedRates = selectedTier || base.rates
|
|
1180
|
+
base.rates = { input: selectedRates.input, output: selectedRates.output, cacheRead: selectedRates.cacheRead, cacheWrite: selectedRates.cacheWrite }
|
|
1181
|
+
base.contextTokens = contextTokens
|
|
1182
|
+
base.selectedTier = { type: 'context', size: selectedTier ? selectedTier.size : 0 }
|
|
1183
|
+
} else if (plan.status === 'applied' && plan.ruleRates !== null && typeof plan.ruleRates === 'object') {
|
|
1184
|
+
// Peak windows carry their own rates; off-peak plans without an explicit
|
|
1185
|
+
// default inherit the live entry rates (catalog updates flow through).
|
|
1186
|
+
base.rates = { input: plan.ruleRates.input, output: plan.ruleRates.output, cacheRead: plan.ruleRates.cacheRead, cacheWrite: plan.ruleRates.cacheWrite }
|
|
1187
|
+
}
|
|
1188
|
+
base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
|
|
1189
|
+
base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
|
|
1190
|
+
base.breakdown.cacheRead = costPerMillion(cacheRead, base.rates.cacheRead)
|
|
1191
|
+
base.breakdown.cacheWrite = costPerMillion(cacheWrite, base.rates.cacheWrite)
|
|
1192
|
+
base.baseTotal = decimalAdd(decimalAdd(base.breakdown.input, base.breakdown.output), decimalAdd(base.breakdown.cacheRead, base.breakdown.cacheWrite))
|
|
1193
|
+
base.total = decimalMultiply(base.baseTotal, base.multiplier)
|
|
1194
|
+
return base
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function emptyCostAggregate(currency = 'USD') {
|
|
1198
|
+
return { currency, input: '0', output: '0', cacheRead: '0', cacheWrite: '0', baseTotal: '0', total: '0', pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function addCostAggregate(target, cost) {
|
|
1202
|
+
if (isCostAccumulator(target)) return addCostAccumulator(target, cost)
|
|
1203
|
+
const value = isRecord(cost) ? cost : {}
|
|
1204
|
+
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
1205
|
+
if (status === 'priced') {
|
|
1206
|
+
target.input = decimalAdd(target.input, value.breakdown && value.breakdown.input)
|
|
1207
|
+
target.output = decimalAdd(target.output, value.breakdown && value.breakdown.output)
|
|
1208
|
+
target.cacheRead = decimalAdd(target.cacheRead, value.breakdown && value.breakdown.cacheRead)
|
|
1209
|
+
target.cacheWrite = decimalAdd(target.cacheWrite, value.breakdown && value.breakdown.cacheWrite)
|
|
1210
|
+
target.baseTotal = decimalAdd(target.baseTotal, value.baseTotal)
|
|
1211
|
+
target.total = decimalAdd(target.total, value.total)
|
|
1212
|
+
target.pricedCalls += 1
|
|
1213
|
+
} else if (status === 'ambiguous') target.ambiguousCalls += 1
|
|
1214
|
+
else if (status === 'unsupported') target.unsupportedCalls += 1
|
|
1215
|
+
else target.unpricedCalls += 1
|
|
1216
|
+
return target
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function serializeCostAggregate(cost) {
|
|
1220
|
+
const value = cost || emptyCostAggregate()
|
|
1221
|
+
const accumulator = isCostAccumulator(value)
|
|
1222
|
+
const text = (field) => accumulator ? decimalAccumulatorText(value[field]) : decimalText(value[field]) || '0'
|
|
1223
|
+
return {
|
|
1224
|
+
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
1225
|
+
input: text('input'),
|
|
1226
|
+
output: text('output'),
|
|
1227
|
+
cacheRead: text('cacheRead'),
|
|
1228
|
+
cacheWrite: text('cacheWrite'),
|
|
1229
|
+
baseTotal: text('baseTotal'),
|
|
1230
|
+
total: text('total'),
|
|
1231
|
+
pricedCalls: Number.isFinite(value.pricedCalls) ? value.pricedCalls : 0,
|
|
1232
|
+
unpricedCalls: Number.isFinite(value.unpricedCalls) ? value.unpricedCalls : 0,
|
|
1233
|
+
ambiguousCalls: Number.isFinite(value.ambiguousCalls) ? value.ambiguousCalls : 0,
|
|
1234
|
+
unsupportedCalls: Number.isFinite(value.unsupportedCalls) ? value.unsupportedCalls : 0,
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function normalizeCostSnapshot(raw) {
|
|
1239
|
+
if (!isRecord(raw) || !COST_STATUSES.includes(raw.status)) return null
|
|
1240
|
+
const rates = isRecord(raw.rates) ? raw.rates : {}
|
|
1241
|
+
const breakdown = isRecord(raw.breakdown) ? raw.breakdown : {}
|
|
1242
|
+
const fields = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
1243
|
+
const normalizedRates = {}
|
|
1244
|
+
const normalizedBreakdown = {}
|
|
1245
|
+
// Each field is parsed once and formatted from the parsed parts. The accumulation
|
|
1246
|
+
// parts are kept too: the fold needs exactly these six numbers and would
|
|
1247
|
+
// otherwise parse the normalized strings a second time.
|
|
1248
|
+
const parts = {}
|
|
1249
|
+
for (const field of fields) {
|
|
1250
|
+
const rateParts = decimalParts(rates[field])
|
|
1251
|
+
const costParts = decimalParts(breakdown[field])
|
|
1252
|
+
const rate = rateParts === null ? null : decimalAccumulatorText(rateParts)
|
|
1253
|
+
const cost = costParts === null ? null : decimalAccumulatorText(costParts)
|
|
1254
|
+
if (rate === null || cost === null) return null
|
|
1255
|
+
normalizedRates[field] = rate
|
|
1256
|
+
normalizedBreakdown[field] = cost
|
|
1257
|
+
parts[field] = costParts
|
|
1258
|
+
}
|
|
1259
|
+
const baseParts = decimalParts(raw.baseTotal)
|
|
1260
|
+
const totalParts = decimalParts(raw.total)
|
|
1261
|
+
const multiplierParts = decimalParts(raw.multiplier)
|
|
1262
|
+
const baseTotal = baseParts === null ? null : decimalAccumulatorText(baseParts)
|
|
1263
|
+
const total = totalParts === null ? null : decimalAccumulatorText(totalParts)
|
|
1264
|
+
const multiplier = multiplierParts === null ? null : decimalAccumulatorText(multiplierParts)
|
|
1265
|
+
if (baseTotal === null || total === null || multiplier === null) return null
|
|
1266
|
+
parts.baseTotal = baseParts
|
|
1267
|
+
parts.total = totalParts
|
|
1268
|
+
let contextTokens = null
|
|
1269
|
+
if (raw.contextTokens !== undefined) {
|
|
1270
|
+
const parsed = finiteNumber(raw.contextTokens)
|
|
1271
|
+
if (parsed === null || parsed < 0 || !Number.isSafeInteger(parsed)) return null
|
|
1272
|
+
contextTokens = parsed
|
|
1273
|
+
}
|
|
1274
|
+
let selectedTier = null
|
|
1275
|
+
if (raw.selectedTier !== undefined && raw.selectedTier !== null) {
|
|
1276
|
+
if (!isRecord(raw.selectedTier) || raw.selectedTier.type !== 'context' || !Number.isSafeInteger(raw.selectedTier.size) || raw.selectedTier.size < 0 || raw.selectedTier.size > 1000000000) return null
|
|
1277
|
+
selectedTier = { type: 'context', size: raw.selectedTier.size }
|
|
1278
|
+
}
|
|
1279
|
+
const isV2 = raw.schemaVersion === COST_SCHEMA_VERSION
|
|
1280
|
+
const legacy = !isV2
|
|
1281
|
+
let pricingAt = null
|
|
1282
|
+
if (raw.pricingAt !== undefined && raw.pricingAt !== null) {
|
|
1283
|
+
if (!Number.isFinite(raw.pricingAt) || raw.pricingAt < 0 || !Number.isSafeInteger(raw.pricingAt)) return null
|
|
1284
|
+
pricingAt = raw.pricingAt
|
|
1285
|
+
}
|
|
1286
|
+
if (raw.pricingTimeSource !== undefined && raw.pricingTimeSource !== null && raw.pricingTimeSource !== 'legacy-unknown' && !TEMPORAL_TIME_SOURCES.includes(raw.pricingTimeSource)) return null
|
|
1287
|
+
if (raw.pricingBand !== undefined && raw.pricingBand !== null && raw.pricingBand !== 'peak' && raw.pricingBand !== 'off-peak') return null
|
|
1288
|
+
if (raw.pricingTimezone !== undefined && raw.pricingTimezone !== null && raw.pricingTimezone !== 'UTC') return null
|
|
1289
|
+
const pricingTimeSource = TEMPORAL_TIME_SOURCES.includes(raw.pricingTimeSource) ? raw.pricingTimeSource : legacy ? 'legacy-unknown' : null
|
|
1290
|
+
const pricingBand = raw.pricingBand === 'peak' || raw.pricingBand === 'off-peak' ? raw.pricingBand : null
|
|
1291
|
+
const pricingTimezone = raw.pricingTimezone === 'UTC' ? 'UTC' : null
|
|
1292
|
+
const pricingPolicyId = typeof raw.pricingPolicyId === 'string' && raw.pricingPolicyId !== '' ? raw.pricingPolicyId.slice(0, 128) : null
|
|
1293
|
+
const pricingPolicyHash = typeof raw.pricingPolicyHash === 'string' && raw.pricingPolicyHash !== '' ? raw.pricingPolicyHash.slice(0, 128) : null
|
|
1294
|
+
const normalizedCost = {
|
|
1295
|
+
schemaVersion: COST_SCHEMA_VERSION,
|
|
1296
|
+
pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
|
|
1297
|
+
status: raw.status,
|
|
1298
|
+
currency: typeof raw.currency === 'string' ? raw.currency : 'USD',
|
|
1299
|
+
source: typeof raw.source === 'string' ? raw.source : 'none',
|
|
1300
|
+
pricingModel: typeof raw.pricingModel === 'string' ? raw.pricingModel : null,
|
|
1301
|
+
providerId: typeof raw.providerId === 'string' ? raw.providerId : null,
|
|
1302
|
+
inputTokenSemantics: INPUT_SEMANTICS.includes(raw.inputTokenSemantics) ? raw.inputTokenSemantics : 'fresh',
|
|
1303
|
+
multiplier,
|
|
1304
|
+
billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
|
|
1305
|
+
billableOutputTokens: Number.isFinite(raw.billableOutputTokens) ? Math.max(0, Math.trunc(raw.billableOutputTokens)) : 0,
|
|
1306
|
+
pricingAt,
|
|
1307
|
+
pricingTimeSource,
|
|
1308
|
+
pricingBand,
|
|
1309
|
+
pricingHoliday: raw.pricingHoliday === true,
|
|
1310
|
+
pricingTimezone,
|
|
1311
|
+
pricingPolicyId,
|
|
1312
|
+
pricingPolicyHash,
|
|
1313
|
+
temporalApplicable: raw.temporalApplicable === true,
|
|
1314
|
+
temporalExemptReason: raw.temporalExemptReason === null || raw.temporalExemptReason === undefined ? null : typeof raw.temporalExemptReason === 'string' ? raw.temporalExemptReason.slice(0, 64) : null,
|
|
1315
|
+
...(contextTokens === null ? {} : { contextTokens }),
|
|
1316
|
+
...(selectedTier === null ? {} : { selectedTier }),
|
|
1317
|
+
rates: normalizedRates,
|
|
1318
|
+
breakdown: normalizedBreakdown,
|
|
1319
|
+
baseTotal,
|
|
1320
|
+
total,
|
|
1321
|
+
reason: typeof raw.reason === 'string' ? raw.reason.slice(0, 200) : '',
|
|
1322
|
+
tiered: raw.tiered === true,
|
|
1323
|
+
reasoningRateAvailable: raw.reasoningRateAvailable === true,
|
|
1324
|
+
}
|
|
1325
|
+
// Hand the parsed parts to the accumulator cache: the fold reads this object and
|
|
1326
|
+
// would otherwise re-parse all six fields from the strings above.
|
|
1327
|
+
COST_PARTS_CACHE.set(normalizedCost, {
|
|
1328
|
+
currency: normalizedCost.currency,
|
|
1329
|
+
status: normalizedCost.status,
|
|
1330
|
+
input: parts.input,
|
|
1331
|
+
output: parts.output,
|
|
1332
|
+
cacheRead: parts.cacheRead,
|
|
1333
|
+
cacheWrite: parts.cacheWrite,
|
|
1334
|
+
baseTotal: parts.baseTotal,
|
|
1335
|
+
total: parts.total,
|
|
1336
|
+
})
|
|
1337
|
+
return normalizedCost
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {}) {
|
|
1341
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
1342
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
|
|
1343
|
+
const url = typeof options.url === 'string' && options.url !== '' ? options.url : MODEL_CATALOG_URL
|
|
1344
|
+
let controller = null
|
|
1345
|
+
let timer = null
|
|
1346
|
+
try {
|
|
1347
|
+
if (typeof AbortController === 'function') {
|
|
1348
|
+
controller = new AbortController()
|
|
1349
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
1350
|
+
}
|
|
1351
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
1352
|
+
if (!response || response.ok !== true) return { ok: false, error: 'http-' + String(response && response.status || 0) }
|
|
1353
|
+
const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
|
|
1354
|
+
if (Number.isFinite(length) && length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
|
|
1355
|
+
const text = await response.text()
|
|
1356
|
+
if (new TextEncoder().encode(text).length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
|
|
1357
|
+
let parsed
|
|
1358
|
+
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { return { ok: false, error: 'catalog-invalid-json' } }
|
|
1359
|
+
return parseModelsDevCatalog(parsed, Date.now())
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
return { ok: false, error: 'catalog-fetch-failed' }
|
|
1362
|
+
} finally {
|
|
1363
|
+
if (timer !== null) clearTimeout(timer)
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** Turn one holiday-cn dataset into the explicit off-day list the policy stores. */
|
|
1368
|
+
export function parseHolidayCalendar(raw, year) {
|
|
1369
|
+
if (!isRecord(raw)) return { ok: false, error: 'holiday-calendar-not-object' }
|
|
1370
|
+
if (raw.year !== undefined && raw.year !== null && Number(raw.year) !== Number(year)) return { ok: false, error: 'holiday-calendar-year-mismatch' }
|
|
1371
|
+
if (!Array.isArray(raw.days)) return { ok: false, error: 'holiday-calendar-days-missing' }
|
|
1372
|
+
const seen = new Set()
|
|
1373
|
+
const dates = []
|
|
1374
|
+
for (const day of raw.days) {
|
|
1375
|
+
if (!isRecord(day) || day.isOffDay !== true) continue
|
|
1376
|
+
const text = normalizeHolidayDate(day.date)
|
|
1377
|
+
if (text === null) return { ok: false, error: 'holiday-calendar-date-invalid' }
|
|
1378
|
+
if (seen.has(text)) continue
|
|
1379
|
+
seen.add(text)
|
|
1380
|
+
dates.push(text)
|
|
1381
|
+
}
|
|
1382
|
+
if (dates.length === 0) return { ok: false, error: 'holiday-calendar-empty' }
|
|
1383
|
+
if (dates.length > MAX_TEMPORAL_HOLIDAYS) return { ok: false, error: 'holiday-calendar-too-large' }
|
|
1384
|
+
dates.sort()
|
|
1385
|
+
const papers = Array.isArray(raw.papers) ? raw.papers.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim().slice(0, 512)) : []
|
|
1386
|
+
return { ok: true, calendar: { year: Number(year), dates, paperUrl: papers.length > 0 ? papers[0] : '', fetchedAt: 0 } }
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Fetch one year of the Chinese holiday arrangement. The result is never applied
|
|
1391
|
+
* automatically: the caller freezes the returned dates into a pricing policy, so
|
|
1392
|
+
* later ledger reconciliation stays reproducible even if the upstream changes.
|
|
1393
|
+
*/
|
|
1394
|
+
export async function fetchHolidayCalendar(year, fetchImpl = globalThis.fetch, options = {}) {
|
|
1395
|
+
const value = Number(year)
|
|
1396
|
+
if (!Number.isSafeInteger(value) || value < 2000 || value > 2100) return { ok: false, error: 'holiday-year-invalid' }
|
|
1397
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
1398
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
|
|
1399
|
+
const bases = Array.isArray(options.bases) && options.bases.length > 0 ? options.bases : HOLIDAY_CALENDAR_BASES
|
|
1400
|
+
let lastError = 'holiday-calendar-unavailable'
|
|
1401
|
+
for (const base of bases) {
|
|
1402
|
+
const url = String(base) + value + '.json'
|
|
1403
|
+
let controller = null
|
|
1404
|
+
let timer = null
|
|
1405
|
+
try {
|
|
1406
|
+
if (typeof AbortController === 'function') {
|
|
1407
|
+
controller = new AbortController()
|
|
1408
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
1409
|
+
}
|
|
1410
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
1411
|
+
if (!response || response.ok !== true) { lastError = 'http-' + String((response && response.status) || 0); continue }
|
|
1412
|
+
const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
|
|
1413
|
+
if (Number.isFinite(length) && length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
|
|
1414
|
+
const text = await response.text()
|
|
1415
|
+
if (new TextEncoder().encode(text).length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
|
|
1416
|
+
let parsed = null
|
|
1417
|
+
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { lastError = 'holiday-calendar-invalid-json'; continue }
|
|
1418
|
+
const result = parseHolidayCalendar(parsed, value)
|
|
1419
|
+
if (!result.ok) { lastError = result.error; continue }
|
|
1420
|
+
return { ok: true, calendar: { ...result.calendar, sourceUrl: url, fetchedAt: Date.now() } }
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
lastError = 'holiday-calendar-fetch-failed'
|
|
1423
|
+
} finally {
|
|
1424
|
+
if (timer !== null) clearTimeout(timer)
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return { ok: false, error: lastError }
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export {
|
|
1431
|
+
COST_SCHEMA_VERSION,
|
|
1432
|
+
COST_STATUSES,
|
|
1433
|
+
DEFAULT_SYNC_INTERVAL_MS,
|
|
1434
|
+
INPUT_SEMANTICS,
|
|
1435
|
+
MODEL_CATALOG_URL,
|
|
1436
|
+
PRICING_SCHEMA_VERSION,
|
|
1437
|
+
RATE_KEYS,
|
|
1438
|
+
addCostAggregate,
|
|
1439
|
+
addCostAccumulator,
|
|
1440
|
+
calculateCost,
|
|
1441
|
+
createCostAccumulator,
|
|
1442
|
+
createEmptyPricingState,
|
|
1443
|
+
decimalAdd,
|
|
1444
|
+
decimalMultiply,
|
|
1445
|
+
decimalSubtract,
|
|
1446
|
+
decimalText,
|
|
1447
|
+
emptyCostAggregate,
|
|
1448
|
+
fetchModelsDevCatalog,
|
|
1449
|
+
isCostAccumulator,
|
|
1450
|
+
modelPricingCandidates,
|
|
1451
|
+
officialProviderIds,
|
|
1452
|
+
normalizeCostSnapshot,
|
|
1453
|
+
normalizeModelId,
|
|
1454
|
+
normalizePricingState,
|
|
1455
|
+
normalizeTemporalPricing,
|
|
1456
|
+
parseModelsDevCatalog,
|
|
1457
|
+
resolvePricing,
|
|
1458
|
+
serializeCostAggregate,
|
|
1459
|
+
serializePricingState,
|
|
1460
|
+
temporalBand,
|
|
1461
|
+
temporalPlanFor,
|
|
1462
|
+
temporalPolicyHash,
|
|
1463
|
+
}
|