dsh-all-usage 1.1.3 → 1.1.5

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/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 = 1
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: tierScheduleValid && resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : tiered ? 'unsupported' : 'unpriced',
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: tiered && !tierScheduleValid ? 'tiered-pricing-not-modeled' : resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found',
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
  }