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