auto-model-router 0.30.3 → 0.32.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 (112) hide show
  1. package/.omp-plugin/marketplace.json +2 -2
  2. package/README.md +32 -2
  3. package/omp-extension/router-configure.ts +9 -7
  4. package/package.json +1 -1
  5. package/src/cli/config-cmd.ts +8 -7
  6. package/src/cli/explain.ts +10 -5
  7. package/src/cli/export.ts +6 -5
  8. package/src/cli/models.ts +10 -7
  9. package/src/cli/report.ts +6 -1
  10. package/src/cli/stats.ts +7 -7
  11. package/src/config/load.ts +10 -1
  12. package/src/config/types.ts +10 -1
  13. package/src/context/bridge.ts +7 -7
  14. package/src/context/index.ts +3 -3
  15. package/src/context/store.ts +39 -56
  16. package/src/context/types.ts +7 -6
  17. package/src/cost/blended.ts +28 -7
  18. package/src/cost/feedback.ts +33 -37
  19. package/src/cost/ledger-sql.ts +547 -0
  20. package/src/cost/ledger.ts +30 -459
  21. package/src/cost/report.ts +171 -129
  22. package/src/cost/retention.ts +10 -10
  23. package/src/cost/summary.ts +15 -10
  24. package/src/cost/types.ts +43 -62
  25. package/src/cost/views.ts +79 -49
  26. package/src/eval/calibrate.ts +47 -12
  27. package/src/eval/run.ts +18 -2
  28. package/src/lib.ts +6 -2
  29. package/src/router/candidates.ts +7 -15
  30. package/src/router/classify.ts +6 -4
  31. package/src/router/index.ts +95 -9
  32. package/src/router/select.ts +38 -21
  33. package/src/router/state.ts +90 -102
  34. package/src/router/types.ts +11 -5
  35. package/src/server/advise.ts +6 -4
  36. package/src/server/compaction-digest.ts +1 -1
  37. package/src/server/digest.ts +9 -10
  38. package/src/server/http.ts +109 -46
  39. package/src/server/providers.ts +18 -4
  40. package/src/server/turn.ts +32 -9
  41. package/src/tokens/estimate.ts +16 -6
  42. package/src/upstream/ollama-usage.ts +21 -11
  43. package/src/util/schema.ts +201 -0
  44. package/src/util/sql.ts +246 -0
  45. package/src/wire/anthropic/messages.ts +3 -4
  46. package/src/wire/openai/request.ts +1 -0
  47. package/src/wire/types.ts +7 -0
  48. package/test/anthropic-wire.test.ts +9 -9
  49. package/test/benchmark-feeds.test.ts +7 -7
  50. package/test/cache-control.test.ts +7 -7
  51. package/test/cache-estimate.test.ts +5 -5
  52. package/test/catalog-view.test.ts +4 -4
  53. package/test/catalog.test.ts +11 -11
  54. package/test/classify.test.ts +24 -24
  55. package/test/compaction.test.ts +20 -20
  56. package/test/config-wizard.test.ts +32 -32
  57. package/test/config.test.ts +10 -10
  58. package/test/connect-harnesses.test.ts +11 -11
  59. package/test/context-bridge.test.ts +40 -30
  60. package/test/context-prune.test.ts +43 -36
  61. package/test/context-query.test.ts +8 -8
  62. package/test/controls.test.ts +54 -27
  63. package/test/cost.test.ts +12 -12
  64. package/test/digest.test.ts +55 -44
  65. package/test/embed-lifecycle.test.ts +5 -5
  66. package/test/embed-logic.test.ts +26 -26
  67. package/test/escalate.test.ts +17 -17
  68. package/test/eval.test.ts +73 -16
  69. package/test/executable.test.ts +6 -6
  70. package/test/exploration.test.ts +19 -20
  71. package/test/failover.test.ts +22 -21
  72. package/test/fakes.ts +105 -0
  73. package/test/features.test.ts +21 -21
  74. package/test/harness-requests.test.ts +3 -3
  75. package/test/harness-switch.test.ts +5 -5
  76. package/test/hold-exploration.test.ts +13 -13
  77. package/test/hot-reload.test.ts +5 -5
  78. package/test/learned.test.ts +5 -5
  79. package/test/ledger-sql.test.ts +342 -0
  80. package/test/mcp-entry.test.ts +5 -5
  81. package/test/migrations.test.ts +28 -22
  82. package/test/models-yml.test.ts +18 -18
  83. package/test/ollama.test.ts +40 -34
  84. package/test/omp-credentials.test.ts +16 -16
  85. package/test/policy.test.ts +3 -3
  86. package/test/reconfigure.test.ts +4 -4
  87. package/test/redaction.test.ts +41 -35
  88. package/test/remote.test.ts +12 -12
  89. package/test/report-logic.test.ts +8 -8
  90. package/test/report.test.ts +95 -87
  91. package/test/retention.test.ts +79 -66
  92. package/test/schema.test.ts +123 -0
  93. package/test/scope.test.ts +8 -8
  94. package/test/select.test.ts +216 -257
  95. package/test/skills.test.ts +3 -3
  96. package/test/sql-shim.test.ts +154 -0
  97. package/test/state.test.ts +43 -36
  98. package/test/summary.test.ts +38 -27
  99. package/test/tier-plan.test.ts +45 -62
  100. package/test/toast-logic.test.ts +31 -31
  101. package/test/tokens.test.ts +95 -80
  102. package/test/trust-attribution.test.ts +217 -187
  103. package/test/trust-window.test.ts +37 -32
  104. package/test/turn.test.ts +55 -23
  105. package/test/upstreams.test.ts +13 -13
  106. package/test/views.test.ts +81 -59
  107. package/test/wire-request.test.ts +17 -17
  108. package/test/wire-responses.test.ts +4 -4
  109. package/tools/agentdox-e2e.ts +5 -2
  110. package/tools/export-benchmarks.ts +5 -5
  111. package/tools/ledger-parity.ts +266 -0
  112. package/tools/replay.ts +16 -8
@@ -47,24 +47,24 @@ async function drive(lines: string[]): Promise<{
47
47
  }
48
48
 
49
49
  describe("getPath / setPath", () => {
50
- test("reads nested and top-level config paths", () => {
50
+ test("reads nested and top-level config paths", async () => {
51
51
  expect(getPath(cfg, "server.port")).toBe(cfg.server.port);
52
52
  expect(getPath(cfg, "logLevel")).toBe(cfg.logLevel);
53
53
  expect(getPath(cfg, "tiers.hard.minQuality")).toBe(cfg.tiers.hard.minQuality);
54
54
  });
55
55
 
56
- test("returns undefined for missing paths without throwing", () => {
56
+ test("returns undefined for missing paths without throwing", async () => {
57
57
  expect(getPath(cfg, "nope.missing.deep")).toBeUndefined();
58
58
  expect(getPath(null, "a.b")).toBeUndefined();
59
59
  });
60
60
 
61
- test("creates intermediate objects", () => {
61
+ test("creates intermediate objects", async () => {
62
62
  const target: Record<string, unknown> = {};
63
63
  setPath(target, "a.b.c", 1);
64
64
  expect(target).toEqual({ a: { b: { c: 1 } } });
65
65
  });
66
66
 
67
- test("replaces a non-object on the path rather than throwing", () => {
67
+ test("replaces a non-object on the path rather than throwing", async () => {
68
68
  const target: Record<string, unknown> = { a: 5 };
69
69
  setPath(target, "a.b", 1);
70
70
  expect(target).toEqual({ a: { b: 1 } });
@@ -78,19 +78,19 @@ describe("validateField", () => {
78
78
  const en: FieldSpec = { path: "e", label: "e", kind: "enum", options: ["a", "b"] };
79
79
  const arr: FieldSpec = { path: "a", label: "a", kind: "stringArray" };
80
80
 
81
- test("accepts in-range numbers", () => {
81
+ test("accepts in-range numbers", async () => {
82
82
  expect(validateField(num, "5")).toEqual({ ok: true, value: 5 });
83
83
  expect(validateField(num, " 0 ")).toEqual({ ok: true, value: 0 });
84
84
  });
85
85
 
86
- test("rejects non-numbers and out-of-range numbers", () => {
86
+ test("rejects non-numbers and out-of-range numbers", async () => {
87
87
  expect(validateField(num, "abc").ok).toBe(false);
88
88
  expect(validateField(num, "-1").ok).toBe(false);
89
89
  expect(validateField(num, "11").ok).toBe(false);
90
90
  expect(validateField(num, "Infinity").ok).toBe(false);
91
91
  });
92
92
 
93
- test("parses both boolean spellings", () => {
93
+ test("parses both boolean spellings", async () => {
94
94
  for (const yes of ["y", "yes", "true", "1", "on", "Y", "TRUE"]) {
95
95
  expect(validateField(bool, yes)).toEqual({ ok: true, value: true });
96
96
  }
@@ -100,23 +100,23 @@ describe("validateField", () => {
100
100
  expect(validateField(bool, "maybe").ok).toBe(false);
101
101
  });
102
102
 
103
- test("enforces enum options", () => {
103
+ test("enforces enum options", async () => {
104
104
  expect(validateField(en, "b")).toEqual({ ok: true, value: "b" });
105
105
  const bad = validateField(en, "z");
106
106
  expect(bad.ok).toBe(false);
107
107
  if (!bad.ok) expect(bad.error).toContain("a, b");
108
108
  });
109
109
 
110
- test("splits and trims string arrays, dropping blanks", () => {
110
+ test("splits and trims string arrays, dropping blanks", async () => {
111
111
  expect(validateField(arr, "x, y ,, z")).toEqual({ ok: true, value: ["x", "y", "z"] });
112
112
  });
113
113
 
114
- test("clear token is allowed only on optional fields", () => {
114
+ test("clear token is allowed only on optional fields", async () => {
115
115
  expect(validateField(opt, CLEAR_TOKEN)).toEqual({ ok: true, value: null });
116
116
  expect(validateField(num, CLEAR_TOKEN).ok).toBe(false);
117
117
  });
118
118
 
119
- test("a restricted string array rejects unknown items", () => {
119
+ test("a restricted string array rejects unknown items", async () => {
120
120
  const tiers: FieldSpec = { path: "escalation.probeTiers", label: "t", kind: "stringArray", options: ["trivial", "simple"] };
121
121
  expect(validateField(tiers, "simple, trivial")).toEqual({ ok: true, value: ["simple", "trivial"] });
122
122
  const bad = validateField(tiers, "simple, hard");
@@ -124,14 +124,14 @@ describe("validateField", () => {
124
124
  if (!bad.ok) expect(bad.error).toContain("hard");
125
125
  });
126
126
 
127
- test("number arrays parse, bound-check and reject non-numbers", () => {
127
+ test("number arrays parse, bound-check and reject non-numbers", async () => {
128
128
  const arms: FieldSpec = { path: "exploration.holdTurns.values", label: "a", kind: "numberArray", min: 1 };
129
129
  expect(validateField(arms, "2, 3,4")).toEqual({ ok: true, value: [2, 3, 4] });
130
130
  expect(validateField(arms, "2, x").ok).toBe(false);
131
131
  expect(validateField(arms, "0, 2").ok).toBe(false);
132
132
  });
133
133
 
134
- test("secrets display as set/unset", () => {
134
+ test("secrets display as set/unset", async () => {
135
135
  const key: FieldSpec = { path: "openrouter.apiKey", label: "k", kind: "string", optional: true, secret: true };
136
136
  expect(displayValue(key, "sk-abc")).toBe("set");
137
137
  expect(displayValue(key, "")).toBe("unset");
@@ -155,13 +155,13 @@ describe("WIZARD_SECTIONS coverage", () => {
155
155
  return out;
156
156
  }
157
157
 
158
- test("every config leaf has a wizard field (so /router can edit all of it)", () => {
158
+ test("every config leaf has a wizard field (so /router can edit all of it)", async () => {
159
159
  const fields = new Set(WIZARD_SECTIONS.flatMap((s) => s.fields.map((f) => f.path)));
160
160
  const missing = leaves(DEFAULT_CONFIG).filter((p) => !fields.has(p));
161
161
  expect(missing).toEqual([]);
162
162
  });
163
163
 
164
- test("every wizard field points at a real config path or a known optional", () => {
164
+ test("every wizard field points at a real config path or a known optional", async () => {
165
165
  // Optional keys absent from DEFAULT_CONFIG still have to be spelled right;
166
166
  // they are listed here so a typo in a new field path fails loudly.
167
167
  const KNOWN_OPTIONAL = new Set([
@@ -179,19 +179,19 @@ describe("WIZARD_SECTIONS coverage", () => {
179
179
  expect(unknown).toEqual([]);
180
180
  });
181
181
 
182
- test("field paths are unique across sections", () => {
182
+ test("field paths are unique across sections", async () => {
183
183
  const all = WIZARD_SECTIONS.flatMap((s) => s.fields.map((f) => f.path));
184
184
  expect(new Set(all).size).toBe(all.length);
185
185
  });
186
186
 
187
- test("secrets are exactly the credential keys", () => {
187
+ test("secrets are exactly the credential keys", async () => {
188
188
  const secrets = WIZARD_SECTIONS.flatMap((s) => s.fields.filter((f) => f.secret === true).map((f) => f.path)).sort();
189
189
  expect(secrets).toEqual(["benchmarks.artificialAnalysisApiKey", "context.token", "ollama.apiKey", "openrouter.apiKey", "server.apiKey"]);
190
190
  });
191
191
  });
192
192
 
193
193
  describe("applyAnswers", () => {
194
- test("nests a flat edit map into a deep partial", () => {
194
+ test("nests a flat edit map into a deep partial", async () => {
195
195
  expect(
196
196
  applyAnswers({
197
197
  "server.port": 9000,
@@ -207,7 +207,7 @@ describe("applyAnswers", () => {
207
207
  });
208
208
 
209
209
  describe("mergeConfigPartial", () => {
210
- test("deep-merges without clobbering sibling keys", () => {
210
+ test("deep-merges without clobbering sibling keys", async () => {
211
211
  const merged = mergeConfigPartial(
212
212
  { server: { host: "127.0.0.1", port: 8788 } },
213
213
  { server: { port: 9000 } },
@@ -215,7 +215,7 @@ describe("mergeConfigPartial", () => {
215
215
  expect(merged).toEqual({ server: { host: "127.0.0.1", port: 9000 } });
216
216
  });
217
217
 
218
- test("a null leaf deletes the key instead of writing null", () => {
218
+ test("a null leaf deletes the key instead of writing null", async () => {
219
219
  const merged = mergeConfigPartial(
220
220
  { budget: { perDayUsd: 1, onExceeded: "reject" } },
221
221
  { budget: { perDayUsd: null } },
@@ -223,12 +223,12 @@ describe("mergeConfigPartial", () => {
223
223
  expect(merged).toEqual({ budget: { onExceeded: "reject" } });
224
224
  });
225
225
 
226
- test("prunes a section left empty by a clear", () => {
226
+ test("prunes a section left empty by a clear", async () => {
227
227
  const merged = mergeConfigPartial({ budget: { perDayUsd: 1 } }, { budget: { perDayUsd: null } });
228
228
  expect(merged).toEqual({});
229
229
  });
230
230
 
231
- test("replaces arrays wholesale rather than merging by index", () => {
231
+ test("replaces arrays wholesale rather than merging by index", async () => {
232
232
  const merged = mergeConfigPartial(
233
233
  { filters: { deny: ["a", "b", "c"] } },
234
234
  { filters: { deny: ["z"] } },
@@ -236,7 +236,7 @@ describe("mergeConfigPartial", () => {
236
236
  expect(merged).toEqual({ filters: { deny: ["z"] } });
237
237
  });
238
238
 
239
- test("does not mutate the base object", () => {
239
+ test("does not mutate the base object", async () => {
240
240
  const base = { server: { port: 8788 } };
241
241
  mergeConfigPartial(base, { server: { port: 1 } });
242
242
  expect(base).toEqual({ server: { port: 8788 } });
@@ -244,7 +244,7 @@ describe("mergeConfigPartial", () => {
244
244
  });
245
245
 
246
246
  describe("formatValue", () => {
247
- test("renders scalars, arrays, and absent values", () => {
247
+ test("renders scalars, arrays, and absent values", async () => {
248
248
  expect(formatValue(0.5)).toBe("0.5");
249
249
  expect(formatValue(true)).toBe("y");
250
250
  expect(formatValue(false)).toBe("n");
@@ -429,7 +429,7 @@ describe("runWizard: profiles", () => {
429
429
  expect(profiles[0]).toMatchObject({ id: "auto", contextWindow: 400000 });
430
430
  });
431
431
 
432
- test("profile edits survive a round-trip through the config file", () => {
432
+ test("profile edits survive a round-trip through the config file", async () => {
433
433
  const dir = mkdtempSync(join(tmpdir(), "ompr-prof-"));
434
434
  try {
435
435
  const target = join(dir, "config.yml");
@@ -448,7 +448,7 @@ describe("runWizard: profiles", () => {
448
448
  });
449
449
 
450
450
  describe("WIZARD_SECTIONS", () => {
451
- test("required fields resolve against the default config", () => {
451
+ test("required fields resolve against the default config", async () => {
452
452
  for (const section of WIZARD_SECTIONS) {
453
453
  for (const field of section.fields) {
454
454
  if (field.optional === true) continue;
@@ -457,12 +457,12 @@ describe("WIZARD_SECTIONS", () => {
457
457
  }
458
458
  });
459
459
 
460
- test("field paths are unique", () => {
460
+ test("field paths are unique", async () => {
461
461
  const paths = WIZARD_SECTIONS.flatMap((s) => s.fields.map((f) => f.path));
462
462
  expect(new Set(paths).size).toBe(paths.length);
463
463
  });
464
464
 
465
- test("every enum field declares its options", () => {
465
+ test("every enum field declares its options", async () => {
466
466
  for (const section of WIZARD_SECTIONS) {
467
467
  for (const field of section.fields) {
468
468
  if (field.kind !== "enum") continue;
@@ -514,7 +514,7 @@ describe("writeRouterConfig", () => {
514
514
  }
515
515
  }
516
516
 
517
- test("creates the file and reloads to the chosen value", () => {
517
+ test("creates the file and reloads to the chosen value", async () => {
518
518
  withTemp((dir) => {
519
519
  const target = join(dir, "config.yml");
520
520
  const backup = writeRouterConfig(target, { server: { port: 9123 } });
@@ -525,7 +525,7 @@ describe("writeRouterConfig", () => {
525
525
  });
526
526
  });
527
527
 
528
- test("preserves unrelated existing keys and backs up the old file", () => {
528
+ test("preserves unrelated existing keys and backs up the old file", async () => {
529
529
  withTemp((dir) => {
530
530
  const target = join(dir, "config.yml");
531
531
  writeFileSync(target, "logLevel: debug\nserver:\n host: 0.0.0.0\n", "utf8");
@@ -541,7 +541,7 @@ describe("writeRouterConfig", () => {
541
541
  });
542
542
  });
543
543
 
544
- test("refuses to write an invalid config and leaves the file untouched", () => {
544
+ test("refuses to write an invalid config and leaves the file untouched", async () => {
545
545
  withTemp((dir) => {
546
546
  const target = join(dir, "config.yml");
547
547
  writeFileSync(target, "logLevel: debug\n", "utf8");
@@ -550,7 +550,7 @@ describe("writeRouterConfig", () => {
550
550
  });
551
551
  });
552
552
 
553
- test("a clear deletes the key from the written file", () => {
553
+ test("a clear deletes the key from the written file", async () => {
554
554
  withTemp((dir) => {
555
555
  const target = join(dir, "config.yml");
556
556
  writeFileSync(target, "budget:\n perDayUsd: 5\n onExceeded: reject\n", "utf8");
@@ -39,7 +39,7 @@ afterEach(() => {
39
39
  });
40
40
 
41
41
  describe("loadConfig", () => {
42
- test("loads defaults with no config file and no API key", () => {
42
+ test("loads defaults with no config file and no API key", async () => {
43
43
  setEnv("OPENROUTER_API_KEY", undefined);
44
44
  setEnv("AUTO_MODEL_ROUTER_HOME", tempDir());
45
45
  setEnv("PI_CODING_AGENT_DIR", tempDir());
@@ -51,14 +51,14 @@ describe("loadConfig", () => {
51
51
  expect(cfg.tiers.hard.minQuality).toBe(DEFAULT_CONFIG.tiers.hard.minQuality);
52
52
  });
53
53
 
54
- test("resolves the ledger path under the router home", () => {
54
+ test("resolves the ledger path under the router home", async () => {
55
55
  const home = tempDir();
56
56
  setEnv("AUTO_MODEL_ROUTER_HOME", home);
57
57
  const cfg = loadConfig({});
58
58
  expect(cfg.ledger.path).toContain(home);
59
59
  });
60
60
 
61
- test("deep-merges nested objects from the config file", () => {
61
+ test("deep-merges nested objects from the config file", async () => {
62
62
  const path = writeConfig("tiers:\n hard:\n minQuality: 88\n");
63
63
  const cfg = loadConfig({ path });
64
64
  expect(cfg.tiers.hard.minQuality).toBe(88);
@@ -67,7 +67,7 @@ describe("loadConfig", () => {
67
67
  expect(cfg.tiers.trivial.minQuality).toBe(DEFAULT_CONFIG.tiers.trivial.minQuality);
68
68
  });
69
69
 
70
- test("replaces arrays wholesale rather than merging them", () => {
70
+ test("replaces arrays wholesale rather than merging them", async () => {
71
71
  // Matches omp's own settings semantics: arrays never union or append.
72
72
  expect(DEFAULT_CONFIG.escalation.probeTiers.length).toBeGreaterThan(1);
73
73
  const path = writeConfig("escalation:\n probeTiers:\n - trivial\n");
@@ -75,7 +75,7 @@ describe("loadConfig", () => {
75
75
  expect(cfg.escalation.probeTiers).toEqual(["trivial"]);
76
76
  });
77
77
 
78
- test("names the offending path when a value is invalid", () => {
78
+ test("names the offending path when a value is invalid", async () => {
79
79
  const path = writeConfig("server:\n port: not-a-number\n");
80
80
  let message = "";
81
81
  try {
@@ -87,33 +87,33 @@ describe("loadConfig", () => {
87
87
  expect(message).toContain("port");
88
88
  });
89
89
 
90
- test("environment variables override file values", () => {
90
+ test("environment variables override file values", async () => {
91
91
  const path = writeConfig("server:\n port: 9001\n");
92
92
  setEnv("AUTO_MODEL_ROUTER_PORT", "9999");
93
93
  const cfg = loadConfig({ path });
94
94
  expect(cfg.server.port).toBe(9999);
95
95
  });
96
96
 
97
- test("explicit overrides beat the environment", () => {
97
+ test("explicit overrides beat the environment", async () => {
98
98
  setEnv("AUTO_MODEL_ROUTER_PORT", "9999");
99
99
  const cfg = loadConfig({ overrides: { server: { host: "127.0.0.1", port: 7777, maxConcurrentTurns: 24 } } });
100
100
  expect(cfg.server.port).toBe(7777);
101
101
  });
102
102
 
103
- test("reads the OpenRouter key from the environment", () => {
103
+ test("reads the OpenRouter key from the environment", async () => {
104
104
  setEnv("OPENROUTER_API_KEY", "sk-or-test-value");
105
105
  const cfg = loadConfig({});
106
106
  expect(cfg.openrouter.apiKey).toBe("sk-or-test-value");
107
107
  });
108
108
 
109
- test("accepts a config that only overrides one scalar", () => {
109
+ test("accepts a config that only overrides one scalar", async () => {
110
110
  const path = writeConfig("logLevel: debug\n");
111
111
  const cfg = loadConfig({ path });
112
112
  expect(cfg.logLevel).toBe("debug");
113
113
  expect(cfg.escalation.enabled).toBe(DEFAULT_CONFIG.escalation.enabled);
114
114
  });
115
115
 
116
- test("an empty config file is valid and changes nothing", () => {
116
+ test("an empty config file is valid and changes nothing", async () => {
117
117
  const path = writeConfig("");
118
118
  const cfg = loadConfig({ path });
119
119
  expect(cfg.server.port).toBe(DEFAULT_CONFIG.server.port);
@@ -55,7 +55,7 @@ const configuredFor = (r: ConnectReport, prefix: string): string | undefined =>
55
55
  const backupsIn = (dir: string): string[] => readdirSync(dir).filter((f) => f.endsWith(".bak"));
56
56
 
57
57
  describe("OpenCode", () => {
58
- test("the provider block is merged into opencode.json, the plugin is copied, and a second run changes nothing", () => {
58
+ test("the provider block is merged into opencode.json, the plugin is copied, and a second run changes nothing", async () => {
59
59
  const { home, o } = bare();
60
60
  const dir = join(home, ".config", "opencode");
61
61
  const cfg = plant(home, "opencode.json", ".config", "opencode", "opencode.json");
@@ -95,7 +95,7 @@ describe("OpenCode", () => {
95
95
  }
96
96
  });
97
97
 
98
- test("--harness opencode selects it alone, and an absent OpenCode is reported as skipped", () => {
98
+ test("--harness opencode selects it alone, and an absent OpenCode is reported as skipped", async () => {
99
99
  const { home, o } = bare({ only: ["opencode"] });
100
100
  plant(home, "opencode.json", ".config", "opencode", "opencode.json");
101
101
  try {
@@ -112,7 +112,7 @@ describe("OpenCode", () => {
112
112
  }
113
113
  });
114
114
 
115
- test("a pinned scope rides in the provider's headers; a file we cannot parse is left alone", () => {
115
+ test("a pinned scope rides in the provider's headers; a file we cannot parse is left alone", async () => {
116
116
  expect(JSON.parse(mergeOpenCodeConfig("", "https://t", "k", "omp-router")!).provider["auto-model-router"].options.headers).toEqual({ "X-Omp-Harness": "opencode", "X-Agentdox-Scope": "omp-router" });
117
117
  expect(mergeOpenCodeConfig("[]", "https://t", "k")).toBeNull();
118
118
  expect(mergeOpenCodeConfig("{ not json", "https://t", "k")).toBeNull();
@@ -123,7 +123,7 @@ describe("OpenCode", () => {
123
123
  describe("Cline", () => {
124
124
  const REL = [".cline", "data", "settings"];
125
125
 
126
- test("the openai-compatible provider is merged into providers.json beside the user's own, and a second run changes nothing", () => {
126
+ test("the openai-compatible provider is merged into providers.json beside the user's own, and a second run changes nothing", async () => {
127
127
  const { home, o } = bare();
128
128
  const dir = join(home, ...REL);
129
129
  const cfg = plant(home, "cline-providers.json", ...REL, "providers.json");
@@ -158,7 +158,7 @@ describe("Cline", () => {
158
158
  }
159
159
  });
160
160
 
161
- test("--harness cline selects it alone, and an absent Cline is reported as skipped", () => {
161
+ test("--harness cline selects it alone, and an absent Cline is reported as skipped", async () => {
162
162
  const { home, o } = bare({ only: ["cline"] });
163
163
  plant(home, "cline-providers.json", ...REL, "providers.json");
164
164
  try {
@@ -175,7 +175,7 @@ describe("Cline", () => {
175
175
  }
176
176
  });
177
177
 
178
- test("the stamp only moves when the settings do, a pinned scope rides along, and an unreadable file is left alone", () => {
178
+ test("the stamp only moves when the settings do, a pinned scope rides along, and an unreadable file is left alone", async () => {
179
179
  const first = mergeClineProviders("", "https://t", "k", "2026-01-01T00:00:00.000Z", "omp-router")!;
180
180
  expect(JSON.parse(first).providers["openai-compatible"].settings.headers).toEqual({ "X-Omp-Harness": "cline", "X-Agentdox-Scope": "omp-router" });
181
181
  expect(mergeClineProviders(first, "https://t", "k", "2027-01-01T00:00:00.000Z", "omp-router")).toBeNull();
@@ -187,7 +187,7 @@ describe("Cline", () => {
187
187
  });
188
188
 
189
189
  describe("Continue", () => {
190
- test("the three profiles become model entries in config.yaml, keeping the file's comments, and a second run changes nothing", () => {
190
+ test("the three profiles become model entries in config.yaml, keeping the file's comments, and a second run changes nothing", async () => {
191
191
  const { home, o } = bare();
192
192
  const dir = join(home, ".continue");
193
193
  const cfg = plant(home, "continue-config.yaml", ".continue", "config.yaml");
@@ -218,7 +218,7 @@ describe("Continue", () => {
218
218
  }
219
219
  });
220
220
 
221
- test("--harness continue selects it alone, and an absent Continue is reported as skipped", () => {
221
+ test("--harness continue selects it alone, and an absent Continue is reported as skipped", async () => {
222
222
  const { home, o } = bare({ only: ["continue"] });
223
223
  plant(home, "continue-config.yaml", ".continue", "config.yaml");
224
224
  try {
@@ -235,7 +235,7 @@ describe("Continue", () => {
235
235
  }
236
236
  });
237
237
 
238
- test("a fresh file gets the schema's required fields first, a re-key replaces entries in place, and broken YAML is left alone", () => {
238
+ test("a fresh file gets the schema's required fields first, a re-key replaces entries in place, and broken YAML is left alone", async () => {
239
239
  const fresh = mergeContinueConfig("", "https://t", "k")!;
240
240
  expect(fresh.startsWith("name: auto-model-router\nversion: 0.0.1\nschema: v1\nmodels:\n - name:")).toBe(true);
241
241
  expect(mergeContinueConfig(fresh, "https://t", "k")).toBeNull();
@@ -249,7 +249,7 @@ describe("Continue", () => {
249
249
  });
250
250
 
251
251
  describe("Cursor and Windsurf keep their provider settings where no file can reach them", () => {
252
- test("connect prints what to set and writes nothing under either home", () => {
252
+ test("connect prints what to set and writes nothing under either home", async () => {
253
253
  const { home, o } = bare({ only: ["cursor", "windsurf"] });
254
254
  mkdirSync(join(home, ".cursor"), { recursive: true });
255
255
  mkdirSync(join(home, ".codeium", "windsurf"), { recursive: true });
@@ -270,7 +270,7 @@ describe("Cursor and Windsurf keep their provider settings where no file can rea
270
270
  }
271
271
  });
272
272
 
273
- test("a loopback router is called out for Cursor, which proxies chat through its own servers", () => {
273
+ test("a loopback router is called out for Cursor, which proxies chat through its own servers", async () => {
274
274
  const { home, o } = bare({ only: ["cursor"], url: "http://127.0.0.1:8788" });
275
275
  mkdirSync(join(home, ".cursor"), { recursive: true });
276
276
  try {