Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/google/adk/models/anthropic_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ def message_to_generate_content_response(
parts=parts,
),
usage_metadata=usage_metadata,
cache_creation_token_count=cache_creation,
finish_reason=to_google_genai_finish_reason(message.stop_reason),
)

Expand Down Expand Up @@ -1145,6 +1146,7 @@ async def _generate_content_streaming(
yield LlmResponse(
content=types.Content(role="model", parts=all_parts),
usage_metadata=usage_metadata,
cache_creation_token_count=cache_creation_tokens,
finish_reason=to_google_genai_finish_reason(stop_reason),
partial=False,
)
Expand Down
9 changes: 9 additions & 0 deletions src/google/adk/models/lite_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2409,6 +2409,7 @@ def _model_response_to_generate_content_response(
"cache_creation_input_tokens",
cache_creation,
)
llm_response.cache_creation_token_count = cache_creation

grounding_metadata = _extract_grounding_metadata(response)
if grounding_metadata:
Expand Down Expand Up @@ -3133,6 +3134,7 @@ async def generate_content_async(
aggregated_llm_response = None
aggregated_llm_response_with_tool_call = None
usage_metadata = None
cache_creation_token_count = None
grounding_metadata = None
fallback_index = 0

Expand Down Expand Up @@ -3294,6 +3296,7 @@ def _reset_stream_buffers() -> None:
"cache_creation_input_tokens",
chunk.cache_creation_tokens,
)
cache_creation_token_count = chunk.cache_creation_tokens

# LiteLLM 1.81+ can set finish_reason="stop" on partial chunks. Only
# finalize tool calls on an explicit tool_calls/length finish_reason,
Expand Down Expand Up @@ -3344,6 +3347,9 @@ def _reset_stream_buffers() -> None:
if aggregated_llm_response:
if usage_metadata:
aggregated_llm_response.usage_metadata = usage_metadata
aggregated_llm_response.cache_creation_token_count = (
cache_creation_token_count
)
usage_metadata = None
if grounding_metadata:
aggregated_llm_response.grounding_metadata = grounding_metadata
Expand All @@ -3352,6 +3358,9 @@ def _reset_stream_buffers() -> None:
if aggregated_llm_response_with_tool_call:
if usage_metadata:
aggregated_llm_response_with_tool_call.usage_metadata = usage_metadata
aggregated_llm_response_with_tool_call.cache_creation_token_count = (
cache_creation_token_count
)
if grounding_metadata:
aggregated_llm_response_with_tool_call.grounding_metadata = (
grounding_metadata
Expand Down
16 changes: 16 additions & 0 deletions src/google/adk/models/llm_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ class LlmResponse(BaseModel):
usage_metadata: Optional[types.GenerateContentResponseUsageMetadata] = None
"""The usage metadata of the LlmResponse"""

cache_creation_token_count: Optional[int] = None
"""Number of prompt tokens written to the prompt cache for this response.

The cache-*write* counterpart to
``usage_metadata.cached_content_token_count`` (cache-*read*); both are a
breakdown of ``usage_metadata.prompt_token_count``.

The model backends also attach this value to ``usage_metadata`` as
``cache_creation_input_tokens`` for OpenTelemetry, but ``google.genai``'s
usage type forbids extra fields, so that attribute is dropped on
serialization. This field surfaces the same count on the (serializable)
``LlmResponse`` / ``Event`` so it is visible to the dev UI, persisted
sessions, and other event consumers -- not only telemetry. Only populated
for providers/turns that report cache writes; ``None`` otherwise.
"""

live_session_resumption_update: Optional[
types.LiveServerSessionResumptionUpdate
] = None
Expand Down
12 changes: 12 additions & 0 deletions tests/unittests/models/test_anthropic_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,16 @@ def test_message_to_generate_content_response_reports_cache_creation_tokens():
response = message_to_generate_content_response(message)

assert response.usage_metadata.cache_creation_input_tokens == 50
# Serializable mirror: the usage_metadata attribute above is dropped by
# model_dump (google.genai forbids extra fields), so the count is also
# exposed on LlmResponse.cache_creation_token_count for event consumers.
assert response.cache_creation_token_count == 50
assert (
response.model_dump(by_alias=True, exclude_none=True)[
"cacheCreationTokenCount"
]
== 50
)
dumped = response.model_dump()
assert "usage_metadata" in dumped

Expand Down Expand Up @@ -2041,6 +2051,7 @@ def test_message_to_generate_content_response_no_cache_creation_tokens():
response = message_to_generate_content_response(message)

assert not hasattr(response.usage_metadata, "cache_creation_input_tokens")
assert response.cache_creation_token_count is None


@pytest.mark.asyncio
Expand Down Expand Up @@ -2097,6 +2108,7 @@ async def test_streaming_reports_cache_creation_tokens():
assert len(responses) == 2
final_response = responses[-1]
assert final_response.usage_metadata.cache_creation_input_tokens == 50
assert final_response.cache_creation_token_count == 50
dumped = final_response.model_dump()
assert "usage_metadata" in dumped

Expand Down
2 changes: 2 additions & 0 deletions tests/unittests/models/test_litellm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,7 @@ async def test_generate_content_async_with_bedrock_cache_tokens(
assert response.usage_metadata.total_token_count == 15
assert response.usage_metadata.cached_content_token_count == 8
assert response.usage_metadata.cache_creation_input_tokens == 4
assert response.cache_creation_token_count == 4

mock_acompletion.assert_called_once()

Expand Down Expand Up @@ -4597,6 +4598,7 @@ async def test_generate_content_async_stream_with_bedrock_cache_tokens(
assert responses[3].usage_metadata.total_token_count == 15
assert responses[3].usage_metadata.cached_content_token_count == 8
assert responses[3].usage_metadata.cache_creation_input_tokens == 4
assert responses[3].cache_creation_token_count == 4


@pytest.mark.asyncio
Expand Down