@tanstack/ai 0.41.0 → 0.43.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 (272) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/activities/chat/adapter.js +23 -16
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +10 -4
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -17
  6. package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
  7. package/dist/esm/activities/chat/cancel.d.ts +40 -0
  8. package/dist/esm/activities/chat/cancel.js +54 -0
  9. package/dist/esm/activities/chat/cancel.js.map +1 -0
  10. package/dist/esm/activities/chat/index.d.ts +28 -16
  11. package/dist/esm/activities/chat/index.js +2100 -1744
  12. package/dist/esm/activities/chat/index.js.map +1 -1
  13. package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
  14. package/dist/esm/activities/chat/mcp/manager.js +90 -77
  15. package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
  16. package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
  17. package/dist/esm/activities/chat/messages.js +397 -346
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/builder.js +17 -15
  20. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  21. package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
  22. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
  23. package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
  24. package/dist/esm/activities/chat/middleware/compose.js +623 -531
  25. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  26. package/dist/esm/activities/chat/middleware/define.js +12 -5
  27. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  28. package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
  29. package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
  30. package/dist/esm/activities/chat/middleware/locks.js +71 -0
  31. package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
  32. package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
  33. package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
  34. package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
  35. package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
  36. package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
  37. package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
  38. package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
  39. package/dist/esm/activities/chat/middleware/run-store.js +176 -0
  40. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
  41. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
  42. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
  43. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
  44. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
  45. package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
  46. package/dist/esm/activities/chat/middleware/validate.js +23 -28
  47. package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
  48. package/dist/esm/activities/chat/stream/json-parser.js +39 -25
  49. package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
  50. package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
  53. package/dist/esm/activities/chat/stream/processor.js +1341 -1542
  54. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  55. package/dist/esm/activities/chat/stream/strategies.js +69 -53
  56. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  57. package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
  58. package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
  59. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  62. package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
  63. package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
  64. package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
  65. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  66. package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
  67. package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
  68. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  69. package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
  70. package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
  71. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  72. package/dist/esm/activities/error-payload.js +85 -47
  73. package/dist/esm/activities/error-payload.js.map +1 -1
  74. package/dist/esm/activities/generateAudio/adapter.js +22 -15
  75. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  76. package/dist/esm/activities/generateAudio/index.d.ts +4 -0
  77. package/dist/esm/activities/generateAudio/index.js +141 -105
  78. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  79. package/dist/esm/activities/generateImage/adapter.js +22 -15
  80. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  81. package/dist/esm/activities/generateImage/index.d.ts +4 -0
  82. package/dist/esm/activities/generateImage/index.js +155 -111
  83. package/dist/esm/activities/generateImage/index.js.map +1 -1
  84. package/dist/esm/activities/generateSpeech/adapter.js +22 -15
  85. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  86. package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
  87. package/dist/esm/activities/generateSpeech/index.js +159 -110
  88. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  89. package/dist/esm/activities/generateTranscription/adapter.js +22 -15
  90. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  91. package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
  92. package/dist/esm/activities/generateTranscription/index.js +159 -100
  93. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  94. package/dist/esm/activities/generateVideo/adapter.js +36 -29
  95. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  96. package/dist/esm/activities/generateVideo/index.d.ts +143 -19
  97. package/dist/esm/activities/generateVideo/index.js +456 -279
  98. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  99. package/dist/esm/activities/generateVideo/snap.js +60 -48
  100. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  101. package/dist/esm/activities/index.js +8 -34
  102. package/dist/esm/activities/middleware/index.d.ts +1 -1
  103. package/dist/esm/activities/middleware/run.d.ts +10 -0
  104. package/dist/esm/activities/middleware/run.js +53 -29
  105. package/dist/esm/activities/middleware/run.js.map +1 -1
  106. package/dist/esm/activities/middleware/types.d.ts +44 -6
  107. package/dist/esm/activities/stream-generation-result.d.ts +4 -1
  108. package/dist/esm/activities/stream-generation-result.js +79 -44
  109. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  110. package/dist/esm/activities/summarize/adapter.js +22 -15
  111. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  112. package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
  113. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  114. package/dist/esm/activities/summarize/index.d.ts +27 -0
  115. package/dist/esm/activities/summarize/index.js +268 -102
  116. package/dist/esm/activities/summarize/index.js.map +1 -1
  117. package/dist/esm/adapter-internals.d.ts +2 -1
  118. package/dist/esm/adapter-internals.js +4 -11
  119. package/dist/esm/client.d.ts +25 -3
  120. package/dist/esm/client.js +131 -64
  121. package/dist/esm/client.js.map +1 -1
  122. package/dist/esm/custom-events.d.ts +76 -0
  123. package/dist/esm/custom-events.js +37 -0
  124. package/dist/esm/custom-events.js.map +1 -0
  125. package/dist/esm/delivery-detach.d.ts +50 -0
  126. package/dist/esm/delivery-detach.js +71 -0
  127. package/dist/esm/delivery-detach.js.map +1 -0
  128. package/dist/esm/delivery-disconnect.d.ts +62 -0
  129. package/dist/esm/delivery-disconnect.js +81 -0
  130. package/dist/esm/delivery-disconnect.js.map +1 -0
  131. package/dist/esm/extend-adapter.js +19 -17
  132. package/dist/esm/extend-adapter.js.map +1 -1
  133. package/dist/esm/index.d.ts +23 -5
  134. package/dist/esm/index.js +30 -97
  135. package/dist/esm/interrupt-resume.d.ts +71 -0
  136. package/dist/esm/interrupt-resume.js +438 -0
  137. package/dist/esm/interrupt-resume.js.map +1 -0
  138. package/dist/esm/interrupt-serialization.d.ts +12 -0
  139. package/dist/esm/interrupt-serialization.js +178 -0
  140. package/dist/esm/interrupt-serialization.js.map +1 -0
  141. package/dist/esm/interrupts.d.ts +84 -0
  142. package/dist/esm/interrupts.js +31 -0
  143. package/dist/esm/interrupts.js.map +1 -0
  144. package/dist/esm/locks.d.ts +10 -0
  145. package/dist/esm/locks.js +2 -0
  146. package/dist/esm/logger/console-logger.js +101 -78
  147. package/dist/esm/logger/console-logger.js.map +1 -1
  148. package/dist/esm/logger/internal-logger.js +104 -89
  149. package/dist/esm/logger/internal-logger.js.map +1 -1
  150. package/dist/esm/logger/resolve.js +54 -49
  151. package/dist/esm/logger/resolve.js.map +1 -1
  152. package/dist/esm/logger/types.d.ts +1 -1
  153. package/dist/esm/middlewares/content-guard.js +142 -148
  154. package/dist/esm/middlewares/content-guard.js.map +1 -1
  155. package/dist/esm/middlewares/index.js +2 -6
  156. package/dist/esm/middlewares/otel.js +598 -732
  157. package/dist/esm/middlewares/otel.js.map +1 -1
  158. package/dist/esm/middlewares/usage-attributes.js +47 -40
  159. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  160. package/dist/esm/realtime/event-emitter.js +24 -25
  161. package/dist/esm/realtime/event-emitter.js.map +1 -1
  162. package/dist/esm/realtime/index.d.ts +5 -9
  163. package/dist/esm/realtime/index.js +29 -6
  164. package/dist/esm/realtime/index.js.map +1 -1
  165. package/dist/esm/scope.d.ts +47 -0
  166. package/dist/esm/stream-durability.d.ts +171 -0
  167. package/dist/esm/stream-durability.js +295 -0
  168. package/dist/esm/stream-durability.js.map +1 -0
  169. package/dist/esm/stream-to-response.d.ts +178 -13
  170. package/dist/esm/stream-to-response.js +663 -115
  171. package/dist/esm/stream-to-response.js.map +1 -1
  172. package/dist/esm/strip-to-spec-middleware.js +30 -16
  173. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  174. package/dist/esm/system-prompts.js +27 -21
  175. package/dist/esm/system-prompts.js.map +1 -1
  176. package/dist/esm/tool-registry.js +72 -45
  177. package/dist/esm/tool-registry.js.map +1 -1
  178. package/dist/esm/tools/provider-tool.js +14 -5
  179. package/dist/esm/tools/provider-tool.js.map +1 -1
  180. package/dist/esm/types.d.ts +332 -21
  181. package/dist/esm/types.js +2 -0
  182. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  183. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  184. package/dist/esm/utilities/chat-params.d.ts +26 -4
  185. package/dist/esm/utilities/chat-params.js +218 -92
  186. package/dist/esm/utilities/chat-params.js.map +1 -1
  187. package/dist/esm/utilities/errors.js +28 -18
  188. package/dist/esm/utilities/errors.js.map +1 -1
  189. package/dist/esm/utilities/media-prompt.js +46 -41
  190. package/dist/esm/utilities/media-prompt.js.map +1 -1
  191. package/dist/esm/utilities/numbers.js +13 -10
  192. package/dist/esm/utilities/numbers.js.map +1 -1
  193. package/dist/esm/utilities/provider-executed.js +20 -11
  194. package/dist/esm/utilities/provider-executed.js.map +1 -1
  195. package/dist/esm/utilities/sampling-keys.js +31 -19
  196. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  197. package/dist/esm/utilities/tool-result.js +42 -30
  198. package/dist/esm/utilities/tool-result.js.map +1 -1
  199. package/dist/esm/utilities/usage.js +27 -9
  200. package/dist/esm/utilities/usage.js.map +1 -1
  201. package/dist/esm/utils.js +26 -18
  202. package/dist/esm/utils.js.map +1 -1
  203. package/package.json +10 -6
  204. package/skills/ai-core/SKILL.md +69 -18
  205. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  206. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  207. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  208. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  209. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  211. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  212. package/skills/ai-core/chat-experience/SKILL.md +156 -11
  213. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  214. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  215. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  216. package/skills/ai-core/locks/SKILL.md +143 -0
  217. package/skills/ai-core/media-generation/SKILL.md +144 -12
  218. package/skills/ai-core/middleware/SKILL.md +258 -33
  219. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  220. package/skills/ai-core/tool-calling/SKILL.md +54 -59
  221. package/src/activities/chat/agent-loop-strategies.ts +10 -4
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1152 -153
  224. package/src/activities/chat/mcp/manager.ts +4 -4
  225. package/src/activities/chat/mcp/types.ts +2 -2
  226. package/src/activities/chat/messages.ts +5 -3
  227. package/src/activities/chat/middleware/builder.ts +1 -1
  228. package/src/activities/chat/middleware/compose.ts +186 -9
  229. package/src/activities/chat/middleware/index.ts +26 -0
  230. package/src/activities/chat/middleware/locks.ts +102 -0
  231. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  232. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  233. package/src/activities/chat/middleware/run-store.ts +412 -0
  234. package/src/activities/chat/middleware/types.ts +62 -1
  235. package/src/activities/chat/stream/processor.ts +189 -5
  236. package/src/activities/chat/tools/approval-schema.ts +205 -0
  237. package/src/activities/chat/tools/tool-calls.ts +106 -13
  238. package/src/activities/chat/tools/tool-definition.ts +210 -39
  239. package/src/activities/generateAudio/index.ts +20 -3
  240. package/src/activities/generateImage/index.ts +20 -3
  241. package/src/activities/generateSpeech/index.ts +25 -3
  242. package/src/activities/generateTranscription/index.ts +26 -3
  243. package/src/activities/generateVideo/index.ts +345 -82
  244. package/src/activities/middleware/index.ts +2 -0
  245. package/src/activities/middleware/run.ts +31 -0
  246. package/src/activities/middleware/types.ts +49 -5
  247. package/src/activities/stream-generation-result.ts +30 -2
  248. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  249. package/src/activities/summarize/index.ts +200 -10
  250. package/src/adapter-internals.ts +10 -1
  251. package/src/client.ts +244 -0
  252. package/src/custom-events.ts +107 -0
  253. package/src/delivery-detach.ts +72 -0
  254. package/src/delivery-disconnect.ts +84 -0
  255. package/src/index.ts +138 -0
  256. package/src/interrupt-resume.ts +824 -0
  257. package/src/interrupt-serialization.ts +183 -0
  258. package/src/interrupts.ts +146 -0
  259. package/src/locks.ts +17 -0
  260. package/src/logger/types.ts +1 -1
  261. package/src/middlewares/otel.ts +1 -0
  262. package/src/realtime/index.ts +5 -9
  263. package/src/scope.ts +47 -0
  264. package/src/stream-durability.ts +598 -0
  265. package/src/stream-to-response.ts +1051 -95
  266. package/src/strip-to-spec-middleware.ts +3 -3
  267. package/src/types.ts +416 -24
  268. package/src/utilities/chat-params.ts +245 -55
  269. package/dist/esm/activities/index.js.map +0 -1
  270. package/dist/esm/adapter-internals.js.map +0 -1
  271. package/dist/esm/index.js.map +0 -1
  272. package/dist/esm/middlewares/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport {\n MAX_TOKENS_KEYS,\n NESTED_MAX_TOKENS_KEY,\n} from '../utilities/sampling-keys'\nimport { firstNumber } from '../utilities/numbers'\nimport { errorMessage, errorTypeName } from '../utilities/errors'\nimport { usageAttributes } from './usage-attributes'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type {\n GenerationActivity,\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../activities/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n * - `generation` — the single span for a media activity call\n * (`generateImage`, `generateVideo`, `generateSpeech`, …)\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool' | 'generation'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : TScope extends 'generation'\n ? { kind: 'generation'; ctx: GenerationMiddlewareContext }\n : never\n\n/**\n * `gen_ai.operation.name` per activity. Chat uses the GenAI semconv value;\n * media operations have no semconv entry yet, so these are the de-facto names\n * consumed by GenAI backends (PostHog, Langfuse, …). Documented in\n * `docs/advanced/otel.md`.\n */\nconst OPERATION_NAME: Record<GenerationActivity, string> = {\n chat: 'chat',\n image: 'image_generation',\n video: 'video_generation',\n audio: 'audio_generation',\n tts: 'text_to_speech',\n transcription: 'transcription',\n}\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n /**\n * Finish reason from the most recent `RUN_FINISHED` chunk. Captured in\n * `onChunk` so `onFinish` can stamp it on the root span without reading it\n * from the (base-shaped) finish info, which doesn't carry it.\n */\n lastFinishReason: string | null\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(\n options: OtelMiddlewareOptions,\n): GenerationMiddleware & ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n // --- Media activities -----------------------------------------------------\n // Media calls (image/video/audio/tts/transcription) are single request →\n // response, so they get exactly one CLIENT span — opened in `onStart` and\n // closed in the terminal hook. Keyed by the per-call context object, which is\n // distinct from the chat state map above so the two paths never collide.\n const mediaSpans = new WeakMap<GenerationMiddlewareContext, Span>()\n\n const recordMediaDuration = (\n ctx: GenerationMiddlewareContext,\n durationMs: number,\n errorType?: string,\n ): void => {\n if (!durationHistogram) return\n durationHistogram.record(durationMs / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': OPERATION_NAME[ctx.activity],\n 'gen_ai.request.model': ctx.model,\n ...(errorType ? { 'error.type': errorType } : {}),\n })\n }\n\n const startMediaSpan = (ctx: GenerationMiddlewareContext): void => {\n safeCall('otel.onStart', () => {\n const operationName = OPERATION_NAME[ctx.activity]\n const info: OtelSpanInfo<'generation'> = { kind: 'generation', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `${operationName} ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': operationName,\n 'gen_ai.request.model': ctx.model,\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const span = tracer.startSpan(name, spanOptions)\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) span.setAttributes(enriched)\n mediaSpans.set(ctx, span)\n })\n }\n\n const endMediaSpan = (\n ctx: GenerationMiddlewareContext,\n finalize: (span: Span) => void,\n ): void => {\n const span = mediaSpans.get(ctx)\n mediaSpans.delete(ctx)\n if (!span) return\n finalize(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'generation', ctx }, span),\n )\n span.end()\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n // Media activities get one CLIENT span; chat builds the root/iteration\n // tree below. The cast is sound: the chat runner only ever passes a\n // ChatMiddlewareContext, which `activity: 'chat'` narrows to at runtime.\n if (ctx.activity !== 'chat') {\n startMediaSpan(ctx)\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx: chatCtx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${chatCtx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.request.model': chatCtx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(chatCtx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n lastFinishReason: null,\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n // Sampling options now live in provider-native `modelOptions`, and\n // providers spell them differently (e.g. `max_output_tokens`,\n // `max_completion_tokens`, `maxOutputTokens`, `num_predict`). Read the\n // first numeric value among the known spellings — including Ollama's\n // nested `options` — so gen_ai attributes populate across providers.\n const sampling = config.modelOptions ?? {}\n const nestedOptions =\n sampling['options'] && typeof sampling['options'] === 'object'\n ? (sampling['options'] as Record<string, unknown>)\n : undefined\n const samplingTemperature = firstNumber(\n sampling['temperature'],\n nestedOptions?.['temperature'],\n )\n const samplingTopP = firstNumber(\n sampling['top_p'],\n sampling['topP'],\n nestedOptions?.['top_p'],\n )\n // Spellings come from the shared `MAX_TOKENS_KEYS` table so this stays\n // in lockstep with the summarize wrapper's caller-limit detection.\n const samplingMaxTokens = firstNumber(\n ...MAX_TOKENS_KEYS.map((k) => sampling[k]),\n nestedOptions?.[NESTED_MAX_TOKENS_KEY],\n )\n if (samplingTemperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = samplingTemperature\n if (samplingTopP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = samplingTopP\n if (samplingMaxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = samplingMaxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (chunk.finishReason) state.lastFinishReason = chunk.finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes(usageAttributes(chunk.usage))\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n if (ctx.activity !== 'chat') {\n // Media: stamp usage on the single span. No token histogram — media\n // unit billing lands as span attributes via usageAttributes, matching\n // prior media behavior and avoiding chat-shaped token metrics.\n safeCall('otel.onUsage', () => {\n const span = mediaSpans.get(ctx)\n if (span) span.setAttributes(usageAttributes(usage))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes(usageAttributes(usage))\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onError', () => {\n const message = errorMessage(info.error)\n endMediaSpan(ctx, (span) => {\n span.recordException(info.error as Exception)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(message !== undefined ? { message } : {}),\n })\n })\n recordMediaDuration(ctx, info.duration, errorTypeName(info.error))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onAbort(ctx, info) {\n if (ctx.activity !== 'chat') {\n // Media abandonment (e.g. a video stream dropped before completion).\n safeCall('otel.onAbort', () => {\n endMediaSpan(ctx, (span) => {\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: info.reason ?? 'cancelled',\n })\n })\n recordMediaDuration(ctx, info.duration, 'cancelled')\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onFinish(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onFinish', () => {\n endMediaSpan(ctx, (span) => {\n if (info.usage) span.setAttributes(usageAttributes(info.usage))\n })\n recordMediaDuration(ctx, info.duration)\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, chatCtx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes(usageAttributes(info.usage))\n }\n if (state.lastFinishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n state.lastFinishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
1
+ {"version":3,"file":"otel.js","names":[],"sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport {\n MAX_TOKENS_KEYS,\n NESTED_MAX_TOKENS_KEY,\n} from '../utilities/sampling-keys'\nimport { firstNumber } from '../utilities/numbers'\nimport { errorMessage, errorTypeName } from '../utilities/errors'\nimport { usageAttributes } from './usage-attributes'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type {\n GenerationActivity,\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../activities/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n * - `generation` — the single span for a media activity call\n * (`generateImage`, `generateVideo`, `generateSpeech`, …)\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool' | 'generation'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : TScope extends 'generation'\n ? { kind: 'generation'; ctx: GenerationMiddlewareContext }\n : never\n\n/**\n * `gen_ai.operation.name` per activity. Chat uses the GenAI semconv value;\n * media operations have no semconv entry yet, so these are the de-facto names\n * consumed by GenAI backends (PostHog, Langfuse, …). Documented in\n * `docs/advanced/otel.md`.\n */\nconst OPERATION_NAME: Record<GenerationActivity, string> = {\n chat: 'chat',\n image: 'image_generation',\n video: 'video_generation',\n audio: 'audio_generation',\n tts: 'text_to_speech',\n transcription: 'transcription',\n summarize: 'summarize',\n}\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n /**\n * Finish reason from the most recent `RUN_FINISHED` chunk. Captured in\n * `onChunk` so `onFinish` can stamp it on the root span without reading it\n * from the (base-shaped) finish info, which doesn't carry it.\n */\n lastFinishReason: string | null\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(\n options: OtelMiddlewareOptions,\n): GenerationMiddleware & ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n // --- Media activities -----------------------------------------------------\n // Media calls (image/video/audio/tts/transcription) are single request →\n // response, so they get exactly one CLIENT span — opened in `onStart` and\n // closed in the terminal hook. Keyed by the per-call context object, which is\n // distinct from the chat state map above so the two paths never collide.\n const mediaSpans = new WeakMap<GenerationMiddlewareContext, Span>()\n\n const recordMediaDuration = (\n ctx: GenerationMiddlewareContext,\n durationMs: number,\n errorType?: string,\n ): void => {\n if (!durationHistogram) return\n durationHistogram.record(durationMs / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': OPERATION_NAME[ctx.activity],\n 'gen_ai.request.model': ctx.model,\n ...(errorType ? { 'error.type': errorType } : {}),\n })\n }\n\n const startMediaSpan = (ctx: GenerationMiddlewareContext): void => {\n safeCall('otel.onStart', () => {\n const operationName = OPERATION_NAME[ctx.activity]\n const info: OtelSpanInfo<'generation'> = { kind: 'generation', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `${operationName} ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': operationName,\n 'gen_ai.request.model': ctx.model,\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const span = tracer.startSpan(name, spanOptions)\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) span.setAttributes(enriched)\n mediaSpans.set(ctx, span)\n })\n }\n\n const endMediaSpan = (\n ctx: GenerationMiddlewareContext,\n finalize: (span: Span) => void,\n ): void => {\n const span = mediaSpans.get(ctx)\n mediaSpans.delete(ctx)\n if (!span) return\n finalize(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'generation', ctx }, span),\n )\n span.end()\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n // Media activities get one CLIENT span; chat builds the root/iteration\n // tree below. The cast is sound: the chat runner only ever passes a\n // ChatMiddlewareContext, which `activity: 'chat'` narrows to at runtime.\n if (ctx.activity !== 'chat') {\n startMediaSpan(ctx)\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx: chatCtx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${chatCtx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.request.model': chatCtx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(chatCtx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n lastFinishReason: null,\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n // Sampling options now live in provider-native `modelOptions`, and\n // providers spell them differently (e.g. `max_output_tokens`,\n // `max_completion_tokens`, `maxOutputTokens`, `num_predict`). Read the\n // first numeric value among the known spellings — including Ollama's\n // nested `options` — so gen_ai attributes populate across providers.\n const sampling = config.modelOptions ?? {}\n const nestedOptions =\n sampling['options'] && typeof sampling['options'] === 'object'\n ? (sampling['options'] as Record<string, unknown>)\n : undefined\n const samplingTemperature = firstNumber(\n sampling['temperature'],\n nestedOptions?.['temperature'],\n )\n const samplingTopP = firstNumber(\n sampling['top_p'],\n sampling['topP'],\n nestedOptions?.['top_p'],\n )\n // Spellings come from the shared `MAX_TOKENS_KEYS` table so this stays\n // in lockstep with the summarize wrapper's caller-limit detection.\n const samplingMaxTokens = firstNumber(\n ...MAX_TOKENS_KEYS.map((k) => sampling[k]),\n nestedOptions?.[NESTED_MAX_TOKENS_KEY],\n )\n if (samplingTemperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = samplingTemperature\n if (samplingTopP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = samplingTopP\n if (samplingMaxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = samplingMaxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (chunk.finishReason) state.lastFinishReason = chunk.finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes(usageAttributes(chunk.usage))\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n if (ctx.activity !== 'chat') {\n // Media: stamp usage on the single span. No token histogram — media\n // unit billing lands as span attributes via usageAttributes, matching\n // prior media behavior and avoiding chat-shaped token metrics.\n safeCall('otel.onUsage', () => {\n const span = mediaSpans.get(ctx)\n if (span) span.setAttributes(usageAttributes(usage))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes(usageAttributes(usage))\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onError', () => {\n const message = errorMessage(info.error)\n endMediaSpan(ctx, (span) => {\n span.recordException(info.error as Exception)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(message !== undefined ? { message } : {}),\n })\n })\n recordMediaDuration(ctx, info.duration, errorTypeName(info.error))\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onAbort(ctx, info) {\n if (ctx.activity !== 'chat') {\n // Media abandonment (e.g. a video stream dropped before completion).\n safeCall('otel.onAbort', () => {\n endMediaSpan(ctx, (span) => {\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: info.reason ?? 'cancelled',\n })\n })\n recordMediaDuration(ctx, info.duration, 'cancelled')\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx: chatCtx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n\n onFinish(ctx, info) {\n if (ctx.activity !== 'chat') {\n safeCall('otel.onFinish', () => {\n endMediaSpan(ctx, (span) => {\n if (info.usage) span.setAttributes(usageAttributes(info.usage))\n })\n recordMediaDuration(ctx, info.duration)\n })\n return\n }\n const chatCtx = ctx as ChatMiddlewareContext\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(chatCtx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx: chatCtx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, chatCtx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': chatCtx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': chatCtx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes(usageAttributes(info.usage))\n }\n if (state.lastFinishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n state.lastFinishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx: chatCtx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(chatCtx)\n })\n },\n }\n}\n"],"mappings":"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"}
@@ -1,43 +1,50 @@
1
1
  import { firstNumber } from "../utilities/numbers.js";
2
+ //#region src/middlewares/usage-attributes.ts
3
+ /**
4
+ * Build the full set of `gen_ai.usage.*` span attributes from a `TokenUsage`.
5
+ *
6
+ * Beyond input/output tokens, this emits provider-reported cost, total tokens,
7
+ * cache and reasoning breakdowns, duration-based billing, and media unit counts
8
+ * — every field is guarded so spans stay clean when a provider doesn't report
9
+ * it. Cache and reasoning use the official GenAI semconv names;
10
+ * `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions
11
+ * consumed by backends like PostHog (which otherwise re-derive cost from their
12
+ * own price tables, losing cache discounts and gateway markup). Fields with no
13
+ * semconv or de-facto convention (`costDetails`, `durationSeconds`,
14
+ * `unitsBilled`) are TanStack-namespaced.
15
+ *
16
+ * Shared by `otelMiddleware` across every activity (chat and the media
17
+ * activities) so usage lands identically whichever activity produced the span.
18
+ *
19
+ * Deliberately not emitted: `providerUsageDetails` (a provider-shaped bag,
20
+ * unsafe to spread onto spans) and the per-modality token breakdowns
21
+ * (`promptTokensDetails.audioTokens`, etc.) — those can balloon the attribute
22
+ * set and have no agreed convention yet.
23
+ */
2
24
  function usageAttributes(usage) {
3
- const attrs = {
4
- "gen_ai.usage.input_tokens": usage.promptTokens,
5
- "gen_ai.usage.output_tokens": usage.completionTokens
6
- };
7
- const optional = [
8
- ["gen_ai.usage.total_tokens", usage.totalTokens],
9
- ["gen_ai.usage.cost", usage.cost],
10
- [
11
- "gen_ai.usage.cache_read.input_tokens",
12
- usage.promptTokensDetails?.cachedTokens
13
- ],
14
- [
15
- "gen_ai.usage.cache_creation.input_tokens",
16
- usage.promptTokensDetails?.cacheWriteTokens
17
- ],
18
- [
19
- "gen_ai.usage.reasoning.output_tokens",
20
- usage.completionTokensDetails?.reasoningTokens
21
- ],
22
- ["tanstack.ai.usage.duration_seconds", usage.durationSeconds],
23
- ["tanstack.ai.usage.units_billed", usage.unitsBilled],
24
- ["tanstack.ai.usage.upstream_cost", usage.costDetails?.upstreamCost],
25
- [
26
- "tanstack.ai.usage.upstream_input_cost",
27
- usage.costDetails?.upstreamInputCost
28
- ],
29
- [
30
- "tanstack.ai.usage.upstream_output_cost",
31
- usage.costDetails?.upstreamOutputCost
32
- ]
33
- ];
34
- for (const [key, value] of optional) {
35
- const num = firstNumber(value);
36
- if (num !== void 0) attrs[key] = num;
37
- }
38
- return attrs;
25
+ const attrs = {
26
+ "gen_ai.usage.input_tokens": usage.promptTokens,
27
+ "gen_ai.usage.output_tokens": usage.completionTokens
28
+ };
29
+ const optional = [
30
+ ["gen_ai.usage.total_tokens", usage.totalTokens],
31
+ ["gen_ai.usage.cost", usage.cost],
32
+ ["gen_ai.usage.cache_read.input_tokens", usage.promptTokensDetails?.cachedTokens],
33
+ ["gen_ai.usage.cache_creation.input_tokens", usage.promptTokensDetails?.cacheWriteTokens],
34
+ ["gen_ai.usage.reasoning.output_tokens", usage.completionTokensDetails?.reasoningTokens],
35
+ ["tanstack.ai.usage.duration_seconds", usage.durationSeconds],
36
+ ["tanstack.ai.usage.units_billed", usage.unitsBilled],
37
+ ["tanstack.ai.usage.upstream_cost", usage.costDetails?.upstreamCost],
38
+ ["tanstack.ai.usage.upstream_input_cost", usage.costDetails?.upstreamInputCost],
39
+ ["tanstack.ai.usage.upstream_output_cost", usage.costDetails?.upstreamOutputCost]
40
+ ];
41
+ for (const [key, value] of optional) {
42
+ const num = firstNumber(value);
43
+ if (num !== void 0) attrs[key] = num;
44
+ }
45
+ return attrs;
39
46
  }
40
- export {
41
- usageAttributes
42
- };
43
- //# sourceMappingURL=usage-attributes.js.map
47
+ //#endregion
48
+ export { usageAttributes };
49
+
50
+ //# sourceMappingURL=usage-attributes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usage-attributes.js","sources":["../../../src/middlewares/usage-attributes.ts"],"sourcesContent":["import { firstNumber } from '../utilities/numbers'\nimport type { AttributeValue } from '@opentelemetry/api'\nimport type { TokenUsage } from '../types'\n\n/**\n * Build the full set of `gen_ai.usage.*` span attributes from a `TokenUsage`.\n *\n * Beyond input/output tokens, this emits provider-reported cost, total tokens,\n * cache and reasoning breakdowns, duration-based billing, and media unit counts\n * — every field is guarded so spans stay clean when a provider doesn't report\n * it. Cache and reasoning use the official GenAI semconv names;\n * `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions\n * consumed by backends like PostHog (which otherwise re-derive cost from their\n * own price tables, losing cache discounts and gateway markup). Fields with no\n * semconv or de-facto convention (`costDetails`, `durationSeconds`,\n * `unitsBilled`) are TanStack-namespaced.\n *\n * Shared by `otelMiddleware` across every activity (chat and the media\n * activities) so usage lands identically whichever activity produced the span.\n *\n * Deliberately not emitted: `providerUsageDetails` (a provider-shaped bag,\n * unsafe to spread onto spans) and the per-modality token breakdowns\n * (`promptTokensDetails.audioTokens`, etc.) — those can balloon the attribute\n * set and have no agreed convention yet.\n */\nexport function usageAttributes(\n usage: TokenUsage,\n): Record<string, AttributeValue> {\n const attrs: Record<string, AttributeValue> = {\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n }\n const optional: Array<[key: string, value: unknown]> = [\n ['gen_ai.usage.total_tokens', usage.totalTokens],\n ['gen_ai.usage.cost', usage.cost],\n [\n 'gen_ai.usage.cache_read.input_tokens',\n usage.promptTokensDetails?.cachedTokens,\n ],\n [\n 'gen_ai.usage.cache_creation.input_tokens',\n usage.promptTokensDetails?.cacheWriteTokens,\n ],\n [\n 'gen_ai.usage.reasoning.output_tokens',\n usage.completionTokensDetails?.reasoningTokens,\n ],\n ['tanstack.ai.usage.duration_seconds', usage.durationSeconds],\n ['tanstack.ai.usage.units_billed', usage.unitsBilled],\n ['tanstack.ai.usage.upstream_cost', usage.costDetails?.upstreamCost],\n [\n 'tanstack.ai.usage.upstream_input_cost',\n usage.costDetails?.upstreamInputCost,\n ],\n [\n 'tanstack.ai.usage.upstream_output_cost',\n usage.costDetails?.upstreamOutputCost,\n ],\n ]\n for (const [key, value] of optional) {\n const num = firstNumber(value)\n if (num !== undefined) attrs[key] = num\n }\n return attrs\n}\n"],"names":[],"mappings":";AAyBO,SAAS,gBACd,OACgC;AAChC,QAAM,QAAwC;AAAA,IAC5C,6BAA6B,MAAM;AAAA,IACnC,8BAA8B,MAAM;AAAA,EAAA;AAEtC,QAAM,WAAiD;AAAA,IACrD,CAAC,6BAA6B,MAAM,WAAW;AAAA,IAC/C,CAAC,qBAAqB,MAAM,IAAI;AAAA,IAChC;AAAA,MACE;AAAA,MACA,MAAM,qBAAqB;AAAA,IAAA;AAAA,IAE7B;AAAA,MACE;AAAA,MACA,MAAM,qBAAqB;AAAA,IAAA;AAAA,IAE7B;AAAA,MACE;AAAA,MACA,MAAM,yBAAyB;AAAA,IAAA;AAAA,IAEjC,CAAC,sCAAsC,MAAM,eAAe;AAAA,IAC5D,CAAC,kCAAkC,MAAM,WAAW;AAAA,IACpD,CAAC,mCAAmC,MAAM,aAAa,YAAY;AAAA,IACnE;AAAA,MACE;AAAA,MACA,MAAM,aAAa;AAAA,IAAA;AAAA,IAErB;AAAA,MACE;AAAA,MACA,MAAM,aAAa;AAAA,IAAA;AAAA,EACrB;AAEF,aAAW,CAAC,KAAK,KAAK,KAAK,UAAU;AACnC,UAAM,MAAM,YAAY,KAAK;AAC7B,QAAI,QAAQ,OAAW,OAAM,GAAG,IAAI;AAAA,EACtC;AACA,SAAO;AACT;"}
1
+ {"version":3,"file":"usage-attributes.js","names":[],"sources":["../../../src/middlewares/usage-attributes.ts"],"sourcesContent":["import { firstNumber } from '../utilities/numbers'\nimport type { AttributeValue } from '@opentelemetry/api'\nimport type { TokenUsage } from '../types'\n\n/**\n * Build the full set of `gen_ai.usage.*` span attributes from a `TokenUsage`.\n *\n * Beyond input/output tokens, this emits provider-reported cost, total tokens,\n * cache and reasoning breakdowns, duration-based billing, and media unit counts\n * — every field is guarded so spans stay clean when a provider doesn't report\n * it. Cache and reasoning use the official GenAI semconv names;\n * `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions\n * consumed by backends like PostHog (which otherwise re-derive cost from their\n * own price tables, losing cache discounts and gateway markup). Fields with no\n * semconv or de-facto convention (`costDetails`, `durationSeconds`,\n * `unitsBilled`) are TanStack-namespaced.\n *\n * Shared by `otelMiddleware` across every activity (chat and the media\n * activities) so usage lands identically whichever activity produced the span.\n *\n * Deliberately not emitted: `providerUsageDetails` (a provider-shaped bag,\n * unsafe to spread onto spans) and the per-modality token breakdowns\n * (`promptTokensDetails.audioTokens`, etc.) — those can balloon the attribute\n * set and have no agreed convention yet.\n */\nexport function usageAttributes(\n usage: TokenUsage,\n): Record<string, AttributeValue> {\n const attrs: Record<string, AttributeValue> = {\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n }\n const optional: Array<[key: string, value: unknown]> = [\n ['gen_ai.usage.total_tokens', usage.totalTokens],\n ['gen_ai.usage.cost', usage.cost],\n [\n 'gen_ai.usage.cache_read.input_tokens',\n usage.promptTokensDetails?.cachedTokens,\n ],\n [\n 'gen_ai.usage.cache_creation.input_tokens',\n usage.promptTokensDetails?.cacheWriteTokens,\n ],\n [\n 'gen_ai.usage.reasoning.output_tokens',\n usage.completionTokensDetails?.reasoningTokens,\n ],\n ['tanstack.ai.usage.duration_seconds', usage.durationSeconds],\n ['tanstack.ai.usage.units_billed', usage.unitsBilled],\n ['tanstack.ai.usage.upstream_cost', usage.costDetails?.upstreamCost],\n [\n 'tanstack.ai.usage.upstream_input_cost',\n usage.costDetails?.upstreamInputCost,\n ],\n [\n 'tanstack.ai.usage.upstream_output_cost',\n usage.costDetails?.upstreamOutputCost,\n ],\n ]\n for (const [key, value] of optional) {\n const num = firstNumber(value)\n if (num !== undefined) attrs[key] = num\n }\n return attrs\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,gBACd,OACgC;CAChC,MAAM,QAAwC;EAC5C,6BAA6B,MAAM;EACnC,8BAA8B,MAAM;CACtC;CACA,MAAM,WAAiD;EACrD,CAAC,6BAA6B,MAAM,WAAW;EAC/C,CAAC,qBAAqB,MAAM,IAAI;EAChC,CACE,wCACA,MAAM,qBAAqB,YAC7B;EACA,CACE,4CACA,MAAM,qBAAqB,gBAC7B;EACA,CACE,wCACA,MAAM,yBAAyB,eACjC;EACA,CAAC,sCAAsC,MAAM,eAAe;EAC5D,CAAC,kCAAkC,MAAM,WAAW;EACpD,CAAC,mCAAmC,MAAM,aAAa,YAAY;EACnE,CACE,yCACA,MAAM,aAAa,iBACrB;EACA,CACE,0CACA,MAAM,aAAa,kBACrB;CACF;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,UAAU;EACnC,MAAM,MAAM,YAAY,KAAK;EAC7B,IAAI,QAAQ,KAAA,GAAW,MAAM,OAAO;CACtC;CACA,OAAO;AACT"}
@@ -1,27 +1,26 @@
1
+ //#region src/realtime/event-emitter.ts
1
2
  function createRealtimeEventEmitter() {
2
- const eventHandlers = /* @__PURE__ */ new Map();
3
- return {
4
- emit(event, payload) {
5
- const handlers = eventHandlers.get(event);
6
- if (!handlers) return;
7
- for (const handler of handlers) {
8
- handler(payload);
9
- }
10
- },
11
- on(event, handler) {
12
- let handlers = eventHandlers.get(event);
13
- if (!handlers) {
14
- handlers = /* @__PURE__ */ new Set();
15
- eventHandlers.set(event, handlers);
16
- }
17
- handlers.add(handler);
18
- return () => {
19
- handlers.delete(handler);
20
- };
21
- }
22
- };
3
+ const eventHandlers = /* @__PURE__ */ new Map();
4
+ return {
5
+ emit(event, payload) {
6
+ const handlers = eventHandlers.get(event);
7
+ if (!handlers) return;
8
+ for (const handler of handlers) handler(payload);
9
+ },
10
+ on(event, handler) {
11
+ let handlers = eventHandlers.get(event);
12
+ if (!handlers) {
13
+ handlers = /* @__PURE__ */ new Set();
14
+ eventHandlers.set(event, handlers);
15
+ }
16
+ handlers.add(handler);
17
+ return () => {
18
+ handlers.delete(handler);
19
+ };
20
+ }
21
+ };
23
22
  }
24
- export {
25
- createRealtimeEventEmitter
26
- };
27
- //# sourceMappingURL=event-emitter.js.map
23
+ //#endregion
24
+ export { createRealtimeEventEmitter };
25
+
26
+ //# sourceMappingURL=event-emitter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"event-emitter.js","sources":["../../../src/realtime/event-emitter.ts"],"sourcesContent":["import type {\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeEventPayloads,\n} from './types'\n\n/**\n * Handlers are stored with a `never` payload so any specific\n * `RealtimeEventHandler<TEvent>` is assignable in (contravariance), keeping the\n * heterogeneous handler map type-safe without `any`. `emit` narrows back to the\n * event's real payload type via its signature; the lone `as never` at the call\n * site is the inverse of that stored `never`.\n */\ntype StoredHandler = (payload: never) => void\n\nexport function createRealtimeEventEmitter() {\n const eventHandlers = new Map<RealtimeEvent, Set<StoredHandler>>()\n\n return {\n emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: RealtimeEventPayloads[TEvent],\n ) {\n const handlers = eventHandlers.get(event)\n if (!handlers) return\n for (const handler of handlers) {\n handler(payload as never)\n }\n },\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set<StoredHandler>()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n handlers.delete(handler)\n }\n },\n }\n}\n"],"names":[],"mappings":"AAeO,SAAS,6BAA6B;AAC3C,QAAM,oCAAoB,IAAA;AAE1B,SAAO;AAAA,IACL,KACE,OACA,SACA;AACA,YAAM,WAAW,cAAc,IAAI,KAAK;AACxC,UAAI,CAAC,SAAU;AACf,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAgB;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,GACE,OACA,SACY;AACZ,UAAI,WAAW,cAAc,IAAI,KAAK;AACtC,UAAI,CAAC,UAAU;AACb,uCAAe,IAAA;AACf,sBAAc,IAAI,OAAO,QAAQ;AAAA,MACnC;AACA,eAAS,IAAI,OAAO;AAEpB,aAAO,MAAM;AACX,iBAAS,OAAO,OAAO;AAAA,MACzB;AAAA,IACF;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"event-emitter.js","names":[],"sources":["../../../src/realtime/event-emitter.ts"],"sourcesContent":["import type {\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeEventPayloads,\n} from './types'\n\n/**\n * Handlers are stored with a `never` payload so any specific\n * `RealtimeEventHandler<TEvent>` is assignable in (contravariance), keeping the\n * heterogeneous handler map type-safe without `any`. `emit` narrows back to the\n * event's real payload type via its signature; the lone `as never` at the call\n * site is the inverse of that stored `never`.\n */\ntype StoredHandler = (payload: never) => void\n\nexport function createRealtimeEventEmitter() {\n const eventHandlers = new Map<RealtimeEvent, Set<StoredHandler>>()\n\n return {\n emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: RealtimeEventPayloads[TEvent],\n ) {\n const handlers = eventHandlers.get(event)\n if (!handlers) return\n for (const handler of handlers) {\n handler(payload as never)\n }\n },\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set<StoredHandler>()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n handlers.delete(handler)\n }\n },\n }\n}\n"],"mappings":";AAeA,SAAgB,6BAA6B;CAC3C,MAAM,gCAAgB,IAAI,IAAuC;CAEjE,OAAO;EACL,KACE,OACA,SACA;GACA,MAAM,WAAW,cAAc,IAAI,KAAK;GACxC,IAAI,CAAC,UAAU;GACf,KAAK,MAAM,WAAW,UACpB,QAAQ,OAAgB;EAE5B;EACA,GACE,OACA,SACY;GACZ,IAAI,WAAW,cAAc,IAAI,KAAK;GACtC,IAAI,CAAC,UAAU;IACb,2BAAW,IAAI,IAAmB;IAClC,cAAc,IAAI,OAAO,QAAQ;GACnC;GACA,SAAS,IAAI,OAAO;GAEpB,aAAa;IACX,SAAS,OAAO,OAAO;GACzB;EACF;CACF;AACF"}
@@ -15,15 +15,11 @@ export type * from './types.js';
15
15
  * import { realtimeToken } from '@tanstack/ai'
16
16
  * import { openaiRealtimeToken } from '@tanstack/ai-openai'
17
17
  *
18
- * // Server function (TanStack Start example)
19
- * export const getRealtimeToken = createServerFn()
20
- * .handler(async () => {
21
- * return realtimeToken({
22
- * adapter: openaiRealtimeToken({
23
- * model: 'gpt-realtime',
24
- * }),
25
- * })
26
- * })
18
+ * // On the server (e.g. inside a server route or framework server
19
+ * // function), mint an ephemeral token for the client:
20
+ * const token = await realtimeToken({
21
+ * adapter: openaiRealtimeToken({ model: 'gpt-realtime' }),
22
+ * })
27
23
  * ```
28
24
  */
29
25
  export declare function realtimeToken(options: RealtimeTokenOptions): Promise<RealtimeToken>;
@@ -1,8 +1,31 @@
1
+ import { createRealtimeEventEmitter } from "./event-emitter.js";
2
+ //#region src/realtime/index.ts
3
+ /**
4
+ * Generate a realtime token using the provided adapter.
5
+ *
6
+ * This function is used on the server to generate ephemeral tokens
7
+ * that clients can use to establish realtime connections.
8
+ *
9
+ * @param options - Token generation options including the adapter
10
+ * @returns Promise resolving to a RealtimeToken
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { realtimeToken } from '@tanstack/ai'
15
+ * import { openaiRealtimeToken } from '@tanstack/ai-openai'
16
+ *
17
+ * // On the server (e.g. inside a server route or framework server
18
+ * // function), mint an ephemeral token for the client:
19
+ * const token = await realtimeToken({
20
+ * adapter: openaiRealtimeToken({ model: 'gpt-realtime' }),
21
+ * })
22
+ * ```
23
+ */
1
24
  async function realtimeToken(options) {
2
- const { adapter } = options;
3
- return adapter.generateToken();
25
+ const { adapter } = options;
26
+ return adapter.generateToken();
4
27
  }
5
- export {
6
- realtimeToken
7
- };
8
- //# sourceMappingURL=index.js.map
28
+ //#endregion
29
+ export { createRealtimeEventEmitter, realtimeToken };
30
+
31
+ //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\nexport { createRealtimeEventEmitter } from './event-emitter'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // Server function (TanStack Start example)\n * export const getRealtimeToken = createServerFn()\n * .handler(async () => {\n * return realtimeToken({\n * adapter: openaiRealtimeToken({\n * model: 'gpt-realtime',\n * }),\n * })\n * })\n * ```\n */\nexport async function realtimeToken(\n options: RealtimeTokenOptions,\n): Promise<RealtimeToken> {\n const { adapter } = options\n return adapter.generateToken()\n}\n"],"names":[],"mappings":"AAgCA,eAAsB,cACpB,SACwB;AACxB,QAAM,EAAE,YAAY;AACpB,SAAO,QAAQ,cAAA;AACjB;"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/realtime/index.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenOptions } from './types'\n\nexport { createRealtimeEventEmitter } from './event-emitter'\n\n// Re-export all types\nexport type * from './types'\n\n/**\n * Generate a realtime token using the provided adapter.\n *\n * This function is used on the server to generate ephemeral tokens\n * that clients can use to establish realtime connections.\n *\n * @param options - Token generation options including the adapter\n * @returns Promise resolving to a RealtimeToken\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { openaiRealtimeToken } from '@tanstack/ai-openai'\n *\n * // On the server (e.g. inside a server route or framework server\n * // function), mint an ephemeral token for the client:\n * const token = await realtimeToken({\n * adapter: openaiRealtimeToken({ model: 'gpt-realtime' }),\n * })\n * ```\n */\nexport async function realtimeToken(\n options: RealtimeTokenOptions,\n): Promise<RealtimeToken> {\n const { adapter } = options\n return adapter.generateToken()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,cACpB,SACwB;CACxB,MAAM,EAAE,YAAY;CACpB,OAAO,QAAQ,cAAc;AAC/B"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared identity/isolation scope for the TanStack AI subsystems that persist
3
+ * or recall per-conversation data — `@tanstack/ai-persistence` (keyed CRUD over
4
+ * threads, runs, interrupts) and `@tanstack/ai-memory` (ranked recall/save).
5
+ *
6
+ * Both subsystems answer the same underlying question — "whose data is this?" —
7
+ * so they share ONE identity vocabulary defined here rather than each inventing
8
+ * its own. `threadId` is the single conversation key across the codebase (it is
9
+ * `ChatMiddlewareContext.threadId`, and `conversationId` is deprecated in favor
10
+ * of it); a subsystem must never introduce a second name (`sessionId`,
11
+ * `conversationId`, …) for the same concept.
12
+ *
13
+ * ## Security
14
+ *
15
+ * A `Scope` is an isolation boundary. Derive every field server-side from
16
+ * trusted, validated session state — **never** from client input. `threadId`
17
+ * in particular resolves from a client-supplied value on the request, so a
18
+ * subsystem that reads/writes user-owned data (memory, per-user metadata) must
19
+ * pair it with a server-trusted `userId`/`tenantId` and must not treat a bare
20
+ * client `threadId` as sufficient isolation: thread ids are guessable, so on
21
+ * their own they let one caller read another's data.
22
+ */
23
+ export interface Scope {
24
+ /**
25
+ * The conversation this data belongs to. Required — the minimal isolation
26
+ * key both subsystems already center on. Same concept as
27
+ * `ChatMiddlewareContext.threadId`.
28
+ */
29
+ threadId: string;
30
+ /**
31
+ * Durable end-user identity, for cross-thread recall and per-user isolation.
32
+ * Optional, but required in practice for any multi-user deployment — a
33
+ * `threadId` alone is not an authorization boundary (see Security above).
34
+ */
35
+ userId?: string;
36
+ /**
37
+ * Tenant/organization boundary for multi-tenant deployments. When present,
38
+ * every read and write must be confined to it.
39
+ */
40
+ tenantId?: string;
41
+ /**
42
+ * Logical partition within a tenant/user (e.g. separating distinct memory
43
+ * banks or persistence namespaces). Reserved — no subsystem keys on it yet;
44
+ * adapters that don't understand it must ignore it rather than error.
45
+ */
46
+ namespace?: string;
47
+ }