pi-llama-cpp 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/package.json +3 -3
- package/src/api/client.ts +25 -0
- package/src/constants.ts +8 -5
- package/src/enums/status.ts +0 -1
- package/src/interfaces/endpoints/models.ts +1 -1
- package/src/interfaces/settings.ts +3 -2
- package/src/interfaces/sortBy.ts +4 -0
- package/src/managers/command/models.ts +247 -0
- package/src/managers/command.ts +29 -459
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +29 -5
- package/src/managers/settings.ts +12 -88
- package/src/models/baseModel.ts +27 -10
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +2 -2
- package/src/models/singleModel.ts +1 -1
- package/src/server.ts +35 -48
- package/src/sse/client.ts +113 -59
- package/src/sse/fetch.ts +43 -0
- package/src/sse/manager.ts +9 -27
- package/src/sse/types.ts +0 -4
- package/src/ui/dialog/base.ts +118 -0
- package/src/ui/dialog/confirm.ts +45 -0
- package/src/ui/dialog/factory.ts +111 -0
- package/src/ui/dialog/input.ts +63 -0
- package/src/ui/dialog/options.ts +23 -0
- package/src/ui/editors/editorOptions.ts +43 -0
- package/src/ui/editors/itemBuilder.ts +47 -0
- package/src/ui/editors/listEditor.ts +291 -0
- package/src/ui/editors/override/entry.ts +24 -0
- package/src/ui/editors/override/entryEditor.ts +166 -0
- package/src/ui/editors/override/fields.ts +294 -0
- package/src/ui/editors/override/handlers.ts +118 -0
- package/src/ui/editors/override/itemBuilder.ts +42 -0
- package/src/ui/editors/override/overrideList.ts +127 -0
- package/src/ui/editors/server/builder.ts +60 -0
- package/src/ui/editors/server/fields.ts +84 -0
- package/src/ui/editors/server/handlers.ts +32 -0
- package/src/ui/editors/server/itemBuilder.ts +68 -0
- package/src/ui/editors/server/serverEditor.ts +161 -0
- package/src/ui/editors/server/utils.ts +45 -0
- package/src/ui/editors/server/wizard.ts +110 -0
- package/src/ui/editors/settingField.ts +37 -0
- package/src/ui/editors/settingsListFactory.ts +33 -0
- package/src/ui/settings/index.ts +237 -0
- package/src/ui/strings.ts +8 -4
- package/src/utils/health.ts +48 -0
- package/src/utils/serverIds.ts +21 -0
- package/src/utils/settingsStore.ts +1 -1
- package/src/utils/urlResolver.ts +129 -0
- package/src/utils/urls.ts +33 -13
- package/tests/commandManager.test.ts +38 -7
- package/tests/dialog.test.ts +95 -2
- package/tests/health.test.ts +116 -0
- package/tests/legacyModel.test.ts +34 -28
- package/tests/overrides.test.ts +123 -68
- package/tests/routerModel.test.ts +67 -68
- package/tests/server.test.ts +47 -16
- package/tests/serverManager.test.ts +4 -4
- package/tests/settings.test.ts +10 -8
- package/tests/singleModel.test.ts +12 -12
- package/tests/sseManager.test.ts +6 -24
- package/src/ui/dialog.ts +0 -287
- package/src/ui/overrideEntryEditor.ts +0 -119
- package/src/ui/overrideSettingsList.ts +0 -682
- package/src/ui/serverListEditor.ts +0 -32
- package/src/ui/serverSettingsList.ts +0 -466
package/src/managers/command.ts
CHANGED
|
@@ -1,54 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type ExtensionCommandContext,
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
5
4
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
AutocompleteItem,
|
|
8
|
-
SettingsList,
|
|
9
|
-
type SettingItem,
|
|
10
|
-
} from "@earendil-works/pi-tui";
|
|
5
|
+
import { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
11
6
|
import { PROVIDER_NAME } from "../constants";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { BaseModel } from "../models/baseModel";
|
|
17
|
-
import { createOverrideSettingsList } from "../ui/overrideSettingsList";
|
|
18
|
-
import { ServerSettingsList } from "../ui/serverSettingsList";
|
|
19
|
-
import { errorMessage } from "../utils/errors";
|
|
20
|
-
import { EventManager } from "./events";
|
|
7
|
+
import { OverrideSettingsList } from "../ui/editors/override/overrideList";
|
|
8
|
+
import { ServerSettingsList } from "../ui/editors/server/serverEditor";
|
|
9
|
+
import { SettingsEditor } from "../ui/settings";
|
|
10
|
+
import { ModelsMenu } from "./command/models";
|
|
21
11
|
import { ServerManager } from "./server";
|
|
22
12
|
import type { LlamaSettingsManager } from "./settings";
|
|
23
13
|
|
|
24
|
-
/**
|
|
25
|
-
* Identifiers of the editable fields shown in `/models settings`.
|
|
26
|
-
* Values match the scalar `LlamaSettings` keys.
|
|
27
|
-
*/
|
|
28
|
-
export enum Options {
|
|
29
|
-
REACT_TO_MODEL_SELECT = "reactToModelSelect",
|
|
30
|
-
AUTOLOAD_ON_MESSAGE = "autoloadOnMessage",
|
|
31
|
-
SORT_BY = "sortBy",
|
|
32
|
-
POLLING_TIMEOUT = "pollingTimeout",
|
|
33
|
-
SERVER_TIMEOUT = "serverTimeout",
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type SortByValue = NonNullable<LlamaSettings["sortBy"]>;
|
|
37
|
-
|
|
38
|
-
const SORT_VALUES: SortByValue[] = [
|
|
39
|
-
"asc",
|
|
40
|
-
"desc",
|
|
41
|
-
"asc-name",
|
|
42
|
-
"desc-name",
|
|
43
|
-
"api",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/** Presets (ms) for `pollingTimeout` */
|
|
47
|
-
const POLLING_PRESETS = [15000, 30000, 60000, 120000, 300000];
|
|
48
|
-
|
|
49
|
-
/** Presets (ms) for `serverTimeout` */
|
|
50
|
-
const SERVER_PRESETS = [500, 1000, 2000, 5000, 10000];
|
|
51
|
-
|
|
52
14
|
/**
|
|
53
15
|
* `/models` subcommand completions. Module-level so
|
|
54
16
|
* {@link CommandManager.getArgumentCompletions} doesn't rebuild the
|
|
@@ -82,105 +44,15 @@ const ARGUMENT_COMPLETIONS: AutocompleteItem[] = [
|
|
|
82
44
|
},
|
|
83
45
|
];
|
|
84
46
|
|
|
85
|
-
/**
|
|
86
|
-
* Formats milliseconds compactly for display (e.g. `500 -> "500ms"`,
|
|
87
|
-
* `60000 -> "60s"`).
|
|
88
|
-
*/
|
|
89
|
-
export const formatMs = (ms: number): string =>
|
|
90
|
-
ms % 1000 === 0 ? `${ms / 1000}s` : `${ms}ms`;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Parses a value produced by `formatMs()` back to milliseconds.
|
|
94
|
-
* Only ever called with values from the preset lists.
|
|
95
|
-
*/
|
|
96
|
-
const parseMs = (value: string): number =>
|
|
97
|
-
value.endsWith("ms")
|
|
98
|
-
? Number(value.slice(0, -2))
|
|
99
|
-
: Number(value.slice(0, -1)) * 1000;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Builds the `SettingsList` items for `/models settings` from the current
|
|
103
|
-
* (merged) values of the scalar `llamaSettings` fields.
|
|
104
|
-
*/
|
|
105
|
-
export const buildSettingsItems = async (
|
|
106
|
-
settings: LlamaSettingsManager,
|
|
107
|
-
): Promise<SettingItem[]> => {
|
|
108
|
-
const { pollingTimeout, serverTimeout } = await settings.resolveTimeouts();
|
|
109
|
-
|
|
110
|
-
return [
|
|
111
|
-
{
|
|
112
|
-
id: Options.REACT_TO_MODEL_SELECT,
|
|
113
|
-
label: "React to model selection",
|
|
114
|
-
description: "Load the model when you pick it in Pi (immediate)",
|
|
115
|
-
currentValue: (await settings.resolveReactToModelSelect()) ? "on" : "off",
|
|
116
|
-
values: ["on", "off"],
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: Options.AUTOLOAD_ON_MESSAGE,
|
|
120
|
-
label: "Autoload on message",
|
|
121
|
-
description:
|
|
122
|
-
"Auto-load the selected model when you send a message (immediate)",
|
|
123
|
-
currentValue: (await settings.resolveAutoloadOnMessage()) ? "on" : "off",
|
|
124
|
-
values: ["on", "off"],
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: Options.SORT_BY,
|
|
128
|
-
label: "Sort models by",
|
|
129
|
-
description: "Order of models in /models (next open)",
|
|
130
|
-
currentValue: await settings.resolveSortBy(),
|
|
131
|
-
values: [...SORT_VALUES],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: Options.POLLING_TIMEOUT,
|
|
135
|
-
label: "Polling timeout",
|
|
136
|
-
description: "Max model-load wait (next model load)",
|
|
137
|
-
currentValue: formatMs(pollingTimeout),
|
|
138
|
-
values: POLLING_PRESETS.map(formatMs),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: Options.SERVER_TIMEOUT,
|
|
142
|
-
label: "Server timeout",
|
|
143
|
-
description: "Health check / SSE probe timeout (next model load)",
|
|
144
|
-
currentValue: formatMs(serverTimeout),
|
|
145
|
-
values: SERVER_PRESETS.map(formatMs),
|
|
146
|
-
},
|
|
147
|
-
];
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Persists a change made in the settings menu.
|
|
152
|
-
* Maps the `SettingsList` id/value pair to the matching `llamaSettings`
|
|
153
|
-
* key and writes it via `LlamaSettingsManager.setLlamaSetting()`.
|
|
154
|
-
*/
|
|
155
|
-
export const applySettingChange = async (
|
|
156
|
-
id: string,
|
|
157
|
-
newValue: string,
|
|
158
|
-
settings: LlamaSettingsManager,
|
|
159
|
-
): Promise<void> => {
|
|
160
|
-
switch (id) {
|
|
161
|
-
case Options.REACT_TO_MODEL_SELECT:
|
|
162
|
-
await settings.setLlamaSetting("reactToModelSelect", newValue === "on");
|
|
163
|
-
return;
|
|
164
|
-
case Options.AUTOLOAD_ON_MESSAGE:
|
|
165
|
-
await settings.setLlamaSetting("autoloadOnMessage", newValue === "on");
|
|
166
|
-
return;
|
|
167
|
-
case Options.SORT_BY:
|
|
168
|
-
await settings.setLlamaSetting("sortBy", newValue as SortByValue);
|
|
169
|
-
return;
|
|
170
|
-
case Options.POLLING_TIMEOUT:
|
|
171
|
-
await settings.setLlamaSetting("pollingTimeout", parseMs(newValue));
|
|
172
|
-
return;
|
|
173
|
-
case Options.SERVER_TIMEOUT:
|
|
174
|
-
await settings.setLlamaSetting("serverTimeout", parseMs(newValue));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
47
|
export class CommandManager {
|
|
48
|
+
private readonly modelsMenu: ModelsMenu;
|
|
49
|
+
|
|
180
50
|
constructor(
|
|
181
51
|
private readonly serverManager: ServerManager,
|
|
182
52
|
private readonly settings: LlamaSettingsManager,
|
|
183
|
-
) {
|
|
53
|
+
) {
|
|
54
|
+
this.modelsMenu = new ModelsMenu(serverManager);
|
|
55
|
+
}
|
|
184
56
|
|
|
185
57
|
/**
|
|
186
58
|
* Sets up the argument completions for the `/models` command
|
|
@@ -207,8 +79,8 @@ export class CommandManager {
|
|
|
207
79
|
ctx: ExtensionCommandContext,
|
|
208
80
|
pi: ExtensionAPI,
|
|
209
81
|
) {
|
|
210
|
-
// Settings menu: no
|
|
211
|
-
//
|
|
82
|
+
// Settings menu: no provider re-registration needed (sortBy, timeouts,
|
|
83
|
+
// etc. don't affect the model registry)
|
|
212
84
|
if (args === "settings") {
|
|
213
85
|
await this.runSettingsMenu(ctx);
|
|
214
86
|
return;
|
|
@@ -251,18 +123,12 @@ export class CommandManager {
|
|
|
251
123
|
}
|
|
252
124
|
|
|
253
125
|
// Interactive menu: show <name> (<server_url>)
|
|
254
|
-
await this.
|
|
126
|
+
await this.modelsMenu.show(ctx, pi);
|
|
255
127
|
}
|
|
256
128
|
|
|
257
129
|
/**
|
|
258
130
|
* Runs the interactive settings menu for the scalar `llamaSettings`
|
|
259
131
|
* fields. Enter/Space cycles the value under the cursor; Esc closes.
|
|
260
|
-
*
|
|
261
|
-
* Writes go to the global `~/.pi/agent/settings.json` via
|
|
262
|
-
* `LlamaSettingsManager.setLlamaSetting()`; write errors are notified
|
|
263
|
-
* and leave the dialog open with values unchanged. These settings
|
|
264
|
-
* (reactToModelSelect, autoloadOnMessage, sortBy, timeouts) do not
|
|
265
|
-
* require provider re-registration.
|
|
266
132
|
*/
|
|
267
133
|
private async runSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
268
134
|
if (ctx.mode !== "tui") {
|
|
@@ -273,36 +139,12 @@ export class CommandManager {
|
|
|
273
139
|
return;
|
|
274
140
|
}
|
|
275
141
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await ctx.ui.custom<void>(
|
|
279
|
-
(_tui, _theme, _kb, done) =>
|
|
280
|
-
new SettingsList(
|
|
281
|
-
items,
|
|
282
|
-
Math.min(items.length + 2, 15),
|
|
283
|
-
getSettingsListTheme(),
|
|
284
|
-
(id, newValue) => {
|
|
285
|
-
applySettingChange(id, newValue, this.settings).catch(
|
|
286
|
-
(err: unknown) => {
|
|
287
|
-
const message = errorMessage(err);
|
|
288
|
-
ctx.ui.notify(message, "error");
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
},
|
|
292
|
-
() => done(undefined),
|
|
293
|
-
),
|
|
294
|
-
);
|
|
142
|
+
await SettingsEditor.show(ctx.ui, this.settings);
|
|
295
143
|
}
|
|
296
144
|
|
|
297
145
|
/**
|
|
298
|
-
* Runs the interactive servers editor for `llamaSettings.servers
|
|
299
|
-
*
|
|
300
|
-
* a adds a new server (inline Input), d deletes (after confirmation);
|
|
301
|
-
* Esc closes.
|
|
302
|
-
*
|
|
303
|
-
* Writes go to the global `~/.pi/agent/settings.json` via
|
|
304
|
-
* `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
|
|
305
|
-
* the editor stays open with the pre-mutation list. After closing,
|
|
146
|
+
* Runs the interactive servers editor for `llamaSettings.servers`
|
|
147
|
+
* (see `ServerSettingsList` for the editing semantics). After closing,
|
|
306
148
|
* providers are re-registered so server changes apply immediately.
|
|
307
149
|
*/
|
|
308
150
|
private async runServersEditor(
|
|
@@ -317,47 +159,17 @@ export class CommandManager {
|
|
|
317
159
|
return;
|
|
318
160
|
}
|
|
319
161
|
|
|
320
|
-
|
|
162
|
+
await ServerSettingsList.show(ctx.ui, this.settings);
|
|
321
163
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
new ServerSettingsList({
|
|
325
|
-
tui,
|
|
326
|
-
theme,
|
|
327
|
-
keybindings,
|
|
328
|
-
servers,
|
|
329
|
-
persist: (next) => this.settings.setLlamaSetting("servers", next),
|
|
330
|
-
done: () => {
|
|
331
|
-
done(undefined);
|
|
332
|
-
// Re-register providers so the updated server list takes effect
|
|
333
|
-
this.serverManager.update(pi);
|
|
334
|
-
},
|
|
335
|
-
onError: (message) => ctx.ui.notify(message, "error"),
|
|
336
|
-
}),
|
|
337
|
-
);
|
|
164
|
+
// Re-register providers so the updated server list takes effect
|
|
165
|
+
await this.serverManager.update(pi);
|
|
338
166
|
}
|
|
339
167
|
|
|
340
168
|
/**
|
|
341
169
|
* Runs the interactive overrides editor for
|
|
342
|
-
* `llamaSettings.servers[].overrides
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* the field-edit submenu; a adds, d deletes (after confirmation).
|
|
346
|
-
*
|
|
347
|
-
* Fields use a mix of finite (Enter to cycle) and infinite (Enter to
|
|
348
|
-
* type) editing:
|
|
349
|
-
*
|
|
350
|
-
* - Pattern / costs (input, output, cacheRead, cacheWrite): infinite —
|
|
351
|
-
* Enter opens an Input for typing.
|
|
352
|
-
* - Capabilities: finite — Enter cycles between `text` and `text | image`.
|
|
353
|
-
* - Reasoning: finite — Enter cycles between `true` and `false`.
|
|
354
|
-
*
|
|
355
|
-
* Servers themselves are not managed here — use `/models servers`.
|
|
356
|
-
*
|
|
357
|
-
* Writes go to the global `~/.pi/agent/settings.json` via
|
|
358
|
-
* `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
|
|
359
|
-
* leave the values unchanged. After closing, providers are
|
|
360
|
-
* re-registered so new overrides take effect on the next request.
|
|
170
|
+
* `llamaSettings.servers[].overrides` (see `OverrideSettingsList` for
|
|
171
|
+
* the editing semantics). After closing, providers are re-registered
|
|
172
|
+
* so new overrides take effect on the next request.
|
|
361
173
|
*/
|
|
362
174
|
private async runOverridesEditor(
|
|
363
175
|
ctx: ExtensionCommandContext,
|
|
@@ -371,23 +183,10 @@ export class CommandManager {
|
|
|
371
183
|
return;
|
|
372
184
|
}
|
|
373
185
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
theme,
|
|
379
|
-
keybindings,
|
|
380
|
-
servers,
|
|
381
|
-
persist: (next) => this.settings.setLlamaSetting("servers", next),
|
|
382
|
-
done: () => {
|
|
383
|
-
done(undefined);
|
|
384
|
-
// Re-register providers so the updated overrides take effect
|
|
385
|
-
this.serverManager.update(pi);
|
|
386
|
-
},
|
|
387
|
-
onError: (message) => ctx.ui.notify(message, "error"),
|
|
388
|
-
onChanged: () => {}, // no per-change notification needed
|
|
389
|
-
}),
|
|
390
|
-
);
|
|
186
|
+
await OverrideSettingsList.show(ctx.ui, this.settings);
|
|
187
|
+
|
|
188
|
+
// Re-register providers so the updated overrides take effect
|
|
189
|
+
await this.serverManager.update(pi);
|
|
391
190
|
}
|
|
392
191
|
|
|
393
192
|
/**
|
|
@@ -396,233 +195,4 @@ export class CommandManager {
|
|
|
396
195
|
private notifyNotFound(ctx: ExtensionCommandContext, url: string): void {
|
|
397
196
|
ctx.ui.notify(`${PROVIDER_NAME} unreachable at ${url}`, "error");
|
|
398
197
|
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Runs the interactive model selection menu.
|
|
402
|
-
*/
|
|
403
|
-
private async runModelsMenu(
|
|
404
|
-
ctx: ExtensionCommandContext,
|
|
405
|
-
pi: ExtensionAPI,
|
|
406
|
-
): Promise<void> {
|
|
407
|
-
const event = await this.modelSelectionHandler(
|
|
408
|
-
ctx,
|
|
409
|
-
await this.serverManager.getAllModels(),
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
if (!event) return;
|
|
413
|
-
const { action, model } = event;
|
|
414
|
-
|
|
415
|
-
// Action: Cancel
|
|
416
|
-
if (!action || action === Action.CANCEL) return;
|
|
417
|
-
|
|
418
|
-
// Action: Info
|
|
419
|
-
if (action === Action.INFO) {
|
|
420
|
-
const info = await model.getInfo();
|
|
421
|
-
ctx.ui.notify(`${info}`, "info");
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Action: Unload
|
|
426
|
-
if (action === Action.UNLOAD) {
|
|
427
|
-
await model.unload();
|
|
428
|
-
ctx.ui.notify(`Unloaded ${model.name}`, "info");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Action: Switch
|
|
433
|
-
if (action === Action.SWITCH) {
|
|
434
|
-
const { serverId } = model;
|
|
435
|
-
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
436
|
-
if (!piModel)
|
|
437
|
-
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
438
|
-
|
|
439
|
-
await pi.setModel(piModel);
|
|
440
|
-
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Actions: Load / Load & Switch / Retry
|
|
445
|
-
const loadActions = [Action.LOAD, Action.LOAD_AND_SWITCH, Action.RETRY];
|
|
446
|
-
if (loadActions.includes(action)) {
|
|
447
|
-
ctx.ui.notify(`Loading ${model.name}...`, "info");
|
|
448
|
-
// Mark the load as in-flight so session_before_switch can warn about
|
|
449
|
-
// it (see EventManager.inflightModel for the coupling rationale)
|
|
450
|
-
EventManager.inflightModel = model;
|
|
451
|
-
|
|
452
|
-
// Subscribe to progress events; skip when the server is gone
|
|
453
|
-
// (removed/edited away mid-load → getServer returns undefined)
|
|
454
|
-
const server = this.serverManager.getServer(model);
|
|
455
|
-
const cleanupProgress =
|
|
456
|
-
server?.sseManager.subscribeToProgress(
|
|
457
|
-
model.id,
|
|
458
|
-
(percentage, stage) => {
|
|
459
|
-
const stageText = stage ? ` (${stage})` : "";
|
|
460
|
-
ctx.ui.notify(
|
|
461
|
-
`Loading ${model.name}... [${percentage}%${stageText}]`,
|
|
462
|
-
"info",
|
|
463
|
-
);
|
|
464
|
-
},
|
|
465
|
-
) ?? (() => {});
|
|
466
|
-
|
|
467
|
-
const onSuccess = async () => {
|
|
468
|
-
const { serverId } = model;
|
|
469
|
-
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
470
|
-
if (!piModel)
|
|
471
|
-
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
472
|
-
|
|
473
|
-
// Verify auth
|
|
474
|
-
if ((await model.getStatus()) === Status.UNAUTHORIZED)
|
|
475
|
-
throw new Error(
|
|
476
|
-
`Unauthorized for ${model.name}. Use /login and add your API key.`,
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
// Verify failure
|
|
480
|
-
if ((await model.getStatus()) === Status.FAILED)
|
|
481
|
-
throw new Error(`Failed to load model ${model.name}`);
|
|
482
|
-
|
|
483
|
-
// Select the model if asked
|
|
484
|
-
if (action === Action.LOAD_AND_SWITCH) await pi.setModel(piModel);
|
|
485
|
-
|
|
486
|
-
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const onFailure = (err: any) => {
|
|
490
|
-
const message = errorMessage(err);
|
|
491
|
-
|
|
492
|
-
try {
|
|
493
|
-
ctx.ui.notify(message, "error");
|
|
494
|
-
} catch {
|
|
495
|
-
// ctx went stale between error and notification
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
const onFinished = async () => {
|
|
500
|
-
cleanupProgress();
|
|
501
|
-
EventManager.resetInflightModel();
|
|
502
|
-
|
|
503
|
-
// Re-scan providers to ensure accuracy of loaded models
|
|
504
|
-
await this.serverManager.update(pi);
|
|
505
|
-
|
|
506
|
-
// Force TUI refresh so Pi picks up the updated model states
|
|
507
|
-
ctx.ui.setStatus(PROVIDER_NAME, " ");
|
|
508
|
-
ctx.ui.setStatus(PROVIDER_NAME, undefined);
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
// Load the model without blocking the UI
|
|
512
|
-
model.load().then(onSuccess).catch(onFailure).finally(onFinished);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Handles the menu for model selection.
|
|
518
|
-
* Loops: select model → select action → handle action.
|
|
519
|
-
*
|
|
520
|
-
* Escape on actions menu goes back to model selection.
|
|
521
|
-
* Escape on model selection exits.
|
|
522
|
-
*
|
|
523
|
-
* @returns The selected action and model
|
|
524
|
-
*/
|
|
525
|
-
private async modelSelectionHandler(
|
|
526
|
-
ctx: ExtensionCommandContext,
|
|
527
|
-
models: BaseModel[],
|
|
528
|
-
): Promise<{ action: Action; model: BaseModel } | null> {
|
|
529
|
-
while (true) {
|
|
530
|
-
// Select the model
|
|
531
|
-
const model = await this.selectModel(ctx, models);
|
|
532
|
-
if (!model) return null;
|
|
533
|
-
|
|
534
|
-
// Select the action
|
|
535
|
-
const actions = await this.getActionsForModel(model);
|
|
536
|
-
const action = await this.selectAction(ctx, model, actions);
|
|
537
|
-
if (action === null) {
|
|
538
|
-
// Escape key pressed => back to model selection
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Return the selected action and model
|
|
543
|
-
return { action, model };
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Select a model from the list. Returns null if user cancels.
|
|
549
|
-
*
|
|
550
|
-
* @returns The model selected by the user
|
|
551
|
-
*/
|
|
552
|
-
private async selectModel(
|
|
553
|
-
ctx: ExtensionCommandContext,
|
|
554
|
-
models: BaseModel[],
|
|
555
|
-
): Promise<BaseModel | null> {
|
|
556
|
-
const labels = await Promise.all(
|
|
557
|
-
models.map(async (model) => ({
|
|
558
|
-
label: (await model.getLabel()).trim(),
|
|
559
|
-
serverUrl: model.serverUrl,
|
|
560
|
-
})),
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
// Count grapheme clusters (not UTF-16 code units) so emoji padding aligns visually
|
|
564
|
-
const graphemeLength = (str: string) =>
|
|
565
|
-
[...new Intl.Segmenter().segment(str)].length;
|
|
566
|
-
|
|
567
|
-
// Decorate the label so the spacing makes it seem more like a table
|
|
568
|
-
const maxLength = Math.max(
|
|
569
|
-
...labels.map(({ label }) => graphemeLength(label)),
|
|
570
|
-
);
|
|
571
|
-
const choices = labels.map(({ label, serverUrl }) => {
|
|
572
|
-
const extraPadding = 2;
|
|
573
|
-
const padLen = maxLength - graphemeLength(label) + extraPadding;
|
|
574
|
-
return `${label}${" ".repeat(padLen)} [Server: ${serverUrl}]`;
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, choices);
|
|
578
|
-
if (!choice) return null;
|
|
579
|
-
const idx = choices.indexOf(choice);
|
|
580
|
-
|
|
581
|
-
return models[idx];
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Get available actions for a model based on its mode and status.
|
|
586
|
-
*
|
|
587
|
-
* @returns A mapping of actions for each status
|
|
588
|
-
*/
|
|
589
|
-
private async getActionsForModel(model: BaseModel): Promise<Array<Action>> {
|
|
590
|
-
const base = [Action.INFO, Action.CANCEL];
|
|
591
|
-
|
|
592
|
-
const actions: Record<Status, Array<Action>> = {
|
|
593
|
-
[Status.LOADED]:
|
|
594
|
-
model.mode === Mode.ROUTER
|
|
595
|
-
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
596
|
-
: [Action.SWITCH, ...base],
|
|
597
|
-
[Status.LOADING]: [...base],
|
|
598
|
-
[Status.FAILED]: [Action.RETRY, ...base],
|
|
599
|
-
[Status.SLEEPING]:
|
|
600
|
-
model.mode === Mode.ROUTER
|
|
601
|
-
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
602
|
-
: [Action.SWITCH, ...base],
|
|
603
|
-
[Status.UNLOADED]: [Action.LOAD_AND_SWITCH, Action.LOAD, ...base],
|
|
604
|
-
[Status.UNAUTHORIZED]: [...base],
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
const status = await model.getStatus();
|
|
608
|
-
return actions[status];
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Selects an action for a model.
|
|
613
|
-
*
|
|
614
|
-
* @returns The selected action
|
|
615
|
-
*/
|
|
616
|
-
private async selectAction(
|
|
617
|
-
ctx: ExtensionCommandContext,
|
|
618
|
-
model: BaseModel,
|
|
619
|
-
actions: Array<Action>,
|
|
620
|
-
): Promise<Action | null> {
|
|
621
|
-
const labels = actions.map((a) => String(a));
|
|
622
|
-
const choice = await ctx.ui.select(`${model.name}`, labels);
|
|
623
|
-
if (!choice) return null;
|
|
624
|
-
|
|
625
|
-
const idx = labels.indexOf(choice);
|
|
626
|
-
return actions[idx];
|
|
627
|
-
}
|
|
628
198
|
}
|
package/src/managers/events.ts
CHANGED
|
@@ -11,8 +11,8 @@ import { ServerManager } from "./server";
|
|
|
11
11
|
|
|
12
12
|
export class EventManager {
|
|
13
13
|
/**
|
|
14
|
-
* Model with a load currently in flight. Deliberately a class static
|
|
15
|
-
*
|
|
14
|
+
* Model with a load currently in flight. Deliberately a class static:
|
|
15
|
+
* the load is started by CommandManager
|
|
16
16
|
* (fire-and-forget from the /models editor) while the "session switched
|
|
17
17
|
* mid-load" warning must be emitted here, from the session_before_switch
|
|
18
18
|
* hook — a shared static is the least-plumbing bridge between the two
|
package/src/managers/server.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { ApiError } from "../api/client";
|
|
3
|
+
import { API_TYPE, PROVIDER_NAME } from "../constants";
|
|
3
4
|
import { ServerStatus } from "../enums/serverStatus";
|
|
5
|
+
import type { SortBy } from "../interfaces/sortBy";
|
|
4
6
|
import { BaseModel } from "../models/baseModel";
|
|
5
7
|
import { Server } from "../server";
|
|
6
8
|
import type { LlamaSettingsManager } from "./settings";
|
|
@@ -80,7 +82,28 @@ export class ServerManager {
|
|
|
80
82
|
try {
|
|
81
83
|
await server.initialize();
|
|
82
84
|
await this.registerProvider(server, pi);
|
|
83
|
-
} catch {
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err instanceof ApiError && err.type === "authentication") {
|
|
87
|
+
// Register the provider with an empty model list so the user can
|
|
88
|
+
// still configure the API key via `/login` or `auth.json`. On the
|
|
89
|
+
// next scan the provider will re-initialize and discover models.
|
|
90
|
+
// Don't add to `failedUrls` — the server IS reachable, auth just
|
|
91
|
+
// isn't configured yet, so the health indicator should stay green.
|
|
92
|
+
const message = [
|
|
93
|
+
"[pi-llama-cpp]",
|
|
94
|
+
`Server at '${server.baseUrl}' requires a valid API key.`,
|
|
95
|
+
"Configure the key via `/login` or in `~/.pi/agent/auth.json`.",
|
|
96
|
+
].join("\n");
|
|
97
|
+
this.warnings.push(message);
|
|
98
|
+
pi.registerProvider(server.providerId, {
|
|
99
|
+
name: server.providerName,
|
|
100
|
+
baseUrl: server.apiBaseUrl,
|
|
101
|
+
api: API_TYPE,
|
|
102
|
+
apiKey: server.getApiKey(),
|
|
103
|
+
models: [],
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
84
107
|
this.failedUrls.push(server.baseUrl);
|
|
85
108
|
continue;
|
|
86
109
|
}
|
|
@@ -128,12 +151,13 @@ export class ServerManager {
|
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
/**
|
|
131
|
-
* Creates a Pi provider for the given server
|
|
154
|
+
* Creates a Pi provider for the given server.
|
|
132
155
|
*
|
|
133
156
|
* @param server The server
|
|
157
|
+
* @param pi The Pi API
|
|
134
158
|
*/
|
|
135
159
|
private async registerProvider(server: Server, pi: ExtensionAPI) {
|
|
136
|
-
const {
|
|
160
|
+
const { apiBaseUrl, models, providerId, providerName } = server;
|
|
137
161
|
const apiKey = server.getApiKey();
|
|
138
162
|
const modelConfigs = await Promise.all(
|
|
139
163
|
models.map((m) => m.toProviderConfig()),
|
|
@@ -141,7 +165,7 @@ export class ServerManager {
|
|
|
141
165
|
|
|
142
166
|
pi.registerProvider(providerId, {
|
|
143
167
|
name: providerName,
|
|
144
|
-
baseUrl:
|
|
168
|
+
baseUrl: apiBaseUrl,
|
|
145
169
|
api: API_TYPE,
|
|
146
170
|
apiKey: apiKey,
|
|
147
171
|
models: modelConfigs,
|