auto-model-router 0.30.2 → 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 +29 -9
|
@@ -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,
|
|
@@ -252,7 +265,7 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
252
265
|
* Background local-benchmark runs. In memory and not persisted: a run is an explicit,
|
|
253
266
|
* attended admin action, and its lasting output is the `local_scores` row it writes.
|
|
254
267
|
*/
|
|
255
|
-
const benchmarkJobs = new Map<string, { state: "running" | "done" | "error"; slug: string; startedAtMs: number; result?: Record<string, unknown>; error?: string }>();
|
|
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 }>();
|
|
256
269
|
const digester = createDigester({ cfg, catalog, ledger, upstream, log });
|
|
257
270
|
const turnDeps = { config: cfg, router, upstream, ledger, conversations, catalog, context, overrides, ollamaCostScale, digester };
|
|
258
271
|
|
|
@@ -779,15 +792,22 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
779
792
|
// lost. The caller polls instead.
|
|
780
793
|
const jobId = `bench_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
781
794
|
const started = Date.now();
|
|
782
|
-
|
|
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 });
|
|
783
799
|
void (async () => {
|
|
784
800
|
try {
|
|
785
801
|
const results = await runEval({
|
|
786
802
|
slugs: [slug, ...anchors],
|
|
787
803
|
complete,
|
|
788
804
|
toolComplete,
|
|
789
|
-
repeats
|
|
805
|
+
repeats,
|
|
790
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
|
+
},
|
|
791
811
|
...(judgeSlug === "" ? {} : { judge: makeJudge(complete, judgeSlug) }),
|
|
792
812
|
});
|
|
793
813
|
const target = results[0]!;
|
|
@@ -797,28 +817,28 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
797
817
|
const fresh = toLocalFeedScores([target], cal, authorOf);
|
|
798
818
|
const shared = { slug, anchors, raw: target.axes, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, errors: target.errors, tookMs: Date.now() - started };
|
|
799
819
|
if (fresh.length === 0) {
|
|
800
|
-
benchmarkJobs.set(jobId, { state: "done",
|
|
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" } });
|
|
801
821
|
return;
|
|
802
822
|
}
|
|
803
823
|
// Merge, never replace: other models' measurements are not this run's to discard.
|
|
804
824
|
const kept = loadLocalScores(db).filter((s) => s.key !== fresh[0]!.key);
|
|
805
825
|
saveLocalScores(db, [...kept, ...fresh]);
|
|
806
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 });
|
|
807
|
-
benchmarkJobs.set(jobId, { state: "done",
|
|
827
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "done", result: { ...shared, calibrated: fresh[0], applied: cfg.benchmarks.useLocalScores } });
|
|
808
828
|
} catch (err) {
|
|
809
829
|
const message = err instanceof Error ? err.message : String(err);
|
|
810
830
|
log.warn("local benchmark run failed", { slug, error: message });
|
|
811
|
-
benchmarkJobs.set(jobId, { state: "error",
|
|
831
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "error", error: message });
|
|
812
832
|
}
|
|
813
833
|
})();
|
|
814
|
-
return json({ jobId, state: "running", slug, anchors });
|
|
834
|
+
return json({ jobId, state: "running", slug, anchors, totalPasses });
|
|
815
835
|
}
|
|
816
836
|
if (req.method === "GET" && url.pathname === "/v1/router/benchmark") {
|
|
817
837
|
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 })) });
|
|
838
|
+
if (jobId === null) return json({ jobs: [...benchmarkJobs.entries()].map(([id, j]) => ({ id, slug: j.slug, state: j.state, startedAtMs: j.startedAtMs, ...benchmarkProgress(j) })) });
|
|
819
839
|
const job = benchmarkJobs.get(jobId);
|
|
820
840
|
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 }) });
|
|
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 }) });
|
|
822
842
|
}
|
|
823
843
|
if (req.method === "POST" && url.pathname === "/v1/router/feedback") {
|
|
824
844
|
// A user verdict on the newest routed turn of an omp session.
|