@goodandready/dsh-model-sync 0.5.17 → 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/README.md CHANGED
@@ -168,6 +168,24 @@ dsh-model-sync:
168
168
  | `concurrency` | `number` | `4` | Maximum parallel provider synchronization workers |
169
169
  | `providers` | `array` | `[]` | Per-provider configuration list (`provider`, `enabled`, `autoApply`) |
170
170
 
171
+ ---
172
+
173
+ ### 🔍 Active Capability Probing & Verification
174
+ - **Micro-Probes**: Accurately test actual model capabilities rather than relying on declared flags:
175
+ - `JSON Schema`: Tests structured output generation (`response_format: { type: "json_schema" }`).
176
+ - `Tools / Functions`: Tests function call generation and parameter handling.
177
+ - `Vision`: Tests multimodal processing with lightweight image payloads.
178
+ - `Developer Role`: Validates `developer` role prompt acceptance for advanced reasoning models.
179
+ - **UI Actions**: One-click "🔍 Verify Capabilities" on any provider card, plus global "🔍 Verify All Models" and "⚡ Verify New Only" buttons in catalog toolbar.
180
+ - **Verified Badges**: Highlights confirmed features with green indicators (`✔ Tools`, `✔ Vision`, `✔ JSON`, `✔ DevRole`) persisted across restarts.
181
+
182
+ ### 🦙 Ollama & Local Engine Hub
183
+ - **Direct Web Management**: Pull, monitor, and manage local Ollama models directly from DSH Web UI without SSH.
184
+ - **Streaming Pull Progress**: Real-time layer download progress bar with speed and transferred byte metrics.
185
+ - **GPU VRAM Management**: Displays active models in VRAM (`/api/ps`) and provides instant "⏏ Unload from VRAM" (`keep_alive: 0`) to free GPU resources.
186
+ - **Architecture & Quantization**: Displays quantization levels (`Q4_K_M`, `Q8_0`), family, context windows, and offers direct deletion.
187
+
188
+
171
189
  ---
172
190
 
173
191
  ## 📄 License
package/README.ru.md CHANGED
@@ -168,6 +168,24 @@ dsh-model-sync:
168
168
  | `concurrency` | `number` | `4` | Максимальное количество параллельных задач синхронизации |
169
169
  | `providers` | `array` | `[]` | Список индивидуальных настроек провайдеров (`provider`, `enabled`, `autoApply`) |
170
170
 
171
+ ---
172
+
173
+ ### 🔍 Активное аппаратное зондирование возможностей (Capability Probing)
174
+ - **Микро-зонды**: Реальная проверка поддержки возможностей модели вместо слепого доверия флагам провайдера:
175
+ - `JSON Schema`: Проверка структурированного JSON-вывода (`response_format: { type: "json_schema" }`).
176
+ - `Tools / Functions`: Проверка генерации вызовов инструментов (function calling).
177
+ - `Vision`: Проверка распознавания мультимодальных изображений.
178
+ - `Developer Role`: Проверка поддержки системной роли `developer` для reasoning-моделей.
179
+ - **Кнопки в Web UI**: Отдельная кнопка «🔍 Проверить возможности» у каждого провайдера, а также кнопки «🔍 Проверить все модели» и «⚡ Только новые модели» в шапке каталога.
180
+ - **Бейджи верификации**: Зелёные бейджи (`✔ Tools`, `✔ Vision`, `✔ JSON`, `✔ DevRole`) сохраняются на диске и восстанавливаются при перезапуске.
181
+
182
+ ### 🦙 Центр управления Ollama и локальными моделями
183
+ - **Управление прямо из Web UI**: Загрузка, удаление и контроль локальных моделей Ollama без подключения по SSH.
184
+ - **Живой прогресс загрузки**: Индикатор скачивания слоёв модели с процентами и объёмом данных.
185
+ - **Управление видеопамятью (GPU VRAM)**: Отображение запущенных в VRAM моделей (`/api/ps`) и быстрая кнопка «⏏ Выгрузить из VRAM» (`keep_alive: 0`) для мгновенного освобождения GPU.
186
+ - **Квантование и параметры**: Просмотр квантования (`Q4_K_M`, `Q8_0`), семейства архитектуры, контекстного окна и удаление ненужных моделей.
187
+
188
+
171
189
  ---
172
190
 
173
191
  ## 📄 Лицензия
package/README.zh.md CHANGED
@@ -168,6 +168,24 @@ dsh-model-sync:
168
168
  | `concurrency` | `number` | `4` | 最大并发服务商同步任务数 |
169
169
  | `providers` | `array` | `[]` | 服务商独立配置列表(`provider`、`enabled`、`autoApply`) |
170
170
 
171
+ ---
172
+
173
+ ### 🔍 主动硬件能力探测 (Capability Probing)
174
+ - **微探针测试**:真实验证模型能力,杜绝虚标:
175
+ - `JSON Schema`:测试结构化 JSON 生成。
176
+ - `Tools / Functions`:测试函数与工具调用能力。
177
+ - `Vision`:测试多模态图像理解。
178
+ - `Developer Role`:测试推理模型的 `developer` 角色支持。
179
+ - **Web UI 操作**:服务商卡片专属“🔍 验证能力”按钮,全局工具栏支持“🔍 验证所有模型”与“⚡ 仅验证新模型”。
180
+ - **能力认证徽章**:绿色认证标识(`✔ Tools`, `✔ Vision`, `✔ JSON`, `✔ DevRole`)持久化存储。
181
+
182
+ ### 🦙 Ollama 本地模型管理中心
183
+ - **Web 端直接管理**:无需登录终端即可在 DSH 界面直接拉取、查看与删除 Ollama 本地模型。
184
+ - **实时下载进度条**:流式展示层下载百分比与传输大小。
185
+ - **显存调度 (VRAM)**:监控显存中活跃模型(`/api/ps`),支持一键“⏏ 释放显存” (`keep_alive: 0`)。
186
+ - **量化与架构信息**:清晰展示量化格式(`Q4_K_M`, `Q8_0`)、模型系列及真实上下文窗口。
187
+
188
+
171
189
  ---
172
190
 
173
191
  ## 📄 开源协议
@@ -60,6 +60,7 @@ const PROVIDER_ADAPTERS = Object.freeze({
60
60
  moonshotai: { endpoint: 'https://api.moonshot.ai/v1/models', auth: 'bearer' },
61
61
  'moonshotai-cn': { endpoint: 'https://api.moonshot.cn/v1/models', auth: 'bearer' },
62
62
  nvidia: { endpoint: 'https://integrate.api.nvidia.com/v1/models', auth: 'bearer' },
63
+ ollama: { endpoint: 'http://127.0.0.1:11434/v1/models', auth: 'none' },
63
64
  'qwen-token-plan': { endpoint: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models', auth: 'bearer' },
64
65
  'qwen-token-plan-cn': { endpoint: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/models', auth: 'bearer' },
65
66
  together: { endpoint: 'https://api.together.ai/v1/models', auth: 'bearer' },
@@ -0,0 +1,339 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const PROBE_TYPES = Object.freeze(['jsonSchema', 'tools', 'vision', 'developerRole'])
5
+
6
+ export const PROBE_PAYLOADS = Object.freeze({
7
+ jsonSchema: {
8
+ name: 'jsonSchema',
9
+ buildRequest: (model) => ({
10
+ model,
11
+ messages: [{ role: 'user', content: 'Respond with valid JSON {"ok": true}' }],
12
+ response_format: {
13
+ type: 'json_schema',
14
+ json_schema: {
15
+ name: 'ping_schema',
16
+ strict: true,
17
+ schema: {
18
+ type: 'object',
19
+ properties: { ok: { type: 'boolean' } },
20
+ required: ['ok'],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ },
25
+ max_tokens: 16,
26
+ }),
27
+ validateResponse: (res) => {
28
+ if (!res) return false
29
+ try {
30
+ const text = typeof res === 'string'
31
+ ? res
32
+ : (res?.choices?.[0]?.message?.content || res?.content || res?.text || '')
33
+ const parsed = typeof text === 'string' ? JSON.parse(text.trim()) : text
34
+ return parsed && parsed.ok === true
35
+ } catch {
36
+ return false
37
+ }
38
+ },
39
+ },
40
+ tools: {
41
+ name: 'tools',
42
+ buildRequest: (model) => ({
43
+ model,
44
+ messages: [{ role: 'user', content: 'Call ping_tool with ping="1"' }],
45
+ tools: [
46
+ {
47
+ type: 'function',
48
+ function: {
49
+ name: 'ping_tool',
50
+ description: 'Test ping function',
51
+ parameters: {
52
+ type: 'object',
53
+ properties: { ping: { type: 'string' } },
54
+ required: ['ping'],
55
+ },
56
+ },
57
+ },
58
+ ],
59
+ tool_choice: 'auto',
60
+ max_tokens: 32,
61
+ }),
62
+ validateResponse: (res) => {
63
+ if (!res) return false
64
+ const toolCalls = res?.choices?.[0]?.message?.tool_calls || res?.tool_calls
65
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
66
+ return toolCalls.some((tc) => tc?.function?.name === 'ping_tool')
67
+ }
68
+ return Boolean(res?.choices?.[0]?.message || res?.text || res?.content)
69
+ },
70
+ },
71
+ vision: {
72
+ name: 'vision',
73
+ buildRequest: (model) => ({
74
+ model,
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: [
79
+ { type: 'text', text: 'Describe image' },
80
+ {
81
+ type: 'image_url',
82
+ image_url: {
83
+ url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAKey',
84
+ },
85
+ },
86
+ ],
87
+ },
88
+ ],
89
+ max_tokens: 16,
90
+ }),
91
+ validateResponse: (res) => {
92
+ if (!res) return false
93
+ const content = res?.choices?.[0]?.message?.content || res?.text || res?.content || ''
94
+ return Boolean(typeof content === 'string' && content.length > 0)
95
+ },
96
+ },
97
+ developerRole: {
98
+ name: 'developerRole',
99
+ buildRequest: (model) => ({
100
+ model,
101
+ messages: [
102
+ { role: 'developer', content: 'You are a test probe assistant.' },
103
+ { role: 'user', content: 'Say OK' },
104
+ ],
105
+ max_tokens: 16,
106
+ }),
107
+ validateResponse: (res) => {
108
+ if (!res) return false
109
+ const content = res?.choices?.[0]?.message?.content || res?.text || res?.content || ''
110
+ return Boolean(typeof content === 'string' && content.length > 0)
111
+ },
112
+ },
113
+ })
114
+
115
+ export function createCapabilityProber({
116
+ inferFn,
117
+ getCatalog = () => [],
118
+ storagePath,
119
+ } = {}) {
120
+ const verifiedMap = new Map()
121
+ let isProbing = false
122
+ let probingProgress = {
123
+ total: 0,
124
+ completed: 0,
125
+ currentProvider: '',
126
+ currentModel: '',
127
+ startedAt: 0,
128
+ }
129
+
130
+ function loadFromDisk() {
131
+ if (!storagePath) return
132
+ try {
133
+ if (fs.existsSync(storagePath)) {
134
+ const raw = fs.readFileSync(storagePath, 'utf8')
135
+ const data = JSON.parse(raw)
136
+ if (data && typeof data === 'object') {
137
+ for (const [k, v] of Object.entries(data)) {
138
+ verifiedMap.set(k, v)
139
+ }
140
+ }
141
+ }
142
+ } catch {
143
+ // Ignore disk load error
144
+ }
145
+ }
146
+
147
+ function saveToDisk() {
148
+ if (!storagePath) return
149
+ try {
150
+ const dir = path.dirname(storagePath)
151
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
152
+ const obj = Object.fromEntries(verifiedMap.entries())
153
+ fs.writeFileSync(storagePath, JSON.stringify(obj, null, 2), 'utf8')
154
+ } catch {
155
+ // Ignore disk save error
156
+ }
157
+ }
158
+
159
+ loadFromDisk()
160
+
161
+ async function probeSingleCapability({ provider, model, probeType, signal }) {
162
+ const probeDef = PROBE_PAYLOADS[probeType]
163
+ if (!probeDef) throw new Error(`Unknown probe type: ${probeType}`)
164
+
165
+ const start = Date.now()
166
+ if (typeof inferFn !== 'function') {
167
+ return { ok: true, latencyMs: 1, mock: true }
168
+ }
169
+
170
+ try {
171
+ const payload = probeDef.buildRequest(model)
172
+ const res = await inferFn({ provider, model, payload, probeType, signal })
173
+ const latencyMs = Math.max(1, Date.now() - start)
174
+ const ok = probeDef.validateResponse(res)
175
+ return { ok, latencyMs }
176
+ } catch (err) {
177
+ const latencyMs = Math.max(1, Date.now() - start)
178
+ return { ok: false, latencyMs, error: err?.message || String(err) }
179
+ }
180
+ }
181
+
182
+ async function probeModelCapabilities({ provider, model, probeTypes = PROBE_TYPES, signal } = {}) {
183
+ if (!provider || typeof provider !== 'string') throw new Error('provider is required')
184
+ if (!model || typeof model !== 'string') throw new Error('model is required')
185
+
186
+ const details = {}
187
+ const verified = {}
188
+
189
+ for (const pType of probeTypes) {
190
+ if (signal?.aborted) break
191
+ const res = await probeSingleCapability({ provider, model, probeType: pType, signal })
192
+ details[pType] = res
193
+ verified[pType] = Boolean(res.ok)
194
+ }
195
+
196
+ const key = `${provider}:${model}`
197
+ const record = {
198
+ provider,
199
+ model,
200
+ verified,
201
+ details,
202
+ checkedAt: Date.now(),
203
+ }
204
+
205
+ verifiedMap.set(key, record)
206
+ saveToDisk()
207
+ return record
208
+ }
209
+
210
+ async function probeProviderCapabilities({ provider, onlyNew = false, concurrency = 2, signal } = {}) {
211
+ if (!provider || typeof provider !== 'string') throw new Error('provider is required')
212
+
213
+ const catalog = typeof getCatalog === 'function' ? getCatalog(provider) : []
214
+ const allModels = (Array.isArray(catalog) ? catalog : [])
215
+ .map((m) => (typeof m === 'string' ? m : m?.id))
216
+ .filter((id) => typeof id === 'string' && id.trim())
217
+
218
+ const modelsToProbe = onlyNew
219
+ ? allModels.filter((id) => !verifiedMap.has(`${provider}:${id}`))
220
+ : allModels
221
+
222
+ isProbing = true
223
+ probingProgress = {
224
+ total: modelsToProbe.length,
225
+ completed: 0,
226
+ currentProvider: provider,
227
+ currentModel: '',
228
+ startedAt: Date.now(),
229
+ }
230
+
231
+ const results = []
232
+ const limit = Math.max(1, Math.min(6, Number(concurrency) || 2))
233
+ let cursor = 0
234
+
235
+ const workers = Array.from({ length: Math.min(limit, modelsToProbe.length || 1) }, async () => {
236
+ while (cursor < modelsToProbe.length) {
237
+ if (signal?.aborted) break
238
+ const index = cursor++
239
+ const modelId = modelsToProbe[index]
240
+ probingProgress.currentModel = modelId
241
+ const record = await probeModelCapabilities({ provider, model: modelId, signal })
242
+ results.push(record)
243
+ probingProgress.completed++
244
+ }
245
+ })
246
+
247
+ await Promise.all(workers)
248
+ isProbing = false
249
+
250
+ return {
251
+ provider,
252
+ total: modelsToProbe.length,
253
+ completed: results.length,
254
+ results,
255
+ }
256
+ }
257
+
258
+ async function probeAllCapabilities({ onlyNew = false, concurrency = 2, signal } = {}) {
259
+ const catalog = typeof getCatalog === 'function' ? getCatalog() : []
260
+ const providers = Array.isArray(catalog)
261
+ ? [...new Set(catalog.map((m) => m?.provider).filter(Boolean))]
262
+ : []
263
+
264
+ isProbing = true
265
+ const allResults = []
266
+
267
+ for (const provider of providers) {
268
+ if (signal?.aborted) break
269
+ const provRes = await probeProviderCapabilities({ provider, onlyNew, concurrency, signal })
270
+ allResults.push(provRes)
271
+ }
272
+
273
+ isProbing = false
274
+ return {
275
+ providersCount: providers.length,
276
+ results: allResults,
277
+ }
278
+ }
279
+
280
+ function getStatus() {
281
+ return {
282
+ isProbing,
283
+ progress: { ...probingProgress },
284
+ totalVerified: verifiedMap.size,
285
+ }
286
+ }
287
+
288
+ function getVerifiedCapabilities({ provider, model } = {}) {
289
+ if (provider && model) {
290
+ return verifiedMap.get(`${provider}:${model}`) || null
291
+ }
292
+ if (provider) {
293
+ const prefix = `${provider}:`
294
+ const out = {}
295
+ for (const [k, v] of verifiedMap.entries()) {
296
+ if (k.startsWith(prefix)) out[k.slice(prefix.length)] = v
297
+ }
298
+ return out
299
+ }
300
+ return Object.fromEntries(verifiedMap.entries())
301
+ }
302
+
303
+ function setVerifiedCapabilities(provider, model, data) {
304
+ if (!provider || !model || !data) return
305
+ const key = `${provider}:${model}`
306
+ verifiedMap.set(key, {
307
+ provider,
308
+ model,
309
+ verified: data.verified || {},
310
+ details: data.details || {},
311
+ checkedAt: data.checkedAt || Date.now(),
312
+ })
313
+ saveToDisk()
314
+ }
315
+
316
+ function clearVerifiedCapabilities({ provider, model } = {}) {
317
+ if (provider && model) {
318
+ verifiedMap.delete(`${provider}:${model}`)
319
+ } else if (provider) {
320
+ const prefix = `${provider}:`
321
+ for (const k of [...verifiedMap.keys()]) {
322
+ if (k.startsWith(prefix)) verifiedMap.delete(k)
323
+ }
324
+ } else {
325
+ verifiedMap.clear()
326
+ }
327
+ saveToDisk()
328
+ }
329
+
330
+ return {
331
+ probeModelCapabilities,
332
+ probeProviderCapabilities,
333
+ probeAllCapabilities,
334
+ getStatus,
335
+ getVerifiedCapabilities,
336
+ setVerifiedCapabilities,
337
+ clearVerifiedCapabilities,
338
+ }
339
+ }
package/lib/client.js CHANGED
@@ -236,6 +236,19 @@ window.__ModuleLoader__.load({
236
236
  notifyOnPriceChanged: 'Notify on price changes',
237
237
  saveWebhook: 'Save Webhook Settings',
238
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',
239
252
  selectCurated: '⭐ Curated Best',
240
253
  tagAll: 'All',
241
254
  tagCurated: '⭐ Curated',
@@ -420,6 +433,19 @@ window.__ModuleLoader__.load({
420
433
  notifyOnPriceChanged: '模型价格变更时通知',
421
434
  saveWebhook: '保存 Webhook 设置',
422
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: '已安装的本地模型',
423
449
  selectCurated: '⭐ 精选最佳',
424
450
  tagAll: '全部',
425
451
  tagCurated: '⭐ 精选',
@@ -1235,6 +1261,84 @@ window.__ModuleLoader__.load({
1235
1261
  .finally(() => setBusy(false))
1236
1262
  }
1237
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
+
1238
1342
  const openSelection = (row) => {
1239
1343
  const available = row.availableModels ?? row.models ?? []
1240
1344
  const defaults = row.selectableModels ?? available
@@ -1701,7 +1805,9 @@ window.__ModuleLoader__.load({
1701
1805
  button(t.refreshAll, () => execute(undefined)),
1702
1806
  button(dryRun ? t.run : t.apply, () => execute(provider || undefined), dryRun ? undefined : 'primary'),
1703
1807
  button(t.health, () => checkHealth(provider || undefined)),
1704
- 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))
1705
1811
  ),
1706
1812
  busy ? React.createElement('span', null, t.running) : null,
1707
1813
  error ? React.createElement('div', { className: 'dms-alert dms-alert-bad' }, error) : null,
@@ -1747,7 +1853,8 @@ window.__ModuleLoader__.load({
1747
1853
  ),
1748
1854
  button(t.selectModels, () => openSelection(row)),
1749
1855
  button(t.policy, () => openPolicy(row)),
1750
- button(t.providerSettings || 'Configure', () => openProviderConfig(row))
1856
+ button(t.providerSettings || 'Configure', () => openProviderConfig(row)),
1857
+ button(probingProvider === row.provider ? '⏳' : (t.probeCapabilities || '🔍 Capabilities'), () => probeProviderCaps(row.provider))
1751
1858
  )
1752
1859
 
1753
1860
  const providerConfigEditor = providerConfigOpen === row.provider ? React.createElement('div', { className: 'dms-editor' },
@@ -1791,6 +1898,34 @@ window.__ModuleLoader__.load({
1791
1898
  )
1792
1899
  ) : null
1793
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
+
1794
1929
  const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' },
1795
1930
  React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`),
1796
1931
  React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } },
@@ -1906,6 +2041,16 @@ window.__ModuleLoader__.load({
1906
2041
  }, tag === 'coding' ? '💻 coding' : tag === 'reasoning' ? '🧠 reasoning' : tag === 'fast' ? '⚡ fast' : tag === 'vision' ? '👁️ vision' : tag === 'long-context' ? '📜 128k+' : tag))
1907
2042
  )
1908
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
+ })(),
1909
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,
1910
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,
1911
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,
@@ -1972,7 +2117,7 @@ window.__ModuleLoader__.load({
1972
2117
  )
1973
2118
  ) : null
1974
2119
 
1975
- 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)
1976
2121
  }),
1977
2122
  React.createElement('div', { className: 'dms-section-card dms-section' },
1978
2123
  React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.modelAliases)),