dsh-all-usage 1.1.2 → 1.1.3
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 +66 -0
- package/README.md +193 -19
- package/fixtures/usage-events.json +172 -0
- package/lib/aggregation.js +1002 -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 +464 -0
- package/lib/plugin.js +276 -0
- package/lib/pricing-runtime.js +282 -0
- package/lib/pricing.js +299 -36
- package/lib/session-sync.js +589 -0
- package/lib/usage-core.js +127 -0
- package/package.json +28 -3
- package/scripts/replay-fixture.mjs +155 -0
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
totals: { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: createCostAccumulator() },
|
|
35
|
+
sessionCount: new Set(),
|
|
36
|
+
sessionSeq: new Map(),
|
|
37
|
+
chains: new Map(),
|
|
38
|
+
liveResyncPending: new Set(),
|
|
39
|
+
liveResyncTimers: new Map(),
|
|
40
|
+
liveResyncAttempts: new Map(),
|
|
41
|
+
scan: { started: false, done: false, scanned: 0, total: 0, failed: 0 },
|
|
42
|
+
aliases: {},
|
|
43
|
+
kvUnit: null,
|
|
44
|
+
aliasWriteChain: Promise.resolve(),
|
|
45
|
+
aliasesReady: Promise.resolve(),
|
|
46
|
+
balanceCache: { fetchedAt: 0, payload: null },
|
|
47
|
+
requestToken: randomBytes(32).toString('base64url'),
|
|
48
|
+
instanceId: randomBytes(12).toString('base64url'),
|
|
49
|
+
statsRevision: 0,
|
|
50
|
+
dataRevision: 0,
|
|
51
|
+
metadataRevision: 0,
|
|
52
|
+
scanRevision: 0,
|
|
53
|
+
pricingRevision: 0,
|
|
54
|
+
statsUpdatedAt: Date.now(),
|
|
55
|
+
statsDirtyScheduled: false,
|
|
56
|
+
statsDirtyKinds: new Set(),
|
|
57
|
+
sync: {
|
|
58
|
+
lastStartedAt: 0,
|
|
59
|
+
lastCompletedAt: 0,
|
|
60
|
+
lastErrorAt: 0,
|
|
61
|
+
lastErrorCode: null,
|
|
62
|
+
persistenceSnapshotsAvailable: false,
|
|
63
|
+
sessionsTotal: 0,
|
|
64
|
+
sessionsRead: 0,
|
|
65
|
+
sessionsSkippedByRevision: 0,
|
|
66
|
+
sessionsRestoredFromLedger: 0,
|
|
67
|
+
sessionsFailed: 0,
|
|
68
|
+
},
|
|
69
|
+
ledgerRecords: new Map(),
|
|
70
|
+
queryCache: new Map(),
|
|
71
|
+
recordsQueryCache: new Map(),
|
|
72
|
+
snapshotCache: null,
|
|
73
|
+
ledgerRevision: Date.now(),
|
|
74
|
+
ledgerUnit: null,
|
|
75
|
+
ledgerReady: Promise.resolve(),
|
|
76
|
+
ledgerWriteChain: Promise.resolve(),
|
|
77
|
+
ledgerPending: new Map(),
|
|
78
|
+
ledgerWriteWaiters: new Map(),
|
|
79
|
+
ledgerWriteRunning: false,
|
|
80
|
+
ledgerWriteScheduled: false,
|
|
81
|
+
ledgerWriteTimer: null,
|
|
82
|
+
ledgerDirtySessions: new Set(),
|
|
83
|
+
ledgerDirtyEpochs: new Map(),
|
|
84
|
+
ledgerWriteFailedSessions: new Set(),
|
|
85
|
+
pricingState: createEmptyPricingState(),
|
|
86
|
+
pricingResolutionCache: new Map(),
|
|
87
|
+
pricingUnit: null,
|
|
88
|
+
pricingReady: Promise.resolve(),
|
|
89
|
+
pricingWriteChain: Promise.resolve(),
|
|
90
|
+
pricingSyncInFlight: false,
|
|
91
|
+
pricingSyncTimer: null,
|
|
92
|
+
aggregationGeneration: 0,
|
|
93
|
+
disposed: false,
|
|
94
|
+
baselineRetryDelay: 1000,
|
|
95
|
+
baselineRetryScheduled: false,
|
|
96
|
+
baselineFallbackTimer: null,
|
|
97
|
+
knownSessionIds: new Set(),
|
|
98
|
+
reconcileHintScheduled: false,
|
|
99
|
+
reconcilePending: false,
|
|
100
|
+
reconcileInFlight: false,
|
|
101
|
+
reconcileTimer: null,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const host = { ctx, services, state, webServer: services.webServer, aggregation: null, ledger: null, pricing: null, balance: null, sessionSync: null, aliases: null }
|
|
105
|
+
function commitPendingStats() {
|
|
106
|
+
if (!state.statsDirtyScheduled) return
|
|
107
|
+
state.statsDirtyScheduled = false
|
|
108
|
+
const kinds = state.statsDirtyKinds
|
|
109
|
+
state.statsDirtyKinds = new Set()
|
|
110
|
+
if (state.disposed) return
|
|
111
|
+
state.statsRevision += 1
|
|
112
|
+
if (kinds.has('data')) state.dataRevision += 1
|
|
113
|
+
if (kinds.has('metadata')) state.metadataRevision += 1
|
|
114
|
+
if (kinds.has('scan')) state.scanRevision += 1
|
|
115
|
+
if (kinds.has('pricing')) state.pricingRevision += 1
|
|
116
|
+
state.statsUpdatedAt = Date.now()
|
|
117
|
+
}
|
|
118
|
+
function markStatsChanged(kind = 'data') {
|
|
119
|
+
const kinds = Array.isArray(kind) ? kind : [kind]
|
|
120
|
+
let recordsChanged = false
|
|
121
|
+
for (const value of kinds) {
|
|
122
|
+
if (value !== 'data' && value !== 'metadata' && value !== 'scan' && value !== 'pricing') continue
|
|
123
|
+
state.statsDirtyKinds.add(value)
|
|
124
|
+
if (value === 'data' || value === 'pricing') recordsChanged = true
|
|
125
|
+
}
|
|
126
|
+
state.snapshotCache = null
|
|
127
|
+
if (recordsChanged) state.recordsQueryCache.clear()
|
|
128
|
+
if (state.disposed || state.statsDirtyScheduled) return
|
|
129
|
+
state.statsDirtyScheduled = true
|
|
130
|
+
if (typeof queueMicrotask === 'function') queueMicrotask(commitPendingStats)
|
|
131
|
+
else Promise.resolve().then(commitPendingStats)
|
|
132
|
+
}
|
|
133
|
+
function resetSyncState() {
|
|
134
|
+
state.sync.lastStartedAt = 0
|
|
135
|
+
state.sync.lastCompletedAt = 0
|
|
136
|
+
state.sync.lastErrorAt = 0
|
|
137
|
+
state.sync.lastErrorCode = null
|
|
138
|
+
state.sync.persistenceSnapshotsAvailable = false
|
|
139
|
+
state.sync.sessionsTotal = 0
|
|
140
|
+
state.sync.sessionsRead = 0
|
|
141
|
+
state.sync.sessionsSkippedByRevision = 0
|
|
142
|
+
state.sync.sessionsRestoredFromLedger = 0
|
|
143
|
+
state.sync.sessionsFailed = 0
|
|
144
|
+
}
|
|
145
|
+
function beginSync() {
|
|
146
|
+
resetSyncState()
|
|
147
|
+
state.sync.lastStartedAt = Date.now()
|
|
148
|
+
markStatsChanged('scan')
|
|
149
|
+
}
|
|
150
|
+
function noteSyncError(code) {
|
|
151
|
+
state.sync.lastErrorAt = Date.now()
|
|
152
|
+
state.sync.lastErrorCode = code
|
|
153
|
+
markStatsChanged('scan')
|
|
154
|
+
}
|
|
155
|
+
function syncSnapshot() {
|
|
156
|
+
return {
|
|
157
|
+
lastStartedAt: state.sync.lastStartedAt,
|
|
158
|
+
lastCompletedAt: state.sync.lastCompletedAt,
|
|
159
|
+
lastErrorAt: state.sync.lastErrorAt,
|
|
160
|
+
lastErrorCode: state.sync.lastErrorCode,
|
|
161
|
+
persistenceSnapshotsAvailable: state.sync.persistenceSnapshotsAvailable,
|
|
162
|
+
sessionsTotal: state.sync.sessionsTotal,
|
|
163
|
+
sessionsRead: state.sync.sessionsRead,
|
|
164
|
+
sessionsSkippedByRevision: state.sync.sessionsSkippedByRevision,
|
|
165
|
+
sessionsRestoredFromLedger: state.sync.sessionsRestoredFromLedger,
|
|
166
|
+
sessionsFailed: state.sync.sessionsFailed,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Object.assign(host, { commitPendingStats, markStatsChanged, resetSyncState, beginSync, noteSyncError, syncSnapshot })
|
|
170
|
+
|
|
171
|
+
host.pricingSnapshot = (...args) => host.pricing.pricingSnapshot(...args)
|
|
172
|
+
host.syncSnapshot = (...args) => syncSnapshot(...args)
|
|
173
|
+
host.aggregation = createAggregation(host)
|
|
174
|
+
host.ledger = createLedger(host)
|
|
175
|
+
host.pricing = createPricingRuntime(host)
|
|
176
|
+
host.balance = createBalance(host)
|
|
177
|
+
host.sessionSync = createSessionSync(host)
|
|
178
|
+
|
|
179
|
+
async function loadAliases() {
|
|
180
|
+
const storage = services.storage
|
|
181
|
+
if (storage === undefined || storage === null) return
|
|
182
|
+
try {
|
|
183
|
+
const backend = storage.backend.get('json')
|
|
184
|
+
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
185
|
+
const unit = await backend.kv.open({ name: 'all_usage_aliases', version: 0, tables: [], hasGlobal: true })
|
|
186
|
+
if (state.disposed) { await unit.close().catch(() => {}); return }
|
|
187
|
+
state.kvUnit = unit
|
|
188
|
+
const snap = await unit.loadAll()
|
|
189
|
+
const global = snap && snap.global
|
|
190
|
+
if (global !== null && global !== undefined && typeof global === 'object') {
|
|
191
|
+
let changed = false
|
|
192
|
+
for (const key of Object.keys(global)) {
|
|
193
|
+
const value = global[key]
|
|
194
|
+
const next = typeof value === 'string' ? value.trim() : ''
|
|
195
|
+
if (next !== '' && state.aliases[key] !== next) {
|
|
196
|
+
state.aliases[key] = next
|
|
197
|
+
changed = true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (changed) markStatsChanged('metadata')
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('[all-usage] alias storage unavailable:', err)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function persistAliases() {
|
|
207
|
+
if (state.disposed) return state.aliasWriteChain
|
|
208
|
+
const snapshotAliases = {}
|
|
209
|
+
for (const key of Object.keys(state.aliases)) snapshotAliases[key] = state.aliases[key]
|
|
210
|
+
state.aliasWriteChain = state.aliasWriteChain.then(() => {
|
|
211
|
+
if (state.kvUnit === null || state.kvUnit === undefined) return undefined
|
|
212
|
+
return state.kvUnit.setGlobal(snapshotAliases).catch((err) => {
|
|
213
|
+
console.error('[all-usage] alias persist failed:', err)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
return state.aliasWriteChain
|
|
217
|
+
}
|
|
218
|
+
function setAlias(wsId, raw) {
|
|
219
|
+
if (typeof wsId !== 'string' || wsId.length === 0 || wsId.length > 256) return { ok: false, message: 'invalid-workspace', aliases: Object.assign({}, state.aliases) }
|
|
220
|
+
if (typeof raw !== 'string') return { ok: false, message: 'invalid-alias', aliases: Object.assign({}, state.aliases) }
|
|
221
|
+
const alias = raw.trim().slice(0, 80)
|
|
222
|
+
if (!state.wsMeta.has(wsId)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, state.aliases) }
|
|
223
|
+
const previous = typeof state.aliases[wsId] === 'string' ? state.aliases[wsId] : ''
|
|
224
|
+
if (alias === previous) return { ok: true, aliases: Object.assign({}, state.aliases) }
|
|
225
|
+
if (alias === '') delete state.aliases[wsId]
|
|
226
|
+
else state.aliases[wsId] = alias
|
|
227
|
+
persistAliases()
|
|
228
|
+
markStatsChanged('metadata')
|
|
229
|
+
return { ok: true, aliases: Object.assign({}, state.aliases) }
|
|
230
|
+
}
|
|
231
|
+
host.aliases = { loadAliases, persistAliases, setAlias }
|
|
232
|
+
|
|
233
|
+
ctx.effect(() => async () => {
|
|
234
|
+
state.disposed = true
|
|
235
|
+
if (state.reconcileTimer !== null) {
|
|
236
|
+
clearTimeout(state.reconcileTimer)
|
|
237
|
+
state.reconcileTimer = null
|
|
238
|
+
}
|
|
239
|
+
if (state.baselineFallbackTimer !== null) {
|
|
240
|
+
clearTimeout(state.baselineFallbackTimer)
|
|
241
|
+
state.baselineFallbackTimer = null
|
|
242
|
+
}
|
|
243
|
+
if (state.pricingSyncTimer !== null) {
|
|
244
|
+
clearTimeout(state.pricingSyncTimer)
|
|
245
|
+
state.pricingSyncTimer = null
|
|
246
|
+
}
|
|
247
|
+
for (const timer of state.liveResyncTimers.values()) clearTimeout(timer)
|
|
248
|
+
state.liveResyncTimers.clear()
|
|
249
|
+
state.liveResyncAttempts.clear()
|
|
250
|
+
state.liveResyncPending.clear()
|
|
251
|
+
state.baselineRetryScheduled = false
|
|
252
|
+
if (state.ledgerWriteTimer !== null) {
|
|
253
|
+
clearTimeout(state.ledgerWriteTimer)
|
|
254
|
+
state.ledgerWriteTimer = null
|
|
255
|
+
}
|
|
256
|
+
state.ledgerWriteScheduled = false
|
|
257
|
+
state.chains.clear()
|
|
258
|
+
await Promise.all([state.aliasesReady, state.ledgerReady, state.pricingReady, state.aliasWriteChain, state.pricingWriteChain])
|
|
259
|
+
await host.ledger.drainLedgerWrites()
|
|
260
|
+
const units = [state.kvUnit, state.ledgerUnit, state.pricingUnit]
|
|
261
|
+
state.kvUnit = null
|
|
262
|
+
state.ledgerUnit = null
|
|
263
|
+
state.pricingUnit = null
|
|
264
|
+
await Promise.all(units.map((unit) => unit === null || unit === undefined ? undefined : unit.close().catch(() => {})))
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
registerRoutes(host)
|
|
268
|
+
state.ledgerReady = host.ledger.loadLedger()
|
|
269
|
+
state.pricingReady = host.pricing.loadPricing().then(() => { host.pricing.schedulePricingSync() })
|
|
270
|
+
void host.sessionSync.runBaseline()
|
|
271
|
+
host.sessionSync.scheduleReconcileTimer()
|
|
272
|
+
state.aliasesReady = loadAliases()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { name, inject, apply }
|
|
276
|
+
export default { name, inject, apply }
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState } from './pricing.js'
|
|
2
|
+
|
|
3
|
+
const LEDGER_VERSION = 3
|
|
4
|
+
|
|
5
|
+
export function createPricingRuntime(host) {
|
|
6
|
+
const { state, markStatsChanged } = host
|
|
7
|
+
const { storage } = host.services
|
|
8
|
+
const {
|
|
9
|
+
coerceIdentity,
|
|
10
|
+
dateKeys,
|
|
11
|
+
ensureDay,
|
|
12
|
+
ensureDayModel,
|
|
13
|
+
ensureDayWs,
|
|
14
|
+
ensureModel,
|
|
15
|
+
ensureWs,
|
|
16
|
+
addCostAggregateDirection,
|
|
17
|
+
adjustQueryCost,
|
|
18
|
+
resolveCurrentPricing,
|
|
19
|
+
} = host.aggregation
|
|
20
|
+
const persistLedgerRecord = (...args) => host.ledger.persistLedgerRecord(...args)
|
|
21
|
+
const drainLedgerWrites = (...args) => host.ledger.drainLedgerWrites(...args)
|
|
22
|
+
const nextLedgerRevision = (...args) => host.ledger.nextLedgerRevision(...args)
|
|
23
|
+
|
|
24
|
+
function adjustCostOnly(item, cost, direction) {
|
|
25
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
26
|
+
const dates = item && typeof item.date === 'string' && typeof item.dateUtc === 'string' ? { local: item.date, utc: item.dateUtc } : dateKeys(item.time)
|
|
27
|
+
const targets = []
|
|
28
|
+
const seen = new Set()
|
|
29
|
+
for (const target of [
|
|
30
|
+
ensureModel(identity).cost,
|
|
31
|
+
state.totals.cost,
|
|
32
|
+
ensureWs(item.wsId).cost,
|
|
33
|
+
ensureDay(state.byDay, dates.local).cost,
|
|
34
|
+
ensureDay(state.byDayUtc, dates.utc).cost,
|
|
35
|
+
ensureDayWs(ensureDay(state.byDay, dates.local), item.wsId).cost,
|
|
36
|
+
ensureDayWs(ensureDay(state.byDayUtc, dates.utc), item.wsId).cost,
|
|
37
|
+
ensureDayModel(ensureDay(state.byDay, dates.local), identity).cost,
|
|
38
|
+
ensureDayModel(ensureDay(state.byDayUtc, dates.utc), identity).cost,
|
|
39
|
+
]) {
|
|
40
|
+
if (seen.has(target)) continue
|
|
41
|
+
seen.add(target)
|
|
42
|
+
targets.push(target)
|
|
43
|
+
}
|
|
44
|
+
for (const target of targets) addCostAggregateDirection(target, cost, direction)
|
|
45
|
+
}
|
|
46
|
+
function usedPricingModels(tierSchedules = null) {
|
|
47
|
+
const rows = []
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
const scheduleIds = new Map()
|
|
50
|
+
const scheduleIdFor = (tiers) => {
|
|
51
|
+
if (!Array.isArray(tierSchedules) || tiers.length === 0) return null
|
|
52
|
+
const key = JSON.stringify(tiers)
|
|
53
|
+
const existing = scheduleIds.get(key)
|
|
54
|
+
if (existing !== undefined) return existing
|
|
55
|
+
const id = 'tier-' + tierSchedules.length
|
|
56
|
+
scheduleIds.set(key, id)
|
|
57
|
+
tierSchedules.push({ id, tiers: tiers.map((tier) => ({ ...tier })) })
|
|
58
|
+
return id
|
|
59
|
+
}
|
|
60
|
+
for (const item of state.usageByStep.values()) {
|
|
61
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
62
|
+
if (seen.has(identity.identityKey)) continue
|
|
63
|
+
seen.add(identity.identityKey)
|
|
64
|
+
const resolved = resolveCurrentPricing(identity)
|
|
65
|
+
const tiers = Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
66
|
+
const tierScheduleId = resolved.tiered === true ? scheduleIdFor(tiers) : null
|
|
67
|
+
rows.push({
|
|
68
|
+
identityKey: identity.identityKey,
|
|
69
|
+
provider: identity.provider,
|
|
70
|
+
requestedModel: identity.requestedModel,
|
|
71
|
+
actualModel: identity.actualModel,
|
|
72
|
+
model: identity.label,
|
|
73
|
+
status: resolved.status,
|
|
74
|
+
reason: resolved.reason || '',
|
|
75
|
+
pricingModel: resolved.pricingModel || null,
|
|
76
|
+
providerId: resolved.providerId || null,
|
|
77
|
+
source: resolved.source || 'none',
|
|
78
|
+
currency: resolved.currency || 'USD',
|
|
79
|
+
rates: resolved.rates || null,
|
|
80
|
+
tiered: resolved.tiered === true,
|
|
81
|
+
tierCount: tiers.length,
|
|
82
|
+
...(tierScheduleId === null ? {} : { tierScheduleId }),
|
|
83
|
+
tieredInvalid: resolved.tieredInvalid === true,
|
|
84
|
+
inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
|
|
85
|
+
multiplier: resolved.multiplier || '1',
|
|
86
|
+
})
|
|
87
|
+
if (rows.length >= 500) break
|
|
88
|
+
}
|
|
89
|
+
rows.sort((a, b) => String(a.model).localeCompare(String(b.model)))
|
|
90
|
+
return rows
|
|
91
|
+
}
|
|
92
|
+
function pricingModelSearch(query, limit = 20) {
|
|
93
|
+
const raw = typeof query === 'string' ? query.trim().toLowerCase() : ''
|
|
94
|
+
if (raw === '') return []
|
|
95
|
+
const normalized = raw.replace(/\s+/g, ' ')
|
|
96
|
+
const selected = new Map()
|
|
97
|
+
for (const entry of state.pricingState.catalogEntries) {
|
|
98
|
+
const official = officialProviderIds(entry.modelId)
|
|
99
|
+
if (!official.has(String(entry.providerId || '').toLowerCase())) continue
|
|
100
|
+
const modelId = entry.modelId.toLowerCase()
|
|
101
|
+
const displayName = String(entry.displayName || '').toLowerCase()
|
|
102
|
+
if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
|
|
103
|
+
const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
|
|
104
|
+
const previous = selected.get(entry.modelId)
|
|
105
|
+
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 })
|
|
106
|
+
}
|
|
107
|
+
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)
|
|
108
|
+
}
|
|
109
|
+
function pricingSnapshot(options = {}) {
|
|
110
|
+
const pricingStateSnapshot = state.pricingState
|
|
111
|
+
const detailed = options.detailed !== false
|
|
112
|
+
const tierSchedules = detailed ? [] : null
|
|
113
|
+
const snapshot = {
|
|
114
|
+
schemaVersion: pricingStateSnapshot.schemaVersion,
|
|
115
|
+
source: { ...pricingStateSnapshot.source },
|
|
116
|
+
sync: { ...pricingStateSnapshot.sync },
|
|
117
|
+
catalogModelCount: pricingStateSnapshot.catalogEntries.length,
|
|
118
|
+
overrideCount: pricingStateSnapshot.overrides.length,
|
|
119
|
+
mappingCount: pricingStateSnapshot.mappings.length,
|
|
120
|
+
configured: pricingStateSnapshot.catalogEntries.length > 0 || pricingStateSnapshot.overrides.length > 0 || pricingStateSnapshot.mappings.length > 0,
|
|
121
|
+
usedModels: usedPricingModels(tierSchedules),
|
|
122
|
+
cost: serializeCostAggregate(state.totals.cost),
|
|
123
|
+
}
|
|
124
|
+
if (detailed) {
|
|
125
|
+
snapshot.config = {
|
|
126
|
+
sync: { ...pricingStateSnapshot.sync },
|
|
127
|
+
mappings: pricingStateSnapshot.mappings.map((mapping) => ({ ...mapping })),
|
|
128
|
+
overrides: pricingStateSnapshot.overrides.map(({ providerId, ...entry }) => ({ ...entry, ...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}) })),
|
|
129
|
+
}
|
|
130
|
+
snapshot.tierSchedules = tierSchedules
|
|
131
|
+
}
|
|
132
|
+
return snapshot
|
|
133
|
+
}
|
|
134
|
+
async function loadPricing() {
|
|
135
|
+
if (storage === undefined || storage.backend === undefined || typeof storage.backend.get !== 'function') return
|
|
136
|
+
try {
|
|
137
|
+
const backend = storage.backend.get('json')
|
|
138
|
+
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
139
|
+
const unit = await backend.kv.open({ name: 'all_usage_pricing', version: 0, tables: [], hasGlobal: true })
|
|
140
|
+
if (state.disposed) { await unit.close().catch(() => {}); return }
|
|
141
|
+
state.pricingUnit = unit
|
|
142
|
+
const snapshot = await unit.loadAll()
|
|
143
|
+
const global = snapshot && snapshot.global
|
|
144
|
+
const raw = global && typeof global === 'object' && global.pricing !== undefined ? global.pricing : global
|
|
145
|
+
state.pricingState = normalizePricingState(raw)
|
|
146
|
+
state.pricingResolutionCache.clear()
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('[all-usage] pricing catalog unavailable:', err)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function persistPricing() {
|
|
152
|
+
if (state.disposed) return state.pricingWriteChain
|
|
153
|
+
const payload = { pricing: serializePricingState(state.pricingState) }
|
|
154
|
+
state.pricingWriteChain = state.pricingWriteChain.then(async () => {
|
|
155
|
+
if (state.pricingUnit === null || state.pricingUnit === undefined) return
|
|
156
|
+
await state.pricingUnit.setGlobal(payload)
|
|
157
|
+
})
|
|
158
|
+
state.pricingWriteChain = state.pricingWriteChain.catch((err) => {
|
|
159
|
+
console.error('[all-usage] pricing persist failed:', err)
|
|
160
|
+
})
|
|
161
|
+
return state.pricingWriteChain
|
|
162
|
+
}
|
|
163
|
+
async function syncPricing(force = false) {
|
|
164
|
+
await Promise.all([state.pricingReady, state.ledgerReady])
|
|
165
|
+
if (state.pricingSyncInFlight) return { ok: false, message: 'pricing-sync-in-progress', pricing: pricingSnapshot() }
|
|
166
|
+
const now = Date.now()
|
|
167
|
+
if (!force && state.pricingState.sync.lastSuccessAt > 0 && now - state.pricingState.sync.lastSuccessAt < state.pricingState.sync.intervalMs) return { ok: true, skipped: true, pricing: pricingSnapshot() }
|
|
168
|
+
state.pricingSyncInFlight = true
|
|
169
|
+
state.pricingState.sync.lastAttemptAt = now
|
|
170
|
+
try {
|
|
171
|
+
const result = await fetchModelsDevCatalog()
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
state.pricingState.source.lastError = result.error
|
|
174
|
+
state.pricingState.sync.lastError = result.error
|
|
175
|
+
// A failed attempt changes only sync health, not any configured price,
|
|
176
|
+
// so it must not invalidate the query snapshot/records caches.
|
|
177
|
+
markStatsChanged('sync-health')
|
|
178
|
+
await persistPricing()
|
|
179
|
+
return { ok: false, message: result.error, pricing: pricingSnapshot() }
|
|
180
|
+
}
|
|
181
|
+
const previousHash = state.pricingState.source && state.pricingState.source.catalogHash !== undefined ? String(state.pricingState.source.catalogHash) : ''
|
|
182
|
+
state.pricingState = normalizePricingState({
|
|
183
|
+
...serializePricingState(state.pricingState),
|
|
184
|
+
source: { url: result.catalog.sourceUrl, fetchedAt: result.catalog.fetchedAt, catalogHash: result.catalog.catalogHash, lastError: '' },
|
|
185
|
+
sync: { ...state.pricingState.sync, lastSuccessAt: result.catalog.fetchedAt, lastError: '' },
|
|
186
|
+
catalogEntries: result.catalog.entries,
|
|
187
|
+
})
|
|
188
|
+
state.pricingResolutionCache.clear()
|
|
189
|
+
const backfill = backfillUnpricedCosts()
|
|
190
|
+
// Only catalog changes or newly priced usage can alter computed costs; a
|
|
191
|
+
// successful sync with an unchanged catalog must not invalidate the query
|
|
192
|
+
// snapshot, scoped caches, or records cursors. The sync-health bump still
|
|
193
|
+
// rebuilds the snapshot cache so lastSuccessAt is not stale on the next
|
|
194
|
+
// full response.
|
|
195
|
+
if (String(result.catalog.catalogHash || '') !== previousHash || backfill.priced > 0) markStatsChanged('pricing')
|
|
196
|
+
else markStatsChanged('sync-health')
|
|
197
|
+
await persistPricing()
|
|
198
|
+
await drainLedgerWrites()
|
|
199
|
+
return { ok: true, skipped: false, backfill, pricing: pricingSnapshot() }
|
|
200
|
+
} finally {
|
|
201
|
+
state.pricingSyncInFlight = false
|
|
202
|
+
schedulePricingSync()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function backfillUnpricedCosts() {
|
|
206
|
+
let considered = 0
|
|
207
|
+
let priced = 0
|
|
208
|
+
const touched = new Set()
|
|
209
|
+
for (const item of state.usageByStep.values()) {
|
|
210
|
+
const oldCost = normalizeCostSnapshot(item.cost)
|
|
211
|
+
if (oldCost !== null && oldCost.pricingMode === 'official-model' && oldCost.status === 'priced') continue
|
|
212
|
+
considered += 1
|
|
213
|
+
const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId))
|
|
214
|
+
if (next.status !== 'priced') continue
|
|
215
|
+
if (oldCost !== null) {
|
|
216
|
+
adjustCostOnly(item, oldCost, -1)
|
|
217
|
+
adjustQueryCost(item, oldCost, -1)
|
|
218
|
+
}
|
|
219
|
+
item.cost = next
|
|
220
|
+
adjustCostOnly(item, next, 1)
|
|
221
|
+
adjustQueryCost(item, next, 1)
|
|
222
|
+
const record = state.ledgerRecords.get(item.sid)
|
|
223
|
+
if (record !== undefined) {
|
|
224
|
+
const stored = record.usage.find((candidate) => candidate.key === item.key)
|
|
225
|
+
if (stored !== undefined) { stored.cost = next; touched.add(record) }
|
|
226
|
+
}
|
|
227
|
+
priced += 1
|
|
228
|
+
}
|
|
229
|
+
for (const record of touched) {
|
|
230
|
+
record.version = LEDGER_VERSION
|
|
231
|
+
record.updatedAt = nextLedgerRevision()
|
|
232
|
+
// Backfilling prices must not clear a mixed-workspace upgrade flag: stay
|
|
233
|
+
// unfoldable until a rebuild normalizes every historical item.
|
|
234
|
+
const stillMixed = record.turns.some((turn) => turn && turn.workspaceId !== record.workspaceId) || record.usage.some((item) => item && item.workspaceId !== record.workspaceId)
|
|
235
|
+
record.needsUpgrade = stillMixed
|
|
236
|
+
void persistLedgerRecord(record)
|
|
237
|
+
}
|
|
238
|
+
let remaining = 0
|
|
239
|
+
for (const item of state.usageByStep.values()) {
|
|
240
|
+
const cost = normalizeCostSnapshot(item.cost)
|
|
241
|
+
if (cost === null || cost.status !== 'priced') remaining += 1
|
|
242
|
+
}
|
|
243
|
+
return { considered, priced, remaining }
|
|
244
|
+
}
|
|
245
|
+
function updatePricingState(raw, backfill) {
|
|
246
|
+
const input = raw && typeof raw === 'object' && raw.pricing && typeof raw.pricing === 'object' ? raw.pricing : raw
|
|
247
|
+
const current = serializePricingState(state.pricingState)
|
|
248
|
+
const merged = { ...current, ...(input && typeof input === 'object' ? input : {}) }
|
|
249
|
+
if (input && typeof input === 'object' && input.sync && typeof input.sync === 'object' && !Array.isArray(input.sync)) merged.sync = { ...current.sync, ...input.sync }
|
|
250
|
+
state.pricingState = normalizePricingState(merged)
|
|
251
|
+
state.pricingResolutionCache.clear()
|
|
252
|
+
const result = backfill === true ? backfillUnpricedCosts() : { considered: 0, priced: 0, remaining: state.totals.cost.unpricedCalls + state.totals.cost.ambiguousCalls + state.totals.cost.unsupportedCalls }
|
|
253
|
+
markStatsChanged('pricing')
|
|
254
|
+
schedulePricingSync()
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
function schedulePricingSync() {
|
|
258
|
+
if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
|
|
259
|
+
if (state.disposed || state.pricingState.sync.autoEnabled !== true) return
|
|
260
|
+
const elapsed = state.pricingState.sync.lastSuccessAt > 0 ? Date.now() - state.pricingState.sync.lastSuccessAt : state.pricingState.sync.intervalMs
|
|
261
|
+
const delay = Math.max(0, state.pricingState.sync.intervalMs - elapsed)
|
|
262
|
+
state.pricingSyncTimer = setTimeout(() => {
|
|
263
|
+
state.pricingSyncTimer = null
|
|
264
|
+
void syncPricing(true)
|
|
265
|
+
}, delay)
|
|
266
|
+
if (state.pricingSyncTimer && typeof state.pricingSyncTimer.unref === 'function') state.pricingSyncTimer.unref()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
adjustCostOnly,
|
|
272
|
+
usedPricingModels,
|
|
273
|
+
pricingModelSearch,
|
|
274
|
+
pricingSnapshot,
|
|
275
|
+
loadPricing,
|
|
276
|
+
persistPricing,
|
|
277
|
+
syncPricing,
|
|
278
|
+
backfillUnpricedCosts,
|
|
279
|
+
updatePricingState,
|
|
280
|
+
schedulePricingSync
|
|
281
|
+
}
|
|
282
|
+
}
|