dsh-all-usage 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/plugin.js ADDED
@@ -0,0 +1,277 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { createAggregation } from './aggregation.js'
3
+ import { createBalance } from './balance.js'
4
+ import { registerRoutes } from './http.js'
5
+ import { createLedger } from './ledger.js'
6
+ import { createPricingRuntime } from './pricing-runtime.js'
7
+ import { createSessionSync } from './session-sync.js'
8
+ import { createCostAccumulator, createEmptyPricingState } from './pricing.js'
9
+
10
+ const name = 'dsh-all-usage'
11
+ const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage', 'webServer']
12
+
13
+ function apply(ctx) {
14
+ const services = {
15
+ credentials: ctx.get('credentials'),
16
+ settings: ctx.get('settings'),
17
+ storage: ctx.get('storage'),
18
+ webServer: ctx.get('webServer'),
19
+ sessionPersistence: ctx.get('sessionPersistence'),
20
+ }
21
+ const state = {
22
+ wsMeta: new Map(),
23
+ pathIndex: new Map(),
24
+ memberOf: new Map(),
25
+ byDay: new Map(),
26
+ byDayUtc: new Map(),
27
+ perWorkspace: new Map(),
28
+ perModel: new Map(),
29
+ usageByStep: new Map(),
30
+ turnRecords: new Map(),
31
+ usageByLocalDate: new Map(),
32
+ usageByUtcDate: new Map(),
33
+ sessionModel: new Map(),
34
+ sessionContextTimes: new Map(),
35
+ totals: { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: createCostAccumulator() },
36
+ sessionCount: new Set(),
37
+ sessionSeq: new Map(),
38
+ chains: new Map(),
39
+ liveResyncPending: new Set(),
40
+ liveResyncTimers: new Map(),
41
+ liveResyncAttempts: new Map(),
42
+ scan: { started: false, done: false, scanned: 0, total: 0, failed: 0 },
43
+ aliases: {},
44
+ kvUnit: null,
45
+ aliasWriteChain: Promise.resolve(),
46
+ aliasesReady: Promise.resolve(),
47
+ balanceCache: { fetchedAt: 0, payload: null },
48
+ requestToken: randomBytes(32).toString('base64url'),
49
+ instanceId: randomBytes(12).toString('base64url'),
50
+ statsRevision: 0,
51
+ dataRevision: 0,
52
+ metadataRevision: 0,
53
+ scanRevision: 0,
54
+ pricingRevision: 0,
55
+ statsUpdatedAt: Date.now(),
56
+ statsDirtyScheduled: false,
57
+ statsDirtyKinds: new Set(),
58
+ sync: {
59
+ lastStartedAt: 0,
60
+ lastCompletedAt: 0,
61
+ lastErrorAt: 0,
62
+ lastErrorCode: null,
63
+ persistenceSnapshotsAvailable: false,
64
+ sessionsTotal: 0,
65
+ sessionsRead: 0,
66
+ sessionsSkippedByRevision: 0,
67
+ sessionsRestoredFromLedger: 0,
68
+ sessionsFailed: 0,
69
+ },
70
+ ledgerRecords: new Map(),
71
+ queryCache: new Map(),
72
+ recordsQueryCache: new Map(),
73
+ snapshotCache: null,
74
+ ledgerRevision: Date.now(),
75
+ ledgerUnit: null,
76
+ ledgerReady: Promise.resolve(),
77
+ ledgerWriteChain: Promise.resolve(),
78
+ ledgerPending: new Map(),
79
+ ledgerWriteWaiters: new Map(),
80
+ ledgerWriteRunning: false,
81
+ ledgerWriteScheduled: false,
82
+ ledgerWriteTimer: null,
83
+ ledgerDirtySessions: new Set(),
84
+ ledgerDirtyEpochs: new Map(),
85
+ ledgerWriteFailedSessions: new Set(),
86
+ pricingState: createEmptyPricingState(),
87
+ pricingResolutionCache: new Map(),
88
+ pricingUnit: null,
89
+ pricingReady: Promise.resolve(),
90
+ pricingWriteChain: Promise.resolve(),
91
+ pricingSyncInFlight: false,
92
+ pricingSyncTimer: null,
93
+ aggregationGeneration: 0,
94
+ disposed: false,
95
+ baselineRetryDelay: 1000,
96
+ baselineRetryScheduled: false,
97
+ baselineFallbackTimer: null,
98
+ knownSessionIds: new Set(),
99
+ reconcileHintScheduled: false,
100
+ reconcilePending: false,
101
+ reconcileInFlight: false,
102
+ reconcileTimer: null,
103
+ }
104
+
105
+ const host = { ctx, services, state, webServer: services.webServer, aggregation: null, ledger: null, pricing: null, balance: null, sessionSync: null, aliases: null }
106
+ function commitPendingStats() {
107
+ if (!state.statsDirtyScheduled) return
108
+ state.statsDirtyScheduled = false
109
+ const kinds = state.statsDirtyKinds
110
+ state.statsDirtyKinds = new Set()
111
+ if (state.disposed) return
112
+ state.statsRevision += 1
113
+ if (kinds.has('data')) state.dataRevision += 1
114
+ if (kinds.has('metadata')) state.metadataRevision += 1
115
+ if (kinds.has('scan')) state.scanRevision += 1
116
+ if (kinds.has('pricing')) state.pricingRevision += 1
117
+ state.statsUpdatedAt = Date.now()
118
+ }
119
+ function markStatsChanged(kind = 'data') {
120
+ const kinds = Array.isArray(kind) ? kind : [kind]
121
+ let recordsChanged = false
122
+ for (const value of kinds) {
123
+ if (value !== 'data' && value !== 'metadata' && value !== 'scan' && value !== 'pricing') continue
124
+ state.statsDirtyKinds.add(value)
125
+ if (value === 'data' || value === 'pricing') recordsChanged = true
126
+ }
127
+ state.snapshotCache = null
128
+ if (recordsChanged) state.recordsQueryCache.clear()
129
+ if (state.disposed || state.statsDirtyScheduled) return
130
+ state.statsDirtyScheduled = true
131
+ if (typeof queueMicrotask === 'function') queueMicrotask(commitPendingStats)
132
+ else Promise.resolve().then(commitPendingStats)
133
+ }
134
+ function resetSyncState() {
135
+ state.sync.lastStartedAt = 0
136
+ state.sync.lastCompletedAt = 0
137
+ state.sync.lastErrorAt = 0
138
+ state.sync.lastErrorCode = null
139
+ state.sync.persistenceSnapshotsAvailable = false
140
+ state.sync.sessionsTotal = 0
141
+ state.sync.sessionsRead = 0
142
+ state.sync.sessionsSkippedByRevision = 0
143
+ state.sync.sessionsRestoredFromLedger = 0
144
+ state.sync.sessionsFailed = 0
145
+ }
146
+ function beginSync() {
147
+ resetSyncState()
148
+ state.sync.lastStartedAt = Date.now()
149
+ markStatsChanged('scan')
150
+ }
151
+ function noteSyncError(code) {
152
+ state.sync.lastErrorAt = Date.now()
153
+ state.sync.lastErrorCode = code
154
+ markStatsChanged('scan')
155
+ }
156
+ function syncSnapshot() {
157
+ return {
158
+ lastStartedAt: state.sync.lastStartedAt,
159
+ lastCompletedAt: state.sync.lastCompletedAt,
160
+ lastErrorAt: state.sync.lastErrorAt,
161
+ lastErrorCode: state.sync.lastErrorCode,
162
+ persistenceSnapshotsAvailable: state.sync.persistenceSnapshotsAvailable,
163
+ sessionsTotal: state.sync.sessionsTotal,
164
+ sessionsRead: state.sync.sessionsRead,
165
+ sessionsSkippedByRevision: state.sync.sessionsSkippedByRevision,
166
+ sessionsRestoredFromLedger: state.sync.sessionsRestoredFromLedger,
167
+ sessionsFailed: state.sync.sessionsFailed,
168
+ }
169
+ }
170
+ Object.assign(host, { commitPendingStats, markStatsChanged, resetSyncState, beginSync, noteSyncError, syncSnapshot })
171
+
172
+ host.pricingSnapshot = (...args) => host.pricing.pricingSnapshot(...args)
173
+ host.syncSnapshot = (...args) => syncSnapshot(...args)
174
+ host.aggregation = createAggregation(host)
175
+ host.ledger = createLedger(host)
176
+ host.pricing = createPricingRuntime(host)
177
+ host.balance = createBalance(host)
178
+ host.sessionSync = createSessionSync(host)
179
+
180
+ async function loadAliases() {
181
+ const storage = services.storage
182
+ if (storage === undefined || storage === null) return
183
+ try {
184
+ const backend = storage.backend.get('json')
185
+ if (backend === undefined || backend === null || backend.kv === undefined) return
186
+ const unit = await backend.kv.open({ name: 'all_usage_aliases', version: 0, tables: [], hasGlobal: true })
187
+ if (state.disposed) { await unit.close().catch(() => {}); return }
188
+ state.kvUnit = unit
189
+ const snap = await unit.loadAll()
190
+ const global = snap && snap.global
191
+ if (global !== null && global !== undefined && typeof global === 'object') {
192
+ let changed = false
193
+ for (const key of Object.keys(global)) {
194
+ const value = global[key]
195
+ const next = typeof value === 'string' ? value.trim() : ''
196
+ if (next !== '' && state.aliases[key] !== next) {
197
+ state.aliases[key] = next
198
+ changed = true
199
+ }
200
+ }
201
+ if (changed) markStatsChanged('metadata')
202
+ }
203
+ } catch (err) {
204
+ console.error('[all-usage] alias storage unavailable:', err)
205
+ }
206
+ }
207
+ function persistAliases() {
208
+ if (state.disposed) return state.aliasWriteChain
209
+ const snapshotAliases = {}
210
+ for (const key of Object.keys(state.aliases)) snapshotAliases[key] = state.aliases[key]
211
+ state.aliasWriteChain = state.aliasWriteChain.then(() => {
212
+ if (state.kvUnit === null || state.kvUnit === undefined) return undefined
213
+ return state.kvUnit.setGlobal(snapshotAliases).catch((err) => {
214
+ console.error('[all-usage] alias persist failed:', err)
215
+ })
216
+ })
217
+ return state.aliasWriteChain
218
+ }
219
+ function setAlias(wsId, raw) {
220
+ if (typeof wsId !== 'string' || wsId.length === 0 || wsId.length > 256) return { ok: false, message: 'invalid-workspace', aliases: Object.assign({}, state.aliases) }
221
+ if (typeof raw !== 'string') return { ok: false, message: 'invalid-alias', aliases: Object.assign({}, state.aliases) }
222
+ const alias = raw.trim().slice(0, 80)
223
+ if (!state.wsMeta.has(wsId)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, state.aliases) }
224
+ const previous = typeof state.aliases[wsId] === 'string' ? state.aliases[wsId] : ''
225
+ if (alias === previous) return { ok: true, aliases: Object.assign({}, state.aliases) }
226
+ if (alias === '') delete state.aliases[wsId]
227
+ else state.aliases[wsId] = alias
228
+ persistAliases()
229
+ markStatsChanged('metadata')
230
+ return { ok: true, aliases: Object.assign({}, state.aliases) }
231
+ }
232
+ host.aliases = { loadAliases, persistAliases, setAlias }
233
+
234
+ ctx.effect(() => async () => {
235
+ state.disposed = true
236
+ if (state.reconcileTimer !== null) {
237
+ clearTimeout(state.reconcileTimer)
238
+ state.reconcileTimer = null
239
+ }
240
+ if (state.baselineFallbackTimer !== null) {
241
+ clearTimeout(state.baselineFallbackTimer)
242
+ state.baselineFallbackTimer = null
243
+ }
244
+ if (state.pricingSyncTimer !== null) {
245
+ clearTimeout(state.pricingSyncTimer)
246
+ state.pricingSyncTimer = null
247
+ }
248
+ for (const timer of state.liveResyncTimers.values()) clearTimeout(timer)
249
+ state.liveResyncTimers.clear()
250
+ state.liveResyncAttempts.clear()
251
+ state.liveResyncPending.clear()
252
+ state.baselineRetryScheduled = false
253
+ if (state.ledgerWriteTimer !== null) {
254
+ clearTimeout(state.ledgerWriteTimer)
255
+ state.ledgerWriteTimer = null
256
+ }
257
+ state.ledgerWriteScheduled = false
258
+ state.chains.clear()
259
+ await Promise.all([state.aliasesReady, state.ledgerReady, state.pricingReady, state.aliasWriteChain, state.pricingWriteChain])
260
+ await host.ledger.drainLedgerWrites()
261
+ const units = [state.kvUnit, state.ledgerUnit, state.pricingUnit]
262
+ state.kvUnit = null
263
+ state.ledgerUnit = null
264
+ state.pricingUnit = null
265
+ await Promise.all(units.map((unit) => unit === null || unit === undefined ? undefined : unit.close().catch(() => {})))
266
+ })
267
+
268
+ registerRoutes(host)
269
+ state.ledgerReady = host.ledger.loadLedger()
270
+ state.pricingReady = host.pricing.loadPricing().then(() => { host.pricing.schedulePricingSync() })
271
+ void host.sessionSync.runBaseline()
272
+ host.sessionSync.scheduleReconcileTimer()
273
+ state.aliasesReady = loadAliases()
274
+ }
275
+
276
+ export { name, inject, apply }
277
+ export default { name, inject, apply }
@@ -0,0 +1,406 @@
1
+ import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
2
+ import { billingInstantOf } from './usage-core.js'
3
+
4
+ const LEDGER_VERSION = 3
5
+
6
+ export function createPricingRuntime(host) {
7
+ const { state, markStatsChanged } = host
8
+ const { storage } = host.services
9
+ const {
10
+ coerceIdentity,
11
+ dateKeys,
12
+ ensureDay,
13
+ ensureDayModel,
14
+ ensureDayWs,
15
+ ensureModel,
16
+ ensureWs,
17
+ addCostAggregateDirection,
18
+ adjustQueryCost,
19
+ resolveCurrentPricing,
20
+ } = host.aggregation
21
+ const persistLedgerRecord = (...args) => host.ledger.persistLedgerRecord(...args)
22
+ const drainLedgerWrites = (...args) => host.ledger.drainLedgerWrites(...args)
23
+ const nextLedgerRevision = (...args) => host.ledger.nextLedgerRevision(...args)
24
+
25
+ function adjustCostOnly(item, cost, direction) {
26
+ const identity = coerceIdentity(item.identity || item.modelId)
27
+ const dates = item && typeof item.date === 'string' && typeof item.dateUtc === 'string' ? { local: item.date, utc: item.dateUtc } : dateKeys(item.time)
28
+ const targets = []
29
+ const seen = new Set()
30
+ for (const target of [
31
+ ensureModel(identity).cost,
32
+ state.totals.cost,
33
+ ensureWs(item.wsId).cost,
34
+ ensureDay(state.byDay, dates.local).cost,
35
+ ensureDay(state.byDayUtc, dates.utc).cost,
36
+ ensureDayWs(ensureDay(state.byDay, dates.local), item.wsId).cost,
37
+ ensureDayWs(ensureDay(state.byDayUtc, dates.utc), item.wsId).cost,
38
+ ensureDayModel(ensureDay(state.byDay, dates.local), identity).cost,
39
+ ensureDayModel(ensureDay(state.byDayUtc, dates.utc), identity).cost,
40
+ ]) {
41
+ if (seen.has(target)) continue
42
+ seen.add(target)
43
+ targets.push(target)
44
+ }
45
+ for (const target of targets) addCostAggregateDirection(target, cost, direction)
46
+ }
47
+ function usedPricingModels(tierSchedules = null) {
48
+ const rows = []
49
+ const seen = new Set()
50
+ const scheduleIds = new Map()
51
+ const scheduleIdFor = (tiers) => {
52
+ if (!Array.isArray(tierSchedules) || tiers.length === 0) return null
53
+ const key = JSON.stringify(tiers)
54
+ const existing = scheduleIds.get(key)
55
+ if (existing !== undefined) return existing
56
+ const id = 'tier-' + tierSchedules.length
57
+ scheduleIds.set(key, id)
58
+ tierSchedules.push({ id, tiers: tiers.map((tier) => ({ ...tier })) })
59
+ return id
60
+ }
61
+ for (const item of state.usageByStep.values()) {
62
+ const identity = coerceIdentity(item.identity || item.modelId)
63
+ if (seen.has(identity.identityKey)) continue
64
+ seen.add(identity.identityKey)
65
+ const resolved = resolveCurrentPricing(identity)
66
+ const tiers = Array.isArray(resolved.tiers) ? resolved.tiers : []
67
+ const tierScheduleId = resolved.tiered === true ? scheduleIdFor(tiers) : null
68
+ rows.push({
69
+ identityKey: identity.identityKey,
70
+ provider: identity.provider,
71
+ requestedModel: identity.requestedModel,
72
+ actualModel: identity.actualModel,
73
+ model: identity.label,
74
+ status: resolved.status,
75
+ reason: resolved.reason || '',
76
+ pricingModel: resolved.pricingModel || null,
77
+ providerId: resolved.providerId || null,
78
+ source: resolved.source || 'none',
79
+ currency: resolved.currency || 'USD',
80
+ rates: resolved.rates || null,
81
+ tiered: resolved.tiered === true,
82
+ tierCount: tiers.length,
83
+ ...(tierScheduleId === null ? {} : { tierScheduleId }),
84
+ tieredInvalid: resolved.tieredInvalid === true,
85
+ inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
86
+ multiplier: resolved.multiplier || '1',
87
+ ...(resolved.temporalRoute !== undefined && resolved.temporalRoute !== null ? { temporalRoute: resolved.temporalRoute } : {}),
88
+ ...(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 } : {}),
89
+ })
90
+ if (rows.length >= 500) break
91
+ }
92
+ rows.sort((a, b) => String(a.model).localeCompare(String(b.model)))
93
+ return rows
94
+ }
95
+ function pricingModelSearch(query, limit = 20) {
96
+ const raw = typeof query === 'string' ? query.trim().toLowerCase() : ''
97
+ if (raw === '') return []
98
+ const normalized = raw.replace(/\s+/g, ' ')
99
+ const selected = new Map()
100
+ for (const entry of state.pricingState.catalogEntries) {
101
+ const official = officialProviderIds(entry.modelId)
102
+ if (!official.has(String(entry.providerId || '').toLowerCase())) continue
103
+ const modelId = entry.modelId.toLowerCase()
104
+ const displayName = String(entry.displayName || '').toLowerCase()
105
+ if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
106
+ const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
107
+ const previous = selected.get(entry.modelId)
108
+ if (previous === undefined || score < previous.score) selected.set(entry.modelId, { value: entry.modelId, label: entry.displayName, providerId: entry.providerId, tiered: entry.tiered === true, tierCount: Array.isArray(entry.tiers) ? entry.tiers.length : 0, score })
109
+ }
110
+ 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)
111
+ }
112
+ function pricingSnapshot(options = {}) {
113
+ const pricingStateSnapshot = state.pricingState
114
+ const detailed = options.detailed !== false
115
+ const tierSchedules = detailed ? [] : null
116
+ const snapshot = {
117
+ schemaVersion: pricingStateSnapshot.schemaVersion,
118
+ source: { ...pricingStateSnapshot.source },
119
+ sync: { ...pricingStateSnapshot.sync },
120
+ catalogModelCount: pricingStateSnapshot.catalogEntries.length,
121
+ overrideCount: pricingStateSnapshot.overrides.length,
122
+ mappingCount: pricingStateSnapshot.mappings.length,
123
+ configured: pricingStateSnapshot.catalogEntries.length > 0 || pricingStateSnapshot.overrides.length > 0 || pricingStateSnapshot.mappings.length > 0,
124
+ usedModels: usedPricingModels(tierSchedules),
125
+ cost: serializeCostAggregate(state.totals.cost),
126
+ }
127
+ if (detailed) {
128
+ snapshot.config = {
129
+ sync: { ...pricingStateSnapshot.sync },
130
+ mappings: pricingStateSnapshot.mappings.map((mapping) => ({ ...mapping })),
131
+ overrides: pricingStateSnapshot.overrides.map(({ providerId, ...entry }) => ({ ...entry, ...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}) })),
132
+ }
133
+ snapshot.tierSchedules = tierSchedules
134
+ }
135
+ return snapshot
136
+ }
137
+ async function loadPricing() {
138
+ if (storage === undefined || storage.backend === undefined || typeof storage.backend.get !== 'function') return
139
+ try {
140
+ const backend = storage.backend.get('json')
141
+ if (backend === undefined || backend === null || backend.kv === undefined) return
142
+ const unit = await backend.kv.open({ name: 'all_usage_pricing', version: 0, tables: [], hasGlobal: true })
143
+ if (state.disposed) { await unit.close().catch(() => {}); return }
144
+ state.pricingUnit = unit
145
+ const snapshot = await unit.loadAll()
146
+ const global = snapshot && snapshot.global
147
+ const raw = global && typeof global === 'object' && global.pricing !== undefined ? global.pricing : global
148
+ state.pricingState = normalizePricingState(raw)
149
+ state.pricingResolutionCache.clear()
150
+ } catch (err) {
151
+ console.error('[all-usage] pricing catalog unavailable:', err)
152
+ }
153
+ }
154
+ function persistPricing() {
155
+ if (state.disposed) return state.pricingWriteChain
156
+ const payload = { pricing: serializePricingState(state.pricingState) }
157
+ state.pricingWriteChain = state.pricingWriteChain.then(async () => {
158
+ if (state.pricingUnit === null || state.pricingUnit === undefined) return
159
+ await state.pricingUnit.setGlobal(payload)
160
+ })
161
+ state.pricingWriteChain = state.pricingWriteChain.catch((err) => {
162
+ console.error('[all-usage] pricing persist failed:', err)
163
+ })
164
+ return state.pricingWriteChain
165
+ }
166
+ async function syncPricing(force = false) {
167
+ await Promise.all([state.pricingReady, state.ledgerReady])
168
+ if (state.pricingSyncInFlight) return { ok: false, message: 'pricing-sync-in-progress', pricing: pricingSnapshot() }
169
+ const now = Date.now()
170
+ if (!force && state.pricingState.sync.lastSuccessAt > 0 && now - state.pricingState.sync.lastSuccessAt < state.pricingState.sync.intervalMs) return { ok: true, skipped: true, pricing: pricingSnapshot() }
171
+ state.pricingSyncInFlight = true
172
+ state.pricingState.sync.lastAttemptAt = now
173
+ try {
174
+ const result = await fetchModelsDevCatalog()
175
+ if (!result.ok) {
176
+ state.pricingState.source.lastError = result.error
177
+ state.pricingState.sync.lastError = result.error
178
+ // A failed attempt changes only sync health, not any configured price,
179
+ // so it must not invalidate the query snapshot/records caches.
180
+ markStatsChanged('sync-health')
181
+ await persistPricing()
182
+ return { ok: false, message: result.error, pricing: pricingSnapshot() }
183
+ }
184
+ const previousHash = state.pricingState.source && state.pricingState.source.catalogHash !== undefined ? String(state.pricingState.source.catalogHash) : ''
185
+ state.pricingState = normalizePricingState({
186
+ ...serializePricingState(state.pricingState),
187
+ source: { url: result.catalog.sourceUrl, fetchedAt: result.catalog.fetchedAt, catalogHash: result.catalog.catalogHash, lastError: '' },
188
+ sync: { ...state.pricingState.sync, lastSuccessAt: result.catalog.fetchedAt, lastError: '' },
189
+ catalogEntries: result.catalog.entries,
190
+ })
191
+ state.pricingResolutionCache.clear()
192
+ const backfill = backfillUnpricedCosts()
193
+ const temporal = reconcileTemporalPricing()
194
+ // Only catalog changes, newly priced usage, or temporal snapshot
195
+ // reconciliation can alter computed costs; a successful sync with an
196
+ // unchanged catalog must not invalidate the query snapshot, scoped caches,
197
+ // or records cursors. The sync-health bump still rebuilds the snapshot
198
+ // cache so lastSuccessAt is not stale on the next full response.
199
+ if (String(result.catalog.catalogHash || '') !== previousHash || backfill.priced > 0 || temporal.reconciled > 0) markStatsChanged('pricing')
200
+ else markStatsChanged('sync-health')
201
+ await persistPricing()
202
+ await drainLedgerWrites()
203
+ return { ok: true, skipped: false, backfill, pricing: pricingSnapshot() }
204
+ } finally {
205
+ state.pricingSyncInFlight = false
206
+ schedulePricingSync()
207
+ }
208
+ }
209
+ function backfillUnpricedCosts() {
210
+ let considered = 0
211
+ let priced = 0
212
+ const touched = new Set()
213
+ for (const item of state.usageByStep.values()) {
214
+ const oldCost = normalizeCostSnapshot(item.cost)
215
+ if (oldCost !== null && oldCost.pricingMode === 'official-model' && oldCost.status === 'priced') continue
216
+ considered += 1
217
+ const billing = billingInstantOf(item, oldCost)
218
+ const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId), billing.at, billing.source)
219
+ const temporalFailClosed = next.status === 'unsupported' && typeof next.reason === 'string' && next.reason.startsWith('temporal-')
220
+ // A valid fallback (priced) or a deterministic fail-closed verdict
221
+ // (unsupported: history gap / invalid config) replaces the previous state;
222
+ // merely unresolved pricing stays untouched.
223
+ if (next.status !== 'priced' && !temporalFailClosed) continue
224
+ if (oldCost !== null && oldCost.status === next.status && oldCost.total === next.total && oldCost.baseTotal === next.baseTotal) {
225
+ if (item.cost !== next) {
226
+ item.cost = next
227
+ const record = state.ledgerRecords.get(item.sid)
228
+ if (record !== undefined) {
229
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
230
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
231
+ }
232
+ }
233
+ continue
234
+ }
235
+ if (oldCost !== null) {
236
+ adjustCostOnly(item, oldCost, -1)
237
+ adjustQueryCost(item, oldCost, -1)
238
+ }
239
+ item.cost = next
240
+ adjustCostOnly(item, next, 1)
241
+ adjustQueryCost(item, next, 1)
242
+ const record = state.ledgerRecords.get(item.sid)
243
+ if (record !== undefined) {
244
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
245
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
246
+ }
247
+ priced += 1
248
+ }
249
+ for (const record of touched) {
250
+ record.version = LEDGER_VERSION
251
+ record.updatedAt = nextLedgerRevision()
252
+ // Backfilling prices must not clear a mixed-workspace upgrade flag: stay
253
+ // unfoldable until a rebuild normalizes every historical item.
254
+ const stillMixed = record.turns.some((turn) => turn && turn.workspaceId !== record.workspaceId) || record.usage.some((item) => item && item.workspaceId !== record.workspaceId)
255
+ record.needsUpgrade = stillMixed
256
+ void persistLedgerRecord(record)
257
+ }
258
+ let remaining = 0
259
+ for (const item of state.usageByStep.values()) {
260
+ const cost = normalizeCostSnapshot(item.cost)
261
+ if (cost === null || cost.status !== 'priced') remaining += 1
262
+ }
263
+ return { considered, priced, remaining }
264
+ }
265
+ function costSnapshotEquivalent(left, right) {
266
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false
267
+ 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.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
268
+ for (const key of ['input', 'output', 'cacheRead', 'cacheWrite']) {
269
+ if (left.breakdown !== null && left.breakdown !== undefined && right.breakdown !== null && right.breakdown !== undefined) {
270
+ if (left.breakdown[key] !== right.breakdown[key]) return false
271
+ }
272
+ if (left.rates[key] !== right.rates[key] || right.rates[key] === undefined) return false
273
+ }
274
+ const leftTier = left.selectedTier || null
275
+ const rightTier = right.selectedTier || null
276
+ if (leftTier === null !== (rightTier === null)) return false
277
+ if (leftTier !== null && (leftTier.type !== rightTier.type || leftTier.size !== rightTier.size)) return false
278
+ return true
279
+ }
280
+ /**
281
+ * Controlled DeepSeek temporal reconciliation with auditable-history semantics:
282
+ * legacy v1 snapshots of first-party DeepSeek routes are migrated against the
283
+ * usage instant once; already-priced v2 snapshots are NEVER rewritten by a
284
+ * catalog refresh (their policy hash intentionally excludes live rates) and
285
+ * only a user-explicit repricing (repriceTemporal) recomputes them. Anything
286
+ * that cannot be verified keeps its previous value (never guess a price).
287
+ * Routes that are not first-party DeepSeek are never touched.
288
+ */
289
+ function snapshotPolicyMatches(rawCost, resolved) {
290
+ const exemptReason = rawCost.temporalExemptReason
291
+ const route = resolved.temporalRoute
292
+ if (exemptReason === 'route-not-official') return route !== 'official' && route !== 'mapped'
293
+ if (exemptReason === 'no-temporal-profile' && resolved.temporalProfile === undefined && resolved.temporalConfigInvalid !== true) return true
294
+ if (typeof rawCost.pricingPolicyId !== 'string' || typeof rawCost.pricingPolicyHash !== 'string') return false
295
+ const policies = resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) ? resolved.temporalProfile.policies : []
296
+ if (!policies.some((policy) => policy.policyId === rawCost.pricingPolicyId && policy.policyHash === rawCost.pricingPolicyHash)) return false
297
+ // The snapshot's billing instant must still be covered by (and consistent
298
+ // with) its own policy: an archive that shrank or moved the window leaves
299
+ // the snapshot inside a gap, which must fail closed instead of staying priced.
300
+ const plan = temporalPlanFor(resolved, rawCost.pricingAt)
301
+ if (plan.status !== 'applied') return false
302
+ return plan.policyHash === rawCost.pricingPolicyHash && plan.band === rawCost.pricingBand
303
+ }
304
+
305
+ function reconcileTemporalPricing(options = {}) {
306
+ const force = options.force === true
307
+ let considered = 0
308
+ let reconciled = 0
309
+ const touched = new Set()
310
+ for (const item of state.usageByStep.values()) {
311
+ const rawCost = item.cost
312
+ if (rawCost === null || rawCost === undefined || typeof rawCost !== 'object' || rawCost.pricingMode !== 'official-model') continue
313
+ const identity = coerceIdentity(item.identity || item.modelId)
314
+ const resolved = resolveCurrentPricing(identity)
315
+ const route = resolved.temporalRoute
316
+ if (route !== 'official' && route !== 'mapped') continue
317
+ const legacyShape = rawCost.schemaVersion !== COST_SCHEMA_VERSION || rawCost.pricingTimeSource === null || rawCost.pricingTimeSource === undefined || rawCost.pricingTimeSource === 'legacy-unknown'
318
+ if (!legacyShape && !force) {
319
+ // Priced snapshots stay auditable only while their policy (or exempt
320
+ // verdict) still matches the current archive; snapshots priced under a
321
+ // retired policy (e.g. an earlier effective instant) are migrated once.
322
+ if (rawCost.status === 'priced' && snapshotPolicyMatches(rawCost, resolved)) continue
323
+ if (rawCost.status !== 'priced') continue
324
+ }
325
+ considered += 1
326
+ const oldCost = normalizeCostSnapshot(rawCost)
327
+ const billing = billingInstantOf(item, oldCost)
328
+ const next = calculateCost(item.values, resolved, billing.at, billing.source)
329
+ if (costSnapshotEquivalent(oldCost, next)) {
330
+ if (rawCost !== next) {
331
+ item.cost = next
332
+ const record = state.ledgerRecords.get(item.sid)
333
+ if (record !== undefined) {
334
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
335
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
336
+ }
337
+ reconciled += 1
338
+ }
339
+ continue
340
+ }
341
+ if (oldCost !== null) {
342
+ adjustCostOnly(item, oldCost, -1)
343
+ adjustQueryCost(item, oldCost, -1)
344
+ }
345
+ item.cost = next
346
+ adjustCostOnly(item, next, 1)
347
+ adjustQueryCost(item, next, 1)
348
+ const record = state.ledgerRecords.get(item.sid)
349
+ if (record !== undefined) {
350
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
351
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
352
+ }
353
+ reconciled += 1
354
+ }
355
+ for (const record of touched) {
356
+ record.version = LEDGER_VERSION
357
+ record.updatedAt = nextLedgerRevision()
358
+ // Reconciliation must not clear a mixed-workspace upgrade flag: stay
359
+ // unfoldable until a rebuild normalizes every historical item.
360
+ const stillMixed = record.turns.some((turn) => turn && turn.workspaceId !== record.workspaceId) || record.usage.some((item) => item && item.workspaceId !== record.workspaceId)
361
+ record.needsUpgrade = stillMixed
362
+ void persistLedgerRecord(record)
363
+ }
364
+ return { considered, reconciled }
365
+ }
366
+
367
+ function updatePricingState(raw, backfill, repriceTemporal = false) {
368
+ const input = raw && typeof raw === 'object' && raw.pricing && typeof raw.pricing === 'object' ? raw.pricing : raw
369
+ const current = serializePricingState(state.pricingState)
370
+ const merged = { ...current, ...(input && typeof input === 'object' ? input : {}) }
371
+ if (input && typeof input === 'object' && input.sync && typeof input.sync === 'object' && !Array.isArray(input.sync)) merged.sync = { ...current.sync, ...input.sync }
372
+ state.pricingState = normalizePricingState(merged)
373
+ state.pricingResolutionCache.clear()
374
+ const result = backfill === true ? backfillUnpricedCosts() : { considered: 0, priced: 0, remaining: state.totals.cost.unpricedCalls + state.totals.cost.ambiguousCalls + state.totals.cost.unsupportedCalls }
375
+ const temporal = reconcileTemporalPricing({ force: repriceTemporal === true })
376
+ markStatsChanged('pricing')
377
+ schedulePricingSync()
378
+ return { ...result, temporalReconciled: temporal.reconciled }
379
+ }
380
+ function schedulePricingSync() {
381
+ if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
382
+ if (state.disposed || state.pricingState.sync.autoEnabled !== true) return
383
+ const elapsed = state.pricingState.sync.lastSuccessAt > 0 ? Date.now() - state.pricingState.sync.lastSuccessAt : state.pricingState.sync.intervalMs
384
+ const delay = Math.max(0, state.pricingState.sync.intervalMs - elapsed)
385
+ state.pricingSyncTimer = setTimeout(() => {
386
+ state.pricingSyncTimer = null
387
+ void syncPricing(true)
388
+ }, delay)
389
+ if (state.pricingSyncTimer && typeof state.pricingSyncTimer.unref === 'function') state.pricingSyncTimer.unref()
390
+ }
391
+
392
+
393
+ return {
394
+ adjustCostOnly,
395
+ usedPricingModels,
396
+ pricingModelSearch,
397
+ pricingSnapshot,
398
+ loadPricing,
399
+ persistPricing,
400
+ syncPricing,
401
+ backfillUnpricedCosts,
402
+ reconcileTemporalPricing,
403
+ updatePricingState,
404
+ schedulePricingSync
405
+ }
406
+ }