@@ -750,8 +750,7 @@ async def _map_user_message(self, message: ModelRequest) -> AsyncIterable[chat.C
750750 else :
751751 assert_never (part )
752752
753- @staticmethod
754- async def _map_user_prompt (part : UserPromptPart ) -> chat .ChatCompletionUserMessageParam :
753+ async def _map_user_prompt (self , part : UserPromptPart ) -> chat .ChatCompletionUserMessageParam : # noqa: C901
755754 content : str | list [ChatCompletionContentPartParam ]
756755 if isinstance (part .content , str ):
757756 content = part .content
@@ -766,28 +765,40 @@ async def _map_user_prompt(part: UserPromptPart) -> chat.ChatCompletionUserMessa
766765 image_url ['detail' ] = metadata .get ('detail' , 'auto' )
767766 content .append (ChatCompletionContentPartImageParam (image_url = image_url , type = 'image_url' ))
768767 elif isinstance (item , BinaryContent ):
769- base64_encoded = base64 .b64encode (item .data ).decode ('utf-8' )
770- if item .is_image :
771- image_url : ImageURL = {'url' : f'data:{ item .media_type } ;base64,{ base64_encoded } ' }
772- if metadata := item .vendor_metadata :
773- image_url ['detail' ] = metadata .get ('detail' , 'auto' )
774- content .append (ChatCompletionContentPartImageParam (image_url = image_url , type = 'image_url' ))
775- elif item .is_audio :
776- assert item .format in ('wav' , 'mp3' )
777- audio = InputAudio (data = base64_encoded , format = item .format )
778- content .append (ChatCompletionContentPartInputAudioParam (input_audio = audio , type = 'input_audio' ))
779- elif item .is_document :
768+ if self ._is_text_like_media_type (item .media_type ):
769+ # Inline text-like binary content as a text block
780770 content .append (
781- File (
782- file = FileFile (
783- file_data = f'data:{ item .media_type } ;base64,{ base64_encoded } ' ,
784- filename = f'filename.{ item .format } ' ,
785- ),
786- type = 'file' ,
771+ self ._inline_text_file_part (
772+ item .data .decode ('utf-8' ),
773+ media_type = item .media_type ,
774+ identifier = item .identifier ,
787775 )
788776 )
789- else : # pragma: no cover
790- raise RuntimeError (f'Unsupported binary content type: { item .media_type } ' )
777+ else :
778+ base64_encoded = base64 .b64encode (item .data ).decode ('utf-8' )
779+ if item .is_image :
780+ image_url : ImageURL = {'url' : f'data:{ item .media_type } ;base64,{ base64_encoded } ' }
781+ if metadata := item .vendor_metadata :
782+ image_url ['detail' ] = metadata .get ('detail' , 'auto' )
783+ content .append (ChatCompletionContentPartImageParam (image_url = image_url , type = 'image_url' ))
784+ elif item .is_audio :
785+ assert item .format in ('wav' , 'mp3' )
786+ audio = InputAudio (data = base64_encoded , format = item .format )
787+ content .append (
788+ ChatCompletionContentPartInputAudioParam (input_audio = audio , type = 'input_audio' )
789+ )
790+ elif item .is_document :
791+ content .append (
792+ File (
793+ file = FileFile (
794+ file_data = f'data:{ item .media_type } ;base64,{ base64_encoded } ' ,
795+ filename = f'filename.{ item .format } ' ,
796+ ),
797+ type = 'file' ,
798+ )
799+ )
800+ else : # pragma: no cover
801+ raise RuntimeError (f'Unsupported binary content type: { item .media_type } ' )
791802 elif isinstance (item , AudioUrl ):
792803 downloaded_item = await download_item (item , data_format = 'base64' , type_format = 'extension' )
793804 assert downloaded_item ['data_type' ] in (
@@ -797,20 +808,54 @@ async def _map_user_prompt(part: UserPromptPart) -> chat.ChatCompletionUserMessa
797808 audio = InputAudio (data = downloaded_item ['data' ], format = downloaded_item ['data_type' ])
798809 content .append (ChatCompletionContentPartInputAudioParam (input_audio = audio , type = 'input_audio' ))
799810 elif isinstance (item , DocumentUrl ):
800- downloaded_item = await download_item (item , data_format = 'base64_uri' , type_format = 'extension' )
801- file = File (
802- file = FileFile (
803- file_data = downloaded_item ['data' ], filename = f'filename.{ downloaded_item ["data_type" ]} '
804- ),
805- type = 'file' ,
806- )
807- content .append (file )
811+ if self ._is_text_like_media_type (item .media_type ):
812+ downloaded_text = await download_item (item , data_format = 'text' )
813+ content .append (
814+ self ._inline_text_file_part (
815+ downloaded_text ['data' ],
816+ media_type = item .media_type ,
817+ identifier = item .identifier ,
818+ )
819+ )
820+ else :
821+ downloaded_item = await download_item (item , data_format = 'base64_uri' , type_format = 'extension' )
822+ content .append (
823+ File (
824+ file = FileFile (
825+ file_data = downloaded_item ['data' ],
826+ filename = f'filename.{ downloaded_item ["data_type" ]} ' ,
827+ ),
828+ type = 'file' ,
829+ )
830+ )
808831 elif isinstance (item , VideoUrl ): # pragma: no cover
809832 raise NotImplementedError ('VideoUrl is not supported for OpenAI' )
810833 else :
811834 assert_never (item )
812835 return chat .ChatCompletionUserMessageParam (role = 'user' , content = content )
813836
837+ @staticmethod
838+ def _is_text_like_media_type (media_type : str ) -> bool :
839+ return (
840+ media_type .startswith ('text/' )
841+ or media_type == 'application/json'
842+ or media_type .endswith ('+json' )
843+ or media_type == 'application/xml'
844+ or media_type .endswith ('+xml' )
845+ or media_type in ('application/x-yaml' , 'application/yaml' )
846+ )
847+
848+ @staticmethod
849+ def _inline_text_file_part (text : str , * , media_type : str , identifier : str ) -> ChatCompletionContentPartTextParam :
850+ text = '\n ' .join (
851+ [
852+ f'-----BEGIN FILE id="{ identifier } " type="{ media_type } "-----' ,
853+ text ,
854+ f'-----END FILE id="{ identifier } "-----' ,
855+ ]
856+ )
857+ return ChatCompletionContentPartTextParam (text = text , type = 'text' )
858+
814859
815860@deprecated (
816861 '`OpenAIModel` was renamed to `OpenAIChatModel` to clearly distinguish it from `OpenAIResponsesModel` which '
0 commit comments