auto-model-router 0.1.3 → 0.2.0

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.
Files changed (54) hide show
  1. package/.omp-plugin/marketplace.json +2 -2
  2. package/README.md +127 -46
  3. package/bun.lock +606 -0
  4. package/omp-extension/router-embed.ts +14 -6
  5. package/omp-extension/router-toast.ts +6 -1
  6. package/omp-extension/toast-logic.ts +7 -0
  7. package/package.json +2 -1
  8. package/research/analyze-ledger.ts +173 -0
  9. package/research/apply-cost-tuning.ts +73 -0
  10. package/research/cost-analysis.ts +150 -0
  11. package/research/feed-check.ts +64 -0
  12. package/research/model-recommendations.ts +86 -0
  13. package/research/project-yield.ts +96 -0
  14. package/research/run-eval.ts +133 -0
  15. package/research/status.ts +55 -0
  16. package/research/tier-fill.ts +109 -0
  17. package/research/tier-map.ts +123 -0
  18. package/src/catalog/benchmark-feeds.ts +397 -0
  19. package/src/catalog/openrouter-catalog.ts +30 -0
  20. package/src/config/defaults.ts +30 -0
  21. package/src/config/load.ts +2 -0
  22. package/src/config/schema.ts +34 -0
  23. package/src/config/types.ts +106 -0
  24. package/src/cost/ledger.ts +27 -3
  25. package/src/cost/types.ts +30 -0
  26. package/src/eval/calibrate.ts +131 -0
  27. package/src/eval/grade.ts +115 -0
  28. package/src/eval/judge.ts +71 -0
  29. package/src/eval/run.ts +126 -0
  30. package/src/eval/tasks.ts +272 -0
  31. package/src/index.ts +0 -1
  32. package/src/router/candidates.ts +13 -6
  33. package/src/router/explore.ts +59 -0
  34. package/src/router/select.ts +54 -4
  35. package/src/router/tier-plan.ts +57 -1
  36. package/src/router/types.ts +13 -0
  37. package/src/server/turn.ts +10 -2
  38. package/src/util/sqlite.ts +79 -1
  39. package/src/wire/openai/request.ts +5 -0
  40. package/src/wire/types.ts +7 -0
  41. package/test/benchmark-feeds.test.ts +222 -0
  42. package/test/escalate.test.ts +1 -0
  43. package/test/eval.test.ts +184 -0
  44. package/test/exploration.test.ts +251 -0
  45. package/test/failover.test.ts +5 -0
  46. package/test/hold-exploration.test.ts +124 -0
  47. package/test/tier-plan.test.ts +55 -1
  48. package/test/toast-logic.test.ts +32 -0
  49. package/test/tokens.test.ts +8 -0
  50. package/test/trust-attribution.test.ts +110 -2
  51. package/test/turn.test.ts +46 -0
  52. package/test/wire-request.test.ts +11 -0
  53. package/tools/smoke.ts +2 -0
  54. package/tools/sync-marketplace-version.ts +60 -0
@@ -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.1.0",
10
+ "version": "0.2.0",
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.1.0",
17
+ "version": "0.2.0",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/README.md CHANGED
@@ -6,13 +6,14 @@ OpenRouter model **per turn** based on measured price and estimated task
6
6
  complexity — including mid-conversation, when a session shifts from mechanical
7
7
  tool-loop churn to genuine reasoning work.
8
8
 
9
- All LLM inference is offloaded to OpenRouter. Nothing runs on-device except
10
- routing arithmetic.
11
-
12
9
  auto-model-router runs **embedded inside the omp process** (as an omp extension) — no
13
10
  separate server, no orphaned process. It binds a free OS-assigned port and
14
11
  lives and dies with the omp session.
15
12
 
13
+ For non-omp harnesses (Hermes, Claude, any OpenAI-compatible client), run it as
14
+ a standalone process with `auto-model-router serve --port <n>` — the same core,
15
+ on a fixed port, owned by you. See [Hermes](#hermes) below.
16
+
16
17
  ## Why this exists when OpenRouter already ships routers
17
18
 
18
19
  OpenRouter has `openrouter/auto` (market-spend classifier) and
@@ -62,7 +63,7 @@ without touching routing.
62
63
  | `src/router/` | Feature extraction, complexity classification, candidate filtering and scoring, hysteresis, cache-breakpoint placement, budget guard, probe planning. |
63
64
  | `src/upstream/` | OpenRouter transport: streaming dispatch, `session_id` stickiness, error classification, fallback arrays. |
64
65
  | `src/config/` | Configuration loading, schema validation, and the built-in defaults. |
65
- | `src/cli/` | `stats`, `models`, `explain`, `config` commands. |
66
+ | `src/cli/` | `serve`, `stats`, `models`, `explain`, `config` commands. |
66
67
  | `omp-extension/` | The omp extensions: `router-embed.ts`, `router-toast.ts`, `router-configure.ts`. |
67
68
 
68
69
  ### Two cost numbers, never conflated
@@ -73,16 +74,33 @@ without touching routing.
73
74
  - **Reported** — `usage.cost` from OpenRouter, authoritative after the fact.
74
75
  Drives the ledger, `stats`, and prediction-error calibration.
75
76
 
76
- ## Requirements
77
+ ## Installing
78
+
79
+ No separate Bun install is needed for the embedded path. The standalone
80
+ `serve` binary (`npm install -g auto-model-router`) bundles Bun.
81
+
82
+ Two ways to get the router into omp. The **npm package** is the modern path —
83
+ it installs the `auto-model-router` binary and wires the omp extensions; the
84
+ **repo-local installer** is for developing against the source.
85
+
86
+ ### Via npm (installs the `auto-model-router` binary)
77
87
 
78
- - **omp** (the Oh My Pi harness) — the router runs as an omp extension.
79
- - **Bun** `>= 1.2.0` — omp itself is a Bun process; the router code runs inside
80
- it. No separate Bun install is needed for the embedded path.
88
+ ```bash
89
+ npm install -g auto-model-router
90
+ ```
91
+
92
+ Then add the shipped extensions to omp's `~/.omp/agent/config.yml`
93
+ (`$PI_CODING_AGENT_DIR/config.yml` when that env var relocates the agent dir):
81
94
 
82
- ## Installation
95
+ ```yaml
96
+ # ~/.omp/agent/config.yml
97
+ extensions:
98
+ - auto-model-router/omp-extension/router-embed.ts
99
+ - auto-model-router/omp-extension/router-toast.ts # optional: chosen-model toasts
100
+ - auto-model-router/omp-extension/router-configure.ts # optional: /router command
101
+ ```
83
102
 
84
- There is nothing to install system-wide. Run the cross-platform installer
85
- (Windows, macOS, Linux) from the repo:
103
+ ### From the repo (cross-platform installer)
86
104
 
87
105
  ```bash
88
106
  bun tools/install.ts
@@ -138,6 +156,9 @@ or in the TUI:
138
156
  /marketplace install auto-model-router@auto-model-router
139
157
  ```
140
158
 
159
+ After installing, restart the omp session (extensions load at session start),
160
+ then `/model` and pick `auto-model-router/auto`.
161
+
141
162
  ### Install from the Pi package marketplace
142
163
 
143
164
  The repo is also a Pi package (see the `pi` manifest and `pi-package` keyword
@@ -154,58 +175,109 @@ or from git:
154
175
  pi install git:github.com/drewappling/auto-model-router
155
176
  ```
156
177
 
157
- To publish to npm (which auto-indexes on pi.dev/packages):
178
+ #### Releasing
179
+
180
+ Cut releases with `npm version` (or `bun run release <patch|minor|major>`), not a
181
+ bare `npm publish`:
158
182
 
159
183
  ```bash
160
- npm publish
184
+ npm version patch && git push --follow-tags # or: bun run release patch
161
185
  ```
162
186
 
187
+ `npm version` runs the `version` lifecycle script
188
+ (`tools/sync-marketplace-version.ts`), which rewrites the Git-marketplace
189
+ catalog (`.omp-plugin/marketplace.json`) to the new version and stages it into
190
+ the version commit — so the npm package and the marketplace catalog can never
191
+ drift. Pushing the `vX.Y.Z` tag triggers the release workflow (npm publish,
192
+ which auto-indexes on pi.dev/packages, plus a GitHub Release). A bare
193
+ `npm publish` skips both the catalog sync and the tag, so avoid it.
194
+
163
195
  ### Hermes
164
196
 
165
- Hermes speaks the OpenAI-compatible wire, so it connects to the router with no
166
- code change. Two ways to run the router for Hermes:
197
+ Install the router globally (puts the `serve` binary on PATH) and
198
+ install the native plugin, then point Hermes at it:
167
199
 
168
- **Standalone server (recommended for Hermes):** run the router as its own
169
- process on a fixed port, then point Hermes at it:
200
+ **1. Install the router binary:**
201
+
202
+ ```bash
203
+ npm install -g auto-model-router
204
+ ```
205
+
206
+ **2. Install the Hermes plugin.** Copy `hermes-plugin/` to
207
+ `$HERMES_HOME/plugins/model-providers/auto-model-router/` (where
208
+ `HERMES_HOME` is `C:\Users\<you>\AppData\Local\hermes` on Windows,
209
+ `~/.hermes` on macOS/Linux):
210
+
211
+ ```bash
212
+ mkdir -p "$HERMES_HOME/plugins/model-providers"
213
+ cp -r hermes-plugin/ "$HERMES_HOME/plugins/model-providers/auto-model-router/"
214
+ ```
215
+
216
+ **3. Surface the provider in Hermes's picker.** Hermes only lists providers
217
+ that have a credential. The router itself is keyless (it resolves its own
218
+ OpenRouter key), but to make Hermes show it as selectable, add a marker value
219
+ to `$HERMES_HOME/.env`:
220
+
221
+ ```bash
222
+ echo "AUTO_MODEL_ROUTER_API_KEY=local" >> "$HERMES_HOME/.env"
223
+ ```
224
+
225
+ **4. Restart Hermes.** On load, the plugin spawns the router (`auto-model-router
226
+ serve`) as a subprocess on port 8788 and registers the provider profile. Select
227
+ `auto-model-router/auto` as the model.
228
+
229
+ The plugin runs the router against its **own** config home
230
+ (`$HERMES_HOME/auto-model-router/`), separate from omp's
231
+ `~/.auto-model-router/`, so the two harnesses never share a ledger or
232
+ conversation state and don't leak routing toasts into each other's UIs.
233
+
234
+ The router serves `GET /v1/models` (returning the `auto`, `auto-cheap`,
235
+ `auto-max` profiles) and `POST /v1/chat/completions`, which Hermes's custom
236
+ endpoint discovery verifies. The router's own OpenRouter key resolution
237
+ (config → env → omp auth store) applies — Hermes does not need its own
238
+ OpenRouter key.
239
+
240
+ **Standalone alternative (no plugin):** run the router yourself, then add a
241
+ custom provider:
170
242
 
171
243
  ```bash
172
244
  auto-model-router serve --port 8788
173
245
  ```
174
246
 
175
247
  ```yaml
176
- # ~/.hermes/config.yaml
248
+ # $HERMES_HOME/config.yaml
177
249
  providers:
178
250
  auto-model-router:
179
251
  base_url: http://127.0.0.1:8788/v1
180
- api_key: no-key-required
252
+ api_key: local
181
253
  default_model: auto
182
254
  ```
183
255
 
184
- **Hermes plugin (native):** copy `hermes-plugin/` to
185
- `$HERMES_HOME/plugins/model-providers/auto-model-router/` and restart Hermes.
186
- The plugin spawns the router as a subprocess on load and registers the provider
187
- profile, so Hermes routes each turn through the router automatically.
188
-
189
- The router serves `GET /v1/models` (returning the `auto`, `auto-cheap`,
190
- `auto-max` profiles) and `POST /v1/chat/completions`, which Hermes's custom
191
- endpoint discovery verifies. Select `auto-model-router/auto` as the model and
192
- the router routes each turn by price and complexity. The router's own OpenRouter
193
- key resolution (config → env → omp auth store) applies — Hermes does not need
194
- its own OpenRouter key.
195
-
196
256
  ### The OpenRouter key
197
257
 
198
- There should be exactly one OpenRouter key on the machine, and omp already owns
199
- a credential store. Resolution order:
200
-
201
- 1. `openrouter.apiKey` in `$AUTO_MODEL_ROUTER_HOME/config.yml`
202
- 2. `OPENROUTER_API_KEY` (including any `.env` omp loaded into the environment)
203
- 3. **omp's own auth store** — `~/.omp/agent/agent.db`, provider `openrouter`
204
-
205
- So `/login openrouter` inside omp is sufficient setup; nothing needs copying.
206
- The store is opened read-only and never written: omp owns it, including OAuth
207
- refresh. An expired OAuth access token is rejected rather than sent, because
208
- refreshing is omp's job and a stale bearer just burns a turn on a 401. Under
258
+ **omp does not need to be authenticated to OpenRouter.** On a routed turn omp
259
+ never calls OpenRouter directly: the embed extension registers the
260
+ `auto-model-router` provider with a placeholder bearer (`embedded`) pointing at
261
+ the in-process router, and the router holds the real OpenRouter key and makes
262
+ the upstream call. omp only needs to see that the provider "has credentials",
263
+ which the placeholder satisfies.
264
+
265
+ There should be exactly one OpenRouter key on the machine. The router resolves
266
+ it in this order:
267
+
268
+ 1. `openrouter.apiKey` in `$AUTO_MODEL_ROUTER_HOME/config.yml` router-owned,
269
+ never enters omp's environment. Set it with `auto-model-router config` or by
270
+ hand.
271
+ 2. `OPENROUTER_API_KEY` in the environment omp launches from (including any
272
+ `.env` omp loaded).
273
+ 3. **omp's own auth store** — `~/.omp/agent/agent.db`, provider `openrouter`, so
274
+ `/login openrouter` inside omp is sufficient and nothing needs copying.
275
+
276
+ Options 1–2 give the router its own key with omp left unauthenticated; option 3
277
+ is a zero-config convenience for when you *have* logged omp in. The store is
278
+ opened read-only and never written: omp owns it, including OAuth refresh. An
279
+ expired OAuth access token is rejected rather than sent, because refreshing is
280
+ omp's job and a stale bearer just burns a turn on a 401. Under
209
281
  `OMP_AUTH_BROKER_URL` the local store is not consulted at all, since a broker
210
282
  replaces it.
211
283
 
@@ -307,7 +379,8 @@ disk and back up the previous file to a timestamped `.bak`.
307
379
  | --- | --- | --- |
308
380
  | `OPENROUTER_API_KEY` | OpenRouter key (overrides the auth store). | — |
309
381
  | `AUTO_MODEL_ROUTER_HOME` | Config + database directory. | `~/.auto-model-router` |
310
- | `AUTO_MODEL_ROUTER_PORT` | Pin a specific bind port (rarely needed; the embedded router picks a free one otherwise). | OS-assigned |
382
+ | `AUTO_MODEL_ROUTER_HOST` | Bind address override. | `127.0.0.1` |
383
+ | `AUTO_MODEL_ROUTER_LOG` | Log level: `silent`/`error`/`warn`/`info`/`debug`. | `info` |
311
384
  | `AUTO_MODEL_ROUTER_LOG` | Log level: `silent`/`error`/`warn`/`info`/`debug`. | `info` |
312
385
  | `AUTO_MODEL_ROUTER_DB` | Override the ledger path. | `$AUTO_MODEL_ROUTER_HOME/router.db` |
313
386
  | `AUTO_MODEL_ROUTER_URL` | Toast/base URL override (the toast reads the shared port file first). | — |
@@ -472,8 +545,15 @@ on each other:
472
545
  - **Per-harness daily budget** — each harness sends an `X-Omp-Harness` header
473
546
  (from the provider block's `headers:`), and the router scopes the rolling
474
547
  24h `perDayUsd` ceiling to it. One harness can't exhaust the day for another.
475
- - **Per-harness toasts** — set `OMP_HARNESS_ID` to the same value so the
476
- extension only toasts that harness's model choices.
548
+ - **Per-session toasts** — the toast surfaces only the decisions made by *its
549
+ own* omp session. The embed extension tags every request with an
550
+ `X-Omp-Session` header (`ctx.sessionManager.getSessionId()`), the router
551
+ records it on each ledger row, and the toast filters on it. Two concurrent
552
+ interactive sessions — even of the same harness — never surface each other's
553
+ model choices. This needs no configuration.
554
+ - **Per-harness toasts** — additionally set `OMP_HARNESS_ID` to the same value
555
+ so the extension only toasts that harness's model choices. Session scoping is
556
+ finer-grained; harness scoping still applies on top when set.
477
557
 
478
558
  Configure a harness by setting `server.harnessId`; set the same id in that
479
559
  harness's `OMP_HARNESS_ID` env var.
@@ -509,7 +589,8 @@ bound, even though it changes every session.
509
589
  The toast logic is a pure, unit-tested module
510
590
  (`omp-extension/toast-logic.ts`, covered by `test/toast-logic.test.ts`): it
511
591
  toasts only decisions newer than the last seen one, skips `wasted` escalation
512
- attempts, and prefers the actual serving slug over the requested one.
592
+ attempts, prefers the actual serving slug over the requested one, and filters
593
+ to the toast's own omp session id (and harness id, when set).
513
594
 
514
595
  ---
515
596