pi-llama-cpp 0.5.1 → 0.7.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.
@@ -1,228 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
5
- SessionBeforeSwitchEvent,
6
- } from "@earendil-works/pi-coding-agent";
7
- import { PROVIDER_ID, PROVIDER_NAME, READABLE_TIMEOUT } from "../constants";
8
- import { Action } from "../enums/action";
9
- import { Mode } from "../enums/mode";
10
- import { Status } from "../enums/status";
11
- import { BaseModel } from "../models/baseModel";
12
- import { resolveUrl } from "../tools/resolver";
13
-
14
- // In-flight model reference — handler gates on this.
15
- let inflightModel: BaseModel | null = null;
16
-
17
- export const resetInflightModel = () => (inflightModel = null);
18
-
19
- /**
20
- * Session-switch handler. Registered once at extension init.
21
- * Only notifies if a model load is actually in-flight.
22
- */
23
- export const onSessionBeforeSwitch = async (
24
- _event: SessionBeforeSwitchEvent,
25
- ctx: ExtensionContext,
26
- ) => {
27
- if (!inflightModel) return;
28
-
29
- const messages = [
30
- `Session change detected while model '${inflightModel.name}' was still loading.`,
31
- "Model load will continue in the background, but UI might not update.",
32
- "",
33
- "Verify that your new model is loaded, or use /models to re-select it afterwards.",
34
- ];
35
- ctx.ui.notify(messages.join("\n"), "warning");
36
-
37
- // Show the notification for a reasonable amount of time
38
- await new Promise((r) => setTimeout(r, READABLE_TIMEOUT));
39
- };
40
-
41
- /**
42
- * Select a model from the list. Returns null if user cancels.
43
- *
44
- * @param ctx Pi context
45
- * @param models A list of models
46
- * @returns The selected model
47
- */
48
- const selectModel = async (
49
- ctx: ExtensionCommandContext,
50
- models: BaseModel[],
51
- ): Promise<BaseModel | null> => {
52
- const labels = await Promise.all(models.map((m) => m.getLabel()));
53
- const choice = await ctx.ui.select(`${PROVIDER_NAME} models:`, labels);
54
- if (!choice) return null;
55
- const idx = labels.indexOf(choice);
56
- return models[idx];
57
- };
58
-
59
- /**
60
- * Get available actions for a model based on its mode and status.
61
- *
62
- * @param model The selected model
63
- * @returns The array of available actions for the given model status
64
- */
65
- const getActionsForModel = async (model: BaseModel): Promise<Array<Action>> => {
66
- const routerModeActions: Record<Status, Array<Action>> = {
67
- [Status.LOADED]: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
68
- [Status.LOADING]: [Action.INFO, Action.CANCEL],
69
- [Status.FAILED]: [Action.RETRY, Action.CANCEL],
70
- [Status.SLEEPING]: [
71
- Action.SWITCH,
72
- Action.UNLOAD,
73
- Action.INFO,
74
- Action.CANCEL,
75
- ],
76
- [Status.UNLOADED]: [Action.LOAD, Action.CANCEL],
77
- };
78
-
79
- const singleModeActions: Record<Status, Array<Action>> = {
80
- [Status.LOADED]: [Action.INFO, Action.CANCEL],
81
- [Status.LOADING]: [Action.CANCEL],
82
- [Status.FAILED]: [Action.CANCEL],
83
- [Status.SLEEPING]: [Action.INFO, Action.CANCEL],
84
- [Status.UNLOADED]: [Action.CANCEL],
85
- };
86
-
87
- const allActions =
88
- model.mode === Mode.ROUTER ? routerModeActions : singleModeActions;
89
-
90
- const status = await model.getStatus();
91
- return allActions[status];
92
- };
93
-
94
- /**
95
- * Selects an action for a model.
96
- *
97
- * @param ctx Pi context
98
- * @param model The selected model
99
- * @param actions Possible actions to execute
100
- * @returns The action, or null if user cancels
101
- */
102
- const selectAction = async (
103
- ctx: ExtensionCommandContext,
104
- model: BaseModel,
105
- actions: Array<Action>,
106
- ): Promise<Action | null> => {
107
- const labels = actions.map((a) => String(a));
108
- const choice = await ctx.ui.select(`${model.name}`, labels);
109
- if (!choice) return null;
110
-
111
- const idx = labels.indexOf(choice);
112
- return actions[idx];
113
- };
114
-
115
- /**
116
- * Handles the menu for model selection
117
- * Loops: select model → select action → handle action.
118
- *
119
- * Escape on actions menu goes back to model selection.
120
- * Escape on model selection exits.
121
- *
122
- * @param ctx Pi context
123
- * @returns The action and model, if detected
124
- */
125
- const modelSelectionHandler = async (
126
- ctx: ExtensionCommandContext,
127
- models: BaseModel[],
128
- ): Promise<{ action: Action; model: BaseModel } | null> => {
129
- while (true) {
130
- // Select the model
131
- const model = await selectModel(ctx, models);
132
- if (!model) return null;
133
-
134
- // Select the action
135
- const actions = await getActionsForModel(model);
136
- const action = await selectAction(ctx, model, actions);
137
- if (action === null) {
138
- // Escape key pressed => back to model selection
139
- continue;
140
- }
141
-
142
- // Return the selected action and model
143
- return { action, model };
144
- }
145
- };
146
-
147
- /**
148
- * Handles the /models command when the server is unreachable.
149
- *
150
- * @param ctx The context used by Pi
151
- */
152
- export const notFoundCommand = async (
153
- ctx: ExtensionCommandContext,
154
- ): Promise<void> => {
155
- const url = await resolveUrl(ctx.cwd);
156
- ctx.ui.notify(`${PROVIDER_NAME} unreachable at ${url}`, "error");
157
- };
158
-
159
- /**
160
- * Handles the /models command
161
- *
162
- * @param args Arguments passed to the command
163
- * @param ctx The context used by Pi
164
- * @param pi The Pi extension
165
- * @param models List of available models
166
- */
167
- export const modelsCommand = async (
168
- ctx: ExtensionCommandContext,
169
- pi: ExtensionAPI,
170
- models: BaseModel[],
171
- ): Promise<void> => {
172
- const event = await modelSelectionHandler(ctx, models);
173
- if (!event) return;
174
-
175
- // Detect the model
176
- const { action, model } = event;
177
-
178
- // Action: Cancel
179
- if (!action || action === Action.CANCEL) return;
180
-
181
- // Action: Info
182
- if (action === Action.INFO) {
183
- const info = await model.getInfo();
184
- ctx.ui.notify(`${info}`, "info");
185
- return;
186
- }
187
-
188
- // Action: Unload
189
- if (action === Action.UNLOAD) {
190
- await model.unload();
191
- ctx.ui.notify(`Unloaded ${model.name}`, "info");
192
- return;
193
- }
194
-
195
- // Actions: Load/Switch/Retry
196
- const loadActions = [Action.LOAD, Action.SWITCH, Action.RETRY];
197
- if (loadActions.includes(action)) {
198
- ctx.ui.notify(`Loading ${model.name}...`, "info");
199
- inflightModel = model;
200
-
201
- const onSuccess = async () => {
202
- const piModel = ctx.modelRegistry.find(PROVIDER_ID, model.id);
203
- if (!piModel) {
204
- throw new Error(`Cannot find model ${model.name} in pi registry`);
205
- }
206
-
207
- if ((await model.getStatus()) === Status.FAILED) {
208
- throw new Error(`Failed to load model ${model.name}`);
209
- }
210
-
211
- await pi.setModel(piModel);
212
- ctx.ui.notify(`Model ${model.name} ready`, "info");
213
- };
214
-
215
- const onFailure = (err: any) => {
216
- const message = err instanceof Error ? err.message : String(err);
217
-
218
- try {
219
- ctx.ui.notify(message, "error");
220
- } catch {
221
- // ctx went stale between error and notification
222
- }
223
- };
224
-
225
- // Load the model without blocking the UI
226
- model.load().then(onSuccess).catch(onFailure).finally(resetInflightModel);
227
- }
228
- };
package/src/events.ts DELETED
@@ -1,26 +0,0 @@
1
- import { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { PROVIDER_ID } from "./constants";
3
- import { ModelSelectEvent } from "./interfaces/events";
4
- import { listModels } from "./tools/retriever";
5
-
6
- /**
7
- * Reacts to a new model event triggered by Pi
8
- * @param event Model selection event
9
- * @param ctx Pi context
10
- */
11
- export const onModelSelect = async (
12
- event: ModelSelectEvent,
13
- ctx: ExtensionContext,
14
- ) => {
15
- if (event.model.provider !== PROVIDER_ID) return;
16
-
17
- const models = await listModels();
18
- const model = models.find((m) => m.id === event.model.id);
19
- if (!model) return;
20
-
21
- ctx.ui.notify(`Loading ${model.name}...`, "info");
22
- await model
23
- .load()
24
- .then(() => ctx.ui.notify(`Model ${model.name} ready`, "info"))
25
- .catch(() => ctx.ui.notify(`Failed to load model ${model.name}`, "error"));
26
- };
package/src/manager.ts DELETED
@@ -1,96 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ProviderModelConfig,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { modelsCommand, notFoundCommand } from "./commands/models";
7
- import {
8
- DEFAULT_LLAMA_SERVER_URL,
9
- PROVIDER_ID,
10
- PROVIDER_NAME,
11
- } from "./constants";
12
- import { BaseModel } from "./models/baseModel";
13
- import { resolveApiKey, resolveUrl } from "./tools/resolver";
14
- import { isServerReady, listModels } from "./tools/retriever";
15
-
16
- export class CommandManager {
17
- private baseUrl: string = DEFAULT_LLAMA_SERVER_URL;
18
- private serverModels: BaseModel[] = [];
19
-
20
- constructor(private readonly pi: ExtensionAPI) {}
21
-
22
- /**
23
- * Sets up the initial state of the provider
24
- */
25
- async initialize() {
26
- if (await isServerReady()) {
27
- await this.update();
28
- } else {
29
- await this.register([]);
30
- }
31
- }
32
-
33
- /**
34
- * Ensures the models are up-to-date with the server
35
- */
36
- async update() {
37
- this.baseUrl = `${await resolveUrl(process.cwd())}`;
38
-
39
- this.serverModels = await listModels();
40
- const modelConfigs = await Promise.all(
41
- this.serverModels.map((m) => m.toProviderConfig()),
42
- );
43
-
44
- await this.register(modelConfigs);
45
- }
46
-
47
- /**
48
- * Registers the provider in Pi with the given configurations
49
- * Note: Registrations overload previous provider
50
- *
51
- * @param models Provider configurations for the models
52
- */
53
- async register(models: ProviderModelConfig[]) {
54
- this.pi.registerProvider(PROVIDER_ID, {
55
- name: PROVIDER_NAME,
56
- baseUrl: this.baseUrl,
57
- api: "openai-completions",
58
- apiKey: await resolveApiKey(),
59
- models,
60
- });
61
- }
62
-
63
- /**
64
- * Dispatches the /models command
65
- *
66
- * @param args Arguments passed to the command
67
- * @param ctx The context used by Pi
68
- * @param pi The Pi extension
69
- */
70
- async run(args: string, ctx: ExtensionCommandContext) {
71
- if (!(await isServerReady())) {
72
- return await notFoundCommand(ctx);
73
- }
74
-
75
- // Refresh the model list from the server
76
- await this.update();
77
-
78
- // Command: `/models info`
79
- if (args === "info") {
80
- const info = await Promise.all(this.serverModels.map((m) => m.getInfo()));
81
- const message = ctx.ui.theme.fg("accent", info.join("\n"));
82
- ctx.ui.notify(message, "info");
83
- return;
84
- }
85
-
86
- // Command: `/models unload`
87
- if (args === "unload") {
88
- await Promise.all(this.serverModels.map((m) => m.unload()));
89
- ctx.ui.notify(`Unloaded all ${PROVIDER_NAME} models`, "info");
90
- return;
91
- }
92
-
93
- // Command: `/models` (interactive menu)
94
- return await modelsCommand(ctx, this.pi, this.serverModels);
95
- }
96
- }
@@ -1,136 +0,0 @@
1
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
- import { access, constants, readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import {
5
- API_KEY_PLACEHOLDER,
6
- DEFAULT_LLAMA_SERVER_URL,
7
- PROVIDER_ID,
8
- } from "../constants";
9
- import { AuthFile } from "../interfaces/auth";
10
-
11
- // The URL is detected once, to reuse forever
12
- let resolvedUrl: string | undefined;
13
-
14
- /**
15
- * Detects if a particular file is present
16
- * @param filePath The path
17
- * @returns True if exists
18
- */
19
- const fileExists = async (filePath: string): Promise<boolean> => {
20
- try {
21
- await access(filePath, constants.F_OK);
22
- return true;
23
- } catch (error) {
24
- return false;
25
- }
26
- };
27
-
28
- /**
29
- * Reads and parses the contents of a file as JSON
30
- * @param filePath The path to the file
31
- * @returns The parsed content, or null if parsing fails
32
- */
33
- const readContents = async <T>(filePath: string): Promise<T | null> => {
34
- const raw = await readFile(filePath, "utf-8");
35
-
36
- try {
37
- const contents = JSON.parse(raw);
38
- return contents;
39
- } catch (err) {
40
- return null;
41
- }
42
- };
43
-
44
- /**
45
- * Reads a value from a JSON config file by key
46
- * @param filePath Path to the JSON config file
47
- * @param key Key to extract from the parsed JSON
48
- * @returns The value at the given key, or null if file/key missing or invalid
49
- */
50
- const readConfigValue = async <T>(
51
- filePath: string,
52
- key: keyof T,
53
- ): Promise<T[keyof T] | null> => {
54
- const cfg = await readContents<T>(filePath);
55
- return cfg?.[key] ?? null;
56
- };
57
-
58
- /**
59
- * Reads API key from Pi's auth file
60
- * @returns The API key, as defined by the auth.json file
61
- */
62
- export const resolveApiKey = async (): Promise<string> => {
63
- const authPath = join(getAgentDir(), "settings.json");
64
- if (!(await fileExists(authPath))) return API_KEY_PLACEHOLDER;
65
-
66
- const cfg = await readConfigValue<AuthFile>(authPath, PROVIDER_ID);
67
- return cfg?.key ?? API_KEY_PLACEHOLDER;
68
- };
69
-
70
- /**
71
- * Resolves the llama-server url by searching for it in the global settings.json file
72
- * @returns The URL, if found.
73
- */
74
- const resolveGlobalUrl = async (): Promise<string | null> => {
75
- const globalPath = join(getAgentDir(), "settings.json");
76
- if (!(await fileExists(globalPath))) return null;
77
-
78
- return readConfigValue<Record<string, string>>(globalPath, "llamaServerUrl");
79
- };
80
-
81
- /**
82
- * Resolves the llama-server url by searching for it in the project's .pi/llama-server.json file
83
- * @param cwd The current working directory
84
- * @returns The URL, if found.
85
- */
86
- const resolveProjectUrl = async (cwd: string): Promise<string | null> => {
87
- const projectPath = join(cwd, ".pi", "llama-server.json");
88
-
89
- if (!(await fileExists(projectPath))) return null;
90
- return readConfigValue<Record<string, string>>(projectPath, "url");
91
- };
92
-
93
- /**
94
- * Resolves the llama-server url by searching for it in the environment
95
- * @returns The URL, if found.
96
- */
97
- const resolveEnvUrl = async (): Promise<string | null> => {
98
- return process.env.LLAMA_SERVER_URL ?? null;
99
- };
100
-
101
- /**
102
- * Tries all possible ways to retrieve the llama-server URL
103
- * @param cwd The current working directory
104
- * @returns The URL, or a default if not found
105
- */
106
- const resolveUrlWithFallbacks = async (cwd: string): Promise<string> => {
107
- // 1. per-project config
108
- let response = await resolveProjectUrl(cwd);
109
- if (response) return response;
110
-
111
- // 2. env
112
- response = await resolveEnvUrl();
113
- if (response) return response;
114
-
115
- // 3. global settings: ~/.pi/agent/settings.json
116
- response = await resolveGlobalUrl();
117
- if (response) return response;
118
-
119
- // 4. default
120
- return DEFAULT_LLAMA_SERVER_URL;
121
- };
122
-
123
- /**
124
- * Resolves the URL where llama-server is running
125
- * @param cwd The current working directory
126
- * @returns The URL, or a default if not found
127
- */
128
- export const resolveUrl = async (cwd: string): Promise<string> => {
129
- if (resolvedUrl) return resolvedUrl;
130
- const result = await resolveUrlWithFallbacks(cwd);
131
-
132
- // Strip trailing slashes
133
- resolvedUrl = result.replace(/\/+$/, "");
134
-
135
- return resolvedUrl;
136
- };
@@ -1,71 +0,0 @@
1
- import { HealthEndpoint } from "../interfaces/endpoints/health";
2
- import { ModelsEndpoint } from "../interfaces/endpoints/models";
3
- import { BaseModel } from "../models/baseModel";
4
- import { RouterModel } from "../models/routerModel";
5
- import { SingleModel } from "../models/singleModel";
6
- import { resolveApiKey, resolveUrl } from "./resolver";
7
-
8
- /**
9
- * Detects if the server is ready
10
- * @returns True if it's ready to work
11
- */
12
- export const isServerReady = async (): Promise<boolean> => {
13
- try {
14
- const { status } = await rpc<HealthEndpoint>("/health");
15
- return status === "ok";
16
- } catch {
17
- return false;
18
- }
19
- };
20
-
21
- /**
22
- * Makes an HTTP request to the llama-server and returns the parsed JSON response
23
- *
24
- * @param endpoint The endpoint path to fetch (e.g. "/health")
25
- * @param body The optional request body for POST requests
26
- * @returns The parsed JSON response from the server
27
- */
28
- export const rpc = async <T>(
29
- endpoint: string,
30
- body?: Record<string, unknown>,
31
- ): Promise<T> => {
32
- const base = await resolveUrl(process.cwd());
33
- const url = `${base}${endpoint}`;
34
-
35
- const data = {
36
- method: body ? "POST" : "GET",
37
- headers: body ? { "Content-Type": "application/json" } : undefined,
38
- body: body ? JSON.stringify(body) : undefined,
39
- };
40
-
41
- const apiKey = await resolveApiKey();
42
- const res = await fetch(url, {
43
- ...data,
44
- headers: {
45
- ...data.headers,
46
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
47
- },
48
- });
49
-
50
- const response: T = await res.json();
51
- return response;
52
- };
53
-
54
- /**
55
- * Retrieves a list of available models from llama-server
56
- * @param base Base URL to use
57
- * @returns The list of models
58
- */
59
- export const listModels = async (): Promise<BaseModel[]> => {
60
- const { models, data } = await rpc<ModelsEndpoint>("/models");
61
-
62
- if (models) {
63
- return data.map((m) => new SingleModel(m));
64
- }
65
-
66
- const response = data
67
- .map((m) => new RouterModel(m))
68
- .sort((a, b) => (a.id > b.id ? 1 : a.id === b.id ? 0 : -1));
69
-
70
- return response;
71
- };