auto-model-router 0.36.0 → 0.37.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.
@@ -5,15 +5,21 @@ import {
5
5
  applyFeedScores,
6
6
  fetchBenchlmScores,
7
7
  invalidateFeedCache,
8
+ MAX_EXTRA_SCORES,
9
+ loadLocalScores,
8
10
  normalizeModelKey,
9
11
  parseAaModels,
10
12
  parseBenchlmModels,
11
13
  refreshFeedScores,
14
+ saveLocalScores,
15
+ suppliedScores,
12
16
  type FeedScore,
13
17
  type FetchLike,
14
18
  } from "../src/catalog/benchmark-feeds.ts";
19
+ import { DEFAULT_CONFIG } from "../src/config/defaults.ts";
15
20
  import { loadConfig } from "../src/config/load.ts";
16
21
  import type { RouterConfig } from "../src/config/types.ts";
22
+ import type { Logger } from "../src/util/log.ts";
17
23
  import { openDb } from "../src/util/sqlite.ts";
18
24
 
19
25
  /** A bare OpenRouter `/models` record, optionally pre-scored. */
@@ -38,6 +44,17 @@ function aaScore(over: Partial<FeedScore> & { key: string }): FeedScore {
38
44
  function blScore(over: Partial<FeedScore> & { key: string }): FeedScore {
39
45
  return { creator: "", source: "benchlm", ...over };
40
46
  }
47
+ function localScore(over: Partial<FeedScore> & { key: string }): FeedScore {
48
+ return { creator: "", source: "local", ...over };
49
+ }
50
+ /** A row as the front door supplies it: a neutral leaderboard number. */
51
+ function neutralScore(over: Partial<FeedScore> & { key: string }): FeedScore {
52
+ return { creator: "", source: "neutral", ...over };
53
+ }
54
+ /** A row as the front door supplies it: a self-reported model-card number. */
55
+ function vendorScore(over: Partial<FeedScore> & { key: string }): FeedScore {
56
+ return { creator: "", source: "vendor", ...over };
57
+ }
41
58
 
42
59
  describe("normalizeModelKey", () => {
43
60
  test("strips provider, tilde, and release words but keeps the parameter size", async () => {
@@ -255,3 +272,180 @@ test("fetchBenchlmScores parses a keyless leaderboard response", async () => {
255
272
  const scores = await fetchBenchlmScores({ fetchImpl: fake });
256
273
  expect(scores).toEqual([{ key: "glm-5-3-flash", creator: "z-ai", coding: 61, intelligence: 58, source: "benchlm" }]);
257
274
  });
275
+
276
+ /**
277
+ * `benchmarks.extraScores`: scores the FRONT DOOR supplies for axes the feeds
278
+ * leave empty. The case it was built for is real — `deepseek/deepseek-v4.1-flash`
279
+ * carries intelligence 39.5 and nothing else, so no tier floor above the cheapest
280
+ * can admit it, and it took 1 dispatch in 30 days against its sibling's 1,455.
281
+ * A model OpenRouter serves is defined by OpenRouter's catalog, which the front
282
+ * door can only read; this is the one seam through which it can say more.
283
+ */
284
+ describe("benchmarks.extraScores", () => {
285
+ function cfgWith(extraScores?: FeedScore[]): RouterConfig {
286
+ const base = structuredClone(DEFAULT_CONFIG);
287
+ return {
288
+ ...base,
289
+ logLevel: "silent",
290
+ benchmarks: { ...base.benchmarks, ...(extraScores === undefined ? {} : { extraScores }) },
291
+ };
292
+ }
293
+
294
+ /** Captures whatever the sanitiser decided to say out loud. */
295
+ function recorder(): { log: Logger; warns: { msg: string; fields?: Record<string, unknown> }[] } {
296
+ const warns: { msg: string; fields?: Record<string, unknown> }[] = [];
297
+ const noop = (): void => {};
298
+ const log: Logger = {
299
+ error: noop,
300
+ info: noop,
301
+ debug: noop,
302
+ warn: (msg, fields) => {
303
+ warns.push(fields === undefined ? { msg } : { msg, fields });
304
+ },
305
+ };
306
+ return { log, warns };
307
+ }
308
+
309
+ test("fills the axes the feeds left empty, and says where each came from", async () => {
310
+ // Exactly the live shape: intelligence published, the other two absent.
311
+ const catalog = [raw("deepseek/deepseek-v4.1-flash", { artificial_analysis: { intelligence_index: 39.5 } })];
312
+ const cfg = cfgWith([
313
+ vendorScore({ key: "deepseek-v4-1-flash", creator: "deepseek", coding: 55.2 }),
314
+ neutralScore({ key: "deepseek-v4-1-flash", creator: "deepseek", agentic: 31.8 }),
315
+ ]);
316
+
317
+ const result = applyFeedScores(catalog, suppliedScores(cfg));
318
+
319
+ expect(normalizeCatalogModel(catalog[0])?.quality).toEqual({ intelligence: 39.5, coding: 55.2, agentic: 31.8 });
320
+ // Provenance survives per axis, and the two kinds stay distinguishable.
321
+ expect(catalog[0]?.benchmarks).toMatchObject({ fill_sources: { coding: "vendor", agentic: "neutral" } });
322
+ expect(result.sources.vendor).toBe(1);
323
+ expect(result.sources.neutral).toBe(1);
324
+ });
325
+
326
+ test("never moves a score a published source measured", async () => {
327
+ const catalog = [raw("google/gemini-3.7-flash", { artificial_analysis: { coding_index: 76.1 } })];
328
+ const cfg = cfgWith([neutralScore({ key: "gemini-3-7-flash", creator: "google", coding: 10, agentic: 40 })]);
329
+ const result = applyFeedScores(catalog, suppliedScores(cfg));
330
+ expect(normalizeCatalogModel(catalog[0])?.quality).toEqual({ coding: 76.1, agentic: 40 });
331
+ expect(result.axes.coding).toBe(0);
332
+ });
333
+
334
+ test("loses to Artificial Analysis and BenchLM, beats local, and neutral leads vendor", async () => {
335
+ const catalog = [raw("z-ai/glm-5.3-flash"), raw("meta/muse-glimmer-30b")];
336
+ const cfg = cfgWith([
337
+ // Every one of these is outranked on glm's coding and intelligence.
338
+ neutralScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 10, agentic: 50 }),
339
+ vendorScore({ key: "glm-5-3-flash", creator: "z-ai", intelligence: 9, agentic: 40 }),
340
+ // ...but both outrank local, and on agentic neutral outranks vendor.
341
+ vendorScore({ key: "muse-glimmer-30b", creator: "meta", agentic: 44 }),
342
+ ]);
343
+ const feeds: FeedScore[] = [
344
+ aaScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 61 }),
345
+ blScore({ key: "glm-5-3-flash", creator: "z-ai", intelligence: 58 }),
346
+ localScore({ key: "glm-5-3-flash", creator: "z-ai", agentic: 1 }),
347
+ localScore({ key: "muse-glimmer-30b", creator: "meta", agentic: 33 }),
348
+ ];
349
+
350
+ applyFeedScores(catalog, [...feeds, ...suppliedScores(cfg)]);
351
+
352
+ expect(normalizeCatalogModel(catalog[0])?.quality).toEqual({ coding: 61, intelligence: 58, agentic: 50 });
353
+ expect(catalog[0]?.benchmarks).toMatchObject({
354
+ fill_sources: { coding: "artificial_analysis", intelligence: "benchlm", agentic: "neutral" },
355
+ });
356
+ expect(normalizeCatalogModel(catalog[1])?.quality).toEqual({ agentic: 44 });
357
+ expect(catalog[1]?.benchmarks).toMatchObject({ fill_sources: { agentic: "vendor" } });
358
+ });
359
+
360
+ test("a malformed entry is dropped loudly, and can neither zero a score nor stop the rest", async () => {
361
+ const { log, warns } = recorder();
362
+ const cfg = cfgWith([
363
+ 42 as unknown as FeedScore, // not an object
364
+ { creator: "x", source: "neutral", coding: 70 } as unknown as FeedScore, // no key
365
+ // Config may not claim a fetched feed, nor the lane `useLocalScores` gates.
366
+ aaScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 99 }),
367
+ localScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 98 }),
368
+ // Well-formed row, unusable axes: a string and an out-of-range number.
369
+ { key: "glm-5-3-flash", creator: "z-ai", coding: "61", agentic: -3, source: "neutral" } as unknown as FeedScore,
370
+ neutralScore({ key: "muse-glimmer-30b", creator: "meta", coding: 55 }),
371
+ ]);
372
+
373
+ const kept = suppliedScores(cfg, log);
374
+
375
+ // Four entries thrown away whole; the fifth survives with both axes gone.
376
+ expect(kept).toHaveLength(2);
377
+ expect(kept[0]).toEqual({ key: "glm-5-3-flash", creator: "z-ai", source: "neutral" });
378
+ expect(warns).toHaveLength(1);
379
+ expect(warns[0]?.fields).toMatchObject({ kept: 2, droppedEntries: 4, droppedAxes: 2 });
380
+
381
+ // The unusable axes stay ABSENT, not 0 — an unscored model must satisfy no
382
+ // floor, and a supplied 0 would satisfy `trivial` and bid for every turn.
383
+ const catalog = [raw("z-ai/glm-5.3-flash"), raw("meta/muse-glimmer-30b")];
384
+ applyFeedScores(catalog, kept);
385
+ expect(normalizeCatalogModel(catalog[0])?.quality).toEqual({});
386
+ expect(normalizeCatalogModel(catalog[1])?.quality).toEqual({ coding: 55 });
387
+ });
388
+
389
+ test("a supplied entry cannot claim a feed's rank by claiming its label", async () => {
390
+ const catalog = [raw("z-ai/glm-5.3-flash")];
391
+ // Sent as `artificial_analysis`, which would outrank BenchLM if it survived.
392
+ const cfg = cfgWith([aaScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 99 })]);
393
+ applyFeedScores(catalog, [blScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 58 }), ...suppliedScores(cfg)]);
394
+ expect(normalizeCatalogModel(catalog[0])?.quality.coding).toBe(58);
395
+ });
396
+
397
+ test("nor by being written into a table that does not produce it", async () => {
398
+ const db = openDb(":memory:");
399
+ // Each stored blob only yields the sources that actually write it, so neither
400
+ // lane is a side door into a rank. `local_scores` in particular stays the
401
+ // local lane — the one `benchmarks.useLocalScores` gates.
402
+ saveLocalScores(db, [
403
+ localScore({ key: "muse-glimmer-30b", creator: "meta", coding: 42 }),
404
+ neutralScore({ key: "muse-glimmer-30b", creator: "meta", agentic: 39 }),
405
+ ]);
406
+ expect(loadLocalScores(db)).toEqual([{ key: "muse-glimmer-30b", creator: "meta", coding: 42, source: "local" }]);
407
+
408
+ const cached = [neutralScore({ key: "minimax-m3", coding: 70 }), blScore({ key: "minimax-m3", coding: 58 })];
409
+ db.query("INSERT INTO benchmark_cache (id, payload, fetched_at_ms) VALUES (1, ?, ?)").run(JSON.stringify(cached), 1000);
410
+ const feeds = await refreshFeedScores(cfgWith(), db, { fetchImpl: async () => Response.json({ models: [] }), now: 1500 });
411
+ expect(feeds).toEqual([{ key: "minimax-m3", creator: "", coding: 58, source: "benchlm" }]);
412
+ db.close();
413
+ });
414
+
415
+ test("the key may be the OpenRouter slug; the front door need not reimplement the match", async () => {
416
+ const catalog = [raw("deepseek/deepseek-v4.1-flash")];
417
+ const cfg = cfgWith([{ key: "deepseek/deepseek-v4.1-flash", creator: "DeepSeek", coding: 55.2, source: "vendor" }]);
418
+ expect(suppliedScores(cfg)[0]).toMatchObject({ key: "deepseek-v4-1-flash", creator: "deepseek" });
419
+ applyFeedScores(catalog, suppliedScores(cfg));
420
+ expect(normalizeCatalogModel(catalog[0])?.quality.coding).toBe(55.2);
421
+ });
422
+
423
+ test("past the cap the excess is dropped, loudly, and the rest still apply", async () => {
424
+ const { log, warns } = recorder();
425
+ const many: FeedScore[] = [];
426
+ for (let i = 0; i < MAX_EXTRA_SCORES + 5; i += 1) many.push(neutralScore({ key: `model-${i}`, coding: 50 }));
427
+ const kept = suppliedScores(cfgWith(many), log);
428
+ expect(kept).toHaveLength(MAX_EXTRA_SCORES);
429
+ expect(warns[0]?.fields).toMatchObject({ droppedEntries: 5, overCap: MAX_EXTRA_SCORES });
430
+ });
431
+
432
+ test("an older front door sends no such field and nothing changes", async () => {
433
+ const cfg = cfgWith();
434
+ expect(cfg.benchmarks.extraScores).toBeUndefined();
435
+ expect(suppliedScores(cfg)).toEqual([]);
436
+
437
+ // Byte for byte the same catalog, with the field absent and with it empty.
438
+ const feeds: FeedScore[] = [
439
+ aaScore({ key: "glm-5-3-flash", creator: "z-ai", coding: 61 }),
440
+ blScore({ key: "muse-glimmer-30b", creator: "meta", agentic: 48 }),
441
+ ];
442
+ const withoutField = [raw("z-ai/glm-5.3-flash"), raw("meta/muse-glimmer-30b")];
443
+ const withEmpty = [raw("z-ai/glm-5.3-flash"), raw("meta/muse-glimmer-30b")];
444
+ const a = applyFeedScores(withoutField, [...feeds, ...suppliedScores(cfg)]);
445
+ const b = applyFeedScores(withEmpty, [...feeds, ...suppliedScores(cfgWith([]))]);
446
+ expect(JSON.stringify(withoutField)).toBe(JSON.stringify(withEmpty));
447
+ expect(a).toEqual(b);
448
+ expect(a.sources.neutral).toBe(0);
449
+ expect(a.sources.vendor).toBe(0);
450
+ });
451
+ });
@@ -118,4 +118,30 @@ describe("loadConfig", () => {
118
118
  const cfg = loadConfig({ path });
119
119
  expect(cfg.server.port).toBe(DEFAULT_CONFIG.server.port);
120
120
  });
121
+
122
+ test("a config file may carry supplied benchmark scores, and a bad row is named", async () => {
123
+ const path = writeConfig(
124
+ "benchmarks:\n extraScores:\n - key: deepseek/deepseek-v4.1-flash\n coding: 55.2\n source: vendor\n",
125
+ );
126
+ const cfg = loadConfig({ path });
127
+ // `creator` defaults rather than going absent: it is only ever a tie-break.
128
+ expect(cfg.benchmarks.extraScores).toEqual([
129
+ { key: "deepseek/deepseek-v4.1-flash", creator: "", coding: 55.2, source: "vendor" },
130
+ ]);
131
+ // Absent by default, so a deployment that sets nothing is untouched.
132
+ expect(DEFAULT_CONFIG.benchmarks.extraScores).toBeUndefined();
133
+
134
+ // A FILE is strict, like every other key in it: a source config may not claim
135
+ // is an error the operator wants told, not a row silently dropped. A patch
136
+ // from a front door is the lenient path instead — see `suppliedScores`.
137
+ const bad = writeConfig("benchmarks:\n extraScores:\n - key: x\n source: artificial_analysis\n");
138
+ let message = "";
139
+ try {
140
+ loadConfig({ path: bad });
141
+ } catch (err) {
142
+ message = err instanceof Error ? err.message : String(err);
143
+ }
144
+ expect(message).toContain("extraScores");
145
+ expect(message).toContain("source");
146
+ });
121
147
  });
@@ -0,0 +1,301 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { connectRemote, MCP_SERVER_NAME } from "../src/cli/connect.ts";
6
+ import { refreshAndRewrite } from "../src/cli/refresh.ts";
7
+ import { CONTEXT_TOKEN_RENEW_AHEAD_MS, dueForRenewal, ensureContextToken, mintContextToken, storedContextToken } from "../src/cli/context-token.ts";
8
+ import { contextAccountOf, loadContextToken, loadRefreshToken, saveContextToken, saveRefreshToken } from "../src/cli/credential-store.ts";
9
+ import { parseRemoteRouter } from "../omp-extension/remote-logic.ts";
10
+
11
+ const NL = String.fromCharCode(10);
12
+ const DAY = 24 * 3_600_000;
13
+ const read = (p: string): Record<string, unknown> => JSON.parse(readFileSync(p, "utf8")) as Record<string, unknown>;
14
+ const servers = (p: string): Record<string, unknown> => (read(p).mcpServers as Record<string, unknown>) ?? {};
15
+ const auth = (p: string): string | undefined => (servers(p)[MCP_SERVER_NAME] as { headers?: { Authorization?: string } } | undefined)?.headers?.Authorization;
16
+
17
+ /** An omp+router home wired the way `connect` expects to find one. */
18
+ function fixture(prefix: string): { home: string; agent: string; rh: string; env: Record<string, string> } {
19
+ const home = mkdtempSync(join(tmpdir(), prefix));
20
+ const agent = join(home, ".omp", "agent");
21
+ mkdirSync(agent, { recursive: true });
22
+ writeFileSync(join(agent, "config.yml"), `extensions: []${NL}`, "utf8");
23
+ const rh = join(home, ".auto-model-router");
24
+ return { home, agent, rh, env: { HOME: home, PI_CODING_AGENT_DIR: agent, AUTO_MODEL_ROUTER_HOME: rh, HERMES_HOME: join(home, "no-hermes") } };
25
+ }
26
+
27
+ /** A fake OS credential store; one map, keyed by account, so both slots land in it. */
28
+ function vault(): { map: Map<string, string>; backend: { save(a: string, s: string): void; load(a: string): string | null; remove(a: string): void } } {
29
+ const map = new Map<string, string>();
30
+ return { map, backend: { save: (a, s) => void map.set(a, s), load: (a) => map.get(a) ?? null, remove: (a) => void map.delete(a) } };
31
+ }
32
+
33
+ describe("when a context token is due for renewal", () => {
34
+ test("none, an unknown expiry, or inside the 30-day window; a year out is left alone", async () => {
35
+ const now = Date.now();
36
+ expect(CONTEXT_TOKEN_RENEW_AHEAD_MS).toBe(30 * DAY);
37
+ expect(dueForRenewal(null, now)).toBe(true);
38
+ expect(dueForRenewal({ value: "" }, now)).toBe(true);
39
+ // An expiry nobody recorded cannot be trusted; minting records one, so this converges.
40
+ expect(dueForRenewal({ value: "amrctx_x" }, now)).toBe(true);
41
+ expect(dueForRenewal({ value: "amrctx_x", expiresAtMs: now + 10 * DAY }, now)).toBe(true);
42
+ expect(dueForRenewal({ value: "amrctx_x", expiresAtMs: now - DAY }, now)).toBe(true);
43
+ expect(dueForRenewal({ value: "amrctx_x", expiresAtMs: now + 31 * DAY }, now)).toBe(false);
44
+ expect(dueForRenewal({ value: "amrctx_x", expiresAtMs: now + 365 * DAY }, now)).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe("minting a context token", () => {
49
+ test("posts the device name with the member key and reads the token back", async () => {
50
+ const seen: { url: string; auth: string | null; body: string }[] = [];
51
+ const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
52
+ seen.push({ url: String(input), auth: (init?.headers as Record<string, string>).authorization ?? null, body: String(init?.body) });
53
+ return Response.json({ token: "amrctx_new", id: "ctx_1", expiresAtMs: 99, deviceId: "dev_1" }, { status: 201 });
54
+ }) as unknown as typeof fetch;
55
+ expect(await mintContextToken("https://team.example", "amrt_key", "laptop", fetchImpl)).toEqual({ value: "amrctx_new", expiresAtMs: 99, id: "ctx_1" });
56
+ expect(seen[0]).toEqual({ url: "https://team.example/me/context-tokens", auth: "Bearer amrt_key", body: JSON.stringify({ name: "laptop" }) });
57
+ });
58
+
59
+ test("an older team (404), a deployment that issues none (503), a junk body or a dead remote all answer null", async () => {
60
+ const status = (code: number): typeof fetch => (async () => new Response("", { status: code })) as unknown as typeof fetch;
61
+ expect(await mintContextToken("https://t", "k", "n", status(404))).toBeNull();
62
+ expect(await mintContextToken("https://t", "k", "n", status(503))).toBeNull();
63
+ expect(await mintContextToken("https://t", "k", "n", (async () => Response.json({ token: 7 })) as unknown as typeof fetch)).toBeNull();
64
+ expect(await mintContextToken("https://t", "k", "n", (async () => new Response("<html>")) as unknown as typeof fetch)).toBeNull();
65
+ expect(
66
+ await mintContextToken("https://t", "k", "n", (async () => {
67
+ throw new Error("down");
68
+ }) as unknown as typeof fetch),
69
+ ).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe("ensureContextToken", () => {
74
+ const never = (async () => {
75
+ throw new Error("should not have been called");
76
+ }) as unknown as typeof fetch;
77
+ const mints = (value: string): typeof fetch => (async () => Response.json({ token: value, id: `id_${value}`, expiresAtMs: Date.now() + 365 * DAY })) as unknown as typeof fetch;
78
+
79
+ test("an older team edition (member-key) mints nothing and keeps the access key", async () => {
80
+ expect(await ensureContextToken({ url: "https://t", key: "amrt_k", mcpAuth: "member-key", routerHome: "/nowhere", name: "laptop", fetchImpl: never })).toBeNull();
81
+ });
82
+
83
+ test("a token the exchange already handed over wins, with no second round trip", async () => {
84
+ const issued = { value: "amrctx_exchange", expiresAtMs: 1, id: "ctx_e" };
85
+ expect(await ensureContextToken({ url: "https://t", key: "amrt_k", mcpAuth: "context-token", routerHome: "/nowhere", name: "laptop", issued, fetchImpl: never })).toEqual(issued);
86
+ });
87
+
88
+ test("a held token far from expiry is reused untouched; one inside the window is renewed", async () => {
89
+ const { backend, map } = vault();
90
+ const rh = "/nowhere";
91
+ const remote = parseRemoteRouter(
92
+ JSON.stringify({ url: "https://t", key: "amrt_k", userId: "u_ada", refreshTokenStore: "keychain", refreshAccount: "u_ada@t", contextTokenStore: "keychain", contextAccount: "u_ada@t#context", contextTokenExpiresAtMs: Date.now() + 200 * DAY, contextTokenId: "ctx_old" }),
93
+ )!;
94
+ map.set("u_ada@t#context", "amrctx_held");
95
+ const opts = { url: "https://t", key: "amrt_k", mcpAuth: "context-token" as const, routerHome: rh, remote, name: "laptop", storeDeps: { backend } };
96
+ expect(await ensureContextToken({ ...opts, fetchImpl: never })).toEqual({ value: "amrctx_held", expiresAtMs: remote.contextTokenExpiresAtMs!, id: "ctx_old" });
97
+ // Move the expiry inside the renewal window and the same call mints instead.
98
+ const soon = { ...remote, contextTokenExpiresAtMs: Date.now() + 10 * DAY };
99
+ const renewed = await ensureContextToken({ ...opts, remote: soon, fetchImpl: mints("amrctx_fresh") });
100
+ expect(renewed?.value).toBe("amrctx_fresh");
101
+ });
102
+
103
+ test("a mint that fails leaves the held token in place rather than breaking the tools", async () => {
104
+ const { backend, map } = vault();
105
+ map.set("u_ada@t#context", "amrctx_held");
106
+ const remote = parseRemoteRouter(JSON.stringify({ url: "https://t", key: "amrt_k", userId: "u_ada", contextTokenStore: "keychain", contextAccount: "u_ada@t#context", contextTokenExpiresAtMs: Date.now() + DAY }))!;
107
+ const dead = (async () => new Response("", { status: 503 })) as unknown as typeof fetch;
108
+ expect((await ensureContextToken({ url: "https://t", key: "k", mcpAuth: "context-token", routerHome: "/nowhere", remote, name: "laptop", storeDeps: { backend }, fetchImpl: dead }))?.value).toBe("amrctx_held");
109
+ // And with nothing held there is nothing to fall back to: the caller keeps the access key.
110
+ map.clear();
111
+ expect(await ensureContextToken({ url: "https://t", key: "k", mcpAuth: "context-token", routerHome: "/nowhere", remote, name: "laptop", storeDeps: { backend }, fetchImpl: dead })).toBeNull();
112
+ });
113
+ });
114
+
115
+ describe("the context token lives beside the refresh token, never in a plain file when a store works", () => {
116
+ test("its own slot: two secrets, two accounts, two fallback files, neither clobbering the other", async () => {
117
+ const home = mkdtempSync(join(tmpdir(), "amr-ctx-store-"));
118
+ const { backend } = vault();
119
+ try {
120
+ expect(contextAccountOf("u_ada@team.example")).toBe("u_ada@team.example#context");
121
+ expect(saveRefreshToken(home, "u@t", "amrr_x", "keychain", { backend })).toBe("keychain");
122
+ expect(saveContextToken(home, "u@t#context", "amrctx_x", "keychain", { backend })).toBe("keychain");
123
+ expect(loadRefreshToken(home, "u@t", "keychain", { backend })).toBe("amrr_x");
124
+ expect(loadContextToken(home, "u@t#context", "keychain", { backend })).toBe("amrctx_x");
125
+ // Nothing on disk while a store takes them.
126
+ expect(existsSync(join(home, "refresh.token"))).toBe(false);
127
+ expect(existsSync(join(home, "context.token"))).toBe(false);
128
+ // The file fallback keeps them apart.
129
+ expect(saveRefreshToken(home, "u@t", "amrr_f", "file")).toBe("file");
130
+ expect(saveContextToken(home, "u@t#context", "amrctx_f", "file")).toBe("file");
131
+ expect(readFileSync(join(home, "refresh.token"), "utf8").trim()).toBe("amrr_f");
132
+ expect(readFileSync(join(home, "context.token"), "utf8").trim()).toBe("amrctx_f");
133
+ expect(loadRefreshToken(home, "u@t", "file")).toBe("amrr_f");
134
+ expect(loadContextToken(home, "u@t#context", "file")).toBe("amrctx_f");
135
+ } finally {
136
+ rmSync(home, { recursive: true, force: true });
137
+ }
138
+ });
139
+
140
+ test("connect files it in the store the refresh token uses; remote.json only names it", async () => {
141
+ const { home, agent, rh, env } = fixture("amr-ctx-connect-");
142
+ const { backend, map } = vault();
143
+ try {
144
+ connectRemote({
145
+ url: "https://team.example",
146
+ key: "amrt_k1",
147
+ userId: "u_ada",
148
+ name: "Ada",
149
+ refreshToken: "amrr_r1",
150
+ keyExpiresAtMs: 1,
151
+ refreshExpiresAtMs: 2,
152
+ device: "laptop",
153
+ store: "keychain",
154
+ storeDeps: { backend },
155
+ profile: false,
156
+ dryRun: false,
157
+ only: ["omp"],
158
+ env,
159
+ home,
160
+ packageDir: process.cwd(),
161
+ mcp: { url: "https://team.example/mcp", token: "amrctx_t1", tokenExpiresAtMs: 4_000, tokenId: "ctx_1" },
162
+ platform: "darwin",
163
+ pathHas: () => false,
164
+ });
165
+ const written = readFileSync(join(rh, "remote.json"), "utf8");
166
+ expect(written).not.toContain("amrctx_t1");
167
+ expect(existsSync(join(rh, "context.token"))).toBe(false);
168
+ const remote = parseRemoteRouter(written)!;
169
+ expect(remote).toMatchObject({ contextTokenStore: "keychain", contextAccount: "u_ada@team.example#context", contextTokenExpiresAtMs: 4_000, contextTokenId: "ctx_1" });
170
+ expect(map.get("u_ada@team.example#context")).toBe("amrctx_t1");
171
+ expect(map.get("u_ada@team.example")).toBe("amrr_r1");
172
+ expect(storedContextToken(remote, rh, { backend })).toEqual({ value: "amrctx_t1", expiresAtMs: 4_000, id: "ctx_1" });
173
+ // The MCP entry carries the context token, NOT the 72-hour access key.
174
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrctx_t1");
175
+ } finally {
176
+ rmSync(home, { recursive: true, force: true });
177
+ }
178
+ });
179
+ });
180
+
181
+ describe("a key refresh leaves the MCP entry alone", () => {
182
+ /** A team edition that mints context tokens; `mcpAuth` is what tells connect so. */
183
+ function teamFetch(opts: { key: string; mint?: { token: string; id: string; expiresAtMs: number } }): { fetchImpl: typeof fetch; seen: string[] } {
184
+ const seen: string[] = [];
185
+ const fetchImpl = (async (input: string | URL | Request) => {
186
+ const url = String(input);
187
+ seen.push(url);
188
+ if (url.endsWith("/setup/info")) return Response.json({ version: "0.39.0", mcp: true, mcpAuth: "context-token" });
189
+ if (url.endsWith("/setup/skills")) return new Response("", { status: 404 });
190
+ if (url.endsWith("/me/context-tokens")) return opts.mint === undefined ? new Response("", { status: 503 }) : Response.json(opts.mint, { status: 201 });
191
+ return Response.json({ key: opts.key, keyExpiresAtMs: 50, refreshToken: "amrr_r2", refreshExpiresAtMs: 90 });
192
+ }) as unknown as typeof fetch;
193
+ return { fetchImpl, seen };
194
+ }
195
+
196
+ function connected(env: Record<string, string>, home: string, backend: { save(a: string, s: string): void; load(a: string): string | null; remove(a: string): void }, expiresAtMs: number): void {
197
+ connectRemote({
198
+ url: "https://team.example",
199
+ key: "amrt_old",
200
+ userId: "u_ada",
201
+ name: "Ada",
202
+ refreshToken: "amrr_r1",
203
+ keyExpiresAtMs: 1,
204
+ refreshExpiresAtMs: 2,
205
+ device: "laptop",
206
+ store: "keychain",
207
+ storeDeps: { backend },
208
+ profile: false,
209
+ dryRun: false,
210
+ only: ["omp"],
211
+ env,
212
+ home,
213
+ packageDir: process.cwd(),
214
+ mcp: { url: "https://team.example/mcp", token: "amrctx_t1", tokenExpiresAtMs: expiresAtMs, tokenId: "ctx_1" },
215
+ platform: "darwin",
216
+ pathHas: () => false,
217
+ });
218
+ }
219
+
220
+ test("the new access key goes everywhere but the team-context server, which keeps its token", async () => {
221
+ const { home, agent, rh, env } = fixture("amr-ctx-refresh-");
222
+ const { backend, map } = vault();
223
+ try {
224
+ connected(env, home, backend, Date.now() + 300 * DAY);
225
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrctx_t1");
226
+ const { fetchImpl, seen } = teamFetch({ key: "amrt_new" });
227
+ const remote = parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!;
228
+ await refreshAndRewrite({ remote, fetchImpl, home, packageDir: process.cwd(), env, platform: "darwin", pathHas: () => false, routerHome: rh, storeDeps: { backend } });
229
+ // Nothing was minted: the held token is nowhere near expiry.
230
+ expect(seen.some((u) => u.endsWith("/me/context-tokens"))).toBe(false);
231
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrctx_t1");
232
+ expect(map.get("u_ada@team.example#context")).toBe("amrctx_t1");
233
+ // ... while the access key really did rotate everywhere else.
234
+ const after = parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!;
235
+ expect(after.key).toBe("amrt_new");
236
+ expect(after.contextTokenId).toBe("ctx_1");
237
+ expect(readFileSync(join(env.PI_CODING_AGENT_DIR!, "models.yml"), "utf8")).toContain("amrt_new");
238
+ } finally {
239
+ rmSync(home, { recursive: true, force: true });
240
+ }
241
+ });
242
+
243
+ test("a token inside the renewal window is re-minted with the freshly refreshed key and re-filed", async () => {
244
+ const { home, agent, rh, env } = fixture("amr-ctx-renew-");
245
+ const { backend, map } = vault();
246
+ try {
247
+ connected(env, home, backend, Date.now() + 10 * DAY);
248
+ const expiresAtMs = Date.now() + 365 * DAY;
249
+ const { fetchImpl, seen } = teamFetch({ key: "amrt_new", mint: { token: "amrctx_t2", id: "ctx_2", expiresAtMs } });
250
+ const remote = parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!;
251
+ await refreshAndRewrite({ remote, fetchImpl, home, packageDir: process.cwd(), env, platform: "darwin", pathHas: () => false, routerHome: rh, storeDeps: { backend } });
252
+ expect(seen.filter((u) => u.endsWith("/me/context-tokens")).length).toBe(1);
253
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrctx_t2");
254
+ expect(map.get("u_ada@team.example#context")).toBe("amrctx_t2");
255
+ const after = parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!;
256
+ expect(after).toMatchObject({ contextTokenId: "ctx_2", contextTokenExpiresAtMs: expiresAtMs, contextTokenStore: "keychain" });
257
+ expect(readFileSync(join(rh, "remote.json"), "utf8")).not.toContain("amrctx_t2");
258
+ } finally {
259
+ rmSync(home, { recursive: true, force: true });
260
+ }
261
+ });
262
+
263
+ test("an older team edition is untouched: no mcpAuth, no mint, and the entry takes the new access key", async () => {
264
+ const { home, agent, rh, env } = fixture("amr-ctx-legacy-");
265
+ const { backend } = vault();
266
+ try {
267
+ // No token at connect time — the team never offered one.
268
+ connectRemote({ url: "https://team.example", key: "amrt_old", userId: "u_ada", name: "Ada", refreshToken: "amrr_r1", keyExpiresAtMs: 1, refreshExpiresAtMs: 2, store: "keychain", storeDeps: { backend }, profile: false, dryRun: false, only: ["omp"], env, home, packageDir: process.cwd(), mcp: { url: "https://team.example/mcp" }, platform: "darwin", pathHas: () => false });
269
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrt_old");
270
+ expect(parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!.contextTokenStore).toBeUndefined();
271
+ const seen: string[] = [];
272
+ const fetchImpl = (async (input: string | URL | Request) => {
273
+ const url = String(input);
274
+ seen.push(url);
275
+ if (url.endsWith("/setup/info")) return Response.json({ version: "0.18.0", mcp: true }); // no mcpAuth
276
+ if (url.endsWith("/setup/skills")) return new Response("", { status: 404 });
277
+ return Response.json({ key: "amrt_new", keyExpiresAtMs: 50, refreshToken: "amrr_r2", refreshExpiresAtMs: 90 });
278
+ }) as unknown as typeof fetch;
279
+ const remote = parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!;
280
+ await refreshAndRewrite({ remote, fetchImpl, home, packageDir: process.cwd(), env, platform: "darwin", pathHas: () => false, routerHome: rh, storeDeps: { backend } });
281
+ expect(seen.some((u) => u.endsWith("/me/context-tokens"))).toBe(false);
282
+ expect(auth(join(agent, "mcp.json"))).toBe("Bearer amrt_new");
283
+ } finally {
284
+ rmSync(home, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ test("a connect that learns nothing about context tokens keeps what remote.json already recorded", async () => {
289
+ const { home, rh, env } = fixture("amr-ctx-carry-");
290
+ const { backend } = vault();
291
+ try {
292
+ connected(env, home, backend, 4_000);
293
+ // A later connect with no mcp token at all (a /setup/info that did not answer) must not
294
+ // orphan the token the credential store is still holding for another year.
295
+ connectRemote({ url: "https://team.example", key: "amrt_k2", userId: "u_ada", name: "Ada", refreshToken: "amrr_r1", store: "keychain", storeDeps: { backend }, profile: false, dryRun: false, only: ["omp"], env, home, packageDir: process.cwd(), platform: "darwin", pathHas: () => false });
296
+ expect(parseRemoteRouter(readFileSync(join(rh, "remote.json"), "utf8"))!).toMatchObject({ contextTokenStore: "keychain", contextAccount: "u_ada@team.example#context", contextTokenId: "ctx_1" });
297
+ } finally {
298
+ rmSync(home, { recursive: true, force: true });
299
+ }
300
+ });
301
+ });
@@ -41,7 +41,7 @@ function entry(over: Partial<LedgerEntry> & { id: string; slug: string }): Ledge
41
41
  conversationKey: `conv-${over.id}`,
42
42
  // `LedgerEntry.sessionId` is a string and the column is NOT NULL: a null
43
43
  // here only ever passed because the second bootstrap declared the column
44
- // laxer than the nineteen shipped migrations do.
44
+ // laxer than the twenty shipped migrations do.
45
45
  sessionId: `sess-${over.id}`,
46
46
  turn: 1,
47
47
  requestedModel: "auto",
@@ -144,14 +144,23 @@ describe("connect writes the team MCP endpoint", () => {
144
144
 
145
145
  describe("fetchSetupInfo", () => {
146
146
  test("reads mcp from a team, and answers false for a plain router, a bad body or a dead remote", async () => {
147
- expect(await fetchSetupInfo("https://t", (async () => Response.json({ mcp: true })) as unknown as typeof fetch)).toEqual({ mcp: true });
148
- expect(await fetchSetupInfo("https://t", (async () => Response.json({ mcp: "yes" })) as unknown as typeof fetch)).toEqual({ mcp: false });
149
- expect(await fetchSetupInfo("https://t", (async () => new Response("", { status: 404 })) as unknown as typeof fetch)).toEqual({ mcp: false });
150
- expect(await fetchSetupInfo("https://t", (async () => new Response("<html>", { status: 200 })) as unknown as typeof fetch)).toEqual({ mcp: false });
147
+ expect(await fetchSetupInfo("https://t", (async () => Response.json({ mcp: true })) as unknown as typeof fetch)).toEqual({ mcp: true, mcpAuth: "member-key" });
148
+ expect(await fetchSetupInfo("https://t", (async () => Response.json({ mcp: "yes" })) as unknown as typeof fetch)).toEqual({ mcp: false, mcpAuth: "member-key" });
149
+ expect(await fetchSetupInfo("https://t", (async () => new Response("", { status: 404 })) as unknown as typeof fetch)).toEqual({ mcp: false, mcpAuth: "member-key" });
150
+ expect(await fetchSetupInfo("https://t", (async () => new Response("<html>", { status: 200 })) as unknown as typeof fetch)).toEqual({ mcp: false, mcpAuth: "member-key" });
151
151
  expect(
152
152
  await fetchSetupInfo("https://t", (async () => {
153
153
  throw new Error("down");
154
154
  }) as unknown as typeof fetch),
155
- ).toEqual({ mcp: false });
155
+ ).toEqual({ mcp: false, mcpAuth: "member-key" });
156
+ });
157
+
158
+ test("mcpAuth says which credential the MCP entry carries; anything but context-token is the member key", async () => {
159
+ const info = async (body: Record<string, unknown>): Promise<{ mcp: boolean; mcpAuth: string }> => fetchSetupInfo("https://t", (async () => Response.json(body)) as unknown as typeof fetch);
160
+ expect(await info({ mcp: true, mcpAuth: "context-token" })).toEqual({ mcp: true, mcpAuth: "context-token" });
161
+ expect(await info({ mcp: true, mcpAuth: "member-key" })).toEqual({ mcp: true, mcpAuth: "member-key" });
162
+ // An older team edition says nothing at all; it must read as the member key.
163
+ expect(await info({ mcp: true })).toEqual({ mcp: true, mcpAuth: "member-key" });
164
+ expect(await info({ mcp: true, mcpAuth: 1 })).toEqual({ mcp: true, mcpAuth: "member-key" });
156
165
  });
157
166
  });
@@ -11,7 +11,7 @@ import { createFeedbackStore } from "../src/cost/feedback.ts";
11
11
  import { createSqlLedger } from "../src/cost/ledger-sql.ts";
12
12
  import { buildUsageReport } from "../src/cost/report.ts";
13
13
  import { buildDailySummary, createKv } from "../src/cost/summary.ts";
14
- import { exportRows, spendUsdSince } from "../src/cost/views.ts";
14
+ import { decisionEntries, exportRows, spendUsdSince } from "../src/cost/views.ts";
15
15
  import { createConversationStore } from "../src/router/state.ts";
16
16
 
17
17
  /**
@@ -26,7 +26,7 @@ import { createConversationStore } from "../src/router/state.ts";
26
26
 
27
27
  const FIXTURES = join(import.meta.dir, "fixtures", "migrations");
28
28
  const files = readdirSync(FIXTURES).filter((f) => /^router-v\d+\.db$/.test(f)).sort((a, b) => Number(/\d+/.exec(a)![0]) - Number(/\d+/.exec(b)![0]));
29
- const CURRENT_VERSION = 19;
29
+ const CURRENT_VERSION = 20;
30
30
 
31
31
  describe("schema migrations from every shipped version", () => {
32
32
  test("fixtures exist for the versions that shipped", async () => {
@@ -42,14 +42,14 @@ describe("schema migrations from every shipped version", () => {
42
42
  const cfg = structuredClone(DEFAULT_CONFIG);
43
43
  cfg.ledger.path = path;
44
44
  // `openDb` is the migration path for a SQLite file: it applies the
45
- // nineteen versions in order. The shim handle then reads the result.
45
+ // twenty versions in order. The shim handle then reads the result.
46
46
  const db = openDb(path);
47
47
  const sdb = openSqlDb(path);
48
48
  try {
49
49
  expect((db.query("PRAGMA user_version").get() as { user_version: number }).user_version).toBe(CURRENT_VERSION);
50
50
  // Every column the current code writes exists after migration.
51
51
  const ledgerCols = new Set((db.query("PRAGMA table_info(ledger)").all() as { name: string }[]).map((c) => c.name));
52
- for (const c of ["harness_id", "error_kind", "omp_session_id", "features", "explored_from", "hold_arm", "prompt_tokens_saved", "scope", "redactions"]) expect(ledgerCols.has(c)).toBe(true);
52
+ for (const c of ["harness_id", "error_kind", "omp_session_id", "features", "explored_from", "hold_arm", "prompt_tokens_saved", "scope", "redactions", "request_id"]) expect(ledgerCols.has(c)).toBe(true);
53
53
  const convCols = new Set((db.query("PRAGMA table_info(conversations)").all() as { name: string }[]).map((c) => c.name));
54
54
  for (const c of ["context_version", "compaction_plan", "compaction_plan_tokens", "upgrade_deferred_tier"]) expect(convCols.has(c)).toBe(true);
55
55
  // The fixture's ledger row survived the ALTERs with its values.
@@ -76,6 +76,13 @@ describe("schema migrations from every shipped version", () => {
76
76
  // v19: the fixture's row predates `redactions`, so nothing claims a
77
77
  // redaction happened on it and the report totals it as zero.
78
78
  expect((await buildUsageReport(sdb, { windowDays: 3650 })).totals.redactions).toBe(0);
79
+ // v20: the fixture's row predates `request_id`, so it reads as ABSENT
80
+ // rather than as an error or an empty string — an old turn genuinely
81
+ // has no request id, and the decision trail says so by omission. The
82
+ // column is there and queryable, so a lookup by id simply finds nothing.
83
+ expect((await ledger.recentEntries(5))[0]?.requestId).toBeUndefined();
84
+ expect(await decisionEntries(sdb, { sinceMs: 0, harness: null, requestId: "anything-at-all" })).toEqual([]);
85
+ expect((await decisionEntries(sdb, { sinceMs: 0, harness: null }))[0]?.requestId).toBeUndefined();
79
86
  } finally {
80
87
  await sdb.close();
81
88
  db.close();