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