@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/README.md CHANGED
@@ -78,6 +78,9 @@ 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.
83
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.
@@ -165,6 +168,24 @@ dsh-model-sync:
165
168
  | `concurrency` | `number` | `4` | Maximum parallel provider synchronization workers |
166
169
  | `providers` | `array` | `[]` | Per-provider configuration list (`provider`, `enabled`, `autoApply`) |
167
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
+
168
189
  ---
169
190
 
170
191
  ## 📄 License
package/README.ru.md CHANGED
@@ -78,6 +78,9 @@ 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.
83
86
  * 📏 **Реальные лимиты контекста и авто-клампинг (`contextWindow` / `maxOutputTokens`)**: точное разделение входного и выходного лимита токенов, защита от ошибок 400 `max_tokens` и индикация `Вход: 128k | Выход: 8k` в Web UI.
@@ -165,6 +168,24 @@ dsh-model-sync:
165
168
  | `concurrency` | `number` | `4` | Максимальное количество параллельных задач синхронизации |
166
169
  | `providers` | `array` | `[]` | Список индивидуальных настроек провайдеров (`provider`, `enabled`, `autoApply`) |
167
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
+
168
189
  ---
169
190
 
170
191
  ## 📄 Лицензия
package/README.zh.md CHANGED
@@ -78,6 +78,9 @@ 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` 等虚拟别名及故障转移链,在熔断器打开或健康检查失败时自动切换至备用模型。
83
86
  * 📏 **真实上下文限制与边界安全 (`contextWindow` / `maxOutputTokens`)**:精确区分输入与输出令牌限制,通过自动限制防止 400 `max_tokens` 错误,并在 Web UI 中显示 `输入: 128k | 输出: 8k` 徽章。
@@ -165,6 +168,24 @@ dsh-model-sync:
165
168
  | `concurrency` | `number` | `4` | 最大并发服务商同步任务数 |
166
169
  | `providers` | `array` | `[]` | 服务商独立配置列表(`provider`、`enabled`、`autoApply`) |
167
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
+
168
189
  ---
169
190
 
170
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
+ }