kiku-agent 1.3.2 → 1.3.3

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 (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -0,0 +1,591 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import {
4
+ buildLmsLoadArgs,
5
+ loadLmStudioModel,
6
+ unloadLmStudioModel,
7
+ wakeUpLmStudio,
8
+ probeLmStudioRuntime,
9
+ } from './lmStudioWakeup.js';
10
+ import {
11
+ registerBackgroundProcess,
12
+ getBackgroundProcess,
13
+ stopBackgroundProcess,
14
+ killWslProcessByName,
15
+ } from './processManager.js';
16
+
17
+ let cachedWslInfo = null;
18
+ let lastWslCheck = 0;
19
+ const WSL_CACHE_TTL_MS = 60000;
20
+
21
+
22
+ export function detectPlatformAndWsl(bypassCache = false) {
23
+ const now = Date.now();
24
+ if (!bypassCache && cachedWslInfo && (now - lastWslCheck < WSL_CACHE_TTL_MS)) {
25
+ return cachedWslInfo;
26
+ }
27
+
28
+ const platform = process.platform;
29
+ let isWsl = false;
30
+ let distros = [];
31
+ let activeDistro = null;
32
+ let wslAvailable = false;
33
+
34
+ if (platform === 'linux') {
35
+ try {
36
+ const release = os.release().toLowerCase();
37
+ isWsl = release.includes('microsoft') || release.includes('wsl');
38
+ } catch {}
39
+ cachedWslInfo = {
40
+ platform: 'linux',
41
+ isNativeLinux: !isWsl,
42
+ isWsl,
43
+ wslAvailable: false,
44
+ distros: [],
45
+ activeDistro: isWsl ? 'wsl-current' : null,
46
+ };
47
+ lastWslCheck = now;
48
+ return cachedWslInfo;
49
+ }
50
+
51
+ if (platform === 'darwin') {
52
+ cachedWslInfo = {
53
+ platform: 'darwin',
54
+ isNativeLinux: false,
55
+ isWsl: false,
56
+ wslAvailable: false,
57
+ distros: [],
58
+ activeDistro: null,
59
+ };
60
+ lastWslCheck = now;
61
+ return cachedWslInfo;
62
+ }
63
+
64
+ if (platform === 'win32') {
65
+ try {
66
+ const stdout = execSync('wsl --list --quiet', {
67
+ encoding: 'utf8',
68
+ timeout: 3000,
69
+ stdio: ['pipe', 'pipe', 'ignore'],
70
+ windowsHide: true,
71
+ });
72
+ distros = stdout
73
+ .split(/\r?\n/)
74
+ .map(d => d.replace(/\0/g, '').trim())
75
+ .filter(Boolean);
76
+ wslAvailable = distros.length > 0;
77
+ activeDistro = distros[0] || null;
78
+ } catch {
79
+ wslAvailable = false;
80
+ }
81
+
82
+ cachedWslInfo = {
83
+ platform: 'win32',
84
+ isNativeLinux: false,
85
+ isWsl: false,
86
+ wslAvailable,
87
+ distros,
88
+ activeDistro,
89
+ };
90
+ lastWslCheck = now;
91
+ return cachedWslInfo;
92
+ }
93
+
94
+ cachedWslInfo = {
95
+ platform,
96
+ isNativeLinux: false,
97
+ isWsl: false,
98
+ wslAvailable: false,
99
+ distros: [],
100
+ activeDistro: null,
101
+ };
102
+ lastWslCheck = now;
103
+ return cachedWslInfo;
104
+ }
105
+
106
+
107
+ export function clearWslCache() {
108
+ cachedWslInfo = null;
109
+ lastWslCheck = 0;
110
+ }
111
+
112
+
113
+ export function checkEngineCompatibility(engine = 'lms', customInfo = null) {
114
+ const normEngine = String(engine).toLowerCase().trim();
115
+ const info = customInfo || detectPlatformAndWsl();
116
+
117
+ if (normEngine === 'lms' || normEngine === 'lmstudio') {
118
+ return {
119
+ supported: true,
120
+ engine: 'lms',
121
+ name: 'LM Studio (llama.cpp)',
122
+ platform: info.platform,
123
+ supportedOs: ['windows', 'macos', 'linux'],
124
+ targetLevel: 'consumer',
125
+ description: 'Consumer & workstation inference engine running on llama.cpp with universal 3-OS support.',
126
+ };
127
+ }
128
+
129
+ if (normEngine === 'vllm') {
130
+ if (info.platform === 'linux') {
131
+ return {
132
+ supported: true,
133
+ isEnterprise: true,
134
+ engine: 'vllm',
135
+ name: 'vLLM (Native Linux)',
136
+ platform: 'linux',
137
+ isWsl: Boolean(info.isWsl),
138
+ distro: info.activeDistro,
139
+ supportedOs: ['linux'],
140
+ targetLevel: 'enterprise',
141
+ description: 'Enterprise-grade PagedAttention engine for high-throughput Data Center workloads.',
142
+ };
143
+ }
144
+
145
+ if (info.platform === 'win32') {
146
+ const hasWsl = info.wslAvailable !== undefined ? Boolean(info.wslAvailable) : Boolean(info.hasWsl);
147
+ if (hasWsl) {
148
+ return {
149
+ supported: true,
150
+ isEnterprise: true,
151
+ viaWsl: true,
152
+ engine: 'vllm',
153
+ name: `vLLM (WSL2 ${info.activeDistro || 'Linux'})`,
154
+ platform: 'win32',
155
+ isWsl: true,
156
+ distro: info.activeDistro,
157
+ distros: info.distros || [],
158
+ supportedOs: ['linux'],
159
+ targetLevel: 'enterprise',
160
+ description: `Enterprise-grade PagedAttention engine running inside WSL2 Linux distribution "${info.activeDistro || 'Linux'}".`,
161
+ };
162
+ }
163
+ return {
164
+ supported: false,
165
+ isEnterprise: true,
166
+ engine: 'vllm',
167
+ name: 'vLLM (WSL Required)',
168
+ platform: 'win32',
169
+ isWsl: false,
170
+ supportedOs: ['linux'],
171
+ targetLevel: 'enterprise',
172
+ reason: 'vLLM is an enterprise Linux-only engine. On Windows, it requires WSL2 (Windows Subsystem for Linux). Install with: wsl --install',
173
+ installHint: 'wsl --install',
174
+ };
175
+ }
176
+
177
+ if (info.platform === 'darwin') {
178
+ return {
179
+ supported: false,
180
+ isEnterprise: true,
181
+ engine: 'vllm',
182
+ name: 'vLLM (Unsupported on macOS)',
183
+ platform: 'darwin',
184
+ isWsl: false,
185
+ supportedOs: ['linux'],
186
+ targetLevel: 'enterprise',
187
+ reason: 'vLLM is designed exclusively for Linux with CUDA/ROCm hardware. macOS is not supported natively.',
188
+ };
189
+ }
190
+ }
191
+
192
+ return {
193
+ supported: false,
194
+ engine: normEngine,
195
+ name: normEngine,
196
+ platform: info.platform,
197
+ reason: `Unknown engine: ${engine}`,
198
+ };
199
+ }
200
+
201
+
202
+ export function buildVllmServeArgs(model, options = {}) {
203
+ const targetModel = model || 'google/gemma-4-e4b';
204
+ const args = ['serve', targetModel];
205
+
206
+ const port = Number(options.port) || 8000;
207
+ args.push('--port', String(port));
208
+
209
+ const host = options.host || '0.0.0.0';
210
+ args.push('--host', host);
211
+
212
+ const memUtil = options.gpuMemoryUtilization !== undefined ? Number(options.gpuMemoryUtilization) : 0.90;
213
+ if (Number.isFinite(memUtil) && memUtil >= 0.1 && memUtil <= 1.0) {
214
+ args.push('--gpu-memory-utilization', String(memUtil));
215
+ }
216
+
217
+ const maxLen = Number(options.maxModelLen);
218
+ if (Number.isFinite(maxLen) && maxLen >= 512 && maxLen <= 131072) {
219
+ args.push('--max-model-len', String(Math.floor(maxLen)));
220
+ }
221
+
222
+ const tp = Number(options.tensorParallelSize);
223
+ if (Number.isFinite(tp) && tp >= 1 && tp <= 8) {
224
+ args.push('--tensor-parallel-size', String(Math.floor(tp)));
225
+ }
226
+
227
+ if (options.dtype && ['auto', 'half', 'float16', 'bfloat16', 'float'].includes(String(options.dtype).toLowerCase())) {
228
+ args.push('--dtype', String(options.dtype).toLowerCase());
229
+ }
230
+
231
+ if (options.enforceEager) {
232
+ args.push('--enforce-eager');
233
+ }
234
+
235
+ if (options.kvCacheDtype && ['auto', 'fp8', 'fp8_e5m2', 'fp8_e4m3'].includes(String(options.kvCacheDtype).toLowerCase())) {
236
+ args.push('--kv-cache-dtype', String(options.kvCacheDtype).toLowerCase());
237
+ }
238
+
239
+ if (options.enablePrefixCaching) {
240
+ args.push('--enable-prefix-caching');
241
+ }
242
+
243
+ if (options.trustRemoteCode) {
244
+ args.push('--trust-remote-code');
245
+ }
246
+
247
+ return args;
248
+ }
249
+
250
+
251
+ export function buildVllmExecutionPlan(model, options = {}) {
252
+ const customInfo = options.customInfo || (options.platform ? {
253
+ platform: options.platform,
254
+ isWsl: options.platform === 'win32',
255
+ wslAvailable: options.platform === 'win32',
256
+ hasWsl: options.platform === 'win32',
257
+ activeDistro: options.distro || 'Ubuntu',
258
+ distros: [options.distro || 'Ubuntu'],
259
+ } : null);
260
+ const compatibility = checkEngineCompatibility('vllm', customInfo);
261
+ if (!compatibility.supported) {
262
+ return { ok: false, reason: compatibility.reason, compatibility };
263
+ }
264
+
265
+ const serveArgs = buildVllmServeArgs(model, options);
266
+ const serveCmdStr = `vllm ${serveArgs.join(' ')}`;
267
+
268
+ if (compatibility.platform === 'win32') {
269
+ const distro = options.distro || compatibility.distro || 'Ubuntu-26.04';
270
+ const customVenv = options.venvPath || process.env.VLLM_VENV_PATH || '';
271
+ const dynamicVenvCheck = [
272
+ customVenv ? `if [ -f "${customVenv}/bin/activate" ]; then source "${customVenv}/bin/activate" 2>/dev/null; fi;` : '',
273
+ 'if ! command -v vllm >/dev/null 2>&1; then',
274
+ ' for act in ~/.venvs/*/bin/activate ~/.venv/bin/activate ~/venv/bin/activate ./.venv/bin/activate; do',
275
+ ' if [ -f "$act" ]; then source "$act" 2>/dev/null; if command -v vllm >/dev/null 2>&1; then break; fi; fi;',
276
+ ' done;',
277
+ 'fi;',
278
+ 'export PATH="$HOME/.local/bin:/usr/local/bin:$PATH";',
279
+ ].filter(Boolean).join(' ');
280
+ const fullBashCmd = `${dynamicVenvCheck} ${serveCmdStr}`;
281
+ const rawCommand = `wsl -d ${distro} bash -c "${fullBashCmd}"`;
282
+ return {
283
+ ok: true,
284
+ engine: 'vllm',
285
+ isWsl: true,
286
+ distro,
287
+ binary: 'wsl.exe',
288
+ command: 'wsl.exe',
289
+ args: ['-d', distro, 'bash', '-c', fullBashCmd],
290
+ rawCommand,
291
+ displayCommand: rawCommand,
292
+ compatibility,
293
+ serveArgs,
294
+ port: options.port || 8000,
295
+ };
296
+ }
297
+
298
+ return {
299
+ ok: true,
300
+ engine: 'vllm',
301
+ isWsl: Boolean(compatibility.isWsl),
302
+ binary: 'vllm',
303
+ command: 'vllm',
304
+ args: serveArgs,
305
+ rawCommand: serveCmdStr,
306
+ displayCommand: serveCmdStr,
307
+ compatibility,
308
+ serveArgs,
309
+ port: options.port || 8000,
310
+ };
311
+ }
312
+
313
+
314
+ export async function getEnginesHealth(endpoints = {}) {
315
+ const lmsUrl = endpoints.lms || 'http://127.0.0.1:1234/v1';
316
+ const vllmUrl = endpoints.vllm || 'http://127.0.0.1:8000/v1';
317
+
318
+ const lmsCompat = checkEngineCompatibility('lms');
319
+ const vllmCompat = checkEngineCompatibility('vllm');
320
+
321
+ const lmsProbe = await probeLmStudioRuntime(lmsUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
322
+
323
+ let vllmProbe = { reachable: false, models: [], loadedModel: null };
324
+ try {
325
+ const root = vllmUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
326
+ const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(2000) });
327
+ if (res.ok) {
328
+ const json = await res.json();
329
+ const modelList = Array.isArray(json?.data) ? json.data.map(m => m.id) : [];
330
+ vllmProbe = {
331
+ reachable: true,
332
+ models: modelList,
333
+ loadedModel: modelList[0] || null,
334
+ count: modelList.length,
335
+ };
336
+ }
337
+ } catch {}
338
+
339
+ return {
340
+ engines: {
341
+ lms: {
342
+ id: 'lms',
343
+ name: lmsCompat.name,
344
+ targetLevel: lmsCompat.targetLevel,
345
+ supported: lmsCompat.supported,
346
+ compatibility: lmsCompat,
347
+ reachable: Boolean(lmsProbe?.reachable),
348
+ loadedModel: lmsProbe?.loadedModel || null,
349
+ loadedModels: lmsProbe?.loadedModels || [],
350
+ models: lmsProbe?.models || [],
351
+ port: 1234,
352
+ baseUrl: lmsUrl,
353
+ process: getBackgroundProcess('lmstudio'),
354
+ },
355
+ vllm: {
356
+ id: 'vllm',
357
+ name: vllmCompat.name,
358
+ targetLevel: vllmCompat.targetLevel,
359
+ supported: vllmCompat.supported,
360
+ compatibility: vllmCompat,
361
+ reachable: Boolean(vllmProbe.reachable),
362
+ loadedModel: vllmProbe.loadedModel,
363
+ models: vllmProbe.models,
364
+ port: 8000,
365
+ baseUrl: vllmUrl,
366
+ process: getBackgroundProcess('vllm'),
367
+ },
368
+ },
369
+ platform: detectPlatformAndWsl(),
370
+ };
371
+ }
372
+
373
+
374
+ export function calculateAutoTensorSplit(gpuMetrics, reservedMainGpuGb = 3.5, mainGpuIndex = 0) {
375
+ const gpus = Array.isArray(gpuMetrics)
376
+ ? gpuMetrics
377
+ : (Array.isArray(gpuMetrics?.gpus) ? gpuMetrics.gpus : []);
378
+
379
+ if (gpus.length < 2) {
380
+ const singleVram = gpus[0]
381
+ ? (Number(gpus[0].freeGb) || (gpus[0].freeMb ? gpus[0].freeMb / 1024 : 0) || Number(gpus[0].totalGb) || 24)
382
+ : 24;
383
+ return {
384
+ applicable: false,
385
+ tensorSplit: '1',
386
+ ratios: [1],
387
+ usableGb: [Math.max(0.5, Number(Number(singleVram).toFixed(1)))],
388
+ reason: 'Requires at least 2 GPUs for multi-GPU tensor split',
389
+ toString() { return this.tensorSplit; },
390
+ };
391
+ }
392
+
393
+ const usableGb = gpus.map((gpu, idx) => {
394
+ const free = Number(gpu.freeGb) || (gpu.freeMb ? gpu.freeMb / 1024 : 0);
395
+ const total = Number(gpu.totalGb) || (gpu.totalMb ? gpu.totalMb / 1024 : 0);
396
+ const rawVram = free > 0 ? free : (total > 0 ? total : 24);
397
+ const headroom = (idx === mainGpuIndex) ? reservedMainGpuGb : 0;
398
+ return Math.max(0.5, Number((rawVram - headroom).toFixed(2)));
399
+ });
400
+
401
+ const ratios = usableGb.map(gb => Math.max(1, Math.round(gb)));
402
+ const tensorSplit = ratios.join(',');
403
+
404
+ return {
405
+ applicable: true,
406
+ tensorSplit,
407
+ ratios,
408
+ usableGb,
409
+ mainGpu: mainGpuIndex,
410
+ reservedMainGpuGb,
411
+ toString() { return this.tensorSplit; },
412
+ };
413
+ }
414
+
415
+
416
+ export async function loadModelOnEngine(engine = 'lms', model, options = {}) {
417
+ const normEngine = String(engine).toLowerCase().trim();
418
+
419
+ if (normEngine === 'lms' || normEngine === 'lmstudio') {
420
+ const res = await wakeUpLmStudio({
421
+ baseUrl: options.baseUrl || 'http://127.0.0.1:1234/v1',
422
+ model,
423
+ quiet: options.quiet ?? false,
424
+ gpuOffload: options.gpuOffload,
425
+ cpuThreads: options.cpuThreads,
426
+ contextLength: options.contextLength,
427
+ splitMode: options.splitMode,
428
+ tensorSplit: options.tensorSplit,
429
+ mainGpu: options.mainGpu,
430
+ flashAttn: options.flashAttn,
431
+ batchSize: options.batchSize,
432
+ ubatchSize: options.ubatchSize,
433
+ htMode: options.htMode || options.hyperThreadingMode,
434
+ physicalCores: options.physicalCores,
435
+ hyperThreading: options.hyperThreading,
436
+ });
437
+ return {
438
+ engine: 'lms',
439
+ ...res,
440
+ };
441
+ }
442
+
443
+ if (normEngine === 'vllm') {
444
+ const plan = buildVllmExecutionPlan(model, options);
445
+ if (!plan.ok) {
446
+ return {
447
+ ok: false,
448
+ engine: 'vllm',
449
+ reason: plan.reason,
450
+ };
451
+ }
452
+
453
+ const port = options.port || 8000;
454
+ const vllmUrl = options.baseUrl || `http://127.0.0.1:${port}/v1`;
455
+
456
+
457
+ try {
458
+ const root = vllmUrl.replace(/\/v1\/?$/, '');
459
+ const ping = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(1500) });
460
+ if (ping.ok) {
461
+ const json = await ping.json();
462
+ const models = (json?.data || []).map(m => m.id);
463
+ const hasModel = models.some(m => m === model || m.includes(model));
464
+ if (hasModel) {
465
+ return {
466
+ ok: true,
467
+ engine: 'vllm',
468
+ alreadyRunning: true,
469
+ modelLoaded: true,
470
+ loadedModel: model,
471
+ };
472
+ }
473
+ }
474
+ } catch {}
475
+
476
+
477
+ try {
478
+ const child = spawn(plan.binary, plan.args, {
479
+ detached: true,
480
+ stdio: 'ignore',
481
+ windowsHide: true,
482
+ });
483
+ if (child.pid) {
484
+ registerBackgroundProcess('vllm', child, {
485
+ command: plan.binary,
486
+ args: plan.args,
487
+ metadata: {
488
+ engine: 'vllm',
489
+ model,
490
+ isWsl: plan.isWsl,
491
+ distro: plan.distro,
492
+ port,
493
+ wslProcessPattern: 'vllm',
494
+ },
495
+ });
496
+ }
497
+ child.unref();
498
+
499
+ const maxWaitMs = options.maxWaitMs || 45000;
500
+ const start = Date.now();
501
+ while (Date.now() - start < maxWaitMs) {
502
+ await new Promise(r => setTimeout(r, 2000));
503
+ try {
504
+ const root = vllmUrl.replace(/\/v1\/?$/, '');
505
+ const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(1500) });
506
+ if (res.ok) {
507
+ const data = await res.json();
508
+ const models = (data?.data || []).map(m => m.id);
509
+ return {
510
+ ok: true,
511
+ engine: 'vllm',
512
+ awakened: true,
513
+ modelLoaded: true,
514
+ loadedModel: models[0] || model,
515
+ models,
516
+ pid: child.pid,
517
+ };
518
+ }
519
+ } catch {}
520
+ }
521
+
522
+ return {
523
+ ok: false,
524
+ engine: 'vllm',
525
+ reason: `vLLM server started via ${plan.isWsl ? `WSL (${plan.distro})` : 'Linux'} but port ${port} did not respond within ${Math.round(maxWaitMs / 1000)}s`,
526
+ rawCommand: plan.rawCommand,
527
+ pid: child.pid,
528
+ };
529
+ } catch (err) {
530
+ return {
531
+ ok: false,
532
+ engine: 'vllm',
533
+ reason: `Failed to spawn vLLM: ${err.message}`,
534
+ };
535
+ }
536
+ }
537
+
538
+ return {
539
+ ok: false,
540
+ engine: normEngine,
541
+ reason: `Unsupported engine "${normEngine}"`,
542
+ };
543
+ }
544
+
545
+ export async function stopEngine(engine = 'vllm', options = {}) {
546
+ const normEngine = String(engine).toLowerCase().trim();
547
+ if (normEngine === 'vllm') {
548
+ const stopRes = stopBackgroundProcess('vllm', options);
549
+ const distro = options.distro || 'Ubuntu-26.04';
550
+ if (process.platform === 'win32') {
551
+ killWslProcessByName(distro, 'vllm');
552
+ }
553
+ return { ok: true, engine: 'vllm', stopped: stopRes.ok };
554
+ }
555
+ if (normEngine === 'lms' || normEngine === 'lmstudio') {
556
+ const stopRes1 = stopBackgroundProcess('lmstudio', options);
557
+ const stopRes2 = stopBackgroundProcess('lmstudio-server', options);
558
+ try {
559
+ const { resolveLmsCli } = await import('./lmStudioWakeup.js');
560
+ const lmsCmd = resolveLmsCli();
561
+ const { spawnSync } = await import('node:child_process');
562
+ spawnSync(lmsCmd, ['server', 'stop'], { stdio: 'ignore', windowsHide: true });
563
+ } catch {}
564
+ return { ok: true, engine: 'lms', stopped: stopRes1.ok || stopRes2.ok };
565
+ }
566
+ return { ok: false, reason: `Unknown engine "${engine}"` };
567
+ }
568
+
569
+ export async function unloadModelFromEngine(engine = 'lms', model = null, options = {}) {
570
+ const normEngine = String(engine).toLowerCase().trim();
571
+ if (normEngine === 'lms' || normEngine === 'lmstudio') {
572
+ return await unloadLmStudioModel(model, options);
573
+ }
574
+ if (normEngine === 'vllm') {
575
+ const stopRes = await stopEngine('vllm', options);
576
+ return {
577
+ ok: stopRes.ok,
578
+ unloaded: stopRes.ok,
579
+ engine: 'vllm',
580
+ model: model || 'all',
581
+ message: stopRes.ok ? 'vLLM server stopped and GPU VRAM freed' : 'Failed to stop vLLM server'
582
+ };
583
+ }
584
+ return {
585
+ ok: false,
586
+ engine: normEngine,
587
+ message: `Unsupported engine "${normEngine}" for unload`
588
+ };
589
+ }
590
+
591
+ export { buildLmsLoadArgs, loadLmStudioModel, unloadLmStudioModel, wakeUpLmStudio };
@@ -50,24 +50,14 @@ export function selectCases({ suite = DEFAULTS.suite, ids, limit } = {}) {
50
50
  }
51
51
 
52
52
  export async function probeModel({ baseUrl, model }) {
53
- let response;
54
- try {
55
- response = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { signal: AbortSignal.timeout(8000) });
56
- } catch {
53
+ const isLocal = baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
54
+ if (isLocal) {
57
55
  const { wakeUpLmStudio } = await import('../../../lmStudioWakeup.js');
58
56
  await wakeUpLmStudio({ baseUrl, model });
59
- response = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { signal: AbortSignal.timeout(8000) });
60
57
  }
58
+ const response = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { signal: AbortSignal.timeout(8000) });
61
59
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
62
- let models = await response.json();
63
- if (!models.data?.some(m => m.id === model)) {
64
- const { wakeUpLmStudio } = await import('../../../lmStudioWakeup.js');
65
- await wakeUpLmStudio({ baseUrl, model });
66
- const retryRes = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { signal: AbortSignal.timeout(8000) });
67
- if (retryRes.ok) {
68
- models = await retryRes.json();
69
- }
70
- }
60
+ const models = await response.json();
71
61
  if (!models.data?.some(m => m.id === model)) throw new Error(`Model not advertised: ${model}`);
72
62
  }
73
63
 
@@ -1,6 +1,18 @@
1
1
  import { computeBenchmarkSummary } from '../reporting/benchmark.js';
2
2
  const normalize = s => String(s).normalize('NFKC').toLowerCase().replace(/\s+/g, ' ');
3
3
 
4
+ function aliasMatches(text, alias) {
5
+ const normalizedText = normalize(text);
6
+ const normalizedAlias = normalize(alias);
7
+ if (!normalizedAlias) return false;
8
+ if (!/[\p{N}]/u.test(normalizedAlias)) return normalizedText.includes(normalizedAlias);
9
+ const timeMatch = normalizedAlias.match(/^(\d{1,2}):(\d{2})$/);
10
+ const escaped = timeMatch
11
+ ? `0?${timeMatch[1]}:${timeMatch[2]}`
12
+ : normalizedAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ return new RegExp(`(?<![\\p{N}])${escaped}(?![\\p{N}])`, 'u').test(normalizedText);
14
+ }
15
+
4
16
  function expandAnswerEquivalences(s) {
5
17
  let text = String(s);
6
18
  text = text.replace(/(?:พ\.ศ\.|พุทธศักราช)\s*(\d{4})/g, (match, beYear) => {
@@ -17,7 +29,7 @@ function expandAnswerEquivalences(s) {
17
29
  export function scoreCoverage(c, answer) {
18
30
  if (!c.groups.length) return { matched: 0, total: 0, allMatched: null, missing: [] };
19
31
  const text = normalize(expandAnswerEquivalences(answer));
20
- const missing = c.groups.filter(group => !group.some(alias => text.includes(normalize(alias))));
32
+ const missing = c.groups.filter(group => !group.some(alias => aliasMatches(text, alias)));
21
33
  return { matched: c.groups.length - missing.length, total: c.groups.length, allMatched: missing.length === 0, missing };
22
34
  }
23
35
 
@@ -191,10 +203,10 @@ export function checkGroundedness(c, answer, trace = []) {
191
203
  let groundedInTools = 0;
192
204
 
193
205
  for (const group of groups) {
194
- const presentInAnswer = group.some(alias => text.includes(normalize(alias)));
206
+ const presentInAnswer = group.some(alias => aliasMatches(text, alias));
195
207
  if (presentInAnswer) {
196
208
  matchedInAnswer++;
197
- const presentInTools = group.some(alias => toolOutputs.includes(normalize(alias)));
209
+ const presentInTools = group.some(alias => aliasMatches(toolOutputs, alias));
198
210
  if (presentInTools) {
199
211
  groundedInTools++;
200
212
  }
@@ -14,6 +14,7 @@ import { renderBenchmarkPng } from '../reporting/benchmarkPng.js';
14
14
  import { judgeReport } from '../analysis/judge.js';
15
15
  import { acquireRunLock, createSaver, loadReportWithBackups } from '../core/store.js';
16
16
  import { runSingleCase } from '../evaluation/worker.js';
17
+ import { evaluateGates, loadGatesFile } from '../analysis/gates.js';
17
18
 
18
19
  let isRunning = false;
19
20
  let activeAbortController = null;
@@ -421,7 +422,8 @@ export async function handleEvalApiRequest(req, res) {
421
422
  'Pragma': 'no-cache',
422
423
  'Expires': '0'
423
424
  });
424
- res.end(getEvalUiHtml());
425
+ const gates = await loadGatesFile(DEFAULTS.gatesFile);
426
+ res.end(getEvalUiHtml(gates));
425
427
  return true;
426
428
  }
427
429
 
@@ -689,10 +691,8 @@ export async function handleEvalApiRequest(req, res) {
689
691
  isRunning = false;
690
692
  activeRunMeta = null;
691
693
  activeCaseMeta = null;
692
- const passAt1Ok = (currentReport.summary?.passAt1 ?? currentReport.summary?.lexicalCoverageRate ?? 0) >= 0.75;
693
- const toolOk = (currentReport.summary?.toolF1 ?? currentReport.summary?.toolAccuracyRate ?? 0) >= 0.80;
694
- const loopOk = (currentReport.summary?.loopFreeRate ?? 1) >= 0.95;
695
- const gatesPassed = passAt1Ok && toolOk && loopOk;
694
+ const gates = await loadGatesFile(DEFAULTS.gatesFile);
695
+ const gatesPassed = evaluateGates(currentReport.summary || {}, gates).passed;
696
696
 
697
697
  broadcastSSE('run_complete', {
698
698
  report: currentReport,