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.
Files changed (45) hide show
  1. package/.omp-plugin/marketplace.json +2 -2
  2. package/README.md +104 -10
  3. package/omp-extension/remote-logic.ts +15 -0
  4. package/package.json +1 -1
  5. package/src/catalog/benchmark-feeds.ts +179 -41
  6. package/src/catalog/openrouter-catalog.ts +10 -2
  7. package/src/cli/connect.ts +89 -17
  8. package/src/cli/context-token.ts +133 -0
  9. package/src/cli/credential-store.ts +65 -20
  10. package/src/cli/refresh.ts +26 -1
  11. package/src/config/defaults.ts +1 -0
  12. package/src/config/schema.ts +22 -0
  13. package/src/config/types.ts +31 -0
  14. package/src/cost/ledger-sql.ts +3 -2
  15. package/src/cost/ledger.ts +4 -0
  16. package/src/cost/types.ts +12 -0
  17. package/src/cost/views.ts +12 -0
  18. package/src/lib.ts +12 -0
  19. package/src/router/candidates.ts +17 -0
  20. package/src/router/index.ts +4 -0
  21. package/src/router/types.ts +2 -0
  22. package/src/server/catalog-view.ts +8 -2
  23. package/src/server/compaction-digest.ts +3 -0
  24. package/src/server/digest.ts +11 -0
  25. package/src/server/http.ts +46 -4
  26. package/src/server/turn.ts +5 -0
  27. package/src/util/requestid.ts +77 -0
  28. package/src/util/schema.ts +42 -3
  29. package/src/util/sqlite.ts +20 -1
  30. package/src/wire/openai/request.ts +8 -0
  31. package/src/wire/types.ts +18 -0
  32. package/test/benchmark-feeds.test.ts +194 -0
  33. package/test/catalog-view.test.ts +11 -0
  34. package/test/config.test.ts +26 -0
  35. package/test/context-token.test.ts +301 -0
  36. package/test/failover.test.ts +1 -1
  37. package/test/ledger-sql.test.ts +1 -1
  38. package/test/mcp-entry.test.ts +14 -5
  39. package/test/migrations.test.ts +11 -4
  40. package/test/reconfigure.test.ts +55 -0
  41. package/test/request-id.test.ts +424 -0
  42. package/test/schema.test.ts +2 -2
  43. package/test/tier-plan.test.ts +51 -0
  44. package/test/trust-attribution.test.ts +2 -2
  45. package/test/turn.test.ts +56 -1
@@ -271,7 +271,7 @@ describe("v4 migration", () => {
271
271
  await db.close();
272
272
  });
273
273
 
274
- test("schema is at user_version 19", () => {
274
+ test("schema is at user_version 20", () => {
275
275
  // `PRAGMA user_version` is SQLite's own migration marker, so this reads
276
276
  // through the bootstrap handle rather than the engine-agnostic one.
277
277
  const path = join(tmpdir(), `uv-${process.pid}-${Date.now()}.db`);
@@ -279,7 +279,7 @@ describe("v4 migration", () => {
279
279
  try {
280
280
  // Our own pragma against our own file; the shape is fixed by SQLite.
281
281
  const row = db.query("PRAGMA user_version").get() as { user_version: number } | null;
282
- expect(row?.user_version).toBe(19);
282
+ expect(row?.user_version).toBe(20);
283
283
  } finally {
284
284
  db.close();
285
285
  }
package/test/turn.test.ts CHANGED
@@ -18,6 +18,7 @@ import { runTurn } from "../src/server/turn.ts";
18
18
  import { BudgetExceededError } from "../src/router/select.ts";
19
19
  import { parseMessagesRequest } from "../src/wire/anthropic/messages.ts";
20
20
  import { parseChatRequest } from "../src/wire/openai/request.ts";
21
+ import { isMintedRequestId } from "../src/util/requestid.ts";
21
22
  import { UpstreamError, type DispatchOptions, type UpstreamClient } from "../src/upstream/types.ts";
22
23
  import type {
23
24
  FinishReason,
@@ -51,7 +52,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
51
52
  data: { axis: "intelligence", minQuality: 0 },
52
53
  chat: { axis: "intelligence", minQuality: 0 },
53
54
  },
54
- 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 },
55
+ 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 },
55
56
  classifier: {
56
57
  ambiguityThreshold: 0,
57
58
  model: "test/adjudicator", learnedModelPath: "",
@@ -603,6 +604,60 @@ describe("the context scope reaches the ledger", () => {
603
604
  });
604
605
  });
605
606
 
607
+ describe("the request id reaches the ledger", () => {
608
+ const runOne = async (req: NormRequest) => {
609
+ const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" })]);
610
+ const { upstream } = mkUpstream([{ kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("hi"), finishChunk("stop"), usageChunk({ promptTokens: 10, completionTokens: 2 }, 0.0001)] }]);
611
+ const { ledger, entries } = mkLedger();
612
+ const { store } = mkConversations();
613
+ const { sink } = mkSink();
614
+ await runTurn(req, sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal);
615
+ return entries;
616
+ };
617
+
618
+ test("the front door's id is recorded verbatim, so a quoted id names this exact turn", async () => {
619
+ const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers({ "x-request-id": "abc123" }));
620
+ expect((await runOne(req))[0]!.requestId).toBe("abc123");
621
+ });
622
+
623
+ test("a caller that sends none still gets an addressable row, marked as minted", async () => {
624
+ const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers());
625
+ const id = (await runOne(req))[0]!.requestId ?? "";
626
+ expect(isMintedRequestId(id)).toBe(true);
627
+ // And it is the id the caller was handed back, not a second one invented
628
+ // for the row: the header the wire read is what the ledger stores.
629
+ expect(id).toBe(req.requestId ?? "");
630
+ });
631
+
632
+ test("every attempt of an escalated turn files under the ONE id the caller holds", async () => {
633
+ // The customer made one request; support must land on the whole turn,
634
+ // including the attempt that was thrown away.
635
+ const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" }), mkDecision("simple", "better/model", { escalateTo: "moderate" })]);
636
+ const { upstream } = mkUpstream([
637
+ { kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("I'm sorry, but I can't help with that request."), finishChunk("stop")] },
638
+ { kind: "chunks", chunks: [startChunk("better/model"), textChunk("Here is the answer."), finishChunk("stop"), usageChunk({ promptTokens: 130, completionTokens: 6 }, 0.0009)] },
639
+ ]);
640
+ const { ledger, entries } = mkLedger();
641
+ const { store } = mkConversations();
642
+ const { sink } = mkSink();
643
+ const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers({ "x-request-id": "one-request" }));
644
+
645
+ await runTurn(req, sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal);
646
+
647
+ expect(entries).toHaveLength(2);
648
+ expect(entries.map((e) => e.requestId)).toEqual(["one-request", "one-request"]);
649
+ });
650
+
651
+ test("a hostile header never reaches the row; the turn is recorded under a minted id instead", async () => {
652
+ for (const hostile of ["x".repeat(129), "id with spaces", "amr-forged0000", '"><script>', "tab\tseparated"]) {
653
+ const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers({ "x-request-id": hostile }));
654
+ const id = (await runOne(req))[0]!.requestId ?? "";
655
+ expect(id).not.toBe(hostile);
656
+ expect(isMintedRequestId(id)).toBe(true);
657
+ }
658
+ });
659
+ });
660
+
606
661
  describe("exploration reaches the ledger", () => {
607
662
  test("an explored turn records the tier it was dropped from", async () => {
608
663
  const explored = { ...mkDecision("simple", "cheap/model"), explored: { from: "moderate" as Tier, to: "simple" as Tier } };