pi-llama-cpp 0.11.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.
- package/README.md +174 -58
- package/package.json +4 -4
- package/src/interfaces/server.ts +7 -0
- package/src/interfaces/settings.ts +65 -0
- package/src/managers/command.ts +108 -31
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +12 -9
- package/src/managers/settings.ts +120 -45
- package/src/models/baseModel.ts +37 -7
- package/src/models/routerModel.ts +2 -1
- package/src/server.ts +38 -4
- package/src/sse/manager.ts +9 -8
- package/src/ui/dialog.ts +287 -0
- package/src/ui/overrideEntryEditor.ts +119 -0
- package/src/ui/overrideSettingsList.ts +682 -0
- package/src/ui/serverListEditor.ts +0 -449
- package/src/ui/serverSettingsList.ts +466 -0
- package/src/ui/strings.ts +127 -0
- package/src/utils/settingsStore.ts +2 -6
- package/tests/commandManager.test.ts +106 -16
- package/tests/dialog.test.ts +186 -0
- package/tests/events.test.ts +12 -9
- package/tests/legacyModel.test.ts +4 -19
- package/tests/mocks.ts +12 -8
- package/tests/overrides.test.ts +352 -0
- package/tests/server.test.ts +12 -12
- package/tests/serverManager.test.ts +24 -58
- package/tests/settings.test.ts +489 -43
- package/tests/settingsStore.test.ts +0 -19
- package/tests/singleModel.test.ts +32 -0
- package/tests/sseManager.test.ts +4 -4
- package/tests/serverListEditor.test.ts +0 -637
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { FALLBACK_CTX } from "../src/constants";
|
|
3
|
+
import type { LlamaServer, ModelOverride } from "../src/interfaces/settings";
|
|
4
|
+
import { SingleModel } from "../src/models/singleModel";
|
|
5
|
+
import { Server } from "../src/server";
|
|
6
|
+
import {
|
|
7
|
+
addOverrideEntry,
|
|
8
|
+
applyCostFieldValue,
|
|
9
|
+
formatOverrideSummary,
|
|
10
|
+
parseCostValue,
|
|
11
|
+
removeOverrideEntry,
|
|
12
|
+
updateOverrideEntry,
|
|
13
|
+
} from "../src/ui/overrideEntryEditor";
|
|
14
|
+
import { createMockServer, mockRpc } from "./mocks";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockRpc.mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createModel = (overrides?: Record<string, ModelOverride>): SingleModel =>
|
|
21
|
+
new SingleModel(
|
|
22
|
+
{
|
|
23
|
+
id: "test",
|
|
24
|
+
tags: [],
|
|
25
|
+
object: "model",
|
|
26
|
+
owned_by: "test",
|
|
27
|
+
created: Date.now(),
|
|
28
|
+
},
|
|
29
|
+
createMockServer({ overrides }),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Standard server responses: /props for capabilities, /v1/models for n_ctx */
|
|
33
|
+
const mockDetection = (nCtx: number | undefined) =>
|
|
34
|
+
mockRpc.mockImplementation((endpoint: string) => {
|
|
35
|
+
if (endpoint.startsWith("/props")) {
|
|
36
|
+
return Promise.resolve({ modalities: { vision: false } });
|
|
37
|
+
}
|
|
38
|
+
return Promise.resolve({
|
|
39
|
+
data: [{ id: "test", meta: nCtx === undefined ? {} : { n_ctx: nCtx } }],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Pattern matching (Server.findOverrideForModel) ───────────────────────
|
|
44
|
+
|
|
45
|
+
describe("Server.findOverrideForModel", () => {
|
|
46
|
+
const makeServer = (overrides: Record<string, ModelOverride>): Server =>
|
|
47
|
+
createMockServer({ overrides });
|
|
48
|
+
|
|
49
|
+
it("should match a model by id prefix", () => {
|
|
50
|
+
const server = makeServer({ lla: { reasoning: false } });
|
|
51
|
+
expect(server.findOverrideForModel("llama-3")).toEqual({
|
|
52
|
+
reasoning: false,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should return undefined when no pattern matches", () => {
|
|
57
|
+
const server = makeServer({ qwen: { reasoning: false } });
|
|
58
|
+
expect(server.findOverrideForModel("llama-3")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should prefer the longest (most specific) matching pattern", () => {
|
|
62
|
+
const server = makeServer({
|
|
63
|
+
llama: { reasoning: false },
|
|
64
|
+
"llama-3-8b": { reasoning: true, contextSize: 128 },
|
|
65
|
+
});
|
|
66
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
67
|
+
reasoning: true,
|
|
68
|
+
contextSize: 128,
|
|
69
|
+
});
|
|
70
|
+
expect(server.findOverrideForModel("llama-3-70b")).toEqual({
|
|
71
|
+
reasoning: false,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should ignore empty patterns", () => {
|
|
76
|
+
const server = makeServer({ "": { reasoning: false } });
|
|
77
|
+
expect(server.findOverrideForModel("llama-3")).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── reasoning override ────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
describe("BaseModel reasoning override", () => {
|
|
84
|
+
it("should default to true when no override matches", () => {
|
|
85
|
+
expect(createModel().reasoning).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should let the override win over the default", () => {
|
|
89
|
+
const model = createModel({ test: { reasoning: false } });
|
|
90
|
+
expect(model.reasoning).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── capabilities override ────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("BaseModel capabilities override", () => {
|
|
97
|
+
it("should fully replace detection without contacting the server", async () => {
|
|
98
|
+
mockRpc.mockImplementation(() => {
|
|
99
|
+
throw new Error("override must not trigger a fetch");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const model = createModel({ test: { capabilities: ["text", "image"] } });
|
|
103
|
+
const capabilities = await model.getCapabilities();
|
|
104
|
+
|
|
105
|
+
expect(capabilities).toEqual(["text", "image"]);
|
|
106
|
+
expect(mockRpc).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should not intercept detection when no override matches", async () => {
|
|
110
|
+
mockDetection(8192);
|
|
111
|
+
|
|
112
|
+
const model = createModel({ other: { capabilities: ["text"] } });
|
|
113
|
+
const capabilities = await model.getCapabilities();
|
|
114
|
+
|
|
115
|
+
expect(capabilities).toEqual(["text"]);
|
|
116
|
+
expect(mockRpc).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── contextSize override ─────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("BaseModel.getContextSize with contextSize override", () => {
|
|
123
|
+
it("should return the overridden context size instead of the detected one", async () => {
|
|
124
|
+
mockDetection(8192);
|
|
125
|
+
|
|
126
|
+
const model = createModel({ test: { contextSize: 32768 } });
|
|
127
|
+
const ctxSize = await model.getContextSize();
|
|
128
|
+
|
|
129
|
+
expect(ctxSize).toBe(32768);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should treat a stored 0 as absent and autodetect", async () => {
|
|
133
|
+
mockDetection(8192);
|
|
134
|
+
|
|
135
|
+
const model = createModel({ test: { contextSize: 0 } });
|
|
136
|
+
const ctxSize = await model.getContextSize();
|
|
137
|
+
|
|
138
|
+
expect(ctxSize).toBe(8192);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should autodetect when no override matches", async () => {
|
|
142
|
+
mockDetection(8192);
|
|
143
|
+
|
|
144
|
+
const model = createModel({ other: { contextSize: 32768 } });
|
|
145
|
+
const ctxSize = await model.getContextSize();
|
|
146
|
+
|
|
147
|
+
expect(ctxSize).toBe(8192);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─── toProviderConfig (cost, maxTokens, compat, precedence) ───────────────
|
|
152
|
+
|
|
153
|
+
describe("toProviderConfig overrides", () => {
|
|
154
|
+
it("should merge a partial cost override with zero defaults", async () => {
|
|
155
|
+
mockDetection(8192);
|
|
156
|
+
|
|
157
|
+
const model = createModel({ test: { cost: { input: 0.2 } } });
|
|
158
|
+
const config = await model.toProviderConfig();
|
|
159
|
+
|
|
160
|
+
expect(config.cost).toEqual({
|
|
161
|
+
input: 0.2,
|
|
162
|
+
output: 0,
|
|
163
|
+
cacheRead: 0,
|
|
164
|
+
cacheWrite: 0,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should default every cost field to zero without an override", async () => {
|
|
169
|
+
mockDetection(8192);
|
|
170
|
+
|
|
171
|
+
const model = createModel();
|
|
172
|
+
const config = await model.toProviderConfig();
|
|
173
|
+
|
|
174
|
+
expect(config.cost).toEqual({
|
|
175
|
+
input: 0,
|
|
176
|
+
output: 0,
|
|
177
|
+
cacheRead: 0,
|
|
178
|
+
cacheWrite: 0,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should pass a compat override through to the provider config", async () => {
|
|
183
|
+
mockDetection(8192);
|
|
184
|
+
|
|
185
|
+
const compat = {
|
|
186
|
+
supportsStore: false,
|
|
187
|
+
maxTokensField: "max_tokens",
|
|
188
|
+
} as const;
|
|
189
|
+
const model = createModel({ test: { compat } });
|
|
190
|
+
const config = await model.toProviderConfig();
|
|
191
|
+
|
|
192
|
+
expect(config.compat).toEqual(compat);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should leave compat undefined without an override", async () => {
|
|
196
|
+
mockDetection(8192);
|
|
197
|
+
|
|
198
|
+
const model = createModel();
|
|
199
|
+
const config = await model.toProviderConfig();
|
|
200
|
+
|
|
201
|
+
expect(config.compat).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should use the overridden context size for contextWindow and as the maxTokens fallback", async () => {
|
|
205
|
+
mockDetection(8192);
|
|
206
|
+
|
|
207
|
+
const model = createModel({ test: { contextSize: 32768 } });
|
|
208
|
+
const config = await model.toProviderConfig();
|
|
209
|
+
|
|
210
|
+
expect(config.contextWindow).toBe(32768);
|
|
211
|
+
expect(config.maxTokens).toBe(32768);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should prefer an explicit maxTokens override over the contextSize override", async () => {
|
|
215
|
+
mockDetection(8192);
|
|
216
|
+
|
|
217
|
+
const model = createModel({
|
|
218
|
+
test: { contextSize: 32768, maxTokens: 4096 },
|
|
219
|
+
});
|
|
220
|
+
const config = await model.toProviderConfig();
|
|
221
|
+
|
|
222
|
+
expect(config.contextWindow).toBe(32768);
|
|
223
|
+
expect(config.maxTokens).toBe(4096);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should use the detected context size when no override matches", async () => {
|
|
227
|
+
mockDetection(8192);
|
|
228
|
+
|
|
229
|
+
const model = createModel();
|
|
230
|
+
const config = await model.toProviderConfig();
|
|
231
|
+
|
|
232
|
+
expect(config.contextWindow).toBe(8192);
|
|
233
|
+
expect(config.maxTokens).toBe(8192);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should fall back to FALLBACK_CTX when the model is missing from detection", async () => {
|
|
237
|
+
mockRpc.mockImplementation((endpoint: string) => {
|
|
238
|
+
if (endpoint.startsWith("/props")) {
|
|
239
|
+
return Promise.resolve({ modalities: { vision: false } });
|
|
240
|
+
}
|
|
241
|
+
// /v1/models: no entry for this model → getContextSize throws → fallback
|
|
242
|
+
return Promise.resolve({ data: [] });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const model = createModel();
|
|
246
|
+
const config = await model.toProviderConfig();
|
|
247
|
+
|
|
248
|
+
expect(config.contextWindow).toBe(FALLBACK_CTX);
|
|
249
|
+
expect(config.maxTokens).toBe(FALLBACK_CTX);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ─── formatOverrideSummary ────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
describe("formatOverrideSummary", () => {
|
|
256
|
+
it("should render every configured field", () => {
|
|
257
|
+
const summary = formatOverrideSummary({
|
|
258
|
+
cost: { input: 0.2, output: 0.6, cacheRead: 0.01 },
|
|
259
|
+
capabilities: ["text", "image"],
|
|
260
|
+
reasoning: false,
|
|
261
|
+
contextSize: 32768,
|
|
262
|
+
maxTokens: 4096,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(summary).toContain("input: $0.2");
|
|
266
|
+
expect(summary).toContain("output: $0.6");
|
|
267
|
+
expect(summary).toContain("cache read: $0.01");
|
|
268
|
+
expect(summary).toContain("capabilities: text,image");
|
|
269
|
+
expect(summary).toContain("reasoning: false");
|
|
270
|
+
expect(summary).toContain("contextSize: 32768");
|
|
271
|
+
expect(summary).toContain("maxTokens: 4096");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should omit zero, undefined and empty fields", () => {
|
|
275
|
+
const summary = formatOverrideSummary({
|
|
276
|
+
cost: { input: 0, output: 0.6 },
|
|
277
|
+
reasoning: undefined,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(summary).not.toContain("input:");
|
|
281
|
+
expect(summary).toContain("output: $0.6");
|
|
282
|
+
expect(summary).not.toContain("capabilities");
|
|
283
|
+
expect(summary).not.toContain("reasoning");
|
|
284
|
+
expect(summary).not.toContain("contextSize");
|
|
285
|
+
expect(summary).not.toContain("maxTokens");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should render an em dash for an empty override", () => {
|
|
289
|
+
expect(formatOverrideSummary({})).toBe("—");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ─── override entry list helpers (overrideEntryEditor) ────────────────────
|
|
294
|
+
|
|
295
|
+
describe("override entry list helpers", () => {
|
|
296
|
+
const servers = [
|
|
297
|
+
{ url: "http://a", overrides: { "a-1": { reasoning: false } } },
|
|
298
|
+
{ url: "http://b" },
|
|
299
|
+
] as LlamaServer[];
|
|
300
|
+
|
|
301
|
+
it("addOverrideEntry should append a pattern → override entry", () => {
|
|
302
|
+
const next = addOverrideEntry(servers, 0, "a-2", { contextSize: 4096 });
|
|
303
|
+
expect(next[0].overrides).toEqual({
|
|
304
|
+
"a-1": { reasoning: false },
|
|
305
|
+
"a-2": { contextSize: 4096 },
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("updateOverrideEntry should replace in place without reordering", () => {
|
|
310
|
+
const next = updateOverrideEntry(servers, 0, 0, "a-renamed", {
|
|
311
|
+
reasoning: true,
|
|
312
|
+
});
|
|
313
|
+
expect(Object.keys(next[0].overrides!)).toEqual(["a-renamed"]);
|
|
314
|
+
expect(next[0].overrides!["a-renamed"]).toEqual({ reasoning: true });
|
|
315
|
+
expect(next[1]).toBe(servers[1]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("removeOverrideEntry should drop only the targeted entry", () => {
|
|
319
|
+
const withTwo = addOverrideEntry(servers, 0, "a-2", {});
|
|
320
|
+
const next = removeOverrideEntry(withTwo, 0, 0);
|
|
321
|
+
expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("parseCostValue should accept blank, non-negative numbers; reject the rest", () => {
|
|
325
|
+
expect(parseCostValue("")).toBe(0);
|
|
326
|
+
expect(parseCostValue(" 0.5 ")).toBe(0.5);
|
|
327
|
+
expect(parseCostValue("-1")).toBeNull();
|
|
328
|
+
expect(parseCostValue("abc")).toBeNull();
|
|
329
|
+
expect(parseCostValue("Infinity")).toBeNull();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("applyCostFieldValue should set a positive value, keeping other fields", () => {
|
|
333
|
+
const next = applyCostFieldValue({ input: 0.2 }, "output", 0.6);
|
|
334
|
+
expect(next).toEqual({ input: 0.2, output: 0.6 });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("applyCostFieldValue should remove the field on zero", () => {
|
|
338
|
+
const next = applyCostFieldValue({ input: 0.2, output: 0.6 }, "input", 0);
|
|
339
|
+
expect(next).toEqual({ output: 0.6 });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("applyCostFieldValue should return undefined when the last field is zeroed", () => {
|
|
343
|
+
expect(applyCostFieldValue({ input: 0.2 }, "input", 0)).toBeUndefined();
|
|
344
|
+
expect(applyCostFieldValue(undefined, "input", 0)).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("applyCostFieldValue should treat an empty override as unset cost", () => {
|
|
348
|
+
expect(applyCostFieldValue(undefined, "input", 0.5)).toEqual({
|
|
349
|
+
input: 0.5,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
package/tests/server.test.ts
CHANGED
|
@@ -154,16 +154,16 @@ describe("Server postRequest", () => {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
describe("Server timeouts", () => {
|
|
158
|
-
it("should resolve timeouts from the injected settings", () => {
|
|
157
|
+
describe("Server timeouts", async () => {
|
|
158
|
+
it("should resolve timeouts from the injected settings", async () => {
|
|
159
159
|
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
160
160
|
|
|
161
|
-
expect(server.
|
|
162
|
-
expect(server.
|
|
161
|
+
expect(await server.getServerTimeout()).toBe(SERVER_TIMEOUT);
|
|
162
|
+
expect(await server.getPollingTimeout()).toBe(POLLING_TIMEOUT);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("should return resolved custom values", () => {
|
|
166
|
-
vi.mocked(settings.resolveTimeouts).
|
|
165
|
+
it("should return resolved custom values", async () => {
|
|
166
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
167
167
|
pollingTimeout: 90000,
|
|
168
168
|
serverTimeout: 2000,
|
|
169
169
|
});
|
|
@@ -174,23 +174,23 @@ describe("Server timeouts", () => {
|
|
|
174
174
|
customName: "My Server",
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
expect(server.
|
|
177
|
+
expect(await server.getPollingTimeout()).toBe(90000);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
it("should read timeouts live from settings", () => {
|
|
180
|
+
it("should read timeouts live from settings", async () => {
|
|
181
181
|
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
182
182
|
|
|
183
|
-
vi.mocked(settings.resolveTimeouts).
|
|
183
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
184
184
|
pollingTimeout: 120000,
|
|
185
185
|
serverTimeout: 3000,
|
|
186
186
|
});
|
|
187
|
-
expect(server.
|
|
187
|
+
expect(await server.getPollingTimeout()).toBe(120000);
|
|
188
188
|
|
|
189
|
-
vi.mocked(settings.resolveTimeouts).
|
|
189
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
190
190
|
pollingTimeout: 90000,
|
|
191
191
|
serverTimeout: 2000,
|
|
192
192
|
});
|
|
193
|
-
expect(server.
|
|
193
|
+
expect(await server.getPollingTimeout()).toBe(90000);
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
196
|
|
|
@@ -27,7 +27,7 @@ const mockPi = {
|
|
|
27
27
|
* `settings.resolveServers()` — the same path a real scan takes.
|
|
28
28
|
*/
|
|
29
29
|
const createManager = async (...servers: Server[]): Promise<ServerManager> => {
|
|
30
|
-
vi.mocked(settingsStub.resolveServers).
|
|
30
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue(servers);
|
|
31
31
|
const manager = new ServerManager(settingsStub);
|
|
32
32
|
await manager.update(mockPi as any);
|
|
33
33
|
return manager;
|
|
@@ -45,40 +45,6 @@ beforeEach(() => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
describe("Server", () => {
|
|
49
|
-
it("should generate provider IDs from URLs", () => {
|
|
50
|
-
const server1 = new Server(settingsStub, {
|
|
51
|
-
baseUrl: "http://127.0.0.1:8080",
|
|
52
|
-
});
|
|
53
|
-
expect(server1.providerId).toBe("llama-server=http://127.0.0.1:8080");
|
|
54
|
-
const server2 = new Server(settingsStub, {
|
|
55
|
-
baseUrl: "http://10.0.0.5:8080",
|
|
56
|
-
});
|
|
57
|
-
expect(server2.providerId).toBe("llama-server=http://10.0.0.5:8080");
|
|
58
|
-
const server3 = new Server(settingsStub, { baseUrl: "http://127.0.0.1" });
|
|
59
|
-
expect(server3.providerId).toBe("llama-server=http://127.0.0.1");
|
|
60
|
-
const server4 = new Server(settingsStub, {
|
|
61
|
-
baseUrl: "http://127.0.0.1:80",
|
|
62
|
-
});
|
|
63
|
-
expect(server4.providerId).toBe("llama-server=http://127.0.0.1:80");
|
|
64
|
-
const server5 = new Server(settingsStub, {
|
|
65
|
-
baseUrl: "https://127.0.0.1:443",
|
|
66
|
-
});
|
|
67
|
-
expect(server5.providerId).toBe("llama-server=https://127.0.0.1:443");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("should generate provider names from URLs", () => {
|
|
71
|
-
const server1 = new Server(settingsStub, {
|
|
72
|
-
baseUrl: "http://127.0.0.1:8080",
|
|
73
|
-
});
|
|
74
|
-
expect(server1.providerName).toBe("Llama.cpp (http://127.0.0.1:8080)");
|
|
75
|
-
const server2 = new Server(settingsStub, {
|
|
76
|
-
baseUrl: "http://10.0.0.5:8080",
|
|
77
|
-
});
|
|
78
|
-
expect(server2.providerName).toBe("Llama.cpp (http://10.0.0.5:8080)");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
48
|
describe("ServerManager", () => {
|
|
83
49
|
it("should register providers for all servers", async () => {
|
|
84
50
|
const mockModel = {
|
|
@@ -134,7 +100,7 @@ describe("ServerManager", () => {
|
|
|
134
100
|
});
|
|
135
101
|
const manager = await createManager(server1, server2);
|
|
136
102
|
|
|
137
|
-
const allModels = manager.getAllModels();
|
|
103
|
+
const allModels = await manager.getAllModels();
|
|
138
104
|
|
|
139
105
|
expect(allModels).toHaveLength(2);
|
|
140
106
|
expect(allModels[0]).toBe(mockModel1);
|
|
@@ -153,13 +119,13 @@ describe("ServerManager", () => {
|
|
|
153
119
|
|
|
154
120
|
it("should register servers added after the first scan", async () => {
|
|
155
121
|
const server1 = makeServer("http://127.0.0.1:8080", "model-1");
|
|
156
|
-
vi.mocked(settingsStub.resolveServers).
|
|
122
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([server1]);
|
|
157
123
|
const manager = new ServerManager(settingsStub);
|
|
158
124
|
await manager.update(mockPi as any);
|
|
159
125
|
expect(manager.servers).toHaveLength(1);
|
|
160
126
|
|
|
161
127
|
const server2 = makeServer("http://127.0.0.1:8081", "model-2");
|
|
162
|
-
vi.mocked(settingsStub.resolveServers).
|
|
128
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([
|
|
163
129
|
server1,
|
|
164
130
|
server2,
|
|
165
131
|
]);
|
|
@@ -170,10 +136,8 @@ describe("ServerManager", () => {
|
|
|
170
136
|
expect.objectContaining({ baseUrl: "http://127.0.0.1:8081" }),
|
|
171
137
|
);
|
|
172
138
|
expect(manager.servers).toHaveLength(2);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"model-2",
|
|
176
|
-
]);
|
|
139
|
+
const models = await manager.getAllModels();
|
|
140
|
+
expect(models.map((m) => m.id)).toEqual(["model-1", "model-2"]);
|
|
177
141
|
});
|
|
178
142
|
|
|
179
143
|
it("should unregister and disconnect removed servers on the next scan", async () => {
|
|
@@ -190,7 +154,7 @@ describe("ServerManager", () => {
|
|
|
190
154
|
const manager = await createManager(server1, server2);
|
|
191
155
|
expect(manager.servers).toHaveLength(2);
|
|
192
156
|
|
|
193
|
-
vi.mocked(settingsStub.resolveServers).
|
|
157
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([server1]);
|
|
194
158
|
await manager.update(mockPi as any);
|
|
195
159
|
|
|
196
160
|
expect(mockPi.unregisterProvider).toHaveBeenCalledTimes(1);
|
|
@@ -200,7 +164,9 @@ describe("ServerManager", () => {
|
|
|
200
164
|
expect(disconnectRemoved).toHaveBeenCalledTimes(1);
|
|
201
165
|
// Surviving providers keep their SSE manager connected (today's behavior)
|
|
202
166
|
expect(disconnectKept).not.toHaveBeenCalled();
|
|
203
|
-
expect(manager.getAllModels().map((m) => m.id)).toEqual([
|
|
167
|
+
expect((await manager.getAllModels()).map((m) => m.id)).toEqual([
|
|
168
|
+
"model-1",
|
|
169
|
+
]);
|
|
204
170
|
});
|
|
205
171
|
|
|
206
172
|
it("should unregister the old provider when a server URL changes", async () => {
|
|
@@ -208,7 +174,7 @@ describe("ServerManager", () => {
|
|
|
208
174
|
makeServer("http://127.0.0.1:8080", "model-1"),
|
|
209
175
|
);
|
|
210
176
|
|
|
211
|
-
vi.mocked(settingsStub.resolveServers).
|
|
177
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([
|
|
212
178
|
makeServer("http://127.0.0.1:9090", "model-1"),
|
|
213
179
|
]);
|
|
214
180
|
await manager.update(mockPi as any);
|
|
@@ -238,7 +204,7 @@ describe("ServerManager", () => {
|
|
|
238
204
|
customId: "my-custom-id",
|
|
239
205
|
customName: "B",
|
|
240
206
|
});
|
|
241
|
-
vi.mocked(settingsStub.resolveServers).
|
|
207
|
+
vi.mocked(settingsStub.resolveServers).mockResolvedValue([renamed]);
|
|
242
208
|
await manager.update(mockPi as any);
|
|
243
209
|
|
|
244
210
|
expect(mockPi.unregisterProvider).not.toHaveBeenCalled();
|
|
@@ -256,7 +222,7 @@ describe("ServerManager", () => {
|
|
|
256
222
|
|
|
257
223
|
expect(manager.servers).toHaveLength(1);
|
|
258
224
|
expect(mockPi.registerProvider).toHaveBeenCalledTimes(1);
|
|
259
|
-
expect(manager.getAllModels()).toHaveLength(1);
|
|
225
|
+
expect(await manager.getAllModels()).toHaveLength(1);
|
|
260
226
|
});
|
|
261
227
|
|
|
262
228
|
it("should return undefined from getServer for a removed server's model", async () => {
|
|
@@ -264,7 +230,7 @@ describe("ServerManager", () => {
|
|
|
264
230
|
makeServer("http://127.0.0.1:8080", "model-1"),
|
|
265
231
|
);
|
|
266
232
|
|
|
267
|
-
const live = manager.getAllModels()[0];
|
|
233
|
+
const live = (await manager.getAllModels())[0];
|
|
268
234
|
expect(manager.getServer(live)).toBe(manager.servers[0]);
|
|
269
235
|
|
|
270
236
|
const orphan = { serverUrl: "http://gone:1" } as unknown as BaseModel;
|
|
@@ -276,7 +242,7 @@ describe("ServerManager", () => {
|
|
|
276
242
|
const sortBy = () => vi.mocked(settingsStub.resolveSortBy);
|
|
277
243
|
|
|
278
244
|
it("should return models in API order when sortBy is 'api'", async () => {
|
|
279
|
-
sortBy().
|
|
245
|
+
sortBy().mockResolvedValue("api");
|
|
280
246
|
|
|
281
247
|
const mockModelA = createMockModel("model-a");
|
|
282
248
|
const mockModelZ = createMockModel("model-z");
|
|
@@ -287,7 +253,7 @@ describe("ServerManager", () => {
|
|
|
287
253
|
}),
|
|
288
254
|
);
|
|
289
255
|
|
|
290
|
-
const allModels = manager.getAllModels();
|
|
256
|
+
const allModels = await manager.getAllModels();
|
|
291
257
|
|
|
292
258
|
expect(allModels).toHaveLength(2);
|
|
293
259
|
expect(allModels[0]).toBe(mockModelA);
|
|
@@ -295,7 +261,7 @@ describe("ServerManager", () => {
|
|
|
295
261
|
});
|
|
296
262
|
|
|
297
263
|
it("should sort models by ID ascending when sortBy is 'asc'", async () => {
|
|
298
|
-
sortBy().
|
|
264
|
+
sortBy().mockResolvedValue("asc");
|
|
299
265
|
|
|
300
266
|
const mockModelZ = createMockModel("model-z");
|
|
301
267
|
const mockModelA = createMockModel("model-a");
|
|
@@ -306,7 +272,7 @@ describe("ServerManager", () => {
|
|
|
306
272
|
}),
|
|
307
273
|
);
|
|
308
274
|
|
|
309
|
-
const allModels = manager.getAllModels();
|
|
275
|
+
const allModels = await manager.getAllModels();
|
|
310
276
|
|
|
311
277
|
expect(allModels).toHaveLength(2);
|
|
312
278
|
expect(allModels[0]).toBe(mockModelA);
|
|
@@ -314,7 +280,7 @@ describe("ServerManager", () => {
|
|
|
314
280
|
});
|
|
315
281
|
|
|
316
282
|
it("should sort models by ID descending when sortBy is 'desc'", async () => {
|
|
317
|
-
sortBy().
|
|
283
|
+
sortBy().mockResolvedValue("desc");
|
|
318
284
|
|
|
319
285
|
const mockModelA = createMockModel("model-a");
|
|
320
286
|
const mockModelZ = createMockModel("model-z");
|
|
@@ -325,7 +291,7 @@ describe("ServerManager", () => {
|
|
|
325
291
|
}),
|
|
326
292
|
);
|
|
327
293
|
|
|
328
|
-
const allModels = manager.getAllModels();
|
|
294
|
+
const allModels = await manager.getAllModels();
|
|
329
295
|
|
|
330
296
|
expect(allModels).toHaveLength(2);
|
|
331
297
|
expect(allModels[0]).toBe(mockModelZ);
|
|
@@ -333,7 +299,7 @@ describe("ServerManager", () => {
|
|
|
333
299
|
});
|
|
334
300
|
|
|
335
301
|
it("should sort models by name ascending when sortBy is 'asc-name'", async () => {
|
|
336
|
-
sortBy().
|
|
302
|
+
sortBy().mockResolvedValue("asc-name");
|
|
337
303
|
|
|
338
304
|
const mockModelB = createMockModel("zebra", { id: "model-b" });
|
|
339
305
|
const mockModelA = createMockModel("alpha", { id: "model-a" });
|
|
@@ -344,7 +310,7 @@ describe("ServerManager", () => {
|
|
|
344
310
|
}),
|
|
345
311
|
);
|
|
346
312
|
|
|
347
|
-
const allModels = manager.getAllModels();
|
|
313
|
+
const allModels = await manager.getAllModels();
|
|
348
314
|
|
|
349
315
|
expect(allModels).toHaveLength(2);
|
|
350
316
|
expect(allModels[0]).toBe(mockModelA);
|
|
@@ -352,7 +318,7 @@ describe("ServerManager", () => {
|
|
|
352
318
|
});
|
|
353
319
|
|
|
354
320
|
it("should sort models by name descending when sortBy is 'desc-name'", async () => {
|
|
355
|
-
sortBy().
|
|
321
|
+
sortBy().mockResolvedValue("desc-name");
|
|
356
322
|
|
|
357
323
|
const mockModelA = createMockModel("alpha", { id: "model-a" });
|
|
358
324
|
const mockModelB = createMockModel("zebra", { id: "model-b" });
|
|
@@ -363,7 +329,7 @@ describe("ServerManager", () => {
|
|
|
363
329
|
}),
|
|
364
330
|
);
|
|
365
331
|
|
|
366
|
-
const allModels = manager.getAllModels();
|
|
332
|
+
const allModels = await manager.getAllModels();
|
|
367
333
|
|
|
368
334
|
expect(allModels).toHaveLength(2);
|
|
369
335
|
expect(allModels[0]).toBe(mockModelB);
|