Skip to content

Commit 02deeb9

Browse files
google-genai-botcopybara-github
authored andcommitted
feat(analytics): add support for logging LLM cache metadata to BigQuery
PiperOrigin-RevId: 905280434
1 parent 599079b commit 02deeb9

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,13 +1824,25 @@ def _get_events_schema() -> list[bigquery.SchemaField]:
18241824
"CAST(JSON_VALUE(content, '$.usage.total')"
18251825
" AS INT64) AS usage_total_tokens"
18261826
),
1827+
(
1828+
"CAST(JSON_VALUE(attributes,"
1829+
" '$.usage_metadata.cached_content_token_count') AS INT64) AS"
1830+
" usage_cached_tokens"
1831+
),
1832+
(
1833+
"SAFE_DIVIDE(CAST(JSON_VALUE(attributes,"
1834+
" '$.usage_metadata.cached_content_token_count') AS"
1835+
" INT64),CAST(JSON_VALUE(content, '$.usage.prompt') AS INT64)) AS"
1836+
" context_cache_hit_rate"
1837+
),
18271838
"CAST(JSON_VALUE(latency_ms, '$.total_ms') AS INT64) AS total_ms",
18281839
(
18291840
"CAST(JSON_VALUE(latency_ms,"
18301841
" '$.time_to_first_token_ms') AS INT64) AS ttft_ms"
18311842
),
18321843
"JSON_VALUE(attributes, '$.model_version') AS model_version",
18331844
"JSON_QUERY(attributes, '$.usage_metadata') AS usage_metadata",
1845+
"JSON_QUERY(attributes, '$.cache_metadata') AS cache_metadata",
18341846
],
18351847
"LLM_ERROR": [
18361848
"CAST(JSON_VALUE(latency_ms, '$.total_ms') AS INT64) AS total_ms",
@@ -1929,6 +1941,7 @@ class EventData:
19291941
model: Optional[str] = None
19301942
model_version: Optional[str] = None
19311943
usage_metadata: Any = None
1944+
cache_metadata: Any = None
19321945
status: str = "OK"
19331946
error_message: Optional[str] = None
19341947
extra_attributes: dict[str, Any] = field(default_factory=dict)
@@ -2772,6 +2785,15 @@ def _enrich_attributes(
27722785
else:
27732786
attrs["usage_metadata"] = event_data.usage_metadata
27742787

2788+
if event_data.cache_metadata:
2789+
cache_meta_dict, _ = _recursive_smart_truncate(
2790+
event_data.cache_metadata, self.config.max_content_length
2791+
)
2792+
if isinstance(cache_meta_dict, dict):
2793+
attrs["cache_metadata"] = cache_meta_dict
2794+
else:
2795+
attrs["cache_metadata"] = event_data.cache_metadata
2796+
27752797
if self.config.log_session_metadata:
27762798
try:
27772799
session = callback_context._invocation_context.session
@@ -3331,6 +3353,7 @@ async def after_model_callback(
33313353
time_to_first_token_ms=tfft,
33323354
model_version=llm_response.model_version,
33333355
usage_metadata=llm_response.usage_metadata,
3356+
cache_metadata=getattr(llm_response, "cache_metadata", None),
33343357
span_id_override=span_id if is_popped else None,
33353358
parent_span_id_override=(parent_span_id if is_popped else None),
33363359
),

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7112,6 +7112,70 @@ def _fake_run_coroutine_threadsafe(coro, loop):
71127112
assert call_args[0][1] is other_loop
71137113

71147114

7115+
class TestCacheMetadataLogging:
7116+
"""Tests for logging cache_metadata from LlmResponse."""
7117+
7118+
@pytest.mark.asyncio
7119+
async def test_cache_metadata_logged_when_present(
7120+
self,
7121+
bq_plugin_inst,
7122+
mock_write_client,
7123+
callback_context,
7124+
dummy_arrow_schema,
7125+
):
7126+
"""Verifies cache_metadata is logged into BigQuery attributes when present."""
7127+
llm_response = llm_response_lib.LlmResponse(
7128+
content=types.Content(parts=[types.Part(text="Cache test")]),
7129+
cache_metadata={"fingerprint": "abc-123", "contents_count": 2},
7130+
)
7131+
bigquery_agent_analytics_plugin.TraceManager.push_span(callback_context)
7132+
await bq_plugin_inst.after_model_callback(
7133+
callback_context=callback_context,
7134+
llm_response=llm_response,
7135+
)
7136+
await asyncio.sleep(0.05)
7137+
rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema)
7138+
log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE")
7139+
7140+
attributes = json.loads(log_entry["attributes"])
7141+
assert "cache_metadata" in attributes
7142+
assert attributes["cache_metadata"]["fingerprint"] == "abc-123"
7143+
assert attributes["cache_metadata"]["contents_count"] == 2
7144+
7145+
@pytest.mark.asyncio
7146+
async def test_missing_cache_metadata_does_not_crash(
7147+
self,
7148+
bq_plugin_inst,
7149+
mock_write_client,
7150+
callback_context,
7151+
dummy_arrow_schema,
7152+
):
7153+
"""Verifies missing cache_metadata gracefully defaults using getattr."""
7154+
7155+
class LegacyLlmResponse:
7156+
7157+
def __init__(self):
7158+
self.content = types.Content(parts=[types.Part(text="Mock text")])
7159+
self.usage_metadata = None
7160+
self.model_version = "v1"
7161+
self.partial = False
7162+
# Deliberately omitting cache_metadata
7163+
7164+
mock_response = LegacyLlmResponse()
7165+
7166+
bigquery_agent_analytics_plugin.TraceManager.push_span(callback_context)
7167+
await bq_plugin_inst.after_model_callback(
7168+
callback_context=callback_context,
7169+
llm_response=mock_response,
7170+
)
7171+
await asyncio.sleep(0.05)
7172+
rows = await _get_captured_rows_async(mock_write_client, dummy_arrow_schema)
7173+
log_entry = next(r for r in rows if r["event_type"] == "LLM_RESPONSE")
7174+
7175+
attributes = json.loads(log_entry["attributes"])
7176+
assert "cache_metadata" not in attributes
7177+
7178+
71157179
# ==============================================================
71167180
# TEST CLASS: A2A_INTERACTION event logging via on_event_callback
71177181
# ==============================================================

0 commit comments

Comments
 (0)