auto-model-router 0.4.8 → 0.4.9

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.4.8",
10
+ "version": "0.4.9",
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.4.8",
17
+ "version": "0.4.9",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/README.md CHANGED
@@ -430,12 +430,14 @@ wire_api = "responses"
430
430
  http_headers = { "X-Omp-Harness" = "codex" }
431
431
  ```
432
432
 
433
- Verified live with codex 0.153: the captured request is
434
- `test/fixtures/harness/codex-responses.json`. Stateless only — Codex sends
435
- `store: false` and the full input each turn; `previous_response_id` is
436
- rejected. Reasoning summaries and encrypted reasoning are not produced. No
437
- session id or hooks: reports are per harness, and there is no toast, digest
438
- or `/router`.
433
+ Verified live with codex 0.153, text and tool-call turns: the captured
434
+ request is `test/fixtures/harness/codex-responses.json`. Stateless only —
435
+ Codex sends `store: false` and the full input each turn; `previous_response_id`
436
+ is rejected. Reasoning summaries and encrypted reasoning are not produced.
437
+ Codex's thread id (sent in the body) becomes the session id and its agent
438
+ name marks subagents, so per-session reports, feedback over the HTTP API and
439
+ the subagent profile work without a plugin. No hooks: there is no toast,
440
+ digest or `/router`.
439
441
 
440
442
  ### Aider
441
443
 
@@ -447,10 +449,18 @@ aider --model openai/auto
447
449
 
448
450
  Verified live with aider 0.86 (captured request:
449
451
  `test/fixtures/harness/aider.json`). Aider sends no tool calls, so every turn
450
- classifies on its text alone, and no custom headers, so its rows carry no
451
- harness id unless you set one in a model settings file
452
- (`extra_params: { extra_headers: { X-Omp-Harness: aider } }`). No session
453
- id or hooks.
452
+ classifies on its text alone. It sends no custom headers by default; a model
453
+ settings file in the project adds the harness id (verified live):
454
+
455
+ ```yaml
456
+ # .aider.model.settings.yml
457
+ - name: openai/auto
458
+ extra_params:
459
+ extra_headers:
460
+ X-Omp-Harness: aider
461
+ ```
462
+
463
+ No session id or hooks.
454
464
 
455
465
  ### Cline, Roo Code, Kilo Code
456
466
 
@@ -1101,6 +1111,25 @@ there); and the switch happens at prompt boundaries, never mid-turn.
1101
1111
 
1102
1112
  ## Multiple coding harnesses, one router
1103
1113
 
1114
+ **One router process for everything.** omp's embed extension binds a private
1115
+ router on an ephemeral port per session by default, while Hermes, Codex,
1116
+ OpenCode and Aider talk to a standalone router on port 8788. Those are two
1117
+ processes over two homes, and per-process state (pins, the digest re-run
1118
+ memory) and reports stay apart. To share one router:
1119
+
1120
+ 1. Run it once, before the harnesses start: `auto-model-router serve --port 8788`
1121
+ (against the default home, `~/.auto-model-router`).
1122
+ 2. Set `AUTO_MODEL_ROUTER_PORT=8788` in omp's environment. The embed then
1123
+ attaches to the router already answering on that port instead of binding
1124
+ its own (it still binds 8788 itself if nothing is there, which the others
1125
+ then reuse).
1126
+ 3. Point Hermes, Codex, OpenCode and Aider at `http://127.0.0.1:8788/v1` as
1127
+ in their recipes. Hermes's provider plugin only spawns a router when
1128
+ nothing listens on 8788, so it joins the shared one too.
1129
+
1130
+ Each harness keeps its own `X-Omp-Harness` id, so budgets and reports stay
1131
+ per harness while the ledger, catalog and conversation state are shared.
1132
+
1104
1133
  What each harness gets today. "Config only" means the OpenAI-compatible wire
1105
1134
  plus a harness header; the rest needs the harness's own hook API.
1106
1135
 
@@ -1108,7 +1137,7 @@ plus a harness header; the rest needs the harness's own hook API.
1108
1137
  | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
1109
1138
  | omp | native provider | yes | yes | yes | yes | full hub | yes | yes | experimental |
1110
1139
  | Hermes | provider plugin | yes | yes (native plugin) | yes (native plugin) | no | text | yes (native plugin) | on demand | no |
1111
- | Codex CLI | Responses API wire | yes | no | no | no | no | compaction only | no | no |
1140
+ | Codex CLI | Responses API wire | yes | yes (from body) | yes (from body) | no | no | compaction only | no | no |
1112
1141
  | Aider | config only | via model settings | no | no | no | no | no tools | no | no |
1113
1142
  | Cline / Roo / Kilo | config only | if headers supported | no | no | no | no | compaction only | no | no |
1114
1143
  | OpenCode | config + plugin | yes | yes (plugin) | yes (plugin) | yes (plugin) | no | yes (plugin) | no | no |
@@ -0,0 +1,103 @@
1
+ """Self-test for the Hermes native plugin's pure functions.
2
+
3
+ Run by test/hermes-plugin.test.ts when a Python interpreter is on PATH (the
4
+ Bun suite cannot import Python), and directly:
5
+
6
+ python hermes-plugin/native/selftest.py
7
+
8
+ Exercises identity headers, the digest gate and replacement, and the /router
9
+ command rendering against fake transports. Exits non-zero on the first
10
+ failure with the assertion that failed.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.util
16
+ import json
17
+ import os
18
+ import sys
19
+
20
+ HERE = os.path.dirname(os.path.abspath(__file__))
21
+ spec = importlib.util.spec_from_file_location("amr_native", os.path.join(HERE, "__init__.py"))
22
+ m = importlib.util.module_from_spec(spec)
23
+ assert spec.loader is not None
24
+ spec.loader.exec_module(m)
25
+
26
+
27
+ def test_identity_headers() -> None:
28
+ m.on_pre_llm_call(session_id="s1", parent_session_id="")
29
+ m.on_pre_llm_call(session_id="s2", parent_session_id="s1")
30
+ r = m.on_llm_request(request={"model": "auto", "extra_headers": {"A": "b"}}, provider="auto-model-router", session_id="s2")
31
+ assert r["request"]["extra_headers"] == {"A": "b", "X-Omp-Harness": m.HARNESS_ID, "X-Omp-Session": "s2", "X-Omp-Subagent": "1"}, r
32
+ main = m.on_llm_request(request={"model": "auto"}, provider="auto-model-router", session_id="s1")
33
+ assert "X-Omp-Subagent" not in main["request"]["extra_headers"], main
34
+ assert m.on_llm_request(request={}, provider="anthropic", session_id="s1") is None
35
+ assert m._CURRENT["session_id"] == "s1"
36
+
37
+
38
+ def test_digest() -> None:
39
+ m.POLICY.value = {"enabled": True, "minBytes": 10, "maxBytes": 100000, "tools": ["read", "grep", "bash"], "toolAliases": {"read_file": "read", "terminal": "bash"}, "fromTier": "moderate"}
40
+ m.POLICY._at = 10**12
41
+ calls = []
42
+
43
+ def fake_post(path, payload, timeout=0):
44
+ calls.append((path, payload["toolName"], len(payload["content"])))
45
+ return {"digested": True, "text": "[digest] short"}
46
+
47
+ big = json.dumps({"content": "x" * 500, "path": "a.ts"})
48
+ out = m.maybe_digest(big, "read_file", {"path": "a.ts"}, "s1", post=fake_post)
49
+ assert json.loads(out)["content"] == "[digest] short", out
50
+ assert json.loads(out)["path"] == "a.ts"
51
+ assert calls == [("/v1/router/digest", "read_file", 500)], calls
52
+ err = json.dumps({"error": "nope", "content": "x" * 500})
53
+ assert m.maybe_digest(err, "read_file", {}, "s1", post=fake_post) == err
54
+ assert m.maybe_digest(json.dumps({"content": "x" * 500}), "write_file", {}, "s1", post=fake_post).startswith('{"content": "xxx')
55
+ plain = "plain text " * 100
56
+ assert m.maybe_digest(plain, "read_file", {}, "s1", post=fake_post) == plain
57
+ assert len(calls) == 1
58
+ assert m.canonical_tool(m.POLICY.value, "TERMINAL") == "bash"
59
+ assert m.largest_string_field({"a": "x" * 90, "b": "y" * 5}) == "a"
60
+ assert m.largest_string_field({"a": "x" * 50, "b": "y" * 50}) is None
61
+
62
+
63
+ def test_router_command() -> None:
64
+ def fake_get(path, timeout=5.0, text=False):
65
+ if path.startswith("/v1/router/report"):
66
+ return "REPORT " + path
67
+ if path.startswith("/v1/router/summary"):
68
+ return "SUMMARY " + path
69
+ if path.startswith("/health"):
70
+ return {"status": "ok", "apiKeyConfigured": True, "apiKeySource": "env", "catalog": {"models": 3}, "ollama": None, "softFailures": {"spikes": [{"slug": "a/b", "recentRate": 0.5, "recentDispatches": 8, "baselineRate": 0.05}]}}
71
+ if path.startswith("/v1/router/decisions"):
72
+ return {"entries": [{"slug": "a/b", "tier": "hard", "classificationSource": "heuristic", "confidence": 0.7, "reportedUsd": 0.0000097, "usage": {"promptTokens": 100, "cachedTokens": 50}, "latencyMs": 900, "reasons": ["r1"]}]}
73
+ return {}
74
+
75
+ def fake_post(path, payload, timeout=5.0):
76
+ if path == "/v1/router/feedback":
77
+ return {"slug": "a/b", "tier": "hard"}
78
+ return {"override": {"slug": payload.get("slug"), "tier": payload.get("tier"), "turnsLeft": payload.get("turns")}}
79
+
80
+ assert m.router_command("report 30 --all", get=fake_get, post=fake_post) == "REPORT /v1/router/report?days=30&format=text"
81
+ assert m.router_command("report", get=fake_get, post=fake_post).endswith(f"days=7&format=text&harness={m.HARNESS_ID}")
82
+ assert m.router_command("summary", get=fake_get, post=fake_post).startswith("SUMMARY /v1/router/summary?format=text&harness=")
83
+ status = m.router_command("status", get=fake_get, post=fake_post)
84
+ assert "soft failures SPIKING (1)" in status and "a/b: 50% of 8" in status, status
85
+ why = m.router_command("why", get=fake_get, post=fake_post)
86
+ assert "last turn: a/b [hard]" in why and "$0.00001" in why and "cache hit 50%" in why, why
87
+ assert m.router_command("good nice", get=fake_get, post=fake_post) == "recorded good for a/b [hard]"
88
+ assert m.router_command("pin a/b", get=fake_get, post=fake_post) == "pin: a/b"
89
+ assert m.router_command("tier hard 5", get=fake_get, post=fake_post) == "tier: hard for 5 turns"
90
+ assert m.router_command("tier off", get=fake_get, post=fake_post) == "tier: cleared"
91
+ assert m.router_command("bogus", get=fake_get, post=fake_post) == m.USAGE
92
+
93
+ def down(path, timeout=5.0, text=False):
94
+ raise OSError("connection refused")
95
+
96
+ assert m.router_command("status", get=down, post=fake_post).startswith("router unreachable at")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ for name, fn in [(n, f) for n, f in globals().items() if n.startswith("test_")]:
101
+ fn()
102
+ print(f"ok {name}")
103
+ sys.exit(0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -376,6 +376,9 @@ export function createLedger(db: Database, cfg: RouterConfig): Ledger {
376
376
  const ratioStmt = db.query("SELECT est_bytes, actual_tokens, samples FROM token_calibration WHERE tokenizer = ?");
377
377
  const recentStmt = db.query("SELECT * FROM ledger ORDER BY created_at_ms DESC LIMIT ?");
378
378
  const pruneStmt = db.query("DELETE FROM ledger WHERE created_at_ms < ?");
379
+ // Ollama meter samples (one per usage poll) only matter for the current
380
+ // billing cycle's calibration; they age out with the ledger rows.
381
+ const pruneMeterStmt = db.query("DELETE FROM ollama_meter_samples WHERE at_ms < ?");
379
382
  const wasteStmt = db.query("UPDATE ledger SET wasted = 1 WHERE id = ?");
380
383
  const providerSpendStmt = db.query(
381
384
  "SELECT COALESCE(SUM(COALESCE(reported_usd, predicted_usd)), 0) AS total FROM ledger WHERE created_at_ms >= ? AND COALESCE(served_slug, slug) LIKE ?",
@@ -627,7 +630,9 @@ export function createLedger(db: Database, cfg: RouterConfig): Ledger {
627
630
  },
628
631
  prune(retentionDays: number, nowMs = Date.now()): number {
629
632
  if (retentionDays <= 0) return 0;
630
- return pruneStmt.run(nowMs - retentionDays * DAY_MS).changes;
633
+ const cutoff = nowMs - retentionDays * DAY_MS;
634
+ pruneMeterStmt.run(cutoff);
635
+ return pruneStmt.run(cutoff).changes;
631
636
  },
632
637
  markWasted(id: string): void {
633
638
  wasteStmt.run(id);
@@ -28,6 +28,7 @@ export interface SummaryWindow {
28
28
  modelSwitches: number;
29
29
  digests: number;
30
30
  digestSpendUsd: number;
31
+ digestReruns: number;
31
32
  subagentSpendUsd: number;
32
33
  }
33
34
 
@@ -81,6 +82,7 @@ function windowOf(r: UsageReport): SummaryWindow {
81
82
  modelSwitches: t.modelSwitches,
82
83
  digests: t.digests,
83
84
  digestSpendUsd: t.digestSpendUsd,
85
+ digestReruns: t.digestReruns,
84
86
  subagentSpendUsd: t.subagentSpendUsd,
85
87
  };
86
88
  }
@@ -172,7 +174,7 @@ export function renderDailySummary(s: DailySummary): string {
172
174
  out.push(b.savedShare >= 0 ? `saved ${pct(b.savedShare)} vs ${b.slug} (${usd(b.usd)} at list)` : `cost ${pct(-b.savedShare)} MORE than ${b.slug} (${usd(b.usd)} at list)`);
173
175
  }
174
176
  const extras: string[] = [];
175
- if (c.digests > 0) extras.push(`${c.digests} digests for ${usd(c.digestSpendUsd)}`);
177
+ if (c.digests > 0) extras.push(`${c.digests} digests for ${usd(c.digestSpendUsd)} (re-run rate ${pct(c.digestReruns / c.digests)})`);
176
178
  if (c.subagentSpendUsd > 0) extras.push(`subagents ${usd(c.subagentSpendUsd)}`);
177
179
  if (extras.length > 0) out.push(extras.join(" · "));
178
180
  }
@@ -123,8 +123,35 @@ export function responsesToChatBody(body: unknown): Rec {
123
123
  return out;
124
124
  }
125
125
 
126
+ /**
127
+ * Codex carries its identity in the body, not in headers: the thread id under
128
+ * `client_metadata` (also the `prompt_cache_key`), and the agent name inside
129
+ * the JSON-encoded `x-codex-turn-metadata` (`/root` is the main agent). When
130
+ * the caller sent no X-Omp-Session / X-Omp-Subagent header, they are derived
131
+ * from those, so Codex gets per-session reports, feedback and the subagent
132
+ * profile without a plugin.
133
+ */
134
+ export function identityHeadersFromBody(body: unknown, headers: Headers): Headers {
135
+ if (!isRec(body)) return headers;
136
+ const h = new Headers(headers);
137
+ const cm = isRec(body.client_metadata) ? body.client_metadata : {};
138
+ if ((h.get("x-omp-session") ?? "").trim() === "") {
139
+ const id = [cm.thread_id, cm.session_id, body.prompt_cache_key].find((v): v is string => typeof v === "string" && v !== "");
140
+ if (id !== undefined) h.set("x-omp-session", id);
141
+ }
142
+ if ((h.get("x-omp-subagent") ?? "").trim() === "" && typeof cm["x-codex-turn-metadata"] === "string") {
143
+ try {
144
+ const meta: unknown = JSON.parse(cm["x-codex-turn-metadata"]);
145
+ if (isRec(meta) && typeof meta.agent_name === "string" && meta.agent_name !== "" && meta.agent_name !== "/root") h.set("x-omp-subagent", "1");
146
+ } catch {
147
+ // Not JSON: no subagent signal.
148
+ }
149
+ }
150
+ return h;
151
+ }
152
+
126
153
  export function parseResponsesRequest(body: unknown, headers: Headers): NormRequest {
127
- const norm = parseChatRequest(responsesToChatBody(body), headers);
154
+ const norm = parseChatRequest(responsesToChatBody(body), identityHeadersFromBody(body, headers));
128
155
  return { ...norm, protocol: "openai-responses" };
129
156
  }
130
157
 
@@ -138,6 +138,9 @@ describe("captured harness requests", () => {
138
138
  expect(req.harnessId).toBe("codex");
139
139
  expect(req.requestedModel).toBe("auto");
140
140
  expect(req.stream).toBe(true);
141
+ // The thread id in the body becomes the session id; the root agent is not a subagent.
142
+ expect(req.ompSessionId).toBe((f.body.client_metadata as { thread_id: string }).thread_id);
143
+ expect(req.isSubagent).toBe(false);
141
144
  // instructions became the system message; the input items follow in order.
142
145
  expect(req.messages.map((m) => m.role)).toEqual(["system", "developer", "user", "user"]);
143
146
  expect(req.messages[0]?.text).toContain("coding agent running in the Codex CLI");
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ /**
4
+ * The Hermes native plugin is Python, which this suite cannot import. Its
5
+ * pure functions (identity headers, digest gate and replacement, /router
6
+ * rendering) are exercised by hermes-plugin/native/selftest.py; this test
7
+ * runs it when a Python interpreter is on PATH and is skipped otherwise, so
8
+ * a machine without Python still passes the suite but a machine with one
9
+ * catches a regression.
10
+ */
11
+
12
+ const python = Bun.which("python") ?? Bun.which("python3");
13
+ const ROOT = `${import.meta.dir}/..`;
14
+
15
+ describe("hermes native plugin", () => {
16
+ (python === null ? test.skip : test)("the Python self-test passes", () => {
17
+ const run = Bun.spawnSync([python!, "hermes-plugin/native/selftest.py"], { cwd: ROOT, stdout: "pipe", stderr: "pipe" });
18
+ const out = `${run.stdout.toString()}\n${run.stderr.toString()}`;
19
+ expect(out).toContain("ok test_identity_headers");
20
+ expect(out).toContain("ok test_digest");
21
+ expect(out).toContain("ok test_router_command");
22
+ expect(run.exitCode).toBe(0);
23
+ });
24
+
25
+ test("the plugin files parse as Python and declare the standalone kind", async () => {
26
+ const manifest = await Bun.file(`${ROOT}/hermes-plugin/native/plugin.yaml`).text();
27
+ expect(manifest).toContain("kind: standalone");
28
+ const src = await Bun.file(`${ROOT}/hermes-plugin/native/__init__.py`).text();
29
+ expect(src).toContain("def register(ctx");
30
+ for (const hook of ['register_hook("pre_llm_call"', 'register_middleware("llm_request"', 'register_middleware("tool_execution"', 'register_command("router"']) expect(src).toContain(hook);
31
+ });
32
+ });
@@ -95,7 +95,7 @@ describe("buildDailySummary", () => {
95
95
  // Tiny seeded turns cost more than Opus would have at list price: the honest branch renders.
96
96
  expect(text).toContain("cost 574% MORE than anthropic/claude-opus-5 ($0.079 at list)");
97
97
  expect(renderDailySummary({ ...s, baseline: { slug: "anthropic/claude-opus-5", usd: 2.5, savedShare: 0.7876 } })).toContain("saved 79% vs anthropic/claude-opus-5 ($2.50 at list)");
98
- expect(text).toContain("1 digests for $0.001");
98
+ expect(text).toContain("1 digests for $0.001 (re-run rate 0%)");
99
99
  expect(text).toContain("soft failures SPIKING (1):\n vendor/big: 50% of 8 failed in the last 1h (7d baseline 3% of 90)");
100
100
  expect(text).toContain("ollama: pro plan $25.20 of $60 · ~23 days of credits left");
101
101
  } finally {
@@ -292,7 +292,9 @@ describe("ledger.prune and markWasted", () => {
292
292
  for (let i = 0; i < 5; i++) ledger.record(entry({ createdAtMs: now - i * 100 * DAY }));
293
293
  expect(ledger.prune?.(0, now)).toBe(0);
294
294
  expect(ledger.recentEntries(10)).toHaveLength(5);
295
+ db.run("INSERT INTO ollama_meter_samples (at_ms, meter_usd, ledger_usd) VALUES (?, 1, 1), (?, 2, 2)", [now - 400 * DAY, now - DAY]);
295
296
  expect(ledger.prune?.(365, now)).toBe(1); // only the 400-day-old row
297
+ expect((db.query("SELECT COUNT(*) AS n FROM ollama_meter_samples").get() as { n: number }).n).toBe(1);
296
298
  expect(ledger.recentEntries(10)).toHaveLength(4);
297
299
  expect(ledger.prune?.(150, now)).toBe(2); // 200 and 300 days old
298
300
  const left = ledger.recentEntries(10);
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { WireErrorException } from "../src/wire/openai/errors.ts";
4
- import { createResponsesBufferedSink, createResponsesStreamingSink, parseResponsesRequest, responsesToChatBody } from "../src/wire/openai/responses.ts";
4
+ import { createResponsesBufferedSink, createResponsesStreamingSink, identityHeadersFromBody, parseResponsesRequest, responsesToChatBody } from "../src/wire/openai/responses.ts";
5
5
  import type { StreamEvent, TurnSummary, UpstreamChunk } from "../src/wire/types.ts";
6
6
 
7
7
  /**
@@ -67,6 +67,20 @@ describe("responsesToChatBody", () => {
67
67
  expect(() => responsesToChatBody({ model: "auto", input: [] })).toThrow(WireErrorException);
68
68
  });
69
69
 
70
+ test("Codex identity is read from the body when the headers carry none", () => {
71
+ const meta = (agent: string) => JSON.stringify({ session_id: "t1", thread_id: "t1", agent_name: agent, turn_id: "u1" });
72
+ const body = { model: "auto", input: "x", prompt_cache_key: "t1", client_metadata: { thread_id: "t1", session_id: "t1", "x-codex-turn-metadata": meta("/root") } };
73
+ const main = parseResponsesRequest(body, HEADERS);
74
+ expect(main.ompSessionId).toBe("t1");
75
+ expect(main.isSubagent).toBe(false);
76
+ const sub = parseResponsesRequest({ ...body, client_metadata: { ...body.client_metadata, "x-codex-turn-metadata": meta("/root/explorer") } }, HEADERS);
77
+ expect(sub.isSubagent).toBe(true);
78
+ // Explicit headers win; a body without metadata adds nothing.
79
+ expect(identityHeadersFromBody(body, new Headers({ "X-Omp-Session": "mine" })).get("x-omp-session")).toBe("mine");
80
+ expect(identityHeadersFromBody({ model: "auto", input: "x" }, HEADERS).get("x-omp-session")).toBeNull();
81
+ expect(identityHeadersFromBody({ model: "auto", input: "x", client_metadata: { "x-codex-turn-metadata": "not json" } }, HEADERS).get("x-omp-subagent")).toBeNull();
82
+ });
83
+
70
84
  test("parseResponsesRequest yields a routed request tagged with the wire", () => {
71
85
  const req = parseResponsesRequest({ model: "auto-model-router/auto-cheap", input: "hello", stream: false }, HEADERS);
72
86
  expect(req.protocol).toBe("openai-responses");