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/CHANGELOG.md +58 -2
- package/README.md +31 -44
- 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 +34 -5
- package/lib/plugin.js +4 -0
- package/lib/pricing-runtime.js +135 -11
- package/lib/pricing.js +335 -6
- package/lib/session-sync.js +329 -121
- package/lib/usage-core.js +51 -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
|
}
|