toad-eye 2.10.0 → 2.11.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.
@@ -11,6 +11,7 @@ const METRIC_MAP = {
11
11
  "gen_ai.mcp.tool.duration": "gen_ai_mcp_tool_duration",
12
12
  "gen_ai.mcp.resource.reads": "gen_ai_mcp_resource_reads_total",
13
13
  "gen_ai.mcp.tool.callers": "gen_ai_mcp_tool_callers_total",
14
+ "gen_ai.mcp.tool.hallucinations": "gen_ai_mcp_tool_hallucinations_total",
14
15
  // Legacy names (backward compat)
15
16
  "llm.request.cost": "gen_ai_client_request_cost_USD_sum",
16
17
  "llm.request.duration": "gen_ai_client_operation_duration_milliseconds",
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn } from "node:child_process";
3
+ import { createConnection } from "node:net";
3
4
  import { cpSync, existsSync, mkdirSync, realpathSync, writeFileSync, } from "node:fs";
4
5
  import { join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -73,17 +74,66 @@ function requireDocker() {
73
74
  process.exit(1);
74
75
  }
75
76
  }
77
+ const PORT_CONFIG = [
78
+ { env: "COLLECTOR_PORT", default: 4318, service: "OTel Collector" },
79
+ { env: "PROMETHEUS_PORT", default: 9090, service: "Prometheus" },
80
+ { env: "JAEGER_PORT", default: 16686, service: "Jaeger" },
81
+ { env: "GRAFANA_PORT", default: 3100, service: "Grafana" },
82
+ ];
83
+ function getPort(env, defaultPort) {
84
+ const val = process.env[env];
85
+ return val ? parseInt(val, 10) : defaultPort;
86
+ }
87
+ function checkPort(port) {
88
+ return new Promise((resolve) => {
89
+ const socket = createConnection({ port, host: "127.0.0.1" });
90
+ socket.setTimeout(500);
91
+ socket.on("connect", () => {
92
+ socket.destroy();
93
+ resolve(true); // port is in use
94
+ });
95
+ socket.on("error", () => resolve(false)); // port is free
96
+ socket.on("timeout", () => {
97
+ socket.destroy();
98
+ resolve(false);
99
+ });
100
+ });
101
+ }
102
+ async function checkPorts() {
103
+ const conflicts = [];
104
+ for (const { env, default: defaultPort, service } of PORT_CONFIG) {
105
+ const port = getPort(env, defaultPort);
106
+ const inUse = await checkPort(port);
107
+ if (inUse) {
108
+ conflicts.push(` ❌ Port ${port} is already in use (${service})\n Fix: stop the other service, or set ${env}=${port + 1}`);
109
+ }
110
+ }
111
+ if (conflicts.length > 0) {
112
+ console.error("Port conflicts detected:\n");
113
+ console.error(conflicts.join("\n\n"));
114
+ console.error();
115
+ process.exit(1);
116
+ }
117
+ }
76
118
  function up() {
77
119
  requireDocker();
78
120
  const composeFile = requireInfra();
79
121
  console.log("🐸 Starting observability stack...");
80
122
  console.log(" (first run downloads ~500MB of Docker images — this may take a few minutes)\n");
81
- execFileSync("docker", ["compose", "-f", composeFile, "up", "-d"], {
82
- stdio: "inherit",
83
- });
84
- console.log();
85
- status();
86
- console.log(" Run 'npx toad-eye demo' to send test data and verify the stack works.");
123
+ // Check ports synchronously via async wrapper — CLI is short-lived
124
+ const checkAndStart = async () => {
125
+ await checkPorts();
126
+ execFileSync("docker", ["compose", "-f", composeFile, "up", "-d"], {
127
+ stdio: "inherit",
128
+ });
129
+ console.log();
130
+ status();
131
+ console.log(" Dashboards will be empty until data arrives.\n");
132
+ console.log(" Next steps:");
133
+ console.log(" npx toad-eye demo ← send mock traffic, see data in Grafana");
134
+ console.log(" or add toad-eye to your app (see README)");
135
+ };
136
+ void checkAndStart();
87
137
  }
88
138
  function down() {
89
139
  const composeFile = requireInfra("Nothing to stop.");
@@ -175,19 +225,37 @@ async function demo() {
175
225
  });
176
226
  console.log("🐸👁️ toad-eye demo — sending mock LLM traffic");
177
227
  console.log(" Press Ctrl+C to stop\n");
178
- console.log("✅ Stack verified! Add this to your app:\n");
179
- console.log(" import { initObservability } from 'toad-eye';\n initObservability({ serviceName: 'my-app', instrument: ['openai'] });\n");
180
228
  process.on("SIGINT", async () => {
181
229
  console.log("\n\u{1f44b} Shutting down...");
182
230
  await shutdown();
183
231
  process.exit(0);
184
232
  });
233
+ let opened = false;
234
+ function openGrafana() {
235
+ if (opened)
236
+ return;
237
+ opened = true;
238
+ const url = "http://localhost:3100";
239
+ const cmd = process.platform === "darwin"
240
+ ? "open"
241
+ : process.platform === "win32"
242
+ ? "start"
243
+ : "xdg-open";
244
+ try {
245
+ spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
246
+ console.log(`\n 📊 Opened Grafana: ${url}\n`);
247
+ }
248
+ catch {
249
+ console.log(`\n 📊 Open Grafana: ${url} (admin/admin)\n`);
250
+ }
251
+ }
185
252
  const tick = async () => {
186
253
  const { provider, model } = pickRandom(DEMO_MODELS);
187
254
  const prompt = pickRandom(DEMO_PROMPTS);
188
255
  try {
189
256
  await traceLLMCall({ provider, model, prompt, temperature: 0.7 }, () => simulateLLMCall(provider, model, prompt));
190
257
  console.log(` \u2705 [${provider}/${model}] ${prompt}`);
258
+ openGrafana();
191
259
  }
192
260
  catch (err) {
193
261
  const msg = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Console span exporter — pretty-prints completed spans to stderr.
3
+ *
4
+ * Used in "light mode" (output: "console") — no Docker, no OTel Collector,
5
+ * just immediate feedback in the terminal.
6
+ *
7
+ * Format:
8
+ * 🐸 tools/call calculate [42ms] ✅
9
+ * 🐸 chat gpt-4o [1.2s] $0.003 ✅
10
+ * 🐸 tools/call broken [12ms] ❌ TypeError
11
+ */
12
+ import type { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base";
13
+ import { type ExportResult } from "@opentelemetry/core";
14
+ export declare class ToadEyeConsoleExporter implements SpanExporter {
15
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
16
+ shutdown(): Promise<void>;
17
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Console span exporter — pretty-prints completed spans to stderr.
3
+ *
4
+ * Used in "light mode" (output: "console") — no Docker, no OTel Collector,
5
+ * just immediate feedback in the terminal.
6
+ *
7
+ * Format:
8
+ * 🐸 tools/call calculate [42ms] ✅
9
+ * 🐸 chat gpt-4o [1.2s] $0.003 ✅
10
+ * 🐸 tools/call broken [12ms] ❌ TypeError
11
+ */
12
+ import { SpanStatusCode } from "@opentelemetry/api";
13
+ import { ExportResultCode } from "@opentelemetry/core";
14
+ function formatDuration(ms) {
15
+ if (ms < 1)
16
+ return "<1ms";
17
+ if (ms < 1000)
18
+ return `${Math.round(ms)}ms`;
19
+ return `${(ms / 1000).toFixed(1)}s`;
20
+ }
21
+ function getAttr(span, key) {
22
+ const val = span.attributes[key];
23
+ if (typeof val === "string" || typeof val === "number")
24
+ return val;
25
+ return undefined;
26
+ }
27
+ function formatSpan(span) {
28
+ const durationMs = (span.endTime[0] - span.startTime[0]) * 1000 +
29
+ (span.endTime[1] - span.startTime[1]) / 1_000_000;
30
+ const isError = span.status.code === SpanStatusCode.ERROR;
31
+ const icon = isError ? "❌" : "✅";
32
+ const cost = getAttr(span, "gen_ai.toad_eye.cost");
33
+ const costStr = typeof cost === "number" && cost > 0 ? ` $${cost.toFixed(4)}` : "";
34
+ const errorType = isError ? getAttr(span, "error.type") : undefined;
35
+ const errorStr = errorType ? ` ${errorType}` : "";
36
+ return ` 🐸 ${span.name} [${formatDuration(durationMs)}]${costStr} ${icon}${errorStr}`;
37
+ }
38
+ // Filter out internal spans that add noise in console mode
39
+ const SKIP_PREFIXES = ["gen_ai.agent.step."];
40
+ function shouldShow(span) {
41
+ return !SKIP_PREFIXES.some((p) => span.name.startsWith(p));
42
+ }
43
+ export class ToadEyeConsoleExporter {
44
+ export(spans, resultCallback) {
45
+ for (const span of spans) {
46
+ if (shouldShow(span)) {
47
+ process.stderr.write(formatSpan(span) + "\n");
48
+ }
49
+ }
50
+ resultCallback({ code: ExportResultCode.SUCCESS });
51
+ }
52
+ shutdown() {
53
+ return Promise.resolve();
54
+ }
55
+ }
@@ -20,3 +20,4 @@ export declare function recordResponseEmpty(provider: string, model: string, att
20
20
  export declare function recordResponseLatencyPerToken(msPerToken: number, provider: string, model: string, attrs?: Record<string, string>): void;
21
21
  export declare function recordContextUtilization(utilization: number, provider: string, model: string): void;
22
22
  export declare function recordContextBlocked(model: string): void;
23
+ export declare function recordThinkingRatio(ratio: number, provider: string, model: string): void;
@@ -19,6 +19,7 @@ let responseEmpty;
19
19
  let responseLatencyPerToken;
20
20
  let contextUtilization;
21
21
  let contextBlocked;
22
+ let thinkingRatio;
22
23
  let initialized = false;
23
24
  export function initMetrics() {
24
25
  if (initialized)
@@ -86,6 +87,9 @@ export function initMetrics() {
86
87
  contextBlocked = meter.createCounter(GEN_AI_METRICS.CONTEXT_BLOCKED, {
87
88
  description: "Number of LLM calls blocked by context guard",
88
89
  });
90
+ thinkingRatio = meter.createHistogram(GEN_AI_METRICS.THINKING_RATIO, {
91
+ description: "Ratio of thinking/reasoning tokens to total output (0.0–1.0). High values indicate deep reasoning.",
92
+ });
89
93
  initialized = true;
90
94
  }
91
95
  /** Reset metrics state so the next initMetrics() creates fresh instruments. Called from shutdown(). */
@@ -219,3 +223,11 @@ export function recordContextBlocked(model) {
219
223
  [GEN_AI_ATTRS.REQUEST_MODEL]: model,
220
224
  });
221
225
  }
226
+ export function recordThinkingRatio(ratio, provider, model) {
227
+ if (!isReady())
228
+ return;
229
+ thinkingRatio.record(ratio, {
230
+ [GEN_AI_ATTRS.PROVIDER]: provider,
231
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
232
+ });
233
+ }
@@ -17,6 +17,10 @@ export interface LLMCallOutput {
17
17
  readonly outputTokens: number;
18
18
  /** Cost in USD. If omitted, auto-calculated from model pricing table. */
19
19
  readonly cost?: number | undefined;
20
+ /** Thinking/reasoning content from thinking models (o1, Claude extended thinking, Gemini). */
21
+ readonly thinkingContent?: string | undefined;
22
+ /** Thinking/reasoning tokens used (separate from outputTokens). */
23
+ readonly thinkingTokens?: number | undefined;
20
24
  }
21
25
  export declare function processContent(text: string): string | undefined;
22
26
  export declare function traceLLMCall(input: LLMCallInput, fn: () => Promise<LLMCallOutput>): Promise<LLMCallOutput>;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { trace, SpanStatusCode } from "@opentelemetry/api";
3
3
  import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "../types/index.js";
4
- import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, recordBudgetExceeded, recordBudgetBlocked, recordBudgetDowngraded, recordResponseEmpty, recordResponseLatencyPerToken, recordContextUtilization, recordContextBlocked, } from "./metrics.js";
4
+ import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, recordBudgetExceeded, recordBudgetBlocked, recordBudgetDowngraded, recordResponseEmpty, recordResponseLatencyPerToken, recordContextUtilization, recordContextBlocked, recordThinkingRatio, } from "./metrics.js";
5
5
  import { getConfig, getBudgetTracker } from "./tracer.js";
6
6
  import { calculateCost, getModelPricing } from "./pricing.js";
7
7
  import { ToadBudgetExceededError } from "../budget/index.js";
@@ -124,6 +124,26 @@ function setSuccessAttributes(span, input, output, resolvedCost) {
124
124
  [GEN_AI_ATTRS.STATUS]: "success",
125
125
  [GEN_AI_ATTRS.FINISH_REASONS]: ["stop"],
126
126
  });
127
+ // Thinking model support (o1, Claude extended thinking, Gemini)
128
+ if (output.thinkingTokens !== undefined && output.thinkingTokens > 0) {
129
+ span.setAttribute("gen_ai.usage.reasoning_tokens", output.thinkingTokens);
130
+ const ratio = output.thinkingTokens / (output.outputTokens + output.thinkingTokens);
131
+ span.setAttribute("gen_ai.toad_eye.thinking.ratio", ratio);
132
+ recordThinkingRatio(ratio, input.provider, input.model);
133
+ }
134
+ if (output.thinkingContent !== undefined) {
135
+ const config = getConfig();
136
+ const recordThinking = config?.recordContent !== false;
137
+ if (recordThinking) {
138
+ const processed = processContent(output.thinkingContent);
139
+ if (processed !== undefined) {
140
+ span.addEvent("gen_ai.thinking", {
141
+ "gen_ai.toad_eye.thinking.content": processed,
142
+ });
143
+ }
144
+ }
145
+ span.setAttribute("gen_ai.toad_eye.thinking.content_length", output.thinkingContent.length);
146
+ }
127
147
  span.setStatus({ code: SpanStatusCode.OK });
128
148
  }
129
149
  function setErrorAttributes(span, message) {
@@ -14,6 +14,7 @@ import { resetMcpMetrics } from "../mcp/metrics.js";
14
14
  import { BudgetTracker } from "../budget/index.js";
15
15
  import { ToadEyeAISpanProcessor } from "../vercel.js";
16
16
  import { ToadEyeSpanEndProcessor } from "./span-end-processor.js";
17
+ import { ToadEyeConsoleExporter } from "./console-exporter.js";
17
18
  const DEFAULT_ENDPOINT = "http://localhost:4318";
18
19
  const DEFAULT_CLOUD_ENDPOINT = "https://cloud.toad-eye.dev";
19
20
  let sdk = null;
@@ -66,22 +67,29 @@ export function initObservability(config) {
66
67
  return;
67
68
  }
68
69
  validateConfig(config);
69
- const { endpoint, headers, isCloudMode } = resolveTransport(config);
70
+ const isConsoleMode = config.output === "console";
71
+ const { endpoint, headers, isCloudMode } = isConsoleMode
72
+ ? { endpoint: "", headers: {}, isCloudMode: false }
73
+ : resolveTransport(config);
70
74
  const resource = resourceFromAttributes({
71
75
  [ATTR_SERVICE_NAME]: config.serviceName,
72
76
  });
73
- const traceExporter = new OTLPTraceExporter({
74
- url: `${endpoint}/v1/traces`,
75
- headers,
76
- });
77
- const metricExporter = new OTLPMetricExporter({
78
- url: `${endpoint}/v1/metrics`,
79
- headers,
80
- });
81
- const metricReader = new PeriodicExportingMetricReader({
82
- exporter: metricExporter,
83
- exportIntervalMillis: isCloudMode ? 10_000 : 5_000,
84
- });
77
+ const traceExporter = isConsoleMode
78
+ ? new ToadEyeConsoleExporter()
79
+ : new OTLPTraceExporter({
80
+ url: `${endpoint}/v1/traces`,
81
+ headers,
82
+ });
83
+ // In console mode, skip metric export — no Prometheus to send to
84
+ const metricReader = isConsoleMode
85
+ ? undefined
86
+ : new PeriodicExportingMetricReader({
87
+ exporter: new OTLPMetricExporter({
88
+ url: `${endpoint}/v1/metrics`,
89
+ headers,
90
+ }),
91
+ exportIntervalMillis: isCloudMode ? 10_000 : 5_000,
92
+ });
85
93
  // When custom SpanProcessors are needed (e.g., Vercel AI SDK, onSpanEnd),
86
94
  // we must also include a BatchSpanProcessor for trace export —
87
95
  // NodeSDK won't create one automatically when spanProcessors is provided.
@@ -107,16 +115,19 @@ export function initObservability(config) {
107
115
  sdk = new NodeSDK({
108
116
  resource,
109
117
  traceExporter,
110
- metricReader,
118
+ ...(metricReader !== undefined && { metricReader }),
111
119
  ...(spanProcessors.length > 0 && { spanProcessors }),
112
120
  ...(sampler !== undefined && { sampler }),
113
121
  });
114
122
  sdk.start();
115
123
  currentConfig = config;
116
124
  initMetrics();
125
+ if (isConsoleMode) {
126
+ console.log(`toad-eye: console mode — spans will be printed to stderr. No Docker needed.`);
127
+ }
117
128
  // Non-blocking connectivity check — warns the user if the OTel Collector is unreachable.
118
- // Fire-and-forget: does not block initObservability(). Cloud mode skips this check.
119
- if (!isCloudMode) {
129
+ // Fire-and-forget: does not block initObservability(). Cloud/console mode skips this check.
130
+ if (!isCloudMode && !isConsoleMode) {
120
131
  void fetch(`${endpoint}/v1/traces`, {
121
132
  method: "POST",
122
133
  body: "[]",
@@ -4,7 +4,6 @@ import { traceLLMCall, processContent } from "../core/spans.js";
4
4
  import { calculateCost, getModelPricing } from "../core/pricing.js";
5
5
  import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, recordTimeToFirstToken, recordResponseEmpty, recordResponseLatencyPerToken, recordContextUtilization, recordContextBlocked, recordBudgetExceeded, recordBudgetDowngraded, } from "../core/metrics.js";
6
6
  import { getConfig, getBudgetTracker } from "../core/tracer.js";
7
- import { ToadBudgetExceededError } from "../budget/index.js";
8
7
  import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "../types/index.js";
9
8
  const require = createRequire(import.meta.url);
10
9
  function isModuleInstalled(moduleName) {
@@ -8,6 +8,7 @@
8
8
  import { createRequire } from "node:module";
9
9
  import { trace, context, propagation, SpanKind } from "@opentelemetry/api";
10
10
  import { MCP_METHODS, endSpanSuccess, endSpanError } from "./spans.js";
11
+ import { recordMcpToolHallucination } from "./metrics.js";
11
12
  const require = createRequire(import.meta.url);
12
13
  const TRACER_NAME = "toad-eye-mcp-client";
13
14
  const PATCHED_FLAG = "__toad_eye_mcp_client_patched";
@@ -92,6 +93,11 @@ function patchClient(Client) {
92
93
  }
93
94
  catch (error) {
94
95
  endSpanError(span, error);
96
+ // JSON-RPC -32601 = Method not found = agent hallucinated a tool name
97
+ const code = error?.code;
98
+ if (code === -32601) {
99
+ recordMcpToolHallucination(toolName);
100
+ }
95
101
  throw error;
96
102
  }
97
103
  };
@@ -3,3 +3,4 @@ export type { TraceSamplingOptions, ToadEyeMiddlewareDispose, } from "./middlewa
3
3
  export type { ToadMcpOptions } from "./types.js";
4
4
  export { MCP_METHODS } from "./spans.js";
5
5
  export { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from "./client.js";
6
+ export { recordMcpToolHallucination } from "./metrics.js";
package/dist/mcp/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { toadEyeMiddleware, traceSampling } from "./middleware.js";
2
2
  export { MCP_METHODS } from "./spans.js";
3
3
  export { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from "./client.js";
4
+ export { recordMcpToolHallucination } from "./metrics.js";
@@ -10,6 +10,7 @@ export declare function recordMcpToolCall(toolName: string, durationMs: number,
10
10
  export declare function recordMcpToolError(toolName: string, errorType: string): void;
11
11
  export declare function recordMcpResourceRead(uri: string): void;
12
12
  export declare function recordMcpSessionStart(): void;
13
+ export declare function recordMcpToolHallucination(toolName: string): void;
13
14
  export declare function recordMcpSessionEnd(): void;
14
15
  /** Reset MCP metrics state — must be called from shutdown() alongside resetMetrics(). */
15
16
  export declare function resetMcpMetrics(): void;
@@ -13,6 +13,7 @@ const METER_NAME = "toad-eye-mcp";
13
13
  let toolDuration;
14
14
  let toolCalls;
15
15
  let toolErrors;
16
+ let toolHallucinations;
16
17
  let resourceReads;
17
18
  let sessionActive;
18
19
  let toolCallers;
@@ -32,6 +33,9 @@ function ensureInit() {
32
33
  toolErrors = meter.createCounter(GEN_AI_METRICS.MCP_TOOL_ERRORS, {
33
34
  description: "MCP tool errors by tool name and error type",
34
35
  });
36
+ toolHallucinations = meter.createCounter(GEN_AI_METRICS.MCP_TOOL_HALLUCINATIONS, {
37
+ description: "Agent tried to call a non-existent tool (JSON-RPC -32601)",
38
+ });
35
39
  resourceReads = meter.createCounter(GEN_AI_METRICS.MCP_RESOURCE_READS, {
36
40
  description: "MCP resource read count by URI",
37
41
  });
@@ -79,6 +83,13 @@ export function recordMcpSessionStart() {
79
83
  ensureInit();
80
84
  sessionActive.add(1);
81
85
  }
86
+ export function recordMcpToolHallucination(toolName) {
87
+ ensureInit();
88
+ toolHallucinations.add(1, {
89
+ "gen_ai.tool.name": toolName,
90
+ "mcp.method.name": MCP_METHODS.TOOLS_CALL,
91
+ });
92
+ }
82
93
  export function recordMcpSessionEnd() {
83
94
  ensureInit();
84
95
  sessionActive.add(-1);
@@ -22,6 +22,8 @@ export interface SamplingConfig {
22
22
  export interface ToadEyeConfig {
23
23
  readonly serviceName: string;
24
24
  readonly endpoint?: string | undefined;
25
+ /** Output mode: "otlp" (default) sends to OTel Collector, "console" prints spans to stderr (no Docker needed). */
26
+ readonly output?: "otlp" | "console" | undefined;
25
27
  /** API key for toad-eye cloud. When set, transport switches to HTTPS cloud endpoint automatically. */
26
28
  readonly apiKey?: string | undefined;
27
29
  /** Custom cloud endpoint override. Defaults to https://cloud.toad-eye.dev. */
@@ -26,12 +26,14 @@ export declare const GEN_AI_METRICS: {
26
26
  readonly RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token";
27
27
  readonly CONTEXT_UTILIZATION: "gen_ai.toad_eye.context_utilization";
28
28
  readonly CONTEXT_BLOCKED: "gen_ai.toad_eye.context.blocked";
29
+ readonly THINKING_RATIO: "gen_ai.toad_eye.thinking.ratio";
29
30
  readonly MCP_TOOL_DURATION: "gen_ai.mcp.tool.duration";
30
31
  readonly MCP_TOOL_CALLS: "gen_ai.mcp.tool.calls";
31
32
  readonly MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors";
32
33
  readonly MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads";
33
34
  readonly MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active";
34
35
  readonly MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers";
36
+ readonly MCP_TOOL_HALLUCINATIONS: "gen_ai.mcp.tool.hallucinations";
35
37
  };
36
38
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
37
39
  export declare const LLM_METRICS: {
@@ -33,6 +33,8 @@ export const GEN_AI_METRICS = {
33
33
  // Context utilization
34
34
  CONTEXT_UTILIZATION: "gen_ai.toad_eye.context_utilization",
35
35
  CONTEXT_BLOCKED: "gen_ai.toad_eye.context.blocked",
36
+ // Thinking/reasoning models
37
+ THINKING_RATIO: "gen_ai.toad_eye.thinking.ratio",
36
38
  // MCP metrics
37
39
  MCP_TOOL_DURATION: "gen_ai.mcp.tool.duration",
38
40
  MCP_TOOL_CALLS: "gen_ai.mcp.tool.calls",
@@ -40,6 +42,7 @@ export const GEN_AI_METRICS = {
40
42
  MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads",
41
43
  MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active",
42
44
  MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers",
45
+ MCP_TOOL_HALLUCINATIONS: "gen_ai.mcp.tool.hallucinations",
43
46
  };
44
47
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
45
48
  export const LLM_METRICS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Observability for MCP servers and LLM applications — traces, metrics, dashboards. One-line instrumentation for OpenAI, Anthropic, Gemini, and MCP. Self-hosted with Grafana + Prometheus + Jaeger.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -4,7 +4,7 @@ services:
4
4
  volumes:
5
5
  - ./otel-collector.yml:/etc/otelcol-contrib/config.yaml
6
6
  ports:
7
- - "4318:4318"
7
+ - "${COLLECTOR_PORT:-4318}:4318"
8
8
  depends_on:
9
9
  - jaeger
10
10
  - prometheus
@@ -14,14 +14,14 @@ services:
14
14
  volumes:
15
15
  - ./prometheus.yml:/etc/prometheus/prometheus.yml
16
16
  ports:
17
- - "9090:9090"
17
+ - "${PROMETHEUS_PORT:-9090}:9090"
18
18
 
19
19
  jaeger:
20
20
  image: jaegertracing/all-in-one:1.58
21
21
  environment:
22
22
  - COLLECTOR_OTLP_ENABLED=true
23
23
  ports:
24
- - "16686:16686"
24
+ - "${JAEGER_PORT:-16686}:16686"
25
25
  - "4317:4317"
26
26
 
27
27
  grafana:
@@ -33,7 +33,7 @@ services:
33
33
  - GF_SECURITY_ADMIN_PASSWORD=admin
34
34
  - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/llm-overview.json
35
35
  ports:
36
- - "3100:3000"
36
+ - "${GRAFANA_PORT:-3100}:3000"
37
37
  depends_on:
38
38
  - prometheus
39
39
  - jaeger
@@ -46,7 +46,8 @@
46
46
  "value": 20
47
47
  }
48
48
  ]
49
- }
49
+ },
50
+ "noValue": "No data yet — run: npx toad-eye demo"
50
51
  },
51
52
  "overrides": []
52
53
  }
@@ -72,7 +73,8 @@
72
73
  ],
73
74
  "fieldConfig": {
74
75
  "defaults": {
75
- "unit": "short"
76
+ "unit": "short",
77
+ "noValue": "No data yet — run: npx toad-eye demo"
76
78
  },
77
79
  "overrides": []
78
80
  }
@@ -98,7 +100,8 @@
98
100
  ],
99
101
  "fieldConfig": {
100
102
  "defaults": {
101
- "unit": "short"
103
+ "unit": "short",
104
+ "noValue": "No data yet — run: npx toad-eye demo"
102
105
  },
103
106
  "overrides": []
104
107
  }
@@ -124,7 +127,8 @@
124
127
  ],
125
128
  "fieldConfig": {
126
129
  "defaults": {
127
- "unit": "short"
130
+ "unit": "short",
131
+ "noValue": "No data yet — run: npx toad-eye demo"
128
132
  },
129
133
  "overrides": []
130
134
  }
@@ -157,6 +161,11 @@
157
161
  "values": ["value", "percent"]
158
162
  },
159
163
  "pieType": "donut"
164
+ },
165
+ "fieldConfig": {
166
+ "defaults": {
167
+ "noValue": "No data yet — run: npx toad-eye demo"
168
+ }
160
169
  }
161
170
  },
162
171
  {
@@ -179,7 +188,12 @@
179
188
  "legendFormat": "{{gen_ai_tool_name}}",
180
189
  "refId": "A"
181
190
  }
182
- ]
191
+ ],
192
+ "fieldConfig": {
193
+ "defaults": {
194
+ "noValue": "No data yet — run: npx toad-eye demo"
195
+ }
196
+ }
183
197
  },
184
198
  {
185
199
  "title": "Steps per Query Over Time",
@@ -211,7 +225,8 @@
211
225
  "unit": "short",
212
226
  "custom": {
213
227
  "fillOpacity": 10
214
- }
228
+ },
229
+ "noValue": "No data yet — run: npx toad-eye demo"
215
230
  },
216
231
  "overrides": []
217
232
  }
@@ -244,7 +259,8 @@
244
259
  "stacking": {
245
260
  "mode": "normal"
246
261
  }
247
- }
262
+ },
263
+ "noValue": "No data yet — run: npx toad-eye demo"
248
264
  },
249
265
  "overrides": []
250
266
  }
@@ -274,7 +290,8 @@
274
290
  "defaults": {
275
291
  "custom": {
276
292
  "fillOpacity": 80
277
- }
293
+ },
294
+ "noValue": "No data yet — run: npx toad-eye demo"
278
295
  },
279
296
  "overrides": []
280
297
  }