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.
- package/README.md +187 -10
- package/package.json +6 -6
- package/src/api/client.ts +76 -32
- package/src/constants.ts +15 -0
- package/src/index.ts +3 -5
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +27 -0
- package/src/interfaces/settings.ts +76 -2
- package/src/managers/command.ts +369 -50
- package/src/managers/events.ts +28 -10
- package/src/managers/server.ts +90 -16
- package/src/managers/settings.ts +181 -44
- package/src/models/baseModel.ts +37 -15
- package/src/models/routerModel.ts +2 -1
- package/src/server.ts +103 -28
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +26 -13
- 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 +32 -0
- package/src/ui/serverSettingsList.ts +466 -0
- package/src/ui/strings.ts +127 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/settingsStore.ts +56 -0
- package/src/utils/urls.ts +16 -0
- package/tests/commandManager.test.ts +346 -11
- package/tests/dialog.test.ts +186 -0
- package/tests/events.test.ts +120 -88
- package/tests/legacyModel.test.ts +4 -19
- package/tests/mocks.ts +149 -32
- package/tests/overrides.test.ts +352 -0
- package/tests/server.test.ts +42 -40
- package/tests/serverManager.test.ts +264 -55
- package/tests/settings.test.ts +654 -51
- package/tests/settingsStore.test.ts +190 -0
- package/tests/singleModel.test.ts +32 -0
- package/tests/sseManager.test.ts +88 -11
- package/src/interfaces/auth.ts +0 -6
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
package/tests/settings.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
import {
|
|
3
4
|
API_KEY_PLACEHOLDER,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
PROVIDER_PREFIX,
|
|
7
8
|
SERVER_TIMEOUT,
|
|
8
9
|
} from "../src/constants";
|
|
10
|
+
import type { ModelOverride } from "../src/interfaces/settings";
|
|
9
11
|
import { settings } from "../src/managers/settings";
|
|
10
12
|
import { Server } from "../src/server";
|
|
11
13
|
|
|
@@ -17,6 +19,7 @@ const mockSettingsManager = vi.hoisted(() => ({
|
|
|
17
19
|
getGlobalSettings: vi.fn(),
|
|
18
20
|
getDefaultThinkingLevel: vi.fn(),
|
|
19
21
|
getThinkingBudgets: vi.fn(),
|
|
22
|
+
reload: vi.fn(),
|
|
20
23
|
}));
|
|
21
24
|
|
|
22
25
|
// Mock getAgentDir, readStoredCredential, and SettingsManager before importing resolver
|
|
@@ -31,10 +34,14 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
|
31
34
|
|
|
32
35
|
vi.mock("node:fs/promises", () => ({
|
|
33
36
|
readFile: vi.fn(),
|
|
37
|
+
writeFile: vi.fn(),
|
|
38
|
+
rename: vi.fn(),
|
|
39
|
+
access: vi.fn(),
|
|
34
40
|
}));
|
|
35
41
|
|
|
36
42
|
// Import mocked modules
|
|
37
43
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
44
|
+
import { access } from "node:fs/promises";
|
|
38
45
|
|
|
39
46
|
describe("URL resolution fallback chain", () => {
|
|
40
47
|
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
@@ -62,7 +69,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
62
69
|
// Ensure env var is not set (and not inherited from environment)
|
|
63
70
|
delete process.env.LLAMA_SERVER_URL;
|
|
64
71
|
|
|
65
|
-
const result = settings.resolveUrls();
|
|
72
|
+
const result = await settings.resolveUrls();
|
|
66
73
|
|
|
67
74
|
expect(result).toEqual([LLAMA_SERVER_URL]);
|
|
68
75
|
});
|
|
@@ -73,7 +80,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
73
80
|
});
|
|
74
81
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
75
82
|
|
|
76
|
-
const result = settings.resolveUrls();
|
|
83
|
+
const result = await settings.resolveUrls();
|
|
77
84
|
|
|
78
85
|
expect(result).toEqual(["http://env-url:8080"]);
|
|
79
86
|
});
|
|
@@ -81,7 +88,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
81
88
|
it("should use env variable when no other config exists", async () => {
|
|
82
89
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
83
90
|
|
|
84
|
-
const result = settings.resolveUrls();
|
|
91
|
+
const result = await settings.resolveUrls();
|
|
85
92
|
|
|
86
93
|
expect(result).toEqual(["http://env-url:8080"]);
|
|
87
94
|
});
|
|
@@ -91,7 +98,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
91
98
|
llamaServerUrl: "http://project:9999",
|
|
92
99
|
});
|
|
93
100
|
|
|
94
|
-
const result = settings.resolveUrls();
|
|
101
|
+
const result = await settings.resolveUrls();
|
|
95
102
|
|
|
96
103
|
expect(result).toEqual(["http://project:9999"]);
|
|
97
104
|
});
|
|
@@ -101,7 +108,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
101
108
|
llamaServerUrl: "http://global:8080",
|
|
102
109
|
});
|
|
103
110
|
|
|
104
|
-
const result = settings.resolveUrls();
|
|
111
|
+
const result = await settings.resolveUrls();
|
|
105
112
|
|
|
106
113
|
expect(result).toEqual(["http://global:8080"]);
|
|
107
114
|
});
|
|
@@ -109,7 +116,7 @@ describe("URL resolution fallback chain", () => {
|
|
|
109
116
|
it("should strip trailing slashes from resolved URL", async () => {
|
|
110
117
|
process.env.LLAMA_SERVER_URL = "http://localhost:8080/";
|
|
111
118
|
|
|
112
|
-
const result = settings.resolveUrls();
|
|
119
|
+
const result = await settings.resolveUrls();
|
|
113
120
|
|
|
114
121
|
expect(result).toEqual(["http://localhost:8080"]);
|
|
115
122
|
});
|
|
@@ -117,8 +124,8 @@ describe("URL resolution fallback chain", () => {
|
|
|
117
124
|
it("should cache the resolved URL on subsequent calls", async () => {
|
|
118
125
|
process.env.LLAMA_SERVER_URL = "http://first:8080";
|
|
119
126
|
|
|
120
|
-
const result1 = settings.resolveUrls();
|
|
121
|
-
const result2 = settings.resolveUrls();
|
|
127
|
+
const result1 = await settings.resolveUrls();
|
|
128
|
+
const result2 = await settings.resolveUrls();
|
|
122
129
|
|
|
123
130
|
expect(result1).toEqual(["http://first:8080"]);
|
|
124
131
|
expect(result2).toEqual(["http://first:8080"]);
|
|
@@ -127,10 +134,38 @@ describe("URL resolution fallback chain", () => {
|
|
|
127
134
|
it("should handle multiple URLs separated by semicolons", async () => {
|
|
128
135
|
process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090/";
|
|
129
136
|
|
|
130
|
-
const result = settings.resolveUrls();
|
|
137
|
+
const result = await settings.resolveUrls();
|
|
131
138
|
|
|
132
139
|
expect(result).toEqual(["http://first:8080", "http://second:9090"]);
|
|
133
140
|
});
|
|
141
|
+
|
|
142
|
+
it("should drop env URLs without an http(s) scheme, warn, and fall through", async () => {
|
|
143
|
+
process.env.LLAMA_SERVER_URL = "127.0.0.1:8080";
|
|
144
|
+
|
|
145
|
+
const result = await settings.resolveUrls();
|
|
146
|
+
|
|
147
|
+
expect(result).toEqual([LLAMA_SERVER_URL]);
|
|
148
|
+
expect(settings.takeWarnings()).toEqual([
|
|
149
|
+
"Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
|
|
150
|
+
]);
|
|
151
|
+
expect(settings.takeWarnings()).toEqual([]); // drained
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should drop server entries without an http(s) scheme and warn", async () => {
|
|
155
|
+
mockGetProjectSettings.mockReturnValue({
|
|
156
|
+
llamaSettings: {
|
|
157
|
+
servers: [{ url: "127.0.0.1:8080" }, { url: "http://good:8080/" }],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = await settings.resolveUrls();
|
|
162
|
+
|
|
163
|
+
expect(result).toEqual(["http://good:8080"]);
|
|
164
|
+
expect(settings.takeWarnings()).toEqual([
|
|
165
|
+
"Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
|
|
166
|
+
]);
|
|
167
|
+
expect(settings.takeWarnings()).toEqual([]); // drained
|
|
168
|
+
});
|
|
134
169
|
});
|
|
135
170
|
|
|
136
171
|
describe("llamaSettings.servers resolution", () => {
|
|
@@ -164,7 +199,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
164
199
|
},
|
|
165
200
|
});
|
|
166
201
|
|
|
167
|
-
const result = settings.resolveUrls();
|
|
202
|
+
const result = await settings.resolveUrls();
|
|
168
203
|
|
|
169
204
|
expect(result).toEqual([
|
|
170
205
|
"http://project-server:8080",
|
|
@@ -184,7 +219,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
184
219
|
},
|
|
185
220
|
});
|
|
186
221
|
|
|
187
|
-
const result = settings.resolveUrls();
|
|
222
|
+
const result = await settings.resolveUrls();
|
|
188
223
|
|
|
189
224
|
expect(result).toEqual(["http://project:8080"]);
|
|
190
225
|
});
|
|
@@ -196,7 +231,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
196
231
|
},
|
|
197
232
|
});
|
|
198
233
|
|
|
199
|
-
const result = settings.resolveUrls();
|
|
234
|
+
const result = await settings.resolveUrls();
|
|
200
235
|
|
|
201
236
|
expect(result).toEqual(["http://global:8080"]);
|
|
202
237
|
});
|
|
@@ -209,7 +244,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
209
244
|
},
|
|
210
245
|
});
|
|
211
246
|
|
|
212
|
-
const result = settings.resolveUrls();
|
|
247
|
+
const result = await settings.resolveUrls();
|
|
213
248
|
|
|
214
249
|
expect(result).toEqual(["http://env:8080"]);
|
|
215
250
|
});
|
|
@@ -221,7 +256,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
221
256
|
},
|
|
222
257
|
});
|
|
223
258
|
|
|
224
|
-
const result = settings.resolveUrls();
|
|
259
|
+
const result = await settings.resolveUrls();
|
|
225
260
|
|
|
226
261
|
expect(result).toEqual(["http://server:9090"]);
|
|
227
262
|
});
|
|
@@ -234,7 +269,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
234
269
|
},
|
|
235
270
|
});
|
|
236
271
|
|
|
237
|
-
const result = settings.resolveUrls();
|
|
272
|
+
const result = await settings.resolveUrls();
|
|
238
273
|
|
|
239
274
|
expect(result).toEqual(["http://server:9090"]);
|
|
240
275
|
});
|
|
@@ -247,7 +282,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
247
282
|
},
|
|
248
283
|
});
|
|
249
284
|
|
|
250
|
-
const result = settings.resolveUrls();
|
|
285
|
+
const result = await settings.resolveUrls();
|
|
251
286
|
|
|
252
287
|
expect(result).toEqual(["http://legacy:8080"]);
|
|
253
288
|
});
|
|
@@ -259,7 +294,7 @@ describe("llamaSettings.servers resolution", () => {
|
|
|
259
294
|
},
|
|
260
295
|
});
|
|
261
296
|
|
|
262
|
-
const result = settings.resolveUrls();
|
|
297
|
+
const result = await settings.resolveUrls();
|
|
263
298
|
|
|
264
299
|
expect(result).toEqual(["http://localhost:8080"]);
|
|
265
300
|
});
|
|
@@ -315,13 +350,16 @@ describe("API key resolution", () => {
|
|
|
315
350
|
|
|
316
351
|
describe("Server with custom id", () => {
|
|
317
352
|
it("should use custom id as providerId when provided", () => {
|
|
318
|
-
const server = new Server(
|
|
353
|
+
const server = new Server(settings, {
|
|
354
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
355
|
+
customId: "my-custom-id",
|
|
356
|
+
});
|
|
319
357
|
|
|
320
358
|
expect(server.providerId).toEqual("my-custom-id");
|
|
321
359
|
});
|
|
322
360
|
|
|
323
361
|
it("should fall back to URL-based providerId when no custom id", () => {
|
|
324
|
-
const server = new Server("http://127.0.0.1:8080");
|
|
362
|
+
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
325
363
|
|
|
326
364
|
expect(server.providerId).toEqual(
|
|
327
365
|
`${PROVIDER_PREFIX}=http://127.0.0.1:8080`,
|
|
@@ -329,14 +367,19 @@ describe("Server with custom id", () => {
|
|
|
329
367
|
});
|
|
330
368
|
|
|
331
369
|
it("should try custom id first in getApiKey(), then fall back to URL-based", () => {
|
|
370
|
+
const server = new Server(settings, {
|
|
371
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
372
|
+
customId: "my-custom-id",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Server construction resolves the key eagerly (ApiClient built there);
|
|
376
|
+
// clear so the assertions below observe only the explicit getApiKey() call
|
|
332
377
|
vi.clearAllMocks();
|
|
333
378
|
// Mock: custom id returns placeholder (no key found)
|
|
334
379
|
mockReadStoredCredential
|
|
335
380
|
.mockReturnValueOnce(API_KEY_PLACEHOLDER)
|
|
336
381
|
.mockReturnValueOnce({ key: "fallback-key" });
|
|
337
382
|
|
|
338
|
-
const server = new Server("http://127.0.0.1:8080", "my-custom-id");
|
|
339
|
-
|
|
340
383
|
const result = server.getApiKey();
|
|
341
384
|
|
|
342
385
|
expect(result).toEqual("fallback-key");
|
|
@@ -350,7 +393,10 @@ describe("Server with custom id", () => {
|
|
|
350
393
|
it("should return custom id key directly when found", () => {
|
|
351
394
|
mockReadStoredCredential.mockReturnValue({ key: "custom-key" });
|
|
352
395
|
|
|
353
|
-
const server = new Server(
|
|
396
|
+
const server = new Server(settings, {
|
|
397
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
398
|
+
customId: "my-custom-id",
|
|
399
|
+
});
|
|
354
400
|
|
|
355
401
|
const result = server.getApiKey();
|
|
356
402
|
|
|
@@ -361,27 +407,26 @@ describe("Server with custom id", () => {
|
|
|
361
407
|
|
|
362
408
|
describe("Server with custom name", () => {
|
|
363
409
|
it("should use custom name as suffix in providerName", () => {
|
|
364
|
-
const server = new Server(
|
|
365
|
-
"http://127.0.0.1:8080",
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
410
|
+
const server = new Server(settings, {
|
|
411
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
412
|
+
customName: "Remote Server",
|
|
413
|
+
});
|
|
369
414
|
|
|
370
415
|
expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
|
|
371
416
|
});
|
|
372
417
|
|
|
373
418
|
it("should fall back to URL-based name when no custom name", () => {
|
|
374
|
-
const server = new Server("http://127.0.0.1:8080");
|
|
419
|
+
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
375
420
|
|
|
376
421
|
expect(server.providerName).toEqual(`Llama.cpp (http://127.0.0.1:8080)`);
|
|
377
422
|
});
|
|
378
423
|
|
|
379
424
|
it("should use custom name even with custom id", () => {
|
|
380
|
-
const server = new Server(
|
|
381
|
-
"http://127.0.0.1:8080",
|
|
382
|
-
"my-custom-id",
|
|
383
|
-
"Remote Server",
|
|
384
|
-
);
|
|
425
|
+
const server = new Server(settings, {
|
|
426
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
427
|
+
customId: "my-custom-id",
|
|
428
|
+
customName: "Remote Server",
|
|
429
|
+
});
|
|
385
430
|
|
|
386
431
|
expect(server.providerId).toEqual("my-custom-id");
|
|
387
432
|
expect(server.providerName).toEqual(`Llama.cpp (Remote Server)`);
|
|
@@ -396,7 +441,7 @@ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
|
|
|
396
441
|
it("should return true when reactToModelSelect is not set", async () => {
|
|
397
442
|
const { settings } = await import("../src/managers/settings");
|
|
398
443
|
|
|
399
|
-
const result = settings.resolveReactToModelSelect();
|
|
444
|
+
const result = await settings.resolveReactToModelSelect();
|
|
400
445
|
|
|
401
446
|
expect(result).toBe(true);
|
|
402
447
|
});
|
|
@@ -404,11 +449,19 @@ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
|
|
|
404
449
|
it("should return false when autoloadOnMessage is not set", async () => {
|
|
405
450
|
const { settings } = await import("../src/managers/settings");
|
|
406
451
|
|
|
407
|
-
const result = settings.resolveAutoloadOnMessage();
|
|
452
|
+
const result = await settings.resolveAutoloadOnMessage();
|
|
408
453
|
|
|
409
454
|
expect(result).toBe(false);
|
|
410
455
|
});
|
|
411
456
|
|
|
457
|
+
it("should return 'asc' when sortBy is not set", async () => {
|
|
458
|
+
const { settings } = await import("../src/managers/settings");
|
|
459
|
+
|
|
460
|
+
const result = await settings.resolveSortBy();
|
|
461
|
+
|
|
462
|
+
expect(result).toBe("asc");
|
|
463
|
+
});
|
|
464
|
+
|
|
412
465
|
it("should return user values when set", async () => {
|
|
413
466
|
mockSettingsManager.getProjectSettings.mockReturnValue({
|
|
414
467
|
llamaSettings: {
|
|
@@ -419,8 +472,8 @@ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
|
|
|
419
472
|
|
|
420
473
|
const { settings } = await import("../src/managers/settings");
|
|
421
474
|
|
|
422
|
-
expect(settings.resolveReactToModelSelect()).toBe(false);
|
|
423
|
-
expect(settings.resolveAutoloadOnMessage()).toBe(true);
|
|
475
|
+
expect(await settings.resolveReactToModelSelect()).toBe(false);
|
|
476
|
+
expect(await settings.resolveAutoloadOnMessage()).toBe(true);
|
|
424
477
|
});
|
|
425
478
|
});
|
|
426
479
|
|
|
@@ -445,7 +498,7 @@ describe("resolveServers", () => {
|
|
|
445
498
|
mockGetGlobalSettings.mockReturnValue({});
|
|
446
499
|
});
|
|
447
500
|
|
|
448
|
-
it("should use llamaSettings.servers when configured", () => {
|
|
501
|
+
it("should use llamaSettings.servers when configured", async () => {
|
|
449
502
|
mockGetProjectSettings.mockReturnValue({
|
|
450
503
|
llamaSettings: {
|
|
451
504
|
servers: [
|
|
@@ -454,7 +507,7 @@ describe("resolveServers", () => {
|
|
|
454
507
|
},
|
|
455
508
|
});
|
|
456
509
|
|
|
457
|
-
const result = settings.resolveServers();
|
|
510
|
+
const result = await settings.resolveServers();
|
|
458
511
|
|
|
459
512
|
expect(result).toHaveLength(1);
|
|
460
513
|
expect(result[0].baseUrl).toBe("http://custom:8080");
|
|
@@ -464,20 +517,20 @@ describe("resolveServers", () => {
|
|
|
464
517
|
it("should fall back to resolveUrls when servers is empty", async () => {
|
|
465
518
|
process.env.LLAMA_SERVER_URL = "http://env-server:9090";
|
|
466
519
|
|
|
467
|
-
const result = settings.resolveServers();
|
|
520
|
+
const result = await settings.resolveServers();
|
|
468
521
|
|
|
469
522
|
expect(result).toHaveLength(1);
|
|
470
523
|
expect(result[0].baseUrl).toBe("http://env-server:9090");
|
|
471
524
|
});
|
|
472
525
|
|
|
473
|
-
it("should fall back to default URL when no config exists", () => {
|
|
474
|
-
const result = settings.resolveServers();
|
|
526
|
+
it("should fall back to default URL when no config exists", async () => {
|
|
527
|
+
const result = await settings.resolveServers();
|
|
475
528
|
|
|
476
529
|
expect(result).toHaveLength(1);
|
|
477
530
|
expect(result[0].baseUrl).toBe(LLAMA_SERVER_URL);
|
|
478
531
|
});
|
|
479
532
|
|
|
480
|
-
it("should apply id/name from llamaSettings.servers as overrides", () => {
|
|
533
|
+
it("should apply id/name from llamaSettings.servers as overrides", async () => {
|
|
481
534
|
mockGetProjectSettings.mockReturnValue({
|
|
482
535
|
llamaSettings: {
|
|
483
536
|
servers: [
|
|
@@ -486,7 +539,7 @@ describe("resolveServers", () => {
|
|
|
486
539
|
},
|
|
487
540
|
});
|
|
488
541
|
|
|
489
|
-
const result = settings.resolveServers();
|
|
542
|
+
const result = await settings.resolveServers();
|
|
490
543
|
|
|
491
544
|
expect(result).toHaveLength(1);
|
|
492
545
|
expect(result[0].baseUrl).toBe("http://127.0.0.1:8080");
|
|
@@ -494,7 +547,7 @@ describe("resolveServers", () => {
|
|
|
494
547
|
expect(result[0].providerName).toBe(`Llama.cpp (Custom)`);
|
|
495
548
|
});
|
|
496
549
|
|
|
497
|
-
it("should handle multiple URLs with partial id/name overrides", () => {
|
|
550
|
+
it("should handle multiple URLs with partial id/name overrides", async () => {
|
|
498
551
|
mockGetProjectSettings.mockReturnValue({
|
|
499
552
|
llamaSettings: {
|
|
500
553
|
servers: [{ url: "http://first:8080", id: "first-server" }],
|
|
@@ -502,7 +555,7 @@ describe("resolveServers", () => {
|
|
|
502
555
|
});
|
|
503
556
|
process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090";
|
|
504
557
|
|
|
505
|
-
const result = settings.resolveServers();
|
|
558
|
+
const result = await settings.resolveServers();
|
|
506
559
|
|
|
507
560
|
expect(result).toHaveLength(2);
|
|
508
561
|
expect(result[0].baseUrl).toBe("http://first:8080");
|
|
@@ -519,7 +572,7 @@ describe("resolveServers", () => {
|
|
|
519
572
|
},
|
|
520
573
|
});
|
|
521
574
|
|
|
522
|
-
const result = settings.resolveServers();
|
|
575
|
+
const result = await settings.resolveServers();
|
|
523
576
|
|
|
524
577
|
// env variable takes precedence via resolveUrls
|
|
525
578
|
expect(result).toHaveLength(1);
|
|
@@ -535,7 +588,7 @@ describe("resolveTimeouts", () => {
|
|
|
535
588
|
it("should return default timeouts when not configured", async () => {
|
|
536
589
|
const { settings } = await import("../src/managers/settings");
|
|
537
590
|
|
|
538
|
-
const result = settings.resolveTimeouts();
|
|
591
|
+
const result = await settings.resolveTimeouts();
|
|
539
592
|
|
|
540
593
|
expect(result).toEqual({
|
|
541
594
|
pollingTimeout: POLLING_TIMEOUT,
|
|
@@ -552,7 +605,7 @@ describe("resolveTimeouts", () => {
|
|
|
552
605
|
|
|
553
606
|
const { settings } = await import("../src/managers/settings");
|
|
554
607
|
|
|
555
|
-
const result = settings.resolveTimeouts();
|
|
608
|
+
const result = await settings.resolveTimeouts();
|
|
556
609
|
|
|
557
610
|
expect(result.pollingTimeout).toBe(120000);
|
|
558
611
|
expect(result.serverTimeout).toBe(SERVER_TIMEOUT);
|
|
@@ -567,7 +620,7 @@ describe("resolveTimeouts", () => {
|
|
|
567
620
|
|
|
568
621
|
const { settings } = await import("../src/managers/settings");
|
|
569
622
|
|
|
570
|
-
const result = settings.resolveTimeouts();
|
|
623
|
+
const result = await settings.resolveTimeouts();
|
|
571
624
|
|
|
572
625
|
expect(result.pollingTimeout).toBe(POLLING_TIMEOUT);
|
|
573
626
|
expect(result.serverTimeout).toBe(3000);
|
|
@@ -583,7 +636,7 @@ describe("resolveTimeouts", () => {
|
|
|
583
636
|
|
|
584
637
|
const { settings } = await import("../src/managers/settings");
|
|
585
638
|
|
|
586
|
-
const result = settings.resolveTimeouts();
|
|
639
|
+
const result = await settings.resolveTimeouts();
|
|
587
640
|
|
|
588
641
|
expect(result).toEqual({
|
|
589
642
|
pollingTimeout: 90000,
|
|
@@ -634,3 +687,553 @@ describe("Thinking config resolution", () => {
|
|
|
634
687
|
);
|
|
635
688
|
});
|
|
636
689
|
});
|
|
690
|
+
|
|
691
|
+
describe("setLlamaSetting", () => {
|
|
692
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
693
|
+
const mockGetProjectSettings = vi.mocked(
|
|
694
|
+
mockSettingsManager.getProjectSettings,
|
|
695
|
+
);
|
|
696
|
+
const mockGetGlobalSettings = vi.mocked(
|
|
697
|
+
mockSettingsManager.getGlobalSettings,
|
|
698
|
+
);
|
|
699
|
+
const mockReload = vi.mocked(mockSettingsManager.reload);
|
|
700
|
+
const mockReadFile = vi.mocked(readFile);
|
|
701
|
+
const mockWriteFile = vi.mocked(writeFile);
|
|
702
|
+
const mockRename = vi.mocked(rename);
|
|
703
|
+
const mockAccess = vi.mocked(access);
|
|
704
|
+
|
|
705
|
+
const GLOBAL_SETTINGS_PATH = "/fake/agent/dir/settings.json";
|
|
706
|
+
const PROJECT_SETTINGS_PATH = "/fake/project/.pi/settings.json";
|
|
707
|
+
const FAKE_CWD = "/fake/project";
|
|
708
|
+
|
|
709
|
+
afterEach(() => {
|
|
710
|
+
vi.resetModules();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
beforeEach(() => {
|
|
714
|
+
vi.clearAllMocks();
|
|
715
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
716
|
+
vi.spyOn(process, "cwd").mockReturnValue(FAKE_CWD);
|
|
717
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
718
|
+
mockGetGlobalSettings.mockReturnValue({});
|
|
719
|
+
mockReload.mockResolvedValue(undefined);
|
|
720
|
+
mockReadFile.mockResolvedValue("{}");
|
|
721
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
722
|
+
mockRename.mockResolvedValue(undefined);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("should write to project settings when .pi/settings.json exists (auto scope)", async () => {
|
|
726
|
+
mockAccess.mockResolvedValue(undefined);
|
|
727
|
+
mockReadFile.mockResolvedValue("{}");
|
|
728
|
+
|
|
729
|
+
const { settings } = await import("../src/managers/settings");
|
|
730
|
+
await settings.setLlamaSetting("sortBy", "desc");
|
|
731
|
+
|
|
732
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
733
|
+
`${PROJECT_SETTINGS_PATH}.tmp`,
|
|
734
|
+
expect.any(String),
|
|
735
|
+
"utf-8",
|
|
736
|
+
);
|
|
737
|
+
expect(mockRename).toHaveBeenCalledWith(
|
|
738
|
+
`${PROJECT_SETTINGS_PATH}.tmp`,
|
|
739
|
+
PROJECT_SETTINGS_PATH,
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("should write to global settings when .pi/settings.json does not exist (auto scope)", async () => {
|
|
744
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
745
|
+
mockReadFile.mockResolvedValue("{}");
|
|
746
|
+
|
|
747
|
+
const { settings } = await import("../src/managers/settings");
|
|
748
|
+
await settings.setLlamaSetting("sortBy", "desc");
|
|
749
|
+
|
|
750
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
751
|
+
`${GLOBAL_SETTINGS_PATH}.tmp`,
|
|
752
|
+
expect.any(String),
|
|
753
|
+
"utf-8",
|
|
754
|
+
);
|
|
755
|
+
expect(mockRename).toHaveBeenCalledWith(
|
|
756
|
+
`${GLOBAL_SETTINGS_PATH}.tmp`,
|
|
757
|
+
GLOBAL_SETTINGS_PATH,
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("should always write to global when scope is explicitly 'global'", async () => {
|
|
762
|
+
mockAccess.mockResolvedValue(undefined); // project exists but we override
|
|
763
|
+
mockReadFile.mockResolvedValue("{}");
|
|
764
|
+
|
|
765
|
+
const { settings } = await import("../src/managers/settings");
|
|
766
|
+
await settings.setLlamaSetting("sortBy", "desc", "global");
|
|
767
|
+
|
|
768
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
769
|
+
`${GLOBAL_SETTINGS_PATH}.tmp`,
|
|
770
|
+
expect.any(String),
|
|
771
|
+
"utf-8",
|
|
772
|
+
);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("should always write to project when scope is explicitly 'project'", async () => {
|
|
776
|
+
mockAccess.mockRejectedValue(new Error("ENOENT")); // project doesn't exist but we override
|
|
777
|
+
mockReadFile.mockResolvedValue("{}");
|
|
778
|
+
|
|
779
|
+
const { settings } = await import("../src/managers/settings");
|
|
780
|
+
await settings.setLlamaSetting("sortBy", "desc", "project");
|
|
781
|
+
|
|
782
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
783
|
+
`${PROJECT_SETTINGS_PATH}.tmp`,
|
|
784
|
+
expect.any(String),
|
|
785
|
+
"utf-8",
|
|
786
|
+
);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("should write the merged llamaSettings key atomically and reload", async () => {
|
|
790
|
+
mockAccess.mockRejectedValue(new Error("ENOENT")); // no project settings
|
|
791
|
+
mockReadFile.mockResolvedValue(
|
|
792
|
+
JSON.stringify(
|
|
793
|
+
{ unrelated: true, llamaSettings: { reactToModelSelect: true } },
|
|
794
|
+
null,
|
|
795
|
+
2,
|
|
796
|
+
),
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const { settings } = await import("../src/managers/settings");
|
|
800
|
+
await settings.setLlamaSetting("sortBy", "desc");
|
|
801
|
+
|
|
802
|
+
expect(mockWriteFile).toHaveBeenCalledTimes(1);
|
|
803
|
+
const [tmpPath, written, encoding] = mockWriteFile.mock.calls[0];
|
|
804
|
+
expect(tmpPath).toBe(`${GLOBAL_SETTINGS_PATH}.tmp`);
|
|
805
|
+
expect(encoding).toBe("utf-8");
|
|
806
|
+
const parsed = JSON.parse(written as string);
|
|
807
|
+
expect(parsed).toEqual({
|
|
808
|
+
unrelated: true,
|
|
809
|
+
llamaSettings: { reactToModelSelect: true, sortBy: "desc" },
|
|
810
|
+
});
|
|
811
|
+
expect(mockRename).toHaveBeenCalledWith(
|
|
812
|
+
`${GLOBAL_SETTINGS_PATH}.tmp`,
|
|
813
|
+
GLOBAL_SETTINGS_PATH,
|
|
814
|
+
);
|
|
815
|
+
expect(mockReload).toHaveBeenCalledTimes(1);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
afterEach(() => {
|
|
819
|
+
vi.resetModules();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("should reflect the new value in resolvers immediately after the write", async () => {
|
|
823
|
+
mockSettingsManager.reload.mockImplementation(async () => {
|
|
824
|
+
mockGetGlobalSettings.mockReturnValue({
|
|
825
|
+
llamaSettings: { sortBy: "desc" },
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const { settings } = await import("../src/managers/settings");
|
|
830
|
+
await settings.setLlamaSetting("sortBy", "desc");
|
|
831
|
+
|
|
832
|
+
expect(await settings.resolveSortBy()).toBe("desc");
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("should reject and skip reload when the write fails", async () => {
|
|
836
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
837
|
+
mockWriteFile.mockRejectedValue(new Error("ENOSPC: simulated"));
|
|
838
|
+
|
|
839
|
+
const { settings } = await import("../src/managers/settings");
|
|
840
|
+
await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
|
|
841
|
+
"ENOSPC",
|
|
842
|
+
);
|
|
843
|
+
expect(mockReload).not.toHaveBeenCalled();
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("should reject and leave the file untouched when the JSON is invalid", async () => {
|
|
847
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
848
|
+
mockReadFile.mockResolvedValue("{ broken");
|
|
849
|
+
|
|
850
|
+
const { settings } = await import("../src/managers/settings");
|
|
851
|
+
await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
|
|
852
|
+
/Cannot parse/,
|
|
853
|
+
);
|
|
854
|
+
expect(mockWriteFile).not.toHaveBeenCalled();
|
|
855
|
+
expect(mockReload).not.toHaveBeenCalled();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("should persist booleans and numbers with type fidelity", async () => {
|
|
859
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
860
|
+
const { settings } = await import("../src/managers/settings");
|
|
861
|
+
await settings.setLlamaSetting("reactToModelSelect", false);
|
|
862
|
+
|
|
863
|
+
const [, firstWrite] = mockWriteFile.mock.calls[0];
|
|
864
|
+
expect(JSON.parse(firstWrite as string)).toEqual({
|
|
865
|
+
llamaSettings: { reactToModelSelect: false },
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
await settings.setLlamaSetting("pollingTimeout", 120000);
|
|
869
|
+
|
|
870
|
+
const [, secondWrite] = mockWriteFile.mock.calls[1];
|
|
871
|
+
expect(JSON.parse(secondWrite as string)).toEqual({
|
|
872
|
+
llamaSettings: { pollingTimeout: 120000 },
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("should still construct the manager without arguments", async () => {
|
|
877
|
+
const { LlamaSettingsManager } = await import("../src/managers/settings");
|
|
878
|
+
|
|
879
|
+
expect(() => new LlamaSettingsManager()).not.toThrow();
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
describe("resolveServerOverrides", () => {
|
|
884
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
885
|
+
const mockGetProjectSettings = vi.mocked(
|
|
886
|
+
mockSettingsManager.getProjectSettings,
|
|
887
|
+
);
|
|
888
|
+
const mockGetGlobalSettings = vi.mocked(
|
|
889
|
+
mockSettingsManager.getGlobalSettings,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
afterEach(() => {
|
|
893
|
+
vi.resetModules();
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
beforeEach(() => {
|
|
897
|
+
vi.clearAllMocks();
|
|
898
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
899
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
900
|
+
mockGetGlobalSettings.mockReturnValue({});
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("should return overrides for a server that has them configured", async () => {
|
|
904
|
+
mockGetProjectSettings.mockReturnValue({
|
|
905
|
+
llamaSettings: {
|
|
906
|
+
servers: [
|
|
907
|
+
{
|
|
908
|
+
url: "http://127.0.0.1:8080",
|
|
909
|
+
overrides: {
|
|
910
|
+
"llama-3-8b": { cost: { input: 0.2, output: 0.6 } },
|
|
911
|
+
"llama-3-70b": {
|
|
912
|
+
cost: {
|
|
913
|
+
input: 0.1,
|
|
914
|
+
output: 0.3,
|
|
915
|
+
cacheRead: 0.01,
|
|
916
|
+
cacheWrite: 0.02,
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
const result = await settings.resolveServerOverrides(
|
|
926
|
+
"http://127.0.0.1:8080",
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
expect(result).toEqual({
|
|
930
|
+
"llama-3-8b": { cost: { input: 0.2, output: 0.6 } },
|
|
931
|
+
"llama-3-70b": {
|
|
932
|
+
cost: {
|
|
933
|
+
input: 0.1,
|
|
934
|
+
output: 0.3,
|
|
935
|
+
cacheRead: 0.01,
|
|
936
|
+
cacheWrite: 0.02,
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it("should return empty object for a server without overrides", async () => {
|
|
943
|
+
mockGetProjectSettings.mockReturnValue({
|
|
944
|
+
llamaSettings: {
|
|
945
|
+
servers: [{ url: "http://127.0.0.1:8080" }],
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
const result = await settings.resolveServerOverrides(
|
|
950
|
+
"http://127.0.0.1:8080",
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
expect(result).toEqual({});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("should return empty object when server URL is not in config", async () => {
|
|
957
|
+
mockGetProjectSettings.mockReturnValue({
|
|
958
|
+
llamaSettings: {
|
|
959
|
+
servers: [{ url: "http://127.0.0.1:9090" }],
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const result = await settings.resolveServerOverrides(
|
|
964
|
+
"http://127.0.0.1:8080",
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
expect(result).toEqual({});
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("should use global settings when no project config exists", async () => {
|
|
971
|
+
mockGetGlobalSettings.mockReturnValue({
|
|
972
|
+
llamaSettings: {
|
|
973
|
+
servers: [
|
|
974
|
+
{
|
|
975
|
+
url: "http://global:8080",
|
|
976
|
+
overrides: { "model-a": { cost: { input: 0.5 } } },
|
|
977
|
+
},
|
|
978
|
+
],
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const result = await settings.resolveServerOverrides("http://global:8080");
|
|
983
|
+
|
|
984
|
+
expect(result).toEqual({ "model-a": { cost: { input: 0.5 } } });
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it("should prioritize project overrides over global overrides", async () => {
|
|
988
|
+
mockGetProjectSettings.mockReturnValue({
|
|
989
|
+
llamaSettings: {
|
|
990
|
+
servers: [
|
|
991
|
+
{
|
|
992
|
+
url: "http://shared:8080",
|
|
993
|
+
overrides: { "model-b": { cost: { input: 0.1, output: 0.2 } } },
|
|
994
|
+
},
|
|
995
|
+
],
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
mockGetGlobalSettings.mockReturnValue({
|
|
999
|
+
llamaSettings: {
|
|
1000
|
+
servers: [
|
|
1001
|
+
{
|
|
1002
|
+
url: "http://shared:8080",
|
|
1003
|
+
overrides: { "model-b": { cost: { input: 0.5, output: 0.5 } } },
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const result = await settings.resolveServerOverrides("http://shared:8080");
|
|
1010
|
+
|
|
1011
|
+
expect(result).toEqual({
|
|
1012
|
+
"model-b": { cost: { input: 0.1, output: 0.2 } },
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it("should return empty object when servers list is empty", async () => {
|
|
1017
|
+
mockGetProjectSettings.mockReturnValue({
|
|
1018
|
+
llamaSettings: { servers: [] },
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const result = await settings.resolveServerOverrides(
|
|
1022
|
+
"http://127.0.0.1:8080",
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
expect(result).toEqual({});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it("should return empty object when llamaSettings is missing", async () => {
|
|
1029
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
1030
|
+
|
|
1031
|
+
const result = await settings.resolveServerOverrides(
|
|
1032
|
+
"http://127.0.0.1:8080",
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
expect(result).toEqual({});
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it("should support partial override objects", async () => {
|
|
1039
|
+
mockGetProjectSettings.mockReturnValue({
|
|
1040
|
+
llamaSettings: {
|
|
1041
|
+
servers: [
|
|
1042
|
+
{
|
|
1043
|
+
url: "http://127.0.0.1:8080",
|
|
1044
|
+
overrides: { "partial-model": { cost: { input: 0.1 } } },
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const result = await settings.resolveServerOverrides(
|
|
1051
|
+
"http://127.0.0.1:8080",
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
expect(result).toEqual({ "partial-model": { cost: { input: 0.1 } } });
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
describe("Server with overrides", () => {
|
|
1059
|
+
it("should store and expose resolved overrides", () => {
|
|
1060
|
+
const server = new Server(settings, {
|
|
1061
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
1062
|
+
overrides: {
|
|
1063
|
+
"model-a": { cost: { input: 0.2, output: 0.6 } },
|
|
1064
|
+
"model-b": { cost: { input: 0.1, output: 0.3, cacheRead: 0.01 } },
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
expect(server.getOverrides()).toEqual({
|
|
1069
|
+
"model-a": { cost: { input: 0.2, output: 0.6 } },
|
|
1070
|
+
"model-b": { cost: { input: 0.1, output: 0.3, cacheRead: 0.01 } },
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it("should return empty object when no overrides are provided", () => {
|
|
1075
|
+
const server = new Server(settings, {
|
|
1076
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
expect(server.getOverrides()).toEqual({});
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
describe("resolveServers passes overrides", () => {
|
|
1084
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
1085
|
+
const mockGetProjectSettings = vi.mocked(
|
|
1086
|
+
mockSettingsManager.getProjectSettings,
|
|
1087
|
+
);
|
|
1088
|
+
const mockGetGlobalSettings = vi.mocked(
|
|
1089
|
+
mockSettingsManager.getGlobalSettings,
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
afterEach(() => {
|
|
1093
|
+
vi.resetModules();
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
beforeEach(() => {
|
|
1097
|
+
vi.clearAllMocks();
|
|
1098
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
1099
|
+
mockGetProjectSettings.mockReturnValue({});
|
|
1100
|
+
mockGetGlobalSettings.mockReturnValue({});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it("should pass resolved overrides to Server instances", async () => {
|
|
1104
|
+
mockGetProjectSettings.mockReturnValue({
|
|
1105
|
+
llamaSettings: {
|
|
1106
|
+
servers: [
|
|
1107
|
+
{
|
|
1108
|
+
url: "http://overrides-server:8080",
|
|
1109
|
+
overrides: { "model-x": { cost: { input: 0.5, output: 1.0 } } },
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
url: "http://no-overrides-server:9090",
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const result = await settings.resolveServers();
|
|
1119
|
+
|
|
1120
|
+
expect(result).toHaveLength(2);
|
|
1121
|
+
expect(result[0].getOverrides()).toEqual({
|
|
1122
|
+
"model-x": { cost: { input: 0.5, output: 1.0 } },
|
|
1123
|
+
});
|
|
1124
|
+
expect(result[1].getOverrides()).toEqual({});
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
describe("Server.findOverrideForModel", () => {
|
|
1129
|
+
function createServer(overrides: Record<string, ModelOverride>): Server {
|
|
1130
|
+
return new Server(settings as any, {
|
|
1131
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
1132
|
+
overrides,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
it("should return undefined when overrides is empty", () => {
|
|
1137
|
+
const server = createServer({});
|
|
1138
|
+
expect(server.findOverrideForModel("llama-3-8b")).toBeUndefined();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it("should return undefined when no key matches", () => {
|
|
1142
|
+
const server = createServer({
|
|
1143
|
+
mistral: { cost: { input: 0.1 } },
|
|
1144
|
+
"gpt-4": { cost: { input: 0.3 } },
|
|
1145
|
+
});
|
|
1146
|
+
expect(server.findOverrideForModel("llama-3-8b")).toBeUndefined();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it("should match exact ID", () => {
|
|
1150
|
+
const server = createServer({
|
|
1151
|
+
"llama-3-8b": { cost: { input: 0.2, output: 0.6 } },
|
|
1152
|
+
});
|
|
1153
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
1154
|
+
cost: { input: 0.2, output: 0.6 },
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("should match prefix", () => {
|
|
1159
|
+
const server = createServer({
|
|
1160
|
+
llama: { cost: { input: 0.01, output: 0.02 } },
|
|
1161
|
+
});
|
|
1162
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
1163
|
+
cost: { input: 0.01, output: 0.02 },
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it("should prefer longest match (most specific)", () => {
|
|
1168
|
+
const server = createServer({
|
|
1169
|
+
llama: { cost: { input: 0.01, output: 0.02 } },
|
|
1170
|
+
"llama-3": { cost: { input: 0.05, output: 0.1 } },
|
|
1171
|
+
"llama-3-8b": { cost: { input: 0.2, output: 0.6 } },
|
|
1172
|
+
});
|
|
1173
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
1174
|
+
cost: { input: 0.2, output: 0.6 },
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it("should match the second-longest when exact match is absent", () => {
|
|
1179
|
+
const server = createServer({
|
|
1180
|
+
llama: { cost: { input: 0.01, output: 0.02 } },
|
|
1181
|
+
"llama-3": { cost: { input: 0.05, output: 0.1 } },
|
|
1182
|
+
"llama-3-8b": { cost: { input: 0.2, output: 0.6 } },
|
|
1183
|
+
});
|
|
1184
|
+
expect(server.findOverrideForModel("llama-3-70b")).toEqual({
|
|
1185
|
+
cost: { input: 0.05, output: 0.1 },
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it("should skip empty keys", () => {
|
|
1190
|
+
const server = createServer({
|
|
1191
|
+
"": { cost: { input: 0.001 } },
|
|
1192
|
+
llama: { cost: { input: 0.01 } },
|
|
1193
|
+
});
|
|
1194
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
1195
|
+
cost: { input: 0.01 },
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it("should not match when model ID is shorter than key", () => {
|
|
1200
|
+
const server = createServer({
|
|
1201
|
+
"llama-3-8b": { cost: { input: 0.2 } },
|
|
1202
|
+
});
|
|
1203
|
+
expect(server.findOverrideForModel("llama")).toBeUndefined();
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("should handle single matching key", () => {
|
|
1207
|
+
const server = createServer({
|
|
1208
|
+
qwen: { cost: { input: 0.1, output: 0.3 } },
|
|
1209
|
+
});
|
|
1210
|
+
expect(server.findOverrideForModel("qwen-3-8b")).toEqual({
|
|
1211
|
+
cost: { input: 0.1, output: 0.3 },
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it("should handle overlapping but non-prefix matches", () => {
|
|
1216
|
+
const server = createServer({
|
|
1217
|
+
model: { cost: { input: 0.1 } },
|
|
1218
|
+
"model-a": { cost: { input: 0.2 } },
|
|
1219
|
+
});
|
|
1220
|
+
// "model" matches "model-a" and "model-b"
|
|
1221
|
+
// "model-a" matches only "model-a"
|
|
1222
|
+
expect(server.findOverrideForModel("model-a")).toEqual({
|
|
1223
|
+
cost: { input: 0.2 },
|
|
1224
|
+
});
|
|
1225
|
+
expect(server.findOverrideForModel("model-b")).toEqual({
|
|
1226
|
+
cost: { input: 0.1 },
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it("should return overrides with capabilities and reasoning", () => {
|
|
1231
|
+
const server = createServer({
|
|
1232
|
+
qwen: { capabilities: ["text", "image"], reasoning: false },
|
|
1233
|
+
});
|
|
1234
|
+
expect(server.findOverrideForModel("qwen-3-8b")).toEqual({
|
|
1235
|
+
capabilities: ["text", "image"],
|
|
1236
|
+
reasoning: false,
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
});
|