@@ -231,10 +231,12 @@ async def mock_receive_generator():
231231 content_response = next ((r for r in responses if r .content ), None )
232232 assert content_response is not None
233233
234+ # The live API's `response_token_count`/`response_tokens_details` are remapped
235+ # to `candidates_token_count`/`candidates_tokens_details`.
234236 expected_usage = types .GenerateContentResponseUsageMetadata (
235237 prompt_token_count = 10 ,
236238 cached_content_token_count = 5 ,
237- candidates_token_count = None ,
239+ candidates_token_count = 20 ,
238240 total_token_count = 35 ,
239241 thoughts_token_count = 2 ,
240242 prompt_tokens_details = [
@@ -243,12 +245,74 @@ async def mock_receive_generator():
243245 cache_tokens_details = [
244246 types .ModalityTokenCount (modality = 'text' , token_count = 5 )
245247 ],
246- candidates_tokens_details = None ,
248+ candidates_tokens_details = [
249+ types .ModalityTokenCount (modality = 'text' , token_count = 20 )
250+ ],
247251 )
248252 assert usage_response .usage_metadata == expected_usage
249253 assert content_response .content == mock_content
250254
251255
256+ async def test_receive_usage_metadata_remaps_output_tokens (
257+ gemini_connection , mock_gemini_session
258+ ):
259+ """Test that live API output tokens are remapped to candidates_token_count."""
260+ usage_metadata = types .UsageMetadata (
261+ prompt_token_count = 10 ,
262+ cached_content_token_count = 5 ,
263+ response_token_count = 20 ,
264+ total_token_count = 35 ,
265+ thoughts_token_count = 2 ,
266+ tool_use_prompt_token_count = 3 ,
267+ prompt_tokens_details = [
268+ types .ModalityTokenCount (modality = 'text' , token_count = 10 )
269+ ],
270+ cache_tokens_details = [
271+ types .ModalityTokenCount (modality = 'text' , token_count = 5 )
272+ ],
273+ response_tokens_details = [
274+ types .ModalityTokenCount (modality = 'text' , token_count = 20 )
275+ ],
276+ )
277+
278+ mock_message = mock .AsyncMock ()
279+ mock_message .usage_metadata = usage_metadata
280+ mock_message .server_content = None
281+ mock_message .tool_call = None
282+ mock_message .session_resumption_update = None
283+ mock_message .go_away = None
284+
285+ async def mock_receive_generator ():
286+ yield mock_message
287+
288+ receive_mock = mock .Mock (return_value = mock_receive_generator ())
289+ mock_gemini_session .receive = receive_mock
290+
291+ responses = [resp async for resp in gemini_connection .receive ()]
292+
293+ usage_response = next ((r for r in responses if r .usage_metadata ), None )
294+ assert usage_response is not None
295+ result = usage_response .usage_metadata
296+ assert isinstance (result , types .GenerateContentResponseUsageMetadata )
297+ # Output tokens are remapped from response_* to candidates_*.
298+ assert result .candidates_token_count == 20
299+ assert result .candidates_tokens_details == [
300+ types .ModalityTokenCount (modality = 'text' , token_count = 20 )
301+ ]
302+ # Shared fields are carried over unchanged.
303+ assert result .prompt_token_count == 10
304+ assert result .cached_content_token_count == 5
305+ assert result .total_token_count == 35
306+ assert result .thoughts_token_count == 2
307+ assert result .tool_use_prompt_token_count == 3
308+ assert result .prompt_tokens_details == [
309+ types .ModalityTokenCount (modality = 'text' , token_count = 10 )
310+ ]
311+ assert result .cache_tokens_details == [
312+ types .ModalityTokenCount (modality = 'text' , token_count = 5 )
313+ ]
314+
315+
252316async def test_receive_populates_live_session_id (
253317 gemini_connection , mock_gemini_session
254318):
0 commit comments