dsh-all-usage 1.1.1 → 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/CHANGELOG.md +87 -0
- package/LICENSE +21 -21
- package/README.md +216 -27
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/cordis.patch.yml +5 -5
- package/fixtures/usage-events.json +172 -0
- package/lib/aggregation.js +1002 -0
- package/lib/balance.js +112 -0
- package/lib/client.js +1 -2271
- package/lib/http.js +305 -0
- package/lib/index.js +2 -1572
- package/lib/ledger.js +464 -0
- package/lib/plugin.js +276 -0
- package/lib/pricing-runtime.js +282 -0
- package/lib/pricing.js +908 -0
- package/lib/session-sync.js +589 -0
- package/lib/usage-core.js +127 -0
- package/package.json +82 -56
- package/screenshots.json +1 -5
- package/scripts/replay-fixture.mjs +155 -0
- package/assets/screenshot-4.png +0 -0
- package/assets/screenshot-5.png +0 -0
- package/assets/screenshot-6.png +0 -0
- package/assets/screenshot-7.png +0 -0
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
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
|
+
// 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
|
|
14
|
+
const MAX_OVERRIDES = 500
|
|
15
|
+
const MAX_MAPPINGS = 500
|
|
16
|
+
const MAX_CONTEXT_TIERS = 32
|
|
17
|
+
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
18
|
+
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
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()
|
|
23
|
+
const OFFICIAL_PROVIDER_RULES = [
|
|
24
|
+
{ providers: ['openai'], prefixes: ['gpt-', 'o1', 'o3', 'o4', 'o5'] },
|
|
25
|
+
{ providers: ['anthropic'], prefixes: ['claude-'] },
|
|
26
|
+
{ providers: ['google'], prefixes: ['gemini-', 'gemma-'] },
|
|
27
|
+
{ providers: ['xai'], prefixes: ['grok-'] },
|
|
28
|
+
{ providers: ['deepseek'], prefixes: ['deepseek-'] },
|
|
29
|
+
{ providers: ['moonshotai', 'moonshot'], prefixes: ['kimi-', 'moonshot-'] },
|
|
30
|
+
{ providers: ['qwen', 'alibaba'], prefixes: ['qwen'] },
|
|
31
|
+
{ providers: ['zai', 'zhipuai', 'zhipu'], prefixes: ['glm-', 'chatglm-'] },
|
|
32
|
+
{ providers: ['minimax'], prefixes: ['minimax-'] },
|
|
33
|
+
{ providers: ['mistral'], prefixes: ['mistral-', 'mixtral-'] },
|
|
34
|
+
{ providers: ['meta'], prefixes: ['llama-', 'meta-llama'] },
|
|
35
|
+
{ providers: ['cohere'], prefixes: ['command-'] },
|
|
36
|
+
{ providers: ['ai21'], prefixes: ['jamba-'] },
|
|
37
|
+
{ providers: ['baidu'], prefixes: ['ernie-'] },
|
|
38
|
+
]
|
|
39
|
+
const MAX_DECIMAL_DIGITS = 40
|
|
40
|
+
const MAX_DECIMAL_EXPONENT = 24
|
|
41
|
+
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function finiteNumber(value) {
|
|
47
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
48
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
49
|
+
const parsed = Number(value)
|
|
50
|
+
if (Number.isFinite(parsed)) return parsed
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function decimalParts(value) {
|
|
56
|
+
const raw = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
57
|
+
const match = raw.match(/^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/)
|
|
58
|
+
if (!match) return null
|
|
59
|
+
let digits = (match[1] || '') + (match[2] || '')
|
|
60
|
+
const exponent = match[3] ? Number(match[3]) : 0
|
|
61
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > MAX_DECIMAL_EXPONENT) return null
|
|
62
|
+
let scale = (match[2] || '').length - exponent
|
|
63
|
+
digits = digits.replace(/^0+(?=\d)/, '')
|
|
64
|
+
if (scale < 0) {
|
|
65
|
+
digits += '0'.repeat(-scale)
|
|
66
|
+
scale = 0
|
|
67
|
+
}
|
|
68
|
+
if (scale > digits.length) digits = '0'.repeat(scale - digits.length + 1) + digits
|
|
69
|
+
if (digits.length > MAX_DECIMAL_DIGITS) return null
|
|
70
|
+
while (scale > 0 && digits.length > 1 && digits.endsWith('0')) {
|
|
71
|
+
digits = digits.slice(0, -1)
|
|
72
|
+
scale -= 1
|
|
73
|
+
}
|
|
74
|
+
digits = digits.replace(/^0+(?=\d)/, '')
|
|
75
|
+
return { digits: BigInt(digits || '0'), scale }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decimalText(value) {
|
|
79
|
+
const parts = decimalParts(value)
|
|
80
|
+
if (parts === null) return null
|
|
81
|
+
if (parts.digits === 0n) return '0'
|
|
82
|
+
const raw = parts.digits.toString()
|
|
83
|
+
if (parts.scale === 0) return raw
|
|
84
|
+
const padded = raw.padStart(parts.scale + 1, '0')
|
|
85
|
+
const split = padded.length - parts.scale
|
|
86
|
+
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
87
|
+
}
|
|
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
|
+
|
|
172
|
+
function decimalAdd(left, right) {
|
|
173
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
174
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
175
|
+
const scale = Math.max(a.scale, b.scale)
|
|
176
|
+
const value = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale)
|
|
177
|
+
return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function decimalMultiply(left, right) {
|
|
181
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
182
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
183
|
+
return decimalText((a.digits * b.digits).toString() + ((a.scale + b.scale) > 0 ? 'e-' + (a.scale + b.scale) : '')) || '0'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function decimalSubtract(left, right) {
|
|
187
|
+
const a = decimalParts(left) || { digits: 0n, scale: 0 }
|
|
188
|
+
const b = decimalParts(right) || { digits: 0n, scale: 0 }
|
|
189
|
+
const scale = Math.max(a.scale, b.scale)
|
|
190
|
+
const value = a.digits * 10n ** BigInt(scale - a.scale) - b.digits * 10n ** BigInt(scale - b.scale)
|
|
191
|
+
if (value <= 0n) return '0'
|
|
192
|
+
return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function decimalGreaterThanZero(value) {
|
|
196
|
+
const parts = decimalParts(value)
|
|
197
|
+
return parts !== null && parts.digits > 0n
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function nonNegativeDecimal(value, fallback = '0') {
|
|
201
|
+
const parsed = decimalText(value)
|
|
202
|
+
if (parsed === null || !decimalParts(parsed) || decimalParts(parsed).digits < 0n) return fallback
|
|
203
|
+
return parsed
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeModelId(value) {
|
|
207
|
+
if (typeof value !== 'string') return ''
|
|
208
|
+
let normalized = value.trim()
|
|
209
|
+
const slash = normalized.lastIndexOf('/')
|
|
210
|
+
if (slash >= 0) normalized = normalized.slice(slash + 1)
|
|
211
|
+
normalized = normalized.split(':', 1)[0].trim().replace(/@/g, '-').toLowerCase()
|
|
212
|
+
normalized = normalized.replace(/\[1m\]$/i, '').trim()
|
|
213
|
+
return normalized
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function pushUnique(list, value) {
|
|
217
|
+
if (typeof value === 'string' && value !== '' && !list.includes(value)) list.push(value)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stripKnownNamespace(value) {
|
|
221
|
+
const claude = value.lastIndexOf('claude-')
|
|
222
|
+
if (claude > 0) return value.slice(claude)
|
|
223
|
+
for (const marker of ['openai.', 'anthropic.', 'google.', 'moonshot.', 'moonshotai.', 'bedrock.', 'global.']) {
|
|
224
|
+
if (value.startsWith(marker)) return value.slice(marker.length)
|
|
225
|
+
}
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stripClaudeDesktopPrefix(value) {
|
|
230
|
+
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']
|
|
231
|
+
if (!value.startsWith('claude-')) return null
|
|
232
|
+
const rest = value.slice('claude-'.length)
|
|
233
|
+
return markers.some((marker) => rest.startsWith(marker)) ? rest : null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function stripBedrockSuffix(value) {
|
|
237
|
+
const match = value.match(/^(.+)-v(\d+)$/)
|
|
238
|
+
return match ? match[1] : null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stripDateSuffix(value) {
|
|
242
|
+
let match = value.match(/^(.+)-(\d{4}-\d{2}-\d{2})$/)
|
|
243
|
+
if (match) return match[1]
|
|
244
|
+
match = value.match(/^(.+)-(\d{8})$/)
|
|
245
|
+
if (match) return match[1]
|
|
246
|
+
match = value.match(/^(.+)-(\d{6})$/)
|
|
247
|
+
if (!match) return null
|
|
248
|
+
const month = Number(match[2].slice(2, 4))
|
|
249
|
+
const day = Number(match[2].slice(4, 6))
|
|
250
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= 31 ? match[1] : null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stripReasoningSuffix(value) {
|
|
254
|
+
for (const suffix of ['-minimal', '-low', '-medium', '-high', '-xhigh']) {
|
|
255
|
+
if (value.endsWith(suffix) && value.length > suffix.length) return value.slice(0, -suffix.length)
|
|
256
|
+
}
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function shouldTryPrefix(value) {
|
|
261
|
+
const dashCount = (value.match(/-/g) || []).length
|
|
262
|
+
if (value.startsWith('claude-')) return dashCount >= 3
|
|
263
|
+
if (['o1', 'o3', 'o4', 'o5'].some((prefix) => value.startsWith(prefix))) return dashCount >= 1
|
|
264
|
+
return ['gpt-', 'gemini-', 'deepseek-', 'qwen-', 'glm-', 'kimi-', 'minimax-'].some((prefix) => value.startsWith(prefix)) && dashCount >= 2
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function modelPricingCandidates(value) {
|
|
268
|
+
const cleaned = normalizeModelId(value)
|
|
269
|
+
if (cleaned === '') return []
|
|
270
|
+
const candidates = []
|
|
271
|
+
const queue = [cleaned]
|
|
272
|
+
while (queue.length > 0) {
|
|
273
|
+
const candidate = queue.pop()
|
|
274
|
+
if (candidates.includes(candidate)) continue
|
|
275
|
+
pushUnique(candidates, candidate)
|
|
276
|
+
for (const next of [stripKnownNamespace(candidate), stripClaudeDesktopPrefix(candidate), stripBedrockSuffix(candidate), stripDateSuffix(candidate), stripReasoningSuffix(candidate)]) {
|
|
277
|
+
if (next !== null) queue.push(next)
|
|
278
|
+
}
|
|
279
|
+
if (candidate.startsWith('claude-') && candidate.includes('.')) queue.push(candidate.replace(/\./g, '-'))
|
|
280
|
+
}
|
|
281
|
+
return candidates
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizeProvider(value) {
|
|
285
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function officialProviderIds(modelId) {
|
|
289
|
+
const providers = new Set()
|
|
290
|
+
for (const candidate of modelPricingCandidates(modelId)) {
|
|
291
|
+
for (const rule of OFFICIAL_PROVIDER_RULES) if (rule.prefixes.some((prefix) => candidate.startsWith(prefix))) for (const provider of rule.providers) providers.add(provider)
|
|
292
|
+
}
|
|
293
|
+
return providers
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function identityModels(identity) {
|
|
297
|
+
const value = isRecord(identity) ? identity : {}
|
|
298
|
+
const result = []
|
|
299
|
+
for (const candidate of [value.actualModel, value.requestedModel, value.model, value.label]) {
|
|
300
|
+
const normalized = normalizeModelId(candidate)
|
|
301
|
+
if (normalized !== '' && !result.includes(normalized)) result.push(normalized)
|
|
302
|
+
}
|
|
303
|
+
return result
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function identityKeyOf(identity) {
|
|
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() : ''
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function priceEntryKey(entry) {
|
|
313
|
+
return normalizeProvider(entry.providerId) + '\0' + normalizeModelId(entry.modelId)
|
|
314
|
+
}
|
|
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
|
+
|
|
344
|
+
function normalizePriceEntry(raw, sourceDefault = 'models.dev') {
|
|
345
|
+
if (!isRecord(raw)) return null
|
|
346
|
+
const modelId = normalizeModelId(raw.modelId || raw.id)
|
|
347
|
+
if (modelId === '') return null
|
|
348
|
+
const input = decimalRate(raw, ['input', 'inputPerMillion'])
|
|
349
|
+
const output = decimalRate(raw, ['output', 'outputPerMillion'])
|
|
350
|
+
if (input === null || output === null || decimalParts(input).digits < 0n || decimalParts(output).digits < 0n) return null
|
|
351
|
+
const cacheRead = decimalRate(raw, ['cacheRead', 'cache_read', 'cacheReadPerMillion'], '0')
|
|
352
|
+
const cacheWrite = decimalRate(raw, ['cacheWrite', 'cache_write', 'cacheCreation', 'cacheWritePerMillion'], '0')
|
|
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
|
|
380
|
+
return {
|
|
381
|
+
providerId: typeof raw.providerId === 'string' ? raw.providerId.trim().toLowerCase() : '',
|
|
382
|
+
providerName: typeof raw.providerName === 'string' ? raw.providerName.trim().slice(0, 200) : '',
|
|
383
|
+
modelId,
|
|
384
|
+
displayName: typeof raw.displayName === 'string' && raw.displayName.trim() !== '' ? raw.displayName.trim().slice(0, 200) : modelId,
|
|
385
|
+
currency: typeof raw.currency === 'string' && raw.currency.trim() !== '' ? raw.currency.trim().toUpperCase() : 'USD',
|
|
386
|
+
input,
|
|
387
|
+
output,
|
|
388
|
+
cacheRead,
|
|
389
|
+
cacheWrite,
|
|
390
|
+
source: raw.source === 'manual' ? 'manual' : sourceDefault,
|
|
391
|
+
tiered,
|
|
392
|
+
...(tiered ? { tiers, tieredInvalid } : {}),
|
|
393
|
+
reasoningRateAvailable: raw.reasoningRateAvailable === true || raw.reasoning !== undefined,
|
|
394
|
+
fetchedAt: Number.isFinite(raw.fetchedAt) ? raw.fetchedAt : 0,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function parseModelsDevCatalog(raw, fetchedAt = Date.now()) {
|
|
399
|
+
if (!isRecord(raw)) return { ok: false, error: 'catalog-not-object' }
|
|
400
|
+
const entries = []
|
|
401
|
+
for (const [providerKey, provider] of Object.entries(raw)) {
|
|
402
|
+
if (!isRecord(provider) || !isRecord(provider.models)) continue
|
|
403
|
+
const providerId = typeof provider.id === 'string' && provider.id.trim() !== '' ? provider.id.trim() : providerKey
|
|
404
|
+
const providerName = typeof provider.name === 'string' ? provider.name : providerId
|
|
405
|
+
for (const [modelKey, model] of Object.entries(provider.models)) {
|
|
406
|
+
if (!isRecord(model)) continue
|
|
407
|
+
const entry = normalizePriceEntry({
|
|
408
|
+
providerId,
|
|
409
|
+
providerName,
|
|
410
|
+
modelId: typeof model.id === 'string' ? model.id : modelKey,
|
|
411
|
+
displayName: model.name,
|
|
412
|
+
currency: 'USD',
|
|
413
|
+
input: model.cost && model.cost.input,
|
|
414
|
+
output: model.cost && model.cost.output,
|
|
415
|
+
cacheRead: model.cost && model.cost.cache_read,
|
|
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,
|
|
419
|
+
tiered: model.cost && model.cost.tiers !== undefined || model.cost && model.cost.context_over_200k !== undefined,
|
|
420
|
+
reasoningRateAvailable: model.cost && model.cost.reasoning !== undefined,
|
|
421
|
+
source: 'models.dev',
|
|
422
|
+
fetchedAt,
|
|
423
|
+
})
|
|
424
|
+
if (entry === null || entry.currency !== 'USD') continue
|
|
425
|
+
// Duplicate normalized keys are NOT resolved here: they are kept and
|
|
426
|
+
// resolved deterministically after the stable sort below.
|
|
427
|
+
entries.push(entry)
|
|
428
|
+
if (entries.length > MAX_CATALOG_CANDIDATES) return { ok: false, error: 'catalog-exceeds-candidate-limit', candidateCount: entries.length }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (entries.length === 0) return { ok: false, error: 'catalog-has-no-priced-models' }
|
|
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
|
+
}))
|
|
467
|
+
return {
|
|
468
|
+
ok: true,
|
|
469
|
+
catalog: {
|
|
470
|
+
schemaVersion: PRICING_SCHEMA_VERSION,
|
|
471
|
+
sourceUrl: MODEL_CATALOG_URL,
|
|
472
|
+
fetchedAt,
|
|
473
|
+
catalogHash: createHash('sha256').update(canonical).digest('hex'),
|
|
474
|
+
entries: ordered,
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizePricingState(raw) {
|
|
480
|
+
const value = isRecord(raw) ? raw : {}
|
|
481
|
+
const sync = isRecord(value.sync) ? value.sync : {}
|
|
482
|
+
const source = isRecord(value.source) ? value.source : {}
|
|
483
|
+
const normalizeEntries = (items, sourceDefault, limit) => (Array.isArray(items) ? items.slice(0, limit) : []).map((item) => normalizePriceEntry(item, sourceDefault)).filter((item) => item !== null)
|
|
484
|
+
const catalogEntries = normalizeEntries(value.catalogEntries || value.entries, 'models.dev', MAX_PRICE_ENTRIES)
|
|
485
|
+
const overrides = normalizeEntries(value.overrides, 'manual', MAX_OVERRIDES).map((entry) => ({ ...entry, source: 'manual' }))
|
|
486
|
+
const mappings = (Array.isArray(value.mappings) ? value.mappings : []).slice(0, MAX_MAPPINGS).map((mapping) => {
|
|
487
|
+
if (!isRecord(mapping)) return null
|
|
488
|
+
const provider = typeof mapping.provider === 'string' ? mapping.provider.trim() : ''
|
|
489
|
+
const model = normalizeModelId(mapping.model || mapping.modelId)
|
|
490
|
+
const catalogProviderId = typeof mapping.catalogProviderId === 'string' ? mapping.catalogProviderId.trim() : ''
|
|
491
|
+
const catalogModelId = normalizeModelId(mapping.catalogModelId || mapping.catalogModel)
|
|
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
|
|
495
|
+
return {
|
|
496
|
+
identityKey: normalizedIdentityKey,
|
|
497
|
+
provider,
|
|
498
|
+
model,
|
|
499
|
+
catalogProviderId,
|
|
500
|
+
catalogModelId,
|
|
501
|
+
inputTokenSemantics: INPUT_SEMANTICS.includes(mapping.inputTokenSemantics) ? mapping.inputTokenSemantics : 'fresh',
|
|
502
|
+
multiplier: decimalText(mapping.multiplier === undefined ? '1' : mapping.multiplier) || '1',
|
|
503
|
+
}
|
|
504
|
+
}).filter((mapping) => mapping !== null)
|
|
505
|
+
const providerAliases = {}
|
|
506
|
+
if (isRecord(value.providerAliases)) {
|
|
507
|
+
for (const [from, to] of Object.entries(value.providerAliases).slice(0, MAX_MAPPINGS)) {
|
|
508
|
+
if (typeof from === 'string' && typeof to === 'string' && from.trim() !== '' && to.trim() !== '') providerAliases[from.trim()] = to.trim()
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const normalized = {
|
|
512
|
+
schemaVersion: PRICING_SCHEMA_VERSION,
|
|
513
|
+
source: {
|
|
514
|
+
url: typeof source.url === 'string' && source.url !== '' ? source.url : MODEL_CATALOG_URL,
|
|
515
|
+
fetchedAt: Number.isFinite(source.fetchedAt) ? source.fetchedAt : 0,
|
|
516
|
+
catalogHash: typeof source.catalogHash === 'string' ? source.catalogHash.slice(0, 128) : '',
|
|
517
|
+
lastError: typeof source.lastError === 'string' ? source.lastError.slice(0, 500) : '',
|
|
518
|
+
},
|
|
519
|
+
sync: {
|
|
520
|
+
autoEnabled: sync.autoEnabled === true,
|
|
521
|
+
intervalMs: Number.isFinite(sync.intervalMs) && sync.intervalMs >= 60 * 60 * 1000 ? sync.intervalMs : DEFAULT_SYNC_INTERVAL_MS,
|
|
522
|
+
lastAttemptAt: Number.isFinite(sync.lastAttemptAt) ? sync.lastAttemptAt : 0,
|
|
523
|
+
lastSuccessAt: Number.isFinite(sync.lastSuccessAt) ? sync.lastSuccessAt : 0,
|
|
524
|
+
lastError: typeof sync.lastError === 'string' ? sync.lastError.slice(0, 500) : '',
|
|
525
|
+
},
|
|
526
|
+
catalogEntries,
|
|
527
|
+
overrides,
|
|
528
|
+
mappings,
|
|
529
|
+
providerAliases,
|
|
530
|
+
}
|
|
531
|
+
const entries = normalized.overrides.concat(normalized.catalogEntries)
|
|
532
|
+
const exactIndex = new Map()
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
const list = exactIndex.get(entry.modelId) || []
|
|
535
|
+
list.push(entry)
|
|
536
|
+
exactIndex.set(entry.modelId, list)
|
|
537
|
+
}
|
|
538
|
+
Object.defineProperty(normalized, '_entries', { value: entries, enumerable: false })
|
|
539
|
+
Object.defineProperty(normalized, '_exactIndex', { value: exactIndex, enumerable: false })
|
|
540
|
+
Object.defineProperty(normalized, '_normalized', { value: true, enumerable: false })
|
|
541
|
+
return normalized
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function createEmptyPricingState() {
|
|
545
|
+
return normalizePricingState({})
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function serializePricingState(state) {
|
|
549
|
+
const normalized = normalizePricingState(state)
|
|
550
|
+
return {
|
|
551
|
+
schemaVersion: normalized.schemaVersion,
|
|
552
|
+
source: { ...normalized.source },
|
|
553
|
+
sync: { ...normalized.sync },
|
|
554
|
+
catalogEntries: normalized.catalogEntries.map((entry) => ({ ...entry })),
|
|
555
|
+
overrides: normalized.overrides.map((entry) => ({ ...entry })),
|
|
556
|
+
mappings: normalized.mappings.map((mapping) => ({ ...mapping })),
|
|
557
|
+
providerAliases: { ...normalized.providerAliases },
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function sameRates(left, right) {
|
|
562
|
+
if (left.input !== right.input || left.output !== right.output || left.cacheRead !== right.cacheRead || left.cacheWrite !== right.cacheWrite || left.currency !== right.currency || left.tiered !== right.tiered || left.tieredInvalid !== right.tieredInvalid) return false
|
|
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
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
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
|
|
578
|
+
const models = identityModels(identity)
|
|
579
|
+
return mapping.model !== '' && models.includes(mapping.model)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function findMappedEntry(mapping, state) {
|
|
583
|
+
if (mapping.catalogProviderId !== '' && mapping.catalogModelId !== '') {
|
|
584
|
+
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
|
|
585
|
+
}
|
|
586
|
+
if (mapping.catalogModelId !== '') {
|
|
587
|
+
const matches = state.overrides.concat(state.catalogEntries).filter((entry) => entry.modelId === mapping.catalogModelId)
|
|
588
|
+
return matches.length === 1 ? matches[0] : null
|
|
589
|
+
}
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function resolvePricing(identity, rawState) {
|
|
594
|
+
const state = rawState && rawState._normalized === true ? rawState : normalizePricingState(rawState)
|
|
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))
|
|
598
|
+
const mappedEntry = mapped === undefined ? null : findMappedEntry(mapped, state)
|
|
599
|
+
if (mapped !== undefined && mappedEntry === null && (mapped.catalogModelId !== '' || mapped.catalogProviderId !== '')) {
|
|
600
|
+
return { status: 'unpriced', reason: 'mapping-target-not-found', pricingModel: mapped.catalogModelId || null, providerId: mapped.catalogProviderId || null }
|
|
601
|
+
}
|
|
602
|
+
const models = mappedEntry === null ? identityModels(identity) : [mappedEntry.modelId]
|
|
603
|
+
const pool = state._entries || state.overrides.concat(state.catalogEntries)
|
|
604
|
+
const exactIndex = state._exactIndex || new Map()
|
|
605
|
+
const officialProviders = new Set(models.flatMap((model) => Array.from(officialProviderIds(model))))
|
|
606
|
+
const choose = (entries, model, candidate, exact) => entries.filter((entry) => {
|
|
607
|
+
if (mappedEntry !== null && entry !== mappedEntry) return false
|
|
608
|
+
if (entry.currency !== 'USD') return false
|
|
609
|
+
if (entry.source === 'manual') return true
|
|
610
|
+
return officialProviders.has(normalizeProvider(entry.providerId))
|
|
611
|
+
}).map((entry) => ({ entry, model, candidate, exact }))
|
|
612
|
+
for (const model of models) {
|
|
613
|
+
const manualMatches = []
|
|
614
|
+
const officialMatches = []
|
|
615
|
+
for (const [index, id] of modelPricingCandidates(model).entries()) {
|
|
616
|
+
const exactMatches = choose(exactIndex.get(id) || [], model, id, true)
|
|
617
|
+
manualMatches.push(...exactMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 })))
|
|
618
|
+
officialMatches.push(...exactMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 })))
|
|
619
|
+
if (shouldTryPrefix(id)) {
|
|
620
|
+
const prefixMatches = choose(pool.filter((entry) => entry.modelId.startsWith(id + '-')), model, id, false)
|
|
621
|
+
manualMatches.push(...prefixMatches.filter((match) => match.entry.source === 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
|
|
622
|
+
officialMatches.push(...prefixMatches.filter((match) => match.entry.source !== 'manual').map((match) => ({ ...match, rank: index * 2 + 1 })))
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const matches = manualMatches.length > 0 ? manualMatches : officialMatches
|
|
626
|
+
if (matches.length === 0) continue
|
|
627
|
+
matches.sort((a, b) => a.rank - b.rank || a.entry.modelId.length - b.entry.modelId.length)
|
|
628
|
+
const best = matches[0]
|
|
629
|
+
const sameRank = matches.filter((match) => match.rank === best.rank)
|
|
630
|
+
const unique = []
|
|
631
|
+
for (const match of sameRank) if (!unique.some((entry) => sameRates(entry, match.entry))) unique.push(match.entry)
|
|
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 })) }
|
|
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)
|
|
637
|
+
return {
|
|
638
|
+
status: tieredSupported ? 'priced' : 'unsupported',
|
|
639
|
+
reason: tieredSupported ? '' : 'tiered-pricing-not-modeled',
|
|
640
|
+
pricingModel: entry.modelId,
|
|
641
|
+
providerId: entry.providerId || null,
|
|
642
|
+
providerName: entry.providerName || null,
|
|
643
|
+
displayName: entry.displayName,
|
|
644
|
+
currency: entry.currency,
|
|
645
|
+
rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
|
|
646
|
+
source: entry.source,
|
|
647
|
+
tiered,
|
|
648
|
+
...(tiered ? { tiers, tieredInvalid: entry.tieredInvalid === true } : {}),
|
|
649
|
+
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
650
|
+
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
651
|
+
multiplier: mapped ? mapped.multiplier : '1',
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const model = models[0] || null
|
|
655
|
+
if (officialProviders.size === 0) return { status: 'unsupported', reason: 'official-provider-unknown', pricingModel: model, providerId: null }
|
|
656
|
+
return { status: 'unpriced', reason: 'official-price-not-found', pricingModel: model, providerId: null }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function tokenCount(value) {
|
|
660
|
+
const parsed = finiteNumber(value)
|
|
661
|
+
if (parsed === null || parsed <= 0) return 0n
|
|
662
|
+
return BigInt(Math.trunc(parsed))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function costPerMillion(tokens, rate) {
|
|
666
|
+
const tokenValue = tokenCount(tokens)
|
|
667
|
+
const rateParts = decimalParts(rate) || { digits: 0n, scale: 0 }
|
|
668
|
+
const numerator = tokenValue * rateParts.digits
|
|
669
|
+
const scale = rateParts.scale + 6
|
|
670
|
+
return decimalText(numerator.toString() + (scale > 0 ? 'e-' + scale : '')) || '0'
|
|
671
|
+
}
|
|
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
|
+
|
|
709
|
+
function calculateCost(values, resolved) {
|
|
710
|
+
const input = finiteNumber(values && values.input) || 0
|
|
711
|
+
const output = finiteNumber(values && values.output) || 0
|
|
712
|
+
const cacheRead = finiteNumber(values && values.cacheRead) || 0
|
|
713
|
+
const cacheWrite = finiteNumber(values && values.cacheWrite) || 0
|
|
714
|
+
const inputSemantics = INPUT_SEMANTICS.includes(resolved && resolved.inputTokenSemantics) ? resolved.inputTokenSemantics : 'fresh'
|
|
715
|
+
const billableInput = inputSemantics === 'total' ? Math.max(0, input - cacheRead - cacheWrite) : inputSemantics === 'legacy' ? Math.max(0, input - cacheRead) : input
|
|
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)
|
|
720
|
+
const base = {
|
|
721
|
+
schemaVersion: COST_SCHEMA_VERSION,
|
|
722
|
+
pricingMode: 'official-model',
|
|
723
|
+
status: tierScheduleValid && resolved && COST_STATUSES.includes(resolved.status) ? resolved.status : tiered ? 'unsupported' : 'unpriced',
|
|
724
|
+
currency: resolved && resolved.currency ? resolved.currency : 'USD',
|
|
725
|
+
source: resolved && resolved.source ? resolved.source : 'none',
|
|
726
|
+
pricingModel: resolved && resolved.pricingModel ? resolved.pricingModel : null,
|
|
727
|
+
providerId: resolved && resolved.providerId ? resolved.providerId : null,
|
|
728
|
+
inputTokenSemantics: inputSemantics,
|
|
729
|
+
multiplier: decimalText(resolved && resolved.multiplier !== undefined ? resolved.multiplier : '1') || '1',
|
|
730
|
+
billableInputTokens: Math.trunc(billableInput),
|
|
731
|
+
billableOutputTokens: Math.trunc(billableOutput),
|
|
732
|
+
rates: resolved && resolved.rates ? { ...resolved.rates } : { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
733
|
+
breakdown: { input: '0', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
734
|
+
baseTotal: '0',
|
|
735
|
+
total: '0',
|
|
736
|
+
reason: tiered && !tierScheduleValid ? 'tiered-pricing-not-modeled' : resolved && typeof resolved.reason === 'string' ? resolved.reason : 'model-not-found',
|
|
737
|
+
tiered,
|
|
738
|
+
reasoningRateAvailable: resolved && resolved.reasoningRateAvailable === true,
|
|
739
|
+
}
|
|
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
|
+
}
|
|
749
|
+
base.breakdown.input = costPerMillion(base.billableInputTokens, base.rates.input)
|
|
750
|
+
base.breakdown.output = costPerMillion(base.billableOutputTokens, base.rates.output)
|
|
751
|
+
base.breakdown.cacheRead = costPerMillion(cacheRead, base.rates.cacheRead)
|
|
752
|
+
base.breakdown.cacheWrite = costPerMillion(cacheWrite, base.rates.cacheWrite)
|
|
753
|
+
base.baseTotal = decimalAdd(decimalAdd(base.breakdown.input, base.breakdown.output), decimalAdd(base.breakdown.cacheRead, base.breakdown.cacheWrite))
|
|
754
|
+
base.total = decimalMultiply(base.baseTotal, base.multiplier)
|
|
755
|
+
return base
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function emptyCostAggregate(currency = 'USD') {
|
|
759
|
+
return { currency, input: '0', output: '0', cacheRead: '0', cacheWrite: '0', baseTotal: '0', total: '0', pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function addCostAggregate(target, cost) {
|
|
763
|
+
if (isCostAccumulator(target)) return addCostAccumulator(target, cost)
|
|
764
|
+
const value = isRecord(cost) ? cost : {}
|
|
765
|
+
const status = COST_STATUSES.includes(value.status) ? value.status : 'unpriced'
|
|
766
|
+
if (status === 'priced') {
|
|
767
|
+
target.input = decimalAdd(target.input, value.breakdown && value.breakdown.input)
|
|
768
|
+
target.output = decimalAdd(target.output, value.breakdown && value.breakdown.output)
|
|
769
|
+
target.cacheRead = decimalAdd(target.cacheRead, value.breakdown && value.breakdown.cacheRead)
|
|
770
|
+
target.cacheWrite = decimalAdd(target.cacheWrite, value.breakdown && value.breakdown.cacheWrite)
|
|
771
|
+
target.baseTotal = decimalAdd(target.baseTotal, value.baseTotal)
|
|
772
|
+
target.total = decimalAdd(target.total, value.total)
|
|
773
|
+
target.pricedCalls += 1
|
|
774
|
+
} else if (status === 'ambiguous') target.ambiguousCalls += 1
|
|
775
|
+
else if (status === 'unsupported') target.unsupportedCalls += 1
|
|
776
|
+
else target.unpricedCalls += 1
|
|
777
|
+
return target
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function serializeCostAggregate(cost) {
|
|
781
|
+
const value = cost || emptyCostAggregate()
|
|
782
|
+
const accumulator = isCostAccumulator(value)
|
|
783
|
+
const text = (field) => accumulator ? decimalAccumulatorText(value[field]) : decimalText(value[field]) || '0'
|
|
784
|
+
return {
|
|
785
|
+
currency: typeof value.currency === 'string' && value.currency !== '' ? value.currency : 'USD',
|
|
786
|
+
input: text('input'),
|
|
787
|
+
output: text('output'),
|
|
788
|
+
cacheRead: text('cacheRead'),
|
|
789
|
+
cacheWrite: text('cacheWrite'),
|
|
790
|
+
baseTotal: text('baseTotal'),
|
|
791
|
+
total: text('total'),
|
|
792
|
+
pricedCalls: Number.isFinite(value.pricedCalls) ? value.pricedCalls : 0,
|
|
793
|
+
unpricedCalls: Number.isFinite(value.unpricedCalls) ? value.unpricedCalls : 0,
|
|
794
|
+
ambiguousCalls: Number.isFinite(value.ambiguousCalls) ? value.ambiguousCalls : 0,
|
|
795
|
+
unsupportedCalls: Number.isFinite(value.unsupportedCalls) ? value.unsupportedCalls : 0,
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function normalizeCostSnapshot(raw) {
|
|
800
|
+
if (!isRecord(raw) || !COST_STATUSES.includes(raw.status)) return null
|
|
801
|
+
const rates = isRecord(raw.rates) ? raw.rates : {}
|
|
802
|
+
const breakdown = isRecord(raw.breakdown) ? raw.breakdown : {}
|
|
803
|
+
const fields = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
804
|
+
const normalizedRates = {}
|
|
805
|
+
const normalizedBreakdown = {}
|
|
806
|
+
for (const field of fields) {
|
|
807
|
+
const rate = decimalText(rates[field])
|
|
808
|
+
const cost = decimalText(breakdown[field])
|
|
809
|
+
if (rate === null || cost === null) return null
|
|
810
|
+
normalizedRates[field] = rate
|
|
811
|
+
normalizedBreakdown[field] = cost
|
|
812
|
+
}
|
|
813
|
+
const baseTotal = decimalText(raw.baseTotal)
|
|
814
|
+
const total = decimalText(raw.total)
|
|
815
|
+
const multiplier = decimalText(raw.multiplier)
|
|
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
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
schemaVersion: COST_SCHEMA_VERSION,
|
|
830
|
+
pricingMode: raw.pricingMode === 'official-model' ? 'official-model' : 'legacy-provider-aware',
|
|
831
|
+
status: raw.status,
|
|
832
|
+
currency: typeof raw.currency === 'string' ? raw.currency : 'USD',
|
|
833
|
+
source: typeof raw.source === 'string' ? raw.source : 'none',
|
|
834
|
+
pricingModel: typeof raw.pricingModel === 'string' ? raw.pricingModel : null,
|
|
835
|
+
providerId: typeof raw.providerId === 'string' ? raw.providerId : null,
|
|
836
|
+
inputTokenSemantics: INPUT_SEMANTICS.includes(raw.inputTokenSemantics) ? raw.inputTokenSemantics : 'fresh',
|
|
837
|
+
multiplier,
|
|
838
|
+
billableInputTokens: Number.isFinite(raw.billableInputTokens) ? Math.max(0, Math.trunc(raw.billableInputTokens)) : 0,
|
|
839
|
+
billableOutputTokens: Number.isFinite(raw.billableOutputTokens) ? Math.max(0, Math.trunc(raw.billableOutputTokens)) : 0,
|
|
840
|
+
...(contextTokens === null ? {} : { contextTokens }),
|
|
841
|
+
...(selectedTier === null ? {} : { selectedTier }),
|
|
842
|
+
rates: normalizedRates,
|
|
843
|
+
breakdown: normalizedBreakdown,
|
|
844
|
+
baseTotal,
|
|
845
|
+
total,
|
|
846
|
+
reason: typeof raw.reason === 'string' ? raw.reason.slice(0, 200) : '',
|
|
847
|
+
tiered: raw.tiered === true,
|
|
848
|
+
reasoningRateAvailable: raw.reasoningRateAvailable === true,
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {}) {
|
|
853
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
854
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
|
|
855
|
+
const url = typeof options.url === 'string' && options.url !== '' ? options.url : MODEL_CATALOG_URL
|
|
856
|
+
let controller = null
|
|
857
|
+
let timer = null
|
|
858
|
+
try {
|
|
859
|
+
if (typeof AbortController === 'function') {
|
|
860
|
+
controller = new AbortController()
|
|
861
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
862
|
+
}
|
|
863
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
864
|
+
if (!response || response.ok !== true) return { ok: false, error: 'http-' + String(response && response.status || 0) }
|
|
865
|
+
const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
|
|
866
|
+
if (Number.isFinite(length) && length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
|
|
867
|
+
const text = await response.text()
|
|
868
|
+
if (new TextEncoder().encode(text).length > MAX_CATALOG_BYTES) return { ok: false, error: 'catalog-too-large' }
|
|
869
|
+
let parsed
|
|
870
|
+
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { return { ok: false, error: 'catalog-invalid-json' } }
|
|
871
|
+
return parseModelsDevCatalog(parsed, Date.now())
|
|
872
|
+
} catch (err) {
|
|
873
|
+
return { ok: false, error: 'catalog-fetch-failed' }
|
|
874
|
+
} finally {
|
|
875
|
+
if (timer !== null) clearTimeout(timer)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export {
|
|
880
|
+
COST_SCHEMA_VERSION,
|
|
881
|
+
COST_STATUSES,
|
|
882
|
+
DEFAULT_SYNC_INTERVAL_MS,
|
|
883
|
+
INPUT_SEMANTICS,
|
|
884
|
+
MODEL_CATALOG_URL,
|
|
885
|
+
PRICING_SCHEMA_VERSION,
|
|
886
|
+
RATE_KEYS,
|
|
887
|
+
addCostAggregate,
|
|
888
|
+
addCostAccumulator,
|
|
889
|
+
calculateCost,
|
|
890
|
+
createCostAccumulator,
|
|
891
|
+
createEmptyPricingState,
|
|
892
|
+
decimalAdd,
|
|
893
|
+
decimalMultiply,
|
|
894
|
+
decimalSubtract,
|
|
895
|
+
decimalText,
|
|
896
|
+
emptyCostAggregate,
|
|
897
|
+
fetchModelsDevCatalog,
|
|
898
|
+
isCostAccumulator,
|
|
899
|
+
modelPricingCandidates,
|
|
900
|
+
officialProviderIds,
|
|
901
|
+
normalizeCostSnapshot,
|
|
902
|
+
normalizeModelId,
|
|
903
|
+
normalizePricingState,
|
|
904
|
+
parseModelsDevCatalog,
|
|
905
|
+
resolvePricing,
|
|
906
|
+
serializeCostAggregate,
|
|
907
|
+
serializePricingState,
|
|
908
|
+
}
|