pi-llama-cpp 0.13.0 → 0.14.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 (65) hide show
  1. package/README.md +7 -8
  2. package/package.json +2 -2
  3. package/src/api/client.ts +25 -0
  4. package/src/constants.ts +0 -5
  5. package/src/enums/status.ts +0 -1
  6. package/src/interfaces/endpoints/models.ts +1 -1
  7. package/src/interfaces/settings.ts +1 -1
  8. package/src/interfaces/sortBy.ts +4 -0
  9. package/src/managers/command/models.ts +247 -0
  10. package/src/managers/command.ts +29 -461
  11. package/src/managers/events.ts +2 -2
  12. package/src/managers/server.ts +27 -3
  13. package/src/managers/settings.ts +12 -88
  14. package/src/models/baseModel.ts +27 -10
  15. package/src/models/legacyModel.ts +2 -2
  16. package/src/models/routerModel.ts +2 -2
  17. package/src/models/singleModel.ts +1 -1
  18. package/src/server.ts +11 -29
  19. package/src/sse/client.ts +113 -59
  20. package/src/sse/fetch.ts +43 -0
  21. package/src/sse/manager.ts +9 -27
  22. package/src/sse/types.ts +0 -4
  23. package/src/ui/dialog/base.ts +118 -0
  24. package/src/ui/dialog/confirm.ts +45 -0
  25. package/src/ui/dialog/factory.ts +111 -0
  26. package/src/ui/dialog/input.ts +63 -0
  27. package/src/ui/dialog/options.ts +23 -0
  28. package/src/ui/editors/editorOptions.ts +43 -0
  29. package/src/ui/editors/itemBuilder.ts +47 -0
  30. package/src/ui/editors/listEditor.ts +291 -0
  31. package/src/ui/editors/override/entry.ts +24 -0
  32. package/src/ui/editors/override/entryEditor.ts +166 -0
  33. package/src/ui/editors/override/fields.ts +294 -0
  34. package/src/ui/editors/override/handlers.ts +118 -0
  35. package/src/ui/editors/override/itemBuilder.ts +42 -0
  36. package/src/ui/editors/override/overrideList.ts +127 -0
  37. package/src/ui/editors/server/builder.ts +60 -0
  38. package/src/ui/editors/server/fields.ts +84 -0
  39. package/src/ui/editors/server/handlers.ts +32 -0
  40. package/src/ui/editors/server/itemBuilder.ts +68 -0
  41. package/src/ui/editors/server/serverEditor.ts +161 -0
  42. package/src/ui/editors/server/utils.ts +45 -0
  43. package/src/ui/editors/server/wizard.ts +110 -0
  44. package/src/ui/editors/settingField.ts +37 -0
  45. package/src/ui/editors/settingsListFactory.ts +33 -0
  46. package/src/ui/settings/index.ts +237 -0
  47. package/src/ui/strings.ts +8 -4
  48. package/src/utils/serverIds.ts +21 -0
  49. package/src/utils/settingsStore.ts +1 -1
  50. package/src/utils/urlResolver.ts +129 -0
  51. package/src/utils/urls.ts +33 -13
  52. package/tests/commandManager.test.ts +14 -9
  53. package/tests/dialog.test.ts +55 -4
  54. package/tests/legacyModel.test.ts +34 -28
  55. package/tests/overrides.test.ts +111 -88
  56. package/tests/routerModel.test.ts +67 -68
  57. package/tests/server.test.ts +0 -12
  58. package/tests/settings.test.ts +10 -8
  59. package/tests/singleModel.test.ts +12 -12
  60. package/tests/sseManager.test.ts +6 -24
  61. package/src/ui/dialog.ts +0 -290
  62. package/src/ui/overrideEntryEditor.ts +0 -119
  63. package/src/ui/overrideSettingsList.ts +0 -710
  64. package/src/ui/serverListEditor.ts +0 -59
  65. package/src/ui/serverSettingsList.ts +0 -513
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
+ import { FALLBACK_CTX } from "../src/constants";
2
3
  import { Mode } from "../src/enums/mode";
3
4
  import { DataProperty } from "../src/interfaces/endpoints/models";
4
5
  import { LegacyModel } from "../src/models/legacyModel";
@@ -28,70 +29,75 @@ describe("LegacyModel mode", () => {
28
29
  });
29
30
  });
30
31
 
31
- describe("LegacyModel capabilities", () => {
32
+ describe("LegacyModel capabilities (via toProviderConfig)", () => {
32
33
  it("should detect image capability when multimodal is in capabilities", async () => {
33
34
  mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
35
+ mockRpc.mockResolvedValue({ n_ctx: 4096, data: [{ max_model_len: 8192 }] });
34
36
 
35
37
  const model = createModel();
36
- const capabilities = await model.getCapabilities();
38
+ const { input } = await model.toProviderConfig();
37
39
 
38
- expect(capabilities).toEqual(["text", "image"]);
40
+ expect(input).toEqual(["text", "image"]);
39
41
  });
40
42
 
41
43
  it("should detect text-only capability when multimodal is not in capabilities", async () => {
42
44
  mockRpc.mockResolvedValueOnce({ modalities: { vision: false } });
45
+ mockRpc.mockResolvedValue({ n_ctx: 4096, data: [{ max_model_len: 8192 }] });
43
46
 
44
47
  const model = createModel();
45
- const capabilities = await model.getCapabilities();
48
+ const { input } = await model.toProviderConfig();
46
49
 
47
- expect(capabilities).toEqual(["text"]);
50
+ expect(input).toEqual(["text"]);
48
51
  });
49
52
 
50
53
  it("should fall back to text-only when auth fails", async () => {
51
- mockRpc.mockRejectedValue(new Error("401 Unauthorized"));
54
+ mockRpc.mockRejectedValueOnce(new Error("401 Unauthorized")); // /props
55
+ mockRpc.mockRejectedValueOnce(new Error("401 Unauthorized")); // /v1/models in BaseModel's catch
56
+ mockRpc.mockResolvedValue({
57
+ n_ctx: 4096,
58
+ data: [{ max_model_len: 8192 }],
59
+ models: [{ capabilities: ["text"] }],
60
+ });
52
61
 
53
62
  const model = createModel();
54
- const capabilities = await model.getCapabilities();
63
+ const { input } = await model.toProviderConfig();
55
64
 
56
- expect(capabilities).toEqual(["text"]);
65
+ expect(input).toEqual(["text"]);
57
66
  });
58
67
  });
59
68
 
60
- describe("LegacyModel getContextSize", () => {
69
+ describe("LegacyModel context size (via toProviderConfig)", () => {
61
70
  it("should use max_model_len when it is non-zero", async () => {
62
- mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
63
- mockRpc.mockResolvedValueOnce({
64
- data: [{ max_model_len: 8192 }],
65
- });
71
+ mockRpc.mockResolvedValueOnce({ modalities: { vision: false } }); // capabilities: /props
72
+ mockRpc.mockResolvedValueOnce({ n_ctx: 4096 }); // /props
73
+ mockRpc.mockResolvedValueOnce({ data: [{ max_model_len: 8192 }] }); // /v1/models
66
74
 
67
75
  const model = createModel();
68
- const ctxSize = await model.getContextSize();
76
+ const { contextWindow } = await model.toProviderConfig();
69
77
 
70
- expect(ctxSize).toBe(8192);
78
+ expect(contextWindow).toBe(8192);
71
79
  expect(mockRpc).toHaveBeenCalledWith("/v1/models");
72
80
  });
73
81
 
74
82
  it("should fall back to n_ctx when max_model_len is 0", async () => {
75
- mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
76
- mockRpc.mockResolvedValueOnce({
77
- data: [{ max_model_len: 0 }],
78
- });
83
+ mockRpc.mockResolvedValueOnce({ modalities: { vision: false } }); // capabilities: /props
84
+ mockRpc.mockResolvedValueOnce({ n_ctx: 4096 }); // /props
85
+ mockRpc.mockResolvedValueOnce({ data: [{ max_model_len: 0 }] }); // /v1/models
79
86
 
80
87
  const model = createModel();
81
- const ctxSize = await model.getContextSize();
88
+ const { contextWindow } = await model.toProviderConfig();
82
89
 
83
- expect(ctxSize).toBe(4096);
90
+ expect(contextWindow).toBe(4096);
84
91
  });
85
92
 
86
- it("should return DEFAULT_CTX when both values are missing/null", async () => {
87
- mockRpc.mockResolvedValueOnce({});
88
- mockRpc.mockResolvedValueOnce({
89
- data: [{ max_model_len: null }],
90
- });
93
+ it("should return FALLBACK_CTX when both values are missing/null", async () => {
94
+ mockRpc.mockResolvedValueOnce({ modalities: { vision: false } }); // capabilities: /props
95
+ mockRpc.mockResolvedValueOnce({}); // /props
96
+ mockRpc.mockResolvedValueOnce({ data: [{ max_model_len: null }] }); // /v1/models
91
97
 
92
98
  const model = createModel();
93
- const ctxSize = await model.getContextSize();
99
+ const { contextWindow } = await model.toProviderConfig();
94
100
 
95
- expect(ctxSize).toBe(128000);
101
+ expect(contextWindow).toBe(FALLBACK_CTX);
96
102
  });
97
103
  });
@@ -3,15 +3,12 @@ import { FALLBACK_CTX } from "../src/constants";
3
3
  import type { LlamaServer, ModelOverride } from "../src/interfaces/settings";
4
4
  import { SingleModel } from "../src/models/singleModel";
5
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";
8
+ OverrideFields,
9
+ OverrideSummary,
10
+ } from "../src/ui/editors/override/fields";
11
+ import { OverrideEntryMutator } from "../src/ui/editors/override/handlers";
15
12
  import { createMockServer, mockRpc } from "./mocks";
16
13
 
17
14
  beforeEach(() => {
@@ -94,16 +91,18 @@ describe("BaseModel reasoning override", () => {
94
91
 
95
92
  // ─── capabilities override ────────────────────────────────────────────────
96
93
 
97
- describe("BaseModel capabilities override", () => {
94
+ describe("BaseModel capabilities override (via toProviderConfig)", () => {
98
95
  it("should fully replace detection without contacting the server", async () => {
99
96
  mockRpc.mockImplementation(() => {
100
97
  throw new Error("override must not trigger a fetch");
101
98
  });
102
99
 
103
- const model = createModel({ test: { capabilities: ["text", "image"] } });
104
- const capabilities = await model.getCapabilities();
100
+ const model = createModel({
101
+ test: { capabilities: ["text", "image"], contextSize: 8192 },
102
+ });
103
+ const { input } = await model.toProviderConfig();
105
104
 
106
- expect(capabilities).toEqual(["text", "image"]);
105
+ expect(input).toEqual(["text", "image"]);
107
106
  expect(mockRpc).not.toHaveBeenCalled();
108
107
  });
109
108
 
@@ -111,41 +110,41 @@ describe("BaseModel capabilities override", () => {
111
110
  mockDetection(8192);
112
111
 
113
112
  const model = createModel({ other: { capabilities: ["text"] } });
114
- const capabilities = await model.getCapabilities();
113
+ const { input } = await model.toProviderConfig();
115
114
 
116
- expect(capabilities).toEqual(["text"]);
115
+ expect(input).toEqual(["text"]);
117
116
  expect(mockRpc).toHaveBeenCalled();
118
117
  });
119
118
  });
120
119
 
121
120
  // ─── contextSize override ─────────────────────────────────────────────────
122
121
 
123
- describe("BaseModel.getContextSize with contextSize override", () => {
122
+ describe("BaseModel contextSize override (via toProviderConfig)", () => {
124
123
  it("should return the overridden context size instead of the detected one", async () => {
125
124
  mockDetection(8192);
126
125
 
127
126
  const model = createModel({ test: { contextSize: 32768 } });
128
- const ctxSize = await model.getContextSize();
127
+ const { contextWindow } = await model.toProviderConfig();
129
128
 
130
- expect(ctxSize).toBe(32768);
129
+ expect(contextWindow).toBe(32768);
131
130
  });
132
131
 
133
132
  it("should treat a stored 0 as absent and autodetect", async () => {
134
133
  mockDetection(8192);
135
134
 
136
135
  const model = createModel({ test: { contextSize: 0 } });
137
- const ctxSize = await model.getContextSize();
136
+ const { contextWindow } = await model.toProviderConfig();
138
137
 
139
- expect(ctxSize).toBe(8192);
138
+ expect(contextWindow).toBe(8192);
140
139
  });
141
140
 
142
141
  it("should autodetect when no override matches", async () => {
143
142
  mockDetection(8192);
144
143
 
145
144
  const model = createModel({ other: { contextSize: 32768 } });
146
- const ctxSize = await model.getContextSize();
145
+ const { contextWindow } = await model.toProviderConfig();
147
146
 
148
- expect(ctxSize).toBe(8192);
147
+ expect(contextWindow).toBe(8192);
149
148
  });
150
149
  });
151
150
 
@@ -251,11 +250,14 @@ describe("toProviderConfig overrides", () => {
251
250
  });
252
251
  });
253
252
 
254
- // ─── formatOverrideSummary ────────────────────────────────────────────────
253
+ // ─── OverrideEntry.summary ────────────────────────────────────────────────────
254
+
255
+ describe("OverrideSummary.of", () => {
256
+ const summary = (override: ModelOverride): string =>
257
+ OverrideSummary.of(override);
255
258
 
256
- describe("formatOverrideSummary", () => {
257
259
  it("should render every configured field", () => {
258
- const summary = formatOverrideSummary({
260
+ const rendered = summary({
259
261
  cost: { input: 0.2, output: 0.6, cacheRead: 0.01 },
260
262
  capabilities: ["text", "image"],
261
263
  reasoning: false,
@@ -263,31 +265,31 @@ describe("formatOverrideSummary", () => {
263
265
  maxTokens: 4096,
264
266
  });
265
267
 
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");
268
+ expect(rendered).toContain("input: $0.2");
269
+ expect(rendered).toContain("output: $0.6");
270
+ expect(rendered).toContain("cache read: $0.01");
271
+ expect(rendered).toContain("capabilities: text,image");
272
+ expect(rendered).toContain("reasoning: false");
273
+ expect(rendered).toContain("contextSize: 32768");
274
+ expect(rendered).toContain("maxTokens: 4096");
273
275
  });
274
276
 
275
277
  it("should omit zero, undefined and empty fields", () => {
276
- const summary = formatOverrideSummary({
278
+ const rendered = summary({
277
279
  cost: { input: 0, output: 0.6 },
278
280
  reasoning: undefined,
279
281
  });
280
282
 
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");
283
+ expect(rendered).not.toContain("input:");
284
+ expect(rendered).toContain("output: $0.6");
285
+ expect(rendered).not.toContain("capabilities");
286
+ expect(rendered).not.toContain("reasoning");
287
+ expect(rendered).not.toContain("contextSize");
288
+ expect(rendered).not.toContain("maxTokens");
287
289
  });
288
290
 
289
291
  it("should render an em dash for an empty override", () => {
290
- expect(formatOverrideSummary({})).toBe("—");
292
+ expect(summary({})).toBe("—");
291
293
  });
292
294
  });
293
295
 
@@ -299,86 +301,107 @@ describe("override entry list helpers", () => {
299
301
  { url: "http://b" },
300
302
  ] as LlamaServer[];
301
303
 
302
- it("addOverrideEntry should append a pattern → override entry", () => {
303
- const next = addOverrideEntry(servers, 0, "a-2", { contextSize: 4096 });
304
+ it("addEntry should append a pattern → override entry", () => {
305
+ const next = new OverrideEntryMutator(servers, 0).addEntry("a-2", {
306
+ contextSize: 4096,
307
+ });
304
308
  expect(next[0].overrides).toEqual({
305
309
  "a-1": { reasoning: false },
306
310
  "a-2": { contextSize: 4096 },
307
311
  });
308
312
  });
309
313
 
310
- it("updateOverrideEntry should replace in place without reordering", () => {
311
- const next = updateOverrideEntry(servers, 0, 0, "a-renamed", {
312
- reasoning: true,
314
+ it("pattern renames should replace in place without reordering", () => {
315
+ const mutator = new OverrideEntryMutator(servers, 0);
316
+ const result = mutator.applyFieldChange(0, "pattern", "a-renamed");
317
+ expect(result).not.toBeNull();
318
+ expect(Object.keys(result!.next[0].overrides!)).toEqual(["a-renamed"]);
319
+ expect(result!.next[0].overrides!["a-renamed"]).toEqual({
320
+ reasoning: false,
313
321
  });
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]);
322
+ expect(result!.next[1]).toBe(servers[1]);
317
323
  });
318
324
 
319
- it("removeOverrideEntry should drop only the targeted entry", () => {
320
- const withTwo = addOverrideEntry(servers, 0, "a-2", {});
321
- const next = removeOverrideEntry(withTwo, 0, 0);
325
+ it("removeEntry should drop only the targeted entry", () => {
326
+ const withTwo = new OverrideEntryMutator(servers, 0).addEntry("a-2");
327
+ const next = new OverrideEntryMutator(withTwo, 0).removeEntry(0);
322
328
  expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
323
329
  });
324
330
 
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();
331
+ it("CostField.validate should accept blank, non-negative numbers; reject the rest", () => {
332
+ const cost = OverrideFields.byId("cost.input");
333
+ expect(cost.validate("")).toBe("0");
334
+ expect(cost.validate(" 0.5 ")).toBe("0.5");
335
+ expect(cost.validate("-1")).toBeNull();
336
+ expect(cost.validate("abc")).toBeNull();
337
+ expect(cost.validate("Infinity")).toBeNull();
331
338
  });
332
339
 
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 });
340
+ it("CostField.apply should set a positive value, keeping other fields", () => {
341
+ const override: ModelOverride = { cost: { input: 0.2 } };
342
+ OverrideFields.byId("cost.output").apply(override, "0.6");
343
+ expect(override.cost).toEqual({ input: 0.2, output: 0.6 });
336
344
  });
337
345
 
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 });
346
+ it("CostField.apply should remove the field on zero", () => {
347
+ const override: ModelOverride = { cost: { input: 0.2, output: 0.6 } };
348
+ OverrideFields.byId("cost.input").apply(override, "0");
349
+ expect(override.cost).toEqual({ output: 0.6 });
341
350
  });
342
351
 
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();
352
+ it("CostField.apply should drop the whole cost object when the last field is zeroed", () => {
353
+ const single: ModelOverride = { cost: { input: 0.2 } };
354
+ OverrideFields.byId("cost.input").apply(single, "0");
355
+ expect(single.cost).toBeUndefined();
356
+
357
+ const empty: ModelOverride = {};
358
+ OverrideFields.byId("cost.input").apply(empty, "0");
359
+ expect(empty.cost).toBeUndefined();
346
360
  });
347
361
 
348
- it("applyCostFieldValue should treat an empty override as unset cost", () => {
349
- expect(applyCostFieldValue(undefined, "input", 0.5)).toEqual({
350
- input: 0.5,
351
- });
362
+ it("CostField.apply should treat an empty override as unset cost", () => {
363
+ const override: ModelOverride = {};
364
+ OverrideFields.byId("cost.input").apply(override, "0.5");
365
+ expect(override.cost).toEqual({ input: 0.5 });
352
366
  });
353
367
  });
354
368
 
355
- describe("overrideFieldValue", () => {
369
+ describe("OverrideField.displayValue", () => {
370
+ const display = (id: string, override: ModelOverride): string =>
371
+ OverrideFields.byId(id).displayValue(new OverrideEntry("gpt-*", override));
372
+
356
373
  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");
374
+ expect(display("cost.input", {})).toBe("0");
375
+ expect(display("cost.input", { cost: { input: 0.2 } })).toBe("0.2");
376
+ expect(display("cost.cacheWrite", {})).toBe("0");
362
377
  });
363
378
 
364
379
  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");
380
+ expect(display("capabilities", {})).toBe("text");
381
+ expect(display("capabilities", { capabilities: ["text", "image"] })).toBe(
382
+ "text | image",
383
+ );
384
+ expect(display("reasoning", {})).toBe("true");
385
+ expect(display("reasoning", { reasoning: false })).toBe("false");
371
386
  });
372
387
 
373
388
  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
- );
389
+ expect(display("maxTokens", {})).toBe("0");
390
+ expect(display("maxTokens", { maxTokens: 4096 })).toBe("4096");
391
+ expect(display("contextSize", { contextSize: 8192 })).toBe("8192");
379
392
  });
380
393
 
381
- it("returns an empty label for unknown fields", () => {
382
- expect(overrideFieldValue("other", {})).toBe("");
394
+ it("returns the entry's pattern for the pattern field", () => {
395
+ expect(
396
+ OverrideFields.byId("pattern").displayValue(
397
+ new OverrideEntry("gpt-*", {}),
398
+ ),
399
+ ).toBe("gpt-*");
400
+ });
401
+
402
+ it("throws for unknown fields", () => {
403
+ expect(() => OverrideFields.byId("other")).toThrow(
404
+ "Unknown override field: other",
405
+ );
383
406
  });
384
407
  });