@tanstack/ai 0.54.0 → 0.57.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 (62) hide show
  1. package/README.md +42 -16
  2. package/dist/esm/activities/chat/index.js +17 -1
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.d.ts +21 -1
  5. package/dist/esm/activities/chat/messages.js +50 -1
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/processor.d.ts +17 -0
  8. package/dist/esm/activities/chat/stream/processor.js +27 -0
  9. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  10. package/dist/esm/activities/chat/tools/tool-calls.js +1 -0
  11. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  12. package/dist/esm/activities/evaluate/adapter.d.ts +160 -0
  13. package/dist/esm/activities/evaluate/adapter.js +23 -0
  14. package/dist/esm/activities/evaluate/adapter.js.map +1 -0
  15. package/dist/esm/activities/evaluate/index.d.ts +255 -0
  16. package/dist/esm/activities/evaluate/index.js +317 -0
  17. package/dist/esm/activities/evaluate/index.js.map +1 -0
  18. package/dist/esm/activities/generateSpeech/adapter.d.ts +39 -1
  19. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  20. package/dist/esm/activities/generateSpeech/index.d.ts +55 -5
  21. package/dist/esm/activities/generateSpeech/index.js +53 -3
  22. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  23. package/dist/esm/activities/generateVoice/adapter.d.ts +62 -0
  24. package/dist/esm/activities/generateVoice/adapter.js +23 -0
  25. package/dist/esm/activities/generateVoice/adapter.js.map +1 -0
  26. package/dist/esm/activities/generateVoice/index.d.ts +133 -0
  27. package/dist/esm/activities/generateVoice/index.js +184 -0
  28. package/dist/esm/activities/generateVoice/index.js.map +1 -0
  29. package/dist/esm/activities/index.d.ts +10 -4
  30. package/dist/esm/activities/index.js +14 -10
  31. package/dist/esm/activities/middleware/types.d.ts +1 -1
  32. package/dist/esm/client.d.ts +3 -2
  33. package/dist/esm/client.js +21 -3
  34. package/dist/esm/client.js.map +1 -1
  35. package/dist/esm/index.d.ts +4 -2
  36. package/dist/esm/index.js +5 -2
  37. package/dist/esm/middlewares/otel.js +2 -0
  38. package/dist/esm/middlewares/otel.js.map +1 -1
  39. package/dist/esm/realtime/index.d.ts +1 -1
  40. package/dist/esm/realtime/index.js +1 -1
  41. package/dist/esm/realtime/index.js.map +1 -1
  42. package/dist/esm/types.d.ts +225 -2
  43. package/package.json +2 -2
  44. package/skills/ai-core/adapter-configuration/SKILL.md +1 -1
  45. package/skills/ai-core/media-generation/SKILL.md +154 -18
  46. package/src/activities/chat/index.ts +23 -0
  47. package/src/activities/chat/messages.ts +60 -0
  48. package/src/activities/chat/stream/processor.ts +31 -0
  49. package/src/activities/chat/tools/tool-calls.ts +9 -0
  50. package/src/activities/evaluate/adapter.ts +212 -0
  51. package/src/activities/evaluate/index.ts +614 -0
  52. package/src/activities/generateSpeech/adapter.ts +47 -1
  53. package/src/activities/generateSpeech/index.ts +149 -8
  54. package/src/activities/generateVoice/adapter.ts +89 -0
  55. package/src/activities/generateVoice/index.ts +371 -0
  56. package/src/activities/index.ts +69 -0
  57. package/src/activities/middleware/types.ts +2 -0
  58. package/src/client.ts +35 -8
  59. package/src/index.ts +21 -0
  60. package/src/middlewares/otel.ts +2 -0
  61. package/src/realtime/index.ts +1 -1
  62. package/src/types.ts +246 -2
@@ -1 +1 @@
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 { rebuildTokenUsage } from '../utilities/ag-ui-usage'\nimport { tanstackMetadata } from '../utilities/merge-metadata'\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'\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 provider model call (agent-loop `beforeModel`\n * turn, or the separate `structuredOutput` finalization when\n * `outputSchema` skips the agent loop — see #1054)\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 embedding: 'embeddings',\n rerank: 'rerank',\n summarize: 'summarize',\n world: 'world_generation',\n liveVideo: 'live_video_generation',\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 rootUsageAttributes: Record<string, number> | null\n rootUsageApplied: boolean\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction accumulateUsageAttributes(\n current: Record<string, number> | null,\n usage: TokenUsage,\n): Record<string, number> {\n const accumulated = current ?? {}\n for (const [key, value] of Object.entries(usageAttributes(usage))) {\n if (typeof value === 'number') {\n accumulated[key] = (accumulated[key] ?? 0) + value\n }\n }\n return accumulated\n}\n\nfunction applyRootUsage(state: RequestState, fallbackUsage?: TokenUsage): void {\n if (state.rootUsageApplied) return\n\n const attributes =\n state.rootUsageAttributes ??\n (fallbackUsage ? usageAttributes(fallbackUsage) : null)\n if (attributes) state.rootSpan.setAttributes(attributes)\n state.rootUsageApplied = true\n}\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 rootUsageAttributes: null,\n rootUsageApplied: false,\n })\n })\n },\n\n onConfig(ctx, config) {\n // Open an iteration span for every provider model call:\n // - `beforeModel`: agent-loop chatStream turns\n // - `structuredOutput`: separate structured-output finalization\n // (no-tools + outputSchema skips the agent loop, so without this\n // phase there is no generation span and captureContent is a silent\n // no-op — see #1054).\n // Native-combined mode never fires `structuredOutput`, so a run that\n // already opened spans via `beforeModel` is not double-counted.\n if (ctx.phase !== 'beforeModel' && ctx.phase !== 'structuredOutput')\n 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 // Number spans by the order of model calls this middleware has seen,\n // not by `ctx.iteration`. After an agent-loop turn, structured-output\n // finalization reuses the engine's last iteration index; using our\n // own counter keeps finalization as a distinct leaf (#N+1).\n const iteration = state.iterationCount\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${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': 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 tanstack = tanstackMetadata(chunk)\n const extra = chunk as {\n finishReason?: string | null\n model?: string\n }\n const finishReason = extra.finishReason ?? tanstack?.finishReason\n const model = extra.model ?? tanstack?.model\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (finishReason) state.lastFinishReason = finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [finishReason])\n }\n if (model) span.setAttribute('gen_ai.response.model', 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 const tokenUsage = rebuildTokenUsage(chunk.usage, tanstack?.usage)\n if (tokenUsage) {\n span.setAttributes(usageAttributes(tokenUsage))\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 state.rootUsageAttributes = accumulateUsageAttributes(\n state.rootUsageAttributes,\n usage,\n )\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 applyRootUsage(state)\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 applyRootUsage(state)\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 (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 applyRootUsage(state, info.usage)\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
+ {"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 { rebuildTokenUsage } from '../utilities/ag-ui-usage'\nimport { tanstackMetadata } from '../utilities/merge-metadata'\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'\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 provider model call (agent-loop `beforeModel`\n * turn, or the separate `structuredOutput` finalization when\n * `outputSchema` skips the agent loop — see #1054)\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 voice: 'voice_generation',\n transcription: 'transcription',\n embedding: 'embeddings',\n rerank: 'rerank',\n evaluate: 'evaluate',\n summarize: 'summarize',\n world: 'world_generation',\n liveVideo: 'live_video_generation',\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 rootUsageAttributes: Record<string, number> | null\n rootUsageApplied: boolean\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction accumulateUsageAttributes(\n current: Record<string, number> | null,\n usage: TokenUsage,\n): Record<string, number> {\n const accumulated = current ?? {}\n for (const [key, value] of Object.entries(usageAttributes(usage))) {\n if (typeof value === 'number') {\n accumulated[key] = (accumulated[key] ?? 0) + value\n }\n }\n return accumulated\n}\n\nfunction applyRootUsage(state: RequestState, fallbackUsage?: TokenUsage): void {\n if (state.rootUsageApplied) return\n\n const attributes =\n state.rootUsageAttributes ??\n (fallbackUsage ? usageAttributes(fallbackUsage) : null)\n if (attributes) state.rootSpan.setAttributes(attributes)\n state.rootUsageApplied = true\n}\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 rootUsageAttributes: null,\n rootUsageApplied: false,\n })\n })\n },\n\n onConfig(ctx, config) {\n // Open an iteration span for every provider model call:\n // - `beforeModel`: agent-loop chatStream turns\n // - `structuredOutput`: separate structured-output finalization\n // (no-tools + outputSchema skips the agent loop, so without this\n // phase there is no generation span and captureContent is a silent\n // no-op — see #1054).\n // Native-combined mode never fires `structuredOutput`, so a run that\n // already opened spans via `beforeModel` is not double-counted.\n if (ctx.phase !== 'beforeModel' && ctx.phase !== 'structuredOutput')\n 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 // Number spans by the order of model calls this middleware has seen,\n // not by `ctx.iteration`. After an agent-loop turn, structured-output\n // finalization reuses the engine's last iteration index; using our\n // own counter keeps finalization as a distinct leaf (#N+1).\n const iteration = state.iterationCount\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${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': 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 tanstack = tanstackMetadata(chunk)\n const extra = chunk as {\n finishReason?: string | null\n model?: string\n }\n const finishReason = extra.finishReason ?? tanstack?.finishReason\n const model = extra.model ?? tanstack?.model\n // Capture for the root-span finish_reasons attribute set in onFinish,\n // which receives base-shaped info without a finishReason field.\n if (finishReason) state.lastFinishReason = finishReason\n const span = state.currentIterationSpan\n if (!span) return\n\n if (finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [finishReason])\n }\n if (model) span.setAttribute('gen_ai.response.model', 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 const tokenUsage = rebuildTokenUsage(chunk.usage, tanstack?.usage)\n if (tokenUsage) {\n span.setAttributes(usageAttributes(tokenUsage))\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 state.rootUsageAttributes = accumulateUsageAttributes(\n state.rootUsageAttributes,\n usage,\n )\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 applyRootUsage(state)\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 applyRootUsage(state)\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 (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 applyRootUsage(state, info.usage)\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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"}
@@ -18,7 +18,7 @@ export type * from './types.js';
18
18
  * // On the server (e.g. inside a server route or framework server
19
19
  * // function), mint an ephemeral token for the client:
20
20
  * const token = await realtimeToken({
21
- * adapter: openaiRealtimeToken({ model: 'gpt-realtime' }),
21
+ * adapter: openaiRealtimeToken({ model: 'gpt-realtime-2.1' }),
22
22
  * })
23
23
  * ```
24
24
  */
@@ -17,7 +17,7 @@ import { createRealtimeEventEmitter } from "./event-emitter.js";
17
17
  * // On the server (e.g. inside a server route or framework server
18
18
  * // function), mint an ephemeral token for the client:
19
19
  * const token = await realtimeToken({
20
- * adapter: openaiRealtimeToken({ model: 'gpt-realtime' }),
20
+ * adapter: openaiRealtimeToken({ model: 'gpt-realtime-2.1' }),
21
21
  * })
22
22
  * ```
23
23
  */
@@ -1 +1 @@
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"}
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-2.1' }),\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"}
@@ -1977,6 +1977,53 @@ export interface LiveVideoGenerationResult {
1977
1977
  /** Token usage / billing, when the adapter can report it */
1978
1978
  usage?: TokenUsage;
1979
1979
  }
1980
+ /**
1981
+ * One turn of a multi-voice dialogue request.
1982
+ *
1983
+ * Providers that expose a dedicated dialogue endpoint (ElevenLabs
1984
+ * `textToDialogue`, Gemini multi-speaker) take these natively instead of a
1985
+ * single `text` + `voice` pair.
1986
+ */
1987
+ export interface TTSTurn {
1988
+ /** The text this voice speaks. */
1989
+ text: string;
1990
+ /** Provider voice id (ElevenLabs) or voice name (Gemini) for this turn. */
1991
+ voice: string;
1992
+ }
1993
+ /**
1994
+ * Timings for the generated audio, returned when `timestamps: true` was
1995
+ * requested and the adapter declares `capabilities.timestamps`.
1996
+ *
1997
+ * Granularity differs per provider — ElevenLabs reports characters, BytePlus
1998
+ * reports words — so `unit` says which, and the three arrays are parallel.
1999
+ * All times are **seconds**; adapters convert.
2000
+ */
2001
+ export interface TTSAlignment {
2002
+ /** Granularity of each entry. */
2003
+ unit: 'character' | 'word';
2004
+ /** Entry text, in audio order. */
2005
+ texts: Array<string>;
2006
+ /** Start of each entry in seconds. Same length as `texts`. */
2007
+ startSeconds: Array<number>;
2008
+ /** End of each entry in seconds. Same length as `texts`. */
2009
+ endSeconds: Array<number>;
2010
+ }
2011
+ /**
2012
+ * A stretch of audio attributable to one turn (multi-voice) or one utterance
2013
+ * (single voice). This is what tells a consumer which turn is where.
2014
+ */
2015
+ export interface TTSSegment {
2016
+ /** Start of the segment in seconds. */
2017
+ startSeconds: number;
2018
+ /** End of the segment in seconds. */
2019
+ endSeconds: number;
2020
+ /** Index into the request's `turns`, when the provider reports it. */
2021
+ turnIndex?: number;
2022
+ /** Voice heard in this segment, when the provider reports it. */
2023
+ voice?: string;
2024
+ /** Text spoken in this segment, when the provider reports it. */
2025
+ text?: string;
2026
+ }
1980
2027
  /**
1981
2028
  * Options for text-to-speech generation.
1982
2029
  * These are the common options supported across providers.
@@ -1984,8 +2031,24 @@ export interface LiveVideoGenerationResult {
1984
2031
  export interface TTSOptions<TProviderOptions extends object = object> {
1985
2032
  /** The model to use for TTS generation */
1986
2033
  model: string;
1987
- /** The text to convert to speech */
2034
+ /**
2035
+ * The text to convert to speech. When the caller passed `turns`, the
2036
+ * activity fills this with the turn texts joined by newlines so adapters
2037
+ * that only read `text` still receive the full script.
2038
+ */
1988
2039
  text: string;
2040
+ /**
2041
+ * Multi-voice dialogue turns, when the caller asked for dialogue. Only
2042
+ * adapters that declare `capabilities.maxSpeakers` ever see this — the
2043
+ * activity rejects `turns` for the rest.
2044
+ */
2045
+ turns?: Array<TTSTurn>;
2046
+ /**
2047
+ * Ask for `alignment` / `segments` on the result. Rejected by the activity
2048
+ * unless the adapter declares `capabilities.timestamps`, because on some
2049
+ * providers this is a different endpoint rather than free metadata.
2050
+ */
2051
+ timestamps?: boolean;
1989
2052
  /** The voice to use for generation */
1990
2053
  voice?: string;
1991
2054
  /** The output audio format */
@@ -2019,8 +2082,18 @@ export interface TTSResult {
2019
2082
  audio: string;
2020
2083
  /** Audio format of the generated audio */
2021
2084
  format: string;
2022
- /** Duration of the audio in seconds, if available */
2085
+ /** Duration of the audio file in seconds, if available */
2023
2086
  duration?: number;
2087
+ /**
2088
+ * Character- or word-level timings, present when `timestamps: true` was
2089
+ * requested. Use this rather than `duration` to find where *speech* ends.
2090
+ */
2091
+ alignment?: TTSAlignment;
2092
+ /**
2093
+ * Per-turn (or per-utterance) spans of the audio, present when
2094
+ * `timestamps: true` was requested and the provider reports segmentation.
2095
+ */
2096
+ segments?: Array<TTSSegment>;
2024
2097
  /** Content type of the audio (e.g., 'audio/mp3') */
2025
2098
  contentType?: string;
2026
2099
  /** Token usage information (if provided by the adapter) */
@@ -2028,6 +2101,156 @@ export interface TTSResult {
2028
2101
  /** Persisted artifact references for generated assets, when available */
2029
2102
  artifacts?: Array<PersistedArtifactRef>;
2030
2103
  }
2104
+ /**
2105
+ * Where a voice in a provider's catalog came from.
2106
+ *
2107
+ * `'premade'` is the provider's own stock catalog. `'generated'` and
2108
+ * `'cloned'` are voices the account made, which is what `generateVoice()`
2109
+ * produces. `'professional'` covers a provider's curated or paid tiers.
2110
+ */
2111
+ export type VoiceOrigin = 'premade' | 'generated' | 'cloned' | 'professional';
2112
+ /** One voice from a provider's catalog. */
2113
+ export interface CatalogVoice {
2114
+ /** Pass this to `generateSpeech()` as `voice` */
2115
+ voiceId: string;
2116
+ /** Display name, when the provider stores one */
2117
+ name?: string;
2118
+ /** Where the voice came from */
2119
+ origin?: VoiceOrigin;
2120
+ /** Provider description of the voice */
2121
+ description?: string;
2122
+ /** URL of a sample, when the provider hosts one */
2123
+ previewUrl?: string;
2124
+ /** Provider labels, such as accent, age, or use case */
2125
+ labels?: Record<string, string>;
2126
+ }
2127
+ /** Options for listing a provider's voices. */
2128
+ export interface ListVoicesOptions {
2129
+ /**
2130
+ * Restrict the result to voices of these origins. Adapters filter server
2131
+ * side when the provider supports it, and in memory otherwise.
2132
+ */
2133
+ origins?: Array<VoiceOrigin>;
2134
+ /**
2135
+ * Effective abort signal. Adapters forward this to the provider SDK when
2136
+ * supported.
2137
+ */
2138
+ abortSignal?: AbortSignal;
2139
+ }
2140
+ /** Result of listing a provider's voices. */
2141
+ export interface ListVoicesResult {
2142
+ /** The voices available to this account */
2143
+ voices: Array<CatalogVoice>;
2144
+ }
2145
+ /**
2146
+ * Options for creating a voice.
2147
+ *
2148
+ * Providers create voices in one of two ways, and some support both:
2149
+ * - **design** — synthesize a brand new voice from a text {@link prompt}.
2150
+ * - **clone** — derive a voice from {@link referenceAudio} of a real speaker.
2151
+ *
2152
+ * At least one of `prompt` / `referenceAudio` is required; which ones an
2153
+ * adapter accepts depends on the model. An adapter may require both — the
2154
+ * only adapter today, `elevenlabsVoiceDesign`, always needs `prompt` and
2155
+ * takes `referenceAudio` as an additional design reference.
2156
+ */
2157
+ export interface VoiceGenerationOptions<TProviderOptions extends object = object> {
2158
+ /** The model to use for voice creation */
2159
+ model: string;
2160
+ /** Text description of the voice to create, for design-capable models */
2161
+ prompt?: string;
2162
+ /**
2163
+ * Reference audio of the speaker to clone - base64 string, base64 data URL,
2164
+ * File, Blob, or ArrayBuffer. For clone-capable models. Remote URLs are not
2165
+ * accepted; read the file and pass the bytes.
2166
+ */
2167
+ referenceAudio?: string | File | Blob | ArrayBuffer;
2168
+ /**
2169
+ * Name to store the voice under in the provider's voice library. Providers
2170
+ * differ on what this implies — ElevenLabs only persists a designed voice
2171
+ * when a name is given. Read {@link GeneratedVoice.saved} to find out what
2172
+ * actually happened.
2173
+ */
2174
+ name?: string;
2175
+ /** Human-readable description stored alongside the voice */
2176
+ description?: string;
2177
+ /** Model-specific options for voice creation */
2178
+ modelOptions?: TProviderOptions;
2179
+ /**
2180
+ * Internal logger threaded from the generateVoice() entry point. Adapters
2181
+ * must call logger.request() before the SDK call and logger.errors() in
2182
+ * catch blocks.
2183
+ */
2184
+ logger: InternalLogger;
2185
+ /**
2186
+ * Effective abort signal composed by the activity from caller `abortSignal`
2187
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2188
+ * supported. Request-specific - never store on a global client config.
2189
+ */
2190
+ abortSignal?: AbortSignal;
2191
+ }
2192
+ /**
2193
+ * A single voice produced by {@link VoiceGenerationOptions}.
2194
+ */
2195
+ export interface GeneratedVoice {
2196
+ /**
2197
+ * The provider's voice identifier. Pass it straight back as the `voice`
2198
+ * option on `generateSpeech()`.
2199
+ */
2200
+ voiceId: string;
2201
+ /** Base64-encoded preview audio, when the provider returns one */
2202
+ audio?: string;
2203
+ /** Audio format of the preview (e.g. 'mp3') */
2204
+ format?: string;
2205
+ /** Content type of the preview (e.g. 'audio/mpeg') */
2206
+ contentType?: string;
2207
+ /** Duration of the preview in seconds, if available */
2208
+ duration?: number;
2209
+ /** Language of the preview, if reported */
2210
+ language?: string;
2211
+ /**
2212
+ * Whether the voice is persisted in the provider's voice library. Unsaved
2213
+ * voices are previews and generally expire.
2214
+ */
2215
+ saved: boolean;
2216
+ /**
2217
+ * Whether the voice can be used in `generateSpeech()` yet. Required so a
2218
+ * caller never has to guess: every adapter states it outright.
2219
+ */
2220
+ status: VoiceTrainingStatus;
2221
+ }
2222
+ /**
2223
+ * Whether a created voice is usable.
2224
+ *
2225
+ * - `'ready'` — usable in `generateSpeech()` now. Every adapter today returns
2226
+ * this, because they all finish the voice inside `generateVoice()`.
2227
+ * - `'training'` — the provider accepted the request but is still building
2228
+ * the voice, so it is not usable yet. Reserved for providers that train
2229
+ * asynchronously; no adapter returns it yet, and reading the state back
2230
+ * will land with the first adapter that needs it.
2231
+ * - `'failed'` — the provider finished without producing a usable voice.
2232
+ */
2233
+ export type VoiceTrainingStatus = 'ready' | 'training' | 'failed';
2234
+ /**
2235
+ * Result of voice creation.
2236
+ *
2237
+ * Design models typically return several candidates to choose between; clone
2238
+ * models return exactly one.
2239
+ */
2240
+ export interface VoiceResult {
2241
+ /** Unique identifier for the generation */
2242
+ id: string;
2243
+ /** Model used for generation */
2244
+ model: string;
2245
+ /** The voices produced, best-first when the provider ranks them */
2246
+ voices: Array<GeneratedVoice>;
2247
+ /** The line spoken in the previews, when the provider generated one */
2248
+ previewText?: string;
2249
+ /** Token usage information (if provided by the adapter) */
2250
+ usage?: TokenUsage;
2251
+ /** Persisted artifact references for generated assets, when available */
2252
+ artifacts?: Array<PersistedArtifactRef>;
2253
+ }
2031
2254
  /**
2032
2255
  * Options for audio transcription.
2033
2256
  * These are the common options supported across providers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.54.0",
3
+ "version": "0.57.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",
@@ -88,7 +88,7 @@
88
88
  "@ag-ui/core": "0.1.1-canary.beta.0",
89
89
  "@standard-schema/spec": "^1.1.0",
90
90
  "partial-json": "^0.1.7",
91
- "@tanstack/ai-event-client": "^0.11.3",
91
+ "@tanstack/ai-event-client": "^0.12.0",
92
92
  "@tanstack/ai-utils": "^0.4.0"
93
93
  },
94
94
  "peerDependencies": {
@@ -129,7 +129,7 @@ const adapterWithKey = createOpenaiChat('gpt-5.2', 'sk-...')
129
129
 
130
130
  - `bedrockText(model)` or `bedrockText(model, { api: 'converse' })` (the default) — Bedrock's native Converse API via `@aws-sdk/client-bedrock-runtime` (adapter name `bedrock-converse`). Reaches the broad catalog: Claude, Nova, Llama, Mistral, DeepSeek, and more.
131
131
  - `bedrockText(model, { api: 'chat' })` — OpenAI-compatible Chat Completions endpoint (adapter name `bedrock`). Open-weight models only (gpt-oss, DeepSeek V3.x, Gemma, Qwen, etc.). Does NOT reach Claude, Nova, or Llama.
132
- - `bedrockText(model, { api: 'responses' })` — OpenAI-compatible Responses API, mantle-only (adapter name `bedrock-responses`). Currently gpt-oss family.
132
+ - `bedrockText(model, { api: 'responses' })` — OpenAI-compatible Responses API, mantle-only (adapter name `bedrock-responses`). Currently gpt-oss and Gemma 4.
133
133
 
134
134
  Use `createBedrockText(model, apiKey, config?)` to pass the key explicitly. Auth resolves from `BEDROCK_API_KEY` / `AWS_BEARER_TOKEN_BEDROCK`, or SigV4 via the standard AWS credential chain (no extra packages needed — handled by `@aws-sdk/client-bedrock-runtime`).
135
135