@introspection-sdk/introspection-node 0.19.1 → 0.21.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 (211) hide show
  1. package/README.md +28 -8
  2. package/dist/auth.d.ts +1 -1
  3. package/dist/client.d.ts +4 -3
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +16 -7
  6. package/dist/client.js.map +1 -1
  7. package/dist/dev-target.js +1 -1
  8. package/dist/index.d.ts +4 -7
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -6
  11. package/dist/index.js.map +1 -1
  12. package/dist/otel/index.d.ts +1 -14
  13. package/dist/otel/index.d.ts.map +1 -1
  14. package/dist/otel/index.js +10 -18
  15. package/dist/otel/index.js.map +1 -1
  16. package/dist/otel/init.d.ts +19 -21
  17. package/dist/otel/init.d.ts.map +1 -1
  18. package/dist/otel/init.js +62 -48
  19. package/dist/otel/init.js.map +1 -1
  20. package/dist/otel/integrations/base.d.ts +17 -37
  21. package/dist/otel/integrations/base.d.ts.map +1 -1
  22. package/dist/otel/integrations/base.js +6 -13
  23. package/dist/otel/integrations/base.js.map +1 -1
  24. package/dist/otel/integrations/index.d.ts +3 -4
  25. package/dist/otel/integrations/index.d.ts.map +1 -1
  26. package/dist/otel/integrations/index.js +6 -25
  27. package/dist/otel/integrations/index.js.map +1 -1
  28. package/dist/otel/integrations/pi.d.ts.map +1 -1
  29. package/dist/otel/integrations/pi.js +9 -1
  30. package/dist/otel/integrations/pi.js.map +1 -1
  31. package/dist/otel/logs.d.ts +41 -8
  32. package/dist/otel/logs.d.ts.map +1 -1
  33. package/dist/otel/logs.js +95 -66
  34. package/dist/otel/logs.js.map +1 -1
  35. package/dist/otel/pi.d.ts +18 -5
  36. package/dist/otel/pi.d.ts.map +1 -1
  37. package/dist/otel/pi.js +38 -9
  38. package/dist/otel/pi.js.map +1 -1
  39. package/dist/otel/register.d.ts +2 -2
  40. package/dist/otel/register.d.ts.map +1 -1
  41. package/dist/otel/register.js +7 -9
  42. package/dist/otel/register.js.map +1 -1
  43. package/dist/otel/setup.d.ts +2 -2
  44. package/dist/otel/setup.d.ts.map +1 -1
  45. package/dist/otel/setup.js +16 -6
  46. package/dist/otel/setup.js.map +1 -1
  47. package/dist/otel/span-processor.d.ts +18 -5
  48. package/dist/otel/span-processor.d.ts.map +1 -1
  49. package/dist/otel/span-processor.js +50 -65
  50. package/dist/otel/span-processor.js.map +1 -1
  51. package/dist/resources/experiments.d.ts +10 -5
  52. package/dist/resources/experiments.d.ts.map +1 -1
  53. package/dist/resources/experiments.js +9 -25
  54. package/dist/resources/experiments.js.map +1 -1
  55. package/dist/resources/recipes.d.ts +6 -6
  56. package/dist/resources/recipes.d.ts.map +1 -1
  57. package/dist/resources/recipes.js +5 -23
  58. package/dist/resources/recipes.js.map +1 -1
  59. package/dist/resources/runtimes.d.ts +2 -2
  60. package/dist/resources/runtimes.d.ts.map +1 -1
  61. package/dist/resources/runtimes.js +2 -2
  62. package/dist/resources/runtimes.js.map +1 -1
  63. package/dist/runner.d.ts +8 -4
  64. package/dist/runner.d.ts.map +1 -1
  65. package/dist/runner.js +26 -18
  66. package/dist/runner.js.map +1 -1
  67. package/dist/types.d.ts +1 -1
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +1 -1
  70. package/dist/types.js.map +1 -1
  71. package/dist/utils.d.ts +45 -0
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +61 -2
  74. package/dist/utils.js.map +1 -1
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.d.ts.map +1 -1
  77. package/dist/version.js +1 -1
  78. package/dist/version.js.map +1 -1
  79. package/package.json +18 -60
  80. package/.turbo/turbo-build.log +0 -4
  81. package/dist/converters/claude.d.ts +0 -150
  82. package/dist/converters/claude.d.ts.map +0 -1
  83. package/dist/converters/claude.js +0 -211
  84. package/dist/converters/claude.js.map +0 -1
  85. package/dist/converters/gemini.d.ts +0 -90
  86. package/dist/converters/gemini.d.ts.map +0 -1
  87. package/dist/converters/gemini.js +0 -171
  88. package/dist/converters/gemini.js.map +0 -1
  89. package/dist/converters/index.d.ts +0 -9
  90. package/dist/converters/index.d.ts.map +0 -1
  91. package/dist/converters/index.js +0 -9
  92. package/dist/converters/index.js.map +0 -1
  93. package/dist/converters/openai.d.ts +0 -51
  94. package/dist/converters/openai.d.ts.map +0 -1
  95. package/dist/converters/openai.js +0 -269
  96. package/dist/converters/openai.js.map +0 -1
  97. package/dist/converters/openinference.d.ts +0 -107
  98. package/dist/converters/openinference.d.ts.map +0 -1
  99. package/dist/converters/openinference.js +0 -553
  100. package/dist/converters/openinference.js.map +0 -1
  101. package/dist/converters/vercel.d.ts +0 -21
  102. package/dist/converters/vercel.d.ts.map +0 -1
  103. package/dist/converters/vercel.js +0 -236
  104. package/dist/converters/vercel.js.map +0 -1
  105. package/dist/otel/anthropic.d.ts +0 -63
  106. package/dist/otel/anthropic.d.ts.map +0 -1
  107. package/dist/otel/anthropic.js +0 -475
  108. package/dist/otel/anthropic.js.map +0 -1
  109. package/dist/otel/claude-hooks.d.ts +0 -346
  110. package/dist/otel/claude-hooks.d.ts.map +0 -1
  111. package/dist/otel/claude-hooks.js +0 -647
  112. package/dist/otel/claude-hooks.js.map +0 -1
  113. package/dist/otel/claude-wrapper.d.ts +0 -93
  114. package/dist/otel/claude-wrapper.d.ts.map +0 -1
  115. package/dist/otel/claude-wrapper.js +0 -155
  116. package/dist/otel/claude-wrapper.js.map +0 -1
  117. package/dist/otel/gemini.d.ts +0 -56
  118. package/dist/otel/gemini.d.ts.map +0 -1
  119. package/dist/otel/gemini.js +0 -350
  120. package/dist/otel/gemini.js.map +0 -1
  121. package/dist/otel/integrations/anthropic.d.ts +0 -10
  122. package/dist/otel/integrations/anthropic.d.ts.map +0 -1
  123. package/dist/otel/integrations/anthropic.js +0 -26
  124. package/dist/otel/integrations/anthropic.js.map +0 -1
  125. package/dist/otel/integrations/claude-agent.d.ts +0 -12
  126. package/dist/otel/integrations/claude-agent.d.ts.map +0 -1
  127. package/dist/otel/integrations/claude-agent.js +0 -28
  128. package/dist/otel/integrations/claude-agent.js.map +0 -1
  129. package/dist/otel/integrations/gemini.d.ts +0 -11
  130. package/dist/otel/integrations/gemini.d.ts.map +0 -1
  131. package/dist/otel/integrations/gemini.js +0 -23
  132. package/dist/otel/integrations/gemini.js.map +0 -1
  133. package/dist/otel/integrations/langchain.d.ts +0 -15
  134. package/dist/otel/integrations/langchain.d.ts.map +0 -1
  135. package/dist/otel/integrations/langchain.js +0 -43
  136. package/dist/otel/integrations/langchain.js.map +0 -1
  137. package/dist/otel/integrations/mastra.d.ts +0 -14
  138. package/dist/otel/integrations/mastra.d.ts.map +0 -1
  139. package/dist/otel/integrations/mastra.js +0 -31
  140. package/dist/otel/integrations/mastra.js.map +0 -1
  141. package/dist/otel/integrations/openai-agents.d.ts +0 -11
  142. package/dist/otel/integrations/openai-agents.d.ts.map +0 -1
  143. package/dist/otel/integrations/openai-agents.js +0 -30
  144. package/dist/otel/integrations/openai-agents.js.map +0 -1
  145. package/dist/otel/integrations/vercel.d.ts +0 -14
  146. package/dist/otel/integrations/vercel.d.ts.map +0 -1
  147. package/dist/otel/integrations/vercel.js +0 -20
  148. package/dist/otel/integrations/vercel.js.map +0 -1
  149. package/dist/otel/langchain-handler.d.ts +0 -112
  150. package/dist/otel/langchain-handler.d.ts.map +0 -1
  151. package/dist/otel/langchain-handler.js +0 -561
  152. package/dist/otel/langchain-handler.js.map +0 -1
  153. package/dist/otel/mastra-exporter.d.ts +0 -87
  154. package/dist/otel/mastra-exporter.d.ts.map +0 -1
  155. package/dist/otel/mastra-exporter.js +0 -530
  156. package/dist/otel/mastra-exporter.js.map +0 -1
  157. package/dist/otel/openai.d.ts +0 -20
  158. package/dist/otel/openai.d.ts.map +0 -1
  159. package/dist/otel/openai.js +0 -55
  160. package/dist/otel/openai.js.map +0 -1
  161. package/dist/otel/tracing-processor.d.ts +0 -139
  162. package/dist/otel/tracing-processor.d.ts.map +0 -1
  163. package/dist/otel/tracing-processor.js +0 -434
  164. package/dist/otel/tracing-processor.js.map +0 -1
  165. package/eslint.config.mjs +0 -4
  166. package/src/auth.ts +0 -236
  167. package/src/client.ts +0 -163
  168. package/src/converters/claude.ts +0 -343
  169. package/src/converters/gemini.ts +0 -243
  170. package/src/converters/index.ts +0 -50
  171. package/src/converters/openai.ts +0 -300
  172. package/src/converters/openinference.ts +0 -665
  173. package/src/converters/vercel.ts +0 -302
  174. package/src/dev-target.ts +0 -79
  175. package/src/http.ts +0 -43
  176. package/src/index.ts +0 -266
  177. package/src/otel/anthropic.ts +0 -619
  178. package/src/otel/claude-hooks.ts +0 -1116
  179. package/src/otel/claude-wrapper.ts +0 -239
  180. package/src/otel/gemini.ts +0 -483
  181. package/src/otel/index.ts +0 -128
  182. package/src/otel/init.ts +0 -411
  183. package/src/otel/integrations/anthropic.ts +0 -36
  184. package/src/otel/integrations/base.ts +0 -158
  185. package/src/otel/integrations/claude-agent.ts +0 -36
  186. package/src/otel/integrations/gemini.ts +0 -30
  187. package/src/otel/integrations/index.ts +0 -152
  188. package/src/otel/integrations/langchain.ts +0 -60
  189. package/src/otel/integrations/mastra.ts +0 -48
  190. package/src/otel/integrations/openai-agents.ts +0 -47
  191. package/src/otel/integrations/pi.ts +0 -28
  192. package/src/otel/integrations/vercel.ts +0 -26
  193. package/src/otel/langchain-handler.ts +0 -804
  194. package/src/otel/logs.ts +0 -395
  195. package/src/otel/mastra-exporter.ts +0 -714
  196. package/src/otel/openai.ts +0 -93
  197. package/src/otel/pi.ts +0 -94
  198. package/src/otel/register.ts +0 -50
  199. package/src/otel/setup.ts +0 -215
  200. package/src/otel/span-processor.ts +0 -512
  201. package/src/otel/tracing-processor.ts +0 -637
  202. package/src/pagination.ts +0 -10
  203. package/src/proxy-from-env.d.ts +0 -7
  204. package/src/resources/experiments.ts +0 -176
  205. package/src/resources/recipes.ts +0 -74
  206. package/src/resources/runtimes.ts +0 -31
  207. package/src/runner.ts +0 -213
  208. package/src/types.ts +0 -13
  209. package/src/utils.ts +0 -95
  210. package/src/version.ts +0 -2
  211. package/tsconfig.json +0 -8
@@ -1,804 +0,0 @@
1
- /**
2
- * LangChain/LangGraph Callback Handler for Introspection.
3
- *
4
- * Hooks into LangChain's native callback system to capture LLM, tool, and
5
- * chain interactions as gen_ai.* OTel spans for the Introspection backend.
6
- *
7
- * Similar to IntrospectionTracingProcessor for OpenAI Agents SDK and
8
- * IntrospectionClaudeHooks for Claude Agent SDK.
9
- *
10
- * **Important:** This module requires `@langchain/core` at runtime.
11
- * Import via the subpath: `@introspection-sdk/introspection-node/langchain`
12
- *
13
- * @example
14
- * ```ts
15
- * import { IntrospectionCallbackHandler } from "@introspection-sdk/introspection-node/langchain";
16
- * import { ChatOpenAI } from "@langchain/openai";
17
- *
18
- * const handler = new IntrospectionCallbackHandler({
19
- * serviceName: "my-langchain-app",
20
- * });
21
- *
22
- * const model = new ChatOpenAI({ modelName: "gpt-4o" });
23
- * const response = await model.invoke("Hello!", { callbacks: [handler] });
24
- *
25
- * await handler.shutdown();
26
- * ```
27
- */
28
-
29
- import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
30
- import type { Serialized } from "@langchain/core/load/serializable";
31
- import type { LLMResult } from "@langchain/core/outputs";
32
- import type { BaseMessage } from "@langchain/core/messages";
33
- import type { ChainValues } from "@langchain/core/utils/types";
34
-
35
- import {
36
- context as otelContext,
37
- propagation,
38
- ROOT_CONTEXT,
39
- SpanStatusCode,
40
- trace as otelTrace,
41
- type Span as OtelSpan,
42
- } from "@opentelemetry/api";
43
- import {
44
- BasicTracerProvider,
45
- BatchSpanProcessor,
46
- SimpleSpanProcessor,
47
- type SpanExporter,
48
- type IdGenerator,
49
- } from "@opentelemetry/sdk-trace-base";
50
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
51
- import { resourceFromAttributes } from "@opentelemetry/resources";
52
- import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
53
- import { randomUUID } from "crypto";
54
-
55
- import { logger, withOtlpHttpsProxy } from "../utils.js";
56
- import { VERSION } from "../version.js";
57
- import type {
58
- InputMessage,
59
- OutputMessage,
60
- MessagePart,
61
- } from "@introspection-sdk/types";
62
- import { providerCostAttributes } from "@introspection-sdk/types";
63
-
64
- // ---------------------------------------------------------------------------
65
- // Configuration
66
- // ---------------------------------------------------------------------------
67
-
68
- /** Advanced options for testing and customization. */
69
- export interface LangChainHandlerAdvancedOptions {
70
- /** Custom span exporter for testing. */
71
- spanExporter?: SpanExporter;
72
- /** Custom ID generator for testing. */
73
- idGenerator?: IdGenerator;
74
- /** Use SimpleSpanProcessor instead of BatchSpanProcessor. */
75
- useSimpleSpanProcessor?: boolean;
76
- }
77
-
78
- /** Configuration for {@link IntrospectionCallbackHandler}. */
79
- export interface IntrospectionCallbackHandlerOptions {
80
- /** Authentication token (env: INTROSPECTION_TOKEN). */
81
- token?: string;
82
- /** Base URL for the API (env: INTROSPECTION_BASE_OTEL_URL). */
83
- baseUrl?: string;
84
- /** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME). */
85
- serviceName?: string;
86
- /** Additional headers to include in requests. */
87
- additionalHeaders?: Record<string, string>;
88
- /** Advanced options for testing. */
89
- advanced?: LangChainHandlerAdvancedOptions;
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // Handler
94
- // ---------------------------------------------------------------------------
95
-
96
- /**
97
- * LangChain callback handler that captures LLM, tool, and chain events
98
- * as gen_ai.* OTel spans and exports them to the Introspection backend.
99
- *
100
- * Pass as a callback to any LangChain invoke call, or set globally via
101
- * `setGlobalHandler()`.
102
- */
103
- export class IntrospectionCallbackHandler extends BaseCallbackHandler {
104
- name = "IntrospectionCallbackHandler";
105
-
106
- private _tracerProvider: BasicTracerProvider;
107
- private _tracer: ReturnType<BasicTracerProvider["getTracer"]>;
108
- private _spans: Map<string, OtelSpan> = new Map();
109
- private _conversationId: string;
110
- // Track span names and parents so children can resolve gen_ai.agent.name
111
- private _spanNames: Map<string, string> = new Map();
112
- private _spanParents: Map<string, string> = new Map();
113
- private _runRoots: Map<string, string> = new Map();
114
- // LangChain wrapper names to skip when resolving agent names
115
- private static _wrapperNames = new Set([
116
- "RunnableSequence",
117
- "RunnableParallel",
118
- "RunnableMap",
119
- "RunnableLambda",
120
- "RunnableRetry",
121
- "_ConfigurableModel",
122
- "ChatOpenAI",
123
- "ChatAnthropic",
124
- "ChatGoogleGenerativeAI",
125
- "ChatGroq",
126
- ]);
127
- // Store LLM input messages per runId for use in handleLLMEnd
128
- private _llmInputs: Map<string, InputMessage[]> = new Map();
129
-
130
- constructor(options: IntrospectionCallbackHandlerOptions = {}) {
131
- super();
132
-
133
- const advanced = options.advanced;
134
- const serviceName =
135
- options.serviceName || process.env.INTROSPECTION_SERVICE_NAME;
136
- const resource = serviceName
137
- ? resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName })
138
- : undefined;
139
-
140
- if (!advanced?.spanExporter) {
141
- const token = options.token || process.env.INTROSPECTION_TOKEN;
142
- if (!token) {
143
- throw new Error("INTROSPECTION_TOKEN is required");
144
- }
145
-
146
- const baseUrl =
147
- options.baseUrl ||
148
- process.env.INTROSPECTION_BASE_OTEL_URL ||
149
- "https://otel.introspection.dev";
150
-
151
- const endpoint = baseUrl.endsWith("/v1/traces")
152
- ? baseUrl
153
- : `${baseUrl.replace(/\/$/, "")}/v1/traces`;
154
-
155
- const headers: Record<string, string> = {
156
- "User-Agent": `introspection-sdk/${VERSION}`,
157
- Authorization: `Bearer ${token}`,
158
- ...options.additionalHeaders,
159
- };
160
-
161
- logger.info(
162
- `IntrospectionCallbackHandler initialized: endpoint=${endpoint}`,
163
- );
164
-
165
- const spanExporter = new OTLPTraceExporter(
166
- withOtlpHttpsProxy({ url: endpoint, headers }),
167
- );
168
-
169
- const effectiveBatchSize =
170
- token.startsWith("intro_dev") || token.startsWith("intro_staging")
171
- ? 1
172
- : undefined;
173
- const useSimple =
174
- advanced?.useSimpleSpanProcessor || effectiveBatchSize === 1;
175
-
176
- const spanProcessor = useSimple
177
- ? new SimpleSpanProcessor(spanExporter)
178
- : new BatchSpanProcessor(spanExporter, {
179
- scheduledDelayMillis: 1000,
180
- ...(effectiveBatchSize
181
- ? { maxExportBatchSize: effectiveBatchSize }
182
- : {}),
183
- });
184
-
185
- this._tracerProvider = new BasicTracerProvider({
186
- resource,
187
- idGenerator: advanced?.idGenerator,
188
- spanProcessors: [spanProcessor],
189
- });
190
- } else {
191
- logger.info(
192
- "IntrospectionCallbackHandler initialized in test mode with custom exporter",
193
- );
194
-
195
- const spanProcessor = advanced.useSimpleSpanProcessor
196
- ? new SimpleSpanProcessor(advanced.spanExporter)
197
- : new BatchSpanProcessor(advanced.spanExporter, {
198
- scheduledDelayMillis: 100,
199
- });
200
-
201
- this._tracerProvider = new BasicTracerProvider({
202
- resource,
203
- idGenerator: advanced.idGenerator,
204
- spanProcessors: [spanProcessor],
205
- });
206
- }
207
-
208
- this._tracer = this._tracerProvider.getTracer("langchain", VERSION);
209
- this._conversationId = `intro_conv_${randomUUID().replace(/-/g, "")}`;
210
- }
211
-
212
- private _rootRunId(runId: string, parentRunId?: string): string {
213
- if (!parentRunId) return runId;
214
- return this._runRoots.get(parentRunId) || parentRunId;
215
- }
216
-
217
- /** Keep the top-level run span aligned with child conversation IDs. */
218
- private _setRootConversationId(
219
- conversationId: string,
220
- runId: string,
221
- parentRunId?: string,
222
- ): void {
223
- const root = this._spans.get(this._rootRunId(runId, parentRunId));
224
- root?.setAttribute("gen_ai.conversation.id", conversationId);
225
- }
226
-
227
- private _endRootIfComplete(runId: string): void {
228
- this._runRoots.delete(runId);
229
- this._spanNames.delete(runId);
230
- this._spanParents.delete(runId);
231
- }
232
-
233
- /** Create a span under a LangChain parent when one exists.
234
- * Sets gen_ai.agent.name to the parent span's name for hierarchy. */
235
- private _createChildSpan(
236
- name: string,
237
- runId: string,
238
- parentRunId?: string,
239
- ): OtelSpan {
240
- const parent = parentRunId ? this._spans.get(parentRunId) : undefined;
241
- this._runRoots.set(runId, this._rootRunId(runId, parentRunId));
242
- const ctx = parent
243
- ? otelTrace.setSpan(otelContext.active(), parent)
244
- : ROOT_CONTEXT;
245
- const span = this._tracer.startSpan(name, {}, ctx);
246
- this._spanNames.set(runId, name);
247
- if (parentRunId) {
248
- this._spanParents.set(runId, parentRunId);
249
- }
250
-
251
- // Baggage wins so per-call agent identity (e.g. via withAgent())
252
- // overrides the parent-span-name walk used as a default.
253
- const baggageAgentName = this._readBaggage("gen_ai.agent.name");
254
- const agentName = baggageAgentName || this._findAgentName(parentRunId);
255
- if (agentName) {
256
- span.setAttribute("gen_ai.agent.name", agentName);
257
- }
258
- const baggageAgentId = this._readBaggage("gen_ai.agent.id");
259
- if (baggageAgentId) {
260
- span.setAttribute("gen_ai.agent.id", baggageAgentId);
261
- }
262
-
263
- return span;
264
- }
265
-
266
- // -------------------------------------------------------------------------
267
- // Chat model callbacks (preferred over LLM callbacks for chat models)
268
- // -------------------------------------------------------------------------
269
-
270
- handleChatModelStart(
271
- llm: Serialized,
272
- messages: BaseMessage[][],
273
- runId: string,
274
- parentRunId?: string,
275
- extraParams?: Record<string, unknown>,
276
- _tags?: string[],
277
- metadata?: Record<string, unknown>,
278
- runName?: string,
279
- ): void {
280
- const modelName = this._extractModelName(llm, extraParams);
281
- const spanName = modelName ? `chat ${modelName}` : runName || "chat";
282
-
283
- const otelSpan = this._createChildSpan(spanName, runId, parentRunId);
284
- this._spans.set(runId, otelSpan);
285
-
286
- const conversationId = this._getConversationId(metadata);
287
- this._setRootConversationId(conversationId, runId, parentRunId);
288
-
289
- otelSpan.setAttribute("gen_ai.operation.name", "chat");
290
- otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
291
- otelSpan.setAttribute("openinference.span.kind", "LLM");
292
-
293
- if (modelName) {
294
- otelSpan.setAttribute("gen_ai.request.model", modelName);
295
- }
296
-
297
- const flatMessages = messages[0] || [];
298
- const { inputMessages, systemInstructions } =
299
- this._convertMessages(flatMessages);
300
-
301
- if (inputMessages.length > 0) {
302
- otelSpan.setAttribute(
303
- "gen_ai.input.messages",
304
- JSON.stringify(inputMessages),
305
- );
306
- this._llmInputs.set(runId, inputMessages);
307
- }
308
-
309
- if (systemInstructions.length > 0) {
310
- otelSpan.setAttribute(
311
- "gen_ai.system_instructions",
312
- JSON.stringify(
313
- systemInstructions.map((s) => ({ type: "text", content: s })),
314
- ),
315
- );
316
- }
317
-
318
- const provider = this._extractProvider(llm);
319
- if (provider) {
320
- otelSpan.setAttribute("gen_ai.provider.name", provider);
321
- }
322
-
323
- const invocationParams = extraParams?.["invocation_params"] as
324
- Record<string, unknown> | undefined;
325
-
326
- const tools = invocationParams?.["tools"] as
327
- Array<Record<string, unknown>> | undefined;
328
- if (tools && tools.length > 0) {
329
- otelSpan.setAttribute(
330
- "gen_ai.tool.definitions",
331
- JSON.stringify(
332
- tools.map((t) => {
333
- const fn = (t.function || t) as Record<string, unknown>;
334
- return {
335
- type: (t.type as string) || "function",
336
- name: (fn.name || "") as string,
337
- description: (fn.description || "") as string,
338
- parameters: fn.parameters,
339
- };
340
- }),
341
- ),
342
- );
343
- }
344
-
345
- if (invocationParams?.["temperature"] != null) {
346
- otelSpan.setAttribute(
347
- "gen_ai.request.temperature",
348
- invocationParams["temperature"] as number,
349
- );
350
- }
351
- }
352
-
353
- handleLLMStart(
354
- llm: Serialized,
355
- prompts: string[],
356
- runId: string,
357
- parentRunId?: string,
358
- extraParams?: Record<string, unknown>,
359
- _tags?: string[],
360
- metadata?: Record<string, unknown>,
361
- runName?: string,
362
- ): void {
363
- const modelName = this._extractModelName(llm, extraParams);
364
- const spanName = modelName ? `chat ${modelName}` : runName || "llm";
365
-
366
- const otelSpan = this._createChildSpan(spanName, runId, parentRunId);
367
- this._spans.set(runId, otelSpan);
368
-
369
- const conversationId = this._getConversationId(metadata);
370
- this._setRootConversationId(conversationId, runId, parentRunId);
371
-
372
- otelSpan.setAttribute("gen_ai.operation.name", "chat");
373
- otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
374
- otelSpan.setAttribute("openinference.span.kind", "LLM");
375
-
376
- if (modelName) {
377
- otelSpan.setAttribute("gen_ai.request.model", modelName);
378
- }
379
-
380
- if (prompts.length > 0) {
381
- const inputMessages: InputMessage[] = prompts.map((p) => ({
382
- role: "user" as const,
383
- parts: [{ type: "text" as const, content: p }],
384
- }));
385
- otelSpan.setAttribute(
386
- "gen_ai.input.messages",
387
- JSON.stringify(inputMessages),
388
- );
389
- this._llmInputs.set(runId, inputMessages);
390
- }
391
- }
392
-
393
- handleLLMEnd(output: LLMResult, runId: string): void {
394
- const otelSpan = this._spans.get(runId);
395
- if (!otelSpan) return;
396
-
397
- this._spans.delete(runId);
398
- this._spanNames.delete(runId);
399
- this._llmInputs.delete(runId);
400
- this._endRootIfComplete(runId);
401
-
402
- const generations = output.generations?.[0];
403
- if (generations && generations.length > 0) {
404
- const parts: MessagePart[] = [];
405
- for (const gen of generations) {
406
- if (gen.text) {
407
- parts.push({ type: "text", content: gen.text });
408
- }
409
- const msg = (gen as unknown as Record<string, unknown>).message as
410
- Record<string, unknown> | undefined;
411
- const kwargs = (msg?.["kwargs"] || msg) as
412
- Record<string, unknown> | undefined;
413
- const additionalKwargs = kwargs?.["additional_kwargs"] as
414
- Record<string, unknown> | undefined;
415
- const toolCalls = (additionalKwargs?.["tool_calls"] ||
416
- kwargs?.["tool_calls"]) as Array<Record<string, unknown>> | undefined;
417
- if (toolCalls) {
418
- for (const tc of toolCalls) {
419
- const fn = (tc.function || tc) as Record<string, unknown>;
420
- parts.push({
421
- type: "tool_call",
422
- name: (fn.name || tc.name || "") as string,
423
- id: (tc.id || "") as string,
424
- arguments: (fn.arguments || tc.args || "") as string,
425
- });
426
- }
427
- }
428
- }
429
-
430
- if (parts.length > 0) {
431
- const outputMessages: OutputMessage[] = [{ role: "assistant", parts }];
432
- otelSpan.setAttribute(
433
- "gen_ai.output.messages",
434
- JSON.stringify(outputMessages),
435
- );
436
- }
437
- }
438
-
439
- const usage = output.llmOutput as Record<string, unknown> | undefined;
440
- const tokenUsage = (usage?.["tokenUsage"] ||
441
- usage?.["token_usage"] ||
442
- usage?.["usage"]) as Record<string, unknown> | undefined;
443
- if (tokenUsage) {
444
- const inputTokens =
445
- tokenUsage["promptTokens"] ||
446
- tokenUsage["prompt_tokens"] ||
447
- tokenUsage["input_tokens"];
448
- const outputTokens =
449
- tokenUsage["completionTokens"] ||
450
- tokenUsage["completion_tokens"] ||
451
- tokenUsage["output_tokens"];
452
- if (typeof inputTokens === "number") {
453
- otelSpan.setAttribute("gen_ai.usage.input_tokens", inputTokens);
454
- }
455
- if (typeof outputTokens === "number") {
456
- otelSpan.setAttribute("gen_ai.usage.output_tokens", outputTokens);
457
- }
458
- // Flat field (legacy format) or nested cache_creation object (Claude 4+).
459
- const cacheCreationObj = tokenUsage["cache_creation"] as
460
- Record<string, unknown> | undefined;
461
- const cacheWriteTokens =
462
- tokenUsage["cache_creation_input_tokens"] ||
463
- tokenUsage["cacheCreationInputTokens"] ||
464
- ((cacheCreationObj?.["ephemeral_5m_input_tokens"] as number) || 0) +
465
- ((cacheCreationObj?.["ephemeral_1h_input_tokens"] as number) || 0) ||
466
- undefined;
467
- if (typeof cacheWriteTokens === "number") {
468
- otelSpan.setAttribute(
469
- "gen_ai.usage.cache_creation.input_tokens",
470
- cacheWriteTokens,
471
- );
472
- }
473
- const cacheReadTokens =
474
- tokenUsage["cache_read_input_tokens"] ||
475
- tokenUsage["cacheReadInputTokens"];
476
- if (typeof cacheReadTokens === "number") {
477
- otelSpan.setAttribute(
478
- "gen_ai.usage.cache_read.input_tokens",
479
- cacheReadTokens,
480
- );
481
- }
482
- // Provider-reported cost (e.g. OpenRouter usage.cost) when present.
483
- otelSpan.setAttributes(providerCostAttributes(tokenUsage));
484
- }
485
-
486
- const model = usage?.["model"] || usage?.["model_name"];
487
- if (typeof model === "string") {
488
- otelSpan.setAttribute("gen_ai.response.model", model);
489
- }
490
-
491
- // gen_ai.response.id — required by the server for conversation tracking.
492
- const responseId = usage?.["id"] || usage?.["system_fingerprint"];
493
- otelSpan.setAttribute(
494
- "gen_ai.response.id",
495
- typeof responseId === "string" ? responseId : `langchain-${runId}`,
496
- );
497
-
498
- otelSpan.end();
499
- }
500
-
501
- handleLLMError(err: Error, runId: string): void {
502
- const otelSpan = this._spans.get(runId);
503
- if (!otelSpan) return;
504
- this._spans.delete(runId);
505
- this._spanNames.delete(runId);
506
- this._llmInputs.delete(runId);
507
- this._endRootIfComplete(runId);
508
- otelSpan.recordException(err);
509
- otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
510
- otelSpan.setAttribute("exception.message", err.message);
511
- otelSpan.end();
512
- }
513
-
514
- // -------------------------------------------------------------------------
515
- // Chain callbacks
516
- // -------------------------------------------------------------------------
517
-
518
- handleChainStart(
519
- chain: Serialized,
520
- _inputs: ChainValues,
521
- runId: string,
522
- _runType?: string,
523
- _tags?: string[],
524
- metadata?: Record<string, unknown>,
525
- runName?: string,
526
- parentRunId?: string,
527
- ): void {
528
- const name =
529
- runName || chain?.name || chain?.id?.[chain.id.length - 1] || "chain";
530
-
531
- const otelSpan = this._createChildSpan(name, runId, parentRunId);
532
- this._spans.set(runId, otelSpan);
533
-
534
- const conversationId = this._getConversationId(metadata);
535
- this._setRootConversationId(conversationId, runId, parentRunId);
536
- otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
537
- }
538
-
539
- handleChainEnd(_outputs: ChainValues, runId: string): void {
540
- const otelSpan = this._spans.get(runId);
541
- if (!otelSpan) return;
542
- this._spans.delete(runId);
543
- this._endRootIfComplete(runId);
544
- otelSpan.end();
545
- }
546
-
547
- handleChainError(err: Error, runId: string): void {
548
- const otelSpan = this._spans.get(runId);
549
- if (!otelSpan) return;
550
- this._spans.delete(runId);
551
- this._endRootIfComplete(runId);
552
- otelSpan.recordException(err);
553
- otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
554
- otelSpan.setAttribute("exception.message", err.message);
555
- otelSpan.end();
556
- }
557
-
558
- // -------------------------------------------------------------------------
559
- // Tool callbacks
560
- // -------------------------------------------------------------------------
561
-
562
- handleToolStart(
563
- tool: Serialized,
564
- input: string,
565
- runId: string,
566
- parentRunId?: string,
567
- _tags?: string[],
568
- metadata?: Record<string, unknown>,
569
- runName?: string,
570
- ): void {
571
- const toolName =
572
- runName || tool?.name || tool?.id?.[tool.id.length - 1] || "tool";
573
-
574
- const otelSpan = this._createChildSpan(
575
- `execute_tool ${toolName}`,
576
- runId,
577
- parentRunId,
578
- );
579
- this._spans.set(runId, otelSpan);
580
-
581
- const conversationId = this._getConversationId(metadata);
582
- this._setRootConversationId(conversationId, runId, parentRunId);
583
-
584
- otelSpan.setAttribute("gen_ai.operation.name", "execute_tool");
585
- otelSpan.setAttribute("gen_ai.tool.name", toolName);
586
- otelSpan.setAttribute("gen_ai.tool.type", "function");
587
- otelSpan.setAttribute("gen_ai.tool.call.id", runId);
588
- otelSpan.setAttribute("openinference.span.kind", "TOOL");
589
- otelSpan.setAttribute("gen_ai.tool.call.arguments", input);
590
- otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
591
- }
592
-
593
- handleToolEnd(output: unknown, runId: string): void {
594
- const otelSpan = this._spans.get(runId);
595
- if (!otelSpan) return;
596
- this._spans.delete(runId);
597
- this._endRootIfComplete(runId);
598
-
599
- if (output != null) {
600
- otelSpan.setAttribute(
601
- "gen_ai.tool.call.result",
602
- typeof output === "string" ? output : JSON.stringify(output),
603
- );
604
- }
605
-
606
- otelSpan.end();
607
- }
608
-
609
- handleToolError(err: Error, runId: string): void {
610
- const otelSpan = this._spans.get(runId);
611
- if (!otelSpan) return;
612
- this._spans.delete(runId);
613
- this._endRootIfComplete(runId);
614
- otelSpan.recordException(err);
615
- otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
616
- otelSpan.setAttribute("exception.message", err.message);
617
- otelSpan.end();
618
- }
619
-
620
- // -------------------------------------------------------------------------
621
- // Lifecycle
622
- // -------------------------------------------------------------------------
623
-
624
- async shutdown(): Promise<void> {
625
- for (const span of this._spans.values()) span.end();
626
- this._spans.clear();
627
- this._spanNames.clear();
628
- this._spanParents.clear();
629
- this._runRoots.clear();
630
- await this._tracerProvider.shutdown();
631
- }
632
-
633
- async forceFlush(): Promise<void> {
634
- await this._tracerProvider.forceFlush();
635
- }
636
-
637
- // -------------------------------------------------------------------------
638
- // Helpers
639
- // -------------------------------------------------------------------------
640
-
641
- private _extractModelName(
642
- llm: Serialized,
643
- extraParams?: Record<string, unknown>,
644
- ): string | undefined {
645
- const invocationParams = extraParams?.["invocation_params"] as
646
- Record<string, unknown> | undefined;
647
- if (invocationParams) {
648
- const model =
649
- invocationParams["model"] ||
650
- invocationParams["model_name"] ||
651
- invocationParams["modelName"];
652
- if (typeof model === "string") return model;
653
- }
654
-
655
- const kwargs = (llm as unknown as Record<string, unknown>)?.kwargs as
656
- Record<string, unknown> | undefined;
657
- if (kwargs) {
658
- const model =
659
- kwargs["model"] || kwargs["model_name"] || kwargs["modelName"];
660
- if (typeof model === "string") return model;
661
- }
662
-
663
- return undefined;
664
- }
665
-
666
- private _extractProvider(llm: Serialized): string | undefined {
667
- const id = llm?.id;
668
- if (Array.isArray(id) && id.length > 0) {
669
- return id[id.length - 1] as string;
670
- }
671
- return undefined;
672
- }
673
-
674
- /** Walk up the span tree to find the first non-wrapper span name. */
675
- private _findAgentName(runId?: string): string | undefined {
676
- let current = runId;
677
- for (let i = 0; i < 20 && current; i++) {
678
- const name = this._spanNames.get(current);
679
- if (name && !IntrospectionCallbackHandler._wrapperNames.has(name)) {
680
- return name;
681
- }
682
- current = this._spanParents.get(current);
683
- }
684
- return undefined;
685
- }
686
-
687
- private _getConversationId(metadata?: Record<string, unknown>): string {
688
- const metaConvId = metadata?.["gen_ai.conversation.id"];
689
- if (metaConvId !== undefined && metaConvId !== null) {
690
- return String(metaConvId);
691
- }
692
- const threadId = metadata?.["thread_id"];
693
- if (threadId !== undefined && threadId !== null) {
694
- return String(threadId);
695
- }
696
- const baggageConvId = this._readBaggage("gen_ai.conversation.id");
697
- if (baggageConvId) return baggageConvId;
698
- return this._conversationId;
699
- }
700
-
701
- /**
702
- * Read a single value from the active OTel baggage. Returns undefined when
703
- * no propagator is installed or the key is absent.
704
- *
705
- * Used so callers can scope LangChain calls with
706
- * `IntrospectionClient.withAgent()` / `.withConversation()` /
707
- * `.withGenAi()` without threading metadata through every invoke().
708
- */
709
- private _readBaggage(key: string): string | undefined {
710
- const baggage = propagation.getBaggage(otelContext.active());
711
- return baggage?.getEntry(key)?.value;
712
- }
713
-
714
- private _convertMessages(messages: BaseMessage[]): {
715
- inputMessages: InputMessage[];
716
- systemInstructions: string[];
717
- } {
718
- const inputMessages: InputMessage[] = [];
719
- const systemInstructions: string[] = [];
720
-
721
- for (const msg of messages) {
722
- const msgType =
723
- (msg as unknown as { _getType?: () => string })._getType?.() ||
724
- (msg as unknown as Record<string, unknown>).type ||
725
- "unknown";
726
- const role = this._mapRole(msgType as string);
727
- const content = this._extractContent(msg.content);
728
-
729
- if (role === "system") {
730
- systemInstructions.push(
731
- typeof msg.content === "string"
732
- ? msg.content
733
- : JSON.stringify(msg.content),
734
- );
735
- continue;
736
- }
737
-
738
- const parts: MessagePart[] = [];
739
-
740
- if (role === "tool") {
741
- const toolCallId =
742
- ((msg as unknown as Record<string, unknown>)
743
- .tool_call_id as string) || "";
744
- parts.push({
745
- type: "tool_call_response",
746
- id: toolCallId,
747
- response: content,
748
- });
749
- } else if (content) {
750
- parts.push({ type: "text", content });
751
- }
752
-
753
- const toolCalls = (msg as unknown as Record<string, unknown>)
754
- .tool_calls as Array<Record<string, unknown>> | undefined;
755
- if (toolCalls) {
756
- for (const tc of toolCalls) {
757
- parts.push({
758
- type: "tool_call",
759
- name: (tc.name || "") as string,
760
- id: (tc.id || "") as string,
761
- arguments:
762
- typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args),
763
- });
764
- }
765
- }
766
-
767
- if (parts.length > 0) {
768
- inputMessages.push({ role, parts });
769
- }
770
- }
771
-
772
- return { inputMessages, systemInstructions };
773
- }
774
-
775
- private _mapRole(msgType: string): "user" | "assistant" | "system" | "tool" {
776
- switch (msgType) {
777
- case "human":
778
- return "user";
779
- case "ai":
780
- return "assistant";
781
- case "system":
782
- return "system";
783
- case "tool":
784
- return "tool";
785
- default:
786
- return "user";
787
- }
788
- }
789
-
790
- private _extractContent(content: unknown): string {
791
- if (typeof content === "string") return content;
792
- if (Array.isArray(content)) {
793
- return content
794
- .map((part: unknown) => {
795
- const p = part as Record<string, unknown>;
796
- if (p.type === "text") return (p.text || "") as string;
797
- return "";
798
- })
799
- .filter(Boolean)
800
- .join("");
801
- }
802
- return String(content || "");
803
- }
804
- }