pi-llama-cpp 0.13.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 +7 -8
- package/package.json +2 -2
- package/src/api/client.ts +25 -0
- package/src/constants.ts +0 -5
- package/src/enums/status.ts +0 -1
- package/src/interfaces/endpoints/models.ts +1 -1
- package/src/interfaces/settings.ts +1 -1
- package/src/interfaces/sortBy.ts +4 -0
- package/src/managers/command/models.ts +247 -0
- package/src/managers/command.ts +29 -461
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +27 -3
- 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 +11 -29
- 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/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 +14 -9
- package/tests/dialog.test.ts +55 -4
- package/tests/legacyModel.test.ts +34 -28
- package/tests/overrides.test.ts +111 -88
- package/tests/routerModel.test.ts +67 -68
- package/tests/server.test.ts +0 -12
- 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 -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/README.md
CHANGED
|
@@ -17,14 +17,13 @@ A [Pi Coding Agent](https://pi.dev/) extension that integrates with running [lla
|
|
|
17
17
|
|
|
18
18
|
### Status Indicators
|
|
19
19
|
|
|
20
|
-
| Icon | Status
|
|
21
|
-
| ---- |
|
|
22
|
-
| 🟢 | Loaded
|
|
23
|
-
| 🟡 | Loading
|
|
24
|
-
| 🔴 | Failed
|
|
25
|
-
| 🔵 | Sleeping
|
|
26
|
-
| ⚪ | Unloaded
|
|
27
|
-
| ⛔ | Unauthorized | Model can't be used (API key required) |
|
|
20
|
+
| Icon | Status | Description |
|
|
21
|
+
| ---- | -------- | --------------------------------- |
|
|
22
|
+
| 🟢 | Loaded | Model is active and ready to use |
|
|
23
|
+
| 🟡 | Loading | Model is currently being loaded |
|
|
24
|
+
| 🔴 | Failed | Model failed to load |
|
|
25
|
+
| 🔵 | Sleeping | Model is available, but inactive |
|
|
26
|
+
| ⚪ | Unloaded | Model is not loaded on the server |
|
|
28
27
|
|
|
29
28
|
> **Note**: The `Sleeping` status only shows when you start your server with `llama-server --sleep-idle-seconds <n> ...`.
|
|
30
29
|
> This is a **llama.cpp server flag** that tells the server to put idle models to sleep after `n` seconds.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-llama-cpp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pi extension for llama.cpp integration. Supports router, single and legacy models. Supports multiple servers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"type": "module",
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^26.
|
|
41
|
+
"@types/node": "^26.6.2",
|
|
42
42
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
43
43
|
"vitest": "^5.0.1"
|
|
44
44
|
}
|
package/src/api/client.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { POLLING_INTERVAL } from "../constants";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown by the API client when the server rejects a request.
|
|
5
|
+
*/
|
|
6
|
+
export class ApiError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
public readonly type: "authentication" | "server" | "unknown",
|
|
9
|
+
public readonly statusCode: number,
|
|
10
|
+
) {
|
|
11
|
+
super(`API error (${statusCode})`);
|
|
12
|
+
this.name = "ApiError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
/**
|
|
4
17
|
* How long GET responses stay cached: half the polling interval, so a poll
|
|
5
18
|
* tick always reaches the server while multiple reads within one tick
|
|
@@ -126,6 +139,8 @@ export class ApiClient {
|
|
|
126
139
|
*
|
|
127
140
|
* @param endpoint The endpoint path to fetch (e.g. "/health")
|
|
128
141
|
* @returns The parsed JSON response from the server
|
|
142
|
+
* @throws ApiError with status 401 when the server rejects the request
|
|
143
|
+
* due to authentication (invalid/missing API key).
|
|
129
144
|
*/
|
|
130
145
|
private async do_get<T>(endpoint: string): Promise<T> {
|
|
131
146
|
const url = `${this.baseUrl}${endpoint}`;
|
|
@@ -134,6 +149,10 @@ export class ApiClient {
|
|
|
134
149
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
135
150
|
});
|
|
136
151
|
|
|
152
|
+
if (res.status === 401) {
|
|
153
|
+
throw new ApiError("authentication", res.status);
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
return res.json();
|
|
138
157
|
}
|
|
139
158
|
|
|
@@ -144,6 +163,8 @@ export class ApiClient {
|
|
|
144
163
|
* @param endpoint The endpoint path to post to
|
|
145
164
|
* @param body The optional request body
|
|
146
165
|
* @returns The parsed JSON response from the server
|
|
166
|
+
* @throws ApiError with status 401 when the server rejects the request
|
|
167
|
+
* due to authentication (invalid/missing API key).
|
|
147
168
|
*/
|
|
148
169
|
private async do_post<T>(
|
|
149
170
|
endpoint: string,
|
|
@@ -160,6 +181,10 @@ export class ApiClient {
|
|
|
160
181
|
body: body ? JSON.stringify(body) : undefined,
|
|
161
182
|
});
|
|
162
183
|
|
|
184
|
+
if (res.status === 401) {
|
|
185
|
+
throw new ApiError("authentication", res.status);
|
|
186
|
+
}
|
|
187
|
+
|
|
163
188
|
return res.json();
|
|
164
189
|
}
|
|
165
190
|
}
|
package/src/constants.ts
CHANGED
|
@@ -76,11 +76,6 @@ export const AUTOLOAD_ON_MESSAGE = false;
|
|
|
76
76
|
*/
|
|
77
77
|
export const SORT_BY = "asc";
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
* Sort order options for model lists.
|
|
81
|
-
*/
|
|
82
|
-
export type SortBy = "asc" | "desc" | "asc-name" | "desc-name" | "api";
|
|
83
|
-
|
|
84
79
|
/**
|
|
85
80
|
* Thinking budgets to send to the server, depending on user-selected level in Pi.
|
|
86
81
|
*/
|
package/src/enums/status.ts
CHANGED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { PROVIDER_NAME } from "../../constants";
|
|
6
|
+
import { Action } from "../../enums/action";
|
|
7
|
+
import { Mode } from "../../enums/mode";
|
|
8
|
+
import { Status } from "../../enums/status";
|
|
9
|
+
import { BaseModel } from "../../models/baseModel";
|
|
10
|
+
import { errorMessage } from "../../utils/errors";
|
|
11
|
+
import { EventManager } from "../events";
|
|
12
|
+
import type { ServerManager } from "../server";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interactive model selection/action flow for the `/models` command.
|
|
16
|
+
*
|
|
17
|
+
* Owns the select-model → select-action loop and the per-status action
|
|
18
|
+
* tables, plus the non-blocking model load pipeline (progress events,
|
|
19
|
+
* inflight tracking, success/failure handling).
|
|
20
|
+
*
|
|
21
|
+
* Split out of `CommandManager`: command routing stays there;
|
|
22
|
+
* this class is the self-contained interactive concern.
|
|
23
|
+
* `ServerManager` is injected so load/refresh paths can reach the
|
|
24
|
+
* server registry without coupling the menu to command routing.
|
|
25
|
+
*/
|
|
26
|
+
export class ModelsMenu {
|
|
27
|
+
constructor(private readonly serverManager: ServerManager) {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs the interactive model selection menu.
|
|
31
|
+
*/
|
|
32
|
+
async show(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
33
|
+
const event = await this.selectionLoop(
|
|
34
|
+
ctx,
|
|
35
|
+
await this.serverManager.getAllModels(),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!event) return;
|
|
39
|
+
const { action, model } = event;
|
|
40
|
+
|
|
41
|
+
// Action: Cancel
|
|
42
|
+
if (!action || action === Action.CANCEL) return;
|
|
43
|
+
|
|
44
|
+
// Action: Info
|
|
45
|
+
if (action === Action.INFO) {
|
|
46
|
+
const info = await model.getInfo();
|
|
47
|
+
ctx.ui.notify(`${info}`, "info");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Action: Unload
|
|
52
|
+
if (action === Action.UNLOAD) {
|
|
53
|
+
await model.unload();
|
|
54
|
+
ctx.ui.notify(`Unloaded ${model.name}`, "info");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Action: Switch
|
|
59
|
+
if (action === Action.SWITCH) {
|
|
60
|
+
const { serverId } = model;
|
|
61
|
+
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
62
|
+
if (!piModel)
|
|
63
|
+
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
64
|
+
|
|
65
|
+
await pi.setModel(piModel);
|
|
66
|
+
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Actions: Load / Load & Switch / Retry
|
|
71
|
+
const loadActions = [Action.LOAD, Action.LOAD_AND_SWITCH, Action.RETRY];
|
|
72
|
+
if (loadActions.includes(action)) {
|
|
73
|
+
ctx.ui.notify(`Loading ${model.name}...`, "info");
|
|
74
|
+
// Mark the load as in-flight so session_before_switch can warn about
|
|
75
|
+
// it (see EventManager.inflightModel for the coupling rationale)
|
|
76
|
+
EventManager.inflightModel = model;
|
|
77
|
+
|
|
78
|
+
// Subscribe to progress events; skip when the server is gone
|
|
79
|
+
// (removed/edited away mid-load → getServer returns undefined)
|
|
80
|
+
const server = this.serverManager.getServer(model);
|
|
81
|
+
const cleanupProgress =
|
|
82
|
+
server?.sseManager.subscribeToProgress(
|
|
83
|
+
model.id,
|
|
84
|
+
(percentage, stage) => {
|
|
85
|
+
const stageText = stage ? ` (${stage})` : "";
|
|
86
|
+
ctx.ui.notify(
|
|
87
|
+
`Loading ${model.name}... [${percentage}%${stageText}]`,
|
|
88
|
+
"info",
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
) ?? (() => {});
|
|
92
|
+
|
|
93
|
+
const onSuccess = async () => {
|
|
94
|
+
const { serverId } = model;
|
|
95
|
+
const piModel = ctx.modelRegistry.find(serverId, model.id);
|
|
96
|
+
if (!piModel)
|
|
97
|
+
throw new Error(`Cannot find model ${model.name} in pi registry`);
|
|
98
|
+
|
|
99
|
+
// Verify failure
|
|
100
|
+
if ((await model.getStatus()) === Status.FAILED)
|
|
101
|
+
throw new Error(`Failed to load model ${model.name}`);
|
|
102
|
+
|
|
103
|
+
// Select the model if asked
|
|
104
|
+
if (action === Action.LOAD_AND_SWITCH) await pi.setModel(piModel);
|
|
105
|
+
|
|
106
|
+
ctx.ui.notify(`Model ${model.name} ready`, "info");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onFailure = (err: any) => {
|
|
110
|
+
const message = errorMessage(err);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
ctx.ui.notify(message, "error");
|
|
114
|
+
} catch {
|
|
115
|
+
// ctx went stale between error and notification
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const onFinished = async () => {
|
|
120
|
+
cleanupProgress();
|
|
121
|
+
EventManager.resetInflightModel();
|
|
122
|
+
|
|
123
|
+
// Re-scan providers to ensure accuracy of loaded models
|
|
124
|
+
await this.serverManager.update(pi);
|
|
125
|
+
|
|
126
|
+
// Force TUI refresh so Pi picks up the updated model states
|
|
127
|
+
ctx.ui.setStatus(PROVIDER_NAME, " ");
|
|
128
|
+
ctx.ui.setStatus(PROVIDER_NAME, undefined);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Load the model without blocking the UI
|
|
132
|
+
model.load().then(onSuccess).catch(onFailure).finally(onFinished);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handles the menu for model selection.
|
|
138
|
+
* Loops: select model → select action → handle action.
|
|
139
|
+
*
|
|
140
|
+
* Escape on actions menu goes back to model selection.
|
|
141
|
+
* Escape on model selection exits.
|
|
142
|
+
*
|
|
143
|
+
* @returns The selected action and model
|
|
144
|
+
*/
|
|
145
|
+
private async selectionLoop(
|
|
146
|
+
ctx: ExtensionCommandContext,
|
|
147
|
+
models: BaseModel[],
|
|
148
|
+
): Promise<{ action: Action; model: BaseModel } | null> {
|
|
149
|
+
while (true) {
|
|
150
|
+
// Select the model
|
|
151
|
+
const model = await this.selectModel(ctx, models);
|
|
152
|
+
if (!model) return null;
|
|
153
|
+
|
|
154
|
+
// Select the action
|
|
155
|
+
const actions = await this.getActionsForModel(model);
|
|
156
|
+
const action = await this.selectAction(ctx, model, actions);
|
|
157
|
+
if (action === null) {
|
|
158
|
+
// Escape key pressed => back to model selection
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Return the selected action and model
|
|
163
|
+
return { action, model };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Select a model from the list. Returns null if user cancels.
|
|
169
|
+
*
|
|
170
|
+
* @returns The model selected by the user
|
|
171
|
+
*/
|
|
172
|
+
private async selectModel(
|
|
173
|
+
ctx: ExtensionCommandContext,
|
|
174
|
+
models: BaseModel[],
|
|
175
|
+
): Promise<BaseModel | null> {
|
|
176
|
+
const labels = await Promise.all(
|
|
177
|
+
models.map(async (model) => ({
|
|
178
|
+
label: (await model.getLabel()).trim(),
|
|
179
|
+
serverUrl: model.serverUrl,
|
|
180
|
+
})),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Count grapheme clusters (not UTF-16 code units) so emoji padding aligns visually
|
|
184
|
+
const graphemeLength = (str: string) =>
|
|
185
|
+
[...new Intl.Segmenter().segment(str)].length;
|
|
186
|
+
|
|
187
|
+
// Decorate the label so the spacing makes it seem more like a table
|
|
188
|
+
const maxLength = Math.max(
|
|
189
|
+
...labels.map(({ label }) => graphemeLength(label)),
|
|
190
|
+
);
|
|
191
|
+
const choices = labels.map(({ label, serverUrl }) => {
|
|
192
|
+
const extraPadding = 2;
|
|
193
|
+
const padLen = maxLength - graphemeLength(label) + extraPadding;
|
|
194
|
+
return `${label}${" ".repeat(padLen)} [Server: ${serverUrl}]`;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, choices);
|
|
198
|
+
if (!choice) return null;
|
|
199
|
+
const idx = choices.indexOf(choice);
|
|
200
|
+
|
|
201
|
+
return models[idx];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get available actions for a model based on its mode and status.
|
|
206
|
+
*
|
|
207
|
+
* @returns A mapping of actions for each status
|
|
208
|
+
*/
|
|
209
|
+
private async getActionsForModel(model: BaseModel): Promise<Array<Action>> {
|
|
210
|
+
const base = [Action.INFO, Action.CANCEL];
|
|
211
|
+
|
|
212
|
+
const actions: Record<Status, Array<Action>> = {
|
|
213
|
+
[Status.LOADED]:
|
|
214
|
+
model.mode === Mode.ROUTER
|
|
215
|
+
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
216
|
+
: [Action.SWITCH, ...base],
|
|
217
|
+
[Status.LOADING]: [...base],
|
|
218
|
+
[Status.FAILED]: [Action.RETRY, ...base],
|
|
219
|
+
[Status.SLEEPING]:
|
|
220
|
+
model.mode === Mode.ROUTER
|
|
221
|
+
? [Action.SWITCH, Action.UNLOAD, ...base]
|
|
222
|
+
: [Action.SWITCH, ...base],
|
|
223
|
+
[Status.UNLOADED]: [Action.LOAD_AND_SWITCH, Action.LOAD, ...base],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const status = await model.getStatus();
|
|
227
|
+
return actions[status];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Selects an action for a model.
|
|
232
|
+
*
|
|
233
|
+
* @returns The selected action
|
|
234
|
+
*/
|
|
235
|
+
private async selectAction(
|
|
236
|
+
ctx: ExtensionCommandContext,
|
|
237
|
+
model: BaseModel,
|
|
238
|
+
actions: Array<Action>,
|
|
239
|
+
): Promise<Action | null> {
|
|
240
|
+
const labels = actions.map((a) => String(a));
|
|
241
|
+
const choice = await ctx.ui.select(`${model.name}`, labels);
|
|
242
|
+
if (!choice) return null;
|
|
243
|
+
|
|
244
|
+
const idx = labels.indexOf(choice);
|
|
245
|
+
return actions[idx];
|
|
246
|
+
}
|
|
247
|
+
}
|