toad-eye 2.2.0 β 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -152
- package/dist/agent.d.ts +19 -28
- package/dist/agent.js +46 -27
- package/dist/alerts/channels.js +24 -6
- package/dist/alerts/index.js +46 -2
- package/dist/alerts/manager.d.ts +1 -0
- package/dist/alerts/manager.js +36 -21
- package/dist/budget/tracker.d.ts +16 -6
- package/dist/budget/tracker.js +38 -17
- package/dist/budget/types.d.ts +2 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +106 -54
- package/dist/core/metrics.d.ts +4 -0
- package/dist/core/metrics.js +19 -0
- package/dist/core/pricing.d.ts +2 -0
- package/dist/core/pricing.js +4 -0
- package/dist/core/spans.d.ts +5 -3
- package/dist/core/spans.js +79 -20
- package/dist/core/tracer.d.ts +0 -3
- package/dist/core/tracer.js +40 -7
- package/dist/drift/baseline.js +7 -1
- package/dist/drift/monitor.js +6 -6
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/instrumentations/anthropic.d.ts +1 -1
- package/dist/instrumentations/anthropic.js +2 -3
- package/dist/instrumentations/create.js +80 -19
- package/dist/instrumentations/gemini.d.ts +1 -1
- package/dist/instrumentations/gemini.js +6 -7
- package/dist/instrumentations/openai.d.ts +1 -1
- package/dist/instrumentations/openai.js +2 -3
- package/dist/instrumentations/registry.d.ts +3 -1
- package/dist/instrumentations/registry.js +28 -5
- package/dist/instrumentations/types.d.ts +4 -2
- package/dist/types/attributes.d.ts +3 -0
- package/dist/types/attributes.js +3 -0
- package/dist/types/config.d.ts +22 -2
- package/dist/types/index.d.ts +3 -3
- package/dist/types/metrics.d.ts +2 -0
- package/dist/types/metrics.js +3 -0
- package/dist/types/providers.d.ts +6 -1
- package/dist/types/spans.d.ts +11 -2
- package/dist/vercel.d.ts +53 -0
- package/dist/vercel.js +106 -0
- package/package.json +29 -3
- package/templates/grafana/dashboards/agent-workflow.json +207 -0
- package/templates/otel-collector.yml +25 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# toad-eye
|
|
1
|
+
# toad-eye πΈποΈβπ¨οΈπ€
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|

|
|
11
11
|

|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**LLM observability in 3 commands. Auto-instrument, trace, alert, save money.**
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
You don't know what your LLMs cost, how slow they are, or why they fail. toad-eye gives you complete visibility with zero manual instrumentation.
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
@@ -25,48 +25,57 @@ npx toad-eye up # start Grafana + Prometheus + Jaeger
|
|
|
25
25
|
npx toad-eye demo # send mock LLM traffic, see data immediately
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your dashboards.
|
|
29
|
-
|
|
30
|
-
### Cloud mode
|
|
31
|
-
|
|
32
|
-
No Docker needed. Send telemetry to toad-eye cloud with one line:
|
|
33
|
-
|
|
34
28
|
```typescript
|
|
29
|
+
import { initObservability } from "toad-eye";
|
|
30
|
+
|
|
35
31
|
initObservability({
|
|
36
32
|
serviceName: "my-app",
|
|
37
|
-
|
|
38
|
-
instrument: ["openai"],
|
|
33
|
+
instrument: ["openai", "anthropic"],
|
|
39
34
|
});
|
|
35
|
+
|
|
36
|
+
// That's it. Every SDK call is auto-traced β including streaming.
|
|
40
37
|
```
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your dashboards.
|
|
43
40
|
|
|
44
|
-
##
|
|
41
|
+
## What you get
|
|
42
|
+
|
|
43
|
+
| Feature | Description |
|
|
44
|
+
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
45
|
+
| **Auto-instrumentation** | OpenAI, Anthropic, Gemini, Vercel AI SDK β regular + streaming |
|
|
46
|
+
| **8 Grafana dashboards** | Overview, Cost, Latency, Errors, Model Comparison, FinOps, Provider Health, Agent Workflow |
|
|
47
|
+
| **Cost tracking** | Per-request USD cost, daily totals, projected monthly spend |
|
|
48
|
+
| **Budget guards** | Daily/per-user/per-model spend limits β warn, block, or auto-downgrade |
|
|
49
|
+
| **Multi-agent tracing** | ReAct steps, handoffs, loop detection, maxSteps guard |
|
|
50
|
+
| **Shadow guardrails** | Record what _would_ be blocked without blocking β tune thresholds on live traffic |
|
|
51
|
+
| **Quality proxy** | Empty response rate, latency per token β zero-dependency quality metrics |
|
|
52
|
+
| **Semantic drift** | Detect silent quality degradation via embedding comparison |
|
|
53
|
+
| **Privacy controls** | Built-in PII redaction (email, SSN, CC, phone), hashing, content masking |
|
|
54
|
+
| **Tail sampling** | Keep 100% errors + slow traces, sample healthy traffic via OTel Collector |
|
|
55
|
+
| **Alerting** | Cost spikes, latency anomalies, error rate β via Telegram, Slack, email, webhook |
|
|
56
|
+
| **FinOps attribution** | Break down costs by team, user, feature, environment |
|
|
57
|
+
| **TTFT metric** | Time To First Token for streaming β separate from total duration |
|
|
58
|
+
| **Trace export** | Convert production Jaeger traces into regression test cases |
|
|
59
|
+
|
|
60
|
+
## Auto-instrumentation π€
|
|
45
61
|
|
|
46
62
|
Zero-code instrumentation for popular LLM SDKs. No wrappers needed.
|
|
47
63
|
|
|
48
64
|
```typescript
|
|
49
|
-
import { initObservability } from "toad-eye";
|
|
50
|
-
|
|
51
65
|
initObservability({
|
|
52
66
|
serviceName: "my-app",
|
|
53
67
|
instrument: ["openai", "anthropic"],
|
|
54
68
|
});
|
|
55
|
-
|
|
56
|
-
// That's it. Every SDK call is auto-traced.
|
|
57
|
-
const result = await openai.chat.completions.create({
|
|
58
|
-
model: "gpt-4o",
|
|
59
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
60
|
-
});
|
|
61
69
|
```
|
|
62
70
|
|
|
63
|
-
**Supported SDKs:** OpenAI (`openai`), Anthropic (`@anthropic-ai/sdk`), Google GenAI (`@google/generative-ai`)
|
|
71
|
+
**Supported SDKs:** OpenAI (`openai`), Anthropic (`@anthropic-ai/sdk`), Google GenAI (`@google/generative-ai`), Vercel AI SDK (`ai`)
|
|
64
72
|
|
|
65
73
|
Both regular and **streaming** calls are fully instrumented β spans, metrics, and cost tracking work transparently for `stream: true`.
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
Vercel AI SDK uses a SpanProcessor (not monkey-patching) β add `instrument: ['ai']` and `experimental_telemetry: withToadEye()` to your calls.
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
<details>
|
|
78
|
+
<summary>Manual instrumentation (custom providers)</summary>
|
|
70
79
|
|
|
71
80
|
```typescript
|
|
72
81
|
import { initObservability, traceLLMCall } from "toad-eye";
|
|
@@ -79,9 +88,33 @@ const result = await traceLLMCall(
|
|
|
79
88
|
);
|
|
80
89
|
```
|
|
81
90
|
|
|
91
|
+
</details>
|
|
92
|
+
|
|
93
|
+
## Budget guards
|
|
94
|
+
|
|
95
|
+
Prevent cost overruns at runtime. Three modes: warn, block, or auto-downgrade to a cheaper model.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
initObservability({
|
|
99
|
+
serviceName: "my-app",
|
|
100
|
+
budgets: {
|
|
101
|
+
daily: 50, // $50/day max
|
|
102
|
+
perUser: 5, // $5/day per user
|
|
103
|
+
perModel: { "gpt-4o": 30 }, // $30/day on GPT-4o
|
|
104
|
+
},
|
|
105
|
+
onBudgetExceeded: "block", // throws ToadBudgetExceededError
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Mode | Behavior |
|
|
110
|
+
| ----------- | --------------------------------------------------- |
|
|
111
|
+
| `warn` | Log warning, continue normally |
|
|
112
|
+
| `block` | Throw `ToadBudgetExceededError` before LLM call |
|
|
113
|
+
| `downgrade` | Call `downgradeCallback` to switch to cheaper model |
|
|
114
|
+
|
|
82
115
|
## Agent observability
|
|
83
116
|
|
|
84
|
-
Structured tracing for ReAct agents
|
|
117
|
+
Structured tracing for ReAct agents with multi-agent support, handoffs, and loop detection.
|
|
85
118
|
|
|
86
119
|
```typescript
|
|
87
120
|
import { traceAgentQuery } from "toad-eye";
|
|
@@ -111,17 +144,37 @@ const result = await traceAgentQuery(
|
|
|
111
144
|
);
|
|
112
145
|
```
|
|
113
146
|
|
|
114
|
-
|
|
147
|
+
## FinOps attribution
|
|
148
|
+
|
|
149
|
+
Track costs by team, user, feature, or any business dimension:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
initObservability({
|
|
153
|
+
serviceName: "my-app",
|
|
154
|
+
attributes: { team: "checkout", environment: "production" },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await traceLLMCall(
|
|
158
|
+
{
|
|
159
|
+
provider: "openai",
|
|
160
|
+
model: "gpt-4o",
|
|
161
|
+
prompt: "Summarize order",
|
|
162
|
+
attributes: { userId: "user-123", feature: "order-summary" },
|
|
163
|
+
},
|
|
164
|
+
() => callLLM(),
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
<details>
|
|
169
|
+
<summary>More features</summary>
|
|
115
170
|
|
|
116
|
-
|
|
171
|
+
### Shadow guardrails
|
|
117
172
|
|
|
118
|
-
|
|
173
|
+
Record what _would_ have been blocked without blocking. Tune thresholds on live traffic.
|
|
119
174
|
|
|
120
175
|
```typescript
|
|
121
176
|
import { recordGuardResult } from "toad-eye";
|
|
122
177
|
|
|
123
|
-
// toad-guard validates, toad-eye records
|
|
124
|
-
const result = guard.validate(response);
|
|
125
178
|
recordGuardResult({
|
|
126
179
|
mode: "shadow",
|
|
127
180
|
passed: false,
|
|
@@ -130,49 +183,20 @@ recordGuardResult({
|
|
|
130
183
|
});
|
|
131
184
|
```
|
|
132
185
|
|
|
133
|
-
|
|
186
|
+
### Semantic drift monitoring
|
|
134
187
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Detect silent LLM quality degradation by comparing current responses to a saved baseline via embeddings.
|
|
188
|
+
Detect silent LLM quality degradation by comparing responses to a saved baseline via embeddings.
|
|
138
189
|
|
|
139
190
|
```typescript
|
|
140
|
-
import { createDriftMonitor
|
|
191
|
+
import { createDriftMonitor } from "toad-eye";
|
|
141
192
|
|
|
142
193
|
const monitor = createDriftMonitor({
|
|
143
194
|
embedding: { provider: "openai", apiKey: process.env.OPENAI_API_KEY! },
|
|
144
195
|
baselinePath: "./baseline.json",
|
|
145
|
-
sampleRate: 10,
|
|
196
|
+
sampleRate: 10,
|
|
146
197
|
});
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
const drift = await monitor.check(response, "openai", "gpt-4o");
|
|
150
|
-
// drift = 0 β identical to baseline
|
|
151
|
-
// drift > 0.3 β significant deviation
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Trace-to-dataset export
|
|
155
|
-
|
|
156
|
-
Convert production Jaeger traces into test cases. Production failure becomes a regression test.
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
npx toad-eye export-trace <trace_id> --output ./evals/
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Generates YAML in toad-eval format with auto-generated assertions (max length, refusal detection, JSON validation).
|
|
163
|
-
|
|
164
|
-
## Features
|
|
165
|
-
|
|
166
|
-
### Built-in cost tracking
|
|
167
|
-
|
|
168
|
-
Automatic cost calculation for major models (GPT-4o, Claude, Gemini) based on token usage. Custom pricing for enterprise contracts:
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
import { setCustomPricing } from "toad-eye";
|
|
172
|
-
|
|
173
|
-
setCustomPricing({
|
|
174
|
-
"my-fine-tuned-model": { inputPer1M: 5, outputPer1M: 15 },
|
|
175
|
-
});
|
|
199
|
+
monitor.checkInBackground(response, "openai", "gpt-4o");
|
|
176
200
|
```
|
|
177
201
|
|
|
178
202
|
### Privacy controls
|
|
@@ -180,28 +204,36 @@ setCustomPricing({
|
|
|
180
204
|
```typescript
|
|
181
205
|
initObservability({
|
|
182
206
|
serviceName: "my-app",
|
|
207
|
+
redactDefaults: true, // built-in PII patterns (email, SSN, CC, phone)
|
|
183
208
|
recordContent: false, // disable prompt/completion recording
|
|
184
209
|
hashContent: true, // SHA-256 hash instead of plain text
|
|
185
|
-
|
|
210
|
+
salt: "my-secret", // prevent rainbow table attacks
|
|
211
|
+
auditMasking: true, // log what was masked (debug only)
|
|
212
|
+
redactPatterns: [/custom-pattern/g], // additional regex redaction
|
|
186
213
|
});
|
|
187
214
|
```
|
|
188
215
|
|
|
189
216
|
### Session tracking
|
|
190
217
|
|
|
191
|
-
Group traces by conversation session:
|
|
192
|
-
|
|
193
218
|
```typescript
|
|
194
219
|
initObservability({
|
|
195
220
|
serviceName: "my-app",
|
|
196
221
|
sessionId: "static-session-id",
|
|
197
|
-
// or dynamic
|
|
198
|
-
sessionExtractor: () => getCurrentSessionId(),
|
|
222
|
+
sessionExtractor: () => getCurrentSessionId(), // or dynamic
|
|
199
223
|
});
|
|
200
224
|
```
|
|
201
225
|
|
|
202
|
-
###
|
|
226
|
+
### Built-in cost tracking
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { setCustomPricing } from "toad-eye";
|
|
203
230
|
|
|
204
|
-
|
|
231
|
+
setCustomPricing({
|
|
232
|
+
"my-fine-tuned-model": { inputPer1M: 5, outputPer1M: 15 },
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Alerting
|
|
205
237
|
|
|
206
238
|
```yaml
|
|
207
239
|
alerts:
|
|
@@ -210,78 +242,52 @@ alerts:
|
|
|
210
242
|
condition: sum_1h > 10
|
|
211
243
|
channels: [slack]
|
|
212
244
|
|
|
213
|
-
- name: latency_anomaly
|
|
214
|
-
metric: gen_ai.client.operation.duration
|
|
215
|
-
condition: p95_pct_5m_7d > 50
|
|
216
|
-
channels: [slack]
|
|
217
|
-
|
|
218
245
|
- name: error_rate
|
|
219
246
|
metric: gen_ai.client.errors
|
|
220
247
|
condition: ratio_15m > 0.05
|
|
221
|
-
channels: [
|
|
248
|
+
channels: [telegram]
|
|
222
249
|
```
|
|
223
250
|
|
|
224
|
-
|
|
251
|
+
Channels: Telegram, Slack webhook, HTTP webhook, email (SMTP).
|
|
225
252
|
|
|
226
|
-
###
|
|
253
|
+
### Trace-to-dataset export
|
|
227
254
|
|
|
228
|
-
|
|
255
|
+
Convert production Jaeger traces into test cases:
|
|
229
256
|
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
serviceName: "my-app",
|
|
233
|
-
budgets: {
|
|
234
|
-
daily: 50, // $50/day max
|
|
235
|
-
perUser: 5, // $5/day per user
|
|
236
|
-
perModel: { "gpt-4o": 30 }, // $30/day on GPT-4o
|
|
237
|
-
},
|
|
238
|
-
onBudgetExceeded: "block", // throws ToadBudgetExceededError
|
|
239
|
-
});
|
|
257
|
+
```bash
|
|
258
|
+
npx toad-eye export-trace <trace_id> --output ./evals/
|
|
240
259
|
```
|
|
241
260
|
|
|
242
|
-
|
|
243
|
-
| ----------- | --------------------------------------------------- |
|
|
244
|
-
| `warn` | Log warning, continue normally |
|
|
245
|
-
| `block` | Throw `ToadBudgetExceededError` before LLM call |
|
|
246
|
-
| `downgrade` | Call `downgradeCallback` to switch to cheaper model |
|
|
247
|
-
|
|
248
|
-
## FinOps attribution
|
|
261
|
+
### Cloud mode (coming soon)
|
|
249
262
|
|
|
250
|
-
|
|
263
|
+
No Docker needed. Send telemetry to toad-eye cloud with one line:
|
|
251
264
|
|
|
252
265
|
```typescript
|
|
253
|
-
// Global attributes β applied to all spans/metrics
|
|
254
266
|
initObservability({
|
|
255
267
|
serviceName: "my-app",
|
|
256
|
-
|
|
268
|
+
apiKey: "toad_xxxxxxxx",
|
|
269
|
+
instrument: ["openai"],
|
|
257
270
|
});
|
|
258
|
-
|
|
259
|
-
// Per-request attributes β override or extend global
|
|
260
|
-
await traceLLMCall(
|
|
261
|
-
{
|
|
262
|
-
provider: "openai",
|
|
263
|
-
model: "gpt-4o",
|
|
264
|
-
prompt: "Summarize order",
|
|
265
|
-
attributes: { userId: "user-123", feature: "order-summary" },
|
|
266
|
-
},
|
|
267
|
-
() => callLLM(),
|
|
268
|
-
);
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
Self-hosted mode remains the default. Cloud mode activates automatically when `apiKey` is set.
|
|
274
|
+
|
|
275
|
+
</details>
|
|
272
276
|
|
|
273
|
-
|
|
277
|
+
## Grafana dashboards
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
| ---------------------- | --------------------------------------------------------- |
|
|
277
|
-
| **Overview** | Request rate, error rate, latency p50/p95, cost, totals |
|
|
278
|
-
| **Cost Breakdown** | Spend by provider/model, daily trend, projected monthly |
|
|
279
|
-
| **Latency Analysis** | p50/p95/p99 by model, distribution histogram |
|
|
280
|
-
| **Error Drill-down** | Error rate by provider/model, error vs success |
|
|
281
|
-
| **Model Comparison** | Latency vs cost vs error rate vs throughput per model |
|
|
282
|
-
| **FinOps Attribution** | Cost by team/user/feature, projected spend, what-if table |
|
|
279
|
+
8 pre-built dashboards auto-provisioned on `npx toad-eye init`:
|
|
283
280
|
|
|
284
|
-
|
|
281
|
+
| Dashboard | What it shows |
|
|
282
|
+
| ---------------------- | ---------------------------------------------------------------- |
|
|
283
|
+
| **Overview** | Request rate, error rate, latency p50/p95, cost, totals |
|
|
284
|
+
| **Cost Breakdown** | Spend by provider/model, daily trend, projected monthly |
|
|
285
|
+
| **Latency Analysis** | p50/p95/p99 by model, distribution histogram, TTFT |
|
|
286
|
+
| **Error Drill-down** | Error rate by provider/model, error vs success |
|
|
287
|
+
| **Model Comparison** | Latency vs cost vs error rate vs throughput per model |
|
|
288
|
+
| **FinOps Attribution** | Cost by team/user/feature, projected spend, what-if table |
|
|
289
|
+
| **Provider Health** | Provider status (healthy/degraded/down), uptime, error breakdown |
|
|
290
|
+
| **Agent Workflow** | Steps per query, tool usage frequency, step type breakdown |
|
|
285
291
|
|
|
286
292
|
## CLI
|
|
287
293
|
|
|
@@ -300,40 +306,48 @@ All dashboards have template variables for filtering (`$provider`, `$model`, `$t
|
|
|
300
306
|
|
|
301
307
|
## What it tracks
|
|
302
308
|
|
|
309
|
+
<details>
|
|
310
|
+
<summary>Metrics & span attributes</summary>
|
|
311
|
+
|
|
303
312
|
### Metrics (OTel GenAI semconv)
|
|
304
313
|
|
|
305
|
-
| Metric
|
|
306
|
-
|
|
|
307
|
-
| `gen_ai.client.operation.duration`
|
|
308
|
-
| `gen_ai.client.
|
|
309
|
-
| `gen_ai.client.
|
|
310
|
-
| `gen_ai.client.
|
|
311
|
-
| `gen_ai.client.
|
|
312
|
-
| `gen_ai.
|
|
313
|
-
| `gen_ai.agent.
|
|
314
|
-
| `gen_ai.
|
|
315
|
-
| `gen_ai.toad_eye.guard.
|
|
316
|
-
| `gen_ai.toad_eye.
|
|
317
|
-
| `gen_ai.toad_eye.
|
|
318
|
-
| `gen_ai.toad_eye.budget.
|
|
319
|
-
| `gen_ai.toad_eye.budget.
|
|
320
|
-
|
|
321
|
-
|
|
314
|
+
| Metric | Type | Description |
|
|
315
|
+
| -------------------------------------------- | --------- | --------------------------- |
|
|
316
|
+
| `gen_ai.client.operation.duration` | Histogram | Request latency (ms) |
|
|
317
|
+
| `gen_ai.client.time_to_first_token` | Histogram | TTFT for streaming (ms) |
|
|
318
|
+
| `gen_ai.client.request.cost` | Histogram | Cost per request (USD) |
|
|
319
|
+
| `gen_ai.client.token.usage` | Counter | Total tokens consumed |
|
|
320
|
+
| `gen_ai.client.requests` | Counter | Total requests |
|
|
321
|
+
| `gen_ai.client.errors` | Counter | Total failed requests |
|
|
322
|
+
| `gen_ai.agent.steps_per_query` | Histogram | Agent steps per query |
|
|
323
|
+
| `gen_ai.agent.tool_usage` | Counter | Agent tool invocations |
|
|
324
|
+
| `gen_ai.toad_eye.guard.evaluations` | Counter | Guard evaluations per rule |
|
|
325
|
+
| `gen_ai.toad_eye.guard.would_block` | Counter | Would-have-blocked per rule |
|
|
326
|
+
| `gen_ai.toad_eye.semantic_drift` | Histogram | Drift from baseline (0..1) |
|
|
327
|
+
| `gen_ai.toad_eye.budget.exceeded` | Counter | Budget exceeded events |
|
|
328
|
+
| `gen_ai.toad_eye.budget.blocked` | Counter | LLM calls blocked by budget |
|
|
329
|
+
| `gen_ai.toad_eye.budget.downgraded` | Counter | LLM calls downgraded |
|
|
330
|
+
| `gen_ai.toad_eye.response.empty` | Counter | Empty/whitespace responses |
|
|
331
|
+
| `gen_ai.toad_eye.response.latency_per_token` | Histogram | Generation speed (ms/token) |
|
|
322
332
|
|
|
323
333
|
### Span attributes
|
|
324
334
|
|
|
325
|
-
| Attribute | Description
|
|
326
|
-
| ---------------------------- |
|
|
327
|
-
| `gen_ai.provider.name` | `anthropic`, `gemini`, `openai`
|
|
328
|
-
| `gen_ai.request.model` | Model identifier
|
|
329
|
-
| `gen_ai.usage.input_tokens` | Tokens in the prompt
|
|
330
|
-
| `gen_ai.usage.output_tokens` | Tokens in the completion
|
|
331
|
-
| `gen_ai.request.temperature` | Temperature parameter
|
|
332
|
-
| `gen_ai.toad_eye.cost` | Cost in USD
|
|
333
|
-
| `gen_ai.agent.step.type` | Agent step: think/act/observe/answer |
|
|
334
|
-
| `gen_ai.agent.tool.name` | Tool name for agent act steps
|
|
335
|
-
| `gen_ai.
|
|
336
|
-
| `
|
|
335
|
+
| Attribute | Description |
|
|
336
|
+
| ---------------------------- | -------------------------------------------- |
|
|
337
|
+
| `gen_ai.provider.name` | `anthropic`, `gemini`, `openai` |
|
|
338
|
+
| `gen_ai.request.model` | Model identifier |
|
|
339
|
+
| `gen_ai.usage.input_tokens` | Tokens in the prompt |
|
|
340
|
+
| `gen_ai.usage.output_tokens` | Tokens in the completion |
|
|
341
|
+
| `gen_ai.request.temperature` | Temperature parameter |
|
|
342
|
+
| `gen_ai.toad_eye.cost` | Cost in USD |
|
|
343
|
+
| `gen_ai.agent.step.type` | Agent step: think/act/observe/answer/handoff |
|
|
344
|
+
| `gen_ai.agent.tool.name` | Tool name for agent act steps |
|
|
345
|
+
| `gen_ai.agent.handoff.to` | Target agent for handoff steps |
|
|
346
|
+
| `gen_ai.agent.loop_count` | Agent loop iterations (observeβthink) |
|
|
347
|
+
| `gen_ai.toad_eye.guard.mode` | Guard mode: shadow/enforce |
|
|
348
|
+
| `session.id` | Session identifier (if configured) |
|
|
349
|
+
|
|
350
|
+
</details>
|
|
337
351
|
|
|
338
352
|
## Services
|
|
339
353
|
|
|
@@ -349,4 +363,4 @@ All metrics labeled with `gen_ai.provider.name` and `gen_ai.request.model`.
|
|
|
349
363
|
- TypeScript, OpenTelemetry SDK 2.x, OTel GenAI semantic conventions
|
|
350
364
|
- Hono (demo server + cloud ingestion server)
|
|
351
365
|
- Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector)
|
|
352
|
-
- Vitest (
|
|
366
|
+
- Vitest (143+ tests)
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,41 +1,32 @@
|
|
|
1
|
-
import type { AgentStepInput } from "./types/index.js";
|
|
1
|
+
import type { AgentStepInput, AgentQueryOptions } from "./types/index.js";
|
|
2
2
|
/**
|
|
3
|
-
* Record a single agent step as a child span (ReAct pattern
|
|
4
|
-
*
|
|
5
|
-
* Creates a short-lived span under the current active context. Call this inside
|
|
6
|
-
* {@link traceAgentQuery} (preferred) or any other active span (e.g. traceLLMCall).
|
|
7
|
-
*
|
|
8
|
-
* - Respects `recordContent` config β content is omitted when recording is disabled.
|
|
9
|
-
* - For `act` steps with a `toolName`, automatically increments the `agent.tool_usage` metric.
|
|
3
|
+
* Record a single agent step as a child span (ReAct pattern).
|
|
4
|
+
* Supports: think, act, observe, answer, handoff.
|
|
10
5
|
*/
|
|
11
6
|
declare function traceAgentStep(input: AgentStepInput): void;
|
|
12
7
|
/**
|
|
13
|
-
* Wrap an entire agent query in a parent span
|
|
8
|
+
* Wrap an entire agent query in a parent span with ReAct tracing.
|
|
14
9
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `agent.steps_per_query` histogram metric with the total step count.
|
|
10
|
+
* Supports multi-agent via nesting β child traceAgentQuery calls
|
|
11
|
+
* automatically become child spans in Jaeger.
|
|
18
12
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* [parent] agent.query "Is anything dangerous near Earth?"
|
|
22
|
-
* βββ agent.step.think "I need to check asteroids"
|
|
23
|
-
* βββ agent.step.act tool=near-earth-asteroids
|
|
24
|
-
* βββ agent.step.observe "7 asteroids found"
|
|
25
|
-
* βββ agent.step.answer "7 asteroids passing safely..."
|
|
26
|
-
* ```
|
|
13
|
+
* Loop detection: counts observeβthink transitions. Records loop count
|
|
14
|
+
* as span attribute. Emits warning when maxSteps exceeded.
|
|
27
15
|
*
|
|
28
16
|
* @example
|
|
29
17
|
* ```ts
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* step({ type:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
18
|
+
* // Multi-agent: orchestrator delegates to specialist
|
|
19
|
+
* await traceAgentQuery('orchestrator', async (step) => {
|
|
20
|
+
* step({ type: 'think', stepNumber: 1, content: 'Need domain expert' });
|
|
21
|
+
* step({ type: 'handoff', stepNumber: 2, toAgent: 'specialist', handoffReason: 'domain expertise' });
|
|
22
|
+
*
|
|
23
|
+
* const result = await traceAgentQuery('specialist', async (step) => {
|
|
24
|
+
* step({ type: 'act', stepNumber: 1, toolName: 'analyze' });
|
|
25
|
+
* step({ type: 'answer', stepNumber: 2, content: 'analysis complete' });
|
|
26
|
+
* return { answer: 'done' };
|
|
27
|
+
* });
|
|
37
28
|
* });
|
|
38
29
|
* ```
|
|
39
30
|
*/
|
|
40
|
-
export declare function traceAgentQuery<T>(query: string, fn: (step: (input: AgentStepInput) => void) => Promise<T
|
|
31
|
+
export declare function traceAgentQuery<T>(query: string, fn: (step: (input: AgentStepInput) => void) => Promise<T>, options?: AgentQueryOptions): Promise<T>;
|
|
41
32
|
export { traceAgentStep };
|
package/dist/agent.js
CHANGED
|
@@ -3,14 +3,10 @@ import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "./types/index.js";
|
|
|
3
3
|
import { recordAgentSteps, recordAgentToolUsage } from "./core/metrics.js";
|
|
4
4
|
import { getConfig } from "./core/tracer.js";
|
|
5
5
|
const tracer = trace.getTracer(INSTRUMENTATION_NAME);
|
|
6
|
+
const DEFAULT_MAX_STEPS = 25;
|
|
6
7
|
/**
|
|
7
|
-
* Record a single agent step as a child span (ReAct pattern
|
|
8
|
-
*
|
|
9
|
-
* Creates a short-lived span under the current active context. Call this inside
|
|
10
|
-
* {@link traceAgentQuery} (preferred) or any other active span (e.g. traceLLMCall).
|
|
11
|
-
*
|
|
12
|
-
* - Respects `recordContent` config β content is omitted when recording is disabled.
|
|
13
|
-
* - For `act` steps with a `toolName`, automatically increments the `agent.tool_usage` metric.
|
|
8
|
+
* Record a single agent step as a child span (ReAct pattern).
|
|
9
|
+
* Supports: think, act, observe, answer, handoff.
|
|
14
10
|
*/
|
|
15
11
|
function traceAgentStep(input) {
|
|
16
12
|
const span = tracer.startSpan(`agent.step.${input.type}`);
|
|
@@ -26,6 +22,13 @@ function traceAgentStep(input) {
|
|
|
26
22
|
input.content !== undefined && {
|
|
27
23
|
[GEN_AI_ATTRS.AGENT_STEP_CONTENT]: input.content,
|
|
28
24
|
}),
|
|
25
|
+
// Handoff attributes
|
|
26
|
+
...(input.toAgent !== undefined && {
|
|
27
|
+
[GEN_AI_ATTRS.AGENT_HANDOFF_TO]: input.toAgent,
|
|
28
|
+
}),
|
|
29
|
+
...(input.handoffReason !== undefined && {
|
|
30
|
+
[GEN_AI_ATTRS.AGENT_HANDOFF_REASON]: input.handoffReason,
|
|
31
|
+
}),
|
|
29
32
|
});
|
|
30
33
|
if (input.type === "act" && input.toolName !== undefined) {
|
|
31
34
|
recordAgentToolUsage(input.toolName);
|
|
@@ -34,51 +37,67 @@ function traceAgentStep(input) {
|
|
|
34
37
|
span.end();
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
|
-
* Wrap an entire agent query in a parent span
|
|
40
|
+
* Wrap an entire agent query in a parent span with ReAct tracing.
|
|
38
41
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `agent.steps_per_query` histogram metric with the total step count.
|
|
42
|
+
* Supports multi-agent via nesting β child traceAgentQuery calls
|
|
43
|
+
* automatically become child spans in Jaeger.
|
|
42
44
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* [parent] agent.query "Is anything dangerous near Earth?"
|
|
46
|
-
* βββ agent.step.think "I need to check asteroids"
|
|
47
|
-
* βββ agent.step.act tool=near-earth-asteroids
|
|
48
|
-
* βββ agent.step.observe "7 asteroids found"
|
|
49
|
-
* βββ agent.step.answer "7 asteroids passing safely..."
|
|
50
|
-
* ```
|
|
45
|
+
* Loop detection: counts observeβthink transitions. Records loop count
|
|
46
|
+
* as span attribute. Emits warning when maxSteps exceeded.
|
|
51
47
|
*
|
|
52
48
|
* @example
|
|
53
49
|
* ```ts
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* step({ type:
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
50
|
+
* // Multi-agent: orchestrator delegates to specialist
|
|
51
|
+
* await traceAgentQuery('orchestrator', async (step) => {
|
|
52
|
+
* step({ type: 'think', stepNumber: 1, content: 'Need domain expert' });
|
|
53
|
+
* step({ type: 'handoff', stepNumber: 2, toAgent: 'specialist', handoffReason: 'domain expertise' });
|
|
54
|
+
*
|
|
55
|
+
* const result = await traceAgentQuery('specialist', async (step) => {
|
|
56
|
+
* step({ type: 'act', stepNumber: 1, toolName: 'analyze' });
|
|
57
|
+
* step({ type: 'answer', stepNumber: 2, content: 'analysis complete' });
|
|
58
|
+
* return { answer: 'done' };
|
|
59
|
+
* });
|
|
61
60
|
* });
|
|
62
61
|
* ```
|
|
63
62
|
*/
|
|
64
|
-
export async function traceAgentQuery(query, fn) {
|
|
63
|
+
export async function traceAgentQuery(query, fn, options) {
|
|
65
64
|
return tracer.startActiveSpan(`agent.query`, async (span) => {
|
|
66
65
|
const config = getConfig();
|
|
67
66
|
const recordContent = config?.recordContent !== false;
|
|
67
|
+
const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
68
68
|
if (recordContent) {
|
|
69
69
|
span.setAttribute(GEN_AI_ATTRS.AGENT_STEP_CONTENT, query);
|
|
70
70
|
}
|
|
71
71
|
let stepCount = 0;
|
|
72
|
+
let loopCount = 0;
|
|
73
|
+
let lastStepType;
|
|
74
|
+
let maxStepsWarned = false;
|
|
72
75
|
try {
|
|
73
76
|
const result = await fn((input) => {
|
|
74
77
|
stepCount++;
|
|
78
|
+
// Loop detection: observe β think = one loop iteration
|
|
79
|
+
if (input.type === "think" && lastStepType === "observe") {
|
|
80
|
+
loopCount++;
|
|
81
|
+
}
|
|
82
|
+
lastStepType = input.type;
|
|
83
|
+
// Max steps guard
|
|
84
|
+
if (stepCount > maxSteps && !maxStepsWarned) {
|
|
85
|
+
maxStepsWarned = true;
|
|
86
|
+
console.warn(`toad-eye: agent exceeded maxSteps (${maxSteps}). Current: ${stepCount}`);
|
|
87
|
+
span.addEvent("agent.max_steps_exceeded", {
|
|
88
|
+
"agent.max_steps": maxSteps,
|
|
89
|
+
"agent.current_steps": stepCount,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
75
92
|
traceAgentStep(input);
|
|
76
93
|
});
|
|
94
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_LOOP_COUNT, loopCount);
|
|
77
95
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
78
96
|
return result;
|
|
79
97
|
}
|
|
80
98
|
catch (error) {
|
|
81
99
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
span.setAttribute(GEN_AI_ATTRS.AGENT_LOOP_COUNT, loopCount);
|
|
82
101
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
83
102
|
throw error;
|
|
84
103
|
}
|