Skip to content

Commit 60420d1

Browse files
committed
Fixed bug usage.prompt_tokens_details=None
1 parent dc76021 commit 60420d1

File tree

1 file changed

+48
-3
lines changed

1 file changed

+48
-3
lines changed

src/openai/_models.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
494529
def 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

Comments
 (0)