google-genai 1.55.0__py3-none-any.whl → 1.57.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/_api_client.py +37 -18
- google/genai/_interactions/_base_client.py +8 -2
- google/genai/_interactions/resources/interactions.py +6 -6
- google/genai/_interactions/types/__init__.py +4 -0
- google/genai/_interactions/types/audio_content.py +0 -1
- google/genai/_interactions/types/audio_content_param.py +0 -1
- google/genai/_interactions/types/code_execution_call_content.py +0 -1
- google/genai/_interactions/types/code_execution_call_content_param.py +0 -1
- google/genai/_interactions/types/code_execution_result_content.py +0 -1
- google/genai/_interactions/types/code_execution_result_content_param.py +0 -1
- google/genai/_interactions/types/content.py +63 -0
- google/genai/_interactions/types/content_delta.py +7 -23
- google/genai/_interactions/types/content_param.py +61 -0
- google/genai/_interactions/types/content_start.py +4 -44
- google/genai/_interactions/types/deep_research_agent_config.py +0 -1
- google/genai/_interactions/types/deep_research_agent_config_param.py +0 -1
- google/genai/_interactions/types/document_content.py +3 -2
- google/genai/_interactions/types/document_content_param.py +3 -2
- google/genai/_interactions/types/document_mime_type.py +23 -0
- google/genai/_interactions/types/document_mime_type_param.py +25 -0
- google/genai/_interactions/types/dynamic_agent_config.py +0 -1
- google/genai/_interactions/types/dynamic_agent_config_param.py +0 -1
- google/genai/_interactions/types/file_search_result_content.py +0 -1
- google/genai/_interactions/types/file_search_result_content_param.py +0 -1
- google/genai/_interactions/types/function_call_content.py +0 -1
- google/genai/_interactions/types/function_call_content_param.py +0 -1
- google/genai/_interactions/types/function_result_content.py +1 -2
- google/genai/_interactions/types/function_result_content_param.py +1 -2
- google/genai/_interactions/types/google_search_call_content.py +0 -1
- google/genai/_interactions/types/google_search_call_content_param.py +0 -1
- google/genai/_interactions/types/google_search_result_content.py +0 -1
- google/genai/_interactions/types/google_search_result_content_param.py +0 -1
- google/genai/_interactions/types/image_content.py +1 -2
- google/genai/_interactions/types/image_content_param.py +1 -2
- google/genai/_interactions/types/interaction.py +4 -52
- google/genai/_interactions/types/interaction_create_params.py +2 -22
- google/genai/_interactions/types/mcp_server_tool_call_content.py +0 -1
- google/genai/_interactions/types/mcp_server_tool_call_content_param.py +0 -1
- google/genai/_interactions/types/mcp_server_tool_result_content.py +1 -2
- google/genai/_interactions/types/mcp_server_tool_result_content_param.py +1 -2
- google/genai/_interactions/types/model.py +1 -0
- google/genai/_interactions/types/model_param.py +1 -0
- google/genai/_interactions/types/text_content.py +0 -1
- google/genai/_interactions/types/text_content_param.py +0 -1
- google/genai/_interactions/types/thinking_level.py +1 -1
- google/genai/_interactions/types/thought_content.py +0 -1
- google/genai/_interactions/types/thought_content_param.py +0 -1
- google/genai/_interactions/types/turn.py +3 -44
- google/genai/_interactions/types/turn_param.py +4 -40
- google/genai/_interactions/types/url_context_call_content.py +0 -1
- google/genai/_interactions/types/url_context_call_content_param.py +0 -1
- google/genai/_interactions/types/url_context_result_content.py +0 -1
- google/genai/_interactions/types/url_context_result_content_param.py +0 -1
- google/genai/_interactions/types/usage.py +1 -1
- google/genai/_interactions/types/usage_param.py +1 -1
- google/genai/_interactions/types/video_content.py +1 -2
- google/genai/_interactions/types/video_content_param.py +1 -2
- google/genai/_live_converters.py +36 -64
- google/genai/_local_tokenizer_loader.py +1 -0
- google/genai/_tokens_converters.py +14 -14
- google/genai/batches.py +27 -22
- google/genai/caches.py +42 -42
- google/genai/chats.py +0 -2
- google/genai/client.py +3 -1
- google/genai/files.py +224 -0
- google/genai/models.py +57 -72
- google/genai/tests/chats/test_get_history.py +9 -8
- google/genai/tests/chats/test_validate_response.py +1 -1
- google/genai/tests/client/test_client_requests.py +1 -135
- google/genai/tests/files/test_register.py +272 -0
- google/genai/tests/files/test_register_table.py +70 -0
- google/genai/tests/interactions/test_auth.py +479 -0
- google/genai/tests/interactions/test_integration.py +2 -0
- google/genai/tests/interactions/test_paths.py +105 -0
- google/genai/tests/live/test_live.py +2 -36
- google/genai/tests/local_tokenizer/test_local_tokenizer.py +1 -1
- google/genai/tests/models/test_function_call_streaming.py +90 -90
- google/genai/tests/models/test_generate_content.py +1 -2
- google/genai/tests/models/test_recontext_image.py +1 -1
- google/genai/tests/pytest_helper.py +17 -0
- google/genai/tunings.py +1 -27
- google/genai/types.py +603 -518
- google/genai/version.py +1 -1
- {google_genai-1.55.0.dist-info → google_genai-1.57.0.dist-info}/METADATA +224 -22
- {google_genai-1.55.0.dist-info → google_genai-1.57.0.dist-info}/RECORD +88 -80
- {google_genai-1.55.0.dist-info → google_genai-1.57.0.dist-info}/WHEEL +0 -0
- {google_genai-1.55.0.dist-info → google_genai-1.57.0.dist-info}/licenses/LICENSE +0 -0
- {google_genai-1.55.0.dist-info → google_genai-1.57.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,479 @@
|
|
|
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
|
+
|
|
17
|
+
"""Tests for Interactions API."""
|
|
18
|
+
|
|
19
|
+
from ... import Client
|
|
20
|
+
from ... import _base_url
|
|
21
|
+
from unittest import mock
|
|
22
|
+
import pytest
|
|
23
|
+
from httpx import Request, Response
|
|
24
|
+
from ..._api_client import AsyncHttpxClient, BaseApiClient
|
|
25
|
+
from httpx import Client as HTTPClient
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
ENV_VARS = [
|
|
29
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
30
|
+
"GEMINI_API_KEY",
|
|
31
|
+
"GOOGLE_API_KEY",
|
|
32
|
+
"GOOGLE_CLOUD_LOCATION",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
@pytest.fixture(autouse=True)
|
|
36
|
+
def clear_env_vars(monkeypatch):
|
|
37
|
+
for var in ENV_VARS:
|
|
38
|
+
monkeypatch.delenv(var, raising=False)
|
|
39
|
+
|
|
40
|
+
def test_interactions_gemini_url(monkeypatch):
|
|
41
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
42
|
+
client = Client()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
with mock.patch.object(HTTPClient, "send") as mock_send:
|
|
46
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
47
|
+
client.interactions.create(
|
|
48
|
+
model='gemini-1.5-flash',
|
|
49
|
+
input='Hello',
|
|
50
|
+
)
|
|
51
|
+
mock_send.assert_called_once()
|
|
52
|
+
request = mock_send.call_args[0][0]
|
|
53
|
+
assert str(request.url).endswith('/v1beta/interactions')
|
|
54
|
+
assert request.headers['x-goog-api-key'] == 'test-api-key'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_interactions_gemini_no_vertex_auth(monkeypatch):
|
|
58
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
59
|
+
client = Client()
|
|
60
|
+
|
|
61
|
+
with (
|
|
62
|
+
mock.patch.object(BaseApiClient, "_access_token") as mock_access_token,
|
|
63
|
+
mock.patch.object(HTTPClient, "send") as mock_send,
|
|
64
|
+
):
|
|
65
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
66
|
+
client.interactions.create(
|
|
67
|
+
model='gemini-1.5-flash',
|
|
68
|
+
input='Hello',
|
|
69
|
+
)
|
|
70
|
+
mock_access_token.assert_not_called()
|
|
71
|
+
|
|
72
|
+
def test_interactions_gemini_retry(monkeypatch):
|
|
73
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
74
|
+
client = Client()
|
|
75
|
+
client._api_client.max_retries = 2
|
|
76
|
+
|
|
77
|
+
with mock.patch.object(HTTPClient, "send") as mock_send:
|
|
78
|
+
mock_send.side_effect = [
|
|
79
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
80
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
81
|
+
Response(200, request=Request('POST', '')),
|
|
82
|
+
]
|
|
83
|
+
client.interactions.create(model='gemini-1.5-flash', input='Hello')
|
|
84
|
+
assert mock_send.call_count == 3
|
|
85
|
+
|
|
86
|
+
def test_interactions_gemini_extra_headers(monkeypatch):
|
|
87
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
88
|
+
client = Client()
|
|
89
|
+
|
|
90
|
+
with mock.patch.object(HTTPClient, "send") as mock_send:
|
|
91
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
92
|
+
client.interactions.create(
|
|
93
|
+
model='gemini-1.5-flash',
|
|
94
|
+
input='Hello',
|
|
95
|
+
extra_headers={'X-Custom-Header': 'TestValue'}
|
|
96
|
+
)
|
|
97
|
+
mock_send.assert_called_once()
|
|
98
|
+
request = mock_send.call_args[0][0]
|
|
99
|
+
assert request.headers['x-custom-header'] == 'TestValue'
|
|
100
|
+
assert request.headers['x-goog-api-key'] == 'test-api-key'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_interactions_vertex_auth_header():
|
|
104
|
+
from ..._api_client import BaseApiClient
|
|
105
|
+
from ..._interactions._base_client import SyncAPIClient
|
|
106
|
+
from httpx import Client as HTTPClient
|
|
107
|
+
|
|
108
|
+
creds = mock.Mock()
|
|
109
|
+
creds.quota_project_id = "test-quota-project"
|
|
110
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
111
|
+
|
|
112
|
+
with (
|
|
113
|
+
mock.patch.object(
|
|
114
|
+
BaseApiClient, "_access_token", return_value='fake-vertex-token'
|
|
115
|
+
) as mock_access_token,
|
|
116
|
+
mock.patch.object(
|
|
117
|
+
HTTPClient, "send",
|
|
118
|
+
return_value=mock.Mock(),
|
|
119
|
+
) as mock_send,
|
|
120
|
+
):
|
|
121
|
+
|
|
122
|
+
response = client.interactions.create(
|
|
123
|
+
model='gemini-2.5-flash',
|
|
124
|
+
input='What is the largest planet in our solar system?',
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
mock_send.assert_called_once()
|
|
128
|
+
mock_access_token.assert_called_once()
|
|
129
|
+
args, kwargs = mock_send.call_args
|
|
130
|
+
headers = args[0].headers
|
|
131
|
+
assert any(
|
|
132
|
+
key == "authorization" and value == 'Bearer fake-vertex-token'
|
|
133
|
+
for key, value in headers.items())
|
|
134
|
+
assert any(
|
|
135
|
+
key == "x-goog-user-project" and value == 'test-quota-project'
|
|
136
|
+
for key, value in headers.items())
|
|
137
|
+
|
|
138
|
+
def test_interactions_vertex_key_no_auth_header():
|
|
139
|
+
from ..._api_client import BaseApiClient
|
|
140
|
+
from httpx import Client as HTTPClient
|
|
141
|
+
|
|
142
|
+
creds = mock.Mock()
|
|
143
|
+
client = Client(vertexai=True, api_key='test-api-key')
|
|
144
|
+
|
|
145
|
+
with (
|
|
146
|
+
mock.patch.object(
|
|
147
|
+
BaseApiClient, "_access_token", return_value='fake-vertex-token'
|
|
148
|
+
) as mock_access_token,
|
|
149
|
+
mock.patch.object(
|
|
150
|
+
HTTPClient, "send",
|
|
151
|
+
return_value=mock.Mock(),
|
|
152
|
+
) as mock_send,
|
|
153
|
+
):
|
|
154
|
+
|
|
155
|
+
response = client.interactions.create(
|
|
156
|
+
model='gemini-2.5-flash',
|
|
157
|
+
input='What is the largest planet in our solar system?',
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
mock_send.assert_called_once()
|
|
161
|
+
mock_access_token.assert_not_called()
|
|
162
|
+
args, kwargs = mock_send.call_args
|
|
163
|
+
headers = args[0].headers
|
|
164
|
+
assert any(
|
|
165
|
+
key == "x-goog-api-key" and value == 'test-api-key'
|
|
166
|
+
for key, value in headers.items())
|
|
167
|
+
|
|
168
|
+
def test_interactions_vertex_url():
|
|
169
|
+
creds = mock.Mock()
|
|
170
|
+
creds.quota_project_id = "test-quota-project"
|
|
171
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
172
|
+
|
|
173
|
+
with mock.patch("httpx.Client.send") as mock_send:
|
|
174
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
175
|
+
client.interactions.create(
|
|
176
|
+
model='gemini-1.5-flash',
|
|
177
|
+
input='Hello',
|
|
178
|
+
)
|
|
179
|
+
mock_send.assert_called_once()
|
|
180
|
+
request = mock_send.call_args[0][0]
|
|
181
|
+
assert str(request.url) == 'https://us-central1-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/us-central1/interactions'
|
|
182
|
+
|
|
183
|
+
def test_interactions_vertex_auth_refresh_on_retry():
|
|
184
|
+
from ..._api_client import BaseApiClient
|
|
185
|
+
from httpx import Client as HTTPClient
|
|
186
|
+
|
|
187
|
+
creds = mock.Mock()
|
|
188
|
+
creds.quota_project_id = "test-quota-project"
|
|
189
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
190
|
+
client._api_client.max_retries = 2
|
|
191
|
+
|
|
192
|
+
token_values = ['token1', 'token2', 'token3']
|
|
193
|
+
token_iter = iter(token_values)
|
|
194
|
+
|
|
195
|
+
def get_token():
|
|
196
|
+
return next(token_iter)
|
|
197
|
+
|
|
198
|
+
with (
|
|
199
|
+
mock.patch.object(BaseApiClient, "_access_token", side_effect=get_token) as mock_access_token,
|
|
200
|
+
mock.patch.object(HTTPClient, "send") as mock_send,
|
|
201
|
+
):
|
|
202
|
+
mock_send.side_effect = [
|
|
203
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
204
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
205
|
+
Response(200, request=Request('POST', '')),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
client.interactions.create(model='gemini-1.5-flash', input='Hello')
|
|
209
|
+
|
|
210
|
+
assert mock_access_token.call_count == 3
|
|
211
|
+
assert mock_send.call_count == 3
|
|
212
|
+
# Check headers of each call
|
|
213
|
+
for i in range(3):
|
|
214
|
+
headers = mock_send.call_args_list[i][0][0].headers
|
|
215
|
+
assert headers['authorization'] == f'Bearer {token_values[i]}'
|
|
216
|
+
|
|
217
|
+
@pytest.mark.xfail(reason="extra_headers don't override default auth")
|
|
218
|
+
def test_interactions_vertex_extra_headers_override():
|
|
219
|
+
from ..._api_client import BaseApiClient
|
|
220
|
+
from httpx import Client as HTTPClient
|
|
221
|
+
|
|
222
|
+
creds = mock.Mock()
|
|
223
|
+
creds.quota_project_id = "test-quota-project"
|
|
224
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
225
|
+
|
|
226
|
+
with (
|
|
227
|
+
mock.patch.object(BaseApiClient, "_access_token", return_value='default-token') as mock_access_token,
|
|
228
|
+
mock.patch.object(HTTPClient, "send") as mock_send,
|
|
229
|
+
):
|
|
230
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
231
|
+
|
|
232
|
+
# Override Authorization
|
|
233
|
+
client.interactions.create(
|
|
234
|
+
model='gemini-1.5-flash',
|
|
235
|
+
input='Hello',
|
|
236
|
+
extra_headers={'Authorization': 'Bearer manual-token'}
|
|
237
|
+
)
|
|
238
|
+
mock_send.assert_called_once()
|
|
239
|
+
headers = mock_send.call_args[0][0].headers
|
|
240
|
+
assert headers['authorization'] == 'Bearer manual-token'
|
|
241
|
+
mock_access_token.assert_not_called() # Should not fetch default token
|
|
242
|
+
|
|
243
|
+
mock_send.reset_mock()
|
|
244
|
+
mock_access_token.reset_mock()
|
|
245
|
+
|
|
246
|
+
# Provide API Key
|
|
247
|
+
client.interactions.create(
|
|
248
|
+
model='gemini-1.5-flash',
|
|
249
|
+
input='Hello',
|
|
250
|
+
extra_headers={'x-goog-api-key': 'manual-key'}
|
|
251
|
+
)
|
|
252
|
+
mock_send.assert_called_once()
|
|
253
|
+
headers = mock_send.call_args[0][0].headers
|
|
254
|
+
assert headers['x-goog-api-key'] == 'manual-key'
|
|
255
|
+
assert 'authorization' not in headers
|
|
256
|
+
mock_access_token.assert_not_called()
|
|
257
|
+
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_async_interactions_gemini_url(monkeypatch):
|
|
260
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
261
|
+
client = Client()
|
|
262
|
+
|
|
263
|
+
with mock.patch.object(AsyncHttpxClient, "send") as mock_send:
|
|
264
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
265
|
+
await client.aio.interactions.create(
|
|
266
|
+
model='gemini-1.5-flash',
|
|
267
|
+
input='Hello',
|
|
268
|
+
)
|
|
269
|
+
mock_send.assert_called_once()
|
|
270
|
+
request = mock_send.call_args[0][0]
|
|
271
|
+
assert str(request.url).endswith('/v1beta/interactions')
|
|
272
|
+
assert request.headers['x-goog-api-key'] == 'test-api-key'
|
|
273
|
+
|
|
274
|
+
@pytest.mark.asyncio
|
|
275
|
+
async def test_async_interactions_gemini_no_vertex_auth(monkeypatch):
|
|
276
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
277
|
+
client = Client()
|
|
278
|
+
|
|
279
|
+
with (
|
|
280
|
+
mock.patch.object(BaseApiClient, "_async_access_token") as mock_access_token,
|
|
281
|
+
mock.patch.object(AsyncHttpxClient, "send") as mock_send,
|
|
282
|
+
):
|
|
283
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
284
|
+
await client.aio.interactions.create(
|
|
285
|
+
model='gemini-1.5-flash',
|
|
286
|
+
input='Hello',
|
|
287
|
+
)
|
|
288
|
+
mock_access_token.assert_not_called()
|
|
289
|
+
|
|
290
|
+
@pytest.mark.asyncio
|
|
291
|
+
async def test_async_interactions_gemini_retry(monkeypatch):
|
|
292
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
293
|
+
client = Client()
|
|
294
|
+
client.aio._api_client.max_retries = 2
|
|
295
|
+
|
|
296
|
+
with mock.patch.object(AsyncHttpxClient, "send") as mock_send:
|
|
297
|
+
mock_send.side_effect = [
|
|
298
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
299
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
300
|
+
Response(200, request=Request('POST', '')),
|
|
301
|
+
]
|
|
302
|
+
await client.aio.interactions.create(model='gemini-1.5-flash', input='Hello')
|
|
303
|
+
assert mock_send.call_count == 3
|
|
304
|
+
|
|
305
|
+
@pytest.mark.asyncio
|
|
306
|
+
async def test_async_interactions_gemini_extra_headers(monkeypatch):
|
|
307
|
+
monkeypatch.setenv('GOOGLE_API_KEY', 'test-api-key')
|
|
308
|
+
client = Client()
|
|
309
|
+
|
|
310
|
+
with mock.patch.object(AsyncHttpxClient, "send") as mock_send:
|
|
311
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
312
|
+
await client.aio.interactions.create(
|
|
313
|
+
model='gemini-1.5-flash',
|
|
314
|
+
input='Hello',
|
|
315
|
+
extra_headers={'X-Custom-Header': 'TestValue'}
|
|
316
|
+
)
|
|
317
|
+
mock_send.assert_called_once()
|
|
318
|
+
request = mock_send.call_args[0][0]
|
|
319
|
+
assert request.headers['x-custom-header'] == 'TestValue'
|
|
320
|
+
assert request.headers['x-goog-api-key'] == 'test-api-key'
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_async_interactions_vertex_auth_header():
|
|
324
|
+
from ..._api_client import BaseApiClient
|
|
325
|
+
from ..._interactions._base_client import SyncAPIClient
|
|
326
|
+
from ..._api_client import AsyncHttpxClient
|
|
327
|
+
|
|
328
|
+
creds = mock.Mock()
|
|
329
|
+
creds.quota_project_id = "test-quota-project"
|
|
330
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
331
|
+
|
|
332
|
+
with (
|
|
333
|
+
mock.patch.object(
|
|
334
|
+
BaseApiClient, "_async_access_token", return_value='fake-vertex-token'
|
|
335
|
+
) as mock_access_token,
|
|
336
|
+
mock.patch.object(
|
|
337
|
+
AsyncHttpxClient, "send",
|
|
338
|
+
return_value=mock.Mock(),
|
|
339
|
+
) as mock_send,
|
|
340
|
+
):
|
|
341
|
+
|
|
342
|
+
response = await client.aio.interactions.create(
|
|
343
|
+
model='gemini-2.5-flash',
|
|
344
|
+
input='What is the largest planet in our solar system?',
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
mock_send.assert_called_once()
|
|
348
|
+
mock_access_token.assert_called_once()
|
|
349
|
+
args, kwargs = mock_send.call_args
|
|
350
|
+
headers = args[0].headers
|
|
351
|
+
assert any(
|
|
352
|
+
key == "authorization" and value == 'Bearer fake-vertex-token'
|
|
353
|
+
for key, value in headers.items())
|
|
354
|
+
assert any(
|
|
355
|
+
key == "x-goog-user-project" and value == 'test-quota-project'
|
|
356
|
+
for key, value in headers.items())
|
|
357
|
+
|
|
358
|
+
@pytest.mark.asyncio
|
|
359
|
+
async def test_async_interactions_vertex_key_no_auth_header():
|
|
360
|
+
from ..._api_client import BaseApiClient
|
|
361
|
+
from ..._api_client import AsyncHttpxClient
|
|
362
|
+
creds = mock.Mock()
|
|
363
|
+
client = Client(vertexai=True, api_key='test-api-key')
|
|
364
|
+
|
|
365
|
+
with (
|
|
366
|
+
mock.patch.object(
|
|
367
|
+
BaseApiClient, "_async_access_token", return_value='fake-vertex-token'
|
|
368
|
+
) as mock_access_token,
|
|
369
|
+
mock.patch.object(
|
|
370
|
+
AsyncHttpxClient, "send",
|
|
371
|
+
return_value=mock.Mock(),
|
|
372
|
+
) as mock_send,
|
|
373
|
+
):
|
|
374
|
+
|
|
375
|
+
response = await client.aio.interactions.create(
|
|
376
|
+
model='gemini-2.5-flash',
|
|
377
|
+
input='What is the largest planet in our solar system?',
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
mock_send.assert_called_once()
|
|
381
|
+
mock_access_token.assert_not_called()
|
|
382
|
+
args, kwargs = mock_send.call_args
|
|
383
|
+
headers = args[0].headers
|
|
384
|
+
assert any(
|
|
385
|
+
key == "x-goog-api-key" and value == 'test-api-key'
|
|
386
|
+
for key, value in headers.items())
|
|
387
|
+
|
|
388
|
+
@pytest.mark.asyncio
|
|
389
|
+
async def test_async_interactions_vertex_url():
|
|
390
|
+
from ..._api_client import AsyncHttpxClient
|
|
391
|
+
creds = mock.Mock()
|
|
392
|
+
creds.quota_project_id = "test-quota-project"
|
|
393
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
394
|
+
|
|
395
|
+
with mock.patch.object(AsyncHttpxClient, "send") as mock_send:
|
|
396
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
397
|
+
await client.aio.interactions.create(
|
|
398
|
+
model='gemini-1.5-flash',
|
|
399
|
+
input='Hello',
|
|
400
|
+
)
|
|
401
|
+
mock_send.assert_called_once()
|
|
402
|
+
request = mock_send.call_args[0][0]
|
|
403
|
+
assert str(request.url) == 'https://us-central1-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/us-central1/interactions'
|
|
404
|
+
|
|
405
|
+
@pytest.mark.asyncio
|
|
406
|
+
async def test_async_interactions_vertex_auth_refresh_on_retry():
|
|
407
|
+
from ..._api_client import BaseApiClient
|
|
408
|
+
from ..._api_client import AsyncHttpxClient
|
|
409
|
+
|
|
410
|
+
creds = mock.Mock()
|
|
411
|
+
creds.quota_project_id = "test-quota-project"
|
|
412
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
413
|
+
client.aio._api_client.max_retries = 2
|
|
414
|
+
|
|
415
|
+
token_values = ['token1', 'token2', 'token3']
|
|
416
|
+
token_iter = iter(token_values)
|
|
417
|
+
|
|
418
|
+
async def get_token():
|
|
419
|
+
return next(token_iter)
|
|
420
|
+
|
|
421
|
+
with (
|
|
422
|
+
mock.patch.object(BaseApiClient, "_async_access_token", side_effect=get_token) as mock_access_token,
|
|
423
|
+
mock.patch.object(AsyncHttpxClient, "send") as mock_send,
|
|
424
|
+
):
|
|
425
|
+
mock_send.side_effect = [
|
|
426
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
427
|
+
Response(500, request=Request('POST', ''), headers={"retry-after-ms": "1"}),
|
|
428
|
+
Response(200, request=Request('POST', '')),
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
await client.aio.interactions.create(model='gemini-1.5-flash', input='Hello')
|
|
432
|
+
|
|
433
|
+
assert mock_access_token.call_count == 3
|
|
434
|
+
assert mock_send.call_count == 3
|
|
435
|
+
for i in range(3):
|
|
436
|
+
headers = mock_send.call_args_list[i][0][0].headers
|
|
437
|
+
assert headers['authorization'] == f'Bearer {token_values[i]}'
|
|
438
|
+
|
|
439
|
+
@pytest.mark.xfail(reason="extra_headers don't override default auth")
|
|
440
|
+
@pytest.mark.asyncio
|
|
441
|
+
async def test_async_interactions_vertex_extra_headers_override():
|
|
442
|
+
from ..._api_client import BaseApiClient
|
|
443
|
+
from ..._api_client import AsyncHttpxClient
|
|
444
|
+
|
|
445
|
+
creds = mock.Mock()
|
|
446
|
+
creds.quota_project_id = "test-quota-project"
|
|
447
|
+
client = Client(vertexai=True, project='test-project', location='us-central1', credentials=creds)
|
|
448
|
+
|
|
449
|
+
with (
|
|
450
|
+
mock.patch.object(BaseApiClient, "_async_access_token", return_value='default-token') as mock_access_token,
|
|
451
|
+
mock.patch.object(AsyncHttpxClient, "send") as mock_send,
|
|
452
|
+
):
|
|
453
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
454
|
+
|
|
455
|
+
# Override Authorization
|
|
456
|
+
await client.aio.interactions.create(
|
|
457
|
+
model='gemini-1.5-flash',
|
|
458
|
+
input='Hello',
|
|
459
|
+
extra_headers={'Authorization': 'Bearer manual-token'}
|
|
460
|
+
)
|
|
461
|
+
mock_send.assert_called_once()
|
|
462
|
+
headers = mock_send.call_args[0][0].headers
|
|
463
|
+
assert headers['authorization'] == 'Bearer manual-token'
|
|
464
|
+
mock_access_token.assert_not_called()
|
|
465
|
+
|
|
466
|
+
mock_send.reset_mock()
|
|
467
|
+
mock_access_token.reset_mock()
|
|
468
|
+
|
|
469
|
+
# Provide API Key
|
|
470
|
+
await client.aio.interactions.create(
|
|
471
|
+
model='gemini-1.5-flash',
|
|
472
|
+
input='Hello',
|
|
473
|
+
extra_headers={'x-goog-api-key': 'manual-key'}
|
|
474
|
+
)
|
|
475
|
+
mock_send.assert_called_once()
|
|
476
|
+
headers = mock_send.call_args[0][0].headers
|
|
477
|
+
assert headers['x-goog-api-key'] == 'manual-key'
|
|
478
|
+
assert 'authorization' not in headers
|
|
479
|
+
mock_access_token.assert_not_called()
|
|
@@ -50,6 +50,7 @@ def test_client_timeout():
|
|
|
50
50
|
mock_nextgen_client.assert_called_once_with(
|
|
51
51
|
base_url=mock.ANY,
|
|
52
52
|
api_key="placeholder",
|
|
53
|
+
api_version="v1alpha",
|
|
53
54
|
default_headers=mock.ANY,
|
|
54
55
|
http_client=mock.ANY,
|
|
55
56
|
timeout=5.0,
|
|
@@ -73,6 +74,7 @@ async def test_async_client_timeout():
|
|
|
73
74
|
mock_nextgen_client.assert_called_once_with(
|
|
74
75
|
base_url=mock.ANY,
|
|
75
76
|
api_key="placeholder",
|
|
77
|
+
api_version="v1alpha",
|
|
76
78
|
default_headers=mock.ANY,
|
|
77
79
|
http_client=mock.ANY,
|
|
78
80
|
timeout=5.0,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
|
|
2
|
+
# Copyright 2025 Google LLC
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
"""Tests for Interactions API URL paths."""
|
|
19
|
+
|
|
20
|
+
from unittest import mock
|
|
21
|
+
import pytest
|
|
22
|
+
from httpx import Request, Response
|
|
23
|
+
from ..._api_client import AsyncHttpxClient
|
|
24
|
+
from httpx import Client as HTTPClient
|
|
25
|
+
from .. import pytest_helper
|
|
26
|
+
import google.auth
|
|
27
|
+
|
|
28
|
+
@mock.patch.object(google.auth, "default", autospec=True)
|
|
29
|
+
def test_interactions_paths(mock_auth_default, client):
|
|
30
|
+
interaction_id = "test-interaction-id"
|
|
31
|
+
|
|
32
|
+
mock_creds = mock.Mock()
|
|
33
|
+
mock_creds.token = "test-token"
|
|
34
|
+
mock_creds.expired = False
|
|
35
|
+
mock_creds.quota_project_id = "test-quota-project"
|
|
36
|
+
mock_auth_default.return_value = (mock_creds, "test-project")
|
|
37
|
+
|
|
38
|
+
if client._api_client.vertexai:
|
|
39
|
+
expected_base_url = f'https://{client._api_client.location}-aiplatform.googleapis.com/v1beta1/projects/{client._api_client.project}/locations/{client._api_client.location}'
|
|
40
|
+
else:
|
|
41
|
+
expected_base_url = "https://generativelanguage.googleapis.com/v1beta"
|
|
42
|
+
|
|
43
|
+
with mock.patch.object(HTTPClient, "send") as mock_send:
|
|
44
|
+
mock_send.return_value = Response(200, request=Request('GET', ''))
|
|
45
|
+
client.interactions.get(id=interaction_id)
|
|
46
|
+
mock_send.assert_called_once()
|
|
47
|
+
request = mock_send.call_args[0][0]
|
|
48
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}'
|
|
49
|
+
|
|
50
|
+
mock_send.reset_mock()
|
|
51
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
52
|
+
client.interactions.cancel(id=interaction_id)
|
|
53
|
+
mock_send.assert_called_once()
|
|
54
|
+
request = mock_send.call_args[0][0]
|
|
55
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}/cancel'
|
|
56
|
+
|
|
57
|
+
mock_send.reset_mock()
|
|
58
|
+
mock_send.return_value = Response(200, request=Request('DELETE', ''))
|
|
59
|
+
client.interactions.delete(id=interaction_id)
|
|
60
|
+
mock_send.assert_called_once()
|
|
61
|
+
request = mock_send.call_args[0][0]
|
|
62
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}'
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
@mock.patch.object(google.auth, "default", autospec=True)
|
|
66
|
+
async def test_async_interactions_paths(mock_auth_default, client):
|
|
67
|
+
interaction_id = "test-interaction-id"
|
|
68
|
+
|
|
69
|
+
mock_creds = mock.Mock()
|
|
70
|
+
mock_creds.token = "test-token"
|
|
71
|
+
mock_creds.expired = False
|
|
72
|
+
mock_creds.quota_project_id = "test-quota-project"
|
|
73
|
+
mock_auth_default.return_value = (mock_creds, "test-project")
|
|
74
|
+
|
|
75
|
+
if client._api_client.vertexai:
|
|
76
|
+
expected_base_url = f'https://{client._api_client.location}-aiplatform.googleapis.com/v1beta1/projects/{client._api_client.project}/locations/{client._api_client.location}'
|
|
77
|
+
else:
|
|
78
|
+
expected_base_url = "https://generativelanguage.googleapis.com/v1beta"
|
|
79
|
+
|
|
80
|
+
with mock.patch.object(AsyncHttpxClient, "send") as mock_send:
|
|
81
|
+
mock_send.return_value = Response(200, request=Request('GET', ''))
|
|
82
|
+
await client.aio.interactions.get(id=interaction_id)
|
|
83
|
+
mock_send.assert_called_once()
|
|
84
|
+
request = mock_send.call_args[0][0]
|
|
85
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}'
|
|
86
|
+
|
|
87
|
+
mock_send.reset_mock()
|
|
88
|
+
mock_send.return_value = Response(200, request=Request('POST', ''))
|
|
89
|
+
await client.aio.interactions.cancel(id=interaction_id)
|
|
90
|
+
mock_send.assert_called_once()
|
|
91
|
+
request = mock_send.call_args[0][0]
|
|
92
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}/cancel'
|
|
93
|
+
|
|
94
|
+
mock_send.reset_mock()
|
|
95
|
+
mock_send.return_value = Response(200, request=Request('DELETE', ''))
|
|
96
|
+
await client.aio.interactions.delete(id=interaction_id)
|
|
97
|
+
mock_send.assert_called_once()
|
|
98
|
+
request = mock_send.call_args[0][0]
|
|
99
|
+
assert str(request.url) == f'{expected_base_url}/interactions/{interaction_id}'
|
|
100
|
+
|
|
101
|
+
pytestmark = pytest_helper.setup(
|
|
102
|
+
file=__file__,
|
|
103
|
+
globals_for_file=globals(),
|
|
104
|
+
test_table=[],
|
|
105
|
+
)
|
|
@@ -839,42 +839,6 @@ async def test_bidi_setup_error_if_multispeaker_voice_config(vertexai):
|
|
|
839
839
|
)
|
|
840
840
|
|
|
841
841
|
|
|
842
|
-
@pytest.mark.parametrize('vertexai', [True, False])
|
|
843
|
-
@pytest.mark.asyncio
|
|
844
|
-
async def test_replicated_voice_config(vertexai):
|
|
845
|
-
# Config is a dict
|
|
846
|
-
config_dict = {
|
|
847
|
-
'speech_config': {
|
|
848
|
-
'voice_config': {
|
|
849
|
-
'replicated_voice_config': {
|
|
850
|
-
'mime_type': 'audio/pcm',
|
|
851
|
-
'voice_sample_audio': bytes([0, 0, 0]),
|
|
852
|
-
},
|
|
853
|
-
},
|
|
854
|
-
},
|
|
855
|
-
}
|
|
856
|
-
result = await get_connect_message(
|
|
857
|
-
mock_api_client(vertexai=vertexai),
|
|
858
|
-
model='test_model',
|
|
859
|
-
config=config_dict,
|
|
860
|
-
)
|
|
861
|
-
if vertexai:
|
|
862
|
-
try:
|
|
863
|
-
replicated_voice_config = result['setup']['generationConfig'][
|
|
864
|
-
'speechConfig'
|
|
865
|
-
]['voiceConfig']['replicatedVoiceConfig']
|
|
866
|
-
except KeyError:
|
|
867
|
-
replicated_voice_config = result['setup']['generationConfig'][
|
|
868
|
-
'speechConfig'
|
|
869
|
-
]['voiceConfig']['replicated_voice_config']
|
|
870
|
-
assert replicated_voice_config == {
|
|
871
|
-
'mime_type': 'audio/pcm',
|
|
872
|
-
'voice_sample_audio': 'AAAA',
|
|
873
|
-
}
|
|
874
|
-
else:
|
|
875
|
-
return
|
|
876
|
-
|
|
877
|
-
|
|
878
842
|
@pytest.mark.parametrize('vertexai', [True, False])
|
|
879
843
|
@pytest.mark.asyncio
|
|
880
844
|
async def test_explicit_vad(vertexai):
|
|
@@ -1690,6 +1654,8 @@ async def test_bidi_setup_to_api_with_thinking_config(vertexai):
|
|
|
1690
1654
|
result = await get_connect_message(
|
|
1691
1655
|
mock_api_client(vertexai=vertexai), model='test_model', config=config_dict
|
|
1692
1656
|
)
|
|
1657
|
+
result = pytest_helper.camel_to_snake_all_keys(result)
|
|
1658
|
+
expected_result = pytest_helper.camel_to_snake_all_keys(expected_result)
|
|
1693
1659
|
assert result == expected_result
|
|
1694
1660
|
|
|
1695
1661
|
|
|
@@ -37,7 +37,7 @@ class TestLocalTokenizer(unittest.TestCase):
|
|
|
37
37
|
self.mock_tokenizer = MagicMock()
|
|
38
38
|
self.mock_get_sentencepiece.return_value = self.mock_tokenizer
|
|
39
39
|
|
|
40
|
-
self.tokenizer = local_tokenizer.LocalTokenizer(model_name='gemini-
|
|
40
|
+
self.tokenizer = local_tokenizer.LocalTokenizer(model_name='gemini-3-pro-preview')
|
|
41
41
|
|
|
42
42
|
def tearDown(self):
|
|
43
43
|
patch.stopall()
|