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.
- package/.omp-plugin/marketplace.json +2 -2
- package/README.md +32 -2
- package/omp-extension/router-configure.ts +9 -7
- package/package.json +1 -1
- package/src/cli/config-cmd.ts +8 -7
- package/src/cli/explain.ts +10 -5
- package/src/cli/export.ts +6 -5
- package/src/cli/models.ts +10 -7
- package/src/cli/report.ts +6 -1
- package/src/cli/stats.ts +7 -7
- package/src/config/load.ts +10 -1
- package/src/config/types.ts +10 -1
- package/src/context/bridge.ts +7 -7
- package/src/context/index.ts +3 -3
- package/src/context/store.ts +39 -56
- package/src/context/types.ts +7 -6
- package/src/cost/blended.ts +28 -7
- package/src/cost/feedback.ts +33 -37
- package/src/cost/ledger-sql.ts +547 -0
- package/src/cost/ledger.ts +30 -459
- package/src/cost/report.ts +171 -129
- package/src/cost/retention.ts +10 -10
- package/src/cost/summary.ts +15 -10
- package/src/cost/types.ts +43 -62
- package/src/cost/views.ts +79 -49
- package/src/eval/calibrate.ts +47 -12
- package/src/eval/run.ts +18 -2
- package/src/lib.ts +6 -2
- package/src/router/candidates.ts +7 -15
- package/src/router/classify.ts +6 -4
- package/src/router/index.ts +95 -9
- package/src/router/select.ts +38 -21
- package/src/router/state.ts +90 -102
- package/src/router/types.ts +11 -5
- package/src/server/advise.ts +6 -4
- package/src/server/compaction-digest.ts +1 -1
- package/src/server/digest.ts +9 -10
- package/src/server/http.ts +109 -46
- package/src/server/providers.ts +18 -4
- package/src/server/turn.ts +32 -9
- package/src/tokens/estimate.ts +16 -6
- package/src/upstream/ollama-usage.ts +21 -11
- package/src/util/schema.ts +201 -0
- package/src/util/sql.ts +246 -0
- package/src/wire/anthropic/messages.ts +3 -4
- package/src/wire/openai/request.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/test/anthropic-wire.test.ts +9 -9
- package/test/benchmark-feeds.test.ts +7 -7
- package/test/cache-control.test.ts +7 -7
- package/test/cache-estimate.test.ts +5 -5
- package/test/catalog-view.test.ts +4 -4
- package/test/catalog.test.ts +11 -11
- package/test/classify.test.ts +24 -24
- package/test/compaction.test.ts +20 -20
- package/test/config-wizard.test.ts +32 -32
- package/test/config.test.ts +10 -10
- package/test/connect-harnesses.test.ts +11 -11
- package/test/context-bridge.test.ts +40 -30
- package/test/context-prune.test.ts +43 -36
- package/test/context-query.test.ts +8 -8
- package/test/controls.test.ts +54 -27
- package/test/cost.test.ts +12 -12
- package/test/digest.test.ts +55 -44
- package/test/embed-lifecycle.test.ts +5 -5
- package/test/embed-logic.test.ts +26 -26
- package/test/escalate.test.ts +17 -17
- package/test/eval.test.ts +73 -16
- package/test/executable.test.ts +6 -6
- package/test/exploration.test.ts +19 -20
- package/test/failover.test.ts +22 -21
- package/test/fakes.ts +105 -0
- package/test/features.test.ts +21 -21
- package/test/harness-requests.test.ts +3 -3
- package/test/harness-switch.test.ts +5 -5
- package/test/hold-exploration.test.ts +13 -13
- package/test/hot-reload.test.ts +5 -5
- package/test/learned.test.ts +5 -5
- package/test/ledger-sql.test.ts +342 -0
- package/test/mcp-entry.test.ts +5 -5
- package/test/migrations.test.ts +28 -22
- package/test/models-yml.test.ts +18 -18
- package/test/ollama.test.ts +40 -34
- package/test/omp-credentials.test.ts +16 -16
- package/test/policy.test.ts +3 -3
- package/test/reconfigure.test.ts +4 -4
- package/test/redaction.test.ts +41 -35
- package/test/remote.test.ts +12 -12
- package/test/report-logic.test.ts +8 -8
- package/test/report.test.ts +95 -87
- package/test/retention.test.ts +79 -66
- package/test/schema.test.ts +123 -0
- package/test/scope.test.ts +8 -8
- package/test/select.test.ts +216 -257
- package/test/skills.test.ts +3 -3
- package/test/sql-shim.test.ts +154 -0
- package/test/state.test.ts +43 -36
- package/test/summary.test.ts +38 -27
- package/test/tier-plan.test.ts +45 -62
- package/test/toast-logic.test.ts +31 -31
- package/test/tokens.test.ts +95 -80
- package/test/trust-attribution.test.ts +217 -187
- package/test/trust-window.test.ts +37 -32
- package/test/turn.test.ts +55 -23
- package/test/upstreams.test.ts +13 -13
- package/test/views.test.ts +81 -59
- package/test/wire-request.test.ts +17 -17
- package/test/wire-responses.test.ts +4 -4
- package/tools/agentdox-e2e.ts +5 -2
- package/tools/export-benchmarks.ts +5 -5
- package/tools/ledger-parity.ts +266 -0
- package/tools/replay.ts +16 -8
package/test/ollama.test.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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);
|
package/test/policy.test.ts
CHANGED
|
@@ -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/*"]);
|
package/test/reconfigure.test.ts
CHANGED
|
@@ -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);
|