@tanstack/ai 0.32.0 → 0.33.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 (60) hide show
  1. package/dist/esm/activities/chat/index.js +24 -3
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
  4. package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
  5. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
  6. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  7. package/dist/esm/activities/generateAudio/index.d.ts +7 -0
  8. package/dist/esm/activities/generateAudio/index.js +26 -1
  9. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  10. package/dist/esm/activities/generateImage/index.d.ts +7 -0
  11. package/dist/esm/activities/generateImage/index.js +26 -1
  12. package/dist/esm/activities/generateImage/index.js.map +1 -1
  13. package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
  14. package/dist/esm/activities/generateSpeech/index.js +26 -1
  15. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  16. package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
  17. package/dist/esm/activities/generateTranscription/index.js +26 -1
  18. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  19. package/dist/esm/activities/generateVideo/index.d.ts +9 -0
  20. package/dist/esm/activities/generateVideo/index.js +52 -2
  21. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  22. package/dist/esm/activities/middleware/index.d.ts +2 -0
  23. package/dist/esm/activities/middleware/run.d.ts +20 -0
  24. package/dist/esm/activities/middleware/run.js +42 -0
  25. package/dist/esm/activities/middleware/run.js.map +1 -0
  26. package/dist/esm/activities/middleware/types.d.ts +118 -0
  27. package/dist/esm/index.d.ts +2 -0
  28. package/dist/esm/index.js +2 -0
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/middlewares/otel.d.ts +8 -2
  31. package/dist/esm/middlewares/otel.js +145 -95
  32. package/dist/esm/middlewares/otel.js.map +1 -1
  33. package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
  34. package/dist/esm/middlewares/usage-attributes.js +43 -0
  35. package/dist/esm/middlewares/usage-attributes.js.map +1 -0
  36. package/dist/esm/types.d.ts +7 -7
  37. package/dist/esm/utilities/errors.d.ts +13 -0
  38. package/dist/esm/utilities/errors.js +22 -0
  39. package/dist/esm/utilities/errors.js.map +1 -0
  40. package/dist/esm/utilities/numbers.d.ts +8 -0
  41. package/dist/esm/utilities/numbers.js +12 -0
  42. package/dist/esm/utilities/numbers.js.map +1 -0
  43. package/package.json +2 -2
  44. package/src/activities/chat/index.ts +32 -4
  45. package/src/activities/chat/middleware/types.ts +7 -0
  46. package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
  47. package/src/activities/generateAudio/index.ts +42 -1
  48. package/src/activities/generateImage/index.ts +42 -1
  49. package/src/activities/generateSpeech/index.ts +42 -1
  50. package/src/activities/generateTranscription/index.ts +42 -1
  51. package/src/activities/generateVideo/index.ts +88 -2
  52. package/src/activities/middleware/index.ts +20 -0
  53. package/src/activities/middleware/run.ts +88 -0
  54. package/src/activities/middleware/types.ts +173 -0
  55. package/src/index.ts +19 -0
  56. package/src/middlewares/otel.ts +195 -120
  57. package/src/middlewares/usage-attributes.ts +65 -0
  58. package/src/types.ts +7 -7
  59. package/src/utilities/errors.ts +29 -0
  60. package/src/utilities/numbers.ts +15 -0
@@ -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 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 { TokenUsage } from '../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 */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\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 : never\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\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\n/**\n * Return the first candidate that is a finite `number`, or `undefined`. Used to\n * pick a sampling attribute from among the several provider-native spellings.\n */\nfunction firstNumber(...candidates: Array<unknown>): number | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'number' && Number.isFinite(candidate)) {\n return candidate\n }\n }\n return undefined\n}\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, and duration-based billing — every field is\n * guarded so spans stay clean when a provider doesn't report it. Cache and\n * reasoning use the official GenAI semconv names; `gen_ai.usage.cost` and\n * `gen_ai.usage.total_tokens` are de-facto extensions consumed by backends\n * like PostHog (which otherwise re-derive cost from their own price tables,\n * losing cache discounts and gateway markup). Fields with no semconv or\n * de-facto convention (`costDetails`, `durationSeconds`) are\n * TanStack-namespaced. Deliberately not emitted: `unitsBilled`,\n * `providerUsageDetails`, and the per-modality token breakdowns — those are\n * media-oriented; media-activity observability is tracked in #720.\n */\nfunction usageAttributes(usage: TokenUsage): 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.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\nfunction errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\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(options: OtelMiddlewareOptions): 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 return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.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(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\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 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 safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\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': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.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 safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\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,\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,\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': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\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,\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,\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': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\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,\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, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes(usageAttributes(info.usage))\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
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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"}
@@ -0,0 +1,24 @@
1
+ import { AttributeValue } from '@opentelemetry/api';
2
+ import { TokenUsage } from '../types.js';
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
+ */
24
+ export declare function usageAttributes(usage: TokenUsage): Record<string, AttributeValue>;
@@ -0,0 +1,43 @@
1
+ import { firstNumber } from "../utilities/numbers.js";
2
+ 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;
39
+ }
40
+ export {
41
+ usageAttributes
42
+ };
43
+ //# sourceMappingURL=usage-attributes.js.map
@@ -0,0 +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;"}
@@ -636,14 +636,14 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
636
636
  systemPrompts?: Array<SystemPrompt>;
637
637
  agentLoopStrategy?: AgentLoopStrategy;
638
638
  /**
639
- * Additional metadata to attach to the request.
640
- * Can be used for tracking, debugging, or passing custom information.
641
- * Structure and constraints vary by provider.
639
+ * Observability metadata attached to this call. Surfaced to middleware,
640
+ * devtools, and the event client; values may be arbitrarily structured
641
+ * (objects, arrays). Adapters never forward this field onto the provider
642
+ * wire request.
642
643
  *
643
- * Provider usage:
644
- * - OpenAI: `metadata` (Record<string, string>) - max 16 key-value pairs, keys max 64 chars, values max 512 chars
645
- * - Anthropic: `metadata` (Record<string, any>) - includes optional user_id (max 256 chars)
646
- * - Gemini: Not directly available in TextProviderOptions
644
+ * To send provider-side request metadata, use the provider's
645
+ * `modelOptions` field instead, where the provider supports one (e.g.
646
+ * OpenAI's and OpenRouter's `metadata` are both Record<string, string>).
647
647
  */
648
648
  metadata?: Record<string, any> | undefined;
649
649
  modelOptions?: TProviderOptionsForModel;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Best-effort extraction of a human-readable message from an unknown thrown
3
+ * value, returning `undefined` when none can be found.
4
+ *
5
+ * Used by `otelMiddleware` so error reporting stays identical across chat and
6
+ * media spans.
7
+ */
8
+ export declare function errorMessage(err: unknown): string | undefined;
9
+ /**
10
+ * Best-effort extraction of an error's type name (used for the `error.type`
11
+ * metric attribute), falling back to `'Error'` when no name is available.
12
+ */
13
+ export declare function errorTypeName(err: unknown): string;
@@ -0,0 +1,22 @@
1
+ function errorMessage(err) {
2
+ if (err instanceof Error) return err.message;
3
+ if (typeof err === "string") return err;
4
+ if (err && typeof err === "object" && "message" in err) {
5
+ const m = err.message;
6
+ if (typeof m === "string") return m;
7
+ }
8
+ return void 0;
9
+ }
10
+ function errorTypeName(err) {
11
+ if (err instanceof Error) return err.name || "Error";
12
+ if (err && typeof err === "object" && "name" in err) {
13
+ const n = err.name;
14
+ if (typeof n === "string") return n;
15
+ }
16
+ return "Error";
17
+ }
18
+ export {
19
+ errorMessage,
20
+ errorTypeName
21
+ };
22
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sources":["../../../src/utilities/errors.ts"],"sourcesContent":["/**\n * Best-effort extraction of a human-readable message from an unknown thrown\n * value, returning `undefined` when none can be found.\n *\n * Used by `otelMiddleware` so error reporting stays identical across chat and\n * media spans.\n */\nexport function errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\n/**\n * Best-effort extraction of an error's type name (used for the `error.type`\n * metric attribute), falling back to `'Error'` when no name is available.\n */\nexport function errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\n}\n"],"names":[],"mappings":"AAOO,SAAS,aAAa,KAAkC;AAC7D,MAAI,eAAe,MAAO,QAAO,IAAI;AACrC,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,OAAO,QAAQ,YAAY,aAAa,KAAK;AACtD,UAAM,IAAK,IAA8B;AACzC,QAAI,OAAO,MAAM,SAAU,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAMO,SAAS,cAAc,KAAsB;AAClD,MAAI,eAAe,MAAO,QAAO,IAAI,QAAQ;AAC7C,MAAI,OAAO,OAAO,QAAQ,YAAY,UAAU,KAAK;AACnD,UAAM,IAAK,IAA2B;AACtC,QAAI,OAAO,MAAM,SAAU,QAAO;AAAA,EACpC;AACA,SAAO;AACT;"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Return the first candidate that is a finite `number`, or `undefined`.
3
+ *
4
+ * Handy for picking a value from among several possible spellings/sources where
5
+ * only some are populated — e.g. the provider-native sampling option names read
6
+ * by the OTel middleware, or the optional numeric fields on `TokenUsage`.
7
+ */
8
+ export declare function firstNumber(...candidates: Array<unknown>): number | undefined;
@@ -0,0 +1,12 @@
1
+ function firstNumber(...candidates) {
2
+ for (const candidate of candidates) {
3
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
4
+ return candidate;
5
+ }
6
+ }
7
+ return void 0;
8
+ }
9
+ export {
10
+ firstNumber
11
+ };
12
+ //# sourceMappingURL=numbers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbers.js","sources":["../../../src/utilities/numbers.ts"],"sourcesContent":["/**\n * Return the first candidate that is a finite `number`, or `undefined`.\n *\n * Handy for picking a value from among several possible spellings/sources where\n * only some are populated — e.g. the provider-native sampling option names read\n * by the OTel middleware, or the optional numeric fields on `TokenUsage`.\n */\nexport function firstNumber(...candidates: Array<unknown>): number | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'number' && Number.isFinite(candidate)) {\n return candidate\n }\n }\n return undefined\n}\n"],"names":[],"mappings":"AAOO,SAAS,eAAe,YAAgD;AAC7E,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAAG;AAC/D,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -76,7 +76,7 @@
76
76
  "@ag-ui/core": "^0.0.52",
77
77
  "@standard-schema/spec": "^1.1.0",
78
78
  "partial-json": "^0.1.7",
79
- "@tanstack/ai-event-client": "0.6.3"
79
+ "@tanstack/ai-event-client": "0.6.4"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@opentelemetry/api": ">=1.9.0"
@@ -33,7 +33,11 @@ import type {
33
33
  ClientToolRequest,
34
34
  ToolResult,
35
35
  } from './tools/tool-calls'
36
- import type { AnyTextAdapter, StructuredOutputOptions } from './adapter'
36
+ import type {
37
+ AnyTextAdapter,
38
+ StructuredOutputOptions,
39
+ StructuredOutputResult,
40
+ } from './adapter'
37
41
  import type {
38
42
  AgentLoopStrategy,
39
43
  AnyTool,
@@ -646,6 +650,7 @@ class TextEngine<
646
650
  this.deferredPromises.push(promise)
647
651
  },
648
652
  // Provider / adapter info
653
+ activity: 'chat',
649
654
  provider: config.adapter.name,
650
655
  model: config.params.model,
651
656
  source: 'server',
@@ -1197,6 +1202,23 @@ class TextEngine<
1197
1202
  }
1198
1203
  }
1199
1204
 
1205
+ /**
1206
+ * Tools available for execution this turn. The discovery tool is dropped
1207
+ * from the advertised set (`this.tools`) once every lazy tool is discovered,
1208
+ * but a model may still re-request discovery; this widens execution lookup
1209
+ * to include it so such calls don't fail with "Unknown tool". Centralised so
1210
+ * both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
1211
+ * stay in sync.
1212
+ */
1213
+ private resolveExecutableTools(
1214
+ toolCalls: ReadonlyArray<ToolCall>,
1215
+ ): ReadonlyArray<AnyTool> {
1216
+ return this.lazyToolManager.getExecutableTools(
1217
+ this.tools,
1218
+ toolCalls.map((tc) => tc.function.name),
1219
+ )
1220
+ }
1221
+
1200
1222
  private async *checkForPendingToolCalls(): AsyncGenerator<
1201
1223
  StreamChunk,
1202
1224
  ToolPhaseResult,
@@ -1245,7 +1267,7 @@ class TextEngine<
1245
1267
 
1246
1268
  const generator = executeToolCalls(
1247
1269
  executablePendingCalls,
1248
- this.tools,
1270
+ this.resolveExecutableTools(executablePendingCalls),
1249
1271
  approvals,
1250
1272
  clientToolResults,
1251
1273
  (eventName, data) => this.createCustomEventChunk(eventName, data),
@@ -1407,7 +1429,7 @@ class TextEngine<
1407
1429
 
1408
1430
  const generator = executeToolCalls(
1409
1431
  executableToolCalls,
1410
- this.tools,
1432
+ this.resolveExecutableTools(executableToolCalls),
1411
1433
  approvals,
1412
1434
  clientToolResults,
1413
1435
  (eventName, data) => this.createCustomEventChunk(eventName, data),
@@ -2861,7 +2883,7 @@ async function* fallbackStructuredOutputStream(
2861
2883
  timestamp,
2862
2884
  }
2863
2885
 
2864
- let result: { data: unknown; rawText: string }
2886
+ let result: StructuredOutputResult<unknown>
2865
2887
  try {
2866
2888
  result = await adapter.structuredOutput(options)
2867
2889
  } catch (error) {
@@ -2917,6 +2939,12 @@ async function* fallbackStructuredOutputStream(
2917
2939
  model,
2918
2940
  timestamp,
2919
2941
  finishReason: 'stop',
2942
+ // Forward adapter-reported token usage so consumers reading
2943
+ // `RUN_FINISHED.usage` (and the engine's `runOnUsage` middleware hook) see
2944
+ // it on the fallback path, mirroring the native streaming path. The
2945
+ // conditional spread avoids emitting `usage: undefined` for adapters that
2946
+ // don't report it. See #758.
2947
+ ...(result.usage ? { usage: result.usage } : {}),
2920
2948
  }
2921
2949
  }
2922
2950
 
@@ -80,6 +80,13 @@ export interface ChatMiddlewareContext<TContext = unknown> {
80
80
 
81
81
  // --- Provider / adapter info (immutable for the lifetime of the request) ---
82
82
 
83
+ /**
84
+ * Which activity this context describes — always `'chat'`. Present so the
85
+ * chat context structurally satisfies the base `GenerationMiddlewareContext`,
86
+ * letting an observe-only middleware authored against the base (e.g.
87
+ * `otelMiddleware`) run on both chat and media activities.
88
+ */
89
+ activity: 'chat'
83
90
  /** Provider name (e.g., 'openai', 'anthropic') */
84
91
  provider: string
85
92
  /** Model identifier (e.g., 'gpt-4o') */
@@ -1,7 +1,14 @@
1
1
  import { convertSchemaToJsonSchema } from './schema-converter'
2
- import type { Tool } from '../../../types'
2
+ import type { AnyTool, Tool } from '../../../types'
3
3
 
4
- const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'
4
+ /**
5
+ * Name of the synthetic tool the LLM calls to discover lazy tools.
6
+ *
7
+ * Exported so callers building custom message-compaction / history-trimming
8
+ * logic can reference the discovery tool by constant instead of hard-coding
9
+ * the string (which is an internal contract that could change).
10
+ */
11
+ export const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'
5
12
 
6
13
  /**
7
14
  * Manages lazy tool discovery for the chat agent loop.
@@ -87,6 +94,35 @@ export class LazyToolManager {
87
94
  return active
88
95
  }
89
96
 
97
+ /**
98
+ * Returns the tools that should be available for *execution* this turn.
99
+ *
100
+ * This is the advertised set (`getActiveTools()`, passed in as `activeTools`)
101
+ * plus the discovery tool when a pending call references it but it is no
102
+ * longer advertised. Once every lazy tool has been discovered the discovery
103
+ * tool is dropped from the advertised set, but a model may still re-request
104
+ * discovery (long context / hallucination); keeping it executable lets that
105
+ * call return the schemas again instead of failing with "Unknown tool".
106
+ *
107
+ * The advertised set is intentionally left unchanged — only execution lookup
108
+ * is widened. Operates on the already-built `activeTools`: it must NOT call
109
+ * `getActiveTools()`, which would reset `hasNewDiscoveries` before the
110
+ * post-execution refresh check in the agent loop.
111
+ */
112
+ getExecutableTools(
113
+ activeTools: ReadonlyArray<AnyTool>,
114
+ pendingToolCallNames: ReadonlyArray<string>,
115
+ ): ReadonlyArray<AnyTool> {
116
+ if (
117
+ this.discoveryTool &&
118
+ pendingToolCallNames.includes(DISCOVERY_TOOL_NAME) &&
119
+ !activeTools.some((t) => t.name === DISCOVERY_TOOL_NAME)
120
+ ) {
121
+ return [...activeTools, this.discoveryTool]
122
+ }
123
+ return activeTools
124
+ }
125
+
90
126
  /**
91
127
  * Returns whether new tools have been discovered since the last getActiveTools() call.
92
128
  */
@@ -221,8 +257,14 @@ export class LazyToolManager {
221
257
  for (const name of args.toolNames) {
222
258
  const tool = lazyToolMap.get(name)
223
259
  if (tool) {
224
- manager.discoveredTools.add(name)
225
- manager.hasNewDiscoveries = true
260
+ // Only flag a refresh for genuinely new discoveries. Re-requesting
261
+ // an already-discovered tool still returns its schema below (the
262
+ // model asked for it), but must not trigger a redundant tool-list
263
+ // refresh + continue in the agent loop.
264
+ if (!manager.discoveredTools.has(name)) {
265
+ manager.discoveredTools.add(name)
266
+ manager.hasNewDiscoveries = true
267
+ }
226
268
  const jsonSchema = tool.inputSchema
227
269
  ? convertSchemaToJsonSchema(tool.inputSchema)
228
270
  : undefined