dsh-all-usage 1.1.18 → 1.2.1

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