dsh-all-usage 1.1.2 → 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 +111 -0
- package/README.md +183 -36
- 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/fixtures/usage-events.json +172 -0
- package/lib/aggregation.js +1050 -0
- package/lib/balance.js +112 -0
- package/lib/client.js +1 -2906
- package/lib/http.js +305 -0
- package/lib/index.js +2 -2119
- package/lib/ledger.js +491 -0
- package/lib/plugin.js +277 -0
- package/lib/pricing-runtime.js +406 -0
- package/lib/pricing.js +631 -39
- package/lib/session-sync.js +642 -0
- package/lib/usage-core.js +171 -0
- package/package.json +28 -3
- package/scripts/replay-fixture.mjs +155 -0
package/lib/pricing.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
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
|
|
8
8
|
const MAX_PRICE_ENTRIES = 10000
|
|
9
|
+
// Hard candidate ceiling before the sorting-heavy selection: a pathological
|
|
10
|
+
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
11
|
+
// Hard candidate ceiling before any normalization-heavy sort: a pathological
|
|
12
|
+
// upstream catalog is rejected instead of consuming unbounded memory.
|
|
13
|
+
const MAX_CATALOG_CANDIDATES = 30000
|
|
9
14
|
const MAX_OVERRIDES = 500
|
|
10
15
|
const MAX_MAPPINGS = 500
|
|
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']
|
|
11
22
|
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
12
23
|
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
13
24
|
const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
|
|
25
|
+
const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']
|
|
26
|
+
const COST_ACCUMULATOR = Symbol('costAccumulator')
|
|
27
|
+
const COST_PARTS_CACHE = new WeakMap()
|
|
14
28
|
const OFFICIAL_PROVIDER_RULES = [
|
|
15
29
|
{ providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
|
|
16
30
|
{ providers: ['anthropic'], prefixes: ['claude-'] },
|
|
17
31
|
{ providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
|
|
18
32
|
{ providers: ['xai'], prefixes: ['grok-'] },
|
|
19
|
-
{ providers: ['deepseek'], prefixes: ['deepseek-'] },
|
|
33
|
+
{ providers: ['deepseek', 'deepseek-official'], prefixes: ['deepseek-'] },
|
|
20
34
|
{ providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
|
|
21
35
|
{ providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
|
|
22
36
|
{ providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
|
|
@@ -77,6 +91,89 @@ function decimalText(value) {
|
|
|
77
91
|
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
function normalizedDecimalParts(parts) {
|
|
95
|
+
let digits = parts && typeof parts.digits === 'bigint' ? parts.digits : 0n
|
|
96
|
+
let scale = parts && Number.isSafeInteger(parts.scale) && parts.scale >= 0 ? parts.scale : 0
|
|
97
|
+
if (digits < 0n) digits = 0n
|
|
98
|
+
while (scale > 0 && digits !== 0n && digits % 10n === 0n) {
|
|
99
|
+
digits /= 10n
|
|
100
|
+
scale -= 1
|
|
101
|
+
}
|
|
102
|
+
return { digits, scale }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decimalAccumulatorText(value) {
|
|
106
|
+
const parts = normalizedDecimalParts(value)
|
|
107
|
+
if (parts.digits === 0n) return '0'
|
|
108
|
+
const raw = parts.digits.toString()
|
|
109
|
+
if (parts.scale === 0) return raw
|
|
110
|
+
const padded = raw.padStart(parts.scale + 1, '0')
|
|
111
|
+
const split = padded.length - parts.scale
|
|
112
|
+
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCostAccumulator(value) {
|
|
116
|
+
return value !== null && typeof value === 'object' && value[COST_ACCUMULATOR] === true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createCostAccumulator(currency = 'USD') {
|
|
120
|
+
const result = { currency, input: { digits: 0n, scale: 0 }, output: { digits: 0n, scale: 0 }, cacheRead: { digits: 0n, scale: 0 }, cacheWrite: { digits: 0n, scale: 0 }, baseTotal: { digits: 0n, scale: 0 }, total: { digits: 0n, scale: 0 }, pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
121
|
+
Object.defineProperty(result, COST_ACCUMULATOR, { value: true })
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function costAccumulatorParts(cost) {
|
|
126
|
+
if (isCostAccumulator(cost)) return cost
|
|
127
|
+
if (cost !== null && typeof cost === 'object') {
|
|
128
|
+
const cached = COST_PARTS_CACHE.get(cost)
|
|
129
|
+
if (cached !== undefined) return cached
|
|
130
|
+
}
|
|
131
|
+
const value = isRecord(cost) ? cost : {}
|
|
132
|
+
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
133
|
+
const breakdown = isRecord(value.breakdown) ? value.breakdown : {}
|
|
134
|
+
const result = {
|
|
135
|
+
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
136
|
+
status,
|
|
137
|
+
input: decimalParts(breakdown.input) || { digits: 0n, scale: 0 },
|
|
138
|
+
output: decimalParts(breakdown.output) || { digits: 0n, scale: 0 },
|
|
139
|
+
cacheRead: decimalParts(breakdown.cacheRead) || { digits: 0n, scale: 0 },
|
|
140
|
+
cacheWrite: decimalParts(breakdown.cacheWrite) || { digits: 0n, scale: 0 },
|
|
141
|
+
baseTotal: decimalParts(value.baseTotal) || { digits: 0n, scale: 0 },
|
|
142
|
+
total: decimalParts(value.total) || { digits: 0n, scale: 0 },
|
|
143
|
+
}
|
|
144
|
+
if (cost !== null && typeof cost === 'object') COST_PARTS_CACHE.set(cost, result)
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function addDecimalAccumulator(target, field, value, direction) {
|
|
149
|
+
const left = normalizedDecimalParts(target[field])
|
|
150
|
+
const right = normalizedDecimalParts(value)
|
|
151
|
+
const scale = Math.max(left.scale, right.scale)
|
|
152
|
+
let digits = left.digits * 10n ** BigInt(scale - left.scale) + BigInt(direction) * right.digits * 10n ** BigInt(scale - right.scale)
|
|
153
|
+
if (digits < 0n) digits = 0n
|
|
154
|
+
target[field] = normalizedDecimalParts({ digits, scale })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function addCostAccumulator(target, cost, direction = 1) {
|
|
158
|
+
if (!isCostAccumulator(target)) return target
|
|
159
|
+
if (isCostAccumulator(cost)) {
|
|
160
|
+
for (const field of COST_FIELDS) addDecimalAccumulator(target, field, cost[field], direction)
|
|
161
|
+
target.pricedCalls = Math.max(0, target.pricedCalls + direction * cost.pricedCalls)
|
|
162
|
+
target.unpricedCalls = Math.max(0, target.unpricedCalls + direction * cost.unpricedCalls)
|
|
163
|
+
target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction * cost.ambiguousCalls)
|
|
164
|
+
target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction * cost.unsupportedCalls)
|
|
165
|
+
return target
|
|
166
|
+
}
|
|
167
|
+
const value = costAccumulatorParts(cost)
|
|
168
|
+
if (value.status === 'priced') {
|
|
169
|
+
for (const field of COST_FIELDS) addDecimalAccumulator(target, field, value[field], direction)
|
|
170
|
+
target.pricedCalls = Math.max(0, target.pricedCalls + direction)
|
|
171
|
+
} else if (value.status === 'ambiguous') target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction)
|
|
172
|
+
else if (value.status === 'unsupported') target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction)
|
|
173
|
+
else target.unpricedCalls = Math.max(0, target.unpricedCalls + direction)
|
|
174
|
+
return target
|
|
175
|
+
}
|
|
176
|
+
|
|
80
177
|
function decimalAdd(left, right) {
|
|
81
178
|
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
82
179
|
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
@@ -212,30 +309,308 @@ function identityModels(identity) {
|
|
|
212
309
|
}
|
|
213
310
|
|
|
214
311
|
function identityKeyOf(identity) {
|
|
215
|
-
|
|
312
|
+
if (!isRecord(identity)) return ''
|
|
313
|
+
const key = typeof identity.identityKey === 'string' && identity.identityKey.trim() !== '' ? identity.identityKey : identity.usageIdentityKey
|
|
314
|
+
return typeof key === 'string' ? key.trim() : ''
|
|
216
315
|
}
|
|
217
316
|
|
|
218
317
|
function priceEntryKey(entry) {
|
|
219
318
|
return normalizeProvider(entry.providerId) + '\0' + normalizeModelId(entry.modelId)
|
|
220
319
|
}
|
|
221
320
|
|
|
321
|
+
function decimalRate(raw, keys, fallback = undefined) {
|
|
322
|
+
let value
|
|
323
|
+
for (const key of keys) {
|
|
324
|
+
if (raw && raw[key] !== undefined) {
|
|
325
|
+
value = raw[key]
|
|
326
|
+
break
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (value === undefined || value === null || value === '') value = fallback
|
|
330
|
+
return decimalText(value)
|
|
331
|
+
}
|
|
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
|
+
|
|
554
|
+
function normalizeContextTier(raw, fallbackRates, legacySize = null) {
|
|
555
|
+
if (!isRecord(raw)) return null
|
|
556
|
+
const descriptor = isRecord(raw.tier) ? raw.tier : raw
|
|
557
|
+
const type = legacySize === null ? (typeof descriptor.type === 'string' ? descriptor.type.trim().toLowerCase() : '') : 'context'
|
|
558
|
+
const sizeValue = legacySize === null ? descriptor.size : legacySize
|
|
559
|
+
const size = finiteNumber(sizeValue)
|
|
560
|
+
if (type !== 'context' || size === null || !Number.isSafeInteger(size) || size <= 0 || size > 1000000000) return null
|
|
561
|
+
const input = decimalRate(raw, ['input', 'inputPerMillion'], fallbackRates.input)
|
|
562
|
+
const output = decimalRate(raw, ['output', 'outputPerMillion'], fallbackRates.output)
|
|
563
|
+
const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], fallbackRates.cacheRead)
|
|
564
|
+
const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], fallbackRates.cacheWrite)
|
|
565
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null) return null
|
|
566
|
+
if (decimalParts(input).digits < 0n || decimalParts(output).digits < 0n || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
|
|
567
|
+
return { type: 'context', size, input, output, cacheRead, cacheWrite }
|
|
568
|
+
}
|
|
569
|
+
|
|
222
570
|
function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
223
571
|
if (!isRecord(raw)) return null
|
|
224
572
|
const modelId = normalizeModelId(raw.modelId || raw.id)
|
|
225
573
|
if (modelId === '') return null
|
|
226
|
-
const input =
|
|
227
|
-
const output =
|
|
574
|
+
const input = decimalRate(raw, ['input', 'inputPerMillion'])
|
|
575
|
+
const output = decimalRate(raw, ['output', 'outputPerMillion'])
|
|
228
576
|
if (input === null || output === null || decimalParts(input).digits < 0n || decimalParts(output).digits < 0n) return null
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
if (value === undefined || value === null || value === '') return '0'
|
|
232
|
-
return decimalText(value)
|
|
233
|
-
}
|
|
234
|
-
const cacheRead = optionalDecimal(raw.cacheRead, raw.cacheReadPerMillion)
|
|
235
|
-
const cacheWrite = optionalDecimal(raw.cacheWrite !== undefined ? raw.cacheWrite : raw.cacheCreation, raw.cacheWritePerMillion)
|
|
577
|
+
const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], '0')
|
|
578
|
+
const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], '0')
|
|
236
579
|
if (cacheRead === null || cacheWrite === null || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
|
|
580
|
+
const fallbackRates = { input, output, cacheRead, cacheWrite }
|
|
581
|
+
const tiers = []
|
|
582
|
+
let tieredInvalid = false
|
|
583
|
+
const hasTierData = raw.tiers !== undefined || raw.context_over_200k !== undefined
|
|
584
|
+
if (raw.tiers !== undefined) {
|
|
585
|
+
if (!Array.isArray(raw.tiers)) tieredInvalid = true
|
|
586
|
+
else {
|
|
587
|
+
if (raw.tiers.length > MAX_CONTEXT_TIERS) tieredInvalid = true
|
|
588
|
+
for (const rawTier of raw.tiers.slice(0, MAX_CONTEXT_TIERS)) {
|
|
589
|
+
const tier = normalizeContextTier(rawTier, fallbackRates)
|
|
590
|
+
if (tier === null || tiers.some((existing) => existing.size === tier.size)) tieredInvalid = true
|
|
591
|
+
else tiers.push(tier)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (raw.context_over_200k !== undefined && raw.tiers === undefined) {
|
|
596
|
+
const tier = normalizeContextTier(raw.context_over_200k, fallbackRates, 200000)
|
|
597
|
+
if (tier === null) tieredInvalid = true
|
|
598
|
+
else tiers.push(tier)
|
|
599
|
+
}
|
|
600
|
+
tiers.sort((left, right) => left.size - right.size)
|
|
601
|
+
const tiered = raw.tiered !== undefined ? raw.tiered === true : hasTierData || tiers.length > 0
|
|
602
|
+
if (!tiered) {
|
|
603
|
+
tiers.length = 0
|
|
604
|
+
tieredInvalid = false
|
|
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
|
|
237
612
|
return {
|
|
238
|
-
providerId: typeof raw.providerId === 'string' ? raw.providerId.trim() : '',
|
|
613
|
+
providerId: typeof raw.providerId === 'string' ? raw.providerId.trim().toLowerCase() : '',
|
|
239
614
|
providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
|
|
240
615
|
modelId,
|
|
241
616
|
displayName: typeof raw.displayName === 'string' && raw.displayName.trim() !== '' ? raw.displayName.trim().slice(0, 200) : modelId,
|
|
@@ -245,16 +620,18 @@ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
|
245
620
|
cacheRead,
|
|
246
621
|
cacheWrite,
|
|
247
622
|
source: raw.source === 'manual' ? 'manual' : sourceDefault,
|
|
248
|
-
tiered
|
|
623
|
+
tiered,
|
|
624
|
+
...(tiered ? { tiers, tieredInvalid } : {}),
|
|
249
625
|
reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
|
|
250
626
|
fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
|
|
627
|
+
...(temporalPricing === undefined ? {} : { temporalPricing }),
|
|
628
|
+
...(temporalPricingInvalid === true ? { temporalPricingInvalid: true } : {}),
|
|
251
629
|
}
|
|
252
630
|
}
|
|
253
631
|
|
|
254
632
|
function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
|
|
255
633
|
if (!isRecord(raw)) return { ok: false, error: 'catalog-not-object' }
|
|
256
634
|
const entries = []
|
|
257
|
-
const seen = new Set()
|
|
258
635
|
for (const [providerKey, provider] of Object.entries(raw)) {
|
|
259
636
|
if (!isRecord(provider) || !isRecord(provider.models)) continue
|
|
260
637
|
const providerId = typeof provider.id === 'string' && provider.id.trim() !== '' ? provider.id.trim() : providerKey
|
|
@@ -271,22 +648,56 @@ function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
|
|
|
271
648
|
output: model.cost && model.cost.output,
|
|
272
649
|
cacheRead: model.cost && model.cost.cache_read,
|
|
273
650
|
cacheWrite: model.cost && model.cost.cache_write,
|
|
651
|
+
tiers: model.cost && model.cost.tiers,
|
|
652
|
+
context_over_200k: model.cost && model.cost.context_over_200k,
|
|
274
653
|
tiered: model.cost && model.cost.tiers !== undefined || model.cost && model.cost.context_over_200k !== undefined,
|
|
275
654
|
reasoningRateAvailable: model.cost && model.cost.reasoning !== undefined,
|
|
276
655
|
source: 'models.dev',
|
|
277
656
|
fetchedAt,
|
|
278
657
|
})
|
|
279
658
|
if (entry === null || entry.currency !== 'USD') continue
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
seen.add(key)
|
|
659
|
+
// Duplicate normalized keys are NOT resolved here: they are kept and
|
|
660
|
+
// resolved deterministically after the stable sort below.
|
|
283
661
|
entries.push(entry)
|
|
284
|
-
if (entries.length
|
|
662
|
+
if (entries.length > MAX_CATALOG_CANDIDATES) return { ok: false, error: 'catalog-exceeds-candidate-limit', candidateCount: entries.length }
|
|
285
663
|
}
|
|
286
|
-
if (entries.length >= MAX_PRICE_ENTRIES) break
|
|
287
664
|
}
|
|
288
665
|
if (entries.length === 0) return { ok: false, error: 'catalog-has-no-priced-models' }
|
|
289
|
-
|
|
666
|
+
// Sort by the stable key first: upstream object order must not decide which
|
|
667
|
+
// models survive a large catalog or which conflicting duplicate is kept.
|
|
668
|
+
// Code-unit comparison: default-locale localeCompare can report ties for
|
|
669
|
+
// distinct strings (e.g. composed/decomposed accents), which would let
|
|
670
|
+
// upstream enumeration order decide the surviving entry.
|
|
671
|
+
entries.sort((left, right) => { const ka = priceEntryKey(left); const kb = priceEntryKey(right); return ka < kb ? -1 : ka > kb ? 1 : 0 })
|
|
672
|
+
// Deterministic duplicate selection: within one normalized key, compare only
|
|
673
|
+
// the small conflict group by their full content (fetchedAt excluded), so
|
|
674
|
+
// ordering swaps cannot change the surviving price.
|
|
675
|
+
const stableText = (entry) => {
|
|
676
|
+
const copy = Object.assign({}, entry)
|
|
677
|
+
delete copy.fetchedAt
|
|
678
|
+
if (typeof copy.providerId === 'string') copy.providerId = copy.providerId.trim().toLowerCase()
|
|
679
|
+
return JSON.stringify(copy)
|
|
680
|
+
}
|
|
681
|
+
const ordered = []
|
|
682
|
+
{
|
|
683
|
+
let index = 0
|
|
684
|
+
while (index < entries.length) {
|
|
685
|
+
const key = priceEntryKey(entries[index])
|
|
686
|
+
let end = index + 1
|
|
687
|
+
while (end < entries.length && priceEntryKey(entries[end]) === key) end += 1
|
|
688
|
+
let group = end - index > 1 ? entries.slice(index, end).sort((left, right) => { const la = stableText(left); const lb = stableText(right); return la < lb ? -1 : la > lb ? 1 : 0 }) : entries.slice(index, end)
|
|
689
|
+
ordered.push(group[0])
|
|
690
|
+
index = end
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
ordered.length = Math.min(ordered.length, MAX_PRICE_ENTRIES)
|
|
694
|
+
// The fetchedAt stamp differs on every fetch; exclude it from the content
|
|
695
|
+
// hash so identical catalog contents hash identically across syncs.
|
|
696
|
+
const canonical = JSON.stringify(ordered.map((entry) => {
|
|
697
|
+
if (entry.fetchedAt === undefined) return entry
|
|
698
|
+
const { fetchedAt, ...stable } = entry
|
|
699
|
+
return stable
|
|
700
|
+
}))
|
|
290
701
|
return {
|
|
291
702
|
ok: true,
|
|
292
703
|
catalog: {
|
|
@@ -294,7 +705,7 @@ function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
|
|
|
294
705
|
sourceUrl: MODEL_CATALOG_URL,
|
|
295
706
|
fetchedAt,
|
|
296
707
|
catalogHash: createHash('sha256').update(canonical).digest('hex'),
|
|
297
|
-
entries,
|
|
708
|
+
entries: ordered,
|
|
298
709
|
},
|
|
299
710
|
}
|
|
300
711
|
}
|
|
@@ -312,9 +723,11 @@ function normalizePricingState(raw) {
|
|
|
312
723
|
const model = normalizeModelId(mapping.model || mapping.modelId)
|
|
313
724
|
const catalogProviderId = typeof mapping.catalogProviderId === 'string' ? mapping.catalogProviderId.trim() : ''
|
|
314
725
|
const catalogModelId = normalizeModelId(mapping.catalogModelId || mapping.catalogModel)
|
|
315
|
-
|
|
726
|
+
const identityKey = (typeof mapping.identityKey === 'string' && mapping.identityKey.trim() !== '' ? mapping.identityKey : mapping.usageIdentityKey)
|
|
727
|
+
const normalizedIdentityKey = typeof identityKey === 'string' ? identityKey.trim().slice(0, 1024) : ''
|
|
728
|
+
if (provider === '' && model === '' && catalogProviderId === '' && catalogModelId === '' && normalizedIdentityKey === '') return null
|
|
316
729
|
return {
|
|
317
|
-
identityKey:
|
|
730
|
+
identityKey: normalizedIdentityKey,
|
|
318
731
|
provider,
|
|
319
732
|
model,
|
|
320
733
|
catalogProviderId,
|
|
@@ -380,10 +793,25 @@ function serializePricingState(state) {
|
|
|
380
793
|
}
|
|
381
794
|
|
|
382
795
|
function sameRates(left, right) {
|
|
383
|
-
|
|
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
|
|
800
|
+
const leftTiers = Array.isArray(left.tiers) ? left.tiers : []
|
|
801
|
+
const rightTiers = Array.isArray(right.tiers) ? right.tiers : []
|
|
802
|
+
if (leftTiers.length !== rightTiers.length) return false
|
|
803
|
+
return leftTiers.every((leftTier, index) => {
|
|
804
|
+
const rightTier = rightTiers[index]
|
|
805
|
+
return rightTier && leftTier.type === rightTier.type && leftTier.size === rightTier.size && leftTier.input === rightTier.input && leftTier.output === rightTier.output && leftTier.cacheRead === rightTier.cacheRead && leftTier.cacheWrite === rightTier.cacheWrite
|
|
806
|
+
})
|
|
384
807
|
}
|
|
385
808
|
|
|
386
809
|
function mappingMatches(mapping, identity) {
|
|
810
|
+
if (mapping.identityKey !== '') {
|
|
811
|
+
if (mapping.identityKey !== identityKeyOf(identity)) return false
|
|
812
|
+
return mapping.provider === '' || (isRecord(identity) && typeof identity.provider === 'string' && identity.provider.trim().toLowerCase() === mapping.provider.toLowerCase())
|
|
813
|
+
}
|
|
814
|
+
if (mapping.provider !== '' && (!isRecord(identity) || typeof identity.provider !== 'string' || identity.provider.trim().toLowerCase() !== mapping.provider.toLowerCase())) return false
|
|
387
815
|
const models = identityModels(identity)
|
|
388
816
|
return mapping.model !== '' && models.includes(mapping.model)
|
|
389
817
|
}
|
|
@@ -399,9 +827,37 @@ function findMappedEntry(mapping, state) {
|
|
|
399
827
|
return null
|
|
400
828
|
}
|
|
401
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
|
+
|
|
402
856
|
function resolvePricing(identity, rawState) {
|
|
403
857
|
const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
|
|
404
|
-
const
|
|
858
|
+
const identityKey = identityKeyOf(identity)
|
|
859
|
+
const exactMapped = identityKey === '' ? undefined : state.mappings.find((mapping) => mapping.identityKey !== '' && mapping.identityKey === identityKey && mappingMatches(mapping, identity))
|
|
860
|
+
const mapped = exactMapped || state.mappings.find((mapping) => mapping.identityKey === '' && mappingMatches(mapping, identity))
|
|
405
861
|
const mappedEntry = mapped === undefined ? null : findMappedEntry(mapped, state)
|
|
406
862
|
if (mapped !== undefined && mappedEntry === null && (mapped.catalogModelId !== '' || mapped.catalogProviderId !== '')) {
|
|
407
863
|
return { status: 'unpriced', reason: 'mapping-target-not-found', pricingModel: mapped.catalogModelId || null, providerId: mapped.catalogProviderId || null }
|
|
@@ -438,9 +894,12 @@ function resolvePricing(identity, rawState) {
|
|
|
438
894
|
for (const match of sameRank) if (!unique.some((entry) => sameRates(entry, match.entry))) unique.push(match.entry)
|
|
439
895
|
if (unique.length > 1) return { status: 'ambiguous', reason: 'multiple-official-prices', pricingModel: best.entry.modelId, providerId: null, candidates: unique.map((entry) => ({ providerId: entry.providerId, modelId: entry.modelId })) }
|
|
440
896
|
const entry = unique[0] || best.entry
|
|
897
|
+
const tiered = entry.tiered === true
|
|
898
|
+
const tiers = Array.isArray(entry.tiers) ? entry.tiers.map((tier) => ({ ...tier })) : []
|
|
899
|
+
const tieredSupported = !tiered || (entry.tieredInvalid !== true && tiers.length > 0)
|
|
441
900
|
return {
|
|
442
|
-
status: 'priced',
|
|
443
|
-
reason: '',
|
|
901
|
+
status: tieredSupported ? 'priced' : 'unsupported',
|
|
902
|
+
reason: tieredSupported ? '' : 'tiered-pricing-not-modeled',
|
|
444
903
|
pricingModel: entry.modelId,
|
|
445
904
|
providerId: entry.providerId || null,
|
|
446
905
|
providerName: entry.providerName || null,
|
|
@@ -448,10 +907,12 @@ function resolvePricing(identity, rawState) {
|
|
|
448
907
|
currency: entry.currency,
|
|
449
908
|
rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
|
|
450
909
|
source: entry.source,
|
|
451
|
-
tiered
|
|
910
|
+
tiered,
|
|
911
|
+
...(tiered ? { tiers, tieredInvalid: entry.tieredInvalid === true } : {}),
|
|
452
912
|
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
453
913
|
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
454
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 } })(),
|
|
455
916
|
}
|
|
456
917
|
}
|
|
457
918
|
const model = models[0] || null
|
|
@@ -473,7 +934,43 @@ function costPerMillion(tokens, rate) {
|
|
|
473
934
|
return decimalText(numerator.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
474
935
|
}
|
|
475
936
|
|
|
476
|
-
function
|
|
937
|
+
function contextTokenCount(input, cacheRead, cacheWrite, inputSemantics) {
|
|
938
|
+
const values = inputSemantics === 'total' ? [input] : inputSemantics === 'legacy' ? [input, cacheWrite] : [input, cacheRead, cacheWrite]
|
|
939
|
+
let total = 0
|
|
940
|
+
for (const value of values) {
|
|
941
|
+
const tokens = Math.max(0, Math.trunc(value))
|
|
942
|
+
if (!Number.isFinite(tokens) || total > Number.MAX_SAFE_INTEGER - tokens) return Number.MAX_SAFE_INTEGER
|
|
943
|
+
total += tokens
|
|
944
|
+
}
|
|
945
|
+
return total
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function validContextTier(tier, previousSize = 0) {
|
|
949
|
+
if (!isRecord(tier) || tier.type !== 'context' || !Number.isSafeInteger(tier.size) || tier.size <= previousSize || tier.size > 1000000000) return false
|
|
950
|
+
return RATE_KEYS.every((key) => decimalText(tier[key]) !== null)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function validContextTierSchedule(tiers) {
|
|
954
|
+
if (!Array.isArray(tiers) || tiers.length === 0) return false
|
|
955
|
+
let previousSize = 0
|
|
956
|
+
for (const tier of tiers) {
|
|
957
|
+
if (!validContextTier(tier, previousSize)) return false
|
|
958
|
+
previousSize = tier.size
|
|
959
|
+
}
|
|
960
|
+
return true
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function selectContextTier(tiers, contextTokens) {
|
|
964
|
+
let selected = null
|
|
965
|
+
for (const tier of tiers) {
|
|
966
|
+
// models.dev defines size as the point where the next band starts.
|
|
967
|
+
if (contextTokens > tier.size) selected = tier
|
|
968
|
+
else break
|
|
969
|
+
}
|
|
970
|
+
return selected
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource = 'usage-event') {
|
|
477
974
|
const input = finiteNumber(values && values.input) || 0
|
|
478
975
|
const output = finiteNumber(values && values.output) || 0
|
|
479
976
|
const cacheRead = finiteNumber(values && values.cacheRead) || 0
|
|
@@ -481,10 +978,39 @@ function calculateCost(values, resolved) {
|
|
|
481
978
|
const inputSemantics = INPUT_SEMANTICS.includes(resolved && resolved.inputTokenSemantics) ? resolved.inputTokenSemantics : 'fresh'
|
|
482
979
|
const billableInput = inputSemantics === 'total' ? Math.max(0, input - cacheRead - cacheWrite) : inputSemantics === 'legacy' ? Math.max(0, input - cacheRead) : input
|
|
483
980
|
const billableOutput = output
|
|
981
|
+
const tiered = resolved && resolved.tiered === true
|
|
982
|
+
const tiers = tiered && Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
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
|
+
}
|
|
484
1010
|
const base = {
|
|
485
1011
|
schemaVersion: COST_SCHEMA_VERSION,
|
|
486
1012
|
pricingMode: 'official-model',
|
|
487
|
-
status
|
|
1013
|
+
status,
|
|
488
1014
|
currency: resolved && resolved.currency ? resolved.currency : 'USD',
|
|
489
1015
|
source: resolved && resolved.source ? resolved.source : 'none',
|
|
490
1016
|
pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
|
|
@@ -493,15 +1019,35 @@ function calculateCost(values, resolved) {
|
|
|
493
1019
|
multiplier: decimalText(resolved && resolved.multiplier !== undefined ? resolved.multiplier : '1') || '1',
|
|
494
1020
|
billableInputTokens: Math.trunc(billableInput),
|
|
495
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,
|
|
496
1030
|
rates: resolved && resolved.rates ? { ...resolved.rates } : { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
497
1031
|
breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
498
1032
|
baseTotal: '0',
|
|
499
1033
|
total: '0',
|
|
500
|
-
reason
|
|
501
|
-
tiered
|
|
1034
|
+
reason,
|
|
1035
|
+
tiered,
|
|
502
1036
|
reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
|
|
503
1037
|
}
|
|
504
1038
|
if (base.status !== 'priced') return base
|
|
1039
|
+
if (tiered) {
|
|
1040
|
+
const contextTokens = contextTokenCount(input, cacheRead, cacheWrite, inputSemantics)
|
|
1041
|
+
const selectedTier = selectContextTier(tiers, contextTokens)
|
|
1042
|
+
const selectedRates = selectedTier || base.rates
|
|
1043
|
+
base.rates = { input: selectedRates.input, output: selectedRates.output, cacheRead: selectedRates.cacheRead, cacheWrite: selectedRates.cacheWrite }
|
|
1044
|
+
base.contextTokens = contextTokens
|
|
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 }
|
|
1050
|
+
}
|
|
505
1051
|
base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
|
|
506
1052
|
base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
|
|
507
1053
|
base.breakdown.cacheRead = costPerMillion(cacheRead, base.rates.cacheRead)
|
|
@@ -516,6 +1062,7 @@ function emptyCostAggregate(currency = 'USD') {
|
|
|
516
1062
|
}
|
|
517
1063
|
|
|
518
1064
|
function addCostAggregate(target, cost) {
|
|
1065
|
+
if (isCostAccumulator(target)) return addCostAccumulator(target, cost)
|
|
519
1066
|
const value = isRecord(cost) ? cost : {}
|
|
520
1067
|
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
521
1068
|
if (status === 'priced') {
|
|
@@ -534,14 +1081,16 @@ function addCostAggregate(target, cost) {
|
|
|
534
1081
|
|
|
535
1082
|
function serializeCostAggregate(cost) {
|
|
536
1083
|
const value = cost || emptyCostAggregate()
|
|
1084
|
+
const accumulator = isCostAccumulator(value)
|
|
1085
|
+
const text = (field) => accumulator ? decimalAccumulatorText(value[field]) : decimalText(value[field]) || '0'
|
|
537
1086
|
return {
|
|
538
1087
|
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
539
|
-
input:
|
|
540
|
-
output:
|
|
541
|
-
cacheRead:
|
|
542
|
-
cacheWrite:
|
|
543
|
-
baseTotal:
|
|
544
|
-
total:
|
|
1088
|
+
input: text('input'),
|
|
1089
|
+
output: text('output'),
|
|
1090
|
+
cacheRead: text('cacheRead'),
|
|
1091
|
+
cacheWrite: text('cacheWrite'),
|
|
1092
|
+
baseTotal: text('baseTotal'),
|
|
1093
|
+
total: text('total'),
|
|
545
1094
|
pricedCalls: Number.isFinite(value.pricedCalls) ? value.pricedCalls : 0,
|
|
546
1095
|
unpricedCalls: Number.isFinite(value.unpricedCalls) ? value.unpricedCalls : 0,
|
|
547
1096
|
ambiguousCalls: Number.isFinite(value.ambiguousCalls) ? value.ambiguousCalls : 0,
|
|
@@ -567,6 +1116,32 @@ function normalizeCostSnapshot(raw) {
|
|
|
567
1116
|
const total = decimalText(raw.total)
|
|
568
1117
|
const multiplier = decimalText(raw.multiplier)
|
|
569
1118
|
if (baseTotal === null || total === null || multiplier === null) return null
|
|
1119
|
+
let contextTokens = null
|
|
1120
|
+
if (raw.contextTokens !== undefined) {
|
|
1121
|
+
const parsed = finiteNumber(raw.contextTokens)
|
|
1122
|
+
if (parsed === null || parsed < 0 || !Number.isSafeInteger(parsed)) return null
|
|
1123
|
+
contextTokens = parsed
|
|
1124
|
+
}
|
|
1125
|
+
let selectedTier = null
|
|
1126
|
+
if (raw.selectedTier !== undefined && raw.selectedTier !== null) {
|
|
1127
|
+
if (!isRecord(raw.selectedTier) || raw.selectedTier.type !== 'context' || !Number.isSafeInteger(raw.selectedTier.size) || raw.selectedTier.size < 0 || raw.selectedTier.size > 1000000000) return null
|
|
1128
|
+
selectedTier = { type: 'context', size: raw.selectedTier.size }
|
|
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
|
|
570
1145
|
return {
|
|
571
1146
|
schemaVersion: COST_SCHEMA_VERSION,
|
|
572
1147
|
pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
|
|
@@ -579,6 +1154,16 @@ function normalizeCostSnapshot(raw) {
|
|
|
579
1154
|
multiplier,
|
|
580
1155
|
billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
|
|
581
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,
|
|
1165
|
+
...(contextTokens === null ? {} : { contextTokens }),
|
|
1166
|
+
...(selectedTier === null ? {} : { selectedTier }),
|
|
582
1167
|
rates: normalizedRates,
|
|
583
1168
|
breakdown: normalizedBreakdown,
|
|
584
1169
|
baseTotal,
|
|
@@ -625,7 +1210,9 @@ export {
|
|
|
625
1210
|
PRICING_SCHEMA_VERSION,
|
|
626
1211
|
RATE_KEYS,
|
|
627
1212
|
addCostAggregate,
|
|
1213
|
+
addCostAccumulator,
|
|
628
1214
|
calculateCost,
|
|
1215
|
+
createCostAccumulator,
|
|
629
1216
|
createEmptyPricingState,
|
|
630
1217
|
decimalAdd,
|
|
631
1218
|
decimalMultiply,
|
|
@@ -633,13 +1220,18 @@ export {
|
|
|
633
1220
|
decimalText,
|
|
634
1221
|
emptyCostAggregate,
|
|
635
1222
|
fetchModelsDevCatalog,
|
|
1223
|
+
isCostAccumulator,
|
|
636
1224
|
modelPricingCandidates,
|
|
637
1225
|
officialProviderIds,
|
|
638
1226
|
normalizeCostSnapshot,
|
|
639
1227
|
normalizeModelId,
|
|
640
1228
|
normalizePricingState,
|
|
1229
|
+
normalizeTemporalPricing,
|
|
641
1230
|
parseModelsDevCatalog,
|
|
642
1231
|
resolvePricing,
|
|
643
1232
|
serializeCostAggregate,
|
|
644
1233
|
serializePricingState,
|
|
1234
|
+
temporalBand,
|
|
1235
|
+
temporalPlanFor,
|
|
1236
|
+
temporalPolicyHash,
|
|
645
1237
|
}
|