agentburn 0.2.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentburn
3
+ Version: 0.2.0
4
+ Summary: Where does your AI agent burn money? Local token/cost profiler for always-on agents (Hermes Agent first). Zero dependencies.
5
+ Author: Ion
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Socialpranker/agentburn
8
+ Keywords: hermes-agent,tokens,cost,profiler,ai-agents,openrouter
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # agentburn
20
+
21
+ > **Where does your AI agent burn money — while you sleep?**
22
+
23
+ Always-on agents bill you around the clock. Hermes Agent users wake up to
24
+ [**$47 overnight bills**](https://dev.to/chintanonweb/hermes-agent-gets-smarter-every-day-so-does-the-bill-4i8o)
25
+ from recursive subagent runs; one user measured that
26
+ [**73% of every API call is fixed overhead**](https://github.com/NousResearch/hermes-agent/issues/4379)
27
+ (tool definitions + system prompt, resent every time); chained delegation means
28
+ *"step 3 costs 4× step 1 — no alert, just a bill."* Built-in `/usage` shows totals.
29
+ Nothing shows **where** it burns.
30
+
31
+ agentburn is a local profiler for your agent's own accounting database. One command, zero dependencies, nothing leaves your machine:
32
+
33
+ ```bash
34
+ uvx agentburn # or: pipx run agentburn / pip install agentburn
35
+ ```
36
+
37
+ ```text
38
+ 🔥 agentburn — hermes · last 30d
39
+
40
+ ~$45.50 total · 1.75M tokens · 7 sessions · 123 API calls
41
+ ≈ ~$431.24/month at the current pace
42
+
43
+ WHERE IT BURNS (by source)
44
+ cron ██████████████···· 79% ~$36.00 1.24M 2 sess
45
+ cli ██················ 9% ~$4.00 185K 1 sess
46
+ gateway:telegram █················· 7% ~$3.00 210K 1 sess
47
+ subagent █················· 5% ~$2.50 113K 2 sess
48
+
49
+ 🌙 WHILE YOU SLEPT (00:00–08:00): ~$36.00 (79% of spend) · 2 sessions
50
+ mostly: cron
51
+
52
+ FIXED OVERHEAD (avg input tokens per API call)
53
+ gateway:telegram 20,000 ← heavy
54
+ cron 15,000 ← heavy
55
+ input composition (sampled from 3 request dumps): system 30% · tools 58% · history 12%
56
+
57
+ 💡 DO THIS
58
+ 1. 79% of spend happens at night — that's ≈$341/mo while you sleep. Route night work to a cheaper model.
59
+ 2. Scheduled (cron) sessions run on anthropic/claude-opus-x — maintenance rarely needs a frontier model.
60
+ 3. 20,000 input tokens per call on telegram: trim per-platform toolsets, prune unused skills.
61
+ ```
62
+
63
+ ## What it answers
64
+
65
+ - **Where it burns** — by source: `cron` / `subagent` / `gateway:telegram|discord|whatsapp` / `cli`. Always-on ≠ free: scheduled jobs and gateways spend without you.
66
+ - **🌙 While you slept** — the overnight bill, isolated and named (configurable window: `--night 23-7`).
67
+ - **Fixed overhead** — average input tokens per API call per source. The "73% overhead" pattern is visible in one glance; with request dumps enabled, you get the sampled composition (system prompt vs tool definitions vs history).
68
+ - **Subagent rollups** — delegation cost chained back to the session that spawned it. Recursion compounds; here is the receipt.
69
+ - **Top tools** — which tool results weigh most in your context.
70
+ - **What to do** — up to 4 conservative, named recommendations with monthly estimates.
71
+
72
+ ## Why trust these numbers
73
+
74
+ Most token trackers quietly disagree with each other (2–91× in public issue threads). agentburn takes the opposite stance:
75
+
76
+ - Numbers come from **the agent's own accounting** (`~/.hermes/state.db`: per-session token counters and cost fields). No scraping, no proxies, no guessing.
77
+ - Provider-billed costs are shown as-is; Hermes estimates are marked with `~`. Mixed data is labeled mixed.
78
+ - Sessions with messages but **zero recorded tokens** (known Hermes accounting gaps, e.g. [#12023](https://github.com/NousResearch/hermes-agent/issues/12023)) are detected and reported: totals are then explicitly a **lower bound** — and fixing the accounting becomes recommendation #1.
79
+ - Input composition from request dumps is char-proportional and labeled *sampled estimate*, not truth.
80
+
81
+ ## Privacy
82
+
83
+ Everything runs locally and reads your database **read-only**. No network calls. No telemetry. The report is yours.
84
+
85
+ ## Usage
86
+
87
+ ```bash
88
+ agentburn # autodetect agent, last 30 days
89
+ agentburn --days 7
90
+ agentburn --db /path/to/state.db
91
+ agentburn --night 23-7 # custom overnight window (local time)
92
+ agentburn --json # machine-readable, pipe it anywhere
93
+ agentburn --no-color
94
+ ```
95
+
96
+ ## Mechanics
97
+
98
+ **📤 Share your burn (`--share`).** An anonymized card — categories, models and totals only; session titles, paths and content are excluded *by construction*. Safe to paste into a post; `--svg card.svg` renders the same card as an image:
99
+
100
+ ```text
101
+ 🔥 my hermes agent · last 30d
102
+ ~$45.50 · 1.75M tokens → ~$430/mo pace
103
+ cron 79% · cli 9% · telegram 7% · subagent 5%
104
+ 🌙 while I slept (00–08): ~$36.00 (79%)
105
+ heaviest overhead: telegram 20,000 tokens/call (community baseline ≈8k/call: +150%)
106
+ ```
107
+
108
+ **📏 Calibration against public benchmarks.** "Is 15k input tokens per call normal?" The report compares your fixed overhead with community-measured references embedded as dated constants (e.g. the [Phala always-on-agent benchmark](https://phala.com/posts/understanding-openclaws-token-usage), 2026-03: ≈8k/call baseline). No network — sources are cited inline.
109
+
110
+ **📐 Optimize → prove it (`--save-baseline` / `--compare`).** Snapshot your pace, change the config (cheaper cron model, trimmed toolsets), then `agentburn --compare` shows the delta in $/month — pace-normalized, so a 7-day baseline compares honestly with a 30-day window. Every recommendation becomes a testable promise.
111
+
112
+ **🩺 `agentburn doctor`.** Trackers disagree because the agent's own accounting has gaps. doctor names the broken combinations (provider × model × source) for zero-usage and unpriced sessions, and generates a ready-to-paste upstream bug report — counters only, no message content.
113
+
114
+ ## Supported agents
115
+
116
+ | Agent | Status | Data source |
117
+ |---|---|---|
118
+ | **Hermes Agent** | ✅ v0.1 | `~/.hermes/state.db` (+ optional `request_dump_*.json` for input composition) |
119
+ | OpenClaw | roadmap | session JSONL |
120
+ | Claude Code | roadmap | `~/.claude/projects/**.jsonl` |
121
+
122
+ The core is agent-agnostic (normalized session/event model); adapters are ~150 lines each. PRs welcome.
123
+
124
+ ## Related
125
+
126
+ [token-history](https://github.com/Socialpranker/token-history) — the macro view: daily archive of *which agents the world uses* (OpenRouter rankings). agentburn is the micro view: *where yours burns*.
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,112 @@
1
+ # agentburn
2
+
3
+ > **Where does your AI agent burn money — while you sleep?**
4
+
5
+ Always-on agents bill you around the clock. Hermes Agent users wake up to
6
+ [**$47 overnight bills**](https://dev.to/chintanonweb/hermes-agent-gets-smarter-every-day-so-does-the-bill-4i8o)
7
+ from recursive subagent runs; one user measured that
8
+ [**73% of every API call is fixed overhead**](https://github.com/NousResearch/hermes-agent/issues/4379)
9
+ (tool definitions + system prompt, resent every time); chained delegation means
10
+ *"step 3 costs 4× step 1 — no alert, just a bill."* Built-in `/usage` shows totals.
11
+ Nothing shows **where** it burns.
12
+
13
+ agentburn is a local profiler for your agent's own accounting database. One command, zero dependencies, nothing leaves your machine:
14
+
15
+ ```bash
16
+ uvx agentburn # or: pipx run agentburn / pip install agentburn
17
+ ```
18
+
19
+ ```text
20
+ 🔥 agentburn — hermes · last 30d
21
+
22
+ ~$45.50 total · 1.75M tokens · 7 sessions · 123 API calls
23
+ ≈ ~$431.24/month at the current pace
24
+
25
+ WHERE IT BURNS (by source)
26
+ cron ██████████████···· 79% ~$36.00 1.24M 2 sess
27
+ cli ██················ 9% ~$4.00 185K 1 sess
28
+ gateway:telegram █················· 7% ~$3.00 210K 1 sess
29
+ subagent █················· 5% ~$2.50 113K 2 sess
30
+
31
+ 🌙 WHILE YOU SLEPT (00:00–08:00): ~$36.00 (79% of spend) · 2 sessions
32
+ mostly: cron
33
+
34
+ FIXED OVERHEAD (avg input tokens per API call)
35
+ gateway:telegram 20,000 ← heavy
36
+ cron 15,000 ← heavy
37
+ input composition (sampled from 3 request dumps): system 30% · tools 58% · history 12%
38
+
39
+ 💡 DO THIS
40
+ 1. 79% of spend happens at night — that's ≈$341/mo while you sleep. Route night work to a cheaper model.
41
+ 2. Scheduled (cron) sessions run on anthropic/claude-opus-x — maintenance rarely needs a frontier model.
42
+ 3. 20,000 input tokens per call on telegram: trim per-platform toolsets, prune unused skills.
43
+ ```
44
+
45
+ ## What it answers
46
+
47
+ - **Where it burns** — by source: `cron` / `subagent` / `gateway:telegram|discord|whatsapp` / `cli`. Always-on ≠ free: scheduled jobs and gateways spend without you.
48
+ - **🌙 While you slept** — the overnight bill, isolated and named (configurable window: `--night 23-7`).
49
+ - **Fixed overhead** — average input tokens per API call per source. The "73% overhead" pattern is visible in one glance; with request dumps enabled, you get the sampled composition (system prompt vs tool definitions vs history).
50
+ - **Subagent rollups** — delegation cost chained back to the session that spawned it. Recursion compounds; here is the receipt.
51
+ - **Top tools** — which tool results weigh most in your context.
52
+ - **What to do** — up to 4 conservative, named recommendations with monthly estimates.
53
+
54
+ ## Why trust these numbers
55
+
56
+ Most token trackers quietly disagree with each other (2–91× in public issue threads). agentburn takes the opposite stance:
57
+
58
+ - Numbers come from **the agent's own accounting** (`~/.hermes/state.db`: per-session token counters and cost fields). No scraping, no proxies, no guessing.
59
+ - Provider-billed costs are shown as-is; Hermes estimates are marked with `~`. Mixed data is labeled mixed.
60
+ - Sessions with messages but **zero recorded tokens** (known Hermes accounting gaps, e.g. [#12023](https://github.com/NousResearch/hermes-agent/issues/12023)) are detected and reported: totals are then explicitly a **lower bound** — and fixing the accounting becomes recommendation #1.
61
+ - Input composition from request dumps is char-proportional and labeled *sampled estimate*, not truth.
62
+
63
+ ## Privacy
64
+
65
+ Everything runs locally and reads your database **read-only**. No network calls. No telemetry. The report is yours.
66
+
67
+ ## Usage
68
+
69
+ ```bash
70
+ agentburn # autodetect agent, last 30 days
71
+ agentburn --days 7
72
+ agentburn --db /path/to/state.db
73
+ agentburn --night 23-7 # custom overnight window (local time)
74
+ agentburn --json # machine-readable, pipe it anywhere
75
+ agentburn --no-color
76
+ ```
77
+
78
+ ## Mechanics
79
+
80
+ **📤 Share your burn (`--share`).** An anonymized card — categories, models and totals only; session titles, paths and content are excluded *by construction*. Safe to paste into a post; `--svg card.svg` renders the same card as an image:
81
+
82
+ ```text
83
+ 🔥 my hermes agent · last 30d
84
+ ~$45.50 · 1.75M tokens → ~$430/mo pace
85
+ cron 79% · cli 9% · telegram 7% · subagent 5%
86
+ 🌙 while I slept (00–08): ~$36.00 (79%)
87
+ heaviest overhead: telegram 20,000 tokens/call (community baseline ≈8k/call: +150%)
88
+ ```
89
+
90
+ **📏 Calibration against public benchmarks.** "Is 15k input tokens per call normal?" The report compares your fixed overhead with community-measured references embedded as dated constants (e.g. the [Phala always-on-agent benchmark](https://phala.com/posts/understanding-openclaws-token-usage), 2026-03: ≈8k/call baseline). No network — sources are cited inline.
91
+
92
+ **📐 Optimize → prove it (`--save-baseline` / `--compare`).** Snapshot your pace, change the config (cheaper cron model, trimmed toolsets), then `agentburn --compare` shows the delta in $/month — pace-normalized, so a 7-day baseline compares honestly with a 30-day window. Every recommendation becomes a testable promise.
93
+
94
+ **🩺 `agentburn doctor`.** Trackers disagree because the agent's own accounting has gaps. doctor names the broken combinations (provider × model × source) for zero-usage and unpriced sessions, and generates a ready-to-paste upstream bug report — counters only, no message content.
95
+
96
+ ## Supported agents
97
+
98
+ | Agent | Status | Data source |
99
+ |---|---|---|
100
+ | **Hermes Agent** | ✅ v0.1 | `~/.hermes/state.db` (+ optional `request_dump_*.json` for input composition) |
101
+ | OpenClaw | roadmap | session JSONL |
102
+ | Claude Code | roadmap | `~/.claude/projects/**.jsonl` |
103
+
104
+ The core is agent-agnostic (normalized session/event model); adapters are ~150 lines each. PRs welcome.
105
+
106
+ ## Related
107
+
108
+ [token-history](https://github.com/Socialpranker/token-history) — the macro view: daily archive of *which agents the world uses* (OpenRouter rankings). agentburn is the micro view: *where yours burns*.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,8 @@
1
+ """agentburn — where does your AI agent burn money?
2
+
3
+ Local, zero-dependency token/cost profiler for always-on AI agents.
4
+ Adapter #1: Hermes Agent (~/.hermes/state.db). Honest methodology:
5
+ numbers come from the agent's own accounting; gaps are surfaced, not hidden.
6
+ """
7
+
8
+ __version__ = "0.2.0"
@@ -0,0 +1,14 @@
1
+ """Adapter registry. v0.1 ships Hermes; OpenClaw and Claude Code are next."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import hermes
6
+
7
+ ADAPTERS = {
8
+ "hermes": hermes,
9
+ }
10
+
11
+
12
+ def detect() -> list[str]:
13
+ """Return adapter names whose data is present on this machine."""
14
+ return [name for name, mod in ADAPTERS.items() if mod.available()]
@@ -0,0 +1,239 @@
1
+ """Hermes Agent adapter: reads ~/.hermes/state.db (SQLite) read-only.
2
+
3
+ Schema observed in NousResearch/hermes-agent `hermes_state.py` (June 2026):
4
+ sessions(id, source, model, parent_session_id, started_at, ended_at,
5
+ message_count, tool_call_count, api_call_count,
6
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
7
+ reasoning_tokens, estimated_cost_usd, actual_cost_usd, cost_status,
8
+ title, archived, ...)
9
+ messages(session_id, role, tool_name, tool_calls, timestamp, token_count, ...)
10
+
11
+ Known upstream accounting gaps (hermes-agent #12023, #6775, #8337): some
12
+ providers/streams record zero tokens. We DETECT and REPORT those gaps instead
13
+ of silently presenting totals as truth.
14
+
15
+ Optional precision layer: request_dump_*.json files (written when request
16
+ dumping is enabled) contain the full API body; we sample them to estimate the
17
+ input composition (system prompt vs tool definitions vs history).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import glob
23
+ import json
24
+ import os
25
+ import sqlite3
26
+ import time
27
+ from typing import Optional
28
+
29
+ from ..model import DumpComposition, SessionRec, Snapshot, ToolStat
30
+
31
+ GATEWAY_SOURCES = {
32
+ "telegram",
33
+ "whatsapp",
34
+ "discord",
35
+ "slack",
36
+ "signal",
37
+ "imessage",
38
+ "email",
39
+ "api",
40
+ "api_server",
41
+ "web",
42
+ }
43
+
44
+
45
+ def default_db_path() -> str:
46
+ return os.path.join(os.path.expanduser("~"), ".hermes", "state.db")
47
+
48
+
49
+ def available() -> bool:
50
+ return os.path.exists(default_db_path())
51
+
52
+
53
+ def normalize_source(raw: Optional[str]) -> str:
54
+ s = (raw or "unknown").strip().lower()
55
+ if s in ("cli", "cron", "subagent"):
56
+ return s
57
+ if s in GATEWAY_SOURCES:
58
+ return f"gateway:{s}"
59
+ if s.startswith(("gateway:", "other:")):
60
+ return s
61
+ return f"other:{s}"
62
+
63
+
64
+ def _columns(con: sqlite3.Connection, table: str) -> set:
65
+ try:
66
+ return {r[1] for r in con.execute(f"PRAGMA table_info({table})")}
67
+ except sqlite3.Error:
68
+ return set()
69
+
70
+
71
+ def _col(cols: set, name: str, default_sql: str = "NULL") -> str:
72
+ return name if name in cols else f"{default_sql} AS {name}"
73
+
74
+
75
+ def load(
76
+ db_path: Optional[str] = None,
77
+ days: Optional[int] = 30,
78
+ dumps_dir: Optional[str] = None,
79
+ now: Optional[float] = None,
80
+ ) -> Snapshot:
81
+ path = db_path or default_db_path()
82
+ if not os.path.exists(path):
83
+ raise FileNotFoundError(
84
+ f"Hermes state not found at {path}. Pass --db /path/to/state.db "
85
+ "or run on the machine where Hermes Agent lives."
86
+ )
87
+ now = now or time.time()
88
+ since = now - days * 86400 if days else 0
89
+
90
+ snap = Snapshot(
91
+ agent="hermes",
92
+ source_path=path,
93
+ generated_at=now,
94
+ days=days,
95
+ )
96
+
97
+ con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
98
+ try:
99
+ con.row_factory = sqlite3.Row
100
+ scols = _columns(con, "sessions")
101
+ if "id" not in scols:
102
+ raise RuntimeError(
103
+ "sessions table not found — is this really a Hermes state.db? "
104
+ "(schema may have changed; please open an issue with `PRAGMA table_info(sessions)` output)"
105
+ )
106
+
107
+ fields = ", ".join(
108
+ [
109
+ "id",
110
+ _col(scols, "source", "'unknown'"),
111
+ _col(scols, "model"),
112
+ _col(scols, "parent_session_id"),
113
+ _col(scols, "started_at"),
114
+ _col(scols, "ended_at"),
115
+ _col(scols, "title"),
116
+ _col(scols, "message_count", "0"),
117
+ _col(scols, "api_call_count", "0"),
118
+ _col(scols, "input_tokens", "0"),
119
+ _col(scols, "output_tokens", "0"),
120
+ _col(scols, "cache_read_tokens", "0"),
121
+ _col(scols, "cache_write_tokens", "0"),
122
+ _col(scols, "reasoning_tokens", "0"),
123
+ _col(scols, "estimated_cost_usd"),
124
+ _col(scols, "actual_cost_usd"),
125
+ _col(scols, "billing_provider"),
126
+ ]
127
+ )
128
+ where = "WHERE COALESCE(started_at, 0) >= ?" if days else ""
129
+ rows = con.execute(
130
+ f"SELECT {fields} FROM sessions {where}", (since,) if days else ()
131
+ ).fetchall()
132
+
133
+ for r in rows:
134
+ actual = r["actual_cost_usd"]
135
+ est = r["estimated_cost_usd"]
136
+ cost, basis = (
137
+ (actual, "actual")
138
+ if actual is not None
139
+ else (est, "estimated")
140
+ if est is not None
141
+ else (None, "unknown")
142
+ )
143
+ snap.sessions.append(
144
+ SessionRec(
145
+ id=str(r["id"]),
146
+ source=normalize_source(r["source"]),
147
+ model=r["model"],
148
+ started_at=r["started_at"],
149
+ ended_at=r["ended_at"],
150
+ parent_id=r["parent_session_id"],
151
+ title=r["title"],
152
+ api_calls=int(r["api_call_count"] or 0),
153
+ input_tokens=int(r["input_tokens"] or 0),
154
+ output_tokens=int(r["output_tokens"] or 0),
155
+ cache_read_tokens=int(r["cache_read_tokens"] or 0),
156
+ cache_write_tokens=int(r["cache_write_tokens"] or 0),
157
+ reasoning_tokens=int(r["reasoning_tokens"] or 0),
158
+ cost_usd=float(cost) if cost is not None else None,
159
+ cost_basis=basis,
160
+ message_count=int(r["message_count"] or 0),
161
+ provider=r["billing_provider"],
162
+ )
163
+ )
164
+
165
+ mcols = _columns(con, "messages")
166
+ if {"tool_name", "timestamp"} <= mcols:
167
+ tok = "COALESCE(token_count, 0)" if "token_count" in mcols else "0"
168
+ mwhere = "AND timestamp >= ?" if days else ""
169
+ for r in con.execute(
170
+ f"""SELECT tool_name, COUNT(*) AS calls, SUM({tok}) AS toks
171
+ FROM messages
172
+ WHERE tool_name IS NOT NULL AND tool_name != '' {mwhere}
173
+ GROUP BY tool_name ORDER BY toks DESC""",
174
+ (since,) if days else (),
175
+ ):
176
+ snap.tools.append(
177
+ ToolStat(name=r["tool_name"], calls=int(r["calls"]), result_tokens=int(r["toks"] or 0))
178
+ )
179
+ finally:
180
+ con.close()
181
+
182
+ comp = _sample_dumps(dumps_dir or os.path.join(os.path.dirname(path), "sessions"))
183
+ if comp:
184
+ snap.composition = comp
185
+ return snap
186
+
187
+
188
+ def _sample_dumps(dumps_dir: str, limit: int = 20) -> Optional[DumpComposition]:
189
+ """Estimate input composition from request dumps (newest `limit` files).
190
+
191
+ Char-proportional split of the request body: system prompt vs tool
192
+ definitions vs message history. Proportions, not exact tokens — labeled
193
+ as sampled estimate in the report.
194
+ """
195
+ try:
196
+ files = sorted(glob.glob(os.path.join(dumps_dir, "request_dump_*.json")))[-limit:]
197
+ except OSError:
198
+ return None
199
+ if not files:
200
+ return None
201
+ sys_c = tools_c = hist_c = 0
202
+ samples = 0
203
+ for f in files:
204
+ try:
205
+ with open(f, "r", encoding="utf-8", errors="replace") as fh:
206
+ payload = json.load(fh)
207
+ except (OSError, json.JSONDecodeError):
208
+ continue
209
+ body = payload.get("body") or payload.get("request") or payload
210
+ if not isinstance(body, dict):
211
+ continue
212
+ msgs = body.get("messages") or []
213
+ tools = body.get("tools") or []
214
+ s = t = h = 0
215
+ if isinstance(body.get("system"), str):
216
+ s += len(body["system"])
217
+ for m in msgs if isinstance(msgs, list) else []:
218
+ chunk = len(json.dumps(m, ensure_ascii=False, default=str))
219
+ if isinstance(m, dict) and m.get("role") == "system":
220
+ s += chunk
221
+ else:
222
+ h += chunk
223
+ t = len(json.dumps(tools, ensure_ascii=False, default=str)) if tools else 0
224
+ total = s + t + h
225
+ if total <= 0:
226
+ continue
227
+ sys_c += s
228
+ tools_c += t
229
+ hist_c += h
230
+ samples += 1
231
+ total = sys_c + tools_c + hist_c
232
+ if samples == 0 or total == 0:
233
+ return None
234
+ return DumpComposition(
235
+ samples=samples,
236
+ system_share=sys_c / total,
237
+ tools_share=tools_c / total,
238
+ history_share=hist_c / total,
239
+ )