@@ -321,3 +321,188 @@ 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+ * Maps a single Vercel AI SDK content part to its OTel-compliant part object.
347+ *
348+ * Field names follow AI SDK v6:
349+ * ToolCallPart: toolCallId, toolName, input
350+ * ToolResultPart: toolCallId, toolName, output (ToolResultOutput union)
351+ * ImagePart: image (DataContent | URL), optional mediaType
352+ * FilePart: data (DataContent | URL), mediaType (required)
353+ * ReasoningPart: text
354+ */
355+ export const mapAiSdkContentPart = ( part : any ) : any => {
356+ if ( ! part || typeof part !== "object" ) {
357+ return { type : "text" , content : String ( part ?? "" ) } ;
358+ }
359+
360+ switch ( part . type ) {
361+ case "text" :
362+ return { type : "text" , content : part . text ?? "" } ;
363+
364+ // OTel type is "reasoning", AI SDK v6 field is `text`
365+ case "reasoning" :
366+ return { type : "reasoning" , content : part . text ?? "" } ;
367+
368+ case "tool-call" :
369+ // AI SDK v6: toolCallId, toolName, input
370+ return {
371+ type : "tool_call" ,
372+ id : part . toolCallId ?? null ,
373+ name : part . toolName ,
374+ arguments : part . input ,
375+ } ;
376+
377+ case "tool-result" : {
378+ // AI SDK v6: output is ToolResultOutput — { type: 'text'|'json', value } union
379+ // Unwrap to the actual value for tracing; fall back to the full object if unknown shape
380+ const output = part . output ;
381+ const response =
382+ output && typeof output === "object" && "value" in output
383+ ? output . value
384+ : output ;
385+ return {
386+ type : "tool_call_response" ,
387+ id : part . toolCallId ?? null ,
388+ response,
389+ } ;
390+ }
391+
392+ case "image" : {
393+ // AI SDK v6: image is DataContent | URL; optional mediaType
394+ const imgSrc = part . image ?? null ;
395+ const mimeType = part . mediaType ?? null ;
396+
397+ if ( imgSrc instanceof URL ) {
398+ return {
399+ type : "uri" ,
400+ modality : "image" ,
401+ uri : imgSrc . href ,
402+ mime_type : mimeType ,
403+ } ;
404+ }
405+ if ( typeof imgSrc === "string" ) {
406+ if ( imgSrc . startsWith ( "data:" ) ) {
407+ const [ header , data ] = imgSrc . split ( "," ) ;
408+ const detectedMime = header . match ( / d a t a : ( [ ^ ; ] + ) / ) ?. [ 1 ] ?? mimeType ;
409+ return {
410+ type : "blob" ,
411+ modality : "image" ,
412+ ...( detectedMime ? { mime_type : detectedMime } : { } ) ,
413+ content : data ,
414+ } ;
415+ }
416+ return {
417+ type : "uri" ,
418+ modality : "image" ,
419+ uri : imgSrc ,
420+ mime_type : mimeType ,
421+ } ;
422+ }
423+ if ( imgSrc != null ) {
424+ // Binary data (Uint8Array / ArrayBuffer / Buffer) — base64 encode
425+ const bytes =
426+ imgSrc instanceof ArrayBuffer ? new Uint8Array ( imgSrc ) : imgSrc ;
427+ const b64 = Buffer . from ( bytes ) . toString ( "base64" ) ;
428+ return {
429+ type : "blob" ,
430+ modality : "image" ,
431+ mime_type : mimeType ,
432+ content : b64 ,
433+ } ;
434+ }
435+ return { type : "blob" , modality : "image" , content : "" } ;
436+ }
437+
438+ case "file" : {
439+ // AI SDK v6: data is DataContent | URL; mediaType is required
440+ const fileSrc = part . data ?? null ;
441+ const mimeType = part . mediaType ?? null ;
442+ // Infer modality from MIME type (best-effort)
443+ const modality = mimeType ?. startsWith ( "image/" )
444+ ? "image"
445+ : mimeType ?. startsWith ( "audio/" )
446+ ? "audio"
447+ : mimeType ?. startsWith ( "video/" )
448+ ? "video"
449+ : "document" ;
450+
451+ if ( fileSrc instanceof URL ) {
452+ return {
453+ type : "uri" ,
454+ modality,
455+ uri : fileSrc . href ,
456+ mime_type : mimeType ,
457+ } ;
458+ }
459+ if ( typeof fileSrc === "string" ) {
460+ if ( fileSrc . startsWith ( "data:" ) ) {
461+ const [ , data ] = fileSrc . split ( "," ) ;
462+ return { type : "blob" , modality, mime_type : mimeType , content : data } ;
463+ }
464+ return { type : "uri" , modality, uri : fileSrc , mime_type : mimeType } ;
465+ }
466+ if ( fileSrc != null ) {
467+ const bytes =
468+ fileSrc instanceof ArrayBuffer ? new Uint8Array ( fileSrc ) : fileSrc ;
469+ const b64 = Buffer . from ( bytes ) . toString ( "base64" ) ;
470+ return { type : "blob" , modality, mime_type : mimeType , content : b64 } ;
471+ }
472+ return { type : "blob" , modality, content : "" } ;
473+ }
474+
475+ default :
476+ // GenericPart — preserve unknown types as-is
477+ return { type : part . type , ...part } ;
478+ }
479+ } ;
480+
481+ /**
482+ * Converts Vercel AI SDK message content to an array of OTel parts.
483+ * Accepts: plain string, array of SDK content parts, or a JSON-serialized array
484+ * (the AI SDK serializes content arrays as JSON strings in span attributes).
485+ */
486+ export const mapAiSdkMessageContent = ( content : any ) : any [ ] => {
487+ if ( typeof content === "string" ) {
488+ try {
489+ const parsed = JSON . parse ( content ) ;
490+ if ( Array . isArray ( parsed ) ) {
491+ return parsed . map ( mapAiSdkContentPart ) ;
492+ }
493+ } catch {
494+ // plain string
495+ }
496+ return [ { type : "text" , content } ] ;
497+ }
498+
499+ if ( Array . isArray ( content ) ) {
500+ return content . map ( mapAiSdkContentPart ) ;
501+ }
502+
503+ if ( content && typeof content === "object" ) {
504+ return [ mapAiSdkContentPart ( content ) ] ;
505+ }
506+
507+ return [ { type : "text" , content : String ( content ?? "" ) } ] ;
508+ } ;
0 commit comments