@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 +18 -0
- package/README.ru.md +18 -0
- package/README.zh.md +18 -0
- package/lib/adapter-registry.js +1 -0
- package/lib/capability-prober.js +339 -0
- package/lib/client.js +148 -3
- package/lib/http.js +164 -38
- package/lib/inventory.js +1 -0
- package/lib/ollama-manager.js +330 -0
- package/lib/provider-service.js +14 -0
- package/lib/synchronizer.js +68 -1
- package/package.json +1 -1
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
|
## 📄 开源协议
|
package/lib/adapter-registry.js
CHANGED
|
@@ -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)),
|