@@ -491,12 +491,46 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
491491 return cast (_T , construct_type (value = value , type_ = type_ ))
492492
493493
494+ def _preprocess_completion_usage (type_ : object , value : Any ) -> Any :
495+ """Preprocess CompletionUsage data to ensure token details fields are dicts, not None.
496+
497+ This handles cases where APIs (like Gemini) don't include prompt_tokens_details
498+ or completion_tokens_details in their response. Instead of leaving them as None,
499+ we create empty dicts so the nested models get constructed with default None values.
500+
501+ Args:
502+ type_: The type being constructed (should be CompletionUsage, already unwrapped)
503+ value: The data dict from the API response
504+
505+ Returns:
506+ Preprocessed data dict with empty dicts for missing token details
507+ """
508+ # Only preprocess if constructing CompletionUsage
509+ if not (isinstance (type_ , type ) and type_ .__name__ == "CompletionUsage" ):
510+ return value
511+
512+ if not is_mapping (value ):
513+ return value
514+
515+ # Make a copy to avoid mutating the original
516+ result = dict (value )
517+
518+ # If prompt_tokens_details is missing or None, set it to empty dict
519+ if "prompt_tokens_details" not in result or result .get ("prompt_tokens_details" ) is None :
520+ result ["prompt_tokens_details" ] = {}
521+
522+ # If completion_tokens_details is missing or None, set it to empty dict
523+ if "completion_tokens_details" not in result or result .get ("completion_tokens_details" ) is None :
524+ result ["completion_tokens_details" ] = {}
525+
526+ return result
527+
528+
494529def construct_type (* , value : object , type_ : object , metadata : Optional [List [Any ]] = None ) -> object :
495530 """Loose coercion to the expected type with construction of nested values.
496531
497532 If the given value does not match the expected type then it is returned as-is.
498533 """
499-
500534 # store a reference to the original type we were given before we extract any inner
501535 # types so that we can properly resolve forward references in `TypeAliasType` annotations
502536 original_type = None
@@ -523,6 +557,14 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
523557 args = get_args (type_ )
524558
525559 if is_union (origin ):
560+ # Preprocess before validation for Optional[CompletionUsage]
561+ # Check if any of the union args is CompletionUsage
562+ completion_usage_type = next (
563+ (arg for arg in args if isinstance (arg , type ) and arg .__name__ == "CompletionUsage" ), None
564+ )
565+ if completion_usage_type is not None :
566+ value = _preprocess_completion_usage (completion_usage_type , value )
567+
526568 try :
527569 return validate_type (type_ = cast ("type[object]" , original_type or type_ ), value = value )
528570 except Exception :
@@ -540,7 +582,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
540582 # kind: Literal['bar']
541583 # value: int
542584 #
543- # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then
585+ # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}' then
544586 # we'd end up constructing `FooType` when it should be `BarType`.
545587 discriminator = _build_discriminated_union_meta (union = type_ , meta_annotations = meta )
546588 if discriminator and is_mapping (value ):
@@ -576,7 +618,10 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
576618
577619 if is_mapping (value ):
578620 if issubclass (type_ , BaseModel ):
579- return type_ .construct (** value ) # type: ignore[arg-type]
621+ # Preprocess data for CompletionUsage to ensure prompt_tokens_details
622+ # is always a dict (not None) when missing from API response
623+ preprocessed_value = _preprocess_completion_usage (type_ , value )
624+ return type_ .construct (** preprocessed_value ) # type: ignore[arg-type]
580625
581626 return cast (Any , type_ ).construct (** value )
582627
0 commit comments