tokencur 0.3.0__py3-none-any.whl
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.
- tokencur/__init__.py +7 -0
- tokencur/__main__.py +6 -0
- tokencur/cli.py +349 -0
- tokencur/dashboard.py +177 -0
- tokencur/doctor.py +239 -0
- tokencur/export.py +61 -0
- tokencur/focus.py +275 -0
- tokencur/ingest/__init__.py +1 -0
- tokencur/ingest/claude_code.py +160 -0
- tokencur/ingest/codex.py +131 -0
- tokencur/ingest/fields.py +49 -0
- tokencur/ingest/identity.py +38 -0
- tokencur/ingest/kimi_code.py +142 -0
- tokencur/ingest/runpod.py +97 -0
- tokencur/ingest/stats.py +25 -0
- tokencur/ledger.py +412 -0
- tokencur/observatory.py +617 -0
- tokencur/outcomes.py +384 -0
- tokencur/prices.py +413 -0
- tokencur/pricing.py +260 -0
- tokencur/pricing_data/litellm_snapshot.json +2383 -0
- tokencur/recommend.py +190 -0
- tokencur/recommend_cli.py +20 -0
- tokencur/records.py +118 -0
- tokencur/report.py +140 -0
- tokencur/sources.py +50 -0
- tokencur/terminal.py +33 -0
- tokencur-0.3.0.dist-info/METADATA +386 -0
- tokencur-0.3.0.dist-info/RECORD +33 -0
- tokencur-0.3.0.dist-info/WHEEL +5 -0
- tokencur-0.3.0.dist-info/entry_points.txt +2 -0
- tokencur-0.3.0.dist-info/licenses/LICENSE +21 -0
- tokencur-0.3.0.dist-info/top_level.txt +1 -0
tokencur/__init__.py
ADDED
tokencur/__main__.py
ADDED
tokencur/cli.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Command-line interface: ``tokencur <command>``.
|
|
2
|
+
|
|
3
|
+
One argparse parser for every command, installed as the ``tokencur``
|
|
4
|
+
console script and reachable as ``python -m tokencur``. The older
|
|
5
|
+
per-module entry points (``python -m tokencur.report`` and friends)
|
|
6
|
+
keep working: each delegates here.
|
|
7
|
+
|
|
8
|
+
Periods follow billing convention: ``--since`` is the first UTC day
|
|
9
|
+
included, ``--until`` the first day excluded, so ``--since 2026-09-01
|
|
10
|
+
--until 2026-10-01`` is exactly September.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sqlite3
|
|
17
|
+
import sys
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from datetime import date
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from tokencur import __version__, doctor, ledger, observatory, outcomes, prices
|
|
23
|
+
from tokencur.export import export_csv
|
|
24
|
+
from tokencur.focus import undated_count, unpriced_models
|
|
25
|
+
from tokencur.ingest import claude_code, runpod
|
|
26
|
+
from tokencur.pricing import ConfigError, load_discounts
|
|
27
|
+
from tokencur.recommend import recommendations, render
|
|
28
|
+
from tokencur.records import UsageRecord, in_period
|
|
29
|
+
from tokencur.report import summarize
|
|
30
|
+
from tokencur.sources import load_records
|
|
31
|
+
|
|
32
|
+
_NOTHING = "no usage in known log locations or the ledger"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="tokencur",
|
|
38
|
+
description="The CUR for your tokens: AI coding-agent usage as FOCUS cost data.",
|
|
39
|
+
epilog="Figures are API-equivalent list prices (showback), not a bill: "
|
|
40
|
+
"see the README's 'Money concepts'. With no command, runs `report`.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
44
|
+
)
|
|
45
|
+
commands = parser.add_subparsers(dest="command", metavar="<command>")
|
|
46
|
+
|
|
47
|
+
period = argparse.ArgumentParser(add_help=False)
|
|
48
|
+
period.add_argument(
|
|
49
|
+
"--since", type=_day, metavar="YYYY-MM-DD", help="first UTC day included"
|
|
50
|
+
)
|
|
51
|
+
period.add_argument(
|
|
52
|
+
"--until", type=_day, metavar="YYYY-MM-DD", help="first UTC day excluded"
|
|
53
|
+
)
|
|
54
|
+
root_help = "read these Claude Code logs instead; never stored in the ledger"
|
|
55
|
+
contract = argparse.ArgumentParser(add_help=False)
|
|
56
|
+
contract.add_argument(
|
|
57
|
+
"--discounts",
|
|
58
|
+
type=Path,
|
|
59
|
+
metavar="FILE",
|
|
60
|
+
help='negotiated discounts: {"discounts": {"Anthropic": 0.15}} (15%% off list)',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
report = commands.add_parser(
|
|
64
|
+
"report", parents=[period, contract], help="cost summary in the terminal"
|
|
65
|
+
)
|
|
66
|
+
report.add_argument("root", nargs="?", type=Path, help=root_help)
|
|
67
|
+
report.add_argument(
|
|
68
|
+
"--currency", type=_currency, metavar="CODE", help="also show totals in CODE"
|
|
69
|
+
)
|
|
70
|
+
report.add_argument(
|
|
71
|
+
"--fx-rate",
|
|
72
|
+
type=_positive,
|
|
73
|
+
metavar="RATE",
|
|
74
|
+
help="units of --currency per USD; you give the rate, nothing is fetched",
|
|
75
|
+
)
|
|
76
|
+
report.set_defaults(handler=_report)
|
|
77
|
+
|
|
78
|
+
export = commands.add_parser(
|
|
79
|
+
"export", parents=[period, contract], help="write a FOCUS 1.2 CSV"
|
|
80
|
+
)
|
|
81
|
+
export.add_argument("output", type=Path, help="CSV file to write")
|
|
82
|
+
export.add_argument("root", nargs="?", type=Path, help=root_help)
|
|
83
|
+
export.set_defaults(handler=_export)
|
|
84
|
+
|
|
85
|
+
recommend = commands.add_parser(
|
|
86
|
+
"recommend", parents=[period], help="avoided cost and what-if headroom"
|
|
87
|
+
)
|
|
88
|
+
recommend.set_defaults(handler=_recommend)
|
|
89
|
+
|
|
90
|
+
unit = commands.add_parser(
|
|
91
|
+
"outcomes",
|
|
92
|
+
parents=[period],
|
|
93
|
+
help="usage value per commit, repository by repository",
|
|
94
|
+
)
|
|
95
|
+
unit.add_argument(
|
|
96
|
+
"repos",
|
|
97
|
+
nargs="*",
|
|
98
|
+
type=Path,
|
|
99
|
+
metavar="REPO",
|
|
100
|
+
help="git repositories to measure (default: every one the agents worked in)",
|
|
101
|
+
)
|
|
102
|
+
unit.add_argument(
|
|
103
|
+
"--all-authors",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="count every author's commits, not only your git user.email's",
|
|
106
|
+
)
|
|
107
|
+
unit.set_defaults(handler=_outcomes)
|
|
108
|
+
|
|
109
|
+
obs = commands.add_parser(
|
|
110
|
+
"observatory", help="render the static spend dashboard (aggregates only)"
|
|
111
|
+
)
|
|
112
|
+
obs.add_argument(
|
|
113
|
+
"outdir",
|
|
114
|
+
nargs="?",
|
|
115
|
+
type=Path,
|
|
116
|
+
default=observatory.DEFAULT_OUTPUT,
|
|
117
|
+
help=f"default: {observatory.DEFAULT_OUTPUT}",
|
|
118
|
+
)
|
|
119
|
+
obs.set_defaults(handler=_observatory)
|
|
120
|
+
|
|
121
|
+
card = commands.add_parser("prices", help="render the static price card")
|
|
122
|
+
card.add_argument(
|
|
123
|
+
"outdir",
|
|
124
|
+
nargs="?",
|
|
125
|
+
type=Path,
|
|
126
|
+
default=prices.DEFAULT_OUTPUT,
|
|
127
|
+
help=f"default: {prices.DEFAULT_OUTPUT}",
|
|
128
|
+
)
|
|
129
|
+
card.set_defaults(handler=_prices)
|
|
130
|
+
|
|
131
|
+
keep = commands.add_parser(
|
|
132
|
+
"import",
|
|
133
|
+
help="keep a provider's billing export (billed cost) in the ledger",
|
|
134
|
+
)
|
|
135
|
+
keep.add_argument("provider", choices=["runpod"])
|
|
136
|
+
keep.add_argument("files", nargs="+", type=Path, help="exports to import")
|
|
137
|
+
keep.set_defaults(handler=_import)
|
|
138
|
+
|
|
139
|
+
check = commands.add_parser(
|
|
140
|
+
"doctor",
|
|
141
|
+
help="check log formats, the ledger and pricing (read-only)",
|
|
142
|
+
)
|
|
143
|
+
check.set_defaults(handler=_doctor)
|
|
144
|
+
return parser
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
148
|
+
parser = build_parser()
|
|
149
|
+
args = parser.parse_args(argv)
|
|
150
|
+
if args.command is None:
|
|
151
|
+
args = parser.parse_args(["report"])
|
|
152
|
+
since, until = getattr(args, "since", None), getattr(args, "until", None)
|
|
153
|
+
if since and until and since >= until:
|
|
154
|
+
parser.error("--since must be an earlier day than --until")
|
|
155
|
+
if (getattr(args, "currency", None) is None) != (
|
|
156
|
+
getattr(args, "fx_rate", None) is None
|
|
157
|
+
):
|
|
158
|
+
parser.error("--currency and --fx-rate go together")
|
|
159
|
+
try:
|
|
160
|
+
return args.handler(args)
|
|
161
|
+
except (ledger.LedgerError, ConfigError, outcomes.OutcomesError) as exc:
|
|
162
|
+
_fail(str(exc))
|
|
163
|
+
except sqlite3.DatabaseError as exc:
|
|
164
|
+
_fail(f"the ledger looks damaged ({exc}); run `tokencur doctor`")
|
|
165
|
+
return 1
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _currency(text: str) -> str:
|
|
169
|
+
if len(text) != 3 or not text.isalpha() or not text.isupper():
|
|
170
|
+
raise argparse.ArgumentTypeError(f"not a 3-letter currency code: {text!r}")
|
|
171
|
+
return text
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _positive(text: str) -> float:
|
|
175
|
+
try:
|
|
176
|
+
value = float(text)
|
|
177
|
+
except ValueError:
|
|
178
|
+
raise argparse.ArgumentTypeError(f"not a number: {text!r}") from None
|
|
179
|
+
if not value > 0:
|
|
180
|
+
raise argparse.ArgumentTypeError(f"must be above 0: {text!r}")
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _charges(args: argparse.Namespace) -> list:
|
|
185
|
+
"""Billed charges in the ledger for the command's period. An explicit
|
|
186
|
+
log root is an ad hoc look at those logs alone, so it gets none."""
|
|
187
|
+
if getattr(args, "root", None) is not None:
|
|
188
|
+
return []
|
|
189
|
+
return in_period(
|
|
190
|
+
ledger.read_charges(), args.since, args.until, when=lambda c: c.period_start
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _discounts(args: argparse.Namespace) -> dict[str, float] | None:
|
|
195
|
+
return load_discounts(args.discounts) if args.discounts else None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _day(text: str) -> date:
|
|
199
|
+
try:
|
|
200
|
+
return date.fromisoformat(text)
|
|
201
|
+
except ValueError:
|
|
202
|
+
raise argparse.ArgumentTypeError(f"not a YYYY-MM-DD date: {text!r}") from None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _records(args: argparse.Namespace) -> list[UsageRecord] | None:
|
|
206
|
+
"""The command's records, or None after printing why there are none."""
|
|
207
|
+
root = getattr(args, "root", None)
|
|
208
|
+
if root is not None:
|
|
209
|
+
if not root.exists():
|
|
210
|
+
return _fail(f"{root} does not exist")
|
|
211
|
+
records = list(claude_code.iter_usage_records(root))
|
|
212
|
+
else:
|
|
213
|
+
records = load_records()
|
|
214
|
+
if not records:
|
|
215
|
+
return _fail(_NOTHING)
|
|
216
|
+
return in_period(records, args.since, args.until)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _period(args: argparse.Namespace) -> str | None:
|
|
220
|
+
since, until = args.since, args.until
|
|
221
|
+
if since and until:
|
|
222
|
+
return f"{since} to {until} (UTC, end excluded)"
|
|
223
|
+
if since:
|
|
224
|
+
return f"since {since} (UTC)"
|
|
225
|
+
if until:
|
|
226
|
+
return f"before {until} (UTC)"
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _fail(message: str) -> None:
|
|
231
|
+
print(f"error: {message}", file=sys.stderr)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _report(args: argparse.Namespace) -> int:
|
|
235
|
+
discounts = _discounts(args) # a bad file fails before any scan
|
|
236
|
+
records = _records(args)
|
|
237
|
+
if records is None:
|
|
238
|
+
return 1
|
|
239
|
+
if not records:
|
|
240
|
+
_fail(f"no usage {_period(args)}")
|
|
241
|
+
return 1
|
|
242
|
+
fx = (args.currency, args.fx_rate) if args.currency else None
|
|
243
|
+
print(
|
|
244
|
+
summarize(
|
|
245
|
+
records,
|
|
246
|
+
period=_period(args),
|
|
247
|
+
discounts=discounts,
|
|
248
|
+
fx=fx,
|
|
249
|
+
charges=_charges(args),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _export(args: argparse.Namespace) -> int:
|
|
256
|
+
discounts = _discounts(args) # a bad file fails before any scan
|
|
257
|
+
records = _records(args)
|
|
258
|
+
if records is None:
|
|
259
|
+
return 1
|
|
260
|
+
rows = export_csv(records, args.output, discounts, _charges(args))
|
|
261
|
+
print(f"wrote {rows} FOCUS charge rows to {args.output}", file=sys.stderr)
|
|
262
|
+
skipped = unpriced_models(records)
|
|
263
|
+
if skipped:
|
|
264
|
+
pairs = ", ".join(f"{m} x{n}" for m, n in sorted(skipped.items()))
|
|
265
|
+
print(f"skipped unpriced usage: {pairs}", file=sys.stderr)
|
|
266
|
+
undated = undated_count(records)
|
|
267
|
+
if undated:
|
|
268
|
+
print(
|
|
269
|
+
f"skipped undated usage: {undated} records (no parseable timestamp)",
|
|
270
|
+
file=sys.stderr,
|
|
271
|
+
)
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _recommend(args: argparse.Namespace) -> int:
|
|
276
|
+
records = _records(args)
|
|
277
|
+
if not records:
|
|
278
|
+
if records is not None:
|
|
279
|
+
_fail(f"no usage {_period(args)}")
|
|
280
|
+
return 1
|
|
281
|
+
print(render(recommendations(records)))
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _outcomes(args: argparse.Namespace) -> int:
|
|
286
|
+
records = _records(args)
|
|
287
|
+
if records is None:
|
|
288
|
+
return 1
|
|
289
|
+
result = outcomes.outcomes(
|
|
290
|
+
records,
|
|
291
|
+
repos=args.repos,
|
|
292
|
+
since=args.since,
|
|
293
|
+
until=args.until,
|
|
294
|
+
all_authors=args.all_authors,
|
|
295
|
+
period=_period(args),
|
|
296
|
+
)
|
|
297
|
+
print(outcomes.render(result))
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _observatory(args: argparse.Namespace) -> int:
|
|
302
|
+
records = load_records()
|
|
303
|
+
if not records:
|
|
304
|
+
_fail(_NOTHING)
|
|
305
|
+
return 1
|
|
306
|
+
snap = observatory.snapshot(
|
|
307
|
+
records, observatory.load_subscriptions(), ledger.read_charges()
|
|
308
|
+
)
|
|
309
|
+
observatory.write_site(snap, args.outdir)
|
|
310
|
+
print(f"observatory written to {args.outdir} ({len(records)} records aggregated)")
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _prices(args: argparse.Namespace) -> int:
|
|
315
|
+
changes = prices.price_changes()
|
|
316
|
+
prices.write_site(args.outdir, changes)
|
|
317
|
+
moved = sum(len(c.changed) for c in changes)
|
|
318
|
+
gained = sum(len(c.added) for c in changes if not c.introduced)
|
|
319
|
+
print(
|
|
320
|
+
f"prices page written to {args.outdir} "
|
|
321
|
+
f"({len(changes)} events: {moved} rate moves, {gained} models added)"
|
|
322
|
+
)
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _doctor(args: argparse.Namespace) -> int:
|
|
327
|
+
diagnosis = doctor.diagnose()
|
|
328
|
+
print(doctor.render(diagnosis))
|
|
329
|
+
return 1 if diagnosis.problems else 0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _import(args: argparse.Namespace) -> int:
|
|
333
|
+
charges = []
|
|
334
|
+
for path in args.files:
|
|
335
|
+
if not path.exists():
|
|
336
|
+
_fail(f"{path} does not exist")
|
|
337
|
+
return 1
|
|
338
|
+
charges.extend(runpod.iter_charges(path))
|
|
339
|
+
if not charges:
|
|
340
|
+
_fail("no billed charges in those files")
|
|
341
|
+
return 1
|
|
342
|
+
added = ledger.record_charges(charges)
|
|
343
|
+
total = sum(c.amount_usd for c in charges)
|
|
344
|
+
providers = ", ".join(sorted({c.provider for c in charges}))
|
|
345
|
+
print(
|
|
346
|
+
f"{len(charges)} billed charges ({added} new), ${total:,.2f} from "
|
|
347
|
+
f"{providers} — {ledger.default_path()}"
|
|
348
|
+
)
|
|
349
|
+
return 0
|
tokencur/dashboard.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Local cost dashboard over the FOCUS dataset.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
pip install -e ".[dashboard]"
|
|
5
|
+
streamlit run src/tokencur/dashboard.py
|
|
6
|
+
|
|
7
|
+
Loads the full usage history (every known local source, through the
|
|
8
|
+
ledger — see ``tokencur.sources``), normalizes to FOCUS charge rows and
|
|
9
|
+
lets DuckDB run the analytics — the same SQL any FinOps analyst would
|
|
10
|
+
write over a FOCUS dataset. Every view exposes its SQL and its table
|
|
11
|
+
(the table doubles as the accessibility fallback for chart colors).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import duckdb
|
|
17
|
+
import pandas as pd
|
|
18
|
+
import streamlit as st
|
|
19
|
+
|
|
20
|
+
from tokencur.focus import to_focus_rows, unpriced_models
|
|
21
|
+
from tokencur.recommend import recommendations
|
|
22
|
+
from tokencur.sources import load_records
|
|
23
|
+
|
|
24
|
+
# Fixed service→color mapping (color follows the entity, never the
|
|
25
|
+
# rank). Both palettes validated for their surface; see the dataviz
|
|
26
|
+
# palette notes in the commit message.
|
|
27
|
+
SERVICE_ORDER = ["Claude Code", "Codex CLI", "Kimi Code"]
|
|
28
|
+
SERVICE_COLORS = {
|
|
29
|
+
"dark": {"Claude Code": "#3987e5", "Codex CLI": "#199e70", "Kimi Code": "#c98500"},
|
|
30
|
+
"light": {"Claude Code": "#2a78d6", "Codex CLI": "#1baf7a", "Kimi Code": "#eda100"},
|
|
31
|
+
}
|
|
32
|
+
SINGLE_HUE = {"dark": "#3987e5", "light": "#2a78d6"} # magnitude charts
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _mode() -> str:
|
|
36
|
+
try:
|
|
37
|
+
return st.context.theme.type or "dark"
|
|
38
|
+
except Exception:
|
|
39
|
+
return "dark"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@st.cache_data(show_spinner="Scanning local usage logs…")
|
|
43
|
+
def load() -> tuple[pd.DataFrame, dict[str, int], list]:
|
|
44
|
+
records = load_records()
|
|
45
|
+
return pd.DataFrame(to_focus_rows(records)), unpriced_models(records), records
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def view(title: str, sql: str, frame: pd.DataFrame) -> pd.DataFrame:
|
|
49
|
+
"""Run one DuckDB query and render its SQL + table under the title."""
|
|
50
|
+
result = duckdb.sql(sql.replace("FOCUS", "frame")).df()
|
|
51
|
+
st.subheader(title)
|
|
52
|
+
with st.expander("SQL + tabla"):
|
|
53
|
+
st.code(sql, language="sql")
|
|
54
|
+
st.dataframe(result, use_container_width=True)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
st.set_page_config(page_title="tokencur", page_icon="🧾", layout="wide")
|
|
59
|
+
st.title("tokencur — local AI spend, FOCUS-shaped")
|
|
60
|
+
|
|
61
|
+
focus, unpriced, records = load()
|
|
62
|
+
if focus.empty:
|
|
63
|
+
st.warning("No usage found in any known local source.")
|
|
64
|
+
st.stop()
|
|
65
|
+
|
|
66
|
+
mode = _mode()
|
|
67
|
+
total = focus["BilledCost"].sum()
|
|
68
|
+
days = focus["ChargePeriodStart"].str[:10].nunique()
|
|
69
|
+
|
|
70
|
+
k1, k2, k3, k4 = st.columns(4)
|
|
71
|
+
k1.metric("API-equiv. usage value (showback)", f"${total:,.2f}")
|
|
72
|
+
k2.metric("Charge rows", f"{len(focus):,}")
|
|
73
|
+
k3.metric("Days covered", days)
|
|
74
|
+
k4.metric("Avg cost / day", f"${total / max(days, 1):,.2f}")
|
|
75
|
+
if unpriced:
|
|
76
|
+
st.caption(
|
|
77
|
+
"Unpriced usage (excluded): "
|
|
78
|
+
+ ", ".join(f"{m} ×{n}" for m, n in sorted(unpriced.items()))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
daily = view(
|
|
82
|
+
"Daily cost by service",
|
|
83
|
+
"""
|
|
84
|
+
SELECT substr(ChargePeriodStart, 1, 10) AS day,
|
|
85
|
+
ServiceName,
|
|
86
|
+
round(sum(BilledCost), 4) AS cost_usd
|
|
87
|
+
FROM FOCUS
|
|
88
|
+
GROUP BY 1, 2
|
|
89
|
+
ORDER BY 1
|
|
90
|
+
""",
|
|
91
|
+
focus,
|
|
92
|
+
)
|
|
93
|
+
pivot = daily.pivot(index="day", columns="ServiceName", values="cost_usd")
|
|
94
|
+
services = [s for s in SERVICE_ORDER if s in pivot.columns]
|
|
95
|
+
st.line_chart(
|
|
96
|
+
pivot[services],
|
|
97
|
+
color=[SERVICE_COLORS[mode][s] for s in services],
|
|
98
|
+
height=320,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
by_model = view(
|
|
102
|
+
"Cost by model",
|
|
103
|
+
"""
|
|
104
|
+
SELECT regexp_replace(SkuId, '/[^/]+$', '') AS model,
|
|
105
|
+
round(sum(BilledCost), 2) AS cost_usd,
|
|
106
|
+
sum(ConsumedQuantity)::BIGINT AS tokens
|
|
107
|
+
FROM FOCUS
|
|
108
|
+
GROUP BY 1
|
|
109
|
+
ORDER BY 2 DESC
|
|
110
|
+
""",
|
|
111
|
+
focus,
|
|
112
|
+
)
|
|
113
|
+
st.bar_chart(
|
|
114
|
+
by_model.set_index("model")["cost_usd"],
|
|
115
|
+
color=SINGLE_HUE[mode],
|
|
116
|
+
horizontal=True,
|
|
117
|
+
height=320,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
by_bucket = view(
|
|
121
|
+
"Where the money goes (token type)",
|
|
122
|
+
"""
|
|
123
|
+
SELECT regexp_extract(SkuId, '[^/]+$') AS token_type,
|
|
124
|
+
round(sum(BilledCost), 2) AS cost_usd
|
|
125
|
+
FROM FOCUS
|
|
126
|
+
GROUP BY 1
|
|
127
|
+
ORDER BY 2 DESC
|
|
128
|
+
""",
|
|
129
|
+
focus,
|
|
130
|
+
)
|
|
131
|
+
st.bar_chart(
|
|
132
|
+
by_bucket.set_index("token_type")["cost_usd"],
|
|
133
|
+
color=SINGLE_HUE[mode],
|
|
134
|
+
height=260,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
view(
|
|
138
|
+
"Unit economics by model",
|
|
139
|
+
"""
|
|
140
|
+
SELECT regexp_replace(SkuId, '/[^/]+$', '') AS model,
|
|
141
|
+
round(sum(BilledCost), 2) AS cost_usd,
|
|
142
|
+
round(sum(BilledCost) / count(DISTINCT substr(ChargePeriodStart, 1, 10)), 3)
|
|
143
|
+
AS usd_per_active_day,
|
|
144
|
+
round(1e6 * sum(BilledCost) / sum(ConsumedQuantity), 3)
|
|
145
|
+
AS usd_per_mtok_effective
|
|
146
|
+
FROM FOCUS
|
|
147
|
+
GROUP BY 1
|
|
148
|
+
ORDER BY 2 DESC
|
|
149
|
+
""",
|
|
150
|
+
focus,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
st.subheader("Recommendations")
|
|
154
|
+
recs = recommendations(records)
|
|
155
|
+
achieved = sum(r.savings_usd for r in recs if r.kind == "achieved")
|
|
156
|
+
potential = sum(r.savings_usd for r in recs if r.kind == "potential")
|
|
157
|
+
r1, r2 = st.columns(2)
|
|
158
|
+
r1.metric("Avoided by caching (counterfactual)", f"${achieved:,.2f}")
|
|
159
|
+
r2.metric("Right-sizing headroom (what-if)", f"${potential:,.2f}")
|
|
160
|
+
st.dataframe(
|
|
161
|
+
pd.DataFrame(
|
|
162
|
+
{
|
|
163
|
+
"kind": r.kind,
|
|
164
|
+
"recommendation": r.title,
|
|
165
|
+
"savings USD": round(r.savings_usd, 2),
|
|
166
|
+
"% of baseline": round(r.savings_pct, 1),
|
|
167
|
+
"detail": r.detail,
|
|
168
|
+
}
|
|
169
|
+
for r in recs
|
|
170
|
+
),
|
|
171
|
+
use_container_width=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
st.caption(
|
|
175
|
+
"Costs are API-equivalent list prices (showback) over local agent logs. "
|
|
176
|
+
"Source: tokencur FOCUS export."
|
|
177
|
+
)
|