pi-llama-cpp 0.5.1 → 0.6.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/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
- };
@@ -1,164 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { Action } from "../src/enums/action";
3
- import { Mode } from "../src/enums/mode";
4
- import { Status } from "../src/enums/status";
5
- import { DataProperty } from "../src/interfaces/endpoints/models";
6
-
7
- // Mock the retriever module before importing anything that depends on it
8
- vi.mock("../src/tools/retriever", () => ({
9
- rpc: vi.fn(),
10
- isServerReady: vi.fn(),
11
- listModels: vi.fn(),
12
- }));
13
-
14
- class TestModel {
15
- constructor(
16
- private readonly model: DataProperty,
17
- private readonly _mode: Mode,
18
- private readonly _status: Status,
19
- ) {}
20
-
21
- get mode(): Mode {
22
- return this._mode;
23
- }
24
-
25
- get capabilities(): ["text"] | ["image"] {
26
- return ["text"];
27
- }
28
-
29
- async getStatus(): Promise<Status> {
30
- return this._status;
31
- }
32
-
33
- async getContextSize(): Promise<number> {
34
- return 4096;
35
- }
36
- }
37
-
38
- const createModel = (
39
- mode: Mode,
40
- status: Status,
41
- overrides: Partial<DataProperty> = {},
42
- ) =>
43
- new TestModel(
44
- {
45
- id: "test",
46
- tags: [],
47
- object: "model",
48
- owned_by: "test",
49
- created: Date.now(),
50
- ...overrides,
51
- },
52
- mode,
53
- status,
54
- );
55
-
56
- /**
57
- * Replicates the getActionsForModel logic from handlers.ts for testing
58
- * without needing the full Pi extension context.
59
- */
60
- const getActionsForModel = async (model: TestModel): Promise<Array<Action>> => {
61
- const routerModeActions: Record<Status, Array<Action>> = {
62
- [Status.LOADED]: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
63
- [Status.LOADING]: [Action.INFO, Action.CANCEL],
64
- [Status.FAILED]: [Action.RETRY, Action.CANCEL],
65
- [Status.SLEEPING]: [
66
- Action.SWITCH,
67
- Action.UNLOAD,
68
- Action.INFO,
69
- Action.CANCEL,
70
- ],
71
- [Status.UNLOADED]: [Action.LOAD, Action.CANCEL],
72
- };
73
-
74
- const singleModeActions: Record<Status, Array<Action>> = {
75
- [Status.LOADED]: [Action.INFO, Action.CANCEL],
76
- [Status.LOADING]: [Action.CANCEL],
77
- [Status.FAILED]: [Action.CANCEL],
78
- [Status.SLEEPING]: [Action.INFO, Action.CANCEL],
79
- [Status.UNLOADED]: [Action.CANCEL],
80
- };
81
-
82
- const allActions =
83
- model.mode === Mode.ROUTER ? routerModeActions : singleModeActions;
84
-
85
- const status = await model.getStatus();
86
- return allActions[status];
87
- };
88
-
89
- describe("Action availability", () => {
90
- const actionMatrix: Array<{
91
- mode: Mode;
92
- status: Status;
93
- expected: Action[];
94
- }> = [
95
- // Router mode
96
- {
97
- mode: Mode.ROUTER,
98
- status: Status.LOADED,
99
- expected: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
100
- },
101
- {
102
- mode: Mode.ROUTER,
103
- status: Status.LOADING,
104
- expected: [Action.INFO, Action.CANCEL],
105
- },
106
- {
107
- mode: Mode.ROUTER,
108
- status: Status.FAILED,
109
- expected: [Action.RETRY, Action.CANCEL],
110
- },
111
- {
112
- mode: Mode.ROUTER,
113
- status: Status.SLEEPING,
114
- expected: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
115
- },
116
- {
117
- mode: Mode.ROUTER,
118
- status: Status.UNLOADED,
119
- expected: [Action.LOAD, Action.CANCEL],
120
- },
121
- // Single mode
122
- {
123
- mode: Mode.SINGLE,
124
- status: Status.LOADED,
125
- expected: [Action.INFO, Action.CANCEL],
126
- },
127
- { mode: Mode.SINGLE, status: Status.LOADING, expected: [Action.CANCEL] },
128
- { mode: Mode.SINGLE, status: Status.FAILED, expected: [Action.CANCEL] },
129
- {
130
- mode: Mode.SINGLE,
131
- status: Status.SLEEPING,
132
- expected: [Action.INFO, Action.CANCEL],
133
- },
134
- { mode: Mode.SINGLE, status: Status.UNLOADED, expected: [Action.CANCEL] },
135
- ];
136
-
137
- it.each(actionMatrix)(
138
- "should return correct actions for $mode/$status",
139
- async ({ mode, status, expected }) => {
140
- const model = createModel(mode, status);
141
- const actions = await getActionsForModel(model);
142
- expect(actions).toEqual(expected);
143
- },
144
- );
145
-
146
- it("should always include CANCEL regardless of mode or status", async () => {
147
- for (const mode of [Mode.ROUTER, Mode.SINGLE]) {
148
- for (const status of Object.values(Status)) {
149
- const model = createModel(mode, status);
150
- const actions = await getActionsForModel(model);
151
- expect(actions).toContain(Action.CANCEL);
152
- }
153
- }
154
- });
155
-
156
- it("should not include mode-exclusive actions", async () => {
157
- const singleLoaded = createModel(Mode.SINGLE, Status.LOADED);
158
- expect(await getActionsForModel(singleLoaded)).not.toContain(Action.SWITCH);
159
- expect(await getActionsForModel(singleLoaded)).not.toContain(Action.LOAD);
160
-
161
- const singleFailed = createModel(Mode.SINGLE, Status.FAILED);
162
- expect(await getActionsForModel(singleFailed)).not.toContain(Action.RETRY);
163
- });
164
- });