localpi 0.1.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/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/src/cli/cli.js +57 -0
- package/dist/src/cli/main.js +12 -0
- package/dist/src/common/json.js +21 -0
- package/dist/src/common/result.js +9 -0
- package/dist/src/llm/openai.js +78 -0
- package/dist/src/llm/types.js +1 -0
- package/dist/src/localpi/catalog.js +191 -0
- package/dist/src/localpi/llama-server.js +505 -0
- package/dist/src/localpi/managed-runtime.js +225 -0
- package/dist/src/localpi/models.js +169 -0
- package/dist/src/localpi/options.js +240 -0
- package/dist/src/localpi/provider-registry.js +121 -0
- package/dist/src/localpi/runtime-connection.js +75 -0
- package/dist/src/localpi/runtime-selection.js +75 -0
- package/dist/src/localpi/runtime-types.js +1 -0
- package/dist/src/localpi/runtime.js +89 -0
- package/dist/src/pi/config.js +108 -0
- package/dist/src/pi/extensions.js +348 -0
- package/dist/src/pi/launch.js +64 -0
- package/docs/2026-06-15-model-catalog-implementation-plan.md +220 -0
- package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +129 -0
- package/docs/implementation-plan.md +75 -0
- package/docs/runtime-specification.md +148 -0
- package/docs/structured-output.md +9 -0
- package/package.json +54 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, openSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { listModels, normalizeBaseUrl } from "../llm/openai.js";
|
|
6
|
+
export async function ensureLlamaServer(options, model) {
|
|
7
|
+
const baseUrl = llamaBaseUrl(options);
|
|
8
|
+
const warnings = await lmStudioWarnings(options);
|
|
9
|
+
const existingResult = await handleExistingServer(options, model, baseUrl, warnings);
|
|
10
|
+
if (existingResult.runtime !== undefined) {
|
|
11
|
+
return existingResult.runtime;
|
|
12
|
+
}
|
|
13
|
+
assertSafeToStart(warnings);
|
|
14
|
+
const pid = await startManagedServer(options, model);
|
|
15
|
+
await writeMetadata(options, metadata(options, model, pid));
|
|
16
|
+
const models = await waitForManagedModels(options, baseUrl, model.id);
|
|
17
|
+
return {
|
|
18
|
+
baseUrl,
|
|
19
|
+
model: model.id,
|
|
20
|
+
availableModels: models.map((entry) => entry.id),
|
|
21
|
+
managed: true,
|
|
22
|
+
warnings,
|
|
23
|
+
contextWindow: requestedContextWindow(options, model)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function handleExistingServer(options, model, baseUrl, warnings) {
|
|
27
|
+
const state = await existingServerState(options, baseUrl);
|
|
28
|
+
if (state.existing === undefined) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const matchingModel = state.existing.find((entry) => entry.id === model.id);
|
|
32
|
+
if (matchingModel !== undefined) {
|
|
33
|
+
return handleMatchingExistingServer(options, model, baseUrl, warnings, state.existing, state.owned, matchingModel);
|
|
34
|
+
}
|
|
35
|
+
if (state.owned === undefined) {
|
|
36
|
+
return rejectExternalModelConflict(baseUrl, state.existing, model.id);
|
|
37
|
+
}
|
|
38
|
+
await stopManagedLlamaServer(options);
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
async function existingServerState(options, baseUrl) {
|
|
42
|
+
const owned = await readActiveMetadataFile(options);
|
|
43
|
+
const existing = await getLlamaServerModels(options);
|
|
44
|
+
if (owned !== undefined && shouldStopOwnedBeforeStart(owned, existing, baseUrl)) {
|
|
45
|
+
await stopManagedLlamaServer(options);
|
|
46
|
+
return { existing, owned: undefined };
|
|
47
|
+
}
|
|
48
|
+
return { existing, owned };
|
|
49
|
+
}
|
|
50
|
+
function shouldStopOwnedBeforeStart(owned, existing, baseUrl) {
|
|
51
|
+
return existing === undefined || owned.baseUrl !== baseUrl;
|
|
52
|
+
}
|
|
53
|
+
async function handleMatchingExistingServer(options, model, baseUrl, warnings, existing, owned, matchingModel) {
|
|
54
|
+
if (owned !== undefined && managedLlamaServerNeedsRestart(options, owned, model)) {
|
|
55
|
+
await stopManagedLlamaServer(options);
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
assertCompatibleExternalContext(baseUrl, matchingModel, options.contextWindow);
|
|
59
|
+
return {
|
|
60
|
+
runtime: existingModelRuntime(model, baseUrl, existing, owned !== undefined, warnings, owned?.contextWindow ?? matchingModel.contextWindow)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function stopManagedLlamaServer(options) {
|
|
64
|
+
const info = await readActiveMetadataFile(options);
|
|
65
|
+
if (info === undefined) {
|
|
66
|
+
return "no localpi-owned llama-server metadata found";
|
|
67
|
+
}
|
|
68
|
+
if (isProcessAlive(info.pid)) {
|
|
69
|
+
signalProcess(info.pid, "SIGTERM");
|
|
70
|
+
await waitForExit(info.pid, 5000);
|
|
71
|
+
}
|
|
72
|
+
if (isProcessAlive(info.pid)) {
|
|
73
|
+
signalProcess(info.pid, "SIGKILL");
|
|
74
|
+
await waitForExit(info.pid, 2000);
|
|
75
|
+
}
|
|
76
|
+
if (isProcessAlive(info.pid)) {
|
|
77
|
+
throw new Error(`failed to stop localpi-owned llama-server pid ${String(info.pid)}`);
|
|
78
|
+
}
|
|
79
|
+
await rm(metadataPath(options), { force: true });
|
|
80
|
+
return `stopped localpi-owned llama-server pid ${String(info.pid)}`;
|
|
81
|
+
}
|
|
82
|
+
export async function llamaServerStatus(options) {
|
|
83
|
+
const baseUrl = llamaBaseUrl(options);
|
|
84
|
+
const info = await readActiveMetadataFile(options);
|
|
85
|
+
const models = await getLlamaServerModels(options);
|
|
86
|
+
return [
|
|
87
|
+
`runtime: llama-server`,
|
|
88
|
+
`base url: ${baseUrl}`,
|
|
89
|
+
`metadata: ${info === undefined ? "none" : metadataSummary(info)}`,
|
|
90
|
+
`server: ${models === undefined ? "not responding" : models.map((model) => model.id).join(", ")}`
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
export function llamaBaseUrl(options) {
|
|
94
|
+
return normalizeBaseUrl(options.baseUrl ?? `http://${options.host}:${String(options.port)}/v1`);
|
|
95
|
+
}
|
|
96
|
+
export async function getLlamaServerModels(options) {
|
|
97
|
+
return probe(llamaBaseUrl(options), options.timeoutMs);
|
|
98
|
+
}
|
|
99
|
+
export async function isManagedLlamaServerActive(options) {
|
|
100
|
+
return (await readActiveMetadataFile(options)) !== undefined;
|
|
101
|
+
}
|
|
102
|
+
export async function getManagedLlamaServerMetadata(options) {
|
|
103
|
+
return readActiveMetadataFile(options);
|
|
104
|
+
}
|
|
105
|
+
function existingModelRuntime(model, baseUrl, existing, managed, warnings, existingContextWindow) {
|
|
106
|
+
const ids = existing.map((entry) => entry.id);
|
|
107
|
+
return {
|
|
108
|
+
baseUrl,
|
|
109
|
+
model: model.id,
|
|
110
|
+
availableModels: ids,
|
|
111
|
+
managed,
|
|
112
|
+
warnings,
|
|
113
|
+
...optionalContextWindow(model.contextWindow ?? existingContextWindow)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function rejectExternalModelConflict(baseUrl, existing, requestedModel) {
|
|
117
|
+
const ids = existing.map((entry) => entry.id);
|
|
118
|
+
throw new Error(`server at ${baseUrl} is already serving ${ids.join(", ")}; stop it or choose that model before starting ${requestedModel}`);
|
|
119
|
+
}
|
|
120
|
+
async function startManagedServer(options, model) {
|
|
121
|
+
await mkdir(serverDir(options), { recursive: true });
|
|
122
|
+
const logPath = path.join(serverDir(options), "llama-server.log");
|
|
123
|
+
const logFd = openSync(logPath, "a");
|
|
124
|
+
try {
|
|
125
|
+
const child = spawn(options.serverCommand, serverArgs(options, model), {
|
|
126
|
+
detached: true,
|
|
127
|
+
stdio: ["ignore", logFd, logFd]
|
|
128
|
+
});
|
|
129
|
+
const pid = child.pid;
|
|
130
|
+
return await new Promise((resolve, reject) => {
|
|
131
|
+
const cleanup = () => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
child.off("error", onError);
|
|
134
|
+
child.off("exit", onExit);
|
|
135
|
+
};
|
|
136
|
+
const onError = (error) => {
|
|
137
|
+
cleanup();
|
|
138
|
+
reject(new Error(`failed to start llama-server: ${error.message}; see ${logPath}`));
|
|
139
|
+
};
|
|
140
|
+
const onExit = (code, signal) => {
|
|
141
|
+
cleanup();
|
|
142
|
+
reject(new Error(`llama-server exited before startup completed (${exitDescription(code, signal)}); see ${logPath}`));
|
|
143
|
+
};
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
if (pid === undefined) {
|
|
147
|
+
reject(new Error(`failed to start llama-server: process id was unavailable; see ${logPath}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
child.unref();
|
|
151
|
+
resolve(pid);
|
|
152
|
+
}, 250);
|
|
153
|
+
child.once("error", onError);
|
|
154
|
+
child.once("exit", onExit);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
closeSync(logFd);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function serverArgs(options, model) {
|
|
162
|
+
const endpoint = managedEndpoint(options);
|
|
163
|
+
return [
|
|
164
|
+
"--host",
|
|
165
|
+
endpoint.host,
|
|
166
|
+
"--port",
|
|
167
|
+
String(endpoint.port),
|
|
168
|
+
"--model",
|
|
169
|
+
model.modelPath,
|
|
170
|
+
"--alias",
|
|
171
|
+
model.id,
|
|
172
|
+
"--ctx-size",
|
|
173
|
+
String(options.contextWindow ?? model.contextWindow ?? 32768),
|
|
174
|
+
"--parallel",
|
|
175
|
+
String(options.parallel),
|
|
176
|
+
"--gpu-layers",
|
|
177
|
+
String(options.gpuLayers),
|
|
178
|
+
...chatTemplateArgs(model.chatTemplate),
|
|
179
|
+
...reasoningArgs(options.thinking),
|
|
180
|
+
"--reasoning-format",
|
|
181
|
+
"deepseek",
|
|
182
|
+
"--metrics"
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
function chatTemplateArgs(chatTemplate) {
|
|
186
|
+
return chatTemplate === undefined ? [] : ["--chat-template-file", chatTemplate];
|
|
187
|
+
}
|
|
188
|
+
function reasoningArgs(thinking) {
|
|
189
|
+
const config = reasoningConfig(thinking);
|
|
190
|
+
return config.budget === undefined
|
|
191
|
+
? ["--reasoning", config.mode]
|
|
192
|
+
: ["--reasoning", config.mode, "--reasoning-budget", String(config.budget)];
|
|
193
|
+
}
|
|
194
|
+
async function waitForModels(baseUrl, modelId, timeoutMs) {
|
|
195
|
+
const deadline = Date.now() + timeoutMs;
|
|
196
|
+
let lastError = "server did not respond";
|
|
197
|
+
while (Date.now() < deadline) {
|
|
198
|
+
const models = await probe(baseUrl, 1000);
|
|
199
|
+
if (models?.some((model) => model.id === modelId) === true) {
|
|
200
|
+
return models;
|
|
201
|
+
}
|
|
202
|
+
if (models !== undefined) {
|
|
203
|
+
lastError = `server reported: ${models.map((model) => model.id).join(", ")}`;
|
|
204
|
+
}
|
|
205
|
+
await sleep(500);
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`llama-server did not become ready for ${modelId}: ${lastError}`);
|
|
208
|
+
}
|
|
209
|
+
async function waitForManagedModels(options, baseUrl, modelId) {
|
|
210
|
+
try {
|
|
211
|
+
return await waitForModels(baseUrl, modelId, startupTimeoutMs());
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
await stopManagedLlamaServer(options);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function probe(baseUrl, timeoutMs) {
|
|
219
|
+
try {
|
|
220
|
+
return await listModels(baseUrl, timeoutMs);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function metadata(options, model, pid) {
|
|
227
|
+
const endpoint = managedEndpoint(options);
|
|
228
|
+
return {
|
|
229
|
+
pid,
|
|
230
|
+
baseUrl: endpoint.baseUrl,
|
|
231
|
+
modelId: model.id,
|
|
232
|
+
modelPath: model.modelPath,
|
|
233
|
+
contextWindow: requestedContextWindow(options, model),
|
|
234
|
+
serverCommand: options.serverCommand,
|
|
235
|
+
host: endpoint.host,
|
|
236
|
+
port: endpoint.port,
|
|
237
|
+
gpuLayers: options.gpuLayers,
|
|
238
|
+
parallel: options.parallel,
|
|
239
|
+
...reasoningMetadata(options.thinking),
|
|
240
|
+
...optionalChatTemplate(model.chatTemplate)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function writeMetadata(options, value) {
|
|
244
|
+
await writeFile(metadataPath(options), `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
245
|
+
}
|
|
246
|
+
async function readMetadataFile(options) {
|
|
247
|
+
try {
|
|
248
|
+
return parseMetadata(await readFile(metadataPath(options), "utf8"));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function readActiveMetadataFile(options) {
|
|
255
|
+
const info = await readMetadataFile(options);
|
|
256
|
+
if (info === undefined) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (await metadataProcessMatches(info)) {
|
|
260
|
+
return info;
|
|
261
|
+
}
|
|
262
|
+
await rm(metadataPath(options), { force: true });
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
async function metadataProcessMatches(info) {
|
|
266
|
+
if (!isProcessAlive(info.pid)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (info.modelPath === "" || info.serverCommand === "") {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const command = await processCommand(info.pid);
|
|
273
|
+
return command === undefined ? true : commandMatchesMetadata(command, info);
|
|
274
|
+
}
|
|
275
|
+
function parseMetadata(raw) {
|
|
276
|
+
const value = JSON.parse(raw);
|
|
277
|
+
if (typeof value.pid !== "number" || typeof value.modelId !== "string") {
|
|
278
|
+
throw new Error("invalid llama-server metadata");
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
pid: value.pid,
|
|
282
|
+
baseUrl: metadataString(value.baseUrl),
|
|
283
|
+
modelId: value.modelId,
|
|
284
|
+
modelPath: metadataString(value.modelPath),
|
|
285
|
+
contextWindow: metadataNumber(value.contextWindow),
|
|
286
|
+
serverCommand: metadataString(value.serverCommand),
|
|
287
|
+
host: metadataString(value.host),
|
|
288
|
+
port: metadataNumber(value.port),
|
|
289
|
+
gpuLayers: metadataNumber(value.gpuLayers),
|
|
290
|
+
parallel: metadataNumber(value.parallel),
|
|
291
|
+
...parseReasoningMetadata(value),
|
|
292
|
+
...optionalChatTemplate(value.chatTemplate)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function metadataString(value) {
|
|
296
|
+
return value ?? "";
|
|
297
|
+
}
|
|
298
|
+
function metadataNumber(value) {
|
|
299
|
+
return value ?? 0;
|
|
300
|
+
}
|
|
301
|
+
function metadataSummary(info) {
|
|
302
|
+
return `pid ${String(info.pid)}, model ${info.modelId}, reasoning ${reasoningSummary(info)}, path ${info.modelPath}`;
|
|
303
|
+
}
|
|
304
|
+
function metadataPath(options) {
|
|
305
|
+
return path.join(serverDir(options), "llama-server.json");
|
|
306
|
+
}
|
|
307
|
+
function serverDir(options) {
|
|
308
|
+
return path.join(options.stateDir, "server");
|
|
309
|
+
}
|
|
310
|
+
function isProcessAlive(pid) {
|
|
311
|
+
if (pid <= 0) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
process.kill(pid, 0);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function signalProcess(pid, signal) {
|
|
323
|
+
try {
|
|
324
|
+
process.kill(pid, signal);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Final liveness checks decide whether a failed signal matters.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function requestedContextWindow(options, model) {
|
|
331
|
+
return options.contextWindow ?? model.contextWindow ?? 32768;
|
|
332
|
+
}
|
|
333
|
+
export function managedLlamaServerNeedsRestart(options, info, model) {
|
|
334
|
+
const endpoint = managedEndpoint(options);
|
|
335
|
+
const fieldsChanged = [
|
|
336
|
+
info.baseUrl !== endpoint.baseUrl,
|
|
337
|
+
info.serverCommand !== options.serverCommand,
|
|
338
|
+
info.host !== endpoint.host,
|
|
339
|
+
info.port !== endpoint.port,
|
|
340
|
+
info.gpuLayers !== options.gpuLayers,
|
|
341
|
+
info.parallel !== options.parallel,
|
|
342
|
+
reasoningChanged(options.thinking, info)
|
|
343
|
+
].some(Boolean);
|
|
344
|
+
return fieldsChanged || chatTemplateChanged(options, info) || modelChanged(options, info, model);
|
|
345
|
+
}
|
|
346
|
+
function reasoningChanged(thinking, info) {
|
|
347
|
+
const expected = reasoningConfig(thinking);
|
|
348
|
+
return info.reasoningMode !== expected.mode || info.reasoningBudget !== expected.budget;
|
|
349
|
+
}
|
|
350
|
+
function chatTemplateChanged(options, info) {
|
|
351
|
+
return options.chatTemplate !== undefined && info.chatTemplate !== options.chatTemplate;
|
|
352
|
+
}
|
|
353
|
+
function modelChanged(options, info, model) {
|
|
354
|
+
return model === undefined ? false : !managedModelMatches(options, info, model);
|
|
355
|
+
}
|
|
356
|
+
function managedModelMatches(options, info, model) {
|
|
357
|
+
return (info.modelId === model.id &&
|
|
358
|
+
info.modelPath === model.modelPath &&
|
|
359
|
+
info.contextWindow === requestedContextWindow(options, model) &&
|
|
360
|
+
info.chatTemplate === model.chatTemplate);
|
|
361
|
+
}
|
|
362
|
+
function managedEndpoint(options) {
|
|
363
|
+
const baseUrl = llamaBaseUrl(options);
|
|
364
|
+
if (options.baseUrl === undefined) {
|
|
365
|
+
return { baseUrl, host: options.host, port: options.port };
|
|
366
|
+
}
|
|
367
|
+
const url = new URL(baseUrl);
|
|
368
|
+
const defaultPort = url.protocol === "https:" ? 443 : 80;
|
|
369
|
+
const port = url.port === "" ? defaultPort : Number.parseInt(url.port, 10);
|
|
370
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
371
|
+
throw new Error(`cannot derive llama-server port from --base-url ${baseUrl}`);
|
|
372
|
+
}
|
|
373
|
+
return { baseUrl, host: url.hostname, port };
|
|
374
|
+
}
|
|
375
|
+
function assertCompatibleExternalContext(baseUrl, model, requestedContextWindow) {
|
|
376
|
+
if (requestedContextWindow !== undefined &&
|
|
377
|
+
model.contextWindow !== undefined &&
|
|
378
|
+
model.contextWindow !== requestedContextWindow) {
|
|
379
|
+
throw new Error(`server at ${baseUrl} reports ${model.id} ctx=${String(model.contextWindow)}, but --ctx ${String(requestedContextWindow)} was requested`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function exitDescription(code, signal) {
|
|
383
|
+
if (code !== null) {
|
|
384
|
+
return `exit code ${String(code)}`;
|
|
385
|
+
}
|
|
386
|
+
if (signal !== null) {
|
|
387
|
+
return `signal ${signal}`;
|
|
388
|
+
}
|
|
389
|
+
return "exit status unavailable";
|
|
390
|
+
}
|
|
391
|
+
async function waitForExit(pid, timeoutMs) {
|
|
392
|
+
const deadline = Date.now() + timeoutMs;
|
|
393
|
+
while (Date.now() < deadline && isProcessAlive(pid)) {
|
|
394
|
+
await sleep(100);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function startupTimeoutMs() {
|
|
398
|
+
const raw = process.env["LOCALPI_SERVER_STARTUP_TIMEOUT_MS"];
|
|
399
|
+
return raw === undefined ? 120000 : Number.parseInt(raw, 10);
|
|
400
|
+
}
|
|
401
|
+
async function lmStudioWarnings(options) {
|
|
402
|
+
if (process.env["LOCALPI_LMSTUDIO_SAFETY_PROBE"] === "0") {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
if (usesLmStudioProbeEndpoint(options)) {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
const models = await probe("http://127.0.0.1:1234/v1", 1000);
|
|
409
|
+
if (models === undefined || models.length === 0) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
return [`LM Studio also reports loaded models: ${models.map((model) => model.id).join(", ")}`];
|
|
413
|
+
}
|
|
414
|
+
async function processCommand(pid) {
|
|
415
|
+
const procCommand = await procCommandLine(pid);
|
|
416
|
+
return procCommand ?? psCommandLine(pid);
|
|
417
|
+
}
|
|
418
|
+
async function procCommandLine(pid) {
|
|
419
|
+
try {
|
|
420
|
+
const raw = await readFile(`/proc/${String(pid)}/cmdline`, "utf8");
|
|
421
|
+
return raw.replaceAll("\u0000", " ");
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function psCommandLine(pid) {
|
|
428
|
+
if (process.platform === "win32") {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
return new Promise((resolve) => {
|
|
432
|
+
execFile("ps", ["-p", String(pid), "-o", "command="], { timeout: 1000 }, (error, stdout) => {
|
|
433
|
+
if (error !== null) {
|
|
434
|
+
resolve(undefined);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const command = stdout.trim();
|
|
438
|
+
resolve(command.length === 0 ? undefined : command);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function commandMatchesMetadata(command, info) {
|
|
443
|
+
return (command.includes(info.modelPath) &&
|
|
444
|
+
commandMarkers(info.serverCommand).some((marker) => command.includes(marker)));
|
|
445
|
+
}
|
|
446
|
+
function reasoningConfig(thinking) {
|
|
447
|
+
switch (thinking) {
|
|
448
|
+
case "off":
|
|
449
|
+
return { mode: "off" };
|
|
450
|
+
case "minimal":
|
|
451
|
+
return { mode: "on", budget: 32 };
|
|
452
|
+
case "low":
|
|
453
|
+
return { mode: "on", budget: 128 };
|
|
454
|
+
case "medium":
|
|
455
|
+
return { mode: "on", budget: 512 };
|
|
456
|
+
case "high":
|
|
457
|
+
return { mode: "on", budget: 2048 };
|
|
458
|
+
case "xhigh":
|
|
459
|
+
return { mode: "on", budget: 8192 };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function reasoningMetadata(thinking) {
|
|
463
|
+
const config = reasoningConfig(thinking);
|
|
464
|
+
return config.budget === undefined
|
|
465
|
+
? { reasoningMode: config.mode }
|
|
466
|
+
: { reasoningMode: config.mode, reasoningBudget: config.budget };
|
|
467
|
+
}
|
|
468
|
+
function parseReasoningMetadata(value) {
|
|
469
|
+
const mode = value.reasoningMode === "on" ? "on" : "off";
|
|
470
|
+
return value.reasoningBudget === undefined
|
|
471
|
+
? { reasoningMode: mode }
|
|
472
|
+
: { reasoningMode: mode, reasoningBudget: metadataNumber(value.reasoningBudget) };
|
|
473
|
+
}
|
|
474
|
+
function reasoningSummary(info) {
|
|
475
|
+
return info.reasoningBudget === undefined
|
|
476
|
+
? info.reasoningMode
|
|
477
|
+
: `${info.reasoningMode}:${String(info.reasoningBudget)}`;
|
|
478
|
+
}
|
|
479
|
+
function commandMarkers(serverCommand) {
|
|
480
|
+
return [serverCommand, path.basename(serverCommand), "llama-server"].filter((marker, index, markers) => marker.length > 0 && markers.indexOf(marker) === index);
|
|
481
|
+
}
|
|
482
|
+
function usesLmStudioProbeEndpoint(options) {
|
|
483
|
+
const endpoint = managedEndpoint(options);
|
|
484
|
+
return endpoint.port === 1234 && localHostnames().includes(endpoint.host);
|
|
485
|
+
}
|
|
486
|
+
function localHostnames() {
|
|
487
|
+
return ["127.0.0.1", "localhost", "0.0.0.0", "::1"];
|
|
488
|
+
}
|
|
489
|
+
function assertSafeToStart(warnings) {
|
|
490
|
+
if (warnings.length === 0) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
throw new Error(`${warnings.join("; ")}; unload LM Studio or use --runtime lmstudio`);
|
|
494
|
+
}
|
|
495
|
+
async function sleep(ms) {
|
|
496
|
+
await new Promise((resolve) => {
|
|
497
|
+
setTimeout(resolve, ms);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function optionalContextWindow(contextWindow) {
|
|
501
|
+
return contextWindow === undefined ? {} : { contextWindow };
|
|
502
|
+
}
|
|
503
|
+
function optionalChatTemplate(chatTemplate) {
|
|
504
|
+
return chatTemplate === undefined ? {} : { chatTemplate };
|
|
505
|
+
}
|