toad-eye 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -152
- package/dist/agent.d.ts +19 -28
- package/dist/agent.js +46 -27
- package/dist/alerts/channels.js +24 -6
- package/dist/alerts/index.js +46 -2
- package/dist/alerts/manager.d.ts +1 -0
- package/dist/alerts/manager.js +36 -21
- package/dist/budget/tracker.d.ts +16 -6
- package/dist/budget/tracker.js +38 -17
- package/dist/budget/types.d.ts +2 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +106 -54
- package/dist/core/metrics.d.ts +4 -0
- package/dist/core/metrics.js +19 -0
- package/dist/core/pricing.d.ts +2 -0
- package/dist/core/pricing.js +4 -0
- package/dist/core/spans.d.ts +5 -3
- package/dist/core/spans.js +79 -20
- package/dist/core/tracer.d.ts +0 -3
- package/dist/core/tracer.js +40 -7
- package/dist/drift/baseline.js +7 -1
- package/dist/drift/monitor.js +6 -6
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/instrumentations/anthropic.d.ts +1 -1
- package/dist/instrumentations/anthropic.js +2 -3
- package/dist/instrumentations/create.js +80 -19
- package/dist/instrumentations/gemini.d.ts +1 -1
- package/dist/instrumentations/gemini.js +6 -7
- package/dist/instrumentations/openai.d.ts +1 -1
- package/dist/instrumentations/openai.js +2 -3
- package/dist/instrumentations/registry.d.ts +3 -1
- package/dist/instrumentations/registry.js +28 -5
- package/dist/instrumentations/types.d.ts +4 -2
- package/dist/types/attributes.d.ts +3 -0
- package/dist/types/attributes.js +3 -0
- package/dist/types/config.d.ts +22 -2
- package/dist/types/index.d.ts +3 -3
- package/dist/types/metrics.d.ts +2 -0
- package/dist/types/metrics.js +3 -0
- package/dist/types/providers.d.ts +6 -1
- package/dist/types/spans.d.ts +11 -2
- package/dist/vercel.d.ts +53 -0
- package/dist/vercel.js +106 -0
- package/package.json +29 -3
- package/templates/grafana/dashboards/agent-workflow.json +207 -0
- package/templates/otel-collector.yml +25 -1
|
@@ -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;
|
|
@@ -19,6 +19,9 @@ export declare const GEN_AI_ATTRS: {
|
|
|
19
19
|
readonly AGENT_STEP_NUMBER: "gen_ai.agent.step.number";
|
|
20
20
|
readonly AGENT_TOOL_NAME: "gen_ai.agent.tool.name";
|
|
21
21
|
readonly AGENT_STEP_CONTENT: "gen_ai.agent.step.content";
|
|
22
|
+
readonly AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to";
|
|
23
|
+
readonly AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason";
|
|
24
|
+
readonly AGENT_LOOP_COUNT: "gen_ai.agent.loop_count";
|
|
22
25
|
readonly GUARD_MODE: "gen_ai.toad_eye.guard.mode";
|
|
23
26
|
readonly GUARD_PASSED: "gen_ai.toad_eye.guard.passed";
|
|
24
27
|
readonly GUARD_FAILURE_REASON: "gen_ai.toad_eye.guard.failure_reason";
|
package/dist/types/attributes.js
CHANGED
|
@@ -21,6 +21,9 @@ export const GEN_AI_ATTRS = {
|
|
|
21
21
|
AGENT_STEP_NUMBER: "gen_ai.agent.step.number",
|
|
22
22
|
AGENT_TOOL_NAME: "gen_ai.agent.tool.name",
|
|
23
23
|
AGENT_STEP_CONTENT: "gen_ai.agent.step.content",
|
|
24
|
+
AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to",
|
|
25
|
+
AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason",
|
|
26
|
+
AGENT_LOOP_COUNT: "gen_ai.agent.loop_count",
|
|
24
27
|
// Guard (shadow mode) attributes
|
|
25
28
|
GUARD_MODE: "gen_ai.toad_eye.guard.mode",
|
|
26
29
|
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, 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,23 @@
|
|
|
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
|
+
/** Target agent name for handoff steps */
|
|
13
|
+
readonly toAgent?: string | undefined;
|
|
14
|
+
/** Reason for handoff */
|
|
15
|
+
readonly handoffReason?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Options for traceAgentQuery */
|
|
18
|
+
export interface AgentQueryOptions {
|
|
19
|
+
/** Max steps before recording a warning. Default: 25 */
|
|
20
|
+
readonly maxSteps?: number | undefined;
|
|
12
21
|
}
|
|
13
22
|
/** Guard execution mode */
|
|
14
23
|
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
|
+
};
|
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.
|
|
3
|
+
"version": "2.3.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]
|