google-genai 1.28.0__tar.gz → 1.29.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.28.0/google_genai.egg-info → google_genai-1.29.0}/PKG-INFO +2 -2
  2. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_api_client.py +139 -54
  3. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_automatic_function_calling_util.py +35 -7
  4. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_live_converters.py +14 -0
  5. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/batches.py +19 -2
  6. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/errors.py +5 -2
  7. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/models.py +393 -12
  8. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/types.py +336 -39
  9. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/version.py +1 -1
  10. {google_genai-1.28.0 → google_genai-1.29.0/google_genai.egg-info}/PKG-INFO +2 -2
  11. {google_genai-1.28.0 → google_genai-1.29.0}/google_genai.egg-info/requires.txt +1 -1
  12. {google_genai-1.28.0 → google_genai-1.29.0}/pyproject.toml +2 -2
  13. {google_genai-1.28.0 → google_genai-1.29.0}/LICENSE +0 -0
  14. {google_genai-1.28.0 → google_genai-1.29.0}/MANIFEST.in +0 -0
  15. {google_genai-1.28.0 → google_genai-1.29.0}/README.md +0 -0
  16. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/__init__.py +0 -0
  17. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_adapters.py +0 -0
  18. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_api_module.py +0 -0
  19. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_base_url.py +0 -0
  20. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_common.py +0 -0
  21. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_extra_utils.py +0 -0
  22. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_mcp_utils.py +0 -0
  23. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_replay_api_client.py +0 -0
  24. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_test_api_client.py +0 -0
  25. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_tokens_converters.py +0 -0
  26. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/_transformers.py +0 -0
  27. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/caches.py +0 -0
  28. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/chats.py +0 -0
  29. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/client.py +0 -0
  30. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/files.py +0 -0
  31. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/live.py +0 -0
  32. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/live_music.py +0 -0
  33. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/operations.py +0 -0
  34. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/pagers.py +0 -0
  35. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/py.typed +0 -0
  36. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/tokens.py +0 -0
  37. {google_genai-1.28.0 → google_genai-1.29.0}/google/genai/tunings.py +0 -0
  38. {google_genai-1.28.0 → google_genai-1.29.0}/google_genai.egg-info/SOURCES.txt +0 -0
  39. {google_genai-1.28.0 → google_genai-1.29.0}/google_genai.egg-info/dependency_links.txt +0 -0
  40. {google_genai-1.28.0 → google_genai-1.29.0}/google_genai.egg-info/top_level.txt +0 -0
  41. {google_genai-1.28.0 → google_genai-1.29.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-genai
3
- Version: 1.28.0
3
+ Version: 1.29.0
4
4
  Summary: GenAI Python SDK
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache-2.0
@@ -25,7 +25,7 @@ Requires-Dist: google-auth<3.0.0,>=2.14.1
25
25
  Requires-Dist: httpx<1.0.0,>=0.28.1
26
26
  Requires-Dist: pydantic<3.0.0,>=2.0.0
27
27
  Requires-Dist: requests<3.0.0,>=2.28.1
28
- Requires-Dist: tenacity<9.0.0,>=8.2.3
28
+ Requires-Dist: tenacity<9.2.0,>=8.2.3
29
29
  Requires-Dist: websockets<15.1.0,>=13.0.0
30
30
  Requires-Dist: typing-extensions<5.0.0,>=4.11.0
31
31
  Provides-Extra: aiohttp
@@ -20,23 +20,21 @@ The BaseApiClient is intended to be a private module and is subject to change.
20
20
  """
21
21
 
22
22
  import asyncio
23
- from collections.abc import Awaitable, Generator
23
+ from collections.abc import Generator
24
24
  import copy
25
25
  from dataclasses import dataclass
26
- import datetime
27
- import http
28
26
  import inspect
29
27
  import io
30
28
  import json
31
29
  import logging
32
30
  import math
33
31
  import os
34
- import ssl
35
32
  import random
33
+ import ssl
36
34
  import sys
37
35
  import threading
38
36
  import time
39
- from typing import Any, AsyncIterator, Optional, TYPE_CHECKING, Tuple, Union
37
+ from typing import Any, AsyncIterator, Iterator, Optional, Tuple, TYPE_CHECKING, Union
40
38
  from urllib.parse import urlparse
41
39
  from urllib.parse import urlunparse
42
40
 
@@ -48,7 +46,6 @@ from google.auth.credentials import Credentials
48
46
  from google.auth.transport.requests import Request
49
47
  import httpx
50
48
  from pydantic import BaseModel
51
- from pydantic import Field
52
49
  from pydantic import ValidationError
53
50
  import tenacity
54
51
 
@@ -56,11 +53,11 @@ from . import _common
56
53
  from . import errors
57
54
  from . import version
58
55
  from .types import HttpOptions
59
- from .types import HttpOptionsDict
60
56
  from .types import HttpOptionsOrDict
61
57
  from .types import HttpResponse as SdkHttpResponse
62
58
  from .types import HttpRetryOptions
63
59
 
60
+
64
61
  try:
65
62
  from websockets.asyncio.client import connect as ws_connect
66
63
  except ModuleNotFoundError:
@@ -238,12 +235,12 @@ class HttpResponse:
238
235
  self.headers = headers
239
236
  elif isinstance(headers, httpx.Headers):
240
237
  self.headers = {
241
- key: ', '.join(headers.get_list(key))
242
- for key in headers.keys()}
238
+ key: ', '.join(headers.get_list(key)) for key in headers.keys()
239
+ }
243
240
  elif type(headers).__name__ == 'CIMultiDictProxy':
244
241
  self.headers = {
245
- key: ', '.join(headers.getall(key))
246
- for key in headers.keys()}
242
+ key: ', '.join(headers.getall(key)) for key in headers.keys()
243
+ }
247
244
 
248
245
  self.status_code: int = 200
249
246
  self.response_stream = response_stream
@@ -265,68 +262,32 @@ class HttpResponse:
265
262
  def json(self) -> Any:
266
263
  if not self.response_stream[0]: # Empty response
267
264
  return ''
268
- return json.loads(self.response_stream[0])
265
+ return self._load_json_from_response(self.response_stream[0])
269
266
 
270
267
  def segments(self) -> Generator[Any, None, None]:
271
268
  if isinstance(self.response_stream, list):
272
269
  # list of objects retrieved from replay or from non-streaming API.
273
270
  for chunk in self.response_stream:
274
- yield json.loads(chunk) if chunk else {}
271
+ yield self._load_json_from_response(chunk) if chunk else {}
275
272
  elif self.response_stream is None:
276
273
  yield from []
277
274
  else:
278
275
  # Iterator of objects retrieved from the API.
279
- for chunk in self.response_stream.iter_lines(): # type: ignore[union-attr]
280
- if chunk:
281
- # In streaming mode, the chunk of JSON is prefixed with "data:" which
282
- # we must strip before parsing.
283
- if not isinstance(chunk, str):
284
- chunk = chunk.decode('utf-8')
285
- if chunk.startswith('data: '):
286
- chunk = chunk[len('data: ') :]
287
- yield json.loads(chunk)
276
+ for chunk in self._iter_response_stream():
277
+ yield self._load_json_from_response(chunk)
288
278
 
289
279
  async def async_segments(self) -> AsyncIterator[Any]:
290
280
  if isinstance(self.response_stream, list):
291
281
  # list of objects retrieved from replay or from non-streaming API.
292
282
  for chunk in self.response_stream:
293
- yield json.loads(chunk) if chunk else {}
283
+ yield self._load_json_from_response(chunk) if chunk else {}
294
284
  elif self.response_stream is None:
295
285
  async for c in []: # type: ignore[attr-defined]
296
286
  yield c
297
287
  else:
298
288
  # Iterator of objects retrieved from the API.
299
- if hasattr(self.response_stream, 'aiter_lines'):
300
- async for chunk in self.response_stream.aiter_lines():
301
- # This is httpx.Response.
302
- if chunk:
303
- # In async streaming mode, the chunk of JSON is prefixed with
304
- # "data:" which we must strip before parsing.
305
- if not isinstance(chunk, str):
306
- chunk = chunk.decode('utf-8')
307
- if chunk.startswith('data: '):
308
- chunk = chunk[len('data: ') :]
309
- yield json.loads(chunk)
310
- elif hasattr(self.response_stream, 'content'):
311
- # This is aiohttp.ClientResponse.
312
- try:
313
- while True:
314
- chunk = await self.response_stream.content.readline()
315
- if not chunk:
316
- break
317
- # In async streaming mode, the chunk of JSON is prefixed with
318
- # "data:" which we must strip before parsing.
319
- chunk = chunk.decode('utf-8')
320
- if chunk.startswith('data: '):
321
- chunk = chunk[len('data: ') :]
322
- chunk = chunk.strip()
323
- if chunk:
324
- yield json.loads(chunk)
325
- finally:
326
- if hasattr(self, '_session') and self._session:
327
- await self._session.close()
328
- else:
329
- raise ValueError('Error parsing streaming response.')
289
+ async for chunk in self._aiter_response_stream():
290
+ yield self._load_json_from_response(chunk)
330
291
 
331
292
  def byte_segments(self) -> Generator[Union[bytes, Any], None, None]:
332
293
  if isinstance(self.byte_stream, list):
@@ -345,6 +306,130 @@ class HttpResponse:
345
306
  for attribute in dir(self):
346
307
  response_payload[attribute] = copy.deepcopy(getattr(self, attribute))
347
308
 
309
+ def _iter_response_stream(self) -> Iterator[str]:
310
+ """Iterates over chunks retrieved from the API."""
311
+ if not isinstance(self.response_stream, httpx.Response):
312
+ raise TypeError(
313
+ 'Expected self.response_stream to be an httpx.Response object, '
314
+ f'but got {type(self.response_stream).__name__}.'
315
+ )
316
+
317
+ chunk = ''
318
+ balance = 0
319
+ for line in self.response_stream.iter_lines():
320
+ if not line:
321
+ continue
322
+
323
+ # In streaming mode, the response of JSON is prefixed with "data: " which
324
+ # we must strip before parsing.
325
+ if line.startswith('data: '):
326
+ yield line[len('data: '):]
327
+ continue
328
+
329
+ # When API returns an error message, it comes line by line. So we buffer
330
+ # the lines until a complete JSON string is read. A complete JSON string
331
+ # is found when the balance is 0.
332
+ for c in line:
333
+ if c == '{':
334
+ balance += 1
335
+ elif c == '}':
336
+ balance -= 1
337
+
338
+ chunk += line
339
+ if balance == 0:
340
+ yield chunk
341
+ chunk = ''
342
+
343
+ # If there is any remaining chunk, yield it.
344
+ if chunk:
345
+ yield chunk
346
+
347
+ async def _aiter_response_stream(self) -> AsyncIterator[str]:
348
+ """Asynchronously iterates over chunks retrieved from the API."""
349
+ if not isinstance(
350
+ self.response_stream, (httpx.Response, aiohttp.ClientResponse)
351
+ ):
352
+ raise TypeError(
353
+ 'Expected self.response_stream to be an httpx.Response or'
354
+ ' aiohttp.ClientResponse object, but got'
355
+ f' {type(self.response_stream).__name__}.'
356
+ )
357
+
358
+ chunk = ''
359
+ balance = 0
360
+ # httpx.Response has a dedicated async line iterator.
361
+ if isinstance(self.response_stream, httpx.Response):
362
+ async for line in self.response_stream.aiter_lines():
363
+ if not line:
364
+ continue
365
+ # In streaming mode, the response of JSON is prefixed with "data: "
366
+ # which we must strip before parsing.
367
+ if line.startswith('data: '):
368
+ yield line[len('data: '):]
369
+ continue
370
+
371
+ # When API returns an error message, it comes line by line. So we buffer
372
+ # the lines until a complete JSON string is read. A complete JSON string
373
+ # is found when the balance is 0.
374
+ for c in line:
375
+ if c == '{':
376
+ balance += 1
377
+ elif c == '}':
378
+ balance -= 1
379
+
380
+ chunk += line
381
+ if balance == 0:
382
+ yield chunk
383
+ chunk = ''
384
+
385
+ # aiohttp.ClientResponse uses a content stream that we read line by line.
386
+ elif isinstance(self.response_stream, aiohttp.ClientResponse):
387
+ while True:
388
+ # Read a line from the stream. This returns bytes.
389
+ line_bytes = await self.response_stream.content.readline()
390
+ if not line_bytes:
391
+ break
392
+ # Decode the bytes and remove trailing whitespace and newlines.
393
+ line = line_bytes.decode('utf-8').rstrip()
394
+ if not line:
395
+ continue
396
+
397
+ # In streaming mode, the response of JSON is prefixed with "data: "
398
+ # which we must strip before parsing.
399
+ if line.startswith('data: '):
400
+ yield line[len('data: '):]
401
+ continue
402
+
403
+ # When API returns an error message, it comes line by line. So we buffer
404
+ # the lines until a complete JSON string is read. A complete JSON string
405
+ # is found when the balance is 0.
406
+ for c in line:
407
+ if c == '{':
408
+ balance += 1
409
+ elif c == '}':
410
+ balance -= 1
411
+
412
+ chunk += line
413
+ if balance == 0:
414
+ yield chunk
415
+ chunk = ''
416
+
417
+ # If there is any remaining chunk, yield it.
418
+ if chunk:
419
+ yield chunk
420
+
421
+ if hasattr(self, '_session') and self._session:
422
+ await self._session.close()
423
+
424
+ @classmethod
425
+ def _load_json_from_response(cls, response: Any) -> Any:
426
+ """Loads JSON from the response, or raises an error if the parsing fails."""
427
+ try:
428
+ return json.loads(response)
429
+ except json.JSONDecodeError as e:
430
+ raise errors.UnknownApiResponseError(
431
+ f'Failed to parse response as JSON. Raw response: {response}'
432
+ ) from e
348
433
 
349
434
  # Default retry options.
350
435
  # The config is based on https://cloud.google.com/storage/docs/retry-strategy.
@@ -41,6 +41,39 @@ _py_builtin_type_to_schema_type = {
41
41
  }
42
42
 
43
43
 
44
+ def _raise_for_unsupported_param(
45
+ param: inspect.Parameter, func_name: str, exception: Union[Exception, type[Exception]]
46
+ ) -> None:
47
+ raise ValueError(
48
+ f'Failed to parse the parameter {param} of function {func_name} for'
49
+ ' automatic function calling.Automatic function calling works best with'
50
+ ' simpler function signature schema, consider manually parsing your'
51
+ f' function declaration for function {func_name}.'
52
+ ) from exception
53
+
54
+
55
+ def _handle_params_as_deferred_annotations(param: inspect.Parameter, annotation_under_future: dict[str, Any], name: str) -> inspect.Parameter:
56
+ """Catches the case when type hints are stored as strings."""
57
+ if isinstance(param.annotation, str):
58
+ param = param.replace(annotation=annotation_under_future[name])
59
+ return param
60
+
61
+
62
+ def _add_unevaluated_items_to_fixed_len_tuple_schema(
63
+ json_schema: dict[str, Any]
64
+ ) -> dict[str, Any]:
65
+ if (
66
+ json_schema.get('maxItems')
67
+ and (
68
+ json_schema.get('prefixItems')
69
+ and len(json_schema['prefixItems']) == json_schema['maxItems']
70
+ )
71
+ and json_schema.get('type') == 'array'
72
+ ):
73
+ json_schema['unevaluatedItems'] = False
74
+ return json_schema
75
+
76
+
44
77
  def _is_builtin_primitive_or_compound(
45
78
  annotation: inspect.Parameter.annotation, # type: ignore[valid-type]
46
79
  ) -> bool:
@@ -92,7 +125,7 @@ def _is_default_value_compatible(
92
125
  return False
93
126
 
94
127
 
95
- def _parse_schema_from_parameter(
128
+ def _parse_schema_from_parameter( # type: ignore[return]
96
129
  api_option: Literal['VERTEX_AI', 'GEMINI_API'],
97
130
  param: inspect.Parameter,
98
131
  func_name: str,
@@ -267,12 +300,7 @@ def _parse_schema_from_parameter(
267
300
  )
268
301
  schema.required = _get_required_fields(schema)
269
302
  return schema
270
- raise ValueError(
271
- f'Failed to parse the parameter {param} of function {func_name} for'
272
- ' automatic function calling.Automatic function calling works best with'
273
- ' simpler function signature schema, consider manually parsing your'
274
- f' function declaration for function {func_name}.'
275
- )
303
+ _raise_for_unsupported_param(param, func_name, ValueError)
276
304
 
277
305
 
278
306
  def _get_required_fields(schema: types.Schema) -> Optional[list[str]]:
@@ -1098,6 +1098,13 @@ def _LiveMusicGenerationConfig_to_mldev(
1098
1098
  getv(from_object, ['only_bass_and_drums']),
1099
1099
  )
1100
1100
 
1101
+ if getv(from_object, ['music_generation_mode']) is not None:
1102
+ setv(
1103
+ to_object,
1104
+ ['musicGenerationMode'],
1105
+ getv(from_object, ['music_generation_mode']),
1106
+ )
1107
+
1101
1108
  return to_object
1102
1109
 
1103
1110
 
@@ -2871,6 +2878,13 @@ def _LiveMusicGenerationConfig_from_mldev(
2871
2878
  getv(from_object, ['onlyBassAndDrums']),
2872
2879
  )
2873
2880
 
2881
+ if getv(from_object, ['musicGenerationMode']) is not None:
2882
+ setv(
2883
+ to_object,
2884
+ ['music_generation_mode'],
2885
+ getv(from_object, ['musicGenerationMode']),
2886
+ )
2887
+
2874
2888
  return to_object
2875
2889
 
2876
2890
 
@@ -1492,6 +1492,9 @@ def _GenerateContentResponse_from_mldev(
1492
1492
  if getv(from_object, ['promptFeedback']) is not None:
1493
1493
  setv(to_object, ['prompt_feedback'], getv(from_object, ['promptFeedback']))
1494
1494
 
1495
+ if getv(from_object, ['responseId']) is not None:
1496
+ setv(to_object, ['response_id'], getv(from_object, ['responseId']))
1497
+
1495
1498
  if getv(from_object, ['usageMetadata']) is not None:
1496
1499
  setv(to_object, ['usage_metadata'], getv(from_object, ['usageMetadata']))
1497
1500
 
@@ -1648,6 +1651,11 @@ def _DeleteResourceJob_from_mldev(
1648
1651
  parent_object: Optional[dict[str, Any]] = None,
1649
1652
  ) -> dict[str, Any]:
1650
1653
  to_object: dict[str, Any] = {}
1654
+ if getv(from_object, ['sdkHttpResponse']) is not None:
1655
+ setv(
1656
+ to_object, ['sdk_http_response'], getv(from_object, ['sdkHttpResponse'])
1657
+ )
1658
+
1651
1659
  if getv(from_object, ['name']) is not None:
1652
1660
  setv(to_object, ['name'], getv(from_object, ['name']))
1653
1661
 
@@ -1815,6 +1823,11 @@ def _DeleteResourceJob_from_vertex(
1815
1823
  parent_object: Optional[dict[str, Any]] = None,
1816
1824
  ) -> dict[str, Any]:
1817
1825
  to_object: dict[str, Any] = {}
1826
+ if getv(from_object, ['sdkHttpResponse']) is not None:
1827
+ setv(
1828
+ to_object, ['sdk_http_response'], getv(from_object, ['sdkHttpResponse'])
1829
+ )
1830
+
1818
1831
  if getv(from_object, ['name']) is not None:
1819
1832
  setv(to_object, ['name'], getv(from_object, ['name']))
1820
1833
 
@@ -2186,7 +2199,9 @@ class Batches(_api_module.BaseModule):
2186
2199
  return_value = types.DeleteResourceJob._from_response(
2187
2200
  response=response_dict, kwargs=parameter_model.model_dump()
2188
2201
  )
2189
-
2202
+ return_value.sdk_http_response = types.HttpResponse(
2203
+ headers=response.headers
2204
+ )
2190
2205
  self._api_client._verify_response(return_value)
2191
2206
  return return_value
2192
2207
 
@@ -2619,7 +2634,9 @@ class AsyncBatches(_api_module.BaseModule):
2619
2634
  return_value = types.DeleteResourceJob._from_response(
2620
2635
  response=response_dict, kwargs=parameter_model.model_dump()
2621
2636
  )
2622
-
2637
+ return_value.sdk_http_response = types.HttpResponse(
2638
+ headers=response.headers
2639
+ )
2623
2640
  self._api_client._verify_response(return_value)
2624
2641
  return return_value
2625
2642
 
@@ -172,18 +172,21 @@ class ServerError(APIError):
172
172
 
173
173
  class UnknownFunctionCallArgumentError(ValueError):
174
174
  """Raised when the function call argument cannot be converted to the parameter annotation."""
175
-
176
175
  pass
177
176
 
178
177
 
179
178
  class UnsupportedFunctionError(ValueError):
180
179
  """Raised when the function is not supported."""
180
+ pass
181
181
 
182
182
 
183
183
  class FunctionInvocationError(ValueError):
184
184
  """Raised when the function cannot be invoked with the given arguments."""
185
-
186
185
  pass
187
186
 
188
187
 
188
+ class UnknownApiResponseError(ValueError):
189
+ """Raised when the response from the API cannot be parsed as JSON."""
190
+ pass
191
+
189
192
  ExperimentalWarning = _common.ExperimentalWarning