@@ -512,6 +512,7 @@ def _process_response(self, response: chat.ChatCompletion | str) -> ModelRespons
512512 provider_details = vendor_details ,
513513 provider_response_id = response .id ,
514514 provider_name = self ._provider .name ,
515+ finish_reason = choice .finish_reason ,
515516 )
516517
517518 async def _process_streamed_response (
@@ -603,6 +604,39 @@ def _map_tool_call(t: ToolCallPart) -> ChatCompletionMessageFunctionToolCallPara
603604 function = {'name' : t .tool_name , 'arguments' : t .args_as_json_str ()},
604605 )
605606
607+
608+ def _map_openai_responses_finish (status : str | None , incomplete_reason : str | None ) -> tuple [str | None , str | None ]:
609+ """Map OpenAI Responses status/incomplete_details to (raw, OTEL-mapped) finish reasons.
610+
611+ Raw holds provider data for provider_details, while the mapped value is used for ModelResponse.finish_reason
612+ to comply with gen_ai.response.finish_reasons.
613+ """
614+ if status is None :
615+ return None , None
616+
617+ # Incomplete: use the reason for more specific mapping
618+ if status == 'incomplete' :
619+ raw = incomplete_reason or status
620+ if incomplete_reason == 'max_output_tokens' :
621+ return raw , 'length'
622+ if incomplete_reason == 'content_filter' :
623+ return raw , 'content_filter'
624+ if incomplete_reason == 'timeout' :
625+ return raw , 'timeout'
626+ # Unknown reason for incomplete
627+ return raw , 'other'
628+
629+ # Completed/cancelled/failed map to stop/cancelled/error
630+ if status == 'completed' :
631+ return status , 'stop'
632+ if status == 'cancelled' :
633+ return status , 'cancelled'
634+ if status == 'failed' :
635+ return status , 'error'
636+
637+ # Unknown/other statuses -> keep raw, do not set mapped
638+ return status , None
639+
606640 def _map_json_schema (self , o : OutputObjectDefinition ) -> chat .completion_create_params .ResponseFormat :
607641 response_format_param : chat .completion_create_params .ResponseFormatJSONSchema = { # pyright: ignore[reportPrivateImportUsage]
608642 'type' : 'json_schema' ,
@@ -820,13 +854,26 @@ def _process_response(self, response: responses.Response) -> ModelResponse:
820854 items .append (TextPart (content .text ))
821855 elif item .type == 'function_call' :
822856 items .append (ToolCallPart (item .name , item .arguments , tool_call_id = item .call_id ))
857+
858+ # Map OpenAI Responses status/incomplete_details to OTEL-compliant finish_reasons
859+ incomplete_reason = getattr (getattr (response , 'incomplete_details' , None ), 'reason' , None )
860+ raw_finish , mapped_finish = _map_openai_responses_finish (response .status , incomplete_reason )
861+
862+ provider_details : dict [str , Any ] | None = None
863+ if raw_finish is not None or mapped_finish is not None :
864+ provider_details = {'finish_reason' : raw_finish }
865+ if mapped_finish is not None :
866+ provider_details ['final_reason' ] = mapped_finish
867+
823868 return ModelResponse (
824869 parts = items ,
825870 usage = _map_usage (response ),
826871 model_name = response .model ,
827872 provider_response_id = response .id ,
828873 timestamp = timestamp ,
829874 provider_name = self ._provider .name ,
875+ finish_reason = mapped_finish ,
876+ provider_details = provider_details ,
830877 )
831878
832879 async def _process_streamed_response (
@@ -1166,11 +1213,19 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
11661213 async for chunk in self ._response :
11671214 self ._usage += _map_usage (chunk )
11681215
1216+ # Capture the response ID from the chunk
1217+ if chunk .id and self .provider_response_id is None :
1218+ self .provider_response_id = chunk .id
1219+
11691220 try :
11701221 choice = chunk .choices [0 ]
11711222 except IndexError :
11721223 continue
11731224
1225+ # Capture the finish_reason when it becomes available
1226+ if choice .finish_reason :
1227+ self .finish_reason = choice .finish_reason
1228+
11741229 # Handle the text part of the response
11751230 content = choice .delta .content
11761231 if content is not None :
@@ -1229,6 +1284,13 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
12291284 # NOTE: You can inspect the builtin tools used checking the `ResponseCompletedEvent`.
12301285 if isinstance (chunk , responses .ResponseCompletedEvent ):
12311286 self ._usage += _map_usage (chunk .response )
1287+ # Capture id and mapped finish_reason from completed response
1288+ if chunk .response .id and self .provider_response_id is None :
1289+ self .provider_response_id = chunk .response .id
1290+ if self .finish_reason is None :
1291+ incomplete_reason = getattr (getattr (chunk .response , 'incomplete_details' , None ), 'reason' , None )
1292+ _raw , mapped = _map_openai_responses_finish (chunk .response .status , incomplete_reason )
1293+ self .finish_reason = mapped
12321294
12331295 elif isinstance (chunk , responses .ResponseContentPartAddedEvent ):
12341296 pass # there's nothing we need to do here
@@ -1237,7 +1299,9 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
12371299 pass # there's nothing we need to do here
12381300
12391301 elif isinstance (chunk , responses .ResponseCreatedEvent ):
1240- pass # there's nothing we need to do here
1302+ # Capture id from created response
1303+ if chunk .response .id and self .provider_response_id is None :
1304+ self .provider_response_id = chunk .response .id
12411305
12421306 elif isinstance (chunk , responses .ResponseFailedEvent ): # pragma: no cover
12431307 self ._usage += _map_usage (chunk .response )
0 commit comments