@duckmind/dm-windows-x64 0.61.3 → 0.61.5

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/dm.exe CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "ok",
3
- "prepared_at": "2026-08-03T02:55:20.310618+00:00",
3
+ "prepared_at": "2026-08-19T15:37:04.127639+00:00",
4
4
  "managed_entries": [
5
5
  {
6
6
  "id": "dm-context",
@@ -46,6 +46,11 @@
46
46
  "id": "dm-caveman",
47
47
  "target_dir": "extensions/dm-caveman",
48
48
  "bundle_mode": "source-package"
49
+ },
50
+ {
51
+ "id": "dm-localllm-provider",
52
+ "target_dir": "extensions/dm-localllm-provider",
53
+ "bundle_mode": "source-package"
49
54
  }
50
55
  ],
51
56
  "local_entries": [
@@ -559,6 +564,44 @@
559
564
  "runtime_specifier_rewrites": 0
560
565
  }
561
566
  },
567
+ {
568
+ "id": "dm-localllm-provider",
569
+ "staged_dir": "extensions/dm-localllm-provider",
570
+ "bundle_mode": "source-package",
571
+ "dependencies_installed": false,
572
+ "dependency_mode": "platform-package-dependency",
573
+ "dependencies_declared": [
574
+ "@duckmind/dm-coding-agent"
575
+ ],
576
+ "dependency_patches": [
577
+ "dm.extensions ts->js",
578
+ "main ts->js",
579
+ "drop-devDependencies",
580
+ "drop-peerDependencies",
581
+ "drop-scripts",
582
+ "drop-files"
583
+ ],
584
+ "stripped_documentation": [
585
+ "LICENSE",
586
+ "README.md"
587
+ ],
588
+ "javascript_compile": {
589
+ "tool": "bun build --no-bundle",
590
+ "mode": "module-preserving-transpile",
591
+ "files": [
592
+ "detect.test.js",
593
+ "detect.js",
594
+ "index.test.js",
595
+ "index.js",
596
+ "keychain.test.js",
597
+ "keychain.js"
598
+ ],
599
+ "file_count": 6,
600
+ "before_bytes": 131212,
601
+ "after_bytes": 82230,
602
+ "runtime_specifier_rewrites": 4
603
+ }
604
+ },
562
605
  {
563
606
  "id": "dm-cua",
564
607
  "staged_dir": "extensions/dm-cua",
@@ -628,7 +671,7 @@
628
671
  },
629
672
  "runtime_loader": {
630
673
  "status": "verified",
631
- "entries": 12,
674
+ "entries": 13,
632
675
  "loader": "dm-coding-agent",
633
676
  "platform_dependencies": [
634
677
  "@ff-labs/fff-node",
@@ -0,0 +1,460 @@
1
+ const STANDALONE_PROBE_TIMEOUT_MS = 5000;
2
+ function recordFailure(diagnostics, url, status) {
3
+ diagnostics?.push({
4
+ url,
5
+ status,
6
+ reason: status === 401 ? "unauthorized" : status === 403 ? "forbidden" : "http-error"
7
+ });
8
+ }
9
+ function recordException(diagnostics, url, err) {
10
+ diagnostics?.push({
11
+ url,
12
+ reason: err instanceof Error && err.name === "AbortError" ? "timeout" : "network-error"
13
+ });
14
+ }
15
+ async function fetchJson(url, apiKey, signal, diagnostics) {
16
+ try {
17
+ const headers = {};
18
+ if (apiKey)
19
+ headers["Authorization"] = `Bearer ${apiKey}`;
20
+ const res = await fetch(url, {
21
+ headers,
22
+ signal: signal ?? AbortSignal.timeout(STANDALONE_PROBE_TIMEOUT_MS)
23
+ });
24
+ if (!res.ok) {
25
+ recordFailure(diagnostics, url, res.status);
26
+ return null;
27
+ }
28
+ return await res.json();
29
+ } catch (err) {
30
+ recordException(diagnostics, url, err);
31
+ return null;
32
+ }
33
+ }
34
+ async function postJson(url, apiKey, body, signal, diagnostics) {
35
+ try {
36
+ const headers = { "Content-Type": "application/json" };
37
+ if (apiKey)
38
+ headers["Authorization"] = `Bearer ${apiKey}`;
39
+ const res = await fetch(url, {
40
+ method: "POST",
41
+ headers,
42
+ body: JSON.stringify(body),
43
+ signal: signal ?? AbortSignal.timeout(STANDALONE_PROBE_TIMEOUT_MS)
44
+ });
45
+ if (!res.ok) {
46
+ recordFailure(diagnostics, url, res.status);
47
+ return null;
48
+ }
49
+ return await res.json();
50
+ } catch (err) {
51
+ recordException(diagnostics, url, err);
52
+ return null;
53
+ }
54
+ }
55
+ function capTokens(contextWindow, reasoning = false) {
56
+ return Math.min(Math.floor(contextWindow / 2), reasoning ? 65536 : 8192);
57
+ }
58
+ export async function detectMtplx(root, apiKey, signal, diagnostics) {
59
+ const health = await fetchJson(`${root}/health`, apiKey, signal, diagnostics);
60
+ if (!health?.model || typeof health.context_window !== "number")
61
+ return null;
62
+ const reasoning = health.enable_thinking === true || health.reasoning === "on";
63
+ const vision = health.vision?.enabled === true;
64
+ return {
65
+ apiType: "mtplx",
66
+ models: [
67
+ {
68
+ id: health.model,
69
+ name: health.model.split("/").pop() ?? health.model,
70
+ contextWindow: health.context_window,
71
+ maxTokens: health.max_response_tokens ?? capTokens(health.context_window, reasoning),
72
+ reasoning,
73
+ input: vision ? ["text", "image"] : ["text"]
74
+ }
75
+ ]
76
+ };
77
+ }
78
+ export async function detectOmlx(root, apiKey, signal, diagnostics) {
79
+ const res = await fetchJson(`${root}/v1/models/status`, apiKey, signal, diagnostics);
80
+ if (!res?.models?.length)
81
+ return null;
82
+ const models = [];
83
+ for (const m of res.models) {
84
+ if (!m.id || !m.model_type)
85
+ continue;
86
+ const type = m.model_type.toLowerCase();
87
+ if (type !== "llm" && type !== "vlm")
88
+ continue;
89
+ const contextWindow = m.max_context_window ?? 32768;
90
+ models.push({
91
+ id: m.id,
92
+ name: m.model_alias || m.display_name || m.id,
93
+ contextWindow,
94
+ maxTokens: m.max_tokens ?? capTokens(contextWindow, m.thinking_default === true),
95
+ reasoning: m.thinking_default === true,
96
+ input: type === "vlm" ? ["text", "image"] : ["text"],
97
+ loaded: m.loaded === true,
98
+ sizeBytes: firstNumber(m.estimated_size)
99
+ });
100
+ }
101
+ return models.length > 0 ? { apiType: "omlx", models } : null;
102
+ }
103
+ export async function detectLmStudio(root, apiKey, signal, diagnostics) {
104
+ const res = await fetchJson(`${root}/api/v1/models`, apiKey, signal, diagnostics);
105
+ if (!res?.models?.length)
106
+ return null;
107
+ const models = [];
108
+ for (const m of res.models) {
109
+ const type = (m.type ?? "").toLowerCase();
110
+ if (type !== "llm" && type !== "vlm")
111
+ continue;
112
+ const contextWindow = m.max_context_length ?? 32768;
113
+ models.push({
114
+ id: m.key,
115
+ name: m.display_name || m.key,
116
+ contextWindow,
117
+ maxTokens: capTokens(contextWindow, !!m.capabilities?.reasoning),
118
+ reasoning: !!m.capabilities?.reasoning,
119
+ input: m.capabilities?.vision || type === "vlm" ? ["text", "image"] : ["text"],
120
+ loaded: (m.loaded_instances?.length ?? 0) > 0,
121
+ sizeBytes: firstNumber(m.size_bytes),
122
+ quantization: m.quantization?.name
123
+ });
124
+ }
125
+ return models.length > 0 ? { apiType: "lmstudio", models } : null;
126
+ }
127
+ export async function detectLlamaCpp(root, apiKey, signal, diagnostics) {
128
+ const props = await fetchJson(`${root}/props`, apiKey, signal, diagnostics);
129
+ if (typeof props?.default_generation_settings?.n_ctx !== "number" || !props.model_path) {
130
+ return null;
131
+ }
132
+ const modelsRes = await fetchJson(`${root}/v1/models`, apiKey, signal, diagnostics);
133
+ const entry = modelsRes?.data?.[0];
134
+ const contextWindow = props.default_generation_settings.n_ctx || entry?.meta?.n_ctx_train || 32768;
135
+ const id = entry?.id ?? props.model_path;
136
+ return {
137
+ apiType: "llamacpp",
138
+ models: [
139
+ {
140
+ id,
141
+ name: id.split(/[\\/]/).pop() ?? id,
142
+ contextWindow,
143
+ maxTokens: capTokens(contextWindow),
144
+ reasoning: false,
145
+ input: props.modalities?.vision ? ["text", "image"] : ["text"],
146
+ sizeBytes: firstNumber(entry?.meta?.size)
147
+ }
148
+ ]
149
+ };
150
+ }
151
+ export async function detectOllama(root, apiKey, signal, diagnostics) {
152
+ const tags = await fetchJson(`${root}/api/tags`, apiKey, signal, diagnostics);
153
+ if (!tags?.models?.length)
154
+ return null;
155
+ const [shows, ps] = await Promise.all([
156
+ Promise.all(tags.models.map((m) => postJson(`${root}/api/show`, apiKey, { model: m.model }, signal, diagnostics))),
157
+ fetchJson(`${root}/api/ps`, apiKey, signal, diagnostics)
158
+ ]);
159
+ const runningModels = new Set(ps?.models?.map((m) => m.model) ?? []);
160
+ const models = tags.models.map((m, i) => {
161
+ const show = shows[i];
162
+ const info = show?.model_info;
163
+ const arch = typeof info?.["general.architecture"] === "string" ? info["general.architecture"] : undefined;
164
+ const rawContextWindow = arch ? info?.[`${arch}.context_length`] : undefined;
165
+ const contextWindow = typeof rawContextWindow === "number" ? rawContextWindow : 32768;
166
+ const capabilities = show?.capabilities ?? [];
167
+ return {
168
+ id: m.model,
169
+ name: m.name,
170
+ contextWindow,
171
+ maxTokens: capTokens(contextWindow, capabilities.includes("thinking")),
172
+ reasoning: capabilities.includes("thinking"),
173
+ input: capabilities.includes("vision") ? ["text", "image"] : ["text"],
174
+ loaded: runningModels.has(m.model),
175
+ sizeBytes: firstNumber(m.size),
176
+ quantization: m.details?.quantization_level
177
+ };
178
+ });
179
+ return { apiType: "ollama", models };
180
+ }
181
+ const THINKING_TIERS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
182
+ const PI_LEVELS = [
183
+ "off",
184
+ "minimal",
185
+ "low",
186
+ "medium",
187
+ "high",
188
+ "xhigh",
189
+ "max"
190
+ ];
191
+ async function probe(url, apiKey, payload, signal) {
192
+ try {
193
+ const headers = { "Content-Type": "application/json" };
194
+ if (apiKey)
195
+ headers["Authorization"] = `Bearer ${apiKey}`;
196
+ const res = await fetch(url, {
197
+ method: "POST",
198
+ headers,
199
+ body: JSON.stringify(payload),
200
+ signal: signal ?? AbortSignal.timeout(STANDALONE_PROBE_TIMEOUT_MS)
201
+ });
202
+ const body = await res.text().catch(() => "");
203
+ return { status: res.status, body };
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ async function probeStatus(url, apiKey, payload, signal) {
209
+ return (await probe(url, apiKey, payload, signal))?.status ?? null;
210
+ }
211
+ export function nearestTier(level, accepted) {
212
+ const wanted = PI_LEVELS.indexOf(level);
213
+ if (wanted < 0)
214
+ return;
215
+ const candidates = THINKING_TIERS.map((tier, i) => ({ tier, i })).filter((c) => c.tier !== "none" && accepted.includes(c.tier));
216
+ let best;
217
+ for (const c of candidates) {
218
+ if (!best || Math.abs(c.i - wanted) < Math.abs(best.i - wanted))
219
+ best = c;
220
+ }
221
+ return best?.tier;
222
+ }
223
+ export async function probeRequestCompat(baseUrl, apiKey, modelId, signal) {
224
+ const url = `${baseUrl}/chat/completions`;
225
+ const base = { model: modelId, max_tokens: 1, stream: false };
226
+ const user = { role: "user", content: "hi" };
227
+ const [tierStatuses, developerStatus] = await Promise.all([
228
+ Promise.all(THINKING_TIERS.map((tier) => probeStatus(url, apiKey, { ...base, messages: [user], reasoning_effort: tier }, signal))),
229
+ probeStatus(url, apiKey, { ...base, messages: [{ role: "developer", content: "You are terse." }, user] }, signal)
230
+ ]);
231
+ const isDefinitive = (s) => s === 200 || s === 400 || s === 422;
232
+ if (!tierStatuses.every(isDefinitive))
233
+ return {};
234
+ const accepted = THINKING_TIERS.filter((_, i) => tierStatuses[i] === 200);
235
+ const result = {};
236
+ if (accepted.length > 0) {
237
+ result.compat = { supportsReasoningEffort: true, supportsDeveloperRole: developerStatus === 200 };
238
+ const map = {};
239
+ if (accepted.includes("none"))
240
+ map.off = "none";
241
+ for (const level of PI_LEVELS) {
242
+ if (level === "off")
243
+ continue;
244
+ const tier = nearestTier(level, accepted);
245
+ if (tier)
246
+ map[level] = tier;
247
+ }
248
+ if (Object.keys(map).length > 0)
249
+ result.thinkingLevelMap = map;
250
+ } else if (developerStatus === 200) {
251
+ result.compat = { supportsDeveloperRole: true };
252
+ }
253
+ return result;
254
+ }
255
+ const QWEN_THINKING_TEMPERATURE = 0.6;
256
+ function samplingParamsFor(modelType, reasoning) {
257
+ if (!reasoning || !modelType)
258
+ return;
259
+ return /^qwen/i.test(modelType) ? { temperature: QWEN_THINKING_TEMPERATURE } : undefined;
260
+ }
261
+ export async function detectSglang(root, baseUrl, apiKey, signal, diagnostics) {
262
+ const info = await fetchJson(`${root}/get_model_info`, apiKey, signal, diagnostics);
263
+ if (!info?.model_path || info.is_generation !== true)
264
+ return null;
265
+ const [serverInfo, modelsRes] = await Promise.all([
266
+ fetchJson(`${root}/get_server_info`, apiKey, signal, diagnostics),
267
+ fetchJson(`${baseUrl}/models`, apiKey, signal, diagnostics)
268
+ ]);
269
+ const reasoning = typeof serverInfo?.reasoning_parser === "string" && serverInfo.reasoning_parser !== "";
270
+ const vision = info.has_image_understanding === true;
271
+ const entries = modelsRes?.data?.length ? modelsRes.data : [{ id: info.model_path }];
272
+ const requestCompat = reasoning ? await probeRequestCompat(baseUrl, apiKey, entries[0].id, signal) : {};
273
+ const samplingParams = samplingParamsFor(info.model_type, reasoning);
274
+ return {
275
+ apiType: "sglang",
276
+ models: entries.map((m) => {
277
+ const contextWindow = firstNumber(m.max_model_len) ?? 32768;
278
+ return {
279
+ id: m.id,
280
+ name: m.id.split(/[\\/]/).filter(Boolean).pop() ?? m.id,
281
+ contextWindow,
282
+ maxTokens: capTokens(contextWindow, reasoning),
283
+ reasoning,
284
+ input: vision ? ["text", "image"] : ["text"],
285
+ ...requestCompat,
286
+ ...samplingParams ? { samplingParams } : {}
287
+ };
288
+ })
289
+ };
290
+ }
291
+ export async function detectVllm(root, baseUrl, apiKey, signal, diagnostics) {
292
+ const version = await fetchJson(`${root}/version`, apiKey, signal, diagnostics);
293
+ if (typeof version?.version !== "string")
294
+ return null;
295
+ const modelsRes = await fetchJson(`${baseUrl}/models`, apiKey, signal, diagnostics);
296
+ const entries = (modelsRes?.data ?? []).filter((m) => typeof m.max_model_len === "number");
297
+ if (entries.length === 0)
298
+ return null;
299
+ return {
300
+ apiType: "vllm",
301
+ models: entries.map((m) => ({
302
+ id: m.id,
303
+ name: m.id.split("/").pop() ?? m.id,
304
+ contextWindow: m.max_model_len,
305
+ maxTokens: capTokens(m.max_model_len),
306
+ reasoning: false,
307
+ input: ["text"]
308
+ }))
309
+ };
310
+ }
311
+ function firstNumber(...values) {
312
+ for (const v of values)
313
+ if (typeof v === "number" && v > 0)
314
+ return v;
315
+ return;
316
+ }
317
+ const NINFER_OWNER = "ninfer";
318
+ export function isNinferCards(cards) {
319
+ return cards.length > 0 && cards.every((m) => m.owned_by === NINFER_OWNER);
320
+ }
321
+ const NINFER_DEFAULT_CONTEXT = 8192;
322
+ const NINFER_CONTEXT_PROBE_TOKENS = 300000;
323
+ const NINFER_PROBE_CHARS_PER_TOKEN = 2;
324
+ export function parseNinferContext(body) {
325
+ const match = /max_context\s+(\d+)/.exec(body);
326
+ if (!match)
327
+ return;
328
+ const value = Number(match[1]);
329
+ return Number.isFinite(value) && value > 0 ? value : undefined;
330
+ }
331
+ function acceptedPromptTokens(reply) {
332
+ if (reply.status !== 200)
333
+ return;
334
+ try {
335
+ const tokens = JSON.parse(reply.body).usage?.prompt_tokens;
336
+ return typeof tokens === "number" && tokens > 0 ? tokens : undefined;
337
+ } catch {
338
+ return;
339
+ }
340
+ }
341
+ export async function probeNinfer(baseUrl, apiKey, modelId, signal) {
342
+ const oversized = "x ".repeat(Math.ceil(NINFER_CONTEXT_PROBE_TOKENS * NINFER_PROBE_CHARS_PER_TOKEN / 2));
343
+ const [context, vision] = await Promise.all([
344
+ probe(`${baseUrl}/chat/completions`, apiKey, { model: modelId, max_tokens: 1, stream: false, messages: [{ role: "user", content: oversized }] }, signal),
345
+ probe(`${baseUrl}/responses/input_tokens`, apiKey, {
346
+ model: modelId,
347
+ input: [
348
+ {
349
+ role: "user",
350
+ content: [
351
+ {
352
+ type: "input_image",
353
+ image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
354
+ }
355
+ ]
356
+ }
357
+ ]
358
+ }, signal)
359
+ ]);
360
+ return {
361
+ contextWindow: (context && parseNinferContext(context.body)) ?? (context && acceptedPromptTokens(context)) ?? NINFER_DEFAULT_CONTEXT,
362
+ vision: vision === null ? undefined : vision.status === 200 ? true : /vision_disabled/.test(vision.body) ? false : undefined
363
+ };
364
+ }
365
+ async function ninferModels(cards, baseUrl, apiKey, signal) {
366
+ const [measured, compat] = await Promise.all([
367
+ probeNinfer(baseUrl, apiKey, cards[0].id, signal),
368
+ probeRequestCompat(baseUrl, apiKey, cards[0].id, signal)
369
+ ]);
370
+ const reasoning = Boolean(compat.thinkingLevelMap);
371
+ return cards.map((card) => ({
372
+ id: card.id,
373
+ name: card.name || (card.id.split(/[\\/]/).filter(Boolean).pop() ?? card.id),
374
+ contextWindow: measured.contextWindow,
375
+ maxTokens: capTokens(measured.contextWindow, reasoning),
376
+ reasoning,
377
+ input: measured.vision ? ["text", "image"] : ["text"],
378
+ ...compat
379
+ }));
380
+ }
381
+ const DS4_OWNER = "ds4.c";
382
+ const DS4_THINKING_LEVELS = {
383
+ off: "none",
384
+ max: "max"
385
+ };
386
+ export function isDs4Cards(cards) {
387
+ return cards.length > 0 && cards.every((m) => m.owned_by === DS4_OWNER);
388
+ }
389
+ export async function detectOpenAI(baseUrl, apiKey, signal, diagnostics) {
390
+ const res = await fetchJson(`${baseUrl}/models`, apiKey, signal, diagnostics);
391
+ if (!res?.data?.length)
392
+ return { apiType: "openai", models: [] };
393
+ if (isNinferCards(res.data)) {
394
+ return { apiType: "ninfer", models: await ninferModels(res.data, baseUrl, apiKey, signal) };
395
+ }
396
+ if (isDs4Cards(res.data)) {
397
+ return {
398
+ apiType: "ds4",
399
+ models: res.data.map((m) => {
400
+ const contextWindow = firstNumber(m.top_provider?.context_length, m.context_length) ?? 32768;
401
+ const declaredMax = firstNumber(m.top_provider?.max_completion_tokens);
402
+ return {
403
+ id: m.id,
404
+ name: m.name || m.id,
405
+ contextWindow,
406
+ maxTokens: declaredMax !== undefined && declaredMax < contextWindow ? declaredMax : capTokens(contextWindow, true),
407
+ reasoning: true,
408
+ input: ["text"],
409
+ compat: { supportsReasoningEffort: true, supportsDeveloperRole: true },
410
+ thinkingLevelMap: DS4_THINKING_LEVELS
411
+ };
412
+ })
413
+ };
414
+ }
415
+ return {
416
+ apiType: "openai",
417
+ models: res.data.map((m) => {
418
+ const contextWindow = firstNumber(m.max_model_len, m.top_provider?.context_length, m.context_window, m.context_length) ?? 32768;
419
+ const params = m.supported_parameters ?? [];
420
+ const modalities = m.architecture?.input_modalities ?? [];
421
+ const reasoning = params.includes("reasoning_effort") || params.includes("include_reasoning");
422
+ const declaredMax = firstNumber(m.top_provider?.max_completion_tokens);
423
+ const maxTokens = declaredMax !== undefined && declaredMax < contextWindow ? declaredMax : capTokens(contextWindow, reasoning);
424
+ return {
425
+ id: m.id,
426
+ name: m.name || (m.id.split("/").pop() ?? m.id),
427
+ contextWindow,
428
+ maxTokens,
429
+ reasoning,
430
+ input: modalities.includes("image") ? ["text", "image"] : ["text"]
431
+ };
432
+ })
433
+ };
434
+ }
435
+ function summarizeFailure(diagnostics) {
436
+ const authFailure = diagnostics.find((d) => d.reason === "unauthorized" || d.reason === "forbidden");
437
+ if (authFailure) {
438
+ return `Authentication failed (HTTP ${authFailure.status}) — check the API key.`;
439
+ }
440
+ if (diagnostics.length > 0 && diagnostics.every((d) => d.reason === "timeout")) {
441
+ return "Timed out waiting for a response — check the server is running and reachable.";
442
+ }
443
+ if (diagnostics.length > 0 && diagnostics.every((d) => d.reason === "timeout" || d.reason === "network-error")) {
444
+ return "Could not connect to the server — check the URL and that it's running.";
445
+ }
446
+ return;
447
+ }
448
+ const CHAIN_TIMEOUT_MS = 8000;
449
+ export async function detectModels(baseUrl, apiKey, signal) {
450
+ const chainSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(CHAIN_TIMEOUT_MS)]) : AbortSignal.timeout(CHAIN_TIMEOUT_MS);
451
+ const root = baseUrl.replace(/\/v1$/, "");
452
+ const diagnostics = [];
453
+ const result = await detectMtplx(root, apiKey, chainSignal, diagnostics) ?? await detectOmlx(root, apiKey, chainSignal, diagnostics) ?? await detectLmStudio(root, apiKey, chainSignal, diagnostics) ?? await detectLlamaCpp(root, apiKey, chainSignal, diagnostics) ?? await detectSglang(root, baseUrl, apiKey, chainSignal, diagnostics) ?? await detectOllama(root, apiKey, chainSignal, diagnostics) ?? await detectVllm(root, baseUrl, apiKey, chainSignal, diagnostics) ?? await detectOpenAI(baseUrl, apiKey, chainSignal, diagnostics);
454
+ if (result.models.length === 0) {
455
+ const error = summarizeFailure(diagnostics);
456
+ if (error)
457
+ return { ...result, error };
458
+ }
459
+ return result;
460
+ }