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