@tanstack/ai 0.13.0 → 0.15.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 (81) hide show
  1. package/dist/esm/activities/chat/index.js +61 -9
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/messages.js +22 -1
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/chat/stream/message-updaters.d.ts +3 -2
  6. package/dist/esm/activities/chat/stream/message-updaters.js +7 -3
  7. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
  9. package/dist/esm/activities/chat/stream/processor.js +107 -15
  10. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  11. package/dist/esm/activities/chat/stream/types.d.ts +4 -1
  12. package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  14. package/dist/esm/activities/error-payload.d.ts +12 -0
  15. package/dist/esm/activities/error-payload.js +25 -0
  16. package/dist/esm/activities/error-payload.js.map +1 -0
  17. package/dist/esm/activities/generateAudio/adapter.d.ts +62 -0
  18. package/dist/esm/activities/generateAudio/adapter.js +14 -0
  19. package/dist/esm/activities/generateAudio/adapter.js.map +1 -0
  20. package/dist/esm/activities/generateAudio/index.d.ts +74 -0
  21. package/dist/esm/activities/generateAudio/index.js +80 -0
  22. package/dist/esm/activities/generateAudio/index.js.map +1 -0
  23. package/dist/esm/activities/generateImage/adapter.d.ts +1 -1
  24. package/dist/esm/activities/generateImage/adapter.js +1 -1
  25. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  26. package/dist/esm/activities/generateSpeech/adapter.d.ts +1 -1
  27. package/dist/esm/activities/generateSpeech/adapter.js +1 -1
  28. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  29. package/dist/esm/activities/generateSpeech/index.d.ts +3 -3
  30. package/dist/esm/activities/generateSpeech/index.js +11 -0
  31. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  32. package/dist/esm/activities/generateTranscription/adapter.d.ts +1 -1
  33. package/dist/esm/activities/generateTranscription/adapter.js +1 -1
  34. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  35. package/dist/esm/activities/generateTranscription/index.d.ts +3 -3
  36. package/dist/esm/activities/generateTranscription/index.js +11 -0
  37. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  38. package/dist/esm/activities/generateVideo/index.js +7 -7
  39. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  40. package/dist/esm/activities/index.d.ts +5 -2
  41. package/dist/esm/activities/index.js +11 -6
  42. package/dist/esm/activities/index.js.map +1 -1
  43. package/dist/esm/activities/stream-generation-result.js +5 -6
  44. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  45. package/dist/esm/adapter-internals.d.ts +1 -0
  46. package/dist/esm/adapter-internals.js +3 -1
  47. package/dist/esm/adapter-internals.js.map +1 -1
  48. package/dist/esm/index.d.ts +3 -2
  49. package/dist/esm/index.js +3 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/middlewares/otel.d.ts +75 -0
  52. package/dist/esm/middlewares/otel.js +624 -0
  53. package/dist/esm/middlewares/otel.js.map +1 -0
  54. package/dist/esm/stream-to-response.js +3 -8
  55. package/dist/esm/stream-to-response.js.map +1 -1
  56. package/dist/esm/types.d.ts +71 -6
  57. package/package.json +15 -2
  58. package/skills/ai-core/media-generation/SKILL.md +154 -10
  59. package/src/activities/chat/index.ts +74 -11
  60. package/src/activities/chat/messages.ts +23 -1
  61. package/src/activities/chat/stream/message-updaters.ts +10 -3
  62. package/src/activities/chat/stream/processor.ts +140 -17
  63. package/src/activities/chat/stream/types.ts +4 -1
  64. package/src/activities/chat/tools/tool-calls.ts +3 -1
  65. package/src/activities/error-payload.ts +35 -0
  66. package/src/activities/generateAudio/adapter.ts +89 -0
  67. package/src/activities/generateAudio/index.ts +224 -0
  68. package/src/activities/generateImage/adapter.ts +1 -1
  69. package/src/activities/generateSpeech/adapter.ts +1 -1
  70. package/src/activities/generateSpeech/index.ts +17 -7
  71. package/src/activities/generateTranscription/adapter.ts +1 -1
  72. package/src/activities/generateTranscription/index.ts +27 -4
  73. package/src/activities/generateVideo/index.ts +8 -8
  74. package/src/activities/index.ts +22 -0
  75. package/src/activities/stream-generation-result.ts +6 -7
  76. package/src/adapter-internals.ts +1 -0
  77. package/src/index.ts +4 -0
  78. package/src/middlewares/index.ts +5 -0
  79. package/src/middlewares/otel.ts +861 -0
  80. package/src/stream-to-response.ts +5 -10
  81. package/src/types.ts +79 -5
@@ -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