pi-llama-cpp 0.13.0 → 0.15.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 +19 -18
- package/package.json +3 -3
- package/src/api/client.ts +25 -0
- package/src/constants.ts +2 -2
- package/src/enums/status.ts +0 -1
- package/src/index.ts +2 -1
- package/src/interfaces/endpoints/models.ts +1 -1
- package/src/interfaces/settings.ts +7 -1
- package/src/interfaces/sortBy.ts +4 -0
- package/src/managers/command/models.ts +254 -0
- package/src/managers/command.ts +34 -461
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +34 -18
- package/src/managers/settings.ts +26 -90
- 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 +17 -38
- 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 +50 -0
- package/src/ui/editors/itemBuilder.ts +43 -0
- package/src/ui/editors/listEditor.ts +300 -0
- package/src/ui/editors/override/entry.ts +24 -0
- package/src/ui/editors/override/entryEditor.ts +248 -0
- package/src/ui/editors/override/fields/base.ts +53 -0
- package/src/ui/editors/override/fields/capabilities.ts +36 -0
- package/src/ui/editors/override/fields/cost.ts +64 -0
- package/src/ui/editors/override/fields/index.ts +68 -0
- package/src/ui/editors/override/fields/numeric.ts +55 -0
- package/src/ui/editors/override/fields/pattern.ts +24 -0
- package/src/ui/editors/override/fields/reasoning.ts +33 -0
- package/src/ui/editors/override/itemBuilder.ts +39 -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/itemBuilder.ts +64 -0
- package/src/ui/editors/server/serverEditor.ts +197 -0
- package/src/ui/editors/server/utils.ts +95 -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 +248 -0
- package/src/ui/strings.ts +25 -4
- package/src/utils/health.ts +2 -1
- 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 → command/commandManager.test.ts} +88 -12
- package/tests/{events.test.ts → events/events.test.ts} +6 -6
- package/tests/mocks.ts +2 -0
- package/tests/models/legacyModel.test.ts +103 -0
- package/tests/{routerModel.test.ts → models/routerModel.test.ts} +71 -72
- package/tests/{singleModel.test.ts → models/singleModel.test.ts} +17 -17
- package/tests/{health.test.ts → server/health.test.ts} +2 -2
- package/tests/{server.test.ts → server/server.test.ts} +5 -17
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +184 -141
- package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
- package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +8 -26
- package/tests/{dialog.test.ts → ui/dialog.test.ts} +56 -4
- package/tests/{overrides.test.ts → ui/overrides.test.ts} +101 -111
- package/src/ui/dialog.ts +0 -290
- package/src/ui/overrideEntryEditor.ts +0 -119
- package/src/ui/overrideSettingsList.ts +0 -710
- package/src/ui/serverListEditor.ts +0 -59
- package/src/ui/serverSettingsList.ts +0 -513
- package/tests/legacyModel.test.ts +0 -97
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, settings);
|
|
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;
|
|
@@ -236,6 +108,11 @@ export class CommandManager {
|
|
|
236
108
|
this.notifyNotFound(ctx, url);
|
|
237
109
|
}
|
|
238
110
|
|
|
111
|
+
// Notify about other warnings (e.g. unauthorized servers)
|
|
112
|
+
for (const warning of this.serverManager.getWarnings()) {
|
|
113
|
+
ctx.ui.notify(warning, "warning");
|
|
114
|
+
}
|
|
115
|
+
|
|
239
116
|
if (args === "unload") {
|
|
240
117
|
const models = await this.serverManager.getAllModels();
|
|
241
118
|
await Promise.all(models.map((model) => model.unload()));
|
|
@@ -251,18 +128,12 @@ export class CommandManager {
|
|
|
251
128
|
}
|
|
252
129
|
|
|
253
130
|
// Interactive menu: show <name> (<server_url>)
|
|
254
|
-
await this.
|
|
131
|
+
await this.modelsMenu.show(ctx, pi);
|
|
255
132
|
}
|
|
256
133
|
|
|
257
134
|
/**
|
|
258
135
|
* Runs the interactive settings menu for the scalar `llamaSettings`
|
|
259
136
|
* 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
137
|
*/
|
|
267
138
|
private async runSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
268
139
|
if (ctx.mode !== "tui") {
|
|
@@ -273,36 +144,12 @@ export class CommandManager {
|
|
|
273
144
|
return;
|
|
274
145
|
}
|
|
275
146
|
|
|
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
|
-
);
|
|
147
|
+
await SettingsEditor.show(ctx.ui, this.settings);
|
|
295
148
|
}
|
|
296
149
|
|
|
297
150
|
/**
|
|
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,
|
|
151
|
+
* Runs the interactive servers editor for `llamaSettings.servers`
|
|
152
|
+
* (see `ServerSettingsList` for the editing semantics). After closing,
|
|
306
153
|
* providers are re-registered so server changes apply immediately.
|
|
307
154
|
*/
|
|
308
155
|
private async runServersEditor(
|
|
@@ -317,49 +164,17 @@ export class CommandManager {
|
|
|
317
164
|
return;
|
|
318
165
|
}
|
|
319
166
|
|
|
320
|
-
|
|
321
|
-
const { serverTimeout } = await this.settings.resolveTimeouts();
|
|
167
|
+
await ServerSettingsList.show(ctx.ui, this.settings);
|
|
322
168
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
new ServerSettingsList({
|
|
326
|
-
tui,
|
|
327
|
-
theme,
|
|
328
|
-
keybindings,
|
|
329
|
-
servers,
|
|
330
|
-
persist: (next) => this.settings.setLlamaSetting("servers", next),
|
|
331
|
-
done: () => {
|
|
332
|
-
done(undefined);
|
|
333
|
-
// Re-register providers so the updated server list takes effect
|
|
334
|
-
this.serverManager.update(pi);
|
|
335
|
-
},
|
|
336
|
-
onError: (message) => ctx.ui.notify(message, "error"),
|
|
337
|
-
serverTimeout,
|
|
338
|
-
}),
|
|
339
|
-
);
|
|
169
|
+
// Re-register providers so the updated server list takes effect
|
|
170
|
+
await this.serverManager.update(pi);
|
|
340
171
|
}
|
|
341
172
|
|
|
342
173
|
/**
|
|
343
174
|
* Runs the interactive overrides editor for
|
|
344
|
-
* `llamaSettings.servers[].overrides
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* the field-edit submenu; a adds, d deletes (after confirmation).
|
|
348
|
-
*
|
|
349
|
-
* Fields use a mix of finite (Enter to cycle) and infinite (Enter to
|
|
350
|
-
* type) editing:
|
|
351
|
-
*
|
|
352
|
-
* - Pattern / costs (input, output, cacheRead, cacheWrite): infinite —
|
|
353
|
-
* Enter opens an Input for typing.
|
|
354
|
-
* - Capabilities: finite — Enter cycles between `text` and `text | image`.
|
|
355
|
-
* - Reasoning: finite — Enter cycles between `true` and `false`.
|
|
356
|
-
*
|
|
357
|
-
* Servers themselves are not managed here — use `/models servers`.
|
|
358
|
-
*
|
|
359
|
-
* Writes go to the global `~/.pi/agent/settings.json` via
|
|
360
|
-
* `LlamaSettingsManager.setLlamaSetting()`; write errors are notified and
|
|
361
|
-
* leave the values unchanged. After closing, providers are
|
|
362
|
-
* re-registered so new overrides take effect on the next request.
|
|
175
|
+
* `llamaSettings.servers[].overrides` (see `OverrideSettingsList` for
|
|
176
|
+
* the editing semantics). After closing, providers are re-registered
|
|
177
|
+
* so new overrides take effect on the next request.
|
|
363
178
|
*/
|
|
364
179
|
private async runOverridesEditor(
|
|
365
180
|
ctx: ExtensionCommandContext,
|
|
@@ -373,23 +188,10 @@ export class CommandManager {
|
|
|
373
188
|
return;
|
|
374
189
|
}
|
|
375
190
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
theme,
|
|
381
|
-
keybindings,
|
|
382
|
-
servers,
|
|
383
|
-
persist: (next) => this.settings.setLlamaSetting("servers", next),
|
|
384
|
-
done: () => {
|
|
385
|
-
done(undefined);
|
|
386
|
-
// Re-register providers so the updated overrides take effect
|
|
387
|
-
this.serverManager.update(pi);
|
|
388
|
-
},
|
|
389
|
-
onError: (message) => ctx.ui.notify(message, "error"),
|
|
390
|
-
onChanged: () => {}, // no per-change notification needed
|
|
391
|
-
}),
|
|
392
|
-
);
|
|
191
|
+
await OverrideSettingsList.show(ctx.ui, this.settings);
|
|
192
|
+
|
|
193
|
+
// Re-register providers so the updated overrides take effect
|
|
194
|
+
await this.serverManager.update(pi);
|
|
393
195
|
}
|
|
394
196
|
|
|
395
197
|
/**
|
|
@@ -398,233 +200,4 @@ export class CommandManager {
|
|
|
398
200
|
private notifyNotFound(ctx: ExtensionCommandContext, url: string): void {
|
|
399
201
|
ctx.ui.notify(`${PROVIDER_NAME} unreachable at ${url}`, "error");
|
|
400
202
|
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Runs the interactive model selection menu.
|
|
404
|
-
*/
|
|
405
|
-
private async runModelsMenu(
|
|
406
|
-
ctx: ExtensionCommandContext,
|
|
407
|
-
pi: ExtensionAPI,
|
|
408
|
-
): Promise<void> {
|
|
409
|
-
const event = await this.modelSelectionHandler(
|
|
410
|
-
ctx,
|
|
411
|
-
await this.serverManager.getAllModels(),
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
if (!event) return;
|
|
415
|
-
const { action, model } = event;
|
|
416
|
-
|
|
417
|
-
// Action: Cancel
|
|
418
|
-
if (!action || action === Action.CANCEL) return;
|
|
419
|
-
|
|
420
|
-
// Action: Info
|
|
421
|
-
if (action === Action.INFO) {
|
|
422
|
-
const info = await model.getInfo();
|
|
423
|
-
ctx.ui.notify(`${info}`, "info");
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Action: Unload
|
|
428
|
-
if (action === Action.UNLOAD) {
|
|
429
|
-
await model.unload();
|
|
430
|
-
ctx.ui.notify(`Unloaded ${model.name}`, "info");
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Action: Switch
|
|
435
|
-
if (action === Action.SWITCH) {
|
|
436
|
-
const { serverId } = model;
|
|
437
|
-
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
438
|
-
if (!piModel)
|
|
439
|
-
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
440
|
-
|
|
441
|
-
await pi.setModel(piModel);
|
|
442
|
-
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Actions: Load / Load & Switch / Retry
|
|
447
|
-
const loadActions = [Action.LOAD, Action.LOAD_AND_SWITCH, Action.RETRY];
|
|
448
|
-
if (loadActions.includes(action)) {
|
|
449
|
-
ctx.ui.notify(`Loading ${model.name}...`, "info");
|
|
450
|
-
// Mark the load as in-flight so session_before_switch can warn about
|
|
451
|
-
// it (see EventManager.inflightModel for the coupling rationale)
|
|
452
|
-
EventManager.inflightModel = model;
|
|
453
|
-
|
|
454
|
-
// Subscribe to progress events; skip when the server is gone
|
|
455
|
-
// (removed/edited away mid-load → getServer returns undefined)
|
|
456
|
-
const server = this.serverManager.getServer(model);
|
|
457
|
-
const cleanupProgress =
|
|
458
|
-
server?.sseManager.subscribeToProgress(
|
|
459
|
-
model.id,
|
|
460
|
-
(percentage, stage) => {
|
|
461
|
-
const stageText = stage ? ` (${stage})` : "";
|
|
462
|
-
ctx.ui.notify(
|
|
463
|
-
`Loading ${model.name}... [${percentage}%${stageText}]`,
|
|
464
|
-
"info",
|
|
465
|
-
);
|
|
466
|
-
},
|
|
467
|
-
) ?? (() => {});
|
|
468
|
-
|
|
469
|
-
const onSuccess = async () => {
|
|
470
|
-
const { serverId } = model;
|
|
471
|
-
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
472
|
-
if (!piModel)
|
|
473
|
-
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
474
|
-
|
|
475
|
-
// Verify auth
|
|
476
|
-
if ((await model.getStatus()) === Status.UNAUTHORIZED)
|
|
477
|
-
throw new Error(
|
|
478
|
-
`Unauthorized for ${model.name}. Use /login and add your API key.`,
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
// Verify failure
|
|
482
|
-
if ((await model.getStatus()) === Status.FAILED)
|
|
483
|
-
throw new Error(`Failed to load model ${model.name}`);
|
|
484
|
-
|
|
485
|
-
// Select the model if asked
|
|
486
|
-
if (action === Action.LOAD_AND_SWITCH) await pi.setModel(piModel);
|
|
487
|
-
|
|
488
|
-
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
const onFailure = (err: any) => {
|
|
492
|
-
const message = errorMessage(err);
|
|
493
|
-
|
|
494
|
-
try {
|
|
495
|
-
ctx.ui.notify(message, "error");
|
|
496
|
-
} catch {
|
|
497
|
-
// ctx went stale between error and notification
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
const onFinished = async () => {
|
|
502
|
-
cleanupProgress();
|
|
503
|
-
EventManager.resetInflightModel();
|
|
504
|
-
|
|
505
|
-
// Re-scan providers to ensure accuracy of loaded models
|
|
506
|
-
await this.serverManager.update(pi);
|
|
507
|
-
|
|
508
|
-
// Force TUI refresh so Pi picks up the updated model states
|
|
509
|
-
ctx.ui.setStatus(PROVIDER_NAME, " ");
|
|
510
|
-
ctx.ui.setStatus(PROVIDER_NAME, undefined);
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// Load the model without blocking the UI
|
|
514
|
-
model.load().then(onSuccess).catch(onFailure).finally(onFinished);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Handles the menu for model selection.
|
|
520
|
-
* Loops: select model → select action → handle action.
|
|
521
|
-
*
|
|
522
|
-
* Escape on actions menu goes back to model selection.
|
|
523
|
-
* Escape on model selection exits.
|
|
524
|
-
*
|
|
525
|
-
* @returns The selected action and model
|
|
526
|
-
*/
|
|
527
|
-
private async modelSelectionHandler(
|
|
528
|
-
ctx: ExtensionCommandContext,
|
|
529
|
-
models: BaseModel[],
|
|
530
|
-
): Promise<{ action: Action; model: BaseModel } | null> {
|
|
531
|
-
while (true) {
|
|
532
|
-
// Select the model
|
|
533
|
-
const model = await this.selectModel(ctx, models);
|
|
534
|
-
if (!model) return null;
|
|
535
|
-
|
|
536
|
-
// Select the action
|
|
537
|
-
const actions = await this.getActionsForModel(model);
|
|
538
|
-
const action = await this.selectAction(ctx, model, actions);
|
|
539
|
-
if (action === null) {
|
|
540
|
-
// Escape key pressed => back to model selection
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Return the selected action and model
|
|
545
|
-
return { action, model };
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Select a model from the list. Returns null if user cancels.
|
|
551
|
-
*
|
|
552
|
-
* @returns The model selected by the user
|
|
553
|
-
*/
|
|
554
|
-
private async selectModel(
|
|
555
|
-
ctx: ExtensionCommandContext,
|
|
556
|
-
models: BaseModel[],
|
|
557
|
-
): Promise<BaseModel | null> {
|
|
558
|
-
const labels = await Promise.all(
|
|
559
|
-
models.map(async (model) => ({
|
|
560
|
-
label: (await model.getLabel()).trim(),
|
|
561
|
-
serverUrl: model.serverUrl,
|
|
562
|
-
})),
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
// Count grapheme clusters (not UTF-16 code units) so emoji padding aligns visually
|
|
566
|
-
const graphemeLength = (str: string) =>
|
|
567
|
-
[...new Intl.Segmenter().segment(str)].length;
|
|
568
|
-
|
|
569
|
-
// Decorate the label so the spacing makes it seem more like a table
|
|
570
|
-
const maxLength = Math.max(
|
|
571
|
-
...labels.map(({ label }) => graphemeLength(label)),
|
|
572
|
-
);
|
|
573
|
-
const choices = labels.map(({ label, serverUrl }) => {
|
|
574
|
-
const extraPadding = 2;
|
|
575
|
-
const padLen = maxLength - graphemeLength(label) + extraPadding;
|
|
576
|
-
return `${label}${" ".repeat(padLen)} [Server: ${serverUrl}]`;
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, choices);
|
|
580
|
-
if (!choice) return null;
|
|
581
|
-
const idx = choices.indexOf(choice);
|
|
582
|
-
|
|
583
|
-
return models[idx];
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Get available actions for a model based on its mode and status.
|
|
588
|
-
*
|
|
589
|
-
* @returns A mapping of actions for each status
|
|
590
|
-
*/
|
|
591
|
-
private async getActionsForModel(model: BaseModel): Promise<Array<Action>> {
|
|
592
|
-
const base = [Action.INFO, Action.CANCEL];
|
|
593
|
-
|
|
594
|
-
const actions: Record<Status, Array<Action>> = {
|
|
595
|
-
[Status.LOADED]:
|
|
596
|
-
model.mode === Mode.ROUTER
|
|
597
|
-
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
598
|
-
: [Action.SWITCH, ...base],
|
|
599
|
-
[Status.LOADING]: [...base],
|
|
600
|
-
[Status.FAILED]: [Action.RETRY, ...base],
|
|
601
|
-
[Status.SLEEPING]:
|
|
602
|
-
model.mode === Mode.ROUTER
|
|
603
|
-
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
604
|
-
: [Action.SWITCH, ...base],
|
|
605
|
-
[Status.UNLOADED]: [Action.LOAD_AND_SWITCH, Action.LOAD, ...base],
|
|
606
|
-
[Status.UNAUTHORIZED]: [...base],
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
const status = await model.getStatus();
|
|
610
|
-
return actions[status];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Selects an action for a model.
|
|
615
|
-
*
|
|
616
|
-
* @returns The selected action
|
|
617
|
-
*/
|
|
618
|
-
private async selectAction(
|
|
619
|
-
ctx: ExtensionCommandContext,
|
|
620
|
-
model: BaseModel,
|
|
621
|
-
actions: Array<Action>,
|
|
622
|
-
): Promise<Action | null> {
|
|
623
|
-
const labels = actions.map((a) => String(a));
|
|
624
|
-
const choice = await ctx.ui.select(`${model.name}`, labels);
|
|
625
|
-
if (!choice) return null;
|
|
626
|
-
|
|
627
|
-
const idx = labels.indexOf(choice);
|
|
628
|
-
return actions[idx];
|
|
629
|
-
}
|
|
630
203
|
}
|
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,8 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ProviderModelConfig,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { ApiError } from "../api/client";
|
|
6
|
+
import { API_TYPE, PROVIDER_NAME } from "../constants";
|
|
3
7
|
import { ServerStatus } from "../enums/serverStatus";
|
|
8
|
+
import type { SortBy } from "../interfaces/sortBy";
|
|
4
9
|
import { BaseModel } from "../models/baseModel";
|
|
5
10
|
import { Server } from "../server";
|
|
11
|
+
import { authRequiredMessage } from "../ui/strings";
|
|
6
12
|
import type { LlamaSettingsManager } from "./settings";
|
|
7
13
|
|
|
8
14
|
/** Model-list comparator: negative if a sorts first, positive if b does. */
|
|
@@ -43,6 +49,7 @@ export class ServerManager {
|
|
|
43
49
|
*/
|
|
44
50
|
async update(pi: ExtensionAPI, timeout?: number) {
|
|
45
51
|
this.failedUrls.length = 0;
|
|
52
|
+
this.warnings.length = 0;
|
|
46
53
|
|
|
47
54
|
// Surface warnings from strict URL parsing (dropped invalid entries)
|
|
48
55
|
this.warnings.push(...this.settings.takeWarnings());
|
|
@@ -58,9 +65,9 @@ export class ServerManager {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// Unregister providers that disappeared (removed or edited away);
|
|
61
|
-
//
|
|
68
|
+
// `seen` tracks all kept providerIds, so we skip those still present.
|
|
62
69
|
for (const old of this.servers) {
|
|
63
|
-
if (
|
|
70
|
+
if (seen.has(old.providerId)) continue;
|
|
64
71
|
pi.unregisterProvider(old.providerId);
|
|
65
72
|
// Optional chain is intentional despite the non-optional type: `sse`
|
|
66
73
|
// is undefined until initialize() runs (async-constructor hack — see Server)
|
|
@@ -77,13 +84,7 @@ export class ServerManager {
|
|
|
77
84
|
|
|
78
85
|
// Initialization and registration
|
|
79
86
|
for (const server of registrableServers) {
|
|
80
|
-
|
|
81
|
-
await server.initialize();
|
|
82
|
-
await this.registerProvider(server, pi);
|
|
83
|
-
} catch {
|
|
84
|
-
this.failedUrls.push(server.baseUrl);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
+
await this.registerProvider(server, pi);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -128,22 +129,37 @@ export class ServerManager {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
|
-
*
|
|
132
|
+
* Initializes the server and creates a Pi provider.
|
|
133
|
+
* Handles auth errors by registering with an empty model list so the user
|
|
134
|
+
* can still configure the API key via `/login` or `auth.json`.
|
|
132
135
|
*
|
|
133
136
|
* @param server The server
|
|
137
|
+
* @param pi The Pi API
|
|
134
138
|
*/
|
|
135
139
|
private async registerProvider(server: Server, pi: ExtensionAPI) {
|
|
136
|
-
const { apiBaseUrl, models, providerId, providerName } =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const { apiBaseUrl, apiKey, baseUrl, models, providerId, providerName } =
|
|
141
|
+
server;
|
|
142
|
+
let modelConfigs: ProviderModelConfig[] = [];
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await server.initialize();
|
|
146
|
+
modelConfigs = await Promise.all(models.map((m) => m.toProviderConfig()));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof ApiError && err.type === "authentication") {
|
|
149
|
+
// Don't add to `failedUrls` — the server IS reachable, auth just
|
|
150
|
+
// isn't configured yet, so the health indicator should stay green.
|
|
151
|
+
this.warnings.push(authRequiredMessage(baseUrl, providerId));
|
|
152
|
+
} else {
|
|
153
|
+
this.failedUrls.push(baseUrl);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
141
157
|
|
|
142
158
|
pi.registerProvider(providerId, {
|
|
143
159
|
name: providerName,
|
|
144
160
|
baseUrl: apiBaseUrl,
|
|
145
161
|
api: API_TYPE,
|
|
146
|
-
apiKey
|
|
162
|
+
apiKey,
|
|
147
163
|
models: modelConfigs,
|
|
148
164
|
});
|
|
149
165
|
}
|