@ai-sdk/otel 0.0.0-6b196531-20260710185421

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.
@@ -0,0 +1,74 @@
1
+ import {
2
+ SpanStatusCode,
3
+ context,
4
+ type Attributes,
5
+ type Span,
6
+ type Tracer,
7
+ } from '@opentelemetry/api';
8
+ export async function recordSpan<T>({
9
+ name,
10
+ tracer,
11
+ attributes,
12
+ fn,
13
+ endWhenDone = true,
14
+ }: {
15
+ name: string;
16
+ tracer: Tracer;
17
+ attributes: Attributes | PromiseLike<Attributes>;
18
+ fn: (span: Span) => Promise<T>;
19
+ endWhenDone?: boolean;
20
+ }) {
21
+ return tracer.startActiveSpan(
22
+ name,
23
+ { attributes: await attributes },
24
+ async span => {
25
+ // Capture the current context to maintain it across async generator yields
26
+ const ctx = context.active();
27
+
28
+ try {
29
+ // Execute within the captured context to ensure async generators
30
+ // don't lose the active span when they yield
31
+ const result = await context.with(ctx, () => fn(span));
32
+
33
+ if (endWhenDone) {
34
+ span.end();
35
+ }
36
+
37
+ return result;
38
+ } catch (error) {
39
+ try {
40
+ recordErrorOnSpan(span, error);
41
+ } finally {
42
+ // always stop the span when there is an error:
43
+ span.end();
44
+ }
45
+
46
+ throw error;
47
+ }
48
+ },
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Record an error on a span. Sets the span status to error. If the error is
54
+ * an instance of Error, an exception event with name, message, and stack
55
+ * will also be recorded.
56
+ *
57
+ * @param span - The span to record the error on.
58
+ * @param error - The error to record on the span.
59
+ */
60
+ export function recordErrorOnSpan(span: Span, error: unknown) {
61
+ if (error instanceof Error) {
62
+ span.recordException({
63
+ name: error.name,
64
+ message: error.message,
65
+ stack: error.stack,
66
+ });
67
+ span.setStatus({
68
+ code: SpanStatusCode.ERROR,
69
+ message: error.message,
70
+ });
71
+ } else {
72
+ span.setStatus({ code: SpanStatusCode.ERROR });
73
+ }
74
+ }
@@ -0,0 +1,53 @@
1
+ import type { Attributes, AttributeValue } from '@opentelemetry/api';
2
+
3
+ function isPrimitiveAttributeValue(
4
+ value: unknown,
5
+ ): value is string | number | boolean {
6
+ return (
7
+ typeof value === 'string' ||
8
+ typeof value === 'number' ||
9
+ typeof value === 'boolean'
10
+ );
11
+ }
12
+
13
+ export function sanitizeAttributeValue(
14
+ value: AttributeValue,
15
+ ): AttributeValue | undefined {
16
+ if (!Array.isArray(value)) {
17
+ return value;
18
+ }
19
+
20
+ const primitiveTypes = new Set(
21
+ value.filter(isPrimitiveAttributeValue).map(item => typeof item),
22
+ );
23
+
24
+ if (primitiveTypes.size !== 1) {
25
+ return undefined;
26
+ }
27
+
28
+ const [primitiveType] = primitiveTypes;
29
+
30
+ if (primitiveType === 'string') {
31
+ return value.filter((item): item is string => typeof item === 'string');
32
+ }
33
+
34
+ if (primitiveType === 'number') {
35
+ return value.filter((item): item is number => typeof item === 'number');
36
+ }
37
+
38
+ return value.filter((item): item is boolean => typeof item === 'boolean');
39
+ }
40
+
41
+ export function sanitizeAttributes(
42
+ attributes: Attributes | undefined,
43
+ ): Attributes {
44
+ const result: Attributes = {};
45
+
46
+ for (const [key, value] of Object.entries(attributes ?? {})) {
47
+ if (value == null) continue;
48
+ const sanitized = sanitizeAttributeValue(value);
49
+ if (sanitized != null) result[key] = sanitized;
50
+ }
51
+
52
+ return result;
53
+ }
@@ -0,0 +1,65 @@
1
+ import type { Attributes, AttributeValue } from '@opentelemetry/api';
2
+ import type { TelemetryOptions } from 'ai';
3
+ import { sanitizeAttributeValue } from './sanitize-attribute-value';
4
+
5
+ export type AttributeSpec =
6
+ | AttributeValue
7
+ | { input: () => AttributeValue | undefined }
8
+ | { output: () => AttributeValue | undefined }
9
+ | undefined;
10
+
11
+ export type AttributeSpecMap = Record<string, AttributeSpec>;
12
+
13
+ function shouldRecord(
14
+ telemetry: TelemetryOptions | undefined,
15
+ ): telemetry is TelemetryOptions {
16
+ return telemetry?.isEnabled !== false;
17
+ }
18
+
19
+ export function selectAttributes(
20
+ telemetry: TelemetryOptions | undefined,
21
+ attributes: AttributeSpecMap,
22
+ ): Attributes {
23
+ if (!shouldRecord(telemetry)) {
24
+ return {};
25
+ }
26
+
27
+ const result: Attributes = {};
28
+
29
+ for (const [key, value] of Object.entries(attributes)) {
30
+ if (value == null) continue;
31
+
32
+ if (
33
+ typeof value === 'object' &&
34
+ 'input' in value &&
35
+ typeof value.input === 'function'
36
+ ) {
37
+ if (telemetry?.recordInputs === false) continue;
38
+ const resolved = value.input();
39
+ if (resolved != null) {
40
+ const sanitized = sanitizeAttributeValue(resolved);
41
+ if (sanitized != null) result[key] = sanitized;
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (
47
+ typeof value === 'object' &&
48
+ 'output' in value &&
49
+ typeof value.output === 'function'
50
+ ) {
51
+ if (telemetry?.recordOutputs === false) continue;
52
+ const resolved = value.output();
53
+ if (resolved != null) {
54
+ const sanitized = sanitizeAttributeValue(resolved);
55
+ if (sanitized != null) result[key] = sanitized;
56
+ }
57
+ continue;
58
+ }
59
+
60
+ const sanitized = sanitizeAttributeValue(value as AttributeValue);
61
+ if (sanitized != null) result[key] = sanitized;
62
+ }
63
+
64
+ return result;
65
+ }
@@ -0,0 +1,78 @@
1
+ import type { Attributes, AttributeValue } from '@opentelemetry/api';
2
+ import type { TelemetryOptions } from 'ai';
3
+
4
+ type ResolvableAttributeValue = () =>
5
+ | AttributeValue
6
+ | PromiseLike<AttributeValue>
7
+ | undefined;
8
+
9
+ export async function selectTelemetryAttributes({
10
+ telemetry,
11
+ attributes,
12
+ }: {
13
+ telemetry?: TelemetryOptions;
14
+ attributes: {
15
+ [attributeKey: string]:
16
+ | AttributeValue
17
+ | { input: ResolvableAttributeValue }
18
+ | { output: ResolvableAttributeValue }
19
+ | undefined;
20
+ };
21
+ }): Promise<Attributes> {
22
+ // when telemetry is disabled, return an empty object to avoid serialization overhead:
23
+ if (telemetry?.isEnabled === false) {
24
+ return {};
25
+ }
26
+
27
+ const resultAttributes: Attributes = {};
28
+
29
+ for (const [key, value] of Object.entries(attributes)) {
30
+ if (value == null) {
31
+ continue;
32
+ }
33
+
34
+ // input value, check if it should be recorded:
35
+ if (
36
+ typeof value === 'object' &&
37
+ 'input' in value &&
38
+ typeof value.input === 'function'
39
+ ) {
40
+ // default to true:
41
+ if (telemetry?.recordInputs === false) {
42
+ continue;
43
+ }
44
+
45
+ const result = await value.input();
46
+
47
+ if (result != null) {
48
+ resultAttributes[key] = result;
49
+ }
50
+
51
+ continue;
52
+ }
53
+
54
+ // output value, check if it should be recorded:
55
+ if (
56
+ typeof value === 'object' &&
57
+ 'output' in value &&
58
+ typeof value.output === 'function'
59
+ ) {
60
+ // default to true:
61
+ if (telemetry?.recordOutputs === false) {
62
+ continue;
63
+ }
64
+
65
+ const result = await value.output();
66
+
67
+ if (result != null) {
68
+ resultAttributes[key] = result;
69
+ }
70
+ continue;
71
+ }
72
+
73
+ // value is an attribute value already:
74
+ resultAttributes[key] = value as AttributeValue;
75
+ }
76
+
77
+ return resultAttributes;
78
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ LanguageModelV4Message,
3
+ LanguageModelV4Prompt,
4
+ } from '@ai-sdk/provider';
5
+ import { convertDataContentToBase64String } from 'ai';
6
+
7
+ /**
8
+ * Helper utility to serialize prompt content for OpenTelemetry tracing.
9
+ * It is initially created because normalized LanguageModelV4Prompt carries
10
+ * images as Uint8Arrays, on which JSON.stringify acts weirdly, converting
11
+ * them to objects with stringified indices as keys, e.g. {"0": 42, "1": 69 }.
12
+ */
13
+ export function stringifyForTelemetry(prompt: LanguageModelV4Prompt): string {
14
+ return JSON.stringify(
15
+ prompt.map((message: LanguageModelV4Message) => ({
16
+ ...message,
17
+ content:
18
+ typeof message.content === 'string'
19
+ ? message.content
20
+ : message.content.map(part =>
21
+ part.type === 'file'
22
+ ? {
23
+ ...part,
24
+ data: serializeFileData(part.data),
25
+ }
26
+ : part,
27
+ ),
28
+ })),
29
+ );
30
+ }
31
+
32
+ function serializeFileData(
33
+ data:
34
+ | { type: 'data'; data: string | Uint8Array }
35
+ | { type: 'url'; url: URL }
36
+ | { type: 'reference'; reference: Record<string, string> }
37
+ | { type: 'text'; text: string },
38
+ ): unknown {
39
+ switch (data.type) {
40
+ case 'data':
41
+ return data.data instanceof Uint8Array
42
+ ? convertDataContentToBase64String(data.data)
43
+ : data.data;
44
+ case 'url':
45
+ return data.url.toString();
46
+ case 'reference':
47
+ return data.reference;
48
+ case 'text':
49
+ return data.text;
50
+ }
51
+ }
@@ -0,0 +1,201 @@
1
+ import type { Attributes, Tracer } from '@opentelemetry/api';
2
+ import type { TelemetryOptions } from 'ai';
3
+ import {
4
+ selectAttributes,
5
+ type AttributeSpec,
6
+ type AttributeSpecMap,
7
+ } from './select-attributes';
8
+
9
+ type SupplementalAttributeOption =
10
+ | 'usage'
11
+ | 'providerMetadata'
12
+ | 'embedding'
13
+ | 'reranking'
14
+ | 'runtimeContext'
15
+ | 'headers'
16
+ | 'toolChoice'
17
+ | 'schema';
18
+
19
+ export type SupplementalAttributeOptions = Record<
20
+ SupplementalAttributeOption,
21
+ boolean
22
+ >;
23
+
24
+ export type OpenTelemetrySpanType =
25
+ | 'operation'
26
+ | 'step'
27
+ | 'languageModel'
28
+ | 'tool'
29
+ | 'embedding'
30
+ | 'reranking';
31
+
32
+ export type EnrichSpan = (options: {
33
+ spanType: OpenTelemetrySpanType;
34
+ operationId: string;
35
+ callId: string;
36
+ runtimeContext: Record<string, unknown> | undefined;
37
+ }) => Attributes | undefined;
38
+
39
+ export type OpenTelemetryOptions = {
40
+ /**
41
+ * The tracer to use for the telemetry data.
42
+ */
43
+ tracer?: Tracer;
44
+
45
+ /**
46
+ * Adds custom attributes to spans when they are created. These attributes are
47
+ * not AI SDK-owned semantics and are intended for observability integrations.
48
+ */
49
+ enrichSpan?: EnrichSpan;
50
+
51
+ /**
52
+ * Emit AI SDK usage details that are not represented by GenAI SemConv.
53
+ */
54
+ usage?: boolean;
55
+
56
+ /**
57
+ * Emit provider metadata on response spans.
58
+ */
59
+ providerMetadata?: boolean;
60
+
61
+ /**
62
+ * Emit embedding input and output values.
63
+ */
64
+ embedding?: boolean;
65
+
66
+ /**
67
+ * Emit reranking input documents and output ranking.
68
+ */
69
+ reranking?: boolean;
70
+
71
+ /**
72
+ * Emit runtime context values.
73
+ */
74
+ runtimeContext?: boolean;
75
+
76
+ /**
77
+ * Emit request headers.
78
+ */
79
+ headers?: boolean;
80
+
81
+ /**
82
+ * Emit selected tool choice information.
83
+ */
84
+ toolChoice?: boolean;
85
+
86
+ /**
87
+ * Emit object generation schema information.
88
+ */
89
+ schema?: boolean;
90
+ };
91
+
92
+ const disabledSupplementalAttributes: SupplementalAttributeOptions = {
93
+ usage: false,
94
+ providerMetadata: false,
95
+ embedding: false,
96
+ reranking: false,
97
+ runtimeContext: false,
98
+ headers: false,
99
+ toolChoice: false,
100
+ schema: false,
101
+ };
102
+
103
+ export function normalizeSupplementalAttributes(
104
+ options: OpenTelemetryOptions,
105
+ ): SupplementalAttributeOptions {
106
+ return {
107
+ ...disabledSupplementalAttributes,
108
+ usage: options.usage ?? false,
109
+ providerMetadata: options.providerMetadata ?? false,
110
+ embedding: options.embedding ?? false,
111
+ reranking: options.reranking ?? false,
112
+ runtimeContext: options.runtimeContext ?? false,
113
+ headers: options.headers ?? false,
114
+ toolChoice: options.toolChoice ?? false,
115
+ schema: options.schema ?? false,
116
+ };
117
+ }
118
+
119
+ export function getRuntimeContextAttributes(
120
+ context: Record<string, unknown> | undefined,
121
+ ): AttributeSpecMap {
122
+ const attributes: AttributeSpecMap = {};
123
+
124
+ for (const [key, value] of Object.entries(context ?? {})) {
125
+ addRuntimeContextAttribute(attributes, `ai.settings.context.${key}`, value);
126
+ }
127
+
128
+ return attributes;
129
+ }
130
+
131
+ /**
132
+ * Flattens nested runtime context objects into OTel-compatible attribute keys.
133
+ * Arrays are preserved because OTel supports primitive array attribute values.
134
+ */
135
+ function addRuntimeContextAttribute(
136
+ attributes: AttributeSpecMap,
137
+ key: string,
138
+ value: unknown,
139
+ ): void {
140
+ if (value == null) {
141
+ return;
142
+ }
143
+
144
+ if (Array.isArray(value) || typeof value !== 'object') {
145
+ attributes[key] = value as AttributeSpec;
146
+ return;
147
+ }
148
+
149
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
150
+ addRuntimeContextAttribute(attributes, `${key}.${nestedKey}`, nestedValue);
151
+ }
152
+ }
153
+
154
+ export function getHeaderAttributes(
155
+ headers: Record<string, string | undefined> | undefined,
156
+ ): AttributeSpecMap {
157
+ return Object.fromEntries(
158
+ Object.entries(headers ?? {})
159
+ .filter(([, value]) => value != null)
160
+ .map(([key, value]) => [`ai.request.headers.${key}`, value]),
161
+ ) as AttributeSpecMap;
162
+ }
163
+
164
+ export function getDetailedUsageAttributes(usage: {
165
+ inputTokenDetails?: {
166
+ noCacheTokens?: number | undefined;
167
+ };
168
+ outputTokenDetails?: {
169
+ textTokens?: number | undefined;
170
+ reasoningTokens?: number | undefined;
171
+ };
172
+ }): AttributeSpecMap {
173
+ return {
174
+ 'ai.usage.inputTokenDetails.noCacheTokens':
175
+ usage.inputTokenDetails?.noCacheTokens,
176
+ 'ai.usage.outputTokenDetails.textTokens':
177
+ usage.outputTokenDetails?.textTokens,
178
+ 'ai.usage.outputTokenDetails.reasoningTokens':
179
+ usage.outputTokenDetails?.reasoningTokens,
180
+ };
181
+ }
182
+
183
+ export function selectSupplementalAttributes(
184
+ telemetry: TelemetryOptions | undefined,
185
+ enabledAttributes: SupplementalAttributeOptions,
186
+ attributes: Partial<Record<SupplementalAttributeOption, AttributeSpecMap>>,
187
+ ): Attributes {
188
+ const result: Attributes = {};
189
+
190
+ for (const [key, value] of Object.entries(attributes) as Array<
191
+ [SupplementalAttributeOption, AttributeSpecMap | undefined]
192
+ >) {
193
+ if (!enabledAttributes[key] || value == null) {
194
+ continue;
195
+ }
196
+
197
+ Object.assign(result, selectAttributes(telemetry, value));
198
+ }
199
+
200
+ return result;
201
+ }