dsh-all-usage 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/http.js CHANGED
@@ -220,7 +220,7 @@ export function registerRoutes(host) {
220
220
  // before mutating it: an early POST would otherwise be skipped because
221
221
  // pricingUnit is still null and then overwritten by the loaded state.
222
222
  await Promise.all([state.pricingReady, state.ledgerReady])
223
- const result = updatePricingState(args.pricing || args, args.backfill === true)
223
+ const result = updatePricingState(args.pricing || args, args.backfill === true, args.repriceTemporal === true)
224
224
  await persistPricing()
225
225
  await drainLedgerWrites()
226
226
  sendJson(res, 200, { ok: true, backfill: result, pricing: pricingSnapshot() })
package/lib/ledger.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { calculateCost, normalizeCostSnapshot } from './pricing.js'
2
- import { extractUsageEvent, normalizeUsageValues as usageValues, upsertUsageSample as upsertUsageSampleState, usageStepKey } from './usage-core.js'
2
+ import { billingInstantOf, contextTimeKey, extractUsageEvent, normalizeUsageValues as usageValues, pickPricingTime, touchContextTimes, upsertUsageSample as upsertUsageSampleState, usageStepKey } from './usage-core.js'
3
3
 
4
4
  const LEDGER_VERSION = 3
5
5
  const PREVIOUS_LEDGER_VERSION = 2
@@ -189,13 +189,24 @@ export function createLedger(host) {
189
189
  const usage = canFoldTail ? new Map((previousRecord.usage || []).map((item) => [item.key, { ...item }])) : new Map()
190
190
  const previousUsage = new Map(Array.isArray(previousRecord && previousRecord.usage) ? previousRecord.usage.map((item) => [item.key, item]) : [])
191
191
  let currentIdentity = canFoldTail ? coerceIdentity(previousRecord.lastIdentity) : makeIdentity(null, null, null, null)
192
+ // Request-context instants are kept per turn/step so parallel requests each
193
+ // carry their own billing time while scanning the persisted log. When the
194
+ // tail is folded onto a persisted record, the previous context archive seeds
195
+ // the map so an open request whose context arrived in the previous batch
196
+ // still bills its usage with the matching request instant after a restart.
197
+ const contextTimes = canFoldTail ? new Map(Array.isArray(previousRecord.contextTimes) ? previousRecord.contextTimes.map((entry) => [entry.key, entry.time]) : []) : new Map()
192
198
  const startIndex = canFoldTail ? previousRecord.lastSeq + 1 : 0
193
199
  for (let index = startIndex; index < events.length; index += 1) {
194
200
  const event = events[index]
195
201
  if (event === null || typeof event !== 'object') continue
196
202
  if (canFoldTail && event.seq <= previousRecord.lastSeq) continue
197
203
  const data = event.data
198
- if (event.type === 'request/context' || event.type === 'request/header') {
204
+ if (event.type === 'request/context') {
205
+ currentIdentity = identityFromRoute(data, currentIdentity)
206
+ if (validEventTime(event.time)) touchContextTimes(contextTimes, contextTimeKey(data && data.turn, data && data.step), event.time)
207
+ continue
208
+ }
209
+ if (event.type === 'request/header') {
199
210
  currentIdentity = identityFromRoute(data, currentIdentity)
200
211
  continue
201
212
  }
@@ -211,6 +222,7 @@ export function createLedger(host) {
211
222
  if (usageEvent === null) continue
212
223
  const identity = usageEvent.kind === 'message' ? identityFromMessage(data, currentIdentity) : currentIdentity
213
224
  const key = usageStepKey(sid, data, event.seq)
225
+ const pricing = pickPricingTime(contextTimes, event.time, usageEvent.turn, usageEvent.step)
214
226
  const result = upsertUsageSample(usage, {
215
227
  key,
216
228
  seq: validLedgerSeq(event.seq) ? event.seq : -1,
@@ -221,10 +233,12 @@ export function createLedger(host) {
221
233
  turn: usageEvent.turn,
222
234
  step: usageEvent.step,
223
235
  values: usageEvent.values,
236
+ pricingAt: pricing.time,
237
+ pricingTimeSource: pricing.source,
224
238
  }, { costPrevious: previousUsage.get(key) })
225
239
  if (result.accepted && usageEvent.kind === 'message') currentIdentity = identity
226
240
  }
227
- return { version: LEDGER_VERSION, sessionId: sid, workspaceId, lastSeq, 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()) }
241
+ return { version: LEDGER_VERSION, sessionId: sid, workspaceId, lastSeq, source, updatedAt: nextLedgerRevision(), lastRevision: typeof revision === 'string' ? revision : undefined, sourceRevision: typeof revision === 'string' ? revision : undefined, lastIdentity: currentIdentity, contextTimes: Array.from(contextTimes.entries()).map(([key, time]) => ({ key, time })), turns: Array.from(turns.values()), usage: Array.from(usage.values()) }
228
242
  }
229
243
  function normalizeLedgerRecord(raw, key) {
230
244
  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
@@ -256,9 +270,16 @@ export function createLedger(host) {
256
270
  }
257
271
  const updatedAt = Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0
258
272
  state.ledgerRevision = Math.max(Number.isFinite(state.ledgerRevision) ? state.ledgerRevision : 0, updatedAt)
273
+ const contextMap = new Map()
274
+ if (Array.isArray(raw.contextTimes)) {
275
+ for (const entry of raw.contextTimes) {
276
+ if (entry === null || typeof entry !== 'object' || typeof entry.key !== 'string' || !entry.key.startsWith('context:') || entry.key.length > 128 || !validEventTime(entry.time)) continue
277
+ touchContextTimes(contextMap, entry.key, entry.time)
278
+ }
279
+ }
259
280
  const lastUsage = Array.from(usageMap.values()).at(-1)
260
281
  const lastIdentity = coerceIdentity(raw.lastIdentity || raw.sourceRevisionIdentity || (lastUsage && lastUsage.identity))
261
- const normalizedRecord = { version: LEDGER_VERSION, sessionId: raw.sessionId, workspaceId: raw.workspaceId, lastSeq: validLedgerSeq(raw.lastSeq) ? 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()) }
282
+ const normalizedRecord = { version: LEDGER_VERSION, sessionId: raw.sessionId, workspaceId: raw.workspaceId, lastSeq: validLedgerSeq(raw.lastSeq) ? 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, contextTimes: Array.from(contextMap.entries()).map(([key, time]) => ({ key, time })), turns: Array.from(turnMap.values()), usage: Array.from(usageMap.values()) }
262
283
  if (rebuildReason !== undefined) normalizedRecord.rebuildRequired = rebuildReason
263
284
  Object.defineProperty(normalizedRecord, 'needsUpgrade', { value: needsUpgrade, enumerable: false, writable: true })
264
285
  return normalizedRecord
@@ -275,7 +296,8 @@ export function createLedger(host) {
275
296
  }
276
297
  if (cost !== null && cost.pricingMode === 'official-model') { item.cost = cost; continue }
277
298
  const identity = item.identity || identityFromLegacy(item.modelId)
278
- item.cost = calculateCost(item.values, resolveCurrentPricing(identity))
299
+ const billing = billingInstantOf(item, cost)
300
+ item.cost = calculateCost(item.values, resolveCurrentPricing(identity), billing.at, billing.source)
279
301
  changed = true
280
302
  }
281
303
  if (changed) {
@@ -292,6 +314,11 @@ export function createLedger(host) {
292
314
  if (record === null || record === undefined) return
293
315
  prepareLedgerRecord(record)
294
316
  if (record.lastIdentity !== undefined) state.sessionModel.set(record.sessionId, record.lastIdentity)
317
+ if (Array.isArray(record.contextTimes)) {
318
+ const times = new Map()
319
+ for (const entry of record.contextTimes) if (entry && typeof entry.key === 'string' && Number.isFinite(entry.time)) times.set(entry.key, entry.time)
320
+ state.sessionContextTimes.set(record.sessionId, times)
321
+ }
295
322
  for (const turn of record.turns) addTurn(turn.workspaceId, turn.time, record.sessionId, turn.turn, turn.identity, materialization, turn.seq)
296
323
  for (const item of record.usage) {
297
324
  const identity = item.identity || identityFromLegacy(item.modelId)
package/lib/plugin.js CHANGED
@@ -31,6 +31,7 @@ function apply(ctx) {
31
31
  usageByLocalDate: new Map(),
32
32
  usageByUtcDate: new Map(),
33
33
  sessionModel: new Map(),
34
+ sessionContextTimes: new Map(),
34
35
  totals: { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: createCostAccumulator() },
35
36
  sessionCount: new Set(),
36
37
  sessionSeq: new Map(),
@@ -1,4 +1,5 @@
1
- import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState } from './pricing.js'
1
+ import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
2
+ import { billingInstantOf } from './usage-core.js'
2
3
 
3
4
  const LEDGER_VERSION = 3
4
5
 
@@ -83,6 +84,8 @@ export function createPricingRuntime(host) {
83
84
  tieredInvalid: resolved.tieredInvalid === true,
84
85
  inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
85
86
  multiplier: resolved.multiplier || '1',
87
+ ...(resolved.temporalRoute !== undefined && resolved.temporalRoute !== null ? { temporalRoute: resolved.temporalRoute } : {}),
88
+ ...(resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) && resolved.temporalProfile.policies.length > 0 ? { temporalPolicyId: String(resolved.temporalProfile.policies[0].policyId || ''), temporalTimezone: resolved.temporalProfile.policies[0].timezone === 'UTC' ? 'UTC' : null } : {}),
86
89
  })
87
90
  if (rows.length >= 500) break
88
91
  }
@@ -187,12 +190,13 @@ export function createPricingRuntime(host) {
187
190
  })
188
191
  state.pricingResolutionCache.clear()
189
192
  const backfill = backfillUnpricedCosts()
190
- // Only catalog changes or newly priced usage can alter computed costs; a
191
- // successful sync with an unchanged catalog must not invalidate the query
192
- // snapshot, scoped caches, or records cursors. The sync-health bump still
193
- // rebuilds the snapshot cache so lastSuccessAt is not stale on the next
194
- // full response.
195
- if (String(result.catalog.catalogHash || '') !== previousHash || backfill.priced > 0) markStatsChanged('pricing')
193
+ const temporal = reconcileTemporalPricing()
194
+ // Only catalog changes, newly priced usage, or temporal snapshot
195
+ // reconciliation can alter computed costs; a successful sync with an
196
+ // unchanged catalog must not invalidate the query snapshot, scoped caches,
197
+ // or records cursors. The sync-health bump still rebuilds the snapshot
198
+ // cache so lastSuccessAt is not stale on the next full response.
199
+ if (String(result.catalog.catalogHash || '') !== previousHash || backfill.priced > 0 || temporal.reconciled > 0) markStatsChanged('pricing')
196
200
  else markStatsChanged('sync-health')
197
201
  await persistPricing()
198
202
  await drainLedgerWrites()
@@ -210,8 +214,24 @@ export function createPricingRuntime(host) {
210
214
  const oldCost = normalizeCostSnapshot(item.cost)
211
215
  if (oldCost !== null && oldCost.pricingMode === 'official-model' && oldCost.status === 'priced') continue
212
216
  considered += 1
213
- const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId))
214
- if (next.status !== 'priced') continue
217
+ const billing = billingInstantOf(item, oldCost)
218
+ const next = calculateCost(item.values, resolveCurrentPricing(item.identity || item.modelId), billing.at, billing.source)
219
+ const temporalFailClosed = next.status === 'unsupported' && typeof next.reason === 'string' && next.reason.startsWith('temporal-')
220
+ // A valid fallback (priced) or a deterministic fail-closed verdict
221
+ // (unsupported: history gap / invalid config) replaces the previous state;
222
+ // merely unresolved pricing stays untouched.
223
+ if (next.status !== 'priced' && !temporalFailClosed) continue
224
+ if (oldCost !== null && oldCost.status === next.status && oldCost.total === next.total && oldCost.baseTotal === next.baseTotal) {
225
+ if (item.cost !== next) {
226
+ item.cost = next
227
+ const record = state.ledgerRecords.get(item.sid)
228
+ if (record !== undefined) {
229
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
230
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
231
+ }
232
+ }
233
+ continue
234
+ }
215
235
  if (oldCost !== null) {
216
236
  adjustCostOnly(item, oldCost, -1)
217
237
  adjustQueryCost(item, oldCost, -1)
@@ -242,7 +262,109 @@ export function createPricingRuntime(host) {
242
262
  }
243
263
  return { considered, priced, remaining }
244
264
  }
245
- function updatePricingState(raw, backfill) {
265
+ function costSnapshotEquivalent(left, right) {
266
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false
267
+ if (left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
268
+ for (const key of ['input', 'output', 'cacheRead', 'cacheWrite']) {
269
+ if (left.breakdown !== null && left.breakdown !== undefined && right.breakdown !== null && right.breakdown !== undefined) {
270
+ if (left.breakdown[key] !== right.breakdown[key]) return false
271
+ }
272
+ if (left.rates[key] !== right.rates[key] || right.rates[key] === undefined) return false
273
+ }
274
+ const leftTier = left.selectedTier || null
275
+ const rightTier = right.selectedTier || null
276
+ if (leftTier === null !== (rightTier === null)) return false
277
+ if (leftTier !== null && (leftTier.type !== rightTier.type || leftTier.size !== rightTier.size)) return false
278
+ return true
279
+ }
280
+ /**
281
+ * Controlled DeepSeek temporal reconciliation with auditable-history semantics:
282
+ * legacy v1 snapshots of first-party DeepSeek routes are migrated against the
283
+ * usage instant once; already-priced v2 snapshots are NEVER rewritten by a
284
+ * catalog refresh (their policy hash intentionally excludes live rates) and
285
+ * only a user-explicit repricing (repriceTemporal) recomputes them. Anything
286
+ * that cannot be verified keeps its previous value (never guess a price).
287
+ * Routes that are not first-party DeepSeek are never touched.
288
+ */
289
+ function snapshotPolicyMatches(rawCost, resolved) {
290
+ const exemptReason = rawCost.temporalExemptReason
291
+ const route = resolved.temporalRoute
292
+ if (exemptReason === 'route-not-official') return route !== 'official' && route !== 'mapped'
293
+ if (exemptReason === 'no-temporal-profile' && resolved.temporalProfile === undefined && resolved.temporalConfigInvalid !== true) return true
294
+ if (typeof rawCost.pricingPolicyId !== 'string' || typeof rawCost.pricingPolicyHash !== 'string') return false
295
+ const policies = resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) ? resolved.temporalProfile.policies : []
296
+ if (!policies.some((policy) => policy.policyId === rawCost.pricingPolicyId && policy.policyHash === rawCost.pricingPolicyHash)) return false
297
+ // The snapshot's billing instant must still be covered by (and consistent
298
+ // with) its own policy: an archive that shrank or moved the window leaves
299
+ // the snapshot inside a gap, which must fail closed instead of staying priced.
300
+ const plan = temporalPlanFor(resolved, rawCost.pricingAt)
301
+ if (plan.status !== 'applied') return false
302
+ return plan.policyHash === rawCost.pricingPolicyHash && plan.band === rawCost.pricingBand
303
+ }
304
+
305
+ function reconcileTemporalPricing(options = {}) {
306
+ const force = options.force === true
307
+ let considered = 0
308
+ let reconciled = 0
309
+ const touched = new Set()
310
+ for (const item of state.usageByStep.values()) {
311
+ const rawCost = item.cost
312
+ if (rawCost === null || rawCost === undefined || typeof rawCost !== 'object' || rawCost.pricingMode !== 'official-model') continue
313
+ const identity = coerceIdentity(item.identity || item.modelId)
314
+ const resolved = resolveCurrentPricing(identity)
315
+ const route = resolved.temporalRoute
316
+ if (route !== 'official' && route !== 'mapped') continue
317
+ const legacyShape = rawCost.schemaVersion !== COST_SCHEMA_VERSION || rawCost.pricingTimeSource === null || rawCost.pricingTimeSource === undefined || rawCost.pricingTimeSource === 'legacy-unknown'
318
+ if (!legacyShape && !force) {
319
+ // Priced snapshots stay auditable only while their policy (or exempt
320
+ // verdict) still matches the current archive; snapshots priced under a
321
+ // retired policy (e.g. an earlier effective instant) are migrated once.
322
+ if (rawCost.status === 'priced' && snapshotPolicyMatches(rawCost, resolved)) continue
323
+ if (rawCost.status !== 'priced') continue
324
+ }
325
+ considered += 1
326
+ const oldCost = normalizeCostSnapshot(rawCost)
327
+ const billing = billingInstantOf(item, oldCost)
328
+ const next = calculateCost(item.values, resolved, billing.at, billing.source)
329
+ if (costSnapshotEquivalent(oldCost, next)) {
330
+ if (rawCost !== next) {
331
+ item.cost = next
332
+ const record = state.ledgerRecords.get(item.sid)
333
+ if (record !== undefined) {
334
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
335
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
336
+ }
337
+ reconciled += 1
338
+ }
339
+ continue
340
+ }
341
+ if (oldCost !== null) {
342
+ adjustCostOnly(item, oldCost, -1)
343
+ adjustQueryCost(item, oldCost, -1)
344
+ }
345
+ item.cost = next
346
+ adjustCostOnly(item, next, 1)
347
+ adjustQueryCost(item, next, 1)
348
+ const record = state.ledgerRecords.get(item.sid)
349
+ if (record !== undefined) {
350
+ const stored = record.usage.find((candidate) => candidate.key === item.key)
351
+ if (stored !== undefined) { stored.cost = next; touched.add(record) }
352
+ }
353
+ reconciled += 1
354
+ }
355
+ for (const record of touched) {
356
+ record.version = LEDGER_VERSION
357
+ record.updatedAt = nextLedgerRevision()
358
+ // Reconciliation must not clear a mixed-workspace upgrade flag: stay
359
+ // unfoldable until a rebuild normalizes every historical item.
360
+ const stillMixed = record.turns.some((turn) => turn && turn.workspaceId !== record.workspaceId) || record.usage.some((item) => item && item.workspaceId !== record.workspaceId)
361
+ record.needsUpgrade = stillMixed
362
+ void persistLedgerRecord(record)
363
+ }
364
+ return { considered, reconciled }
365
+ }
366
+
367
+ function updatePricingState(raw, backfill, repriceTemporal = false) {
246
368
  const input = raw && typeof raw === 'object' && raw.pricing && typeof raw.pricing === 'object' ? raw.pricing : raw
247
369
  const current = serializePricingState(state.pricingState)
248
370
  const merged = { ...current, ...(input && typeof input === 'object' ? input : {}) }
@@ -250,9 +372,10 @@ export function createPricingRuntime(host) {
250
372
  state.pricingState = normalizePricingState(merged)
251
373
  state.pricingResolutionCache.clear()
252
374
  const result = backfill === true ? backfillUnpricedCosts() : { considered: 0, priced: 0, remaining: state.totals.cost.unpricedCalls + state.totals.cost.ambiguousCalls + state.totals.cost.unsupportedCalls }
375
+ const temporal = reconcileTemporalPricing({ force: repriceTemporal === true })
253
376
  markStatsChanged('pricing')
254
377
  schedulePricingSync()
255
- return result
378
+ return { ...result, temporalReconciled: temporal.reconciled }
256
379
  }
257
380
  function schedulePricingSync() {
258
381
  if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
@@ -276,6 +399,7 @@ export function createPricingRuntime(host) {
276
399
  persistPricing,
277
400
  syncPricing,
278
401
  backfillUnpricedCosts,
402
+ reconcileTemporalPricing,
279
403
  updatePricingState,
280
404
  schedulePricingSync
281
405
  }