auto-model-router 0.30.3 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.omp-plugin/marketplace.json +2 -2
  2. package/README.md +32 -2
  3. package/omp-extension/router-configure.ts +9 -7
  4. package/package.json +1 -1
  5. package/src/cli/config-cmd.ts +8 -7
  6. package/src/cli/explain.ts +10 -5
  7. package/src/cli/export.ts +6 -5
  8. package/src/cli/models.ts +10 -7
  9. package/src/cli/report.ts +6 -1
  10. package/src/cli/stats.ts +7 -7
  11. package/src/config/load.ts +10 -1
  12. package/src/config/types.ts +10 -1
  13. package/src/context/bridge.ts +7 -7
  14. package/src/context/index.ts +3 -3
  15. package/src/context/store.ts +39 -56
  16. package/src/context/types.ts +7 -6
  17. package/src/cost/blended.ts +28 -7
  18. package/src/cost/feedback.ts +33 -37
  19. package/src/cost/ledger-sql.ts +547 -0
  20. package/src/cost/ledger.ts +30 -459
  21. package/src/cost/report.ts +171 -129
  22. package/src/cost/retention.ts +10 -10
  23. package/src/cost/summary.ts +15 -10
  24. package/src/cost/types.ts +43 -62
  25. package/src/cost/views.ts +79 -49
  26. package/src/eval/calibrate.ts +47 -12
  27. package/src/eval/run.ts +18 -2
  28. package/src/lib.ts +6 -2
  29. package/src/router/candidates.ts +7 -15
  30. package/src/router/classify.ts +6 -4
  31. package/src/router/index.ts +95 -9
  32. package/src/router/select.ts +38 -21
  33. package/src/router/state.ts +90 -102
  34. package/src/router/types.ts +11 -5
  35. package/src/server/advise.ts +6 -4
  36. package/src/server/compaction-digest.ts +1 -1
  37. package/src/server/digest.ts +9 -10
  38. package/src/server/http.ts +109 -46
  39. package/src/server/providers.ts +18 -4
  40. package/src/server/turn.ts +32 -9
  41. package/src/tokens/estimate.ts +16 -6
  42. package/src/upstream/ollama-usage.ts +21 -11
  43. package/src/util/schema.ts +201 -0
  44. package/src/util/sql.ts +246 -0
  45. package/src/wire/anthropic/messages.ts +3 -4
  46. package/src/wire/openai/request.ts +1 -0
  47. package/src/wire/types.ts +7 -0
  48. package/test/anthropic-wire.test.ts +9 -9
  49. package/test/benchmark-feeds.test.ts +7 -7
  50. package/test/cache-control.test.ts +7 -7
  51. package/test/cache-estimate.test.ts +5 -5
  52. package/test/catalog-view.test.ts +4 -4
  53. package/test/catalog.test.ts +11 -11
  54. package/test/classify.test.ts +24 -24
  55. package/test/compaction.test.ts +20 -20
  56. package/test/config-wizard.test.ts +32 -32
  57. package/test/config.test.ts +10 -10
  58. package/test/connect-harnesses.test.ts +11 -11
  59. package/test/context-bridge.test.ts +40 -30
  60. package/test/context-prune.test.ts +43 -36
  61. package/test/context-query.test.ts +8 -8
  62. package/test/controls.test.ts +54 -27
  63. package/test/cost.test.ts +12 -12
  64. package/test/digest.test.ts +55 -44
  65. package/test/embed-lifecycle.test.ts +5 -5
  66. package/test/embed-logic.test.ts +26 -26
  67. package/test/escalate.test.ts +17 -17
  68. package/test/eval.test.ts +73 -16
  69. package/test/executable.test.ts +6 -6
  70. package/test/exploration.test.ts +19 -20
  71. package/test/failover.test.ts +22 -21
  72. package/test/fakes.ts +105 -0
  73. package/test/features.test.ts +21 -21
  74. package/test/harness-requests.test.ts +3 -3
  75. package/test/harness-switch.test.ts +5 -5
  76. package/test/hold-exploration.test.ts +13 -13
  77. package/test/hot-reload.test.ts +5 -5
  78. package/test/learned.test.ts +5 -5
  79. package/test/ledger-sql.test.ts +342 -0
  80. package/test/mcp-entry.test.ts +5 -5
  81. package/test/migrations.test.ts +28 -22
  82. package/test/models-yml.test.ts +18 -18
  83. package/test/ollama.test.ts +40 -34
  84. package/test/omp-credentials.test.ts +16 -16
  85. package/test/policy.test.ts +3 -3
  86. package/test/reconfigure.test.ts +4 -4
  87. package/test/redaction.test.ts +41 -35
  88. package/test/remote.test.ts +12 -12
  89. package/test/report-logic.test.ts +8 -8
  90. package/test/report.test.ts +95 -87
  91. package/test/retention.test.ts +79 -66
  92. package/test/schema.test.ts +123 -0
  93. package/test/scope.test.ts +8 -8
  94. package/test/select.test.ts +216 -257
  95. package/test/skills.test.ts +3 -3
  96. package/test/sql-shim.test.ts +154 -0
  97. package/test/state.test.ts +43 -36
  98. package/test/summary.test.ts +38 -27
  99. package/test/tier-plan.test.ts +45 -62
  100. package/test/toast-logic.test.ts +31 -31
  101. package/test/tokens.test.ts +95 -80
  102. package/test/trust-attribution.test.ts +217 -187
  103. package/test/trust-window.test.ts +37 -32
  104. package/test/turn.test.ts +55 -23
  105. package/test/upstreams.test.ts +13 -13
  106. package/test/views.test.ts +81 -59
  107. package/test/wire-request.test.ts +17 -17
  108. package/test/wire-responses.test.ts +4 -4
  109. package/tools/agentdox-e2e.ts +5 -2
  110. package/tools/export-benchmarks.ts +5 -5
  111. package/tools/ledger-parity.ts +266 -0
  112. package/tools/replay.ts +16 -8
@@ -26,6 +26,13 @@ const OUTPUT_TO_INPUT_PRIOR = 5;
26
26
  const CACHE_READ_TO_INPUT_PRIOR = 0.1;
27
27
  const CACHE_WRITE_TO_INPUT_PRIOR = 1.25;
28
28
 
29
+ /** One window row, already parsed, whichever engine produced it. */
30
+ export interface BlendSample {
31
+ usage: UsageCounts;
32
+ reportedUsd: number;
33
+ breakdown: CostBreakdown;
34
+ }
35
+
29
36
  export function computeBlendedRate(db: Database, cfg: RouterConfig, windowDays: number): BlendedRate | null {
30
37
  const sinceMs = Date.now() - windowDays * 86_400_000;
31
38
  // We own the schema; row shape fixed by util/sqlite.ts and cost/ledger.ts.
@@ -35,7 +42,23 @@ export function computeBlendedRate(db: Database, cfg: RouterConfig, windowDays:
35
42
  WHERE created_at_ms >= ? AND reported_usd IS NOT NULL AND cost_breakdown IS NOT NULL`,
36
43
  )
37
44
  .all(sinceMs) as BlendRow[];
45
+ return foldBlendSamples(
46
+ rows.map((row) => ({
47
+ usage: JSON.parse(row.usage) as UsageCounts,
48
+ reportedUsd: row.reported_usd,
49
+ breakdown: JSON.parse(row.cost_breakdown) as CostBreakdown,
50
+ })),
51
+ cfg,
52
+ windowDays,
53
+ );
54
+ }
38
55
 
56
+ /**
57
+ * The apportioning itself, over already-parsed rows. Pure so both the local
58
+ * and the shared-database ledger produce the same blend from the same window
59
+ * instead of each carrying a copy of the arithmetic.
60
+ */
61
+ export function foldBlendSamples(samples: readonly BlendSample[], cfg: RouterConfig, windowDays: number): BlendedRate | null {
39
62
  let sampleCount = 0;
40
63
  let inputUsd = 0;
41
64
  let inputTokens = 0;
@@ -46,22 +69,20 @@ export function computeBlendedRate(db: Database, cfg: RouterConfig, windowDays:
46
69
  let cacheWriteUsd = 0;
47
70
  let cacheWriteTokens = 0;
48
71
 
49
- for (const row of rows) {
50
- const usage = JSON.parse(row.usage) as UsageCounts;
51
- const split = JSON.parse(row.cost_breakdown) as CostBreakdown;
72
+ for (const { usage, reportedUsd, breakdown: split } of samples) {
52
73
  const tokenSplit = split.freshPrompt + split.cacheRead + split.cacheWrite + split.completion + split.reasoning;
53
74
  // Entries whose usage produced no token-billed cost (pure image/request
54
75
  // billing) carry no price signal per token; skip them.
55
76
  if (tokenSplit <= 0) continue;
56
77
  const freshTokens = Math.max(usage.promptTokens - usage.cachedTokens - usage.cacheWriteTokens, 0);
57
78
 
58
- inputUsd += (row.reported_usd * split.freshPrompt) / tokenSplit;
79
+ inputUsd += (reportedUsd * split.freshPrompt) / tokenSplit;
59
80
  inputTokens += freshTokens;
60
- outputUsd += (row.reported_usd * (split.completion + split.reasoning)) / tokenSplit;
81
+ outputUsd += (reportedUsd * (split.completion + split.reasoning)) / tokenSplit;
61
82
  outputTokens += usage.completionTokens;
62
- cacheReadUsd += (row.reported_usd * split.cacheRead) / tokenSplit;
83
+ cacheReadUsd += (reportedUsd * split.cacheRead) / tokenSplit;
63
84
  cacheReadTokens += usage.cachedTokens;
64
- cacheWriteUsd += (row.reported_usd * split.cacheWrite) / tokenSplit;
85
+ cacheWriteUsd += (reportedUsd * split.cacheWrite) / tokenSplit;
65
86
  cacheWriteTokens += usage.cacheWriteTokens;
66
87
  sampleCount += 1;
67
88
  }
@@ -7,7 +7,7 @@
7
7
  * judged, so it aggregates by served model, tier and task.
8
8
  */
9
9
 
10
- import type { Database } from "bun:sqlite";
10
+ import { num, type SqlDb } from "../util/sql.ts";
11
11
 
12
12
  export type Verdict = "good" | "bad";
13
13
 
@@ -25,57 +25,53 @@ export interface FeedbackCounts {
25
25
  bad: number;
26
26
  }
27
27
 
28
+ /** Asynchronous throughout: the store may be a shared database, not a file. */
28
29
  export interface FeedbackStore {
29
- record(rec: FeedbackRecord, nowMs?: number): string;
30
+ record(rec: FeedbackRecord, nowMs?: number): Promise<string>;
30
31
  /** Verdict counts per served slug since `sinceMs`. */
31
- countsBySlug(sinceMs: number, harnessId?: string): Map<string, FeedbackCounts>;
32
+ countsBySlug(sinceMs: number, harnessId?: string): Promise<Map<string, FeedbackCounts>>;
32
33
  /** Verdicts for one ledger row (a user may re-judge). */
33
- forLedgerId(ledgerId: string): Array<{ verdict: Verdict; note: string; createdAtMs: number }>;
34
+ forLedgerId(ledgerId: string): Promise<Array<{ verdict: Verdict; note: string; createdAtMs: number }>>;
34
35
  }
35
36
 
36
- export function createFeedbackStore(db: Database): FeedbackStore {
37
- const insert = db.query(
38
- `INSERT INTO feedback (id, ledger_id, omp_session_id, slug, tier, verdict, note, created_at_ms)
39
- VALUES ($id, $ledgerId, $ompSessionId, $slug, $tier, $verdict, $note, $createdAtMs)`,
40
- );
41
- const bySlug = db.query(
42
- `SELECT f.slug, f.verdict, COUNT(*) AS n FROM feedback f
43
- LEFT JOIN ledger l ON l.id = f.ledger_id
44
- WHERE f.created_at_ms >= $since AND ($harness = '' OR l.harness_id = $harness)
45
- GROUP BY f.slug, f.verdict`,
46
- );
47
- const forRow = db.query("SELECT verdict, note, created_at_ms FROM feedback WHERE ledger_id = ? ORDER BY created_at_ms DESC");
37
+ export function createFeedbackStore(db: SqlDb): FeedbackStore {
38
+ const { sql } = db;
48
39
  return {
49
- record(rec, nowMs = Date.now()) {
40
+ async record(rec, nowMs = Date.now()) {
50
41
  const id = crypto.randomUUID();
51
- insert.run({
52
- $id: id,
53
- $ledgerId: rec.ledgerId,
54
- $ompSessionId: rec.ompSessionId,
55
- $slug: rec.slug,
56
- $tier: rec.tier,
57
- $verdict: rec.verdict,
58
- $note: rec.note.slice(0, 500),
59
- $createdAtMs: nowMs,
60
- });
42
+ await sql`
43
+ INSERT INTO feedback (id, ledger_id, omp_session_id, slug, tier, verdict, note, created_at_ms)
44
+ VALUES (${id}, ${rec.ledgerId}, ${rec.ompSessionId}, ${rec.slug}, ${rec.tier}, ${rec.verdict},
45
+ ${rec.note.slice(0, 500)}, ${nowMs})`;
61
46
  return id;
62
47
  },
63
- countsBySlug(sinceMs, harnessId = "") {
48
+
49
+ async countsBySlug(sinceMs, harnessId = "") {
64
50
  const out = new Map<string, FeedbackCounts>();
65
- for (const r of bySlug.all({ $since: sinceMs, $harness: harnessId }) as { slug: string; verdict: string; n: number }[]) {
51
+ // An empty harness means "every harness"; the join only matters when
52
+ // scoping, because the judging harness lives on the ledger row.
53
+ const rows = await db.query<{ slug: string; verdict: string; n: unknown }>(
54
+ `SELECT f.slug, f.verdict, COUNT(*) AS n FROM feedback f
55
+ LEFT JOIN ledger l ON l.id = f.ledger_id
56
+ WHERE f.created_at_ms >= $since AND ($harness = '' OR l.harness_id = $harness)
57
+ GROUP BY f.slug, f.verdict`,
58
+ { since: sinceMs, harness: harnessId },
59
+ );
60
+ for (const r of rows) {
66
61
  const c = out.get(r.slug) ?? { good: 0, bad: 0 };
67
- if (r.verdict === "good") c.good += r.n;
68
- else c.bad += r.n;
62
+ if (r.verdict === "good") c.good += num(r.n);
63
+ else c.bad += num(r.n);
69
64
  out.set(r.slug, c);
70
65
  }
71
66
  return out;
72
67
  },
73
- forLedgerId(ledgerId) {
74
- return (forRow.all(ledgerId) as { verdict: Verdict; note: string; created_at_ms: number }[]).map((r) => ({
75
- verdict: r.verdict,
76
- note: r.note,
77
- createdAtMs: r.created_at_ms,
78
- }));
68
+
69
+ async forLedgerId(ledgerId) {
70
+ const rows = await db.query<{ verdict: Verdict; note: string; created_at_ms: unknown }>(
71
+ "SELECT verdict, note, created_at_ms FROM feedback WHERE ledger_id = $id ORDER BY created_at_ms DESC",
72
+ { id: ledgerId },
73
+ );
74
+ return rows.map((r) => ({ verdict: r.verdict, note: r.note, createdAtMs: num(r.created_at_ms) }));
79
75
  },
80
76
  };
81
77
  }