@@ -321,3 +321,206 @@ export const mapBedrockContentBlock = (block: any): object => {
321321 return { type : block . type , ...block } ;
322322 }
323323} ;
324+
325+ // =============================================================================
326+ // Vercel AI SDK
327+ // =============================================================================
328+ // Maps a single Vercel AI SDK content part (from ai.prompt.messages span attr)
329+ // to its OTel-compliant part object. Verified against:
330+ // - AI SDK v6 ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart types
331+ // - OTel gen_ai input/output messages JSON schemas (v1.40)
332+ //
333+ // text → TextPart { type:"text", content }
334+ // reasoning → ReasoningPart { type:"reasoning", content }
335+ // tool-call → ToolCallRequestPart { type:"tool_call", id?, name, arguments? }
336+ // tool-result→ ToolCallResponsePart { type:"tool_call_response", id?, response }
337+ // image (string data URI) → BlobPart { modality:"image", mime_type?, content }
338+ // image (string URL) → UriPart { modality:"image", uri }
339+ // image (URL object) → UriPart { modality:"image", uri: url.href }
340+ // image (binary data) → BlobPart { modality:"image", content (base64) }
341+ // file (inline data) → BlobPart { modality inferred from mediaType, content }
342+ // file (URL) → UriPart { modality inferred from mediaType, uri }
343+ // <unknown> → GenericPart
344+
345+ /**
346+ * Content part type values as emitted by the Vercel AI SDK v6 in span attributes.
347+ * Source: @ai-sdk/provider-utils ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart
348+ */
349+ const enum AiSdkPartType {
350+ Text = "text" ,
351+ Reasoning = "reasoning" ,
352+ ToolCall = "tool-call" ,
353+ ToolResult = "tool-result" ,
354+ Image = "image" ,
355+ File = "file" ,
356+ }
357+
358+ /**
359+ * Maps a single Vercel AI SDK content part to its OTel-compliant part object.
360+ *
361+ * Field names follow AI SDK v6:
362+ * ToolCallPart: toolCallId, toolName, input
363+ * ToolResultPart: toolCallId, toolName, output (ToolResultOutput union)
364+ * ImagePart: image (DataContent | URL), optional mediaType
365+ * FilePart: data (DataContent | URL), mediaType (required)
366+ * ReasoningPart: text
367+ */
368+ export const mapAiSdkContentPart = ( part : any ) : any => {
369+ if ( ! part || typeof part !== "object" ) {
370+ return { type : AiSdkPartType . Text , content : String ( part ?? "" ) } ;
371+ }
372+
373+ switch ( part . type ) {
374+ case AiSdkPartType . Text :
375+ return { type : AiSdkPartType . Text , content : part . text ?? "" } ;
376+
377+ // OTel type is "reasoning", AI SDK v6 field is `text`
378+ case AiSdkPartType . Reasoning :
379+ return { type : AiSdkPartType . Reasoning , content : part . text ?? "" } ;
380+
381+ case AiSdkPartType . ToolCall :
382+ // AI SDK v6: toolCallId, toolName, input
383+ return {
384+ type : "tool_call" ,
385+ id : part . toolCallId ?? null ,
386+ name : part . toolName ,
387+ arguments : part . input ,
388+ } ;
389+
390+ case AiSdkPartType . ToolResult : {
391+ // AI SDK v6: output is ToolResultOutput — { type: 'text'|'json', value } union
392+ // Unwrap to the actual value for tracing; fall back to the full object if unknown shape
393+ const output = part . output ;
394+ const response =
395+ output && typeof output === "object" && "value" in output
396+ ? output . value
397+ : output ;
398+ return {
399+ type : "tool_call_response" ,
400+ id : part . toolCallId ?? null ,
401+ response,
402+ } ;
403+ }
404+
405+ case AiSdkPartType . Image : {
406+ // AI SDK v6: image is DataContent | URL; optional mediaType
407+ const imgSrc = part . image ?? null ;
408+ const mimeType = part . mediaType ?? null ;
409+
410+ if ( imgSrc instanceof URL ) {
411+ return {
412+ type : "uri" ,
413+ modality : "image" ,
414+ uri : imgSrc . href ,
415+ mime_type : mimeType ,
416+ } ;
417+ }
418+ if ( typeof imgSrc === "string" ) {
419+ if ( imgSrc . startsWith ( "data:" ) ) {
420+ const [ header , data ] = imgSrc . split ( "," ) ;
421+ const detectedMime = header . match ( / d a t a : ( [ ^ ; ] + ) / ) ?. [ 1 ] ?? mimeType ;
422+ return {
423+ type : "blob" ,
424+ modality : "image" ,
425+ ...( detectedMime ? { mime_type : detectedMime } : { } ) ,
426+ content : data || "" ,
427+ } ;
428+ }
429+ return {
430+ type : "uri" ,
431+ modality : "image" ,
432+ uri : imgSrc ,
433+ mime_type : mimeType ,
434+ } ;
435+ }
436+ if ( imgSrc != null ) {
437+ // Binary data (Uint8Array / ArrayBuffer / Buffer) — base64 encode
438+ const bytes =
439+ imgSrc instanceof ArrayBuffer ? new Uint8Array ( imgSrc ) : imgSrc ;
440+ const b64 = Buffer . from ( bytes ) . toString ( "base64" ) ;
441+ return {
442+ type : "blob" ,
443+ modality : "image" ,
444+ mime_type : mimeType ,
445+ content : b64 ,
446+ } ;
447+ }
448+ return { type : "blob" , modality : "image" , content : "" } ;
449+ }
450+
451+ case AiSdkPartType . File : {
452+ // AI SDK v6: data is DataContent | URL; mediaType is required
453+ const fileSrc = part . data ?? null ;
454+ const mimeType = part . mediaType ?? null ;
455+ // Infer modality from MIME type (best-effort)
456+ const modality = mimeType ?. startsWith ( "image/" )
457+ ? "image"
458+ : mimeType ?. startsWith ( "audio/" )
459+ ? "audio"
460+ : mimeType ?. startsWith ( "video/" )
461+ ? "video"
462+ : "document" ;
463+
464+ if ( fileSrc instanceof URL ) {
465+ return {
466+ type : "uri" ,
467+ modality,
468+ uri : fileSrc . href ,
469+ mime_type : mimeType ,
470+ } ;
471+ }
472+ if ( typeof fileSrc === "string" ) {
473+ if ( fileSrc . startsWith ( "data:" ) ) {
474+ const [ , data ] = fileSrc . split ( "," ) ;
475+ return {
476+ type : "blob" ,
477+ modality,
478+ mime_type : mimeType ,
479+ content : data || "" ,
480+ } ;
481+ }
482+ return { type : "uri" , modality, uri : fileSrc , mime_type : mimeType } ;
483+ }
484+ if ( fileSrc != null ) {
485+ const bytes =
486+ fileSrc instanceof ArrayBuffer ? new Uint8Array ( fileSrc ) : fileSrc ;
487+ const b64 = Buffer . from ( bytes ) . toString ( "base64" ) ;
488+ return { type : "blob" , modality, mime_type : mimeType , content : b64 } ;
489+ }
490+ return { type : "blob" , modality, content : "" } ;
491+ }
492+
493+ default :
494+ // GenericPart — preserve unknown types as-is
495+ return { type : part . type , ...part } ;
496+ }
497+ } ;
498+
499+ /**
500+ * Converts Vercel AI SDK message content to an array of OTel parts.
501+ * Accepts: plain string, array of SDK content parts, or a JSON-serialized array
502+ * (the AI SDK serializes content arrays as JSON strings in span attributes).
503+ */
504+ export const mapAiSdkMessageContent = ( content : any ) : any [ ] => {
505+ if ( typeof content === "string" ) {
506+ try {
507+ const parsed = JSON . parse ( content ) ;
508+ if ( Array . isArray ( parsed ) ) {
509+ return parsed . map ( mapAiSdkContentPart ) ;
510+ }
511+ } catch {
512+ // plain string
513+ }
514+ return [ { type : "text" , content } ] ;
515+ }
516+
517+ if ( Array . isArray ( content ) ) {
518+ return content . map ( mapAiSdkContentPart ) ;
519+ }
520+
521+ if ( content && typeof content === "object" ) {
522+ return [ mapAiSdkContentPart ( content ) ] ;
523+ }
524+
525+ return [ { type : "text" , content : String ( content ?? "" ) } ] ;
526+ } ;
0 commit comments