@ai-sdk/otel 1.0.0-beta.3 → 1.0.0-beta.30

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,573 @@
1
+ import {
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 data = part.data;
188
+ let content: string;
189
+ if (data instanceof Uint8Array) {
190
+ content = convertDataContentToBase64String(data);
191
+ } else if (typeof data === 'string') {
192
+ if (data.startsWith('http://') || data.startsWith('https://')) {
193
+ return {
194
+ type: 'uri',
195
+ modality: getModality(part.mediaType),
196
+ mime_type: part.mediaType ?? null,
197
+ uri: data,
198
+ };
199
+ }
200
+ content = data;
201
+ } else {
202
+ content = String(data);
203
+ }
204
+ return {
205
+ type: 'blob',
206
+ modality: getModality(part.mediaType),
207
+ mime_type: part.mediaType ?? null,
208
+ content,
209
+ };
210
+ }
211
+
212
+ case 'tool-approval-response':
213
+ return {
214
+ type: 'tool_approval_response',
215
+ approval_id: part.approvalId,
216
+ approved: part.approved,
217
+ reason: part.reason,
218
+ };
219
+
220
+ case 'custom':
221
+ return { type: 'custom', kind: part.kind };
222
+
223
+ case 'reasoning-file':
224
+ return { type: String(part.type) };
225
+
226
+ default: {
227
+ const _exhaustive: never = part;
228
+ return { type: String((_exhaustive as { type: string }).type) };
229
+ }
230
+ }
231
+ }
232
+
233
+ function getModality(mediaType: string | undefined): string {
234
+ if (!mediaType) return 'image';
235
+ if (mediaType.startsWith('image/')) return 'image';
236
+ if (mediaType.startsWith('video/')) return 'video';
237
+ if (mediaType.startsWith('audio/')) return 'audio';
238
+ return 'image';
239
+ }
240
+
241
+ /**
242
+ * Converts a LanguageModelV4Prompt to the gen_ai.input.messages SemConv format.
243
+ * System messages are excluded (they go into gen_ai.system_instructions).
244
+ */
245
+ export function formatInputMessages(
246
+ prompt: LanguageModelV4Prompt,
247
+ ): SemConvInputMessage[] {
248
+ return prompt
249
+ .filter(msg => msg.role !== 'system')
250
+ .map((message: LanguageModelV4Message) => {
251
+ if (message.role === 'system') {
252
+ return {
253
+ role: 'system',
254
+ parts: [{ type: 'text', content: message.content }],
255
+ };
256
+ }
257
+
258
+ const parts = message.content.map(convertMessagePartToSemConv);
259
+ return { role: message.role, parts };
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Converts user-facing ModelMessage[] (and optional prompt string) to the
265
+ * gen_ai.input.messages SemConv format. System messages are excluded
266
+ * (they belong in gen_ai.system_instructions).
267
+ */
268
+ export function formatModelMessages({
269
+ prompt,
270
+ messages,
271
+ }: {
272
+ prompt: string | Array<ModelMessage> | undefined;
273
+ messages: Array<ModelMessage> | undefined;
274
+ }): SemConvInputMessage[] {
275
+ const result: SemConvInputMessage[] = [];
276
+
277
+ if (typeof prompt === 'string') {
278
+ result.push({
279
+ role: 'user',
280
+ parts: [{ type: 'text', content: prompt }],
281
+ });
282
+ } else if (Array.isArray(prompt)) {
283
+ for (const msg of prompt) {
284
+ const converted = convertModelMessageToSemConv(msg);
285
+ if (converted) result.push(converted);
286
+ }
287
+ }
288
+
289
+ if (messages) {
290
+ for (const msg of messages) {
291
+ const converted = convertModelMessageToSemConv(msg);
292
+ if (converted) result.push(converted);
293
+ }
294
+ }
295
+
296
+ return result;
297
+ }
298
+
299
+ function convertModelMessageToSemConv(
300
+ msg: ModelMessage,
301
+ ): SemConvInputMessage | undefined {
302
+ if (msg.role === 'system') return undefined;
303
+
304
+ if (msg.role === 'user') {
305
+ if (typeof msg.content === 'string') {
306
+ return {
307
+ role: 'user',
308
+ parts: [{ type: 'text', content: msg.content }],
309
+ };
310
+ }
311
+ const parts: SemConvPart[] = msg.content.map(part => {
312
+ switch (part.type) {
313
+ case 'text':
314
+ return { type: 'text' as const, content: part.text };
315
+ case 'image': {
316
+ const data = part.image;
317
+ if (data instanceof URL) {
318
+ return {
319
+ type: 'uri' as const,
320
+ modality: 'image',
321
+ mime_type: part.mediaType ?? null,
322
+ uri: data.toString(),
323
+ };
324
+ }
325
+ if (typeof data === 'string') {
326
+ if (data.startsWith('http://') || data.startsWith('https://')) {
327
+ return {
328
+ type: 'uri' as const,
329
+ modality: 'image',
330
+ mime_type: part.mediaType ?? null,
331
+ uri: data,
332
+ };
333
+ }
334
+ return {
335
+ type: 'blob' as const,
336
+ modality: 'image',
337
+ mime_type: part.mediaType ?? null,
338
+ content: data,
339
+ };
340
+ }
341
+ return {
342
+ type: 'blob' as const,
343
+ modality: 'image',
344
+ mime_type: part.mediaType ?? null,
345
+ content: convertDataContentToBase64String(data as Uint8Array),
346
+ };
347
+ }
348
+ case 'file': {
349
+ const data = part.data;
350
+ if (data instanceof URL) {
351
+ return {
352
+ type: 'uri' as const,
353
+ modality: getModality(part.mediaType),
354
+ mime_type: part.mediaType ?? null,
355
+ uri: data.toString(),
356
+ };
357
+ }
358
+ if (typeof data === 'string') {
359
+ if (data.startsWith('http://') || data.startsWith('https://')) {
360
+ return {
361
+ type: 'uri' as const,
362
+ modality: getModality(part.mediaType),
363
+ mime_type: part.mediaType ?? null,
364
+ uri: data,
365
+ };
366
+ }
367
+ return {
368
+ type: 'blob' as const,
369
+ modality: getModality(part.mediaType),
370
+ mime_type: part.mediaType ?? null,
371
+ content: data,
372
+ };
373
+ }
374
+ return {
375
+ type: 'blob' as const,
376
+ modality: getModality(part.mediaType),
377
+ mime_type: part.mediaType ?? null,
378
+ content: convertDataContentToBase64String(data as Uint8Array),
379
+ };
380
+ }
381
+ default:
382
+ return { type: String((part as { type: string }).type) };
383
+ }
384
+ });
385
+ return { role: 'user', parts };
386
+ }
387
+
388
+ if (msg.role === 'assistant') {
389
+ if (typeof msg.content === 'string') {
390
+ return {
391
+ role: 'assistant',
392
+ parts: [{ type: 'text', content: msg.content }],
393
+ };
394
+ }
395
+ const parts: SemConvPart[] = msg.content.map(part => {
396
+ switch (part.type) {
397
+ case 'text':
398
+ return { type: 'text' as const, content: part.text };
399
+ case 'reasoning':
400
+ return { type: 'reasoning' as const, content: part.text };
401
+ case 'tool-call':
402
+ return {
403
+ type: 'tool_call' as const,
404
+ id: part.toolCallId ?? null,
405
+ name: part.toolName,
406
+ arguments: part.input,
407
+ };
408
+ case 'tool-result': {
409
+ const output = part.output;
410
+ let response: unknown;
411
+ if (output) {
412
+ if (output.type === 'text' || output.type === 'error-text') {
413
+ response = output.value;
414
+ } else if (output.type === 'json' || output.type === 'error-json') {
415
+ response = output.value;
416
+ } else if (output.type === 'execution-denied') {
417
+ response = { denied: true, reason: output.reason };
418
+ } else {
419
+ response = output;
420
+ }
421
+ }
422
+ return {
423
+ type: 'tool_call_response' as const,
424
+ id: part.toolCallId ?? null,
425
+ response,
426
+ };
427
+ }
428
+ default:
429
+ return { type: String((part as { type: string }).type) };
430
+ }
431
+ });
432
+ return { role: 'assistant', parts };
433
+ }
434
+
435
+ if (msg.role === 'tool') {
436
+ const parts: SemConvPart[] = msg.content.map(part => {
437
+ if (part.type === 'tool-result') {
438
+ const output = part.output;
439
+ let response: unknown;
440
+ if (output) {
441
+ if (output.type === 'text' || output.type === 'error-text') {
442
+ response = output.value;
443
+ } else if (output.type === 'json' || output.type === 'error-json') {
444
+ response = output.value;
445
+ } else if (output.type === 'execution-denied') {
446
+ response = { denied: true, reason: output.reason };
447
+ } else {
448
+ response = output;
449
+ }
450
+ }
451
+ return {
452
+ type: 'tool_call_response' as const,
453
+ id: part.toolCallId ?? null,
454
+ response,
455
+ };
456
+ }
457
+ return { type: String((part as { type: string }).type) };
458
+ });
459
+ return { role: 'tool', parts };
460
+ }
461
+
462
+ return undefined;
463
+ }
464
+
465
+ /**
466
+ * Extracts the system instruction from a LanguageModelV4Prompt if present.
467
+ */
468
+ export function extractSystemFromPrompt(
469
+ prompt: LanguageModelV4Prompt,
470
+ ): string | undefined {
471
+ const systemMsg = prompt.find(msg => msg.role === 'system');
472
+ if (systemMsg && systemMsg.role === 'system') {
473
+ return systemMsg.content;
474
+ }
475
+ return undefined;
476
+ }
477
+
478
+ /**
479
+ * Converts step result data to the gen_ai.output.messages SemConv format.
480
+ */
481
+ export function formatOutputMessages({
482
+ text,
483
+ reasoning,
484
+ toolCalls,
485
+ files,
486
+ finishReason,
487
+ }: {
488
+ text?: string;
489
+ reasoning?: ReadonlyArray<{ text?: string }>;
490
+ toolCalls?: ReadonlyArray<{
491
+ toolCallId: string;
492
+ toolName: string;
493
+ input: unknown;
494
+ }>;
495
+ files?: ReadonlyArray<{ mediaType: string; base64: string }>;
496
+ finishReason: string;
497
+ }): SemConvOutputMessage[] {
498
+ const parts: SemConvPart[] = [];
499
+
500
+ if (reasoning) {
501
+ for (const r of reasoning) {
502
+ if ('text' in r && r.text) {
503
+ parts.push({ type: 'reasoning', content: r.text });
504
+ }
505
+ }
506
+ }
507
+
508
+ if (text != null && text.length > 0) {
509
+ parts.push({ type: 'text', content: text });
510
+ }
511
+
512
+ if (toolCalls) {
513
+ for (const tc of toolCalls) {
514
+ parts.push({
515
+ type: 'tool_call',
516
+ id: tc.toolCallId,
517
+ name: tc.toolName,
518
+ arguments: tc.input,
519
+ });
520
+ }
521
+ }
522
+
523
+ if (files) {
524
+ for (const file of files) {
525
+ parts.push({
526
+ type: 'blob',
527
+ modality: getModality(file.mediaType),
528
+ mime_type: file.mediaType,
529
+ content: file.base64,
530
+ });
531
+ }
532
+ }
533
+
534
+ return [
535
+ {
536
+ role: 'assistant',
537
+ parts,
538
+ finish_reason: mapFinishReason(finishReason),
539
+ },
540
+ ];
541
+ }
542
+
543
+ /**
544
+ * Converts generateObject result to the gen_ai.output.messages SemConv format.
545
+ */
546
+ export function formatObjectOutputMessages({
547
+ objectText,
548
+ finishReason,
549
+ }: {
550
+ objectText: string;
551
+ finishReason: string;
552
+ }): SemConvOutputMessage[] {
553
+ return [
554
+ {
555
+ role: 'assistant',
556
+ parts: [{ type: 'text', content: objectText }],
557
+ finish_reason: mapFinishReason(finishReason),
558
+ },
559
+ ];
560
+ }
561
+
562
+ function mapFinishReason(reason: string): string {
563
+ const mapping: Record<string, string> = {
564
+ stop: 'stop',
565
+ length: 'length',
566
+ 'content-filter': 'content_filter',
567
+ 'tool-calls': 'tool_call',
568
+ error: 'error',
569
+ other: 'stop',
570
+ unknown: 'stop',
571
+ };
572
+ return mapping[reason] ?? reason;
573
+ }