@ai-sdk/otel 1.0.0-beta.6 → 1.0.0-beta.61

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,644 @@
1
+ import type {
2
+ LanguageModelV4CustomPart,
3
+ LanguageModelV4FilePart,
4
+ LanguageModelV4Message,
5
+ LanguageModelV4Prompt,
6
+ LanguageModelV4ReasoningFilePart,
7
+ LanguageModelV4ReasoningPart,
8
+ LanguageModelV4TextPart,
9
+ LanguageModelV4ToolApprovalResponsePart,
10
+ LanguageModelV4ToolCallPart,
11
+ LanguageModelV4ToolResultPart,
12
+ } from '@ai-sdk/provider';
13
+ import type { ModelMessage } from '@ai-sdk/provider-utils';
14
+ import { convertDataContentToBase64String } from 'ai';
15
+
16
+ type LanguageModelV4ContentPart =
17
+ | LanguageModelV4TextPart
18
+ | LanguageModelV4FilePart
19
+ | LanguageModelV4CustomPart
20
+ | LanguageModelV4ReasoningPart
21
+ | LanguageModelV4ReasoningFilePart
22
+ | LanguageModelV4ToolCallPart
23
+ | LanguageModelV4ToolResultPart
24
+ | LanguageModelV4ToolApprovalResponsePart;
25
+
26
+ type SemConvPart =
27
+ | { type: 'text'; content: string }
28
+ | { type: 'reasoning'; content: string }
29
+ | {
30
+ type: 'tool_call';
31
+ id: string | null;
32
+ name: string;
33
+ arguments?: unknown;
34
+ }
35
+ | {
36
+ type: 'tool_call_response';
37
+ id: string | null;
38
+ response: unknown;
39
+ }
40
+ | {
41
+ type: 'blob';
42
+ modality: string;
43
+ mime_type: string | null;
44
+ content: string;
45
+ }
46
+ | { type: string; [key: string]: unknown };
47
+
48
+ interface SemConvInputMessage {
49
+ role: string;
50
+ parts: SemConvPart[];
51
+ }
52
+
53
+ interface SemConvOutputMessage {
54
+ role: string;
55
+ parts: SemConvPart[];
56
+ finish_reason: string;
57
+ }
58
+
59
+ interface SemConvSystemInstruction {
60
+ type: string;
61
+ content?: string;
62
+ [key: string]: unknown;
63
+ }
64
+
65
+ /**
66
+ * Maps an AI SDK provider string (e.g. "anthropic.messages", "openai.chat")
67
+ * to a well-known gen_ai.provider.name value per the OTel GenAI SemConv.
68
+ *
69
+ * Provider strings come in formats like:
70
+ * "openai.chat", "google.generative-ai", "google.vertex.chat",
71
+ * "google-vertex", "amazon-bedrock.chat", "azure.chat"
72
+ *
73
+ * We match against longest-prefix-first to handle multi-segment prefixes
74
+ * like "google.vertex" and "google.generative-ai" before the single-segment "google".
75
+ */
76
+ export function mapProviderName(provider: string): string {
77
+ const lower = provider.toLowerCase();
78
+
79
+ const wellKnownPrefixes: Array<[string, string]> = [
80
+ ['google.vertex', 'gcp.vertex_ai'],
81
+ ['google.generative-ai', 'gcp.gemini'],
82
+ ['google-vertex', 'gcp.vertex_ai'],
83
+ ['amazon-bedrock', 'aws.bedrock'],
84
+ ['azure-openai', 'azure.ai.openai'],
85
+ ['anthropic', 'anthropic'],
86
+ ['openai', 'openai'],
87
+ ['azure', 'azure.ai.inference'],
88
+ ['google', 'gcp.gemini'],
89
+ ['mistral', 'mistral_ai'],
90
+ ['cohere', 'cohere'],
91
+ ['bedrock', 'aws.bedrock'],
92
+ ['groq', 'groq'],
93
+ ['deepseek', 'deepseek'],
94
+ ['perplexity', 'perplexity'],
95
+ ['xai', 'x_ai'],
96
+ ];
97
+
98
+ for (const [prefix, mapped] of wellKnownPrefixes) {
99
+ if (
100
+ lower === prefix ||
101
+ lower.startsWith(prefix + '.') ||
102
+ lower.startsWith(prefix + '-')
103
+ ) {
104
+ return mapped;
105
+ }
106
+ }
107
+
108
+ return provider;
109
+ }
110
+
111
+ /**
112
+ * Maps an AI SDK operationId to a gen_ai.operation.name value.
113
+ */
114
+ export function mapOperationName(operationId: string): string {
115
+ const mapping: Record<string, string> = {
116
+ 'ai.generateText': 'invoke_agent',
117
+ 'ai.streamText': 'invoke_agent',
118
+ 'ai.generateObject': 'invoke_agent',
119
+ 'ai.streamObject': 'invoke_agent',
120
+ 'ai.embed': 'embeddings',
121
+ 'ai.embedMany': 'embeddings',
122
+ 'ai.rerank': 'rerank',
123
+ };
124
+ return mapping[operationId] ?? operationId;
125
+ }
126
+
127
+ /**
128
+ * Converts a system value to the gen_ai.system_instructions SemConv format.
129
+ * Accepts a plain string, a single SystemModelMessage, or an array of them.
130
+ * Schema: array of parts, each with at least { type, content }.
131
+ */
132
+ export function formatSystemInstructions(
133
+ system:
134
+ | string
135
+ | { role: 'system'; content: string }
136
+ | Array<{ role: 'system'; content: string }>,
137
+ ): SemConvSystemInstruction[] {
138
+ if (typeof system === 'string') {
139
+ return [{ type: 'text', content: system }];
140
+ }
141
+ if (Array.isArray(system)) {
142
+ return system.map(msg => ({ type: 'text', content: msg.content }));
143
+ }
144
+ return [{ type: 'text', content: system.content }];
145
+ }
146
+
147
+ function convertMessagePartToSemConv(
148
+ part: LanguageModelV4ContentPart,
149
+ ): SemConvPart {
150
+ switch (part.type) {
151
+ case 'text':
152
+ return { type: 'text', content: part.text };
153
+
154
+ case 'reasoning':
155
+ return { type: 'reasoning', content: part.text };
156
+
157
+ case 'tool-call':
158
+ return {
159
+ type: 'tool_call',
160
+ id: part.toolCallId ?? null,
161
+ name: part.toolName,
162
+ arguments: part.input,
163
+ };
164
+
165
+ case 'tool-result': {
166
+ const output = part.output;
167
+ let response: unknown;
168
+ if (output) {
169
+ if (output.type === 'text' || output.type === 'error-text') {
170
+ response = output.value;
171
+ } else if (output.type === 'json' || output.type === 'error-json') {
172
+ response = output.value;
173
+ } else if (output.type === 'execution-denied') {
174
+ response = { denied: true, reason: output.reason };
175
+ } else {
176
+ response = output;
177
+ }
178
+ }
179
+ return {
180
+ type: 'tool_call_response',
181
+ id: part.toolCallId ?? null,
182
+ response,
183
+ };
184
+ }
185
+
186
+ case 'file': {
187
+ const rawData = part.data as
188
+ | string
189
+ | Uint8Array
190
+ | URL
191
+ | ArrayBuffer
192
+ | { type: 'data'; data: string | Uint8Array | ArrayBuffer }
193
+ | { type: 'url'; url: URL }
194
+ | { type: 'reference'; reference: Record<string, string> }
195
+ | { type: 'text'; text: string };
196
+
197
+ const data: string | Uint8Array | URL | ArrayBuffer = (() => {
198
+ if (
199
+ typeof rawData === 'object' &&
200
+ rawData !== null &&
201
+ !(rawData instanceof URL) &&
202
+ !(rawData instanceof Uint8Array) &&
203
+ !(rawData instanceof ArrayBuffer) &&
204
+ 'type' in rawData
205
+ ) {
206
+ switch (rawData.type) {
207
+ case 'data':
208
+ return rawData.data;
209
+ case 'url':
210
+ return rawData.url;
211
+ case 'text':
212
+ return rawData.text;
213
+ default:
214
+ return '';
215
+ }
216
+ }
217
+ return rawData as string | Uint8Array | URL | ArrayBuffer;
218
+ })();
219
+
220
+ let content: string;
221
+ if (data instanceof Uint8Array) {
222
+ content = convertDataContentToBase64String(data);
223
+ } else if (typeof data === 'string') {
224
+ if (data.startsWith('http://') || data.startsWith('https://')) {
225
+ return {
226
+ type: 'uri',
227
+ modality: getModality(part.mediaType),
228
+ mime_type: part.mediaType ?? null,
229
+ uri: data,
230
+ };
231
+ }
232
+ content = data;
233
+ } else if (data instanceof URL) {
234
+ return {
235
+ type: 'uri',
236
+ modality: getModality(part.mediaType),
237
+ mime_type: part.mediaType ?? null,
238
+ uri: data.toString(),
239
+ };
240
+ } else {
241
+ content = String(data);
242
+ }
243
+ return {
244
+ type: 'blob',
245
+ modality: getModality(part.mediaType),
246
+ mime_type: part.mediaType ?? null,
247
+ content,
248
+ };
249
+ }
250
+
251
+ case 'tool-approval-response':
252
+ return {
253
+ type: 'tool_approval_response',
254
+ approval_id: part.approvalId,
255
+ approved: part.approved,
256
+ reason: part.reason,
257
+ };
258
+
259
+ case 'custom':
260
+ return { type: 'custom', kind: part.kind };
261
+
262
+ case 'reasoning-file':
263
+ return { type: String(part.type) };
264
+
265
+ default: {
266
+ const _exhaustive: never = part;
267
+ return { type: String((_exhaustive as { type: string }).type) };
268
+ }
269
+ }
270
+ }
271
+
272
+ function getModality(mediaType: string | undefined): string {
273
+ if (!mediaType) return 'image';
274
+ if (mediaType.startsWith('image/')) return 'image';
275
+ if (mediaType.startsWith('video/')) return 'video';
276
+ if (mediaType.startsWith('audio/')) return 'audio';
277
+ return 'image';
278
+ }
279
+
280
+ /**
281
+ * Converts a LanguageModelV4Prompt to the gen_ai.input.messages SemConv format.
282
+ * System messages are excluded (they go into gen_ai.system_instructions).
283
+ */
284
+ export function formatInputMessages(
285
+ prompt: LanguageModelV4Prompt,
286
+ ): SemConvInputMessage[] {
287
+ return prompt
288
+ .filter(msg => msg.role !== 'system')
289
+ .map((message: LanguageModelV4Message) => {
290
+ if (message.role === 'system') {
291
+ return {
292
+ role: 'system',
293
+ parts: [{ type: 'text', content: message.content }],
294
+ };
295
+ }
296
+
297
+ const parts = message.content.map(convertMessagePartToSemConv);
298
+ return { role: message.role, parts };
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Converts user-facing ModelMessage[] (and optional prompt string) to the
304
+ * gen_ai.input.messages SemConv format. System messages are excluded
305
+ * (they belong in gen_ai.system_instructions).
306
+ */
307
+ export function formatModelMessages({
308
+ prompt,
309
+ messages,
310
+ }: {
311
+ prompt: string | Array<ModelMessage> | undefined;
312
+ messages: Array<ModelMessage> | undefined;
313
+ }): SemConvInputMessage[] {
314
+ const result: SemConvInputMessage[] = [];
315
+
316
+ if (typeof prompt === 'string') {
317
+ result.push({
318
+ role: 'user',
319
+ parts: [{ type: 'text', content: prompt }],
320
+ });
321
+ } else if (Array.isArray(prompt)) {
322
+ for (const msg of prompt) {
323
+ const converted = convertModelMessageToSemConv(msg);
324
+ if (converted) result.push(converted);
325
+ }
326
+ }
327
+
328
+ if (messages) {
329
+ for (const msg of messages) {
330
+ const converted = convertModelMessageToSemConv(msg);
331
+ if (converted) result.push(converted);
332
+ }
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ function convertModelMessageToSemConv(
339
+ msg: ModelMessage,
340
+ ): SemConvInputMessage | undefined {
341
+ if (msg.role === 'system') return undefined;
342
+
343
+ if (msg.role === 'user') {
344
+ if (typeof msg.content === 'string') {
345
+ return {
346
+ role: 'user',
347
+ parts: [{ type: 'text', content: msg.content }],
348
+ };
349
+ }
350
+ const parts: SemConvPart[] = msg.content.map(part => {
351
+ switch (part.type) {
352
+ case 'text':
353
+ return { type: 'text' as const, content: part.text };
354
+ case 'image': {
355
+ const data = part.image;
356
+ if (data instanceof URL) {
357
+ return {
358
+ type: 'uri' as const,
359
+ modality: 'image',
360
+ mime_type: part.mediaType ?? null,
361
+ uri: data.toString(),
362
+ };
363
+ }
364
+ if (typeof data === 'string') {
365
+ if (data.startsWith('http://') || data.startsWith('https://')) {
366
+ return {
367
+ type: 'uri' as const,
368
+ modality: 'image',
369
+ mime_type: part.mediaType ?? null,
370
+ uri: data,
371
+ };
372
+ }
373
+ return {
374
+ type: 'blob' as const,
375
+ modality: 'image',
376
+ mime_type: part.mediaType ?? null,
377
+ content: data,
378
+ };
379
+ }
380
+ return {
381
+ type: 'blob' as const,
382
+ modality: 'image',
383
+ mime_type: part.mediaType ?? null,
384
+ content: convertDataContentToBase64String(data as Uint8Array),
385
+ };
386
+ }
387
+ case 'file': {
388
+ const rawData = part.data as
389
+ | string
390
+ | Uint8Array
391
+ | URL
392
+ | ArrayBuffer
393
+ | { type: 'data'; data: string | Uint8Array | ArrayBuffer }
394
+ | { type: 'url'; url: URL }
395
+ | { type: 'reference'; reference: Record<string, string> }
396
+ | { type: 'text'; text: string };
397
+
398
+ const data: string | Uint8Array | URL | ArrayBuffer = (() => {
399
+ if (
400
+ typeof rawData === 'object' &&
401
+ rawData !== null &&
402
+ !(rawData instanceof URL) &&
403
+ !(rawData instanceof Uint8Array) &&
404
+ !(rawData instanceof ArrayBuffer) &&
405
+ 'type' in rawData
406
+ ) {
407
+ switch (rawData.type) {
408
+ case 'data':
409
+ return rawData.data;
410
+ case 'url':
411
+ return rawData.url;
412
+ case 'text':
413
+ return rawData.text;
414
+ default:
415
+ return '';
416
+ }
417
+ }
418
+ return rawData as string | Uint8Array | URL | ArrayBuffer;
419
+ })();
420
+
421
+ if (data instanceof URL) {
422
+ return {
423
+ type: 'uri' as const,
424
+ modality: getModality(part.mediaType),
425
+ mime_type: part.mediaType ?? null,
426
+ uri: data.toString(),
427
+ };
428
+ }
429
+ if (typeof data === 'string') {
430
+ if (data.startsWith('http://') || data.startsWith('https://')) {
431
+ return {
432
+ type: 'uri' as const,
433
+ modality: getModality(part.mediaType),
434
+ mime_type: part.mediaType ?? null,
435
+ uri: data,
436
+ };
437
+ }
438
+ return {
439
+ type: 'blob' as const,
440
+ modality: getModality(part.mediaType),
441
+ mime_type: part.mediaType ?? null,
442
+ content: data,
443
+ };
444
+ }
445
+ return {
446
+ type: 'blob' as const,
447
+ modality: getModality(part.mediaType),
448
+ mime_type: part.mediaType ?? null,
449
+ content: convertDataContentToBase64String(data as Uint8Array),
450
+ };
451
+ }
452
+ default:
453
+ return { type: String((part as { type: string }).type) };
454
+ }
455
+ });
456
+ return { role: 'user', parts };
457
+ }
458
+
459
+ if (msg.role === 'assistant') {
460
+ if (typeof msg.content === 'string') {
461
+ return {
462
+ role: 'assistant',
463
+ parts: [{ type: 'text', content: msg.content }],
464
+ };
465
+ }
466
+ const parts: SemConvPart[] = msg.content.map(part => {
467
+ switch (part.type) {
468
+ case 'text':
469
+ return { type: 'text' as const, content: part.text };
470
+ case 'reasoning':
471
+ return { type: 'reasoning' as const, content: part.text };
472
+ case 'tool-call':
473
+ return {
474
+ type: 'tool_call' as const,
475
+ id: part.toolCallId ?? null,
476
+ name: part.toolName,
477
+ arguments: part.input,
478
+ };
479
+ case 'tool-result': {
480
+ const output = part.output;
481
+ let response: unknown;
482
+ if (output) {
483
+ if (output.type === 'text' || output.type === 'error-text') {
484
+ response = output.value;
485
+ } else if (output.type === 'json' || output.type === 'error-json') {
486
+ response = output.value;
487
+ } else if (output.type === 'execution-denied') {
488
+ response = { denied: true, reason: output.reason };
489
+ } else {
490
+ response = output;
491
+ }
492
+ }
493
+ return {
494
+ type: 'tool_call_response' as const,
495
+ id: part.toolCallId ?? null,
496
+ response,
497
+ };
498
+ }
499
+ default:
500
+ return { type: String((part as { type: string }).type) };
501
+ }
502
+ });
503
+ return { role: 'assistant', parts };
504
+ }
505
+
506
+ if (msg.role === 'tool') {
507
+ const parts: SemConvPart[] = msg.content.map(part => {
508
+ if (part.type === 'tool-result') {
509
+ const output = part.output;
510
+ let response: unknown;
511
+ if (output) {
512
+ if (output.type === 'text' || output.type === 'error-text') {
513
+ response = output.value;
514
+ } else if (output.type === 'json' || output.type === 'error-json') {
515
+ response = output.value;
516
+ } else if (output.type === 'execution-denied') {
517
+ response = { denied: true, reason: output.reason };
518
+ } else {
519
+ response = output;
520
+ }
521
+ }
522
+ return {
523
+ type: 'tool_call_response' as const,
524
+ id: part.toolCallId ?? null,
525
+ response,
526
+ };
527
+ }
528
+ return { type: String((part as { type: string }).type) };
529
+ });
530
+ return { role: 'tool', parts };
531
+ }
532
+
533
+ return undefined;
534
+ }
535
+
536
+ /**
537
+ * Extracts the system instruction from a LanguageModelV4Prompt if present.
538
+ */
539
+ export function extractSystemFromPrompt(
540
+ prompt: LanguageModelV4Prompt,
541
+ ): string | undefined {
542
+ const systemMsg = prompt.find(msg => msg.role === 'system');
543
+ if (systemMsg && systemMsg.role === 'system') {
544
+ return systemMsg.content;
545
+ }
546
+ return undefined;
547
+ }
548
+
549
+ /**
550
+ * Converts step result data to the gen_ai.output.messages SemConv format.
551
+ */
552
+ export function formatOutputMessages({
553
+ text,
554
+ reasoning,
555
+ toolCalls,
556
+ files,
557
+ finishReason,
558
+ }: {
559
+ text?: string;
560
+ reasoning?: ReadonlyArray<{ text?: string }>;
561
+ toolCalls?: ReadonlyArray<{
562
+ toolCallId: string;
563
+ toolName: string;
564
+ input: unknown;
565
+ }>;
566
+ files?: ReadonlyArray<{ mediaType: string; base64: string }>;
567
+ finishReason: string;
568
+ }): SemConvOutputMessage[] {
569
+ const parts: SemConvPart[] = [];
570
+
571
+ if (reasoning) {
572
+ for (const r of reasoning) {
573
+ if ('text' in r && r.text) {
574
+ parts.push({ type: 'reasoning', content: r.text });
575
+ }
576
+ }
577
+ }
578
+
579
+ if (text != null && text.length > 0) {
580
+ parts.push({ type: 'text', content: text });
581
+ }
582
+
583
+ if (toolCalls) {
584
+ for (const tc of toolCalls) {
585
+ parts.push({
586
+ type: 'tool_call',
587
+ id: tc.toolCallId,
588
+ name: tc.toolName,
589
+ arguments: tc.input,
590
+ });
591
+ }
592
+ }
593
+
594
+ if (files) {
595
+ for (const file of files) {
596
+ parts.push({
597
+ type: 'blob',
598
+ modality: getModality(file.mediaType),
599
+ mime_type: file.mediaType,
600
+ content: file.base64,
601
+ });
602
+ }
603
+ }
604
+
605
+ return [
606
+ {
607
+ role: 'assistant',
608
+ parts,
609
+ finish_reason: mapFinishReason(finishReason),
610
+ },
611
+ ];
612
+ }
613
+
614
+ /**
615
+ * Converts generateObject result to the gen_ai.output.messages SemConv format.
616
+ */
617
+ export function formatObjectOutputMessages({
618
+ objectText,
619
+ finishReason,
620
+ }: {
621
+ objectText: string;
622
+ finishReason: string;
623
+ }): SemConvOutputMessage[] {
624
+ return [
625
+ {
626
+ role: 'assistant',
627
+ parts: [{ type: 'text', content: objectText }],
628
+ finish_reason: mapFinishReason(finishReason),
629
+ },
630
+ ];
631
+ }
632
+
633
+ function mapFinishReason(reason: string): string {
634
+ const mapping: Record<string, string> = {
635
+ stop: 'stop',
636
+ length: 'length',
637
+ 'content-filter': 'content_filter',
638
+ 'tool-calls': 'tool_call',
639
+ error: 'error',
640
+ other: 'stop',
641
+ unknown: 'stop',
642
+ };
643
+ return mapping[reason] ?? reason;
644
+ }
@@ -1,16 +1,16 @@
1
- import { Attributes, AttributeValue } from '@opentelemetry/api';
2
- import type { CallSettings, TelemetrySettings } from 'ai';
1
+ import type { Attributes, AttributeValue } from '@opentelemetry/api';
2
+ import type { LanguageModelCallOptions } from 'ai';
3
3
 
4
4
  export function getBaseTelemetryAttributes({
5
5
  model,
6
6
  settings,
7
- telemetry,
8
7
  headers,
8
+ context,
9
9
  }: {
10
10
  model: { modelId: string; provider: string };
11
- settings: Omit<CallSettings, 'abortSignal' | 'headers' | 'temperature'>;
12
- telemetry: TelemetrySettings | undefined;
11
+ settings: LanguageModelCallOptions;
13
12
  headers: Record<string, string | undefined> | undefined;
13
+ context: Record<string, unknown> | undefined;
14
14
  }): Attributes {
15
15
  return {
16
16
  'ai.model.provider': model.provider,
@@ -22,16 +22,13 @@ export function getBaseTelemetryAttributes({
22
22
  return attributes;
23
23
  }, {} as Attributes),
24
24
 
25
- // add metadata as attributes:
26
- ...Object.entries(telemetry?.metadata ?? {}).reduce(
27
- (attributes, [key, value]) => {
28
- if (value != undefined) {
29
- attributes[`ai.telemetry.metadata.${key}`] = value as AttributeValue;
30
- }
31
- return attributes;
32
- },
33
- {} as Attributes,
34
- ),
25
+ // add context as attributes:
26
+ ...Object.entries(context ?? {}).reduce((attributes, [key, value]) => {
27
+ if (value != undefined) {
28
+ attributes[`ai.settings.context.${key}`] = value as AttributeValue;
29
+ }
30
+ return attributes;
31
+ }, {} as Attributes),
35
32
 
36
33
  // request headers
37
34
  ...Object.entries(headers ?? {}).reduce((attributes, [key, value]) => {
package/src/get-tracer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tracer, trace } from '@opentelemetry/api';
1
+ import { trace, type Tracer } from '@opentelemetry/api';
2
2
  import { noopTracer } from './noop-tracer';
3
3
 
4
4
  export function getTracer({
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
- export { OpenTelemetryIntegration } from './open-telemetry-integration';
1
+ export { OpenTelemetry } from './open-telemetry';
2
+ export { LegacyOpenTelemetry } from './legacy-open-telemetry';