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/CHANGELOG.md +54 -0
- package/README.md +59 -12
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/lib/client.js +1668 -100
- package/lib/index.js +1118 -167
- package/lib/pricing.js +645 -0
- package/package.json +3 -1
- package/screenshots.json +5 -0
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.
|
|
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": [
|