@tanstack/ai 0.14.0 → 0.16.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 (30) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +6 -3
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.js +61 -9
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/messages.js +26 -3
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/message-updaters.d.ts +4 -2
  8. package/dist/esm/activities/chat/stream/message-updaters.js +10 -4
  9. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  10. package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
  11. package/dist/esm/activities/chat/stream/processor.js +119 -19
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/types.d.ts +9 -1
  14. package/dist/esm/activities/chat/tools/tool-calls.js +3 -4
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/middlewares/otel.d.ts +75 -0
  17. package/dist/esm/middlewares/otel.js +624 -0
  18. package/dist/esm/middlewares/otel.js.map +1 -0
  19. package/dist/esm/types.d.ts +23 -7
  20. package/package.json +15 -2
  21. package/src/activities/chat/adapter.ts +8 -2
  22. package/src/activities/chat/index.ts +74 -11
  23. package/src/activities/chat/messages.ts +29 -1
  24. package/src/activities/chat/stream/message-updaters.ts +18 -3
  25. package/src/activities/chat/stream/processor.ts +152 -17
  26. package/src/activities/chat/stream/types.ts +9 -1
  27. package/src/activities/chat/tools/tool-calls.ts +4 -4
  28. package/src/middlewares/index.ts +5 -0
  29. package/src/middlewares/otel.ts +861 -0
  30. package/src/types.ts +20 -7
@@ -0,0 +1,75 @@
1
+ import { AttributeValue, Meter, Span, SpanOptions, Tracer } from '@opentelemetry/api';
2
+ import { ChatMiddleware, ChatMiddlewareContext } from '../activities/chat/middleware/types.js';
3
+ /**
4
+ * Scope (role) of an OTel span emitted by this middleware.
5
+ *
6
+ * - `chat` — the root span for a single `chat()` call
7
+ * - `iteration` — one per agent-loop iteration (one model call)
8
+ * - `tool` — one per tool execution inside an iteration
9
+ */
10
+ export type OtelSpanScope = 'chat' | 'iteration' | 'tool';
11
+ /**
12
+ * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.
13
+ *
14
+ * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in
15
+ * `SpanKind` which is also imported by integrations of this middleware.
16
+ */
17
+ export type OtelSpanKind = OtelSpanScope;
18
+ /**
19
+ * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,
20
+ * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that
21
+ * tool-only fields narrow automatically inside callback bodies.
22
+ */
23
+ export type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> = TScope extends 'chat' ? {
24
+ kind: 'chat';
25
+ ctx: ChatMiddlewareContext;
26
+ } : TScope extends 'iteration' ? {
27
+ kind: 'iteration';
28
+ ctx: ChatMiddlewareContext;
29
+ iteration: number;
30
+ } : TScope extends 'tool' ? {
31
+ kind: 'tool';
32
+ ctx: ChatMiddlewareContext;
33
+ iteration: number;
34
+ toolName: string;
35
+ toolCallId: string;
36
+ } : never;
37
+ export interface OtelMiddlewareOptions {
38
+ /** OTel `Tracer` used to start root, iteration, and tool spans. */
39
+ tracer: Tracer;
40
+ /**
41
+ * Optional OTel `Meter`. When provided, the middleware records
42
+ * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`
43
+ * histograms. Omit to disable metrics without disabling tracing.
44
+ */
45
+ meter?: Meter;
46
+ /**
47
+ * When `true`, prompt and completion content is attached to iteration spans
48
+ * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so
49
+ * that PII never lands on a span by accident.
50
+ */
51
+ captureContent?: boolean;
52
+ /**
53
+ * Invoked on every captured content string before it lands on a span.
54
+ * Return a redacted version. If this function throws, the middleware emits
55
+ * the literal sentinel `"[redaction_failed]"` instead of the original text
56
+ * — it never falls back to raw content.
57
+ */
58
+ redact?: (text: string) => string;
59
+ /**
60
+ * Maximum characters kept in the per-iteration assistant text buffer used
61
+ * to emit `gen_ai.choice` events. Extra characters are truncated with a
62
+ * trailing `"…"` marker. Defaults to 100 000. Set to `0` to disable the
63
+ * cap. Exporters typically truncate long attribute values anyway.
64
+ */
65
+ maxContentLength?: number;
66
+ /** Override the default span name for each `kind`. */
67
+ spanNameFormatter?: (info: OtelSpanInfo) => string;
68
+ /** Add extra attributes to each span. */
69
+ attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>;
70
+ /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */
71
+ onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions;
72
+ /** Fires just before every `span.end()`. */
73
+ onSpanEnd?: (info: OtelSpanInfo, span: Span) => void;
74
+ }
75
+ export declare function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware;
@@ -0,0 +1,624 @@
1
+ import { SpanStatusCode, SpanKind, trace, context } from "@opentelemetry/api";
2
+ const stateByCtx = /* @__PURE__ */ new WeakMap();
3
+ const DEFAULT_MAX_CONTENT_LENGTH = 1e5;
4
+ const REDACTION_FAILED_SENTINEL = "[redaction_failed]";
5
+ function serializeContent(content) {
6
+ if (typeof content === "string") return content;
7
+ if (!Array.isArray(content)) return "";
8
+ const parts = [];
9
+ for (const part of content) {
10
+ if (!part || typeof part !== "object") continue;
11
+ const type = part.type;
12
+ switch (type) {
13
+ case "text":
14
+ parts.push(
15
+ (part.text ?? part.content ?? "").toString()
16
+ );
17
+ break;
18
+ case "image":
19
+ parts.push("[image]");
20
+ break;
21
+ case "audio":
22
+ parts.push("[audio]");
23
+ break;
24
+ case "video":
25
+ parts.push("[video]");
26
+ break;
27
+ case "document":
28
+ parts.push("[document]");
29
+ break;
30
+ default:
31
+ parts.push(`[${type ?? "unknown"}]`);
32
+ }
33
+ }
34
+ return parts.join(" ");
35
+ }
36
+ function messageEventName(role) {
37
+ switch (role) {
38
+ case "user":
39
+ return "gen_ai.user.message";
40
+ case "assistant":
41
+ return "gen_ai.assistant.message";
42
+ case "tool":
43
+ return "gen_ai.tool.message";
44
+ case "system":
45
+ return "gen_ai.system.message";
46
+ default:
47
+ return `gen_ai.${role}.message`;
48
+ }
49
+ }
50
+ function errorMessage(err) {
51
+ if (err instanceof Error) return err.message;
52
+ if (typeof err === "string") return err;
53
+ if (err && typeof err === "object" && "message" in err) {
54
+ const m = err.message;
55
+ if (typeof m === "string") return m;
56
+ }
57
+ return void 0;
58
+ }
59
+ function errorTypeName(err) {
60
+ if (err instanceof Error) return err.name || "Error";
61
+ if (err && typeof err === "object" && "name" in err) {
62
+ const n = err.name;
63
+ if (typeof n === "string") return n;
64
+ }
65
+ return "Error";
66
+ }
67
+ function safeCall(label, fn) {
68
+ try {
69
+ return fn();
70
+ } catch (err) {
71
+ console.warn(`[otelMiddleware] ${label} failed`, err);
72
+ return void 0;
73
+ }
74
+ }
75
+ function otelMiddleware(options) {
76
+ const {
77
+ tracer,
78
+ meter,
79
+ captureContent = false,
80
+ redact = (s) => s,
81
+ maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,
82
+ spanNameFormatter,
83
+ attributeEnricher,
84
+ onBeforeSpanStart,
85
+ onSpanEnd
86
+ } = options;
87
+ const durationHistogram = meter?.createHistogram(
88
+ "gen_ai.client.operation.duration",
89
+ {
90
+ description: "GenAI client operation duration",
91
+ unit: "s"
92
+ }
93
+ );
94
+ const tokenHistogram = meter?.createHistogram("gen_ai.client.token.usage", {
95
+ description: "GenAI client token usage",
96
+ unit: "{token}"
97
+ });
98
+ const redactContent = (text) => {
99
+ try {
100
+ return redact(text);
101
+ } catch (err) {
102
+ console.warn("[otelMiddleware] otel.redact failed", err);
103
+ return REDACTION_FAILED_SENTINEL;
104
+ }
105
+ };
106
+ const appendAssistantText = (state, delta) => {
107
+ if (maxContentLength > 0) {
108
+ if (state.assistantTextBufferTruncated) return;
109
+ const remaining = maxContentLength - state.assistantTextBuffer.length;
110
+ if (remaining <= 0) {
111
+ state.assistantTextBufferTruncated = true;
112
+ state.assistantTextBuffer += "…";
113
+ return;
114
+ }
115
+ if (delta.length > remaining) {
116
+ state.assistantTextBuffer += delta.slice(0, remaining) + "…";
117
+ state.assistantTextBufferTruncated = true;
118
+ return;
119
+ }
120
+ }
121
+ state.assistantTextBuffer += delta;
122
+ };
123
+ const closeIterationSpan = (state, ctx) => {
124
+ if (!state.currentIterationSpan) return;
125
+ const span = state.currentIterationSpan;
126
+ const iteration = state.iterationCount - 1;
127
+ safeCall(
128
+ "otel.onSpanEnd",
129
+ () => onSpanEnd?.(
130
+ { kind: "iteration", ctx, iteration },
131
+ span
132
+ )
133
+ );
134
+ span.end();
135
+ state.currentIterationSpan = null;
136
+ };
137
+ return {
138
+ name: "otel",
139
+ onStart(ctx) {
140
+ safeCall("otel.onStart", () => {
141
+ const info = { kind: "chat", ctx };
142
+ const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `chat ${ctx.model}`;
143
+ const baseOptions = {
144
+ kind: SpanKind.INTERNAL,
145
+ attributes: {
146
+ "gen_ai.system": ctx.provider,
147
+ "gen_ai.request.model": ctx.model
148
+ // NOTE: `gen_ai.operation.name` is deliberately NOT set on the
149
+ // root span. The root represents a `chat()` invocation that may
150
+ // span multiple model calls; only iteration spans correspond to
151
+ // a single chat operation. Backends that map `operation.name=chat`
152
+ // to a "generation" event (e.g. PostHog LLM Analytics) would
153
+ // otherwise emit a duplicate generation for the wrapper span.
154
+ }
155
+ };
156
+ const spanOptions = safeCall(
157
+ "otel.onBeforeSpanStart",
158
+ () => onBeforeSpanStart?.(info, baseOptions)
159
+ ) ?? baseOptions;
160
+ const rootSpan = tracer.startSpan(name, spanOptions);
161
+ const enriched = safeCall(
162
+ "otel.attributeEnricher",
163
+ () => attributeEnricher?.(info)
164
+ );
165
+ if (enriched) rootSpan.setAttributes(enriched);
166
+ stateByCtx.set(ctx, {
167
+ rootSpan,
168
+ currentIterationSpan: null,
169
+ toolSpans: /* @__PURE__ */ new Map(),
170
+ iterationCount: 0,
171
+ assistantTextBuffer: "",
172
+ assistantTextBufferTruncated: false,
173
+ startTime: Date.now()
174
+ });
175
+ });
176
+ },
177
+ onConfig(ctx, config) {
178
+ if (ctx.phase !== "beforeModel") return;
179
+ safeCall("otel.onConfig", () => {
180
+ const state = stateByCtx.get(ctx);
181
+ if (!state) return;
182
+ closeIterationSpan(state, ctx);
183
+ const info = {
184
+ kind: "iteration",
185
+ ctx,
186
+ iteration: ctx.iteration
187
+ };
188
+ const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `chat ${ctx.model} #${ctx.iteration}`;
189
+ const baseAttrs = {
190
+ "gen_ai.system": ctx.provider,
191
+ "gen_ai.operation.name": "chat",
192
+ "gen_ai.request.model": ctx.model,
193
+ "tanstack.ai.iteration": ctx.iteration
194
+ };
195
+ if (config.temperature !== void 0)
196
+ baseAttrs["gen_ai.request.temperature"] = config.temperature;
197
+ if (config.topP !== void 0)
198
+ baseAttrs["gen_ai.request.top_p"] = config.topP;
199
+ if (config.maxTokens !== void 0)
200
+ baseAttrs["gen_ai.request.max_tokens"] = config.maxTokens;
201
+ const baseOptions = {
202
+ kind: SpanKind.CLIENT,
203
+ attributes: baseAttrs
204
+ };
205
+ const spanOptions = safeCall(
206
+ "otel.onBeforeSpanStart",
207
+ () => onBeforeSpanStart?.(info, baseOptions)
208
+ ) ?? baseOptions;
209
+ const parentCtx = trace.setSpan(
210
+ context.active(),
211
+ state.rootSpan
212
+ );
213
+ let iterSpan;
214
+ context.with(parentCtx, () => {
215
+ iterSpan = tracer.startSpan(name, spanOptions, parentCtx);
216
+ });
217
+ const enriched = safeCall(
218
+ "otel.attributeEnricher",
219
+ () => attributeEnricher?.(info)
220
+ );
221
+ if (enriched) iterSpan.setAttributes(enriched);
222
+ state.currentIterationSpan = iterSpan;
223
+ state.assistantTextBuffer = "";
224
+ state.assistantTextBufferTruncated = false;
225
+ if (captureContent) {
226
+ for (const sys of config.systemPrompts) {
227
+ iterSpan.addEvent("gen_ai.system.message", {
228
+ content: redactContent(sys)
229
+ });
230
+ }
231
+ for (const m of config.messages) {
232
+ const body = serializeContent(m.content);
233
+ if (body.length === 0) continue;
234
+ iterSpan.addEvent(messageEventName(m.role), {
235
+ content: redactContent(body)
236
+ });
237
+ }
238
+ const inputMessages = [];
239
+ for (const sys of config.systemPrompts) {
240
+ inputMessages.push({
241
+ role: "system",
242
+ content: redactContent(sys)
243
+ });
244
+ }
245
+ for (const m of config.messages) {
246
+ const body = serializeContent(m.content);
247
+ if (body.length === 0) continue;
248
+ inputMessages.push({
249
+ role: m.role,
250
+ content: redactContent(body)
251
+ });
252
+ }
253
+ if (inputMessages.length > 0) {
254
+ const inputJson = JSON.stringify(inputMessages);
255
+ iterSpan.setAttribute("gen_ai.input.messages", inputJson);
256
+ iterSpan.setAttribute("langfuse.observation.input", inputJson);
257
+ if (state.iterationCount === 0) {
258
+ state.rootSpan.setAttribute(
259
+ "langfuse.observation.input",
260
+ inputJson
261
+ );
262
+ state.rootSpan.setAttribute("langfuse.trace.input", inputJson);
263
+ }
264
+ }
265
+ }
266
+ state.iterationCount += 1;
267
+ });
268
+ return void 0;
269
+ },
270
+ onChunk(ctx, chunk) {
271
+ safeCall("otel.onChunk", () => {
272
+ const state = stateByCtx.get(ctx);
273
+ if (!state) return;
274
+ if (captureContent && chunk.type === "TEXT_MESSAGE_CONTENT") {
275
+ appendAssistantText(state, chunk.delta);
276
+ }
277
+ if (chunk.type !== "RUN_FINISHED") return;
278
+ const span = state.currentIterationSpan;
279
+ if (!span) return;
280
+ if (chunk.finishReason) {
281
+ span.setAttribute("gen_ai.response.finish_reasons", [
282
+ chunk.finishReason
283
+ ]);
284
+ }
285
+ if (chunk.model) span.setAttribute("gen_ai.response.model", chunk.model);
286
+ if (chunk.usage) {
287
+ span.setAttributes({
288
+ "gen_ai.usage.input_tokens": chunk.usage.promptTokens,
289
+ "gen_ai.usage.output_tokens": chunk.usage.completionTokens
290
+ });
291
+ }
292
+ if (captureContent && state.assistantTextBuffer.length > 0) {
293
+ const completion = redactContent(state.assistantTextBuffer);
294
+ const outputJson = JSON.stringify([
295
+ { role: "assistant", content: completion }
296
+ ]);
297
+ span.addEvent("gen_ai.choice", { content: completion });
298
+ span.setAttribute("gen_ai.output.messages", outputJson);
299
+ span.setAttribute("langfuse.observation.output", outputJson);
300
+ state.rootSpan.setAttribute("langfuse.observation.output", outputJson);
301
+ state.rootSpan.setAttribute("langfuse.trace.output", outputJson);
302
+ state.assistantTextBuffer = "";
303
+ state.assistantTextBufferTruncated = false;
304
+ }
305
+ });
306
+ return void 0;
307
+ },
308
+ onUsage(ctx, usage) {
309
+ safeCall("otel.onUsage", () => {
310
+ const state = stateByCtx.get(ctx);
311
+ if (!state) return;
312
+ if (tokenHistogram) {
313
+ const metricAttrs = {
314
+ "gen_ai.system": ctx.provider,
315
+ "gen_ai.operation.name": "chat",
316
+ "gen_ai.request.model": ctx.model
317
+ };
318
+ tokenHistogram.record(usage.promptTokens, {
319
+ ...metricAttrs,
320
+ "gen_ai.token.type": "input"
321
+ });
322
+ tokenHistogram.record(usage.completionTokens, {
323
+ ...metricAttrs,
324
+ "gen_ai.token.type": "output"
325
+ });
326
+ }
327
+ const span = state.currentIterationSpan ?? state.rootSpan;
328
+ span.setAttributes({
329
+ "gen_ai.usage.input_tokens": usage.promptTokens,
330
+ "gen_ai.usage.output_tokens": usage.completionTokens
331
+ });
332
+ });
333
+ },
334
+ onBeforeToolCall(ctx, hookCtx) {
335
+ safeCall("otel.onBeforeToolCall", () => {
336
+ const state = stateByCtx.get(ctx);
337
+ if (!state) return;
338
+ const parent = state.currentIterationSpan ?? state.rootSpan;
339
+ const info = {
340
+ kind: "tool",
341
+ ctx,
342
+ toolName: hookCtx.toolName,
343
+ toolCallId: hookCtx.toolCallId,
344
+ iteration: state.iterationCount - 1
345
+ };
346
+ const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `execute_tool ${hookCtx.toolName}`;
347
+ const baseAttrs = {
348
+ "gen_ai.tool.name": hookCtx.toolName,
349
+ "gen_ai.tool.call.id": hookCtx.toolCallId,
350
+ "gen_ai.tool.type": "function"
351
+ };
352
+ const baseOptions = {
353
+ kind: SpanKind.INTERNAL,
354
+ attributes: baseAttrs
355
+ };
356
+ const spanOptions = safeCall(
357
+ "otel.onBeforeSpanStart",
358
+ () => onBeforeSpanStart?.(info, baseOptions)
359
+ ) ?? baseOptions;
360
+ const parentCtx = trace.setSpan(context.active(), parent);
361
+ let toolSpan;
362
+ context.with(parentCtx, () => {
363
+ toolSpan = tracer.startSpan(name, spanOptions, parentCtx);
364
+ });
365
+ const enriched = safeCall(
366
+ "otel.attributeEnricher",
367
+ () => attributeEnricher?.(info)
368
+ );
369
+ if (enriched) toolSpan.setAttributes(enriched);
370
+ if (captureContent) {
371
+ const argsBody = typeof hookCtx.args === "string" ? hookCtx.args : safeCall(
372
+ "otel.serializeToolArgs",
373
+ () => JSON.stringify(hookCtx.args ?? null)
374
+ ) ?? "[unserializable_tool_args]";
375
+ const redactedArgs = redactContent(argsBody);
376
+ const toolInputJson = JSON.stringify([
377
+ { role: "tool", content: redactedArgs }
378
+ ]);
379
+ toolSpan.setAttribute("gen_ai.input.messages", toolInputJson);
380
+ toolSpan.setAttribute("langfuse.observation.input", toolInputJson);
381
+ }
382
+ state.toolSpans.set(hookCtx.toolCallId, {
383
+ span: toolSpan,
384
+ toolName: hookCtx.toolName
385
+ });
386
+ });
387
+ return void 0;
388
+ },
389
+ onAfterToolCall(ctx, info) {
390
+ safeCall("otel.onAfterToolCall", () => {
391
+ const state = stateByCtx.get(ctx);
392
+ if (!state) return;
393
+ const entry = state.toolSpans.get(info.toolCallId);
394
+ if (!entry) return;
395
+ const { span: toolSpan } = entry;
396
+ const outcome = info.ok ? "success" : "error";
397
+ toolSpan.setAttribute("tanstack.ai.tool.outcome", outcome);
398
+ if (!info.ok && info.error !== void 0) {
399
+ toolSpan.recordException(info.error);
400
+ toolSpan.setStatus({
401
+ code: SpanStatusCode.ERROR,
402
+ message: errorMessage(info.error)
403
+ });
404
+ }
405
+ if (captureContent) {
406
+ const body = typeof info.result === "string" ? info.result : safeCall(
407
+ "otel.serializeToolResult",
408
+ () => JSON.stringify(info.result ?? null)
409
+ ) ?? "[unserializable_tool_result]";
410
+ const redactedBody = redactContent(body);
411
+ if (state.currentIterationSpan) {
412
+ state.currentIterationSpan.addEvent("gen_ai.tool.message", {
413
+ content: redactedBody,
414
+ tool_call_id: info.toolCallId
415
+ });
416
+ }
417
+ const toolOutputJson = JSON.stringify([
418
+ { role: "tool", content: redactedBody }
419
+ ]);
420
+ toolSpan.setAttribute("gen_ai.output.messages", toolOutputJson);
421
+ toolSpan.setAttribute("langfuse.observation.output", toolOutputJson);
422
+ }
423
+ safeCall(
424
+ "otel.onSpanEnd",
425
+ () => onSpanEnd?.(
426
+ {
427
+ kind: "tool",
428
+ ctx,
429
+ toolName: info.toolName,
430
+ toolCallId: info.toolCallId,
431
+ iteration: state.iterationCount - 1
432
+ },
433
+ toolSpan
434
+ )
435
+ );
436
+ toolSpan.end();
437
+ state.toolSpans.delete(info.toolCallId);
438
+ });
439
+ },
440
+ onError(ctx, info) {
441
+ safeCall("otel.onError", () => {
442
+ const state = stateByCtx.get(ctx);
443
+ if (!state) return;
444
+ const errType = errorTypeName(info.error);
445
+ const message = errorMessage(info.error);
446
+ const exception = info.error;
447
+ if (state.currentIterationSpan) {
448
+ state.currentIterationSpan.recordException(exception);
449
+ state.currentIterationSpan.setStatus({
450
+ code: SpanStatusCode.ERROR,
451
+ message
452
+ });
453
+ safeCall(
454
+ "otel.onSpanEnd",
455
+ () => onSpanEnd?.(
456
+ {
457
+ kind: "iteration",
458
+ ctx,
459
+ iteration: state.iterationCount - 1
460
+ },
461
+ state.currentIterationSpan
462
+ )
463
+ );
464
+ state.currentIterationSpan.end();
465
+ state.currentIterationSpan = null;
466
+ }
467
+ for (const [id, entry] of state.toolSpans) {
468
+ const { span, toolName } = entry;
469
+ span.recordException(exception);
470
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
471
+ safeCall(
472
+ "otel.onSpanEnd",
473
+ () => onSpanEnd?.(
474
+ {
475
+ kind: "tool",
476
+ ctx,
477
+ toolCallId: id,
478
+ toolName,
479
+ iteration: state.iterationCount - 1
480
+ },
481
+ span
482
+ )
483
+ );
484
+ span.end();
485
+ state.toolSpans.delete(id);
486
+ }
487
+ state.rootSpan.recordException(exception);
488
+ state.rootSpan.setStatus({ code: SpanStatusCode.ERROR, message });
489
+ if (durationHistogram) {
490
+ durationHistogram.record(info.duration / 1e3, {
491
+ "gen_ai.system": ctx.provider,
492
+ "gen_ai.operation.name": "chat",
493
+ "gen_ai.request.model": ctx.model,
494
+ "error.type": errType
495
+ });
496
+ }
497
+ safeCall(
498
+ "otel.onSpanEnd",
499
+ () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
500
+ );
501
+ state.rootSpan.end();
502
+ stateByCtx.delete(ctx);
503
+ });
504
+ },
505
+ onAbort(ctx, info) {
506
+ safeCall("otel.onAbort", () => {
507
+ const state = stateByCtx.get(ctx);
508
+ if (!state) return;
509
+ const closeCancelled = (span) => {
510
+ span.setAttribute("tanstack.ai.completion.reason", "cancelled");
511
+ span.setStatus({ code: SpanStatusCode.ERROR, message: "cancelled" });
512
+ };
513
+ if (state.currentIterationSpan) {
514
+ closeCancelled(state.currentIterationSpan);
515
+ safeCall(
516
+ "otel.onSpanEnd",
517
+ () => onSpanEnd?.(
518
+ {
519
+ kind: "iteration",
520
+ ctx,
521
+ iteration: state.iterationCount - 1
522
+ },
523
+ state.currentIterationSpan
524
+ )
525
+ );
526
+ state.currentIterationSpan.end();
527
+ state.currentIterationSpan = null;
528
+ }
529
+ for (const [id, entry] of state.toolSpans) {
530
+ const { span, toolName } = entry;
531
+ closeCancelled(span);
532
+ safeCall(
533
+ "otel.onSpanEnd",
534
+ () => onSpanEnd?.(
535
+ {
536
+ kind: "tool",
537
+ ctx,
538
+ toolCallId: id,
539
+ toolName,
540
+ iteration: state.iterationCount - 1
541
+ },
542
+ span
543
+ )
544
+ );
545
+ span.end();
546
+ state.toolSpans.delete(id);
547
+ }
548
+ closeCancelled(state.rootSpan);
549
+ if (durationHistogram) {
550
+ durationHistogram.record(info.duration / 1e3, {
551
+ "gen_ai.system": ctx.provider,
552
+ "gen_ai.operation.name": "chat",
553
+ "gen_ai.request.model": ctx.model,
554
+ "error.type": "cancelled"
555
+ });
556
+ }
557
+ safeCall(
558
+ "otel.onSpanEnd",
559
+ () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
560
+ );
561
+ state.rootSpan.end();
562
+ stateByCtx.delete(ctx);
563
+ });
564
+ },
565
+ onFinish(ctx, info) {
566
+ safeCall("otel.onFinish", () => {
567
+ const state = stateByCtx.get(ctx);
568
+ if (!state) return;
569
+ for (const [id, entry] of state.toolSpans) {
570
+ const { span, toolName } = entry;
571
+ span.setAttribute("tanstack.ai.tool.outcome", "unknown");
572
+ safeCall(
573
+ "otel.onSpanEnd",
574
+ () => onSpanEnd?.(
575
+ {
576
+ kind: "tool",
577
+ ctx,
578
+ toolCallId: id,
579
+ toolName,
580
+ iteration: state.iterationCount - 1
581
+ },
582
+ span
583
+ )
584
+ );
585
+ span.end();
586
+ state.toolSpans.delete(id);
587
+ }
588
+ closeIterationSpan(state, ctx);
589
+ if (durationHistogram) {
590
+ durationHistogram.record(info.duration / 1e3, {
591
+ "gen_ai.system": ctx.provider,
592
+ "gen_ai.operation.name": "chat",
593
+ "gen_ai.request.model": ctx.model
594
+ });
595
+ }
596
+ if (info.usage) {
597
+ state.rootSpan.setAttributes({
598
+ "gen_ai.usage.input_tokens": info.usage.promptTokens,
599
+ "gen_ai.usage.output_tokens": info.usage.completionTokens
600
+ });
601
+ }
602
+ if (info.finishReason) {
603
+ state.rootSpan.setAttribute("gen_ai.response.finish_reasons", [
604
+ info.finishReason
605
+ ]);
606
+ }
607
+ state.rootSpan.setAttribute(
608
+ "tanstack.ai.iterations",
609
+ state.iterationCount
610
+ );
611
+ safeCall(
612
+ "otel.onSpanEnd",
613
+ () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
614
+ );
615
+ state.rootSpan.end();
616
+ stateByCtx.delete(ctx);
617
+ });
618
+ }
619
+ };
620
+ }
621
+ export {
622
+ otelMiddleware
623
+ };
624
+ //# sourceMappingURL=otel.js.map