pi-llama-cpp 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +12 -10
  2. package/package.json +3 -3
  3. package/src/constants.ts +5 -0
  4. package/src/index.ts +2 -1
  5. package/src/interfaces/settings.ts +6 -0
  6. package/src/managers/command/models.ts +9 -2
  7. package/src/managers/command.ts +6 -1
  8. package/src/managers/server.ts +30 -38
  9. package/src/managers/settings.ts +14 -2
  10. package/src/server.ts +6 -9
  11. package/src/sse/manager.ts +2 -2
  12. package/src/ui/editors/editorOptions.ts +8 -1
  13. package/src/ui/editors/itemBuilder.ts +2 -6
  14. package/src/ui/editors/listEditor.ts +24 -15
  15. package/src/ui/editors/override/entryEditor.ts +97 -15
  16. package/src/ui/editors/override/fields/base.ts +53 -0
  17. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  18. package/src/ui/editors/override/fields/cost.ts +64 -0
  19. package/src/ui/editors/override/fields/index.ts +68 -0
  20. package/src/ui/editors/override/fields/numeric.ts +55 -0
  21. package/src/ui/editors/override/fields/pattern.ts +24 -0
  22. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  23. package/src/ui/editors/override/itemBuilder.ts +3 -6
  24. package/src/ui/editors/server/itemBuilder.ts +1 -5
  25. package/src/ui/editors/server/serverEditor.ts +51 -15
  26. package/src/ui/editors/server/utils.ts +50 -0
  27. package/src/ui/settings/index.ts +11 -0
  28. package/src/ui/strings.ts +17 -0
  29. package/src/utils/health.ts +2 -1
  30. package/src/utils/urlResolver.ts +1 -1
  31. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
  32. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  33. package/tests/mocks.ts +2 -0
  34. package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
  35. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
  36. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
  37. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  38. package/tests/{server.test.ts → server/server.test.ts} +5 -5
  39. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  40. package/tests/{settings.test.ts → settings/settings.test.ts} +174 -133
  41. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  42. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
  43. package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
  44. package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
  45. package/src/ui/editors/override/fields.ts +0 -294
  46. package/src/ui/editors/override/handlers.ts +0 -118
  47. package/src/ui/editors/server/handlers.ts +0 -32
@@ -6,12 +6,17 @@ import {
6
6
  POLLING_TIMEOUT,
7
7
  PROVIDER_PREFIX,
8
8
  SERVER_TIMEOUT,
9
- } from "../src/constants";
10
- import type { ModelOverride } from "../src/interfaces/settings";
11
- import { settings } from "../src/managers/settings";
12
- import { Server } from "../src/server";
9
+ } from "../../src/constants";
10
+ import type { ModelOverride } from "../../src/interfaces/settings";
11
+ import {
12
+ createSettingsManager,
13
+ type LlamaSettingsManager,
14
+ } from "../../src/managers/settings";
15
+ import { Server } from "../../src/server";
16
+
17
+ let manager: LlamaSettingsManager;
13
18
 
14
- // Hoisted mock instances — survives vi.resetModules()
19
+ // Hoisted mock instances
15
20
  const mockReadStoredCredential = vi.hoisted(() => vi.fn());
16
21
 
17
22
  const mockSettingsManager = vi.hoisted(() => ({
@@ -54,7 +59,6 @@ describe("URL resolution fallback chain", () => {
54
59
 
55
60
  afterEach(() => {
56
61
  delete process.env.LLAMA_SERVER_URL;
57
- vi.resetModules();
58
62
  });
59
63
 
60
64
  beforeEach(() => {
@@ -63,13 +67,14 @@ describe("URL resolution fallback chain", () => {
63
67
  // Default: no settings found
64
68
  mockGetProjectSettings.mockReturnValue({});
65
69
  mockGetGlobalSettings.mockReturnValue({});
70
+ manager = createSettingsManager();
66
71
  });
67
72
 
68
73
  it("should return default URL when no config is found", async () => {
69
74
  // Ensure env var is not set (and not inherited from environment)
70
75
  delete process.env.LLAMA_SERVER_URL;
71
76
 
72
- const result = await settings.resolveUrls();
77
+ const result = await manager.resolveUrls();
73
78
 
74
79
  expect(result).toEqual([LLAMA_SERVER_URL]);
75
80
  });
@@ -80,7 +85,7 @@ describe("URL resolution fallback chain", () => {
80
85
  });
81
86
  process.env.LLAMA_SERVER_URL = "http://env-url:8080";
82
87
 
83
- const result = await settings.resolveUrls();
88
+ const result = await manager.resolveUrls();
84
89
 
85
90
  expect(result).toEqual(["http://env-url:8080"]);
86
91
  });
@@ -88,7 +93,7 @@ describe("URL resolution fallback chain", () => {
88
93
  it("should use env variable when no other config exists", async () => {
89
94
  process.env.LLAMA_SERVER_URL = "http://env-url:8080";
90
95
 
91
- const result = await settings.resolveUrls();
96
+ const result = await manager.resolveUrls();
92
97
 
93
98
  expect(result).toEqual(["http://env-url:8080"]);
94
99
  });
@@ -98,7 +103,7 @@ describe("URL resolution fallback chain", () => {
98
103
  llamaServerUrl: "http://project:9999",
99
104
  });
100
105
 
101
- const result = await settings.resolveUrls();
106
+ const result = await manager.resolveUrls();
102
107
 
103
108
  expect(result).toEqual(["http://project:9999"]);
104
109
  });
@@ -108,7 +113,7 @@ describe("URL resolution fallback chain", () => {
108
113
  llamaServerUrl: "http://global:8080",
109
114
  });
110
115
 
111
- const result = await settings.resolveUrls();
116
+ const result = await manager.resolveUrls();
112
117
 
113
118
  expect(result).toEqual(["http://global:8080"]);
114
119
  });
@@ -116,7 +121,7 @@ describe("URL resolution fallback chain", () => {
116
121
  it("should strip trailing slashes from resolved URL", async () => {
117
122
  process.env.LLAMA_SERVER_URL = "http://localhost:8080/";
118
123
 
119
- const result = await settings.resolveUrls();
124
+ const result = await manager.resolveUrls();
120
125
 
121
126
  expect(result).toEqual(["http://localhost:8080"]);
122
127
  });
@@ -124,8 +129,8 @@ describe("URL resolution fallback chain", () => {
124
129
  it("should cache the resolved URL on subsequent calls", async () => {
125
130
  process.env.LLAMA_SERVER_URL = "http://first:8080";
126
131
 
127
- const result1 = await settings.resolveUrls();
128
- const result2 = await settings.resolveUrls();
132
+ const result1 = await manager.resolveUrls();
133
+ const result2 = await manager.resolveUrls();
129
134
 
130
135
  expect(result1).toEqual(["http://first:8080"]);
131
136
  expect(result2).toEqual(["http://first:8080"]);
@@ -134,7 +139,7 @@ describe("URL resolution fallback chain", () => {
134
139
  it("should handle multiple URLs separated by semicolons", async () => {
135
140
  process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090/";
136
141
 
137
- const result = await settings.resolveUrls();
142
+ const result = await manager.resolveUrls();
138
143
 
139
144
  expect(result).toEqual(["http://first:8080", "http://second:9090"]);
140
145
  });
@@ -142,13 +147,14 @@ describe("URL resolution fallback chain", () => {
142
147
  it("should drop env URLs without an http(s) scheme, warn, and fall through", async () => {
143
148
  process.env.LLAMA_SERVER_URL = "127.0.0.1:8080";
144
149
 
145
- const result = await settings.resolveUrls();
150
+ const manager = createSettingsManager();
151
+ const result = await manager.resolveUrls();
146
152
 
147
153
  expect(result).toEqual([LLAMA_SERVER_URL]);
148
- expect(settings.takeWarnings()).toEqual([
154
+ expect(manager.takeWarnings()).toEqual([
149
155
  "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
150
156
  ]);
151
- expect(settings.takeWarnings()).toEqual([]); // drained
157
+ expect(manager.takeWarnings()).toEqual([]); // drained
152
158
  });
153
159
 
154
160
  it("should drop server entries without an http(s) scheme and warn", async () => {
@@ -158,13 +164,14 @@ describe("URL resolution fallback chain", () => {
158
164
  },
159
165
  });
160
166
 
161
- const result = await settings.resolveUrls();
167
+ const manager = createSettingsManager();
168
+ const result = await manager.resolveUrls();
162
169
 
163
170
  expect(result).toEqual(["http://good:8080"]);
164
- expect(settings.takeWarnings()).toEqual([
171
+ expect(manager.takeWarnings()).toEqual([
165
172
  "Ignoring invalid server URL '127.0.0.1:8080' (needs http(s)://)",
166
173
  ]);
167
- expect(settings.takeWarnings()).toEqual([]); // drained
174
+ expect(manager.takeWarnings()).toEqual([]); // drained
168
175
  });
169
176
  });
170
177
 
@@ -179,7 +186,6 @@ describe("llamaSettings.servers resolution", () => {
179
186
 
180
187
  afterEach(() => {
181
188
  delete process.env.LLAMA_SERVER_URL;
182
- vi.resetModules();
183
189
  });
184
190
 
185
191
  beforeEach(() => {
@@ -187,6 +193,7 @@ describe("llamaSettings.servers resolution", () => {
187
193
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
188
194
  mockGetProjectSettings.mockReturnValue({});
189
195
  mockGetGlobalSettings.mockReturnValue({});
196
+ manager = createSettingsManager();
190
197
  });
191
198
 
192
199
  it("should resolve URLs from llamaSettings.servers in project config", async () => {
@@ -199,7 +206,7 @@ describe("llamaSettings.servers resolution", () => {
199
206
  },
200
207
  });
201
208
 
202
- const result = await settings.resolveUrls();
209
+ const result = await manager.resolveUrls();
203
210
 
204
211
  expect(result).toEqual([
205
212
  "http://project-server:8080",
@@ -219,7 +226,7 @@ describe("llamaSettings.servers resolution", () => {
219
226
  },
220
227
  });
221
228
 
222
- const result = await settings.resolveUrls();
229
+ const result = await manager.resolveUrls();
223
230
 
224
231
  expect(result).toEqual(["http://project:8080"]);
225
232
  });
@@ -231,7 +238,7 @@ describe("llamaSettings.servers resolution", () => {
231
238
  },
232
239
  });
233
240
 
234
- const result = await settings.resolveUrls();
241
+ const result = await manager.resolveUrls();
235
242
 
236
243
  expect(result).toEqual(["http://global:8080"]);
237
244
  });
@@ -244,7 +251,7 @@ describe("llamaSettings.servers resolution", () => {
244
251
  },
245
252
  });
246
253
 
247
- const result = await settings.resolveUrls();
254
+ const result = await manager.resolveUrls();
248
255
 
249
256
  expect(result).toEqual(["http://env:8080"]);
250
257
  });
@@ -256,7 +263,7 @@ describe("llamaSettings.servers resolution", () => {
256
263
  },
257
264
  });
258
265
 
259
- const result = await settings.resolveUrls();
266
+ const result = await manager.resolveUrls();
260
267
 
261
268
  expect(result).toEqual(["http://server:9090"]);
262
269
  });
@@ -269,7 +276,7 @@ describe("llamaSettings.servers resolution", () => {
269
276
  },
270
277
  });
271
278
 
272
- const result = await settings.resolveUrls();
279
+ const result = await manager.resolveUrls();
273
280
 
274
281
  expect(result).toEqual(["http://server:9090"]);
275
282
  });
@@ -282,7 +289,7 @@ describe("llamaSettings.servers resolution", () => {
282
289
  },
283
290
  });
284
291
 
285
- const result = await settings.resolveUrls();
292
+ const result = await manager.resolveUrls();
286
293
 
287
294
  expect(result).toEqual(["http://legacy:8080"]);
288
295
  });
@@ -294,7 +301,7 @@ describe("llamaSettings.servers resolution", () => {
294
301
  },
295
302
  });
296
303
 
297
- const result = await settings.resolveUrls();
304
+ const result = await manager.resolveUrls();
298
305
 
299
306
  expect(result).toEqual(["http://localhost:8080"]);
300
307
  });
@@ -303,20 +310,19 @@ describe("llamaSettings.servers resolution", () => {
303
310
  describe("API key resolution", () => {
304
311
  const mockGetAgentDir = vi.mocked(getAgentDir);
305
312
 
306
- afterEach(() => {
307
- vi.resetModules();
308
- });
313
+ afterEach(() => {});
309
314
 
310
315
  beforeEach(() => {
311
316
  vi.clearAllMocks();
312
317
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
313
318
  mockReadStoredCredential.mockReturnValue(undefined);
319
+ manager = createSettingsManager();
314
320
  });
315
321
 
316
322
  it("should return placeholder when credential is not found", () => {
317
323
  mockReadStoredCredential.mockReturnValue(undefined);
318
324
 
319
- const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
325
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
320
326
 
321
327
  expect(result).toEqual(API_KEY_PLACEHOLDER);
322
328
  });
@@ -324,7 +330,7 @@ describe("API key resolution", () => {
324
330
  it("should return placeholder when apiKey is missing from credential", () => {
325
331
  mockReadStoredCredential.mockReturnValue({});
326
332
 
327
- const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
333
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
328
334
 
329
335
  expect(result).toEqual(API_KEY_PLACEHOLDER);
330
336
  });
@@ -332,7 +338,7 @@ describe("API key resolution", () => {
332
338
  it("should return the key when present in credential", () => {
333
339
  mockReadStoredCredential.mockReturnValue({ key: "test-api-key" });
334
340
 
335
- const result = settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
341
+ const result = manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
336
342
 
337
343
  expect(result).toEqual("test-api-key");
338
344
  });
@@ -340,7 +346,7 @@ describe("API key resolution", () => {
340
346
  it("should call readStoredCredential with the provider ID", () => {
341
347
  mockReadStoredCredential.mockReturnValue({ key: "test-key" });
342
348
 
343
- settings.resolveApiKey("llama-server=http://127.0.0.1:8080");
349
+ manager.resolveApiKey("llama-server=http://127.0.0.1:8080");
344
350
 
345
351
  expect(mockReadStoredCredential).toHaveBeenCalledWith(
346
352
  "llama-server=http://127.0.0.1:8080",
@@ -350,7 +356,7 @@ describe("API key resolution", () => {
350
356
 
351
357
  describe("Server with custom id", () => {
352
358
  it("should use custom id as providerId when provided", () => {
353
- const server = new Server(settings, {
359
+ const server = new Server(manager, {
354
360
  baseUrl: "http://127.0.0.1:8080",
355
361
  customId: "my-custom-id",
356
362
  });
@@ -359,7 +365,9 @@ describe("Server with custom id", () => {
359
365
  });
360
366
 
361
367
  it("should fall back to URL-based providerId when no custom id", () => {
362
- const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
368
+ const server = new Server(manager, {
369
+ baseUrl: "http://127.0.0.1:8080",
370
+ });
363
371
 
364
372
  expect(server.providerId).toEqual(
365
373
  `${PROVIDER_PREFIX}=http://127.0.0.1:8080`,
@@ -367,7 +375,7 @@ describe("Server with custom id", () => {
367
375
  });
368
376
 
369
377
  it("should try custom id first in getApiKey(), then fall back to URL-based", () => {
370
- const server = new Server(settings, {
378
+ const server = new Server(manager, {
371
379
  baseUrl: "http://127.0.0.1:8080",
372
380
  customId: "my-custom-id",
373
381
  });
@@ -380,7 +388,7 @@ describe("Server with custom id", () => {
380
388
  .mockReturnValueOnce(API_KEY_PLACEHOLDER)
381
389
  .mockReturnValueOnce({ key: "fallback-key" });
382
390
 
383
- const result = server.getApiKey();
391
+ const result = server.apiKey;
384
392
 
385
393
  expect(result).toEqual("fallback-key");
386
394
  expect(mockReadStoredCredential).toHaveBeenNthCalledWith(1, "my-custom-id");
@@ -393,12 +401,12 @@ describe("Server with custom id", () => {
393
401
  it("should return custom id key directly when found", () => {
394
402
  mockReadStoredCredential.mockReturnValue({ key: "custom-key" });
395
403
 
396
- const server = new Server(settings, {
404
+ const server = new Server(manager, {
397
405
  baseUrl: "http://127.0.0.1:8080",
398
406
  customId: "my-custom-id",
399
407
  });
400
408
 
401
- const result = server.getApiKey();
409
+ const result = server.apiKey;
402
410
 
403
411
  expect(result).toEqual("custom-key");
404
412
  expect(mockReadStoredCredential).toHaveBeenCalledWith("my-custom-id");
@@ -407,7 +415,7 @@ describe("Server with custom id", () => {
407
415
 
408
416
  describe("Server with custom name", () => {
409
417
  it("should use custom name as suffix in providerName", () => {
410
- const server = new Server(settings, {
418
+ const server = new Server(manager, {
411
419
  baseUrl: "http://127.0.0.1:8080",
412
420
  customName: "Remote Server",
413
421
  });
@@ -416,13 +424,15 @@ describe("Server with custom name", () => {
416
424
  });
417
425
 
418
426
  it("should fall back to URL-based name when no custom name", () => {
419
- const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
427
+ const server = new Server(manager, {
428
+ baseUrl: "http://127.0.0.1:8080",
429
+ });
420
430
 
421
431
  expect(server.providerName).toEqual(`Llama.cpp (http://127.0.0.1:8080)`);
422
432
  });
423
433
 
424
434
  it("should use custom name even with custom id", () => {
425
- const server = new Server(settings, {
435
+ const server = new Server(manager, {
426
436
  baseUrl: "http://127.0.0.1:8080",
427
437
  customId: "my-custom-id",
428
438
  customName: "Remote Server",
@@ -434,30 +444,22 @@ describe("Server with custom name", () => {
434
444
  });
435
445
 
436
446
  describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
437
- afterEach(() => {
438
- vi.resetModules();
439
- });
447
+ afterEach(() => {});
440
448
 
441
449
  it("should return true when reactToModelSelect is not set", async () => {
442
- const { settings } = await import("../src/managers/settings");
443
-
444
- const result = await settings.resolveReactToModelSelect();
450
+ const result = await manager.resolveReactToModelSelect();
445
451
 
446
452
  expect(result).toBe(true);
447
453
  });
448
454
 
449
455
  it("should return false when autoloadOnMessage is not set", async () => {
450
- const { settings } = await import("../src/managers/settings");
451
-
452
- const result = await settings.resolveAutoloadOnMessage();
456
+ const result = await manager.resolveAutoloadOnMessage();
453
457
 
454
458
  expect(result).toBe(false);
455
459
  });
456
460
 
457
461
  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();
462
+ const result = await manager.resolveSortBy();
461
463
 
462
464
  expect(result).toBe("asc");
463
465
  });
@@ -470,10 +472,72 @@ describe("reactToModelSelect and autoloadOnMessage fallbacks", () => {
470
472
  },
471
473
  });
472
474
 
473
- const { settings } = await import("../src/managers/settings");
475
+ const manager = createSettingsManager();
476
+
477
+ expect(await manager.resolveReactToModelSelect()).toBe(false);
478
+ expect(await manager.resolveAutoloadOnMessage()).toBe(true);
479
+ });
480
+ });
481
+
482
+ describe("resolveShowServerUrls", () => {
483
+ const mockGetAgentDir = vi.mocked(getAgentDir);
484
+ const mockGetProjectSettings = vi.mocked(
485
+ mockSettingsManager.getProjectSettings,
486
+ );
487
+ const mockGetGlobalSettings = vi.mocked(
488
+ mockSettingsManager.getGlobalSettings,
489
+ );
490
+
491
+ afterEach(() => {});
492
+
493
+ beforeEach(() => {
494
+ vi.clearAllMocks();
495
+ mockGetAgentDir.mockReturnValue("/fake/agent/dir");
496
+ mockGetProjectSettings.mockReturnValue({});
497
+ mockGetGlobalSettings.mockReturnValue({});
498
+ manager = createSettingsManager();
499
+ });
500
+
501
+ it("should return true (default) when showServerUrls is not set", async () => {
502
+ const result = await manager.resolveShowServerUrls();
503
+
504
+ expect(result).toBe(true);
505
+ });
506
+
507
+ it("should return false when explicitly set to false", async () => {
508
+ mockGetProjectSettings.mockReturnValue({
509
+ llamaSettings: {
510
+ showServerUrls: false,
511
+ },
512
+ });
513
+
514
+ const manager = createSettingsManager();
515
+
516
+ expect(await manager.resolveShowServerUrls()).toBe(false);
517
+ });
518
+
519
+ it("should return true when explicitly set to true", async () => {
520
+ mockGetProjectSettings.mockReturnValue({
521
+ llamaSettings: {
522
+ showServerUrls: true,
523
+ },
524
+ });
525
+
526
+ const manager = createSettingsManager();
527
+
528
+ expect(await manager.resolveShowServerUrls()).toBe(true);
529
+ });
530
+
531
+ it("should use global settings when no project config exists", async () => {
532
+ mockGetGlobalSettings.mockReturnValue({
533
+ llamaSettings: {
534
+ showServerUrls: false,
535
+ },
536
+ });
537
+
538
+ const manager = createSettingsManager();
474
539
 
475
- expect(await settings.resolveReactToModelSelect()).toBe(false);
476
- expect(await settings.resolveAutoloadOnMessage()).toBe(true);
540
+ expect(await manager.resolveShowServerUrls()).toBe(false);
477
541
  });
478
542
  });
479
543
 
@@ -488,7 +552,6 @@ describe("resolveServers", () => {
488
552
 
489
553
  afterEach(() => {
490
554
  delete process.env.LLAMA_SERVER_URL;
491
- vi.resetModules();
492
555
  });
493
556
 
494
557
  beforeEach(() => {
@@ -496,6 +559,7 @@ describe("resolveServers", () => {
496
559
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
497
560
  mockGetProjectSettings.mockReturnValue({});
498
561
  mockGetGlobalSettings.mockReturnValue({});
562
+ manager = createSettingsManager();
499
563
  });
500
564
 
501
565
  it("should use llamaSettings.servers when configured", async () => {
@@ -507,7 +571,7 @@ describe("resolveServers", () => {
507
571
  },
508
572
  });
509
573
 
510
- const result = await settings.resolveServers();
574
+ const result = await manager.resolveServers();
511
575
 
512
576
  expect(result).toHaveLength(1);
513
577
  expect(result[0].baseUrl).toBe("http://custom:8080");
@@ -517,14 +581,14 @@ describe("resolveServers", () => {
517
581
  it("should fall back to resolveUrls when servers is empty", async () => {
518
582
  process.env.LLAMA_SERVER_URL = "http://env-server:9090";
519
583
 
520
- const result = await settings.resolveServers();
584
+ const result = await manager.resolveServers();
521
585
 
522
586
  expect(result).toHaveLength(1);
523
587
  expect(result[0].baseUrl).toBe("http://env-server:9090");
524
588
  });
525
589
 
526
590
  it("should fall back to default URL when no config exists", async () => {
527
- const result = await settings.resolveServers();
591
+ const result = await manager.resolveServers();
528
592
 
529
593
  expect(result).toHaveLength(1);
530
594
  expect(result[0].baseUrl).toBe(LLAMA_SERVER_URL);
@@ -539,7 +603,7 @@ describe("resolveServers", () => {
539
603
  },
540
604
  });
541
605
 
542
- const result = await settings.resolveServers();
606
+ const result = await manager.resolveServers();
543
607
 
544
608
  expect(result).toHaveLength(1);
545
609
  expect(result[0].baseUrl).toBe("http://127.0.0.1:8080");
@@ -555,7 +619,7 @@ describe("resolveServers", () => {
555
619
  });
556
620
  process.env.LLAMA_SERVER_URL = "http://first:8080;http://second:9090";
557
621
 
558
- const result = await settings.resolveServers();
622
+ const result = await manager.resolveServers();
559
623
 
560
624
  expect(result).toHaveLength(2);
561
625
  expect(result[0].baseUrl).toBe("http://first:8080");
@@ -572,7 +636,7 @@ describe("resolveServers", () => {
572
636
  },
573
637
  });
574
638
 
575
- const result = await settings.resolveServers();
639
+ const result = await manager.resolveServers();
576
640
 
577
641
  // env variable takes precedence via resolveUrls
578
642
  expect(result).toHaveLength(1);
@@ -581,14 +645,10 @@ describe("resolveServers", () => {
581
645
  });
582
646
 
583
647
  describe("resolveTimeouts", () => {
584
- afterEach(() => {
585
- vi.resetModules();
586
- });
648
+ afterEach(() => {});
587
649
 
588
650
  it("should return default timeouts when not configured", async () => {
589
- const { settings } = await import("../src/managers/settings");
590
-
591
- const result = await settings.resolveTimeouts();
651
+ const result = await manager.resolveTimeouts();
592
652
 
593
653
  expect(result).toEqual({
594
654
  pollingTimeout: POLLING_TIMEOUT,
@@ -603,9 +663,7 @@ describe("resolveTimeouts", () => {
603
663
  },
604
664
  });
605
665
 
606
- const { settings } = await import("../src/managers/settings");
607
-
608
- const result = await settings.resolveTimeouts();
666
+ const result = await manager.resolveTimeouts();
609
667
 
610
668
  expect(result.pollingTimeout).toBe(120000);
611
669
  expect(result.serverTimeout).toBe(SERVER_TIMEOUT);
@@ -618,9 +676,7 @@ describe("resolveTimeouts", () => {
618
676
  },
619
677
  });
620
678
 
621
- const { settings } = await import("../src/managers/settings");
622
-
623
- const result = await settings.resolveTimeouts();
679
+ const result = await manager.resolveTimeouts();
624
680
 
625
681
  expect(result.pollingTimeout).toBe(POLLING_TIMEOUT);
626
682
  expect(result.serverTimeout).toBe(3000);
@@ -634,9 +690,7 @@ describe("resolveTimeouts", () => {
634
690
  },
635
691
  });
636
692
 
637
- const { settings } = await import("../src/managers/settings");
638
-
639
- const result = await settings.resolveTimeouts();
693
+ const result = await manager.resolveTimeouts();
640
694
 
641
695
  expect(result).toEqual({
642
696
  pollingTimeout: 90000,
@@ -648,21 +702,20 @@ describe("resolveTimeouts", () => {
648
702
  describe("Thinking config resolution", () => {
649
703
  const mockGetAgentDir = vi.mocked(getAgentDir);
650
704
 
651
- afterEach(() => {
652
- vi.resetModules();
653
- });
705
+ afterEach(() => {});
654
706
 
655
707
  beforeEach(() => {
656
708
  vi.clearAllMocks();
657
709
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
658
710
  mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
659
711
  mockSettingsManager.getThinkingBudgets.mockReturnValue({});
712
+ manager = createSettingsManager();
660
713
  });
661
714
 
662
715
  it("should return the default thinking level from SettingsManager", () => {
663
716
  mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
664
717
 
665
- const result = settings.resolveThinkingLevel();
718
+ const result = manager.resolveThinkingLevel();
666
719
 
667
720
  expect(result).toEqual("low");
668
721
  });
@@ -672,7 +725,7 @@ describe("Thinking config resolution", () => {
672
725
  low: 4096,
673
726
  });
674
727
 
675
- const result = settings.resolveThinkingBudgets();
728
+ const result = manager.resolveThinkingBudgets();
676
729
 
677
730
  expect(result).toEqual(
678
731
  expect.objectContaining({
@@ -706,9 +759,7 @@ describe("setLlamaSetting", () => {
706
759
  const PROJECT_SETTINGS_PATH = "/fake/project/.pi/settings.json";
707
760
  const FAKE_CWD = "/fake/project";
708
761
 
709
- afterEach(() => {
710
- vi.resetModules();
711
- });
762
+ afterEach(() => {});
712
763
 
713
764
  beforeEach(() => {
714
765
  vi.clearAllMocks();
@@ -720,14 +771,14 @@ describe("setLlamaSetting", () => {
720
771
  mockReadFile.mockResolvedValue("{}");
721
772
  mockWriteFile.mockResolvedValue(undefined);
722
773
  mockRename.mockResolvedValue(undefined);
774
+ manager = createSettingsManager();
723
775
  });
724
776
 
725
777
  it("should write to project settings when .pi/settings.json exists (auto scope)", async () => {
726
778
  mockAccess.mockResolvedValue(undefined);
727
779
  mockReadFile.mockResolvedValue("{}");
728
780
 
729
- const { settings } = await import("../src/managers/settings");
730
- await settings.setLlamaSetting("sortBy", "desc");
781
+ await manager.setLlamaSetting("sortBy", "desc");
731
782
 
732
783
  expect(mockWriteFile).toHaveBeenCalledWith(
733
784
  `${PROJECT_SETTINGS_PATH}.tmp`,
@@ -744,8 +795,7 @@ describe("setLlamaSetting", () => {
744
795
  mockAccess.mockRejectedValue(new Error("ENOENT"));
745
796
  mockReadFile.mockResolvedValue("{}");
746
797
 
747
- const { settings } = await import("../src/managers/settings");
748
- await settings.setLlamaSetting("sortBy", "desc");
798
+ await manager.setLlamaSetting("sortBy", "desc");
749
799
 
750
800
  expect(mockWriteFile).toHaveBeenCalledWith(
751
801
  `${GLOBAL_SETTINGS_PATH}.tmp`,
@@ -762,8 +812,7 @@ describe("setLlamaSetting", () => {
762
812
  mockAccess.mockResolvedValue(undefined); // project exists but we override
763
813
  mockReadFile.mockResolvedValue("{}");
764
814
 
765
- const { settings } = await import("../src/managers/settings");
766
- await settings.setLlamaSetting("sortBy", "desc", "global");
815
+ await manager.setLlamaSetting("sortBy", "desc", "global");
767
816
 
768
817
  expect(mockWriteFile).toHaveBeenCalledWith(
769
818
  `${GLOBAL_SETTINGS_PATH}.tmp`,
@@ -776,8 +825,7 @@ describe("setLlamaSetting", () => {
776
825
  mockAccess.mockRejectedValue(new Error("ENOENT")); // project doesn't exist but we override
777
826
  mockReadFile.mockResolvedValue("{}");
778
827
 
779
- const { settings } = await import("../src/managers/settings");
780
- await settings.setLlamaSetting("sortBy", "desc", "project");
828
+ await manager.setLlamaSetting("sortBy", "desc", "project");
781
829
 
782
830
  expect(mockWriteFile).toHaveBeenCalledWith(
783
831
  `${PROJECT_SETTINGS_PATH}.tmp`,
@@ -796,8 +844,7 @@ describe("setLlamaSetting", () => {
796
844
  ),
797
845
  );
798
846
 
799
- const { settings } = await import("../src/managers/settings");
800
- await settings.setLlamaSetting("sortBy", "desc");
847
+ await manager.setLlamaSetting("sortBy", "desc");
801
848
 
802
849
  expect(mockWriteFile).toHaveBeenCalledTimes(1);
803
850
  const [tmpPath, written, encoding] = mockWriteFile.mock.calls[0];
@@ -815,9 +862,7 @@ describe("setLlamaSetting", () => {
815
862
  expect(mockReload).toHaveBeenCalledTimes(1);
816
863
  });
817
864
 
818
- afterEach(() => {
819
- vi.resetModules();
820
- });
865
+ afterEach(() => {});
821
866
 
822
867
  it("should reflect the new value in resolvers immediately after the write", async () => {
823
868
  mockSettingsManager.reload.mockImplementation(async () => {
@@ -826,18 +871,16 @@ describe("setLlamaSetting", () => {
826
871
  });
827
872
  });
828
873
 
829
- const { settings } = await import("../src/managers/settings");
830
- await settings.setLlamaSetting("sortBy", "desc");
874
+ await manager.setLlamaSetting("sortBy", "desc");
831
875
 
832
- expect(await settings.resolveSortBy()).toBe("desc");
876
+ expect(await manager.resolveSortBy()).toBe("desc");
833
877
  });
834
878
 
835
879
  it("should reject and skip reload when the write fails", async () => {
836
880
  mockAccess.mockRejectedValue(new Error("ENOENT"));
837
881
  mockWriteFile.mockRejectedValue(new Error("ENOSPC: simulated"));
838
882
 
839
- const { settings } = await import("../src/managers/settings");
840
- await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
883
+ await expect(manager.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
841
884
  "ENOSPC",
842
885
  );
843
886
  expect(mockReload).not.toHaveBeenCalled();
@@ -847,8 +890,7 @@ describe("setLlamaSetting", () => {
847
890
  mockAccess.mockRejectedValue(new Error("ENOENT"));
848
891
  mockReadFile.mockResolvedValue("{ broken");
849
892
 
850
- const { settings } = await import("../src/managers/settings");
851
- await expect(settings.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
893
+ await expect(manager.setLlamaSetting("sortBy", "desc")).rejects.toThrow(
852
894
  /Cannot parse/,
853
895
  );
854
896
  expect(mockWriteFile).not.toHaveBeenCalled();
@@ -857,15 +899,15 @@ describe("setLlamaSetting", () => {
857
899
 
858
900
  it("should persist booleans and numbers with type fidelity", async () => {
859
901
  mockAccess.mockRejectedValue(new Error("ENOENT"));
860
- const { settings } = await import("../src/managers/settings");
861
- await settings.setLlamaSetting("reactToModelSelect", false);
902
+
903
+ await manager.setLlamaSetting("reactToModelSelect", false);
862
904
 
863
905
  const [, firstWrite] = mockWriteFile.mock.calls[0];
864
906
  expect(JSON.parse(firstWrite as string)).toEqual({
865
907
  llamaSettings: { reactToModelSelect: false },
866
908
  });
867
909
 
868
- await settings.setLlamaSetting("pollingTimeout", 120000);
910
+ await manager.setLlamaSetting("pollingTimeout", 120000);
869
911
 
870
912
  const [, secondWrite] = mockWriteFile.mock.calls[1];
871
913
  expect(JSON.parse(secondWrite as string)).toEqual({
@@ -874,7 +916,8 @@ describe("setLlamaSetting", () => {
874
916
  });
875
917
 
876
918
  it("should still construct the manager without arguments", async () => {
877
- const { LlamaSettingsManager } = await import("../src/managers/settings");
919
+ const { LlamaSettingsManager } =
920
+ await import("../../src/managers/settings");
878
921
 
879
922
  expect(() => new LlamaSettingsManager()).not.toThrow();
880
923
  });
@@ -889,15 +932,14 @@ describe("resolveServerOverrides", () => {
889
932
  mockSettingsManager.getGlobalSettings,
890
933
  );
891
934
 
892
- afterEach(() => {
893
- vi.resetModules();
894
- });
935
+ afterEach(() => {});
895
936
 
896
937
  beforeEach(() => {
897
938
  vi.clearAllMocks();
898
939
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
899
940
  mockGetProjectSettings.mockReturnValue({});
900
941
  mockGetGlobalSettings.mockReturnValue({});
942
+ manager = createSettingsManager();
901
943
  });
902
944
 
903
945
  it("should return overrides for a server that has them configured", async () => {
@@ -922,7 +964,7 @@ describe("resolveServerOverrides", () => {
922
964
  },
923
965
  });
924
966
 
925
- const result = await settings.resolveServerOverrides(
967
+ const result = await manager.resolveServerOverrides(
926
968
  "http://127.0.0.1:8080",
927
969
  );
928
970
 
@@ -946,7 +988,7 @@ describe("resolveServerOverrides", () => {
946
988
  },
947
989
  });
948
990
 
949
- const result = await settings.resolveServerOverrides(
991
+ const result = await manager.resolveServerOverrides(
950
992
  "http://127.0.0.1:8080",
951
993
  );
952
994
 
@@ -960,7 +1002,7 @@ describe("resolveServerOverrides", () => {
960
1002
  },
961
1003
  });
962
1004
 
963
- const result = await settings.resolveServerOverrides(
1005
+ const result = await manager.resolveServerOverrides(
964
1006
  "http://127.0.0.1:8080",
965
1007
  );
966
1008
 
@@ -979,7 +1021,7 @@ describe("resolveServerOverrides", () => {
979
1021
  },
980
1022
  });
981
1023
 
982
- const result = await settings.resolveServerOverrides("http://global:8080");
1024
+ const result = await manager.resolveServerOverrides("http://global:8080");
983
1025
 
984
1026
  expect(result).toEqual({ "model-a": { cost: { input: 0.5 } } });
985
1027
  });
@@ -1006,7 +1048,7 @@ describe("resolveServerOverrides", () => {
1006
1048
  },
1007
1049
  });
1008
1050
 
1009
- const result = await settings.resolveServerOverrides("http://shared:8080");
1051
+ const result = await manager.resolveServerOverrides("http://shared:8080");
1010
1052
 
1011
1053
  expect(result).toEqual({
1012
1054
  "model-b": { cost: { input: 0.1, output: 0.2 } },
@@ -1018,7 +1060,7 @@ describe("resolveServerOverrides", () => {
1018
1060
  llamaSettings: { servers: [] },
1019
1061
  });
1020
1062
 
1021
- const result = await settings.resolveServerOverrides(
1063
+ const result = await manager.resolveServerOverrides(
1022
1064
  "http://127.0.0.1:8080",
1023
1065
  );
1024
1066
 
@@ -1028,7 +1070,7 @@ describe("resolveServerOverrides", () => {
1028
1070
  it("should return empty object when llamaSettings is missing", async () => {
1029
1071
  mockGetProjectSettings.mockReturnValue({});
1030
1072
 
1031
- const result = await settings.resolveServerOverrides(
1073
+ const result = await manager.resolveServerOverrides(
1032
1074
  "http://127.0.0.1:8080",
1033
1075
  );
1034
1076
 
@@ -1047,7 +1089,7 @@ describe("resolveServerOverrides", () => {
1047
1089
  },
1048
1090
  });
1049
1091
 
1050
- const result = await settings.resolveServerOverrides(
1092
+ const result = await manager.resolveServerOverrides(
1051
1093
  "http://127.0.0.1:8080",
1052
1094
  );
1053
1095
 
@@ -1057,7 +1099,7 @@ describe("resolveServerOverrides", () => {
1057
1099
 
1058
1100
  describe("Server with overrides", () => {
1059
1101
  it("should store and expose resolved overrides", () => {
1060
- const server = new Server(settings, {
1102
+ const server = new Server(manager, {
1061
1103
  baseUrl: "http://127.0.0.1:8080",
1062
1104
  overrides: {
1063
1105
  "model-a": { cost: { input: 0.2, output: 0.6 } },
@@ -1074,7 +1116,7 @@ describe("Server with overrides", () => {
1074
1116
  });
1075
1117
 
1076
1118
  it("should return undefined for findOverrideForModel when no overrides are provided", () => {
1077
- const server = new Server(settings, {
1119
+ const server = new Server(manager, {
1078
1120
  baseUrl: "http://127.0.0.1:8080",
1079
1121
  });
1080
1122
 
@@ -1091,15 +1133,14 @@ describe("resolveServers passes overrides", () => {
1091
1133
  mockSettingsManager.getGlobalSettings,
1092
1134
  );
1093
1135
 
1094
- afterEach(() => {
1095
- vi.resetModules();
1096
- });
1136
+ afterEach(() => {});
1097
1137
 
1098
1138
  beforeEach(() => {
1099
1139
  vi.clearAllMocks();
1100
1140
  mockGetAgentDir.mockReturnValue("/fake/agent/dir");
1101
1141
  mockGetProjectSettings.mockReturnValue({});
1102
1142
  mockGetGlobalSettings.mockReturnValue({});
1143
+ manager = createSettingsManager();
1103
1144
  });
1104
1145
 
1105
1146
  it("should pass resolved overrides to Server instances", async () => {
@@ -1117,7 +1158,7 @@ describe("resolveServers passes overrides", () => {
1117
1158
  },
1118
1159
  });
1119
1160
 
1120
- const result = await settings.resolveServers();
1161
+ const result = await manager.resolveServers();
1121
1162
 
1122
1163
  expect(result).toHaveLength(2);
1123
1164
  expect(result[0].findOverrideForModel("model-x")).toEqual({
@@ -1129,7 +1170,7 @@ describe("resolveServers passes overrides", () => {
1129
1170
 
1130
1171
  describe("Server.findOverrideForModel", () => {
1131
1172
  function createServer(overrides: Record<string, ModelOverride>): Server {
1132
- return new Server(settings as any, {
1173
+ return new Server(createSettingsManager() as any, {
1133
1174
  baseUrl: "http://127.0.0.1:8080",
1134
1175
  overrides,
1135
1176
  });