driftcast 1.0.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.
Files changed (31) hide show
  1. driftcast-1.0.0/LICENSE +21 -0
  2. driftcast-1.0.0/PKG-INFO +163 -0
  3. driftcast-1.0.0/README.md +138 -0
  4. driftcast-1.0.0/pyproject.toml +42 -0
  5. driftcast-1.0.0/setup.cfg +4 -0
  6. driftcast-1.0.0/src/driftcast/__init__.py +17 -0
  7. driftcast-1.0.0/src/driftcast/cli.py +128 -0
  8. driftcast-1.0.0/src/driftcast/dashboard.py +329 -0
  9. driftcast-1.0.0/src/driftcast/otel/__init__.py +6 -0
  10. driftcast-1.0.0/src/driftcast/otel/claude_code_tel.py +591 -0
  11. driftcast-1.0.0/src/driftcast/pricing.py +83 -0
  12. driftcast-1.0.0/src/driftcast/py.typed +0 -0
  13. driftcast-1.0.0/src/driftcast/storage.py +392 -0
  14. driftcast-1.0.0/src/driftcast/tracer.py +460 -0
  15. driftcast-1.0.0/src/driftcast.egg-info/PKG-INFO +163 -0
  16. driftcast-1.0.0/src/driftcast.egg-info/SOURCES.txt +29 -0
  17. driftcast-1.0.0/src/driftcast.egg-info/dependency_links.txt +1 -0
  18. driftcast-1.0.0/src/driftcast.egg-info/entry_points.txt +3 -0
  19. driftcast-1.0.0/src/driftcast.egg-info/requires.txt +3 -0
  20. driftcast-1.0.0/src/driftcast.egg-info/top_level.txt +1 -0
  21. driftcast-1.0.0/tests/test_abandoned_runs.py +153 -0
  22. driftcast-1.0.0/tests/test_cli.py +19 -0
  23. driftcast-1.0.0/tests/test_comment_policy.py +30 -0
  24. driftcast-1.0.0/tests/test_dashboard_teaser.py +10 -0
  25. driftcast-1.0.0/tests/test_equivalence.py +242 -0
  26. driftcast-1.0.0/tests/test_free_boundary.py +27 -0
  27. driftcast-1.0.0/tests/test_off_loop_writes.py +207 -0
  28. driftcast-1.0.0/tests/test_otel_receiver.py +164 -0
  29. driftcast-1.0.0/tests/test_packaging.py +16 -0
  30. driftcast-1.0.0/tests/test_storage.py +27 -0
  31. driftcast-1.0.0/tests/test_tracer.py +503 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AKASH
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,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: driftcast
3
+ Version: 1.0.0
4
+ Summary: Local-first cost and trace telemetry for AI agents — see exactly what each agent spends, per run.
5
+ Author-email: Akash RK <akashrk@avtaarlabs.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AkashRK1216/driftcast
8
+ Project-URL: Repository, https://github.com/AkashRK1216/driftcast
9
+ Project-URL: Issues, https://github.com/AkashRK1216/driftcast/issues
10
+ Keywords: llm,agents,cost,telemetry,observability,tokens,local-first
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: System :: Monitoring
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dashboard
23
+ Requires-Dist: gradio>=4.0.0; extra == "dashboard"
24
+ Dynamic: license-file
25
+
26
+ # driftcast
27
+
28
+ Cost and trace telemetry for agent frameworks. Captures actual tokens, cost,
29
+ latency, and errors as your agent runs, persisted to a local SQLite file.
30
+
31
+ **You own your data.** DriftCast is content-agnostic by default: it records the
32
+ *shape and economics* of execution (token counts, cost, latency, status,
33
+ structure) — never your prompts, completions, or documents. Everything is
34
+ written to a local file you control; there is no DriftCast server in the data
35
+ path. Unlike cloud-coupled tracers (which go dark under Zero Data Retention
36
+ policies), there is nothing to switch off. See [Data ownership](#data-ownership).
37
+
38
+ One decorator, one explicit call. `@lens.track` turns any function into a
39
+ tracked run or span automatically — nesting into a call-tree on its own. The
40
+ only thing you pass by hand is token usage (`driftcast.record(...)`), because
41
+ provider response shapes differ across providers and call types (embeddings vs.
42
+ chat completions). Everything else — cost lookup, latency, structure,
43
+ persistence — is automatic.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install driftcast
49
+ ```
50
+
51
+ Optional local dashboard (Gradio):
52
+
53
+ ```bash
54
+ pip install "driftcast[dashboard]"
55
+ ```
56
+
57
+ The core SDK is stdlib-only. `driftcast[dashboard]` adds the local Gradio
58
+ viewer (`driftcast dashboard`). Claude Code capture via the OTLP receiver
59
+ (`driftcast-otel`) is included in the core install.
60
+
61
+ **Build from source:**
62
+
63
+ ```bash
64
+ git clone https://github.com/AkashRK1216/driftcast
65
+ cd driftcast
66
+ pip install -e ".[dashboard]"
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ Decorate your functions with `@lens.track`. The **outermost** decorated call
72
+ becomes a *run* (one full pipeline execution); **nested** decorated calls become
73
+ *spans* (individual provider calls), auto-parented into a tree. Inside a span,
74
+ call `driftcast.record(...)` once to report token usage.
75
+
76
+ ```python
77
+ import driftcast
78
+
79
+ lens = driftcast.init(project="rag-agent", db_path="./driftcast.db")
80
+
81
+ @lens.track(model="text-embedding-3-small")
82
+ def embed_query(query):
83
+ result = openai_client.embeddings.create(model=EMBED_MODEL, input=query)
84
+ driftcast.record(input_tokens=result.usage.prompt_tokens, output_tokens=0)
85
+ return result
86
+
87
+ @lens.track(model="gpt-4o-mini")
88
+ def generate_answer(query):
89
+ response = openai_client.chat.completions.create(...)
90
+ driftcast.record(
91
+ input_tokens=response.usage.prompt_tokens,
92
+ output_tokens=response.usage.completion_tokens,
93
+ )
94
+ return response.choices[0].message.content
95
+
96
+ @lens.track # the top-level call is the run
97
+ def ask(query):
98
+ driftcast.annotate(customer_id="acme") # business labels onto the run
99
+ embed_query(query)
100
+ return generate_answer(query)
101
+
102
+ ask("what is the refund policy?")
103
+ ```
104
+
105
+ - `@lens.track(model=None, name=None)` — the whole API. The outermost decorated call opens a run; nested decorated calls become spans, auto-nested by call depth. On exit each records `cost`, `latency_ms`, and status; an exception is recorded as `status="error"` and re-raised — tracing never masks a real failure. Works on sync and `async` functions.
106
+ - `driftcast.record(input_tokens, output_tokens, content=None)` — call once inside a `@lens.track(model=...)` function to report what the provider call consumed. This is the one number you pass by hand (provider usage shapes differ). Pass `content=` to persist prompt/response **only** when `capture_content=True` (see Data ownership).
107
+ - `driftcast.annotate(**labels)` — attach business labels (`route`, `customer_id`, judge verdicts…) to the current run's metadata. Always stored, never treated as content.
108
+ - `driftcast.outcome(accepted, wasted=[...])` — at the end of a run, record your own ground-truth label (what you accepted / what was wasted). Content-free; persisted into the run's metadata.
109
+
110
+ ## Data ownership
111
+
112
+ `driftcast.init(..., capture_content=False)` is the default. In that mode:
113
+
114
+ - **Content passed via `content=` is dropped before persistence.** Token counts, cost, latency, and structure are still recorded — enough for cost attribution and tracing, with zero prompt/response data at rest.
115
+ - **Error messages are reduced to the exception type** (e.g. `RateLimitError`), since provider errors can echo input content. The full message is kept only when content capture is on.
116
+ - **Metadata is stored separately from content**, so per-customer attribution (`customer_id=...`) never requires storing a prompt.
117
+
118
+ Set `capture_content=True` to also persist `content=` payloads for debugging — written only to your local `db_path`, never transmitted anywhere. This is the design that lets DriftCast run under Zero Data Retention policies where cloud-coupled tracers cannot.
119
+
120
+ ## CLI
121
+
122
+ ```bash
123
+ driftcast summary --db ./driftcast.db [--project rag-agent]
124
+ ```
125
+
126
+ Prints an aggregate report grouped by run and by model — total cost, total
127
+ tokens, run count, average latency.
128
+
129
+ ## Live dashboard
130
+
131
+ A web dashboard renders the same data as a live, auto-refreshing view (headline
132
+ totals, runs, per-model cost/tokens/latency). Gradio is an **optional** extra —
133
+ the core SDK stays stdlib-only.
134
+
135
+ ```bash
136
+ pip install -e ".[dashboard]" # installs gradio
137
+ driftcast dashboard --db ./driftcast.db --project rag-agent --port 7861
138
+ ```
139
+
140
+ To pop it automatically alongside an agent's own UI, launch it non-blocking:
141
+
142
+ ```python
143
+ import driftcast.dashboard as dashboard
144
+
145
+ # returns immediately; server runs in a background thread on its own port
146
+ dashboard.launch(db_path="./driftcast.db", project="rag-agent", port=7861, block=False)
147
+ ```
148
+
149
+ The RAG test agent does exactly this — running `python main.py` opens the chat
150
+ UI and the stats dashboard side by side (dashboard on port 7861, override with
151
+ `DRIFTCAST_DASHBOARD_PORT`).
152
+
153
+ ## Pricing
154
+
155
+ `src/driftcast/pricing.py` is a plain editable `$ per 1M tokens` dict. Unknown
156
+ models cost `$0.0` and log a warning, so untracked spend is a visible signal
157
+ to add the model rather than a silent miscalculation.
158
+
159
+ ## Storage
160
+
161
+ SQLite via stdlib `sqlite3` — file-based, no extra dependency, matches the
162
+ "under $100, personal dogfood" scale this targets. Two tables: `runs` (one row
163
+ per pipeline execution) and `spans` (one row per provider call).
@@ -0,0 +1,138 @@
1
+ # driftcast
2
+
3
+ Cost and trace telemetry for agent frameworks. Captures actual tokens, cost,
4
+ latency, and errors as your agent runs, persisted to a local SQLite file.
5
+
6
+ **You own your data.** DriftCast is content-agnostic by default: it records the
7
+ *shape and economics* of execution (token counts, cost, latency, status,
8
+ structure) — never your prompts, completions, or documents. Everything is
9
+ written to a local file you control; there is no DriftCast server in the data
10
+ path. Unlike cloud-coupled tracers (which go dark under Zero Data Retention
11
+ policies), there is nothing to switch off. See [Data ownership](#data-ownership).
12
+
13
+ One decorator, one explicit call. `@lens.track` turns any function into a
14
+ tracked run or span automatically — nesting into a call-tree on its own. The
15
+ only thing you pass by hand is token usage (`driftcast.record(...)`), because
16
+ provider response shapes differ across providers and call types (embeddings vs.
17
+ chat completions). Everything else — cost lookup, latency, structure,
18
+ persistence — is automatic.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install driftcast
24
+ ```
25
+
26
+ Optional local dashboard (Gradio):
27
+
28
+ ```bash
29
+ pip install "driftcast[dashboard]"
30
+ ```
31
+
32
+ The core SDK is stdlib-only. `driftcast[dashboard]` adds the local Gradio
33
+ viewer (`driftcast dashboard`). Claude Code capture via the OTLP receiver
34
+ (`driftcast-otel`) is included in the core install.
35
+
36
+ **Build from source:**
37
+
38
+ ```bash
39
+ git clone https://github.com/AkashRK1216/driftcast
40
+ cd driftcast
41
+ pip install -e ".[dashboard]"
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ Decorate your functions with `@lens.track`. The **outermost** decorated call
47
+ becomes a *run* (one full pipeline execution); **nested** decorated calls become
48
+ *spans* (individual provider calls), auto-parented into a tree. Inside a span,
49
+ call `driftcast.record(...)` once to report token usage.
50
+
51
+ ```python
52
+ import driftcast
53
+
54
+ lens = driftcast.init(project="rag-agent", db_path="./driftcast.db")
55
+
56
+ @lens.track(model="text-embedding-3-small")
57
+ def embed_query(query):
58
+ result = openai_client.embeddings.create(model=EMBED_MODEL, input=query)
59
+ driftcast.record(input_tokens=result.usage.prompt_tokens, output_tokens=0)
60
+ return result
61
+
62
+ @lens.track(model="gpt-4o-mini")
63
+ def generate_answer(query):
64
+ response = openai_client.chat.completions.create(...)
65
+ driftcast.record(
66
+ input_tokens=response.usage.prompt_tokens,
67
+ output_tokens=response.usage.completion_tokens,
68
+ )
69
+ return response.choices[0].message.content
70
+
71
+ @lens.track # the top-level call is the run
72
+ def ask(query):
73
+ driftcast.annotate(customer_id="acme") # business labels onto the run
74
+ embed_query(query)
75
+ return generate_answer(query)
76
+
77
+ ask("what is the refund policy?")
78
+ ```
79
+
80
+ - `@lens.track(model=None, name=None)` — the whole API. The outermost decorated call opens a run; nested decorated calls become spans, auto-nested by call depth. On exit each records `cost`, `latency_ms`, and status; an exception is recorded as `status="error"` and re-raised — tracing never masks a real failure. Works on sync and `async` functions.
81
+ - `driftcast.record(input_tokens, output_tokens, content=None)` — call once inside a `@lens.track(model=...)` function to report what the provider call consumed. This is the one number you pass by hand (provider usage shapes differ). Pass `content=` to persist prompt/response **only** when `capture_content=True` (see Data ownership).
82
+ - `driftcast.annotate(**labels)` — attach business labels (`route`, `customer_id`, judge verdicts…) to the current run's metadata. Always stored, never treated as content.
83
+ - `driftcast.outcome(accepted, wasted=[...])` — at the end of a run, record your own ground-truth label (what you accepted / what was wasted). Content-free; persisted into the run's metadata.
84
+
85
+ ## Data ownership
86
+
87
+ `driftcast.init(..., capture_content=False)` is the default. In that mode:
88
+
89
+ - **Content passed via `content=` is dropped before persistence.** Token counts, cost, latency, and structure are still recorded — enough for cost attribution and tracing, with zero prompt/response data at rest.
90
+ - **Error messages are reduced to the exception type** (e.g. `RateLimitError`), since provider errors can echo input content. The full message is kept only when content capture is on.
91
+ - **Metadata is stored separately from content**, so per-customer attribution (`customer_id=...`) never requires storing a prompt.
92
+
93
+ Set `capture_content=True` to also persist `content=` payloads for debugging — written only to your local `db_path`, never transmitted anywhere. This is the design that lets DriftCast run under Zero Data Retention policies where cloud-coupled tracers cannot.
94
+
95
+ ## CLI
96
+
97
+ ```bash
98
+ driftcast summary --db ./driftcast.db [--project rag-agent]
99
+ ```
100
+
101
+ Prints an aggregate report grouped by run and by model — total cost, total
102
+ tokens, run count, average latency.
103
+
104
+ ## Live dashboard
105
+
106
+ A web dashboard renders the same data as a live, auto-refreshing view (headline
107
+ totals, runs, per-model cost/tokens/latency). Gradio is an **optional** extra —
108
+ the core SDK stays stdlib-only.
109
+
110
+ ```bash
111
+ pip install -e ".[dashboard]" # installs gradio
112
+ driftcast dashboard --db ./driftcast.db --project rag-agent --port 7861
113
+ ```
114
+
115
+ To pop it automatically alongside an agent's own UI, launch it non-blocking:
116
+
117
+ ```python
118
+ import driftcast.dashboard as dashboard
119
+
120
+ # returns immediately; server runs in a background thread on its own port
121
+ dashboard.launch(db_path="./driftcast.db", project="rag-agent", port=7861, block=False)
122
+ ```
123
+
124
+ The RAG test agent does exactly this — running `python main.py` opens the chat
125
+ UI and the stats dashboard side by side (dashboard on port 7861, override with
126
+ `DRIFTCAST_DASHBOARD_PORT`).
127
+
128
+ ## Pricing
129
+
130
+ `src/driftcast/pricing.py` is a plain editable `$ per 1M tokens` dict. Unknown
131
+ models cost `$0.0` and log a warning, so untracked spend is a visible signal
132
+ to add the model rather than a silent miscalculation.
133
+
134
+ ## Storage
135
+
136
+ SQLite via stdlib `sqlite3` — file-based, no extra dependency, matches the
137
+ "under $100, personal dogfood" scale this targets. Two tables: `runs` (one row
138
+ per pipeline execution) and `spans` (one row per provider call).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "driftcast"
7
+ version = "1.0.0"
8
+ description = "Local-first cost and trace telemetry for AI agents — see exactly what each agent spends, per run."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Akash RK", email = "akashrk@avtaarlabs.com" }]
13
+ keywords = ["llm", "agents", "cost", "telemetry", "observability", "tokens", "local-first"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: System :: Monitoring",
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.optional-dependencies]
27
+ dashboard = ["gradio>=4.0.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/AkashRK1216/driftcast"
31
+ Repository = "https://github.com/AkashRK1216/driftcast"
32
+ Issues = "https://github.com/AkashRK1216/driftcast/issues"
33
+
34
+ [project.scripts]
35
+ driftcast = "driftcast.cli:main"
36
+ driftcast-otel = "driftcast.otel.claude_code_tel:main"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ "driftcast" = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """driftcast — cost and trace telemetry for agent frameworks."""
2
+
3
+ from .storage import default_db_path
4
+ from .tracer import Lens, annotate, outcome, record
5
+
6
+ __all__ = ["init", "Lens", "record", "annotate", "outcome", "default_db_path"]
7
+
8
+
9
+ def init(
10
+ project: str,
11
+ db_path: str | None = None,
12
+ capture_content: bool = False,
13
+ ) -> Lens:
14
+ """Single entry point so every caller gets the universal db path and the capture_content privacy default (False = shape and economics only, never content)."""
15
+ if db_path is None:
16
+ db_path = default_db_path()
17
+ return Lens(project=project, db_path=db_path, capture_content=capture_content)
@@ -0,0 +1,128 @@
1
+ """
2
+ cli.py — `driftcast summary`: aggregate cost/token report.
3
+
4
+ Plain-table output, mirroring the visual style of rag-agent's
5
+ `ingest.py:preview_ingest` for consistency across the DriftCast tooling.
6
+ """
7
+
8
+ import argparse
9
+ import sqlite3
10
+
11
+ from .storage import default_db_path
12
+
13
+
14
+ def _fetch_rows(db_path: str, project: str | None):
15
+ """Isolates the read-only SQL so the printers work on plain rows."""
16
+ conn = sqlite3.connect(db_path)
17
+ conn.row_factory = sqlite3.Row
18
+ try:
19
+ if project:
20
+ runs = conn.execute(
21
+ "SELECT * FROM runs WHERE project = ? ORDER BY started_at", (project,)
22
+ ).fetchall()
23
+ else:
24
+ runs = conn.execute("SELECT * FROM runs ORDER BY started_at").fetchall()
25
+
26
+ run_ids = [r["run_id"] for r in runs]
27
+ spans = []
28
+ if run_ids:
29
+ placeholders = ",".join("?" * len(run_ids))
30
+ spans = conn.execute(
31
+ f"SELECT * FROM spans WHERE run_id IN ({placeholders})", run_ids
32
+ ).fetchall()
33
+ return runs, spans
34
+ finally:
35
+ conn.close()
36
+
37
+
38
+ def _print_by_run(runs):
39
+ """Answers "what did each pipeline execution cost" at a glance in the terminal."""
40
+ if not runs:
41
+ print("No runs recorded.")
42
+ return
43
+
44
+ col = 28
45
+ print(f"\n {'Run':<{col}} {'Status':>8} {'Cost ($)':>10} {'Started':>26}")
46
+ print(" " + "─" * (col + 48))
47
+ total_cost = 0.0
48
+ for r in runs:
49
+ print(f" {r['name']:<{col}} {r['status']:>8} {r['total_cost']:>10.4f} {r['started_at']:>26}")
50
+ total_cost += r["total_cost"]
51
+ print(" " + "─" * (col + 48))
52
+ print(f" {'TOTAL':<{col}} {'':>8} {total_cost:>10.4f}")
53
+ print()
54
+
55
+
56
+ def _print_by_model(spans):
57
+ """Answers "which model is the money going to" without opening the dashboard."""
58
+ if not spans:
59
+ return
60
+
61
+ stats: dict[str, dict] = {}
62
+ for s in spans:
63
+ model = s["model"] or "(no model)"
64
+ st = stats.setdefault(model, {"calls": 0, "input_tokens": 0, "output_tokens": 0, "cost": 0.0, "latency_ms": 0.0})
65
+ st["calls"] += 1
66
+ st["input_tokens"] += s["input_tokens"]
67
+ st["output_tokens"] += s["output_tokens"]
68
+ st["cost"] += s["cost"]
69
+ st["latency_ms"] += s["latency_ms"]
70
+
71
+ col = 28
72
+ print(f" {'Model':<{col}} {'Calls':>6} {'In tok':>10} {'Out tok':>10} {'Cost ($)':>10} {'Avg ms':>8}")
73
+ print(" " + "─" * (col + 48))
74
+ total_cost = total_calls = 0
75
+ for model, st in stats.items():
76
+ avg_latency = st["latency_ms"] / st["calls"]
77
+ print(f" {model:<{col}} {st['calls']:>6,} {st['input_tokens']:>10,} {st['output_tokens']:>10,} {st['cost']:>10.4f} {avg_latency:>8.0f}")
78
+ total_cost += st["cost"]
79
+ total_calls += st["calls"]
80
+ print(" " + "─" * (col + 48))
81
+ print(f" {'TOTAL':<{col}} {total_calls:>6,}")
82
+ print()
83
+
84
+
85
+ def summary(db_path: str, project: str | None = None) -> None:
86
+ """Gives scripts and the CLI one callable for the full cost/token report."""
87
+ runs, spans = _fetch_rows(db_path, project)
88
+ label = f" for project '{project}'" if project else ""
89
+ print(f"\nDriftCast summary{label} — {db_path}")
90
+ _print_by_run(runs)
91
+ _print_by_model(spans)
92
+
93
+
94
+ def main() -> None:
95
+ """Console-script entry point: routes `driftcast <command>` to the right tool with lazy optional-extra imports."""
96
+ parser = argparse.ArgumentParser(prog="driftcast")
97
+ sub = parser.add_subparsers(dest="command", required=True)
98
+
99
+ summary_parser = sub.add_parser("summary", help="Aggregate cost/token report")
100
+ summary_parser.add_argument("--db", default=None,
101
+ help="Path to the SQLite db (default: the universal db via default_db_path())")
102
+ summary_parser.add_argument("--project", default=None, help="Filter by project name")
103
+
104
+ dash_parser = sub.add_parser("dashboard", help="Live web dashboard (needs the 'dashboard' extra)")
105
+ dash_parser.add_argument("--db", default=None,
106
+ help="Path to the SQLite db (default: the universal db via default_db_path())")
107
+ dash_parser.add_argument("--project", default=None, help="Filter by project name")
108
+ dash_parser.add_argument("--port", type=int, default=7861, help="Port to serve on")
109
+ dash_parser.add_argument("--refresh", type=float, default=2.0, help="Auto-refresh interval (seconds)")
110
+
111
+ args = parser.parse_args()
112
+ if args.command == "summary":
113
+ summary(args.db or default_db_path(), args.project)
114
+ elif args.command == "dashboard":
115
+ try:
116
+ from . import dashboard
117
+ except ImportError:
118
+ raise SystemExit(
119
+ "The dashboard needs Gradio. Install it with: pip install \"driftcast[dashboard]\""
120
+ )
121
+ dashboard.launch(
122
+ db_path=args.db or default_db_path(), project=args.project, port=args.port,
123
+ refresh_seconds=args.refresh, block=True,
124
+ )
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()