auto-model-router 0.4.8 → 0.4.10

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.
@@ -0,0 +1,456 @@
1
+ {
2
+ "harness": "roo",
3
+ "capturedAtMs": 1788827425238,
4
+ "headers": {
5
+ "content-type": "application/json",
6
+ "user-agent": "RooCode/3.54.0",
7
+ "x-omp-harness": "roo",
8
+ "x-title": "Roo Code",
9
+ "http-referer": "https://github.com/RooVetGit/Roo-Cline"
10
+ },
11
+ "body": {
12
+ "model": "auto",
13
+ "temperature": 0,
14
+ "messages": [
15
+ {
16
+ "role": "system",
17
+ "content": [
18
+ {
19
+ "type": "text",
20
+ "text": "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.\n\n====\n\nMARKDOWN RULES\n\nALL responses MUST show ANY `language construct` OR filename reference a…[18131 chars]",
21
+ "cache_control": {
22
+ "type": "ephemeral"
23
+ }
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "role": "user",
29
+ "content": [
30
+ {
31
+ "type": "text",
32
+ "text": "<user_message>\nReply with the single word ok\n</user_message>"
33
+ },
34
+ {
35
+ "type": "text",
36
+ "text": "<environment_details>\n# VSCode Visible Files\n\n\n# VSCode Open Tabs\n\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2026-09-08T00:30:18.293Z\nUser time zone: America/Los_Angeles, UTC-7:00\n\n# Current Cost\n$0.00\n\n# Current Mode\n<slug>architect</slug>\n<name>🏗️ Architect</name>\n<model>auto</model>\n\n\n# Current Workspace Directory (c:/Users/drewa/AppData/Local/Temp/roo-amr) Files\nroo-settings.json\nY…[550 chars]"
37
+ }
38
+ ]
39
+ },
40
+ {
41
+ "role": "assistant",
42
+ "content": "ok"
43
+ },
44
+ {
45
+ "role": "user",
46
+ "content": [
47
+ {
48
+ "type": "text",
49
+ "text": "[ERROR] You did not use a tool in your previous response! Please retry with a tool use.\n\n# Reminder: Instructions for Tool Use\n\nTools are invoked using the platform's native tool calling mechanism. Each tool requires specific parameters as defined in the tool descriptions. Refer to the tool definitions provided in your system instructions for the correct parameter structure and usage examples.\n\nAl…[858 chars]"
50
+ },
51
+ {
52
+ "type": "text",
53
+ "text": "<environment_details>\n# VSCode Visible Files\n\n\n# VSCode Open Tabs\n\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2026-09-08T00:30:21.035Z\nUser time zone: America/Los_Angeles, UTC-7:00\n\n# Current Cost\n$0.00\n\n# Current Mode\n<slug>architect</slug>\n<name>🏗️ Architect</name>\n<model>auto</model>\nYou have not created a todo list yet. Create one with `update_todo_list` if your task is complicated …[450 chars]",
54
+ "cache_control": {
55
+ "type": "ephemeral"
56
+ }
57
+ }
58
+ ]
59
+ },
60
+ {
61
+ "role": "assistant",
62
+ "content": "",
63
+ "tool_calls": [
64
+ {
65
+ "id": "sqiUG7TuL",
66
+ "type": "function",
67
+ "function": {
68
+ "name": "update_todo_list",
69
+ "arguments": "{\"todos\":\"[ ] Analyze requirements\\n[x] Design architecture\\n[-] Implement core logic\\n[ ] Write tests\\n[ ] Update documentation\"}"
70
+ }
71
+ }
72
+ ]
73
+ },
74
+ {
75
+ "role": "tool",
76
+ "tool_call_id": "sqiUG7TuL",
77
+ "content": "Todo list updated successfully."
78
+ },
79
+ {
80
+ "role": "user",
81
+ "content": [
82
+ {
83
+ "type": "text",
84
+ "text": "<environment_details>\n# VSCode Visible Files\n\n\n# VSCode Open Tabs\n\n\n# Current Time\nCurrent time in ISO 8601 UTC format: 2026-09-08T00:30:24.425Z\nUser time zone: America/Los_Angeles, UTC-7:00\n\n# Current Cost\n$0.00\n\n# Current Mode\n<slug>architect</slug>\n<name>🏗️ Architect</name>\n<model>auto</model>\n====\n\nREMINDERS\n\nBelow is your current list of reminders for this task. Keep them updated as you prog…[780 chars]",
85
+ "cache_control": {
86
+ "type": "ephemeral"
87
+ }
88
+ }
89
+ ]
90
+ }
91
+ ],
92
+ "stream": true,
93
+ "stream_options": {
94
+ "include_usage": true
95
+ },
96
+ "tools": [
97
+ {
98
+ "type": "function",
99
+ "function": {
100
+ "name": "apply_diff",
101
+ "description": "Apply precise, targeted modifications to an existing file using one or more search/replace blocks. This tool is for surgical edits only; the 'SEARCH' block must exactly match the existing content, including whitespace and indentation. To make multiple targeted changes, provide multiple SEARCH/REPLACE blocks in the 'diff' parameter. Use the 'read_file' tool first if you are not confident in the exa…[425 chars]",
102
+ "parameters": {
103
+ "type": "object",
104
+ "properties": {
105
+ "path": {
106
+ "type": "string",
107
+ "description": "The path of the file to modify, relative to the current workspace directory."
108
+ },
109
+ "diff": {
110
+ "type": "string",
111
+ "description": "A string containing one or more search/replace blocks defining the changes. The ':start_line:' is required and indicates the starting line number of the original content. You must not add a start line for the replacement content. Each block must follow this format:\n<<<<<<< SEARCH\n:start_line:[line_number]\n-------\n[exact content to find]\n=======\n[new content to replace with]\n>>>>>>> REPLACE"
112
+ }
113
+ },
114
+ "required": [
115
+ "path",
116
+ "diff"
117
+ ],
118
+ "additionalProperties": false
119
+ },
120
+ "strict": true
121
+ }
122
+ },
123
+ {
124
+ "type": "function",
125
+ "function": {
126
+ "name": "ask_followup_question",
127
+ "description": "Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.\n\nParameters:\n- question: (required) A clear, specific question addressing the information needed\n- follow_up: (required) A list of 2-4 suggested answers. Suggestions must be complete, actionable answers without placeholders. Optionally includ…[948 chars]",
128
+ "strict": true,
129
+ "parameters": {
130
+ "type": "object",
131
+ "properties": {
132
+ "question": {
133
+ "type": "string",
134
+ "description": "Clear, specific question that captures the missing information you need"
135
+ },
136
+ "follow_up": {
137
+ "type": "array",
138
+ "description": "Required list of 2-4 suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch",
139
+ "items": {
140
+ "type": "object",
141
+ "properties": {
142
+ "text": {
143
+ "type": "string",
144
+ "description": "Suggested answer the user can pick"
145
+ },
146
+ "mode": {
147
+ "type": "string",
148
+ "description": "Optional mode slug to switch to if this suggestion is chosen (e.g., code, architect)"
149
+ }
150
+ },
151
+ "required": [
152
+ "text",
153
+ "mode"
154
+ ],
155
+ "additionalProperties": false
156
+ },
157
+ "minItems": 1,
158
+ "maxItems": 4
159
+ }
160
+ },
161
+ "required": [
162
+ "question",
163
+ "follow_up"
164
+ ],
165
+ "additionalProperties": false
166
+ }
167
+ }
168
+ },
169
+ {
170
+ "type": "function",
171
+ "function": {
172
+ "name": "attempt_completion",
173
+ "description": "After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make …[1126 chars]",
174
+ "strict": true,
175
+ "parameters": {
176
+ "type": "object",
177
+ "properties": {
178
+ "result": {
179
+ "type": "string",
180
+ "description": "Final result message to deliver to the user once the task is complete"
181
+ }
182
+ },
183
+ "required": [
184
+ "result"
185
+ ],
186
+ "additionalProperties": false
187
+ }
188
+ }
189
+ },
190
+ {
191
+ "type": "function",
192
+ "function": {
193
+ "name": "list_files",
194
+ "description": "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.\n\nParameters:\n- path: …[820 chars]",
195
+ "strict": true,
196
+ "parameters": {
197
+ "type": "object",
198
+ "properties": {
199
+ "path": {
200
+ "type": "string",
201
+ "description": "Directory path to inspect, relative to the workspace"
202
+ },
203
+ "recursive": {
204
+ "type": "boolean",
205
+ "description": "Set true to list contents recursively; false to show only the top level"
206
+ }
207
+ },
208
+ "required": [
209
+ "path",
210
+ "recursive"
211
+ ],
212
+ "additionalProperties": false
213
+ }
214
+ }
215
+ },
216
+ {
217
+ "type": "function",
218
+ "function": {
219
+ "name": "new_task",
220
+ "description": "Create a new task instance in the chosen mode using your provided message and initial todo list (if required).\n\nCRITICAL: This tool MUST be called alone. Do NOT call this tool alongside other tools in the same message turn. If you need to gather information before delegating, use other tools in a separate turn first, then call new_task by itself in the next turn.",
221
+ "strict": true,
222
+ "parameters": {
223
+ "type": "object",
224
+ "properties": {
225
+ "mode": {
226
+ "type": "string",
227
+ "description": "Slug of the mode to begin the new task in (e.g., code, debug, architect)"
228
+ },
229
+ "message": {
230
+ "type": "string",
231
+ "description": "Initial user instructions or context for the new task"
232
+ },
233
+ "todos": {
234
+ "type": "string",
235
+ "description": "Optional initial todo list written as a markdown checklist; required when the workspace mandates todos"
236
+ }
237
+ },
238
+ "required": [
239
+ "mode",
240
+ "message",
241
+ "todos"
242
+ ],
243
+ "additionalProperties": false
244
+ }
245
+ }
246
+ },
247
+ {
248
+ "type": "function",
249
+ "function": {
250
+ "name": "read_file",
251
+ "description": "Read a file and return its contents with line numbers for diffing or discussion. IMPORTANT: This tool reads exactly one file per call. If you need multiple files, issue multiple parallel read_file calls. Supports two modes: 'slice' (default) reads lines sequentially with offset/limit; 'indentation' extracts complete semantic code blocks around an anchor line based on indentation hierarchy. Slice m…[1373 chars]",
252
+ "strict": true,
253
+ "parameters": {
254
+ "type": "object",
255
+ "properties": {
256
+ "path": {
257
+ "type": "string",
258
+ "description": "Path to the file to read, relative to the workspace"
259
+ },
260
+ "mode": {
261
+ "type": "string",
262
+ "enum": [
263
+ "slice",
264
+ "indentation"
265
+ ],
266
+ "description": "Reading mode. 'slice' (default): read lines sequentially with offset/limit - use for general file exploration or when you don't have a target line number (may truncate code mid-function). 'indentation': extract complete semantic code blocks containing anchor_line - PREFERRED when you have a line number because it guarantees complete, valid code blocks. WARNING: Do not use indentation mode without …[472 chars]"
267
+ },
268
+ "offset": {
269
+ "type": "integer",
270
+ "description": "1-based line offset to start reading from (slice mode, default: 1)"
271
+ },
272
+ "limit": {
273
+ "type": "integer",
274
+ "description": "Maximum number of lines to return (slice mode, default: 2000)"
275
+ },
276
+ "indentation": {
277
+ "type": "object",
278
+ "description": "Indentation mode options. Only used when mode='indentation'. You MUST specify anchor_line for useful results - it determines which code block to extract.",
279
+ "properties": {
280
+ "anchor_line": {
281
+ "type": "integer",
282
+ "description": "1-based line number to anchor the extraction. REQUIRED for meaningful indentation mode results. The extractor finds the semantic block (function, method, class) containing this line and returns it completely. Without anchor_line, indentation mode defaults to line 1 and returns only imports/header content. Obtain anchor_line from: search results, error stack traces, definition lookups, codebase_sea…[506 chars]"
283
+ },
284
+ "max_levels": {
285
+ "type": "integer",
286
+ "description": "Maximum indentation levels to include above the anchor (indentation mode, 0 = unlimited (default)). Higher values include more parent context."
287
+ },
288
+ "include_siblings": {
289
+ "type": "boolean",
290
+ "description": "Include sibling blocks at the same indentation level as the anchor block (indentation mode, default: false). Useful for seeing related methods in a class."
291
+ },
292
+ "include_header": {
293
+ "type": "boolean",
294
+ "description": "Include file header content (imports, module-level comments) at the top of output (indentation mode, default: true)."
295
+ },
296
+ "max_lines": {
297
+ "type": "integer",
298
+ "description": "Hard cap on lines returned for indentation mode. Acts as a separate limit from the top-level 'limit' parameter."
299
+ }
300
+ },
301
+ "required": [
302
+ "anchor_line",
303
+ "max_levels",
304
+ "include_siblings",
305
+ "include_header",
306
+ "max_lines"
307
+ ],
308
+ "additionalProperties": false
309
+ }
310
+ },
311
+ "required": [
312
+ "path",
313
+ "mode",
314
+ "offset",
315
+ "limit",
316
+ "indentation"
317
+ ],
318
+ "additionalProperties": false
319
+ }
320
+ }
321
+ },
322
+ {
323
+ "type": "function",
324
+ "function": {
325
+ "name": "skill",
326
+ "description": "Load and execute a skill by name. Skills provide specialized instructions for common tasks like creating MCP servers or custom modes.\n\nUse this tool when you need to follow specific procedures documented in a skill. Available skills are listed in the AVAILABLE SKILLS section of the system prompt.",
327
+ "strict": true,
328
+ "parameters": {
329
+ "type": "object",
330
+ "properties": {
331
+ "skill": {
332
+ "type": "string",
333
+ "description": "Name of the skill to load (e.g., create-mcp-server, create-mode). Must match a skill name from the available skills list."
334
+ },
335
+ "args": {
336
+ "type": "string",
337
+ "description": "Optional context or arguments to pass to the skill"
338
+ }
339
+ },
340
+ "required": [
341
+ "skill",
342
+ "args"
343
+ ],
344
+ "additionalProperties": false
345
+ }
346
+ }
347
+ },
348
+ {
349
+ "type": "function",
350
+ "function": {
351
+ "name": "search_files",
352
+ "description": "Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.\n\nCraft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based i…[1266 chars]",
353
+ "strict": true,
354
+ "parameters": {
355
+ "type": "object",
356
+ "properties": {
357
+ "path": {
358
+ "type": "string",
359
+ "description": "Directory to search recursively, relative to the workspace"
360
+ },
361
+ "regex": {
362
+ "type": "string",
363
+ "description": "Rust-compatible regular expression pattern to match"
364
+ },
365
+ "file_pattern": {
366
+ "type": "string",
367
+ "description": "Optional glob to limit which files are searched (e.g., *.ts)"
368
+ }
369
+ },
370
+ "required": [
371
+ "path",
372
+ "regex",
373
+ "file_pattern"
374
+ ],
375
+ "additionalProperties": false
376
+ }
377
+ }
378
+ },
379
+ {
380
+ "type": "function",
381
+ "function": {
382
+ "name": "switch_mode",
383
+ "description": "Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.",
384
+ "strict": true,
385
+ "parameters": {
386
+ "type": "object",
387
+ "properties": {
388
+ "mode_slug": {
389
+ "type": "string",
390
+ "description": "Slug of the mode to switch to (e.g., code, ask, architect)"
391
+ },
392
+ "reason": {
393
+ "type": "string",
394
+ "description": "Explanation for why the mode switch is needed"
395
+ }
396
+ },
397
+ "required": [
398
+ "mode_slug",
399
+ "reason"
400
+ ],
401
+ "additionalProperties": false
402
+ }
403
+ }
404
+ },
405
+ {
406
+ "type": "function",
407
+ "function": {
408
+ "name": "update_todo_list",
409
+ "description": "Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos disc…[1705 chars]",
410
+ "strict": true,
411
+ "parameters": {
412
+ "type": "object",
413
+ "properties": {
414
+ "todos": {
415
+ "type": "string",
416
+ "description": "Full markdown checklist in execution order, using [ ] for pending, [x] for completed, and [-] for in progress"
417
+ }
418
+ },
419
+ "required": [
420
+ "todos"
421
+ ],
422
+ "additionalProperties": false
423
+ }
424
+ }
425
+ },
426
+ {
427
+ "type": "function",
428
+ "function": {
429
+ "name": "write_to_file",
430
+ "description": "Request to write content to a file. This tool is primarily used for creating new files or for scenarios where a complete rewrite of an existing file is intentionally required. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.\n\n**Important:** You should prefer using other editing tools o…[1359 chars]",
431
+ "strict": true,
432
+ "parameters": {
433
+ "type": "object",
434
+ "properties": {
435
+ "path": {
436
+ "type": "string",
437
+ "description": "The path of the file to write to (relative to the current workspace directory)"
438
+ },
439
+ "content": {
440
+ "type": "string",
441
+ "description": "The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content."
442
+ }
443
+ },
444
+ "required": [
445
+ "path",
446
+ "content"
447
+ ],
448
+ "additionalProperties": false
449
+ }
450
+ }
451
+ }
452
+ ],
453
+ "tool_choice": "auto",
454
+ "parallel_tool_calls": true
455
+ }
456
+ }
@@ -131,6 +131,40 @@ describe("captured harness requests", () => {
131
131
  expect(out.max_tokens).toBe(f.body.max_tokens);
132
132
  });
133
133
 
134
+ test("cline cli 3.0: chat completions with native tool calls, no harness header", async () => {
135
+ const f = await fixture("cline-cli");
136
+ const req = parseChatRequest(structuredClone(f.body), new Headers(f.headers));
137
+ expect(f.headers["user-agent"]).toContain("ai-sdk/openai-compatible");
138
+ expect(req.harnessId).toBe("");
139
+ const names = req.tools.map((t) => t.name);
140
+ for (const n of ["read_files", "search_codebase", "run_commands", "fetch_web_content"]) {
141
+ expect(names).toContain(n);
142
+ expect(DEFAULT_CONFIG.digest.tools).toContain(canonicalTool(DEFAULT_CONFIG.digest, n));
143
+ }
144
+ expect("stream_options" in req.renderUpstreamBody(MUT)).toBe(false);
145
+ });
146
+
147
+ test("kilo cli 7.5: an OpenCode-derived request with the harness header and OpenCode tool names", async () => {
148
+ const f = await fixture("kilo");
149
+ const req = parseChatRequest(structuredClone(f.body), new Headers(f.headers));
150
+ expect(f.headers["user-agent"]).toContain("Kilo-Code");
151
+ expect(req.harnessId).toBe("kilo");
152
+ const names = req.tools.map((t) => t.name);
153
+ for (const n of ["read", "grep", "glob", "bash", "webfetch"]) expect(names).toContain(n);
154
+ });
155
+
156
+ test("roo code 3.54 (VS Code): native tool calls with a tool round trip and the harness header", async () => {
157
+ const f = await fixture("roo");
158
+ const req = parseChatRequest(structuredClone(f.body), new Headers(f.headers));
159
+ expect(f.headers["user-agent"]).toContain("RooCode");
160
+ expect(req.harnessId).toBe("roo");
161
+ const names = req.tools.map((t) => t.name);
162
+ for (const n of ["read_file", "search_files", "list_files", "apply_diff", "attempt_completion"]) expect(names).toContain(n);
163
+ expect(req.messages.some((m) => m.role === "tool")).toBe(true);
164
+ expect(req.messages.find((m) => m.role === "assistant" && m.toolCalls.length > 0)).toBeDefined();
165
+ expect("stream_options" in req.renderUpstreamBody(MUT)).toBe(false);
166
+ });
167
+
134
168
  test("codex 0.153: Responses API body translates to a routed chat request", async () => {
135
169
  const f = await fixture("codex-responses");
136
170
  const req = parseResponsesRequest(structuredClone(f.body), new Headers(f.headers));
@@ -138,6 +172,9 @@ describe("captured harness requests", () => {
138
172
  expect(req.harnessId).toBe("codex");
139
173
  expect(req.requestedModel).toBe("auto");
140
174
  expect(req.stream).toBe(true);
175
+ // The thread id in the body becomes the session id; the root agent is not a subagent.
176
+ expect(req.ompSessionId).toBe((f.body.client_metadata as { thread_id: string }).thread_id);
177
+ expect(req.isSubagent).toBe(false);
141
178
  // instructions became the system message; the input items follow in order.
142
179
  expect(req.messages.map((m) => m.role)).toEqual(["system", "developer", "user", "user"]);
143
180
  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");