Skip to content

Commit 0c36427

Browse files
adridaclaude
andcommitted
feat: 0.3.0: tracer.watch, @tracer/watch (JS/TS), and the tracer cloud CLI
- tracer.watch: one-line observability for the LLM calls your pipeline already makes, recorded on the OpenTelemetry GenAI schema, with local, OTLP, and free Tracer Cloud sinks. The same trace object feeds tracer.fit() and tracer.scan(). - @tracer/watch: a zero-dependency JavaScript/TypeScript watcher with the same one-line API, async-context aware. - tracer cloud: a full command-line interface at parity with the dashboard: login, tracers (create, quick and bulk uploads, agentic onboard), training (retrain, auto-retrain, promote, live status), routing, test batteries, the model library, API keys, observability ingest keys, billing, analytics, trace selection, and scan. - Lazy package initialization so import tracer stays fast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c89d06c commit 0c36427

34 files changed

Lines changed: 6766 additions & 67 deletions

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ notebooks/data/
3232
# Editors
3333
.idea/
3434
.vscode/
35+
36+
# Node (JS package)
37+
node_modules/

‎CHANGELOG.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@
33
All notable changes to TRACER are recorded here. This project follows semantic
44
versioning.
55

6+
## 0.3.0 (2026-06)
7+
8+
### Added
9+
- `tracer.watch`: a decorator (and HTTP fallback) that observes the LLM calls
10+
your pipeline already makes and ships them, free, to Tracer Cloud, where they
11+
accumulate and auto-optimize once there are enough. Captures the full request
12+
and response on the OpenTelemetry GenAI (`gen_ai.*`) schema, with pluggable
13+
sinks (local file, Tracer Cloud, OTLP, or several at once).
14+
- `@tracer/watch`: a zero-dependency JavaScript/TypeScript mirror of the watch
15+
decorator (async-context aware), so JS pipelines get the same one-line
16+
observability as Python.
17+
- `tracer cloud`: a full command-line interface for Tracer Cloud, at parity with
18+
the dashboard. Browser or password login, then manage tracers (create, quick
19+
and bulk uploads, agentic `onboard`, rename, delete), training (retrain,
20+
auto-retrain, promote/rollback, live status), routing (`route`), test
21+
batteries, the model library, API keys, observability ingest keys, billing,
22+
analytics, trace selection (`traces`), and a public `scan`.
23+
- Lazy package initialization so `import tracer` stays fast and only pulls in the
24+
heavy pieces when you actually use them.
25+
26+
### Changed
27+
- `tracer scan` is more robust on messy uploads: tolerant input/label aliasing
28+
and a clarification path so ambiguous files still produce a result.
29+
30+
### Docs
31+
- New guides for the watch decorator and the `tracer cloud` CLI.
32+
633
## 0.2.0 (2026-06)
734

835
### Added

‎README.md‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
1212
[![CI](https://img.shields.io/badge/CI-passing-brightgreen)](https://github.com/adrida/tracer/actions)
1313
[![Website](https://img.shields.io/badge/website-tracerml.ai-blue)](https://tracerml.ai)
14-
[![Docs](https://img.shields.io/badge/docs-docs.tracer.deeprecall.io-blue)](https://docs.tracer.deeprecall.io)
14+
[![Docs](https://img.shields.io/badge/docs-reference-blue)](docs/)
1515

1616
Most LLM-based classification pipelines use a large language model for every single input. In practice, the vast majority of that traffic is predictable - a lightweight traditional ML model (logistic regression, gradient-boosted trees, or a small neural net) can match the LLM's output with near-perfect agreement.
1717

@@ -63,7 +63,35 @@ out = router.predict("What is my balance?")
6363
out = router.predict("Some edge case", fallback=lambda: call_my_llm(text))
6464
```
6565

66-
> **Want to go deeper?** The [concepts guide](docs/concepts.md) explains the full pipeline, model zoo, and parity gate. The [API reference](docs/api.md) covers every parameter. The [CLI reference](docs/cli.md) covers `tracer fit`, `tracer serve`, and more.
66+
> **Want to go deeper?** The [concepts guide](docs/concepts.md) explains the full pipeline, model zoo, and parity gate. The [API reference](docs/api.md) covers every parameter. The [CLI reference](docs/cli.md) covers `tracer fit`, `tracer serve`, and more. For observability see [watch](docs/watch.md), and to drive Tracer Cloud from your shell see the [`tracer cloud` reference](docs/cloud.md).
67+
68+
## Watch your LLM traffic (free observability)
69+
70+
Before you fit anything, just *watch*. Wrap any LLM call and every request is
71+
recorded locally as an OpenTelemetry GenAI span, no account, no key, nothing
72+
leaves your machine:
73+
74+
```python
75+
import tracer
76+
77+
watch = tracer.watch("support_classifier", system="my-provider", model="my-model")
78+
79+
@watch
80+
def classify(ticket: str) -> str:
81+
return call_my_llm(ticket) # traces append to .tracer/watch/*.jsonl
82+
```
83+
84+
Want them in a dashboard? Tracer Cloud observability is **free**. Mint a key
85+
(`tracer cloud ingest-keys create`, or the Watch page in the app) and set one
86+
env var, your traffic streams in within seconds, prod-safe (batched, never adds
87+
latency or throws):
88+
89+
```bash
90+
export TRACER_CLOUD_KEY=trobs_... # or watch(..., cloud_key="trobs_...")
91+
```
92+
93+
The same watched spans map 1:1 to `TraceRecord`, so once you have traffic you can
94+
`tracer fit` a router from it. Full guide: [docs/watch.md](docs/watch.md).
6795

6896
## Using from JavaScript / Node.js
6997

@@ -196,7 +224,7 @@ pip install tracer-llm[all] # everything
196224
## Paper
197225

198226
**TRACER: Trace-Based Adaptive Cost-Efficient Routing for LLM Classification**
199-
Adam Rida — arXiv 2026
227+
Adam Rida, arXiv 2026
200228

201229
[![arXiv](https://img.shields.io/badge/arXiv-2604.14531-b31b1b.svg)](https://arxiv.org/abs/2604.14531) [![Hugging Face](https://img.shields.io/badge/🤗%20HF-Papers-yellow)](https://huggingface.co/papers/2604.14531)
202230

‎docs/api.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@ from tracer import Embedder
216216
# sentence-transformers
217217
embedder = Embedder.from_sentence_transformers("BAAI/bge-small-en-v1.5")
218218

219-
# OpenAI-compatible endpoint
219+
# any standard embeddings endpoint
220220
embedder = Embedder.from_endpoint(
221-
"https://api.openai.com/v1/embeddings",
221+
"https://your-host/v1/embeddings",
222222
headers={"Authorization": "Bearer sk-..."},
223223
input_key="input",
224224
output_key="data.0.embedding",
@@ -254,7 +254,7 @@ router.predict(
254254
{
255255
"label": str, # predicted class label
256256
"decision": str, # "handled" or "deferred"
257-
"accept_score": float, # acceptor confidence (0–1)
257+
"accept_score": float, # acceptor confidence (0-1)
258258
"stage": int, # which pipeline stage handled it
259259
}
260260
```

‎docs/artifacts.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ All candidate pipelines evaluated during fit, for every target TA in `frontier_t
8888
]
8989
```
9090

91-
Use this to understand the coverage–quality tradeoff before committing to a target:
91+
Use this to understand the coverage-quality tradeoff before committing to a target:
9292

9393
```python
9494
import json
@@ -229,6 +229,6 @@ The `.tracer/` directory is fully self-contained. You can:
229229
- Archive it with the traces for audit/reproducibility
230230

231231
Typical size:
232-
- `pipeline.joblib`: 1–50 MB depending on model and label count
232+
- `pipeline.joblib`: 1-50 MB depending on model and label count
233233
- `index.embeddings.npy`: `n_traces × dim × 4` bytes (e.g. 10k × 1024 → 40 MB)
234234
- Everything else: < 1 MB

‎docs/cli.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ sentence-transformers** (`pip install tracer-llm[embeddings]`; pick the model wi
9898
- call **your own embedding service** with `--embed-url` (any HTTP endpoint that
9999
takes JSON and returns a vector; pass auth with `--embed-header`, and the
100100
`--embed-input-key`/`--embed-output-key`/`--embed-batch-key` flags adapt it to
101-
OpenAI-compatible or custom response shapes). No vendor is assumed or required.
101+
standard or custom response shapes). No vendor is assumed or required.
102102

103103
**Examples:**
104104

@@ -142,8 +142,8 @@ tracer fit <traces> [--artifact-dir <dir>] [--target <float>] [--trees] [--skip
142142
|----------|---------|-------------|
143143
| `traces` | required | Path to traces JSONL file |
144144
| `--artifact-dir` | `.tracer` | Where to save artifacts |
145-
| `--target` | `0.90` | Target teacher agreement (0.0–1.0) |
146-
| `--trees` | off | Add the tree-based surrogates (decision tree, random forest, extra-trees, gradient boosting). **Off by default** — they are slower and the linear + MLP heads are usually enough; turn them on for hard, high-class-count tasks. |
145+
| `--target` | `0.90` | Target teacher agreement (0.0-1.0) |
146+
| `--trees` | off | Add the tree-based surrogates (decision tree, random forest, extra-trees, gradient boosting). **Off by default**, they are slower and the linear + MLP heads are usually enough; turn them on for hard, high-class-count tasks. |
147147
| `--skip` | none | Comma-separated surrogate models to drop from the zoo (e.g. `mlp_1h,mlp_2h`). |
148148

149149
The default zoo is lightweight (logistic regression, SGD, small MLPs) so a fit is fast. `--trees` adds the heavier tree models, which can lift coverage on difficult datasets (e.g. Banking77's 77 classes) at the cost of fit time.

‎docs/cloud.md‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# `tracer cloud`: drive Tracer Cloud from the terminal
2+
3+
`tracer cloud` mirrors the Tracer Cloud web app from your shell: log in once,
4+
then create and train tracers, run live routing queries, manage tests, models,
5+
observability keys, billing, and more. It talks to the same API the dashboard
6+
uses, as you.
7+
8+
```bash
9+
pip install tracer-llm
10+
tracer cloud login # opens your browser, pick your account
11+
tracer cloud tracers # list your tracers
12+
```
13+
14+
## Auth
15+
16+
`tracer cloud login` runs a browser device flow: it opens a page where you pick
17+
your account and approve, then hands the session back to the CLI on a localhost
18+
callback. The session is stored only on this machine in `~/.tracer/cloud.json`
19+
and refreshed automatically. Sign out with `tracer cloud logout`.
20+
21+
Headless / CI: pass credentials to skip the browser.
22+
23+
```bash
24+
tracer cloud login --email you@example.com --password "$TRACER_PW"
25+
```
26+
27+
Point at a different deployment or a local dev server with `--url` (or the
28+
`TRACER_CLOUD_URL` env var). The package hardcodes no infrastructure details;
29+
the client config is fetched from `<url>/api/cli/config` at login.
30+
31+
## Commands
32+
33+
### Account & workspaces
34+
```bash
35+
tracer cloud whoami # the logged-in user + workspaces
36+
tracer cloud tenants # list workspaces
37+
tracer cloud tenant-delete <ref> --confirm "<name>" # delete an EMPTY company workspace
38+
```
39+
40+
### Tracers
41+
```bash
42+
tracer cloud tracers # list
43+
tracer cloud get <id|slug> # a tracer + its versions
44+
tracer cloud rename <id|slug> "New name"
45+
tracer cloud delete <id|slug> [--confirm <name>]
46+
47+
# create + train
48+
tracer cloud create "support router" --mode quick \
49+
--file traces.jsonl --teacher my-provider:my-model \
50+
--label-space "billing,fraud,general" --wait
51+
tracer cloud create "watch mode" --mode auto --teacher my-provider:my-model --threshold 200
52+
```
53+
`--mode quick` trains on a labelled upload, `unlabelled` lets the teacher label
54+
it, `auto` starts in watch mode (no data needed). `--wait` blocks until training
55+
finishes. `--teacher` takes `provider:model_id` or a JSON object.
56+
57+
### Training, retraining, promotion
58+
```bash
59+
tracer cloud training <id> # job status
60+
tracer cloud training-cancel <id>
61+
tracer cloud retrain <id> --mode reuse|merge|replace [--trace-ids a,b,c] [--wait]
62+
tracer cloud auto-retrain <id> --threshold 500 # or --disable
63+
tracer cloud promote <id> [--version <version_id>] # promote (or roll back)
64+
```
65+
66+
### Live routing
67+
```bash
68+
tracer cloud route <id> "my card was declined" # the dashboard's Live Query
69+
tracer cloud route <id> "..." --lite # skip the teacher call
70+
tracer cloud route <id> "..." --show-exemplars
71+
```
72+
73+
### Tests (batteries)
74+
```bash
75+
tracer cloud batteries list <id>
76+
tracer cloud batteries create <id> --name "smoke" --cases cases.json
77+
tracer cloud batteries run <id> --battery <battery_id> [--version <id>]
78+
```
79+
80+
### Models
81+
```bash
82+
tracer cloud models list
83+
tracer cloud models add --name my-llm --endpoint https://… --upstream model-id \
84+
--api-key "$KEY" --kind llm --cost-in 0.15 --cost-out 0.6
85+
tracer cloud models delete --id <model_id>
86+
```
87+
88+
### Observability
89+
```bash
90+
tracer cloud ingest-keys create --name "my app" # mint a workspace ingest key
91+
tracer cloud ingest-keys list
92+
tracer cloud ingest-keys revoke --id <key_id>
93+
tracer cloud ingest --key trobs_… --events events.json # send observed events
94+
```
95+
Most teams stream live traffic with [`tracer.watch`](watch.md) instead of
96+
posting batches by hand.
97+
98+
### Keys, analytics, billing, scan, label space
99+
```bash
100+
tracer cloud keys list|create|revoke <id> # per-tracer gateway API keys
101+
tracer cloud analytics <id> --range 24h|7d|30d
102+
tracer cloud label-space <id> [--set a,b,c] [--strict]
103+
tracer cloud billing [--tenant <ref>]
104+
tracer cloud scan path/to/traces.jsonl # public scan
105+
```
106+
107+
Add `--json` to any read command for raw JSON output.
108+
109+
## Environment variables
110+
111+
| Var | Effect |
112+
|-----|--------|
113+
| `TRACER_CLOUD_URL` | Tracer Cloud base URL (default `https://app.tracerml.ai`) |
114+
| `TRACER_TENANT` | default workspace id/slug/name when a command needs one |
115+
| `TRACER_CLOUD_CONFIG` | path to the stored session (default `~/.tracer/cloud.json`) |

‎docs/cold_start.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ Getting good results from TRACER requires enough traces to train a reliable surr
66

77
## How many traces do you need?
88

9-
There is no hard minimum — TRACER will attempt to fit on whatever you provide. But the quality of the routing policy depends directly on how much data the surrogate and acceptor have to learn from.
9+
There is no hard minimum, TRACER will attempt to fit on whatever you provide. But the quality of the routing policy depends directly on how much data the surrogate and acceptor have to learn from.
1010

1111
**Rules of thumb:**
1212

1313
| Trace volume | What to expect |
1414
|---|---|
1515
| **< 50 total** | Fit will likely produce `selected_method: null` (no deployable policy). The train/val/cal split leaves too few samples per class for reliable calibration. |
16-
| **50–200** | A global surrogate _may_ deploy if the classification task is simple (few classes, clean teacher labels). L2D and RSB are unlikely to calibrate well. |
17-
| **200–500** | L2D starts becoming viable. Expect moderate coverage (40–70%) depending on class count and task difficulty. |
18-
| **500–1,500** | The sweet spot for most tasks. All three pipeline families compete meaningfully, and coverage typically reaches 70–90%+. |
16+
| **50-200** | A global surrogate _may_ deploy if the classification task is simple (few classes, clean teacher labels). L2D and RSB are unlikely to calibrate well. |
17+
| **200-500** | L2D starts becoming viable. Expect moderate coverage (40-70%) depending on class count and task difficulty. |
18+
| **500-1,500** | The sweet spot for most tasks. All three pipeline families compete meaningfully, and coverage typically reaches 70-90%+. |
1919
| **1,500+** | Diminishing returns on coverage, but useful for high-class-count tasks (50+ intents) or when you need very tight parity bars (≥ 0.98). |
2020

2121
These numbers assume reasonably separable embeddings. Poor embeddings can make even large datasets underperform.
@@ -41,7 +41,7 @@ If you're evaluating TRACER for a new classification pipeline and don't yet have
4141

4242
### Step 1: Seed with synthetic or sampled inputs
4343

44-
Pick 100–300 representative inputs for your classification task. These can come from:
44+
Pick 100-300 representative inputs for your classification task. These can come from:
4545

4646
- A test set you already have
4747
- Manually written examples covering each class
@@ -127,16 +127,16 @@ config = tracer.FitConfig(
127127

128128
If `best_ta` is well below your target even at zero coverage, the problem is likely:
129129

130-
1. **Embeddings don't separate classes** — try a stronger embedding model
131-
2. **Teacher label noise** — audit traces where the surrogate disagrees
132-
3. **Too few traces per class** — collect more data for underrepresented classes
130+
1. **Embeddings don't separate classes**, try a stronger embedding model
131+
2. **Teacher label noise**, audit traces where the surrogate disagrees
132+
3. **Too few traces per class**, collect more data for underrepresented classes
133133

134134
### Low coverage despite many traces
135135

136136
If coverage plateaus well below 80% with 500+ traces:
137137

138138
- Check class imbalance: a few dominant classes might be well-covered while rare classes drag the average down
139-
- Try the RSB pipeline family explicitly — it's designed for multi-stage residual routing
139+
- Try the RSB pipeline family explicitly, it's designed for multi-stage residual routing
140140
- Improve embedding quality: switching from `all-MiniLM-L6-v2` to `all-mpnet-base-v2` can significantly improve class separation
141141

142142
### Coverage varies between fits

‎docs/javascript.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Each line must have `input` (the text) and `teacher` (the label your LLM returne
2929

3030
### 2. Compute embeddings (offline, once before fit)
3131

32-
The HTTP server at inference time expects embedding vectors, so you need to embed your traces before fitting. Use the same embedding model you plan to use at inference time — this is the only constraint.
32+
The HTTP server at inference time expects embedding vectors, so you need to embed your traces before fitting. Use the same embedding model you plan to use at inference time, this is the only constraint.
3333

3434
**Option A: OpenAI embeddings (natural fit for JS pipelines)**
3535

@@ -38,7 +38,7 @@ pip install tracer-llm openai numpy
3838
```
3939

4040
```python
41-
# embed.py — run once, or in your data pipeline
41+
# embed.py: run once, or in your data pipeline
4242
import json, numpy as np
4343
from openai import OpenAI
4444

@@ -71,7 +71,7 @@ np.save("traces.npy", X)
7171
tracer fit traces.jsonl --target 0.95
7272
```
7373

74-
TRACER reads `traces.jsonl` and auto-discovers `traces.npy` (same stem, `.npy` extension). Run this offline — in a cron job, a GitHub Action, or manually. It does not touch your application.
74+
TRACER reads `traces.jsonl` and auto-discovers `traces.npy` (same stem, `.npy` extension). Run this offline, in a cron job, a GitHub Action, or manually. It does not touch your application.
7575

7676
---
7777

@@ -81,7 +81,7 @@ TRACER reads `traces.jsonl` and auto-discovers `traces.npy` (same stem, `.npy` e
8181
tracer serve .tracer --port 8000
8282
```
8383

84-
Run this as a sidecar next to your Node app — same server, same docker-compose, same machine. It binds to `0.0.0.0:8000` by default.
84+
Run this as a sidecar next to your Node app, same server, same docker-compose, same machine. It binds to `0.0.0.0:8000` by default.
8585

8686
---
8787

@@ -111,7 +111,7 @@ async function route(text) {
111111
const { label, decision } = await res.json()
112112

113113
if (decision === 'handled') {
114-
return label // surrogate answered — no LLM call
114+
return label // surrogate answered, no LLM call
115115
}
116116

117117
// Deferred: call your LLM and log the new trace for the next refit
@@ -132,7 +132,7 @@ const res = await fetch('http://localhost:8000/predict_batch', {
132132
body: JSON.stringify({ embeddings: [embedding1, embedding2, ...] }),
133133
})
134134
const { labels, decisions, handled } = await res.json()
135-
// handled: boolean[] — true means surrogate answered, no LLM needed
135+
// handled: boolean[]: true means surrogate answered, no LLM needed
136136
```
137137

138138
---
@@ -141,7 +141,7 @@ const { labels, decisions, handled } = await res.json()
141141

142142
| Method | Path | Body | Response |
143143
|--------|------|------|----------|
144-
| `GET` | `/health` | — | `{"status": "ok", "method", "coverage", ...}` |
144+
| `GET` | `/health` |, | `{"status": "ok", "method", "coverage", ...}` |
145145
| `POST` | `/predict` | `{"embedding": [float, ...]}` | `{"label", "decision", "accept_score"}` |
146146
| `POST` | `/predict_batch` | `{"embeddings": [[float, ...], ...]}` | `{"labels", "decisions", "handled"}` |
147147

@@ -151,7 +151,7 @@ const { labels, decisions, handled } = await res.json()
151151

152152
## Continual learning
153153

154-
Every deferred input that reaches your LLM is a new trace. Accumulate them and retrain periodically — coverage grows with each refit.
154+
Every deferred input that reaches your LLM is a new trace. Accumulate them and retrain periodically, coverage grows with each refit.
155155

156156
```bash
157157
# Run on a schedule (cron, GitHub Action, whatever)

0 commit comments

Comments
 (0)