This commit is contained in:
JingHao-Leon 2026-09-30 21:34:00 -04:00 • committed by GitHub
commit 8d0f7ad253
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 3 deletions

View file

@ -1832,6 +1832,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

View file

@ -156,11 +156,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():
@ -176,6 +181,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`)