dsh-all-usage 1.1.2 → 1.1.3

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
@@ -6,11 +6,20 @@ 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
11
17
  const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
12
18
  const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
13
19
  const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
20
+ const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']
21
+ const COST_ACCUMULATOR = Symbol('costAccumulator')
22
+ const COST_PARTS_CACHE = new WeakMap()
14
23
  const OFFICIAL_PROVIDER_RULES = [
15
24
  { providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
16
25
  { providers: ['anthropic'], prefixes: ['claude-'] },
@@ -77,6 +86,89 @@ function decimalText(value) {
77
86
  return padded.slice(0, split) + '.' + padded.slice(split)
78
87
  }
79
88
 
89
+ function normalizedDecimalParts(parts) {
90
+ let digits = parts && typeof parts.digits === 'bigint' ? parts.digits : 0n
91
+ let scale = parts && Number.isSafeInteger(parts.scale) && parts.scale >= 0 ? parts.scale : 0
92
+ if (digits < 0n) digits = 0n
93
+ while (scale > 0 && digits !== 0n && digits % 10n === 0n) {
94
+ digits /= 10n
95
+ scale -= 1
96
+ }
97
+ return { digits, scale }
98
+ }
99
+
100
+ function decimalAccumulatorText(value) {
101
+ const parts = normalizedDecimalParts(value)
102
+ if (parts.digits === 0n) return '0'
103
+ const raw = parts.digits.toString()
104
+ if (parts.scale === 0) return raw
105
+ const padded = raw.padStart(parts.scale + 1, '0')
106
+ const split = padded.length - parts.scale
107
+ return padded.slice(0, split) + '.' + padded.slice(split)
108
+ }
109
+
110
+ function isCostAccumulator(value) {
111
+ return value !== null && typeof value === 'object' && value[COST_ACCUMULATOR] === true
112
+ }
113
+
114
+ function createCostAccumulator(currency = 'USD') {
115
+ 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 }
116
+ Object.defineProperty(result, COST_ACCUMULATOR, { value: true })
117
+ return result
118
+ }
119
+
120
+ function costAccumulatorParts(cost) {
121
+ if (isCostAccumulator(cost)) return cost
122
+ if (cost !== null && typeof cost === 'object') {
123
+ const cached = COST_PARTS_CACHE.get(cost)
124
+ if (cached !== undefined) return cached
125
+ }
126
+ const value = isRecord(cost) ? cost : {}
127
+ const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
128
+ const breakdown = isRecord(value.breakdown) ? value.breakdown : {}
129
+ const result = {
130
+ currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
131
+ status,
132
+ input: decimalParts(breakdown.input) || { digits: 0n, scale: 0 },
133
+ output: decimalParts(breakdown.output) || { digits: 0n, scale: 0 },
134
+ cacheRead: decimalParts(breakdown.cacheRead) || { digits: 0n, scale: 0 },
135
+ cacheWrite: decimalParts(breakdown.cacheWrite) || { digits: 0n, scale: 0 },
136
+ baseTotal: decimalParts(value.baseTotal) || { digits: 0n, scale: 0 },
137
+ total: decimalParts(value.total) || { digits: 0n, scale: 0 },
138
+ }
139
+ if (cost !== null && typeof cost === 'object') COST_PARTS_CACHE.set(cost, result)
140
+ return result
141
+ }
142
+
143
+ function addDecimalAccumulator(target, field, value, direction) {
144
+ const left = normalizedDecimalParts(target[field])
145
+ const right = normalizedDecimalParts(value)
146
+ const scale = Math.max(left.scale, right.scale)
147
+ let digits = left.digits * 10n ** BigInt(scale - left.scale) + BigInt(direction) * right.digits * 10n ** BigInt(scale - right.scale)
148
+ if (digits < 0n) digits = 0n
149
+ target[field] = normalizedDecimalParts({ digits, scale })
150
+ }
151
+
152
+ function addCostAccumulator(target, cost, direction = 1) {
153
+ if (!isCostAccumulator(target)) return target
154
+ if (isCostAccumulator(cost)) {
155
+ for (const field of COST_FIELDS) addDecimalAccumulator(target, field, cost[field], direction)
156
+ target.pricedCalls = Math.max(0, target.pricedCalls + direction * cost.pricedCalls)
157
+ target.unpricedCalls = Math.max(0, target.unpricedCalls + direction * cost.unpricedCalls)
158
+ target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction * cost.ambiguousCalls)
159
+ target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction * cost.unsupportedCalls)
160
+ return target
161
+ }
162
+ const value = costAccumulatorParts(cost)
163
+ if (value.status === 'priced') {
164
+ for (const field of COST_FIELDS) addDecimalAccumulator(target, field, value[field], direction)
165
+ target.pricedCalls = Math.max(0, target.pricedCalls + direction)
166
+ } else if (value.status === 'ambiguous') target.ambiguousCalls = Math.max(0, target.ambiguousCalls + direction)
167
+ else if (value.status === 'unsupported') target.unsupportedCalls = Math.max(0, target.unsupportedCalls + direction)
168
+ else target.unpricedCalls = Math.max(0, target.unpricedCalls + direction)
169
+ return target
170
+ }
171
+
80
172
  function decimalAdd(left, right) {
81
173
  const a = decimalParts(left) || { digits: 0n, scale: 0 }
82
174
  const b = decimalParts(right) || { digits: 0n, scale: 0 }
@@ -212,30 +304,81 @@ function identityModels(identity) {
212
304
  }
213
305
 
214
306
  function identityKeyOf(identity) {
215
- return isRecord(identity) && typeof identity.identityKey === 'string' ? identity.identityKey : ''
307
+ if (!isRecord(identity)) return ''
308
+ const key = typeof identity.identityKey === 'string' && identity.identityKey.trim() !== '' ? identity.identityKey : identity.usageIdentityKey
309
+ return typeof key === 'string' ? key.trim() : ''
216
310
  }
217
311
 
218
312
  function priceEntryKey(entry) {
219
313
  return normalizeProvider(entry.providerId) + '\0' + normalizeModelId(entry.modelId)
220
314
  }
221
315
 
316
+ function decimalRate(raw, keys, fallback = undefined) {
317
+ let value
318
+ for (const key of keys) {
319
+ if (raw && raw[key] !== undefined) {
320
+ value = raw[key]
321
+ break
322
+ }
323
+ }
324
+ if (value === undefined || value === null || value === '') value = fallback
325
+ return decimalText(value)
326
+ }
327
+
328
+ function normalizeContextTier(raw, fallbackRates, legacySize = null) {
329
+ if (!isRecord(raw)) return null
330
+ const descriptor = isRecord(raw.tier) ? raw.tier : raw
331
+ const type = legacySize === null ? (typeof descriptor.type === 'string' ? descriptor.type.trim().toLowerCase() : '') : 'context'
332
+ const sizeValue = legacySize === null ? descriptor.size : legacySize
333
+ const size = finiteNumber(sizeValue)
334
+ if (type !== 'context' || size === null || !Number.isSafeInteger(size) || size <= 0 || size > 1000000000) return null
335
+ const input = decimalRate(raw, ['input', 'inputPerMillion'], fallbackRates.input)
336
+ const output = decimalRate(raw, ['output', 'outputPerMillion'], fallbackRates.output)
337
+ const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], fallbackRates.cacheRead)
338
+ const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], fallbackRates.cacheWrite)
339
+ if (input === null || output === null || cacheRead === null || cacheWrite === null) return null
340
+ if (decimalParts(input).digits < 0n || decimalParts(output).digits < 0n || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
341
+ return { type: 'context', size, input, output, cacheRead, cacheWrite }
342
+ }
343
+
222
344
  function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
223
345
  if (!isRecord(raw)) return null
224
346
  const modelId = normalizeModelId(raw.modelId || raw.id)
225
347
  if (modelId === '') return null
226
- const input = decimalText(raw.input !== undefined ? raw.input : raw.inputPerMillion)
227
- const output = decimalText(raw.output !== undefined ? raw.output : raw.outputPerMillion)
348
+ const input = decimalRate(raw, ['input', 'inputPerMillion'])
349
+ const output = decimalRate(raw, ['output', 'outputPerMillion'])
228
350
  if (input === null || output === null || decimalParts(input).digits < 0n || decimalParts(output).digits < 0n) return null
229
- const optionalDecimal = (primary, secondary) => {
230
- const value = primary !== undefined ? primary : secondary
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)
351
+ const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], '0')
352
+ const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], '0')
236
353
  if (cacheRead === null || cacheWrite === null || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
354
+ const fallbackRates = { input, output, cacheRead, cacheWrite }
355
+ const tiers = []
356
+ let tieredInvalid = false
357
+ const hasTierData = raw.tiers !== undefined || raw.context_over_200k !== undefined
358
+ if (raw.tiers !== undefined) {
359
+ if (!Array.isArray(raw.tiers)) tieredInvalid = true
360
+ else {
361
+ if (raw.tiers.length > MAX_CONTEXT_TIERS) tieredInvalid = true
362
+ for (const rawTier of raw.tiers.slice(0, MAX_CONTEXT_TIERS)) {
363
+ const tier = normalizeContextTier(rawTier, fallbackRates)
364
+ if (tier === null || tiers.some((existing) => existing.size === tier.size)) tieredInvalid = true
365
+ else tiers.push(tier)
366
+ }
367
+ }
368
+ }
369
+ if (raw.context_over_200k !== undefined && raw.tiers === undefined) {
370
+ const tier = normalizeContextTier(raw.context_over_200k, fallbackRates, 200000)
371
+ if (tier === null) tieredInvalid = true
372
+ else tiers.push(tier)
373
+ }
374
+ tiers.sort((left, right) => left.size - right.size)
375
+ const tiered = raw.tiered !== undefined ? raw.tiered === true : hasTierData || tiers.length > 0
376
+ if (!tiered) {
377
+ tiers.length = 0
378
+ tieredInvalid = false
379
+ } else if (tiers.length === 0) tieredInvalid = true
237
380
  return {
238
- providerId: typeof raw.providerId === 'string' ? raw.providerId.trim() : '',
381
+ providerId: typeof raw.providerId === 'string' ? raw.providerId.trim().toLowerCase() : '',
239
382
  providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
240
383
  modelId,
241
384
  displayName: typeof raw.displayName === 'string' && raw.displayName.trim() !== '' ? raw.displayName.trim().slice(0, 200) : modelId,
@@ -245,7 +388,8 @@ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
245
388
  cacheRead,
246
389
  cacheWrite,
247
390
  source: raw.source === 'manual' ? 'manual' : sourceDefault,
248
- tiered: raw.tiered === true || raw.tiers !== undefined,
391
+ tiered,
392
+ ...(tiered ? { tiers, tieredInvalid } : {}),
249
393
  reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
250
394
  fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
251
395
  }
@@ -254,7 +398,6 @@ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
254
398
  function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
255
399
  if (!isRecord(raw)) return { ok: false, error: 'catalog-not-object' }
256
400
  const entries = []
257
- const seen = new Set()
258
401
  for (const [providerKey, provider] of Object.entries(raw)) {
259
402
  if (!isRecord(provider) || !isRecord(provider.models)) continue
260
403
  const providerId = typeof provider.id === 'string' && provider.id.trim() !== '' ? provider.id.trim() : providerKey
@@ -271,22 +414,56 @@ function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
271
414
  output: model.cost && model.cost.output,
272
415
  cacheRead: model.cost && model.cost.cache_read,
273
416
  cacheWrite: model.cost && model.cost.cache_write,
417
+ tiers: model.cost && model.cost.tiers,
418
+ context_over_200k: model.cost && model.cost.context_over_200k,
274
419
  tiered: model.cost && model.cost.tiers !== undefined || model.cost && model.cost.context_over_200k !== undefined,
275
420
  reasoningRateAvailable: model.cost && model.cost.reasoning !== undefined,
276
421
  source: 'models.dev',
277
422
  fetchedAt,
278
423
  })
279
424
  if (entry === null || entry.currency !== 'USD') continue
280
- const key = priceEntryKey(entry)
281
- if (seen.has(key)) continue
282
- seen.add(key)
425
+ // Duplicate normalized keys are NOT resolved here: they are kept and
426
+ // resolved deterministically after the stable sort below.
283
427
  entries.push(entry)
284
- if (entries.length >= MAX_PRICE_ENTRIES) break
428
+ if (entries.length > MAX_CATALOG_CANDIDATES) return { ok: false, error: 'catalog-exceeds-candidate-limit', candidateCount: entries.length }
285
429
  }
286
- if (entries.length >= MAX_PRICE_ENTRIES) break
287
430
  }
288
431
  if (entries.length === 0) return { ok: false, error: 'catalog-has-no-priced-models' }
289
- const canonical = JSON.stringify(entries)
432
+ // Sort by the stable key first: upstream object order must not decide which
433
+ // models survive a large catalog or which conflicting duplicate is kept.
434
+ // Code-unit comparison: default-locale localeCompare can report ties for
435
+ // distinct strings (e.g. composed/decomposed accents), which would let
436
+ // upstream enumeration order decide the surviving entry.
437
+ entries.sort((left, right) => { const ka = priceEntryKey(left); const kb = priceEntryKey(right); return ka < kb ? -1 : ka > kb ? 1 : 0 })
438
+ // Deterministic duplicate selection: within one normalized key, compare only
439
+ // the small conflict group by their full content (fetchedAt excluded), so
440
+ // ordering swaps cannot change the surviving price.
441
+ const stableText = (entry) => {
442
+ const copy = Object.assign({}, entry)
443
+ delete copy.fetchedAt
444
+ if (typeof copy.providerId === 'string') copy.providerId = copy.providerId.trim().toLowerCase()
445
+ return JSON.stringify(copy)
446
+ }
447
+ const ordered = []
448
+ {
449
+ let index = 0
450
+ while (index < entries.length) {
451
+ const key = priceEntryKey(entries[index])
452
+ let end = index + 1
453
+ while (end < entries.length && priceEntryKey(entries[end]) === key) end += 1
454
+ 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)
455
+ ordered.push(group[0])
456
+ index = end
457
+ }
458
+ }
459
+ ordered.length = Math.min(ordered.length, MAX_PRICE_ENTRIES)
460
+ // The fetchedAt stamp differs on every fetch; exclude it from the content
461
+ // hash so identical catalog contents hash identically across syncs.
462
+ const canonical = JSON.stringify(ordered.map((entry) => {
463
+ if (entry.fetchedAt === undefined) return entry
464
+ const { fetchedAt, ...stable } = entry
465
+ return stable
466
+ }))
290
467
  return {
291
468
  ok: true,
292
469
  catalog: {
@@ -294,7 +471,7 @@ function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
294
471
  sourceUrl: MODEL_CATALOG_URL,
295
472
  fetchedAt,
296
473
  catalogHash: createHash('sha256').update(canonical).digest('hex'),
297
- entries,
474
+ entries: ordered,
298
475
  },
299
476
  }
300
477
  }
@@ -312,9 +489,11 @@ function normalizePricingState(raw) {
312
489
  const model = normalizeModelId(mapping.model || mapping.modelId)
313
490
  const catalogProviderId = typeof mapping.catalogProviderId === 'string' ? mapping.catalogProviderId.trim() : ''
314
491
  const catalogModelId = normalizeModelId(mapping.catalogModelId || mapping.catalogModel)
315
- if (provider === '' && model === '' && catalogProviderId === '' && catalogModelId === '') return null
492
+ const identityKey = (typeof mapping.identityKey === 'string' && mapping.identityKey.trim() !== '' ? mapping.identityKey : mapping.usageIdentityKey)
493
+ const normalizedIdentityKey = typeof identityKey === 'string' ? identityKey.trim().slice(0, 1024) : ''
494
+ if (provider === '' && model === '' && catalogProviderId === '' && catalogModelId === '' && normalizedIdentityKey === '') return null
316
495
  return {
317
- identityKey: typeof mapping.identityKey === 'string' ? mapping.identityKey.slice(0, 1024) : '',
496
+ identityKey: normalizedIdentityKey,
318
497
  provider,
319
498
  model,
320
499
  catalogProviderId,
@@ -380,10 +559,22 @@ function serializePricingState(state) {
380
559
  }
381
560
 
382
561
  function sameRates(left, right) {
383
- return left.input === right.input && left.output === right.output && left.cacheRead === right.cacheRead && left.cacheWrite === right.cacheWrite && left.currency === right.currency
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
563
+ const leftTiers = Array.isArray(left.tiers) ? left.tiers : []
564
+ const rightTiers = Array.isArray(right.tiers) ? right.tiers : []
565
+ if (leftTiers.length !== rightTiers.length) return false
566
+ return leftTiers.every((leftTier, index) => {
567
+ const rightTier = rightTiers[index]
568
+ 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
569
+ })
384
570
  }
385
571
 
386
572
  function mappingMatches(mapping, identity) {
573
+ if (mapping.identityKey !== '') {
574
+ if (mapping.identityKey !== identityKeyOf(identity)) return false
575
+ return mapping.provider === '' || (isRecord(identity) && typeof identity.provider === 'string' && identity.provider.trim().toLowerCase() === mapping.provider.toLowerCase())
576
+ }
577
+ if (mapping.provider !== '' && (!isRecord(identity) || typeof identity.provider !== 'string' || identity.provider.trim().toLowerCase() !== mapping.provider.toLowerCase())) return false
387
578
  const models = identityModels(identity)
388
579
  return mapping.model !== '' && models.includes(mapping.model)
389
580
  }
@@ -401,7 +592,9 @@ function findMappedEntry(mapping, state) {
401
592
 
402
593
  function resolvePricing(identity, rawState) {
403
594
  const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
404
- const mapped = state.mappings.find((mapping) => mappingMatches(mapping, identity))
595
+ const identityKey = identityKeyOf(identity)
596
+ const exactMapped = identityKey === '' ? undefined : state.mappings.find((mapping) => mapping.identityKey !== '' && mapping.identityKey === identityKey && mappingMatches(mapping, identity))
597
+ const mapped = exactMapped || state.mappings.find((mapping) => mapping.identityKey === '' && mappingMatches(mapping, identity))
405
598
  const mappedEntry = mapped === undefined ? null : findMappedEntry(mapped, state)
406
599
  if (mapped !== undefined && mappedEntry === null && (mapped.catalogModelId !== '' || mapped.catalogProviderId !== '')) {
407
600
  return { status: 'unpriced', reason: 'mapping-target-not-found', pricingModel: mapped.catalogModelId || null, providerId: mapped.catalogProviderId || null }
@@ -438,9 +631,12 @@ function resolvePricing(identity, rawState) {
438
631
  for (const match of sameRank) if (!unique.some((entry) => sameRates(entry, match.entry))) unique.push(match.entry)
439
632
  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
633
  const entry = unique[0] || best.entry
634
+ const tiered = entry.tiered === true
635
+ const tiers = Array.isArray(entry.tiers) ? entry.tiers.map((tier) => ({ ...tier })) : []
636
+ const tieredSupported = !tiered || (entry.tieredInvalid !== true && tiers.length > 0)
441
637
  return {
442
- status: 'priced',
443
- reason: '',
638
+ status: tieredSupported ? 'priced' : 'unsupported',
639
+ reason: tieredSupported ? '' : 'tiered-pricing-not-modeled',
444
640
  pricingModel: entry.modelId,
445
641
  providerId: entry.providerId || null,
446
642
  providerName: entry.providerName || null,
@@ -448,7 +644,8 @@ function resolvePricing(identity, rawState) {
448
644
  currency: entry.currency,
449
645
  rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
450
646
  source: entry.source,
451
- tiered: entry.tiered,
647
+ tiered,
648
+ ...(tiered ? { tiers, tieredInvalid: entry.tieredInvalid === true } : {}),
452
649
  reasoningRateAvailable: entry.reasoningRateAvailable,
453
650
  inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
454
651
  multiplier: mapped ? mapped.multiplier : '1',
@@ -473,6 +670,42 @@ function costPerMillion(tokens, rate) {
473
670
  return decimalText(numerator.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
474
671
  }
475
672
 
673
+ function contextTokenCount(input, cacheRead, cacheWrite, inputSemantics) {
674
+ const values = inputSemantics === 'total' ? [input] : inputSemantics === 'legacy' ? [input, cacheWrite] : [input, cacheRead, cacheWrite]
675
+ let total = 0
676
+ for (const value of values) {
677
+ const tokens = Math.max(0, Math.trunc(value))
678
+ if (!Number.isFinite(tokens) || total > Number.MAX_SAFE_INTEGER - tokens) return Number.MAX_SAFE_INTEGER
679
+ total += tokens
680
+ }
681
+ return total
682
+ }
683
+
684
+ function validContextTier(tier, previousSize = 0) {
685
+ if (!isRecord(tier) || tier.type !== 'context' || !Number.isSafeInteger(tier.size) || tier.size <= previousSize || tier.size > 1000000000) return false
686
+ return RATE_KEYS.every((key) => decimalText(tier[key]) !== null)
687
+ }
688
+
689
+ function validContextTierSchedule(tiers) {
690
+ if (!Array.isArray(tiers) || tiers.length === 0) return false
691
+ let previousSize = 0
692
+ for (const tier of tiers) {
693
+ if (!validContextTier(tier, previousSize)) return false
694
+ previousSize = tier.size
695
+ }
696
+ return true
697
+ }
698
+
699
+ function selectContextTier(tiers, contextTokens) {
700
+ let selected = null
701
+ for (const tier of tiers) {
702
+ // models.dev defines size as the point where the next band starts.
703
+ if (contextTokens > tier.size) selected = tier
704
+ else break
705
+ }
706
+ return selected
707
+ }
708
+
476
709
  function calculateCost(values, resolved) {
477
710
  const input = finiteNumber(values && values.input) || 0
478
711
  const output = finiteNumber(values && values.output) || 0
@@ -481,10 +714,13 @@ function calculateCost(values, resolved) {
481
714
  const inputSemantics = INPUT_SEMANTICS.includes(resolved && resolved.inputTokenSemantics) ? resolved.inputTokenSemantics : 'fresh'
482
715
  const billableInput = inputSemantics === 'total' ? Math.max(0, input - cacheRead - cacheWrite) : inputSemantics === 'legacy' ? Math.max(0, input - cacheRead) : input
483
716
  const billableOutput = output
717
+ const tiered = resolved && resolved.tiered === true
718
+ const tiers = tiered && Array.isArray(resolved.tiers) ? resolved.tiers : []
719
+ const tierScheduleValid = !tiered || validContextTierSchedule(tiers)
484
720
  const base = {
485
721
  schemaVersion: COST_SCHEMA_VERSION,
486
722
  pricingMode: 'official-model',
487
- status: resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : 'unpriced',
723
+ status: tierScheduleValid && resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : tiered ? 'unsupported' : 'unpriced',
488
724
  currency: resolved && resolved.currency ? resolved.currency : 'USD',
489
725
  source: resolved && resolved.source ? resolved.source : 'none',
490
726
  pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
@@ -497,11 +733,19 @@ function calculateCost(values, resolved) {
497
733
  breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
498
734
  baseTotal: '0',
499
735
  total: '0',
500
- reason: resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found',
501
- tiered: resolved && resolved.tiered === true,
736
+ reason: tiered && !tierScheduleValid ? 'tiered-pricing-not-modeled' : resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found',
737
+ tiered,
502
738
  reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
503
739
  }
504
740
  if (base.status !== 'priced') return base
741
+ if (tiered) {
742
+ const contextTokens = contextTokenCount(input, cacheRead, cacheWrite, inputSemantics)
743
+ const selectedTier = selectContextTier(tiers, contextTokens)
744
+ const selectedRates = selectedTier || base.rates
745
+ base.rates = { input: selectedRates.input, output: selectedRates.output, cacheRead: selectedRates.cacheRead, cacheWrite: selectedRates.cacheWrite }
746
+ base.contextTokens = contextTokens
747
+ base.selectedTier = { type: 'context', size: selectedTier ? selectedTier.size : 0 }
748
+ }
505
749
  base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
506
750
  base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
507
751
  base.breakdown.cacheRead = costPerMillion(cacheRead, base.rates.cacheRead)
@@ -516,6 +760,7 @@ function emptyCostAggregate(currency = 'USD') {
516
760
  }
517
761
 
518
762
  function addCostAggregate(target, cost) {
763
+ if (isCostAccumulator(target)) return addCostAccumulator(target, cost)
519
764
  const value = isRecord(cost) ? cost : {}
520
765
  const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
521
766
  if (status === 'priced') {
@@ -534,14 +779,16 @@ function addCostAggregate(target, cost) {
534
779
 
535
780
  function serializeCostAggregate(cost) {
536
781
  const value = cost || emptyCostAggregate()
782
+ const accumulator = isCostAccumulator(value)
783
+ const text = (field) => accumulator ? decimalAccumulatorText(value[field]) : decimalText(value[field]) || '0'
537
784
  return {
538
785
  currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
539
- input: decimalText(value.input) || '0',
540
- output: decimalText(value.output) || '0',
541
- cacheRead: decimalText(value.cacheRead) || '0',
542
- cacheWrite: decimalText(value.cacheWrite) || '0',
543
- baseTotal: decimalText(value.baseTotal) || '0',
544
- total: decimalText(value.total) || '0',
786
+ input: text('input'),
787
+ output: text('output'),
788
+ cacheRead: text('cacheRead'),
789
+ cacheWrite: text('cacheWrite'),
790
+ baseTotal: text('baseTotal'),
791
+ total: text('total'),
545
792
  pricedCalls: Number.isFinite(value.pricedCalls) ? value.pricedCalls : 0,
546
793
  unpricedCalls: Number.isFinite(value.unpricedCalls) ? value.unpricedCalls : 0,
547
794
  ambiguousCalls: Number.isFinite(value.ambiguousCalls) ? value.ambiguousCalls : 0,
@@ -567,6 +814,17 @@ function normalizeCostSnapshot(raw) {
567
814
  const total = decimalText(raw.total)
568
815
  const multiplier = decimalText(raw.multiplier)
569
816
  if (baseTotal === null || total === null || multiplier === null) return null
817
+ let contextTokens = null
818
+ if (raw.contextTokens !== undefined) {
819
+ const parsed = finiteNumber(raw.contextTokens)
820
+ if (parsed === null || parsed < 0 || !Number.isSafeInteger(parsed)) return null
821
+ contextTokens = parsed
822
+ }
823
+ let selectedTier = null
824
+ if (raw.selectedTier !== undefined && raw.selectedTier !== null) {
825
+ if (!isRecord(raw.selectedTier) || raw.selectedTier.type !== 'context' || !Number.isSafeInteger(raw.selectedTier.size) || raw.selectedTier.size < 0 || raw.selectedTier.size > 1000000000) return null
826
+ selectedTier = { type: 'context', size: raw.selectedTier.size }
827
+ }
570
828
  return {
571
829
  schemaVersion: COST_SCHEMA_VERSION,
572
830
  pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
@@ -579,6 +837,8 @@ function normalizeCostSnapshot(raw) {
579
837
  multiplier,
580
838
  billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
581
839
  billableOutputTokens: Number.isFinite(raw.billableOutputTokens) ? Math.max(0, Math.trunc(raw.billableOutputTokens)) : 0,
840
+ ...(contextTokens === null ? {} : { contextTokens }),
841
+ ...(selectedTier === null ? {} : { selectedTier }),
582
842
  rates: normalizedRates,
583
843
  breakdown: normalizedBreakdown,
584
844
  baseTotal,
@@ -625,7 +885,9 @@ export {
625
885
  PRICING_SCHEMA_VERSION,
626
886
  RATE_KEYS,
627
887
  addCostAggregate,
888
+ addCostAccumulator,
628
889
  calculateCost,
890
+ createCostAccumulator,
629
891
  createEmptyPricingState,
630
892
  decimalAdd,
631
893
  decimalMultiply,
@@ -633,6 +895,7 @@ export {
633
895
  decimalText,
634
896
  emptyCostAggregate,
635
897
  fetchModelsDevCatalog,
898
+ isCostAccumulator,
636
899
  modelPricingCandidates,
637
900
  officialProviderIds,
638
901
  normalizeCostSnapshot,