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.
- package/README.md +166 -152
- package/dist/agent.d.ts +31 -28
- package/dist/agent.js +116 -35
- 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 +20 -1
- 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 +80 -21
- package/dist/core/tracer.d.ts +6 -3
- package/dist/core/tracer.js +49 -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 +20 -0
- package/dist/types/attributes.js +23 -1
- 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 +21 -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/dist/drift/monitor.js
CHANGED
|
@@ -39,13 +39,13 @@ export function createDriftMonitor(config) {
|
|
|
39
39
|
return {
|
|
40
40
|
check: performCheck,
|
|
41
41
|
checkInBackground(response, providerName, model) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
]).catch((err) => {
|
|
42
|
+
let timer;
|
|
43
|
+
const timeout = new Promise((_, reject) => (timer = setTimeout(() => reject(new Error("Drift check timeout (5s)")), 5000)));
|
|
44
|
+
void Promise.race([performCheck(response, providerName, model), timeout])
|
|
45
|
+
.catch((err) => {
|
|
47
46
|
console.warn(`[toad-eye] Drift check failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
-
})
|
|
47
|
+
})
|
|
48
|
+
.finally(() => clearTimeout(timer));
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
51
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,5 +13,10 @@ export type { ExportTraceOptions } from "./export.js";
|
|
|
13
13
|
export { calculateCost, setCustomPricing, getModelPricing, } from "./core/pricing.js";
|
|
14
14
|
export type { ModelPricing } from "./core/pricing.js";
|
|
15
15
|
export type { LLMCallInput, LLMCallOutput } from "./core/spans.js";
|
|
16
|
-
export type { ToadEyeConfig, LLMSpanAttributes, LLMProvider, SpanStatus, MetricName, AgentStepType, AgentStepInput, GuardMode, GuardResult, } from "./types/index.js";
|
|
17
|
-
export { GEN_AI_METRICS, GEN_AI_ATTRS
|
|
16
|
+
export type { ToadEyeConfig, LLMSpanAttributes, LLMProvider, SpanStatus, MetricName, AgentStepType, AgentStepInput, AgentQueryOptions, GuardMode, GuardResult, InstrumentTarget, SamplingConfig, } from "./types/index.js";
|
|
17
|
+
export { GEN_AI_METRICS, GEN_AI_ATTRS } from "./types/index.js";
|
|
18
|
+
/** @deprecated Use GEN_AI_METRICS instead */
|
|
19
|
+
export { LLM_METRICS } from "./types/index.js";
|
|
20
|
+
/** @deprecated Use GEN_AI_ATTRS instead */
|
|
21
|
+
export { LLM_ATTRS } from "./types/index.js";
|
|
22
|
+
export { ToadEyeAISpanProcessor, withToadEye } from "./vercel.js";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,9 @@ export { recordGuardResult } from "./guard.js";
|
|
|
7
7
|
export { createDriftMonitor, createOpenAIEmbeddingProvider, cosineSimilarity, cosineDrift, saveBaseline, loadBaseline, } from "./drift/index.js";
|
|
8
8
|
export { exportTrace, fetchTrace, traceToEvalYaml } from "./export.js";
|
|
9
9
|
export { calculateCost, setCustomPricing, getModelPricing, } from "./core/pricing.js";
|
|
10
|
-
export { GEN_AI_METRICS, GEN_AI_ATTRS
|
|
10
|
+
export { GEN_AI_METRICS, GEN_AI_ATTRS } from "./types/index.js";
|
|
11
|
+
/** @deprecated Use GEN_AI_METRICS instead */
|
|
12
|
+
export { LLM_METRICS } from "./types/index.js";
|
|
13
|
+
/** @deprecated Use GEN_AI_ATTRS instead */
|
|
14
|
+
export { LLM_ATTRS } from "./types/index.js";
|
|
15
|
+
export { ToadEyeAISpanProcessor, withToadEye } from "./vercel.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const anthropicInstrumentation: import("./types.js").Instrumentation;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { register } from "./registry.js";
|
|
2
1
|
import { createInstrumentation } from "./create.js";
|
|
3
2
|
function extractMessages(messages) {
|
|
4
3
|
if (!Array.isArray(messages))
|
|
@@ -59,8 +58,8 @@ const messagesCreate = {
|
|
|
59
58
|
}
|
|
60
59
|
},
|
|
61
60
|
};
|
|
62
|
-
|
|
61
|
+
export const anthropicInstrumentation = createInstrumentation({
|
|
63
62
|
name: "anthropic",
|
|
64
63
|
moduleName: "@anthropic-ai/sdk",
|
|
65
64
|
patches: [messagesCreate],
|
|
66
|
-
})
|
|
65
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { trace, context, diag, SpanStatusCode, } from "@opentelemetry/api";
|
|
3
|
-
import { traceLLMCall } from "../core/spans.js";
|
|
3
|
+
import { traceLLMCall, processContent } from "../core/spans.js";
|
|
4
4
|
import { calculateCost } from "../core/pricing.js";
|
|
5
|
-
import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, recordTimeToFirstToken, } from "../core/metrics.js";
|
|
5
|
+
import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, recordTimeToFirstToken, recordResponseEmpty, recordResponseLatencyPerToken, recordBudgetExceeded, recordBudgetDowngraded, } from "../core/metrics.js";
|
|
6
|
+
import { getConfig, getBudgetTracker } from "../core/tracer.js";
|
|
7
|
+
import { ToadBudgetExceededError } from "../budget/index.js";
|
|
6
8
|
import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "../types/index.js";
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
function isModuleInstalled(moduleName) {
|
|
@@ -64,17 +66,55 @@ function createStreamingHandler(
|
|
|
64
66
|
original, patch, providerName) {
|
|
65
67
|
const tracer = trace.getTracer(INSTRUMENTATION_NAME);
|
|
66
68
|
return async function handleStreaming(thisArg, body, rest) {
|
|
67
|
-
|
|
69
|
+
// Pass thisArg so extractRequest can access instance properties (e.g., Gemini model name)
|
|
70
|
+
const req = patch.extractRequest(body, thisArg);
|
|
68
71
|
const start = performance.now();
|
|
69
|
-
|
|
70
|
-
const
|
|
72
|
+
// Budget check BEFORE the LLM call — mirrors traceLLMCall behavior
|
|
73
|
+
const budget = getBudgetTracker();
|
|
74
|
+
const config = getConfig();
|
|
75
|
+
const userId = config?.attributes?.[GEN_AI_ATTRS.USER_ID];
|
|
76
|
+
const estimatedCost = budget ? calculateCost(req.model, 500, 200) : 0;
|
|
77
|
+
let effectiveProvider = providerName;
|
|
78
|
+
let effectiveModel = req.model;
|
|
79
|
+
if (budget) {
|
|
80
|
+
const override = budget.checkBefore(providerName, req.model, userId, estimatedCost);
|
|
81
|
+
if (override) {
|
|
82
|
+
effectiveProvider = override.provider;
|
|
83
|
+
effectiveModel = override.model;
|
|
84
|
+
recordBudgetDowngraded(override.budget);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const span = tracer.startSpan(`chat ${effectiveModel}`);
|
|
71
88
|
const ctx = trace.setSpan(context.active(), span);
|
|
89
|
+
const sessionId = config?.sessionExtractor?.() ?? config?.sessionId;
|
|
72
90
|
span.setAttributes({
|
|
73
|
-
[GEN_AI_ATTRS.PROVIDER]:
|
|
74
|
-
[GEN_AI_ATTRS.REQUEST_MODEL]:
|
|
91
|
+
[GEN_AI_ATTRS.PROVIDER]: effectiveProvider,
|
|
92
|
+
[GEN_AI_ATTRS.REQUEST_MODEL]: effectiveModel,
|
|
75
93
|
[GEN_AI_ATTRS.TEMPERATURE]: req.temperature ?? 1.0,
|
|
76
94
|
[GEN_AI_ATTRS.OPERATION]: "chat",
|
|
95
|
+
...(sessionId !== undefined && { [GEN_AI_ATTRS.SESSION_ID]: sessionId }),
|
|
77
96
|
});
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
response = await original.call(thisArg, body, ...rest);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const duration = performance.now() - start;
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
// Release budget reservation — no cost was incurred
|
|
105
|
+
if (budget)
|
|
106
|
+
budget.releaseReservation(estimatedCost);
|
|
107
|
+
span.setAttributes({
|
|
108
|
+
[GEN_AI_ATTRS.STATUS]: "error",
|
|
109
|
+
[GEN_AI_ATTRS.ERROR]: message,
|
|
110
|
+
});
|
|
111
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
112
|
+
span.end();
|
|
113
|
+
recordRequest(effectiveProvider, effectiveModel);
|
|
114
|
+
recordRequestDuration(duration, effectiveProvider, effectiveModel);
|
|
115
|
+
recordError(effectiveProvider, effectiveModel);
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
78
118
|
// Some SDKs (Gemini) return { stream: AsyncIterable } instead of a direct AsyncIterable
|
|
79
119
|
const resp = response;
|
|
80
120
|
const hasStreamProp = resp != null &&
|
|
@@ -87,13 +127,16 @@ original, patch, providerName) {
|
|
|
87
127
|
? resp["stream"]
|
|
88
128
|
: response;
|
|
89
129
|
const wrapped = context.bind(ctx, wrapAsyncIterable(streamIterable, (acc, chunk) => patch.accumulateChunk(acc, chunk), () => {
|
|
90
|
-
|
|
91
|
-
recordTimeToFirstToken(ttft, providerName, req.model);
|
|
130
|
+
recordTimeToFirstToken(performance.now() - start, effectiveProvider, effectiveModel);
|
|
92
131
|
}, (acc) => {
|
|
93
132
|
const duration = performance.now() - start;
|
|
94
|
-
const cost = calculateCost(
|
|
133
|
+
const cost = calculateCost(effectiveModel, acc.inputTokens, acc.outputTokens);
|
|
134
|
+
const processedCompletion = processContent(acc.completion);
|
|
95
135
|
span.setAttributes({
|
|
96
|
-
|
|
136
|
+
...(processedCompletion !== undefined && {
|
|
137
|
+
[GEN_AI_ATTRS.COMPLETION]: processedCompletion,
|
|
138
|
+
}),
|
|
139
|
+
[GEN_AI_ATTRS.RESPONSE_MODEL]: effectiveModel,
|
|
97
140
|
[GEN_AI_ATTRS.INPUT_TOKENS]: acc.inputTokens,
|
|
98
141
|
[GEN_AI_ATTRS.OUTPUT_TOKENS]: acc.outputTokens,
|
|
99
142
|
[GEN_AI_ATTRS.COST]: cost,
|
|
@@ -102,22 +145,40 @@ original, patch, providerName) {
|
|
|
102
145
|
});
|
|
103
146
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
104
147
|
span.end();
|
|
105
|
-
recordRequest(
|
|
106
|
-
recordRequestDuration(duration,
|
|
107
|
-
recordRequestCost(cost,
|
|
108
|
-
recordTokens(acc.inputTokens + acc.outputTokens,
|
|
148
|
+
recordRequest(effectiveProvider, effectiveModel);
|
|
149
|
+
recordRequestDuration(duration, effectiveProvider, effectiveModel);
|
|
150
|
+
recordRequestCost(cost, effectiveProvider, effectiveModel);
|
|
151
|
+
recordTokens(acc.inputTokens + acc.outputTokens, effectiveProvider, effectiveModel);
|
|
152
|
+
// Quality metrics
|
|
153
|
+
if (acc.completion.trim() === "") {
|
|
154
|
+
recordResponseEmpty(effectiveProvider, effectiveModel);
|
|
155
|
+
}
|
|
156
|
+
if (acc.outputTokens > 0) {
|
|
157
|
+
recordResponseLatencyPerToken(duration / acc.outputTokens, effectiveProvider, effectiveModel);
|
|
158
|
+
}
|
|
159
|
+
// Budget recording — releases the reservation made in checkBefore
|
|
160
|
+
if (budget) {
|
|
161
|
+
const exceeded = budget.recordCost(cost, effectiveModel, userId, estimatedCost);
|
|
162
|
+
if (exceeded) {
|
|
163
|
+
recordBudgetExceeded(exceeded.budget);
|
|
164
|
+
console.warn(`toad-eye: ${exceeded.budget} budget exceeded — limit $${exceeded.limit}, current $${exceeded.current.toFixed(2)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
109
167
|
}, (err) => {
|
|
110
168
|
const duration = performance.now() - start;
|
|
111
169
|
const message = err instanceof Error ? err.message : String(err);
|
|
170
|
+
// Release budget reservation on stream error
|
|
171
|
+
if (budget)
|
|
172
|
+
budget.releaseReservation(estimatedCost);
|
|
112
173
|
span.setAttributes({
|
|
113
174
|
[GEN_AI_ATTRS.STATUS]: "error",
|
|
114
175
|
[GEN_AI_ATTRS.ERROR]: message,
|
|
115
176
|
});
|
|
116
177
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
117
178
|
span.end();
|
|
118
|
-
recordRequest(
|
|
119
|
-
recordRequestDuration(duration,
|
|
120
|
-
recordError(
|
|
179
|
+
recordRequest(effectiveProvider, effectiveModel);
|
|
180
|
+
recordRequestDuration(duration, effectiveProvider, effectiveModel);
|
|
181
|
+
recordError(effectiveProvider, effectiveModel);
|
|
121
182
|
}));
|
|
122
183
|
// Proxy preserves the original object shape (getters, private fields, methods)
|
|
123
184
|
// while intercepting only the async iterator for instrumentation
|
|
@@ -144,7 +205,7 @@ original, patch, providerName) {
|
|
|
144
205
|
if (streamHandler && patch.isStreaming?.(body)) {
|
|
145
206
|
return streamHandler(this, body, rest);
|
|
146
207
|
}
|
|
147
|
-
const req = patch.extractRequest(body);
|
|
208
|
+
const req = patch.extractRequest(body, this);
|
|
148
209
|
return traceLLMCall({
|
|
149
210
|
provider: providerName,
|
|
150
211
|
model: req.model,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const geminiInstrumentation: import("./types.js").Instrumentation;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { register } from "./registry.js";
|
|
2
1
|
import { createInstrumentation } from "./create.js";
|
|
3
2
|
function extractPrompt(request) {
|
|
4
3
|
if (typeof request === "string")
|
|
@@ -18,10 +17,10 @@ function extractPrompt(request) {
|
|
|
18
17
|
const generateContent = {
|
|
19
18
|
getPrototype: (sdk) => sdk?.GenerativeModel?.prototype,
|
|
20
19
|
method: "generateContent",
|
|
21
|
-
extractRequest(body) {
|
|
20
|
+
extractRequest(body, thisArg) {
|
|
22
21
|
return {
|
|
23
22
|
prompt: extractPrompt(body),
|
|
24
|
-
model: "unknown",
|
|
23
|
+
model: thisArg?.model ?? "unknown",
|
|
25
24
|
};
|
|
26
25
|
},
|
|
27
26
|
extractResponse: (response) => {
|
|
@@ -43,10 +42,10 @@ const generateContent = {
|
|
|
43
42
|
const generateContentStream = {
|
|
44
43
|
getPrototype: (sdk) => sdk?.GenerativeModel?.prototype,
|
|
45
44
|
method: "generateContentStream",
|
|
46
|
-
extractRequest(body) {
|
|
45
|
+
extractRequest(body, thisArg) {
|
|
47
46
|
return {
|
|
48
47
|
prompt: extractPrompt(body),
|
|
49
|
-
model: "unknown",
|
|
48
|
+
model: thisArg?.model ?? "unknown",
|
|
50
49
|
};
|
|
51
50
|
},
|
|
52
51
|
extractResponse: () => ({
|
|
@@ -74,8 +73,8 @@ const generateContentStream = {
|
|
|
74
73
|
}
|
|
75
74
|
},
|
|
76
75
|
};
|
|
77
|
-
|
|
76
|
+
export const geminiInstrumentation = createInstrumentation({
|
|
78
77
|
name: "gemini",
|
|
79
78
|
moduleName: "@google/generative-ai",
|
|
80
79
|
patches: [generateContent, generateContentStream],
|
|
81
|
-
})
|
|
80
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const openaiInstrumentation: import("./types.js").Instrumentation;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { register } from "./registry.js";
|
|
2
1
|
import { createInstrumentation } from "./create.js";
|
|
3
2
|
function extractMessages(messages) {
|
|
4
3
|
if (!Array.isArray(messages))
|
|
@@ -79,8 +78,8 @@ const embeddings = {
|
|
|
79
78
|
};
|
|
80
79
|
},
|
|
81
80
|
};
|
|
82
|
-
|
|
81
|
+
export const openaiInstrumentation = createInstrumentation({
|
|
83
82
|
name: "openai",
|
|
84
83
|
moduleName: "openai",
|
|
85
84
|
patches: [chatCompletions, embeddings],
|
|
86
|
-
})
|
|
85
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { LLMProvider } from "../types/index.js";
|
|
2
2
|
import type { Instrumentation } from "./types.js";
|
|
3
3
|
export declare function register(inst: Instrumentation): void;
|
|
4
|
-
|
|
4
|
+
/** Skip lazy registration (used in tests that register mocks manually). */
|
|
5
|
+
export declare function skipAutoRegister(): void;
|
|
6
|
+
export declare function enableAll(providers: readonly LLMProvider[]): Promise<void>;
|
|
5
7
|
export declare function disableAll(): void;
|
|
@@ -1,25 +1,48 @@
|
|
|
1
|
-
import { diag } from "@opentelemetry/api";
|
|
2
1
|
const instrumentations = new Map();
|
|
3
2
|
const active = new Set();
|
|
4
3
|
export function register(inst) {
|
|
5
4
|
instrumentations.set(inst.name, inst);
|
|
6
5
|
}
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Lazy registration — loads provider modules and registers instrumentations
|
|
8
|
+
* only when enableAll() is first called. This avoids pulling in create.ts,
|
|
9
|
+
* metrics.ts, and node:module on every `import` of toad-eye.
|
|
10
|
+
*/
|
|
11
|
+
let registered = false;
|
|
12
|
+
/** Skip lazy registration (used in tests that register mocks manually). */
|
|
13
|
+
export function skipAutoRegister() {
|
|
14
|
+
registered = true;
|
|
15
|
+
}
|
|
16
|
+
async function ensureRegistered() {
|
|
17
|
+
if (registered)
|
|
18
|
+
return;
|
|
19
|
+
registered = true;
|
|
20
|
+
const [openai, anthropic, gemini] = await Promise.all([
|
|
21
|
+
import("./openai.js"),
|
|
22
|
+
import("./anthropic.js"),
|
|
23
|
+
import("./gemini.js"),
|
|
24
|
+
]);
|
|
25
|
+
register(openai.openaiInstrumentation);
|
|
26
|
+
register(anthropic.anthropicInstrumentation);
|
|
27
|
+
register(gemini.geminiInstrumentation);
|
|
28
|
+
}
|
|
29
|
+
export async function enableAll(providers) {
|
|
30
|
+
await ensureRegistered();
|
|
31
|
+
const validProviders = Array.from(instrumentations.keys()).join(", ");
|
|
8
32
|
for (const name of providers) {
|
|
9
33
|
if (active.has(name))
|
|
10
34
|
continue;
|
|
11
35
|
const inst = instrumentations.get(name);
|
|
12
36
|
if (!inst) {
|
|
13
|
-
|
|
37
|
+
console.warn(`toad-eye: unknown provider "${name}" — valid providers: ${validProviders}`);
|
|
14
38
|
continue;
|
|
15
39
|
}
|
|
16
40
|
const patched = inst.enable();
|
|
17
41
|
if (patched) {
|
|
18
42
|
active.add(name);
|
|
19
|
-
diag.debug(`toad-eye: auto-instrumented ${name}`);
|
|
20
43
|
}
|
|
21
44
|
else {
|
|
22
|
-
|
|
45
|
+
console.warn(`toad-eye: "${name}" SDK not found — install it to enable auto-instrumentation: npm install ${name === "gemini" ? "@google/generative-ai" : name}`);
|
|
23
46
|
}
|
|
24
47
|
}
|
|
25
48
|
}
|
|
@@ -18,8 +18,10 @@ export interface PatchTarget {
|
|
|
18
18
|
getPrototype: (sdk: any) => any | undefined;
|
|
19
19
|
/** Method name on the prototype to patch */
|
|
20
20
|
method: string;
|
|
21
|
-
/** Extract LLMCallInput fields from the request arguments
|
|
22
|
-
|
|
21
|
+
/** Extract LLMCallInput fields from the request arguments.
|
|
22
|
+
* thisArg is the SDK object instance — use it when the model name is not in the request body
|
|
23
|
+
* (e.g., Gemini's GenerativeModel stores it as this.model). */
|
|
24
|
+
extractRequest: (body: unknown, thisArg?: unknown) => {
|
|
23
25
|
prompt: string;
|
|
24
26
|
model: string;
|
|
25
27
|
temperature?: number;
|
|
@@ -15,10 +15,30 @@ export declare const GEN_AI_ATTRS: {
|
|
|
15
15
|
readonly OPERATION: "gen_ai.operation.name";
|
|
16
16
|
readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
|
|
17
17
|
readonly ERROR: "error.type";
|
|
18
|
+
readonly AGENT_NAME: "gen_ai.agent.name";
|
|
19
|
+
readonly AGENT_ID: "gen_ai.agent.id";
|
|
20
|
+
readonly TOOL_NAME: "gen_ai.tool.name";
|
|
21
|
+
readonly TOOL_TYPE: "gen_ai.tool.type";
|
|
22
|
+
readonly TOAD_AGENT_STEP_TYPE: "gen_ai.toad_eye.agent.step.type";
|
|
23
|
+
readonly TOAD_AGENT_STEP_NUMBER: "gen_ai.toad_eye.agent.step.number";
|
|
24
|
+
readonly TOAD_AGENT_STEP_CONTENT: "gen_ai.toad_eye.agent.step.content";
|
|
25
|
+
readonly TOAD_AGENT_HANDOFF_TO: "gen_ai.toad_eye.agent.handoff.to";
|
|
26
|
+
readonly TOAD_AGENT_HANDOFF_REASON: "gen_ai.toad_eye.agent.handoff.reason";
|
|
27
|
+
readonly TOAD_AGENT_LOOP_COUNT: "gen_ai.toad_eye.agent.loop_count";
|
|
28
|
+
/** @deprecated Use TOAD_AGENT_STEP_TYPE instead. Will be removed in v3.0. */
|
|
18
29
|
readonly AGENT_STEP_TYPE: "gen_ai.agent.step.type";
|
|
30
|
+
/** @deprecated Use TOAD_AGENT_STEP_NUMBER instead. Will be removed in v3.0. */
|
|
19
31
|
readonly AGENT_STEP_NUMBER: "gen_ai.agent.step.number";
|
|
32
|
+
/** @deprecated Use TOOL_NAME instead. Will be removed in v3.0. */
|
|
20
33
|
readonly AGENT_TOOL_NAME: "gen_ai.agent.tool.name";
|
|
34
|
+
/** @deprecated Use TOAD_AGENT_STEP_CONTENT instead. Will be removed in v3.0. */
|
|
21
35
|
readonly AGENT_STEP_CONTENT: "gen_ai.agent.step.content";
|
|
36
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_TO instead. Will be removed in v3.0. */
|
|
37
|
+
readonly AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to";
|
|
38
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_REASON instead. Will be removed in v3.0. */
|
|
39
|
+
readonly AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason";
|
|
40
|
+
/** @deprecated Use TOAD_AGENT_LOOP_COUNT instead. Will be removed in v3.0. */
|
|
41
|
+
readonly AGENT_LOOP_COUNT: "gen_ai.agent.loop_count";
|
|
22
42
|
readonly GUARD_MODE: "gen_ai.toad_eye.guard.mode";
|
|
23
43
|
readonly GUARD_PASSED: "gen_ai.toad_eye.guard.passed";
|
|
24
44
|
readonly GUARD_FAILURE_REASON: "gen_ai.toad_eye.guard.failure_reason";
|
package/dist/types/attributes.js
CHANGED
|
@@ -16,11 +16,33 @@ export const GEN_AI_ATTRS = {
|
|
|
16
16
|
OPERATION: "gen_ai.operation.name",
|
|
17
17
|
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
|
18
18
|
ERROR: "error.type",
|
|
19
|
-
//
|
|
19
|
+
// OTel GenAI agent attributes
|
|
20
|
+
AGENT_NAME: "gen_ai.agent.name",
|
|
21
|
+
AGENT_ID: "gen_ai.agent.id",
|
|
22
|
+
TOOL_NAME: "gen_ai.tool.name",
|
|
23
|
+
TOOL_TYPE: "gen_ai.tool.type",
|
|
24
|
+
// toad-eye ReAct extensions (gen_ai.toad_eye.agent.* — correct namespace)
|
|
25
|
+
TOAD_AGENT_STEP_TYPE: "gen_ai.toad_eye.agent.step.type",
|
|
26
|
+
TOAD_AGENT_STEP_NUMBER: "gen_ai.toad_eye.agent.step.number",
|
|
27
|
+
TOAD_AGENT_STEP_CONTENT: "gen_ai.toad_eye.agent.step.content",
|
|
28
|
+
TOAD_AGENT_HANDOFF_TO: "gen_ai.toad_eye.agent.handoff.to",
|
|
29
|
+
TOAD_AGENT_HANDOFF_REASON: "gen_ai.toad_eye.agent.handoff.reason",
|
|
30
|
+
TOAD_AGENT_LOOP_COUNT: "gen_ai.toad_eye.agent.loop_count",
|
|
31
|
+
// Agent step attributes — @deprecated aliases (will be removed in v3.0)
|
|
32
|
+
/** @deprecated Use TOAD_AGENT_STEP_TYPE instead. Will be removed in v3.0. */
|
|
20
33
|
AGENT_STEP_TYPE: "gen_ai.agent.step.type",
|
|
34
|
+
/** @deprecated Use TOAD_AGENT_STEP_NUMBER instead. Will be removed in v3.0. */
|
|
21
35
|
AGENT_STEP_NUMBER: "gen_ai.agent.step.number",
|
|
36
|
+
/** @deprecated Use TOOL_NAME instead. Will be removed in v3.0. */
|
|
22
37
|
AGENT_TOOL_NAME: "gen_ai.agent.tool.name",
|
|
38
|
+
/** @deprecated Use TOAD_AGENT_STEP_CONTENT instead. Will be removed in v3.0. */
|
|
23
39
|
AGENT_STEP_CONTENT: "gen_ai.agent.step.content",
|
|
40
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_TO instead. Will be removed in v3.0. */
|
|
41
|
+
AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to",
|
|
42
|
+
/** @deprecated Use TOAD_AGENT_HANDOFF_REASON instead. Will be removed in v3.0. */
|
|
43
|
+
AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason",
|
|
44
|
+
/** @deprecated Use TOAD_AGENT_LOOP_COUNT instead. Will be removed in v3.0. */
|
|
45
|
+
AGENT_LOOP_COUNT: "gen_ai.agent.loop_count",
|
|
24
46
|
// Guard (shadow mode) attributes
|
|
25
47
|
GUARD_MODE: "gen_ai.toad_eye.guard.mode",
|
|
26
48
|
GUARD_PASSED: "gen_ai.toad_eye.guard.passed",
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InstrumentTarget } from "./providers.js";
|
|
2
2
|
import type { BudgetConfig, BudgetExceededMode, DowngradeCallback } from "../budget/types.js";
|
|
3
|
+
/** Sampling configuration. Tail sampling runs in OTel Collector, SDK sends all spans. */
|
|
4
|
+
export interface SamplingConfig {
|
|
5
|
+
/** SDK-side sampling rate (0.0–1.0). Default: 1.0 (send everything to Collector). */
|
|
6
|
+
readonly sdkRate?: number | undefined;
|
|
7
|
+
/** Collector-side tail sampling settings (used when generating otel-collector.yml). */
|
|
8
|
+
readonly collector?: {
|
|
9
|
+
/** Keep 100% of error traces. Default: true */
|
|
10
|
+
readonly keepErrors?: boolean | undefined;
|
|
11
|
+
/** Keep 100% of traces slower than this (ms). Default: 2000 */
|
|
12
|
+
readonly highLatencyMs?: number | undefined;
|
|
13
|
+
/** Sample this % of healthy traffic (0–100). Default: 10 */
|
|
14
|
+
readonly healthyRate?: number | undefined;
|
|
15
|
+
} | undefined;
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* Configuration for initObservability().
|
|
5
19
|
* This is what the user passes when connecting toad-eye to their service.
|
|
@@ -20,7 +34,13 @@ export interface ToadEyeConfig {
|
|
|
20
34
|
readonly salt?: string | undefined;
|
|
21
35
|
/** Regex patterns to redact from prompt/completion text before recording. */
|
|
22
36
|
readonly redactPatterns?: readonly RegExp[] | undefined;
|
|
23
|
-
|
|
37
|
+
/** Enable built-in redaction patterns for common PII: email, SSN, credit card, phone. */
|
|
38
|
+
readonly redactDefaults?: boolean | undefined;
|
|
39
|
+
/** Log what was masked to console (for debugging redaction config). No data is sent externally. */
|
|
40
|
+
readonly auditMasking?: boolean | undefined;
|
|
41
|
+
/** Configure trace sampling. Tail sampling happens in OTel Collector, not SDK. */
|
|
42
|
+
readonly sampling?: SamplingConfig | undefined;
|
|
43
|
+
readonly instrument?: readonly InstrumentTarget[] | undefined;
|
|
24
44
|
/** Budget limits — daily, per-user, per-model spend caps in USD. */
|
|
25
45
|
readonly budgets?: BudgetConfig | undefined;
|
|
26
46
|
/** What to do when budget is exceeded: 'warn' (log), 'block' (throw), 'downgrade' (callback). */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
|
|
10
10
|
*/
|
|
11
11
|
export declare const INSTRUMENTATION_NAME = "toad-eye";
|
|
12
|
-
export type { LLMProvider } from "./providers.js";
|
|
13
|
-
export type { ToadEyeConfig } from "./config.js";
|
|
12
|
+
export type { LLMProvider, InstrumentTarget } from "./providers.js";
|
|
13
|
+
export type { ToadEyeConfig, SamplingConfig } from "./config.js";
|
|
14
14
|
export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
|
|
15
15
|
export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
|
|
16
16
|
export type { MetricName } from "./metrics.js";
|
|
17
|
-
export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, GuardMode, GuardResult, } from "./spans.js";
|
|
17
|
+
export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, AgentQueryOptions, AgentQueryInput, GuardMode, GuardResult, } from "./spans.js";
|
package/dist/types/metrics.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare const GEN_AI_METRICS: {
|
|
|
21
21
|
readonly BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded";
|
|
22
22
|
readonly BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked";
|
|
23
23
|
readonly BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded";
|
|
24
|
+
readonly RESPONSE_EMPTY: "gen_ai.toad_eye.response.empty";
|
|
25
|
+
readonly RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token";
|
|
24
26
|
};
|
|
25
27
|
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
26
28
|
export declare const LLM_METRICS: {
|
package/dist/types/metrics.js
CHANGED
|
@@ -26,6 +26,9 @@ export const GEN_AI_METRICS = {
|
|
|
26
26
|
BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded",
|
|
27
27
|
BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked",
|
|
28
28
|
BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded",
|
|
29
|
+
// Response quality proxy metrics
|
|
30
|
+
RESPONSE_EMPTY: "gen_ai.toad_eye.response.empty",
|
|
31
|
+
RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token",
|
|
29
32
|
};
|
|
30
33
|
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
31
34
|
export const LLM_METRICS = {
|
|
@@ -2,4 +2,9 @@
|
|
|
2
2
|
* LLM providers supported by toad-eye.
|
|
3
3
|
* Values follow OTel GenAI semconv `gen_ai.provider.name`.
|
|
4
4
|
*/
|
|
5
|
-
export type LLMProvider = "anthropic" | "gemini" | "openai";
|
|
5
|
+
export type LLMProvider = "anthropic" | "gemini" | "openai" | (string & {});
|
|
6
|
+
/**
|
|
7
|
+
* All instrumentable targets — includes LLM providers + framework SDKs.
|
|
8
|
+
* 'ai' = Vercel AI SDK (uses SpanProcessor, not monkey-patching).
|
|
9
|
+
*/
|
|
10
|
+
export type InstrumentTarget = LLMProvider | "ai";
|
package/dist/types/spans.d.ts
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import type { LLMProvider } from "./providers.js";
|
|
2
2
|
/** Span status values used in trace attributes. */
|
|
3
3
|
export type SpanStatus = "success" | "error";
|
|
4
|
-
/** ReAct agent step types: think → act → observe → answer */
|
|
5
|
-
export type AgentStepType = "think" | "act" | "observe" | "answer";
|
|
4
|
+
/** ReAct agent step types: think → act → observe → answer → handoff */
|
|
5
|
+
export type AgentStepType = "think" | "act" | "observe" | "answer" | "handoff";
|
|
6
6
|
/** Input for traceAgentStep — describes a single agent step */
|
|
7
7
|
export interface AgentStepInput {
|
|
8
8
|
readonly type: AgentStepType;
|
|
9
9
|
readonly stepNumber: number;
|
|
10
10
|
readonly content?: string | undefined;
|
|
11
11
|
readonly toolName?: string | undefined;
|
|
12
|
+
/** Tool type per OTel GenAI spec: function, extension, retrieval, builtin */
|
|
13
|
+
readonly toolType?: "function" | "extension" | "retrieval" | "builtin" | undefined;
|
|
14
|
+
/** Target agent name for handoff steps */
|
|
15
|
+
readonly toAgent?: string | undefined;
|
|
16
|
+
/** Reason for handoff */
|
|
17
|
+
readonly handoffReason?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Options for traceAgentQuery */
|
|
20
|
+
export interface AgentQueryOptions {
|
|
21
|
+
/** Max steps before recording a warning. Default: 25 */
|
|
22
|
+
readonly maxSteps?: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Input for traceAgentQuery — object form with agent metadata */
|
|
25
|
+
export interface AgentQueryInput {
|
|
26
|
+
readonly query: string;
|
|
27
|
+
/** Agent name — maps to gen_ai.agent.name and span name invoke_agent {agentName} */
|
|
28
|
+
readonly agentName?: string | undefined;
|
|
29
|
+
/** Agent identifier — maps to gen_ai.agent.id */
|
|
30
|
+
readonly agentId?: string | undefined;
|
|
12
31
|
}
|
|
13
32
|
/** Guard execution mode */
|
|
14
33
|
export type GuardMode = "shadow" | "enforce";
|
package/dist/vercel.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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 type { SpanProcessor, ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
24
|
+
/**
|
|
25
|
+
* OTel SpanProcessor that enriches Vercel AI SDK spans with toad-eye
|
|
26
|
+
* cost attributes and records metrics.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ToadEyeAISpanProcessor implements SpanProcessor {
|
|
29
|
+
forceFlush(): Promise<void>;
|
|
30
|
+
shutdown(): Promise<void>;
|
|
31
|
+
onStart(): void;
|
|
32
|
+
onEnd(span: ReadableSpan): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Helper for `experimental_telemetry` option — enables telemetry with
|
|
36
|
+
* optional toad-eye metadata.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const result = await generateText({
|
|
40
|
+
* model: openai('gpt-4o'),
|
|
41
|
+
* prompt: 'Hello',
|
|
42
|
+
* experimental_telemetry: withToadEye({ functionId: 'my-feature' }),
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function withToadEye(options?: {
|
|
47
|
+
functionId?: string;
|
|
48
|
+
metadata?: Record<string, string>;
|
|
49
|
+
}): {
|
|
50
|
+
functionId?: string;
|
|
51
|
+
metadata?: Record<string, string>;
|
|
52
|
+
isEnabled: true;
|
|
53
|
+
};
|