@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-lint.log +5 -0
- package/dist/ai-sdk-integration.d.ts.map +1 -1
- package/dist/ai-sdk-integration.js +2 -2
- package/dist/claude-hooks.d.ts.map +1 -1
- package/dist/claude-hooks.js +3 -3
- package/dist/client.d.ts +42 -132
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +66 -365
- package/dist/client.js.map +1 -0
- package/dist/converters/claude.d.ts +5 -1
- package/dist/converters/claude.d.ts.map +1 -1
- package/dist/converters/claude.js +7 -0
- package/dist/converters/claude.js.map +1 -0
- package/dist/converters/gemini.d.ts +90 -0
- package/dist/converters/gemini.d.ts.map +1 -0
- package/dist/converters/gemini.js +171 -0
- package/dist/converters/gemini.js.map +1 -0
- package/dist/converters/index.d.ts +1 -1
- package/dist/converters/index.d.ts.map +1 -1
- package/dist/converters/index.js +2 -1
- package/dist/converters/index.js.map +1 -0
- package/dist/converters/openai.js +1 -0
- package/dist/converters/openai.js.map +1 -0
- package/dist/converters/openclaw.d.ts +16 -0
- package/dist/converters/openclaw.d.ts.map +1 -0
- package/dist/converters/openclaw.js +49 -0
- package/dist/converters/openinference.js +1 -0
- package/dist/converters/openinference.js.map +1 -0
- package/dist/converters/vercel.d.ts.map +1 -1
- package/dist/converters/vercel.js +31 -3
- package/dist/converters/vercel.js.map +1 -0
- package/dist/http.d.ts +36 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +145 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +24 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -32
- package/dist/index.js.map +1 -0
- package/dist/langchain-handler.d.ts.map +1 -1
- package/dist/langchain-handler.js +2 -2
- package/dist/mastra-exporter.d.ts.map +1 -1
- package/dist/mastra-exporter.js +2 -2
- package/dist/otel/anthropic.d.ts +62 -0
- package/dist/otel/anthropic.d.ts.map +1 -0
- package/dist/otel/anthropic.js +425 -0
- package/dist/otel/anthropic.js.map +1 -0
- package/dist/otel/claude-hooks.d.ts +346 -0
- package/dist/otel/claude-hooks.d.ts.map +1 -0
- package/dist/otel/claude-hooks.js +645 -0
- package/dist/otel/claude-hooks.js.map +1 -0
- package/dist/otel/claude-wrapper.d.ts +93 -0
- package/dist/otel/claude-wrapper.d.ts.map +1 -0
- package/dist/otel/claude-wrapper.js +155 -0
- package/dist/otel/claude-wrapper.js.map +1 -0
- package/dist/otel/gemini.d.ts +48 -0
- package/dist/otel/gemini.d.ts.map +1 -0
- package/dist/otel/gemini.js +297 -0
- package/dist/otel/gemini.js.map +1 -0
- package/dist/otel/index.d.ts +27 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +33 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/otel/langchain-handler.d.ts +112 -0
- package/dist/otel/langchain-handler.d.ts.map +1 -0
- package/dist/otel/langchain-handler.js +555 -0
- package/dist/otel/langchain-handler.js.map +1 -0
- package/dist/otel/logs.d.ts +82 -0
- package/dist/otel/logs.d.ts.map +1 -0
- package/dist/otel/logs.js +271 -0
- package/dist/otel/logs.js.map +1 -0
- package/dist/otel/mastra-exporter.d.ts +87 -0
- package/dist/otel/mastra-exporter.d.ts.map +1 -0
- package/dist/otel/mastra-exporter.js +525 -0
- package/dist/otel/mastra-exporter.js.map +1 -0
- package/dist/otel/pi.d.ts +59 -0
- package/dist/otel/pi.d.ts.map +1 -0
- package/dist/otel/pi.js +67 -0
- package/dist/otel/pi.js.map +1 -0
- package/dist/otel/setup.d.ts +23 -0
- package/dist/otel/setup.d.ts.map +1 -0
- package/dist/otel/setup.js +127 -0
- package/dist/otel/setup.js.map +1 -0
- package/dist/otel/span-processor.d.ts +62 -0
- package/dist/otel/span-processor.d.ts.map +1 -0
- package/dist/otel/span-processor.js +319 -0
- package/dist/otel/span-processor.js.map +1 -0
- package/dist/otel/tracing-processor.d.ts +139 -0
- package/dist/otel/tracing-processor.d.ts.map +1 -0
- package/dist/otel/tracing-processor.js +418 -0
- package/dist/otel/tracing-processor.js.map +1 -0
- package/dist/resources/experiments.d.ts +31 -0
- package/dist/resources/experiments.d.ts.map +1 -0
- package/dist/resources/experiments.js +133 -0
- package/dist/resources/experiments.js.map +1 -0
- package/dist/resources/recipes.d.ts +22 -0
- package/dist/resources/recipes.d.ts.map +1 -0
- package/dist/resources/recipes.js +61 -0
- package/dist/resources/recipes.js.map +1 -0
- package/dist/resources/runtimes.d.ts +76 -0
- package/dist/resources/runtimes.d.ts.map +1 -0
- package/dist/resources/runtimes.js +179 -0
- package/dist/resources/runtimes.js.map +1 -0
- package/dist/runner-resources/files.d.ts +35 -0
- package/dist/runner-resources/files.d.ts.map +1 -0
- package/dist/runner-resources/files.js +131 -0
- package/dist/runner-resources/files.js.map +1 -0
- package/dist/runner-resources/tasks.d.ts +39 -0
- package/dist/runner-resources/tasks.d.ts.map +1 -0
- package/dist/runner-resources/tasks.js +129 -0
- package/dist/runner-resources/tasks.js.map +1 -0
- package/dist/runner.d.ts +80 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +159 -0
- package/dist/runner.js.map +1 -0
- package/dist/span-processor.d.ts.map +1 -1
- package/dist/span-processor.js +3 -3
- package/dist/streaming.d.ts +8 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +67 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tracing-processor.d.ts.map +1 -1
- package/dist/tracing-processor.js +3 -3
- package/dist/types/genai.d.ts +165 -0
- package/dist/types/genai.d.ts.map +1 -0
- package/dist/types/genai.js +82 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +1 -0
- package/dist/version.js.map +1 -0
- package/package.json +83 -25
- package/src/client.ts +76 -471
- package/src/converters/claude.ts +11 -1
- package/src/converters/gemini.ts +243 -0
- package/src/converters/index.ts +13 -7
- package/src/converters/vercel.ts +41 -3
- package/src/http.ts +163 -0
- package/src/index.ts +110 -75
- package/src/{anthropic.ts → otel/anthropic.ts} +95 -8
- package/src/{claude-hooks.ts → otel/claude-hooks.ts} +125 -24
- package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
- package/src/otel/gemini.ts +397 -0
- package/src/otel/index.ts +80 -0
- package/src/{langchain-handler.ts → otel/langchain-handler.ts} +56 -7
- package/src/otel/logs.ts +394 -0
- package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +86 -23
- package/src/otel/pi.ts +88 -0
- package/src/otel/setup.ts +174 -0
- package/src/{span-processor.ts → otel/span-processor.ts} +17 -10
- package/src/{tracing-processor.ts → otel/tracing-processor.ts} +100 -23
- package/src/resources/experiments.ts +170 -0
- package/src/resources/recipes.ts +73 -0
- package/src/resources/runtimes.ts +231 -0
- package/src/runner-resources/files.ts +163 -0
- package/src/runner-resources/tasks.ts +156 -0
- package/src/runner.ts +194 -0
- package/src/streaming.ts +57 -0
- package/src/utils.ts +15 -0
- package/src/ai-sdk-integration.ts +0 -689
- package/src/converters/ai-sdk.ts +0 -323
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Google Gemini (`@google/genai`) instrumentor for Introspection SDK.
|
|
3
|
+
*
|
|
4
|
+
* Captures the full Gemini response including per-part `thoughtSignature`
|
|
5
|
+
* payloads (Gemini 3.x extended thinking) as gen_ai `thinking` parts with
|
|
6
|
+
* `signature` set — mirroring how the {@link AnthropicInstrumentor} captures
|
|
7
|
+
* Anthropic thinking blocks.
|
|
8
|
+
*
|
|
9
|
+
* Supports both non-streaming (`client.models.generateContent`) and streaming
|
|
10
|
+
* (`client.models.generateContentStream`) calls.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { GoogleGenAI } from "@google/genai";
|
|
15
|
+
* import { GeminiInstrumentor } from "@introspection-sdk/introspection-node";
|
|
16
|
+
*
|
|
17
|
+
* const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
18
|
+
* new GeminiInstrumentor().instrument({ tracerProvider: provider, client });
|
|
19
|
+
* // All client.models.generateContent / generateContentStream calls are now traced
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @see https://ai.google.dev/gemini-api/docs/thought-signatures
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
26
|
+
import type { Tracer, Span } from "@opentelemetry/api";
|
|
27
|
+
import type { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
28
|
+
|
|
29
|
+
import type { InputMessage, OutputMessage } from "@introspection-sdk/types";
|
|
30
|
+
import {
|
|
31
|
+
convertGeminiContentsToInputMessages,
|
|
32
|
+
convertGeminiCandidatesToOutputMessages,
|
|
33
|
+
convertGeminiSystemInstructionToSemconv,
|
|
34
|
+
convertGeminiToolsToToolDefinitions,
|
|
35
|
+
GEMINI_PROVIDER_NAME,
|
|
36
|
+
type GeminiCandidate,
|
|
37
|
+
type GeminiContent,
|
|
38
|
+
type GeminiPart,
|
|
39
|
+
type GeminiTool,
|
|
40
|
+
} from "../converters/gemini.js";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Span helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function serializeMessages(msgs: InputMessage[] | OutputMessage[]): string {
|
|
47
|
+
return JSON.stringify(
|
|
48
|
+
msgs.map((m) => {
|
|
49
|
+
const obj: Record<string, unknown> = { role: m.role, parts: m.parts };
|
|
50
|
+
if ("finish_reason" in m && m.finish_reason) {
|
|
51
|
+
obj.finish_reason = m.finish_reason;
|
|
52
|
+
}
|
|
53
|
+
return obj;
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function startSpan(
|
|
59
|
+
tracer: Tracer,
|
|
60
|
+
model: string,
|
|
61
|
+
conversationId?: string,
|
|
62
|
+
): Span {
|
|
63
|
+
return tracer.startSpan("chat", {
|
|
64
|
+
kind: SpanKind.CLIENT,
|
|
65
|
+
attributes: {
|
|
66
|
+
"gen_ai.system": GEMINI_PROVIDER_NAME,
|
|
67
|
+
"gen_ai.provider.name": GEMINI_PROVIDER_NAME,
|
|
68
|
+
"gen_ai.operation.name": "chat",
|
|
69
|
+
"gen_ai.request.model": model,
|
|
70
|
+
"openinference.span.kind": "LLM",
|
|
71
|
+
...(conversationId
|
|
72
|
+
? { "gen_ai.conversation.id": conversationId }
|
|
73
|
+
: undefined),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setRequestAttrs(span: Span, kwargs: Record<string, unknown>): void {
|
|
79
|
+
const contents = kwargs.contents as
|
|
80
|
+
| string
|
|
81
|
+
| GeminiContent[]
|
|
82
|
+
| GeminiContent
|
|
83
|
+
| undefined;
|
|
84
|
+
const inputMsgs = convertGeminiContentsToInputMessages(contents);
|
|
85
|
+
if (inputMsgs.length > 0) {
|
|
86
|
+
span.setAttribute("gen_ai.input.messages", serializeMessages(inputMsgs));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = (kwargs.config as Record<string, unknown> | undefined) || {};
|
|
90
|
+
|
|
91
|
+
const systemInstruction = config.systemInstruction as
|
|
92
|
+
| string
|
|
93
|
+
| GeminiContent
|
|
94
|
+
| undefined;
|
|
95
|
+
const systemInstructions =
|
|
96
|
+
convertGeminiSystemInstructionToSemconv(systemInstruction);
|
|
97
|
+
if (systemInstructions && systemInstructions.length > 0) {
|
|
98
|
+
span.setAttribute(
|
|
99
|
+
"gen_ai.system_instructions",
|
|
100
|
+
JSON.stringify(systemInstructions),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tools = config.tools as GeminiTool[] | undefined;
|
|
105
|
+
const toolDefs = convertGeminiToolsToToolDefinitions(tools);
|
|
106
|
+
if (toolDefs && toolDefs.length > 0) {
|
|
107
|
+
span.setAttribute("gen_ai.tool.definitions", JSON.stringify(toolDefs));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function setUsageAttrs(
|
|
112
|
+
span: Span,
|
|
113
|
+
usage: Record<string, unknown> | undefined,
|
|
114
|
+
): void {
|
|
115
|
+
if (!usage) return;
|
|
116
|
+
const promptTokens = usage.promptTokenCount as number | undefined;
|
|
117
|
+
const candidateTokens = usage.candidatesTokenCount as number | undefined;
|
|
118
|
+
const cachedTokens = usage.cachedContentTokenCount as number | undefined;
|
|
119
|
+
const thoughtTokens = usage.thoughtsTokenCount as number | undefined;
|
|
120
|
+
if (typeof promptTokens === "number") {
|
|
121
|
+
span.setAttribute("gen_ai.usage.input_tokens", promptTokens);
|
|
122
|
+
}
|
|
123
|
+
if (typeof candidateTokens === "number") {
|
|
124
|
+
// Gemini reports candidatesTokenCount separately from thoughtsTokenCount;
|
|
125
|
+
// sum them so output_tokens reflects everything billed as model output.
|
|
126
|
+
const total = candidateTokens + (thoughtTokens ?? 0);
|
|
127
|
+
span.setAttribute("gen_ai.usage.output_tokens", total);
|
|
128
|
+
} else if (typeof thoughtTokens === "number") {
|
|
129
|
+
span.setAttribute("gen_ai.usage.output_tokens", thoughtTokens);
|
|
130
|
+
}
|
|
131
|
+
if (typeof cachedTokens === "number") {
|
|
132
|
+
span.setAttribute("gen_ai.usage.cache_read.input_tokens", cachedTokens);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function setResponseAttrs(span: Span, response: Record<string, unknown>): void {
|
|
137
|
+
const candidates = response.candidates as GeminiCandidate[] | undefined;
|
|
138
|
+
const outputMsgs = convertGeminiCandidatesToOutputMessages(candidates);
|
|
139
|
+
if (outputMsgs.length > 0) {
|
|
140
|
+
span.setAttribute("gen_ai.output.messages", serializeMessages(outputMsgs));
|
|
141
|
+
const finishReasons = outputMsgs
|
|
142
|
+
.map((m) => m.finish_reason)
|
|
143
|
+
.filter((r): r is string => typeof r === "string");
|
|
144
|
+
if (finishReasons.length > 0) {
|
|
145
|
+
span.setAttribute("gen_ai.response.finish_reasons", finishReasons);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const responseId = response.responseId as string | undefined;
|
|
150
|
+
if (responseId) span.setAttribute("gen_ai.response.id", responseId);
|
|
151
|
+
|
|
152
|
+
const modelVersion = response.modelVersion as string | undefined;
|
|
153
|
+
if (modelVersion) span.setAttribute("gen_ai.response.model", modelVersion);
|
|
154
|
+
|
|
155
|
+
setUsageAttrs(span, response.usageMetadata as Record<string, unknown>);
|
|
156
|
+
|
|
157
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Streaming aggregator
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
interface StreamChunk {
|
|
165
|
+
candidates?: GeminiCandidate[];
|
|
166
|
+
usageMetadata?: Record<string, unknown>;
|
|
167
|
+
responseId?: string;
|
|
168
|
+
modelVersion?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** True if a streamed part is just incremental text with no metadata to preserve. */
|
|
172
|
+
function isPlainTextPart(part: GeminiPart): boolean {
|
|
173
|
+
return (
|
|
174
|
+
typeof part.text === "string" &&
|
|
175
|
+
part.thought !== true &&
|
|
176
|
+
!part.thoughtSignature &&
|
|
177
|
+
!part.functionCall &&
|
|
178
|
+
!part.functionResponse
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Async-iterable wrapper that aggregates streamed `GenerateContentResponse`
|
|
184
|
+
* chunks so the underlying span captures the complete final message — including
|
|
185
|
+
* the final per-part `thoughtSignature` values that only arrive on the last
|
|
186
|
+
* chunk of a streamed turn.
|
|
187
|
+
*/
|
|
188
|
+
class TracedStream implements AsyncIterable<StreamChunk> {
|
|
189
|
+
private aggregated: Map<number, GeminiCandidate> = new Map();
|
|
190
|
+
private usage: Record<string, unknown> | undefined;
|
|
191
|
+
private responseId: string | undefined;
|
|
192
|
+
private modelVersion: string | undefined;
|
|
193
|
+
private finalized = false;
|
|
194
|
+
|
|
195
|
+
constructor(
|
|
196
|
+
private readonly inner: AsyncIterable<StreamChunk>,
|
|
197
|
+
private readonly span: Span,
|
|
198
|
+
) {}
|
|
199
|
+
|
|
200
|
+
async *[Symbol.asyncIterator](): AsyncIterableIterator<StreamChunk> {
|
|
201
|
+
try {
|
|
202
|
+
for await (const chunk of this.inner) {
|
|
203
|
+
this.processChunk(chunk);
|
|
204
|
+
yield chunk;
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
this.span.setStatus({
|
|
208
|
+
code: SpanStatusCode.ERROR,
|
|
209
|
+
message: String(err),
|
|
210
|
+
});
|
|
211
|
+
this.span.end();
|
|
212
|
+
this.finalized = true;
|
|
213
|
+
throw err;
|
|
214
|
+
} finally {
|
|
215
|
+
this.finalize();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private processChunk(chunk: StreamChunk): void {
|
|
220
|
+
if (chunk.responseId) this.responseId = chunk.responseId;
|
|
221
|
+
if (chunk.modelVersion) this.modelVersion = chunk.modelVersion;
|
|
222
|
+
if (chunk.usageMetadata) this.usage = chunk.usageMetadata;
|
|
223
|
+
for (const cand of chunk.candidates ?? []) {
|
|
224
|
+
const idx = cand.index ?? 0;
|
|
225
|
+
const existing = this.aggregated.get(idx);
|
|
226
|
+
if (!existing) {
|
|
227
|
+
this.aggregated.set(idx, {
|
|
228
|
+
index: idx,
|
|
229
|
+
finishReason: cand.finishReason,
|
|
230
|
+
content: {
|
|
231
|
+
role: cand.content?.role,
|
|
232
|
+
parts: [...(cand.content?.parts ?? [])],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (cand.finishReason) existing.finishReason = cand.finishReason;
|
|
238
|
+
if (cand.content?.role && !existing.content?.role) {
|
|
239
|
+
existing.content!.role = cand.content.role;
|
|
240
|
+
}
|
|
241
|
+
this.mergeParts(
|
|
242
|
+
existing.content!.parts as GeminiPart[],
|
|
243
|
+
cand.content?.parts ?? [],
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Merge streamed parts into the aggregated parts list.
|
|
250
|
+
*
|
|
251
|
+
* Gemini streams text incrementally as separate `text`-only parts; thought
|
|
252
|
+
* signatures and function calls arrive as their own parts. Consecutive
|
|
253
|
+
* plain-text parts are coalesced into a single accumulating text part so the
|
|
254
|
+
* final thought signature on a block survives on its trailing part.
|
|
255
|
+
*/
|
|
256
|
+
private mergeParts(target: GeminiPart[], incoming: GeminiPart[]): void {
|
|
257
|
+
for (const part of incoming) {
|
|
258
|
+
const last = target[target.length - 1];
|
|
259
|
+
if (isPlainTextPart(part) && last && isPlainTextPart(last)) {
|
|
260
|
+
last.text = (last.text || "") + (part.text || "");
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
target.push({ ...part });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private finalize(): void {
|
|
268
|
+
if (this.finalized) return;
|
|
269
|
+
this.finalized = true;
|
|
270
|
+
setResponseAttrs(this.span, {
|
|
271
|
+
candidates: Array.from(this.aggregated.values()),
|
|
272
|
+
usageMetadata: this.usage,
|
|
273
|
+
responseId: this.responseId,
|
|
274
|
+
modelVersion: this.modelVersion,
|
|
275
|
+
});
|
|
276
|
+
this.span.end();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Public API
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
// The instrumentor patches `client.models.generateContent[Stream]` in place.
|
|
285
|
+
// We accept the client as `any` (matching AnthropicInstrumentor) so the typed
|
|
286
|
+
// SDK shape from `@google/genai` is compatible without consumers needing
|
|
287
|
+
// to cast at the call site.
|
|
288
|
+
interface GeminiModelsApi {
|
|
289
|
+
generateContent: (...args: unknown[]) => Promise<unknown>;
|
|
290
|
+
generateContentStream?: (...args: unknown[]) => Promise<unknown>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Auto-instrumentor that wraps a `@google/genai` client instance to add
|
|
295
|
+
* Introspection tracing with full thought-signature capture.
|
|
296
|
+
*
|
|
297
|
+
* Captures all content parts including thought summaries and signed
|
|
298
|
+
* `thoughtSignature` payloads required for multi-turn replay.
|
|
299
|
+
*/
|
|
300
|
+
export class GeminiInstrumentor {
|
|
301
|
+
private tracer: Tracer | null = null;
|
|
302
|
+
private restores: Array<() => void> = [];
|
|
303
|
+
private conversationId: string | undefined;
|
|
304
|
+
|
|
305
|
+
instrument(opts: {
|
|
306
|
+
tracerProvider?: BasicTracerProvider;
|
|
307
|
+
/** A `@google/genai` `GoogleGenAI` client instance. */
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
309
|
+
client: any;
|
|
310
|
+
/**
|
|
311
|
+
* Conversation ID shared across all spans produced by this instrumentor.
|
|
312
|
+
* Defaults to a random UUID generated at `instrument()` time.
|
|
313
|
+
*/
|
|
314
|
+
conversationId?: string;
|
|
315
|
+
}): void {
|
|
316
|
+
const { client } = opts;
|
|
317
|
+
if (!client?.models?.generateContent) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"Invalid client — pass a GoogleGenAI client instance (new GoogleGenAI({...}))",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.conversationId = opts.conversationId ?? crypto.randomUUID();
|
|
324
|
+
|
|
325
|
+
const provider = opts.tracerProvider ?? trace.getTracerProvider();
|
|
326
|
+
this.tracer = (provider as BasicTracerProvider).getTracer(
|
|
327
|
+
"introspection-gemini",
|
|
328
|
+
);
|
|
329
|
+
const tracer = this.tracer;
|
|
330
|
+
const conversationId = this.conversationId;
|
|
331
|
+
|
|
332
|
+
const api = client.models as GeminiModelsApi;
|
|
333
|
+
const origGenerate = api.generateContent.bind(api);
|
|
334
|
+
const origGenerateStream = api.generateContentStream?.bind(api);
|
|
335
|
+
this.restores.push(() => {
|
|
336
|
+
api.generateContent = origGenerate;
|
|
337
|
+
if (origGenerateStream) api.generateContentStream = origGenerateStream;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
api.generateContent = async (...args: unknown[]) => {
|
|
341
|
+
const kwargs = (args[0] as Record<string, unknown>) || {};
|
|
342
|
+
const span = startSpan(
|
|
343
|
+
tracer,
|
|
344
|
+
(kwargs.model as string) || "unknown",
|
|
345
|
+
conversationId,
|
|
346
|
+
);
|
|
347
|
+
setRequestAttrs(span, kwargs);
|
|
348
|
+
try {
|
|
349
|
+
const response = (await origGenerate(...args)) as Record<
|
|
350
|
+
string,
|
|
351
|
+
unknown
|
|
352
|
+
>;
|
|
353
|
+
setResponseAttrs(span, response);
|
|
354
|
+
return response;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
span.setStatus({
|
|
357
|
+
code: SpanStatusCode.ERROR,
|
|
358
|
+
message: String(err),
|
|
359
|
+
});
|
|
360
|
+
throw err;
|
|
361
|
+
} finally {
|
|
362
|
+
span.end();
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (origGenerateStream) {
|
|
367
|
+
api.generateContentStream = async (...args: unknown[]) => {
|
|
368
|
+
const kwargs = (args[0] as Record<string, unknown>) || {};
|
|
369
|
+
const span = startSpan(
|
|
370
|
+
tracer,
|
|
371
|
+
(kwargs.model as string) || "unknown",
|
|
372
|
+
conversationId,
|
|
373
|
+
);
|
|
374
|
+
setRequestAttrs(span, kwargs);
|
|
375
|
+
try {
|
|
376
|
+
const stream = (await origGenerateStream(
|
|
377
|
+
...args,
|
|
378
|
+
)) as AsyncIterable<StreamChunk>;
|
|
379
|
+
return new TracedStream(stream, span);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
span.setStatus({
|
|
382
|
+
code: SpanStatusCode.ERROR,
|
|
383
|
+
message: String(err),
|
|
384
|
+
});
|
|
385
|
+
span.end();
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
uninstrument(): void {
|
|
393
|
+
for (const restore of this.restores) restore();
|
|
394
|
+
this.restores = [];
|
|
395
|
+
this.tracer = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry surface for the Introspection Node SDK.
|
|
3
|
+
*
|
|
4
|
+
* Subpath entry point: `@introspection-sdk/introspection-node/otel`.
|
|
5
|
+
*
|
|
6
|
+
* Importing from here pulls in OTel SDK packages (peer deps). The
|
|
7
|
+
* REST-only `IntrospectionClient` is available from the package root
|
|
8
|
+
* and does not require these to be installed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// IntrospectionLogs — OTel logs exporter with track/feedback/identify
|
|
12
|
+
// and baggage context helpers (extracted from the old IntrospectionClient).
|
|
13
|
+
export { IntrospectionLogs } from "./logs.js";
|
|
14
|
+
export type { IntrospectionLogsOptions } from "./logs.js";
|
|
15
|
+
|
|
16
|
+
// One-call tracing bootstrap (NodeTracerProvider + AsyncLocalStorage +
|
|
17
|
+
// W3C baggage propagator + IntrospectionSpanProcessor).
|
|
18
|
+
export { setupTracing } from "./setup.js";
|
|
19
|
+
export type { SetupTracingOptions, ConflictBehavior } from "./setup.js";
|
|
20
|
+
|
|
21
|
+
// Span processor — attach to your own TracerProvider.
|
|
22
|
+
export { IntrospectionSpanProcessor } from "./span-processor.js";
|
|
23
|
+
export type { IntrospectionSpanProcessorOptions } from "./span-processor.js";
|
|
24
|
+
|
|
25
|
+
// OpenAI Agents SDK tracing processor.
|
|
26
|
+
export { IntrospectionTracingProcessor } from "./tracing-processor.js";
|
|
27
|
+
export type {
|
|
28
|
+
IntrospectionTracingProcessorOptions,
|
|
29
|
+
TracingProcessorAdvancedOptions,
|
|
30
|
+
} from "./tracing-processor.js";
|
|
31
|
+
|
|
32
|
+
// Claude Agent SDK hooks.
|
|
33
|
+
export { IntrospectionClaudeHooks } from "./claude-hooks.js";
|
|
34
|
+
export type {
|
|
35
|
+
IntrospectionClaudeHooksOptions,
|
|
36
|
+
ClaudeHooksAdvancedOptions,
|
|
37
|
+
ClaudeHooksConfig,
|
|
38
|
+
ClaudeHookCallbackMatcher,
|
|
39
|
+
ClaudeHookEvent,
|
|
40
|
+
ClaudeHookInput,
|
|
41
|
+
ClaudeHookOutput,
|
|
42
|
+
ClaudeHookCallback,
|
|
43
|
+
ClaudeUsage,
|
|
44
|
+
ClaudeModelUsage,
|
|
45
|
+
ClaudeResultMessage,
|
|
46
|
+
ClaudeAssistantMessage,
|
|
47
|
+
ClaudeSDKMessage,
|
|
48
|
+
} from "./claude-hooks.js";
|
|
49
|
+
|
|
50
|
+
// Claude Agent SDK wrapper.
|
|
51
|
+
export { withIntrospection } from "./claude-wrapper.js";
|
|
52
|
+
export type {
|
|
53
|
+
WithIntrospectionOptions,
|
|
54
|
+
InstrumentedClaudeAgentSDK,
|
|
55
|
+
ClaudeAgentSDKModule,
|
|
56
|
+
} from "./claude-wrapper.js";
|
|
57
|
+
|
|
58
|
+
// Anthropic SDK instrumentor.
|
|
59
|
+
export {
|
|
60
|
+
AnthropicInstrumentor,
|
|
61
|
+
tracedMessagesCreate,
|
|
62
|
+
REDACTED_THINKING_CONTENT,
|
|
63
|
+
} from "./anthropic.js";
|
|
64
|
+
|
|
65
|
+
// Pi Agent SDK instrumentor.
|
|
66
|
+
export { IntrospectionPiInstrumentor } from "./pi.js";
|
|
67
|
+
export type {
|
|
68
|
+
IntrospectionPiInstrumentorOptions,
|
|
69
|
+
AgentMeta as PiAgentMeta,
|
|
70
|
+
} from "./pi.js";
|
|
71
|
+
|
|
72
|
+
// Gemini SDK instrumentor.
|
|
73
|
+
export { GeminiInstrumentor } from "./gemini.js";
|
|
74
|
+
|
|
75
|
+
// OpenInference span exporter (drops Arize/OpenInference attrs onto a
|
|
76
|
+
// downstream OTel SpanExporter).
|
|
77
|
+
export {
|
|
78
|
+
addOpenInferenceAttributes,
|
|
79
|
+
OpenInferenceSpanExporter,
|
|
80
|
+
} from "../converters/openinference.js";
|
|
@@ -34,6 +34,7 @@ import type { ChainValues } from "@langchain/core/utils/types";
|
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
36
|
context as otelContext,
|
|
37
|
+
propagation,
|
|
37
38
|
ROOT_CONTEXT,
|
|
38
39
|
SpanStatusCode,
|
|
39
40
|
trace as otelTrace,
|
|
@@ -51,8 +52,8 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
|
51
52
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
52
53
|
import { randomUUID } from "crypto";
|
|
53
54
|
|
|
54
|
-
import { logger } from "
|
|
55
|
-
import { VERSION } from "
|
|
55
|
+
import { logger, withOtlpHttpsProxy } from "../utils.js";
|
|
56
|
+
import { VERSION } from "../version.js";
|
|
56
57
|
import type {
|
|
57
58
|
InputMessage,
|
|
58
59
|
OutputMessage,
|
|
@@ -77,7 +78,7 @@ export interface LangChainHandlerAdvancedOptions {
|
|
|
77
78
|
export interface IntrospectionCallbackHandlerOptions {
|
|
78
79
|
/** Authentication token (env: INTROSPECTION_TOKEN). */
|
|
79
80
|
token?: string;
|
|
80
|
-
/** Base URL for the API (env:
|
|
81
|
+
/** Base URL for the API (env: INTROSPECTION_BASE_OTEL_URL). */
|
|
81
82
|
baseUrl?: string;
|
|
82
83
|
/** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME). */
|
|
83
84
|
serviceName?: string;
|
|
@@ -143,7 +144,7 @@ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
|
|
|
143
144
|
|
|
144
145
|
const baseUrl =
|
|
145
146
|
options.baseUrl ||
|
|
146
|
-
process.env.
|
|
147
|
+
process.env.INTROSPECTION_BASE_OTEL_URL ||
|
|
147
148
|
"https://otel.introspection.dev";
|
|
148
149
|
|
|
149
150
|
const endpoint = baseUrl.endsWith("/v1/traces")
|
|
@@ -160,7 +161,9 @@ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
|
|
|
160
161
|
`IntrospectionCallbackHandler initialized: endpoint=${endpoint}`,
|
|
161
162
|
);
|
|
162
163
|
|
|
163
|
-
const spanExporter = new OTLPTraceExporter(
|
|
164
|
+
const spanExporter = new OTLPTraceExporter(
|
|
165
|
+
withOtlpHttpsProxy({ url: endpoint, headers }),
|
|
166
|
+
);
|
|
164
167
|
|
|
165
168
|
const effectiveBatchSize =
|
|
166
169
|
token.startsWith("intro_dev") || token.startsWith("intro_staging")
|
|
@@ -244,11 +247,17 @@ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
|
|
|
244
247
|
this._spanParents.set(runId, parentRunId);
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
//
|
|
248
|
-
|
|
250
|
+
// Baggage wins so per-call agent identity (e.g. via withAgent())
|
|
251
|
+
// overrides the parent-span-name walk used as a default.
|
|
252
|
+
const baggageAgentName = this._readBaggage("gen_ai.agent.name");
|
|
253
|
+
const agentName = baggageAgentName || this._findAgentName(parentRunId);
|
|
249
254
|
if (agentName) {
|
|
250
255
|
span.setAttribute("gen_ai.agent.name", agentName);
|
|
251
256
|
}
|
|
257
|
+
const baggageAgentId = this._readBaggage("gen_ai.agent.id");
|
|
258
|
+
if (baggageAgentId) {
|
|
259
|
+
span.setAttribute("gen_ai.agent.id", baggageAgentId);
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
return span;
|
|
254
263
|
}
|
|
@@ -450,6 +459,31 @@ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
|
|
|
450
459
|
if (typeof outputTokens === "number") {
|
|
451
460
|
otelSpan.setAttribute("gen_ai.usage.output_tokens", outputTokens);
|
|
452
461
|
}
|
|
462
|
+
// Flat field (legacy format) or nested cache_creation object (Claude 4+).
|
|
463
|
+
const cacheCreationObj = tokenUsage["cache_creation"] as
|
|
464
|
+
| Record<string, unknown>
|
|
465
|
+
| undefined;
|
|
466
|
+
const cacheWriteTokens =
|
|
467
|
+
tokenUsage["cache_creation_input_tokens"] ||
|
|
468
|
+
tokenUsage["cacheCreationInputTokens"] ||
|
|
469
|
+
((cacheCreationObj?.["ephemeral_5m_input_tokens"] as number) || 0) +
|
|
470
|
+
((cacheCreationObj?.["ephemeral_1h_input_tokens"] as number) || 0) ||
|
|
471
|
+
undefined;
|
|
472
|
+
if (typeof cacheWriteTokens === "number") {
|
|
473
|
+
otelSpan.setAttribute(
|
|
474
|
+
"gen_ai.usage.cache_creation.input_tokens",
|
|
475
|
+
cacheWriteTokens,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const cacheReadTokens =
|
|
479
|
+
tokenUsage["cache_read_input_tokens"] ||
|
|
480
|
+
tokenUsage["cacheReadInputTokens"];
|
|
481
|
+
if (typeof cacheReadTokens === "number") {
|
|
482
|
+
otelSpan.setAttribute(
|
|
483
|
+
"gen_ai.usage.cache_read.input_tokens",
|
|
484
|
+
cacheReadTokens,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
453
487
|
}
|
|
454
488
|
|
|
455
489
|
const model = usage?.["model"] || usage?.["model_name"];
|
|
@@ -657,9 +691,24 @@ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
|
|
|
657
691
|
if (threadId !== undefined && threadId !== null) {
|
|
658
692
|
return String(threadId);
|
|
659
693
|
}
|
|
694
|
+
const baggageConvId = this._readBaggage("gen_ai.conversation.id");
|
|
695
|
+
if (baggageConvId) return baggageConvId;
|
|
660
696
|
return this._conversationId;
|
|
661
697
|
}
|
|
662
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Read a single value from the active OTel baggage. Returns undefined when
|
|
701
|
+
* no propagator is installed or the key is absent.
|
|
702
|
+
*
|
|
703
|
+
* Used so callers can scope LangChain calls with
|
|
704
|
+
* `IntrospectionClient.withAgent()` / `.withConversation()` /
|
|
705
|
+
* `.withGenAi()` without threading metadata through every invoke().
|
|
706
|
+
*/
|
|
707
|
+
private _readBaggage(key: string): string | undefined {
|
|
708
|
+
const baggage = propagation.getBaggage(otelContext.active());
|
|
709
|
+
return baggage?.getEntry(key)?.value;
|
|
710
|
+
}
|
|
711
|
+
|
|
663
712
|
private _convertMessages(messages: BaseMessage[]): {
|
|
664
713
|
inputMessages: InputMessage[];
|
|
665
714
|
systemInstructions: string[];
|