pi-llama-cpp 0.5.0 → 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,93 +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
- // Command: `/models info`
76
- if (args === "info") {
77
- const info = await Promise.all(this.serverModels.map((m) => m.getInfo()));
78
- const message = ctx.ui.theme.fg("accent", info.join("\n"));
79
- ctx.ui.notify(message, "info");
80
- return;
81
- }
82
-
83
- // Command: `/models unload`
84
- if (args === "unload") {
85
- await Promise.all(this.serverModels.map((m) => m.unload()));
86
- ctx.ui.notify(`Unloaded all ${PROVIDER_NAME} models`, "info");
87
- return;
88
- }
89
-
90
- // Command: `/models` (interactive menu)
91
- return await modelsCommand(ctx, this.pi, this.serverModels);
92
- }
93
- }
@@ -1,141 +0,0 @@
1
- import { access, constants, readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import {
4
- API_KEY_PLACEHOLDER,
5
- DEFAULT_LLAMA_SERVER_URL,
6
- PROVIDER_ID,
7
- } from "../constants";
8
- import { AuthFile } from "../interfaces/auth";
9
-
10
- // The URL is detected once, to reuse forever
11
- let resolvedUrl: string | undefined;
12
-
13
- /**
14
- * Detects if a particular file is present
15
- * @param filePath The path
16
- * @returns True if exists
17
- */
18
- const fileExists = async (filePath: string): Promise<boolean> => {
19
- try {
20
- await access(filePath, constants.F_OK);
21
- return true;
22
- } catch (error) {
23
- return false;
24
- }
25
- };
26
-
27
- /**
28
- * Reads and parses the contents of a file as JSON
29
- * @param filePath The path to the file
30
- * @returns The parsed content, or null if parsing fails
31
- */
32
- const readContents = async <T>(filePath: string): Promise<T | null> => {
33
- const raw = await readFile(filePath, "utf-8");
34
-
35
- try {
36
- const contents = JSON.parse(raw);
37
- return contents;
38
- } catch (err) {
39
- return null;
40
- }
41
- };
42
-
43
- /**
44
- * Reads a value from a JSON config file by key
45
- * @param filePath Path to the JSON config file
46
- * @param key Key to extract from the parsed JSON
47
- * @returns The value at the given key, or null if file/key missing or invalid
48
- */
49
- const readConfigValue = async <T>(
50
- filePath: string,
51
- key: keyof T,
52
- ): Promise<T[keyof T] | null> => {
53
- const cfg = await readContents<T>(filePath);
54
- return cfg?.[key] ?? null;
55
- };
56
-
57
- /**
58
- * Reads API key from Pi's auth file
59
- * @returns The API key, as defined by the auth.json file
60
- */
61
- export const resolveApiKey = async (): Promise<string> => {
62
- const authPath = join(process.env.HOME || ".", ".pi", "agent", "auth.json");
63
- if (!(await fileExists(authPath))) return API_KEY_PLACEHOLDER;
64
-
65
- const cfg = await readConfigValue<AuthFile>(authPath, PROVIDER_ID);
66
- return cfg?.key ?? API_KEY_PLACEHOLDER;
67
- };
68
-
69
- /**
70
- * Resolves the llama-server url by searching for it in the global settings.json file
71
- * @returns The URL, if found.
72
- */
73
- const resolveGlobalUrl = async (): Promise<string | null> => {
74
- const globalPath = join(
75
- process.env.HOME || ".",
76
- ".pi",
77
- "agent",
78
- "settings.json",
79
- );
80
-
81
- if (!(await fileExists(globalPath))) return null;
82
-
83
- return readConfigValue<Record<string, string>>(globalPath, "llamaServerUrl");
84
- };
85
-
86
- /**
87
- * Resolves the llama-server url by searching for it in the project's .pi/llama-server.json file
88
- * @param cwd The current working directory
89
- * @returns The URL, if found.
90
- */
91
- const resolveProjectUrl = async (cwd: string): Promise<string | null> => {
92
- const projectPath = join(cwd, ".pi", "llama-server.json");
93
-
94
- if (!(await fileExists(projectPath))) return null;
95
- return readConfigValue<Record<string, string>>(projectPath, "url");
96
- };
97
-
98
- /**
99
- * Resolves the llama-server url by searching for it in the environment
100
- * @returns The URL, if found.
101
- */
102
- const resolveEnvUrl = async (): Promise<string | null> => {
103
- return process.env.LLAMA_SERVER_URL ?? null;
104
- };
105
-
106
- /**
107
- * Tries all possible ways to retrieve the llama-server URL
108
- * @param cwd The current working directory
109
- * @returns The URL, or a default if not found
110
- */
111
- const resolveUrlWithFallbacks = async (cwd: string): Promise<string> => {
112
- // 1. per-project config
113
- let response = await resolveProjectUrl(cwd);
114
- if (response) return response;
115
-
116
- // 2. env
117
- response = await resolveEnvUrl();
118
- if (response) return response;
119
-
120
- // 3. global settings: ~/.pi/agent/settings.json
121
- response = await resolveGlobalUrl();
122
- if (response) return response;
123
-
124
- // 4. default
125
- return DEFAULT_LLAMA_SERVER_URL;
126
- };
127
-
128
- /**
129
- * Resolves the URL where llama-server is running
130
- * @param cwd The current working directory
131
- * @returns The URL, or a default if not found
132
- */
133
- export const resolveUrl = async (cwd: string): Promise<string> => {
134
- if (resolvedUrl) return resolvedUrl;
135
- const result = await resolveUrlWithFallbacks(cwd);
136
-
137
- // Strip trailing slashes
138
- resolvedUrl = result.replace(/\/+$/, "");
139
-
140
- return resolvedUrl;
141
- };
@@ -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
- });