auto-model-router 0.30.1 → 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.
@@ -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.1",
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.1",
17
+ "version": "0.30.2",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -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
- const results = await runEval({
772
- slugs: [slug, ...anchors],
773
- complete,
774
- toolComplete,
775
- repeats: Math.min(Math.max(1, typeof body?.repeats === "number" ? Math.floor(body.repeats) : 1), 20),
776
- concurrency: Math.min(Math.max(1, asked), 8),
777
- ...(judgeSlug === "" ? {} : { judge: makeJudge(complete, judgeSlug) }),
778
- });
779
- const target = results[0]!;
780
- const anchorResults = results.slice(1);
781
- const published = (s: string, axis: QualityAxis): number | undefined => models.find((m) => m.slug === s)?.quality[axis];
782
- const cal = fitCalibration(anchorResults, published);
783
- const authorOf = (s: string): string => models.find((m) => m.slug === s)?.author ?? "";
784
- const fresh = toLocalFeedScores([target], cal, authorOf);
785
- if (fresh.length === 0) {
786
- return json({ slug, anchors, calibrated: null, raw: target.axes, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, errors: target.errors, applied: false, reason: "no axis produced a usable fit; try more or better-spread anchors" });
787
- }
788
- // Merge, never replace: other models' measurements are not this run's to discard.
789
- const kept = loadLocalScores(db).filter((s) => s.key !== fresh[0]!.key);
790
- saveLocalScores(db, [...kept, ...fresh]);
791
- log.info("benchmarked a model with the local eval suite", { slug, anchors: anchors.length, errors: target.errors, useLocalScores: cfg.benchmarks.useLocalScores });
792
- return json({ slug, anchors, raw: target.axes, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, calibrated: fresh[0], errors: target.errors, applied: cfg.benchmarks.useLocalScores });
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.