pi-llama-cpp 0.14.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.
Files changed (47) hide show
  1. package/README.md +12 -10
  2. package/package.json +3 -3
  3. package/src/constants.ts +5 -0
  4. package/src/index.ts +2 -1
  5. package/src/interfaces/settings.ts +6 -0
  6. package/src/managers/command/models.ts +9 -2
  7. package/src/managers/command.ts +6 -1
  8. package/src/managers/server.ts +30 -38
  9. package/src/managers/settings.ts +14 -2
  10. package/src/server.ts +6 -9
  11. package/src/sse/manager.ts +2 -2
  12. package/src/ui/editors/editorOptions.ts +8 -1
  13. package/src/ui/editors/itemBuilder.ts +2 -6
  14. package/src/ui/editors/listEditor.ts +24 -15
  15. package/src/ui/editors/override/entryEditor.ts +97 -15
  16. package/src/ui/editors/override/fields/base.ts +53 -0
  17. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  18. package/src/ui/editors/override/fields/cost.ts +64 -0
  19. package/src/ui/editors/override/fields/index.ts +68 -0
  20. package/src/ui/editors/override/fields/numeric.ts +55 -0
  21. package/src/ui/editors/override/fields/pattern.ts +24 -0
  22. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  23. package/src/ui/editors/override/itemBuilder.ts +3 -6
  24. package/src/ui/editors/server/itemBuilder.ts +1 -5
  25. package/src/ui/editors/server/serverEditor.ts +51 -15
  26. package/src/ui/editors/server/utils.ts +50 -0
  27. package/src/ui/settings/index.ts +11 -0
  28. package/src/ui/strings.ts +17 -0
  29. package/src/utils/health.ts +2 -1
  30. package/src/utils/urlResolver.ts +1 -1
  31. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
  32. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  33. package/tests/mocks.ts +2 -0
  34. package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
  35. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
  36. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
  37. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  38. package/tests/{server.test.ts → server/server.test.ts} +5 -5
  39. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  40. package/tests/{settings.test.ts → settings/settings.test.ts} +174 -133
  41. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  42. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
  43. package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
  44. package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
  45. package/src/ui/editors/override/fields.ts +0 -294
  46. package/src/ui/editors/override/handlers.ts +0 -118
  47. package/src/ui/editors/server/handlers.ts +0 -32
@@ -1,8 +1,12 @@
1
+ import { ENDPOINT_PREFIX } from "../../../constants";
1
2
  import { ServerStatus } from "../../../enums/serverStatus";
2
3
  import type { LlamaServer } from "../../../interfaces/settings";
3
4
  import { checkServerHealth } from "../../../utils/health";
4
5
  import { ServerIds } from "../../../utils/serverIds";
5
6
 
7
+ /** Emoji shown when a server requires an API key (401 on /v1/models). */
8
+ const UNAUTHORIZED_EMOJI = "⛔";
9
+
6
10
  /**
7
11
  * Display concerns for a server row in the top-level list: the dim
8
12
  * `(...)` suffix after the URL and the health-status icon.
@@ -33,6 +37,52 @@ export class ServerDisplay {
33
37
  static async healthEmoji(url: string, timeout: number): Promise<string> {
34
38
  return SERVER_STATUS_ICONS[await checkServerHealth(url, timeout)];
35
39
  }
40
+
41
+ /**
42
+ * Checks if the server requires an API key by probing `/v1/models`.
43
+ *
44
+ * @param url - The server URL to check
45
+ * @param apiKey - The API key to send (may be a placeholder)
46
+ * @param timeout - Maximum time (ms) to wait for the request
47
+ * @returns `true` if the server responded with 401, `false` otherwise
48
+ */
49
+ static async requiresApiKey(
50
+ url: string,
51
+ apiKey: string,
52
+ timeout: number,
53
+ ): Promise<boolean> {
54
+ try {
55
+ const response = await fetch(`${url}${ENDPOINT_PREFIX}/models`, {
56
+ signal: AbortSignal.timeout(timeout),
57
+ headers: { Authorization: `Bearer ${apiKey}` },
58
+ });
59
+ return response.status === 401;
60
+ } catch {
61
+ // Timeout or network error — the server is unreachable, so no auth
62
+ // indicator is needed (the health emoji will cover that).
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Probes the server's `/v1/models` endpoint to check if authentication
69
+ * is required. Returns ⛔ when the server responds with 401, empty string
70
+ * otherwise.
71
+ *
72
+ * @param url - The server URL to check
73
+ * @param apiKey - The API key to send (may be a placeholder)
74
+ * @param timeout - Maximum time (ms) to wait for the request
75
+ * @returns The emoji indicator (⛔ if 401, "" otherwise)
76
+ */
77
+ static async authEmoji(
78
+ url: string,
79
+ apiKey: string,
80
+ timeout: number,
81
+ ): Promise<string> {
82
+ return (await this.requiresApiKey(url, apiKey, timeout))
83
+ ? UNAUTHORIZED_EMOJI
84
+ : "";
85
+ }
36
86
  }
37
87
 
38
88
  /**
@@ -26,6 +26,7 @@ export enum Options {
26
26
  SORT_BY = "sortBy",
27
27
  POLLING_TIMEOUT = "pollingTimeout",
28
28
  SERVER_TIMEOUT = "serverTimeout",
29
+ SHOW_SERVER_URLS = "showServerUrls",
29
30
  }
30
31
 
31
32
  // ─── Constants ──────────────────────────────────────────────────────────────
@@ -110,6 +111,13 @@ export const buildSettingsItems = async (
110
111
  currentValue: formatMs(serverTimeout),
111
112
  values: SERVER_PRESETS.map(formatMs),
112
113
  },
114
+ {
115
+ id: Options.SHOW_SERVER_URLS,
116
+ label: "Show server URLs",
117
+ description: "Show [Server: <url>] in /models (next open)",
118
+ currentValue: (await settings.resolveShowServerUrls()) ? "on" : "off",
119
+ values: ["on", "off"],
120
+ },
113
121
  ];
114
122
  };
115
123
 
@@ -139,6 +147,9 @@ export const applySettingChange = async (
139
147
  case Options.SERVER_TIMEOUT:
140
148
  await settings.setLlamaSetting("serverTimeout", parseMs(newValue));
141
149
  return;
150
+ case Options.SHOW_SERVER_URLS:
151
+ await settings.setLlamaSetting("showServerUrls", newValue === "on");
152
+ return;
142
153
  }
143
154
  };
144
155
 
package/src/ui/strings.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
+
1
3
  import { LLAMA_SERVER_URL } from "../constants";
2
4
 
3
5
  /**
@@ -129,3 +131,18 @@ export const HINTS = {
129
131
  /** Row description for a server in the overrides editor */
130
132
  overrideServerRow: "Enter: edit this server's override entries",
131
133
  } as const;
134
+
135
+ /**
136
+ * Generates a warning message for servers that require an API key.
137
+ * Centralized so both the runtime manager and the editor wizard use
138
+ * identical wording.
139
+ */
140
+ export const authRequiredMessage = (
141
+ baseUrl: string,
142
+ providerId: string,
143
+ ): string =>
144
+ [
145
+ "[pi-llama-cpp]",
146
+ `Server at '${baseUrl}' requires a valid API key.`,
147
+ `Configure the key via '/login ${providerId}' or in '${getAgentDir()}/auth.json'.`,
148
+ ].join("\n");
@@ -1,4 +1,5 @@
1
1
  import { ServerStatus } from "../enums/serverStatus";
2
+ import type { HealthEndpoint } from "../interfaces/endpoints/health";
2
3
 
3
4
  /**
4
5
  * Probes a llama-server's `/health` endpoint and classifies the outcome.
@@ -34,7 +35,7 @@ export const checkServerHealth = async (
34
35
  signal: AbortSignal.timeout(timeout),
35
36
  headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
36
37
  });
37
- const data = (await response.json()) as { status?: string };
38
+ const data = (await response.json()) as HealthEndpoint;
38
39
  return data.status === "ok" ? ServerStatus.READY : ServerStatus.UNREACHABLE;
39
40
  } catch (error) {
40
41
  // `AbortSignal.timeout` rejects `fetch` with a `TimeoutError`
@@ -14,7 +14,7 @@ export interface UrlResolverDeps {
14
14
  }
15
15
 
16
16
  /**
17
- * Multi-source llama-server URL resolution chain (see REFACTOR.md #4).
17
+ * Multi-source llama-server URL resolution chain
18
18
  *
19
19
  * Resolves URLs in priority order:
20
20
  *
@@ -2,17 +2,17 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { initTheme } from "@earendil-works/pi-coding-agent";
3
3
  import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
4
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
- import { Action } from "../src/enums/action";
6
- import { CommandManager } from "../src/managers/command";
7
- import { ServerManager } from "../src/managers/server";
8
- import type { LlamaSettingsManager } from "../src/managers/settings";
9
- import { ServerSettingsList } from "../src/ui/editors/server/serverEditor";
10
- import { ServerDisplay } from "../src/ui/editors/server/utils";
5
+ import { Action } from "../../src/enums/action";
6
+ import { CommandManager } from "../../src/managers/command";
7
+ import { ServerManager } from "../../src/managers/server";
8
+ import type { LlamaSettingsManager } from "../../src/managers/settings";
9
+ import { ServerSettingsList } from "../../src/ui/editors/server/serverEditor";
10
+ import { ServerDisplay } from "../../src/ui/editors/server/utils";
11
11
  import {
12
12
  applySettingChange,
13
13
  buildSettingsItems,
14
14
  formatMs,
15
- } from "../src/ui/settings";
15
+ } from "../../src/ui/settings";
16
16
  import {
17
17
  createMockCtx,
18
18
  createMockModel,
@@ -20,14 +20,15 @@ import {
20
20
  createMockServer,
21
21
  makeSettingsStub,
22
22
  mockRpc,
23
- } from "./mocks";
23
+ } from "../mocks";
24
24
 
25
25
  beforeEach(() => {
26
26
  initTheme();
27
27
  vi.clearAllMocks();
28
28
  mockRpc.mockResolvedValue({ data: [] });
29
- // Mock health checks to return "healthy" immediately
29
+ // Mock health checks and auth probes to return immediately
30
30
  vi.spyOn(ServerDisplay, "healthEmoji").mockResolvedValue("🟢");
31
+ vi.spyOn(ServerDisplay, "authEmoji").mockResolvedValue("");
31
32
  });
32
33
 
33
34
  /**
@@ -163,6 +164,7 @@ describe("CommandManager", () => {
163
164
  "sortBy",
164
165
  "pollingTimeout",
165
166
  "serverTimeout",
167
+ "showServerUrls",
166
168
  ]);
167
169
  // Booleans are displayed as on/off
168
170
  expect(items[0].values).toEqual(["on", "off"]);
@@ -203,6 +205,18 @@ describe("CommandManager", () => {
203
205
  "serverTimeout",
204
206
  500,
205
207
  );
208
+
209
+ await applySettingChange("showServerUrls", "on", settingsStub);
210
+ expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
211
+ "showServerUrls",
212
+ true,
213
+ );
214
+
215
+ await applySettingChange("showServerUrls", "off", settingsStub);
216
+ expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
217
+ "showServerUrls",
218
+ false,
219
+ );
206
220
  });
207
221
  });
208
222
 
@@ -564,5 +578,62 @@ describe("CommandManager", () => {
564
578
  expect(choices).toHaveLength(2);
565
579
  expect(choices[1]).toContain("model-b");
566
580
  });
581
+
582
+ it("should omit server URLs when showServerUrls is false", async () => {
583
+ const models = [createMockModel("model-a")];
584
+ const mockPi = createMockPi();
585
+ const servers = models.map((model) =>
586
+ createMockServer({
587
+ baseUrl: model.serverUrl,
588
+ models: [model],
589
+ }),
590
+ );
591
+ const settingsStub = makeSettingsStub({
592
+ resolveServers: vi.fn(async () => servers),
593
+ resolveShowServerUrls: vi.fn(async () => false),
594
+ });
595
+ const serverManager = new ServerManager(settingsStub);
596
+ const commandManager = new CommandManager(serverManager, settingsStub);
597
+
598
+ const ctx = createMockCtx((prompt, options) => {
599
+ if (prompt.includes("models")) return options[0];
600
+ return Action.INFO;
601
+ });
602
+
603
+ await commandManager.handleCommand("", ctx as any, mockPi as any);
604
+
605
+ const choices = vi.mocked(ctx.ui.select).mock.calls[0][1] as string[];
606
+ expect(choices).toHaveLength(1);
607
+ expect(choices[0]).toBe("model-a ");
608
+ expect(choices[0]).not.toContain("[Server:");
609
+ });
610
+
611
+ it("should include server URLs when showServerUrls is true", async () => {
612
+ const models = [createMockModel("model-a")];
613
+ const mockPi = createMockPi();
614
+ const servers = models.map((model) =>
615
+ createMockServer({
616
+ baseUrl: model.serverUrl,
617
+ models: [model],
618
+ }),
619
+ );
620
+ const settingsStub = makeSettingsStub({
621
+ resolveServers: vi.fn(async () => servers),
622
+ resolveShowServerUrls: vi.fn(async () => true),
623
+ });
624
+ const serverManager = new ServerManager(settingsStub);
625
+ const commandManager = new CommandManager(serverManager, settingsStub);
626
+
627
+ const ctx = createMockCtx((prompt, options) => {
628
+ if (prompt.includes("models")) return options[0];
629
+ return Action.INFO;
630
+ });
631
+
632
+ await commandManager.handleCommand("", ctx as any, mockPi as any);
633
+
634
+ const choices = vi.mocked(ctx.ui.select).mock.calls[0][1] as string[];
635
+ expect(choices).toHaveLength(1);
636
+ expect(choices[0]).toContain("[Server: http://127.0.0.1:8080]");
637
+ });
567
638
  });
568
639
  });
@@ -1,10 +1,10 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { THINKING_BUDGETS } from "../src/constants";
3
- import { Status } from "../src/enums/status";
4
- import { EventManager } from "../src/managers/events";
5
- import { ServerManager } from "../src/managers/server";
6
- import type { Server } from "../src/server";
7
- import { createMockModel, createMockServer, makeSettingsStub } from "./mocks";
2
+ import { THINKING_BUDGETS } from "../../src/constants";
3
+ import { Status } from "../../src/enums/status";
4
+ import { EventManager } from "../../src/managers/events";
5
+ import { ServerManager } from "../../src/managers/server";
6
+ import type { Server } from "../../src/server";
7
+ import { createMockModel, createMockServer, makeSettingsStub } from "../mocks";
8
8
 
9
9
  /**
10
10
  * Injected settings stub (EventManager and — in the live-list test — the
package/tests/mocks.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  POLLING_TIMEOUT,
8
8
  REACT_TO_MODEL_SELECT,
9
9
  SERVER_TIMEOUT,
10
+ SHOW_SERVER_URLS,
10
11
  SORT_BY,
11
12
  THINKING_BUDGETS,
12
13
  } from "../src/constants";
@@ -40,6 +41,7 @@ export const makeSettingsStub = (
40
41
  })),
41
42
  resolveServers: vi.fn(async () => []),
42
43
  resolveSortBy: vi.fn(async () => SORT_BY),
44
+ resolveShowServerUrls: vi.fn(async () => SHOW_SERVER_URLS),
43
45
  resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
44
46
  resolveReactToModelSelect: vi.fn(async () => REACT_TO_MODEL_SELECT),
45
47
  resolveAutoloadOnMessage: vi.fn(async () => AUTOLOAD_ON_MESSAGE),
@@ -1,9 +1,9 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { FALLBACK_CTX } from "../src/constants";
3
- import { Mode } from "../src/enums/mode";
4
- import { DataProperty } from "../src/interfaces/endpoints/models";
5
- import { LegacyModel } from "../src/models/legacyModel";
6
- import { createMockServer, mockRpc } from "./mocks";
2
+ import { FALLBACK_CTX } from "../../src/constants";
3
+ import { Mode } from "../../src/enums/mode";
4
+ import { DataProperty } from "../../src/interfaces/endpoints/models";
5
+ import { LegacyModel } from "../../src/models/legacyModel";
6
+ import { createMockServer, mockRpc } from "../mocks";
7
7
 
8
8
  beforeEach(() => {
9
9
  mockRpc.mockReset();
@@ -1,9 +1,9 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { FALLBACK_CTX } from "../src/constants";
3
- import { Mode } from "../src/enums/mode";
4
- import { DataProperty } from "../src/interfaces/endpoints/models";
5
- import { RouterModel } from "../src/models/routerModel";
6
- import { createMockServer, mockRpc } from "./mocks";
2
+ import { FALLBACK_CTX } from "../../src/constants";
3
+ import { Mode } from "../../src/enums/mode";
4
+ import { DataProperty } from "../../src/interfaces/endpoints/models";
5
+ import { RouterModel } from "../../src/models/routerModel";
6
+ import { createMockServer, mockRpc } from "../mocks";
7
7
 
8
8
  // Helper to create a mock DataProperty
9
9
  const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
@@ -1,9 +1,9 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { Mode } from "../src/enums/mode";
3
- import { Status } from "../src/enums/status";
4
- import { DataProperty } from "../src/interfaces/endpoints/models";
5
- import { SingleModel } from "../src/models/singleModel";
6
- import { createMockServer, mockRpc } from "./mocks";
2
+ import { Mode } from "../../src/enums/mode";
3
+ import { Status } from "../../src/enums/status";
4
+ import { DataProperty } from "../../src/interfaces/endpoints/models";
5
+ import { SingleModel } from "../../src/models/singleModel";
6
+ import { createMockServer, mockRpc } from "../mocks";
7
7
 
8
8
  beforeEach(() => {
9
9
  mockRpc.mockReset();
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { ServerStatus } from "../src/enums/serverStatus";
3
- import { checkServerHealth } from "../src/utils/health";
2
+ import { ServerStatus } from "../../src/enums/serverStatus";
3
+ import { checkServerHealth } from "../../src/utils/health";
4
4
 
5
5
  /**
6
6
  * Stubs `global.fetch` for one test. The probe only reads the parsed body,
@@ -1,9 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../src/constants";
3
- import { ServerStatus } from "../src/enums/serverStatus";
4
- import type { LlamaSettingsManager } from "../src/managers/settings";
5
- import { Server } from "../src/server";
6
- import { createMockServer, makeSettingsStub, mockRpc } from "./mocks";
2
+ import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../../src/constants";
3
+ import { ServerStatus } from "../../src/enums/serverStatus";
4
+ import type { LlamaSettingsManager } from "../../src/managers/settings";
5
+ import { Server } from "../../src/server";
6
+ import { createMockServer, makeSettingsStub, mockRpc } from "../mocks";
7
7
 
8
8
  // Injected into every real Server below; fresh per test so per-case
9
9
  // overrides never leak. The Server constructor resolves the API key eagerly
@@ -1,13 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { ServerManager } from "../src/managers/server";
3
- import { BaseModel } from "../src/models/baseModel";
4
- import { Server } from "../src/server";
2
+ import { ServerManager } from "../../src/managers/server";
3
+ import { BaseModel } from "../../src/models/baseModel";
4
+ import { Server } from "../../src/server";
5
5
  import {
6
6
  createMockModel,
7
7
  createMockServer,
8
8
  makeSettingsStub,
9
9
  mockRpc,
10
- } from "./mocks";
10
+ } from "../mocks";
11
11
 
12
12
  // Injected settings stub — the single instance passed to ServerManager and
13
13
  // to every real `new Server(...)` construction below (Server's eager