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.
- package/README.md +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- 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
|
+
}
|