toad-eye 2.7.0 → 2.9.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 CHANGED
@@ -1,170 +1,98 @@
1
- # toad-eye 🐸👁️‍🗨️🤖
1
+ # toad-eye 🐸👁️
2
2
 
3
+ **Observability for MCP servers and LLM applications.**
4
+
5
+ One line of code. Full traces, metrics, and Grafana dashboards.
6
+ Self-hosted. Privacy-first. No vendor lock-in.
7
+
8
+ [![npm](https://img.shields.io/npm/v/toad-eye?color=CB3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/toad-eye)
3
9
  ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)
4
10
  ![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-000000?logo=opentelemetry&logoColor=white)
5
- ![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=white)
6
- ![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?logo=prometheus&logoColor=white)
7
- ![Grafana](https://img.shields.io/badge/Grafana-F46800?logo=grafana&logoColor=white)
8
- ![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white)
9
- [![npm](https://img.shields.io/npm/v/toad-eye?color=CB3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/toad-eye)
10
11
  ![CI](https://github.com/vola-trebla/toad-eye/actions/workflows/ci.yml/badge.svg)
11
12
  ![License](https://img.shields.io/badge/License-ISC-blue)
12
13
 
13
- **LLM observability in 3 commands. Auto-instrument, trace, alert, save money.**
14
-
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
-
17
14
  ![toad-eye demo](https://raw.githubusercontent.com/vola-trebla/toad-eye/main/demo/toad-eye-demo-gifski.gif)
18
15
 
19
- ## Quick start
20
-
21
- **Prerequisites:** [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or Docker Engine + Compose plugin)
16
+ ---
22
17
 
23
- ```bash
24
- npm install toad-eye
25
- npx toad-eye init # scaffold observability stack
26
- npx toad-eye up # start Grafana + Prometheus + Jaeger
27
- npx toad-eye demo # send mock LLM traffic, see data immediately
28
- ```
18
+ ## Quick Start — MCP Server
29
19
 
30
- > Dashboards will be empty until data arrives. Run `npx toad-eye demo` to send mock traffic and verify the stack works.
20
+ Add observability to any MCP server in 2 lines:
31
21
 
32
22
  ```typescript
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
24
  import { initObservability } from "toad-eye";
25
+ import { toadEyeMiddleware } from "toad-eye/mcp";
34
26
 
35
- initObservability({
36
- serviceName: "my-app",
37
- instrument: ["openai", "anthropic"],
38
- });
39
-
40
- // That's it. Every SDK call is auto-traced — including streaming.
41
- ```
42
-
43
- > ⚠️ By default, toad-eye records prompt and completion text in spans. Set `recordContent: false` for PII-sensitive workloads (healthcare, finance, legal).
44
-
45
- Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your dashboards.
27
+ initObservability({ serviceName: "my-mcp-server" });
46
28
 
47
- ## What you get
29
+ const server = new McpServer({ name: "my-server", version: "1.0.0" });
30
+ toadEyeMiddleware(server);
48
31
 
49
- | Feature | Description |
50
- | ------------------------- | -------------------------------------------------------------------------------------- |
51
- | **Auto-instrumentation** | OpenAI, Anthropic, Gemini, Vercel AI SDK — regular + streaming |
52
- | **MCP server monitoring** | One-line middleware for MCP servers — trace every tool call, resource read, prompt |
53
- | **9 Grafana dashboards** | Overview, Cost, Latency, Errors, Model Comparison, FinOps, Provider Health, Agent, MCP |
54
- | **Cost tracking** | Per-request USD cost, daily totals, projected monthly spend |
55
- | **Budget guards** | Daily/per-user/per-model spend limits — warn, block, or auto-downgrade |
56
- | **Privacy controls** | Built-in PII redaction (email, SSN, CC, phone), hashing, content masking |
32
+ // Every tool call, resource read, and prompt is now traced.
33
+ // Spans appear in Jaeger. Metrics flow to Prometheus. Dashboards ready in Grafana.
34
+ ```
57
35
 
58
- <details>
59
- <summary>Advanced features</summary>
36
+ Privacy by default — tool arguments and results are NOT recorded unless you opt in:
60
37
 
61
- | Feature | Description |
62
- | ----------------------- | --------------------------------------------------------------------------------- |
63
- | **Multi-agent tracing** | ReAct steps, handoffs, loop detection, maxSteps guard |
64
- | **Shadow guardrails** | Record what _would_ be blocked without blocking — tune thresholds on live traffic |
65
- | **Quality proxy** | Empty response rate, latency per token — zero-dependency quality metrics |
66
- | **Semantic drift** | Detect silent quality degradation via embedding comparison |
67
- | **Tail sampling** | Keep 100% errors + slow traces, sample healthy traffic via OTel Collector |
68
- | **Alerting** | Cost spikes, latency anomalies, error rate — via Telegram, Slack, email, webhook |
69
- | **FinOps attribution** | Break down costs by team, user, feature, environment |
70
- | **TTFT metric** | Time To First Token for streaming — separate from total duration |
71
- | **Trace export** | Convert production Jaeger traces into regression test cases |
38
+ ```typescript
39
+ toadEyeMiddleware(server, {
40
+ recordInputs: true,
41
+ redactKeys: ["apiKey", "token"],
42
+ });
43
+ ```
72
44
 
73
- </details>
45
+ Safe for stdio transport — OTel diagnostics are redirected to stderr.
74
46
 
75
- ## Auto-instrumentation 🤖
47
+ ## Quick Start — LLM Calls
76
48
 
77
- Zero-code instrumentation for popular LLM SDKs. No wrappers needed.
49
+ Auto-instrument OpenAI, Anthropic, Gemini, and Vercel AI SDK — zero wrappers:
78
50
 
79
51
  ```typescript
52
+ import { initObservability } from "toad-eye";
53
+
80
54
  initObservability({
81
55
  serviceName: "my-app",
82
56
  instrument: ["openai", "anthropic"],
83
57
  });
84
- ```
85
-
86
- **Supported SDKs:** OpenAI (`openai`), Anthropic (`@anthropic-ai/sdk`), Google GenAI (`@google/generative-ai`), Vercel AI SDK (`ai`)
87
-
88
- Both regular and **streaming** calls are fully instrumented — spans, metrics, and cost tracking work transparently for `stream: true`.
89
58
 
90
- Vercel AI SDK uses a SpanProcessor (not monkey-patching) — add `instrument: ['ai']` and `experimental_telemetry: withToadEye()` to your calls.
91
-
92
- <details>
93
- <summary>Manual instrumentation (custom providers)</summary>
94
-
95
- Use `traceLLMCall` when you need to instrument a provider not in the auto-instrument list, or want explicit control over span data:
96
-
97
- ```typescript
98
- import OpenAI from "openai";
99
- import { initObservability, traceLLMCall } from "toad-eye";
100
-
101
- initObservability({ serviceName: "my-app" });
102
-
103
- const openai = new OpenAI();
104
-
105
- const result = await traceLLMCall(
106
- { provider: "openai", model: "gpt-4o", prompt: "Hello" },
107
- async () => {
108
- const res = await openai.chat.completions.create({
109
- model: "gpt-4o",
110
- messages: [{ role: "user", content: "Hello" }],
111
- });
112
- return {
113
- completion: res.choices[0]?.message.content ?? "",
114
- inputTokens: res.usage?.prompt_tokens ?? 0,
115
- outputTokens: res.usage?.completion_tokens ?? 0,
116
- // cost is auto-calculated from model + tokens if omitted
117
- };
118
- },
119
- );
59
+ // Every SDK call is auto-traced — including streaming.
120
60
  ```
121
61
 
122
- > Or skip this entirely — use `instrument: ['openai']` for zero-code instrumentation.
62
+ ## Set Up the Stack
123
63
 
124
- > **Note:** In short-lived scripts, call `await shutdown()` before exit to flush buffered spans. Long-running servers (Express, Hono, Next.js) don't need this — spans flush automatically.
64
+ ```bash
65
+ npm install toad-eye
66
+ npx toad-eye init # scaffold observability configs
67
+ npx toad-eye up # start Grafana + Prometheus + Jaeger + OTel Collector
68
+ npx toad-eye demo # send mock traffic — see data in Grafana immediately
69
+ ```
125
70
 
126
- </details>
71
+ Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin). 11 dashboards are pre-built and ready.
127
72
 
128
- ## MCP Server observability
73
+ > **Requires:** [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or Docker Engine + Compose plugin)
129
74
 
130
- 8,600+ MCP servers. Zero built-in observability. toad-eye fixes that with one line:
75
+ <!-- TODO: add Grafana dashboard screenshot here -->
131
76
 
132
- ```typescript
133
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
134
- import { initObservability } from "toad-eye";
135
- import { toadEyeMiddleware } from "toad-eye/mcp";
77
+ ## What You Get
136
78
 
137
- initObservability({ serviceName: "my-mcp-server" });
79
+ **Instrumentation:** auto-instrument OpenAI, Anthropic, Gemini, Vercel AI SDK, and MCP servers — regular and streaming calls, with full cost tracking.
138
80
 
139
- const server = new McpServer({ name: "my-server", version: "1.0.0" });
140
- toadEyeMiddleware(server); // every tool call, resource read, prompt — traced
81
+ **11 Grafana dashboards:** Overview, Cost Breakdown, Latency Analysis, Errors, Model Comparison, FinOps Attribution, Provider Health, Agent Workflow, MCP Server, MCP End-to-End, MCP Tool Analytics.
141
82
 
142
- server.tool("calculate", { expression: z.string() }, async ({ expression }) => {
143
- return { content: [{ type: "text", text: String(eval(expression)) }] };
144
- });
145
- ```
83
+ **Budget guards:** daily, per-user, per-model spend limits. Three modes — warn, block, or auto-downgrade to a cheaper model.
146
84
 
147
- Spans follow [OTel MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/): `tools/call calculate`, `resources/read file:///docs`, `prompts/get greeting`.
85
+ **Agent tracing:** structured ReAct tracing with multi-agent support, handoffs, and loop detection. Follows OTel GenAI semantic conventions.
148
86
 
149
- Every span includes: `mcp.method.name`, `mcp.session.id`, `mcp.protocol.version`, `network.transport`.
150
-
151
- Privacy by default — tool arguments and results are NOT recorded unless you opt in:
152
-
153
- ```typescript
154
- toadEyeMiddleware(server, {
155
- recordInputs: true,
156
- recordOutputs: true,
157
- redactKeys: ["apiKey", "token"],
158
- });
159
- ```
87
+ **Privacy controls:** `recordContent: false` to disable prompt/completion recording, built-in PII redaction (email, SSN, CC, phone), SHA-256 hashing, key redaction.
160
88
 
161
- Dedicated MCP dashboard in Grafana: tool call rate, duration p50/p95, error rate, resource reads, tool performance table.
89
+ **Alerting:** cost spikes, latency anomalies, error rate alerts via Telegram, Slack, email, or webhook.
162
90
 
163
- > Safe for stdio transport — OTel diagnostics are redirected to stderr.
91
+ **Semantic drift detection:** catch silent LLM quality degradation by comparing responses to a saved baseline via embeddings.
164
92
 
165
- ## Budget guards
93
+ **Trace export:** convert production Jaeger traces into regression test cases for [toad-eval](https://github.com/vola-trebla/toad-eval).
166
94
 
167
- Prevent cost overruns at runtime. Three modes: warn, block, or auto-downgrade to a cheaper model.
95
+ ## Budget Guards
168
96
 
169
97
  ```typescript
170
98
  initObservability({
@@ -174,327 +102,71 @@ initObservability({
174
102
  perUser: 5, // $5/day per user
175
103
  perModel: { "gpt-4o": 30 }, // $30/day on GPT-4o
176
104
  },
177
- onBudgetExceeded: "block", // throws ToadBudgetExceededError
105
+ onBudgetExceeded: "block", // or "warn" or "downgrade"
178
106
  });
179
107
  ```
180
108
 
181
- | Mode | Behavior |
182
- | ----------- | --------------------------------------------------- |
183
- | `warn` | Log warning, continue normally |
184
- | `block` | Throw `ToadBudgetExceededError` before LLM call |
185
- | `downgrade` | Call `downgradeCallback` to switch to cheaper model |
186
-
187
- ## Agent observability
188
-
189
- Structured tracing for ReAct agents with multi-agent support, handoffs, and loop detection. Span names follow [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/).
109
+ ## Agent Observability
190
110
 
191
111
  ```typescript
192
112
  import { traceAgentQuery } from "toad-eye";
193
113
 
194
114
  const result = await traceAgentQuery(
195
- {
196
- query: "What's the weather like?",
197
- agentName: "weather-bot", // → gen_ai.agent.name (OTel semconv)
198
- },
115
+ { query: "What's the weather?", agentName: "weather-bot" },
199
116
  async (step) => {
200
117
  step({ type: "think", stepNumber: 1, content: "Need weather data" });
201
118
  const data = await getWeather();
202
- step({
203
- type: "act",
204
- stepNumber: 2,
205
- toolName: "get_weather",
206
- toolType: "function",
207
- });
119
+ step({ type: "act", stepNumber: 2, toolName: "get_weather" });
208
120
  step({ type: "answer", stepNumber: 3, content: data.summary });
209
121
  return { answer: data.summary };
210
122
  },
211
123
  );
212
- // Produces spans: invoke_agent weather-bot → execute_tool get_weather
124
+ // Produces: invoke_agent weather-bot → execute_tool get_weather
213
125
  ```
214
126
 
215
- <details>
216
- <summary>Full ReAct pattern (think → act → observe → answer)</summary>
217
-
218
- ```typescript
219
- const result = await traceAgentQuery(
220
- "Is anything dangerous near Earth?",
221
- async (step) => {
222
- step({
223
- type: "think",
224
- stepNumber: 1,
225
- content: "I need to check asteroids",
226
- });
227
- const data = await callTool("near-earth-asteroids");
228
- step({ type: "act", stepNumber: 2, toolName: "near-earth-asteroids" });
229
- step({
230
- type: "observe",
231
- stepNumber: 3,
232
- content: `${data.length} asteroids found`,
233
- });
234
- step({
235
- type: "answer",
236
- stepNumber: 4,
237
- content: "7 asteroids passing safely",
238
- });
239
- return { answer: "7 asteroids passing safely" };
240
- },
241
- );
242
- ```
243
-
244
- </details>
245
-
246
- ## FinOps attribution
247
-
248
- Track costs by team, user, feature, or any business dimension:
249
-
250
- ```typescript
251
- initObservability({
252
- serviceName: "my-app",
253
- attributes: { team: "checkout", environment: "production" },
254
- });
255
-
256
- await traceLLMCall(
257
- {
258
- provider: "openai",
259
- model: "gpt-4o",
260
- prompt: "Summarize order",
261
- attributes: { userId: "user-123", feature: "order-summary" },
262
- },
263
- () => callLLM(),
264
- );
265
- ```
266
-
267
- <details>
268
- <summary>More features</summary>
269
-
270
- ### Shadow guardrails
271
-
272
- Record what _would_ have been blocked without blocking. Tune thresholds on live traffic.
273
-
274
- ```typescript
275
- import { recordGuardResult } from "toad-eye";
276
-
277
- recordGuardResult({
278
- mode: "shadow",
279
- passed: false,
280
- ruleName: "pii_filter",
281
- failureReason: "SSN detected in response",
282
- });
283
- ```
284
-
285
- ### Semantic drift monitoring
286
-
287
- Detect silent LLM quality degradation by comparing responses to a saved baseline via embeddings.
288
-
289
- ```typescript
290
- import { createDriftMonitor } from "toad-eye";
291
-
292
- const monitor = createDriftMonitor({
293
- embedding: { provider: "openai", apiKey: process.env.OPENAI_API_KEY! },
294
- baselinePath: "./baseline.json",
295
- sampleRate: 10,
296
- });
297
-
298
- monitor.checkInBackground(response, "openai", "gpt-4o");
299
- ```
300
-
301
- ### Privacy controls
302
-
303
- | Goal | Config |
304
- | --------------------------- | -------------------------------- |
305
- | Don't store text at all | `recordContent: false` |
306
- | Remove common PII | `redactDefaults: true` |
307
- | Remove custom patterns | `redactPatterns: [/regex/g]` |
308
- | Store hashes only (no text) | `hashContent: true, salt: "..."` |
309
- | Audit what was masked | `auditMasking: true` |
310
-
311
- ```typescript
312
- initObservability({
313
- serviceName: "my-app",
314
- redactDefaults: true, // built-in PII patterns (email, SSN, CC, phone)
315
- recordContent: false, // disable prompt/completion recording
316
- hashContent: true, // SHA-256 hash instead of plain text
317
- salt: "my-secret", // prevent rainbow table attacks
318
- auditMasking: true, // log what was masked (debug only)
319
- redactPatterns: [/custom-pattern/g], // additional regex redaction
320
- });
321
- ```
322
-
323
- ### Session tracking
324
-
325
- ```typescript
326
- initObservability({
327
- serviceName: "my-app",
328
- sessionId: "static-session-id",
329
- sessionExtractor: () => getCurrentSessionId(), // or dynamic
330
- });
331
- ```
332
-
333
- ### Built-in cost tracking
334
-
335
- ```typescript
336
- import { setCustomPricing } from "toad-eye";
337
-
338
- setCustomPricing({
339
- "my-fine-tuned-model": { inputPer1M: 5, outputPer1M: 15 },
340
- });
341
- ```
342
-
343
- ### Alerting
344
-
345
- ```yaml
346
- alerts:
347
- - name: cost_spike
348
- metric: gen_ai.client.request.cost
349
- condition: sum_1h > 10
350
- channels: [slack]
351
-
352
- - name: error_rate
353
- metric: gen_ai.client.errors
354
- condition: ratio_15m > 0.05
355
- channels: [telegram]
356
- ```
357
-
358
- Channels: Telegram, Slack webhook, HTTP webhook, email (SMTP).
359
-
360
- ### Trace-to-dataset export
361
-
362
- Convert production Jaeger traces into test cases:
127
+ ## CLI
363
128
 
364
- ```bash
365
- npx toad-eye export-trace <trace_id> --output ./evals/
366
129
  ```
367
-
368
- ### Cloud mode (coming soon)
369
-
370
- No Docker needed. Send telemetry to toad-eye cloud with one line:
371
-
372
- ```typescript
373
- initObservability({
374
- serviceName: "my-app",
375
- apiKey: "toad_xxxxxxxx",
376
- instrument: ["openai"],
377
- });
130
+ npx toad-eye init [--force] Scaffold Docker Compose + observability configs
131
+ npx toad-eye up Start the stack
132
+ npx toad-eye down Stop the stack
133
+ npx toad-eye status Show running services and URLs
134
+ npx toad-eye demo Send mock LLM traffic to Grafana
135
+ npx toad-eye export-trace <id> Export a Jaeger trace to toad-eval YAML
378
136
  ```
379
137
 
380
- Self-hosted mode remains the default. Cloud mode activates automatically when `apiKey` is set.
381
-
382
- </details>
383
-
384
- ## Grafana dashboards
385
-
386
- 9 pre-built dashboards auto-provisioned on `npx toad-eye init`:
387
-
388
- | Dashboard | What it shows |
389
- | ---------------------- | ---------------------------------------------------------------- |
390
- | **Overview** | Request rate, error rate, latency p50/p95, cost, totals |
391
- | **Cost Breakdown** | Spend by provider/model, daily trend, projected monthly |
392
- | **Latency Analysis** | p50/p95/p99 by model, distribution histogram, TTFT |
393
- | **Error Drill-down** | Error rate by provider/model, error vs success |
394
- | **Model Comparison** | Latency vs cost vs error rate vs throughput per model |
395
- | **FinOps Attribution** | Cost by team/user/feature, projected spend, what-if table |
396
- | **Provider Health** | Provider status (healthy/degraded/down), uptime, error breakdown |
397
- | **Agent Workflow** | Steps per query, tool usage frequency, step type breakdown |
398
- | **MCP Server** | Tool call rate, duration p50/p95, errors by tool, resource reads |
138
+ ## Architecture
399
139
 
400
- ## Subpath imports
140
+ ![toad-eye architecture](https://raw.githubusercontent.com/vola-trebla/toad-eye/main/demo/toad_eye_architecture_v2.svg)
401
141
 
402
- Advanced modules are available as separate entry points to keep your bundle lean:
142
+ ## Imports
403
143
 
404
144
  ```typescript
405
145
  import { initObservability, traceLLMCall } from "toad-eye"; // core
406
- import { AlertManager } from "toad-eye/alerts"; // alerting
146
+ import { toadEyeMiddleware } from "toad-eye/mcp"; // MCP server middleware
147
+ import { AlertManager } from "toad-eye/alerts"; // alerting engine
407
148
  import { createDriftMonitor } from "toad-eye/drift"; // semantic drift
408
149
  import { exportTrace } from "toad-eye/export"; // trace → YAML
409
150
  import { ToadEyeAISpanProcessor, withToadEye } from "toad-eye/vercel"; // Vercel AI SDK
410
151
  ```
411
152
 
412
- > Everything is also re-exported from the main `"toad-eye"` entry point for convenience.
413
-
414
- ## CLI
415
-
416
- | Command | Description |
417
- | -------------------------------- | ----------------------------------------------- |
418
- | `npx toad-eye init [--force]` | Scaffold Docker Compose + observability configs |
419
- | `npx toad-eye up` | Start the stack |
420
- | `npx toad-eye down` | Stop the stack |
421
- | `npx toad-eye status` | Show running services and URLs |
422
- | `npx toad-eye demo` | Send mock LLM traffic to see data in Grafana |
423
- | `npx toad-eye export-trace <id>` | Export a Jaeger trace to toad-eval YAML |
424
-
425
- ## Architecture
426
-
427
- ![toad-eye architecture](https://raw.githubusercontent.com/vola-trebla/toad-eye/main/demo/toad_eye_architecture_v2.svg)
428
-
429
- ## What it tracks
430
-
431
- <details>
432
- <summary>Metrics & span attributes</summary>
433
-
434
- ### Metrics (OTel GenAI semconv)
435
-
436
- | Metric | Type | Description |
437
- | -------------------------------------------- | --------- | --------------------------- |
438
- | `gen_ai.client.operation.duration` | Histogram | Request latency (ms) |
439
- | `gen_ai.client.time_to_first_token` | Histogram | TTFT for streaming (ms) |
440
- | `gen_ai.client.request.cost` | Histogram | Cost per request (USD) |
441
- | `gen_ai.client.token.usage` | Counter | Total tokens consumed |
442
- | `gen_ai.client.requests` | Counter | Total requests |
443
- | `gen_ai.client.errors` | Counter | Total failed requests |
444
- | `gen_ai.agent.steps_per_query` | Histogram | Agent steps per query |
445
- | `gen_ai.agent.tool_usage` | Counter | Agent tool invocations |
446
- | `gen_ai.toad_eye.guard.evaluations` | Counter | Guard evaluations per rule |
447
- | `gen_ai.toad_eye.guard.would_block` | Counter | Would-have-blocked per rule |
448
- | `gen_ai.toad_eye.semantic_drift` | Histogram | Drift from baseline (0..1) |
449
- | `gen_ai.toad_eye.budget.exceeded` | Counter | Budget exceeded events |
450
- | `gen_ai.toad_eye.budget.blocked` | Counter | LLM calls blocked by budget |
451
- | `gen_ai.toad_eye.budget.downgraded` | Counter | LLM calls downgraded |
452
- | `gen_ai.toad_eye.response.empty` | Counter | Empty/whitespace responses |
453
- | `gen_ai.toad_eye.response.latency_per_token` | Histogram | Generation speed (ms/token) |
454
-
455
- ### Span attributes (OTel GenAI semconv)
456
-
457
- | Attribute | Description |
458
- | ---------------------------------- | ---------------------------------------------------------------------- |
459
- | `gen_ai.operation.name` | `chat`, `invoke_agent`, `execute_tool`, `tools/call`, `resources/read` |
460
- | `gen_ai.provider.name` | `anthropic`, `gemini`, `openai` |
461
- | `gen_ai.request.model` | Model identifier |
462
- | `gen_ai.usage.input_tokens` | Tokens in the prompt |
463
- | `gen_ai.usage.output_tokens` | Tokens in the completion |
464
- | `gen_ai.request.temperature` | Temperature parameter |
465
- | `gen_ai.agent.name` | Agent name (for agent spans) |
466
- | `gen_ai.agent.id` | Agent identifier |
467
- | `gen_ai.tool.name` | Tool name for execute_tool spans |
468
- | `gen_ai.tool.type` | `function`, `extension`, `retrieval`, `builtin` |
469
- | `gen_ai.toad_eye.cost` | Cost in USD |
470
- | `gen_ai.toad_eye.agent.step.type` | ReAct step: think/act/observe/answer/handoff |
471
- | `gen_ai.toad_eye.agent.handoff.to` | Target agent for handoff steps |
472
- | `gen_ai.toad_eye.agent.loop_count` | Agent loop iterations (observe→think) |
473
- | `gen_ai.toad_eye.guard.mode` | Guard mode: shadow/enforce |
474
- | `session.id` | Session identifier (if configured) |
475
-
476
- </details>
477
-
478
153
  ## Services
479
154
 
480
- | Service | URL |
481
- | -------------- | ------------------------------------------------------- |
482
- | Grafana | [localhost:3100](http://localhost:3100) (admin / admin) |
483
- | Jaeger UI | [localhost:16686](http://localhost:16686) |
484
- | Prometheus | [localhost:9090](http://localhost:9090) |
485
- | OTel Collector | [localhost:4318](http://localhost:4318) |
155
+ After `npx toad-eye up`:
486
156
 
487
- ## OTel GenAI semconv
157
+ ```
158
+ Grafana http://localhost:3100 (admin / admin)
159
+ Jaeger UI http://localhost:16686
160
+ Prometheus http://localhost:9090
161
+ OTel Collector http://localhost:4318
162
+ ```
488
163
 
489
- toad-eye follows [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for span names, attributes, and metrics. Traces are natively compatible with Jaeger, Arize Phoenix, SigNoz, Datadog, Langfuse, and other OTel backends.
164
+ ## OTel Compatibility
490
165
 
491
- See [COMPATIBILITY.md](https://github.com/vola-trebla/toad-eye/blob/main/packages/instrumentation/COMPATIBILITY.md) for a full backend support matrix.
166
+ toad-eye follows [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Traces work natively with Jaeger, Datadog, Grafana Tempo, Honeycomb, SigNoz, Arize Phoenix, and any OTel-compatible backend.
492
167
 
493
- Set `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` to disable deprecated attribute aliases and emit only the latest conventions.
168
+ Full metrics and span attribute reference: [COMPATIBILITY.md](https://github.com/vola-trebla/toad-eye/blob/main/packages/instrumentation/COMPATIBILITY.md)
494
169
 
495
- ## Tech stack
170
+ ## Tech Stack
496
171
 
497
- - TypeScript, OpenTelemetry SDK 2.x, OTel GenAI semantic conventions
498
- - Hono (demo server + cloud ingestion server)
499
- - Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector)
500
- - Vitest (252+ tests)
172
+ TypeScript · OpenTelemetry SDK 2.x · Hono · Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector) · Vitest (273+ tests)
@@ -5,6 +5,12 @@ const METRIC_MAP = {
5
5
  "gen_ai.client.requests": "gen_ai_client_requests_total",
6
6
  "gen_ai.client.errors": "gen_ai_client_errors_total",
7
7
  "gen_ai.client.token.usage": "gen_ai_client_token_usage_total",
8
+ // MCP metrics
9
+ "gen_ai.mcp.tool.calls": "gen_ai_mcp_tool_calls_total",
10
+ "gen_ai.mcp.tool.errors": "gen_ai_mcp_tool_errors_total",
11
+ "gen_ai.mcp.tool.duration": "gen_ai_mcp_tool_duration",
12
+ "gen_ai.mcp.resource.reads": "gen_ai_mcp_resource_reads_total",
13
+ "gen_ai.mcp.tool.callers": "gen_ai_mcp_tool_callers_total",
8
14
  // Legacy names (backward compat)
9
15
  "llm.request.cost": "gen_ai_client_request_cost_USD_sum",
10
16
  "llm.request.duration": "gen_ai_client_operation_duration_milliseconds",
@@ -15,6 +21,9 @@ const METRIC_MAP = {
15
21
  function toPromMetric(metric) {
16
22
  return METRIC_MAP[metric] ?? metric.replace(/\./g, "_");
17
23
  }
24
+ function isMcpMetric(metric) {
25
+ return metric.startsWith("gen_ai.mcp.");
26
+ }
18
27
  /**
19
28
  * Parse condition string into structured form.
20
29
  *
@@ -65,7 +74,10 @@ function buildPromQL(metric, operator, window) {
65
74
  }
66
75
  function buildTopModelsQuery(metric, window) {
67
76
  const m = toPromMetric(metric);
68
- return `topk(5, sum by (gen_ai_request_model) (increase(${m}[${window}])))`;
77
+ const groupBy = isMcpMetric(metric)
78
+ ? "gen_ai_tool_name"
79
+ : "gen_ai_request_model";
80
+ return `topk(5, sum by (${groupBy}) (increase(${m}[${window}])))`;
69
81
  }
70
82
  function matches(value, comparator, threshold) {
71
83
  switch (comparator) {
@@ -94,10 +106,13 @@ async function queryTopModels(prometheusUrl, metric, window) {
94
106
  const res = await fetch(url);
95
107
  if (!res.ok)
96
108
  return [];
109
+ const labelKey = isMcpMetric(metric)
110
+ ? "gen_ai_tool_name"
111
+ : "gen_ai_request_model";
97
112
  const data = (await res.json());
98
113
  return data.data.result
99
114
  .map((r) => ({
100
- model: r.metric.gen_ai_request_model ?? "unknown",
115
+ model: r.metric[labelKey] ?? "unknown",
101
116
  value: parseFloat(r.value[1]),
102
117
  }))
103
118
  .sort((a, b) => b.value - a.value);
@@ -113,9 +128,19 @@ async function evalP95Baseline(prometheusUrl, metric, window, baselineWindow) {
113
128
  return ((currentP95 - baselineP95) / baselineP95) * 100;
114
129
  }
115
130
  /** Evaluate error ratio: errors / requests over window */
116
- async function evalErrorRatio(prometheusUrl, window) {
117
- const errors = await queryScalar(prometheusUrl, `sum(increase(gen_ai_client_errors_total[${window}]))`);
118
- const requests = await queryScalar(prometheusUrl, `sum(increase(gen_ai_client_requests_total[${window}]))`);
131
+ async function evalErrorRatio(prometheusUrl, metric, window) {
132
+ let errorMetric;
133
+ let requestMetric;
134
+ if (isMcpMetric(metric)) {
135
+ errorMetric = "gen_ai_mcp_tool_errors_total";
136
+ requestMetric = "gen_ai_mcp_tool_calls_total";
137
+ }
138
+ else {
139
+ errorMetric = "gen_ai_client_errors_total";
140
+ requestMetric = "gen_ai_client_requests_total";
141
+ }
142
+ const errors = await queryScalar(prometheusUrl, `sum(increase(${errorMetric}[${window}]))`);
143
+ const requests = await queryScalar(prometheusUrl, `sum(increase(${requestMetric}[${window}]))`);
119
144
  if (requests === 0)
120
145
  return 0;
121
146
  return errors / requests;
@@ -128,7 +153,7 @@ export async function evaluateCondition(prometheusUrl, rule) {
128
153
  value = await evalP95Baseline(prometheusUrl, rule.metric, parsed.window, parsed.baselineWindow);
129
154
  }
130
155
  else if (parsed.operator === "ratio") {
131
- value = await evalErrorRatio(prometheusUrl, parsed.window);
156
+ value = await evalErrorRatio(prometheusUrl, rule.metric, parsed.window);
132
157
  }
133
158
  else {
134
159
  const promql = buildPromQL(rule.metric, parsed.operator, parsed.window);