@tanstack/ai 0.32.0 → 0.34.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 (64) hide show
  1. package/dist/esm/activities/chat/index.js +47 -20
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
  4. package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
  5. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
  6. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  7. package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -0
  8. package/dist/esm/activities/chat/tools/schema-converter.js +61 -33
  9. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  10. package/dist/esm/activities/generateAudio/index.d.ts +7 -0
  11. package/dist/esm/activities/generateAudio/index.js +26 -1
  12. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  13. package/dist/esm/activities/generateImage/index.d.ts +7 -0
  14. package/dist/esm/activities/generateImage/index.js +26 -1
  15. package/dist/esm/activities/generateImage/index.js.map +1 -1
  16. package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
  17. package/dist/esm/activities/generateSpeech/index.js +26 -1
  18. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  19. package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
  20. package/dist/esm/activities/generateTranscription/index.js +26 -1
  21. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  22. package/dist/esm/activities/generateVideo/index.d.ts +9 -0
  23. package/dist/esm/activities/generateVideo/index.js +52 -2
  24. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  25. package/dist/esm/activities/middleware/index.d.ts +2 -0
  26. package/dist/esm/activities/middleware/run.d.ts +20 -0
  27. package/dist/esm/activities/middleware/run.js +42 -0
  28. package/dist/esm/activities/middleware/run.js.map +1 -0
  29. package/dist/esm/activities/middleware/types.d.ts +118 -0
  30. package/dist/esm/index.d.ts +2 -0
  31. package/dist/esm/index.js +2 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/middlewares/otel.d.ts +8 -2
  34. package/dist/esm/middlewares/otel.js +145 -95
  35. package/dist/esm/middlewares/otel.js.map +1 -1
  36. package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
  37. package/dist/esm/middlewares/usage-attributes.js +43 -0
  38. package/dist/esm/middlewares/usage-attributes.js.map +1 -0
  39. package/dist/esm/types.d.ts +7 -7
  40. package/dist/esm/utilities/errors.d.ts +13 -0
  41. package/dist/esm/utilities/errors.js +22 -0
  42. package/dist/esm/utilities/errors.js.map +1 -0
  43. package/dist/esm/utilities/numbers.d.ts +8 -0
  44. package/dist/esm/utilities/numbers.js +12 -0
  45. package/dist/esm/utilities/numbers.js.map +1 -0
  46. package/package.json +3 -2
  47. package/src/activities/chat/index.ts +125 -35
  48. package/src/activities/chat/middleware/types.ts +7 -0
  49. package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
  50. package/src/activities/chat/tools/schema-converter.ts +146 -93
  51. package/src/activities/generateAudio/index.ts +42 -1
  52. package/src/activities/generateImage/index.ts +42 -1
  53. package/src/activities/generateSpeech/index.ts +42 -1
  54. package/src/activities/generateTranscription/index.ts +42 -1
  55. package/src/activities/generateVideo/index.ts +88 -2
  56. package/src/activities/middleware/index.ts +20 -0
  57. package/src/activities/middleware/run.ts +88 -0
  58. package/src/activities/middleware/types.ts +173 -0
  59. package/src/index.ts +19 -0
  60. package/src/middlewares/otel.ts +195 -120
  61. package/src/middlewares/usage-attributes.ts +65 -0
  62. package/src/types.ts +7 -7
  63. package/src/utilities/errors.ts +29 -0
  64. package/src/utilities/numbers.ts +15 -0
@@ -1,5 +1,16 @@
1
- import { SpanStatusCode, SpanKind, trace, context } from "@opentelemetry/api";
1
+ import { SpanKind, SpanStatusCode, trace, context } from "@opentelemetry/api";
2
2
  import { MAX_TOKENS_KEYS, NESTED_MAX_TOKENS_KEY } from "../utilities/sampling-keys.js";
3
+ import { firstNumber } from "../utilities/numbers.js";
4
+ import { errorMessage, errorTypeName } from "../utilities/errors.js";
5
+ import { usageAttributes } from "./usage-attributes.js";
6
+ const OPERATION_NAME = {
7
+ chat: "chat",
8
+ image: "image_generation",
9
+ video: "video_generation",
10
+ audio: "audio_generation",
11
+ tts: "text_to_speech",
12
+ transcription: "transcription"
13
+ };
3
14
  const stateByCtx = /* @__PURE__ */ new WeakMap();
4
15
  const DEFAULT_MAX_CONTENT_LENGTH = 1e5;
5
16
  const REDACTION_FAILED_SENTINEL = "[redaction_failed]";
@@ -51,68 +62,6 @@ function messageEventName(role) {
51
62
  return `gen_ai.${role}.message`;
52
63
  }
53
64
  }
54
- function firstNumber(...candidates) {
55
- for (const candidate of candidates) {
56
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
57
- return candidate;
58
- }
59
- }
60
- return void 0;
61
- }
62
- function usageAttributes(usage) {
63
- const attrs = {
64
- "gen_ai.usage.input_tokens": usage.promptTokens,
65
- "gen_ai.usage.output_tokens": usage.completionTokens
66
- };
67
- const optional = [
68
- ["gen_ai.usage.total_tokens", usage.totalTokens],
69
- ["gen_ai.usage.cost", usage.cost],
70
- [
71
- "gen_ai.usage.cache_read.input_tokens",
72
- usage.promptTokensDetails?.cachedTokens
73
- ],
74
- [
75
- "gen_ai.usage.cache_creation.input_tokens",
76
- usage.promptTokensDetails?.cacheWriteTokens
77
- ],
78
- [
79
- "gen_ai.usage.reasoning.output_tokens",
80
- usage.completionTokensDetails?.reasoningTokens
81
- ],
82
- ["tanstack.ai.usage.duration_seconds", usage.durationSeconds],
83
- ["tanstack.ai.usage.upstream_cost", usage.costDetails?.upstreamCost],
84
- [
85
- "tanstack.ai.usage.upstream_input_cost",
86
- usage.costDetails?.upstreamInputCost
87
- ],
88
- [
89
- "tanstack.ai.usage.upstream_output_cost",
90
- usage.costDetails?.upstreamOutputCost
91
- ]
92
- ];
93
- for (const [key, value] of optional) {
94
- const num = firstNumber(value);
95
- if (num !== void 0) attrs[key] = num;
96
- }
97
- return attrs;
98
- }
99
- function errorMessage(err) {
100
- if (err instanceof Error) return err.message;
101
- if (typeof err === "string") return err;
102
- if (err && typeof err === "object" && "message" in err) {
103
- const m = err.message;
104
- if (typeof m === "string") return m;
105
- }
106
- return void 0;
107
- }
108
- function errorTypeName(err) {
109
- if (err instanceof Error) return err.name || "Error";
110
- if (err && typeof err === "object" && "name" in err) {
111
- const n = err.name;
112
- if (typeof n === "string") return n;
113
- }
114
- return "Error";
115
- }
116
65
  function safeCall(label, fn) {
117
66
  try {
118
67
  return fn();
@@ -180,17 +129,69 @@ function otelMiddleware(options) {
180
129
  span.end();
181
130
  state.currentIterationSpan = null;
182
131
  };
132
+ const mediaSpans = /* @__PURE__ */ new WeakMap();
133
+ const recordMediaDuration = (ctx, durationMs, errorType) => {
134
+ if (!durationHistogram) return;
135
+ durationHistogram.record(durationMs / 1e3, {
136
+ "gen_ai.system": ctx.provider,
137
+ "gen_ai.operation.name": OPERATION_NAME[ctx.activity],
138
+ "gen_ai.request.model": ctx.model,
139
+ ...errorType ? { "error.type": errorType } : {}
140
+ });
141
+ };
142
+ const startMediaSpan = (ctx) => {
143
+ safeCall("otel.onStart", () => {
144
+ const operationName = OPERATION_NAME[ctx.activity];
145
+ const info = { kind: "generation", ctx };
146
+ const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `${operationName} ${ctx.model}`;
147
+ const baseOptions = {
148
+ kind: SpanKind.CLIENT,
149
+ attributes: {
150
+ "gen_ai.system": ctx.provider,
151
+ "gen_ai.operation.name": operationName,
152
+ "gen_ai.request.model": ctx.model
153
+ }
154
+ };
155
+ const spanOptions = safeCall(
156
+ "otel.onBeforeSpanStart",
157
+ () => onBeforeSpanStart?.(info, baseOptions)
158
+ ) ?? baseOptions;
159
+ const span = tracer.startSpan(name, spanOptions);
160
+ const enriched = safeCall(
161
+ "otel.attributeEnricher",
162
+ () => attributeEnricher?.(info)
163
+ );
164
+ if (enriched) span.setAttributes(enriched);
165
+ mediaSpans.set(ctx, span);
166
+ });
167
+ };
168
+ const endMediaSpan = (ctx, finalize) => {
169
+ const span = mediaSpans.get(ctx);
170
+ mediaSpans.delete(ctx);
171
+ if (!span) return;
172
+ finalize(span);
173
+ safeCall(
174
+ "otel.onSpanEnd",
175
+ () => onSpanEnd?.({ kind: "generation", ctx }, span)
176
+ );
177
+ span.end();
178
+ };
183
179
  return {
184
180
  name: "otel",
185
181
  onStart(ctx) {
182
+ if (ctx.activity !== "chat") {
183
+ startMediaSpan(ctx);
184
+ return;
185
+ }
186
+ const chatCtx = ctx;
186
187
  safeCall("otel.onStart", () => {
187
- const info = { kind: "chat", ctx };
188
- const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `chat ${ctx.model}`;
188
+ const info = { kind: "chat", ctx: chatCtx };
189
+ const name = safeCall("otel.spanNameFormatter", () => spanNameFormatter?.(info)) ?? `chat ${chatCtx.model}`;
189
190
  const baseOptions = {
190
191
  kind: SpanKind.INTERNAL,
191
192
  attributes: {
192
- "gen_ai.system": ctx.provider,
193
- "gen_ai.request.model": ctx.model
193
+ "gen_ai.system": chatCtx.provider,
194
+ "gen_ai.request.model": chatCtx.model
194
195
  // NOTE: `gen_ai.operation.name` is deliberately NOT set on the
195
196
  // root span. The root represents a `chat()` invocation that may
196
197
  // span multiple model calls; only iteration spans correspond to
@@ -209,14 +210,15 @@ function otelMiddleware(options) {
209
210
  () => attributeEnricher?.(info)
210
211
  );
211
212
  if (enriched) rootSpan.setAttributes(enriched);
212
- stateByCtx.set(ctx, {
213
+ stateByCtx.set(chatCtx, {
213
214
  rootSpan,
214
215
  currentIterationSpan: null,
215
216
  toolSpans: /* @__PURE__ */ new Map(),
216
217
  iterationCount: 0,
217
218
  assistantTextBuffer: "",
218
219
  assistantTextBufferTruncated: false,
219
- startTime: Date.now()
220
+ startTime: Date.now(),
221
+ lastFinishReason: null
220
222
  });
221
223
  });
222
224
  },
@@ -348,6 +350,7 @@ function otelMiddleware(options) {
348
350
  appendAssistantText(state, chunk.delta);
349
351
  }
350
352
  if (chunk.type !== "RUN_FINISHED") return;
353
+ if (chunk.finishReason) state.lastFinishReason = chunk.finishReason;
351
354
  const span = state.currentIterationSpan;
352
355
  if (!span) return;
353
356
  if (chunk.finishReason) {
@@ -376,14 +379,22 @@ function otelMiddleware(options) {
376
379
  return void 0;
377
380
  },
378
381
  onUsage(ctx, usage) {
382
+ if (ctx.activity !== "chat") {
383
+ safeCall("otel.onUsage", () => {
384
+ const span = mediaSpans.get(ctx);
385
+ if (span) span.setAttributes(usageAttributes(usage));
386
+ });
387
+ return;
388
+ }
389
+ const chatCtx = ctx;
379
390
  safeCall("otel.onUsage", () => {
380
- const state = stateByCtx.get(ctx);
391
+ const state = stateByCtx.get(chatCtx);
381
392
  if (!state) return;
382
393
  if (tokenHistogram) {
383
394
  const metricAttrs = {
384
- "gen_ai.system": ctx.provider,
395
+ "gen_ai.system": chatCtx.provider,
385
396
  "gen_ai.operation.name": "chat",
386
- "gen_ai.request.model": ctx.model
397
+ "gen_ai.request.model": chatCtx.model
387
398
  };
388
399
  tokenHistogram.record(usage.promptTokens, {
389
400
  ...metricAttrs,
@@ -506,8 +517,23 @@ function otelMiddleware(options) {
506
517
  });
507
518
  },
508
519
  onError(ctx, info) {
520
+ if (ctx.activity !== "chat") {
521
+ safeCall("otel.onError", () => {
522
+ const message = errorMessage(info.error);
523
+ endMediaSpan(ctx, (span) => {
524
+ span.recordException(info.error);
525
+ span.setStatus({
526
+ code: SpanStatusCode.ERROR,
527
+ ...message !== void 0 ? { message } : {}
528
+ });
529
+ });
530
+ recordMediaDuration(ctx, info.duration, errorTypeName(info.error));
531
+ });
532
+ return;
533
+ }
534
+ const chatCtx = ctx;
509
535
  safeCall("otel.onError", () => {
510
- const state = stateByCtx.get(ctx);
536
+ const state = stateByCtx.get(chatCtx);
511
537
  if (!state) return;
512
538
  const errType = errorTypeName(info.error);
513
539
  const message = errorMessage(info.error);
@@ -525,7 +551,7 @@ function otelMiddleware(options) {
525
551
  () => onSpanEnd?.(
526
552
  {
527
553
  kind: "iteration",
528
- ctx,
554
+ ctx: chatCtx,
529
555
  iteration: state.iterationCount - 1
530
556
  },
531
557
  iterationSpan
@@ -543,7 +569,7 @@ function otelMiddleware(options) {
543
569
  () => onSpanEnd?.(
544
570
  {
545
571
  kind: "tool",
546
- ctx,
572
+ ctx: chatCtx,
547
573
  toolCallId: id,
548
574
  toolName,
549
575
  iteration: state.iterationCount - 1
@@ -561,23 +587,37 @@ function otelMiddleware(options) {
561
587
  });
562
588
  if (durationHistogram) {
563
589
  durationHistogram.record(info.duration / 1e3, {
564
- "gen_ai.system": ctx.provider,
590
+ "gen_ai.system": chatCtx.provider,
565
591
  "gen_ai.operation.name": "chat",
566
- "gen_ai.request.model": ctx.model,
592
+ "gen_ai.request.model": chatCtx.model,
567
593
  "error.type": errType
568
594
  });
569
595
  }
570
596
  safeCall(
571
597
  "otel.onSpanEnd",
572
- () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
598
+ () => onSpanEnd?.({ kind: "chat", ctx: chatCtx }, state.rootSpan)
573
599
  );
574
600
  state.rootSpan.end();
575
- stateByCtx.delete(ctx);
601
+ stateByCtx.delete(chatCtx);
576
602
  });
577
603
  },
578
604
  onAbort(ctx, info) {
605
+ if (ctx.activity !== "chat") {
606
+ safeCall("otel.onAbort", () => {
607
+ endMediaSpan(ctx, (span) => {
608
+ span.setAttribute("tanstack.ai.completion.reason", "cancelled");
609
+ span.setStatus({
610
+ code: SpanStatusCode.ERROR,
611
+ message: info.reason ?? "cancelled"
612
+ });
613
+ });
614
+ recordMediaDuration(ctx, info.duration, "cancelled");
615
+ });
616
+ return;
617
+ }
618
+ const chatCtx = ctx;
579
619
  safeCall("otel.onAbort", () => {
580
- const state = stateByCtx.get(ctx);
620
+ const state = stateByCtx.get(chatCtx);
581
621
  if (!state) return;
582
622
  const closeCancelled = (span) => {
583
623
  span.setAttribute("tanstack.ai.completion.reason", "cancelled");
@@ -591,7 +631,7 @@ function otelMiddleware(options) {
591
631
  () => onSpanEnd?.(
592
632
  {
593
633
  kind: "iteration",
594
- ctx,
634
+ ctx: chatCtx,
595
635
  iteration: state.iterationCount - 1
596
636
  },
597
637
  iterationSpan
@@ -608,7 +648,7 @@ function otelMiddleware(options) {
608
648
  () => onSpanEnd?.(
609
649
  {
610
650
  kind: "tool",
611
- ctx,
651
+ ctx: chatCtx,
612
652
  toolCallId: id,
613
653
  toolName,
614
654
  iteration: state.iterationCount - 1
@@ -622,23 +662,33 @@ function otelMiddleware(options) {
622
662
  closeCancelled(state.rootSpan);
623
663
  if (durationHistogram) {
624
664
  durationHistogram.record(info.duration / 1e3, {
625
- "gen_ai.system": ctx.provider,
665
+ "gen_ai.system": chatCtx.provider,
626
666
  "gen_ai.operation.name": "chat",
627
- "gen_ai.request.model": ctx.model,
667
+ "gen_ai.request.model": chatCtx.model,
628
668
  "error.type": "cancelled"
629
669
  });
630
670
  }
631
671
  safeCall(
632
672
  "otel.onSpanEnd",
633
- () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
673
+ () => onSpanEnd?.({ kind: "chat", ctx: chatCtx }, state.rootSpan)
634
674
  );
635
675
  state.rootSpan.end();
636
- stateByCtx.delete(ctx);
676
+ stateByCtx.delete(chatCtx);
637
677
  });
638
678
  },
639
679
  onFinish(ctx, info) {
680
+ if (ctx.activity !== "chat") {
681
+ safeCall("otel.onFinish", () => {
682
+ endMediaSpan(ctx, (span) => {
683
+ if (info.usage) span.setAttributes(usageAttributes(info.usage));
684
+ });
685
+ recordMediaDuration(ctx, info.duration);
686
+ });
687
+ return;
688
+ }
689
+ const chatCtx = ctx;
640
690
  safeCall("otel.onFinish", () => {
641
- const state = stateByCtx.get(ctx);
691
+ const state = stateByCtx.get(chatCtx);
642
692
  if (!state) return;
643
693
  for (const [id, entry] of state.toolSpans) {
644
694
  const { span, toolName } = entry;
@@ -648,7 +698,7 @@ function otelMiddleware(options) {
648
698
  () => onSpanEnd?.(
649
699
  {
650
700
  kind: "tool",
651
- ctx,
701
+ ctx: chatCtx,
652
702
  toolCallId: id,
653
703
  toolName,
654
704
  iteration: state.iterationCount - 1
@@ -659,20 +709,20 @@ function otelMiddleware(options) {
659
709
  span.end();
660
710
  state.toolSpans.delete(id);
661
711
  }
662
- closeIterationSpan(state, ctx);
712
+ closeIterationSpan(state, chatCtx);
663
713
  if (durationHistogram) {
664
714
  durationHistogram.record(info.duration / 1e3, {
665
- "gen_ai.system": ctx.provider,
715
+ "gen_ai.system": chatCtx.provider,
666
716
  "gen_ai.operation.name": "chat",
667
- "gen_ai.request.model": ctx.model
717
+ "gen_ai.request.model": chatCtx.model
668
718
  });
669
719
  }
670
720
  if (info.usage) {
671
721
  state.rootSpan.setAttributes(usageAttributes(info.usage));
672
722
  }
673
- if (info.finishReason) {
723
+ if (state.lastFinishReason) {
674
724
  state.rootSpan.setAttribute("gen_ai.response.finish_reasons", [
675
- info.finishReason
725
+ state.lastFinishReason
676
726
  ]);
677
727
  }
678
728
  state.rootSpan.setAttribute(
@@ -681,10 +731,10 @@ function otelMiddleware(options) {
681
731
  );
682
732
  safeCall(
683
733
  "otel.onSpanEnd",
684
- () => onSpanEnd?.({ kind: "chat", ctx }, state.rootSpan)
734
+ () => onSpanEnd?.({ kind: "chat", ctx: chatCtx }, state.rootSpan)
685
735
  );
686
736
  state.rootSpan.end();
687
- stateByCtx.delete(ctx);
737
+ stateByCtx.delete(chatCtx);
688
738
  });
689
739
  }
690
740
  };