auto-model-router 0.2.21 → 0.2.23

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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Verifies the embed port contract that omp's model resolution depends on.
3
+ *
4
+ * omp resolves `modelRoles.default` from `models.yml` during startup — BEFORE
5
+ * extensions load, so before the router can bind and rewrite that file. With an
6
+ * ephemeral port the block therefore names the PREVIOUS session's port, which
7
+ * is dead once that session exits: every main-agent turn fails with "Unable to
8
+ * connect" while utility calls (resolved later, from the live registration)
9
+ * still work. This asserts the properties that make that impossible:
10
+ *
11
+ * 1. The bind port is deterministic across restarts (configured port wins).
12
+ * 2. A second session finds the first one healthy, so it can REUSE it.
13
+ * 3. A port held by something that is not our router is not mistaken for one,
14
+ * and binding falls back instead of leaving the session with no provider.
15
+ *
16
+ * Run: bun tools/verify-embed-port.ts
17
+ */
18
+ import { mkdtempSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+
22
+ import { probeEmbed, resolveEmbedPort } from "../omp-extension/embed-logic.ts";
23
+ import { loadConfig } from "../src/config/load.ts";
24
+ import { startServer, type StartedServer } from "../src/server/http.ts";
25
+ import type { RouterConfig } from "../src/config/types.ts";
26
+
27
+ let failures = 0;
28
+ const check = (label: string, ok: boolean, detail?: unknown): void => {
29
+ console.log(` ${ok ? "PASS" : "FAIL"} ${label}${ok || detail === undefined ? "" : `\n ${JSON.stringify(detail)}`}`);
30
+ if (!ok) failures++;
31
+ };
32
+
33
+ const home = mkdtempSync(join(tmpdir(), "verify-embed-port-"));
34
+ function baseCfg(): RouterConfig {
35
+ const cfg = loadConfig({ overrides: { server: { host: "127.0.0.1" } } });
36
+ cfg.ledger.path = join(home, "router.db");
37
+ cfg.logLevel = "error";
38
+ cfg.benchmarks.enabled = false;
39
+ cfg.context.enabled = false;
40
+ return cfg;
41
+ }
42
+
43
+ // 1. Determinism: two independent startups must choose the same port.
44
+ const cfg1 = baseCfg();
45
+ const configured = cfg1.server.port;
46
+ const portA = resolveEmbedPort(undefined, configured);
47
+ const portB = resolveEmbedPort(undefined, baseCfg().server.port);
48
+ check("bind port is deterministic across restarts", portA === portB && portA !== 0, { portA, portB });
49
+ check("deterministic port comes from config.yml server.port", portA === configured, { portA, configured });
50
+ check("an explicit env port still wins", resolveEmbedPort("8812", configured) === 8812);
51
+ check("an explicit env 0 still requests an ephemeral port", resolveEmbedPort("0", configured) === 0);
52
+
53
+ // 2. First session binds it; a second must see it healthy (=> reuse, no bind war).
54
+ cfg1.server.port = portA;
55
+ let first: StartedServer | null = null;
56
+ try {
57
+ first = startServer(cfg1);
58
+ } catch (err) {
59
+ check("first session can bind the deterministic port", false, String(err));
60
+ }
61
+
62
+ if (first !== null) {
63
+ check("first session bound the deterministic port", first.server.port === portA, { bound: first.server.port, wanted: portA });
64
+ check("router answers /health there (so a peer session reuses it)", await probeEmbed(portA), { port: portA });
65
+
66
+ // 3. A non-router occupant must not be mistaken for our router.
67
+ const squatter = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("not a router", { status: 404 }) });
68
+ const squatted = squatter.port as number;
69
+ check("a non-router occupant fails the health probe", (await probeEmbed(squatted)) === false, { port: squatted });
70
+
71
+ // ...and binding over it must still leave the session with a usable router.
72
+ const cfgFallback = baseCfg();
73
+ cfgFallback.server.port = squatted;
74
+ let fell: StartedServer | null = null;
75
+ try {
76
+ fell = startServer(cfgFallback);
77
+ check("bind on an occupied port yields a different, usable port", fell.server.port !== squatted, { got: fell.server.port });
78
+ } catch {
79
+ cfgFallback.server.port = 0;
80
+ fell = startServer(cfgFallback);
81
+ check("falls back to an ephemeral port when the desired one is taken", fell.server.port !== undefined && fell.server.port !== squatted, {
82
+ got: fell.server.port,
83
+ });
84
+ }
85
+ if (fell !== null) await fell.stop();
86
+ squatter.stop(true);
87
+ await first.stop();
88
+ }
89
+
90
+ console.log(failures === 0 ? "\nAll checks passed." : `\n${failures} check(s) FAILED.`);
91
+ process.exit(failures === 0 ? 0 : 1);
@@ -0,0 +1,127 @@
1
+ /**
2
+ * End-to-end verification of compaction plan stability.
3
+ *
4
+ * Two properties, both measured against a real router process over a growing
5
+ * agentic conversation:
6
+ *
7
+ * 1. STABILITY — an edit applied on one turn is re-applied byte-identically on
8
+ * every later turn. Any change to already-sent bytes invalidates the
9
+ * upstream prompt cache from that message onward.
10
+ * 2. CHURN — how many turns change the plan at all. Each change is a cache
11
+ * invalidation; live ledger data puts a changed-plan turn at 15.4% cold vs
12
+ * 8.9% when the plan holds, and a cold prompt costs 4.34x a warm one per
13
+ * token. `compaction.floorRatio` trades a little extra elision for far
14
+ * fewer changes.
15
+ *
16
+ * Run: bun tools/verify-plan-persist.ts [floorRatio]
17
+ */
18
+ import { mkdtempSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+
22
+ import { loadConfig } from "../src/config/load.ts";
23
+ import { startServer } from "../src/server/http.ts";
24
+ import { startMockOpenRouter } from "./mock-openrouter.ts";
25
+
26
+ const floorRatio = Number.parseFloat(process.argv[2] ?? "0.75");
27
+ const home = mkdtempSync(join(tmpdir(), "verify-plan-persist-"));
28
+ const mock = await startMockOpenRouter("test/fixtures/openrouter-models.json");
29
+
30
+ const cfg = loadConfig({});
31
+ cfg.server = { host: "127.0.0.1", port: 0 };
32
+ cfg.openrouter.baseUrl = `${mock.url}/api/v1`;
33
+ cfg.openrouter.apiKey = "sk-mock";
34
+ cfg.ledger.path = join(home, "router.db");
35
+ cfg.logLevel = "error";
36
+ cfg.classifier.ambiguityThreshold = 0;
37
+ cfg.benchmarks.enabled = false;
38
+ cfg.context.enabled = false;
39
+ // Scaled-down budget so the fixture behaves like a 40k-budget real conversation.
40
+ cfg.compaction.enabled = true;
41
+ cfg.compaction.budgetTokens = 1_500;
42
+ cfg.compaction.floorRatio = floorRatio;
43
+ cfg.compaction.maxToolResultBytes = 256;
44
+ cfg.compaction.keepHeadBytes = 16;
45
+ cfg.compaction.keepTailBytes = 16;
46
+
47
+ const app = startServer(cfg);
48
+ const base = `http://127.0.0.1:${app.server.port}`;
49
+
50
+ const big = (marker: string): string => `${marker}: ${"payload ".repeat(60)}`;
51
+ const call = (id: string, name: string, args: unknown): unknown => ({
52
+ role: "assistant",
53
+ content: null,
54
+ tool_calls: [{ id, type: "function", function: { name, arguments: JSON.stringify(args) } }],
55
+ });
56
+ const result = (id: string, content: string): unknown => ({ role: "tool", tool_call_id: id, content });
57
+ const cycle = (n: number): unknown[] => [
58
+ call(`c${n}`, "read", { path: `src/file${n}.ts` }),
59
+ result(`c${n}`, big(`READ${n}`)),
60
+ { role: "assistant", content: `read file${n}` },
61
+ ];
62
+
63
+ async function dispatch(messages: unknown[]): Promise<{ role: string; content: unknown }[]> {
64
+ const res = await fetch(`${base}/v1/chat/completions`, {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json" },
67
+ body: JSON.stringify({ model: "auto", messages, stream: true }),
68
+ });
69
+ await res.text();
70
+ const body = mock.requests.at(-1)?.body as Record<string, unknown>;
71
+ return body.messages as { role: string; content: unknown }[];
72
+ }
73
+
74
+ const fail = (label: string, detail?: unknown): never => {
75
+ console.error(`FAIL ${label}`, detail === undefined ? "" : JSON.stringify(detail).slice(0, 500));
76
+ process.exit(1);
77
+ };
78
+ const shrunkOf = (msgs: { content: unknown }[]): Map<number, string> => {
79
+ const out = new Map<number, string>();
80
+ msgs.forEach((m, i) => {
81
+ if (typeof m.content === "string" && m.content.includes("omp-router: elided")) out.set(i, m.content);
82
+ });
83
+ return out;
84
+ };
85
+
86
+ // A 20-cycle conversation, dispatched turn by turn exactly as omp would: the
87
+ // full history every time, one cycle longer each turn.
88
+ const TURNS = 20;
89
+ let history: unknown[] = [{ role: "user", content: "audit the project" }];
90
+ let prev = new Map<number, string>();
91
+ let changes = 0;
92
+ let firstPlanTurn = 0;
93
+
94
+ for (let n = 1; n <= TURNS; n++) {
95
+ history = [...history, ...cycle(n)];
96
+ const msgs = await dispatch(history);
97
+ const shrunk = shrunkOf(msgs);
98
+
99
+ // STABILITY: every previously-shrunk message must still be shrunk, with the
100
+ // same bytes. A dropped or altered edit rewrites the cached prefix.
101
+ for (const [i, content] of prev) {
102
+ const now = shrunk.get(i);
103
+ if (now === undefined) fail(`turn ${n}: edit at message ${i} was DROPPED (bytes re-inflated)`, { turn: n, i });
104
+ if (now !== content) fail(`turn ${n}: edit at message ${i} changed bytes`, { was: content.slice(0, 90), now: now.slice(0, 90) });
105
+ }
106
+
107
+ const added = [...shrunk.keys()].filter((i) => !prev.has(i));
108
+ if (added.length > 0) {
109
+ changes++;
110
+ if (firstPlanTurn === 0) firstPlanTurn = n;
111
+ console.log(`turn ${String(n).padStart(2)}: plan CHANGED (+${added.length} edits, ${shrunk.size} total)`);
112
+ } else if (shrunk.size > 0) {
113
+ console.log(`turn ${String(n).padStart(2)}: plan held (${shrunk.size} edits)`);
114
+ } else {
115
+ console.log(`turn ${String(n).padStart(2)}: no compaction`);
116
+ }
117
+ prev = shrunk;
118
+ }
119
+
120
+ const planningTurns = TURNS - firstPlanTurn + 1;
121
+ console.log(`\nfloorRatio ${floorRatio}`);
122
+ console.log(`PASS stability: no edit was ever dropped or rewritten across ${TURNS} turns`);
123
+ console.log(`plan changes: ${changes} over ${planningTurns} compacting turns (${((changes / planningTurns) * 100).toFixed(0)}% of turns invalidate cache)`);
124
+
125
+ app.stop(true);
126
+ await mock.stop();
127
+ process.exit(0);