pi-llama-cpp 0.13.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 (77) hide show
  1. package/README.md +19 -18
  2. package/package.json +3 -3
  3. package/src/api/client.ts +25 -0
  4. package/src/constants.ts +2 -2
  5. package/src/enums/status.ts +0 -1
  6. package/src/index.ts +2 -1
  7. package/src/interfaces/endpoints/models.ts +1 -1
  8. package/src/interfaces/settings.ts +7 -1
  9. package/src/interfaces/sortBy.ts +4 -0
  10. package/src/managers/command/models.ts +254 -0
  11. package/src/managers/command.ts +34 -461
  12. package/src/managers/events.ts +2 -2
  13. package/src/managers/server.ts +34 -18
  14. package/src/managers/settings.ts +26 -90
  15. package/src/models/baseModel.ts +27 -10
  16. package/src/models/legacyModel.ts +2 -2
  17. package/src/models/routerModel.ts +2 -2
  18. package/src/models/singleModel.ts +1 -1
  19. package/src/server.ts +17 -38
  20. package/src/sse/client.ts +113 -59
  21. package/src/sse/fetch.ts +43 -0
  22. package/src/sse/manager.ts +9 -27
  23. package/src/sse/types.ts +0 -4
  24. package/src/ui/dialog/base.ts +118 -0
  25. package/src/ui/dialog/confirm.ts +45 -0
  26. package/src/ui/dialog/factory.ts +111 -0
  27. package/src/ui/dialog/input.ts +63 -0
  28. package/src/ui/dialog/options.ts +23 -0
  29. package/src/ui/editors/editorOptions.ts +50 -0
  30. package/src/ui/editors/itemBuilder.ts +43 -0
  31. package/src/ui/editors/listEditor.ts +300 -0
  32. package/src/ui/editors/override/entry.ts +24 -0
  33. package/src/ui/editors/override/entryEditor.ts +248 -0
  34. package/src/ui/editors/override/fields/base.ts +53 -0
  35. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  36. package/src/ui/editors/override/fields/cost.ts +64 -0
  37. package/src/ui/editors/override/fields/index.ts +68 -0
  38. package/src/ui/editors/override/fields/numeric.ts +55 -0
  39. package/src/ui/editors/override/fields/pattern.ts +24 -0
  40. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  41. package/src/ui/editors/override/itemBuilder.ts +39 -0
  42. package/src/ui/editors/override/overrideList.ts +127 -0
  43. package/src/ui/editors/server/builder.ts +60 -0
  44. package/src/ui/editors/server/fields.ts +84 -0
  45. package/src/ui/editors/server/itemBuilder.ts +64 -0
  46. package/src/ui/editors/server/serverEditor.ts +197 -0
  47. package/src/ui/editors/server/utils.ts +95 -0
  48. package/src/ui/editors/server/wizard.ts +110 -0
  49. package/src/ui/editors/settingField.ts +37 -0
  50. package/src/ui/editors/settingsListFactory.ts +33 -0
  51. package/src/ui/settings/index.ts +248 -0
  52. package/src/ui/strings.ts +25 -4
  53. package/src/utils/health.ts +2 -1
  54. package/src/utils/serverIds.ts +21 -0
  55. package/src/utils/settingsStore.ts +1 -1
  56. package/src/utils/urlResolver.ts +129 -0
  57. package/src/utils/urls.ts +33 -13
  58. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +88 -12
  59. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  60. package/tests/mocks.ts +2 -0
  61. package/tests/models/legacyModel.test.ts +103 -0
  62. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +71 -72
  63. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +17 -17
  64. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  65. package/tests/{server.test.ts → server/server.test.ts} +5 -17
  66. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  67. package/tests/{settings.test.ts → settings/settings.test.ts} +184 -141
  68. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  69. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +8 -26
  70. package/tests/{dialog.test.ts → ui/dialog.test.ts} +56 -4
  71. package/tests/{overrides.test.ts → ui/overrides.test.ts} +101 -111
  72. package/src/ui/dialog.ts +0 -290
  73. package/src/ui/overrideEntryEditor.ts +0 -119
  74. package/src/ui/overrideSettingsList.ts +0 -710
  75. package/src/ui/serverListEditor.ts +0 -59
  76. package/src/ui/serverSettingsList.ts +0 -513
  77. package/tests/legacyModel.test.ts +0 -97
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { SettingsStore } from "../src/utils/settingsStore";
2
+ import { SettingsStore } from "../../src/utils/settingsStore";
3
3
 
4
4
  const mockReadFile = vi.hoisted(() => vi.fn());
5
5
  const mockWriteFile = vi.hoisted(() => vi.fn());
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { SSEManager } from "../src/sse/manager";
3
- import { createMockServer } from "./mocks";
2
+ import { SSEManager } from "../../src/sse/manager";
3
+ import { createMockServer } from "../mocks";
4
4
 
5
5
  /**
6
6
  * Builds an SSEManager bound to a stub server, mirroring how
@@ -16,29 +16,15 @@ const createManager = (
16
16
  pollingTimeout,
17
17
  serverTimeout,
18
18
  }),
19
- "",
20
19
  );
21
20
 
22
- describe("SSEManager timeouts", async () => {
23
- it("should expose the timeouts of its server", async () => {
24
- const manager = createManager(5678, 1234);
25
-
26
- expect(await manager.getPollingTimeout()).toBe(5678);
27
- expect(await manager.getServerTimeout()).toBe(1234);
28
- });
29
- });
30
-
31
21
  describe("SSEManager subscribeToStatus", () => {
32
22
  it("should reject after the server's pollingTimeout, not the constant", async () => {
33
23
  vi.useFakeTimers();
24
+ // Never-resolving fetch: the connection stays open but no events arrive
34
25
  vi.stubGlobal(
35
- "EventSource",
36
- class StubEventSource {
37
- onopen = null;
38
- onerror = null;
39
- onmessage = null;
40
- close() {}
41
- },
26
+ "fetch",
27
+ vi.fn(async () => new Promise<Response>(() => {})),
42
28
  );
43
29
 
44
30
  try {
@@ -60,14 +46,10 @@ describe("SSEManager subscribeToStatus", () => {
60
46
 
61
47
  it("should resolve when a terminal status arrives before the timeout", async () => {
62
48
  vi.useFakeTimers();
49
+ // Never-resolving fetch: events are dispatched manually below
63
50
  vi.stubGlobal(
64
- "EventSource",
65
- class StubEventSource {
66
- onopen = null;
67
- onerror = null;
68
- onmessage = null;
69
- close() {}
70
- },
51
+ "fetch",
52
+ vi.fn(async () => new Promise<Response>(() => {})),
71
53
  );
72
54
 
73
55
  try {
@@ -5,9 +5,10 @@ import {
5
5
  type TUI,
6
6
  } from "@earendil-works/pi-tui";
7
7
  import { beforeEach, describe, expect, it, vi } from "vitest";
8
- import { ConfirmDialog, InputDialog } from "../src/ui/dialog";
9
- import { createOverrideSettingsList } from "../src/ui/overrideSettingsList";
10
- import { ServerSettingsList } from "../src/ui/serverSettingsList";
8
+ import { ConfirmDialog } from "../../src/ui/dialog/confirm";
9
+ import { InputDialog } from "../../src/ui/dialog/input";
10
+ import { OverrideSettingsList } from "../../src/ui/editors/override/overrideList";
11
+ import { ServerSettingsList } from "../../src/ui/editors/server/serverEditor";
11
12
 
12
13
  beforeEach(() => {
13
14
  initTheme();
@@ -138,6 +139,7 @@ describe("ServerSettingsList add wizard", () => {
138
139
  persist,
139
140
  done,
140
141
  onError,
142
+ authResolver: () => Promise.resolve("sk-placeholder"),
141
143
  });
142
144
  return { editor, persist, done, onError };
143
145
  };
@@ -191,7 +193,7 @@ describe("OverrideEntryListEditor cost re-prefill", () => {
191
193
 
192
194
  it("re-entering a cost field prefills the just-saved value", async () => {
193
195
  const persist = vi.fn().mockResolvedValue(undefined);
194
- const list = createOverrideSettingsList({
196
+ const list = new OverrideSettingsList({
195
197
  tui: createMockTui(),
196
198
  theme: createMockTheme(),
197
199
  keybindings: createKeybindings(),
@@ -226,3 +228,53 @@ describe("OverrideEntryListEditor cost re-prefill", () => {
226
228
  expect(rendered).not.toContain("0.2");
227
229
  });
228
230
  });
231
+
232
+ describe("OverrideSettingsList escape navigation", () => {
233
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
234
+
235
+ const build = () => {
236
+ const done = vi.fn();
237
+ const list = new OverrideSettingsList({
238
+ tui: createMockTui(),
239
+ theme: createMockTheme(),
240
+ keybindings: createKeybindings(),
241
+ servers: [
242
+ {
243
+ url: "http://x:1",
244
+ overrides: { "gpt-*": { cost: { input: 0.2 } } },
245
+ },
246
+ ] as never[],
247
+ persist: vi.fn().mockResolvedValue(undefined),
248
+ done,
249
+ onError: vi.fn(),
250
+ onChanged: vi.fn(),
251
+ });
252
+ return { list, done };
253
+ };
254
+
255
+ it("Esc from the field submenu then the entry list lands on the server list", async () => {
256
+ const { list, done } = build();
257
+
258
+ list.handleInput(ENTER); // drill into the server row (entry list)
259
+ list.handleInput(ENTER); // open the field submenu
260
+ expect(list.render(80).join("\n")).not.toContain("http://x:1");
261
+
262
+ list.handleInput(ESC); // field submenu → entry list
263
+ await flush();
264
+ expect(done).not.toHaveBeenCalled();
265
+ expect(list.render(80).join("\n")).not.toContain("http://x:1");
266
+
267
+ list.handleInput(ESC); // entry list → server list
268
+ await flush();
269
+ // The dialog must stay open, showing the server row again
270
+ expect(done).not.toHaveBeenCalled();
271
+ expect(list.render(80).join("\n")).toContain("http://x:1");
272
+ });
273
+
274
+ it("Esc from the server list closes the dialog", async () => {
275
+ const { list, done } = build();
276
+
277
+ list.handleInput(ESC);
278
+ expect(done).toHaveBeenCalledTimes(1);
279
+ });
280
+ });
@@ -1,18 +1,14 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { FALLBACK_CTX } from "../src/constants";
3
- import type { LlamaServer, ModelOverride } from "../src/interfaces/settings";
4
- import { SingleModel } from "../src/models/singleModel";
5
- import { Server } from "../src/server";
2
+ import { FALLBACK_CTX } from "../../src/constants";
3
+ import type { ModelOverride } from "../../src/interfaces/settings";
4
+ import { SingleModel } from "../../src/models/singleModel";
5
+ import { Server } from "../../src/server";
6
+ import { OverrideEntry } from "../../src/ui/editors/override/entry";
6
7
  import {
7
- addOverrideEntry,
8
- applyCostFieldValue,
9
- formatOverrideSummary,
10
- parseCostValue,
11
- removeOverrideEntry,
12
- updateOverrideEntry,
13
- } from "../src/ui/overrideEntryEditor";
14
- import { overrideFieldValue } from "../src/ui/overrideSettingsList";
15
- import { createMockServer, mockRpc } from "./mocks";
8
+ OverrideFields,
9
+ OverrideSummary,
10
+ } from "../../src/ui/editors/override/fields";
11
+ import { createMockServer, mockRpc } from "../mocks";
16
12
 
17
13
  beforeEach(() => {
18
14
  mockRpc.mockReset();
@@ -94,16 +90,18 @@ describe("BaseModel reasoning override", () => {
94
90
 
95
91
  // ─── capabilities override ────────────────────────────────────────────────
96
92
 
97
- describe("BaseModel capabilities override", () => {
93
+ describe("BaseModel capabilities override (via toProviderConfig)", () => {
98
94
  it("should fully replace detection without contacting the server", async () => {
99
95
  mockRpc.mockImplementation(() => {
100
96
  throw new Error("override must not trigger a fetch");
101
97
  });
102
98
 
103
- const model = createModel({ test: { capabilities: ["text", "image"] } });
104
- const capabilities = await model.getCapabilities();
99
+ const model = createModel({
100
+ test: { capabilities: ["text", "image"], contextSize: 8192 },
101
+ });
102
+ const { input } = await model.toProviderConfig();
105
103
 
106
- expect(capabilities).toEqual(["text", "image"]);
104
+ expect(input).toEqual(["text", "image"]);
107
105
  expect(mockRpc).not.toHaveBeenCalled();
108
106
  });
109
107
 
@@ -111,41 +109,41 @@ describe("BaseModel capabilities override", () => {
111
109
  mockDetection(8192);
112
110
 
113
111
  const model = createModel({ other: { capabilities: ["text"] } });
114
- const capabilities = await model.getCapabilities();
112
+ const { input } = await model.toProviderConfig();
115
113
 
116
- expect(capabilities).toEqual(["text"]);
114
+ expect(input).toEqual(["text"]);
117
115
  expect(mockRpc).toHaveBeenCalled();
118
116
  });
119
117
  });
120
118
 
121
119
  // ─── contextSize override ─────────────────────────────────────────────────
122
120
 
123
- describe("BaseModel.getContextSize with contextSize override", () => {
121
+ describe("BaseModel contextSize override (via toProviderConfig)", () => {
124
122
  it("should return the overridden context size instead of the detected one", async () => {
125
123
  mockDetection(8192);
126
124
 
127
125
  const model = createModel({ test: { contextSize: 32768 } });
128
- const ctxSize = await model.getContextSize();
126
+ const { contextWindow } = await model.toProviderConfig();
129
127
 
130
- expect(ctxSize).toBe(32768);
128
+ expect(contextWindow).toBe(32768);
131
129
  });
132
130
 
133
131
  it("should treat a stored 0 as absent and autodetect", async () => {
134
132
  mockDetection(8192);
135
133
 
136
134
  const model = createModel({ test: { contextSize: 0 } });
137
- const ctxSize = await model.getContextSize();
135
+ const { contextWindow } = await model.toProviderConfig();
138
136
 
139
- expect(ctxSize).toBe(8192);
137
+ expect(contextWindow).toBe(8192);
140
138
  });
141
139
 
142
140
  it("should autodetect when no override matches", async () => {
143
141
  mockDetection(8192);
144
142
 
145
143
  const model = createModel({ other: { contextSize: 32768 } });
146
- const ctxSize = await model.getContextSize();
144
+ const { contextWindow } = await model.toProviderConfig();
147
145
 
148
- expect(ctxSize).toBe(8192);
146
+ expect(contextWindow).toBe(8192);
149
147
  });
150
148
  });
151
149
 
@@ -251,11 +249,14 @@ describe("toProviderConfig overrides", () => {
251
249
  });
252
250
  });
253
251
 
254
- // ─── formatOverrideSummary ────────────────────────────────────────────────
252
+ // ─── OverrideEntry.summary ────────────────────────────────────────────────────
253
+
254
+ describe("OverrideSummary.of", () => {
255
+ const summary = (override: ModelOverride): string =>
256
+ OverrideSummary.of(override);
255
257
 
256
- describe("formatOverrideSummary", () => {
257
258
  it("should render every configured field", () => {
258
- const summary = formatOverrideSummary({
259
+ const rendered = summary({
259
260
  cost: { input: 0.2, output: 0.6, cacheRead: 0.01 },
260
261
  capabilities: ["text", "image"],
261
262
  reasoning: false,
@@ -263,122 +264,111 @@ describe("formatOverrideSummary", () => {
263
264
  maxTokens: 4096,
264
265
  });
265
266
 
266
- expect(summary).toContain("input: $0.2");
267
- expect(summary).toContain("output: $0.6");
268
- expect(summary).toContain("cache read: $0.01");
269
- expect(summary).toContain("capabilities: text,image");
270
- expect(summary).toContain("reasoning: false");
271
- expect(summary).toContain("contextSize: 32768");
272
- expect(summary).toContain("maxTokens: 4096");
267
+ expect(rendered).toContain("input: $0.2");
268
+ expect(rendered).toContain("output: $0.6");
269
+ expect(rendered).toContain("cache read: $0.01");
270
+ expect(rendered).toContain("capabilities: text,image");
271
+ expect(rendered).toContain("reasoning: false");
272
+ expect(rendered).toContain("contextSize: 32768");
273
+ expect(rendered).toContain("maxTokens: 4096");
273
274
  });
274
275
 
275
276
  it("should omit zero, undefined and empty fields", () => {
276
- const summary = formatOverrideSummary({
277
+ const rendered = summary({
277
278
  cost: { input: 0, output: 0.6 },
278
279
  reasoning: undefined,
279
280
  });
280
281
 
281
- expect(summary).not.toContain("input:");
282
- expect(summary).toContain("output: $0.6");
283
- expect(summary).not.toContain("capabilities");
284
- expect(summary).not.toContain("reasoning");
285
- expect(summary).not.toContain("contextSize");
286
- expect(summary).not.toContain("maxTokens");
282
+ expect(rendered).not.toContain("input:");
283
+ expect(rendered).toContain("output: $0.6");
284
+ expect(rendered).not.toContain("capabilities");
285
+ expect(rendered).not.toContain("reasoning");
286
+ expect(rendered).not.toContain("contextSize");
287
+ expect(rendered).not.toContain("maxTokens");
287
288
  });
288
289
 
289
290
  it("should render an em dash for an empty override", () => {
290
- expect(formatOverrideSummary({})).toBe("—");
291
+ expect(summary({})).toBe("—");
291
292
  });
292
293
  });
293
294
 
294
- // ─── override entry list helpers (overrideEntryEditor) ────────────────────
295
-
296
- describe("override entry list helpers", () => {
297
- const servers = [
298
- { url: "http://a", overrides: { "a-1": { reasoning: false } } },
299
- { url: "http://b" },
300
- ] as LlamaServer[];
301
-
302
- it("addOverrideEntry should append a pattern → override entry", () => {
303
- const next = addOverrideEntry(servers, 0, "a-2", { contextSize: 4096 });
304
- expect(next[0].overrides).toEqual({
305
- "a-1": { reasoning: false },
306
- "a-2": { contextSize: 4096 },
307
- });
308
- });
309
-
310
- it("updateOverrideEntry should replace in place without reordering", () => {
311
- const next = updateOverrideEntry(servers, 0, 0, "a-renamed", {
312
- reasoning: true,
313
- });
314
- expect(Object.keys(next[0].overrides!)).toEqual(["a-renamed"]);
315
- expect(next[0].overrides!["a-renamed"]).toEqual({ reasoning: true });
316
- expect(next[1]).toBe(servers[1]);
317
- });
295
+ // ─── CostField tests ──────────────────────────────────────────────────────
318
296
 
319
- it("removeOverrideEntry should drop only the targeted entry", () => {
320
- const withTwo = addOverrideEntry(servers, 0, "a-2", {});
321
- const next = removeOverrideEntry(withTwo, 0, 0);
322
- expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
297
+ describe("CostField validation and apply", () => {
298
+ it("CostField.validate should accept blank, non-negative numbers; reject the rest", () => {
299
+ const cost = OverrideFields.byId("cost.input");
300
+ expect(cost.validate("")).toBe("0");
301
+ expect(cost.validate(" 0.5 ")).toBe("0.5");
302
+ expect(cost.validate("-1")).toBeNull();
303
+ expect(cost.validate("abc")).toBeNull();
304
+ expect(cost.validate("Infinity")).toBeNull();
323
305
  });
324
306
 
325
- it("parseCostValue should accept blank, non-negative numbers; reject the rest", () => {
326
- expect(parseCostValue("")).toBe(0);
327
- expect(parseCostValue(" 0.5 ")).toBe(0.5);
328
- expect(parseCostValue("-1")).toBeNull();
329
- expect(parseCostValue("abc")).toBeNull();
330
- expect(parseCostValue("Infinity")).toBeNull();
307
+ it("CostField.apply should set a positive value, keeping other fields", () => {
308
+ const override: ModelOverride = { cost: { input: 0.2 } };
309
+ OverrideFields.byId("cost.output").apply(override, "0.6");
310
+ expect(override.cost).toEqual({ input: 0.2, output: 0.6 });
331
311
  });
332
312
 
333
- it("applyCostFieldValue should set a positive value, keeping other fields", () => {
334
- const next = applyCostFieldValue({ input: 0.2 }, "output", 0.6);
335
- expect(next).toEqual({ input: 0.2, output: 0.6 });
313
+ it("CostField.apply should remove the field on zero", () => {
314
+ const override: ModelOverride = { cost: { input: 0.2, output: 0.6 } };
315
+ OverrideFields.byId("cost.input").apply(override, "0");
316
+ expect(override.cost).toEqual({ output: 0.6 });
336
317
  });
337
318
 
338
- it("applyCostFieldValue should remove the field on zero", () => {
339
- const next = applyCostFieldValue({ input: 0.2, output: 0.6 }, "input", 0);
340
- expect(next).toEqual({ output: 0.6 });
341
- });
319
+ it("CostField.apply should drop the whole cost object when the last field is zeroed", () => {
320
+ const single: ModelOverride = { cost: { input: 0.2 } };
321
+ OverrideFields.byId("cost.input").apply(single, "0");
322
+ expect(single.cost).toBeUndefined();
342
323
 
343
- it("applyCostFieldValue should return undefined when the last field is zeroed", () => {
344
- expect(applyCostFieldValue({ input: 0.2 }, "input", 0)).toBeUndefined();
345
- expect(applyCostFieldValue(undefined, "input", 0)).toBeUndefined();
324
+ const empty: ModelOverride = {};
325
+ OverrideFields.byId("cost.input").apply(empty, "0");
326
+ expect(empty.cost).toBeUndefined();
346
327
  });
347
328
 
348
- it("applyCostFieldValue should treat an empty override as unset cost", () => {
349
- expect(applyCostFieldValue(undefined, "input", 0.5)).toEqual({
350
- input: 0.5,
351
- });
329
+ it("CostField.apply should treat an empty override as unset cost", () => {
330
+ const override: ModelOverride = {};
331
+ OverrideFields.byId("cost.input").apply(override, "0.5");
332
+ expect(override.cost).toEqual({ input: 0.5 });
352
333
  });
353
334
  });
354
335
 
355
- describe("overrideFieldValue", () => {
336
+ describe("OverrideField.displayValue", () => {
337
+ const display = (id: string, override: ModelOverride): string =>
338
+ OverrideFields.byId(id).displayValue(new OverrideEntry("gpt-*", override));
339
+
356
340
  it("formats cost fields, defaulting to 0", () => {
357
- expect(overrideFieldValue("cost.input", {})).toBe("0");
358
- expect(overrideFieldValue("cost.input", { cost: { input: 0.2 } })).toBe(
359
- "0.2",
360
- );
361
- expect(overrideFieldValue("cost.cacheWrite", {})).toBe("0");
341
+ expect(display("cost.input", {})).toBe("0");
342
+ expect(display("cost.input", { cost: { input: 0.2 } })).toBe("0.2");
343
+ expect(display("cost.cacheWrite", {})).toBe("0");
362
344
  });
363
345
 
364
346
  it("formats capabilities and reasoning labels", () => {
365
- expect(overrideFieldValue("capabilities", {})).toBe("text");
366
- expect(
367
- overrideFieldValue("capabilities", { capabilities: ["text", "image"] }),
368
- ).toBe("text | image");
369
- expect(overrideFieldValue("reasoning", {})).toBe("true");
370
- expect(overrideFieldValue("reasoning", { reasoning: false })).toBe("false");
347
+ expect(display("capabilities", {})).toBe("text");
348
+ expect(display("capabilities", { capabilities: ["text", "image"] })).toBe(
349
+ "text | image",
350
+ );
351
+ expect(display("reasoning", {})).toBe("true");
352
+ expect(display("reasoning", { reasoning: false })).toBe("false");
371
353
  });
372
354
 
373
355
  it("formats maxTokens/contextSize, defaulting to 0", () => {
374
- expect(overrideFieldValue("maxTokens", {})).toBe("0");
375
- expect(overrideFieldValue("maxTokens", { maxTokens: 4096 })).toBe("4096");
376
- expect(overrideFieldValue("contextSize", { contextSize: 8192 })).toBe(
377
- "8192",
378
- );
356
+ expect(display("maxTokens", {})).toBe("0");
357
+ expect(display("maxTokens", { maxTokens: 4096 })).toBe("4096");
358
+ expect(display("contextSize", { contextSize: 8192 })).toBe("8192");
379
359
  });
380
360
 
381
- it("returns an empty label for unknown fields", () => {
382
- expect(overrideFieldValue("other", {})).toBe("");
361
+ it("returns the entry's pattern for the pattern field", () => {
362
+ expect(
363
+ OverrideFields.byId("pattern").displayValue(
364
+ new OverrideEntry("gpt-*", {}),
365
+ ),
366
+ ).toBe("gpt-*");
367
+ });
368
+
369
+ it("throws for unknown fields", () => {
370
+ expect(() => OverrideFields.byId("other")).toThrow(
371
+ "Unknown override field: other",
372
+ );
383
373
  });
384
374
  });