toad-eye 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +132 -87
  2. package/dist/agent.d.ts +41 -0
  3. package/dist/agent.js +91 -0
  4. package/dist/alerts/channels.d.ts +2 -0
  5. package/dist/alerts/channels.js +81 -0
  6. package/dist/alerts/conditions.d.ts +29 -0
  7. package/dist/alerts/conditions.js +147 -0
  8. package/dist/alerts/grafana.d.ts +5 -0
  9. package/dist/alerts/grafana.js +19 -0
  10. package/dist/alerts/index.d.ts +5 -0
  11. package/dist/alerts/index.js +12 -0
  12. package/dist/alerts/manager.d.ts +13 -0
  13. package/dist/alerts/manager.js +87 -0
  14. package/dist/alerts/types.d.ts +46 -0
  15. package/dist/alerts/types.js +1 -0
  16. package/dist/budget/error.d.ts +10 -0
  17. package/dist/budget/error.js +22 -0
  18. package/dist/budget/index.d.ts +3 -0
  19. package/dist/budget/index.js +2 -0
  20. package/dist/budget/tracker.d.ts +35 -0
  21. package/dist/budget/tracker.js +121 -0
  22. package/dist/budget/types.d.ts +34 -0
  23. package/dist/budget/types.js +1 -0
  24. package/dist/cli.js +127 -18
  25. package/dist/core/metrics.d.ts +14 -0
  26. package/dist/core/metrics.js +119 -0
  27. package/dist/core/pricing.d.ts +24 -0
  28. package/dist/core/pricing.js +52 -0
  29. package/dist/{spans.d.ts → core/spans.d.ts} +3 -1
  30. package/dist/core/spans.js +143 -0
  31. package/dist/core/tracer.d.ts +9 -0
  32. package/dist/core/tracer.js +93 -0
  33. package/dist/drift/baseline.d.ts +16 -0
  34. package/dist/drift/baseline.js +16 -0
  35. package/dist/drift/cosine.d.ts +13 -0
  36. package/dist/drift/cosine.js +39 -0
  37. package/dist/drift/embedding-provider.d.ts +21 -0
  38. package/dist/drift/embedding-provider.js +8 -0
  39. package/dist/drift/index.d.ts +9 -0
  40. package/dist/drift/index.js +8 -0
  41. package/dist/drift/monitor.d.ts +29 -0
  42. package/dist/drift/monitor.js +47 -0
  43. package/dist/drift/openai-embeddings.d.ts +6 -0
  44. package/dist/drift/openai-embeddings.js +26 -0
  45. package/dist/export.d.ts +46 -0
  46. package/dist/export.js +108 -0
  47. package/dist/guard.d.ts +26 -0
  48. package/dist/guard.js +45 -0
  49. package/dist/index.d.ts +17 -5
  50. package/dist/index.js +10 -3
  51. package/dist/instrumentations/anthropic.d.ts +1 -0
  52. package/dist/instrumentations/anthropic.js +73 -0
  53. package/dist/instrumentations/create.d.ts +11 -0
  54. package/dist/instrumentations/create.js +165 -0
  55. package/dist/instrumentations/gemini.d.ts +1 -0
  56. package/dist/instrumentations/gemini.js +48 -0
  57. package/dist/instrumentations/openai.d.ts +1 -0
  58. package/dist/instrumentations/openai.js +94 -0
  59. package/dist/instrumentations/registry.d.ts +5 -0
  60. package/dist/instrumentations/registry.js +31 -0
  61. package/dist/instrumentations/types.d.ts +37 -0
  62. package/dist/instrumentations/types.js +1 -0
  63. package/dist/types/attributes.d.ts +48 -0
  64. package/dist/types/attributes.js +53 -0
  65. package/dist/types/config.d.ts +34 -0
  66. package/dist/types/config.js +1 -0
  67. package/dist/types/index.d.ts +17 -0
  68. package/dist/types/index.js +13 -0
  69. package/dist/types/metrics.d.ts +33 -0
  70. package/dist/types/metrics.js +35 -0
  71. package/dist/types/providers.d.ts +5 -0
  72. package/dist/types/providers.js +1 -0
  73. package/dist/types/spans.d.ts +40 -0
  74. package/dist/types/spans.js +1 -0
  75. package/package.json +38 -4
  76. package/templates/alerts.example.yml +54 -0
  77. package/templates/docker-compose.yml +1 -2
  78. package/templates/grafana/dashboards/cost-breakdown.json +208 -0
  79. package/templates/grafana/dashboards/error-drilldown.json +188 -0
  80. package/templates/grafana/dashboards/finops-attribution.json +278 -0
  81. package/templates/grafana/dashboards/latency-analysis.json +134 -0
  82. package/templates/grafana/dashboards/llm-overview.json +161 -30
  83. package/templates/grafana/dashboards/model-comparison.json +143 -0
  84. package/dist/metrics.d.ts +0 -6
  85. package/dist/metrics.js +0 -46
  86. package/dist/spans.js +0 -61
  87. package/dist/tracer.d.ts +0 -4
  88. package/dist/tracer.js +0 -46
  89. package/dist/types.d.ts +0 -69
  90. package/dist/types.js +0 -32
@@ -0,0 +1,94 @@
1
+ import { register } from "./registry.js";
2
+ import { createInstrumentation } from "./create.js";
3
+ function extractMessages(messages) {
4
+ if (!Array.isArray(messages))
5
+ return "";
6
+ return messages
7
+ .map((m) => {
8
+ if (typeof m.content === "string")
9
+ return m.content;
10
+ if (Array.isArray(m.content)) {
11
+ return m.content
12
+ .map((part) => {
13
+ if (part.type === "text")
14
+ return part.text ?? "";
15
+ if (part.type === "image_url")
16
+ return "[image]";
17
+ if (part.type === "input_audio")
18
+ return "[audio]";
19
+ return "";
20
+ })
21
+ .filter(Boolean)
22
+ .join("");
23
+ }
24
+ return "";
25
+ })
26
+ .filter(Boolean)
27
+ .join("\n");
28
+ }
29
+ const chatCompletions = {
30
+ getPrototype: (sdk) => (sdk?.Chat?.Completions ?? sdk?.Completions)?.prototype,
31
+ method: "create",
32
+ extractRequest: (body) => {
33
+ const b = body;
34
+ return {
35
+ prompt: extractMessages(b?.messages),
36
+ model: b?.model ?? "unknown",
37
+ temperature: b?.temperature ?? 1.0,
38
+ };
39
+ },
40
+ extractResponse: (response) => {
41
+ const r = response;
42
+ return {
43
+ completion: r?.choices?.[0]?.message?.content ?? "",
44
+ inputTokens: r?.usage?.prompt_tokens ?? 0,
45
+ outputTokens: r?.usage?.completion_tokens ?? 0,
46
+ };
47
+ },
48
+ isStreaming: (body) => !!body?.stream,
49
+ extractStreamResponse: (chunks) => {
50
+ // OpenAI stream chunks: { choices: [{ delta: { content } }], usage?: { ... } }
51
+ let completion = "";
52
+ let inputTokens = 0;
53
+ let outputTokens = 0;
54
+ for (const chunk of chunks) {
55
+ const c = chunk;
56
+ const delta = c?.choices?.[0]?.delta?.content;
57
+ if (delta)
58
+ completion += delta;
59
+ // Usage is typically in the final chunk (when stream_options.include_usage is set)
60
+ if (c?.usage) {
61
+ inputTokens = c.usage.prompt_tokens ?? inputTokens;
62
+ outputTokens = c.usage.completion_tokens ?? outputTokens;
63
+ }
64
+ }
65
+ return { completion, inputTokens, outputTokens };
66
+ },
67
+ };
68
+ const embeddings = {
69
+ getPrototype: (sdk) => sdk?.Embeddings?.prototype,
70
+ method: "create",
71
+ extractRequest: (body) => {
72
+ const b = body;
73
+ const input = b?.input;
74
+ const prompt = typeof input === "string"
75
+ ? input
76
+ : Array.isArray(input)
77
+ ? input.join("\n")
78
+ : "";
79
+ return { prompt, model: b?.model ?? "unknown" };
80
+ },
81
+ extractResponse: (response) => {
82
+ const r = response;
83
+ return {
84
+ completion: "[embedding]",
85
+ inputTokens: r?.usage?.prompt_tokens ?? 0,
86
+ outputTokens: 0,
87
+ };
88
+ },
89
+ };
90
+ register(createInstrumentation({
91
+ name: "openai",
92
+ moduleName: "openai",
93
+ patches: [chatCompletions, embeddings],
94
+ }));
@@ -0,0 +1,5 @@
1
+ import type { LLMProvider } from "../types/index.js";
2
+ import type { Instrumentation } from "./types.js";
3
+ export declare function register(inst: Instrumentation): void;
4
+ export declare function enableAll(providers: readonly LLMProvider[]): void;
5
+ export declare function disableAll(): void;
@@ -0,0 +1,31 @@
1
+ import { diag } from "@opentelemetry/api";
2
+ const instrumentations = new Map();
3
+ const active = new Set();
4
+ export function register(inst) {
5
+ instrumentations.set(inst.name, inst);
6
+ }
7
+ export function enableAll(providers) {
8
+ for (const name of providers) {
9
+ if (active.has(name))
10
+ continue;
11
+ const inst = instrumentations.get(name);
12
+ if (!inst) {
13
+ diag.warn(`toad-eye: unknown provider "${name}", skipping`);
14
+ continue;
15
+ }
16
+ const patched = inst.enable();
17
+ if (patched) {
18
+ active.add(name);
19
+ diag.debug(`toad-eye: auto-instrumented ${name}`);
20
+ }
21
+ else {
22
+ diag.debug(`toad-eye: ${name} SDK not found, skipping`);
23
+ }
24
+ }
25
+ }
26
+ export function disableAll() {
27
+ for (const name of active) {
28
+ instrumentations.get(name)?.disable();
29
+ }
30
+ active.clear();
31
+ }
@@ -0,0 +1,37 @@
1
+ import type { LLMProvider } from "../types/index.js";
2
+ export interface Instrumentation {
3
+ readonly name: LLMProvider;
4
+ /** Try to find and patch the SDK. Returns true if patched, false if SDK not found. */
5
+ enable(): boolean;
6
+ /** Restore original methods. */
7
+ disable(): void;
8
+ }
9
+ /** Describes a single method to monkey-patch on an SDK prototype. */
10
+ export interface PatchTarget {
11
+ /** How to find the prototype from the loaded SDK module */
12
+ getPrototype: (sdk: any) => any | undefined;
13
+ /** Method name on the prototype to patch */
14
+ method: string;
15
+ /** Extract LLMCallInput fields from the request arguments */
16
+ extractRequest: (body: unknown) => {
17
+ prompt: string;
18
+ model: string;
19
+ temperature?: number;
20
+ };
21
+ /** Extract LLMCallOutput fields from the SDK response */
22
+ extractResponse: (response: unknown, model: string) => {
23
+ completion: string;
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ };
27
+ /** Return true if this call should skip patching entirely */
28
+ shouldSkip?: (body: unknown) => boolean;
29
+ /** Return true if this call is a streaming request */
30
+ isStreaming?: (body: unknown) => boolean;
31
+ /** Extract final stats from accumulated stream chunks. Required when isStreaming is set. */
32
+ extractStreamResponse?: (chunks: unknown[]) => {
33
+ completion: string;
34
+ inputTokens: number;
35
+ outputTokens: number;
36
+ };
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Span attribute keys following OTel GenAI semantic conventions.
3
+ * See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
4
+ *
5
+ * Standard attributes use `gen_ai.*` prefix.
6
+ * toad-eye extensions use `gen_ai.toad_eye.*` prefix.
7
+ */
8
+ export declare const GEN_AI_ATTRS: {
9
+ readonly PROVIDER: "gen_ai.provider.name";
10
+ readonly REQUEST_MODEL: "gen_ai.request.model";
11
+ readonly RESPONSE_MODEL: "gen_ai.response.model";
12
+ readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
13
+ readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
14
+ readonly TEMPERATURE: "gen_ai.request.temperature";
15
+ readonly OPERATION: "gen_ai.operation.name";
16
+ readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
17
+ readonly ERROR: "error.type";
18
+ readonly AGENT_STEP_TYPE: "gen_ai.agent.step.type";
19
+ readonly AGENT_STEP_NUMBER: "gen_ai.agent.step.number";
20
+ readonly AGENT_TOOL_NAME: "gen_ai.agent.tool.name";
21
+ readonly AGENT_STEP_CONTENT: "gen_ai.agent.step.content";
22
+ readonly GUARD_MODE: "gen_ai.toad_eye.guard.mode";
23
+ readonly GUARD_PASSED: "gen_ai.toad_eye.guard.passed";
24
+ readonly GUARD_FAILURE_REASON: "gen_ai.toad_eye.guard.failure_reason";
25
+ readonly GUARD_RULE_NAME: "gen_ai.toad_eye.guard.rule_name";
26
+ readonly PROMPT: "gen_ai.toad_eye.prompt";
27
+ readonly COMPLETION: "gen_ai.toad_eye.completion";
28
+ readonly COST: "gen_ai.toad_eye.cost";
29
+ readonly STATUS: "gen_ai.toad_eye.status";
30
+ readonly SESSION_ID: "session.id";
31
+ readonly TEAM: "toad_eye.team";
32
+ readonly USER_ID: "toad_eye.user_id";
33
+ readonly FEATURE: "toad_eye.feature";
34
+ readonly ENVIRONMENT: "toad_eye.environment";
35
+ };
36
+ /** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
37
+ export declare const LLM_ATTRS: {
38
+ readonly PROVIDER: "gen_ai.provider.name";
39
+ readonly MODEL: "gen_ai.request.model";
40
+ readonly PROMPT: "gen_ai.toad_eye.prompt";
41
+ readonly COMPLETION: "gen_ai.toad_eye.completion";
42
+ readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
43
+ readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
44
+ readonly COST: "gen_ai.toad_eye.cost";
45
+ readonly TEMPERATURE: "gen_ai.request.temperature";
46
+ readonly STATUS: "gen_ai.toad_eye.status";
47
+ readonly ERROR: "error.type";
48
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Span attribute keys following OTel GenAI semantic conventions.
3
+ * See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
4
+ *
5
+ * Standard attributes use `gen_ai.*` prefix.
6
+ * toad-eye extensions use `gen_ai.toad_eye.*` prefix.
7
+ */
8
+ export const GEN_AI_ATTRS = {
9
+ // OTel GenAI standard
10
+ PROVIDER: "gen_ai.provider.name",
11
+ REQUEST_MODEL: "gen_ai.request.model",
12
+ RESPONSE_MODEL: "gen_ai.response.model",
13
+ INPUT_TOKENS: "gen_ai.usage.input_tokens",
14
+ OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
15
+ TEMPERATURE: "gen_ai.request.temperature",
16
+ OPERATION: "gen_ai.operation.name",
17
+ FINISH_REASONS: "gen_ai.response.finish_reasons",
18
+ ERROR: "error.type",
19
+ // Agent step attributes
20
+ AGENT_STEP_TYPE: "gen_ai.agent.step.type",
21
+ AGENT_STEP_NUMBER: "gen_ai.agent.step.number",
22
+ AGENT_TOOL_NAME: "gen_ai.agent.tool.name",
23
+ AGENT_STEP_CONTENT: "gen_ai.agent.step.content",
24
+ // Guard (shadow mode) attributes
25
+ GUARD_MODE: "gen_ai.toad_eye.guard.mode",
26
+ GUARD_PASSED: "gen_ai.toad_eye.guard.passed",
27
+ GUARD_FAILURE_REASON: "gen_ai.toad_eye.guard.failure_reason",
28
+ GUARD_RULE_NAME: "gen_ai.toad_eye.guard.rule_name",
29
+ // toad-eye extensions
30
+ PROMPT: "gen_ai.toad_eye.prompt",
31
+ COMPLETION: "gen_ai.toad_eye.completion",
32
+ COST: "gen_ai.toad_eye.cost",
33
+ STATUS: "gen_ai.toad_eye.status",
34
+ SESSION_ID: "session.id",
35
+ // FinOps attribution
36
+ TEAM: "toad_eye.team",
37
+ USER_ID: "toad_eye.user_id",
38
+ FEATURE: "toad_eye.feature",
39
+ ENVIRONMENT: "toad_eye.environment",
40
+ };
41
+ /** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
42
+ export const LLM_ATTRS = {
43
+ PROVIDER: GEN_AI_ATTRS.PROVIDER,
44
+ MODEL: GEN_AI_ATTRS.REQUEST_MODEL,
45
+ PROMPT: GEN_AI_ATTRS.PROMPT,
46
+ COMPLETION: GEN_AI_ATTRS.COMPLETION,
47
+ INPUT_TOKENS: GEN_AI_ATTRS.INPUT_TOKENS,
48
+ OUTPUT_TOKENS: GEN_AI_ATTRS.OUTPUT_TOKENS,
49
+ COST: GEN_AI_ATTRS.COST,
50
+ TEMPERATURE: GEN_AI_ATTRS.TEMPERATURE,
51
+ STATUS: GEN_AI_ATTRS.STATUS,
52
+ ERROR: GEN_AI_ATTRS.ERROR,
53
+ };
@@ -0,0 +1,34 @@
1
+ import type { LLMProvider } from "./providers.js";
2
+ import type { BudgetConfig, BudgetExceededMode, DowngradeCallback } from "../budget/types.js";
3
+ /**
4
+ * Configuration for initObservability().
5
+ * This is what the user passes when connecting toad-eye to their service.
6
+ */
7
+ export interface ToadEyeConfig {
8
+ readonly serviceName: string;
9
+ readonly endpoint?: string | undefined;
10
+ /** API key for toad-eye cloud. When set, transport switches to HTTPS cloud endpoint automatically. */
11
+ readonly apiKey?: string | undefined;
12
+ /** Custom cloud endpoint override. Defaults to https://cloud.toad-eye.dev. */
13
+ readonly cloudEndpoint?: string | undefined;
14
+ readonly attributes?: Readonly<Record<string, string>> | undefined;
15
+ /** Set to false to disable recording prompt/completion text in spans. */
16
+ readonly recordContent?: boolean | undefined;
17
+ /** Record SHA-256 hash of content instead of plain text. Allows prompt comparison without reading. */
18
+ readonly hashContent?: boolean | undefined;
19
+ /** Salt for hashContent — prepended before hashing to prevent rainbow table attacks on short strings. */
20
+ readonly salt?: string | undefined;
21
+ /** Regex patterns to redact from prompt/completion text before recording. */
22
+ readonly redactPatterns?: readonly RegExp[] | undefined;
23
+ readonly instrument?: readonly LLMProvider[] | undefined;
24
+ /** Budget limits — daily, per-user, per-model spend caps in USD. */
25
+ readonly budgets?: BudgetConfig | undefined;
26
+ /** What to do when budget is exceeded: 'warn' (log), 'block' (throw), 'downgrade' (callback). */
27
+ readonly onBudgetExceeded?: BudgetExceededMode | undefined;
28
+ /** Callback for 'downgrade' mode — receives original provider/model, returns replacement. */
29
+ readonly downgradeCallback?: DowngradeCallback | undefined;
30
+ /** Static session ID — all spans will carry this value as `session.id`. */
31
+ readonly sessionId?: string | undefined;
32
+ /** Dynamic session ID extractor — called per traceLLMCall to resolve session ID. */
33
+ readonly sessionExtractor?: (() => string | undefined) | undefined;
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Central type definitions for toad-eye.
3
+ *
4
+ * Organized by concern:
5
+ * - providers.ts — supported LLM provider names
6
+ * - config.ts — ToadEyeConfig (initObservability options)
7
+ * - attributes.ts — OTel span attribute keys (GEN_AI_ATTRS)
8
+ * - metrics.ts — OTel metric names (GEN_AI_METRICS)
9
+ * - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
10
+ */
11
+ export declare const INSTRUMENTATION_NAME = "toad-eye";
12
+ export type { LLMProvider } from "./providers.js";
13
+ export type { ToadEyeConfig } from "./config.js";
14
+ export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
15
+ export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
16
+ export type { MetricName } from "./metrics.js";
17
+ export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, GuardMode, GuardResult, } from "./spans.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Central type definitions for toad-eye.
3
+ *
4
+ * Organized by concern:
5
+ * - providers.ts — supported LLM provider names
6
+ * - config.ts — ToadEyeConfig (initObservability options)
7
+ * - attributes.ts — OTel span attribute keys (GEN_AI_ATTRS)
8
+ * - metrics.ts — OTel metric names (GEN_AI_METRICS)
9
+ * - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
10
+ */
11
+ export const INSTRUMENTATION_NAME = "toad-eye";
12
+ export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
13
+ export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Metric names exported to Prometheus via OTel.
3
+ *
4
+ * Naming follows OTel GenAI semantic conventions:
5
+ * - `gen_ai.client.*` prefix for client-side metrics
6
+ * - Prometheus auto-converts dots to underscores
7
+ * - Prometheus adds _total suffix to counters automatically
8
+ */
9
+ export declare const GEN_AI_METRICS: {
10
+ readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
11
+ readonly TOKEN_USAGE: "gen_ai.client.token.usage";
12
+ readonly REQUEST_COST: "gen_ai.client.request.cost";
13
+ readonly REQUESTS: "gen_ai.client.requests";
14
+ readonly ERRORS: "gen_ai.client.errors";
15
+ readonly AGENT_STEPS_PER_QUERY: "gen_ai.agent.steps_per_query";
16
+ readonly AGENT_TOOL_USAGE: "gen_ai.agent.tool_usage";
17
+ readonly GUARD_EVALUATIONS: "gen_ai.toad_eye.guard.evaluations";
18
+ readonly GUARD_WOULD_BLOCK: "gen_ai.toad_eye.guard.would_block";
19
+ readonly SEMANTIC_DRIFT: "gen_ai.toad_eye.semantic_drift";
20
+ readonly BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded";
21
+ readonly BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked";
22
+ readonly BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded";
23
+ };
24
+ /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
25
+ export declare const LLM_METRICS: {
26
+ readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
27
+ readonly REQUEST_COST: "gen_ai.client.request.cost";
28
+ readonly TOKENS: "gen_ai.client.token.usage";
29
+ readonly REQUESTS: "gen_ai.client.requests";
30
+ readonly ERRORS: "gen_ai.client.errors";
31
+ };
32
+ /** Type-safe metric name — only values from GEN_AI_METRICS are allowed. */
33
+ export type MetricName = (typeof GEN_AI_METRICS)[keyof typeof GEN_AI_METRICS];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Metric names exported to Prometheus via OTel.
3
+ *
4
+ * Naming follows OTel GenAI semantic conventions:
5
+ * - `gen_ai.client.*` prefix for client-side metrics
6
+ * - Prometheus auto-converts dots to underscores
7
+ * - Prometheus adds _total suffix to counters automatically
8
+ */
9
+ export const GEN_AI_METRICS = {
10
+ REQUEST_DURATION: "gen_ai.client.operation.duration",
11
+ TOKEN_USAGE: "gen_ai.client.token.usage",
12
+ REQUEST_COST: "gen_ai.client.request.cost",
13
+ REQUESTS: "gen_ai.client.requests",
14
+ ERRORS: "gen_ai.client.errors",
15
+ // Agent metrics
16
+ AGENT_STEPS_PER_QUERY: "gen_ai.agent.steps_per_query",
17
+ AGENT_TOOL_USAGE: "gen_ai.agent.tool_usage",
18
+ // Guard metrics
19
+ GUARD_EVALUATIONS: "gen_ai.toad_eye.guard.evaluations",
20
+ GUARD_WOULD_BLOCK: "gen_ai.toad_eye.guard.would_block",
21
+ // Drift metrics
22
+ SEMANTIC_DRIFT: "gen_ai.toad_eye.semantic_drift",
23
+ // Budget metrics
24
+ BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded",
25
+ BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked",
26
+ BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded",
27
+ };
28
+ /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
29
+ export const LLM_METRICS = {
30
+ REQUEST_DURATION: GEN_AI_METRICS.REQUEST_DURATION,
31
+ REQUEST_COST: GEN_AI_METRICS.REQUEST_COST,
32
+ TOKENS: GEN_AI_METRICS.TOKEN_USAGE,
33
+ REQUESTS: GEN_AI_METRICS.REQUESTS,
34
+ ERRORS: GEN_AI_METRICS.ERRORS,
35
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * LLM providers supported by toad-eye.
3
+ * Values follow OTel GenAI semconv `gen_ai.provider.name`.
4
+ */
5
+ export type LLMProvider = "anthropic" | "gemini" | "openai";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { LLMProvider } from "./providers.js";
2
+ /** Span status values used in trace attributes. */
3
+ export type SpanStatus = "success" | "error";
4
+ /** ReAct agent step types: think → act → observe → answer */
5
+ export type AgentStepType = "think" | "act" | "observe" | "answer";
6
+ /** Input for traceAgentStep — describes a single agent step */
7
+ export interface AgentStepInput {
8
+ readonly type: AgentStepType;
9
+ readonly stepNumber: number;
10
+ readonly content?: string | undefined;
11
+ readonly toolName?: string | undefined;
12
+ }
13
+ /** Guard execution mode */
14
+ export type GuardMode = "shadow" | "enforce";
15
+ /**
16
+ * Result of a toad-guard validation — the contract between toad-guard and toad-eye.
17
+ * toad-guard produces this, toad-eye consumes it via recordGuardResult().
18
+ */
19
+ export interface GuardResult {
20
+ readonly mode: GuardMode;
21
+ readonly passed: boolean;
22
+ readonly ruleName: string;
23
+ readonly failureReason?: string | undefined;
24
+ }
25
+ /**
26
+ * Attributes attached to every LLM span (trace).
27
+ * These become searchable fields in Jaeger and filterable dimensions in Grafana.
28
+ */
29
+ export interface LLMSpanAttributes {
30
+ readonly provider: LLMProvider;
31
+ readonly model: string;
32
+ readonly prompt: string;
33
+ readonly completion: string;
34
+ readonly inputTokens: number;
35
+ readonly outputTokens: number;
36
+ readonly cost: number;
37
+ readonly temperature: number;
38
+ readonly status: SpanStatus;
39
+ readonly error?: string | undefined;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "1.1.0",
4
- "description": "One-line OpenTelemetry instrumentation for LLM services",
3
+ "version": "2.1.0",
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",
@@ -15,6 +15,8 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc -p tsconfig.build.json",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
18
20
  "prepublishOnly": "npm run build"
19
21
  },
20
22
  "repository": {
@@ -30,7 +32,16 @@
30
32
  "observability",
31
33
  "tracing",
32
34
  "metrics",
33
- "grafana"
35
+ "grafana",
36
+ "openai",
37
+ "anthropic",
38
+ "gemini",
39
+ "cost-tracking",
40
+ "auto-instrumentation",
41
+ "gen-ai",
42
+ "budget-guards",
43
+ "finops",
44
+ "streaming"
34
45
  ],
35
46
  "license": "ISC",
36
47
  "dependencies": {
@@ -40,6 +51,29 @@
40
51
  "@opentelemetry/resources": "^2.6.0",
41
52
  "@opentelemetry/sdk-metrics": "^2.6.0",
42
53
  "@opentelemetry/sdk-node": "^0.213.0",
43
- "@opentelemetry/semantic-conventions": "^1.40.0"
54
+ "@opentelemetry/semantic-conventions": "^1.40.0",
55
+ "nodemailer": "^8.0.3",
56
+ "yaml": "^2.7.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@anthropic-ai/sdk": ">=0.30.0",
60
+ "@google/generative-ai": ">=0.20.0",
61
+ "openai": ">=4.0.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "openai": {
65
+ "optional": true
66
+ },
67
+ "@anthropic-ai/sdk": {
68
+ "optional": true
69
+ },
70
+ "@google/generative-ai": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@types/nodemailer": "^7.0.11",
76
+ "playwright": "^1.58.2",
77
+ "vitest": "^4.1.0"
44
78
  }
45
79
  }
@@ -0,0 +1,54 @@
1
+ # toad-eye alerts configuration
2
+ # Copy to alerts.yml and customize
3
+
4
+ # prometheusUrl: http://localhost:9090
5
+ # grafanaUrl: http://localhost:3100
6
+ # evalIntervalSeconds: 30
7
+ # cooldownMinutes: 30
8
+
9
+ # channels:
10
+ # telegram:
11
+ # type: telegram
12
+ # token: "BOT_TOKEN"
13
+ # chatId: "CHAT_ID"
14
+ # slack:
15
+ # type: slack_webhook
16
+ # url: "https://hooks.slack.com/services/..."
17
+
18
+ alerts:
19
+ # Cost spike: $10 in 1 hour
20
+ - name: cost_spike
21
+ metric: gen_ai.client.request.cost
22
+ condition: sum_1h > 10
23
+ channels: [slack]
24
+
25
+ # Daily budget: $50 in 24 hours
26
+ - name: budget_daily
27
+ metric: gen_ai.client.request.cost
28
+ condition: sum_24h > 50
29
+ channels: [slack]
30
+
31
+ # Latency anomaly: p95 increased >50% vs 7-day baseline
32
+ - name: latency_anomaly
33
+ metric: gen_ai.client.operation.duration
34
+ condition: p95_pct_5m_7d > 50
35
+ channels: [slack]
36
+
37
+ # Latency critical: p95 increased >100% vs 7-day baseline
38
+ - name: latency_critical
39
+ metric: gen_ai.client.operation.duration
40
+ condition: p95_pct_5m_7d > 100
41
+ channels: [slack]
42
+
43
+ # Error rate warning: >5% over 15 minutes
44
+ - name: error_rate_warning
45
+ metric: gen_ai.client.errors
46
+ condition: ratio_15m > 0.05
47
+ channels: [slack]
48
+
49
+ # Error rate critical: >20% over 5 minutes (provider likely down)
50
+ - name: error_rate_critical
51
+ metric: gen_ai.client.errors
52
+ condition: ratio_5m > 0.2
53
+ channels: [slack]
54
+ cooldown: 15
@@ -28,8 +28,7 @@ services:
28
28
  image: grafana/grafana:latest
29
29
  volumes:
30
30
  - ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
31
- - ./grafana/dashboards/dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml
32
- - ./grafana/dashboards/llm-overview.json:/etc/grafana/provisioning/dashboards/llm-overview.json
31
+ - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
33
32
  environment:
34
33
  - GF_SECURITY_ADMIN_PASSWORD=admin
35
34
  - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/llm-overview.json