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.
- package/.omp-plugin/marketplace.json +2 -2
- package/README.md +32 -2
- package/omp-extension/router-configure.ts +9 -7
- package/package.json +1 -1
- package/src/cli/config-cmd.ts +8 -7
- package/src/cli/explain.ts +10 -5
- package/src/cli/export.ts +6 -5
- package/src/cli/models.ts +10 -7
- package/src/cli/report.ts +6 -1
- package/src/cli/stats.ts +7 -7
- package/src/config/load.ts +10 -1
- package/src/config/types.ts +10 -1
- package/src/context/bridge.ts +7 -7
- package/src/context/index.ts +3 -3
- package/src/context/store.ts +39 -56
- package/src/context/types.ts +7 -6
- package/src/cost/blended.ts +28 -7
- package/src/cost/feedback.ts +33 -37
- package/src/cost/ledger-sql.ts +547 -0
- package/src/cost/ledger.ts +30 -459
- package/src/cost/report.ts +171 -129
- package/src/cost/retention.ts +10 -10
- package/src/cost/summary.ts +15 -10
- package/src/cost/types.ts +43 -62
- package/src/cost/views.ts +79 -49
- package/src/eval/calibrate.ts +47 -12
- package/src/eval/run.ts +18 -2
- package/src/lib.ts +6 -2
- package/src/router/candidates.ts +7 -15
- package/src/router/classify.ts +6 -4
- package/src/router/index.ts +95 -9
- package/src/router/select.ts +38 -21
- package/src/router/state.ts +90 -102
- package/src/router/types.ts +11 -5
- package/src/server/advise.ts +6 -4
- package/src/server/compaction-digest.ts +1 -1
- package/src/server/digest.ts +9 -10
- package/src/server/http.ts +109 -46
- package/src/server/providers.ts +18 -4
- package/src/server/turn.ts +32 -9
- package/src/tokens/estimate.ts +16 -6
- package/src/upstream/ollama-usage.ts +21 -11
- package/src/util/schema.ts +201 -0
- package/src/util/sql.ts +246 -0
- package/src/wire/anthropic/messages.ts +3 -4
- package/src/wire/openai/request.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/test/anthropic-wire.test.ts +9 -9
- package/test/benchmark-feeds.test.ts +7 -7
- package/test/cache-control.test.ts +7 -7
- package/test/cache-estimate.test.ts +5 -5
- package/test/catalog-view.test.ts +4 -4
- package/test/catalog.test.ts +11 -11
- package/test/classify.test.ts +24 -24
- package/test/compaction.test.ts +20 -20
- package/test/config-wizard.test.ts +32 -32
- package/test/config.test.ts +10 -10
- package/test/connect-harnesses.test.ts +11 -11
- package/test/context-bridge.test.ts +40 -30
- package/test/context-prune.test.ts +43 -36
- package/test/context-query.test.ts +8 -8
- package/test/controls.test.ts +54 -27
- package/test/cost.test.ts +12 -12
- package/test/digest.test.ts +55 -44
- package/test/embed-lifecycle.test.ts +5 -5
- package/test/embed-logic.test.ts +26 -26
- package/test/escalate.test.ts +17 -17
- package/test/eval.test.ts +73 -16
- package/test/executable.test.ts +6 -6
- package/test/exploration.test.ts +19 -20
- package/test/failover.test.ts +22 -21
- package/test/fakes.ts +105 -0
- package/test/features.test.ts +21 -21
- package/test/harness-requests.test.ts +3 -3
- package/test/harness-switch.test.ts +5 -5
- package/test/hold-exploration.test.ts +13 -13
- package/test/hot-reload.test.ts +5 -5
- package/test/learned.test.ts +5 -5
- package/test/ledger-sql.test.ts +342 -0
- package/test/mcp-entry.test.ts +5 -5
- package/test/migrations.test.ts +28 -22
- package/test/models-yml.test.ts +18 -18
- package/test/ollama.test.ts +40 -34
- package/test/omp-credentials.test.ts +16 -16
- package/test/policy.test.ts +3 -3
- package/test/reconfigure.test.ts +4 -4
- package/test/redaction.test.ts +41 -35
- package/test/remote.test.ts +12 -12
- package/test/report-logic.test.ts +8 -8
- package/test/report.test.ts +95 -87
- package/test/retention.test.ts +79 -66
- package/test/schema.test.ts +123 -0
- package/test/scope.test.ts +8 -8
- package/test/select.test.ts +216 -257
- package/test/skills.test.ts +3 -3
- package/test/sql-shim.test.ts +154 -0
- package/test/state.test.ts +43 -36
- package/test/summary.test.ts +38 -27
- package/test/tier-plan.test.ts +45 -62
- package/test/toast-logic.test.ts +31 -31
- package/test/tokens.test.ts +95 -80
- package/test/trust-attribution.test.ts +217 -187
- package/test/trust-window.test.ts +37 -32
- package/test/turn.test.ts +55 -23
- package/test/upstreams.test.ts +13 -13
- package/test/views.test.ts +81 -59
- package/test/wire-request.test.ts +17 -17
- package/test/wire-responses.test.ts +4 -4
- package/tools/agentdox-e2e.ts +5 -2
- package/tools/export-benchmarks.ts +5 -5
- package/tools/ledger-parity.ts +266 -0
- 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");
|
package/test/config.test.ts
CHANGED
|
@@ -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 {
|