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/CHANGELOG.md +46 -1
- package/README.md +10 -37
- 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/lib/aggregation.js +58 -10
- package/lib/client.js +1 -1
- package/lib/http.js +1 -1
- package/lib/ledger.js +32 -5
- package/lib/plugin.js +1 -0
- package/lib/pricing-runtime.js +135 -11
- package/lib/pricing.js +335 -6
- package/lib/session-sync.js +63 -10
- package/lib/usage-core.js +44 -0
- package/package.json +1 -1
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'
|
|
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
|
-
|
|
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(),
|
package/lib/pricing-runtime.js
CHANGED
|
@@ -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
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
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
|
|
214
|
-
|
|
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
|
|
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
|
}
|