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.7",
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.7",
17
+ "version": "0.2.8",
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.2.7",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -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(`SELECT ${LATENCY_SELECT} FROM ledger WHERE slug = ?`);
235
- const latencyHarnessStmt = db.query(`SELECT ${LATENCY_SELECT} FROM ledger WHERE slug = ? AND harness_id = ?`);
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", () => {