auto-model-router 0.30.0 → 0.30.2
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/package.json +1 -1
- package/src/eval/tasks.ts +40 -0
- package/src/server/http.ts +51 -22
- package/test/eval.test.ts +8 -0
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
|
|
10
|
-
"version": "0.30.
|
|
10
|
+
"version": "0.30.2",
|
|
11
11
|
"pluginRoot": "."
|
|
12
12
|
},
|
|
13
13
|
"plugins": [
|
|
14
14
|
{
|
|
15
15
|
"name": "auto-model-router",
|
|
16
16
|
"description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
|
|
17
|
-
"version": "0.30.
|
|
17
|
+
"version": "0.30.2",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/eval/tasks.ts
CHANGED
|
@@ -307,6 +307,46 @@ export const EVAL_TASKS: readonly EvalTask[] = [
|
|
|
307
307
|
return words.every((w) => known.includes(w)) ? 1 : 0.5;
|
|
308
308
|
},
|
|
309
309
|
},
|
|
310
|
+
|
|
311
|
+
// ---- harder still. The first hard band was aced 6/6 by deepseek-v4.1-flash, so it was
|
|
312
|
+
// not measuring a ceiling either. These need multi-step state tracking with no shortcut:
|
|
313
|
+
// the answer cannot be recalled, only computed, and one slip anywhere changes it.
|
|
314
|
+
{
|
|
315
|
+
id: "coding/stack-machine",
|
|
316
|
+
axis: "coding",
|
|
317
|
+
complexity: "hard",
|
|
318
|
+
system: JSON_ONLY,
|
|
319
|
+
user: "A stack machine starts with an empty stack. Execute: PUSH 4, PUSH 7, ADD, PUSH 3, SWAP, SUB, PUSH 2, MUL, DUP, ADD.\nSUB pops a then b and pushes b-a. SWAP exchanges the top two. DUP duplicates the top. Reply with the final stack, bottom to top, comma separated.",
|
|
320
|
+
// 4 | 4,7 | 11 | 11,3 | 3,11 | 3-11=-8 | -8,2 | -16 | -16,-16 | -32
|
|
321
|
+
grade: (o) => answerScore(o, "-32"),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "intel/ledger-balance",
|
|
325
|
+
axis: "intelligence",
|
|
326
|
+
complexity: "hard",
|
|
327
|
+
system: JSON_ONLY,
|
|
328
|
+
user: "An account starts at 0. Apply in order: +120, -45, then double the balance, -30, then halve the balance (round DOWN to a whole number), +7, then subtract a tenth of the balance (round DOWN), finally -1. Reply with the final balance alone.",
|
|
329
|
+
// 0→120→75→150→120→60→67; a tenth of 67 floors to 6 ⇒ 61; −1 ⇒ 60
|
|
330
|
+
grade: (o) => answerScore(o, "60"),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "intel/constraint-conflict",
|
|
334
|
+
axis: "intelligence",
|
|
335
|
+
complexity: "hard",
|
|
336
|
+
system: "Follow every constraint. If two constraints cannot both hold, say IMPOSSIBLE and nothing else.",
|
|
337
|
+
user: "Give a single whole number that is greater than 10, less than 20, divisible by 4, and odd. Reply with the number, or IMPOSSIBLE.",
|
|
338
|
+
// No odd multiple of 4 exists: recognising the contradiction is the capability.
|
|
339
|
+
grade: (o) => (/\bimpossible\b/i.test(o) ? 1 : 0),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "coding/regex-backtrack",
|
|
343
|
+
axis: "coding",
|
|
344
|
+
complexity: "hard",
|
|
345
|
+
system: JSON_ONLY,
|
|
346
|
+
user: "In JavaScript, give the result of each, one per line, in order:\n(1) 'aaa'.replace(/a*/g, 'X')\n(2) 'a1b2'.match(/[a-z](?=\\d)/g).join('')\n(3) /^(a+)+$/.test('aaab')\n(4) 'x.y.z'.split('.', 2).join('|')\n(5) 'abc'.replace(/(b)/, '[$1$$]')",
|
|
347
|
+
// Empty match at end ⇒ "XX"; lookahead ⇒ "ab"; false; "x|y"; "$$" is a literal $ ⇒ "a[b$]c"
|
|
348
|
+
grade: (o) => multiAnswerCoverage(o, ["XX", "ab", "false", "x|y", "a[b$]c"]),
|
|
349
|
+
},
|
|
310
350
|
];
|
|
311
351
|
|
|
312
352
|
/**
|
package/src/server/http.ts
CHANGED
|
@@ -248,6 +248,11 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
248
248
|
const overrides = createSessionOverrides();
|
|
249
249
|
const feedback = createFeedbackStore(db);
|
|
250
250
|
const kv = createKv(db);
|
|
251
|
+
/**
|
|
252
|
+
* Background local-benchmark runs. In memory and not persisted: a run is an explicit,
|
|
253
|
+
* attended admin action, and its lasting output is the `local_scores` row it writes.
|
|
254
|
+
*/
|
|
255
|
+
const benchmarkJobs = new Map<string, { state: "running" | "done" | "error"; slug: string; startedAtMs: number; result?: Record<string, unknown>; error?: string }>();
|
|
251
256
|
const digester = createDigester({ cfg, catalog, ledger, upstream, log });
|
|
252
257
|
const turnDeps = { config: cfg, router, upstream, ledger, conversations, catalog, context, overrides, ollamaCostScale, digester };
|
|
253
258
|
|
|
@@ -768,28 +773,52 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
768
773
|
};
|
|
769
774
|
const judgeSlug = typeof body?.judge === "string" && body.judge !== "" ? body.judge : "";
|
|
770
775
|
const asked = typeof body?.concurrency === "number" ? Math.floor(body.concurrency) : 2;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
776
|
+
// The run is a BACKGROUND job, not a long request. Bun caps `idleTimeout` at 255
|
|
777
|
+
// seconds, so a suite of any size over ten repeats outlives the socket: measured,
|
|
778
|
+
// a single pass returned in 193s and two passes died at ~350s with the response
|
|
779
|
+
// lost. The caller polls instead.
|
|
780
|
+
const jobId = `bench_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
781
|
+
const started = Date.now();
|
|
782
|
+
benchmarkJobs.set(jobId, { state: "running", slug, startedAtMs: started });
|
|
783
|
+
void (async () => {
|
|
784
|
+
try {
|
|
785
|
+
const results = await runEval({
|
|
786
|
+
slugs: [slug, ...anchors],
|
|
787
|
+
complete,
|
|
788
|
+
toolComplete,
|
|
789
|
+
repeats: Math.min(Math.max(1, typeof body?.repeats === "number" ? Math.floor(body.repeats) : 1), 20),
|
|
790
|
+
concurrency: Math.min(Math.max(1, asked), 8),
|
|
791
|
+
...(judgeSlug === "" ? {} : { judge: makeJudge(complete, judgeSlug) }),
|
|
792
|
+
});
|
|
793
|
+
const target = results[0]!;
|
|
794
|
+
const published = (s: string, axis: QualityAxis): number | undefined => models.find((m) => m.slug === s)?.quality[axis];
|
|
795
|
+
const cal = fitCalibration(results.slice(1), published);
|
|
796
|
+
const authorOf = (s: string): string => models.find((m) => m.slug === s)?.author ?? "";
|
|
797
|
+
const fresh = toLocalFeedScores([target], cal, authorOf);
|
|
798
|
+
const shared = { slug, anchors, raw: target.axes, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, errors: target.errors, tookMs: Date.now() - started };
|
|
799
|
+
if (fresh.length === 0) {
|
|
800
|
+
benchmarkJobs.set(jobId, { state: "done", slug, startedAtMs: started, result: { ...shared, calibrated: null, applied: false, reason: "no axis produced a usable fit; try more or better-spread anchors" } });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
// Merge, never replace: other models' measurements are not this run's to discard.
|
|
804
|
+
const kept = loadLocalScores(db).filter((s) => s.key !== fresh[0]!.key);
|
|
805
|
+
saveLocalScores(db, [...kept, ...fresh]);
|
|
806
|
+
log.info("benchmarked a model with the local eval suite", { slug, anchors: anchors.length, repeats: target.repeats, errors: target.errors, useLocalScores: cfg.benchmarks.useLocalScores });
|
|
807
|
+
benchmarkJobs.set(jobId, { state: "done", slug, startedAtMs: started, result: { ...shared, calibrated: fresh[0], applied: cfg.benchmarks.useLocalScores } });
|
|
808
|
+
} catch (err) {
|
|
809
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
810
|
+
log.warn("local benchmark run failed", { slug, error: message });
|
|
811
|
+
benchmarkJobs.set(jobId, { state: "error", slug, startedAtMs: started, error: message });
|
|
812
|
+
}
|
|
813
|
+
})();
|
|
814
|
+
return json({ jobId, state: "running", slug, anchors });
|
|
815
|
+
}
|
|
816
|
+
if (req.method === "GET" && url.pathname === "/v1/router/benchmark") {
|
|
817
|
+
const jobId = url.searchParams.get("job");
|
|
818
|
+
if (jobId === null) return json({ jobs: [...benchmarkJobs.entries()].map(([id, j]) => ({ id, slug: j.slug, state: j.state, startedAtMs: j.startedAtMs })) });
|
|
819
|
+
const job = benchmarkJobs.get(jobId);
|
|
820
|
+
if (job === undefined) return wireErrorResponse({ status: 404, code: "invalid_request_error", message: `no benchmark job ${jobId}` });
|
|
821
|
+
return json({ id: jobId, slug: job.slug, state: job.state, startedAtMs: job.startedAtMs, ...(job.result === undefined ? {} : { result: job.result }), ...(job.error === undefined ? {} : { error: job.error }) });
|
|
793
822
|
}
|
|
794
823
|
if (req.method === "POST" && url.pathname === "/v1/router/feedback") {
|
|
795
824
|
// A user verdict on the newest routed turn of an omp session.
|
package/test/eval.test.ts
CHANGED
|
@@ -117,6 +117,14 @@ describe("calibration", () => {
|
|
|
117
117
|
expect(hard.find((t) => t.id === "coding/sort-lexicographic")!.grade("[9, 10, 80]")).toBeLessThan(1);
|
|
118
118
|
expect(hard.find((t) => t.id === "intel/collatz-steps")!.grade("1")).toBeLessThan(1);
|
|
119
119
|
expect(hard.find((t) => t.id === "intel/strict-format")!.grade("Red, blue, and yellow.")).toBeLessThan(1);
|
|
120
|
+
// The second hard band: computable only, no recall shortcut.
|
|
121
|
+
expect(hard.find((t) => t.id === "coding/stack-machine")!.grade("-32")).toBe(1);
|
|
122
|
+
expect(hard.find((t) => t.id === "coding/stack-machine")!.grade("-16")).toBeLessThan(1);
|
|
123
|
+
expect(hard.find((t) => t.id === "intel/ledger-balance")!.grade("60")).toBe(1);
|
|
124
|
+
expect(hard.find((t) => t.id === "intel/ledger-balance")!.grade("61")).toBeLessThan(1);
|
|
125
|
+
expect(hard.find((t) => t.id === "intel/constraint-conflict")!.grade("IMPOSSIBLE")).toBe(1);
|
|
126
|
+
expect(hard.find((t) => t.id === "intel/constraint-conflict")!.grade("12")).toBe(0);
|
|
127
|
+
expect(hard.find((t) => t.id === "coding/regex-backtrack")!.grade("XX\nab\nfalse\nx|y\na[b$]c")).toBe(1);
|
|
120
128
|
});
|
|
121
129
|
|
|
122
130
|
test("fitCalibration + toLocalFeedScores place a target on the AA scale", () => {
|