@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/README.md
CHANGED
|
@@ -78,8 +78,13 @@ graph LR
|
|
|
78
78
|
|
|
79
79
|
## ✨ Key Features
|
|
80
80
|
|
|
81
|
+
* 🏷️ **Smart Collections & Catalog Auto-Tagging**: Automatically categorizes models into standardized tags (`coding`, `reasoning`, `fast`, `vision`, `long-context`) during discovery, provides fast pill-button filtering in the Web UI, and introduces a one-click "⭐ Curated Best" selection preset for top flagship models.
|
|
82
|
+
* ⚡ **Latency TTFT Radar & Sparklines**: Probes response latency and Time To First Token (TTFT) across models, renders ASCII sparklines (` ▂▃▂ `) and 4-tier speed badges (⚡ `< 300ms`, 🟢 `< 1.2s`, 🟡 `1.5 - 3.0s`, 🔴 `> 3.0s`), and exposes `GET /dsh-model-sync/health/latency` for intelligent downstream routing.
|
|
83
|
+
* 💸 **Dynamic Live Pricing Sync & Prompt Caching**: Periodic background updates of live model pricing from OpenRouter and LiteLLM snapshots with persistent disk fallback cache, tracking prompt cache read/write costs (`cacheReadPerToken`, `cacheWritePerToken`), with `GET /dsh-model-sync/pricing` and `POST /dsh-model-sync/pricing/sync` endpoints.
|
|
81
84
|
* 💰 **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
85
|
* 🎯 **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.
|
|
86
|
+
* 📏 **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.
|
|
87
|
+
* 🧠 **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
88
|
* 🔔 **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
89
|
* ⚡ **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
90
|
* 🔄 **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
|
@@ -78,8 +78,13 @@ graph LR
|
|
|
78
78
|
|
|
79
79
|
## ✨ Ключевые возможности
|
|
80
80
|
|
|
81
|
+
* 🏷️ **Смарт-коллекции и авто-тегирование каталога**: Автоматическое присвоение тегов моделям (`coding`, `reasoning`, `fast`, `vision`, `long-context`) при синхронизации, фильтрация по тегам pill-кнопками в Web UI и пресет «⭐ Curated Best» для выбора лучших флагманских моделей в один клик.
|
|
82
|
+
* ⚡ **Радар задержек (Latency & TTFT) и спарклайны**: Измерение времени ответа и Time To First Token (TTFT), визуализация спарклайнами (` ▂▃▂ `) с 4 градациями скорости (⚡ `< 300мс`, 🟢 `< 1.2с`, 🟡 `1.5 - 3.0с`, 🔴 `> 3.0с`) и эндпоинт `GET /dsh-model-sync/health/latency` для агентов-маршрутизаторов.
|
|
83
|
+
* 💸 **Динамическая синхронизация цен и учёт Prompt Caching**: Фоновое обновление актуальных тарифов из OpenRouter и LiteLLM с дисковым fallback-кэшем, поддержка скидок кэширования промптов (`cacheReadPerToken`, `cacheWritePerToken`) и эндпоинты `GET /dsh-model-sync/pricing`, `POST /dsh-model-sync/pricing/sync`.
|
|
81
84
|
* 💰 **Встроенная база эталонных цен токенов**: Автоматическое обогащение моделей тарифами для OpenAI, Anthropic, Groq, Mistral, xAI и DeepSeek, обеспечивающее работу лимитов цен и псевдонима `@cheap` даже при отсутствии метаданных в API провайдера.
|
|
82
85
|
* 🎯 **Динамические «умные» псевдонимы и авто-фоллбэк**: Поддержка виртуальных алиасов (`@fast`, `@cheap`, `@best-code`) и цепочек резервирования с автопереключением при размыкании Circuit Breaker.
|
|
86
|
+
* 📏 **Реальные лимиты контекста и авто-клампинг (`contextWindow` / `maxOutputTokens`)**: точное разделение входного и выходного лимита токенов, защита от ошибок 400 `max_tokens` и индикация `Вход: 128k | Выход: 8k` в Web UI.
|
|
87
|
+
* 🧠 **Унификация параметров Reasoning & Thinking**: автоматическое определение формата (`effort`, `budget_tokens`, `boolean`) и параметров бюджета рассуждений с подавлением несовместимых параметров (например, `temperature` для o1/o3).
|
|
83
88
|
* 🔔 **Внешние Diff Webhooks и диспетчеризация событий**: Уведомления в реальном времени с проверкой подписи HMAC-SHA256 и событиями Cordis `model-sync/diff` при добавлении моделей, их устаревании или изменении цен.
|
|
84
89
|
* ⚡ **Высокопроизводительная фильтрация моделей**: Быстрый путь (`fast-path`) в `filterModels` мгновенно возвращает каталог при отсутствии активных правил политики, исключая лишнюю компиляцию regex и прогоны массивов.
|
|
85
90
|
* 🔄 **Обновление плагина в один клик**: Встроенный механизм проверки обновлений в Настройках со сбросом кэша npm при ручном запросе (`?fresh=1` / `Cache-Control: no-cache`), гарантирующий получение актуальной версии.
|
package/README.zh.md
CHANGED
|
@@ -78,8 +78,13 @@ graph LR
|
|
|
78
78
|
|
|
79
79
|
## ✨ 核心特性
|
|
80
80
|
|
|
81
|
+
* 🏷️ **智能集合与模型自动标签**:同步时自动识别并标记模型分类标签(`coding` 编程、`reasoning` 思考推理、`fast` 极速轻量、`vision` 多模态、`long-context` 128k+ 长上下文),支持 UI 药丸胶囊快速筛选与一键精选顶级旗舰预设("⭐ 精选最佳")。
|
|
82
|
+
* ⚡ **延迟雷达 (TTFT) 与火花线可视化**:实时测量模型响应时间与首字延迟 (TTFT),提供火花线走势(` ▂▃▂ `)及 4 档速度徽章(⚡ `< 300ms`、🟢 `< 1.2s`、🟡 `1.5 - 3.0s`、🔴 `> 3.0s`),并开放 `GET /dsh-model-sync/health/latency` 路由接口。
|
|
83
|
+
* 💸 **实时价格动态后台同步与提示词缓存**:定期从 OpenRouter 和 LiteLLM 获取最新模型单价,支持本地磁盘降级缓存,支持提示词缓存读写定价(`cacheReadPerToken`、`cacheWritePerToken`),并提供 `GET /dsh-model-sync/pricing` 和 `POST /dsh-model-sync/pricing/sync` 端点。
|
|
81
84
|
* 💰 **内置基准价格数据库**:自动为 OpenAI、Anthropic、Groq、Mistral、xAI 和 DeepSeek 模型补充 Token 价格数据,即使服务商接口未返回价格,也可无缝支持成本上限过滤与 `@cheap` 别名解析。
|
|
82
85
|
* 🎯 **动态智能虚拟别名与故障转移**:支持 `@fast`、`@cheap`、`@best-code` 等虚拟别名及故障转移链,在熔断器打开或健康检查失败时自动切换至备用模型。
|
|
86
|
+
* 📏 **真实上下文限制与边界安全 (`contextWindow` / `maxOutputTokens`)**:精确区分输入与输出令牌限制,通过自动限制防止 400 `max_tokens` 错误,并在 Web UI 中显示 `输入: 128k | 输出: 8k` 徽章。
|
|
87
|
+
* 🧠 **统一的深度思考与推理配置 (Reasoning & Thinking)**:自动检测参数格式 (`effort`, `budget_tokens`, `boolean`) 并转换推理预算,自动屏蔽不兼容参数(如 o1/o3 的 `temperature`)。
|
|
83
88
|
* 🔔 **外部差异通知 Webhook 与事件分发**:支持 HMAC-SHA256 签名验证的实时 Webhook 通知与 Cordis `model-sync/diff` 事件,可在新模型上线、模型废弃或价格变更时即时推送。
|
|
84
89
|
* ⚡ **高性能模型策略过滤**:在未配置过滤策略时,`filterModels` 采用快速路径直接返回模型目录,避免不必要的正则表达式编译和数组遍历。
|
|
85
90
|
* 🔄 **一键检查与更新**:内置设置面板更新检查,在手动刷新时通过 `?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
|
@@ -65,6 +65,14 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
'.dms-alert-ok{background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary)}' +
|
|
66
66
|
'.dms-alert-bad{background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary)}' +
|
|
67
67
|
'.dms-probe-result{padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px}' +
|
|
68
|
+
'.dms-pill{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}' +
|
|
69
|
+
'.dms-pill:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}' +
|
|
70
|
+
'.dms-pill-active{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' +
|
|
71
|
+
'.dms-tier-lightning{border-color:#a855f7;color:#a855f7;background:rgba(168,85,247,0.1)}' +
|
|
72
|
+
'.dms-tier-fast{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-secondary,rgba(34,197,94,0.1))}' +
|
|
73
|
+
'.dms-tier-moderate{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-secondary,rgba(234,179,8,0.1))}' +
|
|
74
|
+
'.dms-tier-slow{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-secondary,rgba(239,68,68,0.1))}' +
|
|
75
|
+
'.dms-sparkline{font-family:monospace;letter-spacing:-1px;margin-left:3px;font-weight:bold}' +
|
|
68
76
|
'@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-section-card,.dms-editor{padding:11px 12px}}'
|
|
69
77
|
|
|
70
78
|
function createErrorBoundary() {
|
|
@@ -228,6 +236,18 @@ window.__ModuleLoader__.load({
|
|
|
228
236
|
notifyOnPriceChanged: 'Notify on price changes',
|
|
229
237
|
saveWebhook: 'Save Webhook Settings',
|
|
230
238
|
testWebhook: 'Test Webhook',
|
|
239
|
+
selectCurated: '⭐ Curated Best',
|
|
240
|
+
tagAll: 'All',
|
|
241
|
+
tagCurated: '⭐ Curated',
|
|
242
|
+
tagReasoning: '🧠 Reasoning',
|
|
243
|
+
tagCoding: '💻 Coding',
|
|
244
|
+
tagFast: '⚡ Fast',
|
|
245
|
+
tagVision: '👁️ Vision',
|
|
246
|
+
tagLongContext: '📜 128k+',
|
|
247
|
+
speedRadar: 'Latency Radar',
|
|
248
|
+
ttft: 'TTFT',
|
|
249
|
+
pricingSync: 'Sync Prices',
|
|
250
|
+
pricingSyncing: 'Syncing prices…',
|
|
231
251
|
webhookTesting: 'Testing...',
|
|
232
252
|
webhookSuccess: 'Webhook test succeeded',
|
|
233
253
|
webhookFailed: 'Webhook test failed: %s',
|
|
@@ -295,6 +315,9 @@ window.__ModuleLoader__.load({
|
|
|
295
315
|
saveFailed: 'Save rejected by settings service',
|
|
296
316
|
scopeReadOnly: 'Settings are read-only in this environment',
|
|
297
317
|
firstSeen: 'First seen',
|
|
318
|
+
contextIn: 'In',
|
|
319
|
+
contextOut: 'Out',
|
|
320
|
+
reasoningFormat: 'Reasoning',
|
|
298
321
|
}
|
|
299
322
|
|
|
300
323
|
const zh = {
|
|
@@ -397,6 +420,18 @@ window.__ModuleLoader__.load({
|
|
|
397
420
|
notifyOnPriceChanged: '模型价格变更时通知',
|
|
398
421
|
saveWebhook: '保存 Webhook 设置',
|
|
399
422
|
testWebhook: '测试 Webhook',
|
|
423
|
+
selectCurated: '⭐ 精选最佳',
|
|
424
|
+
tagAll: '全部',
|
|
425
|
+
tagCurated: '⭐ 精选',
|
|
426
|
+
tagReasoning: '🧠 深度思考',
|
|
427
|
+
tagCoding: '💻 编程优化',
|
|
428
|
+
tagFast: '⚡ 极速轻量',
|
|
429
|
+
tagVision: '👁️ 多模态视觉',
|
|
430
|
+
tagLongContext: '📜 长上下文',
|
|
431
|
+
speedRadar: '延迟雷达',
|
|
432
|
+
ttft: '首字延迟',
|
|
433
|
+
pricingSync: '同步价格',
|
|
434
|
+
pricingSyncing: '正在同步价格…',
|
|
400
435
|
webhookTesting: '正在测试...',
|
|
401
436
|
webhookSuccess: 'Webhook 测试成功',
|
|
402
437
|
webhookFailed: 'Webhook 测试失败:%s',
|
|
@@ -464,6 +499,98 @@ window.__ModuleLoader__.load({
|
|
|
464
499
|
saveFailed: '设置服务拒绝保存',
|
|
465
500
|
scopeReadOnly: '当前环境为只读设置',
|
|
466
501
|
firstSeen: '首次发现',
|
|
502
|
+
contextIn: '输入',
|
|
503
|
+
contextOut: '输出',
|
|
504
|
+
reasoningFormat: '推理',
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
const SPARKLINE_BARS = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█']
|
|
509
|
+
function generateSparkline(values) {
|
|
510
|
+
if (!Array.isArray(values) || values.length === 0) return ''
|
|
511
|
+
const nums = values.filter((v) => typeof v === 'number' && Number.isFinite(v))
|
|
512
|
+
if (nums.length === 0) return ''
|
|
513
|
+
if (nums.length === 1) return '▄'
|
|
514
|
+
const min = Math.min(...nums)
|
|
515
|
+
const max = Math.max(...nums)
|
|
516
|
+
if (min === max) return '▄'.repeat(nums.length)
|
|
517
|
+
const range = max - min
|
|
518
|
+
return nums.map((v) => SPARKLINE_BARS[Math.min(7, Math.max(0, Math.floor(((v - min) / range) * 7)))]).join('')
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function classifySpeedTier(latencyMs, isOk = true) {
|
|
522
|
+
if (!isOk) return 'degraded'
|
|
523
|
+
if (typeof latencyMs !== 'number' || !Number.isFinite(latencyMs) || latencyMs <= 0) return 'unknown'
|
|
524
|
+
if (latencyMs < 300) return 'lightning'
|
|
525
|
+
if (latencyMs < 1200) return 'fast'
|
|
526
|
+
if (latencyMs <= 3000) return 'moderate'
|
|
527
|
+
return 'slow'
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function deriveModelTags(model) {
|
|
531
|
+
if (!model || typeof model !== 'object') return []
|
|
532
|
+
const tags = new Set(Array.isArray(model.tags) ? model.tags.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim().toLowerCase()) : [])
|
|
533
|
+
const idLower = String(model.id || '').toLowerCase()
|
|
534
|
+
const nameLower = String(model.name || '').toLowerCase()
|
|
535
|
+
const provLower = String(model.provider || '').toLowerCase()
|
|
536
|
+
const capabilities = model.capabilities || {}
|
|
537
|
+
|
|
538
|
+
if (capabilities.code === true || idLower.includes('coder') || idLower.includes('codestral') || idLower.includes('starcoder') || idLower.includes('deepseek-coder') || idLower.includes('claude-3-7-sonnet') || idLower.includes('claude-3-5-sonnet') || idLower.includes('kimi-k2.7-code') || idLower.includes('qwen-2.5-coder') || idLower.includes('qwen2.5-coder') || (/(^|[_-])(code|dev|prog)($|[_-])/i.test(idLower)) || (/(^|[_-])(code|coder)($|[_-])/i.test(nameLower))) {
|
|
539
|
+
tags.add('coding')
|
|
540
|
+
}
|
|
541
|
+
if (model.reasoningConfig?.supportsReasoning === true || capabilities.reasoning === true || /(^|[_-])(o1|o3|r1|qwq)($|[_-])/i.test(idLower) || idLower.includes('reasoner') || idLower.includes('deepseek-r1') || idLower.includes('thinking') || idLower.includes('-r1')) {
|
|
542
|
+
tags.add('reasoning')
|
|
543
|
+
}
|
|
544
|
+
if (provLower === 'groq' || provLower === 'cerebras' || idLower.includes('flash') || idLower.includes('mini') || idLower.includes('instant') || idLower.includes('haiku') || idLower.includes('small') || idLower.includes('8b') || idLower.includes('3b') || idLower.includes('1b') || idLower.includes('turbo') || idLower.includes('lite') || idLower.includes('nano')) {
|
|
545
|
+
tags.add('fast')
|
|
546
|
+
}
|
|
547
|
+
if (capabilities.vision === true || (Array.isArray(model.input) && model.input.includes('image')) || idLower.includes('vision') || idLower.includes('vl') || idLower.includes('pixtral') || idLower.includes('gemini') || idLower.includes('claude-3') || idLower.includes('4o')) {
|
|
548
|
+
tags.add('vision')
|
|
549
|
+
}
|
|
550
|
+
if (typeof model.contextWindow === 'number' && model.contextWindow >= 128000) {
|
|
551
|
+
tags.add('long-context')
|
|
552
|
+
}
|
|
553
|
+
return [...tags].sort()
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isCuratedModel(model) {
|
|
557
|
+
if (!model || typeof model !== 'object') return false
|
|
558
|
+
if (model.lifecycle?.deprecated) return false
|
|
559
|
+
const id = String(model.id || '').toLowerCase()
|
|
560
|
+
if (/-(202\d{5}|202\d-\d{2}-\d{2}|\d{4})$/.test(id) && !id.includes('claude-3-7') && !id.includes('claude-3-5')) {
|
|
561
|
+
return false
|
|
562
|
+
}
|
|
563
|
+
if (id.includes('-0314') || id.includes('-0613') || id.includes('-1106') || id.includes('-0125')) {
|
|
564
|
+
return false
|
|
565
|
+
}
|
|
566
|
+
const curatedIds = [
|
|
567
|
+
'claude-3-7-sonnet', 'claude-3-5-sonnet', 'claude-3-5-haiku',
|
|
568
|
+
'gpt-4o', 'gpt-4o-mini', 'o1', 'o3-mini',
|
|
569
|
+
'deepseek-chat', 'deepseek-reasoner', 'deepseek-v3', 'deepseek-r1',
|
|
570
|
+
'gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash',
|
|
571
|
+
'llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'llama-3.1-70b-versatile',
|
|
572
|
+
'qwen-2.5-coder-32b', 'qwen-2.5-72b', 'qwq-32b',
|
|
573
|
+
'mistral-large', 'codestral', 'pixtral-large',
|
|
574
|
+
'kimi-k2.7-code'
|
|
575
|
+
]
|
|
576
|
+
for (const c of curatedIds) {
|
|
577
|
+
if (id === c || id.endsWith('/' + c) || id.includes(c)) return true
|
|
578
|
+
}
|
|
579
|
+
const tags = model.tags || deriveModelTags(model)
|
|
580
|
+
return tags.includes('coding') && tags.includes('reasoning') && tags.includes('long-context')
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function formatTokens(num) {
|
|
584
|
+
if (typeof num !== "number" || !Number.isFinite(num) || num <= 0) return "?";
|
|
585
|
+
if (num >= 1000000) {
|
|
586
|
+
const m = num / 1000000;
|
|
587
|
+
return (m % 1 === 0 ? m : m.toFixed(1)) + "M";
|
|
588
|
+
}
|
|
589
|
+
if (num >= 1000) {
|
|
590
|
+
const k = num / 1000;
|
|
591
|
+
return (k % 1 === 0 ? k : k.toFixed(0)) + "k";
|
|
592
|
+
}
|
|
593
|
+
return String(num);
|
|
467
594
|
}
|
|
468
595
|
|
|
469
596
|
const API = '/dsh-model-sync'
|
|
@@ -660,6 +787,7 @@ window.__ModuleLoader__.load({
|
|
|
660
787
|
})
|
|
661
788
|
const [pageSize, setPageSize] = React.useState(50)
|
|
662
789
|
const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
|
|
790
|
+
const [selectedTag, setSelectedTag] = React.useState('all')
|
|
663
791
|
const [tryState, setTryState] = React.useState({})
|
|
664
792
|
const [batchTrying, setBatchTrying] = React.useState(false)
|
|
665
793
|
const [batchSummary, setBatchSummary] = React.useState(null)
|
|
@@ -1253,7 +1381,15 @@ window.__ModuleLoader__.load({
|
|
|
1253
1381
|
const haystack = (m.id + ' ' + (m.name || '') + ' ' + (m.aliases || []).join(' ') + ' ' + (m.tags || []).join(' ') + ' ' + (m.description || '')).toLowerCase()
|
|
1254
1382
|
return tokens.every((tok) => haystack.includes(tok))
|
|
1255
1383
|
}) : filteredChoicesBase
|
|
1256
|
-
const
|
|
1384
|
+
const taggedChoices = selectedTag === 'all'
|
|
1385
|
+
? searchedChoices
|
|
1386
|
+
: selectedTag === 'curated'
|
|
1387
|
+
? searchedChoices.filter((m) => isCuratedModel(m))
|
|
1388
|
+
: searchedChoices.filter((m) => {
|
|
1389
|
+
const tags = (m.tags && m.tags.length > 0) ? m.tags : deriveModelTags(m)
|
|
1390
|
+
return tags.includes(selectedTag)
|
|
1391
|
+
})
|
|
1392
|
+
const quickFilteredChoices = taggedChoices.filter((m) => {
|
|
1257
1393
|
if (quickFilter.context100k && (!m.contextWindow || m.contextWindow < 100000)) return false
|
|
1258
1394
|
if (quickFilter.cheap) {
|
|
1259
1395
|
const price = m.pricing?.inputPerToken
|
|
@@ -1276,7 +1412,7 @@ window.__ModuleLoader__.load({
|
|
|
1276
1412
|
}
|
|
1277
1413
|
return (a.name || a.id || '').localeCompare(b.name || b.id || '')
|
|
1278
1414
|
})
|
|
1279
|
-
}, [choices, capabilityFilter, debouncedSearchQuery, quickFilter, sortBy])
|
|
1415
|
+
}, [choices, capabilityFilter, debouncedSearchQuery, selectedTag, quickFilter, sortBy])
|
|
1280
1416
|
|
|
1281
1417
|
const maxContext = React.useMemo(() => filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0), [filteredChoices])
|
|
1282
1418
|
const minPrice = React.useMemo(() => filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity), [filteredChoices])
|
|
@@ -1680,6 +1816,13 @@ window.__ModuleLoader__.load({
|
|
|
1680
1816
|
))
|
|
1681
1817
|
) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities),
|
|
1682
1818
|
React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } },
|
|
1819
|
+
button(t.selectCurated || '⭐ Curated Best', () => {
|
|
1820
|
+
const ids = new Set(selectionDraft)
|
|
1821
|
+
for (const m of choices) {
|
|
1822
|
+
if (isCuratedModel(m)) ids.add(m.id)
|
|
1823
|
+
}
|
|
1824
|
+
setSelectionDraft([...ids])
|
|
1825
|
+
}, 'primary'),
|
|
1683
1826
|
button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]) }),
|
|
1684
1827
|
button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))) }),
|
|
1685
1828
|
button(t.invertFiltered, () => { const current = new Set(selectionDraft); for (const m of filteredChoices) { if (current.has(m.id)) current.delete(m.id); else current.add(m.id); } setSelectionDraft([...current]) })
|
|
@@ -1690,6 +1833,22 @@ window.__ModuleLoader__.load({
|
|
|
1690
1833
|
batchSummary ? React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.batchTryResults.replace('%d', batchSummary.reachable).replace('%d', batchSummary.total)) : null
|
|
1691
1834
|
),
|
|
1692
1835
|
React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, `${filteredChoices.length}/${choices.length} ${t.capabilityCount}`),
|
|
1836
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, margin: '8px 0', flexWrap: 'wrap', alignItems: 'center' } },
|
|
1837
|
+
[
|
|
1838
|
+
{ id: 'all', label: t.tagAll || 'All' },
|
|
1839
|
+
{ id: 'curated', label: t.tagCurated || '⭐ Curated' },
|
|
1840
|
+
{ id: 'reasoning', label: t.tagReasoning || '🧠 Reasoning' },
|
|
1841
|
+
{ id: 'coding', label: t.tagCoding || '💻 Coding' },
|
|
1842
|
+
{ id: 'fast', label: t.tagFast || '⚡ Fast' },
|
|
1843
|
+
{ id: 'vision', label: t.tagVision || '👁️ Vision' },
|
|
1844
|
+
{ id: 'long-context', label: t.tagLongContext || '📜 128k+' },
|
|
1845
|
+
].map((pill) => React.createElement('button', {
|
|
1846
|
+
key: pill.id,
|
|
1847
|
+
type: 'button',
|
|
1848
|
+
className: 'dms-pill' + (selectedTag === pill.id ? ' dms-pill-active' : ''),
|
|
1849
|
+
onClick: () => setSelectedTag(pill.id),
|
|
1850
|
+
}, pill.label))
|
|
1851
|
+
),
|
|
1693
1852
|
visibleChoices.map((model) => {
|
|
1694
1853
|
const deprecationDays = (() => {
|
|
1695
1854
|
const d = model.lifecycle?.deprecationDate
|
|
@@ -1697,20 +1856,69 @@ window.__ModuleLoader__.load({
|
|
|
1697
1856
|
const diff = new Date(d).getTime() - Date.now()
|
|
1698
1857
|
return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0
|
|
1699
1858
|
})()
|
|
1700
|
-
const
|
|
1859
|
+
const radar = status?.latencyRadar?.radar?.find((r) => r.provider === row.provider && r.model === model.id)
|
|
1860
|
+
const probe = tryState[`${row.provider}:${model.id}`] || (radar ? { ok: radar.ok, latency: radar.latencyMs, ttftMs: radar.ttftMs, sparkline: radar.sparkline, status: radar.status } : null)
|
|
1701
1861
|
return React.createElement('label', { key: model.id, style: { display: 'block', margin: '4px 0', padding: '4px 6px', borderRadius: 6, background: isNewModel(model, row) ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' } },
|
|
1702
1862
|
React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }),
|
|
1703
1863
|
` ${model.name ?? model.id} (${model.id})`,
|
|
1704
1864
|
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
1865
|
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,
|
|
1866
|
+
(model.contextWindow || model.maxOutputTokens || model.maxTokens) ? React.createElement('span', {
|
|
1867
|
+
style: {
|
|
1868
|
+
marginLeft: 6,
|
|
1869
|
+
fontSize: 11,
|
|
1870
|
+
padding: '1px 5px',
|
|
1871
|
+
borderRadius: 4,
|
|
1872
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1873
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1874
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1875
|
+
},
|
|
1876
|
+
title: `${t.contextIn || 'In'}: ${model.contextWindow || '?'} | ${t.contextOut || 'Out'}: ${model.maxOutputTokens || model.maxTokens || '?'}`
|
|
1877
|
+
}, `${t.contextIn || 'In'}: ${formatTokens(model.contextWindow)} | ${t.contextOut || 'Out'}: ${formatTokens(model.maxOutputTokens || model.maxTokens)}`) : null,
|
|
1878
|
+
(model.reasoningConfig?.supportsReasoning || model.capabilities?.reasoning) ? React.createElement('span', {
|
|
1879
|
+
style: {
|
|
1880
|
+
marginLeft: 6,
|
|
1881
|
+
fontSize: 11,
|
|
1882
|
+
padding: '1px 5px',
|
|
1883
|
+
borderRadius: 4,
|
|
1884
|
+
background: 'var(--dsw-alias-state-business-secondary, var(--dsw-alias-bg-layer-2))',
|
|
1885
|
+
color: 'var(--dsw-alias-state-business-primary)',
|
|
1886
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1887
|
+
},
|
|
1888
|
+
title: model.reasoningConfig?.paramFormat ? `Reasoning format: ${model.reasoningConfig.paramFormat}` : 'Reasoning supported'
|
|
1889
|
+
}, `🧠 ${model.reasoningConfig?.paramFormat || 'reasoning'}`) : null,
|
|
1706
1890
|
(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
1891
|
(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
|
-
|
|
1892
|
+
(() => {
|
|
1893
|
+
const modelTags = (model.tags && model.tags.length > 0) ? model.tags : deriveModelTags(model)
|
|
1894
|
+
if (!modelTags || modelTags.length === 0) return null
|
|
1895
|
+
return React.createElement('span', { style: { marginLeft: 6, display: 'inline-flex', gap: 3, verticalAlign: 'middle' } },
|
|
1896
|
+
modelTags.map((tag) => React.createElement('span', {
|
|
1897
|
+
key: tag,
|
|
1898
|
+
style: {
|
|
1899
|
+
fontSize: 10,
|
|
1900
|
+
padding: '0 4px',
|
|
1901
|
+
borderRadius: 3,
|
|
1902
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1903
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1904
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1905
|
+
}
|
|
1906
|
+
}, tag === 'coding' ? '💻 coding' : tag === 'reasoning' ? '🧠 reasoning' : tag === 'fast' ? '⚡ fast' : tag === 'vision' ? '👁️ vision' : tag === 'long-context' ? '📜 128k+' : tag))
|
|
1907
|
+
)
|
|
1908
|
+
})(),
|
|
1709
1909
|
model.pricing ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, `in $${(model.pricing.inputPerToken * 1000000).toFixed(2)}/1M` + (model.pricing.cacheReadPerToken !== undefined ? ` / cache $${(model.pricing.cacheReadPerToken * 1000000).toFixed(2)}` : '') + (model.pricing.outputPerToken !== undefined ? ` / out $${(model.pricing.outputPerToken * 1000000).toFixed(2)}` : '')) : null,
|
|
1710
1910
|
model.description ? React.createElement('small', { title: model.description, style: { display: 'block', marginLeft: 22, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, model.description.slice(0, 120)) : null,
|
|
1711
1911
|
row.baseURL || row.api ? React.createElement('a', { href: (row.baseURL || '') + '/models/' + encodeURIComponent(model.id), target: '_blank', rel: 'noopener', style: { marginLeft: 8, fontSize: 11 } }, 'docs') : null,
|
|
1712
1912
|
React.createElement('button', { type: 'button', onClick: (e) => { e.preventDefault(); doTry(row.provider, model.id) }, style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 4 } }, (probe && probe.loading) ? '…' : '▶'),
|
|
1713
|
-
probe && !probe.loading ? React.createElement('span', {
|
|
1913
|
+
probe && !probe.loading ? React.createElement('span', {
|
|
1914
|
+
className: 'dms-badge ' + (probe.ok ? (classifySpeedTier(probe.latency, probe.ok) === 'lightning' ? 'dms-tier-lightning' : classifySpeedTier(probe.latency, probe.ok) === 'fast' ? 'dms-tier-fast' : 'dms-tier-moderate') : 'dms-badge-bad'),
|
|
1915
|
+
style: { marginLeft: 6 },
|
|
1916
|
+
title: probe.ttftMs ? `TTFT: ${probe.ttftMs}ms | Latency: ${probe.latency}ms` : (probe.latency ? `${probe.latency}ms` : undefined)
|
|
1917
|
+
},
|
|
1918
|
+
(classifySpeedTier(probe.latency, probe.ok) === 'lightning' ? '⚡ ' : classifySpeedTier(probe.latency, probe.ok) === 'fast' ? '🟢 ' : classifySpeedTier(probe.latency, probe.ok) === 'moderate' ? '🟡 ' : '🔴 ') +
|
|
1919
|
+
(probe.ok ? `${probe.latency}ms` : (probe.error || probe.status || 'offline')) +
|
|
1920
|
+
(probe.sparkline ? ` ${probe.sparkline}` : '')
|
|
1921
|
+
) : null
|
|
1714
1922
|
)
|
|
1715
1923
|
}),
|
|
1716
1924
|
filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } },
|
|
@@ -1872,7 +2080,7 @@ window.__ModuleLoader__.load({
|
|
|
1872
2080
|
trySlot('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ErrorBoundary, null, React.createElement(ModelSyncCard, { ...props, ctx }))))
|
|
1873
2081
|
}
|
|
1874
2082
|
|
|
1875
|
-
module.exports = { apply, inject: ['slots', 'locale', 'configForms'], filterModelsByCapabilities }
|
|
2083
|
+
module.exports = { apply, inject: ['slots', 'locale', 'configForms'], filterModelsByCapabilities, deriveModelTags, isCuratedModel, generateSparkline, classifySpeedTier }
|
|
1876
2084
|
return module.exports
|
|
1877
2085
|
},
|
|
1878
2086
|
})
|
package/lib/http.js
CHANGED
|
@@ -339,7 +339,46 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
if (route === 'import') {
|
|
342
|
-
if (
|
|
342
|
+
if (route === 'health-latency') {
|
|
343
|
+
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
344
|
+
try {
|
|
345
|
+
const urlObj = new URL(req.url, 'http://127.0.0.1')
|
|
346
|
+
const provider = urlObj.searchParams.get('provider') || undefined
|
|
347
|
+
const report = typeof synchronizer.getLatencyRadar === 'function'
|
|
348
|
+
? synchronizer.getLatencyRadar({ provider })
|
|
349
|
+
: { updatedAt: Date.now(), radar: [] }
|
|
350
|
+
return json(res, 200, report)
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (route === 'pricing') {
|
|
356
|
+
if (req.method === 'GET') {
|
|
357
|
+
try {
|
|
358
|
+
const urlObj = new URL(req.url, 'http://127.0.0.1')
|
|
359
|
+
const provider = urlObj.searchParams.get('provider') || undefined
|
|
360
|
+
const model = urlObj.searchParams.get('model') || undefined
|
|
361
|
+
const report = typeof synchronizer.getPricingInfo === 'function'
|
|
362
|
+
? synchronizer.getPricingInfo({ provider, model })
|
|
363
|
+
: { pricing: {} }
|
|
364
|
+
return json(res, 200, report)
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } })
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (req.method === 'POST') {
|
|
370
|
+
try {
|
|
371
|
+
const result = typeof synchronizer.syncPricing === 'function'
|
|
372
|
+
? await synchronizer.syncPricing()
|
|
373
|
+
: { updated: 0, source: 'reference' }
|
|
374
|
+
return json(res, 200, result)
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return json(res, 500, { error: { code: 'sync-failed', message: error instanceof Error ? error.message : String(error) } })
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return json(res, 405, { error: { code: 'method', message: 'GET or POST only' } })
|
|
380
|
+
}
|
|
381
|
+
if (req.method !== 'POST') return json(res, 405, { error: { code: 'method', message: 'POST only' } })
|
|
343
382
|
try {
|
|
344
383
|
const body = await readJson(req)
|
|
345
384
|
const result = await synchronizer.importConfig(body)
|
|
@@ -357,6 +396,45 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
357
396
|
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
358
397
|
return json(res, 200, { notifications: synchronizer.notifications(normalizeNotificationRequest(req.url)) })
|
|
359
398
|
}
|
|
399
|
+
if (route === 'health-latency') {
|
|
400
|
+
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
401
|
+
try {
|
|
402
|
+
const urlObj = new URL(req.url, 'http://127.0.0.1')
|
|
403
|
+
const provider = urlObj.searchParams.get('provider') || undefined
|
|
404
|
+
const report = typeof synchronizer.getLatencyRadar === 'function'
|
|
405
|
+
? synchronizer.getLatencyRadar({ provider })
|
|
406
|
+
: { updatedAt: Date.now(), radar: [] }
|
|
407
|
+
return json(res, 200, report)
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } })
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (route === 'pricing') {
|
|
413
|
+
if (req.method === 'GET') {
|
|
414
|
+
try {
|
|
415
|
+
const urlObj = new URL(req.url, 'http://127.0.0.1')
|
|
416
|
+
const provider = urlObj.searchParams.get('provider') || undefined
|
|
417
|
+
const model = urlObj.searchParams.get('model') || undefined
|
|
418
|
+
const report = typeof synchronizer.getPricingInfo === 'function'
|
|
419
|
+
? synchronizer.getPricingInfo({ provider, model })
|
|
420
|
+
: { pricing: {} }
|
|
421
|
+
return json(res, 200, report)
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } })
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (req.method === 'POST') {
|
|
427
|
+
try {
|
|
428
|
+
const result = typeof synchronizer.syncPricing === 'function'
|
|
429
|
+
? await synchronizer.syncPricing()
|
|
430
|
+
: { updated: 0, source: 'reference' }
|
|
431
|
+
return json(res, 200, result)
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return json(res, 500, { error: { code: 'sync-failed', message: error instanceof Error ? error.message : String(error) } })
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return json(res, 405, { error: { code: 'method', message: 'GET or POST only' } })
|
|
437
|
+
}
|
|
360
438
|
if (req.method !== 'POST') return json(res, 405, { error: { code: 'method', message: 'POST only' } })
|
|
361
439
|
let options
|
|
362
440
|
try {
|
|
@@ -423,5 +501,7 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
423
501
|
ctx.webServer.register({ kind: 'exact', path: BASE + '/notifications/read', handler: makeHandler('notification-read') }),
|
|
424
502
|
ctx.webServer.register({ kind: 'exact', path: BASE + '/notifications/acknowledge', handler: makeHandler('notification-acknowledge') }),
|
|
425
503
|
ctx.webServer.register({ kind: 'exact', path: BASE + '/webhook/test', handler: makeHandler('webhook-test') }),
|
|
504
|
+
ctx.webServer.register({ kind: 'exact', path: BASE + '/health/latency', handler: makeHandler('health-latency') }),
|
|
505
|
+
ctx.webServer.register({ kind: 'exact', path: BASE + '/pricing', handler: makeHandler('pricing') }),
|
|
426
506
|
]
|
|
427
507
|
}
|