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.
- package/.omp-plugin/marketplace.json +2 -2
- package/README.md +40 -11
- package/hermes-plugin/native/__pycache__/__init__.cpython-311.pyc +0 -0
- package/hermes-plugin/native/selftest.py +103 -0
- package/package.json +1 -1
- package/src/cost/ledger.ts +6 -1
- package/src/cost/summary.ts +3 -1
- package/src/wire/openai/responses.ts +28 -1
- package/test/harness-requests.test.ts +3 -0
- package/test/hermes-plugin.test.ts +32 -0
- package/test/summary.test.ts +1 -1
- package/test/tokens.test.ts +2 -0
- package/test/wire-responses.test.ts +15 -1
|
@@ -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.
|
|
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.
|
|
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
|
|
434
|
-
`test/fixtures/harness/codex-responses.json`. Stateless only —
|
|
435
|
-
`store: false` and the full input each turn; `previous_response_id`
|
|
436
|
-
rejected. Reasoning summaries and encrypted reasoning are not produced.
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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 |
|
|
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 |
|
|
Binary file
|
|
@@ -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
package/src/cost/ledger.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/cost/summary.ts
CHANGED
|
@@ -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
|
+
});
|
package/test/summary.test.ts
CHANGED
|
@@ -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 {
|
package/test/tokens.test.ts
CHANGED
|
@@ -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");
|