toad-eye 2.2.0 → 2.4.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.
Files changed (47) hide show
  1. package/README.md +166 -152
  2. package/dist/agent.d.ts +31 -28
  3. package/dist/agent.js +116 -35
  4. package/dist/alerts/channels.js +24 -6
  5. package/dist/alerts/index.js +46 -2
  6. package/dist/alerts/manager.d.ts +1 -0
  7. package/dist/alerts/manager.js +36 -21
  8. package/dist/budget/tracker.d.ts +16 -6
  9. package/dist/budget/tracker.js +38 -17
  10. package/dist/budget/types.d.ts +2 -0
  11. package/dist/cli.d.ts +8 -1
  12. package/dist/cli.js +106 -54
  13. package/dist/core/metrics.d.ts +4 -0
  14. package/dist/core/metrics.js +20 -1
  15. package/dist/core/pricing.d.ts +2 -0
  16. package/dist/core/pricing.js +4 -0
  17. package/dist/core/spans.d.ts +5 -3
  18. package/dist/core/spans.js +80 -21
  19. package/dist/core/tracer.d.ts +6 -3
  20. package/dist/core/tracer.js +49 -7
  21. package/dist/drift/baseline.js +7 -1
  22. package/dist/drift/monitor.js +6 -6
  23. package/dist/index.d.ts +7 -2
  24. package/dist/index.js +6 -1
  25. package/dist/instrumentations/anthropic.d.ts +1 -1
  26. package/dist/instrumentations/anthropic.js +2 -3
  27. package/dist/instrumentations/create.js +80 -19
  28. package/dist/instrumentations/gemini.d.ts +1 -1
  29. package/dist/instrumentations/gemini.js +6 -7
  30. package/dist/instrumentations/openai.d.ts +1 -1
  31. package/dist/instrumentations/openai.js +2 -3
  32. package/dist/instrumentations/registry.d.ts +3 -1
  33. package/dist/instrumentations/registry.js +28 -5
  34. package/dist/instrumentations/types.d.ts +4 -2
  35. package/dist/types/attributes.d.ts +20 -0
  36. package/dist/types/attributes.js +23 -1
  37. package/dist/types/config.d.ts +22 -2
  38. package/dist/types/index.d.ts +3 -3
  39. package/dist/types/metrics.d.ts +2 -0
  40. package/dist/types/metrics.js +3 -0
  41. package/dist/types/providers.d.ts +6 -1
  42. package/dist/types/spans.d.ts +21 -2
  43. package/dist/vercel.d.ts +53 -0
  44. package/dist/vercel.js +106 -0
  45. package/package.json +29 -3
  46. package/templates/grafana/dashboards/agent-workflow.json +207 -0
  47. package/templates/otel-collector.yml +25 -1
package/dist/vercel.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Vercel AI SDK integration — enriches AI SDK OTel spans with toad-eye
3
+ * cost tracking and metrics. No monkey-patching needed.
4
+ *
5
+ * The Vercel AI SDK already emits OTel spans when `experimental_telemetry`
6
+ * is enabled. This module adds a SpanProcessor that intercepts those spans
7
+ * and calculates cost from token usage.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { initObservability } from 'toad-eye';
12
+ * import { withToadEye } from 'toad-eye/vercel';
13
+ *
14
+ * initObservability({ serviceName: 'my-app' });
15
+ *
16
+ * const result = await generateText({
17
+ * model: openai('gpt-4o'),
18
+ * prompt: 'Hello',
19
+ * experimental_telemetry: withToadEye(),
20
+ * });
21
+ * ```
22
+ */
23
+ import { calculateCost } from "./core/pricing.js";
24
+ import { recordRequestCost, recordTokens, recordRequest, recordRequestDuration, } from "./core/metrics.js";
25
+ // Vercel AI SDK span operation names
26
+ const AI_SDK_OPERATIONS = new Set([
27
+ "ai.generateText",
28
+ "ai.streamText",
29
+ "ai.generateObject",
30
+ "ai.streamObject",
31
+ ]);
32
+ function isAiSdkSpan(span) {
33
+ const opId = span.attributes["ai.operationId"];
34
+ if (typeof opId === "string")
35
+ return AI_SDK_OPERATIONS.has(opId);
36
+ return AI_SDK_OPERATIONS.has(span.name);
37
+ }
38
+ function getStringAttr(span, key) {
39
+ const val = span.attributes[key];
40
+ return typeof val === "string" ? val : undefined;
41
+ }
42
+ function getNumberAttr(span, key) {
43
+ const val = span.attributes[key];
44
+ return typeof val === "number" ? val : 0;
45
+ }
46
+ /**
47
+ * OTel SpanProcessor that enriches Vercel AI SDK spans with toad-eye
48
+ * cost attributes and records metrics.
49
+ */
50
+ export class ToadEyeAISpanProcessor {
51
+ forceFlush() {
52
+ return Promise.resolve();
53
+ }
54
+ shutdown() {
55
+ return Promise.resolve();
56
+ }
57
+ onStart() {
58
+ // no-op — we process on end
59
+ }
60
+ onEnd(span) {
61
+ if (!isAiSdkSpan(span))
62
+ return;
63
+ const model = getStringAttr(span, "gen_ai.request.model") ??
64
+ getStringAttr(span, "ai.model.id") ??
65
+ "unknown";
66
+ const provider = getStringAttr(span, "gen_ai.system") ??
67
+ getStringAttr(span, "ai.model.provider")?.split(".")[0] ??
68
+ "unknown";
69
+ const inputTokens = getNumberAttr(span, "gen_ai.usage.input_tokens") ||
70
+ getNumberAttr(span, "ai.usage.promptTokens");
71
+ const outputTokens = getNumberAttr(span, "gen_ai.usage.output_tokens") ||
72
+ getNumberAttr(span, "ai.usage.completionTokens");
73
+ // Calculate cost from token usage
74
+ const cost = calculateCost(model, inputTokens, outputTokens);
75
+ // We can't modify a ReadableSpan's attributes directly, but we can
76
+ // record metrics which is the primary value-add
77
+ const durationMs = (span.endTime[0] - span.startTime[0]) * 1000 +
78
+ (span.endTime[1] - span.startTime[1]) / 1_000_000;
79
+ recordRequest(provider, model);
80
+ recordRequestDuration(durationMs, provider, model);
81
+ if (cost > 0) {
82
+ recordRequestCost(cost, provider, model);
83
+ }
84
+ if (inputTokens + outputTokens > 0) {
85
+ recordTokens(inputTokens + outputTokens, provider, model);
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Helper for `experimental_telemetry` option — enables telemetry with
91
+ * optional toad-eye metadata.
92
+ *
93
+ * ```ts
94
+ * const result = await generateText({
95
+ * model: openai('gpt-4o'),
96
+ * prompt: 'Hello',
97
+ * experimental_telemetry: withToadEye({ functionId: 'my-feature' }),
98
+ * });
99
+ * ```
100
+ */
101
+ export function withToadEye(options) {
102
+ return {
103
+ isEnabled: true,
104
+ ...options,
105
+ };
106
+ }
package/package.json CHANGED
@@ -1,10 +1,32 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Auto-instrumentation, cost tracking, budget guards, and observability for LLM services",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./alerts": {
14
+ "import": "./dist/alerts/index.js",
15
+ "types": "./dist/alerts/index.d.ts"
16
+ },
17
+ "./drift": {
18
+ "import": "./dist/drift/index.js",
19
+ "types": "./dist/drift/index.d.ts"
20
+ },
21
+ "./export": {
22
+ "import": "./dist/export.js",
23
+ "types": "./dist/export.d.ts"
24
+ },
25
+ "./vercel": {
26
+ "import": "./dist/vercel.js",
27
+ "types": "./dist/vercel.d.ts"
28
+ }
29
+ },
8
30
  "bin": {
9
31
  "toad-eye": "dist/cli.js"
10
32
  },
@@ -51,13 +73,15 @@
51
73
  "@opentelemetry/resources": "^2.6.0",
52
74
  "@opentelemetry/sdk-metrics": "^2.6.0",
53
75
  "@opentelemetry/sdk-node": "^0.213.0",
76
+ "@opentelemetry/sdk-trace-base": "^2.6.0",
77
+ "@opentelemetry/sdk-trace-node": "^2.6.0",
54
78
  "@opentelemetry/semantic-conventions": "^1.40.0",
55
- "nodemailer": "^8.0.3",
56
79
  "yaml": "^2.7.0"
57
80
  },
58
81
  "peerDependencies": {
59
82
  "@anthropic-ai/sdk": ">=0.30.0",
60
83
  "@google/generative-ai": ">=0.20.0",
84
+ "nodemailer": ">=8.0.0",
61
85
  "openai": ">=4.0.0"
62
86
  },
63
87
  "peerDependenciesMeta": {
@@ -69,11 +93,13 @@
69
93
  },
70
94
  "@google/generative-ai": {
71
95
  "optional": true
96
+ },
97
+ "nodemailer": {
98
+ "optional": true
72
99
  }
73
100
  },
74
101
  "devDependencies": {
75
102
  "@types/nodemailer": "^7.0.11",
76
- "playwright": "^1.58.2",
77
103
  "vitest": "^4.1.0"
78
104
  }
79
105
  }
@@ -0,0 +1,207 @@
1
+ {
2
+ "annotations": { "list": [] },
3
+ "editable": true,
4
+ "fiscalYearStartMonth": 0,
5
+ "graphTooltip": 1,
6
+ "id": null,
7
+ "links": [],
8
+ "panels": [
9
+ {
10
+ "title": "Avg Steps per Query",
11
+ "type": "stat",
12
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
13
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
14
+ "targets": [
15
+ {
16
+ "expr": "sum(gen_ai_agent_steps_per_query_sum) / sum(gen_ai_agent_steps_per_query_count) or vector(0)",
17
+ "refId": "A"
18
+ }
19
+ ],
20
+ "fieldConfig": {
21
+ "defaults": {
22
+ "unit": "short",
23
+ "decimals": 1,
24
+ "thresholds": {
25
+ "steps": [
26
+ { "color": "green", "value": null },
27
+ { "color": "yellow", "value": 10 },
28
+ { "color": "red", "value": 20 }
29
+ ]
30
+ }
31
+ },
32
+ "overrides": []
33
+ }
34
+ },
35
+ {
36
+ "title": "Total Agent Queries",
37
+ "type": "stat",
38
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
39
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
40
+ "targets": [
41
+ {
42
+ "expr": "sum(gen_ai_agent_steps_per_query_count)",
43
+ "refId": "A"
44
+ }
45
+ ],
46
+ "fieldConfig": {
47
+ "defaults": { "unit": "short" },
48
+ "overrides": []
49
+ }
50
+ },
51
+ {
52
+ "title": "Total Tool Invocations",
53
+ "type": "stat",
54
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
55
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
56
+ "targets": [
57
+ {
58
+ "expr": "sum(gen_ai_agent_tool_usage_total)",
59
+ "refId": "A"
60
+ }
61
+ ],
62
+ "fieldConfig": {
63
+ "defaults": { "unit": "short" },
64
+ "overrides": []
65
+ }
66
+ },
67
+ {
68
+ "title": "Unique Tools Used",
69
+ "type": "stat",
70
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
71
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
72
+ "targets": [
73
+ {
74
+ "expr": "count(count by (gen_ai_agent_tool_name) (gen_ai_agent_tool_usage_total))",
75
+ "refId": "A"
76
+ }
77
+ ],
78
+ "fieldConfig": {
79
+ "defaults": { "unit": "short" },
80
+ "overrides": []
81
+ }
82
+ },
83
+ {
84
+ "title": "Agent Step Type Breakdown",
85
+ "description": "Distribution of think / act / observe / answer steps",
86
+ "type": "piechart",
87
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
88
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
89
+ "targets": [
90
+ {
91
+ "expr": "sum by (gen_ai_agent_step_type) (gen_ai_agent_steps_per_query_sum)",
92
+ "legendFormat": "{{gen_ai_agent_step_type}}",
93
+ "refId": "A"
94
+ }
95
+ ],
96
+ "options": {
97
+ "legend": {
98
+ "displayMode": "table",
99
+ "placement": "right",
100
+ "values": ["value", "percent"]
101
+ },
102
+ "pieType": "donut"
103
+ }
104
+ },
105
+ {
106
+ "title": "Tool Usage Frequency",
107
+ "description": "Invocations per tool name",
108
+ "type": "barchart",
109
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
110
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
111
+ "targets": [
112
+ {
113
+ "expr": "sort_desc(sum by (gen_ai_agent_tool_name) (gen_ai_agent_tool_usage_total))",
114
+ "legendFormat": "{{gen_ai_agent_tool_name}}",
115
+ "refId": "A"
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "title": "Steps per Query Over Time",
121
+ "type": "timeseries",
122
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 },
123
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
124
+ "targets": [
125
+ {
126
+ "expr": "sum(rate(gen_ai_agent_steps_per_query_sum[5m])) / sum(rate(gen_ai_agent_steps_per_query_count[5m]))",
127
+ "legendFormat": "avg steps/query",
128
+ "refId": "avg"
129
+ },
130
+ {
131
+ "expr": "histogram_quantile(0.95, sum by (le) (rate(gen_ai_agent_steps_per_query_bucket[5m])))",
132
+ "legendFormat": "p95 steps/query",
133
+ "refId": "p95"
134
+ }
135
+ ],
136
+ "fieldConfig": {
137
+ "defaults": {
138
+ "unit": "short",
139
+ "custom": { "fillOpacity": 10 }
140
+ },
141
+ "overrides": []
142
+ }
143
+ },
144
+ {
145
+ "title": "Tool Invocations Over Time",
146
+ "type": "timeseries",
147
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 },
148
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
149
+ "targets": [
150
+ {
151
+ "expr": "sum by (gen_ai_agent_tool_name) (rate(gen_ai_agent_tool_usage_total[5m]))",
152
+ "legendFormat": "{{gen_ai_agent_tool_name}}",
153
+ "refId": "A"
154
+ }
155
+ ],
156
+ "fieldConfig": {
157
+ "defaults": {
158
+ "unit": "reqps",
159
+ "custom": { "fillOpacity": 20, "stacking": { "mode": "normal" } }
160
+ },
161
+ "overrides": []
162
+ }
163
+ },
164
+ {
165
+ "title": "Steps Distribution Histogram",
166
+ "description": "How many steps do agent queries typically take?",
167
+ "type": "histogram",
168
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 },
169
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
170
+ "targets": [
171
+ {
172
+ "expr": "sum by (le) (gen_ai_agent_steps_per_query_bucket)",
173
+ "format": "heatmap",
174
+ "refId": "A"
175
+ }
176
+ ],
177
+ "fieldConfig": {
178
+ "defaults": {
179
+ "custom": { "fillOpacity": 80 }
180
+ },
181
+ "overrides": []
182
+ }
183
+ }
184
+ ],
185
+ "schemaVersion": 39,
186
+ "tags": ["toad-eye", "agent"],
187
+ "templating": {
188
+ "list": [
189
+ {
190
+ "name": "tool",
191
+ "type": "query",
192
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
193
+ "query": "label_values(gen_ai_agent_tool_usage_total, gen_ai_agent_tool_name)",
194
+ "includeAll": true,
195
+ "multi": true,
196
+ "current": { "text": "All", "value": "$__all" },
197
+ "regex": ""
198
+ }
199
+ ]
200
+ },
201
+ "time": { "from": "now-6h", "to": "now" },
202
+ "timepicker": {},
203
+ "timezone": "utc",
204
+ "title": "toad-eye / Agent Workflow",
205
+ "uid": "toad-eye-agent-workflow",
206
+ "version": 1
207
+ }
@@ -8,6 +8,30 @@ processors:
8
8
  batch:
9
9
  timeout: 1s
10
10
 
11
+ # Tail-based sampling: keeps 100% errors + slow traces, samples healthy traffic.
12
+ # Only applies to traces pipeline — metrics always get 100%.
13
+ # To disable: remove tail_sampling from traces pipeline processors.
14
+ tail_sampling:
15
+ decision_wait: 10s
16
+ num_traces: 100000
17
+ expected_new_traces_per_sec: 100
18
+ policies:
19
+ # Keep all traces with errors
20
+ - name: errors-always
21
+ type: status_code
22
+ status_code:
23
+ status_codes: [ERROR]
24
+ # Keep all slow traces (> 2s)
25
+ - name: slow-traces
26
+ type: latency
27
+ latency:
28
+ threshold_ms: 2000
29
+ # Sample 10% of remaining healthy traffic
30
+ - name: probabilistic-fallback
31
+ type: probabilistic
32
+ probabilistic:
33
+ sampling_percentage: 10
34
+
11
35
  exporters:
12
36
  otlphttp/jaeger:
13
37
  endpoint: http://jaeger:4318
@@ -21,7 +45,7 @@ service:
21
45
  pipelines:
22
46
  traces:
23
47
  receivers: [otlp]
24
- processors: [batch]
48
+ processors: [tail_sampling, batch]
25
49
  exporters: [otlphttp/jaeger]
26
50
  metrics:
27
51
  receivers: [otlp]