@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 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 缓存,即时获取最新版本。
@@ -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,
@@ -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
- if (typeof ctx?.pluginNotify?.send === "function") {
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 ctx.pluginNotify.send({
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?.connection || ctx?.get?.('connection') || req?.connection
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?.connection || ctx?.get?.('connection') || req?.connection
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
- const value = numericValue(row?.[field], options)
64
- if (value !== undefined) return value
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 contextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
313
- const maxTokens = firstNumber(row, FIELD_ALIASES.maxTokens)
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
- maxTokens: 8192,
634
+ maxOutputTokens: 8192,
635
+ maxTokens: 8192,
550
636
  capabilities: {},
551
637
  input: ['text'],
552
638
  })
@@ -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(provider) {
26
- return listProviders().find((row) => row.provider === provider) ?? null
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?.connection || ctx?.get?.('connection') || request?.connection
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-model-sync",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
5
5
  "keywords": [
6
6
  "deepseek-harness",