@goodandready/dsh-model-sync 0.5.15 → 0.5.17
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/README.md +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +5 -0
- package/lib/adapter-registry.js +4 -2
- package/lib/client.js +214 -6
- package/lib/http.js +81 -1
- package/lib/models.js +206 -6
- package/lib/pricing-database.js +150 -17
- package/lib/provider-service.js +76 -3
- package/lib/reconcile.js +12 -2
- package/lib/scheduler.js +7 -0
- package/lib/specs-database.js +223 -0
- package/lib/synchronizer-prober.js +117 -35
- package/lib/synchronizer.js +25 -1
- package/package.json +1 -1
package/lib/models.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { lookupReferencePricing } from './pricing-database.js'
|
|
2
|
+
import { lookupReferenceLimits, lookupReferenceReasoning, lookupReferenceCompat } from './specs-database.js'
|
|
2
3
|
const FIELD_ALIASES = Object.freeze({
|
|
3
4
|
contextWindow: [
|
|
4
5
|
'contextWindow', 'context_window', 'contextLength', 'context_length',
|
|
5
6
|
'maxContextTokens', 'max_context_tokens', 'maxContextLength', 'max_context_length',
|
|
6
7
|
'inputTokenLimit', 'input_token_limit',
|
|
8
|
+
'top_provider.context_length', 'topProvider.contextLength',
|
|
7
9
|
],
|
|
8
10
|
maxTokens: [
|
|
9
11
|
'maxTokens', 'max_tokens', 'max_output_tokens', 'maxOutputTokens',
|
|
10
12
|
'outputTokenLimit', 'output_token_limit',
|
|
13
|
+
'max_completion_tokens', 'maxCompletionTokens',
|
|
14
|
+
'top_provider.max_completion_tokens', 'topProvider.maxCompletionTokens',
|
|
15
|
+
],
|
|
16
|
+
maxOutputTokens: [
|
|
17
|
+
'maxOutputTokens', 'max_output_tokens', 'maxTokens', 'max_tokens',
|
|
18
|
+
'outputTokenLimit', 'output_token_limit',
|
|
19
|
+
'max_completion_tokens', 'maxCompletionTokens',
|
|
20
|
+
'top_provider.max_completion_tokens', 'topProvider.maxCompletionTokens',
|
|
11
21
|
],
|
|
12
22
|
})
|
|
13
23
|
|
|
@@ -60,8 +70,14 @@ function numericValue(value, { allowZero = false } = {}) {
|
|
|
60
70
|
|
|
61
71
|
function firstNumber(row, fields, options) {
|
|
62
72
|
for (const field of fields) {
|
|
63
|
-
|
|
64
|
-
if (
|
|
73
|
+
let value
|
|
74
|
+
if (field.includes('.')) {
|
|
75
|
+
value = field.split('.').reduce((acc, k) => (acc && typeof acc === 'object') ? acc[k] : undefined, row)
|
|
76
|
+
} else {
|
|
77
|
+
value = row?.[field]
|
|
78
|
+
}
|
|
79
|
+
const val = numericValue(value, options)
|
|
80
|
+
if (val !== undefined) return val
|
|
65
81
|
}
|
|
66
82
|
return undefined
|
|
67
83
|
}
|
|
@@ -184,6 +200,13 @@ function normalizeCompat(provider, row, id) {
|
|
|
184
200
|
source.supportsDeveloperRole = defaultSupportsDeveloperRole
|
|
185
201
|
}
|
|
186
202
|
|
|
203
|
+
const refCompat = lookupReferenceCompat(provider, id)
|
|
204
|
+
if (refCompat) {
|
|
205
|
+
for (const [k, v] of Object.entries(refCompat)) {
|
|
206
|
+
if (source[k] === undefined) source[k] = v
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
return Object.keys(source).length > 0 ? source : undefined
|
|
188
211
|
}
|
|
189
212
|
|
|
@@ -298,6 +321,164 @@ function normalizeDate(val) {
|
|
|
298
321
|
return undefined
|
|
299
322
|
}
|
|
300
323
|
|
|
324
|
+
function normalizeReasoningConfig(provider, row, id, capabilities) {
|
|
325
|
+
const source = row?.reasoningConfig && typeof row.reasoningConfig === 'object' ? { ...row.reasoningConfig } : {}
|
|
326
|
+
const ref = lookupReferenceReasoning(provider, id)
|
|
327
|
+
if (ref) {
|
|
328
|
+
for (const [k, v] of Object.entries(ref)) {
|
|
329
|
+
if (source[k] === undefined) source[k] = v
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const isReasoning = capabilities?.reasoning === true ||
|
|
333
|
+
Boolean(source.supportsReasoning) ||
|
|
334
|
+
/(^|[_-])(o1|o3|r1|qwq|thinking)($|[_-])/i.test(id) ||
|
|
335
|
+
id.toLowerCase().includes('reasoner') ||
|
|
336
|
+
id.toLowerCase().includes('-r1') ||
|
|
337
|
+
id.toLowerCase().includes('thinking')
|
|
338
|
+
|
|
339
|
+
if (!isReasoning && Object.keys(source).length === 0) {
|
|
340
|
+
return undefined
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
source.supportsReasoning = true
|
|
344
|
+
if (!source.paramFormat) {
|
|
345
|
+
const idLower = id.toLowerCase()
|
|
346
|
+
const provLower = String(provider || '').toLowerCase()
|
|
347
|
+
if (provLower === 'openai' || /(^|[_-])(o1|o3)($|[_-])/i.test(idLower)) {
|
|
348
|
+
source.paramFormat = 'effort'
|
|
349
|
+
source.defaultEffort = source.defaultEffort || 'medium'
|
|
350
|
+
source.supportedEfforts = source.supportedEfforts || ['low', 'medium', 'high']
|
|
351
|
+
} else if (provLower === 'anthropic' || idLower.includes('claude-3-7') || idLower.includes('claude-3.7')) {
|
|
352
|
+
source.paramFormat = 'budget_tokens'
|
|
353
|
+
source.defaultBudget = source.defaultBudget || 4096
|
|
354
|
+
source.maxBudget = source.maxBudget || 64000
|
|
355
|
+
} else if (provLower === 'deepseek' && (idLower.includes('reasoner') || idLower.includes('r1'))) {
|
|
356
|
+
source.paramFormat = 'boolean'
|
|
357
|
+
} else if (idLower.includes('thinking') || idLower.includes('r1') || idLower.includes('qwq')) {
|
|
358
|
+
source.paramFormat = 'budget_tokens'
|
|
359
|
+
source.defaultBudget = source.defaultBudget || 4096
|
|
360
|
+
source.maxBudget = source.maxBudget || 32768
|
|
361
|
+
} else {
|
|
362
|
+
source.paramFormat = 'boolean'
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return source
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function deriveModelTags(model) {
|
|
369
|
+
if (!model || typeof model !== "object") return []
|
|
370
|
+
const tags = new Set(Array.isArray(model.tags) ? model.tags.filter((t) => typeof t === "string" && t.trim()).map((t) => t.trim().toLowerCase()) : [])
|
|
371
|
+
const idLower = String(model.id || "").toLowerCase()
|
|
372
|
+
const nameLower = String(model.name || "").toLowerCase()
|
|
373
|
+
const provLower = String(model.provider || "").toLowerCase()
|
|
374
|
+
const capabilities = model.capabilities || {}
|
|
375
|
+
|
|
376
|
+
// 1. coding
|
|
377
|
+
if (
|
|
378
|
+
capabilities.code === true ||
|
|
379
|
+
idLower.includes("coder") ||
|
|
380
|
+
idLower.includes("codestral") ||
|
|
381
|
+
idLower.includes("starcoder") ||
|
|
382
|
+
idLower.includes("deepseek-coder") ||
|
|
383
|
+
idLower.includes("claude-3-7-sonnet") ||
|
|
384
|
+
idLower.includes("claude-3-5-sonnet") ||
|
|
385
|
+
idLower.includes("kimi-k2.7-code") ||
|
|
386
|
+
idLower.includes("qwen-2.5-coder") ||
|
|
387
|
+
idLower.includes("qwen2.5-coder") ||
|
|
388
|
+
(/(^|[_-])(code|dev|prog)($|[_-])/i.test(idLower)) ||
|
|
389
|
+
(/(^|[_-])(code|coder)($|[_-])/i.test(nameLower))
|
|
390
|
+
) {
|
|
391
|
+
tags.add("coding")
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 2. reasoning
|
|
395
|
+
if (
|
|
396
|
+
model.reasoningConfig?.supportsReasoning === true ||
|
|
397
|
+
capabilities.reasoning === true ||
|
|
398
|
+
/(^|[_-])(o1|o3|r1|qwq)($|[_-])/i.test(idLower) ||
|
|
399
|
+
idLower.includes("reasoner") ||
|
|
400
|
+
idLower.includes("deepseek-r1") ||
|
|
401
|
+
idLower.includes("thinking") ||
|
|
402
|
+
idLower.includes("-r1")
|
|
403
|
+
) {
|
|
404
|
+
tags.add("reasoning")
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 3. fast
|
|
408
|
+
if (
|
|
409
|
+
provLower === "groq" ||
|
|
410
|
+
provLower === "cerebras" ||
|
|
411
|
+
idLower.includes("flash") ||
|
|
412
|
+
idLower.includes("mini") ||
|
|
413
|
+
idLower.includes("instant") ||
|
|
414
|
+
idLower.includes("haiku") ||
|
|
415
|
+
idLower.includes("small") ||
|
|
416
|
+
idLower.includes("8b") ||
|
|
417
|
+
idLower.includes("3b") ||
|
|
418
|
+
idLower.includes("1b") ||
|
|
419
|
+
idLower.includes("turbo") ||
|
|
420
|
+
idLower.includes("lite") ||
|
|
421
|
+
idLower.includes("nano")
|
|
422
|
+
) {
|
|
423
|
+
tags.add("fast")
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 4. vision
|
|
427
|
+
if (
|
|
428
|
+
capabilities.vision === true ||
|
|
429
|
+
(Array.isArray(model.input) && model.input.includes("image")) ||
|
|
430
|
+
idLower.includes("vision") ||
|
|
431
|
+
idLower.includes("vl") ||
|
|
432
|
+
idLower.includes("pixtral") ||
|
|
433
|
+
idLower.includes("gemini") ||
|
|
434
|
+
idLower.includes("claude-3") ||
|
|
435
|
+
idLower.includes("4o")
|
|
436
|
+
) {
|
|
437
|
+
tags.add("vision")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 5. long-context (>= 128k tokens)
|
|
441
|
+
if (typeof model.contextWindow === "number" && model.contextWindow >= 128000) {
|
|
442
|
+
tags.add("long-context")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return [...tags].sort()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function isCuratedModel(model) {
|
|
449
|
+
if (!model || typeof model !== "object") return false
|
|
450
|
+
if (model.lifecycle?.deprecated) return false
|
|
451
|
+
const id = String(model.id || "").toLowerCase()
|
|
452
|
+
if (/-(202\d{5}|202\d-\d{2}-\d{2}|\d{4})$/.test(id) && !id.includes("claude-3-7") && !id.includes("claude-3-5")) {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
if (id.includes("-0314") || id.includes("-0613") || id.includes("-1106") || id.includes("-0125")) {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const curatedIds = [
|
|
460
|
+
"claude-3-7-sonnet", "claude-3-5-sonnet", "claude-3-5-haiku",
|
|
461
|
+
"gpt-4o", "gpt-4o-mini", "o1", "o3-mini",
|
|
462
|
+
"deepseek-chat", "deepseek-reasoner", "deepseek-v3", "deepseek-r1",
|
|
463
|
+
"gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash",
|
|
464
|
+
"llama-3.3-70b-versatile", "llama-3.1-8b-instant", "llama-3.1-70b-versatile",
|
|
465
|
+
"qwen-2.5-coder-32b", "qwen-2.5-72b", "qwq-32b",
|
|
466
|
+
"mistral-large", "codestral", "pixtral-large",
|
|
467
|
+
"kimi-k2.7-code"
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
for (const c of curatedIds) {
|
|
471
|
+
if (id === c || id.endsWith("/" + c) || id.includes(c)) return true
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const tags = model.tags || deriveModelTags(model)
|
|
475
|
+
if (tags.includes("coding") && tags.includes("reasoning") && tags.includes("long-context")) {
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
|
|
301
482
|
export function normalizeModel(provider, row) {
|
|
302
483
|
if (typeof row === 'string') {
|
|
303
484
|
row = { id: row }
|
|
@@ -306,11 +487,15 @@ export function normalizeModel(provider, row) {
|
|
|
306
487
|
const effectiveProvider = (typeof provider === 'string' && provider.trim())
|
|
307
488
|
? provider.trim()
|
|
308
489
|
: (typeof row.provider === 'string' ? row.provider.trim() : '')
|
|
309
|
-
const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
|
|
490
|
+
const id = String(row.id ?? row.slug ?? row.model ?? (typeof row.name === 'string' && row.name.startsWith('models/') ? row.name.replace(/^models\//, '') : '')).trim()
|
|
310
491
|
if (!id) return null
|
|
311
492
|
const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
|
|
312
|
-
const
|
|
313
|
-
const
|
|
493
|
+
const rawContextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
|
|
494
|
+
const rawMaxOutputTokens = firstNumber(row, FIELD_ALIASES.maxOutputTokens)
|
|
495
|
+
const refLimits = lookupReferenceLimits(effectiveProvider || provider, id)
|
|
496
|
+
const contextWindow = rawContextWindow ?? refLimits?.contextWindow
|
|
497
|
+
const maxOutputTokens = rawMaxOutputTokens ?? refLimits?.maxOutputTokens
|
|
498
|
+
const maxTokens = maxOutputTokens ?? firstNumber(row, FIELD_ALIASES.maxTokens)
|
|
314
499
|
const capabilities = normalizeCapabilities(row)
|
|
315
500
|
const pricing = normalizePricing(row) ?? lookupReferencePricing(effectiveProvider || provider, id)
|
|
316
501
|
const tags = normalizeTags(row)
|
|
@@ -318,6 +503,7 @@ export function normalizeModel(provider, row) {
|
|
|
318
503
|
const lifecycle = normalizeLifecycle(row)
|
|
319
504
|
const input = normalizeInput(row)
|
|
320
505
|
const compat = normalizeCompat(effectiveProvider, row, id)
|
|
506
|
+
const reasoningConfig = normalizeReasoningConfig(effectiveProvider || provider, row, id, capabilities)
|
|
321
507
|
const firstSeenAt = normalizeDate(row.firstSeenAt)
|
|
322
508
|
const createdAt = normalizeDate(row.createdAt ?? row.created)
|
|
323
509
|
return {
|
|
@@ -327,11 +513,13 @@ export function normalizeModel(provider, row) {
|
|
|
327
513
|
...(firstSeenAt === undefined ? {} : { firstSeenAt }),
|
|
328
514
|
...(createdAt === undefined ? {} : { createdAt }),
|
|
329
515
|
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
516
|
+
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
330
517
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
331
518
|
...(row.description ? { description: String(row.description) } : {}),
|
|
332
519
|
...(input === undefined ? {} : { input }),
|
|
333
520
|
...(compat === undefined ? {} : { compat }),
|
|
334
521
|
...(capabilities === undefined ? {} : { capabilities }),
|
|
522
|
+
...(reasoningConfig === undefined ? {} : { reasoningConfig }),
|
|
335
523
|
...(pricing === undefined ? {} : { pricing }),
|
|
336
524
|
...(tags === undefined ? {} : { tags }),
|
|
337
525
|
...(aliases.length === 0 ? {} : { aliases }),
|
|
@@ -410,6 +598,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
410
598
|
id: 'cline-pass/deepseek-v4-flash',
|
|
411
599
|
name: 'DeepSeek V4 Flash',
|
|
412
600
|
contextWindow: 200000,
|
|
601
|
+
maxOutputTokens: 8192,
|
|
413
602
|
maxTokens: 8192,
|
|
414
603
|
capabilities: { vision: true, code: true, reasoning: true },
|
|
415
604
|
input: ['text', 'image'],
|
|
@@ -418,6 +607,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
418
607
|
id: 'cline-pass/deepseek-v4-pro',
|
|
419
608
|
name: 'DeepSeek V4 Pro',
|
|
420
609
|
contextWindow: 200000,
|
|
610
|
+
maxOutputTokens: 8192,
|
|
421
611
|
maxTokens: 8192,
|
|
422
612
|
capabilities: { vision: true, code: true, reasoning: true },
|
|
423
613
|
input: ['text', 'image'],
|
|
@@ -426,6 +616,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
426
616
|
id: 'cline-pass/glm-5.2',
|
|
427
617
|
name: 'GLM 5.2',
|
|
428
618
|
contextWindow: 200000,
|
|
619
|
+
maxOutputTokens: 8192,
|
|
429
620
|
maxTokens: 8192,
|
|
430
621
|
capabilities: { code: true },
|
|
431
622
|
input: ['text'],
|
|
@@ -434,6 +625,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
434
625
|
id: 'cline-pass/kimi-k3',
|
|
435
626
|
name: 'Kimi K3',
|
|
436
627
|
contextWindow: 200000,
|
|
628
|
+
maxOutputTokens: 8192,
|
|
437
629
|
maxTokens: 8192,
|
|
438
630
|
capabilities: { reasoning: true },
|
|
439
631
|
input: ['text'],
|
|
@@ -442,6 +634,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
442
634
|
id: 'cline-pass/kimi-k2.7-code',
|
|
443
635
|
name: 'Kimi K2.7 Code',
|
|
444
636
|
contextWindow: 200000,
|
|
637
|
+
maxOutputTokens: 8192,
|
|
445
638
|
maxTokens: 8192,
|
|
446
639
|
capabilities: { code: true, tools: true },
|
|
447
640
|
input: ['text'],
|
|
@@ -450,6 +643,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
450
643
|
id: 'cline-pass/kimi-k2.6',
|
|
451
644
|
name: 'Kimi K2.6',
|
|
452
645
|
contextWindow: 200000,
|
|
646
|
+
maxOutputTokens: 8192,
|
|
453
647
|
maxTokens: 8192,
|
|
454
648
|
capabilities: { code: true },
|
|
455
649
|
input: ['text'],
|
|
@@ -458,6 +652,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
458
652
|
id: 'cline-pass/qwen3.7-max',
|
|
459
653
|
name: 'Qwen 3.7 Max',
|
|
460
654
|
contextWindow: 200000,
|
|
655
|
+
maxOutputTokens: 8192,
|
|
461
656
|
maxTokens: 8192,
|
|
462
657
|
capabilities: { vision: true },
|
|
463
658
|
input: ['text', 'image'],
|
|
@@ -466,6 +661,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
466
661
|
id: 'cline-pass/qwen3.7-plus',
|
|
467
662
|
name: 'Qwen 3.7 Plus',
|
|
468
663
|
contextWindow: 200000,
|
|
664
|
+
maxOutputTokens: 8192,
|
|
469
665
|
maxTokens: 8192,
|
|
470
666
|
capabilities: { vision: true },
|
|
471
667
|
input: ['text', 'image'],
|
|
@@ -474,6 +670,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
474
670
|
id: 'cline-pass/minimax-m3',
|
|
475
671
|
name: 'MiniMax M3',
|
|
476
672
|
contextWindow: 200000,
|
|
673
|
+
maxOutputTokens: 8192,
|
|
477
674
|
maxTokens: 8192,
|
|
478
675
|
capabilities: { reasoning: true },
|
|
479
676
|
input: ['text'],
|
|
@@ -482,6 +679,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
482
679
|
id: 'cline-pass/mimo-v2.5',
|
|
483
680
|
name: 'MiMo V2.5',
|
|
484
681
|
contextWindow: 200000,
|
|
682
|
+
maxOutputTokens: 8192,
|
|
485
683
|
maxTokens: 8192,
|
|
486
684
|
capabilities: { code: true },
|
|
487
685
|
input: ['text'],
|
|
@@ -490,6 +688,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
490
688
|
id: 'cline-pass/mimo-v2.5-pro',
|
|
491
689
|
name: 'MiMo V2.5 Pro',
|
|
492
690
|
contextWindow: 200000,
|
|
691
|
+
maxOutputTokens: 8192,
|
|
493
692
|
maxTokens: 8192,
|
|
494
693
|
capabilities: { code: true, reasoning: true },
|
|
495
694
|
input: ['text'],
|
|
@@ -546,7 +745,8 @@ export function parseClinePassModels(includedInput) {
|
|
|
546
745
|
id: `cline-pass/${idPart}`,
|
|
547
746
|
name,
|
|
548
747
|
contextWindow: 200000,
|
|
549
|
-
|
|
748
|
+
maxOutputTokens: 8192,
|
|
749
|
+
maxTokens: 8192,
|
|
550
750
|
capabilities: {},
|
|
551
751
|
input: ['text'],
|
|
552
752
|
})
|
package/lib/pricing-database.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
function cleanTokenPrice(val, { isPerToken = false } = {}) {
|
|
5
|
+
const num = typeof val === 'string' ? Number(val.trim()) : val
|
|
6
|
+
if (typeof num !== 'number' || !Number.isFinite(num) || num < 0) return 0
|
|
7
|
+
if (isPerToken) {
|
|
8
|
+
return Math.round(num * 1e12) / 1e12
|
|
9
|
+
}
|
|
10
|
+
return Math.round((num / 1000000) * 1e12) / 1e12
|
|
4
11
|
}
|
|
5
12
|
|
|
6
13
|
function perMillion(inputUSD, outputUSD, extra = {}) {
|
|
@@ -21,23 +28,23 @@ function perMillion(inputUSD, outputUSD, extra = {}) {
|
|
|
21
28
|
|
|
22
29
|
export const REFERENCE_PRICING = Object.freeze({
|
|
23
30
|
openai: {
|
|
24
|
-
'gpt-4o': perMillion(2.5, 10),
|
|
25
|
-
'gpt-4o-mini': perMillion(0.15, 0.6),
|
|
31
|
+
'gpt-4o': perMillion(2.5, 10, { cacheReadUSD: 1.25 }),
|
|
32
|
+
'gpt-4o-mini': perMillion(0.15, 0.6, { cacheReadUSD: 0.075 }),
|
|
26
33
|
'gpt-4-turbo': perMillion(10, 30),
|
|
27
34
|
'gpt-4': perMillion(30, 60),
|
|
28
35
|
'gpt-3.5-turbo': perMillion(0.5, 1.5),
|
|
29
|
-
'o1': perMillion(15, 60),
|
|
36
|
+
'o1': perMillion(15, 60, { cacheReadUSD: 7.5 }),
|
|
30
37
|
'o1-preview': perMillion(15, 60),
|
|
31
|
-
'o1-mini': perMillion(1.1, 4.4),
|
|
32
|
-
'o3-mini': perMillion(1.1, 4.4),
|
|
38
|
+
'o1-mini': perMillion(1.1, 4.4, { cacheReadUSD: 0.55 }),
|
|
39
|
+
'o3-mini': perMillion(1.1, 4.4, { cacheReadUSD: 0.55 }),
|
|
33
40
|
'chatgpt-4o-latest': perMillion(5, 15),
|
|
34
41
|
},
|
|
35
42
|
anthropic: {
|
|
36
|
-
'claude-3-7-sonnet': perMillion(3, 15),
|
|
37
|
-
'claude-3-5-sonnet': perMillion(3, 15),
|
|
38
|
-
'claude-3-5-haiku': perMillion(0.8, 4),
|
|
39
|
-
'claude-3-opus': perMillion(15, 75),
|
|
40
|
-
'claude-3-haiku': perMillion(0.25, 1.25),
|
|
43
|
+
'claude-3-7-sonnet': perMillion(3, 15, { cacheReadUSD: 0.3, cacheWriteUSD: 3.75 }),
|
|
44
|
+
'claude-3-5-sonnet': perMillion(3, 15, { cacheReadUSD: 0.3, cacheWriteUSD: 3.75 }),
|
|
45
|
+
'claude-3-5-haiku': perMillion(0.8, 4, { cacheReadUSD: 0.08, cacheWriteUSD: 1 }),
|
|
46
|
+
'claude-3-opus': perMillion(15, 75, { cacheReadUSD: 1.5, cacheWriteUSD: 18.75 }),
|
|
47
|
+
'claude-3-haiku': perMillion(0.25, 1.25, { cacheReadUSD: 0.025, cacheWriteUSD: 0.3 }),
|
|
41
48
|
},
|
|
42
49
|
groq: {
|
|
43
50
|
'llama-3.3-70b-versatile': perMillion(0.59, 0.79),
|
|
@@ -83,6 +90,22 @@ export const REFERENCE_PRICING = Object.freeze({
|
|
|
83
90
|
},
|
|
84
91
|
})
|
|
85
92
|
|
|
93
|
+
// Dynamic in-memory pricing store populated from OpenRouter / LiteLLM / disk cache
|
|
94
|
+
const DYNAMIC_PRICING = new Map()
|
|
95
|
+
|
|
96
|
+
export function getDynamicPricingMap() {
|
|
97
|
+
return DYNAMIC_PRICING
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function clearDynamicPricing() {
|
|
101
|
+
DYNAMIC_PRICING.clear()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setDynamicPricing(key, pricing) {
|
|
105
|
+
if (!key || typeof key !== 'string' || !pricing) return
|
|
106
|
+
DYNAMIC_PRICING.set(key.toLowerCase().trim(), Object.freeze({ ...pricing }))
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
function cleanId(id) {
|
|
87
110
|
if (typeof id !== 'string') return ''
|
|
88
111
|
return id
|
|
@@ -100,16 +123,23 @@ export function lookupReferencePricing(provider, modelId) {
|
|
|
100
123
|
const rawId = modelId.toLowerCase().trim()
|
|
101
124
|
const cleaned = cleanId(rawId)
|
|
102
125
|
|
|
103
|
-
// 1.
|
|
126
|
+
// 1. Dynamic pricing cache check (provider:modelId or rawId or cleaned)
|
|
127
|
+
if (DYNAMIC_PRICING.size > 0) {
|
|
128
|
+
const dynScoped = provKey ? DYNAMIC_PRICING.get(`${provKey}:${rawId}`) || DYNAMIC_PRICING.get(`${provKey}:${cleaned}`) : undefined
|
|
129
|
+
if (dynScoped) return { ...dynScoped }
|
|
130
|
+
const dynRaw = DYNAMIC_PRICING.get(rawId) || DYNAMIC_PRICING.get(cleaned)
|
|
131
|
+
if (dynRaw) return { ...dynRaw }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2. Direct check in provider table (exact match or date-cleaned match)
|
|
104
135
|
const table = REFERENCE_PRICING[provKey]
|
|
105
136
|
if (table) {
|
|
106
137
|
if (table[rawId]) return { ...table[rawId] }
|
|
107
138
|
if (table[cleaned]) return { ...table[cleaned] }
|
|
108
|
-
// If the provider has a dedicated pricing table, do not search other providers' tables
|
|
109
139
|
return undefined
|
|
110
140
|
}
|
|
111
141
|
|
|
112
|
-
//
|
|
142
|
+
// 3. Vendor prefix check (e.g. "openai/gpt-4o" or "anthropic/claude-3-5-sonnet")
|
|
113
143
|
const slashIdx = rawId.indexOf('/')
|
|
114
144
|
if (slashIdx > 0) {
|
|
115
145
|
const subProv = rawId.slice(0, slashIdx)
|
|
@@ -122,10 +152,113 @@ export function lookupReferencePricing(provider, modelId) {
|
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
//
|
|
155
|
+
// 4. Fallback search across tables ONLY for unknown/aggregator providers without a dedicated table
|
|
126
156
|
for (const [p, t] of Object.entries(REFERENCE_PRICING)) {
|
|
127
157
|
if (t[cleaned]) return { ...t[cleaned] }
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
return undefined
|
|
131
161
|
}
|
|
162
|
+
|
|
163
|
+
export function parseOpenRouterPricing(modelsData) {
|
|
164
|
+
if (!Array.isArray(modelsData)) return []
|
|
165
|
+
const results = []
|
|
166
|
+
for (const item of modelsData) {
|
|
167
|
+
if (!item?.id || !item?.pricing) continue
|
|
168
|
+
const id = String(item.id).trim().toLowerCase()
|
|
169
|
+
const p = item.pricing
|
|
170
|
+
const inputPerToken = cleanTokenPrice(p.prompt, { isPerToken: true })
|
|
171
|
+
const outputPerToken = cleanTokenPrice(p.completion, { isPerToken: true })
|
|
172
|
+
const cacheReadPerToken = p.input_cache_read !== undefined || p.prompt_cached !== undefined
|
|
173
|
+
? cleanTokenPrice(p.input_cache_read ?? p.prompt_cached, { isPerToken: true })
|
|
174
|
+
: undefined
|
|
175
|
+
const cacheWritePerToken = p.input_cache_write !== undefined
|
|
176
|
+
? cleanTokenPrice(p.input_cache_write, { isPerToken: true })
|
|
177
|
+
: undefined
|
|
178
|
+
|
|
179
|
+
if (inputPerToken > 0 || outputPerToken > 0) {
|
|
180
|
+
const entry = {
|
|
181
|
+
inputPerToken,
|
|
182
|
+
outputPerToken,
|
|
183
|
+
...(cacheReadPerToken !== undefined ? { cacheReadPerToken } : {}),
|
|
184
|
+
...(cacheWritePerToken !== undefined ? { cacheWritePerToken } : {}),
|
|
185
|
+
currency: 'USD',
|
|
186
|
+
source: 'openrouter',
|
|
187
|
+
}
|
|
188
|
+
results.push({ id, entry })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return results
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function fetchPricingUpdates({
|
|
195
|
+
timeoutMs = 15000,
|
|
196
|
+
fetchFn = globalThis.fetch,
|
|
197
|
+
cachePath,
|
|
198
|
+
logger,
|
|
199
|
+
} = {}) {
|
|
200
|
+
let updatedCount = 0
|
|
201
|
+
const controller = new AbortController()
|
|
202
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetchFn('https://openrouter.ai/api/v1/models', {
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
headers: { 'Accept': 'application/json' },
|
|
208
|
+
})
|
|
209
|
+
if (res.ok) {
|
|
210
|
+
const json = await res.json()
|
|
211
|
+
const entries = parseOpenRouterPricing(json?.data)
|
|
212
|
+
for (const { id, entry } of entries) {
|
|
213
|
+
DYNAMIC_PRICING.set(id, Object.freeze(entry))
|
|
214
|
+
const clean = cleanId(id)
|
|
215
|
+
if (clean && clean !== id) {
|
|
216
|
+
DYNAMIC_PRICING.set(clean, Object.freeze(entry))
|
|
217
|
+
}
|
|
218
|
+
// Also index under vendor-prefixed subId
|
|
219
|
+
const slashIdx = id.indexOf('/')
|
|
220
|
+
if (slashIdx > 0) {
|
|
221
|
+
const subId = id.slice(slashIdx + 1)
|
|
222
|
+
DYNAMIC_PRICING.set(subId, Object.freeze(entry))
|
|
223
|
+
const subClean = cleanId(subId)
|
|
224
|
+
if (subClean && subClean !== subId) {
|
|
225
|
+
DYNAMIC_PRICING.set(subClean, Object.freeze(entry))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
updatedCount++
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (cachePath && updatedCount > 0) {
|
|
232
|
+
try {
|
|
233
|
+
const serialized = JSON.stringify(Object.fromEntries(DYNAMIC_PRICING), null, 2)
|
|
234
|
+
fs.writeFileSync(cachePath, serialized, 'utf-8')
|
|
235
|
+
} catch (writeErr) {
|
|
236
|
+
logger?.warn?.(`[dsh-model-sync] failed to write pricing cache: ${writeErr?.message}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { updated: updatedCount, source: 'openrouter', timestamp: Date.now() }
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
logger?.warn?.(`[dsh-model-sync] live pricing fetch failed: ${err?.message}, falling back to cache`)
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timer)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback: try loading cached pricing from disk
|
|
249
|
+
if (cachePath && fs.existsSync(cachePath)) {
|
|
250
|
+
try {
|
|
251
|
+
const data = fs.readFileSync(cachePath, 'utf-8')
|
|
252
|
+
const parsed = JSON.parse(data)
|
|
253
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
254
|
+
DYNAMIC_PRICING.set(k.toLowerCase().trim(), Object.freeze(v))
|
|
255
|
+
updatedCount++
|
|
256
|
+
}
|
|
257
|
+
return { updated: updatedCount, source: 'cache', timestamp: Date.now() }
|
|
258
|
+
} catch (readErr) {
|
|
259
|
+
logger?.warn?.(`[dsh-model-sync] failed to read pricing cache: ${readErr?.message}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { updated: 0, source: 'reference', timestamp: Date.now() }
|
|
264
|
+
}
|
package/lib/provider-service.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { listApiKeyProviders } from './inventory.js'
|
|
2
|
+
import { lookupReferenceLimits, lookupReferenceReasoning, clampOutputTokens, adaptReasoningPayload } from './specs-database.js'
|
|
3
|
+
import { deriveModelTags, isCuratedModel } from './models.js'
|
|
4
|
+
import { lookupReferencePricing } from './pricing-database.js'
|
|
2
5
|
|
|
3
6
|
export function createProviderService(ctx) {
|
|
4
7
|
const readSettings = () => {
|
|
@@ -20,10 +23,80 @@ export function createProviderService(ctx) {
|
|
|
20
23
|
|
|
21
24
|
const listProviders = () => listApiKeyProviders(readLlm(), readSettings())
|
|
22
25
|
|
|
26
|
+
const getProvider = (provider) => listProviders().find((row) => row.provider === provider) ?? null
|
|
27
|
+
|
|
28
|
+
const getLimits = (provider, modelId) => {
|
|
29
|
+
const p = getProvider(provider)
|
|
30
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
31
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
32
|
+
if (found && (found.contextWindow !== undefined || found.maxOutputTokens !== undefined || found.maxTokens !== undefined)) {
|
|
33
|
+
return {
|
|
34
|
+
contextWindow: found.contextWindow,
|
|
35
|
+
maxOutputTokens: found.maxOutputTokens ?? found.maxTokens,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lookupReferenceLimits(provider, modelId) ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const clampCompletionTokens = (provider, modelId, requestedTokens) => {
|
|
42
|
+
const limits = getLimits(provider, modelId)
|
|
43
|
+
return clampOutputTokens(limits?.maxOutputTokens, requestedTokens)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getReasoningConfig = (provider, modelId) => {
|
|
47
|
+
const p = getProvider(provider)
|
|
48
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
49
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
50
|
+
if (found?.reasoningConfig) {
|
|
51
|
+
return found.reasoningConfig
|
|
52
|
+
}
|
|
53
|
+
return lookupReferenceReasoning(provider, modelId) ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const adaptReasoning = (provider, modelId, options = {}) => {
|
|
57
|
+
const p = getProvider(provider)
|
|
58
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
59
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId)) || {
|
|
60
|
+
provider,
|
|
61
|
+
id: modelId,
|
|
62
|
+
reasoningConfig: getReasoningConfig(provider, modelId),
|
|
63
|
+
}
|
|
64
|
+
return adaptReasoningPayload(found, options)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const getPricing = (provider, modelId) => {
|
|
68
|
+
const p = getProvider(provider)
|
|
69
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
70
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
71
|
+
if (found?.pricing) return found.pricing
|
|
72
|
+
return lookupReferencePricing(provider, modelId) ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const getTags = (provider, modelId) => {
|
|
76
|
+
const p = getProvider(provider)
|
|
77
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
78
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
79
|
+
if (found?.tags && found.tags.length > 0) return found.tags
|
|
80
|
+
if (found) return deriveModelTags(found)
|
|
81
|
+
return deriveModelTags({ provider, id: modelId })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isCurated = (provider, modelId) => {
|
|
85
|
+
const p = getProvider(provider)
|
|
86
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
87
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
88
|
+
return isCuratedModel(found || { provider, id: modelId })
|
|
89
|
+
}
|
|
90
|
+
|
|
23
91
|
return Object.freeze({
|
|
24
92
|
listProviders,
|
|
25
|
-
getProvider
|
|
26
|
-
|
|
27
|
-
|
|
93
|
+
getProvider,
|
|
94
|
+
getLimits,
|
|
95
|
+
clampCompletionTokens,
|
|
96
|
+
getReasoningConfig,
|
|
97
|
+
adaptReasoningPayload: adaptReasoning,
|
|
98
|
+
getPricing,
|
|
99
|
+
getTags,
|
|
100
|
+
isCurated,
|
|
28
101
|
})
|
|
29
102
|
}
|
package/lib/reconcile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { diffModels, normalizeModels, stableStringify } from './models.js'
|
|
1
|
+
import { diffModels, normalizeModels, stableStringify, deriveModelTags } from './models.js'
|
|
2
2
|
|
|
3
3
|
function sameModel(left, right) {
|
|
4
4
|
return stableStringify(left) === stableStringify(right)
|
|
@@ -30,8 +30,13 @@ export function reconcileModels(provider, current, discovered, {
|
|
|
30
30
|
...(row.compat || fresh.compat ? {
|
|
31
31
|
compat: { ...(fresh.compat ?? {}), ...(row.compat ?? {}) },
|
|
32
32
|
} : {}),
|
|
33
|
+
...(row.reasoningConfig || fresh.reasoningConfig ? {
|
|
34
|
+
reasoningConfig: { ...(fresh.reasoningConfig ?? {}), ...(row.reasoningConfig ?? {}) },
|
|
35
|
+
} : {}),
|
|
33
36
|
...(fresh.input?.length ? { input: fresh.input } : row.input ? { input: row.input } : {}),
|
|
34
37
|
}
|
|
38
|
+
const derivedTags = deriveModelTags(merged)
|
|
39
|
+
if (derivedTags.length > 0) merged.tags = derivedTags
|
|
35
40
|
next.push(merged)
|
|
36
41
|
} else if (renamedBefore.has(row.id)) {
|
|
37
42
|
// An explicit provider alias is safe evidence that the old id was renamed.
|
|
@@ -42,7 +47,12 @@ export function reconcileModels(provider, current, discovered, {
|
|
|
42
47
|
}
|
|
43
48
|
const known = new Set(before.map((row) => row.id))
|
|
44
49
|
for (const row of advertised) {
|
|
45
|
-
if (!known.has(row.id))
|
|
50
|
+
if (!known.has(row.id)) {
|
|
51
|
+
const copy = { ...row }
|
|
52
|
+
const derivedTags = deriveModelTags(copy)
|
|
53
|
+
if (derivedTags.length > 0) copy.tags = derivedTags
|
|
54
|
+
next.push(copy)
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
const diff = diffModels(before, next)
|
|
48
58
|
return {
|