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.
- driftcast-1.0.0/LICENSE +21 -0
- driftcast-1.0.0/PKG-INFO +163 -0
- driftcast-1.0.0/README.md +138 -0
- driftcast-1.0.0/pyproject.toml +42 -0
- driftcast-1.0.0/setup.cfg +4 -0
- driftcast-1.0.0/src/driftcast/__init__.py +17 -0
- driftcast-1.0.0/src/driftcast/cli.py +128 -0
- driftcast-1.0.0/src/driftcast/dashboard.py +329 -0
- driftcast-1.0.0/src/driftcast/otel/__init__.py +6 -0
- driftcast-1.0.0/src/driftcast/otel/claude_code_tel.py +591 -0
- driftcast-1.0.0/src/driftcast/pricing.py +83 -0
- driftcast-1.0.0/src/driftcast/py.typed +0 -0
- driftcast-1.0.0/src/driftcast/storage.py +392 -0
- driftcast-1.0.0/src/driftcast/tracer.py +460 -0
- driftcast-1.0.0/src/driftcast.egg-info/PKG-INFO +163 -0
- driftcast-1.0.0/src/driftcast.egg-info/SOURCES.txt +29 -0
- driftcast-1.0.0/src/driftcast.egg-info/dependency_links.txt +1 -0
- driftcast-1.0.0/src/driftcast.egg-info/entry_points.txt +3 -0
- driftcast-1.0.0/src/driftcast.egg-info/requires.txt +3 -0
- driftcast-1.0.0/src/driftcast.egg-info/top_level.txt +1 -0
- driftcast-1.0.0/tests/test_abandoned_runs.py +153 -0
- driftcast-1.0.0/tests/test_cli.py +19 -0
- driftcast-1.0.0/tests/test_comment_policy.py +30 -0
- driftcast-1.0.0/tests/test_dashboard_teaser.py +10 -0
- driftcast-1.0.0/tests/test_equivalence.py +242 -0
- driftcast-1.0.0/tests/test_free_boundary.py +27 -0
- driftcast-1.0.0/tests/test_off_loop_writes.py +207 -0
- driftcast-1.0.0/tests/test_otel_receiver.py +164 -0
- driftcast-1.0.0/tests/test_packaging.py +16 -0
- driftcast-1.0.0/tests/test_storage.py +27 -0
- driftcast-1.0.0/tests/test_tracer.py +503 -0
driftcast-1.0.0/LICENSE
ADDED
|
@@ -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.
|
driftcast-1.0.0/PKG-INFO
ADDED
|
@@ -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,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()
|