1313 Optional ,
1414 TypedDict ,
1515 Union ,
16+ cast ,
1617)
1718
1819from google .genai .types import Content , GenerateContentConfig , Part , PartDict
4445
4546if TYPE_CHECKING :
4647 from google .genai .types import (
48+ ContentDict ,
4749 ContentListUnion ,
4850 ContentUnion ,
4951 ContentUnionDict ,
5052 EmbedContentResponse ,
5153 GenerateContentResponse ,
5254 Model ,
5355 Tool ,
56+ ToolUnion ,
5457 )
5558
5659 from sentry_sdk ._types import TextPart
@@ -555,7 +558,7 @@ def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]":
555558
556559
557560def _format_tools_for_span (
558- tools : "Iterable[Tool | Callable[..., Any] ]" ,
561+ tools : "Iterable[ToolUnion ]" ,
559562) -> "Optional[List[dict[str, Any]]]" :
560563 """Format tools parameter for span data."""
561564 formatted_tools = []
@@ -604,13 +607,17 @@ def extract_tool_calls(
604607 tool_calls = []
605608
606609 # Extract from candidates, sometimes tool calls are nested under the content.parts object
607- if getattr (response , "candidates" , []):
608- for candidate in response .candidates :
610+ candidates = getattr (response , "candidates" , [])
611+ if response is not None :
612+ for candidate in candidates :
609613 if not hasattr (candidate , "content" ) or not getattr (
610614 candidate .content , "parts" , []
611615 ):
612616 continue
613617
618+ if candidate .content is None or candidate .content .parts is None :
619+ continue
620+
614621 for part in candidate .content .parts :
615622 if getattr (part , "function_call" , None ):
616623 function_call = part .function_call
@@ -627,34 +634,42 @@ def extract_tool_calls(
627634
628635 # Extract from automatic_function_calling_history
629636 # This is the history of tool calls made by the model
630- if getattr (response , "automatic_function_calling_history" , None ):
631- for content in response .automatic_function_calling_history :
632- if not getattr (content , "parts" , None ):
633- continue
637+ automatic_function_calling_history = getattr (
638+ response , "automatic_function_calling_history" , None
639+ )
640+ if automatic_function_calling_history is None :
641+ return tool_calls if tool_calls else None
634642
635- for part in getattr (content , "parts" , []):
636- if getattr (part , "function_call" , None ):
637- function_call = part .function_call
638- tool_call = {
639- "name" : getattr (function_call , "name" , None ),
640- "type" : "function_call" ,
641- }
643+ for content in automatic_function_calling_history :
644+ if not getattr (content , "parts" , None ):
645+ continue
642646
643- # Extract arguments if available
644- if hasattr (function_call , "args" ):
645- tool_call ["arguments" ] = safe_serialize (function_call .args )
647+ for part in getattr (content , "parts" , []):
648+ if getattr (part , "function_call" , None ):
649+ function_call = part .function_call
650+ tool_call = {
651+ "name" : getattr (function_call , "name" , None ),
652+ "type" : "function_call" ,
653+ }
646654
647- tool_calls .append (tool_call )
655+ # Extract arguments if available
656+ if hasattr (function_call , "args" ):
657+ tool_call ["arguments" ] = safe_serialize (function_call .args )
658+
659+ tool_calls .append (tool_call )
648660
649661 return tool_calls if tool_calls else None
650662
651663
652664def _capture_tool_input (
653- args : "tuple[Any, ...]" , kwargs : "dict[str, Any]" , tool : "Tool"
665+ args : "tuple[Any, ...]" , kwargs : "dict[str, Any]" , tool : "Tool | Callable[..., Any] "
654666) -> "dict[str, Any]" :
655667 """Capture tool input from args and kwargs."""
656668 tool_input = kwargs .copy () if kwargs else {}
657669
670+ if not callable (tool ):
671+ return tool_input
672+
658673 # If we have positional args, try to map them to the function signature
659674 if args :
660675 try :
@@ -767,29 +782,35 @@ def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any":
767782
768783
769784def wrapped_config_with_tools (
770- config : "GenerateContentConfig" ,
771- ) -> "GenerateContentConfig" :
785+ config : "GenerateContentConfig | None " ,
786+ ) -> "GenerateContentConfig | None " :
772787 """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as
773788 callable functions as a part of the config object."""
789+ if not config :
790+ return config
774791
775- if not config or not getattr (config , "tools" , None ):
792+ tools = getattr (config , "tools" , None )
793+ if tools is None :
776794 return config
777795
778796 result = copy .copy (config )
779- result .tools = [wrapped_tool (tool ) for tool in config . tools ]
797+ result .tools = [wrapped_tool (tool ) for tool in tools ]
780798
781799 return result
782800
783801
784802def _extract_response_text (
785803 response : "GenerateContentResponse" ,
786- ) -> "Optional[List[str]]" :
804+ ) -> "Optional[List[str | None ]]" :
787805 """Extract text from response candidates."""
788806
789807 if not response or not getattr (response , "candidates" , []):
790808 return None
791809
792- texts = []
810+ texts : "list[str | None]" = []
811+ if response .candidates is None :
812+ return texts if texts else None
813+
793814 for candidate in response .candidates :
794815 if not hasattr (candidate , "content" ) or not hasattr (candidate .content , "parts" ):
795816 continue
@@ -811,7 +832,11 @@ def extract_finish_reasons(
811832 if not response or not getattr (response , "candidates" , []):
812833 return None
813834
814- finish_reasons = []
835+ finish_reasons : "list[str]" = []
836+
837+ if response .candidates is None :
838+ return finish_reasons if finish_reasons else None
839+
815840 for candidate in response .candidates :
816841 if getattr (candidate , "finish_reason" , None ):
817842 # Convert enum value to string if necessary
@@ -843,16 +868,29 @@ def _transform_system_instruction_one_level(
843868 text_parts .append ({"type" : "text" , "content" : part .text })
844869 return text_parts
845870
846- if isinstance (system_instructions , dict ) and system_instructions .get ("text" ):
847- return [{"type" : "text" , "content" : system_instructions ["text" ]}]
871+ if isinstance (system_instructions , dict ) and "text" in system_instructions :
872+ text = cast ("PartDict" , system_instructions )["text" ]
873+ if text is None :
874+ return []
875+
876+ return [{"type" : "text" , "content" : text }]
848877
849878 elif can_be_content and isinstance (system_instructions , dict ):
850- parts = system_instructions .get ("parts" , [])
879+ parts = cast ("ContentDict" , system_instructions ).get ("parts" , [])
880+ if parts is None :
881+ return text_parts
882+
851883 for part in parts :
852884 if isinstance (part , Part ) and isinstance (part .text , str ):
853885 text_parts .append ({"type" : "text" , "content" : part .text })
854- elif isinstance (part , dict ) and isinstance (part .get ("text" ), str ):
855- text_parts .append ({"type" : "text" , "content" : part ["text" ]})
886+ continue
887+
888+ if not isinstance (part , dict ):
889+ continue
890+
891+ text = part .get ("text" )
892+ if isinstance (text , str ):
893+ text_parts .append ({"type" : "text" , "content" : text })
856894 return text_parts
857895
858896 return text_parts
@@ -1002,11 +1040,13 @@ def set_span_data_for_response(
10021040 span , SPANDATA .GEN_AI_RESPONSE_FINISH_REASONS , finish_reasons
10031041 )
10041042
1005- if getattr (response , "response_id" , None ):
1006- set_on_span (SPANDATA .GEN_AI_RESPONSE_ID , response .response_id )
1043+ response_id = getattr (response , "response_id" , None )
1044+ if response_id is not None :
1045+ set_on_span (SPANDATA .GEN_AI_RESPONSE_ID , response_id )
10071046
1008- if getattr (response , "model_version" , None ):
1009- set_on_span (SPANDATA .GEN_AI_RESPONSE_MODEL , response .model_version )
1047+ model_version = getattr (response , "model_version" , None )
1048+ if model_version is not None :
1049+ set_on_span (SPANDATA .GEN_AI_RESPONSE_MODEL , model_version )
10101050
10111051 usage_data = extract_usage_data (response )
10121052
@@ -1065,7 +1105,7 @@ def prepare_generate_content_args(
10651105 contents = args [1 ] if len (args ) > 1 else kwargs .get ("contents" )
10661106 model_name = get_model_name (model )
10671107
1068- config = kwargs .get ("config" )
1108+ config : "GenerateContentConfig | None" = kwargs .get ("config" )
10691109 wrapped_config = wrapped_config_with_tools (config )
10701110 if wrapped_config is not config :
10711111 kwargs ["config" ] = wrapped_config
0 commit comments