@goodandready/dsh-model-sync 0.5.14 → 0.5.16
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 +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/lib/adapter-registry.js +4 -2
- package/lib/client.js +44 -0
- package/lib/diff-notifier.js +15 -2
- package/lib/http.js +14 -2
- package/lib/models.js +92 -6
- package/lib/provider-service.js +47 -3
- package/lib/reconcile.js +3 -0
- package/lib/specs-database.js +223 -0
- package/lib/synchronizer-helpers.js +12 -0
- package/lib/updater.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ graph LR
|
|
|
80
80
|
|
|
81
81
|
* 💰 **Built-in Reference Pricing Database**: Canonical token pricing database automatically enriches models for OpenAI, Anthropic, Groq, Mistral, xAI, and DeepSeek, enabling cost-limit filtering and `@cheap` auto-resolution even when upstream APIs omit pricing metadata.
|
|
82
82
|
* 🎯 **Dynamic Smart Virtual Aliases & Auto-Fallback**: Route requests to virtual aliases like `@fast`, `@cheap`, and `@best-code`, with automatic fallback chains when circuit breakers open or health probes fail.
|
|
83
|
+
* 📏 **Real Context Limits & Boundary Safety (`contextWindow` / `maxOutputTokens`)**: precise separation of input and generation limits, prevention of 400 `max_tokens` errors via auto-clamping, and clear `In: 128k | Out: 8k` badges in Web UI.
|
|
84
|
+
* 🧠 **Unified Reasoning & Thinking Configuration**: automatic format detection (`effort`, `budget_tokens`, `boolean`) and parameter translation with suppression of incompatible options (such as `temperature` on o1/o3).
|
|
83
85
|
* 🔔 **External Diff Webhooks & Event Dispatching**: Real-time webhook notifications with HMAC-SHA256 signature verification and Cordis `model-sync/diff` events when models are added, deprecated, or prices change.
|
|
84
86
|
* ⚡ **High-Performance Policy Filtering**: Fast-path evaluation in `filterModels` bypasses regular expressions and array filtering when no filtering policy is configured, guaranteeing zero-overhead discovery runs.
|
|
85
87
|
* 🔄 **One-Click In-App Updates**: Built-in update checker in Settings with automatic npm cache bypass on demand (`?fresh=1` / `Cache-Control: no-cache`), ensuring instantaneous discovery of new package releases.
|
package/README.ru.md
CHANGED
|
@@ -80,6 +80,8 @@ graph LR
|
|
|
80
80
|
|
|
81
81
|
* 💰 **Встроенная база эталонных цен токенов**: Автоматическое обогащение моделей тарифами для OpenAI, Anthropic, Groq, Mistral, xAI и DeepSeek, обеспечивающее работу лимитов цен и псевдонима `@cheap` даже при отсутствии метаданных в API провайдера.
|
|
82
82
|
* 🎯 **Динамические «умные» псевдонимы и авто-фоллбэк**: Поддержка виртуальных алиасов (`@fast`, `@cheap`, `@best-code`) и цепочек резервирования с автопереключением при размыкании Circuit Breaker.
|
|
83
|
+
* 📏 **Реальные лимиты контекста и авто-клампинг (`contextWindow` / `maxOutputTokens`)**: точное разделение входного и выходного лимита токенов, защита от ошибок 400 `max_tokens` и индикация `Вход: 128k | Выход: 8k` в Web UI.
|
|
84
|
+
* 🧠 **Унификация параметров Reasoning & Thinking**: автоматическое определение формата (`effort`, `budget_tokens`, `boolean`) и параметров бюджета рассуждений с подавлением несовместимых параметров (например, `temperature` для o1/o3).
|
|
83
85
|
* 🔔 **Внешние Diff Webhooks и диспетчеризация событий**: Уведомления в реальном времени с проверкой подписи HMAC-SHA256 и событиями Cordis `model-sync/diff` при добавлении моделей, их устаревании или изменении цен.
|
|
84
86
|
* ⚡ **Высокопроизводительная фильтрация моделей**: Быстрый путь (`fast-path`) в `filterModels` мгновенно возвращает каталог при отсутствии активных правил политики, исключая лишнюю компиляцию regex и прогоны массивов.
|
|
85
87
|
* 🔄 **Обновление плагина в один клик**: Встроенный механизм проверки обновлений в Настройках со сбросом кэша npm при ручном запросе (`?fresh=1` / `Cache-Control: no-cache`), гарантирующий получение актуальной версии.
|
package/README.zh.md
CHANGED
|
@@ -80,6 +80,8 @@ graph LR
|
|
|
80
80
|
|
|
81
81
|
* 💰 **内置基准价格数据库**:自动为 OpenAI、Anthropic、Groq、Mistral、xAI 和 DeepSeek 模型补充 Token 价格数据,即使服务商接口未返回价格,也可无缝支持成本上限过滤与 `@cheap` 别名解析。
|
|
82
82
|
* 🎯 **动态智能虚拟别名与故障转移**:支持 `@fast`、`@cheap`、`@best-code` 等虚拟别名及故障转移链,在熔断器打开或健康检查失败时自动切换至备用模型。
|
|
83
|
+
* 📏 **真实上下文限制与边界安全 (`contextWindow` / `maxOutputTokens`)**:精确区分输入与输出令牌限制,通过自动限制防止 400 `max_tokens` 错误,并在 Web UI 中显示 `输入: 128k | 输出: 8k` 徽章。
|
|
84
|
+
* 🧠 **统一的深度思考与推理配置 (Reasoning & Thinking)**:自动检测参数格式 (`effort`, `budget_tokens`, `boolean`) 并转换推理预算,自动屏蔽不兼容参数(如 o1/o3 的 `temperature`)。
|
|
83
85
|
* 🔔 **外部差异通知 Webhook 与事件分发**:支持 HMAC-SHA256 签名验证的实时 Webhook 通知与 Cordis `model-sync/diff` 事件,可在新模型上线、模型废弃或价格变更时即时推送。
|
|
84
86
|
* ⚡ **高性能模型策略过滤**:在未配置过滤策略时,`filterModels` 采用快速路径直接返回模型目录,避免不必要的正则表达式编译和数组遍历。
|
|
85
87
|
* 🔄 **一键检查与更新**:内置设置面板更新检查,在手动刷新时通过 `?fresh=1` 或 `Cache-Control: no-cache` 绕过 npm 缓存,即时获取最新版本。
|
package/lib/adapter-registry.js
CHANGED
|
@@ -32,6 +32,7 @@ const PROVIDER_ADAPTERS = Object.freeze({
|
|
|
32
32
|
id: 'id',
|
|
33
33
|
name: 'name',
|
|
34
34
|
contextWindow: 'context_length',
|
|
35
|
+
maxOutputTokens: 'max_tokens',
|
|
35
36
|
},
|
|
36
37
|
defaultModels: [
|
|
37
38
|
'deepseek/deepseek-v4.1-flash',
|
|
@@ -83,7 +84,7 @@ function mappedRows(provider, rows, descriptor) {
|
|
|
83
84
|
return rows.map((row) => {
|
|
84
85
|
const mapped = { ...(row && typeof row === 'object' ? row : {}) }
|
|
85
86
|
for (const [target, source] of Object.entries(fields)) {
|
|
86
|
-
if (!['id', 'name', 'description', 'contextWindow', 'maxTokens', 'pricing', 'tags'].includes(target)) continue
|
|
87
|
+
if (!['id', 'name', 'description', 'contextWindow', 'maxTokens', 'maxOutputTokens', 'pricing', 'tags'].includes(target)) continue
|
|
87
88
|
const value = pathValue(row, source)
|
|
88
89
|
if (value !== undefined) mapped[target] = value
|
|
89
90
|
}
|
|
@@ -128,6 +129,7 @@ function parseRows(provider, payload, descriptor) {
|
|
|
128
129
|
id: String(row.name ?? '').replace(/^models\//, ''),
|
|
129
130
|
name: row.displayName ?? row.display_name ?? row.name,
|
|
130
131
|
contextWindow: row.inputTokenLimit,
|
|
132
|
+
maxOutputTokens: row.outputTokenLimit,
|
|
131
133
|
maxTokens: row.outputTokenLimit,
|
|
132
134
|
})))
|
|
133
135
|
}
|
|
@@ -194,7 +196,7 @@ export function normalizeAdapterConfig(config, profile = {}) {
|
|
|
194
196
|
const capabilityMap = source.capabilityMap && typeof source.capabilityMap === 'object' && !Array.isArray(source.capabilityMap) ? structuredClone(source.capabilityMap) : {}
|
|
195
197
|
for (const key of Object.keys(capabilityMap)) if (!CAPABILITIES.has(key)) delete capabilityMap[key]
|
|
196
198
|
const fields = source.fields && typeof source.fields === 'object' && !Array.isArray(source.fields) ? structuredClone(source.fields) : {}
|
|
197
|
-
for (const key of Object.keys(fields)) if (!['id', 'name', 'description', 'contextWindow', 'maxTokens', 'pricing', 'tags'].includes(key) || typeof fields[key] !== 'string' || fields[key].length > 128) delete fields[key]
|
|
199
|
+
for (const key of Object.keys(fields)) if (!['id', 'name', 'description', 'contextWindow', 'maxTokens', 'maxOutputTokens', 'pricing', 'tags'].includes(key) || typeof fields[key] !== 'string' || fields[key].length > 128) delete fields[key]
|
|
198
200
|
for (const key of Object.keys(capabilityMap)) {
|
|
199
201
|
const values = Array.isArray(capabilityMap[key]) ? capabilityMap[key] : [capabilityMap[key]]
|
|
200
202
|
capabilityMap[key] = values.filter((value) => typeof value === 'string' && value.length <= 128).slice(0, 8)
|
package/lib/client.js
CHANGED
|
@@ -295,6 +295,9 @@ window.__ModuleLoader__.load({
|
|
|
295
295
|
saveFailed: 'Save rejected by settings service',
|
|
296
296
|
scopeReadOnly: 'Settings are read-only in this environment',
|
|
297
297
|
firstSeen: 'First seen',
|
|
298
|
+
contextIn: 'In',
|
|
299
|
+
contextOut: 'Out',
|
|
300
|
+
reasoningFormat: 'Reasoning',
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
const zh = {
|
|
@@ -464,6 +467,23 @@ window.__ModuleLoader__.load({
|
|
|
464
467
|
saveFailed: '设置服务拒绝保存',
|
|
465
468
|
scopeReadOnly: '当前环境为只读设置',
|
|
466
469
|
firstSeen: '首次发现',
|
|
470
|
+
contextIn: '输入',
|
|
471
|
+
contextOut: '输出',
|
|
472
|
+
reasoningFormat: '推理',
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
function formatTokens(num) {
|
|
477
|
+
if (typeof num !== "number" || !Number.isFinite(num) || num <= 0) return "?";
|
|
478
|
+
if (num >= 1000000) {
|
|
479
|
+
const m = num / 1000000;
|
|
480
|
+
return (m % 1 === 0 ? m : m.toFixed(1)) + "M";
|
|
481
|
+
}
|
|
482
|
+
if (num >= 1000) {
|
|
483
|
+
const k = num / 1000;
|
|
484
|
+
return (k % 1 === 0 ? k : k.toFixed(0)) + "k";
|
|
485
|
+
}
|
|
486
|
+
return String(num);
|
|
467
487
|
}
|
|
468
488
|
|
|
469
489
|
const API = '/dsh-model-sync'
|
|
@@ -1703,6 +1723,30 @@ window.__ModuleLoader__.load({
|
|
|
1703
1723
|
` ${model.name ?? model.id} (${model.id})`,
|
|
1704
1724
|
isNewModel(model, row) ? React.createElement('span', { style: { marginLeft: 6, fontSize: 11, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-business-primary)', color: 'white' } }, 'NEW') : null,
|
|
1705
1725
|
deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-warn-primary)', color: 'white' } }, deprecationDays > 0 ? t.deprecatesIn.replace('%d', deprecationDays) : t.deprecated) : null,
|
|
1726
|
+
(model.contextWindow || model.maxOutputTokens || model.maxTokens) ? React.createElement('span', {
|
|
1727
|
+
style: {
|
|
1728
|
+
marginLeft: 6,
|
|
1729
|
+
fontSize: 11,
|
|
1730
|
+
padding: '1px 5px',
|
|
1731
|
+
borderRadius: 4,
|
|
1732
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1733
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1734
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1735
|
+
},
|
|
1736
|
+
title: `${t.contextIn || 'In'}: ${model.contextWindow || '?'} | ${t.contextOut || 'Out'}: ${model.maxOutputTokens || model.maxTokens || '?'}`
|
|
1737
|
+
}, `${t.contextIn || 'In'}: ${formatTokens(model.contextWindow)} | ${t.contextOut || 'Out'}: ${formatTokens(model.maxOutputTokens || model.maxTokens)}`) : null,
|
|
1738
|
+
(model.reasoningConfig?.supportsReasoning || model.capabilities?.reasoning) ? React.createElement('span', {
|
|
1739
|
+
style: {
|
|
1740
|
+
marginLeft: 6,
|
|
1741
|
+
fontSize: 11,
|
|
1742
|
+
padding: '1px 5px',
|
|
1743
|
+
borderRadius: 4,
|
|
1744
|
+
background: 'var(--dsw-alias-state-business-secondary, var(--dsw-alias-bg-layer-2))',
|
|
1745
|
+
color: 'var(--dsw-alias-state-business-primary)',
|
|
1746
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1747
|
+
},
|
|
1748
|
+
title: model.reasoningConfig?.paramFormat ? `Reasoning format: ${model.reasoningConfig.paramFormat}` : 'Reasoning supported'
|
|
1749
|
+
}, `🧠 ${model.reasoningConfig?.paramFormat || 'reasoning'}`) : null,
|
|
1706
1750
|
(model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-warn-primary)', color: 'white' } }, '★') : null,
|
|
1707
1751
|
(model.pricing && minPrice !== Infinity && model.pricing.inputPerToken === minPrice) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-success-primary)', color: 'white' } }, '$') : null,
|
|
1708
1752
|
model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null,
|
package/lib/diff-notifier.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
function getOptionalService(ctx, name) {
|
|
2
|
+
try {
|
|
3
|
+
return ctx?.[name] ?? ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
4
|
+
} catch {
|
|
5
|
+
try {
|
|
6
|
+
return ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
7
|
+
} catch {
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
import { createHmac } from "node:crypto"
|
|
2
14
|
|
|
3
15
|
export function extractDiffEvents(results, options = {}) {
|
|
@@ -220,9 +232,10 @@ export async function dispatchDiffNotifications(ctx, config, results, { fetchImp
|
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
try {
|
|
223
|
-
|
|
235
|
+
const pluginNotify = getOptionalService(ctx, 'pluginNotify')
|
|
236
|
+
if (typeof pluginNotify?.send === "function") {
|
|
224
237
|
const summary = `Model Sync: +${events.counts.added} added, -${events.counts.deprecated} deprecated, ~${events.counts.priceChanged} price changed`
|
|
225
|
-
await
|
|
238
|
+
await pluginNotify.send({
|
|
226
239
|
source: "dsh-model-sync",
|
|
227
240
|
title: "Model Sync Catalog Changes",
|
|
228
241
|
message: summary,
|
package/lib/http.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
function getOptionalService(ctx, name) {
|
|
2
|
+
try {
|
|
3
|
+
return ctx?.[name] ?? ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
4
|
+
} catch {
|
|
5
|
+
try {
|
|
6
|
+
return ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
7
|
+
} catch {
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
import { isForbiddenWebhookHost, isLoopbackHost } from './diff-notifier.js'
|
|
2
14
|
import crypto from 'node:crypto'
|
|
3
15
|
import { validateAliasName } from './alias-resolver.js'
|
|
@@ -43,7 +55,7 @@ export function isLoopbackAddress(ip) {
|
|
|
43
55
|
|
|
44
56
|
export function trusted(req, { requireLoopback = false, ctx = null } = {}) {
|
|
45
57
|
// 1. Core connection rejection check (#237)
|
|
46
|
-
const conn = ctx
|
|
58
|
+
const conn = getOptionalService(ctx, 'connection') || req?.connection
|
|
47
59
|
if (conn && typeof conn.requestRejection === 'function') {
|
|
48
60
|
const rejection = conn.requestRejection(req)
|
|
49
61
|
if (rejection !== undefined) return false
|
|
@@ -274,7 +286,7 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
274
286
|
const makeHandler = (route) => async (req, res) => {
|
|
275
287
|
try {
|
|
276
288
|
// 1. Core connection check on all routes (#237)
|
|
277
|
-
const conn = ctx
|
|
289
|
+
const conn = getOptionalService(ctx, 'connection') || req?.connection
|
|
278
290
|
if (conn && typeof conn.requestRejection === 'function') {
|
|
279
291
|
const rejection = conn.requestRejection(req)
|
|
280
292
|
if (rejection !== undefined) {
|
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,50 @@ 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
|
+
|
|
301
368
|
export function normalizeModel(provider, row) {
|
|
302
369
|
if (typeof row === 'string') {
|
|
303
370
|
row = { id: row }
|
|
@@ -306,11 +373,15 @@ export function normalizeModel(provider, row) {
|
|
|
306
373
|
const effectiveProvider = (typeof provider === 'string' && provider.trim())
|
|
307
374
|
? provider.trim()
|
|
308
375
|
: (typeof row.provider === 'string' ? row.provider.trim() : '')
|
|
309
|
-
const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
|
|
376
|
+
const id = String(row.id ?? row.slug ?? row.model ?? (typeof row.name === 'string' && row.name.startsWith('models/') ? row.name.replace(/^models\//, '') : '')).trim()
|
|
310
377
|
if (!id) return null
|
|
311
378
|
const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
|
|
312
|
-
const
|
|
313
|
-
const
|
|
379
|
+
const rawContextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
|
|
380
|
+
const rawMaxOutputTokens = firstNumber(row, FIELD_ALIASES.maxOutputTokens)
|
|
381
|
+
const refLimits = lookupReferenceLimits(effectiveProvider || provider, id)
|
|
382
|
+
const contextWindow = rawContextWindow ?? refLimits?.contextWindow
|
|
383
|
+
const maxOutputTokens = rawMaxOutputTokens ?? refLimits?.maxOutputTokens
|
|
384
|
+
const maxTokens = maxOutputTokens ?? firstNumber(row, FIELD_ALIASES.maxTokens)
|
|
314
385
|
const capabilities = normalizeCapabilities(row)
|
|
315
386
|
const pricing = normalizePricing(row) ?? lookupReferencePricing(effectiveProvider || provider, id)
|
|
316
387
|
const tags = normalizeTags(row)
|
|
@@ -318,6 +389,7 @@ export function normalizeModel(provider, row) {
|
|
|
318
389
|
const lifecycle = normalizeLifecycle(row)
|
|
319
390
|
const input = normalizeInput(row)
|
|
320
391
|
const compat = normalizeCompat(effectiveProvider, row, id)
|
|
392
|
+
const reasoningConfig = normalizeReasoningConfig(effectiveProvider || provider, row, id, capabilities)
|
|
321
393
|
const firstSeenAt = normalizeDate(row.firstSeenAt)
|
|
322
394
|
const createdAt = normalizeDate(row.createdAt ?? row.created)
|
|
323
395
|
return {
|
|
@@ -327,11 +399,13 @@ export function normalizeModel(provider, row) {
|
|
|
327
399
|
...(firstSeenAt === undefined ? {} : { firstSeenAt }),
|
|
328
400
|
...(createdAt === undefined ? {} : { createdAt }),
|
|
329
401
|
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
402
|
+
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
330
403
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
331
404
|
...(row.description ? { description: String(row.description) } : {}),
|
|
332
405
|
...(input === undefined ? {} : { input }),
|
|
333
406
|
...(compat === undefined ? {} : { compat }),
|
|
334
407
|
...(capabilities === undefined ? {} : { capabilities }),
|
|
408
|
+
...(reasoningConfig === undefined ? {} : { reasoningConfig }),
|
|
335
409
|
...(pricing === undefined ? {} : { pricing }),
|
|
336
410
|
...(tags === undefined ? {} : { tags }),
|
|
337
411
|
...(aliases.length === 0 ? {} : { aliases }),
|
|
@@ -410,6 +484,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
410
484
|
id: 'cline-pass/deepseek-v4-flash',
|
|
411
485
|
name: 'DeepSeek V4 Flash',
|
|
412
486
|
contextWindow: 200000,
|
|
487
|
+
maxOutputTokens: 8192,
|
|
413
488
|
maxTokens: 8192,
|
|
414
489
|
capabilities: { vision: true, code: true, reasoning: true },
|
|
415
490
|
input: ['text', 'image'],
|
|
@@ -418,6 +493,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
418
493
|
id: 'cline-pass/deepseek-v4-pro',
|
|
419
494
|
name: 'DeepSeek V4 Pro',
|
|
420
495
|
contextWindow: 200000,
|
|
496
|
+
maxOutputTokens: 8192,
|
|
421
497
|
maxTokens: 8192,
|
|
422
498
|
capabilities: { vision: true, code: true, reasoning: true },
|
|
423
499
|
input: ['text', 'image'],
|
|
@@ -426,6 +502,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
426
502
|
id: 'cline-pass/glm-5.2',
|
|
427
503
|
name: 'GLM 5.2',
|
|
428
504
|
contextWindow: 200000,
|
|
505
|
+
maxOutputTokens: 8192,
|
|
429
506
|
maxTokens: 8192,
|
|
430
507
|
capabilities: { code: true },
|
|
431
508
|
input: ['text'],
|
|
@@ -434,6 +511,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
434
511
|
id: 'cline-pass/kimi-k3',
|
|
435
512
|
name: 'Kimi K3',
|
|
436
513
|
contextWindow: 200000,
|
|
514
|
+
maxOutputTokens: 8192,
|
|
437
515
|
maxTokens: 8192,
|
|
438
516
|
capabilities: { reasoning: true },
|
|
439
517
|
input: ['text'],
|
|
@@ -442,6 +520,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
442
520
|
id: 'cline-pass/kimi-k2.7-code',
|
|
443
521
|
name: 'Kimi K2.7 Code',
|
|
444
522
|
contextWindow: 200000,
|
|
523
|
+
maxOutputTokens: 8192,
|
|
445
524
|
maxTokens: 8192,
|
|
446
525
|
capabilities: { code: true, tools: true },
|
|
447
526
|
input: ['text'],
|
|
@@ -450,6 +529,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
450
529
|
id: 'cline-pass/kimi-k2.6',
|
|
451
530
|
name: 'Kimi K2.6',
|
|
452
531
|
contextWindow: 200000,
|
|
532
|
+
maxOutputTokens: 8192,
|
|
453
533
|
maxTokens: 8192,
|
|
454
534
|
capabilities: { code: true },
|
|
455
535
|
input: ['text'],
|
|
@@ -458,6 +538,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
458
538
|
id: 'cline-pass/qwen3.7-max',
|
|
459
539
|
name: 'Qwen 3.7 Max',
|
|
460
540
|
contextWindow: 200000,
|
|
541
|
+
maxOutputTokens: 8192,
|
|
461
542
|
maxTokens: 8192,
|
|
462
543
|
capabilities: { vision: true },
|
|
463
544
|
input: ['text', 'image'],
|
|
@@ -466,6 +547,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
466
547
|
id: 'cline-pass/qwen3.7-plus',
|
|
467
548
|
name: 'Qwen 3.7 Plus',
|
|
468
549
|
contextWindow: 200000,
|
|
550
|
+
maxOutputTokens: 8192,
|
|
469
551
|
maxTokens: 8192,
|
|
470
552
|
capabilities: { vision: true },
|
|
471
553
|
input: ['text', 'image'],
|
|
@@ -474,6 +556,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
474
556
|
id: 'cline-pass/minimax-m3',
|
|
475
557
|
name: 'MiniMax M3',
|
|
476
558
|
contextWindow: 200000,
|
|
559
|
+
maxOutputTokens: 8192,
|
|
477
560
|
maxTokens: 8192,
|
|
478
561
|
capabilities: { reasoning: true },
|
|
479
562
|
input: ['text'],
|
|
@@ -482,6 +565,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
482
565
|
id: 'cline-pass/mimo-v2.5',
|
|
483
566
|
name: 'MiMo V2.5',
|
|
484
567
|
contextWindow: 200000,
|
|
568
|
+
maxOutputTokens: 8192,
|
|
485
569
|
maxTokens: 8192,
|
|
486
570
|
capabilities: { code: true },
|
|
487
571
|
input: ['text'],
|
|
@@ -490,6 +574,7 @@ export const CLINEPASS_MODELS = Object.freeze([
|
|
|
490
574
|
id: 'cline-pass/mimo-v2.5-pro',
|
|
491
575
|
name: 'MiMo V2.5 Pro',
|
|
492
576
|
contextWindow: 200000,
|
|
577
|
+
maxOutputTokens: 8192,
|
|
493
578
|
maxTokens: 8192,
|
|
494
579
|
capabilities: { code: true, reasoning: true },
|
|
495
580
|
input: ['text'],
|
|
@@ -546,7 +631,8 @@ export function parseClinePassModels(includedInput) {
|
|
|
546
631
|
id: `cline-pass/${idPart}`,
|
|
547
632
|
name,
|
|
548
633
|
contextWindow: 200000,
|
|
549
|
-
|
|
634
|
+
maxOutputTokens: 8192,
|
|
635
|
+
maxTokens: 8192,
|
|
550
636
|
capabilities: {},
|
|
551
637
|
input: ['text'],
|
|
552
638
|
})
|
package/lib/provider-service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { listApiKeyProviders } from './inventory.js'
|
|
2
|
+
import { lookupReferenceLimits, lookupReferenceReasoning, clampOutputTokens, adaptReasoningPayload } from './specs-database.js'
|
|
2
3
|
|
|
3
4
|
export function createProviderService(ctx) {
|
|
4
5
|
const readSettings = () => {
|
|
@@ -20,10 +21,53 @@ export function createProviderService(ctx) {
|
|
|
20
21
|
|
|
21
22
|
const listProviders = () => listApiKeyProviders(readLlm(), readSettings())
|
|
22
23
|
|
|
24
|
+
const getProvider = (provider) => listProviders().find((row) => row.provider === provider) ?? null
|
|
25
|
+
|
|
26
|
+
const getLimits = (provider, modelId) => {
|
|
27
|
+
const p = getProvider(provider)
|
|
28
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
29
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
30
|
+
if (found && (found.contextWindow !== undefined || found.maxOutputTokens !== undefined || found.maxTokens !== undefined)) {
|
|
31
|
+
return {
|
|
32
|
+
contextWindow: found.contextWindow,
|
|
33
|
+
maxOutputTokens: found.maxOutputTokens ?? found.maxTokens,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return lookupReferenceLimits(provider, modelId) ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clampCompletionTokens = (provider, modelId, requestedTokens) => {
|
|
40
|
+
const limits = getLimits(provider, modelId)
|
|
41
|
+
return clampOutputTokens(limits?.maxOutputTokens, requestedTokens)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getReasoningConfig = (provider, modelId) => {
|
|
45
|
+
const p = getProvider(provider)
|
|
46
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
47
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId))
|
|
48
|
+
if (found?.reasoningConfig) {
|
|
49
|
+
return found.reasoningConfig
|
|
50
|
+
}
|
|
51
|
+
return lookupReferenceReasoning(provider, modelId) ?? null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const adaptReasoning = (provider, modelId, options = {}) => {
|
|
55
|
+
const p = getProvider(provider)
|
|
56
|
+
const catalog = Array.isArray(p?.models) ? p.models : []
|
|
57
|
+
const found = catalog.find((m) => (m?.id === modelId || m?.name === modelId)) || {
|
|
58
|
+
provider,
|
|
59
|
+
id: modelId,
|
|
60
|
+
reasoningConfig: getReasoningConfig(provider, modelId),
|
|
61
|
+
}
|
|
62
|
+
return adaptReasoningPayload(found, options)
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
return Object.freeze({
|
|
24
66
|
listProviders,
|
|
25
|
-
getProvider
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
getProvider,
|
|
68
|
+
getLimits,
|
|
69
|
+
clampCompletionTokens,
|
|
70
|
+
getReasoningConfig,
|
|
71
|
+
adaptReasoningPayload: adaptReasoning,
|
|
28
72
|
})
|
|
29
73
|
}
|
package/lib/reconcile.js
CHANGED
|
@@ -30,6 +30,9 @@ 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
|
}
|
|
35
38
|
next.push(merged)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
function cleanId(id) {
|
|
2
|
+
if (typeof id !== 'string') return ''
|
|
3
|
+
return id
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/^models\//, '')
|
|
7
|
+
.replace(/:latest$/, '')
|
|
8
|
+
.replace(/-latest$/, '')
|
|
9
|
+
.replace(/-(202\d{5}|202\d-\d{2}-\d{2}|\d{4})$/, '')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const REFERENCE_SPECS = Object.freeze({
|
|
13
|
+
openai: {
|
|
14
|
+
'gpt-4o': { contextWindow: 128000, maxOutputTokens: 16384 },
|
|
15
|
+
'gpt-4o-mini': { contextWindow: 128000, maxOutputTokens: 16384 },
|
|
16
|
+
'o1': {
|
|
17
|
+
contextWindow: 200000,
|
|
18
|
+
maxOutputTokens: 100000,
|
|
19
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'effort', defaultEffort: 'medium', supportedEfforts: ['low', 'medium', 'high'] },
|
|
20
|
+
compat: { supportsTemperature: false, supportsMaxTokens: false },
|
|
21
|
+
},
|
|
22
|
+
'o1-preview': {
|
|
23
|
+
contextWindow: 128000,
|
|
24
|
+
maxOutputTokens: 32768,
|
|
25
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'effort', defaultEffort: 'medium', supportedEfforts: ['low', 'medium', 'high'] },
|
|
26
|
+
compat: { supportsTemperature: false, supportsMaxTokens: false },
|
|
27
|
+
},
|
|
28
|
+
'o1-mini': {
|
|
29
|
+
contextWindow: 128000,
|
|
30
|
+
maxOutputTokens: 65536,
|
|
31
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'effort', defaultEffort: 'medium', supportedEfforts: ['low', 'medium', 'high'] },
|
|
32
|
+
compat: { supportsTemperature: false, supportsMaxTokens: false },
|
|
33
|
+
},
|
|
34
|
+
'o3-mini': {
|
|
35
|
+
contextWindow: 200000,
|
|
36
|
+
maxOutputTokens: 100000,
|
|
37
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'effort', defaultEffort: 'medium', supportedEfforts: ['low', 'medium', 'high'] },
|
|
38
|
+
compat: { supportsTemperature: false, supportsMaxTokens: false },
|
|
39
|
+
},
|
|
40
|
+
'gpt-4-turbo': { contextWindow: 128000, maxOutputTokens: 4096 },
|
|
41
|
+
'gpt-4': { contextWindow: 8192, maxOutputTokens: 4096 },
|
|
42
|
+
'gpt-3.5-turbo': { contextWindow: 16385, maxOutputTokens: 4096 },
|
|
43
|
+
'chatgpt-4o-latest': { contextWindow: 128000, maxOutputTokens: 16384 },
|
|
44
|
+
},
|
|
45
|
+
anthropic: {
|
|
46
|
+
'claude-3-7-sonnet': {
|
|
47
|
+
contextWindow: 200000,
|
|
48
|
+
maxOutputTokens: 64000,
|
|
49
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'budget_tokens', defaultBudget: 4096, maxBudget: 64000 },
|
|
50
|
+
},
|
|
51
|
+
'claude-3-5-sonnet': { contextWindow: 200000, maxOutputTokens: 8192 },
|
|
52
|
+
'claude-3-5-haiku': { contextWindow: 200000, maxOutputTokens: 8192 },
|
|
53
|
+
'claude-3-opus': { contextWindow: 200000, maxOutputTokens: 4096 },
|
|
54
|
+
'claude-3-haiku': { contextWindow: 200000, maxOutputTokens: 4096 },
|
|
55
|
+
},
|
|
56
|
+
deepseek: {
|
|
57
|
+
'deepseek-chat': { contextWindow: 64000, maxOutputTokens: 8192 },
|
|
58
|
+
'deepseek-reasoner': {
|
|
59
|
+
contextWindow: 64000,
|
|
60
|
+
maxOutputTokens: 8192,
|
|
61
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'boolean' },
|
|
62
|
+
},
|
|
63
|
+
'deepseek-coder': { contextWindow: 16384, maxOutputTokens: 4096 },
|
|
64
|
+
},
|
|
65
|
+
google: {
|
|
66
|
+
'gemini-2.0-flash': { contextWindow: 1048576, maxOutputTokens: 8192 },
|
|
67
|
+
'gemini-2.0-flash-thinking-exp': {
|
|
68
|
+
contextWindow: 1048576,
|
|
69
|
+
maxOutputTokens: 65536,
|
|
70
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'budget_tokens', defaultBudget: 8192, maxBudget: 65536 },
|
|
71
|
+
},
|
|
72
|
+
'gemini-2.0-flash-lite': { contextWindow: 1048576, maxOutputTokens: 8192 },
|
|
73
|
+
'gemini-1.5-pro': { contextWindow: 2097152, maxOutputTokens: 8192 },
|
|
74
|
+
'gemini-1.5-flash': { contextWindow: 1048576, maxOutputTokens: 8192 },
|
|
75
|
+
'gemini-1.5-flash-8b': { contextWindow: 1048576, maxOutputTokens: 8192 },
|
|
76
|
+
},
|
|
77
|
+
groq: {
|
|
78
|
+
'llama-3.3-70b-versatile': { contextWindow: 131072, maxOutputTokens: 32768 },
|
|
79
|
+
'llama-3.3-70b-specdec': { contextWindow: 8192, maxOutputTokens: 8192 },
|
|
80
|
+
'llama-3.1-70b-versatile': { contextWindow: 131072, maxOutputTokens: 32768 },
|
|
81
|
+
'llama-3.1-8b-instant': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
82
|
+
'llama3-70b-8192': { contextWindow: 8192, maxOutputTokens: 8192 },
|
|
83
|
+
'llama3-8b-8192': { contextWindow: 8192, maxOutputTokens: 8192 },
|
|
84
|
+
'mixtral-8x7b-32768': { contextWindow: 32768, maxOutputTokens: 32768 },
|
|
85
|
+
'gemma2-9b-it': { contextWindow: 8192, maxOutputTokens: 8192 },
|
|
86
|
+
'deepseek-r1-distill-llama-70b': {
|
|
87
|
+
contextWindow: 131072,
|
|
88
|
+
maxOutputTokens: 32768,
|
|
89
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'budget_tokens', defaultBudget: 4096, maxBudget: 32768 },
|
|
90
|
+
},
|
|
91
|
+
'deepseek-r1-distill-qwen-32b': {
|
|
92
|
+
contextWindow: 131072,
|
|
93
|
+
maxOutputTokens: 32768,
|
|
94
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'budget_tokens', defaultBudget: 4096, maxBudget: 32768 },
|
|
95
|
+
},
|
|
96
|
+
'qwen-2.5-32b': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
97
|
+
'qwen-2.5-coder-32b': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
98
|
+
'qwen-qwq-32b': {
|
|
99
|
+
contextWindow: 131072,
|
|
100
|
+
maxOutputTokens: 32768,
|
|
101
|
+
reasoningConfig: { supportsReasoning: true, paramFormat: 'budget_tokens', defaultBudget: 4096, maxBudget: 32768 },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
xai: {
|
|
105
|
+
'grok-2': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
106
|
+
'grok-2-mini': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
107
|
+
'grok-2-vision': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
108
|
+
'grok-beta': { contextWindow: 131072, maxOutputTokens: 8192 },
|
|
109
|
+
},
|
|
110
|
+
mistral: {
|
|
111
|
+
'mistral-large': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
112
|
+
'mistral-small': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
113
|
+
'codestral': { contextWindow: 256000, maxOutputTokens: 8192 },
|
|
114
|
+
'ministral-8b': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
115
|
+
'ministral-3b': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
116
|
+
'pixtral-large': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
117
|
+
'pixtral-12b': { contextWindow: 128000, maxOutputTokens: 8192 },
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export function lookupReferenceSpec(provider, modelId) {
|
|
122
|
+
if (typeof modelId !== 'string' || !modelId.trim()) return undefined
|
|
123
|
+
const provKey = String(provider || '').toLowerCase().trim()
|
|
124
|
+
const rawId = modelId.toLowerCase().trim()
|
|
125
|
+
const cleaned = cleanId(rawId)
|
|
126
|
+
|
|
127
|
+
// 1. Direct provider match
|
|
128
|
+
const table = REFERENCE_SPECS[provKey]
|
|
129
|
+
if (table) {
|
|
130
|
+
if (table[rawId]) return { ...table[rawId] }
|
|
131
|
+
if (table[cleaned]) return { ...table[cleaned] }
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. Slash prefix (e.g. "anthropic/claude-3-7-sonnet" or "openai/o3-mini")
|
|
136
|
+
const slashIdx = rawId.indexOf('/')
|
|
137
|
+
if (slashIdx > 0) {
|
|
138
|
+
const subProv = rawId.slice(0, slashIdx)
|
|
139
|
+
const subId = rawId.slice(slashIdx + 1)
|
|
140
|
+
const subCleaned = cleanId(subId)
|
|
141
|
+
const subTable = REFERENCE_SPECS[subProv]
|
|
142
|
+
if (subTable) {
|
|
143
|
+
if (subTable[subId]) return { ...subTable[subId] }
|
|
144
|
+
if (subTable[subCleaned]) return { ...subTable[subCleaned] }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3. Fallback across all tables for aggregators (OpenRouter, ClineBot, CommandCode)
|
|
149
|
+
for (const [, t] of Object.entries(REFERENCE_SPECS)) {
|
|
150
|
+
if (t[rawId]) return { ...t[rawId] }
|
|
151
|
+
if (t[cleaned]) return { ...t[cleaned] }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function lookupReferenceLimits(provider, modelId) {
|
|
158
|
+
const spec = lookupReferenceSpec(provider, modelId)
|
|
159
|
+
if (!spec) return undefined
|
|
160
|
+
const res = {}
|
|
161
|
+
if (spec.contextWindow !== undefined) res.contextWindow = spec.contextWindow
|
|
162
|
+
if (spec.maxOutputTokens !== undefined) res.maxOutputTokens = spec.maxOutputTokens
|
|
163
|
+
return Object.keys(res).length > 0 ? res : undefined
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function lookupReferenceReasoning(provider, modelId) {
|
|
167
|
+
const spec = lookupReferenceSpec(provider, modelId)
|
|
168
|
+
return spec?.reasoningConfig ? structuredClone(spec.reasoningConfig) : undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function lookupReferenceCompat(provider, modelId) {
|
|
172
|
+
const spec = lookupReferenceSpec(provider, modelId)
|
|
173
|
+
return spec?.compat ? structuredClone(spec.compat) : undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function clampOutputTokens(modelOrLimit, requestedTokens) {
|
|
177
|
+
const limit = typeof modelOrLimit === 'number'
|
|
178
|
+
? modelOrLimit
|
|
179
|
+
: Number(modelOrLimit?.maxOutputTokens ?? modelOrLimit?.maxTokens)
|
|
180
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
181
|
+
return { clamped: false, value: requestedTokens, limit: undefined }
|
|
182
|
+
}
|
|
183
|
+
const requested = Number(requestedTokens)
|
|
184
|
+
if (!Number.isFinite(requested) || requested <= 0) {
|
|
185
|
+
return { clamped: false, value: requestedTokens, limit }
|
|
186
|
+
}
|
|
187
|
+
if (requested > limit) {
|
|
188
|
+
return { clamped: true, value: limit, original: requested, limit }
|
|
189
|
+
}
|
|
190
|
+
return { clamped: false, value: requested, limit }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function adaptReasoningPayload(model, userOptions = {}) {
|
|
194
|
+
const warnings = []
|
|
195
|
+
const payload = {}
|
|
196
|
+
const config = model?.reasoningConfig
|
|
197
|
+
const format = config?.paramFormat || 'none'
|
|
198
|
+
const compat = model?.compat || {}
|
|
199
|
+
|
|
200
|
+
if (format === 'effort') {
|
|
201
|
+
const effort = userOptions.reasoningEffort || config?.defaultEffort || 'medium'
|
|
202
|
+
payload.reasoning_effort = effort
|
|
203
|
+
if (compat.supportsTemperature === false && userOptions.temperature !== undefined && userOptions.temperature !== 1) {
|
|
204
|
+
warnings.push(`temperature ${userOptions.temperature} is not supported by model ${model.id}; omitted`)
|
|
205
|
+
}
|
|
206
|
+
if (compat.supportsMaxTokens === false && userOptions.maxTokens !== undefined) {
|
|
207
|
+
payload.max_completion_tokens = userOptions.maxTokens
|
|
208
|
+
}
|
|
209
|
+
} else if (format === 'budget_tokens') {
|
|
210
|
+
const maxBudget = config?.maxBudget || 64000
|
|
211
|
+
const defaultBudget = config?.defaultBudget || 4096
|
|
212
|
+
const budget = Math.min(Math.max(1, Number(userOptions.thinkingBudget || defaultBudget)), maxBudget)
|
|
213
|
+
if (model?.provider === 'google' || String(model?.id || '').startsWith('gemini')) {
|
|
214
|
+
payload.thinking_config = { thinking_budget: budget }
|
|
215
|
+
} else {
|
|
216
|
+
payload.thinking = { type: 'enabled', budget_tokens: budget }
|
|
217
|
+
}
|
|
218
|
+
} else if (format === 'boolean') {
|
|
219
|
+
payload.thinking = true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { payload, warnings, format }
|
|
223
|
+
}
|
|
@@ -38,6 +38,18 @@ export function reliabilityOptions(config, options) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function getOptionalService(ctx, name) {
|
|
42
|
+
try {
|
|
43
|
+
return ctx?.[name] ?? ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
44
|
+
} catch {
|
|
45
|
+
try {
|
|
46
|
+
return ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
export function settingsOf(ctx) {
|
|
42
54
|
try {
|
|
43
55
|
return ctx?.settings ?? ctx?.reflect?.get?.('settings') ?? (typeof ctx?.get === 'function' ? ctx.get('settings') : undefined)
|
package/lib/updater.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
function getOptionalService(ctx, name) {
|
|
2
|
+
try {
|
|
3
|
+
return ctx?.[name] ?? ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
4
|
+
} catch {
|
|
5
|
+
try {
|
|
6
|
+
return ctx?.reflect?.get?.(name) ?? (typeof ctx?.get === 'function' ? ctx.get(name) : undefined)
|
|
7
|
+
} catch {
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
import { spawn } from 'node:child_process'
|
|
2
14
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
15
|
import { readFile } from 'node:fs/promises'
|
|
@@ -315,7 +327,7 @@ export function registerPluginUpdater(ctx, options = {}) {
|
|
|
315
327
|
const handler = async (request, response) => {
|
|
316
328
|
try {
|
|
317
329
|
// 1. Core connection rejection check on all updater routes (#237)
|
|
318
|
-
const conn = ctx
|
|
330
|
+
const conn = getOptionalService(ctx, 'connection') || request?.connection
|
|
319
331
|
if (conn && typeof conn.requestRejection === 'function') {
|
|
320
332
|
const rejection = conn.requestRejection(request)
|
|
321
333
|
if (rejection !== undefined) {
|