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/index.js
CHANGED
|
@@ -1,2119 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
4
|
-
import { COST_SCHEMA_VERSION, addCostAggregate, calculateCost, createEmptyPricingState, decimalSubtract, fetchModelsDevCatalog, emptyCostAggregate, normalizeCostSnapshot, normalizePricingState, officialProviderIds, resolvePricing, serializeCostAggregate, serializePricingState } from './pricing.js'
|
|
5
|
-
|
|
6
|
-
const name = 'dsh-all-usage'
|
|
7
|
-
const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage']
|
|
8
|
-
|
|
9
|
-
// webServer route handlers do not inherit the connection API fence; keep this plugin
|
|
10
|
-
// local and require a browser-originated capability for state-changing reads/writes.
|
|
11
|
-
function requestHeader(req, name) {
|
|
12
|
-
const headers = req && req.headers
|
|
13
|
-
if (headers === null || headers === undefined || typeof headers !== 'object') return undefined
|
|
14
|
-
const value = headers[name.toLowerCase()]
|
|
15
|
-
if (Array.isArray(value)) return typeof value[0] === 'string' ? value[0] : undefined
|
|
16
|
-
return typeof value === 'string' ? value : undefined
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isLoopbackHostname(hostname) {
|
|
20
|
-
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
21
|
-
const parts = hostname.split('.')
|
|
22
|
-
return parts.length === 4 && parts[0] === '127' && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isTrustedLocalApiRequest(req, requireOrigin) {
|
|
26
|
-
const host = requestHeader(req, 'host')
|
|
27
|
-
if (host === undefined) return false
|
|
28
|
-
let hostUrl
|
|
29
|
-
try {
|
|
30
|
-
hostUrl = new URL('http://' + host)
|
|
31
|
-
} catch (err) {
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
if (!isLoopbackHostname(hostUrl.hostname)) return false
|
|
35
|
-
if (requestHeader(req, 'sec-fetch-site') === 'cross-site') return false
|
|
36
|
-
const origin = requestHeader(req, 'origin')
|
|
37
|
-
if (origin === undefined) return requireOrigin !== true
|
|
38
|
-
try {
|
|
39
|
-
const originUrl = new URL(origin)
|
|
40
|
-
return originUrl.protocol === 'http:' && originUrl.host === hostUrl.host
|
|
41
|
-
} catch (err) {
|
|
42
|
-
return false
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function hasWriteToken(req, expected) {
|
|
47
|
-
const actual = requestHeader(req, 'x-all-usage-request-token')
|
|
48
|
-
if (typeof actual !== 'string' || typeof expected !== 'string') return false
|
|
49
|
-
const actualBytes = Buffer.from(actual)
|
|
50
|
-
const expectedBytes = Buffer.from(expected)
|
|
51
|
-
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function sendJson(res, code, value) {
|
|
55
|
-
res.statusCode = code
|
|
56
|
-
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
57
|
-
res.setHeader('cache-control', 'no-store')
|
|
58
|
-
res.end(JSON.stringify(value))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function readBody(req, maxBytes) {
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
const chunks = []
|
|
64
|
-
const declaredLength = Number(requestHeader(req, 'content-length'))
|
|
65
|
-
let size = Number.isFinite(declaredLength) && declaredLength > maxBytes ? maxBytes + 1 : 0
|
|
66
|
-
let tooLarge = size > maxBytes
|
|
67
|
-
let settled = false
|
|
68
|
-
const finish = (text, oversized) => {
|
|
69
|
-
if (settled) return
|
|
70
|
-
settled = true
|
|
71
|
-
resolve({ text, tooLarge: oversized })
|
|
72
|
-
}
|
|
73
|
-
req.on('data', (chunk) => {
|
|
74
|
-
if (tooLarge) return
|
|
75
|
-
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
76
|
-
size += value.length
|
|
77
|
-
if (size > maxBytes) {
|
|
78
|
-
tooLarge = true
|
|
79
|
-
chunks.length = 0
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
chunks.push(value)
|
|
83
|
-
})
|
|
84
|
-
req.on('end', () => finish(tooLarge ? '' : Buffer.concat(chunks).toString('utf8'), tooLarge))
|
|
85
|
-
req.on('error', () => finish('', false))
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function apply(ctx) {
|
|
90
|
-
const credentials = ctx.get('credentials')
|
|
91
|
-
const settings = ctx.get('settings')
|
|
92
|
-
const storage = ctx.get('storage')
|
|
93
|
-
const webServer = ctx.get('webServer')
|
|
94
|
-
// v1.0.8: optional sessionPersistence exposes listSnapshots() — a cheap per-session
|
|
95
|
-
// revision (header line + stat, no full-log read) that lets the baseline skip
|
|
96
|
-
// re-reading unchanged sessions after a DSH restart.
|
|
97
|
-
const sessionPersistence = ctx.get('sessionPersistence')
|
|
98
|
-
|
|
99
|
-
// ---------- owned aggregation state ----------
|
|
100
|
-
const wsMeta = new Map()
|
|
101
|
-
const pathIndex = new Map()
|
|
102
|
-
const memberOf = new Map()
|
|
103
|
-
const byDay = new Map()
|
|
104
|
-
const byDayUtc = new Map()
|
|
105
|
-
const perWorkspace = new Map()
|
|
106
|
-
const perModel = new Map()
|
|
107
|
-
// One canonical usage contribution per session turn/step. This makes retries and
|
|
108
|
-
// replacement messages update a logical model call instead of double-counting it.
|
|
109
|
-
const usageByStep = new Map()
|
|
110
|
-
const turnRecords = new Map()
|
|
111
|
-
const usageByLocalDate = new Map()
|
|
112
|
-
const usageByUtcDate = new Map()
|
|
113
|
-
const turnsByLocalDate = new Map()
|
|
114
|
-
const turnsByUtcDate = new Map()
|
|
115
|
-
const sessionModel = new Map()
|
|
116
|
-
const totals = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
117
|
-
const sessionCount = new Set()
|
|
118
|
-
const sessionSeq = new Map()
|
|
119
|
-
const chains = new Map()
|
|
120
|
-
const liveResyncPending = new Set()
|
|
121
|
-
const liveResyncTimers = new Map()
|
|
122
|
-
const liveResyncAttempts = new Map()
|
|
123
|
-
const scan = { started: false, done: false, scanned: 0, total: 0, failed: 0 }
|
|
124
|
-
const aliases = {}
|
|
125
|
-
let kvUnit = null
|
|
126
|
-
let aliasWriteChain = Promise.resolve()
|
|
127
|
-
let aliasesReady = Promise.resolve()
|
|
128
|
-
let balanceCache = { fetchedAt: 0, payload: null }
|
|
129
|
-
const requestToken = randomBytes(32).toString('base64url')
|
|
130
|
-
// A non-secret identity distinguishes HMR/restart revision resets from a stale page.
|
|
131
|
-
const instanceId = randomBytes(12).toString('base64url')
|
|
132
|
-
let statsRevision = 0
|
|
133
|
-
let statsUpdatedAt = Date.now()
|
|
134
|
-
let statsDirtyScheduled = false
|
|
135
|
-
const sync = {
|
|
136
|
-
lastStartedAt: 0,
|
|
137
|
-
lastCompletedAt: 0,
|
|
138
|
-
lastErrorAt: 0,
|
|
139
|
-
lastErrorCode: null,
|
|
140
|
-
persistenceSnapshotsAvailable: false,
|
|
141
|
-
sessionsTotal: 0,
|
|
142
|
-
sessionsRead: 0,
|
|
143
|
-
sessionsSkippedByRevision: 0,
|
|
144
|
-
sessionsRestoredFromLedger: 0,
|
|
145
|
-
sessionsFailed: 0,
|
|
146
|
-
}
|
|
147
|
-
const ledgerRecords = new Map()
|
|
148
|
-
const queryCache = new Map()
|
|
149
|
-
const recordsQueryCache = new Map()
|
|
150
|
-
let snapshotCache = null
|
|
151
|
-
let ledgerUnit = null
|
|
152
|
-
let ledgerReady = Promise.resolve()
|
|
153
|
-
let ledgerWriteChain = Promise.resolve()
|
|
154
|
-
let pricingState = createEmptyPricingState()
|
|
155
|
-
const pricingResolutionCache = new Map()
|
|
156
|
-
let pricingUnit = null
|
|
157
|
-
let pricingReady = Promise.resolve()
|
|
158
|
-
let pricingWriteChain = Promise.resolve()
|
|
159
|
-
let pricingSyncInFlight = false
|
|
160
|
-
let pricingSyncTimer = null
|
|
161
|
-
const LEDGER_VERSION = 3
|
|
162
|
-
const PREVIOUS_LEDGER_VERSION = 2
|
|
163
|
-
const LEGACY_LEDGER_VERSION = 1
|
|
164
|
-
let ledgerRevision = Date.now()
|
|
165
|
-
let disposed = false
|
|
166
|
-
let baselineRetryDelay = 1000
|
|
167
|
-
let baselineRetryScheduled = false
|
|
168
|
-
let baselineFallbackTimer = null
|
|
169
|
-
const knownSessionIds = new Set()
|
|
170
|
-
let aggregationGeneration = 0
|
|
171
|
-
let reconcileHintScheduled = false
|
|
172
|
-
let reconcilePending = false
|
|
173
|
-
let reconcileInFlight = false
|
|
174
|
-
let reconcileTimer = null
|
|
175
|
-
const RECONCILE_INTERVAL_MS = 120000
|
|
176
|
-
const RECONCILE_HINT_DELAY_MS = 3000
|
|
177
|
-
|
|
178
|
-
function dayKey(ms) {
|
|
179
|
-
const d = new Date(ms)
|
|
180
|
-
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
|
181
|
-
}
|
|
182
|
-
function dayKeyUtc(ms) {
|
|
183
|
-
const d = new Date(ms)
|
|
184
|
-
return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + '-' + String(d.getUTCDate()).padStart(2, '0')
|
|
185
|
-
}
|
|
186
|
-
function dateKeys(ms) {
|
|
187
|
-
const d = new Date(ms)
|
|
188
|
-
const local = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
|
189
|
-
const utc = d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + '-' + String(d.getUTCDate()).padStart(2, '0')
|
|
190
|
-
return { local, utc }
|
|
191
|
-
}
|
|
192
|
-
function num(v) {
|
|
193
|
-
return typeof v === 'number' && Number.isFinite(v) ? v : 0
|
|
194
|
-
}
|
|
195
|
-
function validEventTime(value) {
|
|
196
|
-
return typeof value === 'number' && Number.isFinite(value)
|
|
197
|
-
}
|
|
198
|
-
function addDateIndex(index, date, key) {
|
|
199
|
-
if (typeof date !== 'string' || date === '') return
|
|
200
|
-
let keys = index.get(date)
|
|
201
|
-
if (keys === undefined) { keys = new Set(); index.set(date, keys) }
|
|
202
|
-
keys.add(key)
|
|
203
|
-
}
|
|
204
|
-
function removeDateIndex(index, date, key) {
|
|
205
|
-
if (typeof date !== 'string' || date === '') return
|
|
206
|
-
const keys = index.get(date)
|
|
207
|
-
if (keys === undefined) return
|
|
208
|
-
keys.delete(key)
|
|
209
|
-
if (keys.size === 0) index.delete(date)
|
|
210
|
-
}
|
|
211
|
-
function indexUsage(item) {
|
|
212
|
-
addDateIndex(usageByLocalDate, item.date, item.key)
|
|
213
|
-
addDateIndex(usageByUtcDate, item.dateUtc, item.key)
|
|
214
|
-
}
|
|
215
|
-
function unindexUsage(item) {
|
|
216
|
-
removeDateIndex(usageByLocalDate, item.date, item.key)
|
|
217
|
-
removeDateIndex(usageByUtcDate, item.dateUtc, item.key)
|
|
218
|
-
}
|
|
219
|
-
function indexTurn(turn) {
|
|
220
|
-
addDateIndex(turnsByLocalDate, turn.date, turn.key)
|
|
221
|
-
addDateIndex(turnsByUtcDate, turn.dateUtc, turn.key)
|
|
222
|
-
}
|
|
223
|
-
function indexedEntries(index, source, scope, heatStart, today) {
|
|
224
|
-
const result = []
|
|
225
|
-
for (const [date, keys] of index) {
|
|
226
|
-
if (!dateInScope(date, scope) && (date < heatStart || date > today)) continue
|
|
227
|
-
for (const key of keys) {
|
|
228
|
-
const item = source.get(key)
|
|
229
|
-
if (item !== undefined) result.push({ item, date })
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return result
|
|
233
|
-
}
|
|
234
|
-
function indexedEntriesInRange(index, source, start, end) {
|
|
235
|
-
const result = []
|
|
236
|
-
for (const [date, keys] of index) {
|
|
237
|
-
if (date < start || date > end) continue
|
|
238
|
-
for (const key of keys) {
|
|
239
|
-
const item = source.get(key)
|
|
240
|
-
if (item !== undefined) result.push({ item, date })
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return result
|
|
244
|
-
}
|
|
245
|
-
function usageBasisEqual(first, identity, values) {
|
|
246
|
-
if (first === null || first === undefined) return false
|
|
247
|
-
const firstIdentity = first.identity || first.modelId
|
|
248
|
-
const left = coerceIdentity(firstIdentity)
|
|
249
|
-
const right = coerceIdentity(identity)
|
|
250
|
-
if (left.identityKey !== right.identityKey) return false
|
|
251
|
-
return ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning'].every((key) => num(first.values && first.values[key]) === num(values && values[key]))
|
|
252
|
-
}
|
|
253
|
-
function resolveCurrentPricing(identity) {
|
|
254
|
-
const normalized = coerceIdentity(identity)
|
|
255
|
-
const key = normalized.identityKey
|
|
256
|
-
const cached = pricingResolutionCache.get(key)
|
|
257
|
-
if (cached !== undefined) return cached
|
|
258
|
-
const resolved = resolvePricing(normalized, pricingState)
|
|
259
|
-
pricingResolutionCache.set(key, resolved)
|
|
260
|
-
while (pricingResolutionCache.size > 5000) pricingResolutionCache.delete(pricingResolutionCache.keys().next().value)
|
|
261
|
-
return resolved
|
|
262
|
-
}
|
|
263
|
-
function costForUsage(values, identity, previous) {
|
|
264
|
-
const previousCost = normalizeCostSnapshot(previous && previous.cost)
|
|
265
|
-
if (previousCost !== null && previousCost.pricingMode === 'official-model' && usageBasisEqual(previous, identity, values)) return previousCost
|
|
266
|
-
return calculateCost(values, resolveCurrentPricing(identity))
|
|
267
|
-
}
|
|
268
|
-
// Coalesce synchronous aggregation writes without adding a timer lifecycle.
|
|
269
|
-
function commitPendingStats() {
|
|
270
|
-
if (!statsDirtyScheduled) return
|
|
271
|
-
statsDirtyScheduled = false
|
|
272
|
-
if (disposed) return
|
|
273
|
-
statsRevision += 1
|
|
274
|
-
statsUpdatedAt = Date.now()
|
|
275
|
-
}
|
|
276
|
-
function markStatsChanged() {
|
|
277
|
-
snapshotCache = null
|
|
278
|
-
recordsQueryCache.clear()
|
|
279
|
-
if (disposed || statsDirtyScheduled) return
|
|
280
|
-
statsDirtyScheduled = true
|
|
281
|
-
if (typeof queueMicrotask === 'function') queueMicrotask(commitPendingStats)
|
|
282
|
-
else Promise.resolve().then(commitPendingStats)
|
|
283
|
-
}
|
|
284
|
-
function resetSyncState() {
|
|
285
|
-
sync.lastStartedAt = 0
|
|
286
|
-
sync.lastCompletedAt = 0
|
|
287
|
-
sync.lastErrorAt = 0
|
|
288
|
-
sync.lastErrorCode = null
|
|
289
|
-
sync.persistenceSnapshotsAvailable = false
|
|
290
|
-
sync.sessionsTotal = 0
|
|
291
|
-
sync.sessionsRead = 0
|
|
292
|
-
sync.sessionsSkippedByRevision = 0
|
|
293
|
-
sync.sessionsRestoredFromLedger = 0
|
|
294
|
-
sync.sessionsFailed = 0
|
|
295
|
-
}
|
|
296
|
-
function beginSync() {
|
|
297
|
-
resetSyncState()
|
|
298
|
-
sync.lastStartedAt = Date.now()
|
|
299
|
-
markStatsChanged()
|
|
300
|
-
}
|
|
301
|
-
function noteSyncError(code) {
|
|
302
|
-
sync.lastErrorAt = Date.now()
|
|
303
|
-
sync.lastErrorCode = code
|
|
304
|
-
markStatsChanged()
|
|
305
|
-
}
|
|
306
|
-
function syncSnapshot() {
|
|
307
|
-
return {
|
|
308
|
-
lastStartedAt: sync.lastStartedAt,
|
|
309
|
-
lastCompletedAt: sync.lastCompletedAt,
|
|
310
|
-
lastErrorAt: sync.lastErrorAt,
|
|
311
|
-
lastErrorCode: sync.lastErrorCode,
|
|
312
|
-
persistenceSnapshotsAvailable: sync.persistenceSnapshotsAvailable,
|
|
313
|
-
sessionsTotal: sync.sessionsTotal,
|
|
314
|
-
sessionsRead: sync.sessionsRead,
|
|
315
|
-
sessionsSkippedByRevision: sync.sessionsSkippedByRevision,
|
|
316
|
-
sessionsRestoredFromLedger: sync.sessionsRestoredFromLedger,
|
|
317
|
-
sessionsFailed: sync.sessionsFailed,
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function safeContextTimeout(ms) {
|
|
321
|
-
if (disposed) return false
|
|
322
|
-
try {
|
|
323
|
-
await ctx.timeout(ms)
|
|
324
|
-
return !disposed
|
|
325
|
-
} catch (err) {
|
|
326
|
-
if (!disposed) console.error('[all-usage] context timer unavailable:', err)
|
|
327
|
-
return false
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function resetAggregationState() {
|
|
331
|
-
aggregationGeneration += 1
|
|
332
|
-
wsMeta.clear()
|
|
333
|
-
pathIndex.clear()
|
|
334
|
-
memberOf.clear()
|
|
335
|
-
byDay.clear()
|
|
336
|
-
byDayUtc.clear()
|
|
337
|
-
perWorkspace.clear()
|
|
338
|
-
perModel.clear()
|
|
339
|
-
usageByStep.clear()
|
|
340
|
-
turnRecords.clear()
|
|
341
|
-
usageByLocalDate.clear()
|
|
342
|
-
usageByUtcDate.clear()
|
|
343
|
-
turnsByLocalDate.clear()
|
|
344
|
-
turnsByUtcDate.clear()
|
|
345
|
-
for (const timer of liveResyncTimers.values()) clearTimeout(timer)
|
|
346
|
-
liveResyncTimers.clear()
|
|
347
|
-
liveResyncAttempts.clear()
|
|
348
|
-
liveResyncPending.clear()
|
|
349
|
-
reconcileHintScheduled = false
|
|
350
|
-
if (baselineFallbackTimer !== null) {
|
|
351
|
-
clearTimeout(baselineFallbackTimer)
|
|
352
|
-
baselineFallbackTimer = null
|
|
353
|
-
}
|
|
354
|
-
baselineRetryScheduled = false
|
|
355
|
-
queryCache.clear()
|
|
356
|
-
recordsQueryCache.clear()
|
|
357
|
-
sessionModel.clear()
|
|
358
|
-
sessionCount.clear()
|
|
359
|
-
sessionSeq.clear()
|
|
360
|
-
chains.clear()
|
|
361
|
-
knownSessionIds.clear()
|
|
362
|
-
totals.turns = 0
|
|
363
|
-
totals.input = 0
|
|
364
|
-
totals.output = 0
|
|
365
|
-
totals.cacheRead = 0
|
|
366
|
-
totals.cacheWrite = 0
|
|
367
|
-
totals.reasoning = 0
|
|
368
|
-
Object.assign(totals.cost, emptyCostAggregate())
|
|
369
|
-
scan.started = false
|
|
370
|
-
scan.done = false
|
|
371
|
-
scan.scanned = 0
|
|
372
|
-
scan.total = 0
|
|
373
|
-
scan.failed = 0
|
|
374
|
-
resetSyncState()
|
|
375
|
-
baselineRetryDelay = 1000
|
|
376
|
-
markStatsChanged()
|
|
377
|
-
return aggregationGeneration
|
|
378
|
-
}
|
|
379
|
-
function ensureDay(dayMap, date) {
|
|
380
|
-
let day = dayMap.get(date)
|
|
381
|
-
if (day === undefined) {
|
|
382
|
-
day = { turns: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }, cost: emptyCostAggregate(), perWs: new Map(), byWs: new Map(), byModel: new Map(), sessionIds: new Set(), sessionRefs: new Map() }
|
|
383
|
-
dayMap.set(date, day)
|
|
384
|
-
}
|
|
385
|
-
return day
|
|
386
|
-
}
|
|
387
|
-
function ensureWs(wsId) {
|
|
388
|
-
let ws = perWorkspace.get(wsId)
|
|
389
|
-
if (ws === undefined) {
|
|
390
|
-
ws = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
391
|
-
perWorkspace.set(wsId, ws)
|
|
392
|
-
}
|
|
393
|
-
return ws
|
|
394
|
-
}
|
|
395
|
-
function ensureDayWs(day, wsId) {
|
|
396
|
-
let w = day.byWs.get(wsId)
|
|
397
|
-
if (w === undefined) {
|
|
398
|
-
w = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
399
|
-
day.byWs.set(wsId, w)
|
|
400
|
-
}
|
|
401
|
-
return w
|
|
402
|
-
}
|
|
403
|
-
function ensureModel(value) {
|
|
404
|
-
const identity = coerceIdentity(value)
|
|
405
|
-
let item = perModel.get(identity.identityKey)
|
|
406
|
-
if (item === undefined) {
|
|
407
|
-
item = { ...identity, model: identity.label, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
408
|
-
perModel.set(identity.identityKey, item)
|
|
409
|
-
}
|
|
410
|
-
return item
|
|
411
|
-
}
|
|
412
|
-
function ensureDayModel(day, value) {
|
|
413
|
-
const identity = coerceIdentity(value)
|
|
414
|
-
let item = day.byModel.get(identity.identityKey)
|
|
415
|
-
if (item === undefined) {
|
|
416
|
-
item = { ...identity, model: identity.label, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
417
|
-
day.byModel.set(identity.identityKey, item)
|
|
418
|
-
}
|
|
419
|
-
return item
|
|
420
|
-
}
|
|
421
|
-
function usageValues(usage) {
|
|
422
|
-
return {
|
|
423
|
-
input: num(usage && usage.inputTokens),
|
|
424
|
-
output: num(usage && usage.outputTokens),
|
|
425
|
-
cacheRead: num(usage && usage.cacheReadTokens),
|
|
426
|
-
cacheWrite: num(usage && usage.cacheWriteTokens),
|
|
427
|
-
reasoning: num(usage && usage.reasoningTokens),
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
function adjustValues(target, values, direction) {
|
|
431
|
-
target.input += values.input * direction
|
|
432
|
-
target.output += values.output * direction
|
|
433
|
-
target.cacheRead += values.cacheRead * direction
|
|
434
|
-
target.cacheWrite += values.cacheWrite * direction
|
|
435
|
-
target.reasoning += values.reasoning * direction
|
|
436
|
-
}
|
|
437
|
-
function noValues(target) {
|
|
438
|
-
return target.input === 0 && target.output === 0 && target.cacheRead === 0 && target.cacheWrite === 0 && target.reasoning === 0
|
|
439
|
-
}
|
|
440
|
-
function adjustDaySession(day, sid, direction) {
|
|
441
|
-
if (sid === undefined || sid === null) return
|
|
442
|
-
const current = day.sessionRefs.get(sid) || 0
|
|
443
|
-
const next = current + direction
|
|
444
|
-
if (next > 0) {
|
|
445
|
-
day.sessionRefs.set(sid, next)
|
|
446
|
-
day.sessionIds.add(sid)
|
|
447
|
-
} else {
|
|
448
|
-
day.sessionRefs.delete(sid)
|
|
449
|
-
day.sessionIds.delete(sid)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
function adjustDay(dayMap, date, wsId, values, identity, direction, sid, cost) {
|
|
453
|
-
const day = ensureDay(dayMap, date)
|
|
454
|
-
adjustDaySession(day, sid, direction)
|
|
455
|
-
adjustValues(day.tokens, values, direction)
|
|
456
|
-
addCostAggregateDirection(day.cost, cost, direction)
|
|
457
|
-
const dayWs = ensureDayWs(day, wsId)
|
|
458
|
-
adjustValues(dayWs, values, direction)
|
|
459
|
-
addCostAggregateDirection(dayWs.cost, cost, direction)
|
|
460
|
-
if (noValues(dayWs)) day.byWs.delete(wsId)
|
|
461
|
-
const dayModel = ensureDayModel(day, identity)
|
|
462
|
-
dayModel.calls += direction
|
|
463
|
-
adjustValues(dayModel, values, direction)
|
|
464
|
-
addCostAggregateDirection(dayModel.cost, cost, direction)
|
|
465
|
-
if (dayModel.calls === 0 && noValues(dayModel)) day.byModel.delete(dayModel.identityKey)
|
|
466
|
-
}
|
|
467
|
-
function addCostAggregateDirection(target, cost, direction) {
|
|
468
|
-
if (direction === 1) {
|
|
469
|
-
addCostAggregate(target, cost)
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
if (direction !== -1) return
|
|
473
|
-
const status = cost && typeof cost.status === 'string' ? cost.status : 'unpriced'
|
|
474
|
-
if (status === 'priced') {
|
|
475
|
-
target.input = decimalSubtract(target.input, cost.breakdown && cost.breakdown.input)
|
|
476
|
-
target.output = decimalSubtract(target.output, cost.breakdown && cost.breakdown.output)
|
|
477
|
-
target.cacheRead = decimalSubtract(target.cacheRead, cost.breakdown && cost.breakdown.cacheRead)
|
|
478
|
-
target.cacheWrite = decimalSubtract(target.cacheWrite, cost.breakdown && cost.breakdown.cacheWrite)
|
|
479
|
-
target.baseTotal = decimalSubtract(target.baseTotal, cost.baseTotal)
|
|
480
|
-
target.total = decimalSubtract(target.total, cost.total)
|
|
481
|
-
target.pricedCalls -= 1
|
|
482
|
-
} else if (status === 'ambiguous') target.ambiguousCalls -= 1
|
|
483
|
-
else if (status === 'unsupported') target.unsupportedCalls -= 1
|
|
484
|
-
else target.unpricedCalls -= 1
|
|
485
|
-
}
|
|
486
|
-
function adjustUsage(wsId, time, values, identity, direction, sid, cachedDates, cost) {
|
|
487
|
-
const normalized = coerceIdentity(identity)
|
|
488
|
-
const dates = cachedDates && typeof cachedDates.local === 'string' && typeof cachedDates.utc === 'string' ? cachedDates : dateKeys(time)
|
|
489
|
-
const modelTotals = ensureModel(normalized)
|
|
490
|
-
modelTotals.calls += direction
|
|
491
|
-
adjustValues(modelTotals, values, direction)
|
|
492
|
-
addCostAggregateDirection(modelTotals.cost, cost, direction)
|
|
493
|
-
if (modelTotals.calls === 0 && noValues(modelTotals)) perModel.delete(normalized.identityKey)
|
|
494
|
-
adjustValues(totals, values, direction)
|
|
495
|
-
addCostAggregateDirection(totals.cost, cost, direction)
|
|
496
|
-
const ws = ensureWs(wsId)
|
|
497
|
-
adjustValues(ws, values, direction)
|
|
498
|
-
addCostAggregateDirection(ws.cost, cost, direction)
|
|
499
|
-
adjustDay(byDay, dates.local, wsId, values, normalized, direction, sid, cost)
|
|
500
|
-
adjustDay(byDayUtc, dates.utc, wsId, values, normalized, direction, sid, cost)
|
|
501
|
-
}
|
|
502
|
-
function usageStepKey(sid, data, seq, fallback) {
|
|
503
|
-
const turn = data && typeof data.turn === 'number' ? data.turn : null
|
|
504
|
-
const step = data && typeof data.step === 'number' ? data.step : null
|
|
505
|
-
if (turn !== null && step !== null) return sid + ':step:' + turn + ':' + step
|
|
506
|
-
if (typeof seq === 'number') return sid + ':event:' + seq
|
|
507
|
-
return sid + ':event:' + (fallback === undefined ? JSON.stringify(data || {}) : String(fallback))
|
|
508
|
-
}
|
|
509
|
-
function addUsage(wsId, time, usage, model, sid, data, seq, materialization = 'live') {
|
|
510
|
-
if (!validEventTime(time)) return
|
|
511
|
-
const values = usageValues(usage)
|
|
512
|
-
const identity = coerceIdentity(model)
|
|
513
|
-
const eventSeq = typeof seq === 'number' ? seq : -1
|
|
514
|
-
// v1.0.7: a usage event carrying no billable token in any bucket must not add a
|
|
515
|
-
// meaningless row nor wipe previously recorded real usage (cc-switch
|
|
516
|
-
// has_billable_tokens parity). Pure cache-read requests are billable and pass.
|
|
517
|
-
if (noValues(values)) return
|
|
518
|
-
const dates = dateKeys(time)
|
|
519
|
-
const key = usageStepKey(sid, data, seq)
|
|
520
|
-
const previous = usageByStep.get(key)
|
|
521
|
-
// A late replay of an older raw event cannot replace the canonical later step.
|
|
522
|
-
if (previous !== undefined && eventSeq >= 0 && previous.seq > eventSeq) return
|
|
523
|
-
if (previous !== undefined) {
|
|
524
|
-
unindexUsage(previous)
|
|
525
|
-
adjustUsage(previous.wsId, previous.time, previous.values, previous.identity || previous.modelId, -1, previous.sid, { local: previous.date, utc: previous.dateUtc }, previous.cost)
|
|
526
|
-
}
|
|
527
|
-
const cost = costForUsage(values, identity, previous)
|
|
528
|
-
const next = { key, seq: eventSeq, wsId, time, date: dates.local, dateUtc: dates.utc, values, identity, modelId: identity.label, cost, turn: data && typeof data.turn === 'number' ? data.turn : null, step: data && typeof data.step === 'number' ? data.step : null, materialization, sid }
|
|
529
|
-
usageByStep.set(key, next)
|
|
530
|
-
indexUsage(next)
|
|
531
|
-
adjustUsage(wsId, time, values, identity, 1, sid, dates, cost)
|
|
532
|
-
markStatsChanged()
|
|
533
|
-
}
|
|
534
|
-
function addDayTurn(dayMap, date, wsId, sid) {
|
|
535
|
-
const day = ensureDay(dayMap, date)
|
|
536
|
-
adjustDaySession(day, sid, 1)
|
|
537
|
-
day.turns += 1
|
|
538
|
-
day.perWs.set(wsId, (day.perWs.get(wsId) || 0) + 1)
|
|
539
|
-
}
|
|
540
|
-
function turnRecordKey(sid, turn, seq, time) {
|
|
541
|
-
if (typeof turn === 'number' && Number.isFinite(turn)) return sid + ':turn:' + turn
|
|
542
|
-
if (typeof seq === 'number' && Number.isFinite(seq)) return sid + ':event:' + seq
|
|
543
|
-
return sid + ':time:' + String(time)
|
|
544
|
-
}
|
|
545
|
-
function addTurn(wsId, time, sid, turn, identity, materialization = 'live', seq) {
|
|
546
|
-
if (!validEventTime(time)) return
|
|
547
|
-
const key = turnRecordKey(sid, turn, seq, time)
|
|
548
|
-
if (turnRecords.has(key)) return
|
|
549
|
-
const normalized = coerceIdentity(identity)
|
|
550
|
-
const dates = dateKeys(time)
|
|
551
|
-
const record = { key, sid, wsId, time, date: dates.local, dateUtc: dates.utc, turn: typeof turn === 'number' ? turn : null, identity: normalized, materialization }
|
|
552
|
-
turnRecords.set(key, record)
|
|
553
|
-
indexTurn(record)
|
|
554
|
-
ensureWs(wsId).turns += 1
|
|
555
|
-
totals.turns += 1
|
|
556
|
-
addDayTurn(byDay, dates.local, wsId, sid)
|
|
557
|
-
addDayTurn(byDayUtc, dates.utc, wsId, sid)
|
|
558
|
-
markStatsChanged()
|
|
559
|
-
}
|
|
560
|
-
const UNKNOWN_MODEL_LABEL = '未知模型(历史记录缺少路由)'
|
|
561
|
-
function textOrNull(value) {
|
|
562
|
-
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null
|
|
563
|
-
}
|
|
564
|
-
function identityLabel(provider, requestedModel, actualModel, legacyLabel) {
|
|
565
|
-
const model = actualModel || requestedModel
|
|
566
|
-
if (model !== null) return provider === null ? model : provider + ' / ' + model
|
|
567
|
-
return legacyLabel || UNKNOWN_MODEL_LABEL
|
|
568
|
-
}
|
|
569
|
-
function makeIdentity(provider, requestedModel, actualModel, legacyLabel) {
|
|
570
|
-
const normalizedProvider = textOrNull(provider)
|
|
571
|
-
const normalizedRequested = textOrNull(requestedModel)
|
|
572
|
-
const normalizedActual = textOrNull(actualModel)
|
|
573
|
-
const normalizedLegacy = textOrNull(legacyLabel)
|
|
574
|
-
const key = JSON.stringify([normalizedProvider, normalizedRequested, normalizedActual, normalizedLegacy])
|
|
575
|
-
return {
|
|
576
|
-
identityKey: key,
|
|
577
|
-
provider: normalizedProvider,
|
|
578
|
-
requestedModel: normalizedRequested,
|
|
579
|
-
actualModel: normalizedActual,
|
|
580
|
-
label: identityLabel(normalizedProvider, normalizedRequested, normalizedActual, normalizedLegacy),
|
|
581
|
-
legacy: normalizedLegacy !== null && normalizedProvider === null && normalizedRequested === null && normalizedActual === null,
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
function identityFromLegacy(label) {
|
|
585
|
-
return makeIdentity(null, null, null, textOrNull(label) || UNKNOWN_MODEL_LABEL)
|
|
586
|
-
}
|
|
587
|
-
function isCanonicalIdentity(value) {
|
|
588
|
-
return value !== null && typeof value === 'object' && typeof value.identityKey === 'string' && typeof value.label === 'string' && (value.provider === null || typeof value.provider === 'string') && (value.requestedModel === null || typeof value.requestedModel === 'string') && (value.actualModel === null || typeof value.actualModel === 'string') && typeof value.legacy === 'boolean'
|
|
589
|
-
}
|
|
590
|
-
function coerceIdentity(value) {
|
|
591
|
-
if (isCanonicalIdentity(value)) return value
|
|
592
|
-
if (value !== null && typeof value === 'object') {
|
|
593
|
-
return makeIdentity(value.provider, value.requestedModel, value.actualModel, value.legacyLabel || (value.legacy === true ? value.label : null))
|
|
594
|
-
}
|
|
595
|
-
if (typeof value === 'string' && value !== '') return identityFromLegacy(value)
|
|
596
|
-
return makeIdentity(null, null, null, UNKNOWN_MODEL_LABEL)
|
|
597
|
-
}
|
|
598
|
-
function routeObject(data) {
|
|
599
|
-
if (data && typeof data === 'object' && (data.provider !== undefined || data.model !== undefined)) return data
|
|
600
|
-
const config = data && data.header && data.header.config
|
|
601
|
-
return config && typeof config === 'object' ? config : null
|
|
602
|
-
}
|
|
603
|
-
function identityFromRoute(data, fallback) {
|
|
604
|
-
const base = fallback === undefined ? makeIdentity(null, null, null, null) : coerceIdentity(fallback)
|
|
605
|
-
const route = routeObject(data)
|
|
606
|
-
if (route === null) return base
|
|
607
|
-
return makeIdentity(route.provider === undefined ? base.provider : route.provider, route.model === undefined ? base.requestedModel : route.model, null, base.legacy ? base.label : null)
|
|
608
|
-
}
|
|
609
|
-
function identityFromMessage(data, fallback) {
|
|
610
|
-
const base = fallback === undefined ? makeIdentity(null, null, null, null) : coerceIdentity(fallback)
|
|
611
|
-
const source = data && data.message && data.message.source
|
|
612
|
-
if (source === null || typeof source !== 'object') return base
|
|
613
|
-
return makeIdentity(source.provider === undefined ? base.provider : source.provider, base.requestedModel || source.model, source.model, base.legacy ? base.label : null)
|
|
614
|
-
}
|
|
615
|
-
function modelFromRoute(data) {
|
|
616
|
-
const identity = identityFromRoute(data)
|
|
617
|
-
return identity.label === UNKNOWN_MODEL_LABEL ? undefined : identity.label
|
|
618
|
-
}
|
|
619
|
-
function modelFromMessage(data, fallback) {
|
|
620
|
-
return identityFromMessage(data, fallback).label
|
|
621
|
-
}
|
|
622
|
-
function nextLedgerRevision() {
|
|
623
|
-
ledgerRevision = Math.max(ledgerRevision + 1, Date.now())
|
|
624
|
-
return ledgerRevision
|
|
625
|
-
}
|
|
626
|
-
function ledgerEventKey(event, index) {
|
|
627
|
-
return typeof event.seq === 'number' ? String(event.seq) : 'event:' + index
|
|
628
|
-
}
|
|
629
|
-
function buildLedgerRecord(session, workspaceId, source = 'scan', revision, previousRecord) {
|
|
630
|
-
const sid = session && typeof session.id === 'string' ? session.id : ''
|
|
631
|
-
const events = session && Array.isArray(session.events) ? session.events : []
|
|
632
|
-
if (sid === '' || workspaceId === undefined) return null
|
|
633
|
-
const turns = new Map()
|
|
634
|
-
const usage = new Map()
|
|
635
|
-
const previousUsage = new Map(Array.isArray(previousRecord && previousRecord.usage) ? previousRecord.usage.map((item) => [item.key, item]) : [])
|
|
636
|
-
let currentIdentity = makeIdentity(null, null, null, null)
|
|
637
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
638
|
-
const event = events[index]
|
|
639
|
-
if (event === null || typeof event !== 'object') continue
|
|
640
|
-
const data = event.data
|
|
641
|
-
if (event.type === 'request/context' || event.type === 'request/header') {
|
|
642
|
-
currentIdentity = identityFromRoute(data, currentIdentity)
|
|
643
|
-
continue
|
|
644
|
-
}
|
|
645
|
-
if (event.type === 'turn/end') {
|
|
646
|
-
if (!validEventTime(event.time)) continue
|
|
647
|
-
const key = ledgerEventKey(event, index)
|
|
648
|
-
const turn = data && typeof data.turn === 'number' ? data.turn : null
|
|
649
|
-
turns.set(key, { key, seq: typeof event.seq === 'number' ? event.seq : -1, time: event.time, workspaceId, turn, identity: currentIdentity })
|
|
650
|
-
continue
|
|
651
|
-
}
|
|
652
|
-
if (event.type !== 'assistant/message' || data === null || typeof data !== 'object' || data.usage === undefined || !validEventTime(event.time)) continue
|
|
653
|
-
const values = usageValues(data.usage)
|
|
654
|
-
// v1.0.7: all-zero usage rows carry no billable tokens and stay out of the
|
|
655
|
-
// durable ledger (cc-switch has_billable_tokens parity).
|
|
656
|
-
if (noValues(values)) continue
|
|
657
|
-
const identity = identityFromMessage(data, currentIdentity)
|
|
658
|
-
const eventSeq = typeof event.seq === 'number' ? event.seq : -1
|
|
659
|
-
const key = usageStepKey(sid, data, event.seq, index)
|
|
660
|
-
const previous = usage.get(key)
|
|
661
|
-
if (previous !== undefined && eventSeq >= 0 && previous.seq > eventSeq) continue
|
|
662
|
-
const previousItem = previousUsage.get(key)
|
|
663
|
-
const previousCost = normalizeCostSnapshot(previousItem && previousItem.cost)
|
|
664
|
-
const cost = previousItem !== undefined && previousCost !== null && previousCost.pricingMode === 'official-model' && usageBasisEqual(previousItem, identity, values) ? previousCost : calculateCost(values, resolveCurrentPricing(identity))
|
|
665
|
-
usage.set(key, {
|
|
666
|
-
key,
|
|
667
|
-
seq: eventSeq,
|
|
668
|
-
time: event.time,
|
|
669
|
-
workspaceId,
|
|
670
|
-
identity,
|
|
671
|
-
modelId: identity.label,
|
|
672
|
-
cost,
|
|
673
|
-
turn: data && typeof data.turn === 'number' ? data.turn : null,
|
|
674
|
-
step: data && typeof data.step === 'number' ? data.step : null,
|
|
675
|
-
values,
|
|
676
|
-
})
|
|
677
|
-
currentIdentity = identity
|
|
678
|
-
}
|
|
679
|
-
return { version: LEDGER_VERSION, sessionId: sid, workspaceId, lastSeq: lastSeqOf(events), source, updatedAt: nextLedgerRevision(), lastRevision: typeof revision === 'string' ? revision : undefined, sourceRevision: typeof revision === 'string' ? revision : undefined, lastIdentity: currentIdentity, turns: Array.from(turns.values()), usage: Array.from(usage.values()) }
|
|
680
|
-
}
|
|
681
|
-
function normalizeLedgerRecord(raw, key) {
|
|
682
|
-
if (raw === null || typeof raw !== 'object' || (raw.version !== LEDGER_VERSION && raw.version !== PREVIOUS_LEDGER_VERSION && raw.version !== LEGACY_LEDGER_VERSION) || typeof raw.sessionId !== 'string' || raw.sessionId !== key) return null
|
|
683
|
-
if (!Array.isArray(raw.turns) || !Array.isArray(raw.usage)) return null
|
|
684
|
-
const needsUpgrade = raw.version !== LEDGER_VERSION || raw.usage.some((item) => { const cost = normalizeCostSnapshot(item && item.cost); return item === null || typeof item !== 'object' || item.identity === undefined || cost === null || cost.pricingMode !== 'official-model' })
|
|
685
|
-
const turnMap = new Map()
|
|
686
|
-
for (const turn of raw.turns) {
|
|
687
|
-
if (turn && typeof turn.key === 'string' && turn.workspaceId !== undefined && Number.isFinite(turn.time)) turnMap.set(turn.key, { key: turn.key, seq: typeof turn.seq === 'number' ? turn.seq : -1, time: turn.time, workspaceId: turn.workspaceId, turn: typeof turn.turn === 'number' ? turn.turn : null, identity: coerceIdentity(turn.identity || turn.modelId) })
|
|
688
|
-
}
|
|
689
|
-
const usageMap = new Map()
|
|
690
|
-
for (const item of raw.usage) {
|
|
691
|
-
if (!item || typeof item.key !== 'string' || item.workspaceId === undefined || !Number.isFinite(item.time) || item.values === null || typeof item.values !== 'object') continue
|
|
692
|
-
const identity = coerceIdentity(item.identity || item.modelId)
|
|
693
|
-
const normalizedCost = normalizeCostSnapshot(item.cost)
|
|
694
|
-
const normalized = { key: item.key, seq: typeof item.seq === 'number' ? item.seq : -1, time: item.time, workspaceId: item.workspaceId, identity, modelId: identity.label, ...(normalizedCost === null ? {} : { cost: normalizedCost }), turn: typeof item.turn === 'number' ? item.turn : null, step: typeof item.step === 'number' ? item.step : null, values: usageValues({ inputTokens: item.values.input, outputTokens: item.values.output, cacheReadTokens: item.values.cacheRead, cacheWriteTokens: item.values.cacheWrite, reasoningTokens: item.values.reasoning }) }
|
|
695
|
-
const previous = usageMap.get(normalized.key)
|
|
696
|
-
if (previous === undefined || previous.seq <= normalized.seq) usageMap.set(normalized.key, normalized)
|
|
697
|
-
}
|
|
698
|
-
const updatedAt = typeof raw.updatedAt === 'number' ? raw.updatedAt : 0
|
|
699
|
-
ledgerRevision = Math.max(ledgerRevision, updatedAt)
|
|
700
|
-
const lastUsage = Array.from(usageMap.values()).at(-1)
|
|
701
|
-
const lastIdentity = coerceIdentity(raw.lastIdentity || raw.sourceRevisionIdentity || (lastUsage && lastUsage.identity))
|
|
702
|
-
const normalizedRecord = { version: LEDGER_VERSION, sessionId: raw.sessionId, workspaceId: raw.workspaceId, lastSeq: typeof raw.lastSeq === 'number' ? raw.lastSeq : -1, source: raw.source === 'flush' ? 'flush' : 'scan', updatedAt, lastRevision: typeof raw.lastRevision === 'string' ? raw.lastRevision : (typeof raw.sourceRevision === 'string' ? raw.sourceRevision : undefined), sourceRevision: typeof raw.sourceRevision === 'string' ? raw.sourceRevision : (typeof raw.lastRevision === 'string' ? raw.lastRevision : undefined), lastIdentity, turns: Array.from(turnMap.values()), usage: Array.from(usageMap.values()) }
|
|
703
|
-
Object.defineProperty(normalizedRecord, 'needsUpgrade', { value: needsUpgrade, enumerable: false, writable: true })
|
|
704
|
-
return normalizedRecord
|
|
705
|
-
}
|
|
706
|
-
function prepareLedgerRecord(record) {
|
|
707
|
-
if (record === null || record === undefined) return record
|
|
708
|
-
let changed = record.needsUpgrade === true || record.version !== LEDGER_VERSION
|
|
709
|
-
for (const item of record.usage) {
|
|
710
|
-
const cost = normalizeCostSnapshot(item.cost)
|
|
711
|
-
if (cost !== null && cost.pricingMode === 'official-model') { item.cost = cost; continue }
|
|
712
|
-
const identity = item.identity || identityFromLegacy(item.modelId)
|
|
713
|
-
item.cost = calculateCost(item.values, resolveCurrentPricing(identity))
|
|
714
|
-
changed = true
|
|
715
|
-
}
|
|
716
|
-
if (changed) {
|
|
717
|
-
record.version = LEDGER_VERSION
|
|
718
|
-
record.updatedAt = nextLedgerRevision()
|
|
719
|
-
record.needsUpgrade = false
|
|
720
|
-
ledgerRecords.set(record.sessionId, record)
|
|
721
|
-
void persistLedgerRecord(record)
|
|
722
|
-
}
|
|
723
|
-
return record
|
|
724
|
-
}
|
|
725
|
-
function applyLedgerRecord(record, materialization = 'ledger-reuse') {
|
|
726
|
-
if (record === null || record === undefined) return
|
|
727
|
-
prepareLedgerRecord(record)
|
|
728
|
-
if (record.lastIdentity !== undefined) sessionModel.set(record.sessionId, record.lastIdentity)
|
|
729
|
-
for (const turn of record.turns) addTurn(turn.workspaceId, turn.time, record.sessionId, turn.turn, turn.identity, materialization, turn.seq)
|
|
730
|
-
for (const item of record.usage) {
|
|
731
|
-
const identity = item.identity || identityFromLegacy(item.modelId)
|
|
732
|
-
const dates = dateKeys(item.time)
|
|
733
|
-
const previous = usageByStep.get(item.key)
|
|
734
|
-
if (previous !== undefined) {
|
|
735
|
-
unindexUsage(previous)
|
|
736
|
-
adjustUsage(previous.wsId, previous.time, previous.values, previous.identity || previous.modelId, -1, previous.sid, { local: previous.date, utc: previous.dateUtc }, previous.cost)
|
|
737
|
-
}
|
|
738
|
-
const next = { key: item.key, seq: item.seq, wsId: item.workspaceId, time: item.time, date: dates.local, dateUtc: dates.utc, values: item.values, identity, modelId: identity.label, cost: item.cost, turn: item.turn, step: item.step, materialization, sid: record.sessionId }
|
|
739
|
-
adjustUsage(item.workspaceId, item.time, item.values, identity, 1, record.sessionId, dates, item.cost)
|
|
740
|
-
usageByStep.set(item.key, next)
|
|
741
|
-
indexUsage(next)
|
|
742
|
-
}
|
|
743
|
-
if (record.turns.length > 0 || record.usage.length > 0) {
|
|
744
|
-
sessionCount.add(record.sessionId)
|
|
745
|
-
if (record.usage.length > 0) markStatsChanged()
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
function ledgerRank(record) {
|
|
749
|
-
return [typeof record.lastSeq === 'number' ? record.lastSeq : -1, record.source === 'flush' ? 1 : 0, typeof record.updatedAt === 'number' ? record.updatedAt : 0]
|
|
750
|
-
}
|
|
751
|
-
function replaceLedgerRecord(record) {
|
|
752
|
-
ledgerRecords.set(record.sessionId, record)
|
|
753
|
-
return record
|
|
754
|
-
}
|
|
755
|
-
function storeLedgerRecord(record) {
|
|
756
|
-
const current = ledgerRecords.get(record.sessionId)
|
|
757
|
-
if (current !== undefined) {
|
|
758
|
-
const nextRank = ledgerRank(record)
|
|
759
|
-
const currentRank = ledgerRank(current)
|
|
760
|
-
if (nextRank[0] < currentRank[0] || (nextRank[0] === currentRank[0] && (nextRank[1] < currentRank[1] || (nextRank[1] === currentRank[1] && nextRank[2] <= currentRank[2])))) return current
|
|
761
|
-
}
|
|
762
|
-
ledgerRecords.set(record.sessionId, record)
|
|
763
|
-
return record
|
|
764
|
-
}
|
|
765
|
-
function persistLedgerRecord(record) {
|
|
766
|
-
if (disposed || ledgerUnit === null || record === null || record === undefined) return ledgerWriteChain
|
|
767
|
-
const write = ledgerWriteChain.then(async () => {
|
|
768
|
-
if (ledgerUnit !== null && ledgerRecords.get(record.sessionId) === record) await ledgerUnit.putRecord('sessions', record.sessionId, record)
|
|
769
|
-
})
|
|
770
|
-
ledgerWriteChain = write.catch((err) => {
|
|
771
|
-
console.error('[all-usage] usage ledger write failed:', err)
|
|
772
|
-
})
|
|
773
|
-
return ledgerWriteChain
|
|
774
|
-
}
|
|
775
|
-
function foldEvent(wsId, time, type, data, sid, seq, materialization = 'live') {
|
|
776
|
-
if ((type === 'turn/end' || type === 'assistant/message') && !validEventTime(time)) return
|
|
777
|
-
if (type === 'request/context' || type === 'request/header') {
|
|
778
|
-
sessionModel.set(sid, identityFromRoute(data, sessionModel.get(sid)))
|
|
779
|
-
} else if (type === 'turn/end') {
|
|
780
|
-
addTurn(wsId, time, sid, data && typeof data.turn === 'number' ? data.turn : null, sessionModel.get(sid), materialization, seq)
|
|
781
|
-
} else if (type === 'assistant/message' && data && data.usage) {
|
|
782
|
-
const identity = identityFromMessage(data, sessionModel.get(sid))
|
|
783
|
-
sessionModel.set(sid, identity)
|
|
784
|
-
addUsage(wsId, time, data.usage, identity, sid, data, seq, materialization)
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
function foldEvents(wsId, events, fromSeq, sid, materialization = 'scan') {
|
|
788
|
-
for (const ev of events) {
|
|
789
|
-
if (fromSeq !== undefined) {
|
|
790
|
-
const s = typeof ev.seq === 'number' ? ev.seq : -1
|
|
791
|
-
if (s <= fromSeq) continue
|
|
792
|
-
}
|
|
793
|
-
if (ev.type === 'turn/end' || ev.type === 'assistant/message' || ev.type === 'request/context' || ev.type === 'request/header') foldEvent(wsId, ev.time, ev.type, ev.data, sid, ev.seq, materialization)
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
function lastSeqOf(events) {
|
|
797
|
-
let last = -1
|
|
798
|
-
for (const ev of events) {
|
|
799
|
-
const s = typeof ev.seq === 'number' ? ev.seq : -1
|
|
800
|
-
if (s > last) last = s
|
|
801
|
-
}
|
|
802
|
-
return last
|
|
803
|
-
}
|
|
804
|
-
function sequenceProfile(events) {
|
|
805
|
-
let previous = -1
|
|
806
|
-
let last = -1
|
|
807
|
-
let nonMonotonic = false
|
|
808
|
-
for (const ev of events) {
|
|
809
|
-
const seq = ev && typeof ev.seq === 'number' ? ev.seq : -1
|
|
810
|
-
if (seq < 0) continue
|
|
811
|
-
if (seq <= previous) nonMonotonic = true
|
|
812
|
-
previous = seq
|
|
813
|
-
if (seq > last) last = seq
|
|
814
|
-
}
|
|
815
|
-
return { lastSeq: last, nonMonotonic }
|
|
816
|
-
}
|
|
817
|
-
function enqueue(sid, task) {
|
|
818
|
-
const prev = chains.get(sid) || Promise.resolve()
|
|
819
|
-
const next = prev.then(() => task(), () => task())
|
|
820
|
-
chains.set(sid, next)
|
|
821
|
-
const cleanup = () => { if (chains.get(sid) === next) chains.delete(sid) }
|
|
822
|
-
void next.then(cleanup, cleanup)
|
|
823
|
-
return next
|
|
824
|
-
}
|
|
825
|
-
function wsForLiveSession(session, sid) {
|
|
826
|
-
let wsId = memberOf.get(sid)
|
|
827
|
-
if (wsId !== undefined) return wsId
|
|
828
|
-
const header = session && session.header
|
|
829
|
-
const cwd = header && typeof header.cwd === 'string' ? header.cwd : ''
|
|
830
|
-
if (cwd === '') return undefined
|
|
831
|
-
wsId = pathIndex.get(cwd)
|
|
832
|
-
if (wsId !== undefined) memberOf.set(sid, wsId)
|
|
833
|
-
return wsId
|
|
834
|
-
}
|
|
835
|
-
function cancelLiveResync(sid) {
|
|
836
|
-
const timer = liveResyncTimers.get(sid)
|
|
837
|
-
if (timer !== undefined) {
|
|
838
|
-
clearTimeout(timer)
|
|
839
|
-
liveResyncTimers.delete(sid)
|
|
840
|
-
}
|
|
841
|
-
liveResyncAttempts.delete(sid)
|
|
842
|
-
liveResyncPending.delete(sid)
|
|
843
|
-
}
|
|
844
|
-
function scheduleLiveResync(sid, wsId, generation) {
|
|
845
|
-
if (disposed || generation !== aggregationGeneration || !liveResyncPending.has(sid) || liveResyncTimers.has(sid)) return
|
|
846
|
-
const attempt = liveResyncAttempts.get(sid) || 0
|
|
847
|
-
const delay = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5)))
|
|
848
|
-
const timer = setTimeout(() => {
|
|
849
|
-
liveResyncTimers.delete(sid)
|
|
850
|
-
if (disposed || generation !== aggregationGeneration || !liveResyncPending.has(sid)) return
|
|
851
|
-
void enqueue(sid, () => resyncLiveSession(sid, wsId, generation))
|
|
852
|
-
}, delay)
|
|
853
|
-
liveResyncTimers.set(sid, timer)
|
|
854
|
-
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
855
|
-
}
|
|
856
|
-
function foldLiveFallback(sid, wsId, event) {
|
|
857
|
-
if (event === null || event === undefined) return
|
|
858
|
-
foldEvent(wsId, event.time, event.type, event.data, sid, event.seq, 'live')
|
|
859
|
-
const seq = typeof event.seq === 'number' ? event.seq : -1
|
|
860
|
-
if (seq >= 0) {
|
|
861
|
-
const current = sessionSeq.get(sid)
|
|
862
|
-
if (current === undefined || seq > current) sessionSeq.set(sid, seq)
|
|
863
|
-
}
|
|
864
|
-
sessionCount.add(sid)
|
|
865
|
-
}
|
|
866
|
-
async function syncLiveSession(sid, wsId, event, generation) {
|
|
867
|
-
try {
|
|
868
|
-
const snap = await ctx.sessionQuery.readSession(sid)
|
|
869
|
-
if (disposed || generation !== aggregationGeneration) return true
|
|
870
|
-
if (snap && Array.isArray(snap.events)) {
|
|
871
|
-
const previousLast = sessionSeq.get(sid)
|
|
872
|
-
const snapshotLast = lastSeqOf(snap.events)
|
|
873
|
-
foldEvents(wsId, snap.events, undefined, sid, 'live')
|
|
874
|
-
let nextLast = Math.max(previousLast === undefined ? -1 : previousLast, snapshotLast)
|
|
875
|
-
const eventSeq = event === null || event === undefined || typeof event.seq !== 'number' ? -1 : event.seq
|
|
876
|
-
const needsFollowup = event !== null && event !== undefined && (eventSeq < 0 || eventSeq > snapshotLast)
|
|
877
|
-
if (needsFollowup) {
|
|
878
|
-
foldLiveFallback(sid, wsId, event)
|
|
879
|
-
const current = sessionSeq.get(sid)
|
|
880
|
-
nextLast = Math.max(nextLast, current === undefined ? -1 : current)
|
|
881
|
-
}
|
|
882
|
-
sessionSeq.set(sid, nextLast)
|
|
883
|
-
sessionCount.add(sid)
|
|
884
|
-
if (needsFollowup) scheduleLiveResync(sid, wsId, generation)
|
|
885
|
-
else cancelLiveResync(sid)
|
|
886
|
-
return true
|
|
887
|
-
}
|
|
888
|
-
} catch (err) {
|
|
889
|
-
// Keep the event as a fallback and retry a complete session sync later.
|
|
890
|
-
}
|
|
891
|
-
return false
|
|
892
|
-
}
|
|
893
|
-
async function resyncLiveSession(sid, wsId, generation) {
|
|
894
|
-
if (disposed || generation !== aggregationGeneration || !liveResyncPending.has(sid)) return
|
|
895
|
-
if (await syncLiveSession(sid, wsId, null, generation)) return
|
|
896
|
-
liveResyncAttempts.set(sid, (liveResyncAttempts.get(sid) || 0) + 1)
|
|
897
|
-
scheduleLiveResync(sid, wsId, generation)
|
|
898
|
-
}
|
|
899
|
-
async function processLiveEvent(sid, wsId, event, generation = aggregationGeneration) {
|
|
900
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
901
|
-
const seq = typeof event.seq === 'number' ? event.seq : -1
|
|
902
|
-
const last = sessionSeq.get(sid)
|
|
903
|
-
const needsSync = last === undefined || liveResyncPending.has(sid) || (seq >= 0 && seq > last + 1)
|
|
904
|
-
if (needsSync) {
|
|
905
|
-
liveResyncPending.add(sid)
|
|
906
|
-
if (await syncLiveSession(sid, wsId, event, generation)) return
|
|
907
|
-
foldLiveFallback(sid, wsId, event)
|
|
908
|
-
liveResyncAttempts.set(sid, (liveResyncAttempts.get(sid) || 0) + 1)
|
|
909
|
-
scheduleLiveResync(sid, wsId, generation)
|
|
910
|
-
return
|
|
911
|
-
}
|
|
912
|
-
if (seq < 0) {
|
|
913
|
-
foldLiveFallback(sid, wsId, event)
|
|
914
|
-
return
|
|
915
|
-
}
|
|
916
|
-
if (seq <= last) return
|
|
917
|
-
foldLiveFallback(sid, wsId, event)
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// ---------- durable usage ledger ----------
|
|
921
|
-
async function loadLedger() {
|
|
922
|
-
if (storage === undefined || storage.backend === undefined || typeof storage.backend.get !== 'function') return
|
|
923
|
-
try {
|
|
924
|
-
const backend = storage.backend.get('json')
|
|
925
|
-
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
926
|
-
const unit = await backend.kv.open({ name: 'all_usage_ledger', version: 0, tables: ['sessions'], hasGlobal: false })
|
|
927
|
-
if (disposed) { await unit.close().catch(() => {}); return }
|
|
928
|
-
ledgerUnit = unit
|
|
929
|
-
const snapshot = await unit.loadAll()
|
|
930
|
-
const rows = snapshot && snapshot.tables && snapshot.tables.sessions
|
|
931
|
-
if (rows !== null && rows !== undefined && typeof rows === 'object') {
|
|
932
|
-
for (const [key, raw] of Object.entries(rows)) {
|
|
933
|
-
const record = normalizeLedgerRecord(raw, key)
|
|
934
|
-
if (record === null) console.warn('[all-usage] ignoring malformed usage ledger row:', key)
|
|
935
|
-
else ledgerRecords.set(key, record)
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
} catch (err) {
|
|
939
|
-
console.error('[all-usage] usage ledger unavailable:', err)
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
// ---------- pricing catalog and cost backfill ----------
|
|
945
|
-
function adjustCostOnly(item, cost, direction) {
|
|
946
|
-
const identity = coerceIdentity(item.identity || item.modelId)
|
|
947
|
-
const dates = item && typeof item.date === 'string' && typeof item.dateUtc === 'string' ? { local: item.date, utc: item.dateUtc } : dateKeys(item.time)
|
|
948
|
-
const targets = []
|
|
949
|
-
const seen = new Set()
|
|
950
|
-
for (const target of [
|
|
951
|
-
ensureModel(identity).cost,
|
|
952
|
-
totals.cost,
|
|
953
|
-
ensureWs(item.wsId).cost,
|
|
954
|
-
ensureDay(byDay, dates.local).cost,
|
|
955
|
-
ensureDay(byDayUtc, dates.utc).cost,
|
|
956
|
-
ensureDayWs(ensureDay(byDay, dates.local), item.wsId).cost,
|
|
957
|
-
ensureDayWs(ensureDay(byDayUtc, dates.utc), item.wsId).cost,
|
|
958
|
-
ensureDayModel(ensureDay(byDay, dates.local), identity).cost,
|
|
959
|
-
ensureDayModel(ensureDay(byDayUtc, dates.utc), identity).cost,
|
|
960
|
-
]) {
|
|
961
|
-
if (seen.has(target)) continue
|
|
962
|
-
seen.add(target)
|
|
963
|
-
targets.push(target)
|
|
964
|
-
}
|
|
965
|
-
for (const target of targets) addCostAggregateDirection(target, cost, direction)
|
|
966
|
-
}
|
|
967
|
-
function usedPricingModels() {
|
|
968
|
-
const rows = []
|
|
969
|
-
const seen = new Set()
|
|
970
|
-
for (const item of usageByStep.values()) {
|
|
971
|
-
const identity = coerceIdentity(item.identity || item.modelId)
|
|
972
|
-
if (seen.has(identity.identityKey)) continue
|
|
973
|
-
seen.add(identity.identityKey)
|
|
974
|
-
const resolved = resolveCurrentPricing(identity)
|
|
975
|
-
rows.push({ identityKey: identity.identityKey, provider: identity.provider, requestedModel: identity.requestedModel, actualModel: identity.actualModel, model: identity.label, status: resolved.status, reason: resolved.reason || '', pricingModel: resolved.pricingModel || null, providerId: resolved.providerId || null, source: resolved.source || 'none', currency: resolved.currency || 'USD', rates: resolved.rates || null, tiered: resolved.tiered === true })
|
|
976
|
-
if (rows.length >= 500) break
|
|
977
|
-
}
|
|
978
|
-
rows.sort((a, b) => String(a.model).localeCompare(String(b.model)))
|
|
979
|
-
return rows
|
|
980
|
-
}
|
|
981
|
-
function pricingModelSearch(query, limit = 20) {
|
|
982
|
-
const raw = typeof query === 'string' ? query.trim().toLowerCase() : ''
|
|
983
|
-
if (raw === '') return []
|
|
984
|
-
const normalized = raw.replace(/\s+/g, ' ')
|
|
985
|
-
const selected = new Map()
|
|
986
|
-
for (const entry of pricingState.catalogEntries) {
|
|
987
|
-
const official = officialProviderIds(entry.modelId)
|
|
988
|
-
if (!official.has(String(entry.providerId || '').toLowerCase())) continue
|
|
989
|
-
const modelId = entry.modelId.toLowerCase()
|
|
990
|
-
const displayName = String(entry.displayName || '').toLowerCase()
|
|
991
|
-
if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
|
|
992
|
-
const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
|
|
993
|
-
const previous = selected.get(entry.modelId)
|
|
994
|
-
if (previous === undefined || score < previous.score) selected.set(entry.modelId, { value: entry.modelId, label: entry.displayName, providerId: entry.providerId, score })
|
|
995
|
-
}
|
|
996
|
-
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)
|
|
997
|
-
}
|
|
998
|
-
function pricingSnapshot() {
|
|
999
|
-
const state = pricingState
|
|
1000
|
-
const used = usedPricingModels()
|
|
1001
|
-
return {
|
|
1002
|
-
schemaVersion: state.schemaVersion,
|
|
1003
|
-
source: { ...state.source },
|
|
1004
|
-
sync: { ...state.sync },
|
|
1005
|
-
catalogModelCount: state.catalogEntries.length,
|
|
1006
|
-
overrideCount: state.overrides.length,
|
|
1007
|
-
mappingCount: state.mappings.length,
|
|
1008
|
-
configured: state.catalogEntries.length > 0 || state.overrides.length > 0 || state.mappings.length > 0,
|
|
1009
|
-
config: { sync: { ...state.sync }, mappings: state.mappings.map(({ provider, identityKey, ...mapping }) => ({ ...mapping })), overrides: state.overrides.map(({ providerId, ...entry }) => ({ ...entry })) },
|
|
1010
|
-
usedModels: used,
|
|
1011
|
-
cost: serializeCostAggregate(totals.cost),
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
async function loadPricing() {
|
|
1015
|
-
if (storage === undefined || storage.backend === undefined || typeof storage.backend.get !== 'function') return
|
|
1016
|
-
try {
|
|
1017
|
-
const backend = storage.backend.get('json')
|
|
1018
|
-
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
1019
|
-
const unit = await backend.kv.open({ name: 'all_usage_pricing', version: 0, tables: [], hasGlobal: true })
|
|
1020
|
-
if (disposed) { await unit.close().catch(() => {}); return }
|
|
1021
|
-
pricingUnit = unit
|
|
1022
|
-
const snapshot = await unit.loadAll()
|
|
1023
|
-
const global = snapshot && snapshot.global
|
|
1024
|
-
const raw = global && typeof global === 'object' && global.pricing !== undefined ? global.pricing : global
|
|
1025
|
-
pricingState = normalizePricingState(raw)
|
|
1026
|
-
pricingResolutionCache.clear()
|
|
1027
|
-
} catch (err) {
|
|
1028
|
-
console.error('[all-usage] pricing catalog unavailable:', err)
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
function persistPricing() {
|
|
1032
|
-
if (disposed) return pricingWriteChain
|
|
1033
|
-
const payload = { pricing: serializePricingState(pricingState) }
|
|
1034
|
-
pricingWriteChain = pricingWriteChain.then(async () => {
|
|
1035
|
-
if (pricingUnit === null || pricingUnit === undefined) return
|
|
1036
|
-
await pricingUnit.setGlobal(payload)
|
|
1037
|
-
})
|
|
1038
|
-
pricingWriteChain = pricingWriteChain.catch((err) => {
|
|
1039
|
-
console.error('[all-usage] pricing persist failed:', err)
|
|
1040
|
-
})
|
|
1041
|
-
return pricingWriteChain
|
|
1042
|
-
}
|
|
1043
|
-
async function syncPricing(force = false) {
|
|
1044
|
-
await pricingReady
|
|
1045
|
-
if (pricingSyncInFlight) return { ok: false, message: 'pricing-sync-in-progress', pricing: pricingSnapshot() }
|
|
1046
|
-
const now = Date.now()
|
|
1047
|
-
if (!force && pricingState.sync.lastSuccessAt > 0 && now - pricingState.sync.lastSuccessAt < pricingState.sync.intervalMs) return { ok: true, skipped: true, pricing: pricingSnapshot() }
|
|
1048
|
-
pricingSyncInFlight = true
|
|
1049
|
-
pricingState.sync.lastAttemptAt = now
|
|
1050
|
-
markStatsChanged()
|
|
1051
|
-
try {
|
|
1052
|
-
const result = await fetchModelsDevCatalog()
|
|
1053
|
-
if (!result.ok) {
|
|
1054
|
-
pricingState.source.lastError = result.error
|
|
1055
|
-
pricingState.sync.lastError = result.error
|
|
1056
|
-
markStatsChanged()
|
|
1057
|
-
await persistPricing()
|
|
1058
|
-
return { ok: false, message: result.error, pricing: pricingSnapshot() }
|
|
1059
|
-
}
|
|
1060
|
-
pricingState = normalizePricingState({
|
|
1061
|
-
...serializePricingState(pricingState),
|
|
1062
|
-
source: { url: result.catalog.sourceUrl, fetchedAt: result.catalog.fetchedAt, catalogHash: result.catalog.catalogHash, lastError: '' },
|
|
1063
|
-
sync: { ...pricingState.sync, lastSuccessAt: result.catalog.fetchedAt, lastError: '' },
|
|
1064
|
-
catalogEntries: result.catalog.entries,
|
|
1065
|
-
})
|
|
1066
|
-
pricingResolutionCache.clear()
|
|
1067
|
-
const backfill = backfillUnpricedCosts()
|
|
1068
|
-
markStatsChanged()
|
|
1069
|
-
await persistPricing()
|
|
1070
|
-
await ledgerWriteChain
|
|
1071
|
-
return { ok: true, skipped: false, backfill, pricing: pricingSnapshot() }
|
|
1072
|
-
} finally {
|
|
1073
|
-
pricingSyncInFlight = false
|
|
1074
|
-
schedulePricingSync()
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
function backfillUnpricedCosts() {
|
|
1078
|
-
let considered = 0
|
|
1079
|
-
let priced = 0
|
|
1080
|
-
const touched = new Set()
|
|
1081
|
-
for (const item of usageByStep.values()) {
|
|
1082
|
-
const oldCost = normalizeCostSnapshot(item.cost)
|
|
1083
|
-
if (oldCost !== null && oldCost.pricingMode === 'official-model' && oldCost.status === 'priced') continue
|
|
1084
|
-
considered += 1
|
|
1085
|
-
const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId))
|
|
1086
|
-
if (next.status !== 'priced') continue
|
|
1087
|
-
if (oldCost !== null) adjustCostOnly(item, oldCost, -1)
|
|
1088
|
-
item.cost = next
|
|
1089
|
-
adjustCostOnly(item, next, 1)
|
|
1090
|
-
const record = ledgerRecords.get(item.sid)
|
|
1091
|
-
if (record !== undefined) {
|
|
1092
|
-
const stored = record.usage.find((candidate) => candidate.key === item.key)
|
|
1093
|
-
if (stored !== undefined) { stored.cost = next; touched.add(record) }
|
|
1094
|
-
}
|
|
1095
|
-
priced += 1
|
|
1096
|
-
}
|
|
1097
|
-
for (const record of touched) {
|
|
1098
|
-
record.version = LEDGER_VERSION
|
|
1099
|
-
record.updatedAt = nextLedgerRevision()
|
|
1100
|
-
record.needsUpgrade = false
|
|
1101
|
-
void persistLedgerRecord(record)
|
|
1102
|
-
}
|
|
1103
|
-
let remaining = 0
|
|
1104
|
-
for (const item of usageByStep.values()) {
|
|
1105
|
-
const cost = normalizeCostSnapshot(item.cost)
|
|
1106
|
-
if (cost === null || cost.status !== 'priced') remaining += 1
|
|
1107
|
-
}
|
|
1108
|
-
return { considered, priced, remaining }
|
|
1109
|
-
}
|
|
1110
|
-
function updatePricingState(raw, backfill) {
|
|
1111
|
-
const input = raw && typeof raw === 'object' && raw.pricing && typeof raw.pricing === 'object' ? raw.pricing : raw
|
|
1112
|
-
const current = serializePricingState(pricingState)
|
|
1113
|
-
const merged = { ...current, ...(input && typeof input === 'object' ? input : {}) }
|
|
1114
|
-
if (input && typeof input === 'object' && input.sync && typeof input.sync === 'object' && !Array.isArray(input.sync)) merged.sync = { ...current.sync, ...input.sync }
|
|
1115
|
-
pricingState = normalizePricingState(merged)
|
|
1116
|
-
pricingResolutionCache.clear()
|
|
1117
|
-
const result = backfill === true ? backfillUnpricedCosts() : { considered: 0, priced: 0, remaining: totals.cost.unpricedCalls + totals.cost.ambiguousCalls + totals.cost.unsupportedCalls }
|
|
1118
|
-
markStatsChanged()
|
|
1119
|
-
schedulePricingSync()
|
|
1120
|
-
return result
|
|
1121
|
-
}
|
|
1122
|
-
function schedulePricingSync() {
|
|
1123
|
-
if (pricingSyncTimer !== null) { clearTimeout(pricingSyncTimer); pricingSyncTimer = null }
|
|
1124
|
-
if (disposed || pricingState.sync.autoEnabled !== true) return
|
|
1125
|
-
const elapsed = pricingState.sync.lastSuccessAt > 0 ? Date.now() - pricingState.sync.lastSuccessAt : pricingState.sync.intervalMs
|
|
1126
|
-
const delay = Math.max(0, pricingState.sync.intervalMs - elapsed)
|
|
1127
|
-
pricingSyncTimer = setTimeout(() => {
|
|
1128
|
-
pricingSyncTimer = null
|
|
1129
|
-
void syncPricing(true)
|
|
1130
|
-
}, delay)
|
|
1131
|
-
if (pricingSyncTimer && typeof pricingSyncTimer.unref === 'function') pricingSyncTimer.unref()
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// ---------- baseline scan over durable logs ----------
|
|
1135
|
-
function scheduleNativeBaselineRetry(generation, delay) {
|
|
1136
|
-
if (disposed || baselineFallbackTimer !== null) return
|
|
1137
|
-
baselineFallbackTimer = setTimeout(() => {
|
|
1138
|
-
baselineFallbackTimer = null
|
|
1139
|
-
if (!disposed && generation === aggregationGeneration && !scan.started && !scan.done) void runBaseline(generation)
|
|
1140
|
-
}, delay)
|
|
1141
|
-
if (baselineFallbackTimer && typeof baselineFallbackTimer.unref === 'function') baselineFallbackTimer.unref()
|
|
1142
|
-
}
|
|
1143
|
-
function scheduleBaselineRetry(generation = aggregationGeneration) {
|
|
1144
|
-
if (disposed || baselineRetryScheduled || scan.done || generation !== aggregationGeneration) return
|
|
1145
|
-
baselineRetryScheduled = true
|
|
1146
|
-
const delay = baselineRetryDelay
|
|
1147
|
-
baselineRetryDelay = Math.min(baselineRetryDelay * 2, 30000)
|
|
1148
|
-
void safeContextTimeout(delay).then((ready) => {
|
|
1149
|
-
if (generation !== aggregationGeneration) return undefined
|
|
1150
|
-
baselineRetryScheduled = false
|
|
1151
|
-
if (ready && !scan.started && !scan.done) return runBaseline(generation)
|
|
1152
|
-
if (!ready && !disposed) scheduleNativeBaselineRetry(generation, delay)
|
|
1153
|
-
return undefined
|
|
1154
|
-
})
|
|
1155
|
-
}
|
|
1156
|
-
async function runBaseline(generation = aggregationGeneration) {
|
|
1157
|
-
if (scan.started || disposed || generation !== aggregationGeneration) return
|
|
1158
|
-
scan.started = true
|
|
1159
|
-
beginSync()
|
|
1160
|
-
await Promise.all([ledgerReady, pricingReady])
|
|
1161
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1162
|
-
let setupFailed = false
|
|
1163
|
-
try {
|
|
1164
|
-
const workspaces = ctx.workspaceRegistry.list()
|
|
1165
|
-
for (const w of workspaces) {
|
|
1166
|
-
const id = w && w.id
|
|
1167
|
-
const path = w && typeof w.path === 'string' ? w.path : ''
|
|
1168
|
-
const title = w && typeof w.title === 'string' ? w.title : ''
|
|
1169
|
-
if (id === undefined) continue
|
|
1170
|
-
wsMeta.set(id, { id, title, path })
|
|
1171
|
-
if (path !== '') pathIndex.set(path, id)
|
|
1172
|
-
if (w && Array.isArray(w.sessionIds)) {
|
|
1173
|
-
for (const sid of w.sessionIds) memberOf.set(sid, id)
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
} catch (err) {
|
|
1177
|
-
console.error('[all-usage] workspace list failed:', err)
|
|
1178
|
-
setupFailed = true
|
|
1179
|
-
noteSyncError('workspace-list-failed')
|
|
1180
|
-
}
|
|
1181
|
-
let records = null
|
|
1182
|
-
try {
|
|
1183
|
-
records = await ctx.sessionQuery.listSessions()
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
console.error('[all-usage] session list failed:', err)
|
|
1186
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1187
|
-
noteSyncError('session-list-failed')
|
|
1188
|
-
}
|
|
1189
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1190
|
-
// v1.0.8: cheap per-session change signal (header line + stat, no full-log read)
|
|
1191
|
-
let snapshots = null
|
|
1192
|
-
if (sessionPersistence !== undefined && typeof sessionPersistence.listSnapshots === 'function') {
|
|
1193
|
-
try {
|
|
1194
|
-
const rows = await sessionPersistence.listSnapshots()
|
|
1195
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1196
|
-
if (Array.isArray(rows)) {
|
|
1197
|
-
sync.persistenceSnapshotsAvailable = true
|
|
1198
|
-
snapshots = new Map()
|
|
1199
|
-
for (const row of rows) {
|
|
1200
|
-
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
1201
|
-
if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
} catch (err) {
|
|
1205
|
-
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
1206
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1207
|
-
sync.persistenceSnapshotsAvailable = false
|
|
1208
|
-
markStatsChanged()
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1212
|
-
if (setupFailed || !Array.isArray(records)) {
|
|
1213
|
-
// A transient registry failure must not be reported as a completed empty scan.
|
|
1214
|
-
scan.started = false
|
|
1215
|
-
markStatsChanged()
|
|
1216
|
-
scheduleBaselineRetry(generation)
|
|
1217
|
-
return
|
|
1218
|
-
}
|
|
1219
|
-
scan.total = records.length
|
|
1220
|
-
sync.sessionsTotal = records.length
|
|
1221
|
-
markStatsChanged()
|
|
1222
|
-
const listedSessionIds = new Set()
|
|
1223
|
-
for (const record of records) {
|
|
1224
|
-
if (record === undefined || record === null || record.header === undefined) continue
|
|
1225
|
-
const sid = record.header.id
|
|
1226
|
-
const cwd = typeof record.header.cwd === 'string' ? record.header.cwd : ''
|
|
1227
|
-
const wsId = cwd === '' ? undefined : pathIndex.get(cwd)
|
|
1228
|
-
if (sid !== undefined && wsId !== undefined) listedSessionIds.add(sid)
|
|
1229
|
-
}
|
|
1230
|
-
for (const [sid, record] of ledgerRecords) {
|
|
1231
|
-
if (!listedSessionIds.has(sid)) {
|
|
1232
|
-
applyLedgerRecord(record, 'ledger-recovery')
|
|
1233
|
-
if (record.turns.length > 0 || record.usage.length > 0) sync.sessionsRestoredFromLedger += 1
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
if (sync.sessionsRestoredFromLedger > 0) markStatsChanged()
|
|
1237
|
-
for (const record of records) {
|
|
1238
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1239
|
-
if (record === undefined || record === null || record.header === undefined) {
|
|
1240
|
-
scan.scanned += 1
|
|
1241
|
-
markStatsChanged()
|
|
1242
|
-
continue
|
|
1243
|
-
}
|
|
1244
|
-
const sid = record.header.id
|
|
1245
|
-
const cwd = typeof record.header.cwd === 'string' ? record.header.cwd : ''
|
|
1246
|
-
const wsId = cwd === '' ? undefined : pathIndex.get(cwd)
|
|
1247
|
-
if (sid === undefined || wsId === undefined) {
|
|
1248
|
-
scan.scanned += 1
|
|
1249
|
-
markStatsChanged()
|
|
1250
|
-
continue
|
|
1251
|
-
}
|
|
1252
|
-
listedSessionIds.add(sid)
|
|
1253
|
-
await enqueue(sid, async () => {
|
|
1254
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1255
|
-
try {
|
|
1256
|
-
if (sessionSeq.has(sid) && !liveResyncPending.has(sid)) return
|
|
1257
|
-
// v1.0.8: when the persisted log revision is unchanged since the last ledger
|
|
1258
|
-
// write, the whole readSession (full event transfer) is skipped — the ledger
|
|
1259
|
-
// record is applied directly and the live feed keeps catching new events.
|
|
1260
|
-
const previousRecord = ledgerRecords.get(sid)
|
|
1261
|
-
const revision = snapshots === null ? undefined : snapshots.get(sid)
|
|
1262
|
-
if (!liveResyncPending.has(sid) && previousRecord !== undefined && previousRecord.needsUpgrade !== true && typeof previousRecord.lastRevision === 'string' && typeof revision === 'string' && revision === previousRecord.lastRevision) {
|
|
1263
|
-
sync.sessionsSkippedByRevision += 1
|
|
1264
|
-
applyLedgerRecord(previousRecord, 'ledger-reuse')
|
|
1265
|
-
sessionSeq.set(sid, previousRecord.lastSeq)
|
|
1266
|
-
sessionCount.add(sid)
|
|
1267
|
-
markStatsChanged()
|
|
1268
|
-
return
|
|
1269
|
-
}
|
|
1270
|
-
sync.sessionsRead += 1
|
|
1271
|
-
markStatsChanged()
|
|
1272
|
-
const snap = await ctx.sessionQuery.readSession(sid)
|
|
1273
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1274
|
-
if (snap && Array.isArray(snap.events)) {
|
|
1275
|
-
// v1.0.7: incremental seed — the durable ledger doubles as a per-session
|
|
1276
|
-
// cursor (cc-switch session_log_sync mtime+offset parity). An unchanged
|
|
1277
|
-
// session applies its canonical record directly and never re-folds;
|
|
1278
|
-
// a changed session seeds the previous record once, then folds only the
|
|
1279
|
-
// new tail (previously every listed session was re-read and fully rebuilt).
|
|
1280
|
-
const sequence = sequenceProfile(snap.events)
|
|
1281
|
-
const currentLastSeq = sequence.lastSeq
|
|
1282
|
-
const previous = ledgerRecords.get(sid)
|
|
1283
|
-
const canFoldTail = previous !== undefined && previous.needsUpgrade !== true && !sequence.nonMonotonic && previous.lastSeq >= 0 && currentLastSeq > previous.lastSeq
|
|
1284
|
-
if (canFoldTail) {
|
|
1285
|
-
applyLedgerRecord(previous, 'ledger-reuse')
|
|
1286
|
-
foldEvents(wsId, snap.events, previous.lastSeq, sid, 'scan')
|
|
1287
|
-
} else {
|
|
1288
|
-
// A changed revision with no new tail may still contain a replacement;
|
|
1289
|
-
// rebuild from the complete read instead of trusting lastSeq alone.
|
|
1290
|
-
foldEvents(wsId, snap.events, undefined, sid, 'scan')
|
|
1291
|
-
}
|
|
1292
|
-
const ledger = buildLedgerRecord({ id: sid, header: record.header, events: snap.events }, wsId, 'scan', revision, previous)
|
|
1293
|
-
const canonical = ledger === null ? ledgerRecords.get(sid) : (canFoldTail ? storeLedgerRecord(ledger) : replaceLedgerRecord(ledger))
|
|
1294
|
-
if (canonical === ledger) {
|
|
1295
|
-
void persistLedgerRecord(ledger)
|
|
1296
|
-
}
|
|
1297
|
-
const observedLastSeq = sessionSeq.get(sid)
|
|
1298
|
-
const nextLastSeq = Math.max(currentLastSeq, observedLastSeq === undefined ? -1 : observedLastSeq)
|
|
1299
|
-
sessionSeq.set(sid, nextLastSeq)
|
|
1300
|
-
sessionCount.add(sid)
|
|
1301
|
-
if (observedLastSeq === undefined || observedLastSeq <= currentLastSeq) cancelLiveResync(sid)
|
|
1302
|
-
else scheduleLiveResync(sid, wsId, generation)
|
|
1303
|
-
}
|
|
1304
|
-
} catch (err) {
|
|
1305
|
-
if (generation !== aggregationGeneration) return
|
|
1306
|
-
sync.sessionsFailed += 1
|
|
1307
|
-
scan.failed += 1
|
|
1308
|
-
noteSyncError('session-read-failed')
|
|
1309
|
-
const saved = ledgerRecords.get(sid)
|
|
1310
|
-
if (saved !== undefined) {
|
|
1311
|
-
applyLedgerRecord(saved, 'ledger-recovery')
|
|
1312
|
-
if (saved.turns.length > 0 || saved.usage.length > 0) sync.sessionsRestoredFromLedger += 1
|
|
1313
|
-
sessionSeq.set(sid, -1)
|
|
1314
|
-
sessionCount.add(sid)
|
|
1315
|
-
} else {
|
|
1316
|
-
sessionSeq.set(sid, -1)
|
|
1317
|
-
}
|
|
1318
|
-
} finally {
|
|
1319
|
-
if (generation === aggregationGeneration) {
|
|
1320
|
-
scan.scanned += 1
|
|
1321
|
-
markStatsChanged()
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
})
|
|
1325
|
-
if (!(await safeContextTimeout(0))) {
|
|
1326
|
-
scan.started = false
|
|
1327
|
-
noteSyncError('baseline-yield-unavailable')
|
|
1328
|
-
scheduleBaselineRetry(generation)
|
|
1329
|
-
return
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1333
|
-
await ledgerWriteChain
|
|
1334
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1335
|
-
const costBackfill = backfillUnpricedCosts()
|
|
1336
|
-
if (costBackfill.priced > 0) {
|
|
1337
|
-
await ledgerWriteChain
|
|
1338
|
-
markStatsChanged()
|
|
1339
|
-
}
|
|
1340
|
-
if (disposed || generation !== aggregationGeneration) return
|
|
1341
|
-
knownSessionIds.clear()
|
|
1342
|
-
for (const sid of listedSessionIds) knownSessionIds.add(sid)
|
|
1343
|
-
scan.done = true
|
|
1344
|
-
sync.lastCompletedAt = Date.now()
|
|
1345
|
-
if (sync.sessionsFailed === 0) {
|
|
1346
|
-
sync.lastErrorAt = 0
|
|
1347
|
-
sync.lastErrorCode = null
|
|
1348
|
-
}
|
|
1349
|
-
markStatsChanged()
|
|
1350
|
-
if (reconcilePending) scheduleReconcileHint()
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
function sessionIdsFromRecords(records) {
|
|
1354
|
-
const ids = new Set()
|
|
1355
|
-
for (const record of records) {
|
|
1356
|
-
if (record === undefined || record === null || record.header === undefined) continue
|
|
1357
|
-
const sid = record.header.id
|
|
1358
|
-
const cwd = typeof record.header.cwd === 'string' ? record.header.cwd : ''
|
|
1359
|
-
const wsId = cwd === '' ? undefined : pathIndex.get(cwd)
|
|
1360
|
-
if (sid !== undefined && wsId !== undefined) ids.add(sid)
|
|
1361
|
-
}
|
|
1362
|
-
return ids
|
|
1363
|
-
}
|
|
1364
|
-
async function reconcileSessions() {
|
|
1365
|
-
if (disposed || reconcileInFlight || !scan.done) return
|
|
1366
|
-
reconcilePending = false
|
|
1367
|
-
reconcileInFlight = true
|
|
1368
|
-
try {
|
|
1369
|
-
const records = await ctx.sessionQuery.listSessions()
|
|
1370
|
-
if (disposed || !Array.isArray(records)) return
|
|
1371
|
-
const currentIds = sessionIdsFromRecords(records)
|
|
1372
|
-
let removed = false
|
|
1373
|
-
for (const sid of knownSessionIds) {
|
|
1374
|
-
if (!currentIds.has(sid)) { removed = true; break }
|
|
1375
|
-
}
|
|
1376
|
-
if (removed && !disposed && scan.done) {
|
|
1377
|
-
console.info('[all-usage] session removal detected; rebuilding usage index')
|
|
1378
|
-
const generation = resetAggregationState()
|
|
1379
|
-
void runBaseline(generation)
|
|
1380
|
-
return
|
|
1381
|
-
}
|
|
1382
|
-
knownSessionIds.clear()
|
|
1383
|
-
for (const sid of currentIds) knownSessionIds.add(sid)
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
console.error('[all-usage] session reconciliation failed:', err)
|
|
1386
|
-
noteSyncError('session-reconcile-failed')
|
|
1387
|
-
} finally {
|
|
1388
|
-
reconcileInFlight = false
|
|
1389
|
-
if (reconcilePending && !disposed) scheduleReconcileHint()
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
function scheduleReconcileHint() {
|
|
1393
|
-
if (disposed) return
|
|
1394
|
-
reconcilePending = true
|
|
1395
|
-
if (reconcileHintScheduled || reconcileInFlight) return
|
|
1396
|
-
reconcileHintScheduled = true
|
|
1397
|
-
const generation = aggregationGeneration
|
|
1398
|
-
void safeContextTimeout(RECONCILE_HINT_DELAY_MS).then((ready) => {
|
|
1399
|
-
if (generation !== aggregationGeneration) return
|
|
1400
|
-
reconcileHintScheduled = false
|
|
1401
|
-
if (ready && !disposed) void reconcileSessions()
|
|
1402
|
-
}, () => {
|
|
1403
|
-
if (generation === aggregationGeneration) reconcileHintScheduled = false
|
|
1404
|
-
})
|
|
1405
|
-
}
|
|
1406
|
-
function scheduleReconcileTimer() {
|
|
1407
|
-
if (disposed || reconcileTimer !== null) return
|
|
1408
|
-
reconcileTimer = setTimeout(() => {
|
|
1409
|
-
reconcileTimer = null
|
|
1410
|
-
if (!disposed) {
|
|
1411
|
-
void reconcileSessions()
|
|
1412
|
-
scheduleReconcileTimer()
|
|
1413
|
-
}
|
|
1414
|
-
}, RECONCILE_INTERVAL_MS)
|
|
1415
|
-
if (reconcileTimer && typeof reconcileTimer.unref === 'function') reconcileTimer.unref()
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// ---------- live feed ----------
|
|
1419
|
-
ctx.on('session/event', (session, event) => {
|
|
1420
|
-
if (disposed) return
|
|
1421
|
-
if (event === undefined || event === null) return
|
|
1422
|
-
const type = event.type
|
|
1423
|
-
if (type !== 'turn/end' && type !== 'assistant/message' && type !== 'request/context' && type !== 'request/header') return
|
|
1424
|
-
const sid = session && session.id
|
|
1425
|
-
if (typeof sid !== 'string') return
|
|
1426
|
-
const wsId = wsForLiveSession(session, sid)
|
|
1427
|
-
if (wsId === undefined) return
|
|
1428
|
-
const generation = aggregationGeneration
|
|
1429
|
-
knownSessionIds.add(sid)
|
|
1430
|
-
enqueue(sid, () => processLiveEvent(sid, wsId, event, generation))
|
|
1431
|
-
})
|
|
1432
|
-
ctx.on('session/flush', async (session) => {
|
|
1433
|
-
if (disposed || session === null || typeof session !== 'object' || typeof session.id !== 'string') return
|
|
1434
|
-
await Promise.all([ledgerReady, pricingReady])
|
|
1435
|
-
if (disposed) return
|
|
1436
|
-
const wsId = wsForLiveSession(session, session.id)
|
|
1437
|
-
if (wsId === undefined) return
|
|
1438
|
-
const ledger = buildLedgerRecord(session, wsId, 'flush', undefined, ledgerRecords.get(session.id))
|
|
1439
|
-
if (ledger === null) return
|
|
1440
|
-
const canonical = storeLedgerRecord(ledger)
|
|
1441
|
-
if (canonical === ledger) await persistLedgerRecord(ledger)
|
|
1442
|
-
})
|
|
1443
|
-
ctx.on('session/disposed', () => {
|
|
1444
|
-
if (!disposed) scheduleReconcileHint()
|
|
1445
|
-
})
|
|
1446
|
-
|
|
1447
|
-
// ---------- workspace aliases (durable, schema-free KV unit) ----------
|
|
1448
|
-
async function loadAliases() {
|
|
1449
|
-
if (storage === undefined) return
|
|
1450
|
-
try {
|
|
1451
|
-
const backend = storage.backend.get('json')
|
|
1452
|
-
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
1453
|
-
const unit = await backend.kv.open({ name: 'all_usage_aliases', version: 0, tables: [], hasGlobal: true })
|
|
1454
|
-
if (disposed) { await unit.close().catch(() => {}); return }
|
|
1455
|
-
kvUnit = unit
|
|
1456
|
-
const snap = await unit.loadAll()
|
|
1457
|
-
const g = snap && snap.global
|
|
1458
|
-
if (g !== null && g !== undefined && typeof g === 'object') {
|
|
1459
|
-
for (const key of Object.keys(g)) {
|
|
1460
|
-
const value = g[key]
|
|
1461
|
-
if (typeof value === 'string' && value.trim() !== '') aliases[key] = value
|
|
1462
|
-
}
|
|
1463
|
-
markStatsChanged()
|
|
1464
|
-
}
|
|
1465
|
-
} catch (err) {
|
|
1466
|
-
console.error('[all-usage] alias storage unavailable:', err)
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
function persistAliases() {
|
|
1470
|
-
if (disposed) return aliasWriteChain
|
|
1471
|
-
const snapshotAliases = {}
|
|
1472
|
-
for (const key of Object.keys(aliases)) snapshotAliases[key] = aliases[key]
|
|
1473
|
-
aliasWriteChain = aliasWriteChain.then(() => {
|
|
1474
|
-
if (kvUnit === null || kvUnit === undefined) return undefined
|
|
1475
|
-
return kvUnit.setGlobal(snapshotAliases).catch((err) => {
|
|
1476
|
-
console.error('[all-usage] alias persist failed:', err)
|
|
1477
|
-
})
|
|
1478
|
-
})
|
|
1479
|
-
}
|
|
1480
|
-
function setAlias(wsId, raw) {
|
|
1481
|
-
if (typeof wsId !== 'string' || wsId.length === 0 || wsId.length > 256) return { ok: false, message: 'invalid-workspace', aliases: Object.assign({}, aliases) }
|
|
1482
|
-
if (typeof raw !== 'string') return { ok: false, message: 'invalid-alias', aliases: Object.assign({}, aliases) }
|
|
1483
|
-
const alias = raw.trim().slice(0, 80)
|
|
1484
|
-
if (!wsMeta.has(wsId)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, aliases) }
|
|
1485
|
-
if (alias === '') delete aliases[wsId]
|
|
1486
|
-
else aliases[wsId] = alias
|
|
1487
|
-
persistAliases()
|
|
1488
|
-
markStatsChanged()
|
|
1489
|
-
return { ok: true, aliases: Object.assign({}, aliases) }
|
|
1490
|
-
}
|
|
1491
|
-
ctx.effect(() => async () => {
|
|
1492
|
-
disposed = true
|
|
1493
|
-
if (reconcileTimer !== null) {
|
|
1494
|
-
clearTimeout(reconcileTimer)
|
|
1495
|
-
reconcileTimer = null
|
|
1496
|
-
}
|
|
1497
|
-
if (baselineFallbackTimer !== null) {
|
|
1498
|
-
clearTimeout(baselineFallbackTimer)
|
|
1499
|
-
baselineFallbackTimer = null
|
|
1500
|
-
}
|
|
1501
|
-
if (pricingSyncTimer !== null) {
|
|
1502
|
-
clearTimeout(pricingSyncTimer)
|
|
1503
|
-
pricingSyncTimer = null
|
|
1504
|
-
}
|
|
1505
|
-
for (const timer of liveResyncTimers.values()) clearTimeout(timer)
|
|
1506
|
-
liveResyncTimers.clear()
|
|
1507
|
-
liveResyncAttempts.clear()
|
|
1508
|
-
liveResyncPending.clear()
|
|
1509
|
-
baselineRetryScheduled = false
|
|
1510
|
-
chains.clear()
|
|
1511
|
-
await Promise.all([aliasesReady, ledgerReady, pricingReady, aliasWriteChain, ledgerWriteChain, pricingWriteChain])
|
|
1512
|
-
const units = [kvUnit, ledgerUnit, pricingUnit]
|
|
1513
|
-
kvUnit = null
|
|
1514
|
-
ledgerUnit = null
|
|
1515
|
-
pricingUnit = null
|
|
1516
|
-
await Promise.all(units.map((unit) => unit === null || unit === undefined ? undefined : unit.close().catch(() => {})))
|
|
1517
|
-
})
|
|
1518
|
-
|
|
1519
|
-
// ---------- snapshot for the client ----------
|
|
1520
|
-
function scanSnapshot() {
|
|
1521
|
-
return { started: scan.started, done: scan.done, scanned: scan.scanned, total: scan.total, failed: scan.failed }
|
|
1522
|
-
}
|
|
1523
|
-
function statusSnapshot() {
|
|
1524
|
-
commitPendingStats()
|
|
1525
|
-
return { instanceId, revision: statsRevision, updatedAt: statsUpdatedAt, scan: scanSnapshot(), sync: syncSnapshot() }
|
|
1526
|
-
}
|
|
1527
|
-
function serializeIdentity(identity) {
|
|
1528
|
-
const value = coerceIdentity(identity)
|
|
1529
|
-
return { identityKey: value.identityKey, provider: value.provider, requestedModel: value.requestedModel, actualModel: value.actualModel, model: value.label, legacy: value.legacy }
|
|
1530
|
-
}
|
|
1531
|
-
function serializeModelAggregate(item) {
|
|
1532
|
-
return { ...serializeIdentity(item), calls: item.calls, input: item.input, output: item.output, cacheRead: item.cacheRead, cacheWrite: item.cacheWrite, reasoning: item.reasoning, cost: serializeCostAggregate(item.cost) }
|
|
1533
|
-
}
|
|
1534
|
-
function serializeDays(dayMap) {
|
|
1535
|
-
const result = []
|
|
1536
|
-
for (const pair of dayMap) {
|
|
1537
|
-
const date = pair[0]
|
|
1538
|
-
const day = pair[1]
|
|
1539
|
-
result.push({
|
|
1540
|
-
date,
|
|
1541
|
-
turns: day.turns,
|
|
1542
|
-
sessions: day.sessionIds.size,
|
|
1543
|
-
sessionIds: Array.from(day.sessionIds).sort(),
|
|
1544
|
-
tokens: { input: day.tokens.input, output: day.tokens.output, cacheRead: day.tokens.cacheRead, cacheWrite: day.tokens.cacheWrite, reasoning: day.tokens.reasoning },
|
|
1545
|
-
cost: serializeCostAggregate(day.cost),
|
|
1546
|
-
perWorkspace: Array.from(day.perWs, (p) => ({ workspaceId: p[0], turns: p[1] })),
|
|
1547
|
-
byWorkspace: Array.from(day.byWs, (p) => ({ workspaceId: p[0], input: p[1].input, output: p[1].output, cacheRead: p[1].cacheRead, cacheWrite: p[1].cacheWrite, reasoning: p[1].reasoning, cost: serializeCostAggregate(day.byWs.get(p[0]).cost) })),
|
|
1548
|
-
byModel: Array.from(day.byModel, (p) => serializeModelAggregate(p[1])),
|
|
1549
|
-
})
|
|
1550
|
-
}
|
|
1551
|
-
result.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0))
|
|
1552
|
-
return result
|
|
1553
|
-
}
|
|
1554
|
-
function snapshot() {
|
|
1555
|
-
commitPendingStats()
|
|
1556
|
-
const generatedAt = Date.now()
|
|
1557
|
-
if (snapshotCache !== null && snapshotCache.revision === statsRevision) return Object.assign({}, snapshotCache.value, { generatedAt })
|
|
1558
|
-
const value = {
|
|
1559
|
-
...statusSnapshot(),
|
|
1560
|
-
generatedAt,
|
|
1561
|
-
usageSchemaVersion: 3,
|
|
1562
|
-
costSchemaVersion: COST_SCHEMA_VERSION,
|
|
1563
|
-
requestToken,
|
|
1564
|
-
workspaces: Array.from(wsMeta.values(), (w) => ({ id: w.id, title: w.title, path: w.path })),
|
|
1565
|
-
aliases: Object.assign({}, aliases),
|
|
1566
|
-
pricing: pricingSnapshot(),
|
|
1567
|
-
tokenSemantics: {
|
|
1568
|
-
processedTotal: 'input + output + cacheRead + cacheWrite + reasoning',
|
|
1569
|
-
cacheRead: 'reused context tokens; not newly generated output',
|
|
1570
|
-
cacheWrite: 'tokens written into a provider cache',
|
|
1571
|
-
// v1.0.7: structured, machine-readable accounting semantics (cc-switch
|
|
1572
|
-
// input_token_semantics parity). DSH reports input as fresh (cache read /
|
|
1573
|
-
// cache write sit in their own buckets) — verified against real ledger data;
|
|
1574
|
-
// reasoning is bucketed separately from output and assumed non-overlapping.
|
|
1575
|
-
semantics: {
|
|
1576
|
-
input: 'fresh (excludes cache-read and cache-write tokens, bucketed separately)',
|
|
1577
|
-
buckets: ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning'],
|
|
1578
|
-
inputIncludesCache: false,
|
|
1579
|
-
cacheBucketed: true,
|
|
1580
|
-
reasoningSeparate: true,
|
|
1581
|
-
gate: 'all-zero usage rows are ignored; pure cache-read requests still count',
|
|
1582
|
-
},
|
|
1583
|
-
},
|
|
1584
|
-
costSemantics: {
|
|
1585
|
-
source: 'models.dev',
|
|
1586
|
-
currency: 'USD',
|
|
1587
|
-
buckets: ['input', 'output', 'cacheRead', 'cacheWrite'],
|
|
1588
|
-
input: 'fresh (DSH TokenUsage already excludes cache)',
|
|
1589
|
-
reasoning: 'not added to output again; provider output already carries completion/thoughts where reported',
|
|
1590
|
-
multiplier: 'applies only to final total',
|
|
1591
|
-
providerMatching: 'DSH provider is ignored; only the official model vendor entry is selected',
|
|
1592
|
-
historical: 'positive cost snapshots are stable; only unresolved usage is eligible for backfill',
|
|
1593
|
-
},
|
|
1594
|
-
totals: { turns: totals.turns, sessions: sessionCount.size, input: totals.input, output: totals.output, cacheRead: totals.cacheRead, cacheWrite: totals.cacheWrite, reasoning: totals.reasoning, cost: serializeCostAggregate(totals.cost) },
|
|
1595
|
-
perWorkspace: Array.from(perWorkspace, (p) => ({ workspaceId: p[0], turns: p[1].turns, input: p[1].input, output: p[1].output, cacheRead: p[1].cacheRead, cacheWrite: p[1].cacheWrite, reasoning: p[1].reasoning, cost: serializeCostAggregate(p[1].cost) })),
|
|
1596
|
-
perModel: Array.from(perModel.values(), (item) => serializeModelAggregate(item)),
|
|
1597
|
-
byDay: serializeDays(byDay),
|
|
1598
|
-
byDayUtc: serializeDays(byDayUtc),
|
|
1599
|
-
}
|
|
1600
|
-
snapshotCache = { revision: statsRevision, value }
|
|
1601
|
-
return Object.assign({}, value, { generatedAt })
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function validDateText(value) {
|
|
1605
|
-
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
|
|
1606
|
-
const year = Number(value.slice(0, 4))
|
|
1607
|
-
const month = Number(value.slice(5, 7))
|
|
1608
|
-
const day = Number(value.slice(8, 10))
|
|
1609
|
-
const date = new Date(Date.UTC(year, month - 1, day))
|
|
1610
|
-
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
|
1611
|
-
}
|
|
1612
|
-
function shiftDateText(value, days, utc) {
|
|
1613
|
-
const parts = value.split('-').map(Number)
|
|
1614
|
-
const date = utc ? new Date(Date.UTC(parts[0], parts[1] - 1, parts[2] + days)) : new Date(parts[0], parts[1] - 1, parts[2] + days)
|
|
1615
|
-
return utc ? dayKeyUtc(date.getTime()) : dayKey(date.getTime())
|
|
1616
|
-
}
|
|
1617
|
-
function queryScopeFromRequest(req) {
|
|
1618
|
-
let url
|
|
1619
|
-
try { url = new URL(req.url || '/', 'http://all-usage.local') } catch (err) { return { ok: false, message: 'bad-query' } }
|
|
1620
|
-
const rawUtc = url.searchParams.get('utc')
|
|
1621
|
-
if (rawUtc !== null && rawUtc !== '' && rawUtc !== '0' && rawUtc !== '1') return { ok: false, message: 'invalid-timezone' }
|
|
1622
|
-
const utc = rawUtc === '1'
|
|
1623
|
-
const today = utc ? dayKeyUtc(Date.now()) : dayKey(Date.now())
|
|
1624
|
-
const start = url.searchParams.get('start') || today
|
|
1625
|
-
const end = url.searchParams.get('end') || today
|
|
1626
|
-
if (!validDateText(start) || !validDateText(end) || start > end) return { ok: false, message: 'invalid-date-range' }
|
|
1627
|
-
const readParam = (name, max) => {
|
|
1628
|
-
const value = url.searchParams.get(name)
|
|
1629
|
-
if (value === null || value === '') return undefined
|
|
1630
|
-
return value.length <= max ? value : null
|
|
1631
|
-
}
|
|
1632
|
-
const workspaceId = readParam('workspaceId', 256)
|
|
1633
|
-
const provider = readParam('provider', 256)
|
|
1634
|
-
const modelKey = readParam('modelKey', 1024)
|
|
1635
|
-
if (workspaceId === null || provider === null || modelKey === null) return { ok: false, message: 'query-too-long' }
|
|
1636
|
-
return { ok: true, scope: { start, end, utc, workspaceId, provider, modelKey } }
|
|
1637
|
-
}
|
|
1638
|
-
function scopeFingerprint(scope) {
|
|
1639
|
-
return JSON.stringify({ start: scope.start, end: scope.end, utc: scope.utc === true, workspaceId: scope.workspaceId || null, provider: scope.provider || null, modelKey: scope.modelKey || null })
|
|
1640
|
-
}
|
|
1641
|
-
function dateInScope(date, scope) {
|
|
1642
|
-
return date >= scope.start && date <= scope.end
|
|
1643
|
-
}
|
|
1644
|
-
function modelNameOfIdentity(identity) {
|
|
1645
|
-
const normalized = coerceIdentity(identity)
|
|
1646
|
-
const structured = normalized.actualModel || normalized.requestedModel
|
|
1647
|
-
if (structured !== null) return structured
|
|
1648
|
-
if (normalized.legacy && typeof normalized.label === 'string') {
|
|
1649
|
-
const separator = normalized.label.indexOf(' / ')
|
|
1650
|
-
if (separator > 0) return normalized.label.slice(separator + 3)
|
|
1651
|
-
}
|
|
1652
|
-
return normalized.label
|
|
1653
|
-
}
|
|
1654
|
-
function identityMatchesScope(identity, scope) {
|
|
1655
|
-
const normalized = coerceIdentity(identity)
|
|
1656
|
-
if (scope.provider !== undefined && scope.provider !== null && normalized.provider !== scope.provider) return false
|
|
1657
|
-
if (scope.modelKey !== undefined && scope.modelKey !== null && normalized.identityKey !== scope.modelKey && modelNameOfIdentity(normalized) !== scope.modelKey) return false
|
|
1658
|
-
return true
|
|
1659
|
-
}
|
|
1660
|
-
function queryMetric() {
|
|
1661
|
-
return { turns: 0, calls: 0, sessions: new Set(), turnKeys: new Set(), input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
1662
|
-
}
|
|
1663
|
-
function queryAggregate() {
|
|
1664
|
-
return { totals: queryMetric(), days: new Map(), workspaces: new Map(), models: new Map() }
|
|
1665
|
-
}
|
|
1666
|
-
function queryDay(aggregate, date) {
|
|
1667
|
-
let day = aggregate.days.get(date)
|
|
1668
|
-
if (day === undefined) { day = queryMetric(); day.date = date; aggregate.days.set(date, day) }
|
|
1669
|
-
return day
|
|
1670
|
-
}
|
|
1671
|
-
function queryWorkspace(aggregate, workspaceId) {
|
|
1672
|
-
let row = aggregate.workspaces.get(workspaceId)
|
|
1673
|
-
if (row === undefined) { row = queryMetric(); row.workspaceId = workspaceId; aggregate.workspaces.set(workspaceId, row) }
|
|
1674
|
-
return row
|
|
1675
|
-
}
|
|
1676
|
-
function queryModel(aggregate, identity) {
|
|
1677
|
-
const normalized = coerceIdentity(identity)
|
|
1678
|
-
let row = aggregate.models.get(normalized.identityKey)
|
|
1679
|
-
if (row === undefined) { row = queryMetric(); Object.assign(row, serializeIdentity(normalized)); aggregate.models.set(normalized.identityKey, row) }
|
|
1680
|
-
return row
|
|
1681
|
-
}
|
|
1682
|
-
function addQueryTokens(metric, values) {
|
|
1683
|
-
metric.input += values.input
|
|
1684
|
-
metric.output += values.output
|
|
1685
|
-
metric.cacheRead += values.cacheRead
|
|
1686
|
-
metric.cacheWrite += values.cacheWrite
|
|
1687
|
-
metric.reasoning += values.reasoning
|
|
1688
|
-
}
|
|
1689
|
-
function addQueryTurn(aggregate, turn, date) {
|
|
1690
|
-
const targets = [aggregate.totals, queryDay(aggregate, date), queryWorkspace(aggregate, turn.wsId)]
|
|
1691
|
-
const model = queryModel(aggregate, turn.identity)
|
|
1692
|
-
targets.push(model)
|
|
1693
|
-
const key = turn.key
|
|
1694
|
-
for (const target of targets) {
|
|
1695
|
-
if (target.turnKeys.has(key)) continue
|
|
1696
|
-
target.turnKeys.add(key)
|
|
1697
|
-
target.turns += 1
|
|
1698
|
-
target.sessions.add(turn.sid)
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
function addQueryUsage(aggregate, item, date) {
|
|
1702
|
-
const targets = [aggregate.totals, queryDay(aggregate, date), queryWorkspace(aggregate, item.wsId)]
|
|
1703
|
-
const model = queryModel(aggregate, item.identity || item.modelId)
|
|
1704
|
-
targets.push(model)
|
|
1705
|
-
for (const target of targets) {
|
|
1706
|
-
target.calls += 1
|
|
1707
|
-
target.sessions.add(item.sid)
|
|
1708
|
-
addQueryTokens(target, item.values)
|
|
1709
|
-
addCostAggregate(target.cost, item.cost)
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
function finalizeQueryMetric(metric) {
|
|
1713
|
-
return { turns: metric.turns, calls: metric.calls, sessions: metric.sessions.size, input: metric.input, output: metric.output, cacheRead: metric.cacheRead, cacheWrite: metric.cacheWrite, reasoning: metric.reasoning, cost: serializeCostAggregate(metric.cost) }
|
|
1714
|
-
}
|
|
1715
|
-
function finalizeQueryAggregate(aggregate) {
|
|
1716
|
-
const daily = Array.from(aggregate.days.values()).sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0).map((day) => ({ date: day.date, ...finalizeQueryMetric(day), tokens: { input: day.input, output: day.output, cacheRead: day.cacheRead, cacheWrite: day.cacheWrite, reasoning: day.reasoning } }))
|
|
1717
|
-
const perWorkspace = Array.from(aggregate.workspaces.values()).map((row) => ({ workspaceId: row.workspaceId, ...finalizeQueryMetric(row) }))
|
|
1718
|
-
const perModel = Array.from(aggregate.models.values()).filter((row) => row.calls > 0 || row.input > 0 || row.output > 0 || row.cacheRead > 0 || row.cacheWrite > 0 || row.reasoning > 0).map((row) => ({ identityKey: row.identityKey, provider: row.provider, requestedModel: row.requestedModel, actualModel: row.actualModel, model: row.model, legacy: row.legacy, ...finalizeQueryMetric(row) }))
|
|
1719
|
-
return { totals: finalizeQueryMetric(aggregate.totals), daily, perWorkspace, perModel }
|
|
1720
|
-
}
|
|
1721
|
-
const HOUR_MS = 60 * 60 * 1000
|
|
1722
|
-
function hourStartOf(time, utc) {
|
|
1723
|
-
const date = new Date(time)
|
|
1724
|
-
return utc ? Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours()) : new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()).getTime()
|
|
1725
|
-
}
|
|
1726
|
-
function calendarStartOf(dateText, utc) {
|
|
1727
|
-
const parts = dateText.split('-').map(Number)
|
|
1728
|
-
return utc ? Date.UTC(parts[0], parts[1] - 1, parts[2]) : new Date(parts[0], parts[1] - 1, parts[2]).getTime()
|
|
1729
|
-
}
|
|
1730
|
-
function nextCalendarStartOf(dateText, utc) {
|
|
1731
|
-
const parts = dateText.split('-').map(Number)
|
|
1732
|
-
return utc ? Date.UTC(parts[0], parts[1] - 1, parts[2] + 1) : new Date(parts[0], parts[1] - 1, parts[2] + 1).getTime()
|
|
1733
|
-
}
|
|
1734
|
-
function hourlyRangeOf(scope, nowMs) {
|
|
1735
|
-
if (scope.start !== scope.end) return null
|
|
1736
|
-
const start = calendarStartOf(scope.start, scope.utc)
|
|
1737
|
-
const today = scope.utc ? dayKeyUtc(nowMs) : dayKey(nowMs)
|
|
1738
|
-
const end = scope.start === today ? nowMs : nextCalendarStartOf(scope.start, scope.utc)
|
|
1739
|
-
return { start, count: Math.max(1, Math.ceil(Math.max(0, end - start) / HOUR_MS)) }
|
|
1740
|
-
}
|
|
1741
|
-
function serializeTrendMetric(time, metric) {
|
|
1742
|
-
const value = finalizeQueryMetric(metric || queryMetric())
|
|
1743
|
-
return { time, date: new Date(time).toISOString(), ...value, tokens: { input: value.input, output: value.output, cacheRead: value.cacheRead, cacheWrite: value.cacheWrite, reasoning: value.reasoning } }
|
|
1744
|
-
}
|
|
1745
|
-
function queryHourlyTrend(matchingUsage, matchingTurns, scope, nowMs) {
|
|
1746
|
-
const range = hourlyRangeOf(scope, nowMs)
|
|
1747
|
-
if (range === null) return []
|
|
1748
|
-
const buckets = new Map()
|
|
1749
|
-
const metricFor = (time) => {
|
|
1750
|
-
const index = Math.floor((hourStartOf(time, scope.utc) - range.start) / HOUR_MS)
|
|
1751
|
-
if (index < 0 || index >= range.count) return null
|
|
1752
|
-
let metric = buckets.get(index)
|
|
1753
|
-
if (metric === undefined) { metric = queryMetric(); buckets.set(index, metric) }
|
|
1754
|
-
return metric
|
|
1755
|
-
}
|
|
1756
|
-
for (const entry of matchingUsage) {
|
|
1757
|
-
if (!dateInScope(entry.date, scope)) continue
|
|
1758
|
-
const metric = metricFor(entry.item.time)
|
|
1759
|
-
if (metric === null) continue
|
|
1760
|
-
metric.calls += 1
|
|
1761
|
-
metric.sessions.add(entry.item.sid)
|
|
1762
|
-
addQueryTokens(metric, entry.item.values)
|
|
1763
|
-
addCostAggregate(metric.cost, entry.item.cost)
|
|
1764
|
-
}
|
|
1765
|
-
for (const entry of matchingTurns) {
|
|
1766
|
-
if (!dateInScope(entry.date, scope)) continue
|
|
1767
|
-
const metric = metricFor(entry.turn.time)
|
|
1768
|
-
if (metric === null || metric.turnKeys.has(entry.turn.key)) continue
|
|
1769
|
-
metric.turnKeys.add(entry.turn.key)
|
|
1770
|
-
metric.turns += 1
|
|
1771
|
-
metric.sessions.add(entry.turn.sid)
|
|
1772
|
-
}
|
|
1773
|
-
return Array.from({ length: range.count }, (_, index) => serializeTrendMetric(range.start + index * HOUR_MS, buckets.get(index)))
|
|
1774
|
-
}
|
|
1775
|
-
function queryUsageScope(scope) {
|
|
1776
|
-
commitPendingStats()
|
|
1777
|
-
const nowMs = Date.now()
|
|
1778
|
-
const today = scope.utc ? dayKeyUtc(nowMs) : dayKey(nowMs)
|
|
1779
|
-
const hourlyCacheKey = scope.start === scope.end ? ':' + (scope.start === today ? hourStartOf(nowMs, scope.utc) : 'fixed') : ''
|
|
1780
|
-
const key = statsRevision + ':' + scopeFingerprint(scope) + hourlyCacheKey
|
|
1781
|
-
const cached = queryCache.get(key)
|
|
1782
|
-
if (cached !== undefined) return cached
|
|
1783
|
-
const now = new Date(nowMs)
|
|
1784
|
-
const weekday = scope.utc ? now.getUTCDay() : now.getDay()
|
|
1785
|
-
const sunday = shiftDateText(today, -weekday, scope.utc)
|
|
1786
|
-
const heatStart = shiftDateText(sunday, -52 * 7, scope.utc)
|
|
1787
|
-
const matchingUsage = []
|
|
1788
|
-
const matchingTurnKeys = new Set()
|
|
1789
|
-
const usageIndex = scope.utc ? usageByUtcDate : usageByLocalDate
|
|
1790
|
-
for (const indexed of indexedEntries(usageIndex, usageByStep, scope, heatStart, today)) {
|
|
1791
|
-
const item = indexed.item
|
|
1792
|
-
const date = indexed.date
|
|
1793
|
-
if (scope.workspaceId !== undefined && scope.workspaceId !== null && item.wsId !== scope.workspaceId) continue
|
|
1794
|
-
if (!identityMatchesScope(item.identity || item.modelId, scope)) continue
|
|
1795
|
-
matchingUsage.push({ item, date })
|
|
1796
|
-
if (item.turn !== null && item.turn !== undefined) matchingTurnKeys.add(item.sid + ':turn:' + item.turn)
|
|
1797
|
-
}
|
|
1798
|
-
const matchingTurns = []
|
|
1799
|
-
const turnIndex = scope.utc ? turnsByUtcDate : turnsByLocalDate
|
|
1800
|
-
for (const indexed of indexedEntries(turnIndex, turnRecords, scope, heatStart, today)) {
|
|
1801
|
-
const turn = indexed.item
|
|
1802
|
-
const date = indexed.date
|
|
1803
|
-
if (scope.workspaceId !== undefined && scope.workspaceId !== null && turn.wsId !== scope.workspaceId) continue
|
|
1804
|
-
if ((scope.provider !== undefined && scope.provider !== null) || (scope.modelKey !== undefined && scope.modelKey !== null)) {
|
|
1805
|
-
if (!identityMatchesScope(turn.identity, scope) && !matchingTurnKeys.has(turn.key)) continue
|
|
1806
|
-
}
|
|
1807
|
-
matchingTurns.push({ turn, date })
|
|
1808
|
-
}
|
|
1809
|
-
const selected = queryAggregate()
|
|
1810
|
-
const heatmap = queryAggregate()
|
|
1811
|
-
for (const entry of matchingUsage) {
|
|
1812
|
-
if (dateInScope(entry.date, scope)) addQueryUsage(selected, entry.item, entry.date)
|
|
1813
|
-
if (entry.date >= heatStart && entry.date <= today) addQueryUsage(heatmap, entry.item, entry.date)
|
|
1814
|
-
}
|
|
1815
|
-
for (const entry of matchingTurns) {
|
|
1816
|
-
if (dateInScope(entry.date, scope)) addQueryTurn(selected, entry.turn, entry.date)
|
|
1817
|
-
if (entry.date >= heatStart && entry.date <= today) addQueryTurn(heatmap, entry.turn, entry.date)
|
|
1818
|
-
}
|
|
1819
|
-
const hourly = queryHourlyTrend(matchingUsage, matchingTurns, scope, nowMs)
|
|
1820
|
-
const result = { schemaVersion: 1, usageSchemaVersion: 3, costSchemaVersion: COST_SCHEMA_VERSION, instanceId, revision: statsRevision, updatedAt: statsUpdatedAt, scope: JSON.parse(scopeFingerprint(scope)), partial: !scan.done, completeThrough: { revision: statsRevision, at: statsUpdatedAt }, ...finalizeQueryAggregate(selected), hourly, heatmap: finalizeQueryAggregate(heatmap).daily }
|
|
1821
|
-
queryCache.set(key, result)
|
|
1822
|
-
while (queryCache.size > 20) queryCache.delete(queryCache.keys().next().value)
|
|
1823
|
-
return result
|
|
1824
|
-
}
|
|
1825
|
-
function opaqueRecordId(item) {
|
|
1826
|
-
return createHash('sha256').update(item.sid + '\0' + item.key).digest('hex').slice(0, 20)
|
|
1827
|
-
}
|
|
1828
|
-
function recordOrder(a, b) {
|
|
1829
|
-
return b.time - a.time || b.seq - a.seq || (a.sid < b.sid ? -1 : a.sid > b.sid ? 1 : 0) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
|
|
1830
|
-
}
|
|
1831
|
-
function queryRecords(scope, cursor, limit) {
|
|
1832
|
-
commitPendingStats()
|
|
1833
|
-
const fingerprint = scopeFingerprint(scope)
|
|
1834
|
-
let offset = 0
|
|
1835
|
-
if (cursor !== undefined && cursor !== '') {
|
|
1836
|
-
try {
|
|
1837
|
-
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))
|
|
1838
|
-
if (decoded.revision !== statsRevision || decoded.scope !== fingerprint || !Number.isInteger(decoded.offset) || decoded.offset < 0) return { error: 'stale-cursor' }
|
|
1839
|
-
offset = decoded.offset
|
|
1840
|
-
} catch (err) { return { error: 'bad-cursor' } }
|
|
1841
|
-
}
|
|
1842
|
-
const cacheKey = statsRevision + ':' + fingerprint
|
|
1843
|
-
let rows = recordsQueryCache.get(cacheKey)
|
|
1844
|
-
if (rows === undefined) {
|
|
1845
|
-
const index = scope.utc ? usageByUtcDate : usageByLocalDate
|
|
1846
|
-
rows = indexedEntriesInRange(index, usageByStep, scope.start, scope.end).filter(({ item }) => {
|
|
1847
|
-
if (scope.workspaceId !== undefined && scope.workspaceId !== null && item.wsId !== scope.workspaceId) return false
|
|
1848
|
-
return identityMatchesScope(item.identity || item.modelId, scope)
|
|
1849
|
-
})
|
|
1850
|
-
rows.sort((left, right) => recordOrder(left.item, right.item))
|
|
1851
|
-
recordsQueryCache.set(cacheKey, rows)
|
|
1852
|
-
while (recordsQueryCache.size > 20) recordsQueryCache.delete(recordsQueryCache.keys().next().value)
|
|
1853
|
-
}
|
|
1854
|
-
const page = rows.slice(offset, offset + limit)
|
|
1855
|
-
const items = page.map(({ item, date }) => {
|
|
1856
|
-
const identity = coerceIdentity(item.identity || item.modelId)
|
|
1857
|
-
return { id: opaqueRecordId(item), date, time: item.time, workspaceId: item.wsId, provider: identity.provider, requestedModel: identity.requestedModel, actualModel: identity.actualModel, model: identity.label, identityKey: identity.identityKey, turn: item.turn, step: item.step, seq: item.seq, values: item.values, cost: item.cost, materialization: item.materialization || 'unknown' }
|
|
1858
|
-
})
|
|
1859
|
-
const nextOffset = offset + items.length
|
|
1860
|
-
return { schemaVersion: 1, usageSchemaVersion: 3, costSchemaVersion: COST_SCHEMA_VERSION, instanceId, revision: statsRevision, scope: JSON.parse(fingerprint), items, hasMore: nextOffset < rows.length, nextCursor: nextOffset < rows.length ? Buffer.from(JSON.stringify({ revision: statsRevision, scope: fingerprint, offset: nextOffset })).toString('base64url') : null }
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// ---------- account balance (DeepSeek open platform) ----------
|
|
1864
|
-
async function requestBalance(url, key) {
|
|
1865
|
-
let controller = null
|
|
1866
|
-
let timer = null
|
|
1867
|
-
try {
|
|
1868
|
-
if (typeof AbortController === 'function') {
|
|
1869
|
-
controller = new AbortController()
|
|
1870
|
-
timer = setTimeout(() => controller.abort(), 30000)
|
|
1871
|
-
}
|
|
1872
|
-
const response = await fetch(url, {
|
|
1873
|
-
method: 'GET',
|
|
1874
|
-
headers: { accept: 'application/json', authorization: 'Bearer ' + key },
|
|
1875
|
-
...(controller === null ? {} : { signal: controller.signal }),
|
|
1876
|
-
})
|
|
1877
|
-
return { ok: response.ok, status: response.status, text: await response.text() }
|
|
1878
|
-
} catch (err) {
|
|
1879
|
-
return { ok: false, status: 0, text: '', error: 'network request failed' }
|
|
1880
|
-
} finally {
|
|
1881
|
-
if (timer !== null) clearTimeout(timer)
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
function moneyOf(v) {
|
|
1885
|
-
if (typeof v === 'number' && Number.isFinite(v)) return v
|
|
1886
|
-
if (typeof v === 'string' && v.trim() !== '') {
|
|
1887
|
-
const n = parseFloat(v)
|
|
1888
|
-
if (Number.isFinite(n)) return n
|
|
1889
|
-
}
|
|
1890
|
-
return null
|
|
1891
|
-
}
|
|
1892
|
-
function parseBalance(text) {
|
|
1893
|
-
let obj = null
|
|
1894
|
-
try {
|
|
1895
|
-
obj = JSON.parse(String(text).replace(/^\uFEFF/, ''))
|
|
1896
|
-
} catch (err) {
|
|
1897
|
-
return null
|
|
1898
|
-
}
|
|
1899
|
-
if (obj === null || typeof obj !== 'object') return null
|
|
1900
|
-
if (obj.is_available === false) return { unavailable: true, currencies: [] }
|
|
1901
|
-
const infos = (Array.isArray(obj.balance_infos) && obj.balance_infos) || (Array.isArray(obj.balance) && obj.balance) || null
|
|
1902
|
-
if (!infos) return null
|
|
1903
|
-
const out = []
|
|
1904
|
-
for (const info of infos) {
|
|
1905
|
-
if (info === null || typeof info !== 'object') continue
|
|
1906
|
-
if (typeof info.currency !== 'string') continue
|
|
1907
|
-
out.push({
|
|
1908
|
-
currency: info.currency,
|
|
1909
|
-
total: moneyOf(info.total_balance !== undefined ? info.total_balance : info.balance),
|
|
1910
|
-
granted: moneyOf(info.granted_balance),
|
|
1911
|
-
toppedUp: moneyOf(info.topped_up_balance),
|
|
1912
|
-
})
|
|
1913
|
-
}
|
|
1914
|
-
return { unavailable: false, currencies: out }
|
|
1915
|
-
}
|
|
1916
|
-
async function fetchBalance(force) {
|
|
1917
|
-
const now = Date.now()
|
|
1918
|
-
if (force !== true && balanceCache.payload !== null && now - balanceCache.fetchedAt < 300000) return balanceCache.payload
|
|
1919
|
-
let ref = 'DEEPSEEK_API_KEY'
|
|
1920
|
-
if (settings !== undefined) {
|
|
1921
|
-
try {
|
|
1922
|
-
const section = settings.get('llm-deepseek')
|
|
1923
|
-
if (section !== null && typeof section === 'object' && typeof section.apiKeyEnv === 'string' && section.apiKeyEnv.length > 0) ref = section.apiKeyEnv
|
|
1924
|
-
} catch (err) { /* default ref */ }
|
|
1925
|
-
}
|
|
1926
|
-
let key
|
|
1927
|
-
if (credentials !== undefined) {
|
|
1928
|
-
try {
|
|
1929
|
-
const hit = await credentials.resolve(ref)
|
|
1930
|
-
if (hit !== null && hit !== undefined && typeof hit.value === 'string' && hit.value.length > 0) key = hit.value
|
|
1931
|
-
} catch (err) { /* unconfigured */ }
|
|
1932
|
-
}
|
|
1933
|
-
if (key === undefined) {
|
|
1934
|
-
const payload = { status: 'missing-key' }
|
|
1935
|
-
balanceCache = { fetchedAt: now, payload }
|
|
1936
|
-
return payload
|
|
1937
|
-
}
|
|
1938
|
-
if (typeof fetch !== 'function') {
|
|
1939
|
-
const payload = { status: 'error', message: '当前 DSH 运行时不支持余额查询' }
|
|
1940
|
-
balanceCache = { fetchedAt: now, payload }
|
|
1941
|
-
return payload
|
|
1942
|
-
}
|
|
1943
|
-
const result = await requestBalance('https://api.deepseek.com/user/balance', key)
|
|
1944
|
-
const body = (result.text || '').trim()
|
|
1945
|
-
if (result.ok && body.length > 0) {
|
|
1946
|
-
const parsed = parseBalance(body)
|
|
1947
|
-
if (parsed !== null && parsed.unavailable) {
|
|
1948
|
-
const payload = { status: 'unavailable', message: 'DeepSeek 接口返回余额不可用(is_available=false)' }
|
|
1949
|
-
balanceCache = { fetchedAt: now, payload }
|
|
1950
|
-
return payload
|
|
1951
|
-
}
|
|
1952
|
-
if (parsed !== null && parsed.currencies.length > 0) {
|
|
1953
|
-
const payload = { status: 'ok', currencies: parsed.currencies, fetchedAt: now }
|
|
1954
|
-
balanceCache = { fetchedAt: now, payload }
|
|
1955
|
-
return payload
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
const detail = body.length > 0 ? body.slice(0, 300) : (result.status > 0 ? 'HTTP ' + result.status : result.error || 'network request failed')
|
|
1959
|
-
const payload = { status: 'error', message: '余额查询失败', detail }
|
|
1960
|
-
balanceCache = { fetchedAt: now, payload }
|
|
1961
|
-
return payload
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
// ---------- HTTP data routes for the client half ----------
|
|
1965
|
-
if (webServer !== undefined) {
|
|
1966
|
-
const rejectRequest = (res) => sendJson(res, 403, { ok: false, message: 'forbidden' })
|
|
1967
|
-
ctx.effect(() => webServer.register({
|
|
1968
|
-
kind: 'exact',
|
|
1969
|
-
path: '/api/all-usage/query',
|
|
1970
|
-
handler: (req, res) => {
|
|
1971
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
1972
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
1973
|
-
const parsed = queryScopeFromRequest(req)
|
|
1974
|
-
if (!parsed.ok) { sendJson(res, 400, { ok: false, message: parsed.message }); return }
|
|
1975
|
-
sendJson(res, 200, queryUsageScope(parsed.scope))
|
|
1976
|
-
},
|
|
1977
|
-
}))
|
|
1978
|
-
ctx.effect(() => webServer.register({
|
|
1979
|
-
kind: 'exact',
|
|
1980
|
-
path: '/api/all-usage/records',
|
|
1981
|
-
handler: (req, res) => {
|
|
1982
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
1983
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
1984
|
-
const parsed = queryScopeFromRequest(req)
|
|
1985
|
-
if (!parsed.ok) { sendJson(res, 400, { ok: false, message: parsed.message }); return }
|
|
1986
|
-
let limit = 50
|
|
1987
|
-
let cursor
|
|
1988
|
-
try {
|
|
1989
|
-
const url = new URL(req.url || '/', 'http://all-usage.local')
|
|
1990
|
-
const rawLimit = url.searchParams.get('limit')
|
|
1991
|
-
if (rawLimit !== null && rawLimit !== '') limit = Number(rawLimit)
|
|
1992
|
-
cursor = url.searchParams.get('cursor') || undefined
|
|
1993
|
-
} catch (err) { sendJson(res, 400, { ok: false, message: 'bad-query' }); return }
|
|
1994
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 200) { sendJson(res, 400, { ok: false, message: 'invalid-limit' }); return }
|
|
1995
|
-
const result = queryRecords(parsed.scope, cursor, limit)
|
|
1996
|
-
if (result.error !== undefined) { sendJson(res, result.error === 'stale-cursor' ? 409 : 400, { ok: false, message: result.error }); return }
|
|
1997
|
-
sendJson(res, 200, result)
|
|
1998
|
-
},
|
|
1999
|
-
}))
|
|
2000
|
-
ctx.effect(() => webServer.register({
|
|
2001
|
-
kind: 'exact',
|
|
2002
|
-
path: '/api/all-usage/pricing/models',
|
|
2003
|
-
handler: (req, res) => {
|
|
2004
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
2005
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
2006
|
-
let query = ''
|
|
2007
|
-
let limit = 20
|
|
2008
|
-
try {
|
|
2009
|
-
const url = new URL(req.url || '/', 'http://all-usage.local')
|
|
2010
|
-
query = url.searchParams.get('q') || ''
|
|
2011
|
-
const rawLimit = url.searchParams.get('limit')
|
|
2012
|
-
if (rawLimit !== null && rawLimit !== '') limit = Number(rawLimit)
|
|
2013
|
-
} catch (err) { sendJson(res, 400, { ok: false, message: 'bad-query' }); return }
|
|
2014
|
-
if (query.length > 120 || !Number.isInteger(limit) || limit < 1 || limit > 50) { sendJson(res, 400, { ok: false, message: 'invalid-model-search' }); return }
|
|
2015
|
-
sendJson(res, 200, { items: pricingModelSearch(query, limit) })
|
|
2016
|
-
},
|
|
2017
|
-
}))
|
|
2018
|
-
ctx.effect(() => webServer.register({
|
|
2019
|
-
kind: 'exact',
|
|
2020
|
-
path: '/api/all-usage/pricing',
|
|
2021
|
-
handler: async (req, res) => {
|
|
2022
|
-
if (req.method === 'GET') {
|
|
2023
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
2024
|
-
sendJson(res, 200, pricingSnapshot())
|
|
2025
|
-
return
|
|
2026
|
-
}
|
|
2027
|
-
if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
|
|
2028
|
-
if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, requestToken)) { rejectRequest(res); return }
|
|
2029
|
-
const body = await readBody(req, 256 * 1024)
|
|
2030
|
-
if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
|
|
2031
|
-
let args = null
|
|
2032
|
-
try { args = JSON.parse(body.text) } catch (err) { /* invalid json */ }
|
|
2033
|
-
if (args === null || typeof args !== 'object' || Array.isArray(args)) { sendJson(res, 400, { ok: false, message: 'bad-pricing-request' }); return }
|
|
2034
|
-
const result = updatePricingState(args.pricing || args, args.backfill === true)
|
|
2035
|
-
await persistPricing()
|
|
2036
|
-
await ledgerWriteChain
|
|
2037
|
-
sendJson(res, 200, { ok: true, backfill: result, pricing: pricingSnapshot() })
|
|
2038
|
-
},
|
|
2039
|
-
}))
|
|
2040
|
-
ctx.effect(() => webServer.register({
|
|
2041
|
-
kind: 'exact',
|
|
2042
|
-
path: '/api/all-usage/pricing/sync',
|
|
2043
|
-
handler: async (req, res) => {
|
|
2044
|
-
if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
|
|
2045
|
-
if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, requestToken)) { rejectRequest(res); return }
|
|
2046
|
-
const result = await syncPricing(true)
|
|
2047
|
-
sendJson(res, result.ok ? 200 : 502, result)
|
|
2048
|
-
},
|
|
2049
|
-
}))
|
|
2050
|
-
ctx.effect(() => webServer.register({
|
|
2051
|
-
kind: 'exact',
|
|
2052
|
-
path: '/api/all-usage/status',
|
|
2053
|
-
handler: (req, res) => {
|
|
2054
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
2055
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
2056
|
-
if (!scan.started) void runBaseline()
|
|
2057
|
-
sendJson(res, 200, statusSnapshot())
|
|
2058
|
-
},
|
|
2059
|
-
}))
|
|
2060
|
-
ctx.effect(() => webServer.register({
|
|
2061
|
-
kind: 'exact',
|
|
2062
|
-
path: '/api/all-usage',
|
|
2063
|
-
handler: (req, res) => {
|
|
2064
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
2065
|
-
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
2066
|
-
if (!scan.started) void runBaseline()
|
|
2067
|
-
sendJson(res, 200, snapshot())
|
|
2068
|
-
},
|
|
2069
|
-
}))
|
|
2070
|
-
ctx.effect(() => webServer.register({
|
|
2071
|
-
kind: 'exact',
|
|
2072
|
-
path: '/api/all-usage/balance',
|
|
2073
|
-
handler: async (req, res) => {
|
|
2074
|
-
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
2075
|
-
// Browsers may omit Origin on same-origin GET; the process token remains required.
|
|
2076
|
-
if (!isTrustedLocalApiRequest(req, false) || !hasWriteToken(req, requestToken)) { rejectRequest(res); return }
|
|
2077
|
-
let force = false
|
|
2078
|
-
try {
|
|
2079
|
-
const url = new URL(req.url ?? '/', 'http://x')
|
|
2080
|
-
force = url.searchParams.get('force') === '1'
|
|
2081
|
-
} catch (err) { /* default */ }
|
|
2082
|
-
sendJson(res, 200, await fetchBalance(force))
|
|
2083
|
-
},
|
|
2084
|
-
}))
|
|
2085
|
-
ctx.effect(() => webServer.register({
|
|
2086
|
-
kind: 'exact',
|
|
2087
|
-
path: '/api/all-usage/alias',
|
|
2088
|
-
handler: async (req, res) => {
|
|
2089
|
-
if (req.method !== 'POST') {
|
|
2090
|
-
res.statusCode = 405
|
|
2091
|
-
res.end()
|
|
2092
|
-
return
|
|
2093
|
-
}
|
|
2094
|
-
if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, requestToken)) { rejectRequest(res); return }
|
|
2095
|
-
const body = await readBody(req, 16 * 1024)
|
|
2096
|
-
if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
|
|
2097
|
-
let args = null
|
|
2098
|
-
try {
|
|
2099
|
-
args = JSON.parse(body.text)
|
|
2100
|
-
} catch (err) { /* invalid json */ }
|
|
2101
|
-
const validAliasRequest = args !== null && args !== undefined && typeof args === 'object' && !Array.isArray(args) && typeof args.workspaceId === 'string' && args.workspaceId.length > 0 && args.workspaceId.length <= 256 && typeof args.alias === 'string'
|
|
2102
|
-
const result = validAliasRequest
|
|
2103
|
-
? setAlias(args.workspaceId, args.alias)
|
|
2104
|
-
: { ok: false, message: 'bad-request', aliases: Object.assign({}, aliases) }
|
|
2105
|
-
sendJson(res, result.ok ? 200 : 400, result)
|
|
2106
|
-
},
|
|
2107
|
-
}))
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// ---------- start the historical backfill immediately ----------
|
|
2111
|
-
ledgerReady = loadLedger()
|
|
2112
|
-
pricingReady = loadPricing().then(() => { schedulePricingSync() })
|
|
2113
|
-
void runBaseline()
|
|
2114
|
-
scheduleReconcileTimer()
|
|
2115
|
-
aliasesReady = loadAliases()
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
export { name, inject, apply }
|
|
2119
|
-
export default { name, inject, apply }
|
|
1
|
+
export { name, inject, apply } from './plugin.js'
|
|
2
|
+
export { default } from './plugin.js'
|