@goodandready/dsh-model-sync 0.5.16 → 0.5.18

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/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,31 @@ window.__ModuleLoader__.load({
228
236
  notifyOnPriceChanged: 'Notify on price changes',
229
237
  saveWebhook: 'Save Webhook Settings',
230
238
  testWebhook: 'Test Webhook',
239
+ probeCapabilities: '🔍 Verify Capabilities',
240
+ probeAll: '🔍 Verify All Models',
241
+ probeNewOnly: '⚡ Verify New Only',
242
+ probingInProgress: 'Verifying capabilities…',
243
+ verifiedBadge: 'Verified',
244
+ ollamaHubTitle: '🦙 Ollama Model Hub',
245
+ ollamaPullPlaceholder: 'e.g. llama3.2:3b or qwen2.5-coder:7b',
246
+ ollamaPullBtn: '⬇ Pull Model',
247
+ ollamaUnloadBtn: '⏏ Unload from VRAM',
248
+ ollamaDeleteBtn: '🗑 Delete',
249
+ ollamaRunningVram: 'Active in VRAM (GPU)',
250
+ ollamaNoRunning: 'No models currently loaded in VRAM',
251
+ ollamaInstalled: 'Installed Local Models',
252
+ selectCurated: '⭐ Curated Best',
253
+ tagAll: 'All',
254
+ tagCurated: '⭐ Curated',
255
+ tagReasoning: '🧠 Reasoning',
256
+ tagCoding: '💻 Coding',
257
+ tagFast: '⚡ Fast',
258
+ tagVision: '👁️ Vision',
259
+ tagLongContext: '📜 128k+',
260
+ speedRadar: 'Latency Radar',
261
+ ttft: 'TTFT',
262
+ pricingSync: 'Sync Prices',
263
+ pricingSyncing: 'Syncing prices…',
231
264
  webhookTesting: 'Testing...',
232
265
  webhookSuccess: 'Webhook test succeeded',
233
266
  webhookFailed: 'Webhook test failed: %s',
@@ -400,6 +433,31 @@ window.__ModuleLoader__.load({
400
433
  notifyOnPriceChanged: '模型价格变更时通知',
401
434
  saveWebhook: '保存 Webhook 设置',
402
435
  testWebhook: '测试 Webhook',
436
+ probeCapabilities: '🔍 验证模型能力',
437
+ probeAll: '🔍 验证所有模型',
438
+ probeNewOnly: '⚡ 仅验证新模型',
439
+ probingInProgress: '正在验证模型能力…',
440
+ verifiedBadge: '已验证',
441
+ ollamaHubTitle: '🦙 Ollama 本地模型中心',
442
+ ollamaPullPlaceholder: '例如 llama3.2:3b 或 qwen2.5-coder:7b',
443
+ ollamaPullBtn: '⬇ 下载/拉取模型',
444
+ ollamaUnloadBtn: '⏏ 释放显存 (VRAM)',
445
+ ollamaDeleteBtn: '🗑 删除模型',
446
+ ollamaRunningVram: '当前显存占用 (VRAM)',
447
+ ollamaNoRunning: '显存中无运行模型',
448
+ ollamaInstalled: '已安装的本地模型',
449
+ selectCurated: '⭐ 精选最佳',
450
+ tagAll: '全部',
451
+ tagCurated: '⭐ 精选',
452
+ tagReasoning: '🧠 深度思考',
453
+ tagCoding: '💻 编程优化',
454
+ tagFast: '⚡ 极速轻量',
455
+ tagVision: '👁️ 多模态视觉',
456
+ tagLongContext: '📜 长上下文',
457
+ speedRadar: '延迟雷达',
458
+ ttft: '首字延迟',
459
+ pricingSync: '同步价格',
460
+ pricingSyncing: '正在同步价格…',
403
461
  webhookTesting: '正在测试...',
404
462
  webhookSuccess: 'Webhook 测试成功',
405
463
  webhookFailed: 'Webhook 测试失败:%s',
@@ -473,6 +531,81 @@ window.__ModuleLoader__.load({
473
531
  }
474
532
 
475
533
 
534
+ const SPARKLINE_BARS = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█']
535
+ function generateSparkline(values) {
536
+ if (!Array.isArray(values) || values.length === 0) return ''
537
+ const nums = values.filter((v) => typeof v === 'number' && Number.isFinite(v))
538
+ if (nums.length === 0) return ''
539
+ if (nums.length === 1) return '▄'
540
+ const min = Math.min(...nums)
541
+ const max = Math.max(...nums)
542
+ if (min === max) return '▄'.repeat(nums.length)
543
+ const range = max - min
544
+ return nums.map((v) => SPARKLINE_BARS[Math.min(7, Math.max(0, Math.floor(((v - min) / range) * 7)))]).join('')
545
+ }
546
+
547
+ function classifySpeedTier(latencyMs, isOk = true) {
548
+ if (!isOk) return 'degraded'
549
+ if (typeof latencyMs !== 'number' || !Number.isFinite(latencyMs) || latencyMs <= 0) return 'unknown'
550
+ if (latencyMs < 300) return 'lightning'
551
+ if (latencyMs < 1200) return 'fast'
552
+ if (latencyMs <= 3000) return 'moderate'
553
+ return 'slow'
554
+ }
555
+
556
+ function deriveModelTags(model) {
557
+ if (!model || typeof model !== 'object') return []
558
+ const tags = new Set(Array.isArray(model.tags) ? model.tags.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim().toLowerCase()) : [])
559
+ const idLower = String(model.id || '').toLowerCase()
560
+ const nameLower = String(model.name || '').toLowerCase()
561
+ const provLower = String(model.provider || '').toLowerCase()
562
+ const capabilities = model.capabilities || {}
563
+
564
+ 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))) {
565
+ tags.add('coding')
566
+ }
567
+ 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')) {
568
+ tags.add('reasoning')
569
+ }
570
+ 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')) {
571
+ tags.add('fast')
572
+ }
573
+ 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')) {
574
+ tags.add('vision')
575
+ }
576
+ if (typeof model.contextWindow === 'number' && model.contextWindow >= 128000) {
577
+ tags.add('long-context')
578
+ }
579
+ return [...tags].sort()
580
+ }
581
+
582
+ function isCuratedModel(model) {
583
+ if (!model || typeof model !== 'object') return false
584
+ if (model.lifecycle?.deprecated) return false
585
+ const id = String(model.id || '').toLowerCase()
586
+ if (/-(202\d{5}|202\d-\d{2}-\d{2}|\d{4})$/.test(id) && !id.includes('claude-3-7') && !id.includes('claude-3-5')) {
587
+ return false
588
+ }
589
+ if (id.includes('-0314') || id.includes('-0613') || id.includes('-1106') || id.includes('-0125')) {
590
+ return false
591
+ }
592
+ const curatedIds = [
593
+ 'claude-3-7-sonnet', 'claude-3-5-sonnet', 'claude-3-5-haiku',
594
+ 'gpt-4o', 'gpt-4o-mini', 'o1', 'o3-mini',
595
+ 'deepseek-chat', 'deepseek-reasoner', 'deepseek-v3', 'deepseek-r1',
596
+ 'gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash',
597
+ 'llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'llama-3.1-70b-versatile',
598
+ 'qwen-2.5-coder-32b', 'qwen-2.5-72b', 'qwq-32b',
599
+ 'mistral-large', 'codestral', 'pixtral-large',
600
+ 'kimi-k2.7-code'
601
+ ]
602
+ for (const c of curatedIds) {
603
+ if (id === c || id.endsWith('/' + c) || id.includes(c)) return true
604
+ }
605
+ const tags = model.tags || deriveModelTags(model)
606
+ return tags.includes('coding') && tags.includes('reasoning') && tags.includes('long-context')
607
+ }
608
+
476
609
  function formatTokens(num) {
477
610
  if (typeof num !== "number" || !Number.isFinite(num) || num <= 0) return "?";
478
611
  if (num >= 1000000) {
@@ -680,6 +813,7 @@ window.__ModuleLoader__.load({
680
813
  })
681
814
  const [pageSize, setPageSize] = React.useState(50)
682
815
  const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
816
+ const [selectedTag, setSelectedTag] = React.useState('all')
683
817
  const [tryState, setTryState] = React.useState({})
684
818
  const [batchTrying, setBatchTrying] = React.useState(false)
685
819
  const [batchSummary, setBatchSummary] = React.useState(null)
@@ -1127,6 +1261,84 @@ window.__ModuleLoader__.load({
1127
1261
  .finally(() => setBusy(false))
1128
1262
  }
1129
1263
 
1264
+
1265
+ const [probingProvider, setProbingProvider] = React.useState(null)
1266
+ const [probingAll, setProbingAll] = React.useState(false)
1267
+ const [verifiedCapabilities, setVerifiedCapabilities] = React.useState({})
1268
+ const [ollamaPs, setOllamaPs] = React.useState([])
1269
+ const [ollamaTags, setOllamaTags] = React.useState([])
1270
+ const [ollamaPullInput, setOllamaPullInput] = React.useState('')
1271
+ const [ollamaPullState, setOllamaPullState] = React.useState(null)
1272
+
1273
+ const loadVerified = () => {
1274
+ fetch(`${API}/probe/verified`)
1275
+ .then(safeJson)
1276
+ .then((d) => { if (d?.verified) setVerifiedCapabilities(d.verified) })
1277
+ .catch(() => {})
1278
+ }
1279
+
1280
+ const probeProviderCaps = (targetProvider, onlyNew = false) => {
1281
+ setProbingProvider(targetProvider)
1282
+ fetch(`${API}/probe/provider`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider, onlyNew }) })
1283
+ .then(safeJson)
1284
+ .then(() => { loadVerified(); load() })
1285
+ .catch((e) => setError(String(e?.message ?? e)))
1286
+ .finally(() => setProbingProvider(null))
1287
+ }
1288
+
1289
+ const probeAllCaps = (onlyNew = false) => {
1290
+ setProbingAll(true)
1291
+ fetch(`${API}/probe/all`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ onlyNew }) })
1292
+ .then(safeJson)
1293
+ .then(() => { loadVerified(); load() })
1294
+ .catch((e) => setError(String(e?.message ?? e)))
1295
+ .finally(() => setProbingAll(false))
1296
+ }
1297
+
1298
+ const loadOllama = () => {
1299
+ fetch(`${API}/ollama/ps`).then(safeJson).then((d) => { if (d?.running) setOllamaPs(d.running) }).catch(() => {})
1300
+ fetch(`${API}/ollama/tags`).then(safeJson).then((d) => { if (d?.models) setOllamaTags(d.models) }).catch(() => {})
1301
+ }
1302
+
1303
+ const pullOllamaModel = (name) => {
1304
+ if (!name) return
1305
+ fetch(`${API}/ollama/pull`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: name }) })
1306
+ .then(safeJson)
1307
+ .then((res) => {
1308
+ setOllamaPullState(res.status || { model: name, status: 'pulling', percent: 0 })
1309
+ const poll = setInterval(() => {
1310
+ fetch(`${API}/ollama/pull/status?model=${encodeURIComponent(name)}`)
1311
+ .then(safeJson)
1312
+ .then((st) => {
1313
+ if (st?.status) {
1314
+ setOllamaPullState(st.status)
1315
+ if (st.status.done) {
1316
+ clearInterval(poll)
1317
+ loadOllama()
1318
+ load()
1319
+ }
1320
+ }
1321
+ })
1322
+ .catch(() => clearInterval(poll))
1323
+ }, 1000)
1324
+ })
1325
+ .catch((e) => setError(String(e?.message ?? e)))
1326
+ }
1327
+
1328
+ const unloadOllama = (name) => {
1329
+ fetch(`${API}/ollama/unload`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: name }) })
1330
+ .then(safeJson)
1331
+ .then(() => loadOllama())
1332
+ .catch((e) => setError(String(e?.message ?? e)))
1333
+ }
1334
+
1335
+ const deleteOllama = (name) => {
1336
+ fetch(`${API}/ollama/delete`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: name }) })
1337
+ .then(safeJson)
1338
+ .then(() => { loadOllama(); load() })
1339
+ .catch((e) => setError(String(e?.message ?? e)))
1340
+ }
1341
+
1130
1342
  const openSelection = (row) => {
1131
1343
  const available = row.availableModels ?? row.models ?? []
1132
1344
  const defaults = row.selectableModels ?? available
@@ -1273,7 +1485,15 @@ window.__ModuleLoader__.load({
1273
1485
  const haystack = (m.id + ' ' + (m.name || '') + ' ' + (m.aliases || []).join(' ') + ' ' + (m.tags || []).join(' ') + ' ' + (m.description || '')).toLowerCase()
1274
1486
  return tokens.every((tok) => haystack.includes(tok))
1275
1487
  }) : filteredChoicesBase
1276
- const quickFilteredChoices = searchedChoices.filter((m) => {
1488
+ const taggedChoices = selectedTag === 'all'
1489
+ ? searchedChoices
1490
+ : selectedTag === 'curated'
1491
+ ? searchedChoices.filter((m) => isCuratedModel(m))
1492
+ : searchedChoices.filter((m) => {
1493
+ const tags = (m.tags && m.tags.length > 0) ? m.tags : deriveModelTags(m)
1494
+ return tags.includes(selectedTag)
1495
+ })
1496
+ const quickFilteredChoices = taggedChoices.filter((m) => {
1277
1497
  if (quickFilter.context100k && (!m.contextWindow || m.contextWindow < 100000)) return false
1278
1498
  if (quickFilter.cheap) {
1279
1499
  const price = m.pricing?.inputPerToken
@@ -1296,7 +1516,7 @@ window.__ModuleLoader__.load({
1296
1516
  }
1297
1517
  return (a.name || a.id || '').localeCompare(b.name || b.id || '')
1298
1518
  })
1299
- }, [choices, capabilityFilter, debouncedSearchQuery, quickFilter, sortBy])
1519
+ }, [choices, capabilityFilter, debouncedSearchQuery, selectedTag, quickFilter, sortBy])
1300
1520
 
1301
1521
  const maxContext = React.useMemo(() => filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0), [filteredChoices])
1302
1522
  const minPrice = React.useMemo(() => filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity), [filteredChoices])
@@ -1585,7 +1805,9 @@ window.__ModuleLoader__.load({
1585
1805
  button(t.refreshAll, () => execute(undefined)),
1586
1806
  button(dryRun ? t.run : t.apply, () => execute(provider || undefined), dryRun ? undefined : 'primary'),
1587
1807
  button(t.health, () => checkHealth(provider || undefined)),
1588
- button(t.checkCredentials, () => checkCredentials(provider || undefined))
1808
+ button(t.checkCredentials, () => checkCredentials(provider || undefined)),
1809
+ button(probingAll ? '⏳ ' + (t.probingInProgress || 'Verifying…') : (t.probeAll || '🔍 Verify All'), () => probeAllCaps(false)),
1810
+ button(t.probeNewOnly || '⚡ Verify New', () => probeAllCaps(true))
1589
1811
  ),
1590
1812
  busy ? React.createElement('span', null, t.running) : null,
1591
1813
  error ? React.createElement('div', { className: 'dms-alert dms-alert-bad' }, error) : null,
@@ -1631,7 +1853,8 @@ window.__ModuleLoader__.load({
1631
1853
  ),
1632
1854
  button(t.selectModels, () => openSelection(row)),
1633
1855
  button(t.policy, () => openPolicy(row)),
1634
- button(t.providerSettings || 'Configure', () => openProviderConfig(row))
1856
+ button(t.providerSettings || 'Configure', () => openProviderConfig(row)),
1857
+ button(probingProvider === row.provider ? '⏳' : (t.probeCapabilities || '🔍 Capabilities'), () => probeProviderCaps(row.provider))
1635
1858
  )
1636
1859
 
1637
1860
  const providerConfigEditor = providerConfigOpen === row.provider ? React.createElement('div', { className: 'dms-editor' },
@@ -1675,6 +1898,34 @@ window.__ModuleLoader__.load({
1675
1898
  )
1676
1899
  ) : null
1677
1900
 
1901
+
1902
+ const ollamaPanel = (row.provider === 'ollama') ? React.createElement('div', { style: { marginTop: 10, padding: '10px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l1)' } },
1903
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, t.ollamaHubTitle || '🦙 Ollama Local Models Hub'),
1904
+ React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
1905
+ React.createElement('input', {
1906
+ type: 'text',
1907
+ placeholder: t.ollamaPullPlaceholder || 'e.g. llama3.2:3b',
1908
+ value: ollamaPullInput,
1909
+ onChange: (e) => setOllamaPullInput(e.target.value),
1910
+ style: { flex: 1, padding: '4px 8px', borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)' }
1911
+ }),
1912
+ button(t.ollamaPullBtn || '⬇ Pull', () => pullOllamaModel(ollamaPullInput.trim()), 'primary')
1913
+ ),
1914
+ ollamaPullState && !ollamaPullState.done ? React.createElement('div', { style: { margin: '6px 0', fontSize: 12 } },
1915
+ `Downloading ${ollamaPullState.model}: ${ollamaPullState.status || ''} ${ollamaPullState.percent || 0}% (${ollamaPullState.completedFormatted || ''} / ${ollamaPullState.totalFormatted || ''})`,
1916
+ React.createElement('div', { style: { width: '100%', height: 6, background: 'var(--dsw-alias-border-l2)', borderRadius: 3, overflow: 'hidden', marginTop: 4 } },
1917
+ React.createElement('div', { style: { width: `${ollamaPullState.percent || 0}%`, height: '100%', background: 'var(--dsw-alias-state-business-primary)' } })
1918
+ )
1919
+ ) : null,
1920
+ ollamaPs.length > 0 ? React.createElement('div', { style: { marginTop: 8 } },
1921
+ React.createElement('small', { style: { fontWeight: 600, display: 'block', marginBottom: 4 } }, t.ollamaRunningVram || 'Active in VRAM (GPU):'),
1922
+ ollamaPs.map((rm) => React.createElement('div', { key: rm.name, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '3px 0', fontSize: 12 } },
1923
+ React.createElement('span', null, `⚡ ${rm.name} · VRAM: ${rm.sizeVramFormatted}`),
1924
+ button(t.ollamaUnloadBtn || '⏏ Unload', () => unloadOllama(rm.name))
1925
+ ))
1926
+ ) : React.createElement('small', { style: { display: 'block', color: 'var(--dsw-alias-label-secondary)', margin: '4px 0' } }, t.ollamaNoRunning || 'No models currently in VRAM')
1927
+ ) : null
1928
+
1678
1929
  const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' },
1679
1930
  React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`),
1680
1931
  React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } },
@@ -1700,6 +1951,13 @@ window.__ModuleLoader__.load({
1700
1951
  ))
1701
1952
  ) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities),
1702
1953
  React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } },
1954
+ button(t.selectCurated || '⭐ Curated Best', () => {
1955
+ const ids = new Set(selectionDraft)
1956
+ for (const m of choices) {
1957
+ if (isCuratedModel(m)) ids.add(m.id)
1958
+ }
1959
+ setSelectionDraft([...ids])
1960
+ }, 'primary'),
1703
1961
  button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]) }),
1704
1962
  button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))) }),
1705
1963
  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]) })
@@ -1710,6 +1968,22 @@ window.__ModuleLoader__.load({
1710
1968
  batchSummary ? React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.batchTryResults.replace('%d', batchSummary.reachable).replace('%d', batchSummary.total)) : null
1711
1969
  ),
1712
1970
  React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, `${filteredChoices.length}/${choices.length} ${t.capabilityCount}`),
1971
+ React.createElement('div', { style: { display: 'flex', gap: 6, margin: '8px 0', flexWrap: 'wrap', alignItems: 'center' } },
1972
+ [
1973
+ { id: 'all', label: t.tagAll || 'All' },
1974
+ { id: 'curated', label: t.tagCurated || '⭐ Curated' },
1975
+ { id: 'reasoning', label: t.tagReasoning || '🧠 Reasoning' },
1976
+ { id: 'coding', label: t.tagCoding || '💻 Coding' },
1977
+ { id: 'fast', label: t.tagFast || '⚡ Fast' },
1978
+ { id: 'vision', label: t.tagVision || '👁️ Vision' },
1979
+ { id: 'long-context', label: t.tagLongContext || '📜 128k+' },
1980
+ ].map((pill) => React.createElement('button', {
1981
+ key: pill.id,
1982
+ type: 'button',
1983
+ className: 'dms-pill' + (selectedTag === pill.id ? ' dms-pill-active' : ''),
1984
+ onClick: () => setSelectedTag(pill.id),
1985
+ }, pill.label))
1986
+ ),
1713
1987
  visibleChoices.map((model) => {
1714
1988
  const deprecationDays = (() => {
1715
1989
  const d = model.lifecycle?.deprecationDate
@@ -1717,7 +1991,8 @@ window.__ModuleLoader__.load({
1717
1991
  const diff = new Date(d).getTime() - Date.now()
1718
1992
  return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0
1719
1993
  })()
1720
- const probe = tryState[`${row.provider}:${model.id}`]
1994
+ const radar = status?.latencyRadar?.radar?.find((r) => r.provider === row.provider && r.model === model.id)
1995
+ const probe = tryState[`${row.provider}:${model.id}`] || (radar ? { ok: radar.ok, latency: radar.latencyMs, ttftMs: radar.ttftMs, sparkline: radar.sparkline, status: radar.status } : null)
1721
1996
  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' } },
1722
1997
  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)) }),
1723
1998
  ` ${model.name ?? model.id} (${model.id})`,
@@ -1749,12 +2024,46 @@ window.__ModuleLoader__.load({
1749
2024
  }, `🧠 ${model.reasoningConfig?.paramFormat || 'reasoning'}`) : null,
1750
2025
  (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,
1751
2026
  (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,
1752
- model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null,
2027
+ (() => {
2028
+ const modelTags = (model.tags && model.tags.length > 0) ? model.tags : deriveModelTags(model)
2029
+ if (!modelTags || modelTags.length === 0) return null
2030
+ return React.createElement('span', { style: { marginLeft: 6, display: 'inline-flex', gap: 3, verticalAlign: 'middle' } },
2031
+ modelTags.map((tag) => React.createElement('span', {
2032
+ key: tag,
2033
+ style: {
2034
+ fontSize: 10,
2035
+ padding: '0 4px',
2036
+ borderRadius: 3,
2037
+ background: 'var(--dsw-alias-bg-layer-2)',
2038
+ border: '1px solid var(--dsw-alias-border-l1)',
2039
+ color: 'var(--dsw-alias-label-secondary)',
2040
+ }
2041
+ }, tag === 'coding' ? '💻 coding' : tag === 'reasoning' ? '🧠 reasoning' : tag === 'fast' ? '⚡ fast' : tag === 'vision' ? '👁️ vision' : tag === 'long-context' ? '📜 128k+' : tag))
2042
+ )
2043
+ })(),
2044
+ (() => {
2045
+ const ver = verifiedCapabilities?.[`${row.provider}:${model.id}`]?.verified
2046
+ if (!ver) return null
2047
+ return React.createElement('span', { style: { marginLeft: 6, display: 'inline-flex', gap: 3, verticalAlign: 'middle' } },
2048
+ ver.tools ? React.createElement('span', { style: { fontSize: 10, padding: '0 4px', borderRadius: 3, background: 'rgba(34, 197, 94, 0.15)', color: '#22c55e', border: '1px solid rgba(34, 197, 94, 0.3)' } }, '✔ Tools') : null,
2049
+ ver.vision ? React.createElement('span', { style: { fontSize: 10, padding: '0 4px', borderRadius: 3, background: 'rgba(34, 197, 94, 0.15)', color: '#22c55e', border: '1px solid rgba(34, 197, 94, 0.3)' } }, '✔ Vision') : null,
2050
+ ver.jsonSchema ? React.createElement('span', { style: { fontSize: 10, padding: '0 4px', borderRadius: 3, background: 'rgba(34, 197, 94, 0.15)', color: '#22c55e', border: '1px solid rgba(34, 197, 94, 0.3)' } }, '✔ JSON') : null,
2051
+ ver.developerRole ? React.createElement('span', { style: { fontSize: 10, padding: '0 4px', borderRadius: 3, background: 'rgba(34, 197, 94, 0.15)', color: '#22c55e', border: '1px solid rgba(34, 197, 94, 0.3)' } }, '✔ DevRole') : null
2052
+ )
2053
+ })(),
1753
2054
  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,
1754
2055
  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,
1755
2056
  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,
1756
2057
  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) ? '…' : '▶'),
1757
- probe && !probe.loading ? React.createElement('span', { className: 'dms-badge ' + (probe.ok ? 'dms-badge-ok' : 'dms-badge-bad'), style: { marginLeft: 6 } }, probe.ok ? `${probe.latency}ms` : (probe.error || probe.status || 'offline')) : null
2058
+ probe && !probe.loading ? React.createElement('span', {
2059
+ 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'),
2060
+ style: { marginLeft: 6 },
2061
+ title: probe.ttftMs ? `TTFT: ${probe.ttftMs}ms | Latency: ${probe.latency}ms` : (probe.latency ? `${probe.latency}ms` : undefined)
2062
+ },
2063
+ (classifySpeedTier(probe.latency, probe.ok) === 'lightning' ? '⚡ ' : classifySpeedTier(probe.latency, probe.ok) === 'fast' ? '🟢 ' : classifySpeedTier(probe.latency, probe.ok) === 'moderate' ? '🟡 ' : '🔴 ') +
2064
+ (probe.ok ? `${probe.latency}ms` : (probe.error || probe.status || 'offline')) +
2065
+ (probe.sparkline ? ` ${probe.sparkline}` : '')
2066
+ ) : null
1758
2067
  )
1759
2068
  }),
1760
2069
  filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } },
@@ -1808,7 +2117,7 @@ window.__ModuleLoader__.load({
1808
2117
  )
1809
2118
  ) : null
1810
2119
 
1811
- return React.createElement('div', { key: row.provider, className: 'dms-provider-wrap' }, providerCard, providerConfigEditor, selectionEditor, policyEditor)
2120
+ return React.createElement('div', { key: row.provider, className: 'dms-provider-wrap' }, providerCard, providerConfigEditor, ollamaPanel, selectionEditor, policyEditor)
1812
2121
  }),
1813
2122
  React.createElement('div', { className: 'dms-section-card dms-section' },
1814
2123
  React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.modelAliases)),
@@ -1916,7 +2225,7 @@ window.__ModuleLoader__.load({
1916
2225
  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 }))))
1917
2226
  }
1918
2227
 
1919
- module.exports = { apply, inject: ['slots', 'locale', 'configForms'], filterModelsByCapabilities }
2228
+ module.exports = { apply, inject: ['slots', 'locale', 'configForms'], filterModelsByCapabilities, deriveModelTags, isCuratedModel, generateSparkline, classifySpeedTier }
1920
2229
  return module.exports
1921
2230
  },
1922
2231
  })