1616
1717from __future__ import annotations
1818
19+ from typing import Any
1920from typing import AsyncGenerator
2021from typing import Optional
2122
@@ -169,24 +170,33 @@ async def handle_before_model_callback(
169170 return None
170171
171172
172- def _inherit_unset_streaming_fields (
173+ def _inherit_unset_model_fields (
173174 original : LlmResponse , replacement : Optional [LlmResponse ]
174175) -> Optional [LlmResponse ]:
175- """Carries streaming-control fields from a replaced response.
176+ """Carries model-owned fields from a replaced response.
176177
177178 A callback replacement that leaves ``partial``/``turn_complete`` unset must
178179 not change the streaming semantics of the response it replaces. Otherwise
179180 every streamed delta looks final downstream: SSE clients render N final
180181 responses, ``Runner`` persists each delta as a separate session event, and
181- the live path can close its request queue early. An explicitly set value on
182- the replacement is always respected.
182+ the live path can close its request queue early.
183+
184+ ``usage_metadata`` is carried the same way. It measures the model call
185+ (prompt, candidate and cached token counts as billed by the provider), not
186+ the content of the response, so replacing the content does not change it.
187+ A replacement that leaves it unset would otherwise erase the call from
188+ token accounting: the finalized ``Event`` copies only non-None fields, so
189+ the persisted session event, the BigQuery analytics plugin and A2A
190+ converters all see no usage for that call.
191+
192+ An explicitly set value on the replacement is always respected.
183193
184194 Args:
185195 original: The response being replaced.
186196 replacement: The callback-provided response, if there is one.
187197
188198 Returns:
189- The replacement, with unset streaming-control fields filled in, or None
199+ The replacement, with unset model-owned fields filled in, or None
190200 when there is no replacement. A copy is returned only when a field
191201 actually needs filling, so explicitly complete replacements keep their
192202 identity.
@@ -198,11 +208,13 @@ def _inherit_unset_streaming_fields(
198208 replacement , LlmResponse
199209 ):
200210 return replacement
201- updates = {}
211+ updates : dict [ str , Any ] = {}
202212 if replacement .partial is None and original .partial is not None :
203213 updates ['partial' ] = original .partial
204214 if replacement .turn_complete is None and original .turn_complete is not None :
205215 updates ['turn_complete' ] = original .turn_complete
216+ if replacement .usage_metadata is None and original .usage_metadata is not None :
217+ updates ['usage_metadata' ] = original .usage_metadata
206218 if updates :
207219 return replacement .model_copy (update = updates )
208220 return replacement
@@ -266,7 +278,7 @@ async def _maybe_add_grounding_metadata(
266278 )
267279 )
268280 if callback_response :
269- return _inherit_unset_streaming_fields (
281+ return _inherit_unset_model_fields (
270282 llm_response ,
271283 await _maybe_add_grounding_metadata (callback_response ),
272284 )
@@ -280,7 +292,7 @@ async def _maybe_add_grounding_metadata(
280292 llm_response = llm_response ,
281293 )
282294 if callback_response :
283- return _inherit_unset_streaming_fields (
295+ return _inherit_unset_model_fields (
284296 llm_response ,
285297 await _maybe_add_grounding_metadata (callback_response ),
286298 )
0 commit comments