@telemetry-dev/tanstack-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,681 @@
1
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { ProtobufMetricsSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
3
+ import { resourceFromAttributes } from "@opentelemetry/resources";
4
+ import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
5
+ import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
6
+ //#region src/config.ts
7
+ const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
8
+ function resolveConfig(options = {}) {
9
+ const env = typeof process !== "undefined" && process.env ? process.env : {};
10
+ const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
11
+ return {
12
+ apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
13
+ baseUrl,
14
+ environment: options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production",
15
+ serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
16
+ fetchImpl: options.fetch ?? globalThis.fetch,
17
+ waitUntil: options.waitUntil,
18
+ onError: options.onError
19
+ };
20
+ }
21
+ //#endregion
22
+ //#region src/otel.ts
23
+ const DURATION_BUCKETS = [
24
+ .01,
25
+ .02,
26
+ .04,
27
+ .08,
28
+ .16,
29
+ .32,
30
+ .64,
31
+ 1.28,
32
+ 2.56,
33
+ 5.12,
34
+ 10.24,
35
+ 20.48,
36
+ 40.96,
37
+ 81.92
38
+ ];
39
+ const TOKEN_BUCKETS = [
40
+ 1,
41
+ 4,
42
+ 16,
43
+ 64,
44
+ 256,
45
+ 1024,
46
+ 4096,
47
+ 16384,
48
+ 65536,
49
+ 262144,
50
+ 1048576,
51
+ 4194304,
52
+ 16777216,
53
+ 67108864
54
+ ];
55
+ const SCOPE_NAME = "@telemetry-dev/tanstack-ai";
56
+ const SCOPE_VERSION = "0.0.0";
57
+ const RETRY_DELAYS_MS = [100, 500];
58
+ const RETRYABLE_STATUSES = new Set([
59
+ 429,
60
+ 502,
61
+ 503,
62
+ 504
63
+ ]);
64
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
65
+ const cancelBody = async (res) => {
66
+ try {
67
+ await res.body?.cancel();
68
+ } catch {}
69
+ };
70
+ const postOtlp = async ({ fetchImpl, url, headers, body }) => {
71
+ for (let attempt = 0;; attempt += 1) {
72
+ try {
73
+ const res = await fetchImpl(url, {
74
+ method: "POST",
75
+ headers,
76
+ body
77
+ });
78
+ if (res.ok || !RETRYABLE_STATUSES.has(res.status) || attempt === RETRY_DELAYS_MS.length) {
79
+ await cancelBody(res);
80
+ return res;
81
+ }
82
+ await cancelBody(res);
83
+ } catch (error) {
84
+ if (attempt === RETRY_DELAYS_MS.length) throw error;
85
+ }
86
+ await delay(RETRY_DELAYS_MS[attempt]);
87
+ }
88
+ };
89
+ const cache = /* @__PURE__ */ new Map();
90
+ const buildCore = (config) => {
91
+ const { apiKey, baseUrl, environment, serviceName } = config;
92
+ const resource = resourceFromAttributes({
93
+ "service.name": serviceName,
94
+ "deployment.environment.name": environment
95
+ });
96
+ const headers = {
97
+ "content-type": "application/x-protobuf",
98
+ authorization: `Bearer ${apiKey}`
99
+ };
100
+ const tracer = new BasicTracerProvider({ resource }).getTracer(SCOPE_NAME, SCOPE_VERSION);
101
+ const sendSpans = async (spans, transport) => {
102
+ const body = ProtobufTraceSerializer.serializeRequest(spans);
103
+ if (!body || body.byteLength === 0) return;
104
+ const { fetchImpl, onError } = transport;
105
+ const res = await postOtlp({
106
+ fetchImpl,
107
+ url: `${baseUrl}/v1/traces`,
108
+ headers,
109
+ body
110
+ });
111
+ if (!res.ok) onError?.(/* @__PURE__ */ new Error(`telemetry.dev trace ingest failed: ${res.status}`));
112
+ };
113
+ const buildMetrics = (transport) => {
114
+ const { fetchImpl, onError } = transport;
115
+ const meterProvider = new MeterProvider({
116
+ resource,
117
+ readers: [new PeriodicExportingMetricReader({
118
+ exporter: {
119
+ export(resourceMetrics, resultCallback) {
120
+ const body = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);
121
+ if (!body || body.byteLength === 0) {
122
+ resultCallback({ code: 0 });
123
+ return;
124
+ }
125
+ fetchImpl(`${baseUrl}/v1/metrics`, {
126
+ method: "POST",
127
+ headers,
128
+ body
129
+ }).then((res) => {
130
+ if (!res.ok) {
131
+ const error = /* @__PURE__ */ new Error(`telemetry.dev metric ingest failed: ${res.status}`);
132
+ onError?.(error);
133
+ resultCallback({
134
+ code: 1,
135
+ error
136
+ });
137
+ return;
138
+ }
139
+ resultCallback({ code: 0 });
140
+ }).catch((error) => {
141
+ onError?.(error);
142
+ resultCallback({
143
+ code: 1,
144
+ error: error instanceof Error ? error : void 0
145
+ });
146
+ });
147
+ },
148
+ selectAggregationTemporality: () => AggregationTemporality.DELTA,
149
+ forceFlush: () => Promise.resolve(),
150
+ shutdown: () => Promise.resolve()
151
+ },
152
+ exportIntervalMillis: 2 ** 31 - 1
153
+ })]
154
+ });
155
+ const meter = meterProvider.getMeter(SCOPE_NAME, SCOPE_VERSION);
156
+ return {
157
+ durationHistogram: meter.createHistogram("gen_ai.client.operation.duration", {
158
+ unit: "s",
159
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
160
+ }),
161
+ tokenHistogram: meter.createHistogram("gen_ai.client.token.usage", {
162
+ unit: "{token}",
163
+ advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
164
+ }),
165
+ shutdown: () => meterProvider.shutdown()
166
+ };
167
+ };
168
+ return {
169
+ tracer,
170
+ sendSpans,
171
+ buildMetrics
172
+ };
173
+ };
174
+ const coreFor = (config) => {
175
+ const key = `${config.apiKey}|${config.baseUrl}|${config.environment}|${config.serviceName}`;
176
+ const cached = cache.get(key);
177
+ if (cached) return cached;
178
+ const core = buildCore(config);
179
+ cache.set(key, core);
180
+ return core;
181
+ };
182
+ const createEmitter = (config, overrides) => {
183
+ const core = coreFor(config);
184
+ const transport = {
185
+ fetchImpl: config.fetchImpl,
186
+ onError: config.onError
187
+ };
188
+ const onError = config.onError;
189
+ const sendSpans = overrides?.sendSpans ?? ((spans) => core.sendSpans(spans, transport));
190
+ let metrics;
191
+ const ensureMetrics = () => metrics ??= core.buildMetrics(transport);
192
+ const recordDuration = overrides?.recordDuration ?? ((seconds, attributes) => ensureMetrics().durationHistogram.record(seconds, attributes));
193
+ const recordTokens = overrides?.recordTokens ?? ((tokenType, count, attributes) => ensureMetrics().tokenHistogram.record(count, {
194
+ ...attributes,
195
+ "gen_ai.token.type": tokenType
196
+ }));
197
+ const flush = (spans) => {
198
+ const pipeline = metrics;
199
+ metrics = void 0;
200
+ const p = (async () => {
201
+ try {
202
+ await sendSpans(spans);
203
+ } catch (e) {
204
+ onError?.(e);
205
+ }
206
+ try {
207
+ if (pipeline) await pipeline.shutdown();
208
+ } catch (e) {
209
+ onError?.(e);
210
+ }
211
+ })();
212
+ if (config.waitUntil) {
213
+ config.waitUntil(p);
214
+ return Promise.resolve();
215
+ }
216
+ return p;
217
+ };
218
+ return {
219
+ tracer: core.tracer,
220
+ recordDuration,
221
+ recordTokens,
222
+ flush
223
+ };
224
+ };
225
+ //#endregion
226
+ //#region src/middleware.ts
227
+ function omitUndefined(attributes) {
228
+ const out = {};
229
+ for (const key of Object.keys(attributes)) {
230
+ const value = attributes[key];
231
+ if (value !== void 0) out[key] = value;
232
+ }
233
+ return out;
234
+ }
235
+ function readId(value) {
236
+ if (typeof value === "string") return value.length > 0 ? value : null;
237
+ if (typeof value === "number" || typeof value === "bigint") return value.toString();
238
+ return null;
239
+ }
240
+ function jsonAttr(value) {
241
+ if (value === void 0) return void 0;
242
+ if (typeof value === "string") return value;
243
+ try {
244
+ return JSON.stringify(value);
245
+ } catch {
246
+ return;
247
+ }
248
+ }
249
+ function firstNumber(...candidates) {
250
+ for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
251
+ }
252
+ function errorTypeName(err) {
253
+ if (err instanceof Error) return err.name || "Error";
254
+ if (err && typeof err === "object" && "name" in err) {
255
+ const n = err.name;
256
+ if (typeof n === "string" && n.length > 0) return n;
257
+ }
258
+ return "Error";
259
+ }
260
+ function errorMessage(err) {
261
+ if (err instanceof Error) return err.message;
262
+ if (typeof err === "string") return err;
263
+ if (err && typeof err === "object" && "message" in err) {
264
+ const m = err.message;
265
+ if (typeof m === "string") return m;
266
+ }
267
+ return String(err);
268
+ }
269
+ const SEVERITY_INFO = 9;
270
+ const SEVERITY_ERROR = 17;
271
+ const MAX_TOKENS_KEYS = [
272
+ "max_output_tokens",
273
+ "max_tokens",
274
+ "max_completion_tokens",
275
+ "maxOutputTokens",
276
+ "maxCompletionTokens",
277
+ "maxTokens"
278
+ ];
279
+ function samplingAttributes(modelOptions) {
280
+ const sampling = modelOptions ?? {};
281
+ const nested = sampling["options"] && typeof sampling["options"] === "object" ? sampling["options"] : void 0;
282
+ return omitUndefined({
283
+ "gen_ai.request.temperature": firstNumber(sampling["temperature"], nested?.["temperature"]),
284
+ "gen_ai.request.top_p": firstNumber(sampling["top_p"], sampling["topP"], nested?.["top_p"]),
285
+ "gen_ai.request.max_tokens": firstNumber(...MAX_TOKENS_KEYS.map((key) => sampling[key]), nested?.["num_predict"])
286
+ });
287
+ }
288
+ /**
289
+ * Build a TanStack AI chat middleware that streams `chat()` runs to telemetry.dev as
290
+ * OpenTelemetry GenAI (`gen_ai.*`) spans + metrics: one root span per `chat()` call, one CLIENT
291
+ * span per agent-loop iteration, and one span per tool execution. Per-run state is keyed by the
292
+ * middleware context in a WeakMap, so a single `telemetryDev()` instance is safe to share across
293
+ * concurrent and overlapping `chat()` calls (e.g. registered once at module scope).
294
+ */
295
+ function telemetryDev(options, overrides) {
296
+ const config = resolveConfig(options);
297
+ if (!config.apiKey) return { name: "telemetry-dev" };
298
+ const emitter = createEmitter(config, overrides);
299
+ const onError = config.onError;
300
+ const states = /* @__PURE__ */ new WeakMap();
301
+ const closeIteration = (state) => {
302
+ const iteration = state.iteration;
303
+ if (!iteration) return;
304
+ const endedAt = /* @__PURE__ */ new Date();
305
+ const usage = iteration.usage;
306
+ iteration.span.setAttributes(omitUndefined({
307
+ "gen_ai.response.model": iteration.responseModel ?? void 0,
308
+ "gen_ai.response.finish_reasons": iteration.finishReason ? [iteration.finishReason] : void 0,
309
+ "gen_ai.usage.input_tokens": usage?.promptTokens,
310
+ "gen_ai.usage.output_tokens": usage?.completionTokens,
311
+ "gen_ai.usage.cache_read.input_tokens": usage?.promptTokensDetails?.cachedTokens,
312
+ "gen_ai.usage.cache_creation.input_tokens": usage?.promptTokensDetails?.cacheWriteTokens,
313
+ "gen_ai.usage.reasoning.output_tokens": usage?.completionTokensDetails?.reasoningTokens,
314
+ "gen_ai.usage.cost": usage?.cost,
315
+ "gen_ai.output.type": iteration.structured ? "json" : "text",
316
+ "gen_ai.output.messages": jsonAttr(iteration.outputText ?? void 0)
317
+ }));
318
+ iteration.span.end(endedAt);
319
+ state.childSpans.push(iteration.span);
320
+ state.iterationMetrics.push({
321
+ durationSec: Math.max(endedAt.getTime() - iteration.startedAt.getTime(), 0) / 1e3,
322
+ inputTokens: usage?.promptTokens ?? null,
323
+ outputTokens: usage?.completionTokens ?? null
324
+ });
325
+ if (iteration.structured && iteration.outputText !== null) state.structuredOutput = iteration.outputText;
326
+ state.iteration = null;
327
+ };
328
+ const setRootBaseAttributes = (state) => {
329
+ state.rootSpan.setAttributes(omitUndefined({
330
+ "gen_ai.operation.name": state.hasToolSpan ? "invoke_agent" : "chat",
331
+ "gen_ai.provider.name": state.provider,
332
+ "gen_ai.request.model": state.requestModel,
333
+ "gen_ai.response.model": state.responseModel ?? void 0,
334
+ "gen_ai.conversation.id": state.sessionId,
335
+ "gen_ai.input.messages": state.rootInput,
336
+ "user.id": state.userId ?? void 0,
337
+ ...state.rootSampling
338
+ }));
339
+ if (state.restMetadata) for (const [key, value] of Object.entries(state.restMetadata)) {
340
+ const attr = typeof value === "string" ? value : jsonAttr(value);
341
+ if (attr !== void 0) state.rootSpan.setAttribute(`td.metadata.${key}`, attr);
342
+ }
343
+ };
344
+ const addSummaryEvent = (state, hasError, message) => {
345
+ const inputPresent = state.iterationMetrics.some((m) => m.inputTokens !== null);
346
+ const outputPresent = state.iterationMetrics.some((m) => m.outputTokens !== null);
347
+ const inputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.inputTokens ?? 0), 0);
348
+ const outputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.outputTokens ?? 0), 0);
349
+ state.rootSpan.addEvent("generation.summary", omitUndefined({
350
+ "log.severity_number": hasError ? SEVERITY_ERROR : SEVERITY_INFO,
351
+ "log.message": message,
352
+ "gen_ai.usage.input_tokens": inputPresent ? inputTokens : void 0,
353
+ "gen_ai.usage.output_tokens": outputPresent ? outputTokens : void 0
354
+ }));
355
+ };
356
+ const recordRunMetrics = (state) => {
357
+ const metricBase = omitUndefined({
358
+ "gen_ai.provider.name": state.provider,
359
+ "gen_ai.request.model": state.requestModel,
360
+ "gen_ai.response.model": state.responseModel ?? void 0
361
+ });
362
+ for (const m of state.iterationMetrics) {
363
+ const attrs = {
364
+ ...metricBase,
365
+ "gen_ai.operation.name": "chat"
366
+ };
367
+ emitter.recordDuration(m.durationSec, attrs);
368
+ if (m.inputTokens !== null) emitter.recordTokens("input", m.inputTokens, attrs);
369
+ if (m.outputTokens !== null) emitter.recordTokens("output", m.outputTokens, attrs);
370
+ }
371
+ for (const t of state.toolMetrics) emitter.recordDuration(t.durationSec, {
372
+ ...metricBase,
373
+ "gen_ai.operation.name": "execute_tool"
374
+ });
375
+ };
376
+ const failOpenSpans = (state, errType, message) => {
377
+ for (const [, entry] of state.openTools) {
378
+ entry.span.setStatus({
379
+ code: SpanStatusCode.ERROR,
380
+ message
381
+ });
382
+ entry.span.setAttribute("error.type", errType);
383
+ entry.span.end();
384
+ state.childSpans.push(entry.span);
385
+ }
386
+ state.openTools.clear();
387
+ if (state.iteration) {
388
+ state.iteration.span.setStatus({
389
+ code: SpanStatusCode.ERROR,
390
+ message
391
+ });
392
+ state.iteration.span.setAttribute("error.type", errType);
393
+ closeIteration(state);
394
+ }
395
+ };
396
+ return {
397
+ name: "telemetry-dev",
398
+ onStart(ctx) {
399
+ try {
400
+ const rawMetadata = ctx.options?.["metadata"];
401
+ const metadata = rawMetadata && typeof rawMetadata === "object" ? rawMetadata : void 0;
402
+ const userId = readId(metadata?.["userId"]);
403
+ const sessionId = readId(metadata?.["sessionId"]) ?? ctx.threadId;
404
+ let restMetadata;
405
+ if (metadata) {
406
+ const rest = {};
407
+ for (const [key, value] of Object.entries(metadata)) if (key !== "userId" && key !== "sessionId") rest[key] = value;
408
+ restMetadata = Object.keys(rest).length > 0 ? rest : void 0;
409
+ }
410
+ const rootSpan = emitter.tracer.startSpan("chat", {
411
+ startTime: /* @__PURE__ */ new Date(),
412
+ kind: SpanKind.INTERNAL
413
+ });
414
+ states.set(ctx, {
415
+ rootSpan,
416
+ rootCtx: trace.setSpan(ROOT_CONTEXT, rootSpan),
417
+ provider: ctx.provider,
418
+ requestModel: ctx.model,
419
+ responseModel: null,
420
+ userId,
421
+ sessionId,
422
+ restMetadata,
423
+ rootInput: void 0,
424
+ rootSampling: {},
425
+ rootCaptured: false,
426
+ iteration: null,
427
+ structuredOutput: null,
428
+ openTools: /* @__PURE__ */ new Map(),
429
+ childSpans: [],
430
+ hasToolSpan: false,
431
+ iterationMetrics: [],
432
+ toolMetrics: []
433
+ });
434
+ } catch (err) {
435
+ onError?.(err);
436
+ }
437
+ },
438
+ onConfig(ctx, chatConfig) {
439
+ if (ctx.phase !== "beforeModel" && ctx.phase !== "structuredOutput") return void 0;
440
+ try {
441
+ const state = states.get(ctx);
442
+ if (!state) return void 0;
443
+ closeIteration(state);
444
+ const inputMessages = [];
445
+ for (const prompt of chatConfig.systemPrompts) inputMessages.push({
446
+ role: "system",
447
+ content: typeof prompt === "string" ? prompt : prompt.content
448
+ });
449
+ for (const message of chatConfig.messages) inputMessages.push({
450
+ role: message.role,
451
+ content: message.content
452
+ });
453
+ const inputJson = jsonAttr(inputMessages);
454
+ const sampling = samplingAttributes(chatConfig.modelOptions ?? ctx.modelOptions);
455
+ if (!state.rootCaptured) {
456
+ state.rootCaptured = true;
457
+ state.rootInput = inputJson;
458
+ state.rootSampling = sampling;
459
+ }
460
+ const startedAt = /* @__PURE__ */ new Date();
461
+ const span = emitter.tracer.startSpan("chat", {
462
+ startTime: startedAt,
463
+ kind: SpanKind.CLIENT,
464
+ attributes: omitUndefined({
465
+ "gen_ai.operation.name": "chat",
466
+ "gen_ai.provider.name": ctx.provider,
467
+ "gen_ai.request.model": ctx.model,
468
+ "gen_ai.conversation.id": state.sessionId,
469
+ "gen_ai.input.messages": inputJson,
470
+ ...sampling
471
+ })
472
+ }, state.rootCtx);
473
+ state.iteration = {
474
+ span,
475
+ otelCtx: trace.setSpan(state.rootCtx, span),
476
+ startedAt,
477
+ usage: null,
478
+ finishReason: null,
479
+ responseModel: null,
480
+ outputText: null,
481
+ structured: ctx.phase === "structuredOutput"
482
+ };
483
+ } catch (err) {
484
+ onError?.(err);
485
+ }
486
+ },
487
+ onChunk(ctx, chunk) {
488
+ if (chunk.type !== "RUN_FINISHED" && chunk.type !== "CUSTOM") return void 0;
489
+ try {
490
+ const state = states.get(ctx);
491
+ const iteration = state?.iteration;
492
+ if (!state || !iteration) return void 0;
493
+ if (chunk.type === "CUSTOM") {
494
+ if (iteration.structured && chunk.name === "structured-output.complete") {
495
+ const raw = chunk.value?.raw;
496
+ if (typeof raw === "string") iteration.outputText = raw;
497
+ }
498
+ return;
499
+ }
500
+ iteration.finishReason = chunk.finishReason ?? null;
501
+ if (chunk.model) {
502
+ iteration.responseModel = chunk.model;
503
+ state.responseModel = chunk.model;
504
+ }
505
+ if (chunk.usage) iteration.usage = chunk.usage;
506
+ if (!iteration.structured) iteration.outputText = ctx.accumulatedContent.length > 0 ? ctx.accumulatedContent : null;
507
+ } catch (err) {
508
+ onError?.(err);
509
+ }
510
+ },
511
+ onUsage(ctx, usage) {
512
+ try {
513
+ const state = states.get(ctx);
514
+ if (state?.iteration) state.iteration.usage = usage;
515
+ } catch (err) {
516
+ onError?.(err);
517
+ }
518
+ },
519
+ onBeforeToolCall(ctx, hookCtx) {
520
+ try {
521
+ const state = states.get(ctx);
522
+ if (!state) return void 0;
523
+ const startedAt = /* @__PURE__ */ new Date();
524
+ const span = emitter.tracer.startSpan("execute_tool", {
525
+ startTime: startedAt,
526
+ kind: SpanKind.INTERNAL,
527
+ attributes: omitUndefined({
528
+ "gen_ai.operation.name": "execute_tool",
529
+ "gen_ai.tool.name": hookCtx.toolName,
530
+ "gen_ai.tool.call.id": hookCtx.toolCallId,
531
+ "gen_ai.conversation.id": state.sessionId,
532
+ "gen_ai.tool.call.arguments": jsonAttr(hookCtx.args ?? null)
533
+ })
534
+ }, state.iteration?.otelCtx ?? state.rootCtx);
535
+ state.openTools.set(hookCtx.toolCallId, {
536
+ span,
537
+ startedAt
538
+ });
539
+ state.hasToolSpan = true;
540
+ } catch (err) {
541
+ onError?.(err);
542
+ }
543
+ },
544
+ onAfterToolCall(ctx, info) {
545
+ try {
546
+ const state = states.get(ctx);
547
+ const entry = state?.openTools.get(info.toolCallId);
548
+ if (!state || !entry) return;
549
+ state.openTools.delete(info.toolCallId);
550
+ const { span } = entry;
551
+ if (info.ok) {
552
+ const result = jsonAttr(info.result ?? null);
553
+ if (result !== void 0) span.setAttribute("gen_ai.tool.call.result", result);
554
+ } else {
555
+ const message = errorMessage(info.error);
556
+ const errType = info.error instanceof Error ? info.error.name : "tool_error";
557
+ span.setStatus({ code: SpanStatusCode.ERROR });
558
+ span.setAttribute("error.type", errType);
559
+ span.addEvent("exception", {
560
+ "exception.type": errType,
561
+ "exception.message": message,
562
+ "log.severity_number": SEVERITY_ERROR
563
+ });
564
+ }
565
+ span.end();
566
+ state.childSpans.push(span);
567
+ state.toolMetrics.push({ durationSec: Math.max(info.duration, 0) / 1e3 });
568
+ } catch (err) {
569
+ onError?.(err);
570
+ }
571
+ },
572
+ async onToolPhaseComplete(ctx, info) {
573
+ if (info.needsApproval.length === 0 && info.needsClientExecution.length === 0) return;
574
+ try {
575
+ const state = states.get(ctx);
576
+ if (!state) return;
577
+ states.delete(ctx);
578
+ for (const [, entry] of state.openTools) {
579
+ entry.span.end();
580
+ state.childSpans.push(entry.span);
581
+ }
582
+ state.openTools.clear();
583
+ const finishReason = state.iteration?.finishReason ?? "tool_calls";
584
+ closeIteration(state);
585
+ state.hasToolSpan = true;
586
+ setRootBaseAttributes(state);
587
+ state.rootSpan.setAttributes(omitUndefined({
588
+ "gen_ai.output.messages": jsonAttr(ctx.accumulatedContent.length > 0 ? ctx.accumulatedContent : void 0),
589
+ "gen_ai.response.finish_reasons": [finishReason]
590
+ }));
591
+ addSummaryEvent(state, false, `Generation paused awaiting tools (${[...info.needsApproval, ...info.needsClientExecution].map((t) => t.toolName).join(", ")})`);
592
+ state.rootSpan.end();
593
+ recordRunMetrics(state);
594
+ await emitter.flush([state.rootSpan, ...state.childSpans]);
595
+ } catch (err) {
596
+ onError?.(err);
597
+ }
598
+ },
599
+ async onFinish(ctx, info) {
600
+ try {
601
+ const state = states.get(ctx);
602
+ if (!state) return;
603
+ states.delete(ctx);
604
+ for (const [, entry] of state.openTools) {
605
+ entry.span.end();
606
+ state.childSpans.push(entry.span);
607
+ }
608
+ state.openTools.clear();
609
+ closeIteration(state);
610
+ setRootBaseAttributes(state);
611
+ state.rootSpan.setAttributes(omitUndefined({
612
+ "gen_ai.output.messages": jsonAttr(state.structuredOutput ?? info.content),
613
+ "gen_ai.response.finish_reasons": info.finishReason ? [info.finishReason] : void 0
614
+ }));
615
+ const finishReason = info.finishReason ?? "unknown";
616
+ const inputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.inputTokens ?? 0), 0);
617
+ const outputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.outputTokens ?? 0), 0);
618
+ const tokenParts = [];
619
+ if (state.iterationMetrics.some((m) => m.inputTokens !== null)) tokenParts.push(`${inputTokens} in`);
620
+ if (state.iterationMetrics.some((m) => m.outputTokens !== null)) tokenParts.push(`${outputTokens} out`);
621
+ addSummaryEvent(state, false, `Generation completed (${finishReason})${tokenParts.length > 0 ? `: ${tokenParts.join(" / ")} tokens` : ""}`);
622
+ state.rootSpan.end();
623
+ recordRunMetrics(state);
624
+ await emitter.flush([state.rootSpan, ...state.childSpans]);
625
+ } catch (err) {
626
+ onError?.(err);
627
+ }
628
+ },
629
+ async onError(ctx, info) {
630
+ try {
631
+ const state = states.get(ctx);
632
+ if (!state) return;
633
+ states.delete(ctx);
634
+ const errType = errorTypeName(info.error);
635
+ const message = errorMessage(info.error);
636
+ failOpenSpans(state, errType, message);
637
+ setRootBaseAttributes(state);
638
+ state.rootSpan.setStatus({
639
+ code: SpanStatusCode.ERROR,
640
+ message
641
+ });
642
+ state.rootSpan.setAttribute("error.type", errType);
643
+ state.rootSpan.addEvent("exception", {
644
+ "exception.type": errType,
645
+ "exception.message": message,
646
+ "log.severity_number": SEVERITY_ERROR
647
+ });
648
+ addSummaryEvent(state, true, `Generation failed (${errType})`);
649
+ state.rootSpan.end();
650
+ recordRunMetrics(state);
651
+ await emitter.flush([state.rootSpan, ...state.childSpans]);
652
+ } catch (err) {
653
+ onError?.(err);
654
+ }
655
+ },
656
+ async onAbort(ctx, info) {
657
+ try {
658
+ const state = states.get(ctx);
659
+ if (!state) return;
660
+ states.delete(ctx);
661
+ const message = info.reason ?? "cancelled";
662
+ failOpenSpans(state, "cancelled", message);
663
+ setRootBaseAttributes(state);
664
+ state.rootSpan.setStatus({
665
+ code: SpanStatusCode.ERROR,
666
+ message
667
+ });
668
+ state.rootSpan.setAttribute("error.type", "cancelled");
669
+ state.rootSpan.setAttribute("gen_ai.response.finish_reasons", ["cancelled"]);
670
+ addSummaryEvent(state, true, `Generation cancelled (${message})`);
671
+ state.rootSpan.end();
672
+ recordRunMetrics(state);
673
+ await emitter.flush([state.rootSpan, ...state.childSpans]);
674
+ } catch (err) {
675
+ onError?.(err);
676
+ }
677
+ }
678
+ };
679
+ }
680
+ //#endregion
681
+ export { telemetryDev };