auto-model-router 0.30.1 → 0.30.3
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/run.ts +7 -0
- package/src/server/http.ts +71 -22
|
@@ -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.3",
|
|
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.3",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/eval/run.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface RunEvalArgs {
|
|
|
67
67
|
concurrency?: number;
|
|
68
68
|
/** Called as each model finishes, for progress logging. */
|
|
69
69
|
onProgress?: (result: EvalResult, done: number, total: number) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Called as each PASS of each model finishes. A run is `models x repeats` passes and takes
|
|
72
|
+
* the better part of an hour at ten repeats, so per-model progress is too coarse to show
|
|
73
|
+
* anyone: without this the only observable states are "running" and "finished".
|
|
74
|
+
*/
|
|
75
|
+
onPass?: (slug: string, pass: number, of: number) => void;
|
|
70
76
|
/**
|
|
71
77
|
* A tool-capable completer. Absent ⇒ the agentic SCENARIOS are skipped and the axis falls
|
|
72
78
|
* back to the text tasks, which only ever measured whether a model can format JSON.
|
|
@@ -171,6 +177,7 @@ async function scoreModel(slug: string, args: RunEvalArgs): Promise<EvalResult>
|
|
|
171
177
|
let errors = 0;
|
|
172
178
|
for (let i = 0; i < passes; i++) {
|
|
173
179
|
const pass = await scorePass(slug, args);
|
|
180
|
+
args.onPass?.(slug, i + 1, passes);
|
|
174
181
|
errors += pass.errors;
|
|
175
182
|
for (const axis of AXES) {
|
|
176
183
|
axes[axis].sum += pass.axes[axis].sum;
|
package/src/server/http.ts
CHANGED
|
@@ -182,6 +182,19 @@ export function ollamaRunway(
|
|
|
182
182
|
return { dailyBurnUsd, creditsLeftUsd, days: dailyBurnUsd > 0 ? creditsLeftUsd / dailyBurnUsd : null };
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Progress for a local benchmark run, as the dashboard needs it. The ETA is measured from
|
|
187
|
+
* this run's own completed passes rather than assumed — passes vary with the model's speed,
|
|
188
|
+
* and a guess from a fixed per-pass constant would be wrong by minutes on a slow upstream.
|
|
189
|
+
* Null until a pass has finished, because until then there is nothing to extrapolate from.
|
|
190
|
+
*/
|
|
191
|
+
function benchmarkProgress(job: { state: string; startedAtMs: number; donePasses: number; totalPasses: number; current: string }): Record<string, unknown> {
|
|
192
|
+
const elapsedMs = Date.now() - job.startedAtMs;
|
|
193
|
+
const fraction = job.totalPasses > 0 ? job.donePasses / job.totalPasses : 0;
|
|
194
|
+
const etaMs = job.state === "running" && job.donePasses > 0 ? Math.round((elapsedMs / job.donePasses) * (job.totalPasses - job.donePasses)) : null;
|
|
195
|
+
return { donePasses: job.donePasses, totalPasses: job.totalPasses, fraction, current: job.current, elapsedMs, etaMs };
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
function json(data: unknown, status = 200): Response {
|
|
186
199
|
return new Response(JSON.stringify(data), {
|
|
187
200
|
status,
|
|
@@ -248,6 +261,11 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
248
261
|
const overrides = createSessionOverrides();
|
|
249
262
|
const feedback = createFeedbackStore(db);
|
|
250
263
|
const kv = createKv(db);
|
|
264
|
+
/**
|
|
265
|
+
* Background local-benchmark runs. In memory and not persisted: a run is an explicit,
|
|
266
|
+
* attended admin action, and its lasting output is the `local_scores` row it writes.
|
|
267
|
+
*/
|
|
268
|
+
const benchmarkJobs = new Map<string, { state: "running" | "done" | "error"; slug: string; startedAtMs: number; donePasses: number; totalPasses: number; current: string; result?: Record<string, unknown>; error?: string }>();
|
|
251
269
|
const digester = createDigester({ cfg, catalog, ledger, upstream, log });
|
|
252
270
|
const turnDeps = { config: cfg, router, upstream, ledger, conversations, catalog, context, overrides, ollamaCostScale, digester };
|
|
253
271
|
|
|
@@ -768,28 +786,59 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
768
786
|
};
|
|
769
787
|
const judgeSlug = typeof body?.judge === "string" && body.judge !== "" ? body.judge : "";
|
|
770
788
|
const asked = typeof body?.concurrency === "number" ? Math.floor(body.concurrency) : 2;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
789
|
+
// The run is a BACKGROUND job, not a long request. Bun caps `idleTimeout` at 255
|
|
790
|
+
// seconds, so a suite of any size over ten repeats outlives the socket: measured,
|
|
791
|
+
// a single pass returned in 193s and two passes died at ~350s with the response
|
|
792
|
+
// lost. The caller polls instead.
|
|
793
|
+
const jobId = `bench_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
794
|
+
const started = Date.now();
|
|
795
|
+
const repeats = Math.min(Math.max(1, typeof body?.repeats === "number" ? Math.floor(body.repeats) : 1), 20);
|
|
796
|
+
// A pass of one model is the unit of progress: models x repeats, known up front.
|
|
797
|
+
const totalPasses = repeats * (1 + anchors.length);
|
|
798
|
+
benchmarkJobs.set(jobId, { state: "running", slug, startedAtMs: started, donePasses: 0, totalPasses, current: slug });
|
|
799
|
+
void (async () => {
|
|
800
|
+
try {
|
|
801
|
+
const results = await runEval({
|
|
802
|
+
slugs: [slug, ...anchors],
|
|
803
|
+
complete,
|
|
804
|
+
toolComplete,
|
|
805
|
+
repeats,
|
|
806
|
+
concurrency: Math.min(Math.max(1, asked), 8),
|
|
807
|
+
onPass: (passSlug, _pass, _of) => {
|
|
808
|
+
const job = benchmarkJobs.get(jobId);
|
|
809
|
+
if (job !== undefined) benchmarkJobs.set(jobId, { ...job, donePasses: job.donePasses + 1, current: passSlug });
|
|
810
|
+
},
|
|
811
|
+
...(judgeSlug === "" ? {} : { judge: makeJudge(complete, judgeSlug) }),
|
|
812
|
+
});
|
|
813
|
+
const target = results[0]!;
|
|
814
|
+
const published = (s: string, axis: QualityAxis): number | undefined => models.find((m) => m.slug === s)?.quality[axis];
|
|
815
|
+
const cal = fitCalibration(results.slice(1), published);
|
|
816
|
+
const authorOf = (s: string): string => models.find((m) => m.slug === s)?.author ?? "";
|
|
817
|
+
const fresh = toLocalFeedScores([target], cal, authorOf);
|
|
818
|
+
const shared = { slug, anchors, raw: target.axes, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, errors: target.errors, tookMs: Date.now() - started };
|
|
819
|
+
if (fresh.length === 0) {
|
|
820
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "done", result: { ...shared, calibrated: null, applied: false, reason: "no axis produced a usable fit; try more or better-spread anchors" } });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
// Merge, never replace: other models' measurements are not this run's to discard.
|
|
824
|
+
const kept = loadLocalScores(db).filter((s) => s.key !== fresh[0]!.key);
|
|
825
|
+
saveLocalScores(db, [...kept, ...fresh]);
|
|
826
|
+
log.info("benchmarked a model with the local eval suite", { slug, anchors: anchors.length, repeats: target.repeats, errors: target.errors, useLocalScores: cfg.benchmarks.useLocalScores });
|
|
827
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "done", result: { ...shared, calibrated: fresh[0], applied: cfg.benchmarks.useLocalScores } });
|
|
828
|
+
} catch (err) {
|
|
829
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
830
|
+
log.warn("local benchmark run failed", { slug, error: message });
|
|
831
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "error", error: message });
|
|
832
|
+
}
|
|
833
|
+
})();
|
|
834
|
+
return json({ jobId, state: "running", slug, anchors, totalPasses });
|
|
835
|
+
}
|
|
836
|
+
if (req.method === "GET" && url.pathname === "/v1/router/benchmark") {
|
|
837
|
+
const jobId = url.searchParams.get("job");
|
|
838
|
+
if (jobId === null) return json({ jobs: [...benchmarkJobs.entries()].map(([id, j]) => ({ id, slug: j.slug, state: j.state, startedAtMs: j.startedAtMs, ...benchmarkProgress(j) })) });
|
|
839
|
+
const job = benchmarkJobs.get(jobId);
|
|
840
|
+
if (job === undefined) return wireErrorResponse({ status: 404, code: "invalid_request_error", message: `no benchmark job ${jobId}` });
|
|
841
|
+
return json({ id: jobId, slug: job.slug, state: job.state, startedAtMs: job.startedAtMs, ...benchmarkProgress(job), ...(job.result === undefined ? {} : { result: job.result }), ...(job.error === undefined ? {} : { error: job.error }) });
|
|
793
842
|
}
|
|
794
843
|
if (req.method === "POST" && url.pathname === "/v1/router/feedback") {
|
|
795
844
|
// A user verdict on the newest routed turn of an omp session.
|