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/pricing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
const PRICING_SCHEMA_VERSION = 1
|
|
4
|
-
const COST_SCHEMA_VERSION =
|
|
4
|
+
const COST_SCHEMA_VERSION = 2
|
|
5
5
|
const MODEL_CATALOG_URL = 'https://models.dev/api.json'
|
|
6
6
|
const DEFAULT_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000
|
|
7
7
|
const MAX_CATALOG_BYTES = 24 * 1024 * 1024
|
|
@@ -14,6 +14,11 @@ const MAX_CATALOG_CANDIDATES = 30000
|
|
|
14
14
|
const MAX_OVERRIDES = 500
|
|
15
15
|
const MAX_MAPPINGS = 500
|
|
16
16
|
const MAX_CONTEXT_TIERS = 32
|
|
17
|
+
const MAX_TEMPORAL_RULES = 16
|
|
18
|
+
const MAX_TEMPORAL_WINDOWS = 16
|
|
19
|
+
const MAX_TEMPORAL_POLICIES = 8
|
|
20
|
+
const TEMPORAL_TIMEZONE = 'UTC'
|
|
21
|
+
const TEMPORAL_TIME_SOURCES = ['request-context', 'usage-event', 'legacy-unknown']
|
|
17
22
|
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
18
23
|
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
19
24
|
const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
|
|
@@ -25,7 +30,7 @@ const OFFICIAL_PROVIDER_RULES = [
|
|
|
25
30
|
{ providers: ['anthropic'], prefixes: ['claude-'] },
|
|
26
31
|
{ providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
|
|
27
32
|
{ providers: ['xai'], prefixes: ['grok-'] },
|
|
28
|
-
{ providers: ['deepseek'], prefixes: ['deepseek-'] },
|
|
33
|
+
{ providers: ['deepseek', 'deepseek-official'], prefixes: ['deepseek-'] },
|
|
29
34
|
{ providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
|
|
30
35
|
{ providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
|
|
31
36
|
{ providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
|
|
@@ -325,6 +330,227 @@ function decimalRate(raw, keys, fallback = undefined) {
|
|
|
325
330
|
return decimalText(value)
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
function temporalRatesOf(raw, fallback) {
|
|
334
|
+
const rates = isRecord(raw) ? raw : {}
|
|
335
|
+
const input = decimalRate(rates, ['input', 'inputPerMillion'], fallback && fallback.input)
|
|
336
|
+
const output = decimalRate(rates, ['output', 'outputPerMillion'], fallback && fallback.output)
|
|
337
|
+
const cacheRead = decimalRate(rates, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], fallback && fallback.cacheRead)
|
|
338
|
+
const cacheWrite = decimalRate(rates, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], fallback && fallback.cacheWrite)
|
|
339
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null) return null
|
|
340
|
+
const parts = [decimalParts(input), decimalParts(output), decimalParts(cacheRead), decimalParts(cacheWrite)]
|
|
341
|
+
if (parts.some((entry) => entry === null || entry.digits < 0n)) return null
|
|
342
|
+
return { input, output, cacheRead, cacheWrite }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeTemporalWindows(raw) {
|
|
346
|
+
if (!Array.isArray(raw)) return null
|
|
347
|
+
const windows = []
|
|
348
|
+
for (const candidate of raw.slice(0, MAX_TEMPORAL_WINDOWS)) {
|
|
349
|
+
if (!isRecord(candidate)) return null
|
|
350
|
+
const startMinute = finiteNumber(candidate.startMinute)
|
|
351
|
+
const endMinute = finiteNumber(candidate.endMinute)
|
|
352
|
+
if (startMinute === null || endMinute === null || !Number.isInteger(startMinute) || !Number.isInteger(endMinute) || startMinute < 0 || startMinute >= 1440 || endMinute <= startMinute || endMinute > 1440) return null
|
|
353
|
+
windows.push({ startMinute, endMinute })
|
|
354
|
+
}
|
|
355
|
+
if (windows.length === 0) return null
|
|
356
|
+
// Half-open [start, end) windows that overlap would make the band ambiguous;
|
|
357
|
+
// deterministic order plus adjacency-only constraint keeps selection stable.
|
|
358
|
+
windows.sort((left, right) => left.startMinute - right.startMinute || left.endMinute - right.endMinute)
|
|
359
|
+
for (let index = 1; index < windows.length; index += 1) {
|
|
360
|
+
if (windows[index].startMinute < windows[index - 1].endMinute) return null
|
|
361
|
+
}
|
|
362
|
+
return windows
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** One versioned temporal policy: effective-windowed, UTC-band rules. */
|
|
366
|
+
function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
|
|
367
|
+
if (!isRecord(raw)) return null
|
|
368
|
+
const policyId = typeof raw.policyId === 'string' && raw.policyId.trim() !== '' ? raw.policyId.trim().slice(0, 128) : ''
|
|
369
|
+
if (policyId === '') return null
|
|
370
|
+
const sourceUrl = typeof raw.sourceUrl === 'string' && raw.sourceUrl.trim() !== '' ? raw.sourceUrl.trim().slice(0, 512) : ''
|
|
371
|
+
const timezone = typeof raw.timezone === 'string' && raw.timezone.trim() !== '' ? raw.timezone.trim().toUpperCase() : TEMPORAL_TIMEZONE
|
|
372
|
+
// DeepSeek peak/off-peak windows are defined in UTC; any other timezone is
|
|
373
|
+
// rejected outright so band selection can never depend on the viewer's clock.
|
|
374
|
+
if (timezone !== TEMPORAL_TIMEZONE) return null
|
|
375
|
+
const effectiveFrom = raw.effectiveFrom === undefined || raw.effectiveFrom === null ? 0 : finiteNumber(raw.effectiveFrom)
|
|
376
|
+
const effectiveUntil = raw.effectiveUntil === undefined || raw.effectiveUntil === null ? null : finiteNumber(raw.effectiveUntil)
|
|
377
|
+
if (effectiveFrom === null || !Number.isSafeInteger(effectiveFrom) || effectiveFrom < 0) return null
|
|
378
|
+
if (effectiveUntil !== null && (!Number.isSafeInteger(effectiveUntil) || effectiveUntil <= effectiveFrom)) return null
|
|
379
|
+
let defaultPlan = null
|
|
380
|
+
if (raw.defaultPlan !== undefined && raw.defaultPlan !== null) {
|
|
381
|
+
if (!isRecord(raw.defaultPlan)) return null
|
|
382
|
+
const rates = temporalRatesOf(raw.defaultPlan.rates || raw.defaultPlan, entryDefaultRates)
|
|
383
|
+
if (rates === null) return null
|
|
384
|
+
defaultPlan = { id: typeof raw.defaultPlan.id === 'string' && raw.defaultPlan.id.trim() !== '' ? raw.defaultPlan.id.trim().slice(0, 64) : 'default', rates }
|
|
385
|
+
}
|
|
386
|
+
if (raw.rules !== undefined && !Array.isArray(raw.rules)) return null
|
|
387
|
+
if (!Array.isArray(raw.rules)) return null
|
|
388
|
+
if (raw.rules.length > MAX_TEMPORAL_RULES) return null
|
|
389
|
+
const rules = []
|
|
390
|
+
for (const rawRule of raw.rules) {
|
|
391
|
+
if (!isRecord(rawRule)) return null
|
|
392
|
+
const id = typeof rawRule.id === 'string' && rawRule.id.trim() !== '' ? rawRule.id.trim().slice(0, 64) : ''
|
|
393
|
+
if (id === '') return null
|
|
394
|
+
const weekdays = Array.from(new Set((Array.isArray(rawRule.weekdays) ? rawRule.weekdays : []).map(Number)))
|
|
395
|
+
if (weekdays.length === 0 || weekdays.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) return null
|
|
396
|
+
weekdays.sort((left, right) => left - right)
|
|
397
|
+
const windows = normalizeTemporalWindows(rawRule.windows)
|
|
398
|
+
if (windows === null) return null
|
|
399
|
+
const rates = temporalRatesOf(rawRule.rates || rawRule, defaultPlan ? defaultPlan.rates : entryDefaultRates)
|
|
400
|
+
if (rates === null) return null
|
|
401
|
+
rules.push({ id, weekdays, windows, rates })
|
|
402
|
+
}
|
|
403
|
+
if (rules.length === 0) return null
|
|
404
|
+
// Rules must never both claim the same weekday minute: a JSON array order
|
|
405
|
+
// that decides a price is nondeterministic and must be rejected instead.
|
|
406
|
+
const dayWindows = new Map()
|
|
407
|
+
for (const rule of rules) {
|
|
408
|
+
for (const day of rule.weekdays) {
|
|
409
|
+
let list = dayWindows.get(day)
|
|
410
|
+
if (list === undefined) { list = []; dayWindows.set(day, list) }
|
|
411
|
+
for (const window of rule.windows) list.push(window)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const list of dayWindows.values()) {
|
|
415
|
+
list.sort((left, right) => left.startMinute - right.startMinute || left.endMinute - right.endMinute)
|
|
416
|
+
for (let index = 1; index < list.length; index += 1) {
|
|
417
|
+
if (list[index].startMinute < list[index - 1].endMinute) return null
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return { policyId, sourceUrl, timezone, effectiveFrom, effectiveUntil, defaultPlan, rules }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Temporal pricing plan: an ordered, non-overlapping archive of policies. */
|
|
424
|
+
function normalizeTemporalPricing(raw, entryDefaultRates = undefined) {
|
|
425
|
+
if (!isRecord(raw)) return null
|
|
426
|
+
const rawPolicies = Array.isArray(raw.policies) ? raw.policies : (typeof raw.policyId === 'string' ? [raw] : null)
|
|
427
|
+
if (rawPolicies === null || rawPolicies.length === 0 || rawPolicies.length > MAX_TEMPORAL_POLICIES) return null
|
|
428
|
+
const policies = []
|
|
429
|
+
for (const rawPolicy of rawPolicies) {
|
|
430
|
+
const policy = normalizeTemporalPolicy(rawPolicy, entryDefaultRates)
|
|
431
|
+
if (policy === null) return null
|
|
432
|
+
policies.push(policy)
|
|
433
|
+
}
|
|
434
|
+
policies.sort((left, right) => left.effectiveFrom - right.effectiveFrom || (left.policyId < right.policyId ? -1 : left.policyId > right.policyId ? 1 : 0))
|
|
435
|
+
// A policy whose window never ends must be the last one; successive windows
|
|
436
|
+
// may leave a gap (which fails closed) but must never overlap.
|
|
437
|
+
for (let index = 1; index < policies.length; index += 1) {
|
|
438
|
+
const previous = policies[index - 1]
|
|
439
|
+
if (previous.effectiveUntil === null || policies[index].effectiveFrom < previous.effectiveUntil) return null
|
|
440
|
+
}
|
|
441
|
+
return { policies }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const DEEPSEEK_TEMPORAL_POLICY_ID = 'deepseek-v4-2026-08-pricing'
|
|
445
|
+
const DEEPSEEK_TEMPORAL_POLICY_URL = 'https://api-docs.deepseek.com/quick_start/pricing/'
|
|
446
|
+
// DeepSeek announced peak/off-peak pricing effective 2026-08-16T16:00:00Z; the
|
|
447
|
+
// V4-Flash-Vision-Exp model itself became available on 2026-08-21. Before those
|
|
448
|
+
// instants there is no verifiable band archive, so usage fails closed instead
|
|
449
|
+
// of inheriting today's V4 rates.
|
|
450
|
+
const DEEPSEEK_TEMPORAL_EFFECTIVE_FROM = Date.UTC(2026, 7, 16, 16, 0, 0)
|
|
451
|
+
const DEEPSEEK_VISION_EFFECTIVE_FROM = Date.UTC(2026, 7, 21, 0, 0, 0)
|
|
452
|
+
// Built-in first-party profiles: each model carries its own explicit off-peak
|
|
453
|
+
// (inherited from the live catalog entry) and peak rates; the peak rates are
|
|
454
|
+
// stored as data, never derived as an automatic 'half price' rule.
|
|
455
|
+
const DEEPSEEK_TEMPORAL_MODELS = new Map([
|
|
456
|
+
['deepseek-v4-flash', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
457
|
+
['deepseek-v4-flash-vision-exp', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_VISION_EFFECTIVE_FROM }],
|
|
458
|
+
['deepseek-v4-pro', { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
459
|
+
])
|
|
460
|
+
|
|
461
|
+
function builtinTemporalProfileFor(modelId) {
|
|
462
|
+
const spec = DEEPSEEK_TEMPORAL_MODELS.get(normalizeModelId(modelId))
|
|
463
|
+
if (spec === undefined) return null
|
|
464
|
+
return {
|
|
465
|
+
policies: [{
|
|
466
|
+
policyId: DEEPSEEK_TEMPORAL_POLICY_ID,
|
|
467
|
+
sourceUrl: DEEPSEEK_TEMPORAL_POLICY_URL,
|
|
468
|
+
timezone: TEMPORAL_TIMEZONE,
|
|
469
|
+
effectiveFrom: spec.effectiveFrom,
|
|
470
|
+
effectiveUntil: null,
|
|
471
|
+
defaultPlan: null,
|
|
472
|
+
rules: [{ id: 'peak', weekdays: [1, 2, 3, 4, 5], windows: [{ startMinute: 60, endMinute: 240 }, { startMinute: 360, endMinute: 600 }], rates: { input: spec.input, output: spec.output, cacheRead: spec.cacheRead, cacheWrite: spec.cacheWrite } }],
|
|
473
|
+
}],
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Policy covering an instant inside a profile's ordered policy archive. */
|
|
478
|
+
function temporalPolicyFor(profile, atMs) {
|
|
479
|
+
const policies = isRecord(profile) ? profile.policies : null
|
|
480
|
+
if (!Array.isArray(policies) || policies.length === 0 || !Number.isFinite(atMs)) return null
|
|
481
|
+
const at = Math.trunc(atMs)
|
|
482
|
+
for (const policy of policies) {
|
|
483
|
+
if (at < policy.effectiveFrom) continue
|
|
484
|
+
if (policy.effectiveUntil !== null && at >= policy.effectiveUntil) continue
|
|
485
|
+
return policy
|
|
486
|
+
}
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** UTC half-open band: weekday in rule set AND minute within [start, end). */
|
|
491
|
+
function temporalBand(profile, atMs) {
|
|
492
|
+
if (!Number.isFinite(atMs) || atMs < 0) return null
|
|
493
|
+
const policy = temporalPolicyFor(profile, atMs)
|
|
494
|
+
if (policy === null) return null
|
|
495
|
+
const matched = matchingTemporalRule(policy, atMs)
|
|
496
|
+
return matched === null ? 'off-peak' : 'peak'
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function matchingTemporalRule(policy, atMs) {
|
|
500
|
+
const date = new Date(Math.trunc(atMs))
|
|
501
|
+
const weekday = date.getUTCDay()
|
|
502
|
+
const minute = date.getUTCHours() * 60 + date.getUTCMinutes()
|
|
503
|
+
for (const rule of (policy && Array.isArray(policy.rules) ? policy.rules : [])) {
|
|
504
|
+
if (!(Array.isArray(rule.weekdays) ? rule.weekdays : []).includes(weekday)) continue
|
|
505
|
+
if ((Array.isArray(rule.windows) ? rule.windows : []).some((window) => minute >= window.startMinute && minute < window.endMinute)) return rule
|
|
506
|
+
}
|
|
507
|
+
return null
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Deterministic hash of one policy: the policy content only, never the live
|
|
512
|
+
* catalog rates. A snapshot's policy hash is therefore stable across catalog
|
|
513
|
+
* refreshes, which keeps historical costs auditable (see the reconciliation
|
|
514
|
+
* rules); changing the policy itself yields a new hash.
|
|
515
|
+
*/
|
|
516
|
+
function temporalPolicyHash(policy) {
|
|
517
|
+
const stable = JSON.stringify([
|
|
518
|
+
policy.policyId,
|
|
519
|
+
policy.timezone,
|
|
520
|
+
policy.effectiveFrom,
|
|
521
|
+
policy.effectiveUntil,
|
|
522
|
+
policy.defaultPlan,
|
|
523
|
+
policy.rules,
|
|
524
|
+
])
|
|
525
|
+
return createHash('sha256').update(stable).digest('hex')
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Resolve one usage instant against a resolved pricing entry: which policy
|
|
530
|
+
* covers it, which band applies and which rates that band carries. Returns the
|
|
531
|
+
* stable signature that both calculateCost() and the snapshot-reuse check
|
|
532
|
+
* consume.
|
|
533
|
+
*/
|
|
534
|
+
function temporalPlanFor(resolved, pricingAtMs) {
|
|
535
|
+
if (isRecord(resolved) && resolved.temporalConfigInvalid === true) {
|
|
536
|
+
return { status: 'config-invalid', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'temporal-config-invalid' }
|
|
537
|
+
}
|
|
538
|
+
const profile = isRecord(resolved) && resolved.temporalProfile ? resolved.temporalProfile : null
|
|
539
|
+
if (profile === null) {
|
|
540
|
+
return { status: 'none', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'no-temporal-profile' }
|
|
541
|
+
}
|
|
542
|
+
const route = resolved.temporalRoute === 'official' || resolved.temporalRoute === 'mapped' ? resolved.temporalRoute : 'other'
|
|
543
|
+
if (route === 'other') return { status: 'other-route', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: 'route-not-official' }
|
|
544
|
+
if (!Number.isFinite(pricingAtMs) || pricingAtMs < 0) return { status: 'time-missing', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
545
|
+
const at = Math.trunc(pricingAtMs)
|
|
546
|
+
const policy = temporalPolicyFor(profile, at)
|
|
547
|
+
if (policy === null) return { status: 'history-gap', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
548
|
+
const common = { policyId: policy.policyId || null, policyHash: policy.policyHash || null, timezone: policy.timezone === TEMPORAL_TIMEZONE ? TEMPORAL_TIMEZONE : null }
|
|
549
|
+
const rule = matchingTemporalRule(policy, at)
|
|
550
|
+
if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, exemptReason: null, ...common }
|
|
551
|
+
return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, exemptReason: null, ...common }
|
|
552
|
+
}
|
|
553
|
+
|
|
328
554
|
function normalizeContextTier(raw, fallbackRates, legacySize = null) {
|
|
329
555
|
if (!isRecord(raw)) return null
|
|
330
556
|
const descriptor = isRecord(raw.tier) ? raw.tier : raw
|
|
@@ -377,6 +603,12 @@ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
|
377
603
|
tiers.length = 0
|
|
378
604
|
tieredInvalid = false
|
|
379
605
|
} else if (tiers.length === 0) tieredInvalid = true
|
|
606
|
+
const hasTemporalConfig = raw.temporalPricing !== undefined && raw.temporalPricing !== null
|
|
607
|
+
const temporalPricing = hasTemporalConfig ? normalizeTemporalPricing(raw.temporalPricing, fallbackRates) || undefined : undefined
|
|
608
|
+
// An explicit invalid config keeps its sentinel across the normalize →
|
|
609
|
+
// serialize → normalize cycle, so a restart cannot silently re-enable the
|
|
610
|
+
// built-in profile after a rejected temporalPricing edit.
|
|
611
|
+
const temporalPricingInvalid = hasTemporalConfig && temporalPricing === undefined ? true : raw.temporalPricingInvalid === true && temporalPricing === undefined
|
|
380
612
|
return {
|
|
381
613
|
providerId: typeof raw.providerId === 'string' ? raw.providerId.trim().toLowerCase() : '',
|
|
382
614
|
providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
|
|
@@ -392,6 +624,8 @@ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
|
392
624
|
...(tiered ? { tiers, tieredInvalid } : {}),
|
|
393
625
|
reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
|
|
394
626
|
fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
|
|
627
|
+
...(temporalPricing === undefined ? {} : { temporalPricing }),
|
|
628
|
+
...(temporalPricingInvalid === true ? { temporalPricingInvalid: true } : {}),
|
|
395
629
|
}
|
|
396
630
|
}
|
|
397
631
|
|
|
@@ -559,7 +793,10 @@ function serializePricingState(state) {
|
|
|
559
793
|
}
|
|
560
794
|
|
|
561
795
|
function sameRates(left, right) {
|
|
562
|
-
if (left.input !== right.input || left.output !== right.output || left.cacheRead !== right.cacheRead || left.cacheWrite !== right.cacheWrite || left.currency !== right.currency || left.tiered !== right.tiered || left.tieredInvalid !== right.tieredInvalid) return false
|
|
796
|
+
if (left.input !== right.input || left.output !== right.output || left.cacheRead !== right.cacheRead || left.cacheWrite !== right.cacheWrite || left.currency !== right.currency || left.tiered !== right.tiered || left.tieredInvalid !== right.tieredInvalid || left.temporalPricingInvalid !== right.temporalPricingInvalid) return false
|
|
797
|
+
// Two entries with identical flat rates but different band plans are NOT the
|
|
798
|
+
// same price: the winner must never depend on enumeration order.
|
|
799
|
+
if (JSON.stringify(left.temporalPricing || null) !== JSON.stringify(right.temporalPricing || null)) return false
|
|
563
800
|
const leftTiers = Array.isArray(left.tiers) ? left.tiers : []
|
|
564
801
|
const rightTiers = Array.isArray(right.tiers) ? right.tiers : []
|
|
565
802
|
if (leftTiers.length !== rightTiers.length) return false
|
|
@@ -590,6 +827,32 @@ function findMappedEntry(mapping, state) {
|
|
|
590
827
|
return null
|
|
591
828
|
}
|
|
592
829
|
|
|
830
|
+
const DEEPSEEK_OFFICIAL_PROVIDERS = ['deepseek', 'deepseek-official']
|
|
831
|
+
|
|
832
|
+
function attachTemporalProfile(entry, mapped, identity) {
|
|
833
|
+
if (entry === null || entry === undefined || !isRecord(entry)) return null
|
|
834
|
+
// An explicit but invalid temporal config must never silently fall back to
|
|
835
|
+
// the built-in profile: the configuration error stays visible and the model
|
|
836
|
+
// fails closed instead of pricing with a plan the user did not write.
|
|
837
|
+
if (entry.temporalPricingInvalid === true) return { invalid: true }
|
|
838
|
+
let profile = isRecord(entry.temporalPricing) ? entry.temporalPricing : null
|
|
839
|
+
if (profile === null && DEEPSEEK_OFFICIAL_PROVIDERS.includes(normalizeProvider(entry.providerId))) profile = builtinTemporalProfileFor(entry.modelId)
|
|
840
|
+
if (profile === null) return null
|
|
841
|
+
const providerId = normalizeProvider(entry.providerId)
|
|
842
|
+
const identityProvider = normalizeProvider(identity && identity.provider)
|
|
843
|
+
// Safety boundary: the peak/off-peak table applies only when the DSH route
|
|
844
|
+
// is the first-party provider, or when the user explicitly mapped this route
|
|
845
|
+
// to the DeepSeek official entry. OpenRouter and other resellers never get
|
|
846
|
+
// the DeepSeek temporal band automatically.
|
|
847
|
+
const route = DEEPSEEK_OFFICIAL_PROVIDERS.includes(providerId) && DEEPSEEK_OFFICIAL_PROVIDERS.includes(identityProvider) ? 'official' : mapped !== undefined && mapped !== null ? 'mapped' : 'other'
|
|
848
|
+
return {
|
|
849
|
+
route,
|
|
850
|
+
profile: {
|
|
851
|
+
policies: profile.policies.map((policy) => ({ ...policy, policyHash: temporalPolicyHash(policy) })),
|
|
852
|
+
},
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
593
856
|
function resolvePricing(identity, rawState) {
|
|
594
857
|
const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
|
|
595
858
|
const identityKey = identityKeyOf(identity)
|
|
@@ -649,6 +912,7 @@ function resolvePricing(identity, rawState) {
|
|
|
649
912
|
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
650
913
|
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
651
914
|
multiplier: mapped ? mapped.multiplier : '1',
|
|
915
|
+
...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route } })(),
|
|
652
916
|
}
|
|
653
917
|
}
|
|
654
918
|
const model = models[0] || null
|
|
@@ -706,7 +970,7 @@ function selectContextTier(tiers, contextTokens) {
|
|
|
706
970
|
return selected
|
|
707
971
|
}
|
|
708
972
|
|
|
709
|
-
function calculateCost(values, resolved) {
|
|
973
|
+
function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource = 'usage-event') {
|
|
710
974
|
const input = finiteNumber(values && values.input) || 0
|
|
711
975
|
const output = finiteNumber(values && values.output) || 0
|
|
712
976
|
const cacheRead = finiteNumber(values && values.cacheRead) || 0
|
|
@@ -717,10 +981,36 @@ function calculateCost(values, resolved) {
|
|
|
717
981
|
const tiered = resolved && resolved.tiered === true
|
|
718
982
|
const tiers = tiered && Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
719
983
|
const tierScheduleValid = !tiered || validContextTierSchedule(tiers)
|
|
984
|
+
const plan = temporalPlanFor(resolved, pricingAtMs)
|
|
985
|
+
const pricingAt = Number.isFinite(pricingAtMs) && pricingAtMs >= 0 ? Math.trunc(pricingAtMs) : null
|
|
986
|
+
const timeSource = TEMPORAL_TIME_SOURCES.includes(pricingTimeSource) ? pricingTimeSource : 'usage-event'
|
|
987
|
+
let status = tierScheduleValid && resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : tiered ? 'unsupported' : 'unpriced'
|
|
988
|
+
let reason = tiered && !tierScheduleValid ? 'tiered-pricing-not-modeled' : resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found'
|
|
989
|
+
if (status === 'priced') {
|
|
990
|
+
if (plan.status === 'config-invalid') {
|
|
991
|
+
// The user submitted a temporal config that failed validation; never
|
|
992
|
+
// silently price with the built-in profile or the static fallback.
|
|
993
|
+
status = 'unsupported'
|
|
994
|
+
reason = 'temporal-config-invalid'
|
|
995
|
+
} else if (tiered && (plan.status === 'applied' || plan.status === 'history-gap' || plan.status === 'time-missing')) {
|
|
996
|
+
// Band plans and context tiers are two independent pricing axes; a model
|
|
997
|
+
// carrying both is not modeled and fails closed instead of guessing.
|
|
998
|
+
status = 'unsupported'
|
|
999
|
+
reason = 'temporal-tiered-unsupported'
|
|
1000
|
+
} else if (plan.status === 'history-gap') {
|
|
1001
|
+
// A profile exists but the usage instant falls outside its effective
|
|
1002
|
+
// window: never discount into an unverifiable period.
|
|
1003
|
+
status = 'unsupported'
|
|
1004
|
+
reason = 'temporal-price-history-unavailable'
|
|
1005
|
+
} else if (plan.status === 'time-missing') {
|
|
1006
|
+
status = 'unsupported'
|
|
1007
|
+
reason = 'temporal-time-unavailable'
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
720
1010
|
const base = {
|
|
721
1011
|
schemaVersion: COST_SCHEMA_VERSION,
|
|
722
1012
|
pricingMode: 'official-model',
|
|
723
|
-
status
|
|
1013
|
+
status,
|
|
724
1014
|
currency: resolved && resolved.currency ? resolved.currency : 'USD',
|
|
725
1015
|
source: resolved && resolved.source ? resolved.source : 'none',
|
|
726
1016
|
pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
|
|
@@ -729,11 +1019,19 @@ function calculateCost(values, resolved) {
|
|
|
729
1019
|
multiplier: decimalText(resolved && resolved.multiplier !== undefined ? resolved.multiplier : '1') || '1',
|
|
730
1020
|
billableInputTokens: Math.trunc(billableInput),
|
|
731
1021
|
billableOutputTokens: Math.trunc(billableOutput),
|
|
1022
|
+
pricingAt,
|
|
1023
|
+
pricingTimeSource: timeSource,
|
|
1024
|
+
pricingBand: plan.band,
|
|
1025
|
+
pricingTimezone: plan.timezone,
|
|
1026
|
+
pricingPolicyId: plan.policyId,
|
|
1027
|
+
pricingPolicyHash: plan.policyHash,
|
|
1028
|
+
temporalApplicable: plan.status === 'applied',
|
|
1029
|
+
temporalExemptReason: plan.exemptReason,
|
|
732
1030
|
rates: resolved && resolved.rates ? { ...resolved.rates } : { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
733
1031
|
breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
734
1032
|
baseTotal: '0',
|
|
735
1033
|
total: '0',
|
|
736
|
-
reason
|
|
1034
|
+
reason,
|
|
737
1035
|
tiered,
|
|
738
1036
|
reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
|
|
739
1037
|
}
|
|
@@ -745,6 +1043,10 @@ function calculateCost(values, resolved) {
|
|
|
745
1043
|
base.rates = { input: selectedRates.input, output: selectedRates.output, cacheRead: selectedRates.cacheRead, cacheWrite: selectedRates.cacheWrite }
|
|
746
1044
|
base.contextTokens = contextTokens
|
|
747
1045
|
base.selectedTier = { type: 'context', size: selectedTier ? selectedTier.size : 0 }
|
|
1046
|
+
} else if (plan.status === 'applied' && plan.ruleRates !== null && typeof plan.ruleRates === 'object') {
|
|
1047
|
+
// Peak windows carry their own rates; off-peak plans without an explicit
|
|
1048
|
+
// default inherit the live entry rates (catalog updates flow through).
|
|
1049
|
+
base.rates = { input: plan.ruleRates.input, output: plan.ruleRates.output, cacheRead: plan.ruleRates.cacheRead, cacheWrite: plan.ruleRates.cacheWrite }
|
|
748
1050
|
}
|
|
749
1051
|
base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
|
|
750
1052
|
base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
|
|
@@ -825,6 +1127,21 @@ function normalizeCostSnapshot(raw) {
|
|
|
825
1127
|
if (!isRecord(raw.selectedTier) || raw.selectedTier.type !== 'context' || !Number.isSafeInteger(raw.selectedTier.size) || raw.selectedTier.size < 0 || raw.selectedTier.size > 1000000000) return null
|
|
826
1128
|
selectedTier = { type: 'context', size: raw.selectedTier.size }
|
|
827
1129
|
}
|
|
1130
|
+
const isV2 = raw.schemaVersion === COST_SCHEMA_VERSION
|
|
1131
|
+
const legacy = !isV2
|
|
1132
|
+
let pricingAt = null
|
|
1133
|
+
if (raw.pricingAt !== undefined && raw.pricingAt !== null) {
|
|
1134
|
+
if (!Number.isFinite(raw.pricingAt) || raw.pricingAt < 0 || !Number.isSafeInteger(raw.pricingAt)) return null
|
|
1135
|
+
pricingAt = raw.pricingAt
|
|
1136
|
+
}
|
|
1137
|
+
if (raw.pricingTimeSource !== undefined && raw.pricingTimeSource !== null && raw.pricingTimeSource !== 'legacy-unknown' && !TEMPORAL_TIME_SOURCES.includes(raw.pricingTimeSource)) return null
|
|
1138
|
+
if (raw.pricingBand !== undefined && raw.pricingBand !== null && raw.pricingBand !== 'peak' && raw.pricingBand !== 'off-peak') return null
|
|
1139
|
+
if (raw.pricingTimezone !== undefined && raw.pricingTimezone !== null && raw.pricingTimezone !== 'UTC') return null
|
|
1140
|
+
const pricingTimeSource = TEMPORAL_TIME_SOURCES.includes(raw.pricingTimeSource) ? raw.pricingTimeSource : legacy ? 'legacy-unknown' : null
|
|
1141
|
+
const pricingBand = raw.pricingBand === 'peak' || raw.pricingBand === 'off-peak' ? raw.pricingBand : null
|
|
1142
|
+
const pricingTimezone = raw.pricingTimezone === 'UTC' ? 'UTC' : null
|
|
1143
|
+
const pricingPolicyId = typeof raw.pricingPolicyId === 'string' && raw.pricingPolicyId !== '' ? raw.pricingPolicyId.slice(0, 128) : null
|
|
1144
|
+
const pricingPolicyHash = typeof raw.pricingPolicyHash === 'string' && raw.pricingPolicyHash !== '' ? raw.pricingPolicyHash.slice(0, 128) : null
|
|
828
1145
|
return {
|
|
829
1146
|
schemaVersion: COST_SCHEMA_VERSION,
|
|
830
1147
|
pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
|
|
@@ -837,6 +1154,14 @@ function normalizeCostSnapshot(raw) {
|
|
|
837
1154
|
multiplier,
|
|
838
1155
|
billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
|
|
839
1156
|
billableOutputTokens: Number.isFinite(raw.billableOutputTokens) ? Math.max(0, Math.trunc(raw.billableOutputTokens)) : 0,
|
|
1157
|
+
pricingAt,
|
|
1158
|
+
pricingTimeSource,
|
|
1159
|
+
pricingBand,
|
|
1160
|
+
pricingTimezone,
|
|
1161
|
+
pricingPolicyId,
|
|
1162
|
+
pricingPolicyHash,
|
|
1163
|
+
temporalApplicable: raw.temporalApplicable === true,
|
|
1164
|
+
temporalExemptReason: raw.temporalExemptReason === null || raw.temporalExemptReason === undefined ? null : typeof raw.temporalExemptReason === 'string' ? raw.temporalExemptReason.slice(0, 64) : null,
|
|
840
1165
|
...(contextTokens === null ? {} : { contextTokens }),
|
|
841
1166
|
...(selectedTier === null ? {} : { selectedTier }),
|
|
842
1167
|
rates: normalizedRates,
|
|
@@ -901,8 +1226,12 @@ export {
|
|
|
901
1226
|
normalizeCostSnapshot,
|
|
902
1227
|
normalizeModelId,
|
|
903
1228
|
normalizePricingState,
|
|
1229
|
+
normalizeTemporalPricing,
|
|
904
1230
|
parseModelsDevCatalog,
|
|
905
1231
|
resolvePricing,
|
|
906
1232
|
serializeCostAggregate,
|
|
907
1233
|
serializePricingState,
|
|
1234
|
+
temporalBand,
|
|
1235
|
+
temporalPlanFor,
|
|
1236
|
+
temporalPolicyHash,
|
|
908
1237
|
}
|
package/lib/session-sync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractUsageEvent } from './usage-core.js'
|
|
1
|
+
import { contextTimeKey, extractUsageEvent, pickPricingTime, touchContextTimes } from './usage-core.js'
|
|
2
2
|
|
|
3
3
|
const RECONCILE_INTERVAL_MS = 120000
|
|
4
4
|
const RECONCILE_HINT_DELAY_MS = 3000
|
|
@@ -25,6 +25,7 @@ export function createSessionSync(host) {
|
|
|
25
25
|
drainLedgerWrites,
|
|
26
26
|
} = host.ledger
|
|
27
27
|
const backfillUnpricedCosts = (...args) => host.pricing.backfillUnpricedCosts(...args)
|
|
28
|
+
const reconcileTemporalPricing = (...args) => host.pricing.reconcileTemporalPricing(...args)
|
|
28
29
|
const safeContextTimeout = async (ms) => {
|
|
29
30
|
if (state.disposed) return false
|
|
30
31
|
try {
|
|
@@ -38,7 +39,14 @@ export function createSessionSync(host) {
|
|
|
38
39
|
|
|
39
40
|
function foldEvent(wsId, time, type, data, sid, seq, materialization = 'live') {
|
|
40
41
|
if ((type === 'turn/end' || type === 'assistant/message' || type === 'assistant/chunk') && !validEventTime(time)) return
|
|
41
|
-
if (type === 'request/context'
|
|
42
|
+
if (type === 'request/context') {
|
|
43
|
+
state.sessionModel.set(sid, identityFromRoute(data, state.sessionModel.get(sid)))
|
|
44
|
+
if (validEventTime(time)) {
|
|
45
|
+
let times = state.sessionContextTimes.get(sid)
|
|
46
|
+
if (times === undefined) { times = new Map(); state.sessionContextTimes.set(sid, times) }
|
|
47
|
+
touchContextTimes(times, contextTimeKey(data && data.turn, data && data.step), time)
|
|
48
|
+
}
|
|
49
|
+
} else if (type === 'request/header') {
|
|
42
50
|
state.sessionModel.set(sid, identityFromRoute(data, state.sessionModel.get(sid)))
|
|
43
51
|
} else if (type === 'turn/end') {
|
|
44
52
|
addTurn(wsId, time, sid, data && typeof data.turn === 'number' ? data.turn : null, state.sessionModel.get(sid), materialization, seq)
|
|
@@ -47,10 +55,15 @@ export function createSessionSync(host) {
|
|
|
47
55
|
if (usageEvent === null) return
|
|
48
56
|
const identity = usageEvent.kind === 'message' ? identityFromMessage(data, state.sessionModel.get(sid)) : coerceIdentity(state.sessionModel.get(sid))
|
|
49
57
|
state.sessionModel.set(sid, identity)
|
|
50
|
-
|
|
58
|
+
const pricing = pickPricingTime(state.sessionContextTimes.get(sid), time, usageEvent.turn, usageEvent.step)
|
|
59
|
+
addUsage(wsId, time, usageEvent.usage, identity, sid, data, seq, materialization, pricing.time, pricing.source)
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
function foldEvents(wsId, events, fromSeq, sid, materialization = 'scan') {
|
|
63
|
+
// Full folds keep their callers responsible for atomicity: runBaseline and
|
|
64
|
+
// syncLiveSession remove the session's old usage/turn/context/identity
|
|
65
|
+
// first when the snapshot is authoritative (see rebuildSession below), so
|
|
66
|
+
// usage deleted by a history rewrite disappears from the aggregate.
|
|
54
67
|
for (const ev of events) {
|
|
55
68
|
if (fromSeq !== undefined) {
|
|
56
69
|
const s = typeof ev.seq === 'number' ? ev.seq : -1
|
|
@@ -140,13 +153,41 @@ export function createSessionSync(host) {
|
|
|
140
153
|
const snap = await ctx.sessionQuery.readSession(sid)
|
|
141
154
|
if (state.disposed || generation !== state.aggregationGeneration) return true
|
|
142
155
|
if (snap && Array.isArray(snap.events)) {
|
|
143
|
-
const previousLast = state.sessionSeq.get(sid)
|
|
144
|
-
const previousLastSafe = Number.isSafeInteger(previousLast) && previousLast >= 0 ? previousLast : -1
|
|
145
156
|
const snapshotLast = lastSeqOf(snap.events)
|
|
146
|
-
|
|
147
|
-
let nextLast = Math.max(previousLastSafe, snapshotLast)
|
|
157
|
+
const snapshotProfile = sequenceProfile(snap.events)
|
|
148
158
|
const eventSeq = event === null || event === undefined ? -1 : (Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : -1)
|
|
149
|
-
|
|
159
|
+
// The snapshot may replace the session only when it is provably
|
|
160
|
+
// complete AND it already contains the triggering event: a timer / full
|
|
161
|
+
// resync (no event), or an explicitly contained, monotonic event
|
|
162
|
+
// stream. A snapshot that merely has a higher tail than the event is
|
|
163
|
+
// NOT proof — the event could be a live append the snapshot missed (or
|
|
164
|
+
// the snapshot could have been rewritten without it), so the event is
|
|
165
|
+
// upserted and the session stays pending for a follow-up alignment.
|
|
166
|
+
// Events without a usable seq never trigger a destructive replacement.
|
|
167
|
+
const containsEvent = event !== null && event !== undefined && eventSeq >= 0 && snap.events.some((candidate) => candidate && Number.isSafeInteger(candidate.seq) && candidate.seq === eventSeq)
|
|
168
|
+
const authoritative = event === null || event === undefined ? true : eventSeq < 0 ? false : containsEvent && !snapshotProfile.nonMonotonic && !snapshotProfile.hasInvalid
|
|
169
|
+
if (authoritative) {
|
|
170
|
+
host.aggregation.removeSession(sid)
|
|
171
|
+
// The authoritative snapshot is the new truth for both the in-memory
|
|
172
|
+
// aggregate and the durable ledger: rebuild the record from scratch
|
|
173
|
+
// and persist it, so a restart after a live history rewrite cannot
|
|
174
|
+
// resurrect usage that the snapshot deleted.
|
|
175
|
+
const rebuiltRecord = buildLedgerRecord({ id: sid, events: snap.events }, wsId, 'scan', undefined, null, true)
|
|
176
|
+
if (rebuiltRecord !== null) {
|
|
177
|
+
const canonical = replaceLedgerRecord(rebuiltRecord)
|
|
178
|
+
if (canonical === rebuiltRecord) {
|
|
179
|
+
void persistLedgerRecord(rebuiltRecord).then(() => {
|
|
180
|
+
if (state.ledgerWriteFailedSessions && state.ledgerWriteFailedSessions.has(sid)) {
|
|
181
|
+
console.error('[all-usage] authoritative resync ledger persist failed; session stays dirty:', sid)
|
|
182
|
+
noteSyncError('resync-ledger-persist-failed')
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
foldEvents(wsId, snap.events, undefined, sid, 'live')
|
|
189
|
+
let nextLast = snapshotLast
|
|
190
|
+
const needsFollowup = !authoritative && event !== null && event !== undefined
|
|
150
191
|
if (needsFollowup) {
|
|
151
192
|
foldLiveFallback(sid, wsId, event)
|
|
152
193
|
const current = state.sessionSeq.get(sid)
|
|
@@ -154,8 +195,14 @@ export function createSessionSync(host) {
|
|
|
154
195
|
}
|
|
155
196
|
state.sessionSeq.set(sid, nextLast)
|
|
156
197
|
state.sessionCount.add(sid)
|
|
157
|
-
if (needsFollowup)
|
|
158
|
-
|
|
198
|
+
if (needsFollowup) {
|
|
199
|
+
// The snapshot is behind: keep the pending flag so the follow-up
|
|
200
|
+
// resync realigns the cursor once the missing history arrives.
|
|
201
|
+
scheduleLiveResync(sid, wsId, generation)
|
|
202
|
+
} else {
|
|
203
|
+
state.liveResyncPending.delete(sid)
|
|
204
|
+
cancelLiveResync(sid)
|
|
205
|
+
}
|
|
159
206
|
return true
|
|
160
207
|
}
|
|
161
208
|
} catch (err) {
|
|
@@ -420,6 +467,12 @@ export function createSessionSync(host) {
|
|
|
420
467
|
markStatsChanged('pricing')
|
|
421
468
|
}
|
|
422
469
|
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
470
|
+
const temporalReconcile = reconcileTemporalPricing()
|
|
471
|
+
if (temporalReconcile.reconciled > 0) {
|
|
472
|
+
await drainLedgerWrites()
|
|
473
|
+
markStatsChanged('pricing')
|
|
474
|
+
}
|
|
475
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
423
476
|
state.knownSessionIds.clear()
|
|
424
477
|
for (const sid of listedSessionIds) state.knownSessionIds.add(sid)
|
|
425
478
|
state.scan.done = true
|
package/lib/usage-core.js
CHANGED
|
@@ -107,6 +107,50 @@ export function usageStepKey(sid, data, seq, fallback) {
|
|
|
107
107
|
return safeSid + ':event:' + (serialized === undefined ? String(fallback === undefined ? '' : fallback) : serialized)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** Recalculate the billing instant for a re-priced sample, preferring the recorded request time. */
|
|
111
|
+
export function billingInstantOf(item, oldCost) {
|
|
112
|
+
const oldAt = oldCost != null && Number.isFinite(oldCost.pricingAt) ? oldCost.pricingAt : null
|
|
113
|
+
const at = Number.isFinite(item.pricingAt) ? item.pricingAt : oldAt !== null ? oldAt : validEventTime(item.time) ? item.time : null
|
|
114
|
+
const oldSource = oldCost != null && (oldCost.pricingTimeSource === 'request-context' || oldCost.pricingTimeSource === 'usage-event') ? oldCost.pricingTimeSource : null
|
|
115
|
+
const source = typeof item.pricingTimeSource === 'string' && (item.pricingTimeSource === 'request-context' || item.pricingTimeSource === 'usage-event') ? item.pricingTimeSource : (oldSource || 'usage-event')
|
|
116
|
+
return { at, source }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Bounded context archive shared by the live fold and the persisted ledger. */
|
|
120
|
+
export const MAX_CONTEXT_TIMES = 512
|
|
121
|
+
|
|
122
|
+
/** Insert-or-refresh a context entry with real LRU eviction (delete + set). */
|
|
123
|
+
export function touchContextTimes(times, key, time) {
|
|
124
|
+
if (typeof key !== 'string' || key === '') return
|
|
125
|
+
if (times.has(key)) times.delete(key)
|
|
126
|
+
times.set(key, time)
|
|
127
|
+
while (times.size > MAX_CONTEXT_TIMES) times.delete(times.keys().next().value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Stable per-(turn, step) key for the pricing-time context of a request. */
|
|
131
|
+
export function contextTimeKey(turn, step) {
|
|
132
|
+
const safeTurn = Number.isSafeInteger(turn) && turn >= 0 ? turn : null
|
|
133
|
+
const safeStep = Number.isSafeInteger(step) && step >= 0 ? step : null
|
|
134
|
+
if (safeTurn !== null && safeStep !== null) return 'context:' + safeTurn + ':' + safeStep
|
|
135
|
+
return 'context:__latest__'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pick the billing instant for a usage event: the request/context time that
|
|
140
|
+
* matches the same turn/step wins (parallel requests stay per-step), otherwise
|
|
141
|
+
* the usage event time itself is the auditable fallback.
|
|
142
|
+
*/
|
|
143
|
+
export function pickPricingTime(contextTimes, eventTime, turn, step) {
|
|
144
|
+
if (contextTimes instanceof Map && validEventTime(eventTime)) {
|
|
145
|
+
const exact = contextTimes.get(contextTimeKey(turn, step))
|
|
146
|
+
const candidate = exact !== undefined ? exact : contextTimes.get(contextTimeKey(null, null))
|
|
147
|
+
if (Number.isFinite(candidate) && candidate <= eventTime) {
|
|
148
|
+
return { time: candidate, source: 'request-context' }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { time: Number.isFinite(eventTime) && eventTime >= 0 ? eventTime : null, source: 'usage-event' }
|
|
152
|
+
}
|
|
153
|
+
|
|
110
154
|
/** Replace one logical sample; stale lower-seq replays are ignored. */
|
|
111
155
|
/** Event sequence contract: -1 (missing) or a non-negative safe integer. */
|
|
112
156
|
export function normalizeEventSeq(value) {
|