toad-eye 2.0.0 → 2.1.1
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 +290 -109
- package/dist/agent.d.ts +41 -0
- package/dist/agent.js +91 -0
- package/dist/budget/error.d.ts +10 -0
- package/dist/budget/error.js +22 -0
- package/dist/budget/index.d.ts +3 -0
- package/dist/budget/index.js +2 -0
- package/dist/budget/tracker.d.ts +35 -0
- package/dist/budget/tracker.js +121 -0
- package/dist/budget/types.d.ts +34 -0
- package/dist/budget/types.js +1 -0
- package/dist/cli.js +66 -28
- package/dist/core/metrics.d.ts +14 -0
- package/dist/core/metrics.js +119 -0
- package/dist/{spans.d.ts → core/spans.d.ts} +3 -1
- package/dist/core/spans.js +143 -0
- package/dist/core/tracer.d.ts +9 -0
- package/dist/core/tracer.js +93 -0
- package/dist/drift/baseline.d.ts +16 -0
- package/dist/drift/baseline.js +16 -0
- package/dist/drift/cosine.d.ts +13 -0
- package/dist/drift/cosine.js +39 -0
- package/dist/drift/embedding-provider.d.ts +21 -0
- package/dist/drift/embedding-provider.js +8 -0
- package/dist/drift/index.d.ts +9 -0
- package/dist/drift/index.js +8 -0
- package/dist/drift/monitor.d.ts +29 -0
- package/dist/drift/monitor.js +47 -0
- package/dist/drift/openai-embeddings.d.ts +6 -0
- package/dist/drift/openai-embeddings.js +26 -0
- package/dist/export.d.ts +46 -0
- package/dist/export.js +108 -0
- package/dist/guard.d.ts +26 -0
- package/dist/guard.js +45 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.js +8 -3
- package/dist/instrumentations/anthropic.js +20 -1
- package/dist/instrumentations/create.js +130 -32
- package/dist/instrumentations/openai.js +38 -2
- package/dist/instrumentations/types.d.ts +9 -1
- package/dist/types/attributes.d.ts +12 -0
- package/dist/types/attributes.js +15 -0
- package/dist/types/config.d.ts +14 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/metrics.d.ts +8 -0
- package/dist/types/metrics.js +12 -0
- package/dist/types/spans.d.ts +21 -0
- package/package.json +28 -5
- package/templates/grafana/dashboards/finops-attribution.json +278 -0
- package/dist/metrics.d.ts +0 -6
- package/dist/metrics.js +0 -61
- package/dist/spans.js +0 -87
- package/dist/tracer.d.ts +0 -7
- package/dist/tracer.js +0 -55
- package/dist/types.d.ts +0 -104
- package/dist/types.js +0 -58
- /package/dist/{pricing.d.ts → core/pricing.d.ts} +0 -0
- /package/dist/{pricing.js → core/pricing.js} +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
{
|
|
2
|
+
"annotations": { "list": [] },
|
|
3
|
+
"editable": true,
|
|
4
|
+
"fiscalYearStartMonth": 0,
|
|
5
|
+
"graphTooltip": 1,
|
|
6
|
+
"id": null,
|
|
7
|
+
"links": [],
|
|
8
|
+
"panels": [
|
|
9
|
+
{
|
|
10
|
+
"title": "Total Spend",
|
|
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_client_request_cost_USD_sum)",
|
|
17
|
+
"refId": "A"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"fieldConfig": {
|
|
21
|
+
"defaults": {
|
|
22
|
+
"unit": "currencyUSD",
|
|
23
|
+
"thresholds": {
|
|
24
|
+
"steps": [
|
|
25
|
+
{ "color": "green", "value": null },
|
|
26
|
+
{ "color": "yellow", "value": 50 },
|
|
27
|
+
{ "color": "red", "value": 500 }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"overrides": []
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"title": "Projected Monthly Spend",
|
|
36
|
+
"description": "Linear projection based on last 7 days",
|
|
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(increase(gen_ai_client_request_cost_USD_sum[7d])) / 7 * 30",
|
|
43
|
+
"refId": "A"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"fieldConfig": {
|
|
47
|
+
"defaults": {
|
|
48
|
+
"unit": "currencyUSD",
|
|
49
|
+
"thresholds": {
|
|
50
|
+
"steps": [
|
|
51
|
+
{ "color": "green", "value": null },
|
|
52
|
+
{ "color": "yellow", "value": 200 },
|
|
53
|
+
{ "color": "red", "value": 1000 }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"overrides": []
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"title": "Requests Today",
|
|
62
|
+
"type": "stat",
|
|
63
|
+
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
|
|
64
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
65
|
+
"targets": [
|
|
66
|
+
{
|
|
67
|
+
"expr": "sum(increase(gen_ai_client_requests_total[24h]))",
|
|
68
|
+
"refId": "A"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"fieldConfig": {
|
|
72
|
+
"defaults": { "unit": "short" },
|
|
73
|
+
"overrides": []
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"title": "Avg Cost per Request",
|
|
78
|
+
"type": "stat",
|
|
79
|
+
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
|
|
80
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
81
|
+
"targets": [
|
|
82
|
+
{
|
|
83
|
+
"expr": "sum(gen_ai_client_request_cost_USD_sum) / sum(gen_ai_client_request_cost_USD_count) or vector(0)",
|
|
84
|
+
"refId": "A"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"fieldConfig": {
|
|
88
|
+
"defaults": { "unit": "currencyUSD", "decimals": 4 },
|
|
89
|
+
"overrides": []
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"title": "Cost by Team",
|
|
94
|
+
"type": "piechart",
|
|
95
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
|
|
96
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
97
|
+
"targets": [
|
|
98
|
+
{
|
|
99
|
+
"expr": "sum by (toad_eye_team) (gen_ai_client_request_cost_USD_sum{toad_eye_team=~\"$team\"})",
|
|
100
|
+
"legendFormat": "{{toad_eye_team}}",
|
|
101
|
+
"refId": "A"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"options": {
|
|
105
|
+
"legend": {
|
|
106
|
+
"displayMode": "table",
|
|
107
|
+
"placement": "right",
|
|
108
|
+
"values": ["value", "percent"]
|
|
109
|
+
},
|
|
110
|
+
"pieType": "donut"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"title": "Cost by Model (stacked by Team)",
|
|
115
|
+
"type": "barchart",
|
|
116
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
|
|
117
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
118
|
+
"targets": [
|
|
119
|
+
{
|
|
120
|
+
"expr": "sum by (gen_ai_request_model, toad_eye_team) (gen_ai_client_request_cost_USD_sum{toad_eye_team=~\"$team\", gen_ai_request_model=~\"$model\"})",
|
|
121
|
+
"legendFormat": "{{gen_ai_request_model}} / {{toad_eye_team}}",
|
|
122
|
+
"refId": "A"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"options": {
|
|
126
|
+
"stacking": "normal"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"title": "Cost by Feature / Endpoint",
|
|
131
|
+
"type": "table",
|
|
132
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 },
|
|
133
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
134
|
+
"targets": [
|
|
135
|
+
{
|
|
136
|
+
"expr": "sort_desc(sum by (toad_eye_feature) (gen_ai_client_request_cost_USD_sum{toad_eye_feature=~\"$feature\"}))",
|
|
137
|
+
"format": "table",
|
|
138
|
+
"instant": true,
|
|
139
|
+
"refId": "A"
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"transformations": [
|
|
143
|
+
{
|
|
144
|
+
"id": "organize",
|
|
145
|
+
"options": {
|
|
146
|
+
"renameByName": {
|
|
147
|
+
"toad_eye_feature": "Feature",
|
|
148
|
+
"Value": "Cost (USD)"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"title": "Top 10 Users by Cost",
|
|
156
|
+
"type": "table",
|
|
157
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 },
|
|
158
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
159
|
+
"targets": [
|
|
160
|
+
{
|
|
161
|
+
"expr": "topk(10, sum by (toad_eye_user_id) (gen_ai_client_request_cost_USD_sum{toad_eye_user_id!=\"\"}))",
|
|
162
|
+
"format": "table",
|
|
163
|
+
"instant": true,
|
|
164
|
+
"refId": "A"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"transformations": [
|
|
168
|
+
{
|
|
169
|
+
"id": "organize",
|
|
170
|
+
"options": {
|
|
171
|
+
"renameByName": {
|
|
172
|
+
"toad_eye_user_id": "User ID",
|
|
173
|
+
"Value": "Cost (USD)"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"title": "Daily Cost Trend (by Team)",
|
|
181
|
+
"type": "timeseries",
|
|
182
|
+
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 20 },
|
|
183
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
184
|
+
"targets": [
|
|
185
|
+
{
|
|
186
|
+
"expr": "sum by (toad_eye_team) (increase(gen_ai_client_request_cost_USD_sum{toad_eye_team=~\"$team\"}[1d]))",
|
|
187
|
+
"legendFormat": "{{toad_eye_team}}",
|
|
188
|
+
"refId": "A"
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
"options": {
|
|
192
|
+
"tooltip": { "mode": "multi" }
|
|
193
|
+
},
|
|
194
|
+
"fieldConfig": {
|
|
195
|
+
"defaults": {
|
|
196
|
+
"unit": "currencyUSD",
|
|
197
|
+
"custom": { "fillOpacity": 20, "stacking": { "mode": "normal" } }
|
|
198
|
+
},
|
|
199
|
+
"overrides": []
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"title": "What-If: Model Substitution Savings",
|
|
204
|
+
"description": "Current cost per model vs estimated cost if all traffic moved to cheapest model",
|
|
205
|
+
"type": "table",
|
|
206
|
+
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 28 },
|
|
207
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
208
|
+
"targets": [
|
|
209
|
+
{
|
|
210
|
+
"expr": "sum by (gen_ai_request_model) (gen_ai_client_request_cost_USD_sum)",
|
|
211
|
+
"format": "table",
|
|
212
|
+
"instant": true,
|
|
213
|
+
"refId": "current_cost"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"expr": "sum by (gen_ai_request_model) (gen_ai_client_token_usage_total)",
|
|
217
|
+
"format": "table",
|
|
218
|
+
"instant": true,
|
|
219
|
+
"refId": "total_tokens"
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
"transformations": [
|
|
223
|
+
{
|
|
224
|
+
"id": "organize",
|
|
225
|
+
"options": {
|
|
226
|
+
"renameByName": {
|
|
227
|
+
"gen_ai_request_model": "Model",
|
|
228
|
+
"Value #current_cost": "Current Cost (USD)",
|
|
229
|
+
"Value #total_tokens": "Total Tokens"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"schemaVersion": 39,
|
|
237
|
+
"tags": ["toad-eye", "finops", "cost"],
|
|
238
|
+
"templating": {
|
|
239
|
+
"list": [
|
|
240
|
+
{
|
|
241
|
+
"name": "team",
|
|
242
|
+
"type": "query",
|
|
243
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
244
|
+
"query": "label_values(gen_ai_client_request_cost_USD_sum, toad_eye_team)",
|
|
245
|
+
"includeAll": true,
|
|
246
|
+
"multi": true,
|
|
247
|
+
"current": { "text": "All", "value": "$__all" },
|
|
248
|
+
"regex": ""
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "feature",
|
|
252
|
+
"type": "query",
|
|
253
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
254
|
+
"query": "label_values(gen_ai_client_request_cost_USD_sum, toad_eye_feature)",
|
|
255
|
+
"includeAll": true,
|
|
256
|
+
"multi": true,
|
|
257
|
+
"current": { "text": "All", "value": "$__all" },
|
|
258
|
+
"regex": ""
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "model",
|
|
262
|
+
"type": "query",
|
|
263
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
264
|
+
"query": "label_values(gen_ai_client_request_cost_USD_sum, gen_ai_request_model)",
|
|
265
|
+
"includeAll": true,
|
|
266
|
+
"multi": true,
|
|
267
|
+
"current": { "text": "All", "value": "$__all" },
|
|
268
|
+
"regex": ""
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
"time": { "from": "now-7d", "to": "now" },
|
|
273
|
+
"timepicker": {},
|
|
274
|
+
"timezone": "utc",
|
|
275
|
+
"title": "toad-eye / FinOps Attribution",
|
|
276
|
+
"uid": "toad-eye-finops",
|
|
277
|
+
"version": 1
|
|
278
|
+
}
|
package/dist/metrics.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare function initMetrics(): void;
|
|
2
|
-
export declare function recordRequestDuration(ms: number, provider: string, model: string): void;
|
|
3
|
-
export declare function recordRequestCost(usd: number, provider: string, model: string): void;
|
|
4
|
-
export declare function recordTokens(count: number, provider: string, model: string): void;
|
|
5
|
-
export declare function recordRequest(provider: string, model: string): void;
|
|
6
|
-
export declare function recordError(provider: string, model: string): void;
|
package/dist/metrics.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { metrics } from "@opentelemetry/api";
|
|
2
|
-
import { GEN_AI_ATTRS, GEN_AI_METRICS, INSTRUMENTATION_NAME, } from "./types/index.js";
|
|
3
|
-
let requestDuration;
|
|
4
|
-
let requestCost;
|
|
5
|
-
let tokenUsage;
|
|
6
|
-
let requestsTotal;
|
|
7
|
-
let errorsTotal;
|
|
8
|
-
let initialized = false;
|
|
9
|
-
export function initMetrics() {
|
|
10
|
-
if (initialized)
|
|
11
|
-
return;
|
|
12
|
-
const meter = metrics.getMeter(INSTRUMENTATION_NAME);
|
|
13
|
-
requestDuration = meter.createHistogram(GEN_AI_METRICS.REQUEST_DURATION, {
|
|
14
|
-
description: "GenAI operation duration in milliseconds",
|
|
15
|
-
unit: "ms",
|
|
16
|
-
});
|
|
17
|
-
requestCost = meter.createHistogram(GEN_AI_METRICS.REQUEST_COST, {
|
|
18
|
-
description: "GenAI request cost in USD",
|
|
19
|
-
unit: "USD",
|
|
20
|
-
});
|
|
21
|
-
tokenUsage = meter.createCounter(GEN_AI_METRICS.TOKEN_USAGE, {
|
|
22
|
-
description: "Total tokens consumed",
|
|
23
|
-
});
|
|
24
|
-
requestsTotal = meter.createCounter(GEN_AI_METRICS.REQUESTS, {
|
|
25
|
-
description: "Total GenAI requests",
|
|
26
|
-
});
|
|
27
|
-
errorsTotal = meter.createCounter(GEN_AI_METRICS.ERRORS, {
|
|
28
|
-
description: "Total failed GenAI requests",
|
|
29
|
-
});
|
|
30
|
-
initialized = true;
|
|
31
|
-
}
|
|
32
|
-
export function recordRequestDuration(ms, provider, model) {
|
|
33
|
-
requestDuration.record(ms, {
|
|
34
|
-
[GEN_AI_ATTRS.PROVIDER]: provider,
|
|
35
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: model,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
export function recordRequestCost(usd, provider, model) {
|
|
39
|
-
requestCost.record(usd, {
|
|
40
|
-
[GEN_AI_ATTRS.PROVIDER]: provider,
|
|
41
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: model,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
export function recordTokens(count, provider, model) {
|
|
45
|
-
tokenUsage.add(count, {
|
|
46
|
-
[GEN_AI_ATTRS.PROVIDER]: provider,
|
|
47
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: model,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
export function recordRequest(provider, model) {
|
|
51
|
-
requestsTotal.add(1, {
|
|
52
|
-
[GEN_AI_ATTRS.PROVIDER]: provider,
|
|
53
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: model,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
export function recordError(provider, model) {
|
|
57
|
-
errorsTotal.add(1, {
|
|
58
|
-
[GEN_AI_ATTRS.PROVIDER]: provider,
|
|
59
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: model,
|
|
60
|
-
});
|
|
61
|
-
}
|
package/dist/spans.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
3
|
-
import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "./types/index.js";
|
|
4
|
-
import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, } from "./metrics.js";
|
|
5
|
-
import { getConfig } from "./tracer.js";
|
|
6
|
-
const tracer = trace.getTracer(INSTRUMENTATION_NAME);
|
|
7
|
-
function sha256(text) {
|
|
8
|
-
return createHash("sha256").update(text).digest("hex");
|
|
9
|
-
}
|
|
10
|
-
function processContent(text) {
|
|
11
|
-
const config = getConfig();
|
|
12
|
-
if (config?.recordContent === false)
|
|
13
|
-
return undefined;
|
|
14
|
-
let processed = text;
|
|
15
|
-
if (config?.redactPatterns?.length) {
|
|
16
|
-
for (const pattern of config.redactPatterns) {
|
|
17
|
-
processed = processed.replace(pattern, "[REDACTED]");
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (config?.hashContent) {
|
|
21
|
-
return `sha256:${sha256(processed)}`;
|
|
22
|
-
}
|
|
23
|
-
return processed;
|
|
24
|
-
}
|
|
25
|
-
function resolveSessionId() {
|
|
26
|
-
const config = getConfig();
|
|
27
|
-
return config?.sessionExtractor?.() ?? config?.sessionId;
|
|
28
|
-
}
|
|
29
|
-
function setBaseAttributes(span, input) {
|
|
30
|
-
const prompt = processContent(input.prompt);
|
|
31
|
-
const sessionId = resolveSessionId();
|
|
32
|
-
span.setAttributes({
|
|
33
|
-
[GEN_AI_ATTRS.PROVIDER]: input.provider,
|
|
34
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]: input.model,
|
|
35
|
-
[GEN_AI_ATTRS.TEMPERATURE]: input.temperature ?? 1.0,
|
|
36
|
-
[GEN_AI_ATTRS.OPERATION]: "chat",
|
|
37
|
-
...(prompt !== undefined && { [GEN_AI_ATTRS.PROMPT]: prompt }),
|
|
38
|
-
...(sessionId !== undefined && { [GEN_AI_ATTRS.SESSION_ID]: sessionId }),
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
export async function traceLLMCall(input, fn) {
|
|
42
|
-
return tracer.startActiveSpan(`gen_ai.${input.provider}.${input.model}`, async (span) => {
|
|
43
|
-
const start = performance.now();
|
|
44
|
-
setBaseAttributes(span, input);
|
|
45
|
-
try {
|
|
46
|
-
const output = await fn();
|
|
47
|
-
const duration = performance.now() - start;
|
|
48
|
-
const completion = processContent(output.completion);
|
|
49
|
-
span.setAttributes({
|
|
50
|
-
...(completion !== undefined && {
|
|
51
|
-
[GEN_AI_ATTRS.COMPLETION]: completion,
|
|
52
|
-
}),
|
|
53
|
-
[GEN_AI_ATTRS.RESPONSE_MODEL]: input.model,
|
|
54
|
-
[GEN_AI_ATTRS.INPUT_TOKENS]: output.inputTokens,
|
|
55
|
-
[GEN_AI_ATTRS.OUTPUT_TOKENS]: output.outputTokens,
|
|
56
|
-
[GEN_AI_ATTRS.COST]: output.cost,
|
|
57
|
-
[GEN_AI_ATTRS.STATUS]: "success",
|
|
58
|
-
[GEN_AI_ATTRS.FINISH_REASONS]: ["stop"],
|
|
59
|
-
});
|
|
60
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
61
|
-
recordRequest(input.provider, input.model);
|
|
62
|
-
recordRequestDuration(duration, input.provider, input.model);
|
|
63
|
-
recordRequestCost(output.cost, input.provider, input.model);
|
|
64
|
-
recordTokens(output.inputTokens + output.outputTokens, input.provider, input.model);
|
|
65
|
-
return output;
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
const duration = performance.now() - start;
|
|
69
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
-
span.setAttributes({
|
|
71
|
-
[GEN_AI_ATTRS.INPUT_TOKENS]: 0,
|
|
72
|
-
[GEN_AI_ATTRS.OUTPUT_TOKENS]: 0,
|
|
73
|
-
[GEN_AI_ATTRS.COST]: 0,
|
|
74
|
-
[GEN_AI_ATTRS.STATUS]: "error",
|
|
75
|
-
[GEN_AI_ATTRS.ERROR]: message,
|
|
76
|
-
});
|
|
77
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
78
|
-
recordRequest(input.provider, input.model);
|
|
79
|
-
recordRequestDuration(duration, input.provider, input.model);
|
|
80
|
-
recordError(input.provider, input.model);
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
span.end();
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
package/dist/tracer.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { ToadEyeConfig } from "./types/index.js";
|
|
2
|
-
import "./instrumentations/openai.js";
|
|
3
|
-
import "./instrumentations/anthropic.js";
|
|
4
|
-
import "./instrumentations/gemini.js";
|
|
5
|
-
export declare function getConfig(): ToadEyeConfig | null;
|
|
6
|
-
export declare function initObservability(config: ToadEyeConfig): void;
|
|
7
|
-
export declare function shutdown(): Promise<void>;
|
package/dist/tracer.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
3
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
5
|
-
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
6
|
-
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
7
|
-
import { initMetrics } from "./metrics.js";
|
|
8
|
-
import { enableAll, disableAll } from "./instrumentations/registry.js";
|
|
9
|
-
// Side-effect imports: register provider instrumentations
|
|
10
|
-
import "./instrumentations/openai.js";
|
|
11
|
-
import "./instrumentations/anthropic.js";
|
|
12
|
-
import "./instrumentations/gemini.js";
|
|
13
|
-
const DEFAULT_ENDPOINT = "http://localhost:4318";
|
|
14
|
-
let sdk = null;
|
|
15
|
-
let currentConfig = null;
|
|
16
|
-
export function getConfig() {
|
|
17
|
-
return currentConfig;
|
|
18
|
-
}
|
|
19
|
-
export function initObservability(config) {
|
|
20
|
-
if (sdk)
|
|
21
|
-
return;
|
|
22
|
-
const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
|
|
23
|
-
const resource = resourceFromAttributes({
|
|
24
|
-
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
25
|
-
});
|
|
26
|
-
const traceExporter = new OTLPTraceExporter({
|
|
27
|
-
url: `${endpoint}/v1/traces`,
|
|
28
|
-
});
|
|
29
|
-
const metricExporter = new OTLPMetricExporter({
|
|
30
|
-
url: `${endpoint}/v1/metrics`,
|
|
31
|
-
});
|
|
32
|
-
const metricReader = new PeriodicExportingMetricReader({
|
|
33
|
-
exporter: metricExporter,
|
|
34
|
-
exportIntervalMillis: 5000,
|
|
35
|
-
});
|
|
36
|
-
sdk = new NodeSDK({
|
|
37
|
-
resource,
|
|
38
|
-
traceExporter,
|
|
39
|
-
metricReader,
|
|
40
|
-
});
|
|
41
|
-
sdk.start();
|
|
42
|
-
currentConfig = config;
|
|
43
|
-
initMetrics();
|
|
44
|
-
if (config.instrument?.length) {
|
|
45
|
-
enableAll(config.instrument);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export async function shutdown() {
|
|
49
|
-
if (!sdk)
|
|
50
|
-
return;
|
|
51
|
-
disableAll();
|
|
52
|
-
await sdk.shutdown();
|
|
53
|
-
sdk = null;
|
|
54
|
-
currentConfig = null;
|
|
55
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
export declare const INSTRUMENTATION_NAME = "toad-eye";
|
|
2
|
-
/**
|
|
3
|
-
* LLM providers supported by toad-eye.
|
|
4
|
-
* Values follow OTel GenAI semconv `gen_ai.provider.name`.
|
|
5
|
-
*/
|
|
6
|
-
export type LLMProvider = "anthropic" | "gemini" | "openai";
|
|
7
|
-
/**
|
|
8
|
-
* Span status
|
|
9
|
-
*/
|
|
10
|
-
export type SpanStatus = "success" | "error";
|
|
11
|
-
/**
|
|
12
|
-
* Attributes attached to every LLM span (trace).
|
|
13
|
-
* These become searchable fields in Jaeger and filterable dimensions in Grafana.
|
|
14
|
-
*/
|
|
15
|
-
export interface LLMSpanAttributes {
|
|
16
|
-
readonly provider: LLMProvider;
|
|
17
|
-
readonly model: string;
|
|
18
|
-
readonly prompt: string;
|
|
19
|
-
readonly completion: string;
|
|
20
|
-
readonly inputTokens: number;
|
|
21
|
-
readonly outputTokens: number;
|
|
22
|
-
readonly cost: number;
|
|
23
|
-
readonly temperature: number;
|
|
24
|
-
readonly status: SpanStatus;
|
|
25
|
-
readonly error?: string | undefined;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Metric names exported to Prometheus.
|
|
29
|
-
*
|
|
30
|
-
* Naming follows OTel GenAI semantic conventions where possible:
|
|
31
|
-
* - `gen_ai.client.*` prefix for client-side metrics
|
|
32
|
-
* - Prometheus auto-converts dots to underscores
|
|
33
|
-
* - Prometheus adds _total suffix to counters automatically
|
|
34
|
-
*/
|
|
35
|
-
export declare const GEN_AI_METRICS: {
|
|
36
|
-
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
37
|
-
readonly TOKEN_USAGE: "gen_ai.client.token.usage";
|
|
38
|
-
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
39
|
-
readonly REQUESTS: "gen_ai.client.requests";
|
|
40
|
-
readonly ERRORS: "gen_ai.client.errors";
|
|
41
|
-
};
|
|
42
|
-
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
43
|
-
export declare const LLM_METRICS: {
|
|
44
|
-
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
45
|
-
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
46
|
-
readonly TOKENS: "gen_ai.client.token.usage";
|
|
47
|
-
readonly REQUESTS: "gen_ai.client.requests";
|
|
48
|
-
readonly ERRORS: "gen_ai.client.errors";
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Span attribute keys following OTel GenAI semantic conventions.
|
|
52
|
-
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
53
|
-
*/
|
|
54
|
-
export declare const GEN_AI_ATTRS: {
|
|
55
|
-
readonly PROVIDER: "gen_ai.provider.name";
|
|
56
|
-
readonly REQUEST_MODEL: "gen_ai.request.model";
|
|
57
|
-
readonly RESPONSE_MODEL: "gen_ai.response.model";
|
|
58
|
-
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
59
|
-
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
60
|
-
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
61
|
-
readonly OPERATION: "gen_ai.operation.name";
|
|
62
|
-
readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
|
|
63
|
-
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
64
|
-
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
65
|
-
readonly COST: "gen_ai.toad_eye.cost";
|
|
66
|
-
readonly STATUS: "gen_ai.toad_eye.status";
|
|
67
|
-
readonly ERROR: "error.type";
|
|
68
|
-
readonly SESSION_ID: "session.id";
|
|
69
|
-
};
|
|
70
|
-
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
71
|
-
export declare const LLM_ATTRS: {
|
|
72
|
-
readonly PROVIDER: "gen_ai.provider.name";
|
|
73
|
-
readonly MODEL: "gen_ai.request.model";
|
|
74
|
-
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
75
|
-
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
76
|
-
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
77
|
-
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
78
|
-
readonly COST: "gen_ai.toad_eye.cost";
|
|
79
|
-
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
80
|
-
readonly STATUS: "gen_ai.toad_eye.status";
|
|
81
|
-
readonly ERROR: "error.type";
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Type-safe metric name
|
|
85
|
-
*/
|
|
86
|
-
export type MetricName = (typeof GEN_AI_METRICS)[keyof typeof GEN_AI_METRICS];
|
|
87
|
-
/**
|
|
88
|
-
* Configuration for initObservability().
|
|
89
|
-
*/
|
|
90
|
-
export interface ToadEyeConfig {
|
|
91
|
-
readonly serviceName: string;
|
|
92
|
-
readonly endpoint?: string | undefined;
|
|
93
|
-
/** Set to false to disable recording prompt/completion text in spans. */
|
|
94
|
-
readonly recordContent?: boolean | undefined;
|
|
95
|
-
/** Record SHA-256 hash of content instead of plain text. Allows prompt comparison without reading. */
|
|
96
|
-
readonly hashContent?: boolean | undefined;
|
|
97
|
-
/** Regex patterns to redact from prompt/completion text before recording. */
|
|
98
|
-
readonly redactPatterns?: readonly RegExp[] | undefined;
|
|
99
|
-
readonly instrument?: readonly LLMProvider[] | undefined;
|
|
100
|
-
/** Static session ID — all spans will carry this value as `session.id`. */
|
|
101
|
-
readonly sessionId?: string | undefined;
|
|
102
|
-
/** Dynamic session ID extractor — called per traceLLMCall to resolve session ID. */
|
|
103
|
-
readonly sessionExtractor?: (() => string | undefined) | undefined;
|
|
104
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export const INSTRUMENTATION_NAME = "toad-eye";
|
|
2
|
-
/**
|
|
3
|
-
* Metric names exported to Prometheus.
|
|
4
|
-
*
|
|
5
|
-
* Naming follows OTel GenAI semantic conventions where possible:
|
|
6
|
-
* - `gen_ai.client.*` prefix for client-side metrics
|
|
7
|
-
* - Prometheus auto-converts dots to underscores
|
|
8
|
-
* - Prometheus adds _total suffix to counters automatically
|
|
9
|
-
*/
|
|
10
|
-
export const GEN_AI_METRICS = {
|
|
11
|
-
REQUEST_DURATION: "gen_ai.client.operation.duration",
|
|
12
|
-
TOKEN_USAGE: "gen_ai.client.token.usage",
|
|
13
|
-
REQUEST_COST: "gen_ai.client.request.cost",
|
|
14
|
-
REQUESTS: "gen_ai.client.requests",
|
|
15
|
-
ERRORS: "gen_ai.client.errors",
|
|
16
|
-
};
|
|
17
|
-
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
18
|
-
export const LLM_METRICS = {
|
|
19
|
-
REQUEST_DURATION: GEN_AI_METRICS.REQUEST_DURATION,
|
|
20
|
-
REQUEST_COST: GEN_AI_METRICS.REQUEST_COST,
|
|
21
|
-
TOKENS: GEN_AI_METRICS.TOKEN_USAGE,
|
|
22
|
-
REQUESTS: GEN_AI_METRICS.REQUESTS,
|
|
23
|
-
ERRORS: GEN_AI_METRICS.ERRORS,
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Span attribute keys following OTel GenAI semantic conventions.
|
|
27
|
-
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
28
|
-
*/
|
|
29
|
-
export const GEN_AI_ATTRS = {
|
|
30
|
-
PROVIDER: "gen_ai.provider.name",
|
|
31
|
-
REQUEST_MODEL: "gen_ai.request.model",
|
|
32
|
-
RESPONSE_MODEL: "gen_ai.response.model",
|
|
33
|
-
INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
|
34
|
-
OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
|
|
35
|
-
TEMPERATURE: "gen_ai.request.temperature",
|
|
36
|
-
OPERATION: "gen_ai.operation.name",
|
|
37
|
-
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
|
38
|
-
// Not in OTel semconv yet — toad-eye extensions
|
|
39
|
-
PROMPT: "gen_ai.toad_eye.prompt",
|
|
40
|
-
COMPLETION: "gen_ai.toad_eye.completion",
|
|
41
|
-
COST: "gen_ai.toad_eye.cost",
|
|
42
|
-
STATUS: "gen_ai.toad_eye.status",
|
|
43
|
-
ERROR: "error.type",
|
|
44
|
-
SESSION_ID: "session.id",
|
|
45
|
-
};
|
|
46
|
-
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
47
|
-
export const LLM_ATTRS = {
|
|
48
|
-
PROVIDER: GEN_AI_ATTRS.PROVIDER,
|
|
49
|
-
MODEL: GEN_AI_ATTRS.REQUEST_MODEL,
|
|
50
|
-
PROMPT: GEN_AI_ATTRS.PROMPT,
|
|
51
|
-
COMPLETION: GEN_AI_ATTRS.COMPLETION,
|
|
52
|
-
INPUT_TOKENS: GEN_AI_ATTRS.INPUT_TOKENS,
|
|
53
|
-
OUTPUT_TOKENS: GEN_AI_ATTRS.OUTPUT_TOKENS,
|
|
54
|
-
COST: GEN_AI_ATTRS.COST,
|
|
55
|
-
TEMPERATURE: GEN_AI_ATTRS.TEMPERATURE,
|
|
56
|
-
STATUS: GEN_AI_ATTRS.STATUS,
|
|
57
|
-
ERROR: GEN_AI_ATTRS.ERROR,
|
|
58
|
-
};
|
|
File without changes
|
|
File without changes
|