@@ -685,18 +685,24 @@ async def generate(
685685 await self ._save_history (filename , messages )
686686 return Prompt .assistant (f"History saved to { filename } " )
687687
688- # Store MCP metadata in context variable
689688 final_request_params = self .get_request_params (request_params )
690- if final_request_params .mcp_metadata :
691- _mcp_metadata_var .set (final_request_params .mcp_metadata )
689+ prepared_messages , prepared_request_params = self ._prepare_structured_request (
690+ messages ,
691+ final_request_params ,
692+ tools ,
693+ )
694+
695+ # Store MCP metadata in context variable
696+ if prepared_request_params .mcp_metadata :
697+ _mcp_metadata_var .set (prepared_request_params .mcp_metadata )
692698
693699 # The caller supplies the full conversation to send
694- full_history = messages
700+ full_history = prepared_messages
695701
696702 timing_capture , cleanup_timing_capture = self ._start_request_timing_capture ()
697703 try :
698704 assistant_response = await self ._execute_with_retry (
699- self ._apply_prompt_provider_specific , full_history , request_params , tools
705+ self ._apply_prompt_provider_specific , full_history , prepared_request_params , tools
700706 )
701707 finally :
702708 cleanup_timing_capture ()
@@ -796,44 +802,20 @@ async def structured(
796802 Returns:
797803 Tuple of (parsed model instance or None, assistant response message)
798804 """
799-
800- # Store MCP metadata in context variable
801- final_request_params = self .get_request_params (request_params )
802-
803- # TODO -- this doesn't need to go here anymore.
804- if final_request_params .mcp_metadata :
805- _mcp_metadata_var .set (final_request_params .mcp_metadata )
806-
807- full_history = messages
808-
809- timing_capture , cleanup_timing_capture = self ._start_request_timing_capture ()
810- try :
811- result_or_response = await self ._execute_with_retry (
812- self ._apply_prompt_provider_specific_structured ,
813- full_history ,
814- model ,
815- request_params ,
816- on_final_error = self ._handle_retry_failure ,
817- )
818- finally :
819- cleanup_timing_capture ()
820- if isinstance (result_or_response , PromptMessageExtended ):
821- result , assistant_response = self ._structured_from_multipart (result_or_response , model )
822- else :
823- result , assistant_response = result_or_response
824- end_time = time .perf_counter ()
825- self ._add_timing_channel (
826- assistant_response ,
827- timing_capture .start_time ,
828- end_time ,
829- ttft_ms = timing_capture .ttft_ms ,
830- time_to_response_ms = timing_capture .time_to_response_ms ,
805+ schema = validate_json_schema_definition (model .model_json_schema ())
806+ parsed_json , assistant_response = await self .structured_schema (
807+ messages ,
808+ schema ,
809+ request_params ,
831810 )
832-
833- self .usage_accumulator .count_tools (len (assistant_response .tool_calls or {}))
834- self ._append_usage_channel (assistant_response )
835-
836- return result , assistant_response
811+ if parsed_json is None :
812+ return None , assistant_response
813+ try :
814+ return model .model_validate (parsed_json ), assistant_response
815+ except Exception as e :
816+ logger = get_logger (__name__ )
817+ logger .warning (f"Failed to validate structured response: { str (e )} " )
818+ return None , assistant_response
837819
838820 async def structured_schema (
839821 self ,
@@ -858,43 +840,12 @@ async def structured_schema(
858840 update = {"structured_schema" : normalized_schema }
859841 )
860842
861- if final_request_params .mcp_metadata :
862- _mcp_metadata_var .set (final_request_params .mcp_metadata )
863-
864- timing_capture , cleanup_timing_capture = self ._start_request_timing_capture ()
865- try :
866- result_or_response = await self ._execute_with_retry (
867- self ._apply_prompt_provider_specific_structured_schema ,
868- messages ,
869- normalized_schema ,
870- final_request_params ,
871- on_final_error = self ._handle_retry_failure ,
872- )
873- finally :
874- cleanup_timing_capture ()
875-
876- if isinstance (result_or_response , PromptMessageExtended ):
877- result , assistant_response = self ._structured_schema_from_multipart (
878- result_or_response ,
879- normalized_schema ,
880- )
881- else :
882- result , assistant_response = result_or_response
883-
884- end_time = time .perf_counter ()
885- self ._add_timing_channel (
843+ assistant_response = await self .generate (messages , final_request_params )
844+ return self ._structured_schema_from_multipart (
886845 assistant_response ,
887- timing_capture .start_time ,
888- end_time ,
889- ttft_ms = timing_capture .ttft_ms ,
890- time_to_response_ms = timing_capture .time_to_response_ms ,
846+ normalized_schema ,
891847 )
892848
893- self .usage_accumulator .count_tools (len (assistant_response .tool_calls or {}))
894- self ._append_usage_channel (assistant_response )
895-
896- return result , assistant_response
897-
898849 @staticmethod
899850 def model_to_response_format (
900851 model : Type [Any ],
@@ -1032,6 +983,16 @@ def _prepare_structured_text(self, text: str) -> str:
1032983 """Hook for subclasses to adjust structured output text before parsing."""
1033984 return text
1034985
986+ def _prepare_structured_request (
987+ self ,
988+ messages : list [PromptMessageExtended ],
989+ request_params : RequestParams ,
990+ tools : list [Tool ] | None = None ,
991+ ) -> tuple [list [PromptMessageExtended ], RequestParams ]:
992+ """Hook for providers to adapt structured-output intent before generation."""
993+ del tools
994+ return messages , request_params
995+
1035996 def record_templates (self , templates : list [PromptMessageExtended ]) -> None :
1036997 """Hook for providers that need template visibility (e.g., caching)."""
1037998 return
0 commit comments