arkaos 2.21.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/arka/SKILL.md +2 -1
- package/arka/skills/costs/SKILL.md +62 -0
- package/core/cognition/__pycache__/auto_documentor.cpython-313.pyc +0 -0
- package/core/cognition/auto_documentor.py +75 -52
- package/core/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/cataloger.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/taxonomy.cpython-313.pyc +0 -0
- package/core/runtime/__init__.py +22 -1
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/base.py +30 -1
- package/core/runtime/claude_code.py +68 -0
- package/core/runtime/codex_cli.py +33 -0
- package/core/runtime/cursor.py +19 -0
- package/core/runtime/gemini_cli.py +33 -0
- package/core/runtime/llm_cost_telemetry.py +306 -0
- package/core/runtime/llm_cost_telemetry_cli.py +138 -0
- package/core/runtime/llm_provider.py +382 -0
- package/core/runtime/pricing.py +85 -0
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/kb_first_decider.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""JSONL cost telemetry writer and aggregator for LLM completions.
|
|
2
|
+
|
|
3
|
+
One call == one line appended to `~/.arkaos/telemetry/llm-cost.jsonl`.
|
|
4
|
+
Writers are concurrent-safe on POSIX (fcntl advisory lock); on Windows
|
|
5
|
+
we rely on O_APPEND atomicity for line-sized writes. Never raises —
|
|
6
|
+
telemetry failures are swallowed so they cannot break a user-facing
|
|
7
|
+
completion call.
|
|
8
|
+
|
|
9
|
+
Per ADR-011 ("Token budgets are informational, not restrictive") this
|
|
10
|
+
module exposes aggregation helpers (`summarise`, `list_expensive_sessions`)
|
|
11
|
+
used by the `/arka costs` visibility command. NO hard caps are enforced
|
|
12
|
+
here — advisories are soft strings attached to the returned summary.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timedelta, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import fcntl # POSIX only
|
|
27
|
+
_HAS_FLOCK = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
_HAS_FLOCK = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT_TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "llm-cost.jsonl"
|
|
33
|
+
|
|
34
|
+
VALID_PERIODS = ("today", "week", "month", "all")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _telemetry_path() -> Path:
|
|
38
|
+
override = os.environ.get("ARKA_LLM_COST_PATH", "").strip()
|
|
39
|
+
if override:
|
|
40
|
+
return Path(override)
|
|
41
|
+
return DEFAULT_TELEMETRY_PATH
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def _locked_append(path: Path):
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
fh = path.open("a", encoding="utf-8")
|
|
48
|
+
try:
|
|
49
|
+
if _HAS_FLOCK:
|
|
50
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
|
51
|
+
yield fh
|
|
52
|
+
finally:
|
|
53
|
+
if _HAS_FLOCK:
|
|
54
|
+
try:
|
|
55
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
fh.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def record_cost(
|
|
62
|
+
session_id: str,
|
|
63
|
+
provider: str,
|
|
64
|
+
model: str,
|
|
65
|
+
tokens_in: int,
|
|
66
|
+
tokens_out: int,
|
|
67
|
+
cached_tokens: int,
|
|
68
|
+
estimated_cost_usd: float | None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Append one JSONL line describing an LLM call's cost.
|
|
71
|
+
|
|
72
|
+
Silently swallows all errors. Telemetry must never break a
|
|
73
|
+
completion call. The caller decides whether to compute the cost via
|
|
74
|
+
`core.runtime.pricing.estimate_cost_usd` or pass None.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
entry: dict[str, Any] = {
|
|
78
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
79
|
+
"session_id": str(session_id or ""),
|
|
80
|
+
"provider": str(provider or ""),
|
|
81
|
+
"model": str(model or ""),
|
|
82
|
+
"tokens_in": int(tokens_in or 0),
|
|
83
|
+
"tokens_out": int(tokens_out or 0),
|
|
84
|
+
"cached_tokens": int(cached_tokens or 0),
|
|
85
|
+
"estimated_cost_usd": (
|
|
86
|
+
float(estimated_cost_usd)
|
|
87
|
+
if estimated_cost_usd is not None
|
|
88
|
+
else None
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
with _locked_append(_telemetry_path()) as fh:
|
|
92
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
93
|
+
except Exception: # noqa: BLE001 — telemetry must never raise
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_entries(path: Path | None = None) -> list[dict[str, Any]]:
|
|
98
|
+
"""Read and parse all JSONL entries from the telemetry file.
|
|
99
|
+
|
|
100
|
+
Returns an empty list if the file does not exist. Malformed lines
|
|
101
|
+
are skipped silently.
|
|
102
|
+
"""
|
|
103
|
+
target = path or _telemetry_path()
|
|
104
|
+
if not target.exists():
|
|
105
|
+
return []
|
|
106
|
+
out: list[dict[str, Any]] = []
|
|
107
|
+
for line in target.read_text(encoding="utf-8").splitlines():
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
out.append(json.loads(line))
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
continue
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Aggregation layer (visibility-only, per ADR-011)
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class CostSummary:
|
|
125
|
+
"""Aggregated cost view over a telemetry slice."""
|
|
126
|
+
|
|
127
|
+
period: str
|
|
128
|
+
total_cost_usd: float | None
|
|
129
|
+
total_tokens_in: int
|
|
130
|
+
total_tokens_out: int
|
|
131
|
+
total_cached_tokens: int
|
|
132
|
+
cache_hit_rate: float
|
|
133
|
+
call_count: int
|
|
134
|
+
by_provider: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
135
|
+
by_model: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
136
|
+
by_session: list[dict[str, Any]] = field(default_factory=list)
|
|
137
|
+
advisories: list[str] = field(default_factory=list)
|
|
138
|
+
corrupt_line_count: int = 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _period_cutoff(period: str, now: datetime | None = None) -> datetime | None:
|
|
142
|
+
ref = now or datetime.now(timezone.utc)
|
|
143
|
+
if period == "today":
|
|
144
|
+
return ref.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
145
|
+
if period == "week":
|
|
146
|
+
return ref - timedelta(days=7)
|
|
147
|
+
if period == "month":
|
|
148
|
+
return ref - timedelta(days=30)
|
|
149
|
+
if period == "all":
|
|
150
|
+
return None
|
|
151
|
+
raise ValueError(f"invalid period: {period!r}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _parse_ts(raw: Any) -> datetime | None:
|
|
155
|
+
if not isinstance(raw, str) or not raw:
|
|
156
|
+
return None
|
|
157
|
+
try:
|
|
158
|
+
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
159
|
+
except ValueError:
|
|
160
|
+
return None
|
|
161
|
+
if parsed.tzinfo is None:
|
|
162
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
163
|
+
return parsed
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _zero_bucket() -> dict[str, Any]:
|
|
167
|
+
return {
|
|
168
|
+
"total_cost_usd": 0.0,
|
|
169
|
+
"any_cost_known": False,
|
|
170
|
+
"total_tokens_in": 0,
|
|
171
|
+
"total_tokens_out": 0,
|
|
172
|
+
"total_cached_tokens": 0,
|
|
173
|
+
"call_count": 0,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _accumulate(bucket: dict[str, Any], entry: dict[str, Any]) -> None:
|
|
178
|
+
bucket["total_tokens_in"] += int(entry.get("tokens_in") or 0)
|
|
179
|
+
bucket["total_tokens_out"] += int(entry.get("tokens_out") or 0)
|
|
180
|
+
bucket["total_cached_tokens"] += int(entry.get("cached_tokens") or 0)
|
|
181
|
+
bucket["call_count"] += 1
|
|
182
|
+
cost = entry.get("estimated_cost_usd")
|
|
183
|
+
if cost is not None:
|
|
184
|
+
bucket["total_cost_usd"] += float(cost)
|
|
185
|
+
bucket["any_cost_known"] = True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _finalise_bucket(bucket: dict[str, Any]) -> dict[str, Any]:
|
|
189
|
+
out = dict(bucket)
|
|
190
|
+
if not out.pop("any_cost_known"):
|
|
191
|
+
out["total_cost_usd"] = None
|
|
192
|
+
else:
|
|
193
|
+
out["total_cost_usd"] = round(out["total_cost_usd"], 6)
|
|
194
|
+
tin = out["total_tokens_in"]
|
|
195
|
+
out["cache_hit_rate"] = (
|
|
196
|
+
round(out["total_cached_tokens"] / tin, 4) if tin > 0 else 0.0
|
|
197
|
+
)
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _load_slice(
|
|
202
|
+
path: Path | None,
|
|
203
|
+
cutoff: datetime | None,
|
|
204
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
205
|
+
target = path or _telemetry_path()
|
|
206
|
+
if not target.exists():
|
|
207
|
+
return [], 0
|
|
208
|
+
kept: list[dict[str, Any]] = []
|
|
209
|
+
corrupt = 0
|
|
210
|
+
for line in target.read_text(encoding="utf-8").splitlines():
|
|
211
|
+
line = line.strip()
|
|
212
|
+
if not line:
|
|
213
|
+
continue
|
|
214
|
+
try:
|
|
215
|
+
entry = json.loads(line)
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
corrupt += 1
|
|
218
|
+
continue
|
|
219
|
+
if cutoff is not None:
|
|
220
|
+
ts = _parse_ts(entry.get("ts"))
|
|
221
|
+
if ts is None or ts < cutoff:
|
|
222
|
+
continue
|
|
223
|
+
kept.append(entry)
|
|
224
|
+
return kept, corrupt
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _group(entries: list[dict[str, Any]], key: str) -> dict[str, dict[str, Any]]:
|
|
228
|
+
out: dict[str, dict[str, Any]] = {}
|
|
229
|
+
for entry in entries:
|
|
230
|
+
k = str(entry.get(key) or "")
|
|
231
|
+
bucket = out.setdefault(k, _zero_bucket())
|
|
232
|
+
_accumulate(bucket, entry)
|
|
233
|
+
return {k: _finalise_bucket(v) for k, v in out.items()}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _top_sessions(
|
|
237
|
+
entries: list[dict[str, Any]], top_n: int
|
|
238
|
+
) -> list[dict[str, Any]]:
|
|
239
|
+
grouped = _group(entries, "session_id")
|
|
240
|
+
rows = [{"session_id": sid, **vals} for sid, vals in grouped.items()]
|
|
241
|
+
rows.sort(key=lambda r: (r["total_cost_usd"] or 0.0), reverse=True)
|
|
242
|
+
return rows[:top_n]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _build_advisories(
|
|
246
|
+
sessions: list[dict[str, Any]], threshold_usd: float
|
|
247
|
+
) -> list[str]:
|
|
248
|
+
out: list[str] = []
|
|
249
|
+
for row in sessions:
|
|
250
|
+
cost = row.get("total_cost_usd") or 0.0
|
|
251
|
+
if cost >= threshold_usd:
|
|
252
|
+
out.append(
|
|
253
|
+
f"Session {row['session_id'] or '<unknown>'} exceeded "
|
|
254
|
+
f"${threshold_usd:.2f} (spent ${cost:.2f})"
|
|
255
|
+
)
|
|
256
|
+
return out
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _totals_bucket(entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
260
|
+
totals = _zero_bucket()
|
|
261
|
+
for entry in entries:
|
|
262
|
+
_accumulate(totals, entry)
|
|
263
|
+
return _finalise_bucket(totals)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def summarise(
|
|
267
|
+
period: str = "today",
|
|
268
|
+
path: Path | None = None,
|
|
269
|
+
advisory_threshold_usd: float = 5.0,
|
|
270
|
+
now: datetime | None = None,
|
|
271
|
+
) -> CostSummary:
|
|
272
|
+
"""Aggregate telemetry for the given period.
|
|
273
|
+
|
|
274
|
+
Graceful on missing file, empty file, corrupt JSONL lines.
|
|
275
|
+
Raises only on invalid `period` (programmer error).
|
|
276
|
+
"""
|
|
277
|
+
if period not in VALID_PERIODS:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"invalid period: {period!r}; expected one of {VALID_PERIODS}"
|
|
280
|
+
)
|
|
281
|
+
entries, corrupt = _load_slice(path, _period_cutoff(period, now=now))
|
|
282
|
+
finalised = _totals_bucket(entries)
|
|
283
|
+
sessions = _top_sessions(entries, top_n=10)
|
|
284
|
+
return CostSummary(
|
|
285
|
+
period=period,
|
|
286
|
+
total_cost_usd=finalised["total_cost_usd"],
|
|
287
|
+
total_tokens_in=finalised["total_tokens_in"],
|
|
288
|
+
total_tokens_out=finalised["total_tokens_out"],
|
|
289
|
+
total_cached_tokens=finalised["total_cached_tokens"],
|
|
290
|
+
cache_hit_rate=finalised["cache_hit_rate"],
|
|
291
|
+
call_count=finalised["call_count"],
|
|
292
|
+
by_provider=_group(entries, "provider"),
|
|
293
|
+
by_model=_group(entries, "model"),
|
|
294
|
+
by_session=sessions,
|
|
295
|
+
advisories=_build_advisories(sessions, advisory_threshold_usd),
|
|
296
|
+
corrupt_line_count=corrupt,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def list_expensive_sessions(
|
|
301
|
+
path: Path | None = None,
|
|
302
|
+
top_n: int = 10,
|
|
303
|
+
) -> list[dict[str, Any]]:
|
|
304
|
+
"""Return the top-N sessions by total cost across the entire history."""
|
|
305
|
+
entries, _ = _load_slice(path, cutoff=None)
|
|
306
|
+
return _top_sessions(entries, top_n=max(0, int(top_n)))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""CLI entry for `/arka costs`. Prints a formatted cost summary.
|
|
2
|
+
|
|
3
|
+
Invoked as `python -m core.runtime.llm_cost_telemetry_cli <period>`
|
|
4
|
+
where `<period>` is one of: today, week, month, all, sessions.
|
|
5
|
+
|
|
6
|
+
Output is plain markdown with pipe tables so it renders cleanly in both
|
|
7
|
+
terminal and Obsidian. No external dependencies.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from core.runtime.llm_cost_telemetry import (
|
|
16
|
+
VALID_PERIODS,
|
|
17
|
+
CostSummary,
|
|
18
|
+
list_expensive_sessions,
|
|
19
|
+
summarise,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fmt_cost(value: float | None) -> str:
|
|
24
|
+
if value is None:
|
|
25
|
+
return "n/a"
|
|
26
|
+
return f"${value:.4f}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fmt_rate(value: float) -> str:
|
|
30
|
+
return f"{value * 100:.1f}%"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _headline(summary: CostSummary) -> list[str]:
|
|
34
|
+
return [
|
|
35
|
+
f"# LLM costs — {summary.period}",
|
|
36
|
+
"",
|
|
37
|
+
f"- Calls: **{summary.call_count}**",
|
|
38
|
+
f"- Total cost: **{_fmt_cost(summary.total_cost_usd)}**",
|
|
39
|
+
f"- Tokens in / out: **{summary.total_tokens_in:,}** / "
|
|
40
|
+
f"**{summary.total_tokens_out:,}**",
|
|
41
|
+
f"- Cached tokens: **{summary.total_cached_tokens:,}** "
|
|
42
|
+
f"(hit rate {_fmt_rate(summary.cache_hit_rate)})",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _render_group(title: str, group: dict[str, dict[str, Any]]) -> list[str]:
|
|
47
|
+
if not group:
|
|
48
|
+
return [f"## {title}", "", "_(no data)_", ""]
|
|
49
|
+
lines = [
|
|
50
|
+
f"## {title}",
|
|
51
|
+
"",
|
|
52
|
+
"| Key | Calls | Tokens in | Tokens out | Cache hit | Cost |",
|
|
53
|
+
"| --- | ---: | ---: | ---: | ---: | ---: |",
|
|
54
|
+
]
|
|
55
|
+
ordered = sorted(
|
|
56
|
+
group.items(),
|
|
57
|
+
key=lambda kv: (kv[1]["total_cost_usd"] or 0.0),
|
|
58
|
+
reverse=True,
|
|
59
|
+
)
|
|
60
|
+
for key, row in ordered:
|
|
61
|
+
label = key or "<unknown>"
|
|
62
|
+
lines.append(
|
|
63
|
+
f"| {label} | {row['call_count']} | "
|
|
64
|
+
f"{row['total_tokens_in']:,} | {row['total_tokens_out']:,} | "
|
|
65
|
+
f"{_fmt_rate(row['cache_hit_rate'])} | "
|
|
66
|
+
f"{_fmt_cost(row['total_cost_usd'])} |"
|
|
67
|
+
)
|
|
68
|
+
lines.append("")
|
|
69
|
+
return lines
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _render_sessions(rows: list[dict[str, Any]], title: str) -> list[str]:
|
|
73
|
+
if not rows:
|
|
74
|
+
return [f"## {title}", "", "_(no sessions)_", ""]
|
|
75
|
+
lines = [
|
|
76
|
+
f"## {title}",
|
|
77
|
+
"",
|
|
78
|
+
"| Session | Calls | Tokens in | Tokens out | Cost |",
|
|
79
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
80
|
+
]
|
|
81
|
+
for row in rows:
|
|
82
|
+
sid = row.get("session_id") or "<unknown>"
|
|
83
|
+
lines.append(
|
|
84
|
+
f"| {sid} | {row['call_count']} | "
|
|
85
|
+
f"{row['total_tokens_in']:,} | {row['total_tokens_out']:,} | "
|
|
86
|
+
f"{_fmt_cost(row['total_cost_usd'])} |"
|
|
87
|
+
)
|
|
88
|
+
lines.append("")
|
|
89
|
+
return lines
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _render_advisories(advisories: list[str]) -> list[str]:
|
|
93
|
+
if not advisories:
|
|
94
|
+
return []
|
|
95
|
+
return ["## Advisories", "", *[f"- {a}" for a in advisories], ""]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _format_summary(summary: CostSummary) -> str:
|
|
99
|
+
parts: list[str] = []
|
|
100
|
+
parts.extend(_headline(summary))
|
|
101
|
+
parts.append("")
|
|
102
|
+
parts.extend(_render_group("By provider", summary.by_provider))
|
|
103
|
+
parts.extend(_render_group("By model", summary.by_model))
|
|
104
|
+
parts.extend(_render_sessions(summary.by_session, "Top 10 sessions"))
|
|
105
|
+
parts.extend(_render_advisories(summary.advisories))
|
|
106
|
+
if summary.corrupt_line_count:
|
|
107
|
+
parts.append(
|
|
108
|
+
f"_Note: skipped {summary.corrupt_line_count} corrupt JSONL line(s)._"
|
|
109
|
+
)
|
|
110
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _format_sessions(rows: list[dict[str, Any]]) -> str:
|
|
114
|
+
parts = ["# LLM costs — top expensive sessions", ""]
|
|
115
|
+
parts.extend(_render_sessions(rows, "Top sessions (all time)"))
|
|
116
|
+
return "\n".join(parts).rstrip() + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _usage_error() -> str:
|
|
120
|
+
valid = "|".join((*VALID_PERIODS, "sessions"))
|
|
121
|
+
return f"Unknown period. Use: {valid}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: list[str]) -> int:
|
|
125
|
+
args = argv[1:] if len(argv) > 1 else ["today"]
|
|
126
|
+
cmd = (args[0] or "today").strip().lower()
|
|
127
|
+
if cmd in VALID_PERIODS:
|
|
128
|
+
print(_format_summary(summarise(period=cmd)))
|
|
129
|
+
return 0
|
|
130
|
+
if cmd == "sessions":
|
|
131
|
+
print(_format_sessions(list_expensive_sessions(top_n=10)))
|
|
132
|
+
return 0
|
|
133
|
+
print(_usage_error(), file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
sys.exit(main(sys.argv))
|