Skip to content

Commit 230d018

Browse files
alexmojakiDouweM
andauthored
Extract google model usage using genai-prices (#3466)
Co-authored-by: Douwe Maan <[email protected]>
1 parent b336cb1 commit 230d018

File tree

4 files changed

+36
-36
lines changed

4 files changed

+36
-36
lines changed

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def _process_response(self, response: GenerateContentResponse) -> ModelResponse:
484484
else:
485485
parts = candidate.content.parts or []
486486

487-
usage = _metadata_as_usage(response)
487+
usage = _metadata_as_usage(response, provider=self._provider.name, provider_url=self._provider.base_url)
488488
return _process_response_from_parts(
489489
parts,
490490
candidate.grounding_metadata,
@@ -511,6 +511,7 @@ async def _process_streamed_response(
511511
_response=peekable_response,
512512
_timestamp=first_chunk.create_time or _utils.now_utc(),
513513
_provider_name=self._provider.name,
514+
_provider_url=self._provider.base_url,
514515
)
515516

516517
async def _map_messages(
@@ -628,11 +629,12 @@ class GeminiStreamedResponse(StreamedResponse):
628629
_response: AsyncIterator[GenerateContentResponse]
629630
_timestamp: datetime
630631
_provider_name: str
632+
_provider_url: str
631633

632634
async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
633635
code_execution_tool_call_id: str | None = None
634636
async for chunk in self._response:
635-
self._usage = _metadata_as_usage(chunk)
637+
self._usage = _metadata_as_usage(chunk, self._provider_name, self._provider_url)
636638

637639
if not chunk.candidates:
638640
continue # pragma: no cover
@@ -881,7 +883,7 @@ def _tool_config(function_names: list[str]) -> ToolConfigDict:
881883
return ToolConfigDict(function_calling_config=function_calling_config)
882884

883885

884-
def _metadata_as_usage(response: GenerateContentResponse) -> usage.RequestUsage:
886+
def _metadata_as_usage(response: GenerateContentResponse, provider: str, provider_url: str) -> usage.RequestUsage:
885887
metadata = response.usage_metadata
886888
if metadata is None:
887889
return usage.RequestUsage()
@@ -895,9 +897,6 @@ def _metadata_as_usage(response: GenerateContentResponse) -> usage.RequestUsage:
895897
if tool_use_prompt_token_count := metadata.tool_use_prompt_token_count:
896898
details['tool_use_prompt_tokens'] = tool_use_prompt_token_count
897899

898-
input_audio_tokens = 0
899-
output_audio_tokens = 0
900-
cache_audio_read_tokens = 0
901900
for prefix, metadata_details in [
902901
('prompt', metadata.prompt_tokens_details),
903902
('cache', metadata.cache_tokens_details),
@@ -911,22 +910,12 @@ def _metadata_as_usage(response: GenerateContentResponse) -> usage.RequestUsage:
911910
if not detail.modality or not detail.token_count:
912911
continue
913912
details[f'{detail.modality.lower()}_{prefix}_tokens'] = detail.token_count
914-
if detail.modality != 'AUDIO':
915-
continue
916-
if metadata_details is metadata.prompt_tokens_details:
917-
input_audio_tokens = detail.token_count
918-
elif metadata_details is metadata.candidates_tokens_details:
919-
output_audio_tokens = detail.token_count
920-
elif metadata_details is metadata.cache_tokens_details: # pragma: no branch
921-
cache_audio_read_tokens = detail.token_count
922-
923-
return usage.RequestUsage(
924-
input_tokens=metadata.prompt_token_count or 0,
925-
output_tokens=(metadata.candidates_token_count or 0) + thoughts_token_count,
926-
cache_read_tokens=cached_content_token_count or 0,
927-
input_audio_tokens=input_audio_tokens,
928-
output_audio_tokens=output_audio_tokens,
929-
cache_audio_read_tokens=cache_audio_read_tokens,
913+
914+
return usage.RequestUsage.extract(
915+
response.model_dump(include={'model_version', 'usage_metadata'}, by_alias=True),
916+
provider=provider,
917+
provider_url=provider_url,
918+
provider_fallback='google',
930919
details=details,
931920
)
932921

pydantic_ai_slim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ dependencies = [
6060
"exceptiongroup; python_version < '3.11'",
6161
"opentelemetry-api>=1.28.0",
6262
"typing-inspection>=0.4.0",
63-
"genai-prices>=0.0.35",
63+
"genai-prices>=0.0.40",
6464
]
6565

6666
[tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies]

tests/models/test_google.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ async def test_google_model_builtin_code_execution_stream(
362362
],
363363
usage=RequestUsage(
364364
input_tokens=46,
365-
output_tokens=528,
365+
output_tokens=1429,
366366
details={
367367
'thoughts_tokens': 396,
368368
'tool_use_prompt_tokens': 901,
@@ -1001,7 +1001,7 @@ async def test_google_model_web_search_tool(allow_model_requests: None, google_p
10011001
],
10021002
usage=RequestUsage(
10031003
input_tokens=17,
1004-
output_tokens=414,
1004+
output_tokens=533,
10051005
details={
10061006
'thoughts_tokens': 213,
10071007
'tool_use_prompt_tokens': 119,
@@ -1078,7 +1078,7 @@ async def test_google_model_web_search_tool(allow_model_requests: None, google_p
10781078
],
10791079
usage=RequestUsage(
10801080
input_tokens=209,
1081-
output_tokens=337,
1081+
output_tokens=623,
10821082
details={
10831083
'thoughts_tokens': 131,
10841084
'tool_use_prompt_tokens': 286,
@@ -1145,7 +1145,7 @@ async def test_google_model_web_search_tool_stream(allow_model_requests: None, g
11451145
],
11461146
usage=RequestUsage(
11471147
input_tokens=17,
1148-
output_tokens=653,
1148+
output_tokens=755,
11491149
details={
11501150
'thoughts_tokens': 412,
11511151
'tool_use_prompt_tokens': 102,
@@ -1322,7 +1322,7 @@ async def test_google_model_web_search_tool_stream(allow_model_requests: None, g
13221322
],
13231323
usage=RequestUsage(
13241324
input_tokens=249,
1325-
output_tokens=541,
1325+
output_tokens=860,
13261326
details={
13271327
'thoughts_tokens': 301,
13281328
'tool_use_prompt_tokens': 319,
@@ -1411,7 +1411,7 @@ async def test_google_model_code_execution_tool(allow_model_requests: None, goog
14111411
],
14121412
usage=RequestUsage(
14131413
input_tokens=15,
1414-
output_tokens=660,
1414+
output_tokens=1335,
14151415
details={
14161416
'thoughts_tokens': 483,
14171417
'tool_use_prompt_tokens': 675,
@@ -1467,7 +1467,7 @@ async def test_google_model_code_execution_tool(allow_model_requests: None, goog
14671467
],
14681468
usage=RequestUsage(
14691469
input_tokens=39,
1470-
output_tokens=598,
1470+
output_tokens=1235,
14711471
details={
14721472
'thoughts_tokens': 540,
14731473
'tool_use_prompt_tokens': 637,
@@ -2719,7 +2719,15 @@ async def get_user_country() -> str:
27192719

27202720

27212721
def test_map_usage():
2722-
assert _metadata_as_usage(GenerateContentResponse()) == RequestUsage()
2722+
assert (
2723+
_metadata_as_usage(
2724+
GenerateContentResponse(),
2725+
# Test the 'google' provider fallback
2726+
provider='',
2727+
provider_url='',
2728+
)
2729+
== RequestUsage()
2730+
)
27232731

27242732
response = GenerateContentResponse(
27252733
usage_metadata=GenerateContentResponseUsageMetadata(
@@ -2732,7 +2740,7 @@ def test_map_usage():
27322740
candidates_tokens_details=[ModalityTokenCount(modality=MediaModality.AUDIO, token_count=9400)],
27332741
)
27342742
)
2735-
assert _metadata_as_usage(response) == snapshot(
2743+
assert _metadata_as_usage(response, provider='', provider_url='') == snapshot(
27362744
RequestUsage(
27372745
input_tokens=1,
27382746
cache_read_tokens=9100,
@@ -3463,6 +3471,7 @@ async def response_iterator() -> AsyncIterator[GenerateContentResponse]:
34633471
_response=response_iterator(),
34643472
_timestamp=datetime.datetime.now(datetime.timezone.utc),
34653473
_provider_name='test-provider',
3474+
_provider_url='',
34663475
)
34673476

34683477
events = [event async for event in streamed_response._get_event_iterator()] # pyright: ignore[reportPrivateUsage]

uv.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)