dsh-all-usage 1.0.9 → 1.1.2

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 ADDED
@@ -0,0 +1,645 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ const PRICING_SCHEMA_VERSION = 1
4
+ const COST_SCHEMA_VERSION = 1
5
+ const MODEL_CATALOG_URL = 'https://models.dev/api.json'
6
+ const DEFAULT_SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000
7
+ const MAX_CATALOG_BYTES = 24 * 1024 * 1024
8
+ const MAX_PRICE_ENTRIES = 10000
9
+ const MAX_OVERRIDES = 500
10
+ const MAX_MAPPINGS = 500
11
+ const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
12
+ const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
13
+ const COST_STATUSES = ['priced', 'unpriced', 'ambiguous', 'unsupported']
14
+ const OFFICIAL_PROVIDER_RULES = [
15
+ { providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
16
+ { providers: ['anthropic'], prefixes: ['claude-'] },
17
+ { providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
18
+ { providers: ['xai'], prefixes: ['grok-'] },
19
+ { providers: ['deepseek'], prefixes: ['deepseek-'] },
20
+ { providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
21
+ { providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
22
+ { providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
23
+ { providers: ['minimax'], prefixes: ['minimax-'] },
24
+ { providers: ['mistral'], prefixes: ['mistral-', 'mixtral-'] },
25
+ { providers: ['meta'], prefixes: ['llama-', 'meta-llama'] },
26
+ { providers: ['cohere'], prefixes: ['command-'] },
27
+ { providers: ['ai21'], prefixes: ['jamba-'] },
28
+ { providers: ['baidu'], prefixes: ['ernie-'] },
29
+ ]
30
+ const MAX_DECIMAL_DIGITS = 40
31
+ const MAX_DECIMAL_EXPONENT = 24
32
+
33
+ function isRecord(value) {
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
35
+ }
36
+
37
+ function finiteNumber(value) {
38
+ if (typeof value === 'number' && Number.isFinite(value)) return value
39
+ if (typeof value === 'string' && value.trim() !== '') {
40
+ const parsed = Number(value)
41
+ if (Number.isFinite(parsed)) return parsed
42
+ }
43
+ return null
44
+ }
45
+
46
+ function decimalParts(value) {
47
+ const raw = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim().toLowerCase() : ''
48
+ const match = raw.match(/^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/)
49
+ if (!match) return null
50
+ let digits = (match[1] || '') + (match[2] || '')
51
+ const exponent = match[3] ? Number(match[3]) : 0
52
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > MAX_DECIMAL_EXPONENT) return null
53
+ let scale = (match[2] || '').length - exponent
54
+ digits = digits.replace(/^0+(?=\d)/, '')
55
+ if (scale < 0) {
56
+ digits += '0'.repeat(-scale)
57
+ scale = 0
58
+ }
59
+ if (scale > digits.length) digits = '0'.repeat(scale - digits.length + 1) + digits
60
+ if (digits.length > MAX_DECIMAL_DIGITS) return null
61
+ while (scale > 0 && digits.length > 1 && digits.endsWith('0')) {
62
+ digits = digits.slice(0, -1)
63
+ scale -= 1
64
+ }
65
+ digits = digits.replace(/^0+(?=\d)/, '')
66
+ return { digits: BigInt(digits || '0'), scale }
67
+ }
68
+
69
+ function decimalText(value) {
70
+ const parts = decimalParts(value)
71
+ if (parts === null) return null
72
+ if (parts.digits === 0n) return '0'
73
+ const raw = parts.digits.toString()
74
+ if (parts.scale === 0) return raw
75
+ const padded = raw.padStart(parts.scale + 1, '0')
76
+ const split = padded.length - parts.scale
77
+ return padded.slice(0, split) + '.' + padded.slice(split)
78
+ }
79
+
80
+ function decimalAdd(left, right) {
81
+ const a = decimalParts(left) || { digits: 0n, scale: 0 }
82
+ const b = decimalParts(right) || { digits: 0n, scale: 0 }
83
+ const scale = Math.max(a.scale, b.scale)
84
+ const value = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale)
85
+ return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
86
+ }
87
+
88
+ function decimalMultiply(left, right) {
89
+ const a = decimalParts(left) || { digits: 0n, scale: 0 }
90
+ const b = decimalParts(right) || { digits: 0n, scale: 0 }
91
+ return decimalText((a.digits * b.digits).toString() + ((a.scale + b.scale) > 0 ? 'e-' + (a.scale + b.scale) : '')) || '0'
92
+ }
93
+
94
+ function decimalSubtract(left, right) {
95
+ const a = decimalParts(left) || { digits: 0n, scale: 0 }
96
+ const b = decimalParts(right) || { digits: 0n, scale: 0 }
97
+ const scale = Math.max(a.scale, b.scale)
98
+ const value = a.digits * 10n ** BigInt(scale - a.scale) - b.digits * 10n ** BigInt(scale - b.scale)
99
+ if (value <= 0n) return '0'
100
+ return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
101
+ }
102
+
103
+ function decimalGreaterThanZero(value) {
104
+ const parts = decimalParts(value)
105
+ return parts !== null && parts.digits > 0n
106
+ }
107
+
108
+ function nonNegativeDecimal(value, fallback = '0') {
109
+ const parsed = decimalText(value)
110
+ if (parsed === null || !decimalParts(parsed) || decimalParts(parsed).digits < 0n) return fallback
111
+ return parsed
112
+ }
113
+
114
+ function normalizeModelId(value) {
115
+ if (typeof value !== 'string') return ''
116
+ let normalized = value.trim()
117
+ const slash = normalized.lastIndexOf('/')
118
+ if (slash >= 0) normalized = normalized.slice(slash + 1)
119
+ normalized = normalized.split(':', 1)[0].trim().replace(/@/g, '-').toLowerCase()
120
+ normalized = normalized.replace(/\[1m\]$/i, '').trim()
121
+ return normalized
122
+ }
123
+
124
+ function pushUnique(list, value) {
125
+ if (typeof value === 'string' && value !== '' && !list.includes(value)) list.push(value)
126
+ }
127
+
128
+ function stripKnownNamespace(value) {
129
+ const claude = value.lastIndexOf('claude-')
130
+ if (claude > 0) return value.slice(claude)
131
+ for (const marker of ['openai.', 'anthropic.', 'google.', 'moonshot.', 'moonshotai.', 'bedrock.', 'global.']) {
132
+ if (value.startsWith(marker)) return value.slice(marker.length)
133
+ }
134
+ return null
135
+ }
136
+
137
+ function stripClaudeDesktopPrefix(value) {
138
+ const markers = ['abab', 'ark-code', 'arctic', 'astron', 'codex', 'command-r', 'deepseek', 'doubao', 'ernie', 'gemini', 'gemma', 'glm', 'gpt', 'grok', 'hermes', 'hy3', 'hunyuan', 'jamba', 'kimi', 'lfm', 'llama', 'longcat', 'mercury', 'mimo', 'minimax', 'mistral', 'mixtral', 'moonshot', 'nemotron', 'nova-', 'openai', 'qianfan', 'qwen', 'seed-', 'solar', 'stepfun']
139
+ if (!value.startsWith('claude-')) return null
140
+ const rest = value.slice('claude-'.length)
141
+ return markers.some((marker) => rest.startsWith(marker)) ? rest : null
142
+ }
143
+
144
+ function stripBedrockSuffix(value) {
145
+ const match = value.match(/^(.+)-v(\d+)$/)
146
+ return match ? match[1] : null
147
+ }
148
+
149
+ function stripDateSuffix(value) {
150
+ let match = value.match(/^(.+)-(\d{4}-\d{2}-\d{2})$/)
151
+ if (match) return match[1]
152
+ match = value.match(/^(.+)-(\d{8})$/)
153
+ if (match) return match[1]
154
+ match = value.match(/^(.+)-(\d{6})$/)
155
+ if (!match) return null
156
+ const month = Number(match[2].slice(2, 4))
157
+ const day = Number(match[2].slice(4, 6))
158
+ return month >= 1 && month <= 12 && day >= 1 && day <= 31 ? match[1] : null
159
+ }
160
+
161
+ function stripReasoningSuffix(value) {
162
+ for (const suffix of ['-minimal', '-low', '-medium', '-high', '-xhigh']) {
163
+ if (value.endsWith(suffix) && value.length > suffix.length) return value.slice(0, -suffix.length)
164
+ }
165
+ return null
166
+ }
167
+
168
+ function shouldTryPrefix(value) {
169
+ const dashCount = (value.match(/-/g) || []).length
170
+ if (value.startsWith('claude-')) return dashCount >= 3
171
+ if (['o1', 'o3', 'o4', 'o5'].some((prefix) => value.startsWith(prefix))) return dashCount >= 1
172
+ return ['gpt-', 'gemini-', 'deepseek-', 'qwen-', 'glm-', 'kimi-', 'minimax-'].some((prefix) => value.startsWith(prefix)) && dashCount >= 2
173
+ }
174
+
175
+ function modelPricingCandidates(value) {
176
+ const cleaned = normalizeModelId(value)
177
+ if (cleaned === '') return []
178
+ const candidates = []
179
+ const queue = [cleaned]
180
+ while (queue.length > 0) {
181
+ const candidate = queue.pop()
182
+ if (candidates.includes(candidate)) continue
183
+ pushUnique(candidates, candidate)
184
+ for (const next of [stripKnownNamespace(candidate), stripClaudeDesktopPrefix(candidate), stripBedrockSuffix(candidate), stripDateSuffix(candidate), stripReasoningSuffix(candidate)]) {
185
+ if (next !== null) queue.push(next)
186
+ }
187
+ if (candidate.startsWith('claude-') && candidate.includes('.')) queue.push(candidate.replace(/\./g, '-'))
188
+ }
189
+ return candidates
190
+ }
191
+
192
+ function normalizeProvider(value) {
193
+ return typeof value === 'string' ? value.trim().toLowerCase() : ''
194
+ }
195
+
196
+ function officialProviderIds(modelId) {
197
+ const providers = new Set()
198
+ for (const candidate of modelPricingCandidates(modelId)) {
199
+ for (const rule of OFFICIAL_PROVIDER_RULES) if (rule.prefixes.some((prefix) => candidate.startsWith(prefix))) for (const provider of rule.providers) providers.add(provider)
200
+ }
201
+ return providers
202
+ }
203
+
204
+ function identityModels(identity) {
205
+ const value = isRecord(identity) ? identity : {}
206
+ const result = []
207
+ for (const candidate of [value.actualModel, value.requestedModel, value.model, value.label]) {
208
+ const normalized = normalizeModelId(candidate)
209
+ if (normalized !== '' && !result.includes(normalized)) result.push(normalized)
210
+ }
211
+ return result
212
+ }
213
+
214
+ function identityKeyOf(identity) {
215
+ return isRecord(identity) && typeof identity.identityKey === 'string' ? identity.identityKey : ''
216
+ }
217
+
218
+ function priceEntryKey(entry) {
219
+ return normalizeProvider(entry.providerId) + '\0' + normalizeModelId(entry.modelId)
220
+ }
221
+
222
+ function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
223
+ if (!isRecord(raw)) return null
224
+ const modelId = normalizeModelId(raw.modelId || raw.id)
225
+ 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)
228
+ 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)
236
+ if (cacheRead === null || cacheWrite === null || decimalParts(cacheRead).digits < 0n || decimalParts(cacheWrite).digits < 0n) return null
237
+ return {
238
+ providerId: typeof raw.providerId === 'string' ? raw.providerId.trim() : '',
239
+ providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
240
+ modelId,
241
+ displayName: typeof raw.displayName === 'string' && raw.displayName.trim() !== '' ? raw.displayName.trim().slice(0, 200) : modelId,
242
+ currency: typeof raw.currency === 'string' && raw.currency.trim() !== '' ? raw.currency.trim().toUpperCase() : 'USD',
243
+ input,
244
+ output,
245
+ cacheRead,
246
+ cacheWrite,
247
+ source: raw.source === 'manual' ? 'manual' : sourceDefault,
248
+ tiered: raw.tiered === true || raw.tiers !== undefined,
249
+ reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
250
+ fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
251
+ }
252
+ }
253
+
254
+ function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
255
+ if (!isRecord(raw)) return { ok: false, error: 'catalog-not-object' }
256
+ const entries = []
257
+ const seen = new Set()
258
+ for (const [providerKey, provider] of Object.entries(raw)) {
259
+ if (!isRecord(provider) || !isRecord(provider.models)) continue
260
+ const providerId = typeof provider.id === 'string' && provider.id.trim() !== '' ? provider.id.trim() : providerKey
261
+ const providerName = typeof provider.name === 'string' ? provider.name : providerId
262
+ for (const [modelKey, model] of Object.entries(provider.models)) {
263
+ if (!isRecord(model)) continue
264
+ const entry = normalizePriceEntry({
265
+ providerId,
266
+ providerName,
267
+ modelId: typeof model.id === 'string' ? model.id : modelKey,
268
+ displayName: model.name,
269
+ currency: 'USD',
270
+ input: model.cost && model.cost.input,
271
+ output: model.cost && model.cost.output,
272
+ cacheRead: model.cost && model.cost.cache_read,
273
+ cacheWrite: model.cost && model.cost.cache_write,
274
+ tiered: model.cost && model.cost.tiers !== undefined || model.cost && model.cost.context_over_200k !== undefined,
275
+ reasoningRateAvailable: model.cost && model.cost.reasoning !== undefined,
276
+ source: 'models.dev',
277
+ fetchedAt,
278
+ })
279
+ if (entry === null || entry.currency !== 'USD') continue
280
+ const key = priceEntryKey(entry)
281
+ if (seen.has(key)) continue
282
+ seen.add(key)
283
+ entries.push(entry)
284
+ if (entries.length >= MAX_PRICE_ENTRIES) break
285
+ }
286
+ if (entries.length >= MAX_PRICE_ENTRIES) break
287
+ }
288
+ if (entries.length === 0) return { ok: false, error: 'catalog-has-no-priced-models' }
289
+ const canonical = JSON.stringify(entries)
290
+ return {
291
+ ok: true,
292
+ catalog: {
293
+ schemaVersion: PRICING_SCHEMA_VERSION,
294
+ sourceUrl: MODEL_CATALOG_URL,
295
+ fetchedAt,
296
+ catalogHash: createHash('sha256').update(canonical).digest('hex'),
297
+ entries,
298
+ },
299
+ }
300
+ }
301
+
302
+ function normalizePricingState(raw) {
303
+ const value = isRecord(raw) ? raw : {}
304
+ const sync = isRecord(value.sync) ? value.sync : {}
305
+ const source = isRecord(value.source) ? value.source : {}
306
+ const normalizeEntries = (items, sourceDefault, limit) => (Array.isArray(items) ? items.slice(0, limit) : []).map((item) => normalizePriceEntry(item, sourceDefault)).filter((item) => item !== null)
307
+ const catalogEntries = normalizeEntries(value.catalogEntries || value.entries, 'models.dev', MAX_PRICE_ENTRIES)
308
+ const overrides = normalizeEntries(value.overrides, 'manual', MAX_OVERRIDES).map((entry) => ({ ...entry, source: 'manual' }))
309
+ const mappings = (Array.isArray(value.mappings) ? value.mappings : []).slice(0, MAX_MAPPINGS).map((mapping) => {
310
+ if (!isRecord(mapping)) return null
311
+ const provider = typeof mapping.provider === 'string' ? mapping.provider.trim() : ''
312
+ const model = normalizeModelId(mapping.model || mapping.modelId)
313
+ const catalogProviderId = typeof mapping.catalogProviderId === 'string' ? mapping.catalogProviderId.trim() : ''
314
+ const catalogModelId = normalizeModelId(mapping.catalogModelId || mapping.catalogModel)
315
+ if (provider === '' && model === '' && catalogProviderId === '' && catalogModelId === '') return null
316
+ return {
317
+ identityKey: typeof mapping.identityKey === 'string' ? mapping.identityKey.slice(0, 1024) : '',
318
+ provider,
319
+ model,
320
+ catalogProviderId,
321
+ catalogModelId,
322
+ inputTokenSemantics: INPUT_SEMANTICS.includes(mapping.inputTokenSemantics) ? mapping.inputTokenSemantics : 'fresh',
323
+ multiplier: decimalText(mapping.multiplier === undefined ? '1' : mapping.multiplier) || '1',
324
+ }
325
+ }).filter((mapping) => mapping !== null)
326
+ const providerAliases = {}
327
+ if (isRecord(value.providerAliases)) {
328
+ for (const [from, to] of Object.entries(value.providerAliases).slice(0, MAX_MAPPINGS)) {
329
+ if (typeof from === 'string' && typeof to === 'string' && from.trim() !== '' && to.trim() !== '') providerAliases[from.trim()] = to.trim()
330
+ }
331
+ }
332
+ const normalized = {
333
+ schemaVersion: PRICING_SCHEMA_VERSION,
334
+ source: {
335
+ url: typeof source.url === 'string' && source.url !== '' ? source.url : MODEL_CATALOG_URL,
336
+ fetchedAt: Number.isFinite(source.fetchedAt) ? source.fetchedAt : 0,
337
+ catalogHash: typeof source.catalogHash === 'string' ? source.catalogHash.slice(0, 128) : '',
338
+ lastError: typeof source.lastError === 'string' ? source.lastError.slice(0, 500) : '',
339
+ },
340
+ sync: {
341
+ autoEnabled: sync.autoEnabled === true,
342
+ intervalMs: Number.isFinite(sync.intervalMs) && sync.intervalMs >= 60 * 60 * 1000 ? sync.intervalMs : DEFAULT_SYNC_INTERVAL_MS,
343
+ lastAttemptAt: Number.isFinite(sync.lastAttemptAt) ? sync.lastAttemptAt : 0,
344
+ lastSuccessAt: Number.isFinite(sync.lastSuccessAt) ? sync.lastSuccessAt : 0,
345
+ lastError: typeof sync.lastError === 'string' ? sync.lastError.slice(0, 500) : '',
346
+ },
347
+ catalogEntries,
348
+ overrides,
349
+ mappings,
350
+ providerAliases,
351
+ }
352
+ const entries = normalized.overrides.concat(normalized.catalogEntries)
353
+ const exactIndex = new Map()
354
+ for (const entry of entries) {
355
+ const list = exactIndex.get(entry.modelId) || []
356
+ list.push(entry)
357
+ exactIndex.set(entry.modelId, list)
358
+ }
359
+ Object.defineProperty(normalized, '_entries', { value: entries, enumerable: false })
360
+ Object.defineProperty(normalized, '_exactIndex', { value: exactIndex, enumerable: false })
361
+ Object.defineProperty(normalized, '_normalized', { value: true, enumerable: false })
362
+ return normalized
363
+ }
364
+
365
+ function createEmptyPricingState() {
366
+ return normalizePricingState({})
367
+ }
368
+
369
+ function serializePricingState(state) {
370
+ const normalized = normalizePricingState(state)
371
+ return {
372
+ schemaVersion: normalized.schemaVersion,
373
+ source: { ...normalized.source },
374
+ sync: { ...normalized.sync },
375
+ catalogEntries: normalized.catalogEntries.map((entry) => ({ ...entry })),
376
+ overrides: normalized.overrides.map((entry) => ({ ...entry })),
377
+ mappings: normalized.mappings.map((mapping) => ({ ...mapping })),
378
+ providerAliases: { ...normalized.providerAliases },
379
+ }
380
+ }
381
+
382
+ 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
384
+ }
385
+
386
+ function mappingMatches(mapping, identity) {
387
+ const models = identityModels(identity)
388
+ return mapping.model !== '' && models.includes(mapping.model)
389
+ }
390
+
391
+ function findMappedEntry(mapping, state) {
392
+ if (mapping.catalogProviderId !== '' && mapping.catalogModelId !== '') {
393
+ return state.overrides.find((entry) => normalizeProvider(entry.providerId) === normalizeProvider(mapping.catalogProviderId) && entry.modelId === mapping.catalogModelId) || state.catalogEntries.find((entry) => normalizeProvider(entry.providerId) === normalizeProvider(mapping.catalogProviderId) && entry.modelId === mapping.catalogModelId) || null
394
+ }
395
+ if (mapping.catalogModelId !== '') {
396
+ const matches = state.overrides.concat(state.catalogEntries).filter((entry) => entry.modelId === mapping.catalogModelId)
397
+ return matches.length === 1 ? matches[0] : null
398
+ }
399
+ return null
400
+ }
401
+
402
+ function resolvePricing(identity, rawState) {
403
+ const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
404
+ const mapped = state.mappings.find((mapping) => mappingMatches(mapping, identity))
405
+ const mappedEntry = mapped === undefined ? null : findMappedEntry(mapped, state)
406
+ if (mapped !== undefined && mappedEntry === null && (mapped.catalogModelId !== '' || mapped.catalogProviderId !== '')) {
407
+ return { status: 'unpriced', reason: 'mapping-target-not-found', pricingModel: mapped.catalogModelId || null, providerId: mapped.catalogProviderId || null }
408
+ }
409
+ const models = mappedEntry === null ? identityModels(identity) : [mappedEntry.modelId]
410
+ const pool = state._entries || state.overrides.concat(state.catalogEntries)
411
+ const exactIndex = state._exactIndex || new Map()
412
+ const officialProviders = new Set(models.flatMap((model) => Array.from(officialProviderIds(model))))
413
+ const choose = (entries, model, candidate, exact) => entries.filter((entry) => {
414
+ if (mappedEntry !== null && entry !== mappedEntry) return false
415
+ if (entry.currency !== 'USD') return false
416
+ if (entry.source === 'manual') return true
417
+ return officialProviders.has(normalizeProvider(entry.providerId))
418
+ }).map((entry) => ({ entry, model, candidate, exact }))
419
+ for (const model of models) {
420
+ const manualMatches = []
421
+ const officialMatches = []
422
+ for (const [index, id] of modelPricingCandidates(model).entries()) {
423
+ const exactMatches = choose(exactIndex.get(id) || [], model, id, true)
424
+ manualMatches.push(...exactMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 })))
425
+ officialMatches.push(...exactMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 })))
426
+ if (shouldTryPrefix(id)) {
427
+ const prefixMatches = choose(pool.filter((entry) => entry.modelId.startsWith(id + '-')), model, id, false)
428
+ manualMatches.push(...prefixMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
429
+ officialMatches.push(...prefixMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
430
+ }
431
+ }
432
+ const matches = manualMatches.length > 0 ? manualMatches : officialMatches
433
+ if (matches.length === 0) continue
434
+ matches.sort((a, b) => a.rank - b.rank || a.entry.modelId.length - b.entry.modelId.length)
435
+ const best = matches[0]
436
+ const sameRank = matches.filter((match) => match.rank === best.rank)
437
+ const unique = []
438
+ for (const match of sameRank) if (!unique.some((entry) => sameRates(entry, match.entry))) unique.push(match.entry)
439
+ 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
+ const entry = unique[0] || best.entry
441
+ return {
442
+ status: 'priced',
443
+ reason: '',
444
+ pricingModel: entry.modelId,
445
+ providerId: entry.providerId || null,
446
+ providerName: entry.providerName || null,
447
+ displayName: entry.displayName,
448
+ currency: entry.currency,
449
+ rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
450
+ source: entry.source,
451
+ tiered: entry.tiered,
452
+ reasoningRateAvailable: entry.reasoningRateAvailable,
453
+ inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
454
+ multiplier: mapped ? mapped.multiplier : '1',
455
+ }
456
+ }
457
+ const model = models[0] || null
458
+ if (officialProviders.size === 0) return { status: 'unsupported', reason: 'official-provider-unknown', pricingModel: model, providerId: null }
459
+ return { status: 'unpriced', reason: 'official-price-not-found', pricingModel: model, providerId: null }
460
+ }
461
+
462
+ function tokenCount(value) {
463
+ const parsed = finiteNumber(value)
464
+ if (parsed === null || parsed <= 0) return 0n
465
+ return BigInt(Math.trunc(parsed))
466
+ }
467
+
468
+ function costPerMillion(tokens, rate) {
469
+ const tokenValue = tokenCount(tokens)
470
+ const rateParts = decimalParts(rate) || { digits: 0n, scale: 0 }
471
+ const numerator = tokenValue * rateParts.digits
472
+ const scale = rateParts.scale + 6
473
+ return decimalText(numerator.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
474
+ }
475
+
476
+ function calculateCost(values, resolved) {
477
+ const input = finiteNumber(values && values.input) || 0
478
+ const output = finiteNumber(values && values.output) || 0
479
+ const cacheRead = finiteNumber(values && values.cacheRead) || 0
480
+ const cacheWrite = finiteNumber(values && values.cacheWrite) || 0
481
+ const inputSemantics = INPUT_SEMANTICS.includes(resolved && resolved.inputTokenSemantics) ? resolved.inputTokenSemantics : 'fresh'
482
+ const billableInput = inputSemantics === 'total' ? Math.max(0, input - cacheRead - cacheWrite) : inputSemantics === 'legacy' ? Math.max(0, input - cacheRead) : input
483
+ const billableOutput = output
484
+ const base = {
485
+ schemaVersion: COST_SCHEMA_VERSION,
486
+ pricingMode: 'official-model',
487
+ status: resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : 'unpriced',
488
+ currency: resolved && resolved.currency ? resolved.currency : 'USD',
489
+ source: resolved && resolved.source ? resolved.source : 'none',
490
+ pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
491
+ providerId: resolved && resolved.providerId ? resolved.providerId : null,
492
+ inputTokenSemantics: inputSemantics,
493
+ multiplier: decimalText(resolved && resolved.multiplier !== undefined ? resolved.multiplier : '1') || '1',
494
+ billableInputTokens: Math.trunc(billableInput),
495
+ billableOutputTokens: Math.trunc(billableOutput),
496
+ rates: resolved && resolved.rates ? { ...resolved.rates } : { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
497
+ breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
498
+ baseTotal: '0',
499
+ total: '0',
500
+ reason: resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found',
501
+ tiered: resolved && resolved.tiered === true,
502
+ reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
503
+ }
504
+ if (base.status !== 'priced') return base
505
+ base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
506
+ base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
507
+ base.breakdown.cacheRead = costPerMillion(cacheRead, base.rates.cacheRead)
508
+ base.breakdown.cacheWrite = costPerMillion(cacheWrite, base.rates.cacheWrite)
509
+ base.baseTotal = decimalAdd(decimalAdd(base.breakdown.input, base.breakdown.output), decimalAdd(base.breakdown.cacheRead, base.breakdown.cacheWrite))
510
+ base.total = decimalMultiply(base.baseTotal, base.multiplier)
511
+ return base
512
+ }
513
+
514
+ function emptyCostAggregate(currency = 'USD') {
515
+ return { currency, input: '0', output: '0', cacheRead: '0', cacheWrite: '0', baseTotal: '0', total: '0', pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
516
+ }
517
+
518
+ function addCostAggregate(target, cost) {
519
+ const value = isRecord(cost) ? cost : {}
520
+ const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
521
+ if (status === 'priced') {
522
+ target.input = decimalAdd(target.input, value.breakdown && value.breakdown.input)
523
+ target.output = decimalAdd(target.output, value.breakdown && value.breakdown.output)
524
+ target.cacheRead = decimalAdd(target.cacheRead, value.breakdown && value.breakdown.cacheRead)
525
+ target.cacheWrite = decimalAdd(target.cacheWrite, value.breakdown && value.breakdown.cacheWrite)
526
+ target.baseTotal = decimalAdd(target.baseTotal, value.baseTotal)
527
+ target.total = decimalAdd(target.total, value.total)
528
+ target.pricedCalls += 1
529
+ } else if (status === 'ambiguous') target.ambiguousCalls += 1
530
+ else if (status === 'unsupported') target.unsupportedCalls += 1
531
+ else target.unpricedCalls += 1
532
+ return target
533
+ }
534
+
535
+ function serializeCostAggregate(cost) {
536
+ const value = cost || emptyCostAggregate()
537
+ return {
538
+ 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',
545
+ pricedCalls: Number.isFinite(value.pricedCalls) ? value.pricedCalls : 0,
546
+ unpricedCalls: Number.isFinite(value.unpricedCalls) ? value.unpricedCalls : 0,
547
+ ambiguousCalls: Number.isFinite(value.ambiguousCalls) ? value.ambiguousCalls : 0,
548
+ unsupportedCalls: Number.isFinite(value.unsupportedCalls) ? value.unsupportedCalls : 0,
549
+ }
550
+ }
551
+
552
+ function normalizeCostSnapshot(raw) {
553
+ if (!isRecord(raw) || !COST_STATUSES.includes(raw.status)) return null
554
+ const rates = isRecord(raw.rates) ? raw.rates : {}
555
+ const breakdown = isRecord(raw.breakdown) ? raw.breakdown : {}
556
+ const fields = ['input', 'output', 'cacheRead', 'cacheWrite']
557
+ const normalizedRates = {}
558
+ const normalizedBreakdown = {}
559
+ for (const field of fields) {
560
+ const rate = decimalText(rates[field])
561
+ const cost = decimalText(breakdown[field])
562
+ if (rate === null || cost === null) return null
563
+ normalizedRates[field] = rate
564
+ normalizedBreakdown[field] = cost
565
+ }
566
+ const baseTotal = decimalText(raw.baseTotal)
567
+ const total = decimalText(raw.total)
568
+ const multiplier = decimalText(raw.multiplier)
569
+ if (baseTotal === null || total === null || multiplier === null) return null
570
+ return {
571
+ schemaVersion: COST_SCHEMA_VERSION,
572
+ pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
573
+ status: raw.status,
574
+ currency: typeof raw.currency === 'string' ? raw.currency : 'USD',
575
+ source: typeof raw.source === 'string' ? raw.source : 'none',
576
+ pricingModel: typeof raw.pricingModel === 'string' ? raw.pricingModel : null,
577
+ providerId: typeof raw.providerId === 'string' ? raw.providerId : null,
578
+ inputTokenSemantics: INPUT_SEMANTICS.includes(raw.inputTokenSemantics) ? raw.inputTokenSemantics : 'fresh',
579
+ multiplier,
580
+ billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
581
+ billableOutputTokens: Number.isFinite(raw.billableOutputTokens) ? Math.max(0, Math.trunc(raw.billableOutputTokens)) : 0,
582
+ rates: normalizedRates,
583
+ breakdown: normalizedBreakdown,
584
+ baseTotal,
585
+ total,
586
+ reason: typeof raw.reason === 'string' ? raw.reason.slice(0, 200) : '',
587
+ tiered: raw.tiered === true,
588
+ reasoningRateAvailable: raw.reasoningRateAvailable === true,
589
+ }
590
+ }
591
+
592
+ async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {}) {
593
+ if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
594
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
595
+ const url = typeof options.url === 'string' && options.url !== '' ? options.url : MODEL_CATALOG_URL
596
+ let controller = null
597
+ let timer = null
598
+ try {
599
+ if (typeof AbortController === 'function') {
600
+ controller = new AbortController()
601
+ timer = setTimeout(() => controller.abort(), timeoutMs)
602
+ }
603
+ const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
604
+ if (!response || response.ok !== true) return { ok: false, error: 'http-' + String(response && response.status || 0) }
605
+ const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
606
+ if (Number.isFinite(length) && length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
607
+ const text = await response.text()
608
+ if (new TextEncoder().encode(text).length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
609
+ let parsed
610
+ try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { return { ok: false, error: 'catalog-invalid-json' } }
611
+ return parseModelsDevCatalog(parsed, Date.now())
612
+ } catch (err) {
613
+ return { ok: false, error: 'catalog-fetch-failed' }
614
+ } finally {
615
+ if (timer !== null) clearTimeout(timer)
616
+ }
617
+ }
618
+
619
+ export {
620
+ COST_SCHEMA_VERSION,
621
+ COST_STATUSES,
622
+ DEFAULT_SYNC_INTERVAL_MS,
623
+ INPUT_SEMANTICS,
624
+ MODEL_CATALOG_URL,
625
+ PRICING_SCHEMA_VERSION,
626
+ RATE_KEYS,
627
+ addCostAggregate,
628
+ calculateCost,
629
+ createEmptyPricingState,
630
+ decimalAdd,
631
+ decimalMultiply,
632
+ decimalSubtract,
633
+ decimalText,
634
+ emptyCostAggregate,
635
+ fetchModelsDevCatalog,
636
+ modelPricingCandidates,
637
+ officialProviderIds,
638
+ normalizeCostSnapshot,
639
+ normalizeModelId,
640
+ normalizePricingState,
641
+ parseModelsDevCatalog,
642
+ resolvePricing,
643
+ serializeCostAggregate,
644
+ serializePricingState,
645
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-all-usage",
3
- "version": "1.0.9",
3
+ "version": "1.1.2",
4
4
  "description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,10 @@
37
37
  "files": [
38
38
  "lib/index.js",
39
39
  "lib/client.js",
40
+ "lib/pricing.js",
40
41
  "CHANGELOG.md",
41
42
  "cordis.patch.yml",
43
+ "screenshots.json",
42
44
  "assets"
43
45
  ],
44
46
  "keywords": [
@@ -0,0 +1,5 @@
1
+ [
2
+ "assets/screenshot-1.png",
3
+ "assets/screenshot-2.png",
4
+ "assets/screenshot-3.png"
5
+ ]