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/CHANGELOG.md +111 -0
- package/README.md +183 -36
- package/assets/model-icons/LICENSE.upstream-lobe-icons.txt +51 -0
- package/assets/model-icons/claude-color.svg +1 -0
- package/assets/model-icons/deepseek-color.svg +1 -0
- package/assets/model-icons/doubao-color.svg +1 -0
- package/assets/model-icons/gemini-color.svg +1 -0
- package/assets/model-icons/grok.svg +1 -0
- package/assets/model-icons/kimi-color.svg +1 -0
- package/assets/model-icons/manifest.json +213 -0
- package/assets/model-icons/meta-color.svg +1 -0
- package/assets/model-icons/minimax-color.svg +1 -0
- package/assets/model-icons/openai-color.svg +1 -0
- package/assets/model-icons/qwen-color.svg +1 -0
- package/assets/model-icons/zhipu-color.svg +1 -0
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/fixtures/usage-events.json +172 -0
- package/lib/aggregation.js +1050 -0
- package/lib/balance.js +112 -0
- package/lib/client.js +1 -2906
- package/lib/http.js +305 -0
- package/lib/index.js +2 -2119
- package/lib/ledger.js +491 -0
- package/lib/plugin.js +277 -0
- package/lib/pricing-runtime.js +406 -0
- package/lib/pricing.js +631 -39
- package/lib/session-sync.js +642 -0
- package/lib/usage-core.js +171 -0
- package/package.json +28 -3
- package/scripts/replay-fixture.mjs +155 -0
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
|
+
}
|