kiku-agent 1.2.0 → 1.3.0

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.
Files changed (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +528 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -0,0 +1,572 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import {
4
+ classifyAllocation,
5
+ estimateVramFootprint,
6
+ gbToBytes,
7
+ knownSwaProfile,
8
+ normalizeQuantName,
9
+ recommendAdjustments,
10
+ } from '../../src/lib/vram/estimator.js';
11
+ import { matchModelIdentifier } from './config.js';
12
+ import { getGpuMetrics } from './gpuMetrics.js';
13
+ import { listInstalledGgufs, resolveModelConfig } from './modelConfig.js';
14
+ import { classifyHardwareTier, calculateDynamicVramBudget, HARDWARE_TIERS } from './telemetry/hardwareTiers.js';
15
+ import { techPowerUpClient } from './telemetry/techPowerUpClient.js';
16
+
17
+ function runProcessCapture(cmd, args, timeoutMs = 2500) {
18
+ return new Promise((resolve) => {
19
+ let stdout = '';
20
+ let stderr = '';
21
+ let settled = false;
22
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
23
+ const finish = (result) => {
24
+ if (settled) return;
25
+ settled = true;
26
+ clearTimeout(timer);
27
+ resolve(result);
28
+ };
29
+ const timer = setTimeout(() => {
30
+ try { child.kill('SIGKILL'); } catch { /* already dead */ }
31
+ finish({ ok: false, stdout: '', stderr: 'timeout' });
32
+ }, timeoutMs);
33
+ child.stdout?.on('data', d => { stdout += d.toString(); });
34
+ child.stderr?.on('data', d => { stderr += d.toString(); });
35
+ child.on('error', (err) => finish({ ok: false, stdout: '', stderr: err.message }));
36
+ child.on('close', (code) => finish({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() }));
37
+ });
38
+ }
39
+
40
+ function parseRocmSmiOutput(stdout) {
41
+
42
+ const rows = stdout.split('\n').map(l => l.trim()).filter(Boolean);
43
+ const gpus = [];
44
+ for (const row of rows) {
45
+ const cells = row.split(',').map(c => c.trim());
46
+ if (cells[0] === '' || Number.isNaN(Number(cells[0]))) continue;
47
+ const totalBytes = Number(cells[1]);
48
+ const usedBytes = cells.length > 2 ? Number(cells[2]) : NaN;
49
+ if (Number.isFinite(totalBytes) && totalBytes > 0) {
50
+ gpus.push({ totalBytes, usedBytes: Number.isFinite(usedBytes) ? usedBytes : null });
51
+ }
52
+ }
53
+ return gpus;
54
+ }
55
+
56
+ async function detectAmdGpu() {
57
+ const smi = await runProcessCapture('rocm-smi', ['--showmeminfo', 'vram', '--csv']);
58
+ if (!smi.ok || !smi.stdout) return null;
59
+ const gpus = parseRocmSmiOutput(smi.stdout);
60
+ if (gpus.length === 0) return null;
61
+ return {
62
+ available: true,
63
+ vendor: 'AMD',
64
+ name: 'AMD GPU (ROCm)',
65
+ totalBytes: gpus[0].totalBytes,
66
+ freeBytes: gpus[0].usedBytes !== null ? gpus[0].totalBytes - gpus[0].usedBytes : null,
67
+ usedBytes: gpus[0].usedBytes,
68
+ source: 'rocm-smi',
69
+ approximate: false,
70
+ count: gpus.length,
71
+ };
72
+ }
73
+
74
+ function appleUnifiedMemoryTarget() {
75
+ const totalBytes = Math.round(os.totalmem() * 0.70);
76
+ return {
77
+ available: true,
78
+ vendor: 'Apple',
79
+ name: `${os.cpus()[0]?.model?.includes('Apple') ? 'Apple Silicon' : 'Unified Memory'} (Metal)`,
80
+ totalBytes,
81
+ freeBytes: null,
82
+ usedBytes: null,
83
+ source: 'unified-memory-heuristic',
84
+ approximate: true,
85
+ count: 1,
86
+ };
87
+ }
88
+
89
+
90
+ export async function getHardwareTarget() {
91
+ const envOverride = Number(process.env.KIKU_VRAM_TOTAL_GB);
92
+ if (Number.isFinite(envOverride) && envOverride > 0) {
93
+ return {
94
+ available: true,
95
+ vendor: 'Configured',
96
+ name: 'Configured VRAM budget (KIKU_VRAM_TOTAL_GB)',
97
+ totalBytes: Math.round(gbToBytes(envOverride)),
98
+ freeBytes: null,
99
+ usedBytes: null,
100
+ source: 'env-override',
101
+ approximate: false,
102
+ count: 1,
103
+ };
104
+ }
105
+
106
+ try {
107
+ const probe = await getGpuMetrics();
108
+ if (probe?.available && probe.primary) {
109
+ const primary = probe.primary;
110
+ if (primary.vendor === 'Apple' && process.platform === 'darwin') {
111
+ return appleUnifiedMemoryTarget();
112
+ }
113
+
114
+ if (primary.totalMb > 0) {
115
+ return {
116
+ available: true,
117
+ vendor: primary.vendor,
118
+ name: primary.name,
119
+ totalBytes: Math.round(primary.totalMb * 1024 * 1024),
120
+ freeBytes: primary.freeMb !== null && primary.freeMb !== undefined
121
+ ? Math.round(primary.freeMb * 1024 * 1024)
122
+ : null,
123
+ usedBytes: primary.usedMb !== null && primary.usedMb !== undefined
124
+ ? Math.round(primary.usedMb * 1024 * 1024)
125
+ : null,
126
+ source: primary.source || 'gpu-probe',
127
+ approximate: Boolean(primary.approximate),
128
+ count: probe.count,
129
+ };
130
+ }
131
+ }
132
+ } catch {
133
+
134
+ }
135
+
136
+ if (process.platform !== 'win32') {
137
+ const amd = await detectAmdGpu();
138
+ if (amd) return amd;
139
+ if (process.platform === 'darwin') return appleUnifiedMemoryTarget();
140
+ }
141
+
142
+ return {
143
+ available: false,
144
+ vendor: null,
145
+ name: null,
146
+ totalBytes: null,
147
+ freeBytes: null,
148
+ usedBytes: null,
149
+ source: null,
150
+ approximate: false,
151
+ reason: 'No discrete GPU with queryable VRAM detected. Set KIKU_VRAM_TOTAL_GB to evaluate a known budget.',
152
+ };
153
+ }
154
+
155
+
156
+ export async function buildHardwareReport() {
157
+ const [target, probe] = await Promise.all([
158
+ getHardwareTarget(),
159
+ getGpuMetrics().catch(() => ({ available: false })),
160
+ ]);
161
+ return {
162
+ available: Boolean(target.available),
163
+ installedModels: listInstalledGgufs(),
164
+ target: {
165
+ vendor: target.vendor,
166
+ name: target.name,
167
+ totalBytes: target.totalBytes,
168
+ freeBytes: target.freeBytes,
169
+ usedBytes: target.usedBytes ?? null,
170
+ totalGb: target.totalBytes ? Number((target.totalBytes / 1024 ** 3).toFixed(2)) : null,
171
+ freeGb: target.freeBytes ? Number((target.freeBytes / 1024 ** 3).toFixed(2)) : null,
172
+ usedGb: target.usedBytes ? Number((target.usedBytes / 1024 ** 3).toFixed(2)) : null,
173
+ source: target.source,
174
+ approximate: Boolean(target.approximate),
175
+ },
176
+ probe: probe.available
177
+ ? {
178
+ vendor: probe.vendor,
179
+ count: probe.count,
180
+ gpus: (probe.gpus || []).map(g => ({
181
+ index: g.index,
182
+ name: g.name,
183
+ vendor: g.vendor,
184
+ totalMb: g.totalMb ?? null,
185
+ usedMb: g.usedMb ?? null,
186
+ freeMb: g.freeMb ?? null,
187
+ utilizationGpu: g.utilizationGpu ?? null,
188
+ temperatureC: g.temperatureC ?? null,
189
+ driverVersion: g.driverVersion ?? null,
190
+ })),
191
+ }
192
+ : null,
193
+ platform: `${process.platform}/${process.arch}`,
194
+ checkedAt: new Date().toISOString(),
195
+ };
196
+ }
197
+
198
+ const MERGE_KEYS = ['layers', 'queryHeads', 'kvHeads', 'headDim', 'hiddenSize', 'paramCount', 'paramsB', 'contextLength', 'quant'];
199
+
200
+
201
+ export function parseEstimateRequest(body = {}, searchParams = null) {
202
+ const pick = (key) => {
203
+ if (body[key] !== undefined && body[key] !== null && body[key] !== '') return body[key];
204
+ if (!searchParams) return undefined;
205
+ for (const candidate of [key, key.toLowerCase(), key.replace(/([A-Z])/g, m => `_${m.toLowerCase()}`)]) {
206
+ const raw = searchParams.get(candidate);
207
+ if (raw !== null) return raw;
208
+ }
209
+ return undefined;
210
+ };
211
+ const num = (value) => {
212
+ if (value === undefined || value === null || value === '') return undefined;
213
+ const parsed = Number(value);
214
+ return Number.isFinite(parsed) ? parsed : undefined;
215
+ };
216
+ return {
217
+ model: pick('model') || pick('model_name') || undefined,
218
+ modelPath: pick('model_path') || pick('modelPath') || undefined,
219
+ quant: pick('quant') || undefined,
220
+ contextLength: num(pick('contextLength') ?? pick('ctx')),
221
+ paramsB: num(pick('paramsB') ?? pick('params')),
222
+ paramCount: num(pick('paramCount')),
223
+ layers: num(pick('layers')),
224
+ kvHeads: num(pick('kvHeads') ?? pick('kv_heads')),
225
+ queryHeads: num(pick('queryHeads') ?? pick('query_heads')),
226
+ headDim: num(pick('headDim') ?? pick('head_dim')),
227
+ hiddenSize: num(pick('hiddenSize') ?? pick('hidden_size')),
228
+ batchSize: num(pick('batchSize')),
229
+ cacheBits: num(pick('cacheBits')),
230
+ images: num(pick('images')),
231
+ hardwareOverrideGb: num(pick('hardwareOverrideGb') ?? pick('availableVramGb')),
232
+ includeHardware: body.includeHardware !== false,
233
+ };
234
+ }
235
+
236
+ function pickFirst(...values) {
237
+ for (const value of values) {
238
+ if (value !== undefined && value !== null && value !== '') return value;
239
+ }
240
+ return null;
241
+ }
242
+
243
+
244
+
245
+ const LMSTUDIO_PROBE_TTL_MS = 15_000;
246
+ const lmStudioProbeCache = new Map();
247
+
248
+ export async function probeLmStudioRuntime(baseUrl = 'http://127.0.0.1:1234/v1') {
249
+ const key = String(baseUrl || 'http://127.0.0.1:1234/v1');
250
+ const cached = lmStudioProbeCache.get(key);
251
+ if (cached) {
252
+ if (cached.inFlight) return cached.inFlight;
253
+ if (Date.now() - cached.fetchedAt < LMSTUDIO_PROBE_TTL_MS) return cached.data;
254
+ }
255
+ const entry = { fetchedAt: Date.now(), data: null, inFlight: probeLmStudioRuntimeUncached(baseUrl) };
256
+ lmStudioProbeCache.set(key, entry);
257
+ try {
258
+ const result = await entry.inFlight;
259
+ entry.data = result;
260
+ entry.fetchedAt = Date.now();
261
+ return result;
262
+ } catch (err) {
263
+ lmStudioProbeCache.delete(key);
264
+ throw err;
265
+ } finally {
266
+ entry.inFlight = null;
267
+ }
268
+ }
269
+
270
+ async function probeLmStudioRuntimeUncached(baseUrl = 'http://127.0.0.1:1234/v1') {
271
+ const result = { reachable: false, loadedModel: null, loadedModelType: null, loadedContextLength: null, quantization: null, pendingModel: null };
272
+ try {
273
+ const root = String(baseUrl || 'http://127.0.0.1:1234/v1').replace(/\/v1\/?$/, '');
274
+ const res = await fetch(`${root}/api/v0/models`, { signal: AbortSignal.timeout(2500) });
275
+ if (!res.ok) return result;
276
+ const data = await res.json();
277
+ const models = Array.isArray(data?.data) ? data.data : [];
278
+ if (models.length === 0) return result;
279
+ result.reachable = true;
280
+ const loaded = models.find(m => m.state === 'loaded' && (m.type === 'llm' || m.type === 'vlm'))
281
+ || models.find(m => m.state === 'loaded');
282
+ if (loaded) {
283
+ result.loadedModel = loaded.id || null;
284
+ result.loadedModelType = loaded.type || null;
285
+ result.loadedContextLength = loaded.loaded_context_length ?? loaded.max_context_length ?? null;
286
+ result.quantization = loaded.quantization || null;
287
+ return result;
288
+ }
289
+
290
+ const pending = models.find(m => m.state === 'loading' && (m.type === 'llm' || m.type === 'vlm'))
291
+ || models.find(m => m.state === 'loading');
292
+ if (pending) result.pendingModel = pending.id || null;
293
+ return result;
294
+ } catch {
295
+ return result;
296
+ }
297
+ }
298
+
299
+ export async function computeVramEstimate(request = {}) {
300
+ let modelName = request.modelName || request.model || request.model_id || null;
301
+ const modelPath = request.modelPath || request.model_path || null;
302
+ const includeHardware = request.includeHardware !== false;
303
+
304
+ const isAuto = !modelName || String(modelName).trim().toLowerCase() === 'auto';
305
+ let lmRuntime = null;
306
+ if (!modelPath) {
307
+ try {
308
+ lmRuntime = await probeLmStudioRuntime(request.baseUrl).catch(() => null);
309
+ if (lmRuntime?.reachable && lmRuntime.loadedModel) {
310
+ const isTargetLoaded = isAuto || matchModelIdentifier(modelName, lmRuntime.loadedModel);
311
+ if (isTargetLoaded) {
312
+ if (isAuto) modelName = lmRuntime.loadedModel;
313
+ if (!request.contextLength && lmRuntime.loadedContextLength) {
314
+ request.contextLength = lmRuntime.loadedContextLength;
315
+ }
316
+ if (!request.quant && lmRuntime.quantization) {
317
+ request.quant = lmRuntime.quantization;
318
+ }
319
+ }
320
+ } else if (isAuto && lmRuntime?.reachable && lmRuntime.pendingModel) {
321
+ modelName = lmRuntime.pendingModel;
322
+ }
323
+ } catch {
324
+ /* ignore probe error */
325
+ }
326
+ }
327
+
328
+ if (isAuto && (!modelName || String(modelName).trim().toLowerCase() === 'auto')) {
329
+ const installed = listInstalledGgufs();
330
+ if (installed.length > 0) {
331
+ modelName = installed[0].path || installed[0].fileName;
332
+ } else {
333
+ modelName = 'google/gemma-4-e4b';
334
+ }
335
+ }
336
+
337
+ const resolved = await resolveModelConfig({
338
+ modelPath,
339
+ modelName,
340
+ overrides: request,
341
+ allowNetwork: request.allowNetwork !== false,
342
+ });
343
+
344
+ const overrides = {};
345
+ for (const key of MERGE_KEYS) {
346
+ const value = pickFirst(request[key], resolved.config?.[key]);
347
+ if (value !== null) overrides[key] = value;
348
+ }
349
+
350
+ const swaProfile = (() => {
351
+ const profile = { ...(knownSwaProfile(resolved.config?.arch) || {}), ...(resolved.config?.swa || {}) };
352
+ return Number(profile.windowTokens) > 0 ? profile : null;
353
+ })();
354
+
355
+ const vision = {
356
+ images: Number(request.images) || 0,
357
+ tokensPerImage: Number(request.tokensPerImage) || resolved.config?.vision?.tokensPerImage || null,
358
+ encoderParamsB: pickFirst(request.visionEncoderParamsB, resolved.config?.vision?.encoderParamsB),
359
+ };
360
+
361
+
362
+ const weightBytesOverride = (resolved.source === 'gguf' &&
363
+ resolved.config?.fileSizeBytes &&
364
+ (!overrides.quant || normalizeQuantName(overrides.quant) === normalizeQuantName(resolved.config.quant)))
365
+ ? resolved.config.fileSizeBytes
366
+ : null;
367
+
368
+ const estimate = estimateVramFootprint({
369
+ modelId: modelName || modelPath || resolved.config?.name || null,
370
+ quant: overrides.quant || 'q4_k_m',
371
+ paramCount: overrides.paramCount,
372
+ paramsB: overrides.paramsB,
373
+ weightBytesOverride,
374
+ layers: overrides.layers,
375
+ queryHeads: overrides.queryHeads,
376
+ kvHeads: overrides.kvHeads,
377
+ headDim: overrides.headDim,
378
+ hiddenSize: overrides.hiddenSize,
379
+ contextLength: overrides.contextLength,
380
+ batchSize: Number(request.batchSize) || 1,
381
+ cacheBits: Number(request.cacheBits) || 16,
382
+ swa: swaProfile,
383
+ vision,
384
+ activationGb: request.activationGb,
385
+ overheadGb: request.overheadGb,
386
+ });
387
+
388
+ const overrideGb = Number(request.hardwareOverrideGb);
389
+ const hardware = overrideGb > 0
390
+ ? {
391
+ available: true,
392
+ vendor: 'Configured',
393
+ name: request.hardwareName || 'Configured VRAM budget',
394
+ totalBytes: Math.round(gbToBytes(overrideGb)),
395
+ freeBytes: null,
396
+ usedBytes: null,
397
+ source: 'request-override',
398
+ approximate: false,
399
+ count: 1,
400
+ }
401
+ : (includeHardware ? await getHardwareTarget() : null);
402
+ const predictiveClassification = hardware?.available
403
+ ? classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: hardware.totalBytes })
404
+ : classifyAllocation({});
405
+
406
+
407
+ const isLoadedInLm = Boolean(lmRuntime?.reachable && lmRuntime.loadedModel &&
408
+ (isAuto || matchModelIdentifier(modelName, lmRuntime.loadedModel)));
409
+
410
+ const measured = isLoadedInLm
411
+ && hardware?.available && hardware.usedBytes > 0
412
+ ? {
413
+ ...classifyAllocation({ totalBytes: hardware.usedBytes, availableBytes: hardware.totalBytes }),
414
+ usedBytes: hardware.usedBytes,
415
+ totalBytes: hardware.totalBytes,
416
+ loadedModel: lmRuntime.loadedModel,
417
+ }
418
+ : null;
419
+
420
+ const classification = measured
421
+ ? {
422
+ zone: measured.zone,
423
+ usagePct: measured.usagePct,
424
+ headroomBytes: measured.headroomBytes,
425
+ message: measured.message,
426
+ requiresForce: measured.requiresForce,
427
+ basis: 'measured',
428
+ usedBytes: measured.usedBytes,
429
+ totalBytes: measured.totalBytes,
430
+ loadedModel: measured.loadedModel,
431
+ }
432
+ : {
433
+ ...predictiveClassification,
434
+ basis: predictiveClassification.zone === 'UNKNOWN' ? 'unavailable' : 'predicted',
435
+ };
436
+
437
+ const notes = [];
438
+ if (measured) {
439
+ notes.push(`Auto mode: "${measured.loadedModel}" is loaded in LM Studio — the zone reflects measured GPU usage from the hardware probe, while the breakdown stays a predictive worst-case for the shown context.`);
440
+ }
441
+ if (swaProfile && estimate.swa) {
442
+ notes.push(`KV cache modeled as sliding-window hybrid: ${estimate.swa.globalLayers} full-context layers + ${estimate.swa.localLayers} windowed layers (window ${estimate.swa.windowTokens} tokens).`);
443
+ }
444
+
445
+ let remediation = hardware?.available && predictiveClassification.zone !== 'GREEN'
446
+ ? recommendAdjustments(estimate, hardware.totalBytes)
447
+ : [];
448
+ if (remediation.length === 0 && hardware?.available && predictiveClassification.zone !== 'GREEN') {
449
+ remediation = recommendAdjustments(estimate, hardware.totalBytes, { targetZone: 'YELLOW' });
450
+ }
451
+
452
+ return {
453
+ ok: true,
454
+ input: {
455
+ modelName,
456
+ modelPath,
457
+ contextLength: overrides.contextLength,
458
+ quant: overrides.quant,
459
+ batchSize: estimate.batchSize,
460
+ cacheBits: estimate.cacheBits,
461
+ images: vision.images,
462
+ },
463
+ configSource: resolved.source,
464
+ approximate: Boolean(resolved.approx),
465
+ configError: resolved.error || null,
466
+ architecture: {
467
+ name: resolved.config?.name || modelName || null,
468
+ arch: resolved.config?.arch || null,
469
+ layers: estimate.layers,
470
+ queryHeads: estimate.queryHeads,
471
+ kvHeads: estimate.kvHeads,
472
+ headDim: estimate.headDim,
473
+ hiddenSize: estimate.hiddenSize,
474
+ paramCount: estimate.paramCount,
475
+ paramsB: estimate.paramCount ? Number((estimate.paramCount / 1e9).toFixed(2)) : null,
476
+ expertCount: resolved.config?.expertCount ?? null,
477
+ registryNotes: resolved.config?.registryNotes ?? null,
478
+ },
479
+ estimate,
480
+ hardware: hardware?.available
481
+ ? {
482
+ vendor: hardware.vendor,
483
+ name: hardware.name,
484
+ totalBytes: hardware.totalBytes,
485
+ freeBytes: hardware.freeBytes,
486
+ usedBytes: hardware.usedBytes ?? null,
487
+ source: hardware.source,
488
+ approximate: hardware.approximate,
489
+ }
490
+ : null,
491
+ classification,
492
+ predictiveClassification,
493
+ notes,
494
+ remediation,
495
+ };
496
+ }
497
+
498
+
499
+ export async function runVramPreflight({
500
+ model = null,
501
+ modelPath = null,
502
+ quant = null,
503
+ contextLength = null,
504
+ force = false,
505
+ allowNetwork = true,
506
+ } = {}) {
507
+ const result = await computeVramEstimate({
508
+ model,
509
+ modelPath,
510
+ quant,
511
+ contextLength,
512
+ includeHardware: true,
513
+ allowNetwork,
514
+ });
515
+
516
+
517
+ const zone = result.predictiveClassification?.zone || result.classification?.zone || 'UNKNOWN';
518
+ const blocked = zone === 'RED' && !force;
519
+
520
+ return {
521
+ ...result,
522
+ verdict: zone,
523
+ blocked,
524
+ force,
525
+ message: zone === 'UNKNOWN'
526
+ ? 'Hardware VRAM unavailable — running without a feasibility guarantee.'
527
+ : result.predictiveClassification?.message || result.classification.message,
528
+ };
529
+ }
530
+
531
+
532
+ export async function resolveSystemHardwareTier(options = {}) {
533
+ const hardware = await getHardwareTarget();
534
+ if (!hardware.available) {
535
+ const tier0 = classifyHardwareTier({ totalMb: 4096, name: 'Fallback Constrained' });
536
+ return {
537
+ available: false,
538
+ hardware,
539
+ tier: tier0,
540
+ profile: null,
541
+ budget: calculateDynamicVramBudget({ totalVramMb: 4096, kvQuant: 'Q4_K_M' }),
542
+ };
543
+ }
544
+
545
+ const totalMb = hardware.totalBytes ? hardware.totalBytes / (1024 * 1024) : 6144;
546
+ const profile = await techPowerUpClient.resolveHardwareProfile({
547
+ name: hardware.name,
548
+ totalMb,
549
+ totalGb: (totalMb / 1024).toFixed(1),
550
+ }, options);
551
+
552
+ const tier = classifyHardwareTier({
553
+ totalMb,
554
+ name: profile.name || hardware.name,
555
+ vendor: hardware.vendor,
556
+ });
557
+
558
+ const budget = calculateDynamicVramBudget({
559
+ totalVramMb: totalMb,
560
+ modelWeightsMb: options.modelWeightsMb || 3900,
561
+ osSafetyMb: options.osSafetyMb || 700,
562
+ kvQuant: tier.effectiveKvQuant,
563
+ });
564
+
565
+ return {
566
+ available: true,
567
+ hardware,
568
+ profile,
569
+ tier,
570
+ budget,
571
+ };
572
+ }