pi-llama-cpp 0.10.0 → 0.12.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 (41) hide show
  1. package/README.md +187 -10
  2. package/package.json +6 -6
  3. package/src/api/client.ts +76 -32
  4. package/src/constants.ts +15 -0
  5. package/src/index.ts +3 -5
  6. package/src/interfaces/events.ts +14 -3
  7. package/src/interfaces/server.ts +27 -0
  8. package/src/interfaces/settings.ts +76 -2
  9. package/src/managers/command.ts +369 -50
  10. package/src/managers/events.ts +28 -10
  11. package/src/managers/server.ts +90 -16
  12. package/src/managers/settings.ts +181 -44
  13. package/src/models/baseModel.ts +37 -15
  14. package/src/models/routerModel.ts +2 -1
  15. package/src/server.ts +103 -28
  16. package/src/sse/client.ts +28 -16
  17. package/src/sse/manager.ts +26 -13
  18. package/src/ui/dialog.ts +287 -0
  19. package/src/ui/overrideEntryEditor.ts +119 -0
  20. package/src/ui/overrideSettingsList.ts +682 -0
  21. package/src/ui/serverListEditor.ts +32 -0
  22. package/src/ui/serverSettingsList.ts +466 -0
  23. package/src/ui/strings.ts +127 -0
  24. package/src/utils/errors.ts +5 -0
  25. package/src/utils/settingsStore.ts +56 -0
  26. package/src/utils/urls.ts +16 -0
  27. package/tests/commandManager.test.ts +346 -11
  28. package/tests/dialog.test.ts +186 -0
  29. package/tests/events.test.ts +120 -88
  30. package/tests/legacyModel.test.ts +4 -19
  31. package/tests/mocks.ts +149 -32
  32. package/tests/overrides.test.ts +352 -0
  33. package/tests/server.test.ts +42 -40
  34. package/tests/serverManager.test.ts +264 -55
  35. package/tests/settings.test.ts +654 -51
  36. package/tests/settingsStore.test.ts +190 -0
  37. package/tests/singleModel.test.ts +32 -0
  38. package/tests/sseManager.test.ts +88 -11
  39. package/src/interfaces/auth.ts +0 -6
  40. package/src/utils/cache.ts +0 -39
  41. package/src/utils/mutex.ts +0 -24
@@ -2,14 +2,37 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { ServerManager } from "../src/managers/server";
3
3
  import { BaseModel } from "../src/models/baseModel";
4
4
  import { Server } from "../src/server";
5
- import { createMockServer, mockRpc } from "./mocks";
5
+ import {
6
+ createMockModel,
7
+ createMockServer,
8
+ makeSettingsStub,
9
+ mockRpc,
10
+ } from "./mocks";
11
+
12
+ // Injected settings stub — the single instance passed to ServerManager and
13
+ // to every real `new Server(...)` construction below (Server's eager
14
+ // getApiKey() resolves the stub's placeholder, never consumed). Tests
15
+ // re-stub members per case instead of module-mocking the singleton.
16
+ const settingsStub = makeSettingsStub();
6
17
 
7
18
  const mockPi = {
8
19
  registerProvider: vi.fn(),
20
+ unregisterProvider: vi.fn(),
9
21
  registerCommand: vi.fn(),
10
22
  setModel: vi.fn(),
11
23
  };
12
24
 
25
+ /**
26
+ * Creates a ServerManager whose list is driven by the mocked
27
+ * `settings.resolveServers()` — the same path a real scan takes.
28
+ */
29
+ const createManager = async (...servers: Server[]): Promise<ServerManager> => {
30
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue(servers);
31
+ const manager = new ServerManager(settingsStub);
32
+ await manager.update(mockPi as any);
33
+ return manager;
34
+ };
35
+
13
36
  beforeEach(() => {
14
37
  vi.clearAllMocks();
15
38
  mockRpc.mockImplementation((endpoint: string, fallback?: unknown) => {
@@ -22,28 +45,6 @@ beforeEach(() => {
22
45
  });
23
46
  });
24
47
 
25
- describe("Server", () => {
26
- it("should generate provider IDs from URLs", () => {
27
- const server1 = new Server("http://127.0.0.1:8080");
28
- expect(server1.providerId).toBe("llama-server=http://127.0.0.1:8080");
29
- const server2 = new Server("http://10.0.0.5:8080");
30
- expect(server2.providerId).toBe("llama-server=http://10.0.0.5:8080");
31
- const server3 = new Server("http://127.0.0.1");
32
- expect(server3.providerId).toBe("llama-server=http://127.0.0.1");
33
- const server4 = new Server("http://127.0.0.1:80");
34
- expect(server4.providerId).toBe("llama-server=http://127.0.0.1:80");
35
- const server5 = new Server("https://127.0.0.1:443");
36
- expect(server5.providerId).toBe("llama-server=https://127.0.0.1:443");
37
- });
38
-
39
- it("should generate provider names from URLs", () => {
40
- const server1 = new Server("http://127.0.0.1:8080");
41
- expect(server1.providerName).toBe("Llama.cpp (http://127.0.0.1:8080)");
42
- const server2 = new Server("http://10.0.0.5:8080");
43
- expect(server2.providerName).toBe("Llama.cpp (http://10.0.0.5:8080)");
44
- });
45
- });
46
-
47
48
  describe("ServerManager", () => {
48
49
  it("should register providers for all servers", async () => {
49
50
  const mockModel = {
@@ -51,32 +52,17 @@ describe("ServerManager", () => {
51
52
  id: "test-model",
52
53
  toProviderConfig: vi.fn().mockResolvedValue({ id: "test-model" }),
53
54
  } as unknown as BaseModel;
54
- mockRpc.mockImplementation((endpoint: string, fallback?: unknown) => {
55
- if (endpoint === "/v1/models") {
56
- return Promise.resolve({ data: [mockModel], object: "list" });
57
- }
58
- const defaults: Record<string, unknown> = {
59
- "/health": { status: "ok" },
60
- "/props?autoload=false": { role: "router" },
61
- };
62
- return Promise.resolve(defaults[endpoint] ?? fallback ?? {});
63
- });
64
-
65
55
  const server1 = createMockServer({
66
56
  baseUrl: "http://127.0.0.1:8080",
67
57
  apiKey: "key-1",
68
- providerId: "llama-server=http://127.0.0.1:8080",
69
- providerName: "Llama.cpp (http://127.0.0.1:8080)",
58
+ models: [mockModel],
70
59
  });
71
60
  const server2 = createMockServer({
72
61
  baseUrl: "http://127.0.0.1:8081",
73
62
  apiKey: "key-2",
74
- providerId: "llama-server=http://127.0.0.1:8081",
75
- providerName: "Llama.cpp (http://127.0.0.1:8081)",
63
+ models: [mockModel],
76
64
  });
77
- const manager = new ServerManager([server1, server2] as any);
78
-
79
- await manager.initialize(mockPi as any);
65
+ const manager = await createManager(server1, server2);
80
66
 
81
67
  expect(mockPi.registerProvider).toHaveBeenCalledTimes(2);
82
68
  expect(mockPi.registerProvider).toHaveBeenCalledWith(
@@ -101,30 +87,253 @@ describe("ServerManager", () => {
101
87
  );
102
88
  });
103
89
 
104
- it("should return all models from all servers", () => {
105
- const mockModel1 = {
106
- name: "model-1",
107
- id: "model-1",
108
- } as unknown as BaseModel;
109
- const mockModel2 = {
110
- name: "model-2",
111
- id: "model-2",
112
- } as unknown as BaseModel;
90
+ it("should return all models from all servers", async () => {
91
+ const mockModel1 = createMockModel("model-1");
92
+ const mockModel2 = createMockModel("model-2");
113
93
  const server1 = createMockServer({
114
94
  baseUrl: "http://127.0.0.1:8080",
95
+ models: [mockModel1],
115
96
  });
116
97
  const server2 = createMockServer({
117
98
  baseUrl: "http://127.0.0.1:8081",
99
+ models: [mockModel2],
118
100
  });
119
- const manager = new ServerManager([
120
- { ...server1, models: [mockModel1] } as any,
121
- { ...server2, models: [mockModel2] } as any,
122
- ] as any);
101
+ const manager = await createManager(server1, server2);
123
102
 
124
- const allModels = manager.getAllModels();
103
+ const allModels = await manager.getAllModels();
125
104
 
126
105
  expect(allModels).toHaveLength(2);
127
106
  expect(allModels[0]).toBe(mockModel1);
128
107
  expect(allModels[1]).toBe(mockModel2);
129
108
  });
109
+
110
+ describe("live server list (re-derived from settings)", () => {
111
+ /** Mock server; providerId/name derive from the URL as in production. */
112
+ const makeServer = (url: string, modelName?: string): Server =>
113
+ createMockServer({
114
+ baseUrl: url,
115
+ ...(modelName
116
+ ? { models: [createMockModel(modelName, { serverUrl: url })] }
117
+ : {}),
118
+ });
119
+
120
+ it("should register servers added after the first scan", async () => {
121
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
122
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue([server1]);
123
+ const manager = new ServerManager(settingsStub);
124
+ await manager.update(mockPi as any);
125
+ expect(manager.servers).toHaveLength(1);
126
+
127
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
128
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue([
129
+ server1,
130
+ server2,
131
+ ]);
132
+ await manager.update(mockPi as any);
133
+
134
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
135
+ "llama-server=http://127.0.0.1:8081",
136
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:8081" }),
137
+ );
138
+ expect(manager.servers).toHaveLength(2);
139
+ const models = await manager.getAllModels();
140
+ expect(models.map((m) => m.id)).toEqual(["model-1", "model-2"]);
141
+ });
142
+
143
+ it("should unregister and disconnect removed servers on the next scan", async () => {
144
+ const disconnectKept = vi.fn();
145
+ const disconnectRemoved = vi.fn();
146
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
147
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
148
+ // createMockServer stubs initialize out (models are pre-seeded), so the
149
+ // SSE managers never get wired; seed the slots ServerManager reaches
150
+ // disconnect through, mirroring an initialized server.
151
+ (server1 as any).sse = { disconnect: disconnectKept };
152
+ (server2 as any).sse = { disconnect: disconnectRemoved };
153
+
154
+ const manager = await createManager(server1, server2);
155
+ expect(manager.servers).toHaveLength(2);
156
+
157
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue([server1]);
158
+ await manager.update(mockPi as any);
159
+
160
+ expect(mockPi.unregisterProvider).toHaveBeenCalledTimes(1);
161
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
162
+ "llama-server=http://127.0.0.1:8081",
163
+ );
164
+ expect(disconnectRemoved).toHaveBeenCalledTimes(1);
165
+ // Surviving providers keep their SSE manager connected (today's behavior)
166
+ expect(disconnectKept).not.toHaveBeenCalled();
167
+ expect((await manager.getAllModels()).map((m) => m.id)).toEqual([
168
+ "model-1",
169
+ ]);
170
+ });
171
+
172
+ it("should unregister the old provider when a server URL changes", async () => {
173
+ const manager = await createManager(
174
+ makeServer("http://127.0.0.1:8080", "model-1"),
175
+ );
176
+
177
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue([
178
+ makeServer("http://127.0.0.1:9090", "model-1"),
179
+ ]);
180
+ await manager.update(mockPi as any);
181
+
182
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
183
+ "llama-server=http://127.0.0.1:8080",
184
+ );
185
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
186
+ "llama-server=http://127.0.0.1:9090",
187
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:9090" }),
188
+ );
189
+ expect(manager.servers[0]?.providerId).toBe(
190
+ "llama-server=http://127.0.0.1:9090",
191
+ );
192
+ });
193
+
194
+ it("should re-register same-id servers without unregistering (name edit)", async () => {
195
+ const original = createMockServer({
196
+ baseUrl: "http://127.0.0.1:8080",
197
+ customId: "my-custom-id",
198
+ customName: "A",
199
+ });
200
+ const manager = await createManager(original);
201
+
202
+ const renamed = createMockServer({
203
+ baseUrl: "http://127.0.0.1:8080",
204
+ customId: "my-custom-id",
205
+ customName: "B",
206
+ });
207
+ vi.mocked(settingsStub.resolveServers).mockResolvedValue([renamed]);
208
+ await manager.update(mockPi as any);
209
+
210
+ expect(mockPi.unregisterProvider).not.toHaveBeenCalled();
211
+ expect(vi.mocked(mockPi.registerProvider).mock.calls.at(-1)).toEqual([
212
+ "my-custom-id",
213
+ expect.objectContaining({ name: "Llama.cpp (B)" }),
214
+ ]);
215
+ expect(manager.servers[0]?.providerId).toBe("my-custom-id");
216
+ });
217
+
218
+ it("should collapse duplicate URLs into a single server", async () => {
219
+ const first = makeServer("http://127.0.0.1:8080", "model-1");
220
+ const duplicate = makeServer("http://127.0.0.1:8080", "model-1");
221
+ const manager = await createManager(first, duplicate);
222
+
223
+ expect(manager.servers).toHaveLength(1);
224
+ expect(mockPi.registerProvider).toHaveBeenCalledTimes(1);
225
+ expect(await manager.getAllModels()).toHaveLength(1);
226
+ });
227
+
228
+ it("should return undefined from getServer for a removed server's model", async () => {
229
+ const manager = await createManager(
230
+ makeServer("http://127.0.0.1:8080", "model-1"),
231
+ );
232
+
233
+ const live = (await manager.getAllModels())[0];
234
+ expect(manager.getServer(live)).toBe(manager.servers[0]);
235
+
236
+ const orphan = { serverUrl: "http://gone:1" } as unknown as BaseModel;
237
+ expect(manager.getServer(orphan)).toBeUndefined();
238
+ });
239
+ });
240
+
241
+ describe("sortBy", () => {
242
+ const sortBy = () => vi.mocked(settingsStub.resolveSortBy);
243
+
244
+ it("should return models in API order when sortBy is 'api'", async () => {
245
+ sortBy().mockResolvedValue("api");
246
+
247
+ const mockModelA = createMockModel("model-a");
248
+ const mockModelZ = createMockModel("model-z");
249
+ const manager = await createManager(
250
+ createMockServer({
251
+ baseUrl: "http://127.0.0.1:8080",
252
+ models: [mockModelA, mockModelZ],
253
+ }),
254
+ );
255
+
256
+ const allModels = await manager.getAllModels();
257
+
258
+ expect(allModels).toHaveLength(2);
259
+ expect(allModels[0]).toBe(mockModelA);
260
+ expect(allModels[1]).toBe(mockModelZ);
261
+ });
262
+
263
+ it("should sort models by ID ascending when sortBy is 'asc'", async () => {
264
+ sortBy().mockResolvedValue("asc");
265
+
266
+ const mockModelZ = createMockModel("model-z");
267
+ const mockModelA = createMockModel("model-a");
268
+ const manager = await createManager(
269
+ createMockServer({
270
+ baseUrl: "http://127.0.0.1:8080",
271
+ models: [mockModelZ, mockModelA],
272
+ }),
273
+ );
274
+
275
+ const allModels = await manager.getAllModels();
276
+
277
+ expect(allModels).toHaveLength(2);
278
+ expect(allModels[0]).toBe(mockModelA);
279
+ expect(allModels[1]).toBe(mockModelZ);
280
+ });
281
+
282
+ it("should sort models by ID descending when sortBy is 'desc'", async () => {
283
+ sortBy().mockResolvedValue("desc");
284
+
285
+ const mockModelA = createMockModel("model-a");
286
+ const mockModelZ = createMockModel("model-z");
287
+ const manager = await createManager(
288
+ createMockServer({
289
+ baseUrl: "http://127.0.0.1:8080",
290
+ models: [mockModelA, mockModelZ],
291
+ }),
292
+ );
293
+
294
+ const allModels = await manager.getAllModels();
295
+
296
+ expect(allModels).toHaveLength(2);
297
+ expect(allModels[0]).toBe(mockModelZ);
298
+ expect(allModels[1]).toBe(mockModelA);
299
+ });
300
+
301
+ it("should sort models by name ascending when sortBy is 'asc-name'", async () => {
302
+ sortBy().mockResolvedValue("asc-name");
303
+
304
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
305
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
306
+ const manager = await createManager(
307
+ createMockServer({
308
+ baseUrl: "http://127.0.0.1:8080",
309
+ models: [mockModelB, mockModelA],
310
+ }),
311
+ );
312
+
313
+ const allModels = await manager.getAllModels();
314
+
315
+ expect(allModels).toHaveLength(2);
316
+ expect(allModels[0]).toBe(mockModelA);
317
+ expect(allModels[1]).toBe(mockModelB);
318
+ });
319
+
320
+ it("should sort models by name descending when sortBy is 'desc-name'", async () => {
321
+ sortBy().mockResolvedValue("desc-name");
322
+
323
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
324
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
325
+ const manager = await createManager(
326
+ createMockServer({
327
+ baseUrl: "http://127.0.0.1:8080",
328
+ models: [mockModelA, mockModelB],
329
+ }),
330
+ );
331
+
332
+ const allModels = await manager.getAllModels();
333
+
334
+ expect(allModels).toHaveLength(2);
335
+ expect(allModels[0]).toBe(mockModelB);
336
+ expect(allModels[1]).toBe(mockModelA);
337
+ });
338
+ });
130
339
  });