google-genai 1.27.0__tar.gz → 1.28.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {google_genai-1.27.0/google_genai.egg-info → google_genai-1.28.0}/PKG-INFO +1 -1
  2. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/__init__.py +1 -0
  3. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_api_client.py +143 -39
  4. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_common.py +9 -6
  5. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_extra_utils.py +8 -8
  6. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_mcp_utils.py +4 -1
  7. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_replay_api_client.py +6 -2
  8. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_transformers.py +13 -12
  9. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/errors.py +1 -1
  10. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/live.py +2 -3
  11. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/models.py +81 -12
  12. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/pagers.py +5 -5
  13. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/tokens.py +3 -3
  14. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/tunings.py +33 -6
  15. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/types.py +59 -0
  16. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/version.py +1 -1
  17. {google_genai-1.27.0 → google_genai-1.28.0/google_genai.egg-info}/PKG-INFO +1 -1
  18. {google_genai-1.27.0 → google_genai-1.28.0}/pyproject.toml +1 -1
  19. {google_genai-1.27.0 → google_genai-1.28.0}/LICENSE +0 -0
  20. {google_genai-1.27.0 → google_genai-1.28.0}/MANIFEST.in +0 -0
  21. {google_genai-1.27.0 → google_genai-1.28.0}/README.md +0 -0
  22. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_adapters.py +0 -0
  23. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_api_module.py +0 -0
  24. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_automatic_function_calling_util.py +0 -0
  25. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_base_url.py +0 -0
  26. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_live_converters.py +0 -0
  27. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_test_api_client.py +0 -0
  28. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/_tokens_converters.py +0 -0
  29. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/batches.py +0 -0
  30. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/caches.py +0 -0
  31. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/chats.py +0 -0
  32. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/client.py +0 -0
  33. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/files.py +0 -0
  34. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/live_music.py +0 -0
  35. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/operations.py +0 -0
  36. {google_genai-1.27.0 → google_genai-1.28.0}/google/genai/py.typed +0 -0
  37. {google_genai-1.27.0 → google_genai-1.28.0}/google_genai.egg-info/SOURCES.txt +0 -0
  38. {google_genai-1.27.0 → google_genai-1.28.0}/google_genai.egg-info/dependency_links.txt +0 -0
  39. {google_genai-1.27.0 → google_genai-1.28.0}/google_genai.egg-info/requires.txt +0 -0
  40. {google_genai-1.27.0 → google_genai-1.28.0}/google_genai.egg-info/top_level.txt +0 -0
  41. {google_genai-1.27.0 → google_genai-1.28.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-genai
3
- Version: 1.27.0
3
+ Version: 1.28.0
4
4
  Summary: GenAI Python SDK
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache-2.0
@@ -15,6 +15,7 @@
15
15
 
16
16
  """Google Gen AI SDK"""
17
17
 
18
+ from . import types
18
19
  from . import version
19
20
  from .client import Client
20
21
 
@@ -32,6 +32,7 @@ import logging
32
32
  import math
33
33
  import os
34
34
  import ssl
35
+ import random
35
36
  import sys
36
37
  import threading
37
38
  import time
@@ -81,6 +82,7 @@ if TYPE_CHECKING:
81
82
 
82
83
  logger = logging.getLogger('google_genai._api_client')
83
84
  CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB chunk size
85
+ READ_BUFFER_SIZE = 2**20
84
86
  MAX_RETRY_COUNT = 3
85
87
  INITIAL_RETRY_DELAY = 1 # second
86
88
  DELAY_MULTIPLIER = 2
@@ -363,7 +365,7 @@ _RETRY_HTTP_STATUS_CODES = (
363
365
  )
364
366
 
365
367
 
366
- def _retry_args(options: Optional[HttpRetryOptions]) -> dict[str, Any]:
368
+ def _retry_args(options: Optional[HttpRetryOptions]) -> _common.StringDict:
367
369
  """Returns the retry args for the given http retry options.
368
370
 
369
371
  Args:
@@ -533,17 +535,30 @@ class BaseApiClient:
533
535
  + ' precedence over the API key from the environment variables.'
534
536
  )
535
537
  self.api_key = None
536
- if not self.project and not self.api_key:
538
+
539
+ # Skip fetching project from ADC if base url is provided in http options.
540
+ if (
541
+ not self.project
542
+ and not self.api_key
543
+ and not validated_http_options.base_url
544
+ ):
537
545
  credentials, self.project = _load_auth(project=None)
538
546
  if not self._credentials:
539
547
  self._credentials = credentials
540
- if not ((self.project and self.location) or self.api_key):
548
+
549
+ has_sufficient_auth = (self.project and self.location) or self.api_key
550
+
551
+ if (not has_sufficient_auth and not validated_http_options.base_url):
552
+ # Skip sufficient auth check if base url is provided in http options.
541
553
  raise ValueError(
542
554
  'Project and location or API key must be set when using the Vertex '
543
555
  'AI API.'
544
556
  )
545
557
  if self.api_key or self.location == 'global':
546
558
  self._http_options.base_url = f'https://aiplatform.googleapis.com/'
559
+ elif validated_http_options.base_url and not has_sufficient_auth:
560
+ # Avoid setting default base url and api version if base_url provided.
561
+ self._http_options.base_url = validated_http_options.base_url
547
562
  else:
548
563
  self._http_options.base_url = (
549
564
  f'https://{self.location}-aiplatform.googleapis.com/'
@@ -592,7 +607,7 @@ class BaseApiClient:
592
607
  @staticmethod
593
608
  def _ensure_httpx_ssl_ctx(
594
609
  options: HttpOptions,
595
- ) -> Tuple[dict[str, Any], dict[str, Any]]:
610
+ ) -> Tuple[_common.StringDict, _common.StringDict]:
596
611
  """Ensures the SSL context is present in the HTTPX client args.
597
612
 
598
613
  Creates a default SSL context if one is not provided.
@@ -626,9 +641,9 @@ class BaseApiClient:
626
641
  )
627
642
 
628
643
  def _maybe_set(
629
- args: Optional[dict[str, Any]],
644
+ args: Optional[_common.StringDict],
630
645
  ctx: ssl.SSLContext,
631
- ) -> dict[str, Any]:
646
+ ) -> _common.StringDict:
632
647
  """Sets the SSL context in the client args if not set.
633
648
 
634
649
  Does not override the SSL context if it is already set.
@@ -656,7 +671,7 @@ class BaseApiClient:
656
671
  )
657
672
 
658
673
  @staticmethod
659
- def _ensure_aiohttp_ssl_ctx(options: HttpOptions) -> dict[str, Any]:
674
+ def _ensure_aiohttp_ssl_ctx(options: HttpOptions) -> _common.StringDict:
660
675
  """Ensures the SSL context is present in the async client args.
661
676
 
662
677
  Creates a default SSL context if one is not provided.
@@ -684,9 +699,9 @@ class BaseApiClient:
684
699
  )
685
700
 
686
701
  def _maybe_set(
687
- args: Optional[dict[str, Any]],
702
+ args: Optional[_common.StringDict],
688
703
  ctx: ssl.SSLContext,
689
- ) -> dict[str, Any]:
704
+ ) -> _common.StringDict:
690
705
  """Sets the SSL context in the client args if not set.
691
706
 
692
707
  Does not override the SSL context if it is already set.
@@ -714,7 +729,7 @@ class BaseApiClient:
714
729
  return _maybe_set(async_args, ctx)
715
730
 
716
731
  @staticmethod
717
- def _ensure_websocket_ssl_ctx(options: HttpOptions) -> dict[str, Any]:
732
+ def _ensure_websocket_ssl_ctx(options: HttpOptions) -> _common.StringDict:
718
733
  """Ensures the SSL context is present in the async client args.
719
734
 
720
735
  Creates a default SSL context if one is not provided.
@@ -742,9 +757,9 @@ class BaseApiClient:
742
757
  )
743
758
 
744
759
  def _maybe_set(
745
- args: Optional[dict[str, Any]],
760
+ args: Optional[_common.StringDict],
746
761
  ctx: ssl.SSLContext,
747
- ) -> dict[str, Any]:
762
+ ) -> _common.StringDict:
748
763
  """Sets the SSL context in the client args if not set.
749
764
 
750
765
  Does not override the SSL context if it is already set.
@@ -864,7 +879,7 @@ class BaseApiClient:
864
879
  self.vertexai
865
880
  and not path.startswith('projects/')
866
881
  and not query_vertex_base_models
867
- and not self.api_key
882
+ and (self.project or self.location)
868
883
  ):
869
884
  path = f'projects/{self.project}/locations/{self.location}/' + path
870
885
 
@@ -920,7 +935,8 @@ class BaseApiClient:
920
935
  stream: bool = False,
921
936
  ) -> HttpResponse:
922
937
  data: Optional[Union[str, bytes]] = None
923
- if self.vertexai and not self.api_key:
938
+ # If using proj/location, fetch ADC
939
+ if self.vertexai and (self.project or self.location):
924
940
  http_request.headers['Authorization'] = f'Bearer {self._access_token()}'
925
941
  if self._credentials and self._credentials.quota_project_id:
926
942
  http_request.headers['x-goog-user-project'] = (
@@ -963,8 +979,21 @@ class BaseApiClient:
963
979
  def _request(
964
980
  self,
965
981
  http_request: HttpRequest,
982
+ http_options: Optional[HttpOptionsOrDict] = None,
966
983
  stream: bool = False,
967
984
  ) -> HttpResponse:
985
+ if http_options:
986
+ parameter_model = (
987
+ HttpOptions(**http_options)
988
+ if isinstance(http_options, dict)
989
+ else http_options
990
+ )
991
+ # Support per request retry options.
992
+ if parameter_model.retry_options:
993
+ retry_kwargs = _retry_args(parameter_model.retry_options)
994
+ retry = tenacity.Retrying(**retry_kwargs)
995
+ return retry(self._request_once, http_request, stream) # type: ignore[no-any-return]
996
+
968
997
  return self._retry(self._request_once, http_request, stream) # type: ignore[no-any-return]
969
998
 
970
999
  async def _async_request_once(
@@ -972,7 +1001,8 @@ class BaseApiClient:
972
1001
  ) -> HttpResponse:
973
1002
  data: Optional[Union[str, bytes]] = None
974
1003
 
975
- if self.vertexai and not self.api_key:
1004
+ # If using proj/location, fetch ADC
1005
+ if self.vertexai and (self.project or self.location):
976
1006
  http_request.headers['Authorization'] = (
977
1007
  f'Bearer {await self._async_access_token()}'
978
1008
  )
@@ -993,15 +1023,43 @@ class BaseApiClient:
993
1023
  session = aiohttp.ClientSession(
994
1024
  headers=http_request.headers,
995
1025
  trust_env=True,
1026
+ read_bufsize=READ_BUFFER_SIZE,
996
1027
  )
997
- response = await session.request(
998
- method=http_request.method,
999
- url=http_request.url,
1000
- headers=http_request.headers,
1001
- data=data,
1002
- timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1003
- **self._async_client_session_request_args,
1004
- )
1028
+ try:
1029
+ response = await session.request(
1030
+ method=http_request.method,
1031
+ url=http_request.url,
1032
+ headers=http_request.headers,
1033
+ data=data,
1034
+ timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1035
+ **self._async_client_session_request_args,
1036
+ )
1037
+ except (
1038
+ aiohttp.ClientConnectorError,
1039
+ aiohttp.ClientConnectorDNSError,
1040
+ aiohttp.ClientOSError,
1041
+ aiohttp.ServerDisconnectedError,
1042
+ ) as e:
1043
+ await asyncio.sleep(1 + random.randint(0, 9))
1044
+ logger.info('Retrying due to aiohttp error: %s' % e)
1045
+ # Retrieve the SSL context from the session.
1046
+ self._async_client_session_request_args = (
1047
+ self._ensure_aiohttp_ssl_ctx(self._http_options)
1048
+ )
1049
+ # Instantiate a new session with the updated SSL context.
1050
+ session = aiohttp.ClientSession(
1051
+ headers=http_request.headers,
1052
+ trust_env=True,
1053
+ read_bufsize=READ_BUFFER_SIZE,
1054
+ )
1055
+ response = await session.request(
1056
+ method=http_request.method,
1057
+ url=http_request.url,
1058
+ headers=http_request.headers,
1059
+ data=data,
1060
+ timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1061
+ **self._async_client_session_request_args,
1062
+ )
1005
1063
 
1006
1064
  await errors.APIError.raise_for_async_response(response)
1007
1065
  return HttpResponse(response.headers, response, session=session)
@@ -1022,20 +1080,50 @@ class BaseApiClient:
1022
1080
  return HttpResponse(client_response.headers, client_response)
1023
1081
  else:
1024
1082
  if self._use_aiohttp():
1025
- async with aiohttp.ClientSession(
1026
- headers=http_request.headers,
1027
- trust_env=True,
1028
- ) as session:
1029
- response = await session.request(
1030
- method=http_request.method,
1031
- url=http_request.url,
1083
+ try:
1084
+ async with aiohttp.ClientSession(
1032
1085
  headers=http_request.headers,
1033
- data=data,
1034
- timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1035
- **self._async_client_session_request_args,
1086
+ trust_env=True,
1087
+ read_bufsize=READ_BUFFER_SIZE,
1088
+ ) as session:
1089
+ response = await session.request(
1090
+ method=http_request.method,
1091
+ url=http_request.url,
1092
+ headers=http_request.headers,
1093
+ data=data,
1094
+ timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1095
+ **self._async_client_session_request_args,
1096
+ )
1097
+ await errors.APIError.raise_for_async_response(response)
1098
+ return HttpResponse(response.headers, [await response.text()])
1099
+ except (
1100
+ aiohttp.ClientConnectorError,
1101
+ aiohttp.ClientConnectorDNSError,
1102
+ aiohttp.ClientOSError,
1103
+ aiohttp.ServerDisconnectedError,
1104
+ ) as e:
1105
+ await asyncio.sleep(1 + random.randint(0, 9))
1106
+ logger.info('Retrying due to aiohttp error: %s' % e)
1107
+ # Retrieve the SSL context from the session.
1108
+ self._async_client_session_request_args = (
1109
+ self._ensure_aiohttp_ssl_ctx(self._http_options)
1036
1110
  )
1037
- await errors.APIError.raise_for_async_response(response)
1038
- return HttpResponse(response.headers, [await response.text()])
1111
+ # Instantiate a new session with the updated SSL context.
1112
+ async with aiohttp.ClientSession(
1113
+ headers=http_request.headers,
1114
+ trust_env=True,
1115
+ read_bufsize=READ_BUFFER_SIZE,
1116
+ ) as session:
1117
+ response = await session.request(
1118
+ method=http_request.method,
1119
+ url=http_request.url,
1120
+ headers=http_request.headers,
1121
+ data=data,
1122
+ timeout=aiohttp.ClientTimeout(connect=http_request.timeout),
1123
+ **self._async_client_session_request_args,
1124
+ )
1125
+ await errors.APIError.raise_for_async_response(response)
1126
+ return HttpResponse(response.headers, [await response.text()])
1039
1127
  else:
1040
1128
  # aiohttp is not available. Fall back to httpx.
1041
1129
  client_response = await self._async_httpx_client.request(
@@ -1051,13 +1139,25 @@ class BaseApiClient:
1051
1139
  async def _async_request(
1052
1140
  self,
1053
1141
  http_request: HttpRequest,
1142
+ http_options: Optional[HttpOptionsOrDict] = None,
1054
1143
  stream: bool = False,
1055
1144
  ) -> HttpResponse:
1145
+ if http_options:
1146
+ parameter_model = (
1147
+ HttpOptions(**http_options)
1148
+ if isinstance(http_options, dict)
1149
+ else http_options
1150
+ )
1151
+ # Support per request retry options.
1152
+ if parameter_model.retry_options:
1153
+ retry_kwargs = _retry_args(parameter_model.retry_options)
1154
+ retry = tenacity.AsyncRetrying(**retry_kwargs)
1155
+ return await retry(self._async_request_once, http_request, stream) # type: ignore[no-any-return]
1056
1156
  return await self._async_retry( # type: ignore[no-any-return]
1057
1157
  self._async_request_once, http_request, stream
1058
1158
  )
1059
1159
 
1060
- def get_read_only_http_options(self) -> dict[str, Any]:
1160
+ def get_read_only_http_options(self) -> _common.StringDict:
1061
1161
  if isinstance(self._http_options, BaseModel):
1062
1162
  copied = self._http_options.model_dump()
1063
1163
  else:
@@ -1074,7 +1174,7 @@ class BaseApiClient:
1074
1174
  http_request = self._build_request(
1075
1175
  http_method, path, request_dict, http_options
1076
1176
  )
1077
- response = self._request(http_request, stream=False)
1177
+ response = self._request(http_request, http_options, stream=False)
1078
1178
  response_body = (
1079
1179
  response.response_stream[0] if response.response_stream else ''
1080
1180
  )
@@ -1091,7 +1191,7 @@ class BaseApiClient:
1091
1191
  http_method, path, request_dict, http_options
1092
1192
  )
1093
1193
 
1094
- session_response = self._request(http_request, stream=True)
1194
+ session_response = self._request(http_request, http_options, stream=True)
1095
1195
  for chunk in session_response.segments():
1096
1196
  yield SdkHttpResponse(
1097
1197
  headers=session_response.headers, body=json.dumps(chunk)
@@ -1108,7 +1208,9 @@ class BaseApiClient:
1108
1208
  http_method, path, request_dict, http_options
1109
1209
  )
1110
1210
 
1111
- result = await self._async_request(http_request=http_request, stream=False)
1211
+ result = await self._async_request(
1212
+ http_request=http_request, http_options=http_options, stream=False
1213
+ )
1112
1214
  response_body = result.response_stream[0] if result.response_stream else ''
1113
1215
  return SdkHttpResponse(headers=result.headers, body=response_body)
1114
1216
 
@@ -1340,6 +1442,7 @@ class BaseApiClient:
1340
1442
  async with aiohttp.ClientSession(
1341
1443
  headers=self._http_options.headers,
1342
1444
  trust_env=True,
1445
+ read_bufsize=READ_BUFFER_SIZE,
1343
1446
  ) as session:
1344
1447
  while True:
1345
1448
  if isinstance(file, io.IOBase):
@@ -1523,6 +1626,7 @@ class BaseApiClient:
1523
1626
  async with aiohttp.ClientSession(
1524
1627
  headers=http_request.headers,
1525
1628
  trust_env=True,
1629
+ read_bufsize=READ_BUFFER_SIZE,
1526
1630
  ) as session:
1527
1631
  response = await session.request(
1528
1632
  method=http_request.method,
@@ -22,15 +22,17 @@ import enum
22
22
  import functools
23
23
  import logging
24
24
  import typing
25
- from typing import Any, Callable, Optional, FrozenSet, Union, get_args, get_origin
25
+ from typing import Any, Callable, FrozenSet, Optional, Union, get_args, get_origin
26
26
  import uuid
27
27
  import warnings
28
-
29
28
  import pydantic
30
29
  from pydantic import alias_generators
30
+ from typing_extensions import TypeAlias
31
31
 
32
32
  logger = logging.getLogger('google_genai._common')
33
33
 
34
+ StringDict: TypeAlias = dict[str, Any]
35
+
34
36
 
35
37
  class ExperimentalWarning(Warning):
36
38
  """Warning for experimental features."""
@@ -355,7 +357,6 @@ def _pretty_repr(
355
357
  return raw_repr.replace('\n', f'\n{next_indent_str}')
356
358
 
357
359
 
358
-
359
360
  def _format_collection(
360
361
  obj: Any,
361
362
  *,
@@ -555,7 +556,9 @@ def _normalize_key_for_matching(key_str: str) -> str:
555
556
  return key_str.replace("_", "").lower()
556
557
 
557
558
 
558
- def align_key_case(target_dict: dict[str, Any], update_dict: dict[str, Any]) -> dict[str, Any]:
559
+ def align_key_case(
560
+ target_dict: StringDict, update_dict: StringDict
561
+ ) -> StringDict:
559
562
  """Aligns the keys of update_dict to the case of target_dict keys.
560
563
 
561
564
  Args:
@@ -565,7 +568,7 @@ def align_key_case(target_dict: dict[str, Any], update_dict: dict[str, Any]) ->
565
568
  Returns:
566
569
  A new dictionary with keys aligned to target_dict's key casing.
567
570
  """
568
- aligned_update_dict: dict[str, Any] = {}
571
+ aligned_update_dict: StringDict = {}
569
572
  target_keys_map = {_normalize_key_for_matching(key): key for key in target_dict.keys()}
570
573
 
571
574
  for key, value in update_dict.items():
@@ -587,7 +590,7 @@ def align_key_case(target_dict: dict[str, Any], update_dict: dict[str, Any]) ->
587
590
 
588
591
 
589
592
  def recursive_dict_update(
590
- target_dict: dict[str, Any], update_dict: dict[str, Any]
593
+ target_dict: StringDict, update_dict: StringDict
591
594
  ) -> None:
592
595
  """Recursively updates a target dictionary with values from an update dictionary.
593
596
 
@@ -155,8 +155,8 @@ def get_function_map(
155
155
 
156
156
 
157
157
  def convert_number_values_for_dict_function_call_args(
158
- args: dict[str, Any],
159
- ) -> dict[str, Any]:
158
+ args: _common.StringDict,
159
+ ) -> _common.StringDict:
160
160
  """Converts float values in dict with no decimal to integers."""
161
161
  return {
162
162
  key: convert_number_values_for_function_call_args(value)
@@ -257,8 +257,8 @@ def convert_if_exist_pydantic_model(
257
257
 
258
258
 
259
259
  def convert_argument_from_function(
260
- args: dict[str, Any], function: Callable[..., Any]
261
- ) -> dict[str, Any]:
260
+ args: _common.StringDict, function: Callable[..., Any]
261
+ ) -> _common.StringDict:
262
262
  signature = inspect.signature(function)
263
263
  func_name = function.__name__
264
264
  converted_args = {}
@@ -274,7 +274,7 @@ def convert_argument_from_function(
274
274
 
275
275
 
276
276
  def invoke_function_from_dict_args(
277
- args: Dict[str, Any], function_to_invoke: Callable[..., Any]
277
+ args: _common.StringDict, function_to_invoke: Callable[..., Any]
278
278
  ) -> Any:
279
279
  converted_args = convert_argument_from_function(args, function_to_invoke)
280
280
  try:
@@ -288,7 +288,7 @@ def invoke_function_from_dict_args(
288
288
 
289
289
 
290
290
  async def invoke_function_from_dict_args_async(
291
- args: Dict[str, Any], function_to_invoke: Callable[..., Any]
291
+ args: _common.StringDict, function_to_invoke: Callable[..., Any]
292
292
  ) -> Any:
293
293
  converted_args = convert_argument_from_function(args, function_to_invoke)
294
294
  try:
@@ -321,7 +321,7 @@ def get_function_response_parts(
321
321
  args = convert_number_values_for_dict_function_call_args(
322
322
  part.function_call.args
323
323
  )
324
- func_response: dict[str, Any]
324
+ func_response: _common.StringDict
325
325
  try:
326
326
  if not isinstance(func, McpToGenAiToolAdapter):
327
327
  func_response = {
@@ -356,7 +356,7 @@ async def get_function_response_parts_async(
356
356
  args = convert_number_values_for_dict_function_call_args(
357
357
  part.function_call.args
358
358
  )
359
- func_response: dict[str, Any]
359
+ func_response: _common.StringDict
360
360
  try:
361
361
  if isinstance(func, McpToGenAiToolAdapter):
362
362
  mcp_tool_response = await func.call_tool(
@@ -19,6 +19,7 @@ from importlib.metadata import PackageNotFoundError, version
19
19
  import typing
20
20
  from typing import Any
21
21
 
22
+ from . import _common
22
23
  from . import types
23
24
 
24
25
  if typing.TYPE_CHECKING:
@@ -89,7 +90,9 @@ def set_mcp_usage_header(headers: dict[str, str]) -> None:
89
90
  ).lstrip()
90
91
 
91
92
 
92
- def _filter_to_supported_schema(schema: dict[str, Any]) -> dict[str, Any]:
93
+ def _filter_to_supported_schema(
94
+ schema: _common.StringDict,
95
+ ) -> _common.StringDict:
93
96
  """Filters the schema to only include fields that are supported by JSONSchema."""
94
97
  supported_fields: set[str] = set(types.JSONSchema.model_fields.keys())
95
98
  schema_field_names: tuple[str] = ("items",) # 'additional_properties' to come
@@ -479,13 +479,14 @@ class ReplayApiClient(BaseApiClient):
479
479
  def _request(
480
480
  self,
481
481
  http_request: HttpRequest,
482
+ http_options: Optional[HttpOptionsOrDict] = None,
482
483
  stream: bool = False,
483
484
  ) -> HttpResponse:
484
485
  self._initialize_replay_session_if_not_loaded()
485
486
  if self._should_call_api():
486
487
  _debug_print('api mode request: %s' % http_request)
487
488
  try:
488
- result = super()._request(http_request, stream)
489
+ result = super()._request(http_request, http_options, stream)
489
490
  except errors.APIError as e:
490
491
  self._record_interaction(http_request, e)
491
492
  raise e
@@ -507,13 +508,16 @@ class ReplayApiClient(BaseApiClient):
507
508
  async def _async_request(
508
509
  self,
509
510
  http_request: HttpRequest,
511
+ http_options: Optional[HttpOptionsOrDict] = None,
510
512
  stream: bool = False,
511
513
  ) -> HttpResponse:
512
514
  self._initialize_replay_session_if_not_loaded()
513
515
  if self._should_call_api():
514
516
  _debug_print('api mode request: %s' % http_request)
515
517
  try:
516
- result = await super()._async_request(http_request, stream)
518
+ result = await super()._async_request(
519
+ http_request, http_options, stream
520
+ )
517
521
  except errors.APIError as e:
518
522
  self._record_interaction(http_request, e)
519
523
  raise e
@@ -35,6 +35,7 @@ if typing.TYPE_CHECKING:
35
35
  import pydantic
36
36
 
37
37
  from . import _api_client
38
+ from . import _common
38
39
  from . import types
39
40
 
40
41
  logger = logging.getLogger('google_genai._transformers')
@@ -195,20 +196,20 @@ def t_models_url(
195
196
 
196
197
 
197
198
  def t_extract_models(
198
- response: dict[str, Any],
199
- ) -> list[dict[str, Any]]:
199
+ response: _common.StringDict,
200
+ ) -> list[_common.StringDict]:
200
201
  if not response:
201
202
  return []
202
203
 
203
- models: Optional[list[dict[str, Any]]] = response.get('models')
204
+ models: Optional[list[_common.StringDict]] = response.get('models')
204
205
  if models is not None:
205
206
  return models
206
207
 
207
- tuned_models: Optional[list[dict[str, Any]]] = response.get('tunedModels')
208
+ tuned_models: Optional[list[_common.StringDict]] = response.get('tunedModels')
208
209
  if tuned_models is not None:
209
210
  return tuned_models
210
211
 
211
- publisher_models: Optional[list[dict[str, Any]]] = response.get(
212
+ publisher_models: Optional[list[_common.StringDict]] = response.get(
212
213
  'publisherModels'
213
214
  )
214
215
  if publisher_models is not None:
@@ -560,7 +561,7 @@ def t_contents(
560
561
  return result
561
562
 
562
563
 
563
- def handle_null_fields(schema: dict[str, Any]) -> None:
564
+ def handle_null_fields(schema: _common.StringDict) -> None:
564
565
  """Process null fields in the schema so it is compatible with OpenAPI.
565
566
 
566
567
  The OpenAPI spec does not support 'type: 'null' in the schema. This function
@@ -639,9 +640,9 @@ def _raise_for_unsupported_mldev_properties(
639
640
 
640
641
 
641
642
  def process_schema(
642
- schema: dict[str, Any],
643
+ schema: _common.StringDict,
643
644
  client: Optional[_api_client.BaseApiClient],
644
- defs: Optional[dict[str, Any]] = None,
645
+ defs: Optional[_common.StringDict] = None,
645
646
  *,
646
647
  order_properties: bool = True,
647
648
  ) -> None:
@@ -740,7 +741,7 @@ def process_schema(
740
741
  if (ref := schema.pop('$ref', None)) is not None:
741
742
  schema.update(defs[ref.split('defs/')[-1]])
742
743
 
743
- def _recurse(sub_schema: dict[str, Any]) -> dict[str, Any]:
744
+ def _recurse(sub_schema: _common.StringDict) -> _common.StringDict:
744
745
  """Returns the processed `sub_schema`, resolving its '$ref' if any."""
745
746
  if (ref := sub_schema.pop('$ref', None)) is not None:
746
747
  sub_schema = defs[ref.split('defs/')[-1]]
@@ -820,7 +821,7 @@ def _process_enum(
820
821
 
821
822
  def _is_type_dict_str_any(
822
823
  origin: Union[types.SchemaUnionDict, Any],
823
- ) -> TypeGuard[dict[str, Any]]:
824
+ ) -> TypeGuard[_common.StringDict]:
824
825
  """Verifies the schema is of type dict[str, Any] for mypy type checking."""
825
826
  return isinstance(origin, dict) and all(
826
827
  isinstance(key, str) for key in origin
@@ -1075,10 +1076,10 @@ LRO_POLLING_MULTIPLIER = 1.5
1075
1076
 
1076
1077
 
1077
1078
  def t_resolve_operation(
1078
- api_client: _api_client.BaseApiClient, struct: dict[str, Any]
1079
+ api_client: _api_client.BaseApiClient, struct: _common.StringDict
1079
1080
  ) -> Any:
1080
1081
  if (name := struct.get('name')) and '/operations/' in name:
1081
- operation: dict[str, Any] = struct
1082
+ operation: _common.StringDict = struct
1082
1083
  total_seconds = 0.0
1083
1084
  delay_seconds = LRO_POLLING_INITIAL_DELAY_SECONDS
1084
1085
  while operation.get('done') != True:
@@ -65,7 +65,7 @@ class APIError(Exception):
65
65
  'code', response_json.get('error', {}).get('code', None)
66
66
  )
67
67
 
68
- def _to_replay_record(self) -> dict[str, Any]:
68
+ def _to_replay_record(self) -> _common.StringDict:
69
69
  """Returns a dictionary representation of the error for replay recording.
70
70
 
71
71
  details is not included since it may expose internal information in the
@@ -33,7 +33,6 @@ from . import _common
33
33
  from . import _live_converters as live_converters
34
34
  from . import _mcp_utils
35
35
  from . import _transformers as t
36
- from . import client
37
36
  from . import errors
38
37
  from . import types
39
38
  from ._api_client import BaseApiClient
@@ -288,7 +287,7 @@ class AsyncSession:
288
287
  print(f'{msg.text}')
289
288
  ```
290
289
  """
291
- kwargs: dict[str, Any] = {}
290
+ kwargs: _common.StringDict = {}
292
291
  if media is not None:
293
292
  kwargs['media'] = media
294
293
  if audio is not None:
@@ -639,7 +638,7 @@ class AsyncSession:
639
638
  elif isinstance(formatted_input, Sequence) and any(
640
639
  isinstance(c, str) for c in formatted_input
641
640
  ):
642
- to_object: dict[str, Any] = {}
641
+ to_object: _common.StringDict = {}
643
642
  content_input_parts: list[types.PartUnion] = []
644
643
  for item in formatted_input:
645
644
  if isinstance(item, get_args(types.PartUnion)):