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.
- package/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -2,12 +2,119 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { resolveDynamicWindowsAppPaths } from './appPaths.js';
|
|
6
|
+
import { registerBackgroundProcess } from './processManager.js';
|
|
7
|
+
|
|
8
|
+
const LMSTUDIO_PROBE_TTL_MS = 15_000;
|
|
9
|
+
const lmStudioProbeCache = new Map();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function clearLmStudioProbeCache() {
|
|
13
|
+
lmStudioProbeCache.clear();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export async function probeLmStudioRuntime(baseUrl = 'http://127.0.0.1:1234/v1', options = {}) {
|
|
18
|
+
const { timeoutMs = 2500, bypassCache = false } = options;
|
|
19
|
+
const key = String(baseUrl || 'http://127.0.0.1:1234/v1');
|
|
20
|
+
|
|
21
|
+
if (!bypassCache) {
|
|
22
|
+
const cached = lmStudioProbeCache.get(key);
|
|
23
|
+
if (cached) {
|
|
24
|
+
if (cached.inFlight) return cached.inFlight;
|
|
25
|
+
if (Date.now() - cached.fetchedAt < LMSTUDIO_PROBE_TTL_MS) return cached.data;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const inFlightPromise = probeLmStudioRuntimeUncached(baseUrl, timeoutMs);
|
|
30
|
+
const entry = { fetchedAt: Date.now(), data: null, inFlight: inFlightPromise };
|
|
31
|
+
lmStudioProbeCache.set(key, entry);
|
|
5
32
|
|
|
6
|
-
export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
|
|
7
33
|
try {
|
|
8
|
-
const
|
|
34
|
+
const result = await inFlightPromise;
|
|
35
|
+
entry.data = result;
|
|
36
|
+
entry.fetchedAt = Date.now();
|
|
37
|
+
return result;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
lmStudioProbeCache.delete(key);
|
|
40
|
+
throw err;
|
|
41
|
+
} finally {
|
|
42
|
+
entry.inFlight = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function probeLmStudioRuntimeUncached(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
|
|
47
|
+
const result = {
|
|
48
|
+
reachable: false,
|
|
49
|
+
loadedModel: null,
|
|
50
|
+
loadedModels: [],
|
|
51
|
+
loadedModelType: null,
|
|
52
|
+
loadedContextLength: null,
|
|
53
|
+
modelMaxContextLength: null,
|
|
54
|
+
quantization: null,
|
|
55
|
+
pendingModel: null,
|
|
56
|
+
models: []
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const root = String(baseUrl || 'http://127.0.0.1:1234/v1').replace(/\/v1\/?$/, '').replace(/\/+$/, '');
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${root}/api/v0/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
64
|
+
if (res.ok) {
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
const models = Array.isArray(data?.data) ? data.data : [];
|
|
67
|
+
if (models.length > 0) {
|
|
68
|
+
result.reachable = true;
|
|
69
|
+
result.models = models.map(m => m.id).filter(Boolean);
|
|
70
|
+
|
|
71
|
+
const loadedList = models.filter(m => m.state === 'loaded');
|
|
72
|
+
result.loadedModels = loadedList.map(m => m.id).filter(Boolean);
|
|
73
|
+
|
|
74
|
+
const activeLoaded = loadedList.find(m => m.type === 'llm' || m.type === 'vlm') || loadedList[0];
|
|
75
|
+
if (activeLoaded) {
|
|
76
|
+
result.loadedModel = activeLoaded.id || null;
|
|
77
|
+
result.loadedModelType = activeLoaded.type || null;
|
|
78
|
+
result.loadedContextLength = activeLoaded.loaded_context_length ?? activeLoaded.max_context_length ?? null;
|
|
79
|
+
result.modelMaxContextLength = activeLoaded.max_context_length ?? activeLoaded.loaded_context_length ?? null;
|
|
80
|
+
result.quantization = activeLoaded.quantization || null;
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pending = models.find(m => (m.state === 'loading') && (m.type === 'llm' || m.type === 'vlm'))
|
|
85
|
+
|| models.find(m => m.state === 'loading');
|
|
86
|
+
if (pending) result.pendingModel = pending.id || null;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
try {
|
|
9
96
|
const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
10
|
-
|
|
97
|
+
if (res.ok) {
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
const models = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []);
|
|
100
|
+
result.reachable = true;
|
|
101
|
+
result.models = models.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
|
|
102
|
+
result.loadedModels = [...result.models];
|
|
103
|
+
result.loadedModel = result.models[0] || null;
|
|
104
|
+
result.loadedModelType = 'llm';
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
|
|
115
|
+
try {
|
|
116
|
+
const probe = await probeLmStudioRuntime(baseUrl, { timeoutMs, bypassCache: true });
|
|
117
|
+
return Boolean(probe?.reachable);
|
|
11
118
|
} catch {
|
|
12
119
|
return false;
|
|
13
120
|
}
|
|
@@ -15,11 +122,8 @@ export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1',
|
|
|
15
122
|
|
|
16
123
|
export async function getLoadedModels(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
|
|
17
124
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (!res.ok) return [];
|
|
21
|
-
const data = await res.json();
|
|
22
|
-
return Array.isArray(data.data) ? data.data.map(m => m.id) : [];
|
|
125
|
+
const probe = await probeLmStudioRuntime(baseUrl, { timeoutMs, bypassCache: true });
|
|
126
|
+
return probe?.loadedModels || [];
|
|
23
127
|
} catch {
|
|
24
128
|
return [];
|
|
25
129
|
}
|
|
@@ -30,6 +134,7 @@ export function resolveLmsCli() {
|
|
|
30
134
|
const isWin = process.platform === 'win32';
|
|
31
135
|
const candidates = [
|
|
32
136
|
path.join(userHome, '.lmstudio', 'bin', isWin ? 'lms.exe' : 'lms'),
|
|
137
|
+
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'LM Studio', 'resources', 'app', 'bin', isWin ? 'lms.exe' : 'lms'),
|
|
33
138
|
isWin ? 'lms.exe' : 'lms'
|
|
34
139
|
];
|
|
35
140
|
return candidates.find(c => {
|
|
@@ -37,76 +142,260 @@ export function resolveLmsCli() {
|
|
|
37
142
|
}) || (isWin ? 'lms.exe' : 'lms');
|
|
38
143
|
}
|
|
39
144
|
|
|
145
|
+
export function normalizeLmStudioModelKey(modelInput) {
|
|
146
|
+
if (!modelInput || typeof modelInput !== 'string') return 'google/gemma-4-e4b';
|
|
147
|
+
const trimmed = modelInput.trim();
|
|
148
|
+
if (!trimmed) return 'google/gemma-4-e4b';
|
|
149
|
+
|
|
150
|
+
if (trimmed === 'google/gemma-4-e4b' || trimmed.startsWith('google/gemma-4-e4b@')) {
|
|
151
|
+
return trimmed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const clean = trimmed.toLowerCase().replace(/\\/g, '/');
|
|
155
|
+
const baseName = clean.split('/').pop() || clean;
|
|
156
|
+
|
|
157
|
+
if (/(?=.*gemma)(?=.*e4b)/i.test(baseName)) {
|
|
158
|
+
if (/(?:q6|6bit)/i.test(baseName)) return 'google/gemma-4-e4b@q6_k';
|
|
159
|
+
if (/(?:q8|8bit)/i.test(baseName)) return 'google/gemma-4-e4b@q8_0';
|
|
160
|
+
return 'google/gemma-4-e4b';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (baseName.endsWith('.gguf')) {
|
|
164
|
+
return baseName.slice(0, -5);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveParallelSlotBudget(requested, availableCpus = os.cpus()?.length, options = {}) {
|
|
171
|
+
const detected = Number(availableCpus);
|
|
172
|
+
const logicalCpus = Number.isFinite(detected) && detected > 0 ? Math.floor(detected) : 4;
|
|
173
|
+
const physicalCores = Number(options?.physicalCores) || (options?.hyperThreading ? Math.max(1, Math.floor(logicalCpus / 2)) : logicalCpus);
|
|
174
|
+
const htMode = String(options?.htMode || 'auto').toLowerCase();
|
|
175
|
+
|
|
176
|
+
let effectiveMax = logicalCpus;
|
|
177
|
+
if (htMode === 'physical' || options?.hyperThreading === false) {
|
|
178
|
+
effectiveMax = physicalCores;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ceiling = Math.max(1, Math.min(256, effectiveMax));
|
|
182
|
+
const num = Number(requested);
|
|
183
|
+
if (!Number.isFinite(num) || num < 1 || num > 256) {
|
|
184
|
+
return { value: undefined, requested: num, ceiling, capped: false, htMode, physicalCores, logicalCpus };
|
|
185
|
+
}
|
|
186
|
+
const floored = Math.floor(num);
|
|
187
|
+
const value = Math.min(floored, ceiling);
|
|
188
|
+
return { value, requested: floored, ceiling, capped: value < floored, htMode, physicalCores, logicalCpus };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buildLmsLoadArgs(model, options = {}) {
|
|
192
|
+
const args = ['load', model, '-y', '--identifier', model];
|
|
193
|
+
const isGemma4E4b = /(?:^|[\\/])gemma[-_. ]?4[-_. ]?e4b(?:@|$)|^google\/gemma-4-e4b/i.test(String(model || ''));
|
|
194
|
+
const gpu = options.gpuOffload ?? options.nGpuLayers ?? options.gpu;
|
|
195
|
+
if (gpu !== undefined && gpu !== null && gpu !== 'auto' && gpu !== '') {
|
|
196
|
+
const gpuStr = String(gpu).toLowerCase().trim();
|
|
197
|
+
if (gpuStr === 'off' || gpuStr === '0') {
|
|
198
|
+
args.push('--gpu', 'off');
|
|
199
|
+
} else if (gpuStr === 'max' || gpuStr === 'all') {
|
|
200
|
+
args.push('--gpu', 'max');
|
|
201
|
+
} else {
|
|
202
|
+
const num = Number(gpuStr);
|
|
203
|
+
if (Number.isFinite(num)) {
|
|
204
|
+
if (num >= 0 && num <= 1) {
|
|
205
|
+
args.push('--gpu', String(num));
|
|
206
|
+
} else if (num >= 90) {
|
|
207
|
+
args.push('--gpu', 'max');
|
|
208
|
+
} else if (num > 1) {
|
|
209
|
+
const ratio = Math.min(1, Math.round((num / 33) * 100) / 100);
|
|
210
|
+
args.push('--gpu', String(ratio));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ctx = Number(options.contextLength ?? (isGemma4E4b ? 32768 : undefined));
|
|
216
|
+
if (Number.isFinite(ctx) && ctx >= 512 && ctx <= 131072) {
|
|
217
|
+
args.push('--context-length', String(Math.floor(ctx)));
|
|
218
|
+
}
|
|
219
|
+
const parallel = options.parallel !== undefined
|
|
220
|
+
? Number(options.parallel)
|
|
221
|
+
: (options.parallelSlots !== undefined
|
|
222
|
+
? Number(options.parallelSlots)
|
|
223
|
+
: (options.cpuThreads !== undefined ? Number(options.cpuThreads) : (options.threads !== undefined ? Number(options.threads) : (isGemma4E4b ? 1 : undefined))));
|
|
224
|
+
const budget = resolveParallelSlotBudget(parallel, options.availableCpus, {
|
|
225
|
+
htMode: options.htMode || options.hyperThreadingMode,
|
|
226
|
+
physicalCores: options.physicalCores,
|
|
227
|
+
hyperThreading: options.hyperThreading,
|
|
228
|
+
});
|
|
229
|
+
if (budget.value !== undefined) {
|
|
230
|
+
args.push('--parallel', String(budget.value));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.ttl && Number.isFinite(Number(options.ttl)) && Number(options.ttl) > 0) {
|
|
234
|
+
args.push('--ttl', String(Math.floor(Number(options.ttl))));
|
|
235
|
+
}
|
|
236
|
+
return args;
|
|
237
|
+
}
|
|
238
|
+
|
|
40
239
|
export async function loadLmStudioModel(model, options = {}) {
|
|
41
240
|
const lmsCmd = resolveLmsCli();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
241
|
+
const normalized = normalizeLmStudioModelKey(model);
|
|
242
|
+
|
|
243
|
+
const executeLoad = (targetModel) => new Promise((resolve) => {
|
|
244
|
+
const args = buildLmsLoadArgs(targetModel, options);
|
|
245
|
+
const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
|
|
246
|
+
let child;
|
|
247
|
+
if (isCmdOrBat) {
|
|
248
|
+
const cmdString = `"${lmsCmd}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
249
|
+
child = spawn(cmdString, {
|
|
250
|
+
windowsHide: true,
|
|
251
|
+
stdio: 'pipe',
|
|
252
|
+
shell: true
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
child = spawn(lmsCmd, args, {
|
|
256
|
+
windowsHide: true,
|
|
257
|
+
stdio: 'pipe',
|
|
258
|
+
shell: false
|
|
259
|
+
});
|
|
260
|
+
}
|
|
49
261
|
let output = '';
|
|
50
262
|
child.stdout?.on('data', d => { output += d.toString(); });
|
|
51
263
|
child.stderr?.on('data', d => { output += d.toString(); });
|
|
52
264
|
child.on('close', code => {
|
|
53
|
-
resolve({ ok: code === 0, output });
|
|
265
|
+
resolve({ ok: code === 0, output, args, exitCode: code });
|
|
54
266
|
});
|
|
55
267
|
child.on('error', err => {
|
|
56
|
-
resolve({ ok: false, error: err.message });
|
|
268
|
+
resolve({ ok: false, error: err.message, args });
|
|
57
269
|
});
|
|
58
270
|
});
|
|
271
|
+
|
|
272
|
+
const primaryResult = await executeLoad(normalized);
|
|
273
|
+
if (primaryResult.ok || primaryResult.exitCode === 0) {
|
|
274
|
+
return primaryResult;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (primaryResult.output && /Model not found|No model found/i.test(primaryResult.output)) {
|
|
278
|
+
try {
|
|
279
|
+
const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
|
|
280
|
+
const lsOutput = await new Promise((res) => {
|
|
281
|
+
const lsChild = isCmdOrBat
|
|
282
|
+
? spawn(`"${lmsCmd}" ls --json`, { windowsHide: true, stdio: 'pipe', shell: true })
|
|
283
|
+
: spawn(lmsCmd, ['ls', '--json'], { windowsHide: true, stdio: 'pipe', shell: false });
|
|
284
|
+
let out = '';
|
|
285
|
+
lsChild.stdout?.on('data', d => { out += d.toString(); });
|
|
286
|
+
lsChild.stderr?.on('data', d => { out += d.toString(); });
|
|
287
|
+
lsChild.on('close', () => res(out));
|
|
288
|
+
lsChild.on('error', () => res(''));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (lsOutput.trim()) {
|
|
292
|
+
const models = JSON.parse(lsOutput.trim());
|
|
293
|
+
if (Array.isArray(models) && models.length > 0) {
|
|
294
|
+
const rawSearch = String(model || '').toLowerCase();
|
|
295
|
+
const cleanSearch = rawSearch.replace(/\\/g, '/').split('/').pop().replace(/\.gguf$/i, '');
|
|
296
|
+
const match = models.find(m => {
|
|
297
|
+
const key = String(m.modelKey || '').toLowerCase();
|
|
298
|
+
const pathStr = String(m.path || '').toLowerCase();
|
|
299
|
+
const name = String(m.displayName || '').toLowerCase();
|
|
300
|
+
return key === cleanSearch || pathStr.includes(cleanSearch) || name.includes(cleanSearch) ||
|
|
301
|
+
(cleanSearch.includes('gemma') && (key.includes('gemma') || name.includes('gemma')));
|
|
302
|
+
});
|
|
303
|
+
if (match?.modelKey && match.modelKey !== normalized) {
|
|
304
|
+
const retryRes = await executeLoad(match.modelKey);
|
|
305
|
+
if (retryRes.ok || retryRes.exitCode === 0) {
|
|
306
|
+
return retryRes;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return primaryResult;
|
|
59
315
|
}
|
|
60
316
|
|
|
61
317
|
export async function wakeUpLmStudio(options = {}) {
|
|
62
318
|
const {
|
|
63
319
|
baseUrl = 'http://127.0.0.1:1234/v1',
|
|
64
|
-
model = 'google/gemma-4-e4b',
|
|
65
|
-
maxWaitMs =
|
|
320
|
+
model: rawModel = 'google/gemma-4-e4b',
|
|
321
|
+
maxWaitMs = 60000,
|
|
322
|
+
quiet = false,
|
|
323
|
+
gpuOffload,
|
|
324
|
+
cpuThreads,
|
|
325
|
+
contextLength,
|
|
326
|
+
parallel,
|
|
327
|
+
splitMode,
|
|
328
|
+
tensorSplit,
|
|
329
|
+
mainGpu,
|
|
330
|
+
flashAttn,
|
|
331
|
+
batchSize,
|
|
332
|
+
ubatchSize,
|
|
333
|
+
htMode,
|
|
334
|
+
hyperThreadingMode,
|
|
335
|
+
physicalCores,
|
|
336
|
+
hyperThreading,
|
|
66
337
|
} = options;
|
|
67
338
|
|
|
68
|
-
const
|
|
339
|
+
const model = normalizeLmStudioModelKey(rawModel);
|
|
340
|
+
|
|
341
|
+
const isLocal = baseUrl.includes('127.0.0.1:1234') || baseUrl.includes('localhost:1234') || baseUrl.includes(':1234');
|
|
69
342
|
if (!isLocal) {
|
|
70
|
-
return { ok: false, reason: 'Non-local endpoint, automatic wakeup skipped' };
|
|
343
|
+
return { ok: false, reason: 'Non-local or non-standard LM Studio endpoint, automatic wakeup skipped' };
|
|
71
344
|
}
|
|
72
345
|
|
|
346
|
+
let probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
|
|
347
|
+
let serverUp = Boolean(probe?.reachable);
|
|
73
348
|
|
|
74
|
-
let serverUp = await isLmStudioResponding(baseUrl, 2000);
|
|
75
349
|
if (!serverUp) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const child = spawn(appExe, [], {
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const lmsCmd = resolveLmsCli();
|
|
353
|
+
const portMatch = String(baseUrl).match(/:([0-9]+)/);
|
|
354
|
+
const targetPort = portMatch ? portMatch[1] : '1234';
|
|
355
|
+
const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
|
|
356
|
+
let lmsChild;
|
|
357
|
+
if (isCmdOrBat) {
|
|
358
|
+
const cmdString = `"${lmsCmd}" server start --port ${targetPort} --cors`;
|
|
359
|
+
lmsChild = spawn(cmdString, {
|
|
87
360
|
detached: true,
|
|
88
361
|
stdio: 'ignore',
|
|
89
|
-
windowsHide:
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
shell: true
|
|
90
364
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
365
|
+
} else {
|
|
366
|
+
lmsChild = spawn(lmsCmd, ['server', 'start', '--port', targetPort, '--cors'], {
|
|
367
|
+
detached: true,
|
|
368
|
+
stdio: 'ignore',
|
|
369
|
+
windowsHide: true,
|
|
370
|
+
shell: false
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (lmsChild.pid) {
|
|
374
|
+
registerBackgroundProcess('lmstudio-server', lmsChild, {
|
|
375
|
+
command: lmsCmd,
|
|
376
|
+
args: ['server', 'start', '--port', targetPort, '--cors'],
|
|
377
|
+
metadata: { type: 'headless-task', port: targetPort, baseUrl, persistOnExit: true },
|
|
378
|
+
persistOnExit: true,
|
|
379
|
+
});
|
|
380
|
+
registerBackgroundProcess('lmstudio', lmsChild, {
|
|
381
|
+
command: lmsCmd,
|
|
382
|
+
args: ['server', 'start', '--port', targetPort, '--cors'],
|
|
383
|
+
metadata: { type: 'headless-task', port: targetPort, baseUrl, persistOnExit: true },
|
|
384
|
+
persistOnExit: true,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
103
387
|
lmsChild.unref();
|
|
104
|
-
} catch {
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (!quiet) {
|
|
390
|
+
try { process.stderr.write(`[Kiku Auto-Wakeup] Failed to launch headless LM Studio server: ${err.message}\n`); } catch {}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
105
393
|
|
|
106
394
|
const start = Date.now();
|
|
107
395
|
while (Date.now() - start < maxWaitMs) {
|
|
108
|
-
await new Promise(r => setTimeout(r,
|
|
109
|
-
|
|
396
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
397
|
+
probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
|
|
398
|
+
if (probe?.reachable) {
|
|
110
399
|
serverUp = true;
|
|
111
400
|
break;
|
|
112
401
|
}
|
|
@@ -114,31 +403,194 @@ export async function wakeUpLmStudio(options = {}) {
|
|
|
114
403
|
}
|
|
115
404
|
|
|
116
405
|
if (!serverUp) {
|
|
117
|
-
return { ok: false, reason: 'Timed out waiting for LM Studio server' };
|
|
406
|
+
return { ok: false, reason: 'Timed out waiting for LM Studio server on port 1234' };
|
|
118
407
|
}
|
|
119
408
|
|
|
120
|
-
|
|
121
409
|
if (model) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
410
|
+
const isModelLoaded = (probe?.loadedModels && probe.loadedModels.includes(model)) ||
|
|
411
|
+
(probe?.loadedModel && (probe.loadedModel === model || probe.loadedModel.includes(model)));
|
|
412
|
+
if (!isModelLoaded) {
|
|
413
|
+
if (!quiet) {
|
|
414
|
+
try {
|
|
415
|
+
process.stdout.write(`\n🌸 [Kiku Auto-Wakeup] LM Studio server active. Auto-loading model "${model}" into VRAM...\n`);
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
const loadRes = await loadLmStudioModel(model, {
|
|
419
|
+
gpuOffload,
|
|
420
|
+
cpuThreads,
|
|
421
|
+
contextLength,
|
|
422
|
+
parallel,
|
|
423
|
+
splitMode,
|
|
424
|
+
tensorSplit,
|
|
425
|
+
mainGpu,
|
|
426
|
+
flashAttn,
|
|
427
|
+
batchSize,
|
|
428
|
+
ubatchSize,
|
|
429
|
+
htMode: htMode || hyperThreadingMode,
|
|
430
|
+
physicalCores,
|
|
431
|
+
hyperThreading,
|
|
432
|
+
});
|
|
433
|
+
if (!loadRes.ok && loadRes.exitCode !== 0) {
|
|
434
|
+
const immediateProbe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
|
|
435
|
+
const loaded = (immediateProbe?.loadedModels && immediateProbe.loadedModels.includes(model)) ||
|
|
436
|
+
(immediateProbe?.loadedModel && (immediateProbe.loadedModel === model || immediateProbe.loadedModel.includes(model)));
|
|
437
|
+
if (!loaded) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
modelLoaded: false,
|
|
441
|
+
reason: `LM Studio CLI failed to load model "${model}" (exit code ${loadRes.exitCode ?? 'error'}). ${loadRes.output || loadRes.error || ''}`.trim()
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
128
445
|
|
|
129
446
|
const start = Date.now();
|
|
447
|
+
let ready = false;
|
|
130
448
|
while (Date.now() - start < maxWaitMs) {
|
|
131
|
-
await new Promise(r => setTimeout(r,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
449
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
450
|
+
probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
|
|
451
|
+
ready = (probe?.loadedModels && probe.loadedModels.includes(model)) ||
|
|
452
|
+
(probe?.loadedModel && (probe.loadedModel === model || probe.loadedModel.includes(model)));
|
|
453
|
+
if (ready) {
|
|
454
|
+
if (!quiet) {
|
|
455
|
+
try {
|
|
456
|
+
process.stdout.write(`🌸 [Kiku Auto-Wakeup] Model "${model}" loaded and ready!\n\n`);
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
return { ok: true, awakened: true, modelLoaded: true, loadedModel: model };
|
|
138
460
|
}
|
|
139
461
|
}
|
|
462
|
+
|
|
463
|
+
if (!ready) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
modelLoaded: false,
|
|
467
|
+
reason: `Timed out waiting for model "${model}" to load into VRAM (${Math.round(maxWaitMs / 1000)}s)`
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { ok: true, alreadyRunning: true, modelLoaded: true, loadedModel: probe?.loadedModel || model };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function unloadLmStudioModel(model = null, options = {}) {
|
|
477
|
+
const baseUrl = options.baseUrl || 'http://127.0.0.1:1234/v1';
|
|
478
|
+
const root = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
|
|
479
|
+
const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 2500;
|
|
480
|
+
const isDefaultPort = !options.baseUrl || options.baseUrl.includes(':1234');
|
|
481
|
+
let restSuccess = false;
|
|
482
|
+
let restMessage = '';
|
|
483
|
+
|
|
484
|
+
let loadedModels = [];
|
|
485
|
+
try {
|
|
486
|
+
const probeRes = await fetch(`${root}/api/v0/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
487
|
+
if (probeRes.ok) {
|
|
488
|
+
const probeData = await probeRes.json().catch(() => null);
|
|
489
|
+
const items = Array.isArray(probeData?.data) ? probeData.data : [];
|
|
490
|
+
loadedModels = items.filter(m => m.state === 'loaded').map(m => m.id).filter(Boolean);
|
|
491
|
+
}
|
|
492
|
+
} catch {}
|
|
493
|
+
|
|
494
|
+
if (loadedModels.length === 0) {
|
|
495
|
+
try {
|
|
496
|
+
const probeRes = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
497
|
+
if (probeRes.ok) {
|
|
498
|
+
const probeData = await probeRes.json().catch(() => null);
|
|
499
|
+
const items = Array.isArray(probeData?.data) ? probeData.data : (Array.isArray(probeData) ? probeData : []);
|
|
500
|
+
loadedModels = items.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
|
|
501
|
+
}
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const rawModel = model ? String(model).trim() : '';
|
|
506
|
+
const normalizedKey = rawModel ? normalizeLmStudioModelKey(rawModel) : '';
|
|
507
|
+
|
|
508
|
+
let targetIds = [];
|
|
509
|
+
if (rawModel && rawModel !== 'all') {
|
|
510
|
+
const exact = loadedModels.find(id => id.toLowerCase() === rawModel.toLowerCase());
|
|
511
|
+
const norm = loadedModels.find(id => id.toLowerCase() === normalizedKey.toLowerCase());
|
|
512
|
+
const partial = loadedModels.find(id => {
|
|
513
|
+
const lid = id.toLowerCase();
|
|
514
|
+
const lraw = rawModel.toLowerCase();
|
|
515
|
+
const lnorm = normalizedKey.toLowerCase();
|
|
516
|
+
return lid.includes(lraw) || lraw.includes(lid) || (lnorm && (lid.includes(lnorm) || lnorm.includes(lid)));
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (exact) targetIds = [exact];
|
|
520
|
+
else if (norm) targetIds = [norm];
|
|
521
|
+
else if (partial) targetIds = [partial];
|
|
522
|
+
else if (loadedModels.length === 1) targetIds = [loadedModels[0]];
|
|
523
|
+
else targetIds = [normalizedKey || rawModel];
|
|
524
|
+
} else {
|
|
525
|
+
targetIds = loadedModels.length > 0 ? [...loadedModels] : [];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let unloadedCount = 0;
|
|
529
|
+
if (targetIds.length > 0) {
|
|
530
|
+
for (const tid of targetIds) {
|
|
531
|
+
try {
|
|
532
|
+
const res = await fetch(`${root}/api/v1/models/unload`, {
|
|
533
|
+
method: 'POST',
|
|
534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
535
|
+
body: JSON.stringify({ instance_id: tid }),
|
|
536
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
537
|
+
});
|
|
538
|
+
if (res.ok) {
|
|
539
|
+
const data = await res.json().catch(() => null);
|
|
540
|
+
if (data && (data.instance_id || !data.error)) {
|
|
541
|
+
unloadedCount++;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch {}
|
|
140
545
|
}
|
|
141
546
|
}
|
|
142
547
|
|
|
143
|
-
|
|
548
|
+
if (unloadedCount > 0) {
|
|
549
|
+
restSuccess = true;
|
|
550
|
+
restMessage = 'Model successfully unloaded from LM Studio and GPU VRAM released';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let cliSuccess = false;
|
|
554
|
+
let cliOutput = '';
|
|
555
|
+
if (!restSuccess && isDefaultPort) {
|
|
556
|
+
try {
|
|
557
|
+
const lmsCmd = resolveLmsCli();
|
|
558
|
+
const runCli = (args) => {
|
|
559
|
+
const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
|
|
560
|
+
return new Promise((resolve) => {
|
|
561
|
+
const child = isCmdOrBat
|
|
562
|
+
? spawn(`"${lmsCmd}" ${args.join(' ')}`, { windowsHide: true, stdio: 'pipe', shell: true })
|
|
563
|
+
: spawn(lmsCmd, args, { windowsHide: true, stdio: 'pipe', shell: false });
|
|
564
|
+
let out = '';
|
|
565
|
+
child.stdout?.on('data', d => { out += d.toString(); });
|
|
566
|
+
child.stderr?.on('data', d => { out += d.toString(); });
|
|
567
|
+
child.on('close', code => {
|
|
568
|
+
const combined = out.trim();
|
|
569
|
+
const hasError = !combined || /(?:model not found|cannot find a model|error|failed)/i.test(combined);
|
|
570
|
+
resolve({ ok: code === 0 && !hasError, code, out: combined });
|
|
571
|
+
});
|
|
572
|
+
child.on('error', err => resolve({ ok: false, code: -1, out: err.message }));
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const primaryTarget = targetIds[0] || (rawModel !== 'all' ? (normalizedKey || rawModel) : null);
|
|
577
|
+
let cliResult = await runCli(primaryTarget ? ['unload', primaryTarget] : ['unload', '--all']);
|
|
578
|
+
if (!cliResult.ok) {
|
|
579
|
+
cliResult = await runCli(['unload', '--all']);
|
|
580
|
+
}
|
|
581
|
+
cliSuccess = cliResult.ok || /(?:unloaded|released|success|no models to unload)/i.test(cliResult.out);
|
|
582
|
+
cliOutput = cliResult.out;
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
clearLmStudioProbeCache();
|
|
587
|
+
|
|
588
|
+
const success = restSuccess || cliSuccess || (loadedModels.length === 0 && Boolean(cliOutput && /no models to unload/i.test(cliOutput)));
|
|
589
|
+
return {
|
|
590
|
+
ok: success,
|
|
591
|
+
unloaded: success,
|
|
592
|
+
engine: 'lms',
|
|
593
|
+
model: targetIds[0] || model || 'all',
|
|
594
|
+
message: success ? (restMessage || cliOutput || 'Model successfully unloaded from VRAM') : (cliOutput || 'Failed to unload model from LM Studio')
|
|
595
|
+
};
|
|
144
596
|
}
|