@tanstack/ai 0.29.0 → 0.31.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.
- package/dist/esm/activities/chat/adapter.d.ts +10 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +3 -2
- package/dist/esm/activities/chat/index.js +13 -1
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.d.ts +46 -0
- package/dist/esm/activities/chat/middleware/builder.js +17 -0
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -0
- package/dist/esm/activities/chat/middleware/capabilities.d.ts +93 -0
- package/dist/esm/activities/chat/middleware/capabilities.js +45 -0
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -0
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -0
- package/dist/esm/activities/chat/middleware/compose.js +47 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.d.ts +20 -0
- package/dist/esm/activities/chat/middleware/define.js +7 -0
- package/dist/esm/activities/chat/middleware/define.js.map +1 -0
- package/dist/esm/activities/chat/middleware/index.d.ts +8 -0
- package/dist/esm/activities/chat/middleware/types.d.ts +51 -0
- package/dist/esm/activities/chat/middleware/validate.d.ts +19 -0
- package/dist/esm/activities/chat/middleware/validate.js +30 -0
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.js +40 -12
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/package.json +10 -2
- package/src/activities/chat/adapter.ts +11 -0
- package/src/activities/chat/index.ts +20 -1
- package/src/activities/chat/middleware/builder.ts +109 -0
- package/src/activities/chat/middleware/capabilities.ts +162 -0
- package/src/activities/chat/middleware/compose.ts +51 -0
- package/src/activities/chat/middleware/define.ts +34 -0
- package/src/activities/chat/middleware/index.ts +20 -0
- package/src/activities/chat/middleware/types.ts +60 -0
- package/src/activities/chat/middleware/validate.ts +55 -0
- package/src/index.ts +14 -0
- package/src/middlewares/otel.ts +57 -12
package/dist/esm/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { brandProviderTool } from './tools/provider-tool.js';
|
|
|
18
18
|
export { maxIterations, untilFinishReason, combineStrategies, } from './activities/chat/agent-loop-strategies.js';
|
|
19
19
|
export { createToolRegistry, createFrozenRegistry, type ToolRegistry, } from './tool-registry.js';
|
|
20
20
|
export type { ChatMiddleware, ChatMiddlewareContext, ChatMiddlewarePhase, ChatMiddlewareConfig, StructuredOutputMiddlewareConfig, ToolCallHookContext, BeforeToolCallDecision, AfterToolCallInfo, IterationInfo, ToolPhaseCompleteInfo, UsageInfo, FinishInfo, AbortInfo, ErrorInfo, } from './activities/chat/middleware/index.js';
|
|
21
|
+
export { createCapability, defineChatMiddleware, createChatMiddleware, } from './activities/chat/middleware/index.js';
|
|
22
|
+
export type { Capability, CapabilityHandle, CapabilityContext, CapabilityGetter, CapabilityProvider, } from './activities/chat/middleware/index.js';
|
|
21
23
|
export * from './types.js';
|
|
22
24
|
export { buildBaseUsage, type BaseUsageInput } from './utilities/usage.js';
|
|
23
25
|
export type { SystemPrompt, NormalizedSystemPrompt } from './system-prompts.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -27,6 +27,9 @@ import { BatchStrategy, CompositeStrategy, ImmediateStrategy, PunctuationStrateg
|
|
|
27
27
|
import { EventType } from "@ag-ui/core";
|
|
28
28
|
import { PartialJSONParser, defaultJSONParser, parsePartialJSON } from "./activities/chat/stream/json-parser.js";
|
|
29
29
|
import { StreamProcessor, createReplayStream } from "./activities/chat/stream/processor.js";
|
|
30
|
+
import { createCapability } from "./activities/chat/middleware/capabilities.js";
|
|
31
|
+
import { createChatMiddleware } from "./activities/chat/middleware/builder.js";
|
|
32
|
+
import { defineChatMiddleware } from "./activities/chat/middleware/define.js";
|
|
30
33
|
export {
|
|
31
34
|
BatchStrategy,
|
|
32
35
|
CompositeStrategy,
|
|
@@ -49,6 +52,8 @@ export {
|
|
|
49
52
|
convertMessagesToModelMessages,
|
|
50
53
|
convertSchemaToJsonSchema,
|
|
51
54
|
createAudioOptions,
|
|
55
|
+
createCapability,
|
|
56
|
+
createChatMiddleware,
|
|
52
57
|
createChatOptions,
|
|
53
58
|
createFrozenRegistry,
|
|
54
59
|
createImageOptions,
|
|
@@ -60,6 +65,7 @@ export {
|
|
|
60
65
|
createTranscriptionOptions,
|
|
61
66
|
createVideoOptions,
|
|
62
67
|
defaultJSONParser,
|
|
68
|
+
defineChatMiddleware,
|
|
63
69
|
detectImageMimeType,
|
|
64
70
|
extendAdapter,
|
|
65
71
|
generateAudio,
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -59,6 +59,43 @@ function firstNumber(...candidates) {
|
|
|
59
59
|
}
|
|
60
60
|
return void 0;
|
|
61
61
|
}
|
|
62
|
+
function usageAttributes(usage) {
|
|
63
|
+
const attrs = {
|
|
64
|
+
"gen_ai.usage.input_tokens": usage.promptTokens,
|
|
65
|
+
"gen_ai.usage.output_tokens": usage.completionTokens
|
|
66
|
+
};
|
|
67
|
+
const optional = [
|
|
68
|
+
["gen_ai.usage.total_tokens", usage.totalTokens],
|
|
69
|
+
["gen_ai.usage.cost", usage.cost],
|
|
70
|
+
[
|
|
71
|
+
"gen_ai.usage.cache_read.input_tokens",
|
|
72
|
+
usage.promptTokensDetails?.cachedTokens
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"gen_ai.usage.cache_creation.input_tokens",
|
|
76
|
+
usage.promptTokensDetails?.cacheWriteTokens
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"gen_ai.usage.reasoning.output_tokens",
|
|
80
|
+
usage.completionTokensDetails?.reasoningTokens
|
|
81
|
+
],
|
|
82
|
+
["tanstack.ai.usage.duration_seconds", usage.durationSeconds],
|
|
83
|
+
["tanstack.ai.usage.upstream_cost", usage.costDetails?.upstreamCost],
|
|
84
|
+
[
|
|
85
|
+
"tanstack.ai.usage.upstream_input_cost",
|
|
86
|
+
usage.costDetails?.upstreamInputCost
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
"tanstack.ai.usage.upstream_output_cost",
|
|
90
|
+
usage.costDetails?.upstreamOutputCost
|
|
91
|
+
]
|
|
92
|
+
];
|
|
93
|
+
for (const [key, value] of optional) {
|
|
94
|
+
const num = firstNumber(value);
|
|
95
|
+
if (num !== void 0) attrs[key] = num;
|
|
96
|
+
}
|
|
97
|
+
return attrs;
|
|
98
|
+
}
|
|
62
99
|
function errorMessage(err) {
|
|
63
100
|
if (err instanceof Error) return err.message;
|
|
64
101
|
if (typeof err === "string") return err;
|
|
@@ -320,10 +357,7 @@ function otelMiddleware(options) {
|
|
|
320
357
|
}
|
|
321
358
|
if (chunk.model) span.setAttribute("gen_ai.response.model", chunk.model);
|
|
322
359
|
if (chunk.usage) {
|
|
323
|
-
span.setAttributes(
|
|
324
|
-
"gen_ai.usage.input_tokens": chunk.usage.promptTokens,
|
|
325
|
-
"gen_ai.usage.output_tokens": chunk.usage.completionTokens
|
|
326
|
-
});
|
|
360
|
+
span.setAttributes(usageAttributes(chunk.usage));
|
|
327
361
|
}
|
|
328
362
|
if (captureContent && state.assistantTextBuffer.length > 0) {
|
|
329
363
|
const completion = redactContent(state.assistantTextBuffer);
|
|
@@ -361,10 +395,7 @@ function otelMiddleware(options) {
|
|
|
361
395
|
});
|
|
362
396
|
}
|
|
363
397
|
const span = state.currentIterationSpan ?? state.rootSpan;
|
|
364
|
-
span.setAttributes(
|
|
365
|
-
"gen_ai.usage.input_tokens": usage.promptTokens,
|
|
366
|
-
"gen_ai.usage.output_tokens": usage.completionTokens
|
|
367
|
-
});
|
|
398
|
+
span.setAttributes(usageAttributes(usage));
|
|
368
399
|
});
|
|
369
400
|
},
|
|
370
401
|
onBeforeToolCall(ctx, hookCtx) {
|
|
@@ -637,10 +668,7 @@ function otelMiddleware(options) {
|
|
|
637
668
|
});
|
|
638
669
|
}
|
|
639
670
|
if (info.usage) {
|
|
640
|
-
state.rootSpan.setAttributes(
|
|
641
|
-
"gen_ai.usage.input_tokens": info.usage.promptTokens,
|
|
642
|
-
"gen_ai.usage.output_tokens": info.usage.completionTokens
|
|
643
|
-
});
|
|
671
|
+
state.rootSpan.setAttributes(usageAttributes(info.usage));
|
|
644
672
|
}
|
|
645
673
|
if (info.finishReason) {
|
|
646
674
|
state.rootSpan.setAttribute("gen_ai.response.finish_reasons", [
|
|
@@ -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'\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\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({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\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({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\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({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\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 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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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|
|
7
|
+
"homepage": "https://tanstack.com/ai",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
11
|
"directory": "packages/ai"
|
|
11
12
|
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/TanStack/ai/issues"
|
|
15
|
+
},
|
|
16
|
+
"funding": {
|
|
17
|
+
"type": "github",
|
|
18
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
19
|
+
},
|
|
12
20
|
"type": "module",
|
|
13
21
|
"module": "./dist/esm/index.js",
|
|
14
22
|
"types": "./dist/esm/index.d.ts",
|
|
@@ -68,7 +76,7 @@
|
|
|
68
76
|
"@ag-ui/core": "^0.0.52",
|
|
69
77
|
"@standard-schema/spec": "^1.1.0",
|
|
70
78
|
"partial-json": "^0.1.7",
|
|
71
|
-
"@tanstack/ai-event-client": "0.6.
|
|
79
|
+
"@tanstack/ai-event-client": "0.6.2"
|
|
72
80
|
},
|
|
73
81
|
"peerDependencies": {
|
|
74
82
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TextOptions,
|
|
7
7
|
TokenUsage,
|
|
8
8
|
} from '../../types'
|
|
9
|
+
import type { CapabilityHandle } from './middleware/capabilities'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Configuration for adapter instances
|
|
@@ -79,6 +80,15 @@ export interface TextAdapter<
|
|
|
79
80
|
/** The model this adapter is configured for */
|
|
80
81
|
readonly model: TModel
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Capabilities this adapter requires at runtime. `chat()` validates that the
|
|
85
|
+
* configured middleware provides each one. Model adapters omit this; harness
|
|
86
|
+
* adapters (e.g. a future `claudeCode()`) declare e.g. `[sandboxCapability]`.
|
|
87
|
+
* Runtime access to capabilities from inside the adapter is not yet wired —
|
|
88
|
+
* this is the declaration/validation surface only.
|
|
89
|
+
*/
|
|
90
|
+
readonly requires?: ReadonlyArray<CapabilityHandle>
|
|
91
|
+
|
|
82
92
|
/**
|
|
83
93
|
* @internal Type-only properties for inference. Not assigned at runtime.
|
|
84
94
|
*/
|
|
@@ -183,6 +193,7 @@ export abstract class BaseTextAdapter<
|
|
|
183
193
|
readonly kind = 'text' as const
|
|
184
194
|
abstract readonly name: string
|
|
185
195
|
readonly model: TModel
|
|
196
|
+
readonly requires?: ReadonlyArray<CapabilityHandle> = undefined
|
|
186
197
|
|
|
187
198
|
// Type-only property - never assigned at runtime
|
|
188
199
|
declare '~types': {
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
26
26
|
import { convertMessagesToModelMessages, generateMessageId } from './messages'
|
|
27
27
|
import { MiddlewareRunner } from './middleware/compose'
|
|
28
|
+
import { CapabilityRegistry } from './middleware/capabilities'
|
|
29
|
+
import { validateCapabilities } from './middleware/validate'
|
|
28
30
|
import { MCPManager } from './mcp/manager'
|
|
29
31
|
import type {
|
|
30
32
|
ApprovalRequest,
|
|
@@ -54,11 +56,13 @@ import type {
|
|
|
54
56
|
UIMessage,
|
|
55
57
|
} from '../../types'
|
|
56
58
|
import type {
|
|
59
|
+
AnyChatMiddleware,
|
|
57
60
|
ChatMiddleware,
|
|
58
61
|
ChatMiddlewareConfig,
|
|
59
62
|
ChatMiddlewareContext,
|
|
60
63
|
StructuredOutputMiddlewareConfig,
|
|
61
64
|
} from './middleware/types'
|
|
65
|
+
import type { CheckCoverage } from './middleware/builder'
|
|
62
66
|
import type { SystemPrompt } from '../../system-prompts'
|
|
63
67
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
64
68
|
import type { DebugOption } from '../../logger/types'
|
|
@@ -141,7 +145,8 @@ type TextActivityOptionsWithContext<
|
|
|
141
145
|
'tools' | 'middleware' | 'context'
|
|
142
146
|
> & {
|
|
143
147
|
tools?: TTools
|
|
144
|
-
middleware?: TMiddleware
|
|
148
|
+
middleware?: TMiddleware &
|
|
149
|
+
CheckCoverage<Extract<TMiddleware, ReadonlyArray<AnyChatMiddleware>>>
|
|
145
150
|
} & RequiredContextFromInputs<TTools, TMiddleware>
|
|
146
151
|
|
|
147
152
|
// ===========================
|
|
@@ -659,6 +664,15 @@ class TextEngine<
|
|
|
659
664
|
// References
|
|
660
665
|
messages: this.messages,
|
|
661
666
|
createId: (prefix: string) => this.createId(prefix),
|
|
667
|
+
// Capability bookkeeping for this request (populated by middleware setup)
|
|
668
|
+
capabilities: new CapabilityRegistry(),
|
|
669
|
+
// Convenience accessors that delegate to a capability handle's own
|
|
670
|
+
// tuple getter/provider, keyed by this context. `getX(ctx)` and
|
|
671
|
+
// `ctx.get(X)` are interchangeable.
|
|
672
|
+
get: (capability) => capability[0](this.middlewareCtx),
|
|
673
|
+
getOptional: (capability) =>
|
|
674
|
+
capability[0](this.middlewareCtx, { optional: true }),
|
|
675
|
+
provide: (capability, value) => capability[1](this.middlewareCtx, value),
|
|
662
676
|
}
|
|
663
677
|
}
|
|
664
678
|
|
|
@@ -706,6 +720,9 @@ class TextEngine<
|
|
|
706
720
|
})
|
|
707
721
|
|
|
708
722
|
try {
|
|
723
|
+
// Provision capabilities before any consumer (onConfig onward) can read them
|
|
724
|
+
await this.middlewareRunner.runSetup(this.middlewareCtx)
|
|
725
|
+
|
|
709
726
|
// Run initial onConfig (phase = init)
|
|
710
727
|
this.middlewareCtx.phase = 'init'
|
|
711
728
|
const initialConfig = this.buildMiddlewareConfig()
|
|
@@ -2564,6 +2581,8 @@ export function chat<
|
|
|
2564
2581
|
TMiddleware
|
|
2565
2582
|
>,
|
|
2566
2583
|
): TextActivityResult<TSchema, TStream> {
|
|
2584
|
+
validateCapabilities(options.middleware ?? [], options.adapter)
|
|
2585
|
+
|
|
2567
2586
|
const { outputSchema, stream } = options
|
|
2568
2587
|
|
|
2569
2588
|
// outputSchema + stream:true is the only branch that streams structured
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CapabilityHandle } from './capabilities'
|
|
2
|
+
import type { AnyChatMiddleware, ChatMiddleware } from './types'
|
|
3
|
+
import type { DefinedChatMiddleware } from './define'
|
|
4
|
+
|
|
5
|
+
/** Union of capability NAME literals from a tuple of handles. */
|
|
6
|
+
export type NamesOf<T extends ReadonlyArray<CapabilityHandle>> =
|
|
7
|
+
T[number]['capabilityName']
|
|
8
|
+
|
|
9
|
+
/** Names provided across a middleware array (imprecise middleware → `string`). */
|
|
10
|
+
export type ProvidedNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
|
|
11
|
+
NonNullable<TList[number]['provides']> extends infer P
|
|
12
|
+
? P extends ReadonlyArray<CapabilityHandle>
|
|
13
|
+
? NamesOf<P>
|
|
14
|
+
: never
|
|
15
|
+
: never
|
|
16
|
+
|
|
17
|
+
/** Names required across a middleware array. */
|
|
18
|
+
export type RequiredNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
|
|
19
|
+
NonNullable<TList[number]['requires']> extends infer P
|
|
20
|
+
? P extends ReadonlyArray<CapabilityHandle>
|
|
21
|
+
? NamesOf<P>
|
|
22
|
+
: never
|
|
23
|
+
: never
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Branded marker surfaced when required capability names are missing from the
|
|
27
|
+
* provided set, so the compiler error names the gap instead of emitting an
|
|
28
|
+
* opaque "not assignable".
|
|
29
|
+
*/
|
|
30
|
+
export type MissingCapabilities<TMissing extends string> = {
|
|
31
|
+
// The human-readable message lives in the property KEY, so TypeScript's
|
|
32
|
+
// "Property '<key>' is missing in type ... but required in type ..." error
|
|
33
|
+
// prints the explanation instead of an opaque `__missingCapabilities`. The
|
|
34
|
+
// key distributes over a union of missing names (one required key each).
|
|
35
|
+
[K in `✖ Missing capability "${TMissing}": no configured middleware provides it. Add a middleware whose \`provides\` includes it (and, with createChatMiddleware().use(), order the provider before this consumer).`]: never
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Missing capability names. When required names are imprecise (`string`, i.e.
|
|
40
|
+
* plain `ChatMiddleware` not authored via `defineChatMiddleware`), we cannot
|
|
41
|
+
* prove a gap, so we allow it (→ `never`). Otherwise the precise literals not
|
|
42
|
+
* present in the provided set.
|
|
43
|
+
*/
|
|
44
|
+
type MissingNames<TList extends ReadonlyArray<AnyChatMiddleware>> =
|
|
45
|
+
string extends RequiredNames<TList>
|
|
46
|
+
? never
|
|
47
|
+
: Exclude<RequiredNames<TList>, ProvidedNames<TList>>
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves to `TList` when coverage holds, otherwise to a `MissingCapabilities`
|
|
51
|
+
* marker (not assignable to a middleware array) — producing a compile error at
|
|
52
|
+
* the `middleware` option that names the missing capability.
|
|
53
|
+
*/
|
|
54
|
+
export type CheckCoverage<TList extends ReadonlyArray<AnyChatMiddleware>> = [
|
|
55
|
+
MissingNames<TList>,
|
|
56
|
+
] extends [never]
|
|
57
|
+
? TList
|
|
58
|
+
: MissingCapabilities<MissingNames<TList>>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Order-aware middleware builder. Each `.use()` requires that the middleware's
|
|
62
|
+
* required capability names are already in the accumulated provided set, then
|
|
63
|
+
* adds its provided names. `.build()` returns the ordered array.
|
|
64
|
+
*
|
|
65
|
+
* `TProvided` is the running union of provided capability name literals.
|
|
66
|
+
*/
|
|
67
|
+
export interface ChatMiddlewareBuilder<
|
|
68
|
+
TList extends ReadonlyArray<AnyChatMiddleware>,
|
|
69
|
+
TProvided extends string,
|
|
70
|
+
> {
|
|
71
|
+
use: <
|
|
72
|
+
TRequires extends ReadonlyArray<CapabilityHandle>,
|
|
73
|
+
TProvides extends ReadonlyArray<CapabilityHandle>,
|
|
74
|
+
TContext = unknown,
|
|
75
|
+
>(
|
|
76
|
+
middleware: [NamesOf<TRequires>] extends [TProvided]
|
|
77
|
+
? DefinedChatMiddleware<TContext, TRequires, TProvides>
|
|
78
|
+
: DefinedChatMiddleware<TContext, TRequires, TProvides> &
|
|
79
|
+
MissingCapabilities<Exclude<NamesOf<TRequires>, TProvided>>,
|
|
80
|
+
) => ChatMiddlewareBuilder<
|
|
81
|
+
readonly [...TList, DefinedChatMiddleware<TContext, TRequires, TProvides>],
|
|
82
|
+
TProvided | NamesOf<TProvides>
|
|
83
|
+
>
|
|
84
|
+
|
|
85
|
+
build: () => [...TList]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Create an order-aware middleware builder. */
|
|
89
|
+
export function createChatMiddleware(): ChatMiddlewareBuilder<
|
|
90
|
+
readonly [],
|
|
91
|
+
never
|
|
92
|
+
> {
|
|
93
|
+
const list: Array<ChatMiddleware<unknown>> = []
|
|
94
|
+
const builder = {
|
|
95
|
+
use(middleware: ChatMiddleware<unknown>) {
|
|
96
|
+
list.push(middleware)
|
|
97
|
+
return builder
|
|
98
|
+
},
|
|
99
|
+
build() {
|
|
100
|
+
return list
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
// The only sanctioned assertion in this PR: the runtime `builder` is a single
|
|
104
|
+
// object reused across `.use()` calls, but the type accumulates `TProvided`
|
|
105
|
+
// and `TList` per call — TypeScript cannot derive that from runtime values, so
|
|
106
|
+
// a structural `as` is impossible and the double assertion is irreducible.
|
|
107
|
+
// eslint-disable-next-line no-restricted-syntax -- irreducible: type-level accumulation cannot be expressed from a single runtime object
|
|
108
|
+
return builder as unknown as ChatMiddlewareBuilder<readonly [], never>
|
|
109
|
+
}
|