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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shim's schema against the SQLite bootstrap's.
|
|
3
|
+
*
|
|
4
|
+
* `util/schema.ts` declares the FINAL shape of every table; `util/sqlite.ts`
|
|
5
|
+
* reaches the same shape by replaying nineteen migrations. They are written
|
|
6
|
+
* twice, so they can drift — and drift here is silent: a missing column makes
|
|
7
|
+
* the real owner's statements fail only when that code path runs, which is how
|
|
8
|
+
* a live server refused to start earlier in this port.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { migrateStore, STORE_TABLES } from "../src/util/schema.ts";
|
|
16
|
+
import { openSqlDb } from "../src/util/sql.ts";
|
|
17
|
+
import { openDb } from "../src/util/sqlite.ts";
|
|
18
|
+
|
|
19
|
+
const PG = process.env.AMR_ROUTER_TEST_PG;
|
|
20
|
+
|
|
21
|
+
/** Column names per table, as the engine reports them. */
|
|
22
|
+
async function columnsOf(url: string): Promise<Map<string, Set<string>>> {
|
|
23
|
+
const db = openSqlDb(url);
|
|
24
|
+
const out = new Map<string, Set<string>>();
|
|
25
|
+
try {
|
|
26
|
+
for (const table of STORE_TABLES) {
|
|
27
|
+
const rows =
|
|
28
|
+
db.dialect === "postgres"
|
|
29
|
+
? await db.query<{ name: string }>(
|
|
30
|
+
"SELECT column_name AS name FROM information_schema.columns WHERE table_name = $t",
|
|
31
|
+
{ t: table },
|
|
32
|
+
)
|
|
33
|
+
: await db.query<{ name: string }>(`SELECT name FROM pragma_table_info(${JSON.stringify(table)})`);
|
|
34
|
+
out.set(table, new Set(rows.map((r) => r.name)));
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await db.close();
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("store schema", () => {
|
|
43
|
+
test("the shim creates every table the sqlite bootstrap does, with the same columns", async () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "amr-schema-"));
|
|
45
|
+
try {
|
|
46
|
+
// The bootstrap's result, after all nineteen migrations.
|
|
47
|
+
const legacyPath = join(dir, "legacy.db");
|
|
48
|
+
openDb(legacyPath).close();
|
|
49
|
+
const legacy = await columnsOf(legacyPath);
|
|
50
|
+
|
|
51
|
+
// The shim's result on an empty store.
|
|
52
|
+
const shimPath = join(dir, "shim.db");
|
|
53
|
+
const shim = openSqlDb(shimPath);
|
|
54
|
+
await migrateStore(shim);
|
|
55
|
+
await shim.close();
|
|
56
|
+
const fresh = await columnsOf(shimPath);
|
|
57
|
+
|
|
58
|
+
for (const table of STORE_TABLES) {
|
|
59
|
+
const expected = legacy.get(table) ?? new Set<string>();
|
|
60
|
+
expect(expected.size, `${table} missing from the sqlite bootstrap`).toBeGreaterThan(0);
|
|
61
|
+
const actual = fresh.get(table) ?? new Set<string>();
|
|
62
|
+
// Sorted lists, so a failure names the column rather than the sizes.
|
|
63
|
+
expect([...actual].sort(), table).toEqual([...expected].sort());
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
try {
|
|
67
|
+
rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
} catch {
|
|
69
|
+
/* Windows may hold the WAL briefly */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("it is idempotent: running it twice changes nothing", async () => {
|
|
75
|
+
const dir = mkdtempSync(join(tmpdir(), "amr-schema-idem-"));
|
|
76
|
+
try {
|
|
77
|
+
const path = join(dir, "twice.db");
|
|
78
|
+
const db = openSqlDb(path);
|
|
79
|
+
await migrateStore(db);
|
|
80
|
+
await migrateStore(db);
|
|
81
|
+
await db.close();
|
|
82
|
+
const after = await columnsOf(path);
|
|
83
|
+
expect(after.get("ledger")?.has("redactions")).toBe(true);
|
|
84
|
+
expect(after.get("conversations")?.has("cache_warm_slug")).toBe(true);
|
|
85
|
+
} finally {
|
|
86
|
+
try {
|
|
87
|
+
rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
} catch {
|
|
89
|
+
/* as above */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test.skipIf(PG === undefined)("it applies to postgres, twice, with the same columns as sqlite", async () => {
|
|
95
|
+
const pg = openSqlDb(PG as string);
|
|
96
|
+
try {
|
|
97
|
+
for (const table of [...STORE_TABLES].reverse()) await pg.sql.unsafe(`DROP TABLE IF EXISTS ${table}`);
|
|
98
|
+
await migrateStore(pg);
|
|
99
|
+
await migrateStore(pg);
|
|
100
|
+
} finally {
|
|
101
|
+
await pg.close();
|
|
102
|
+
}
|
|
103
|
+
const dir = mkdtempSync(join(tmpdir(), "amr-schema-pg-"));
|
|
104
|
+
try {
|
|
105
|
+
const litePath = join(dir, "lite.db");
|
|
106
|
+
const lite = openSqlDb(litePath);
|
|
107
|
+
await migrateStore(lite);
|
|
108
|
+
await lite.close();
|
|
109
|
+
|
|
110
|
+
const onPg = await columnsOf(PG as string);
|
|
111
|
+
const onLite = await columnsOf(litePath);
|
|
112
|
+
for (const table of STORE_TABLES) {
|
|
113
|
+
expect([...(onPg.get(table) ?? [])].sort(), table).toEqual([...(onLite.get(table) ?? [])].sort());
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
try {
|
|
117
|
+
rmSync(dir, { recursive: true, force: true });
|
|
118
|
+
} catch {
|
|
119
|
+
/* as above */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
package/test/scope.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ORIGIN_ENV, SCOPE_ENV, acceptOrigin, acceptScope, isOrigin, isScopeSlug
|
|
|
3
3
|
import { parseChatRequest } from "../src/wire/openai/request.ts";
|
|
4
4
|
|
|
5
5
|
describe("the scope a request may name", () => {
|
|
6
|
-
test("a slug passes; the env-var sentinel and other non-slugs do not", () => {
|
|
6
|
+
test("a slug passes; the env-var sentinel and other non-slugs do not", async () => {
|
|
7
7
|
expect(isScopeSlug("omp-router")).toBe(true);
|
|
8
8
|
expect(isScopeSlug("ashlands")).toBe(true);
|
|
9
9
|
expect(isScopeSlug("my.app_v2")).toBe(true);
|
|
@@ -16,20 +16,20 @@ describe("the scope a request may name", () => {
|
|
|
16
16
|
expect(isScopeSlug("a".repeat(129))).toBe(false);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("acceptScope trims and rejects", () => {
|
|
19
|
+
test("acceptScope trims and rejects", async () => {
|
|
20
20
|
expect(acceptScope(" omp-router ")).toBe("omp-router");
|
|
21
21
|
expect(acceptScope(SCOPE_ENV)).toBe("");
|
|
22
22
|
expect(acceptScope(null)).toBe("");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("the wire parser drops a non-slug X-Agentdox-Scope", () => {
|
|
25
|
+
test("the wire parser drops a non-slug X-Agentdox-Scope", async () => {
|
|
26
26
|
const body = { model: "auto", messages: [{ role: "user", content: "hi" }] };
|
|
27
27
|
const parse = (scope: string) => parseChatRequest(body, new Headers({ "x-agentdox-scope": scope }));
|
|
28
28
|
expect(parse("omp-router").agentdoxScope).toBe("omp-router");
|
|
29
29
|
expect(parse(SCOPE_ENV).agentdoxScope).toBe("");
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test("the wire parser reads X-Agentdox-Group and X-Agentdox-Personal by the same rule", () => {
|
|
32
|
+
test("the wire parser reads X-Agentdox-Group and X-Agentdox-Personal by the same rule", async () => {
|
|
33
33
|
// A team front door names the group-context and personal scopes; a
|
|
34
34
|
// lone router never sees the headers and must end up with empties, so
|
|
35
35
|
// nothing new is sent to agentdox.
|
|
@@ -51,7 +51,7 @@ describe("the scope a request may name", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
describe("the origin a request may name", () => {
|
|
54
|
-
test("normalizeOrigin reduces every remote form to host/path, and a local path to nothing", () => {
|
|
54
|
+
test("normalizeOrigin reduces every remote form to host/path, and a local path to nothing", async () => {
|
|
55
55
|
// The same repository over https and ssh is one fingerprint.
|
|
56
56
|
expect(normalizeOrigin("https://github.com/DrewAppling/omp-router.git")).toBe("github.com/drewappling/omp-router");
|
|
57
57
|
expect(normalizeOrigin("git@github.com:drewappling/omp-router.git")).toBe("github.com/drewappling/omp-router");
|
|
@@ -70,7 +70,7 @@ describe("the origin a request may name", () => {
|
|
|
70
70
|
expect(normalizeOrigin("")).toBe("");
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
test("isOrigin: host/path only; the env-var sentinel never passes", () => {
|
|
73
|
+
test("isOrigin: host/path only; the env-var sentinel never passes", async () => {
|
|
74
74
|
expect(isOrigin("github.com/drewappling/omp-router")).toBe(true);
|
|
75
75
|
expect(isOrigin("gitlab.example.com/team/sub/api")).toBe(true);
|
|
76
76
|
// omp sends the header's literal value when the variable it names is
|
|
@@ -83,14 +83,14 @@ describe("the origin a request may name", () => {
|
|
|
83
83
|
expect(isOrigin("")).toBe(false);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("acceptOrigin trims and rejects", () => {
|
|
86
|
+
test("acceptOrigin trims and rejects", async () => {
|
|
87
87
|
expect(acceptOrigin(" github.com/a/b ")).toBe("github.com/a/b");
|
|
88
88
|
expect(acceptOrigin(ORIGIN_ENV)).toBe("");
|
|
89
89
|
expect(acceptOrigin("https://github.com/a/b")).toBe("");
|
|
90
90
|
expect(acceptOrigin(null)).toBe("");
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test("the wire parser reads X-Agentdox-Origin by that rule and nothing else changes", () => {
|
|
93
|
+
test("the wire parser reads X-Agentdox-Origin by that rule and nothing else changes", async () => {
|
|
94
94
|
const body = { model: "auto", messages: [{ role: "user", content: "hi" }] };
|
|
95
95
|
const parse = (h: Record<string, string>) => parseChatRequest(body, new Headers(h));
|
|
96
96
|
expect(parse({ "x-agentdox-scope": "omp-router" }).agentdoxOrigin).toBe("");
|