pi-llama-cpp 0.9.2 → 0.11.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.
@@ -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).mockReturnValue(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) => {
@@ -24,22 +47,34 @@ beforeEach(() => {
24
47
 
25
48
  describe("Server", () => {
26
49
  it("should generate provider IDs from URLs", () => {
27
- const server1 = new Server("http://127.0.0.1:8080");
50
+ const server1 = new Server(settingsStub, {
51
+ baseUrl: "http://127.0.0.1:8080",
52
+ });
28
53
  expect(server1.providerId).toBe("llama-server=http://127.0.0.1:8080");
29
- const server2 = new Server("http://10.0.0.5:8080");
54
+ const server2 = new Server(settingsStub, {
55
+ baseUrl: "http://10.0.0.5:8080",
56
+ });
30
57
  expect(server2.providerId).toBe("llama-server=http://10.0.0.5:8080");
31
- const server3 = new Server("http://127.0.0.1");
58
+ const server3 = new Server(settingsStub, { baseUrl: "http://127.0.0.1" });
32
59
  expect(server3.providerId).toBe("llama-server=http://127.0.0.1");
33
- const server4 = new Server("http://127.0.0.1:80");
60
+ const server4 = new Server(settingsStub, {
61
+ baseUrl: "http://127.0.0.1:80",
62
+ });
34
63
  expect(server4.providerId).toBe("llama-server=http://127.0.0.1:80");
35
- const server5 = new Server("https://127.0.0.1:443");
64
+ const server5 = new Server(settingsStub, {
65
+ baseUrl: "https://127.0.0.1:443",
66
+ });
36
67
  expect(server5.providerId).toBe("llama-server=https://127.0.0.1:443");
37
68
  });
38
69
 
39
70
  it("should generate provider names from URLs", () => {
40
- const server1 = new Server("http://127.0.0.1:8080");
71
+ const server1 = new Server(settingsStub, {
72
+ baseUrl: "http://127.0.0.1:8080",
73
+ });
41
74
  expect(server1.providerName).toBe("Llama.cpp (http://127.0.0.1:8080)");
42
- const server2 = new Server("http://10.0.0.5:8080");
75
+ const server2 = new Server(settingsStub, {
76
+ baseUrl: "http://10.0.0.5:8080",
77
+ });
43
78
  expect(server2.providerName).toBe("Llama.cpp (http://10.0.0.5:8080)");
44
79
  });
45
80
  });
@@ -51,32 +86,17 @@ describe("ServerManager", () => {
51
86
  id: "test-model",
52
87
  toProviderConfig: vi.fn().mockResolvedValue({ id: "test-model" }),
53
88
  } 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
89
  const server1 = createMockServer({
66
90
  baseUrl: "http://127.0.0.1:8080",
67
91
  apiKey: "key-1",
68
- providerId: "llama-server=http://127.0.0.1:8080",
69
- providerName: "Llama.cpp (http://127.0.0.1:8080)",
92
+ models: [mockModel],
70
93
  });
71
94
  const server2 = createMockServer({
72
95
  baseUrl: "http://127.0.0.1:8081",
73
96
  apiKey: "key-2",
74
- providerId: "llama-server=http://127.0.0.1:8081",
75
- providerName: "Llama.cpp (http://127.0.0.1:8081)",
97
+ models: [mockModel],
76
98
  });
77
- const manager = new ServerManager([server1, server2] as any);
78
-
79
- await manager.initialize(mockPi as any);
99
+ const manager = await createManager(server1, server2);
80
100
 
81
101
  expect(mockPi.registerProvider).toHaveBeenCalledTimes(2);
82
102
  expect(mockPi.registerProvider).toHaveBeenCalledWith(
@@ -101,25 +121,18 @@ describe("ServerManager", () => {
101
121
  );
102
122
  });
103
123
 
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;
124
+ it("should return all models from all servers", async () => {
125
+ const mockModel1 = createMockModel("model-1");
126
+ const mockModel2 = createMockModel("model-2");
113
127
  const server1 = createMockServer({
114
128
  baseUrl: "http://127.0.0.1:8080",
129
+ models: [mockModel1],
115
130
  });
116
131
  const server2 = createMockServer({
117
132
  baseUrl: "http://127.0.0.1:8081",
133
+ models: [mockModel2],
118
134
  });
119
- const manager = new ServerManager([
120
- { ...server1, models: [mockModel1] } as any,
121
- { ...server2, models: [mockModel2] } as any,
122
- ] as any);
135
+ const manager = await createManager(server1, server2);
123
136
 
124
137
  const allModels = manager.getAllModels();
125
138
 
@@ -127,4 +140,234 @@ describe("ServerManager", () => {
127
140
  expect(allModels[0]).toBe(mockModel1);
128
141
  expect(allModels[1]).toBe(mockModel2);
129
142
  });
143
+
144
+ describe("live server list (re-derived from settings)", () => {
145
+ /** Mock server; providerId/name derive from the URL as in production. */
146
+ const makeServer = (url: string, modelName?: string): Server =>
147
+ createMockServer({
148
+ baseUrl: url,
149
+ ...(modelName
150
+ ? { models: [createMockModel(modelName, { serverUrl: url })] }
151
+ : {}),
152
+ });
153
+
154
+ it("should register servers added after the first scan", async () => {
155
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
156
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([server1]);
157
+ const manager = new ServerManager(settingsStub);
158
+ await manager.update(mockPi as any);
159
+ expect(manager.servers).toHaveLength(1);
160
+
161
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
162
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([
163
+ server1,
164
+ server2,
165
+ ]);
166
+ await manager.update(mockPi as any);
167
+
168
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
169
+ "llama-server=http://127.0.0.1:8081",
170
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:8081" }),
171
+ );
172
+ expect(manager.servers).toHaveLength(2);
173
+ expect(manager.getAllModels().map((m) => m.id)).toEqual([
174
+ "model-1",
175
+ "model-2",
176
+ ]);
177
+ });
178
+
179
+ it("should unregister and disconnect removed servers on the next scan", async () => {
180
+ const disconnectKept = vi.fn();
181
+ const disconnectRemoved = vi.fn();
182
+ const server1 = makeServer("http://127.0.0.1:8080", "model-1");
183
+ const server2 = makeServer("http://127.0.0.1:8081", "model-2");
184
+ // createMockServer stubs initialize out (models are pre-seeded), so the
185
+ // SSE managers never get wired; seed the slots ServerManager reaches
186
+ // disconnect through, mirroring an initialized server.
187
+ (server1 as any).sse = { disconnect: disconnectKept };
188
+ (server2 as any).sse = { disconnect: disconnectRemoved };
189
+
190
+ const manager = await createManager(server1, server2);
191
+ expect(manager.servers).toHaveLength(2);
192
+
193
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([server1]);
194
+ await manager.update(mockPi as any);
195
+
196
+ expect(mockPi.unregisterProvider).toHaveBeenCalledTimes(1);
197
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
198
+ "llama-server=http://127.0.0.1:8081",
199
+ );
200
+ expect(disconnectRemoved).toHaveBeenCalledTimes(1);
201
+ // Surviving providers keep their SSE manager connected (today's behavior)
202
+ expect(disconnectKept).not.toHaveBeenCalled();
203
+ expect(manager.getAllModels().map((m) => m.id)).toEqual(["model-1"]);
204
+ });
205
+
206
+ it("should unregister the old provider when a server URL changes", async () => {
207
+ const manager = await createManager(
208
+ makeServer("http://127.0.0.1:8080", "model-1"),
209
+ );
210
+
211
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([
212
+ makeServer("http://127.0.0.1:9090", "model-1"),
213
+ ]);
214
+ await manager.update(mockPi as any);
215
+
216
+ expect(mockPi.unregisterProvider).toHaveBeenCalledWith(
217
+ "llama-server=http://127.0.0.1:8080",
218
+ );
219
+ expect(mockPi.registerProvider).toHaveBeenCalledWith(
220
+ "llama-server=http://127.0.0.1:9090",
221
+ expect.objectContaining({ baseUrl: "http://127.0.0.1:9090" }),
222
+ );
223
+ expect(manager.servers[0]?.providerId).toBe(
224
+ "llama-server=http://127.0.0.1:9090",
225
+ );
226
+ });
227
+
228
+ it("should re-register same-id servers without unregistering (name edit)", async () => {
229
+ const original = createMockServer({
230
+ baseUrl: "http://127.0.0.1:8080",
231
+ customId: "my-custom-id",
232
+ customName: "A",
233
+ });
234
+ const manager = await createManager(original);
235
+
236
+ const renamed = createMockServer({
237
+ baseUrl: "http://127.0.0.1:8080",
238
+ customId: "my-custom-id",
239
+ customName: "B",
240
+ });
241
+ vi.mocked(settingsStub.resolveServers).mockReturnValue([renamed]);
242
+ await manager.update(mockPi as any);
243
+
244
+ expect(mockPi.unregisterProvider).not.toHaveBeenCalled();
245
+ expect(vi.mocked(mockPi.registerProvider).mock.calls.at(-1)).toEqual([
246
+ "my-custom-id",
247
+ expect.objectContaining({ name: "Llama.cpp (B)" }),
248
+ ]);
249
+ expect(manager.servers[0]?.providerId).toBe("my-custom-id");
250
+ });
251
+
252
+ it("should collapse duplicate URLs into a single server", async () => {
253
+ const first = makeServer("http://127.0.0.1:8080", "model-1");
254
+ const duplicate = makeServer("http://127.0.0.1:8080", "model-1");
255
+ const manager = await createManager(first, duplicate);
256
+
257
+ expect(manager.servers).toHaveLength(1);
258
+ expect(mockPi.registerProvider).toHaveBeenCalledTimes(1);
259
+ expect(manager.getAllModels()).toHaveLength(1);
260
+ });
261
+
262
+ it("should return undefined from getServer for a removed server's model", async () => {
263
+ const manager = await createManager(
264
+ makeServer("http://127.0.0.1:8080", "model-1"),
265
+ );
266
+
267
+ const live = manager.getAllModels()[0];
268
+ expect(manager.getServer(live)).toBe(manager.servers[0]);
269
+
270
+ const orphan = { serverUrl: "http://gone:1" } as unknown as BaseModel;
271
+ expect(manager.getServer(orphan)).toBeUndefined();
272
+ });
273
+ });
274
+
275
+ describe("sortBy", () => {
276
+ const sortBy = () => vi.mocked(settingsStub.resolveSortBy);
277
+
278
+ it("should return models in API order when sortBy is 'api'", async () => {
279
+ sortBy().mockReturnValue("api");
280
+
281
+ const mockModelA = createMockModel("model-a");
282
+ const mockModelZ = createMockModel("model-z");
283
+ const manager = await createManager(
284
+ createMockServer({
285
+ baseUrl: "http://127.0.0.1:8080",
286
+ models: [mockModelA, mockModelZ],
287
+ }),
288
+ );
289
+
290
+ const allModels = manager.getAllModels();
291
+
292
+ expect(allModels).toHaveLength(2);
293
+ expect(allModels[0]).toBe(mockModelA);
294
+ expect(allModels[1]).toBe(mockModelZ);
295
+ });
296
+
297
+ it("should sort models by ID ascending when sortBy is 'asc'", async () => {
298
+ sortBy().mockReturnValue("asc");
299
+
300
+ const mockModelZ = createMockModel("model-z");
301
+ const mockModelA = createMockModel("model-a");
302
+ const manager = await createManager(
303
+ createMockServer({
304
+ baseUrl: "http://127.0.0.1:8080",
305
+ models: [mockModelZ, mockModelA],
306
+ }),
307
+ );
308
+
309
+ const allModels = manager.getAllModels();
310
+
311
+ expect(allModels).toHaveLength(2);
312
+ expect(allModels[0]).toBe(mockModelA);
313
+ expect(allModels[1]).toBe(mockModelZ);
314
+ });
315
+
316
+ it("should sort models by ID descending when sortBy is 'desc'", async () => {
317
+ sortBy().mockReturnValue("desc");
318
+
319
+ const mockModelA = createMockModel("model-a");
320
+ const mockModelZ = createMockModel("model-z");
321
+ const manager = await createManager(
322
+ createMockServer({
323
+ baseUrl: "http://127.0.0.1:8080",
324
+ models: [mockModelA, mockModelZ],
325
+ }),
326
+ );
327
+
328
+ const allModels = manager.getAllModels();
329
+
330
+ expect(allModels).toHaveLength(2);
331
+ expect(allModels[0]).toBe(mockModelZ);
332
+ expect(allModels[1]).toBe(mockModelA);
333
+ });
334
+
335
+ it("should sort models by name ascending when sortBy is 'asc-name'", async () => {
336
+ sortBy().mockReturnValue("asc-name");
337
+
338
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
339
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
340
+ const manager = await createManager(
341
+ createMockServer({
342
+ baseUrl: "http://127.0.0.1:8080",
343
+ models: [mockModelB, mockModelA],
344
+ }),
345
+ );
346
+
347
+ const allModels = manager.getAllModels();
348
+
349
+ expect(allModels).toHaveLength(2);
350
+ expect(allModels[0]).toBe(mockModelA);
351
+ expect(allModels[1]).toBe(mockModelB);
352
+ });
353
+
354
+ it("should sort models by name descending when sortBy is 'desc-name'", async () => {
355
+ sortBy().mockReturnValue("desc-name");
356
+
357
+ const mockModelA = createMockModel("alpha", { id: "model-a" });
358
+ const mockModelB = createMockModel("zebra", { id: "model-b" });
359
+ const manager = await createManager(
360
+ createMockServer({
361
+ baseUrl: "http://127.0.0.1:8080",
362
+ models: [mockModelA, mockModelB],
363
+ }),
364
+ );
365
+
366
+ const allModels = manager.getAllModels();
367
+
368
+ expect(allModels).toHaveLength(2);
369
+ expect(allModels[0]).toBe(mockModelB);
370
+ expect(allModels[1]).toBe(mockModelA);
371
+ });
372
+ });
130
373
  });