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
@@ -18,7 +18,11 @@ import { bareCloudName, ollamaRateFor } from "../src/catalog/ollama-prices.ts";
18
18
  import { normalizeCatalogModel } from "../src/catalog/openrouter-catalog.ts";
19
19
  import type { CatalogModel, CatalogSnapshot, CatalogSource } from "../src/catalog/types.ts";
20
20
  import { DEFAULT_CONFIG } from "../src/config/defaults.ts";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
21
23
  import { openDb } from "../src/util/sqlite.ts";
24
+ import { migrateStore } from "../src/util/schema.ts";
25
+ import { num, openSqlDb } from "../src/util/sql.ts";
22
26
  import type { OllamaConfig, RouterConfig } from "../src/config/types.ts";
23
27
  import { buildCandidates } from "../src/router/candidates.ts";
24
28
  import { extractFeatures } from "../src/router/features.ts";
@@ -80,20 +84,20 @@ function listings(source: "daemon" | "ollama.com" = "daemon"): OllamaListing[] {
80
84
  }
81
85
 
82
86
  describe("ollama prices", () => {
83
- test("bare cloud name strips the daemon decoration", () => {
87
+ test("bare cloud name strips the daemon decoration", async () => {
84
88
  expect(bareCloudName("glm-5.3-flash:cloud")).toBe("glm-5.3-flash");
85
89
  expect(bareCloudName("deepseek-v4-pro:0813-cloud")).toBe("deepseek-v4-pro:0813");
86
90
  expect(bareCloudName("GPT-OSS:120b")).toBe("gpt-oss:120b");
87
91
  });
88
92
 
89
- test("tagged rate wins, base rate covers other tags, unknown is null", () => {
93
+ test("tagged rate wins, base rate covers other tags, unknown is null", async () => {
90
94
  expect(ollamaRateFor("gpt-oss:120b-cloud")?.key).toBe("gpt-oss:120b");
91
95
  expect(ollamaRateFor("deepseek-v4-pro:0813")?.key).toBe("deepseek-v4-pro");
92
96
  expect(ollamaRateFor("mistral-large-3:675b")?.key).toBe("mistral-large-3");
93
97
  expect(ollamaRateFor("mystery-model")).toBeNull();
94
98
  });
95
99
 
96
- test("config overrides beat the shipped snapshot and can add models", () => {
100
+ test("config overrides beat the shipped snapshot and can add models", async () => {
97
101
  const o = { "glm-5.3-flash": { input: 0.1, output: 0.2 }, "mystery-model": { input: 1, output: 2 } };
98
102
  expect(ollamaRateFor("glm-5.3-flash:cloud", o)?.rate.input).toBe(0.1);
99
103
  expect(ollamaRateFor("mystery-model:cloud", o)?.rate.output).toBe(2);
@@ -101,7 +105,7 @@ describe("ollama prices", () => {
101
105
  });
102
106
 
103
107
  describe("ollama listing + show parsing", () => {
104
- test("daemon records carry context, capabilities and the remote name", () => {
108
+ test("daemon records carry context, capabilities and the remote name", async () => {
105
109
  const l = parseOllamaListing(DAEMON_TAGS[0], "daemon")!;
106
110
  expect(l.id).toBe("glm-5.3-flash:cloud");
107
111
  expect(l.remoteModel).toBe("glm-5.3-flash");
@@ -112,20 +116,20 @@ describe("ollama listing + show parsing", () => {
112
116
  expect(parseOllamaListing(DAEMON_TAGS[4], "daemon")!.isCloud).toBe(false);
113
117
  });
114
118
 
115
- test("ollama.com records are all cloud, with the id as the remote name", () => {
119
+ test("ollama.com records are all cloud, with the id as the remote name", async () => {
116
120
  const l = parseOllamaListing({ name: "glm-5.3-flash", model: "glm-5.3-flash", details: {} }, "ollama.com")!;
117
121
  expect(l.isCloud).toBe(true);
118
122
  expect(l.remoteModel).toBe("glm-5.3-flash");
119
123
  expect(l.contextLength).toBeNull();
120
124
  });
121
125
 
122
- test("show yields the architecture's context length and capabilities", () => {
126
+ test("show yields the architecture's context length and capabilities", async () => {
123
127
  const s = parseOllamaShow({ capabilities: ["completion", "tools"], model_info: { "glm5_next.context_length": 1048576, "glm5_next.embedding_length": 4096 } });
124
128
  expect(s.contextLength).toBe(1048576);
125
129
  expect(s.capabilities).toEqual(["completion", "tools"]);
126
130
  });
127
131
 
128
- test("url helpers", () => {
132
+ test("url helpers", async () => {
129
133
  expect(isOllamaDotCom("https://ollama.com/v1")).toBe(true);
130
134
  expect(isOllamaDotCom("http://127.0.0.1:11434/v1")).toBe(false);
131
135
  expect(ollamaApiRoot("https://ollama.com/v1/")).toBe("https://ollama.com");
@@ -135,7 +139,7 @@ describe("ollama listing + show parsing", () => {
135
139
  });
136
140
 
137
141
  describe("buildOllamaModels", () => {
138
- test("prices, twins, capabilities and context are assembled; unpriced and local models are dropped", () => {
142
+ test("prices, twins, capabilities and context are assembled; unpriced and local models are dropped", async () => {
139
143
  const models = buildOllamaModels({ listings: listings(), openrouter: OR_MODELS, cfg: OLLAMA, log });
140
144
  const slugs = models.map((m) => m.slug).sort();
141
145
  expect(slugs).toEqual(["ollama/deepseek-v4-pro:0813-cloud", "ollama/glm-5.3-flash:cloud", "ollama/gpt-oss:120b-cloud"]);
@@ -164,7 +168,7 @@ describe("buildOllamaModels", () => {
164
168
  expect(ds.quality).toEqual({});
165
169
  });
166
170
 
167
- test("a model Ollama lists but the price table does not know is priced from its twin", () => {
171
+ test("a model Ollama lists but the price table does not know is priced from its twin", async () => {
168
172
  // Ollama publishes no prices, so they come from a static table. deepseek-v4.1-flash was
169
173
  // listed by /api/tags and dropped here for want of an entry, so routing never saw it at
170
174
  // all — a model the provider was actively offering. The twin sells the same weights.
@@ -180,19 +184,19 @@ describe("buildOllamaModels", () => {
180
184
  expect(or.price.prompt).toBeGreaterThan(0);
181
185
  });
182
186
 
183
- test("a pinned twin beats the name match", () => {
187
+ test("a pinned twin beats the name match", async () => {
184
188
  const cfg: OllamaConfig = { ...OLLAMA, twins: { "deepseek-v4-pro": "moonshotai/kimi-k3" } };
185
189
  const ds = buildOllamaModels({ listings: listings(), openrouter: OR_MODELS, cfg, log }).find((m) => m.slug.startsWith("ollama/deepseek"))!;
186
190
  expect(ds.quality.coding).toBe(76.2);
187
191
  });
188
192
 
189
- test("includeLocal admits a daemon-local model only when priced", () => {
193
+ test("includeLocal admits a daemon-local model only when priced", async () => {
190
194
  const cfg: OllamaConfig = { ...OLLAMA, includeLocal: true, prices: { "nomic-embed-text": { input: 0, output: 0 } } };
191
195
  const models = buildOllamaModels({ listings: listings(), openrouter: OR_MODELS, cfg, log });
192
196
  expect(models.some((m) => m.slug === "ollama/nomic-embed-text:latest")).toBe(true);
193
197
  });
194
198
 
195
- test("ollama.com listings with no metadata fall back to the twin's context and capabilities", () => {
199
+ test("ollama.com listings with no metadata fall back to the twin's context and capabilities", async () => {
196
200
  const l = parseOllamaListing({ name: "glm-5.3-flash", model: "glm-5.3-flash", details: {} }, "ollama.com")!;
197
201
  const [m] = buildOllamaModels({ listings: [l], openrouter: OR_MODELS, cfg: OLLAMA, log });
198
202
  expect(m!.slug).toBe("ollama/glm-5.3-flash");
@@ -253,7 +257,7 @@ describe("createOllamaCatalog", () => {
253
257
  });
254
258
 
255
259
  describe("toOllamaBody", () => {
256
- test("strips OpenRouter-only fields, maps reasoning, drops cache_control, requests usage", () => {
260
+ test("strips OpenRouter-only fields, maps reasoning, drops cache_control, requests usage", async () => {
257
261
  const body = toOllamaBody({
258
262
  model: "ollama/glm-5.3-flash:cloud",
259
263
  models: ["ollama/glm-5.3-flash:cloud", "ollama/gpt-oss:120b-cloud"],
@@ -278,7 +282,7 @@ describe("toOllamaBody", () => {
278
282
  expect(sys[0]).toEqual({ type: "text", text: "sys" });
279
283
  });
280
284
 
281
- test("reasoning off is simply omitted", () => {
285
+ test("reasoning off is simply omitted", async () => {
282
286
  const body = toOllamaBody({ model: "ollama/x", reasoning: { enabled: false }, stream: false });
283
287
  expect(body.reasoning_effort).toBeUndefined();
284
288
  expect(body.stream_options).toBeUndefined();
@@ -286,17 +290,17 @@ describe("toOllamaBody", () => {
286
290
  });
287
291
 
288
292
  describe("classifyOllamaStatus", () => {
289
- test("402 is quota: retryable and account-level", () => {
293
+ test("402 is quota: retryable and account-level", async () => {
290
294
  const e = classifyOllamaStatus(402, { error: { message: "out of credits" } });
291
295
  expect(e.kind).toBe("quota");
292
296
  expect(e.retryable).toBe(true);
293
297
  });
294
- test("429 is a rate limit; 401 is auth; 404 is model_unavailable", () => {
298
+ test("429 is a rate limit; 401 is auth; 404 is model_unavailable", async () => {
295
299
  expect(classifyOllamaStatus(429, {}).kind).toBe("rate_limit");
296
300
  expect(classifyOllamaStatus(401, {}).retryable).toBe(false);
297
301
  expect(classifyOllamaStatus(404, {}).kind).toBe("model_unavailable");
298
302
  });
299
- test("a 403 about billing is quota, any other 403 is moderation", () => {
303
+ test("a 403 about billing is quota, any other 403 is moderation", async () => {
300
304
  expect(classifyOllamaStatus(403, { error: "plan limit reached" }).kind).toBe("quota");
301
305
  expect(classifyOllamaStatus(403, { error: "content blocked" }).kind).toBe("moderation");
302
306
  });
@@ -462,14 +466,13 @@ describe("selection over a mixed catalog", () => {
462
466
  tier: "simple",
463
467
  task: "coding",
464
468
  snapshot,
465
- ledger: null,
466
469
  cfg: { ...BASE, adaptiveTierFloors: false, ollama: { ...OLLAMA, costBias } },
467
470
  expectedCompletionTokens: 512,
468
471
  warmSlug: null,
469
472
  });
470
473
  }
471
474
 
472
- test("Ollama models rank alongside OpenRouter ones on the same economics", () => {
475
+ test("Ollama models rank alongside OpenRouter ones on the same economics", async () => {
473
476
  const { candidates } = build(1);
474
477
  const slugs = candidates.map((c) => c.model.slug);
475
478
  expect(slugs).toContain("ollama/glm-5.3-flash:cloud");
@@ -478,7 +481,7 @@ describe("selection over a mixed catalog", () => {
478
481
  expect(slugs.indexOf("z-ai/glm-5.3-flash")).toBeLessThan(slugs.indexOf("ollama/glm-5.3-flash:cloud"));
479
482
  });
480
483
 
481
- test("costBias below 1 tilts the ranking toward Ollama and says so", () => {
484
+ test("costBias below 1 tilts the ranking toward Ollama and says so", async () => {
482
485
  const { candidates } = build(0.25);
483
486
  const slugs = candidates.map((c) => c.model.slug);
484
487
  expect(slugs.indexOf("ollama/glm-5.3-flash:cloud")).toBeLessThan(slugs.indexOf("z-ai/glm-5.3-flash"));
@@ -493,7 +496,7 @@ describe("ollama plan usage (credit-aware bias)", () => {
493
496
  limits: { monthly: { usage: 0, models: [{ name: "glm-5.3", request_count: 1 }, { name: "nemotron-3-super", request_count: 2 }] } },
494
497
  };
495
498
 
496
- test("parses the observed payload", () => {
499
+ test("parses the observed payload", async () => {
497
500
  const u = parseOllamaUsage(PAYLOAD, 5)!;
498
501
  expect(u.monthlyUsedFraction).toBe(0);
499
502
  expect(u.monthlyUsageRaw).toBe(0);
@@ -504,7 +507,7 @@ describe("ollama plan usage (credit-aware bias)", () => {
504
507
  expect(parseOllamaUsage("nope")).toBeNull();
505
508
  });
506
509
 
507
- test("infers the usage scale: percent above 1, fraction at or below 1, exactly 1 read as 1%", () => {
510
+ test("infers the usage scale: percent above 1, fraction at or below 1, exactly 1 read as 1%", async () => {
508
511
  expect(usageFraction(0)).toBe(0);
509
512
  expect(usageFraction(37)).toBeCloseTo(0.37, 6);
510
513
  expect(usageFraction(250)).toBe(1);
@@ -512,7 +515,7 @@ describe("ollama plan usage (credit-aware bias)", () => {
512
515
  expect(usageFraction(1)).toBeCloseTo(0.01, 6);
513
516
  });
514
517
 
515
- test("the bias holds under the threshold, switches to list price above it, and stays on when usage is unknown", () => {
518
+ test("the bias holds under the threshold, switches to list price above it, and stays on when usage is unknown", async () => {
516
519
  const at = (f: number | null) => (f === null ? null : { monthlyUsedFraction: f, monthlyUsageRaw: f, activityCostUsd: null, requestsThisMonth: 0, plan: null, fetchedAtMs: 0 });
517
520
  expect(effectiveOllamaBias(0.1, 0.9, at(0.5))).toBe(0.1);
518
521
  expect(effectiveOllamaBias(0.1, 0.9, at(0.9))).toBe(1);
@@ -582,7 +585,7 @@ describe("ollama plan usage (credit-aware bias)", () => {
582
585
  // Candidate scoring reads the bias off the snapshot, not the config.
583
586
  const req = parseChatRequest({ model: "auto", tools: [{ type: "function", function: { name: "read", description: "Read", parameters: { type: "object", properties: {} } } }], messages: [{ role: "user", content: "rename the helper" }] }, new Headers());
584
587
  const features = extractFeatures(req, 50_000);
585
- const rank = (snap: CatalogSnapshot) => buildCandidates({ req, features, tier: "simple", task: "coding", snapshot: snap, ledger: null, cfg: { ...BASE, adaptiveTierFloors: false, ollama: { ...OLLAMA, costBias: 1 } }, expectedCompletionTokens: 512, warmSlug: null }).candidates.map((c) => c.model.slug);
588
+ const rank = (snap: CatalogSnapshot) => buildCandidates({ req, features, tier: "simple", task: "coding", snapshot: snap, cfg: { ...BASE, adaptiveTierFloors: false, ollama: { ...OLLAMA, costBias: 1 } }, expectedCompletionTokens: 512, warmSlug: null }).candidates.map((c) => c.model.slug);
586
589
  expect(rank(a).indexOf("ollama/glm-5.3-flash:cloud")).toBeLessThan(rank(a).indexOf("z-ai/glm-5.3-flash"));
587
590
 
588
591
  used = 0.95; // credits nearly gone: list price
@@ -597,24 +600,24 @@ describe("ollama plan usage (credit-aware bias)", () => {
597
600
  describe("ollamaMeter", () => {
598
601
  const usage = (plan: string | null, frac: number | null = 0.104) => ({ monthlyUsedFraction: frac, monthlyUsageRaw: frac, activityCostUsd: 0, requestsThisMonth: 1250, plan, fetchedAtMs: 1 });
599
602
 
600
- test("a detected plan applies its published allowance: 10.4% of Pro's $60 is the $6.24 ollama.com shows", () => {
603
+ test("a detected plan applies its published allowance: 10.4% of Pro's $60 is the $6.24 ollama.com shows", async () => {
601
604
  expect(ollamaMeter(usage("pro"), 0)).toEqual({ usedUsd: 6.24, creditsUsd: 60, plan: "pro" });
602
605
  expect(ollamaMeter(usage("max"), 0)).toEqual({ usedUsd: 31.2, creditsUsd: 300, plan: "max" });
603
606
  });
604
607
 
605
- test("a configured override wins over the detected plan; an unknown plan without one yields no meter", () => {
608
+ test("a configured override wins over the detected plan; an unknown plan without one yields no meter", async () => {
606
609
  expect(ollamaMeter(usage("pro"), 100)).toEqual({ usedUsd: 10.4, creditsUsd: 100, plan: "pro" });
607
610
  expect(ollamaMeter(usage("team"), 0)).toBeNull();
608
611
  expect(ollamaMeter(usage("team"), 500)?.creditsUsd).toBe(500);
609
612
  expect(ollamaMeter(usage(null), 0)).toBeNull();
610
613
  });
611
614
 
612
- test("no usage reading yields no meter", () => {
615
+ test("no usage reading yields no meter", async () => {
613
616
  expect(ollamaMeter(null, 60)).toBeNull();
614
617
  expect(ollamaMeter(usage("pro", null), 60)).toBeNull();
615
618
  });
616
619
 
617
- test("parseOllamaPlan reads the account payload case-insensitively", () => {
620
+ test("parseOllamaPlan reads the account payload case-insensitively", async () => {
618
621
  expect(parseOllamaPlan({ ID: "x", Plan: "Pro" })).toBe("pro");
619
622
  expect(parseOllamaPlan({ plan: "max" })).toBe("max");
620
623
  expect(parseOllamaPlan({ Plan: "" })).toBeNull();
@@ -625,7 +628,7 @@ describe("ollamaMeter", () => {
625
628
  describe("ollama calibration", () => {
626
629
  const s = (h: number, meterUsd: number, ledgerUsd: number) => ({ atMs: h * 3_600_000, meterUsd, ledgerUsd });
627
630
 
628
- test("compares the newest reading with the oldest since the last meter reset", () => {
631
+ test("compares the newest reading with the oldest since the last meter reset", async () => {
629
632
  // Ledger estimated $4 over the span; the meter moved $5 ⇒ estimates run 20% low.
630
633
  const c = calibrationFrom([s(0, 10, 20), s(12, 12.5, 22), s(24, 15, 24)])!;
631
634
  expect(c.factor).toBeCloseTo(1.25, 6);
@@ -637,7 +640,7 @@ describe("ollama calibration", () => {
637
640
  expect(reset.factor).toBeCloseTo(1, 6);
638
641
  });
639
642
 
640
- test("needs enough metered spend to mean anything, and clamps to 0.5–2×", () => {
643
+ test("needs enough metered spend to mean anything, and clamps to 0.5–2×", async () => {
641
644
  expect(calibrationFrom([s(0, 1, 1), s(1, 1.03, 1.2)])).toBeNull(); // meter moved less than its resolution
642
645
  expect(calibrationFrom([s(0, 1, 1), s(1, 2, 1.3)])).toBeNull(); // ledger moved less than $0.50
643
646
  expect(calibrationFrom([s(0, 0, 0), s(1, 10, 1)])!.factor).toBe(CALIBRATION_MAX_FACTOR);
@@ -646,14 +649,16 @@ describe("ollama calibration", () => {
646
649
  });
647
650
 
648
651
  test("the source records a sample per poll and exposes the calibration", async () => {
649
- const db = openDb(":memory:");
652
+ // The calibration store is on the shim now, so this one needs a real file.
653
+ const db = openSqlDb(join(tmpdir(), `t-ollama.test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.db`));
654
+ await migrateStore(db);
650
655
  let ledgerUsd = 1;
651
656
  let frac = 0.1;
652
657
  const fetchImpl = async (url: string): Promise<Response> => {
653
658
  if (url.endsWith("/api/me")) return Response.json({ Plan: "pro" });
654
659
  return Response.json({ limits: { monthly: { usage: frac, models: [] } } });
655
660
  };
656
- const src = createOllamaUsageSource({ apiKey: () => "k", pollMs: 5, timeoutMs: 1000, log, fetchImpl, calibration: { db, ledgerUsd: () => ledgerUsd, planCreditsOverrideUsd: 0 } });
661
+ const src = createOllamaUsageSource({ apiKey: () => "k", pollMs: 5, timeoutMs: 1000, log, fetchImpl, calibration: { db, ledgerUsd: async () => ledgerUsd, planCreditsOverrideUsd: 0 } });
657
662
  await src.get(); // meter $6 (10% of $60), ledger $1
658
663
  expect(src.calibration()).toBeNull(); // one sample
659
664
  await new Promise((r) => setTimeout(r, 20));
@@ -662,7 +667,8 @@ describe("ollama calibration", () => {
662
667
  await src.get();
663
668
  const c = src.calibration()!;
664
669
  expect(c.factor).toBeCloseTo(1.5, 6);
665
- expect((db.query("SELECT COUNT(*) n FROM ollama_meter_samples").get() as { n: number }).n).toBe(2);
666
- db.close();
670
+ const samples = await db.one<{ n: unknown }>("SELECT COUNT(*) AS n FROM ollama_meter_samples");
671
+ expect(num(samples?.n)).toBe(2);
672
+ await db.close();
667
673
  });
668
674
  });
@@ -66,25 +66,25 @@ afterEach(() => {
66
66
  });
67
67
 
68
68
  describe("readOmpCredential", () => {
69
- test("reads an api_key credential in omp's stored shape", () => {
69
+ test("reads an api_key credential in omp's stored shape", async () => {
70
70
  // Matches the real `ollama-cloud` row: {"key": "...", "source": "..."}
71
71
  const path = storeWith([{ provider: "openrouter", type: "api_key", data: { key: "sk-or-real", source: "login" } }]);
72
72
  expect(readOmpCredential("openrouter", path)).toBe("sk-or-real");
73
73
  });
74
74
 
75
- test("returns null for a provider that is not logged in", () => {
75
+ test("returns null for a provider that is not logged in", async () => {
76
76
  const path = storeWith([{ provider: "anthropic", type: "oauth", data: { access: "x", expires: Date.now() + 1e6 } }]);
77
77
  expect(readOmpCredential("openrouter", path)).toBeNull();
78
78
  });
79
79
 
80
- test("accepts an unexpired oauth access token", () => {
80
+ test("accepts an unexpired oauth access token", async () => {
81
81
  const path = storeWith([
82
82
  { provider: "openrouter", type: "oauth", data: { access: "tok-live", refresh: "r", expires: Date.now() + 600_000 } },
83
83
  ]);
84
84
  expect(readOmpCredential("openrouter", path)).toBe("tok-live");
85
85
  });
86
86
 
87
- test("rejects an expired oauth token rather than burning a turn on a 401", () => {
87
+ test("rejects an expired oauth token rather than burning a turn on a 401", async () => {
88
88
  // Refreshing is omp's job; we hold the store read-only.
89
89
  const path = storeWith([
90
90
  { provider: "openrouter", type: "oauth", data: { access: "tok-stale", refresh: "r", expires: Date.now() - 1000 } },
@@ -92,14 +92,14 @@ describe("readOmpCredential", () => {
92
92
  expect(readOmpCredential("openrouter", path)).toBeNull();
93
93
  });
94
94
 
95
- test("skips a disabled credential", () => {
95
+ test("skips a disabled credential", async () => {
96
96
  const path = storeWith([
97
97
  { provider: "openrouter", type: "api_key", data: { key: "sk-or-dead" }, disabled: "revoked" },
98
98
  ]);
99
99
  expect(readOmpCredential("openrouter", path)).toBeNull();
100
100
  });
101
101
 
102
- test("prefers the most recently updated credential", () => {
102
+ test("prefers the most recently updated credential", async () => {
103
103
  const path = storeWith([
104
104
  { provider: "openrouter", type: "api_key", data: { key: "sk-or-old" }, updatedAt: 1000 },
105
105
  { provider: "openrouter", type: "api_key", data: { key: "sk-or-new" }, updatedAt: 2000 },
@@ -107,7 +107,7 @@ describe("readOmpCredential", () => {
107
107
  expect(readOmpCredential("openrouter", path)).toBe("sk-or-new");
108
108
  });
109
109
 
110
- test("survives a missing, unreadable, or unexpected store", () => {
110
+ test("survives a missing, unreadable, or unexpected store", async () => {
111
111
  expect(readOmpCredential("openrouter", join(tmpdir(), "definitely-absent-agent.db"))).toBeNull();
112
112
  const dir = mkdtempSync(join(tmpdir(), "ompr-cred-"));
113
113
  dirs.push(dir);
@@ -118,7 +118,7 @@ describe("readOmpCredential", () => {
118
118
  expect(readOmpCredential("openrouter", garbage)).toBeNull();
119
119
  });
120
120
 
121
- test("skips a row whose data is not valid JSON", () => {
121
+ test("skips a row whose data is not valid JSON", async () => {
122
122
  const dir = mkdtempSync(join(tmpdir(), "ompr-cred-"));
123
123
  dirs.push(dir);
124
124
  const path = join(dir, "agent.db");
@@ -133,7 +133,7 @@ describe("readOmpCredential", () => {
133
133
  expect(readOmpCredential("openrouter", path)).toBeNull();
134
134
  });
135
135
 
136
- test("declines to read a local store when a remote auth broker is configured", () => {
136
+ test("declines to read a local store when a remote auth broker is configured", async () => {
137
137
  const path = storeWith([{ provider: "openrouter", type: "api_key", data: { key: "sk-or-local" } }]);
138
138
  setEnv("OMP_AUTH_BROKER_URL", "https://broker.example");
139
139
  // Broker mode replaces the local store; reading it would be a stale lie.
@@ -142,20 +142,20 @@ describe("readOmpCredential", () => {
142
142
  });
143
143
 
144
144
  describe("resolveOpenRouterKey", () => {
145
- test("explicit configuration wins over the borrowed credential", () => {
145
+ test("explicit configuration wins over the borrowed credential", async () => {
146
146
  setEnv("OPENROUTER_API_KEY", undefined);
147
147
  const resolved = resolveOpenRouterKey("sk-or-explicit");
148
148
  expect(resolved.apiKey).toBe("sk-or-explicit");
149
149
  expect(resolved.source).toBe("config");
150
150
  });
151
151
 
152
- test("attributes a key that came from the environment", () => {
152
+ test("attributes a key that came from the environment", async () => {
153
153
  setEnv("OPENROUTER_API_KEY", "sk-or-from-env");
154
154
  const resolved = resolveOpenRouterKey("sk-or-from-env");
155
155
  expect(resolved.source).toBe("env");
156
156
  });
157
157
 
158
- test("reports an actionable message when nothing is configured", () => {
158
+ test("reports an actionable message when nothing is configured", async () => {
159
159
  setEnv("OPENROUTER_API_KEY", undefined);
160
160
  setEnv("PI_CODING_AGENT_DIR", mkdtempSync(join(tmpdir(), "ompr-empty-agent-")));
161
161
  dirs.push(process.env.PI_CODING_AGENT_DIR ?? "");
@@ -165,7 +165,7 @@ describe("resolveOpenRouterKey", () => {
165
165
  expect(resolved.detail).toContain("/login openrouter");
166
166
  });
167
167
 
168
- test("borrows omp's credential when nothing else is configured", () => {
168
+ test("borrows omp's credential when nothing else is configured", async () => {
169
169
  setEnv("OPENROUTER_API_KEY", undefined);
170
170
  const dir = mkdtempSync(join(tmpdir(), "ompr-agentdir-"));
171
171
  dirs.push(dir);
@@ -185,7 +185,7 @@ describe("resolveOpenRouterKey", () => {
185
185
  });
186
186
 
187
187
  describe("resolveOllamaKey", () => {
188
- test("explicit configuration wins, and an env-sourced key is attributed to OLLAMA_API_KEY", () => {
188
+ test("explicit configuration wins, and an env-sourced key is attributed to OLLAMA_API_KEY", async () => {
189
189
  setEnv("OLLAMA_API_KEY", undefined);
190
190
  expect(resolveOllamaKey("ok-explicit").source).toBe("config");
191
191
  setEnv("OLLAMA_API_KEY", "ok-from-env");
@@ -194,7 +194,7 @@ describe("resolveOllamaKey", () => {
194
194
  expect(resolved.detail).toBe("OLLAMA_API_KEY");
195
195
  });
196
196
 
197
- test("borrows omp's ollama-cloud credential when nothing else is configured", () => {
197
+ test("borrows omp's ollama-cloud credential when nothing else is configured", async () => {
198
198
  setEnv("OLLAMA_API_KEY", undefined);
199
199
  const dir = mkdtempSync(join(tmpdir(), "ompr-agentdir-"));
200
200
  dirs.push(dir);
@@ -214,7 +214,7 @@ describe("resolveOllamaKey", () => {
214
214
  expect(resolveOpenRouterKey("").source).toBe("none");
215
215
  });
216
216
 
217
- test("points at /login ollama-cloud when nothing resolves", () => {
217
+ test("points at /login ollama-cloud when nothing resolves", async () => {
218
218
  setEnv("OLLAMA_API_KEY", undefined);
219
219
  const dir = mkdtempSync(join(tmpdir(), "ompr-empty-agent-"));
220
220
  dirs.push(dir);
@@ -12,7 +12,7 @@ import { parseChatRequest, parsePolicyHeader } from "../src/wire/openai/request.
12
12
  */
13
13
 
14
14
  describe("parsePolicyHeader", () => {
15
- test("accepts the documented fields, drops junk, and never rejects a turn", () => {
15
+ test("accepts the documented fields, drops junk, and never rejects a turn", async () => {
16
16
  expect(parsePolicyHeader(null)).toBeUndefined();
17
17
  expect(parsePolicyHeader("not json")).toBeUndefined();
18
18
  expect(parsePolicyHeader("[]")).toBeUndefined();
@@ -28,7 +28,7 @@ describe("applyRequestPolicy", () => {
28
28
  const cfg = DEFAULT_CONFIG;
29
29
  const profile = resolveProfile(cfg, "auto");
30
30
 
31
- test("narrows the tier envelope, never widens it", () => {
31
+ test("narrows the tier envelope, never widens it", async () => {
32
32
  const r = applyRequestPolicy(profile, cfg, { maxTier: "moderate", minTier: "trivial" }, undefined);
33
33
  expect(r.profile.maxTier).toBe("moderate");
34
34
  expect(r.profile.minTier).toBe(profile.minTier);
@@ -41,7 +41,7 @@ describe("applyRequestPolicy", () => {
41
41
  expect(up.profile.maxTier).toBe(cheap.maxTier);
42
42
  });
43
43
 
44
- test("allow replaces, deny adds, and a pin forces unless a session override already did", () => {
44
+ test("allow replaces, deny adds, and a pin forces unless a session override already did", async () => {
45
45
  const base = { ...cfg, filters: { ...cfg.filters, allow: ["x/*"], deny: ["bad/*"] } };
46
46
  const r = applyRequestPolicy(profile, base, { allow: ["anthropic/*"], deny: ["openai/*"], pin: "anthropic/claude-sonnet-5" }, undefined);
47
47
  expect(r.cfg.filters.allow).toEqual(["anthropic/*"]);
@@ -16,7 +16,7 @@ import { openDb } from "../src/util/sqlite.ts";
16
16
  */
17
17
 
18
18
  describe("applying config in place", () => {
19
- test("blocks keep their identity, only leaves change, and the changed paths come back dotted", () => {
19
+ test("blocks keep their identity, only leaves change, and the changed paths come back dotted", async () => {
20
20
  const cfg = structuredClone(DEFAULT_CONFIG);
21
21
  const ollamaRef = cfg.ollama; // what a client binds at construction
22
22
  const changed = applyConfigPatch(cfg, { ollama: { enabled: true, apiKey: "k" } });
@@ -27,7 +27,7 @@ describe("applying config in place", () => {
27
27
  expect(applyConfigPatch(cfg, { ollama: { apiKey: "k" } })).toEqual([]);
28
28
  });
29
29
 
30
- test("a patch touches only what it names; a full apply prunes what the source dropped", () => {
30
+ test("a patch touches only what it names; a full apply prunes what the source dropped", async () => {
31
31
  const target: Record<string, unknown> = { a: { x: 1, y: 2 }, b: 3 };
32
32
  expect(assignInPlace(target, { a: { x: 9 } })).toEqual(["a.x"]);
33
33
  expect(target).toEqual({ a: { x: 9, y: 2 }, b: 3 });
@@ -35,7 +35,7 @@ describe("applying config in place", () => {
35
35
  expect(target).toEqual({ a: { x: 9 } });
36
36
  });
37
37
 
38
- test("a patch cannot alias live config", () => {
38
+ test("a patch cannot alias live config", async () => {
39
39
  const cfg = structuredClone(DEFAULT_CONFIG);
40
40
  const patch = { filters: { allow: ["a/b"] } };
41
41
  applyConfigPatch(cfg, patch);
@@ -43,7 +43,7 @@ describe("applying config in place", () => {
43
43
  expect(cfg.filters.allow).toEqual(["a/b"]);
44
44
  });
45
45
 
46
- test("touched matches a block and its keys", () => {
46
+ test("touched matches a block and its keys", async () => {
47
47
  expect(touched(["ollama.apiKey"], "ollama")).toBe(true);
48
48
  expect(touched(["context"], "context")).toBe(true);
49
49
  expect(touched(["filters.allow"], "openrouter", "ollama")).toBe(false);