google-genai 1.61.0__py3-none-any.whl → 1.62.0__py3-none-any.whl
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.
- google/genai/_interactions/_base_client.py +5 -2
- google/genai/_interactions/_compat.py +3 -3
- google/genai/_interactions/_utils/_json.py +50 -0
- google/genai/errors.py +19 -6
- google/genai/live.py +22 -2
- google/genai/live_music.py +14 -1
- google/genai/models.py +486 -197
- google/genai/tests/errors/test_api_error.py +38 -0
- google/genai/tunings.py +48 -16
- google/genai/version.py +1 -1
- {google_genai-1.61.0.dist-info → google_genai-1.62.0.dist-info}/METADATA +1 -1
- {google_genai-1.61.0.dist-info → google_genai-1.62.0.dist-info}/RECORD +15 -14
- {google_genai-1.61.0.dist-info → google_genai-1.62.0.dist-info}/WHEEL +0 -0
- {google_genai-1.61.0.dist-info → google_genai-1.62.0.dist-info}/licenses/LICENSE +0 -0
- {google_genai-1.61.0.dist-info → google_genai-1.62.0.dist-info}/top_level.txt +0 -0
|
@@ -101,6 +101,7 @@ from ._exceptions import (
|
|
|
101
101
|
APIConnectionError,
|
|
102
102
|
APIResponseValidationError,
|
|
103
103
|
)
|
|
104
|
+
from ._utils._json import openapi_dumps
|
|
104
105
|
|
|
105
106
|
log: logging.Logger = logging.getLogger(__name__)
|
|
106
107
|
|
|
@@ -578,8 +579,10 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
|
|
|
578
579
|
kwargs["content"] = options.content
|
|
579
580
|
elif isinstance(json_data, bytes):
|
|
580
581
|
kwargs["content"] = json_data
|
|
581
|
-
|
|
582
|
-
|
|
582
|
+
elif not files:
|
|
583
|
+
# Don't set content when JSON is sent as multipart/form-data,
|
|
584
|
+
# since httpx's content param overrides other body arguments
|
|
585
|
+
kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
|
|
583
586
|
kwargs["files"] = files
|
|
584
587
|
else:
|
|
585
588
|
headers.pop("Content-Type", None)
|
|
@@ -154,6 +154,7 @@ def model_dump(
|
|
|
154
154
|
exclude_defaults: bool = False,
|
|
155
155
|
warnings: bool = True,
|
|
156
156
|
mode: Literal["json", "python"] = "python",
|
|
157
|
+
by_alias: bool | None = None,
|
|
157
158
|
) -> dict[str, Any]:
|
|
158
159
|
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
|
|
159
160
|
return model.model_dump(
|
|
@@ -163,13 +164,12 @@ def model_dump(
|
|
|
163
164
|
exclude_defaults=exclude_defaults,
|
|
164
165
|
# warnings are not supported in Pydantic v1
|
|
165
166
|
warnings=True if PYDANTIC_V1 else warnings,
|
|
167
|
+
by_alias=by_alias,
|
|
166
168
|
)
|
|
167
169
|
return cast(
|
|
168
170
|
"dict[str, Any]",
|
|
169
171
|
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
|
|
170
|
-
exclude=exclude,
|
|
171
|
-
exclude_unset=exclude_unset,
|
|
172
|
-
exclude_defaults=exclude_defaults,
|
|
172
|
+
exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
|
|
173
173
|
),
|
|
174
174
|
)
|
|
175
175
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing_extensions import override
|
|
20
|
+
|
|
21
|
+
import pydantic
|
|
22
|
+
|
|
23
|
+
from .._compat import model_dump
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def openapi_dumps(obj: Any) -> bytes:
|
|
27
|
+
"""
|
|
28
|
+
Serialize an object to UTF-8 encoded JSON bytes.
|
|
29
|
+
|
|
30
|
+
Extends the standard json.dumps with support for additional types
|
|
31
|
+
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
|
|
32
|
+
"""
|
|
33
|
+
return json.dumps(
|
|
34
|
+
obj,
|
|
35
|
+
cls=_CustomEncoder,
|
|
36
|
+
# Uses the same defaults as httpx's JSON serialization
|
|
37
|
+
ensure_ascii=False,
|
|
38
|
+
separators=(",", ":"),
|
|
39
|
+
allow_nan=False,
|
|
40
|
+
).encode()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _CustomEncoder(json.JSONEncoder):
|
|
44
|
+
@override
|
|
45
|
+
def default(self, o: Any) -> Any:
|
|
46
|
+
if isinstance(o, datetime):
|
|
47
|
+
return o.isoformat()
|
|
48
|
+
if isinstance(o, pydantic.BaseModel):
|
|
49
|
+
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
|
|
50
|
+
return super().default(o)
|
google/genai/errors.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from typing import Any, Callable, Optional, TYPE_CHECKING, Union
|
|
19
19
|
import httpx
|
|
20
20
|
import json
|
|
21
|
+
import websockets
|
|
21
22
|
from . import _common
|
|
22
23
|
|
|
23
24
|
|
|
@@ -69,14 +70,26 @@ class APIError(Exception):
|
|
|
69
70
|
return obj
|
|
70
71
|
|
|
71
72
|
def _get_status(self, response_json: Any) -> Any:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
try:
|
|
74
|
+
status = response_json.get(
|
|
75
|
+
'status', response_json.get('error', {}).get('status', None)
|
|
76
|
+
)
|
|
77
|
+
return status
|
|
78
|
+
except AttributeError:
|
|
79
|
+
# If response_json is not a dict, return close code to handle the case
|
|
80
|
+
# when encountering a websocket error.
|
|
81
|
+
return None
|
|
75
82
|
|
|
76
83
|
def _get_message(self, response_json: Any) -> Any:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
try:
|
|
85
|
+
message = response_json.get(
|
|
86
|
+
'message', response_json.get('error', {}).get('message', None)
|
|
87
|
+
)
|
|
88
|
+
return message
|
|
89
|
+
except AttributeError:
|
|
90
|
+
# If response_json is not a dict, return it as None.
|
|
91
|
+
# This is to handle the case when encountering a websocket error.
|
|
92
|
+
return None
|
|
80
93
|
|
|
81
94
|
def _get_code(self, response_json: Any) -> Any:
|
|
82
95
|
return response_json.get(
|
google/genai/live.py
CHANGED
|
@@ -26,7 +26,7 @@ import warnings
|
|
|
26
26
|
|
|
27
27
|
import google.auth
|
|
28
28
|
import pydantic
|
|
29
|
-
|
|
29
|
+
import websockets
|
|
30
30
|
|
|
31
31
|
from . import _api_module
|
|
32
32
|
from . import _common
|
|
@@ -41,6 +41,7 @@ from ._common import set_value_by_path as setv
|
|
|
41
41
|
from .live_music import AsyncLiveMusic
|
|
42
42
|
from .models import _Content_to_mldev
|
|
43
43
|
|
|
44
|
+
ConnectionClosed = websockets.ConnectionClosed
|
|
44
45
|
|
|
45
46
|
try:
|
|
46
47
|
from websockets.asyncio.client import ClientConnection
|
|
@@ -534,6 +535,14 @@ class AsyncSession:
|
|
|
534
535
|
raw_response = await self._ws.recv(decode=False)
|
|
535
536
|
except TypeError:
|
|
536
537
|
raw_response = await self._ws.recv() # type: ignore[assignment]
|
|
538
|
+
except ConnectionClosed as e:
|
|
539
|
+
if e.rcvd:
|
|
540
|
+
code = e.rcvd.code
|
|
541
|
+
reason = e.rcvd.reason
|
|
542
|
+
else:
|
|
543
|
+
code = 1006
|
|
544
|
+
reason = websockets.frames.CLOSE_CODE_EXPLANATIONS.get(code, 'Abnormal closure.')
|
|
545
|
+
errors.APIError.raise_error(code, reason, None)
|
|
537
546
|
if raw_response:
|
|
538
547
|
try:
|
|
539
548
|
response = json.loads(raw_response)
|
|
@@ -545,8 +554,11 @@ class AsyncSession:
|
|
|
545
554
|
if self._api_client.vertexai:
|
|
546
555
|
response_dict = live_converters._LiveServerMessage_from_vertex(response)
|
|
547
556
|
else:
|
|
548
|
-
response_dict = response
|
|
557
|
+
response_dict = live_converters._LiveServerMessage_from_mldev(response)
|
|
549
558
|
|
|
559
|
+
if not response_dict and response:
|
|
560
|
+
# Error handling.
|
|
561
|
+
errors.APIError.raise_error(response.get('code'), response, None)
|
|
550
562
|
return types.LiveServerMessage._from_response(
|
|
551
563
|
response=response_dict, kwargs=parameter_model.model_dump()
|
|
552
564
|
)
|
|
@@ -1093,6 +1105,14 @@ class AsyncLive(_api_module.BaseModule):
|
|
|
1093
1105
|
raw_response = await ws.recv(decode=False)
|
|
1094
1106
|
except TypeError:
|
|
1095
1107
|
raw_response = await ws.recv() # type: ignore[assignment]
|
|
1108
|
+
except ConnectionClosed as e:
|
|
1109
|
+
if e.rcvd:
|
|
1110
|
+
code = e.rcvd.code
|
|
1111
|
+
reason = e.rcvd.reason
|
|
1112
|
+
else:
|
|
1113
|
+
code = 1006
|
|
1114
|
+
reason = 'Abnormal closure.'
|
|
1115
|
+
errors.APIError.raise_error(code, reason, None)
|
|
1096
1116
|
if raw_response:
|
|
1097
1117
|
try:
|
|
1098
1118
|
response = json.loads(raw_response)
|
google/genai/live_music.py
CHANGED
|
@@ -19,15 +19,18 @@ import contextlib
|
|
|
19
19
|
import json
|
|
20
20
|
import logging
|
|
21
21
|
from typing import AsyncIterator
|
|
22
|
+
import websockets
|
|
22
23
|
|
|
23
24
|
from . import _api_module
|
|
24
25
|
from . import _common
|
|
25
26
|
from . import _live_converters as live_converters
|
|
26
27
|
from . import _transformers as t
|
|
28
|
+
from . import errors
|
|
27
29
|
from . import types
|
|
28
30
|
from ._api_client import BaseApiClient
|
|
29
31
|
from ._common import set_value_by_path as setv
|
|
30
32
|
|
|
33
|
+
ConnectionClosed = websockets.ConnectionClosed
|
|
31
34
|
|
|
32
35
|
try:
|
|
33
36
|
from websockets.asyncio.client import ClientConnection
|
|
@@ -122,6 +125,14 @@ class AsyncMusicSession:
|
|
|
122
125
|
raw_response = await self._ws.recv(decode=False)
|
|
123
126
|
except TypeError:
|
|
124
127
|
raw_response = await self._ws.recv() # type: ignore[assignment]
|
|
128
|
+
except ConnectionClosed as e:
|
|
129
|
+
if e.rcvd:
|
|
130
|
+
code = e.rcvd.code
|
|
131
|
+
reason = e.rcvd.reason
|
|
132
|
+
else:
|
|
133
|
+
code = 1006
|
|
134
|
+
reason = websockets.frames.CLOSE_CODE_EXPLANATIONS.get(code, 'Abnormal closure.')
|
|
135
|
+
errors.APIError.raise_error(code, reason, None)
|
|
125
136
|
if raw_response:
|
|
126
137
|
try:
|
|
127
138
|
response = json.loads(raw_response)
|
|
@@ -134,7 +145,9 @@ class AsyncMusicSession:
|
|
|
134
145
|
raise NotImplementedError('Live music generation is not supported in Vertex AI.')
|
|
135
146
|
else:
|
|
136
147
|
response_dict = response
|
|
137
|
-
|
|
148
|
+
if not response_dict and response:
|
|
149
|
+
# Error handling.
|
|
150
|
+
errors.APIError.raise_error(response.get('code'), response, None)
|
|
138
151
|
return types.LiveMusicServerMessage._from_response(
|
|
139
152
|
response=response_dict, kwargs=parameter_model.model_dump()
|
|
140
153
|
)
|