mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(types): safe attribute access on PromptTokensDetailsWrapper deleted fields
__init__ deletes None-valued cache fields from the instance dict for payload hygiene, but pydantic v2 then raises AttributeError on direct reads of those deleted fields (issue #43756: first-turn responses with no cache activity crash downstream consumers that read wrapper.cache_creation_tokens directly). Add a __getattr__ fallback on the wrapper returning the declared class default for deleted fields, keeping payload hygiene (model_dump still excludes them) while eliminating the crash. The old hasattr-based expectations in test_prompt_tokens_details_no_cache_write_tokens_when_absent are updated to the new direct-read contract, plus regression tests for both deleted-field reads and dump exclusion. Fixes #43756
This commit is contained in:
parent
118ce3cc91
commit
8e78990f01
2 changed files with 51 additions and 3 deletions
|
|
@ -1827,6 +1827,23 @@ class PromptTokensDetailsWrapper(
|
|||
if self.cached_tokens_details is None:
|
||||
del self.cached_tokens_details
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
"""Return declared defaults for payload-hygiene deletions (issue #43756).
|
||||
|
||||
__init__ deletes None-valued fields from the instance dict so they stay
|
||||
out of serialized payloads; pydantic v2 then raises on direct attribute
|
||||
access to those deleted fields. This fallback returns the declared
|
||||
class default instead, so downstream direct reads
|
||||
(e.g. wrapper.cache_creation_tokens on a first-turn response with no
|
||||
cache activity) behave like a zero-valued field rather than crashing.
|
||||
"""
|
||||
field_info = type(self).model_fields.get(name)
|
||||
if field_info is not None:
|
||||
return field_info.get_default()
|
||||
raise AttributeError(
|
||||
f"{type(self).__name__!s} object has no attribute {name!r}"
|
||||
)
|
||||
|
||||
|
||||
class ServerToolUse(BaseModel):
|
||||
web_search_requests: int | None = None
|
||||
|
|
|
|||
|
|
@ -154,11 +154,16 @@ def test_usage_anthropic_cache_creation_maps_to_cache_write_tokens():
|
|||
|
||||
|
||||
def test_prompt_tokens_details_no_cache_write_tokens_when_absent():
|
||||
"""A read-only cache hit (no cache write) must not surface cache-write fields."""
|
||||
"""A read-only cache hit (no cache write) must not surface cache-write fields
|
||||
in the serialized payload; direct reads of deleted fields return the declared
|
||||
defaults instead of raising AttributeError (issue #43756)."""
|
||||
details = PromptTokensDetailsWrapper(cached_tokens=800)
|
||||
assert details.cached_tokens == 800
|
||||
assert not hasattr(details, "cache_write_tokens")
|
||||
assert not hasattr(details, "cache_creation_tokens")
|
||||
assert details.cache_write_tokens is None
|
||||
assert details.cache_creation_tokens is None
|
||||
dumped = details.model_dump()
|
||||
assert "cache_write_tokens" not in dumped
|
||||
assert "cache_creation_tokens" not in dumped
|
||||
|
||||
|
||||
def test_prompt_tokens_details_cache_write_creation_stay_in_sync_on_assignment():
|
||||
|
|
@ -174,6 +179,32 @@ def test_prompt_tokens_details_cache_write_creation_stay_in_sync_on_assignment()
|
|||
assert details.cache_write_tokens == details.cache_creation_tokens == 375
|
||||
|
||||
|
||||
def test_prompt_tokens_details_none_cache_fields_read_as_none():
|
||||
"""Direct reads of deleted None-valued fields must return the declared
|
||||
defaults instead of raising AttributeError (issue #43756)."""
|
||||
wrapper = PromptTokensDetailsWrapper(
|
||||
prompt_tokens=100,
|
||||
cache_creation_tokens=None,
|
||||
cache_read_input_tokens=None,
|
||||
cache_write_tokens=None,
|
||||
)
|
||||
assert wrapper.cache_creation_tokens is None
|
||||
assert wrapper.cache_write_tokens is None
|
||||
|
||||
|
||||
def test_prompt_tokens_details_dump_excludes_deleted_none_fields():
|
||||
"""Payload hygiene survives: deleted None fields stay out of model_dump."""
|
||||
wrapper = PromptTokensDetailsWrapper(
|
||||
prompt_tokens=100,
|
||||
cache_creation_tokens=None,
|
||||
cache_read_input_tokens=None,
|
||||
cache_write_tokens=None,
|
||||
)
|
||||
dumped = wrapper.model_dump()
|
||||
assert "cache_write_tokens" not in dumped
|
||||
assert "cache_creation_tokens" not in dumped
|
||||
|
||||
|
||||
def test_potential_model_names_keeps_provider_prefixed_candidate():
|
||||
"""A provider whose own model ids repeat the litellm provider name (Perplexity's
|
||||
Agent API serves `perplexity/glm-5.2`, mapped as `perplexity/perplexity/glm-5.2`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue