@@ -438,6 +438,26 @@ export namespace MessageV2 {
438438 export function toModelMessages ( input : WithParts [ ] , model : Provider . Model ) : ModelMessage [ ] {
439439 const result : UIMessage [ ] = [ ]
440440 const toolNames = new Set < string > ( )
441+ // Track media from tool results that need to be injected as user messages
442+ // for providers that don't support media in tool results.
443+ //
444+ // OpenAI-compatible APIs only support string content in tool results, so we need
445+ // to extract media and inject as user messages. Other SDKs (anthropic, google,
446+ // bedrock) handle type: "content" with media parts natively.
447+ //
448+ // Only apply this workaround if the model actually supports image input -
449+ // otherwise there's no point extracting images.
450+ const supportsMediaInToolResults = ( ( ) => {
451+ if ( model . api . npm === "@ai-sdk/anthropic" ) return true
452+ if ( model . api . npm === "@ai-sdk/openai" ) return true
453+ if ( model . api . npm === "@ai-sdk/amazon-bedrock" ) return true
454+ if ( model . api . npm === "@ai-sdk/google-vertex/anthropic" ) return true
455+ if ( model . api . npm === "@ai-sdk/google" ) {
456+ const id = model . api . id . toLowerCase ( )
457+ return id . includes ( "gemini-3" ) && ! id . includes ( "gemini-2" )
458+ }
459+ return false
460+ } ) ( )
441461
442462 const toModelOutput = ( output : unknown ) => {
443463 if ( typeof output === "string" ) {
@@ -514,6 +534,7 @@ export namespace MessageV2 {
514534
515535 if ( msg . info . role === "assistant" ) {
516536 const differentModel = `${ model . providerID } /${ model . id } ` !== `${ msg . info . providerID } /${ msg . info . modelID } `
537+ const media : Array < { mime : string ; url : string } > = [ ]
517538
518539 if (
519540 msg . info . error &&
@@ -545,11 +566,23 @@ export namespace MessageV2 {
545566 if ( part . state . status === "completed" ) {
546567 const outputText = part . state . time . compacted ? "[Old tool result content cleared]" : part . state . output
547568 const attachments = part . state . time . compacted ? [ ] : ( part . state . attachments ?? [ ] )
569+
570+ // For providers that don't support media in tool results, extract media files
571+ // (images, PDFs) to be sent as a separate user message
572+ const isMediaAttachment = ( a : { mime : string } ) =>
573+ a . mime . startsWith ( "image/" ) || a . mime === "application/pdf"
574+ const mediaAttachments = attachments . filter ( isMediaAttachment )
575+ const nonMediaAttachments = attachments . filter ( ( a ) => ! isMediaAttachment ( a ) )
576+ if ( ! supportsMediaInToolResults && mediaAttachments . length > 0 ) {
577+ media . push ( ...mediaAttachments )
578+ }
579+ const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments
580+
548581 const output =
549- attachments . length > 0
582+ finalAttachments . length > 0
550583 ? {
551584 text : outputText ,
552- attachments,
585+ attachments : finalAttachments ,
553586 }
554587 : outputText
555588
@@ -593,6 +626,25 @@ export namespace MessageV2 {
593626 }
594627 if ( assistantMessage . parts . length > 0 ) {
595628 result . push ( assistantMessage )
629+ // Inject pending media as a user message for providers that don't support
630+ // media (images, PDFs) in tool results
631+ if ( media . length > 0 ) {
632+ result . push ( {
633+ id : Identifier . ascending ( "message" ) ,
634+ role : "user" ,
635+ parts : [
636+ {
637+ type : "text" as const ,
638+ text : "Attached image(s) from tool result:" ,
639+ } ,
640+ ...media . map ( ( attachment ) => ( {
641+ type : "file" as const ,
642+ url : attachment . url ,
643+ mediaType : attachment . mime ,
644+ } ) ) ,
645+ ] ,
646+ } )
647+ }
596648 }
597649 }
598650 }
0 commit comments