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.
- package/README.md +163 -27
- package/package.json +4 -3
- package/src/api/client.ts +76 -32
- package/src/constants.ts +28 -3
- package/src/index.ts +4 -12
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +20 -0
- package/src/interfaces/settings.ts +69 -0
- package/src/managers/command.ts +285 -43
- package/src/managers/events.ts +50 -12
- package/src/managers/server.ts +90 -17
- package/src/managers/settings.ts +268 -0
- package/src/models/baseModel.ts +32 -17
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +3 -3
- package/src/server.ts +86 -21
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +23 -10
- package/src/ui/serverListEditor.ts +481 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/settingsStore.ts +60 -0
- package/src/utils/urls.ts +16 -0
- package/tests/commandManager.test.ts +256 -11
- package/tests/events.test.ts +229 -64
- package/tests/mocks.ts +145 -32
- package/tests/server.test.ts +54 -6
- package/tests/serverListEditor.test.ts +637 -0
- package/tests/serverManager.test.ts +282 -39
- package/tests/settings.test.ts +793 -0
- package/tests/settingsStore.test.ts +209 -0
- package/tests/sseManager.test.ts +98 -0
- package/src/interfaces/auth.ts +0 -6
- package/src/resolver.ts +0 -149
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
- package/tests/resolver.test.ts +0 -184
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
providerName: "Llama.cpp (http://127.0.0.1:8081)",
|
|
97
|
+
models: [mockModel],
|
|
76
98
|
});
|
|
77
|
-
const manager =
|
|
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
|
-
|
|
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 =
|
|
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
|
});
|