auto-model-router 0.2.7 → 0.2.8
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.2.
|
|
10
|
+
"version": "0.2.8",
|
|
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.2.
|
|
17
|
+
"version": "0.2.8",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/cost/ledger.ts
CHANGED
|
@@ -117,6 +117,13 @@ const TRUST_SELECT = `COUNT(*) AS attempts,
|
|
|
117
117
|
* body streams once it starts. Errored/aborted and non-streaming rows (null
|
|
118
118
|
* ttft) are excluded; throughput additionally requires a positive completion
|
|
119
119
|
* count and elapsed time.
|
|
120
|
+
*
|
|
121
|
+
* Aggregated over a RECENT WINDOW (LATENCY_WINDOW_ROWS newest rows per slug),
|
|
122
|
+
* NOT all history: a model that degrades — e.g. deepseek-v4-flash collapsing
|
|
123
|
+
* from ~18 tok/s to ~7 — must move its score fast, or the penalty is drowned by
|
|
124
|
+
* hundreds of historical good rows and never demotes it (observed live: it kept
|
|
125
|
+
* 100% of coding at ~53s/turn despite latencyWeight=0.75). Trust (reliability)
|
|
126
|
+
* stays all-time; latency (volatile) is recency-weighted.
|
|
120
127
|
*/
|
|
121
128
|
const LATENCY_SELECT = `COUNT(CASE WHEN ttft_ms IS NOT NULL AND ttft_ms > 0 AND error IS NULL THEN 1 END) AS samples,
|
|
122
129
|
AVG(CASE WHEN ttft_ms IS NOT NULL AND ttft_ms > 0 AND error IS NULL THEN ttft_ms END) AS ttft_ms,
|
|
@@ -127,6 +134,14 @@ const LATENCY_SELECT = `COUNT(CASE WHEN ttft_ms IS NOT NULL AND ttft_ms > 0 AND
|
|
|
127
134
|
AND json_extract(usage, '$.completionTokens') > 0
|
|
128
135
|
THEN latency_ms - ttft_ms END) AS elapsed_ms_sum`;
|
|
129
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Recent-rows window for latency stats: recent enough to react to a degrading
|
|
139
|
+
* model, wide enough to stay stable for a busy one. Rows are taken newest-first
|
|
140
|
+
* and then filtered by LATENCY_SELECT, so recent aborts naturally shrink the
|
|
141
|
+
* qualifying sample count (and can drop a model below latencyMinSamples).
|
|
142
|
+
*/
|
|
143
|
+
export const LATENCY_WINDOW_ROWS = 100;
|
|
144
|
+
|
|
130
145
|
/**
|
|
131
146
|
* Recovers the `UpstreamErrorKind` from the text turn.ts stored.
|
|
132
147
|
*
|
|
@@ -231,8 +246,12 @@ export function createLedger(db: Database, cfg: RouterConfig): Ledger {
|
|
|
231
246
|
const trustStmt = db.query(`SELECT ${TRUST_SELECT} FROM ledger WHERE slug = ?`);
|
|
232
247
|
const trustHarnessStmt = db.query(`SELECT ${TRUST_SELECT} FROM ledger WHERE slug = ? AND harness_id = ?`);
|
|
233
248
|
const allTrustStmt = db.query(`SELECT slug, ${TRUST_SELECT} FROM ledger GROUP BY slug`);
|
|
234
|
-
const latencyStmt = db.query(
|
|
235
|
-
|
|
249
|
+
const latencyStmt = db.query(
|
|
250
|
+
`SELECT ${LATENCY_SELECT} FROM (SELECT * FROM ledger WHERE slug = ? ORDER BY created_at_ms DESC LIMIT ${LATENCY_WINDOW_ROWS})`,
|
|
251
|
+
);
|
|
252
|
+
const latencyHarnessStmt = db.query(
|
|
253
|
+
`SELECT ${LATENCY_SELECT} FROM (SELECT * FROM ledger WHERE slug = ? AND harness_id = ? ORDER BY created_at_ms DESC LIMIT ${LATENCY_WINDOW_ROWS})`,
|
|
254
|
+
);
|
|
236
255
|
const ratioStmt = db.query("SELECT est_bytes, actual_tokens, samples FROM token_calibration WHERE tokenizer = ?");
|
|
237
256
|
const recentStmt = db.query("SELECT * FROM ledger ORDER BY created_at_ms DESC LIMIT ?");
|
|
238
257
|
const cacheMetaStmt = db.query("SELECT fetched_at_ms FROM catalog_cache WHERE id = 1");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { loadConfig } from "../src/config/load.ts";
|
|
4
|
-
import { createLedger } from "../src/cost/ledger.ts";
|
|
4
|
+
import { createLedger, LATENCY_WINDOW_ROWS } from "../src/cost/ledger.ts";
|
|
5
5
|
import { EMPTY_USAGE, type LedgerEntry } from "../src/cost/types.ts";
|
|
6
6
|
import { openDb } from "../src/util/sqlite.ts";
|
|
7
7
|
|
|
@@ -198,6 +198,22 @@ describe("latency signal", () => {
|
|
|
198
198
|
expect(l?.tokensPerSec).toBeCloseTo(100, 5);
|
|
199
199
|
expect(l?.samples).toBe(2);
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
test("throughput and ttft track a recent window, not the lifetime average", () => {
|
|
203
|
+
// Old rows are fast; the recent window is slow. Latency must reflect the
|
|
204
|
+
// recent (slow) behaviour so a degraded model is penalised, not masked by
|
|
205
|
+
// its history. Lifetime blend here would be ~57 tok/s; the window is 10.
|
|
206
|
+
const rows: Array<Partial<LedgerEntry>> = [];
|
|
207
|
+
let t = 1;
|
|
208
|
+
for (let i = 0; i < 50; i++)
|
|
209
|
+
rows.push({ createdAtMs: t++, ttftMs: 200, latencyMs: 1200, usage: { ...EMPTY_USAGE, completionTokens: 1000 } });
|
|
210
|
+
for (let i = 0; i < LATENCY_WINDOW_ROWS; i++)
|
|
211
|
+
rows.push({ createdAtMs: t++, ttftMs: 4000, latencyMs: 14000, usage: { ...EMPTY_USAGE, completionTokens: 100 } });
|
|
212
|
+
const l = latencyOf(rows);
|
|
213
|
+
expect(l?.samples).toBe(LATENCY_WINDOW_ROWS);
|
|
214
|
+
expect(l?.tokensPerSec).toBeCloseTo(10, 0);
|
|
215
|
+
expect(l?.ttftMs).toBeCloseTo(4000, 5);
|
|
216
|
+
});
|
|
201
217
|
});
|
|
202
218
|
|
|
203
219
|
describe("v4 migration", () => {
|