@introspection-sdk/introspection-node 0.4.1 → 0.4.3

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 (166) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-check-types.log +4 -0
  3. package/.turbo/turbo-lint.log +5 -0
  4. package/dist/ai-sdk-integration.d.ts.map +1 -1
  5. package/dist/ai-sdk-integration.js +2 -2
  6. package/dist/claude-hooks.d.ts.map +1 -1
  7. package/dist/claude-hooks.js +3 -3
  8. package/dist/client.d.ts +42 -132
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +66 -365
  11. package/dist/client.js.map +1 -0
  12. package/dist/converters/claude.d.ts +5 -1
  13. package/dist/converters/claude.d.ts.map +1 -1
  14. package/dist/converters/claude.js +7 -0
  15. package/dist/converters/claude.js.map +1 -0
  16. package/dist/converters/gemini.d.ts +90 -0
  17. package/dist/converters/gemini.d.ts.map +1 -0
  18. package/dist/converters/gemini.js +171 -0
  19. package/dist/converters/gemini.js.map +1 -0
  20. package/dist/converters/index.d.ts +1 -1
  21. package/dist/converters/index.d.ts.map +1 -1
  22. package/dist/converters/index.js +2 -1
  23. package/dist/converters/index.js.map +1 -0
  24. package/dist/converters/openai.js +1 -0
  25. package/dist/converters/openai.js.map +1 -0
  26. package/dist/converters/openclaw.d.ts +16 -0
  27. package/dist/converters/openclaw.d.ts.map +1 -0
  28. package/dist/converters/openclaw.js +49 -0
  29. package/dist/converters/openinference.js +1 -0
  30. package/dist/converters/openinference.js.map +1 -0
  31. package/dist/converters/vercel.d.ts.map +1 -1
  32. package/dist/converters/vercel.js +31 -3
  33. package/dist/converters/vercel.js.map +1 -0
  34. package/dist/http.d.ts +36 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +145 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +24 -27
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +22 -32
  41. package/dist/index.js.map +1 -0
  42. package/dist/langchain-handler.d.ts.map +1 -1
  43. package/dist/langchain-handler.js +2 -2
  44. package/dist/mastra-exporter.d.ts.map +1 -1
  45. package/dist/mastra-exporter.js +2 -2
  46. package/dist/otel/anthropic.d.ts +62 -0
  47. package/dist/otel/anthropic.d.ts.map +1 -0
  48. package/dist/otel/anthropic.js +425 -0
  49. package/dist/otel/anthropic.js.map +1 -0
  50. package/dist/otel/claude-hooks.d.ts +346 -0
  51. package/dist/otel/claude-hooks.d.ts.map +1 -0
  52. package/dist/otel/claude-hooks.js +645 -0
  53. package/dist/otel/claude-hooks.js.map +1 -0
  54. package/dist/otel/claude-wrapper.d.ts +93 -0
  55. package/dist/otel/claude-wrapper.d.ts.map +1 -0
  56. package/dist/otel/claude-wrapper.js +155 -0
  57. package/dist/otel/claude-wrapper.js.map +1 -0
  58. package/dist/otel/gemini.d.ts +48 -0
  59. package/dist/otel/gemini.d.ts.map +1 -0
  60. package/dist/otel/gemini.js +297 -0
  61. package/dist/otel/gemini.js.map +1 -0
  62. package/dist/otel/index.d.ts +27 -0
  63. package/dist/otel/index.d.ts.map +1 -0
  64. package/dist/otel/index.js +33 -0
  65. package/dist/otel/index.js.map +1 -0
  66. package/dist/otel/langchain-handler.d.ts +112 -0
  67. package/dist/otel/langchain-handler.d.ts.map +1 -0
  68. package/dist/otel/langchain-handler.js +555 -0
  69. package/dist/otel/langchain-handler.js.map +1 -0
  70. package/dist/otel/logs.d.ts +82 -0
  71. package/dist/otel/logs.d.ts.map +1 -0
  72. package/dist/otel/logs.js +271 -0
  73. package/dist/otel/logs.js.map +1 -0
  74. package/dist/otel/mastra-exporter.d.ts +87 -0
  75. package/dist/otel/mastra-exporter.d.ts.map +1 -0
  76. package/dist/otel/mastra-exporter.js +525 -0
  77. package/dist/otel/mastra-exporter.js.map +1 -0
  78. package/dist/otel/pi.d.ts +59 -0
  79. package/dist/otel/pi.d.ts.map +1 -0
  80. package/dist/otel/pi.js +67 -0
  81. package/dist/otel/pi.js.map +1 -0
  82. package/dist/otel/setup.d.ts +23 -0
  83. package/dist/otel/setup.d.ts.map +1 -0
  84. package/dist/otel/setup.js +127 -0
  85. package/dist/otel/setup.js.map +1 -0
  86. package/dist/otel/span-processor.d.ts +62 -0
  87. package/dist/otel/span-processor.d.ts.map +1 -0
  88. package/dist/otel/span-processor.js +319 -0
  89. package/dist/otel/span-processor.js.map +1 -0
  90. package/dist/otel/tracing-processor.d.ts +139 -0
  91. package/dist/otel/tracing-processor.d.ts.map +1 -0
  92. package/dist/otel/tracing-processor.js +418 -0
  93. package/dist/otel/tracing-processor.js.map +1 -0
  94. package/dist/resources/experiments.d.ts +31 -0
  95. package/dist/resources/experiments.d.ts.map +1 -0
  96. package/dist/resources/experiments.js +133 -0
  97. package/dist/resources/experiments.js.map +1 -0
  98. package/dist/resources/recipes.d.ts +22 -0
  99. package/dist/resources/recipes.d.ts.map +1 -0
  100. package/dist/resources/recipes.js +61 -0
  101. package/dist/resources/recipes.js.map +1 -0
  102. package/dist/resources/runtimes.d.ts +76 -0
  103. package/dist/resources/runtimes.d.ts.map +1 -0
  104. package/dist/resources/runtimes.js +179 -0
  105. package/dist/resources/runtimes.js.map +1 -0
  106. package/dist/runner-resources/files.d.ts +35 -0
  107. package/dist/runner-resources/files.d.ts.map +1 -0
  108. package/dist/runner-resources/files.js +131 -0
  109. package/dist/runner-resources/files.js.map +1 -0
  110. package/dist/runner-resources/tasks.d.ts +39 -0
  111. package/dist/runner-resources/tasks.d.ts.map +1 -0
  112. package/dist/runner-resources/tasks.js +129 -0
  113. package/dist/runner-resources/tasks.js.map +1 -0
  114. package/dist/runner.d.ts +80 -0
  115. package/dist/runner.d.ts.map +1 -0
  116. package/dist/runner.js +159 -0
  117. package/dist/runner.js.map +1 -0
  118. package/dist/span-processor.d.ts.map +1 -1
  119. package/dist/span-processor.js +3 -3
  120. package/dist/streaming.d.ts +8 -0
  121. package/dist/streaming.d.ts.map +1 -0
  122. package/dist/streaming.js +67 -0
  123. package/dist/streaming.js.map +1 -0
  124. package/dist/tracing-processor.d.ts.map +1 -1
  125. package/dist/tracing-processor.js +3 -3
  126. package/dist/types/genai.d.ts +165 -0
  127. package/dist/types/genai.d.ts.map +1 -0
  128. package/dist/types/genai.js +82 -0
  129. package/dist/types.js +1 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/utils.d.ts +1 -0
  132. package/dist/utils.d.ts.map +1 -1
  133. package/dist/utils.js +14 -0
  134. package/dist/utils.js.map +1 -0
  135. package/dist/version.js +1 -0
  136. package/dist/version.js.map +1 -0
  137. package/package.json +83 -25
  138. package/src/client.ts +76 -471
  139. package/src/converters/claude.ts +11 -1
  140. package/src/converters/gemini.ts +243 -0
  141. package/src/converters/index.ts +13 -7
  142. package/src/converters/vercel.ts +41 -3
  143. package/src/http.ts +163 -0
  144. package/src/index.ts +110 -75
  145. package/src/{anthropic.ts → otel/anthropic.ts} +95 -8
  146. package/src/{claude-hooks.ts → otel/claude-hooks.ts} +125 -24
  147. package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
  148. package/src/otel/gemini.ts +397 -0
  149. package/src/otel/index.ts +80 -0
  150. package/src/{langchain-handler.ts → otel/langchain-handler.ts} +56 -7
  151. package/src/otel/logs.ts +394 -0
  152. package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +86 -23
  153. package/src/otel/pi.ts +88 -0
  154. package/src/otel/setup.ts +174 -0
  155. package/src/{span-processor.ts → otel/span-processor.ts} +17 -10
  156. package/src/{tracing-processor.ts → otel/tracing-processor.ts} +100 -23
  157. package/src/resources/experiments.ts +170 -0
  158. package/src/resources/recipes.ts +73 -0
  159. package/src/resources/runtimes.ts +231 -0
  160. package/src/runner-resources/files.ts +163 -0
  161. package/src/runner-resources/tasks.ts +156 -0
  162. package/src/runner.ts +194 -0
  163. package/src/streaming.ts +57 -0
  164. package/src/utils.ts +15 -0
  165. package/src/ai-sdk-integration.ts +0 -689
  166. package/src/converters/ai-sdk.ts +0 -323
@@ -1,689 +0,0 @@
1
- /**
2
- * AI SDK TelemetryIntegration for Introspection.
3
- *
4
- * Implements the Vercel AI SDK's TelemetryIntegration interface to capture
5
- * LLM interactions and export them as OpenTelemetry spans with gen_ai.*
6
- * semantic convention attributes to the Introspection backend.
7
- *
8
- * Similar to IntrospectionTracingProcessor for OpenAI Agents SDK and
9
- * IntrospectionClaudeHooks for Claude Agent SDK.
10
- *
11
- * @example
12
- * ```ts
13
- * import { IntrospectionAISDKIntegration } from "@introspection-sdk/introspection-node";
14
- * import { generateText } from "ai";
15
- * import { openai } from "@ai-sdk/openai";
16
- *
17
- * const introspection = new IntrospectionAISDKIntegration({
18
- * token: "sk-intro-...",
19
- * serviceName: "my-app",
20
- * });
21
- *
22
- * const { text } = await generateText({
23
- * model: openai("gpt-4o"),
24
- * prompt: "Hello!",
25
- * experimental_telemetry: {
26
- * isEnabled: true,
27
- * integrations: [introspection],
28
- * },
29
- * });
30
- *
31
- * await introspection.shutdown();
32
- * ```
33
- */
34
-
35
- import {
36
- context as otelContext,
37
- trace as otelTrace,
38
- type Span as OtelSpan,
39
- } from "@opentelemetry/api";
40
- import {
41
- BasicTracerProvider,
42
- BatchSpanProcessor,
43
- SimpleSpanProcessor,
44
- type SpanExporter,
45
- type IdGenerator,
46
- } from "@opentelemetry/sdk-trace-base";
47
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
48
- import { resourceFromAttributes } from "@opentelemetry/resources";
49
- import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
50
- import { randomUUID } from "crypto";
51
-
52
- import {
53
- convertMessagesToInputMessages,
54
- extractSystemInstructions,
55
- buildOutputMessages,
56
- convertToolsToToolDefinitions,
57
- } from "./converters/ai-sdk.js";
58
- import type { InputMessage } from "@introspection-sdk/types";
59
- import { logger } from "./utils.js";
60
- import { VERSION } from "./version.js";
61
-
62
- // ---------------------------------------------------------------------------
63
- // Configuration types
64
- // ---------------------------------------------------------------------------
65
-
66
- /** Advanced options for testing and customization. */
67
- export interface AISDKIntegrationAdvancedOptions {
68
- /** Custom span exporter (for testing — use InMemorySpanExporter). */
69
- spanExporter?: SpanExporter;
70
- /** Custom ID generator (for testing). */
71
- idGenerator?: IdGenerator;
72
- /** Use SimpleSpanProcessor instead of BatchSpanProcessor (for testing). */
73
- useSimpleSpanProcessor?: boolean;
74
- /** Maximum batch size for span export. */
75
- maxExportBatchSize?: number;
76
- /** Delay between batch exports in milliseconds. */
77
- scheduledDelayMillis?: number;
78
- }
79
-
80
- /** Configuration for {@link IntrospectionAISDKIntegration}. */
81
- export interface IntrospectionAISDKIntegrationOptions {
82
- /** Authentication token (env: INTROSPECTION_TOKEN). */
83
- token?: string;
84
- /** Base URL for the API (env: INTROSPECTION_BASE_URL, default: "https://otel.introspection.dev"). */
85
- baseUrl?: string;
86
- /** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME). */
87
- serviceName?: string;
88
- /** Additional headers to include in requests. */
89
- additionalHeaders?: Record<string, string>;
90
- /** Advanced options for testing and customization. */
91
- advanced?: AISDKIntegrationAdvancedOptions;
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // Internal state
96
- // ---------------------------------------------------------------------------
97
-
98
- interface GenerationState {
99
- rootSpan: OtelSpan;
100
- conversationId: string;
101
- system: unknown;
102
- metadata: Record<string, unknown> | undefined;
103
- stepSpans: Map<number, OtelSpan>;
104
- stepMessages: Map<number, InputMessage[]>;
105
- toolCallSpans: Map<string, OtelSpan>;
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // Integration class
110
- // ---------------------------------------------------------------------------
111
-
112
- /**
113
- * AI SDK {@link TelemetryIntegration} that captures LLM interactions and
114
- * exports them as OpenTelemetry spans with `gen_ai.*` semantic convention
115
- * attributes to the Introspection backend.
116
- *
117
- * Creates its own {@link BasicTracerProvider} — no external OTel setup required.
118
- * Each generation (generateText/streamText call) produces:
119
- * - A root span grouping the entire operation
120
- * - Step spans (one per LLM call) with full gen_ai attributes
121
- * - Tool call spans nested under their parent step
122
- */
123
- export class IntrospectionAISDKIntegration {
124
- private _tracerProvider: BasicTracerProvider;
125
- private _tracer: ReturnType<BasicTracerProvider["getTracer"]>;
126
- private _generation: GenerationState | null = null;
127
-
128
- constructor(options: IntrospectionAISDKIntegrationOptions = {}) {
129
- const advanced = options.advanced;
130
- const serviceName =
131
- options.serviceName || process.env.INTROSPECTION_SERVICE_NAME;
132
- const resource = serviceName
133
- ? resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName })
134
- : undefined;
135
-
136
- // Production mode — requires a token
137
- if (!advanced?.spanExporter) {
138
- const token = options.token || process.env.INTROSPECTION_TOKEN;
139
- if (!token) {
140
- throw new Error("INTROSPECTION_TOKEN is required");
141
- }
142
-
143
- const baseUrl =
144
- options.baseUrl ||
145
- process.env.INTROSPECTION_BASE_URL ||
146
- "https://otel.introspection.dev";
147
-
148
- const headers: Record<string, string> = {
149
- "User-Agent": `introspection-sdk/${VERSION}`,
150
- Authorization: `Bearer ${token}`,
151
- ...options.additionalHeaders,
152
- };
153
-
154
- const endpoint = baseUrl.endsWith("/v1/traces")
155
- ? baseUrl
156
- : `${baseUrl.replace(/\/$/, "")}/v1/traces`;
157
-
158
- logger.info(
159
- `IntrospectionAISDKIntegration initialized: endpoint=${endpoint}`,
160
- );
161
-
162
- const spanExporter = new OTLPTraceExporter({ url: endpoint, headers });
163
-
164
- // Use SimpleSpanProcessor for sequential export (dev/staging tokens)
165
- const effectiveBatchSize =
166
- advanced?.maxExportBatchSize ??
167
- (token.startsWith("intro_dev") || token.startsWith("intro_staging")
168
- ? 1
169
- : undefined);
170
- const useSimple =
171
- advanced?.useSimpleSpanProcessor || effectiveBatchSize === 1;
172
- const scheduledDelayMillis = advanced?.scheduledDelayMillis ?? 1000;
173
-
174
- const spanProcessor = useSimple
175
- ? new SimpleSpanProcessor(spanExporter)
176
- : new BatchSpanProcessor(spanExporter, {
177
- scheduledDelayMillis,
178
- ...(effectiveBatchSize
179
- ? { maxExportBatchSize: effectiveBatchSize }
180
- : {}),
181
- });
182
-
183
- this._tracerProvider = new BasicTracerProvider({
184
- resource,
185
- idGenerator: advanced?.idGenerator,
186
- spanProcessors: [spanProcessor],
187
- });
188
- } else {
189
- // Testing mode — use provided exporter
190
- logger.info(
191
- `IntrospectionAISDKIntegration initialized in test mode with custom exporter`,
192
- );
193
-
194
- const spanProcessor = advanced.useSimpleSpanProcessor
195
- ? new SimpleSpanProcessor(advanced.spanExporter)
196
- : new BatchSpanProcessor(advanced.spanExporter, {
197
- scheduledDelayMillis: 100,
198
- });
199
-
200
- this._tracerProvider = new BasicTracerProvider({
201
- resource,
202
- idGenerator: advanced.idGenerator,
203
- spanProcessors: [spanProcessor],
204
- });
205
- }
206
-
207
- this._tracer = this._tracerProvider.getTracer("ai-sdk", VERSION);
208
- }
209
-
210
- // -------------------------------------------------------------------------
211
- // TelemetryIntegration lifecycle methods
212
- //
213
- // Defined as arrow functions to preserve `this` binding when the AI SDK
214
- // calls them as plain function references.
215
- //
216
- // Event parameters use `unknown` for TypeScript compatibility with the
217
- // AI SDK's TelemetryIntegration interface (avoids index signature mismatch).
218
- // -------------------------------------------------------------------------
219
-
220
- /**
221
- * Called when a generation operation begins (before any LLM calls).
222
- * Creates a root span to group all steps in this generation.
223
- */
224
- onStart = (event: unknown): void => {
225
- try {
226
- const e = event as Record<string, unknown>;
227
- // End any lingering previous generation (safety net)
228
- if (this._generation) {
229
- this._endGeneration();
230
- }
231
-
232
- const model = e.model as
233
- | { provider?: string; modelId?: string }
234
- | undefined;
235
- const functionId = e.functionId as string | undefined;
236
- const metadata = e.metadata as Record<string, unknown> | undefined;
237
- const name = functionId || "ai-sdk-generation";
238
-
239
- // Conversation ID: metadata > auto-generate
240
- const metadataConvId = metadata?.["gen_ai.conversation.id"] as
241
- | string
242
- | undefined;
243
- const conversationId =
244
- metadataConvId || `intro_conv_${randomUUID().replace(/-/g, "")}`;
245
-
246
- const rootSpan = this._tracer.startSpan(name);
247
- rootSpan.setAttribute("gen_ai.conversation.id", conversationId);
248
- if (functionId) {
249
- rootSpan.setAttribute("gen_ai.agent.name", functionId);
250
- }
251
- if (model?.provider) {
252
- rootSpan.setAttribute("gen_ai.system", model.provider);
253
- }
254
-
255
- // Pass through all telemetry metadata as span attributes
256
- if (metadata) {
257
- for (const [key, value] of Object.entries(metadata)) {
258
- if (value !== undefined && value !== null) {
259
- rootSpan.setAttribute(
260
- `ai.telemetry.metadata.${key}`,
261
- typeof value === "string" ? value : JSON.stringify(value),
262
- );
263
- }
264
- }
265
- }
266
-
267
- this._generation = {
268
- rootSpan,
269
- conversationId,
270
- system: e.system,
271
- metadata,
272
- stepSpans: new Map(),
273
- stepMessages: new Map(),
274
- toolCallSpans: new Map(),
275
- };
276
-
277
- logger.debug(`Generation started: ${name} (${conversationId})`);
278
- } catch (error) {
279
- logger.error(
280
- `onStart error: ${error instanceof Error ? error.message : String(error)}`,
281
- );
282
- }
283
- };
284
-
285
- /**
286
- * Called when a step (individual LLM call) begins.
287
- * Creates a child span under the root span and captures input messages.
288
- */
289
- onStepStart = (event: unknown): void => {
290
- try {
291
- const e = event as Record<string, unknown>;
292
- const gen = this._generation;
293
- if (!gen) return;
294
-
295
- const stepNumber = (e.stepNumber as number) ?? 0;
296
- const model = e.model as
297
- | { provider?: string; modelId?: string }
298
- | undefined;
299
- const messages = (e.messages as readonly unknown[]) || [];
300
- const system = e.system ?? gen.system;
301
- const tools = e.tools;
302
- const functionId = e.functionId as string | undefined;
303
-
304
- // Create step span as child of root
305
- const parentCtx = otelTrace.setSpan(otelContext.active(), gen.rootSpan);
306
- const spanName = model?.modelId
307
- ? `chat ${model.modelId}`
308
- : `step-${stepNumber}`;
309
- const stepSpan = this._tracer.startSpan(spanName, {}, parentCtx);
310
-
311
- gen.stepSpans.set(stepNumber, stepSpan);
312
-
313
- // Capture input messages for use in onStepFinish
314
- const inputMessages = convertMessagesToInputMessages(messages);
315
- gen.stepMessages.set(stepNumber, inputMessages);
316
-
317
- // Set input-side attributes immediately
318
- stepSpan.setAttribute("gen_ai.operation.name", "chat");
319
- stepSpan.setAttribute("gen_ai.conversation.id", gen.conversationId);
320
-
321
- if (model?.provider) {
322
- stepSpan.setAttribute("gen_ai.system", model.provider);
323
- }
324
- if (model?.modelId) {
325
- stepSpan.setAttribute("gen_ai.request.model", model.modelId);
326
- }
327
-
328
- // System instructions
329
- const sysInstructions = extractSystemInstructions(system, messages);
330
- if (sysInstructions) {
331
- stepSpan.setAttribute(
332
- "gen_ai.system_instructions",
333
- JSON.stringify(sysInstructions),
334
- );
335
- }
336
-
337
- // Tool definitions
338
- const toolDefs = convertToolsToToolDefinitions(tools);
339
- if (toolDefs) {
340
- stepSpan.setAttribute(
341
- "gen_ai.tool.definitions",
342
- JSON.stringify(toolDefs),
343
- );
344
- }
345
-
346
- // Agent name from functionId
347
- if (functionId) {
348
- stepSpan.setAttribute("gen_ai.agent.name", functionId);
349
- }
350
-
351
- // Pass through telemetry metadata to step spans
352
- if (gen.metadata) {
353
- for (const [key, value] of Object.entries(gen.metadata)) {
354
- if (value !== undefined && value !== null) {
355
- stepSpan.setAttribute(
356
- `ai.telemetry.metadata.${key}`,
357
- typeof value === "string" ? value : JSON.stringify(value),
358
- );
359
- }
360
- }
361
- }
362
-
363
- logger.debug(`Step ${stepNumber} started: ${spanName}`);
364
- } catch (error) {
365
- logger.error(
366
- `onStepStart error: ${error instanceof Error ? error.message : String(error)}`,
367
- );
368
- }
369
- };
370
-
371
- /**
372
- * Called when a tool execution begins.
373
- * Creates a child span under the current step span.
374
- */
375
- onToolCallStart = (event: unknown): void => {
376
- try {
377
- const e = event as Record<string, unknown>;
378
- const gen = this._generation;
379
- if (!gen) return;
380
-
381
- const toolCall = e.toolCall as
382
- | { toolCallId?: string; toolName?: string; input?: unknown }
383
- | undefined;
384
- if (!toolCall) return;
385
-
386
- const stepNumber = (e.stepNumber as number) ?? 0;
387
- const parentSpan = gen.stepSpans.get(stepNumber) || gen.rootSpan;
388
- const parentCtx = otelTrace.setSpan(otelContext.active(), parentSpan);
389
-
390
- const toolName = toolCall.toolName || "unknown-tool";
391
- const toolSpan = this._tracer.startSpan(toolName, {}, parentCtx);
392
- toolSpan.setAttribute("gen_ai.tool.name", toolName);
393
- toolSpan.setAttribute("openinference.span.kind", "TOOL");
394
-
395
- if (toolCall.input !== undefined) {
396
- toolSpan.setAttribute(
397
- "gen_ai.tool.input",
398
- typeof toolCall.input === "string"
399
- ? toolCall.input
400
- : JSON.stringify(toolCall.input),
401
- );
402
- }
403
-
404
- // Propagate conversation ID
405
- toolSpan.setAttribute("gen_ai.conversation.id", gen.conversationId);
406
-
407
- const toolCallId =
408
- toolCall.toolCallId || `tool-${stepNumber}-${toolName}`;
409
- gen.toolCallSpans.set(toolCallId, toolSpan);
410
-
411
- logger.debug(`Tool call started: ${toolName} (${toolCallId})`);
412
- } catch (error) {
413
- logger.error(
414
- `onToolCallStart error: ${error instanceof Error ? error.message : String(error)}`,
415
- );
416
- }
417
- };
418
-
419
- /**
420
- * Called when a tool execution completes.
421
- * Sets output/error attributes and ends the tool span.
422
- */
423
- onToolCallFinish = (event: unknown): void => {
424
- try {
425
- const e = event as Record<string, unknown>;
426
- const gen = this._generation;
427
- if (!gen) return;
428
-
429
- const toolCall = e.toolCall as
430
- | { toolCallId?: string; toolName?: string }
431
- | undefined;
432
- if (!toolCall) return;
433
-
434
- const stepNumber = (e.stepNumber as number) ?? 0;
435
- const toolCallId =
436
- toolCall.toolCallId ||
437
- `tool-${stepNumber}-${toolCall.toolName || "unknown"}`;
438
- const toolSpan = gen.toolCallSpans.get(toolCallId);
439
- if (!toolSpan) return;
440
-
441
- gen.toolCallSpans.delete(toolCallId);
442
-
443
- const success = e.success as boolean;
444
- if (success && e.output !== undefined) {
445
- toolSpan.setAttribute(
446
- "gen_ai.tool.output",
447
- typeof e.output === "string" ? e.output : JSON.stringify(e.output),
448
- );
449
- } else if (!success && e.error !== undefined) {
450
- toolSpan.setAttribute(
451
- "gen_ai.tool.error",
452
- typeof e.error === "string"
453
- ? e.error
454
- : e.error instanceof Error
455
- ? e.error.message
456
- : JSON.stringify(e.error),
457
- );
458
- }
459
-
460
- if (typeof e.durationMs === "number") {
461
- toolSpan.setAttribute("gen_ai.tool.duration_ms", e.durationMs);
462
- }
463
-
464
- toolSpan.end();
465
- logger.debug(`Tool call finished: ${toolCall.toolName} (${toolCallId})`);
466
- } catch (error) {
467
- logger.error(
468
- `onToolCallFinish error: ${error instanceof Error ? error.message : String(error)}`,
469
- );
470
- }
471
- };
472
-
473
- /**
474
- * Called when a step (LLM call) completes.
475
- * Sets gen_ai attributes (input/output messages, usage, model) and ends
476
- * the step span.
477
- */
478
- onStepFinish = (event: unknown): void => {
479
- try {
480
- const e = event as Record<string, unknown>;
481
- const gen = this._generation;
482
- if (!gen) return;
483
-
484
- const stepNumber = (e.stepNumber as number) ?? 0;
485
- const stepSpan = gen.stepSpans.get(stepNumber);
486
- if (!stepSpan) {
487
- logger.debug(`No step span found for step ${stepNumber}`);
488
- return;
489
- }
490
-
491
- gen.stepSpans.delete(stepNumber);
492
-
493
- // Input messages (captured in onStepStart)
494
- const inputMessages = gen.stepMessages.get(stepNumber);
495
- gen.stepMessages.delete(stepNumber);
496
-
497
- if (inputMessages && inputMessages.length > 0) {
498
- stepSpan.setAttribute(
499
- "gen_ai.input.messages",
500
- JSON.stringify(inputMessages),
501
- );
502
- }
503
-
504
- // Output messages
505
- const outputMessages = buildOutputMessages({
506
- text: e.text as string | undefined,
507
- reasoningText: e.reasoningText as string | undefined,
508
- reasoning: e.reasoning as readonly { text?: string }[] | undefined,
509
- toolCalls: e.toolCalls as
510
- | readonly {
511
- toolCallId: string;
512
- toolName: string;
513
- input: unknown;
514
- }[]
515
- | undefined,
516
- finishReason: e.finishReason as string | undefined,
517
- });
518
- if (outputMessages.length > 0) {
519
- stepSpan.setAttribute(
520
- "gen_ai.output.messages",
521
- JSON.stringify(outputMessages),
522
- );
523
- }
524
-
525
- // Token usage
526
- const usage = e.usage as
527
- | {
528
- inputTokens?: number;
529
- outputTokens?: number;
530
- inputTokenDetails?: {
531
- cacheReadTokens?: number;
532
- cacheWriteTokens?: number;
533
- };
534
- outputTokenDetails?: { reasoningTokens?: number };
535
- }
536
- | undefined;
537
- if (usage) {
538
- if (usage.inputTokens !== undefined) {
539
- stepSpan.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
540
- }
541
- if (usage.outputTokens !== undefined) {
542
- stepSpan.setAttribute(
543
- "gen_ai.usage.output_tokens",
544
- usage.outputTokens,
545
- );
546
- }
547
- if (usage.inputTokenDetails?.cacheReadTokens !== undefined) {
548
- stepSpan.setAttribute(
549
- "gen_ai.usage.cache_read.input_tokens",
550
- usage.inputTokenDetails.cacheReadTokens,
551
- );
552
- }
553
- if (usage.inputTokenDetails?.cacheWriteTokens !== undefined) {
554
- stepSpan.setAttribute(
555
- "gen_ai.usage.cache_creation.input_tokens",
556
- usage.inputTokenDetails.cacheWriteTokens,
557
- );
558
- }
559
- }
560
-
561
- // Response metadata
562
- const response = e.response as
563
- | { id?: string; modelId?: string }
564
- | undefined;
565
- if (response?.id) {
566
- stepSpan.setAttribute("gen_ai.response.id", response.id);
567
- }
568
- if (response?.modelId) {
569
- stepSpan.setAttribute("gen_ai.response.model", response.modelId);
570
- }
571
-
572
- // Finish reason
573
- const finishReason = e.finishReason as string | undefined;
574
- if (finishReason) {
575
- stepSpan.setAttribute("gen_ai.response.finish_reasons", [finishReason]);
576
- }
577
-
578
- // Model info (reinforces what was set in onStepStart)
579
- const model = e.model as
580
- | { provider?: string; modelId?: string }
581
- | undefined;
582
- if (model?.provider) {
583
- stepSpan.setAttribute("gen_ai.system", model.provider);
584
- }
585
- if (model?.modelId) {
586
- stepSpan.setAttribute("gen_ai.request.model", model.modelId);
587
- }
588
-
589
- stepSpan.setAttribute("openinference.span.kind", "LLM");
590
- stepSpan.end();
591
- logger.debug(`Step ${stepNumber} finished`);
592
- } catch (error) {
593
- logger.error(
594
- `onStepFinish error: ${error instanceof Error ? error.message : String(error)}`,
595
- );
596
- }
597
- };
598
-
599
- /**
600
- * Called when the entire generation completes (all steps finished).
601
- * Sets total usage on the root span and ends it.
602
- */
603
- onFinish = (event: unknown): void => {
604
- try {
605
- const e = event as Record<string, unknown>;
606
- const gen = this._generation;
607
- if (!gen) return;
608
-
609
- // End any orphaned tool call spans
610
- for (const [id, span] of gen.toolCallSpans) {
611
- span.end();
612
- logger.debug(`Ending orphaned tool call span: ${id}`);
613
- }
614
- gen.toolCallSpans.clear();
615
-
616
- // End any orphaned step spans
617
- for (const [stepNumber, span] of gen.stepSpans) {
618
- span.end();
619
- logger.debug(`Ending orphaned step span: ${stepNumber}`);
620
- }
621
- gen.stepSpans.clear();
622
-
623
- // Total usage on root span
624
- const totalUsage = e.totalUsage as
625
- | { inputTokens?: number; outputTokens?: number }
626
- | undefined;
627
- if (totalUsage) {
628
- if (totalUsage.inputTokens !== undefined) {
629
- gen.rootSpan.setAttribute(
630
- "gen_ai.usage.input_tokens",
631
- totalUsage.inputTokens,
632
- );
633
- }
634
- if (totalUsage.outputTokens !== undefined) {
635
- gen.rootSpan.setAttribute(
636
- "gen_ai.usage.output_tokens",
637
- totalUsage.outputTokens,
638
- );
639
- }
640
- }
641
-
642
- gen.rootSpan.end();
643
- this._generation = null;
644
- logger.debug("Generation finished");
645
- } catch (error) {
646
- logger.error(
647
- `onFinish error: ${error instanceof Error ? error.message : String(error)}`,
648
- );
649
- }
650
- };
651
-
652
- // -------------------------------------------------------------------------
653
- // Lifecycle methods
654
- // -------------------------------------------------------------------------
655
-
656
- /**
657
- * Shut down the tracer provider, flushing all pending spans.
658
- *
659
- * @returns A promise that resolves once shutdown is complete.
660
- */
661
- async shutdown(): Promise<void> {
662
- this._endGeneration();
663
- await this._tracerProvider.shutdown();
664
- }
665
-
666
- /**
667
- * Force-flush any buffered spans to the Introspection backend.
668
- *
669
- * @returns A promise that resolves once the flush completes.
670
- */
671
- async forceFlush(): Promise<void> {
672
- await this._tracerProvider.forceFlush();
673
- }
674
-
675
- // -------------------------------------------------------------------------
676
- // Internal helpers
677
- // -------------------------------------------------------------------------
678
-
679
- /** Cleanly end any active generation, closing all open spans. */
680
- private _endGeneration(): void {
681
- const gen = this._generation;
682
- if (!gen) return;
683
-
684
- for (const span of gen.toolCallSpans.values()) span.end();
685
- for (const span of gen.stepSpans.values()) span.end();
686
- gen.rootSpan.end();
687
- this._generation = null;
688
- }
689
- }