auto-model-router 0.35.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.
- package/.omp-plugin/marketplace.json +2 -2
- package/README.md +104 -10
- package/omp-extension/remote-logic.ts +15 -0
- package/package.json +1 -1
- package/src/catalog/benchmark-feeds.ts +179 -41
- package/src/catalog/openrouter-catalog.ts +10 -2
- package/src/cli/connect.ts +89 -17
- package/src/cli/context-token.ts +133 -0
- package/src/cli/credential-store.ts +65 -20
- package/src/cli/refresh.ts +26 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.ts +22 -0
- package/src/config/types.ts +31 -0
- package/src/cost/ledger-sql.ts +3 -2
- package/src/cost/ledger.ts +4 -0
- package/src/cost/types.ts +12 -0
- package/src/cost/views.ts +12 -0
- package/src/lib.ts +12 -0
- package/src/router/candidates.ts +17 -0
- package/src/router/index.ts +4 -0
- package/src/router/types.ts +2 -0
- package/src/server/catalog-view.ts +8 -2
- package/src/server/compaction-digest.ts +3 -0
- package/src/server/digest.ts +11 -0
- package/src/server/http.ts +46 -4
- package/src/server/turn.ts +5 -0
- package/src/util/requestid.ts +77 -0
- package/src/util/schema.ts +42 -3
- package/src/util/sqlite.ts +20 -1
- package/src/wire/openai/request.ts +8 -0
- package/src/wire/types.ts +18 -0
- package/test/benchmark-feeds.test.ts +194 -0
- package/test/catalog-view.test.ts +11 -0
- package/test/config.test.ts +26 -0
- package/test/context-token.test.ts +301 -0
- package/test/failover.test.ts +1 -1
- package/test/ledger-sql.test.ts +1 -1
- package/test/mcp-entry.test.ts +14 -5
- package/test/migrations.test.ts +11 -4
- package/test/reconfigure.test.ts +55 -0
- package/test/request-id.test.ts +424 -0
- package/test/schema.test.ts +2 -2
- package/test/tier-plan.test.ts +51 -0
- package/test/trust-attribution.test.ts +2 -2
- package/test/turn.test.ts +56 -1
package/src/wire/types.ts
CHANGED
|
@@ -17,6 +17,11 @@ export type WireProtocol = "openai-chat" | "openai-responses" | "anthropic-messa
|
|
|
17
17
|
* like `filters.allow`/`filters.deny` (a request allow list replaces the
|
|
18
18
|
* configured one; a deny list adds to it); `minTier`/`maxTier` narrow the
|
|
19
19
|
* profile's tier envelope; `pin` forces one slug, like `/router pin`.
|
|
20
|
+
* `providerLocks` restricts WHERE a model may be served from: a model whose
|
|
21
|
+
* slug matches the key may only dispatch through a provider whose id matches
|
|
22
|
+
* the value (`{"anthropic/*": "anthropic-subscription"}` keeps Claude on the
|
|
23
|
+
* subscription upstream and away from OpenRouter's billed twins). Keys and
|
|
24
|
+
* values are slug globs; a model matching several locks must satisfy each.
|
|
20
25
|
*/
|
|
21
26
|
export interface RequestPolicy {
|
|
22
27
|
allow?: string[];
|
|
@@ -24,6 +29,7 @@ export interface RequestPolicy {
|
|
|
24
29
|
minTier?: "trivial" | "simple" | "moderate" | "hard";
|
|
25
30
|
maxTier?: "trivial" | "simple" | "moderate" | "hard";
|
|
26
31
|
pin?: string;
|
|
32
|
+
providerLocks?: Record<string, string>;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export type Role = "system" | "developer" | "user" | "assistant" | "tool";
|
|
@@ -120,6 +126,18 @@ export interface NormRequest {
|
|
|
120
126
|
agentdoxOrigin: string;
|
|
121
127
|
/** `X-Omp-Subagent: 1`: the caller is an omp subagent (a session without a UI). */
|
|
122
128
|
isSubagent: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The id of the HTTP request this turn is answering, recorded on every
|
|
131
|
+
* ledger row it writes so support can go from a request id a customer
|
|
132
|
+
* quoted straight to the turn, rather than joining on member and time.
|
|
133
|
+
*
|
|
134
|
+
* Taken from the `X-Request-Id` header when the caller sends one this
|
|
135
|
+
* router will carry (the rules are in `util/requestid.ts`), and MINTED —
|
|
136
|
+
* with the `amr-` prefix that says so — when it does not. Every wire sets
|
|
137
|
+
* it; it is optional only because the router's own synthetic requests
|
|
138
|
+
* (advise, a standalone digest) are not answering a turn of their own.
|
|
139
|
+
*/
|
|
140
|
+
requestId?: string;
|
|
123
141
|
/**
|
|
124
142
|
* Per-request routing policy from the `X-Omp-Policy` header (JSON), set by
|
|
125
143
|
* a front door such as the team edition: narrows what this turn may route
|
|
@@ -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
|
+
});
|
|
@@ -137,8 +137,19 @@ describe("catalogView", () => {
|
|
|
137
137
|
// So is a pin naming no model.
|
|
138
138
|
expect(bySlug(catalogView({ models: MODELS, fetchedAtMs: 0, verdict: { filters, pin: "nobody/here" }, unserved: served })).get("anthropic/claude-sonnet-5")!.admitted).toBe(true);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
test("a provider lock drops a model whose upstream cannot serve it", async () => {
|
|
142
|
+
// MODELS are all openrouter-served except vllm/ and azure-eu/; a lock
|
|
143
|
+
// confining anthropic to a named subscription upstream admits only the
|
|
144
|
+
// models that upstream actually carries.
|
|
145
|
+
const filters = { ...DEFAULT_CONFIG.filters, providerLocks: { "anthropic/*": "claude-sub" } };
|
|
146
|
+
const view = bySlug(catalogView({ models: MODELS, fetchedAtMs: 0, verdict: { filters }, unserved: served }));
|
|
147
|
+
expect(view.get("anthropic/claude-sonnet-5")).toMatchObject({ admitted: false, reason: "locked to providers matching claude-sub (filters.providerLocks)" });
|
|
148
|
+
expect(view.get("openai/gpt-5")).toMatchObject({ admitted: true }); // no key matches
|
|
149
|
+
});
|
|
140
150
|
});
|
|
141
151
|
|
|
152
|
+
|
|
142
153
|
describe("GET /v1/router/catalog", () => {
|
|
143
154
|
let handle: StartedServer;
|
|
144
155
|
let empty: StartedServer;
|
package/test/config.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/test/failover.test.ts
CHANGED
|
@@ -48,7 +48,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
|
|
|
48
48
|
data: { axis: "intelligence", minQuality: 0 },
|
|
49
49
|
chat: { axis: "intelligence", minQuality: 0 },
|
|
50
50
|
},
|
|
51
|
-
filters: { allow: [], deny: [], includeFree: false, requireToolSupport: true, agenticAxisForToolTurns: true, minAgenticForToolTurns: 0, minTrust: 0.6, feedbackWeight: 0, feedbackByTask: false, minTrustSamples: 5, trustScopedByHarness: false, trustWindowDays: 0, contextHeadroom: 1.2, reasoningCompletionFloor: 0, latencyWeight: 0, latencyReferenceMs: 5000, latencyReferenceTokensPerSec: 30, cacheReliabilityMinSamples: 10, latencyMinSamples: 20, escalationCostWeight: 0 },
|
|
51
|
+
filters: { allow: [], deny: [], providerLocks: {}, includeFree: false, requireToolSupport: true, agenticAxisForToolTurns: true, minAgenticForToolTurns: 0, minTrust: 0.6, feedbackWeight: 0, feedbackByTask: false, minTrustSamples: 5, trustScopedByHarness: false, trustWindowDays: 0, contextHeadroom: 1.2, reasoningCompletionFloor: 0, latencyWeight: 0, latencyReferenceMs: 5000, latencyReferenceTokensPerSec: 30, cacheReliabilityMinSamples: 10, latencyMinSamples: 20, escalationCostWeight: 0 },
|
|
52
52
|
classifier: {
|
|
53
53
|
ambiguityThreshold: 0,
|
|
54
54
|
model: "test/adjudicator", learnedModelPath: "",
|
package/test/ledger-sql.test.ts
CHANGED
|
@@ -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
|
|
44
|
+
// laxer than the twenty shipped migrations do.
|
|
45
45
|
sessionId: `sess-${over.id}`,
|
|
46
46
|
turn: 1,
|
|
47
47
|
requestedModel: "auto",
|