google-genai 1.43.0__tar.gz → 1.45.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 (45) hide show
  1. {google_genai-1.43.0/google_genai.egg-info → google_genai-1.45.0}/PKG-INFO +21 -1
  2. {google_genai-1.43.0 → google_genai-1.45.0}/README.md +19 -0
  3. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_api_client.py +19 -4
  4. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_common.py +1 -3
  5. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_operations_converters.py +6 -6
  6. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_replay_api_client.py +8 -3
  7. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_transformers.py +63 -63
  8. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/live.py +17 -4
  9. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/models.py +38 -8
  10. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/types.py +131 -16
  11. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/version.py +1 -1
  12. {google_genai-1.43.0 → google_genai-1.45.0/google_genai.egg-info}/PKG-INFO +21 -1
  13. {google_genai-1.43.0 → google_genai-1.45.0}/pyproject.toml +3 -2
  14. {google_genai-1.43.0 → google_genai-1.45.0}/LICENSE +0 -0
  15. {google_genai-1.43.0 → google_genai-1.45.0}/MANIFEST.in +0 -0
  16. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/__init__.py +0 -0
  17. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_adapters.py +0 -0
  18. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_api_module.py +0 -0
  19. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_automatic_function_calling_util.py +0 -0
  20. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_base_transformers.py +0 -0
  21. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_base_url.py +0 -0
  22. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_extra_utils.py +0 -0
  23. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_live_converters.py +0 -0
  24. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_local_tokenizer_loader.py +0 -0
  25. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_mcp_utils.py +0 -0
  26. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_test_api_client.py +0 -0
  27. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/_tokens_converters.py +0 -0
  28. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/batches.py +0 -0
  29. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/caches.py +0 -0
  30. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/chats.py +0 -0
  31. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/client.py +0 -0
  32. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/errors.py +0 -0
  33. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/files.py +0 -0
  34. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/live_music.py +0 -0
  35. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/local_tokenizer.py +0 -0
  36. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/operations.py +0 -0
  37. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/pagers.py +0 -0
  38. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/py.typed +0 -0
  39. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/tokens.py +0 -0
  40. {google_genai-1.43.0 → google_genai-1.45.0}/google/genai/tunings.py +0 -0
  41. {google_genai-1.43.0 → google_genai-1.45.0}/google_genai.egg-info/SOURCES.txt +0 -0
  42. {google_genai-1.43.0 → google_genai-1.45.0}/google_genai.egg-info/dependency_links.txt +0 -0
  43. {google_genai-1.43.0 → google_genai-1.45.0}/google_genai.egg-info/requires.txt +0 -0
  44. {google_genai-1.43.0 → google_genai-1.45.0}/google_genai.egg-info/top_level.txt +0 -0
  45. {google_genai-1.43.0 → google_genai-1.45.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-genai
3
- Version: 1.43.0
3
+ Version: 1.45.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 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Topic :: Internet
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
21
  Requires-Python: >=3.9
@@ -279,6 +280,25 @@ http_options = types.HttpOptions(
279
280
  client=Client(..., http_options=http_options)
280
281
  ```
281
282
 
283
+ ### Custom base url
284
+
285
+ In some cases you might need a custom base url (for example, API gateway proxy
286
+ server) and bypass some authentication checks for project, location, or API key.
287
+ You may pass the custom base url like this:
288
+
289
+ ```python
290
+
291
+ base_url = 'https://test-api-gateway-proxy.com'
292
+ client = Client(
293
+ vertexai=True,
294
+ http_options={
295
+ 'base_url': base_url,
296
+ 'headers': {'Authorization': 'Bearer test_token'},
297
+ },
298
+ )
299
+
300
+ ```
301
+
282
302
  ## Types
283
303
 
284
304
  Parameter types can be specified as either dictionaries(`TypedDict`) or
@@ -242,6 +242,25 @@ http_options = types.HttpOptions(
242
242
  client=Client(..., http_options=http_options)
243
243
  ```
244
244
 
245
+ ### Custom base url
246
+
247
+ In some cases you might need a custom base url (for example, API gateway proxy
248
+ server) and bypass some authentication checks for project, location, or API key.
249
+ You may pass the custom base url like this:
250
+
251
+ ```python
252
+
253
+ base_url = 'https://test-api-gateway-proxy.com'
254
+ client = Client(
255
+ vertexai=True,
256
+ http_options={
257
+ 'base_url': base_url,
258
+ 'headers': {'Authorization': 'Bearer test_token'},
259
+ },
260
+ )
261
+
262
+ ```
263
+
245
264
  ## Types
246
265
 
247
266
  Parameter types can be specified as either dictionaries(`TypedDict`) or
@@ -547,6 +547,7 @@ class BaseApiClient:
547
547
  http_options: Optional[HttpOptionsOrDict] = None,
548
548
  ):
549
549
  self.vertexai = vertexai
550
+ self.custom_base_url = None
550
551
  if self.vertexai is None:
551
552
  if os.environ.get('GOOGLE_GENAI_USE_VERTEXAI', '0').lower() in [
552
553
  'true',
@@ -628,11 +629,17 @@ class BaseApiClient:
628
629
  )
629
630
  self.api_key = None
630
631
 
632
+ self.custom_base_url = (
633
+ validated_http_options.base_url
634
+ if validated_http_options.base_url
635
+ else None
636
+ )
637
+
631
638
  # Skip fetching project from ADC if base url is provided in http options.
632
639
  if (
633
640
  not self.project
634
641
  and not self.api_key
635
- and not validated_http_options.base_url
642
+ and not self.custom_base_url
636
643
  ):
637
644
  credentials, self.project = load_auth(project=None)
638
645
  if not self._credentials:
@@ -640,7 +647,7 @@ class BaseApiClient:
640
647
 
641
648
  has_sufficient_auth = (self.project and self.location) or self.api_key
642
649
 
643
- if not has_sufficient_auth and not validated_http_options.base_url:
650
+ if not has_sufficient_auth and not self.custom_base_url:
644
651
  # Skip sufficient auth check if base url is provided in http options.
645
652
  raise ValueError(
646
653
  'Project and location or API key must be set when using the Vertex '
@@ -648,9 +655,11 @@ class BaseApiClient:
648
655
  )
649
656
  if self.api_key or self.location == 'global':
650
657
  self._http_options.base_url = f'https://aiplatform.googleapis.com/'
651
- elif validated_http_options.base_url and not has_sufficient_auth:
658
+ elif self.custom_base_url and not has_sufficient_auth:
652
659
  # Avoid setting default base url and api version if base_url provided.
653
- self._http_options.base_url = validated_http_options.base_url
660
+ # API gateway proxy can use the auth in custom headers, not url.
661
+ # Enable custom url if auth is not sufficient.
662
+ self._http_options.base_url = self.custom_base_url
654
663
  else:
655
664
  self._http_options.base_url = (
656
665
  f'https://{self.location}-aiplatform.googleapis.com/'
@@ -683,6 +692,7 @@ class BaseApiClient:
683
692
  client_args, async_client_args = self._ensure_httpx_ssl_ctx(
684
693
  self._http_options
685
694
  )
695
+ self._async_httpx_client_args = async_client_args
686
696
  self._httpx_client = SyncHttpxClient(**client_args)
687
697
  self._async_httpx_client = AsyncHttpxClient(**async_client_args)
688
698
  if self._use_aiohttp():
@@ -897,6 +907,11 @@ class BaseApiClient:
897
907
  )
898
908
 
899
909
  def _websocket_base_url(self) -> str:
910
+ has_sufficient_auth = (self.project and self.location) or self.api_key
911
+ if self.custom_base_url and not has_sufficient_auth:
912
+ # API gateway proxy can use the auth in custom headers, not url.
913
+ # Enable custom url if auth is not sufficient.
914
+ return self.custom_base_url
900
915
  url_parts = urlparse(self._http_options.base_url)
901
916
  return url_parts._replace(scheme='wss').geturl() # type: ignore[arg-type, return-value]
902
917
 
@@ -272,9 +272,7 @@ def convert_to_dict(obj: object, convert_keys: bool = False) -> Any:
272
272
  return convert_to_dict(obj.model_dump(exclude_none=True), convert_keys)
273
273
  elif isinstance(obj, dict):
274
274
  return {
275
- maybe_snake_to_camel(key, convert_keys): convert_to_dict(
276
- value, convert_keys
277
- )
275
+ maybe_snake_to_camel(key, convert_keys): convert_to_dict(value)
278
276
  for key, value in obj.items()
279
277
  }
280
278
  elif isinstance(obj, list):
@@ -201,11 +201,11 @@ def _GeneratedVideo_from_mldev(
201
201
  parent_object: Optional[dict[str, Any]] = None,
202
202
  ) -> dict[str, Any]:
203
203
  to_object: dict[str, Any] = {}
204
- if getv(from_object, ['_self']) is not None:
204
+ if getv(from_object, ['video']) is not None:
205
205
  setv(
206
206
  to_object,
207
207
  ['video'],
208
- _Video_from_mldev(getv(from_object, ['_self']), to_object),
208
+ _Video_from_mldev(getv(from_object, ['video']), to_object),
209
209
  )
210
210
 
211
211
  return to_object
@@ -261,14 +261,14 @@ def _Video_from_mldev(
261
261
  parent_object: Optional[dict[str, Any]] = None,
262
262
  ) -> dict[str, Any]:
263
263
  to_object: dict[str, Any] = {}
264
- if getv(from_object, ['video', 'uri']) is not None:
265
- setv(to_object, ['uri'], getv(from_object, ['video', 'uri']))
264
+ if getv(from_object, ['uri']) is not None:
265
+ setv(to_object, ['uri'], getv(from_object, ['uri']))
266
266
 
267
- if getv(from_object, ['video', 'encodedVideo']) is not None:
267
+ if getv(from_object, ['encodedVideo']) is not None:
268
268
  setv(
269
269
  to_object,
270
270
  ['video_bytes'],
271
- base_t.t_bytes(getv(from_object, ['video', 'encodedVideo'])),
271
+ base_t.t_bytes(getv(from_object, ['encodedVideo'])),
272
272
  )
273
273
 
274
274
  if getv(from_object, ['encoding']) is not None:
@@ -56,8 +56,13 @@ def _normalize_json_case(obj: Any) -> Any:
56
56
  return [_normalize_json_case(item) for item in obj]
57
57
  elif isinstance(obj, enum.Enum):
58
58
  return obj.value
59
- else:
60
- return obj
59
+ elif isinstance(obj, str):
60
+ # Python >= 3.14 has a new division by zero error message.
61
+ if 'division by zero' in obj:
62
+ return obj.replace(
63
+ 'division by zero', 'integer division or modulo by zero'
64
+ )
65
+ return obj
61
66
 
62
67
 
63
68
  def _equals_ignore_key_case(obj1: Any, obj2: Any) -> bool:
@@ -88,7 +93,7 @@ def _equals_ignore_key_case(obj1: Any, obj2: Any) -> bool:
88
93
 
89
94
  def _redact_version_numbers(version_string: str) -> str:
90
95
  """Redacts version numbers in the form x.y.z from a string."""
91
- return re.sub(r'\d+\.\d+\.\d+', '{VERSION_NUMBER}', version_string)
96
+ return re.sub(r'\d+\.\d+\.\d+[a-zA-Z0-9]*', '{VERSION_NUMBER}', version_string)
92
97
 
93
98
 
94
99
  def _redact_language_label(language_label: str) -> str:
@@ -41,28 +41,6 @@ from . import types
41
41
 
42
42
  logger = logging.getLogger('google_genai._transformers')
43
43
 
44
-
45
- def _is_duck_type_of(obj: Any, cls: type[pydantic.BaseModel]) -> bool:
46
- """Checks if an object has all of the fields of a Pydantic model.
47
-
48
- This is a duck-typing alternative to `isinstance` to solve dual-import
49
- problems. It returns False for dictionaries, which should be handled by
50
- `isinstance(obj, dict)`.
51
-
52
- Args:
53
- obj: The object to check.
54
- cls: The Pydantic model class to duck-type against.
55
-
56
- Returns:
57
- True if the object has all the fields defined in the Pydantic model, False
58
- otherwise.
59
- """
60
- if isinstance(obj, dict) or not hasattr(cls, 'model_fields'):
61
- return False
62
-
63
- # Check if the object has all of the Pydantic model's defined fields.
64
- return all(hasattr(obj, field) for field in cls.model_fields)
65
-
66
44
  if sys.version_info >= (3, 10):
67
45
  VersionedUnionType = builtin_types.UnionType
68
46
  _UNION_TYPES = (typing.Union, builtin_types.UnionType)
@@ -94,6 +72,28 @@ metric_name_sdk_api_map = {
94
72
  metric_name_api_sdk_map = {v: k for k, v in metric_name_sdk_api_map.items()}
95
73
 
96
74
 
75
+ def _is_duck_type_of(obj: Any, cls: type[pydantic.BaseModel]) -> bool:
76
+ """Checks if an object has all of the fields of a Pydantic model.
77
+
78
+ This is a duck-typing alternative to `isinstance` to solve dual-import
79
+ problems. It returns False for dictionaries, which should be handled by
80
+ `isinstance(obj, dict)`.
81
+
82
+ Args:
83
+ obj: The object to check.
84
+ cls: The Pydantic model class to duck-type against.
85
+
86
+ Returns:
87
+ True if the object has all the fields defined in the Pydantic model, False
88
+ otherwise.
89
+ """
90
+ if isinstance(obj, dict) or not hasattr(cls, 'model_fields'):
91
+ return False
92
+
93
+ # Check if the object has all of the Pydantic model's defined fields.
94
+ return all(hasattr(obj, field) for field in cls.model_fields)
95
+
96
+
97
97
  def _resource_name(
98
98
  client: _api_client.BaseApiClient,
99
99
  resource_name: str,
@@ -306,7 +306,7 @@ def t_function_response(
306
306
  raise ValueError('function_response is required.')
307
307
  if isinstance(function_response, dict):
308
308
  return types.FunctionResponse.model_validate(function_response)
309
- elif isinstance(function_response, types.FunctionResponse):
309
+ elif _is_duck_type_of(function_response, types.FunctionResponse):
310
310
  return function_response
311
311
  else:
312
312
  raise TypeError(
@@ -342,8 +342,8 @@ def t_blob(blob: types.BlobImageUnionDict) -> types.Blob:
342
342
  if not blob:
343
343
  raise ValueError('blob is required.')
344
344
 
345
- if isinstance(blob, types.Blob):
346
- return blob
345
+ if _is_duck_type_of(blob, types.Blob):
346
+ return blob # type: ignore[return-value]
347
347
 
348
348
  if isinstance(blob, dict):
349
349
  return types.Blob.model_validate(blob)
@@ -383,10 +383,10 @@ def t_part(part: Optional[types.PartUnionDict]) -> types.Part:
383
383
  raise ValueError('content part is required.')
384
384
  if isinstance(part, str):
385
385
  return types.Part(text=part)
386
- if isinstance(part, types.File):
387
- if not part.uri or not part.mime_type:
386
+ if _is_duck_type_of(part, types.File):
387
+ if not part.uri or not part.mime_type: # type: ignore[union-attr]
388
388
  raise ValueError('file uri and mime_type are required.')
389
- return types.Part.from_uri(file_uri=part.uri, mime_type=part.mime_type)
389
+ return types.Part.from_uri(file_uri=part.uri, mime_type=part.mime_type) # type: ignore[union-attr]
390
390
  if isinstance(part, dict):
391
391
  try:
392
392
  return types.Part.model_validate(part)
@@ -449,8 +449,8 @@ def t_content(
449
449
  ) -> types.Content:
450
450
  if content is None:
451
451
  raise ValueError('content is required.')
452
- if isinstance(content, types.Content):
453
- return content
452
+ if _is_duck_type_of(content, types.Content):
453
+ return content # type: ignore[return-value]
454
454
  if isinstance(content, dict):
455
455
  try:
456
456
  return types.Content.model_validate(content)
@@ -461,15 +461,15 @@ def t_content(
461
461
  if possible_part.function_call
462
462
  else types.UserContent(parts=[possible_part])
463
463
  )
464
- if isinstance(content, types.File):
465
- return types.UserContent(parts=[t_part(content)])
466
- if isinstance(content, types.Part):
464
+ if _is_duck_type_of(content, types.File):
465
+ return types.UserContent(parts=[t_part(content)]) # type: ignore[arg-type]
466
+ if _is_duck_type_of(content, types.Part):
467
467
  return (
468
- types.ModelContent(parts=[content])
469
- if content.function_call
470
- else types.UserContent(parts=[content])
468
+ types.ModelContent(parts=[content]) # type: ignore[arg-type]
469
+ if content.function_call # type: ignore[union-attr]
470
+ else types.UserContent(parts=[content]) # type: ignore[arg-type]
471
471
  )
472
- return types.UserContent(parts=content)
472
+ return types.UserContent(parts=content) # type: ignore[arg-type]
473
473
 
474
474
 
475
475
  def t_contents_for_embed(
@@ -516,8 +516,8 @@ def t_contents(
516
516
  ) -> TypeGuard[types.PartUnionDict]:
517
517
  if (
518
518
  isinstance(part, str)
519
- or isinstance(part, types.File)
520
- or isinstance(part, types.Part)
519
+ or _is_duck_type_of(part, types.File)
520
+ or _is_duck_type_of(part, types.Part)
521
521
  ):
522
522
  return True
523
523
 
@@ -884,12 +884,12 @@ def t_schema(
884
884
  return types.Schema.model_validate(origin)
885
885
  if isinstance(origin, EnumMeta):
886
886
  return _process_enum(origin, client)
887
- if isinstance(origin, types.Schema):
888
- if dict(origin) == dict(types.Schema()):
887
+ if _is_duck_type_of(origin, types.Schema):
888
+ if dict(origin) == dict(types.Schema()): # type: ignore [arg-type]
889
889
  # response_schema value was coerced to an empty Schema instance because
890
890
  # it did not adhere to the Schema field annotation
891
891
  _raise_for_unsupported_schema_type(origin)
892
- schema = origin.model_dump(exclude_unset=True)
892
+ schema = origin.model_dump(exclude_unset=True) # type: ignore[union-attr]
893
893
  process_schema(schema, client)
894
894
  return types.Schema.model_validate(schema)
895
895
 
@@ -926,8 +926,8 @@ def t_speech_config(
926
926
  ) -> Optional[types.SpeechConfig]:
927
927
  if not origin:
928
928
  return None
929
- if isinstance(origin, types.SpeechConfig):
930
- return origin
929
+ if _is_duck_type_of(origin, types.SpeechConfig):
930
+ return origin # type: ignore[return-value]
931
931
  if isinstance(origin, str):
932
932
  return types.SpeechConfig(
933
933
  voice_config=types.VoiceConfig(
@@ -943,17 +943,17 @@ def t_speech_config(
943
943
  def t_live_speech_config(
944
944
  origin: types.SpeechConfigOrDict,
945
945
  ) -> Optional[types.SpeechConfig]:
946
- if isinstance(origin, types.SpeechConfig):
946
+ if _is_duck_type_of(origin, types.SpeechConfig):
947
947
  speech_config = origin
948
948
  if isinstance(origin, dict):
949
949
  speech_config = types.SpeechConfig.model_validate(origin)
950
950
 
951
- if speech_config.multi_speaker_voice_config is not None:
951
+ if speech_config.multi_speaker_voice_config is not None: # type: ignore[union-attr]
952
952
  raise ValueError(
953
953
  'multi_speaker_voice_config is not supported in the live API.'
954
954
  )
955
955
 
956
- return speech_config
956
+ return speech_config # type: ignore[return-value]
957
957
 
958
958
 
959
959
  def t_tool(
@@ -969,7 +969,7 @@ def t_tool(
969
969
  )
970
970
  ]
971
971
  )
972
- elif McpTool is not None and isinstance(origin, McpTool):
972
+ elif McpTool is not None and _is_duck_type_of(origin, McpTool):
973
973
  return mcp_to_gemini_tool(origin)
974
974
  elif isinstance(origin, dict):
975
975
  return types.Tool.model_validate(origin)
@@ -1012,13 +1012,13 @@ def t_batch_job_source(
1012
1012
  ) -> types.BatchJobSource:
1013
1013
  if isinstance(src, dict):
1014
1014
  src = types.BatchJobSource(**src)
1015
- if isinstance(src, types.BatchJobSource):
1015
+ if _is_duck_type_of(src, types.BatchJobSource):
1016
1016
  vertex_sources = sum(
1017
- [src.gcs_uri is not None, src.bigquery_uri is not None]
1017
+ [src.gcs_uri is not None, src.bigquery_uri is not None] # type: ignore[union-attr]
1018
1018
  )
1019
1019
  mldev_sources = sum([
1020
- src.inlined_requests is not None,
1021
- src.file_name is not None,
1020
+ src.inlined_requests is not None, # type: ignore[union-attr]
1021
+ src.file_name is not None, # type: ignore[union-attr]
1022
1022
  ])
1023
1023
  if client.vertexai:
1024
1024
  if mldev_sources or vertex_sources != 1:
@@ -1033,7 +1033,7 @@ def t_batch_job_source(
1033
1033
  '`inlined_embed_content_requests`, or `embed_content_file_name` '
1034
1034
  'must be set, other sources are not supported in Gemini API.'
1035
1035
  )
1036
- return src
1036
+ return src # type: ignore[return-value]
1037
1037
 
1038
1038
  elif isinstance(src, list):
1039
1039
  return types.BatchJobSource(inlined_requests=src)
@@ -1063,7 +1063,7 @@ def t_embedding_batch_job_source(
1063
1063
  if isinstance(src, dict):
1064
1064
  src = types.EmbeddingsBatchJobSource(**src)
1065
1065
 
1066
- if isinstance(src, types.EmbeddingsBatchJobSource):
1066
+ if _is_duck_type_of(src, types.EmbeddingsBatchJobSource):
1067
1067
  mldev_sources = sum([
1068
1068
  src.inlined_requests is not None,
1069
1069
  src.file_name is not None,
@@ -1098,7 +1098,7 @@ def t_batch_job_destination(
1098
1098
  )
1099
1099
  else:
1100
1100
  raise ValueError(f'Unsupported destination: {dest}')
1101
- elif isinstance(dest, types.BatchJobDestination):
1101
+ elif _is_duck_type_of(dest, types.BatchJobDestination):
1102
1102
  return dest
1103
1103
  else:
1104
1104
  raise ValueError(f'Unsupported destination: {dest}')
@@ -1198,13 +1198,13 @@ def t_file_name(
1198
1198
  name: Optional[Union[str, types.File, types.Video, types.GeneratedVideo]],
1199
1199
  ) -> str:
1200
1200
  # Remove the files/ prefix since it's added to the url path.
1201
- if isinstance(name, types.File):
1202
- name = name.name
1203
- elif isinstance(name, types.Video):
1204
- name = name.uri
1205
- elif isinstance(name, types.GeneratedVideo):
1206
- if name.video is not None:
1207
- name = name.video.uri
1201
+ if _is_duck_type_of(name, types.File):
1202
+ name = name.name # type: ignore[union-attr]
1203
+ elif _is_duck_type_of(name, types.Video):
1204
+ name = name.uri # type: ignore[union-attr]
1205
+ elif _is_duck_type_of(name, types.GeneratedVideo):
1206
+ if name.video is not None: # type: ignore[union-attr]
1207
+ name = name.video.uri # type: ignore[union-attr]
1208
1208
  else:
1209
1209
  name = None
1210
1210
 
@@ -1247,7 +1247,7 @@ def t_tuning_job_status(status: str) -> Union[types.JobState, str]:
1247
1247
  def t_content_strict(content: types.ContentOrDict) -> types.Content:
1248
1248
  if isinstance(content, dict):
1249
1249
  return types.Content.model_validate(content)
1250
- elif isinstance(content, types.Content):
1250
+ elif _is_duck_type_of(content, types.Content):
1251
1251
  return content
1252
1252
  else:
1253
1253
  raise ValueError(
@@ -980,7 +980,8 @@ class AsyncLive(_api_module.BaseModule):
980
980
  api_key = self._api_client.api_key
981
981
  version = self._api_client._http_options.api_version
982
982
  uri = f'{base_url}/ws/google.cloud.aiplatform.{version}.LlmBidiService/BidiGenerateContent'
983
- headers = self._api_client._http_options.headers or {}
983
+ original_headers = self._api_client._http_options.headers
984
+ headers = original_headers.copy() if original_headers is not None else {}
984
985
 
985
986
  request_dict = _common.convert_to_dict(
986
987
  live_converters._LiveConnectParameters_to_vertex(
@@ -1012,12 +1013,24 @@ class AsyncLive(_api_module.BaseModule):
1012
1013
  bearer_token = creds.token
1013
1014
  original_headers = self._api_client._http_options.headers
1014
1015
  headers = original_headers.copy() if original_headers is not None else {}
1015
- headers['Authorization'] = f'Bearer {bearer_token}'
1016
+ if not headers.get('Authorization'):
1017
+ headers['Authorization'] = f'Bearer {bearer_token}'
1016
1018
  version = self._api_client._http_options.api_version
1017
- uri = f'{base_url}/ws/google.cloud.aiplatform.{version}.LlmBidiService/BidiGenerateContent'
1019
+
1020
+ has_sufficient_auth = (
1021
+ self._api_client.project and self._api_client.location
1022
+ )
1023
+ if self._api_client.custom_base_url and not has_sufficient_auth:
1024
+ # API gateway proxy can use the auth in custom headers, not url.
1025
+ # Enable custom url if auth is not sufficient.
1026
+ uri = self._api_client.custom_base_url
1027
+ # Keep the model as is.
1028
+ transformed_model = model
1029
+ else:
1030
+ uri = f'{base_url}/ws/google.cloud.aiplatform.{version}.LlmBidiService/BidiGenerateContent'
1018
1031
  location = self._api_client.location
1019
1032
  project = self._api_client.project
1020
- if transformed_model.startswith('publishers/'):
1033
+ if transformed_model.startswith('publishers/') and project and location:
1021
1034
  transformed_model = (
1022
1035
  f'projects/{project}/locations/{location}/' + transformed_model
1023
1036
  )
@@ -2262,11 +2262,11 @@ def _GeneratedVideo_from_mldev(
2262
2262
  parent_object: Optional[dict[str, Any]] = None,
2263
2263
  ) -> dict[str, Any]:
2264
2264
  to_object: dict[str, Any] = {}
2265
- if getv(from_object, ['_self']) is not None:
2265
+ if getv(from_object, ['video']) is not None:
2266
2266
  setv(
2267
2267
  to_object,
2268
2268
  ['video'],
2269
- _Video_from_mldev(getv(from_object, ['_self']), to_object),
2269
+ _Video_from_mldev(getv(from_object, ['video']), to_object),
2270
2270
  )
2271
2271
 
2272
2272
  return to_object
@@ -3680,14 +3680,14 @@ def _Video_from_mldev(
3680
3680
  parent_object: Optional[dict[str, Any]] = None,
3681
3681
  ) -> dict[str, Any]:
3682
3682
  to_object: dict[str, Any] = {}
3683
- if getv(from_object, ['video', 'uri']) is not None:
3684
- setv(to_object, ['uri'], getv(from_object, ['video', 'uri']))
3683
+ if getv(from_object, ['uri']) is not None:
3684
+ setv(to_object, ['uri'], getv(from_object, ['uri']))
3685
3685
 
3686
- if getv(from_object, ['video', 'encodedVideo']) is not None:
3686
+ if getv(from_object, ['encodedVideo']) is not None:
3687
3687
  setv(
3688
3688
  to_object,
3689
3689
  ['video_bytes'],
3690
- base_t.t_bytes(getv(from_object, ['video', 'encodedVideo'])),
3690
+ base_t.t_bytes(getv(from_object, ['encodedVideo'])),
3691
3691
  )
3692
3692
 
3693
3693
  if getv(from_object, ['encoding']) is not None:
@@ -3723,12 +3723,12 @@ def _Video_to_mldev(
3723
3723
  ) -> dict[str, Any]:
3724
3724
  to_object: dict[str, Any] = {}
3725
3725
  if getv(from_object, ['uri']) is not None:
3726
- setv(to_object, ['video', 'uri'], getv(from_object, ['uri']))
3726
+ setv(to_object, ['uri'], getv(from_object, ['uri']))
3727
3727
 
3728
3728
  if getv(from_object, ['video_bytes']) is not None:
3729
3729
  setv(
3730
3730
  to_object,
3731
- ['video', 'encodedVideo'],
3731
+ ['encodedVideo'],
3732
3732
  base_t.t_bytes(getv(from_object, ['video_bytes'])),
3733
3733
  )
3734
3734
 
@@ -5448,6 +5448,36 @@ class Models(_api_module.BaseModule):
5448
5448
  'Source and prompt/image/video are mutually exclusive.'
5449
5449
  + ' Please only use source.'
5450
5450
  )
5451
+ # Gemini Developer API does not support video bytes.
5452
+ video_dct: dict[str, Any] = {}
5453
+ if not self._api_client.vertexai and video:
5454
+ if isinstance(video, types.Video):
5455
+ video_dct = video.model_dump()
5456
+ else:
5457
+ video_dct = dict(video)
5458
+
5459
+ if video_dct.get('uri') and video_dct.get('video_bytes'):
5460
+ video = types.Video(
5461
+ uri=video_dct.get('uri'), mime_type=video_dct.get('mime_type')
5462
+ )
5463
+ elif not self._api_client.vertexai and source:
5464
+ if isinstance(source, types.GenerateVideosSource):
5465
+ source_dct = source.model_dump()
5466
+ video_dct = source_dct.get('video', {})
5467
+ else:
5468
+ source_dct = dict(source)
5469
+ if isinstance(source_dct.get('video'), types.Video):
5470
+ video_obj: types.Video = source_dct.get('video', types.Video())
5471
+ video_dct = video_obj.model_dump()
5472
+ if video_dct and video_dct.get('uri') and video_dct.get('video_bytes'):
5473
+ source = types.GenerateVideosSource(
5474
+ prompt=source_dct.get('prompt'),
5475
+ image=source_dct.get('image'),
5476
+ video=types.Video(
5477
+ uri=video_dct.get('uri'),
5478
+ mime_type=video_dct.get('mime_type'),
5479
+ ),
5480
+ )
5451
5481
  return self._generate_videos(
5452
5482
  model=model,
5453
5483
  prompt=prompt,
@@ -1696,6 +1696,10 @@ class JSONSchema(_common.BaseModel):
1696
1696
  ' matches the instance successfully.'
1697
1697
  ),
1698
1698
  )
1699
+ additional_properties: Optional[Any] = Field(
1700
+ default=None,
1701
+ description="""Can either be a boolean or an object; controls the presence of additional properties.""",
1702
+ )
1699
1703
  any_of: Optional[list['JSONSchema']] = Field(
1700
1704
  default=None,
1701
1705
  description=(
@@ -1704,6 +1708,20 @@ class JSONSchema(_common.BaseModel):
1704
1708
  ' keyword’s value.'
1705
1709
  ),
1706
1710
  )
1711
+ unique_items: Optional[bool] = Field(
1712
+ default=None,
1713
+ description="""Boolean value that indicates whether the items in an array are unique.""",
1714
+ )
1715
+ ref: Optional[str] = Field(
1716
+ default=None,
1717
+ alias='$ref',
1718
+ description="""Allows indirect references between schema nodes.""",
1719
+ )
1720
+ defs: Optional[dict[str, 'JSONSchema']] = Field(
1721
+ default=None,
1722
+ alias='$defs',
1723
+ description="""Schema definitions to be used with $ref.""",
1724
+ )
1707
1725
 
1708
1726
 
1709
1727
  class Schema(_common.BaseModel):
@@ -1915,7 +1933,7 @@ class Schema(_common.BaseModel):
1915
1933
  list_schema_field_names: tuple[str, ...] = (
1916
1934
  'any_of', # 'one_of', 'all_of', 'not' to come
1917
1935
  )
1918
- dict_schema_field_names: tuple[str, ...] = ('properties',) # 'defs' to come
1936
+ dict_schema_field_names: tuple[str, ...] = ('properties',)
1919
1937
 
1920
1938
  related_field_names_by_type: dict[str, tuple[str, ...]] = {
1921
1939
  JSONSchemaType.NUMBER.value: (
@@ -1964,6 +1982,23 @@ class Schema(_common.BaseModel):
1964
1982
  # placeholder for potential gemini api unsupported fields
1965
1983
  gemini_api_unsupported_field_names: tuple[str, ...] = ()
1966
1984
 
1985
+ def _resolve_ref(
1986
+ ref_path: str, root_schema_dict: dict[str, Any]
1987
+ ) -> dict[str, Any]:
1988
+ """Helper to resolve a $ref path."""
1989
+ current = root_schema_dict
1990
+ for part in ref_path.lstrip('#/').split('/'):
1991
+ if part == '$defs':
1992
+ part = 'defs'
1993
+ current = current[part]
1994
+ current.pop('title', None)
1995
+ if 'properties' in current and current['properties'] is not None:
1996
+ for prop_schema in current['properties'].values():
1997
+ if isinstance(prop_schema, dict):
1998
+ prop_schema.pop('title', None)
1999
+
2000
+ return current
2001
+
1967
2002
  def normalize_json_schema_type(
1968
2003
  json_schema_type: Optional[
1969
2004
  Union[JSONSchemaType, Sequence[JSONSchemaType], str, Sequence[str]]
@@ -1972,11 +2007,16 @@ class Schema(_common.BaseModel):
1972
2007
  """Returns (non_null_types, nullable)"""
1973
2008
  if json_schema_type is None:
1974
2009
  return [], False
1975
- if not isinstance(json_schema_type, Sequence):
1976
- json_schema_type = [json_schema_type]
2010
+ type_sequence: Sequence[Union[JSONSchemaType, str]]
2011
+ if isinstance(json_schema_type, str) or not isinstance(
2012
+ json_schema_type, Sequence
2013
+ ):
2014
+ type_sequence = [json_schema_type]
2015
+ else:
2016
+ type_sequence = json_schema_type
1977
2017
  non_null_types = []
1978
2018
  nullable = False
1979
- for type_value in json_schema_type:
2019
+ for type_value in type_sequence:
1980
2020
  if isinstance(type_value, JSONSchemaType):
1981
2021
  type_value = type_value.value
1982
2022
  if type_value == JSONSchemaType.NULL.value:
@@ -1996,7 +2036,10 @@ class Schema(_common.BaseModel):
1996
2036
  for field_name, field_value in json_schema_dict.items():
1997
2037
  if field_value is None:
1998
2038
  continue
1999
- if field_name not in google_schema_field_names:
2039
+ if field_name not in google_schema_field_names and field_name not in [
2040
+ 'ref',
2041
+ 'defs',
2042
+ ]:
2000
2043
  raise ValueError(
2001
2044
  f'JSONSchema field "{field_name}" is not supported by the '
2002
2045
  'Schema object. And the "raise_error_on_unsupported_field" '
@@ -2026,12 +2069,19 @@ class Schema(_common.BaseModel):
2026
2069
  )
2027
2070
 
2028
2071
  def convert_json_schema(
2029
- json_schema: JSONSchema,
2072
+ current_json_schema: JSONSchema,
2073
+ root_json_schema_dict: dict[str, Any],
2030
2074
  api_option: Literal['VERTEX_AI', 'GEMINI_API'],
2031
2075
  raise_error_on_unsupported_field: bool,
2032
2076
  ) -> 'Schema':
2033
2077
  schema = Schema()
2034
- json_schema_dict = json_schema.model_dump()
2078
+ json_schema_dict = current_json_schema.model_dump()
2079
+
2080
+ if json_schema_dict.get('ref'):
2081
+ json_schema_dict = _resolve_ref(
2082
+ json_schema_dict['ref'], root_json_schema_dict
2083
+ )
2084
+
2035
2085
  raise_error_if_cannot_convert(
2036
2086
  json_schema_dict=json_schema_dict,
2037
2087
  api_option=api_option,
@@ -2057,6 +2107,7 @@ class Schema(_common.BaseModel):
2057
2107
  non_null_types, nullable = normalize_json_schema_type(
2058
2108
  json_schema_dict.get('type', None)
2059
2109
  )
2110
+ is_union_like_type = len(non_null_types) > 1
2060
2111
  if len(non_null_types) > 1:
2061
2112
  logger.warning(
2062
2113
  'JSONSchema type is union-like, e.g. ["null", "string", "array"]. '
@@ -2086,11 +2137,14 @@ class Schema(_common.BaseModel):
2086
2137
  # Pass 2: the JSONSchema.type is not union-like,
2087
2138
  # e.g. 'string', ['string'], ['null', 'string'].
2088
2139
  for field_name, field_value in json_schema_dict.items():
2089
- if field_value is None:
2140
+ if field_value is None or field_name == 'defs':
2090
2141
  continue
2091
2142
  if field_name in schema_field_names:
2143
+ if field_name == 'items' and not field_value:
2144
+ continue
2092
2145
  schema_field_value: 'Schema' = convert_json_schema(
2093
- json_schema=JSONSchema(**field_value),
2146
+ current_json_schema=JSONSchema(**field_value),
2147
+ root_json_schema_dict=root_json_schema_dict,
2094
2148
  api_option=api_option,
2095
2149
  raise_error_on_unsupported_field=raise_error_on_unsupported_field,
2096
2150
  )
@@ -2098,17 +2152,21 @@ class Schema(_common.BaseModel):
2098
2152
  elif field_name in list_schema_field_names:
2099
2153
  list_schema_field_value: list['Schema'] = [
2100
2154
  convert_json_schema(
2101
- json_schema=JSONSchema(**this_field_value),
2155
+ current_json_schema=JSONSchema(**this_field_value),
2156
+ root_json_schema_dict=root_json_schema_dict,
2102
2157
  api_option=api_option,
2103
2158
  raise_error_on_unsupported_field=raise_error_on_unsupported_field,
2104
2159
  )
2105
2160
  for this_field_value in field_value
2106
2161
  ]
2107
2162
  setattr(schema, field_name, list_schema_field_value)
2163
+ if not schema.type and not is_union_like_type:
2164
+ schema.type = Type('OBJECT')
2108
2165
  elif field_name in dict_schema_field_names:
2109
2166
  dict_schema_field_value: dict[str, 'Schema'] = {
2110
2167
  key: convert_json_schema(
2111
- json_schema=JSONSchema(**value),
2168
+ current_json_schema=JSONSchema(**value),
2169
+ root_json_schema_dict=root_json_schema_dict,
2112
2170
  api_option=api_option,
2113
2171
  raise_error_on_unsupported_field=raise_error_on_unsupported_field,
2114
2172
  )
@@ -2116,20 +2174,52 @@ class Schema(_common.BaseModel):
2116
2174
  }
2117
2175
  setattr(schema, field_name, dict_schema_field_value)
2118
2176
  elif field_name == 'type':
2119
- # non_null_types can only be empty or have one element.
2120
- # because already handled union-like case above.
2121
2177
  non_null_types, nullable = normalize_json_schema_type(field_value)
2122
2178
  if nullable:
2123
2179
  schema.nullable = True
2124
2180
  if non_null_types:
2125
2181
  schema.type = Type(non_null_types[0])
2126
2182
  else:
2127
- setattr(schema, field_name, field_value)
2183
+ if (
2184
+ hasattr(schema, field_name)
2185
+ and field_name != 'additional_properties'
2186
+ ):
2187
+ setattr(schema, field_name, field_value)
2188
+
2189
+ if (
2190
+ schema.type == 'ARRAY'
2191
+ and schema.items
2192
+ and not schema.items.model_dump(exclude_unset=True)
2193
+ ):
2194
+ schema.items = None
2195
+
2196
+ if schema.any_of and len(schema.any_of) == 2:
2197
+ nullable_part = None
2198
+ type_part = None
2199
+ for part in schema.any_of:
2200
+ # A schema representing `None` will either be of type NULL or just be nullable.
2201
+ part_dict = part.model_dump(exclude_unset=True)
2202
+ if part_dict == {'nullable': True} or part_dict == {'type': 'NULL'}:
2203
+ nullable_part = part
2204
+ else:
2205
+ type_part = part
2206
+
2207
+ # If we found both parts, unwrap them into a single schema.
2208
+ if nullable_part and type_part:
2209
+ default_value = schema.default
2210
+ schema = type_part
2211
+ schema.nullable = True
2212
+ # Carry the default value over to the unwrapped schema
2213
+ if default_value is not None:
2214
+ schema.default = default_value
2128
2215
 
2129
2216
  return schema
2130
2217
 
2218
+ # This is the initial call to the recursive function.
2219
+ root_schema_dict = json_schema.model_dump()
2131
2220
  return convert_json_schema(
2132
- json_schema=json_schema,
2221
+ current_json_schema=json_schema,
2222
+ root_json_schema_dict=root_schema_dict,
2133
2223
  api_option=api_option,
2134
2224
  raise_error_on_unsupported_field=raise_error_on_unsupported_field,
2135
2225
  )
@@ -2371,7 +2461,32 @@ class FunctionDeclaration(_common.BaseModel):
2371
2461
  json_schema_dict = _automatic_function_calling_util._add_unevaluated_items_to_fixed_len_tuple_schema(
2372
2462
  json_schema_dict
2373
2463
  )
2374
- parameters_json_schema[name] = json_schema_dict
2464
+ if 'prefixItems' in json_schema_dict:
2465
+ parameters_json_schema[name] = json_schema_dict
2466
+ continue
2467
+
2468
+ union_args = typing.get_args(param.annotation)
2469
+ has_primitive = any(
2470
+ _automatic_function_calling_util._is_builtin_primitive_or_compound(
2471
+ arg
2472
+ )
2473
+ for arg in union_args
2474
+ )
2475
+ if (
2476
+ '$ref' in json_schema_dict or '$defs' in json_schema_dict
2477
+ ) and has_primitive:
2478
+ # This is a complex schema with a primitive (e.g., str | MyModel)
2479
+ # that is better represented by raw JSON schema.
2480
+ parameters_json_schema[name] = json_schema_dict
2481
+ continue
2482
+
2483
+ schema = Schema.from_json_schema(
2484
+ json_schema=JSONSchema(**json_schema_dict),
2485
+ api_option=api_option,
2486
+ )
2487
+ if param.default is not inspect.Parameter.empty:
2488
+ schema.default = param.default
2489
+ parameters_properties[name] = schema
2375
2490
  except Exception as e:
2376
2491
  _automatic_function_calling_util._raise_for_unsupported_param(
2377
2492
  param, callable.__name__, e
@@ -13,4 +13,4 @@
13
13
  # limitations under the License.
14
14
  #
15
15
 
16
- __version__ = '1.43.0' # x-release-please-version
16
+ __version__ = '1.45.0' # x-release-please-version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-genai
3
- Version: 1.43.0
3
+ Version: 1.45.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 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Topic :: Internet
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
21
  Requires-Python: >=3.9
@@ -279,6 +280,25 @@ http_options = types.HttpOptions(
279
280
  client=Client(..., http_options=http_options)
280
281
  ```
281
282
 
283
+ ### Custom base url
284
+
285
+ In some cases you might need a custom base url (for example, API gateway proxy
286
+ server) and bypass some authentication checks for project, location, or API key.
287
+ You may pass the custom base url like this:
288
+
289
+ ```python
290
+
291
+ base_url = 'https://test-api-gateway-proxy.com'
292
+ client = Client(
293
+ vertexai=True,
294
+ http_options={
295
+ 'base_url': base_url,
296
+ 'headers': {'Authorization': 'Bearer test_token'},
297
+ },
298
+ )
299
+
300
+ ```
301
+
282
302
  ## Types
283
303
 
284
304
  Parameter types can be specified as either dictionaries(`TypedDict`) or
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel", "twine>=6.1.0", "packaging>=24.2", "pkginfo>=
3
3
 
4
4
  [project]
5
5
  name = "google-genai"
6
- version = "1.43.0"
6
+ version = "1.45.0"
7
7
  description = "GenAI Python SDK"
8
8
  readme = "README.md"
9
9
  license = {text = "Apache-2.0"}
@@ -22,6 +22,7 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.11",
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
25
26
  "Topic :: Internet",
26
27
  "Topic :: Software Development :: Libraries :: Python Modules",
27
28
  ]
@@ -51,4 +52,4 @@ packages = [
51
52
  include-package-data = true
52
53
 
53
54
  [tools.setuptools.package_data]
54
- "google.genai" = ["py.typed"]
55
+ "google.genai" = ["py.typed"]
File without changes
File without changes
File without changes