google-genai 1.54.0__py3-none-any.whl → 1.55.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/__init__.py +1 -0
- google/genai/_interactions/__init__.py +117 -0
- google/genai/_interactions/_base_client.py +2019 -0
- google/genai/_interactions/_client.py +511 -0
- google/genai/_interactions/_compat.py +234 -0
- google/genai/_interactions/_constants.py +29 -0
- google/genai/_interactions/_exceptions.py +122 -0
- google/genai/_interactions/_files.py +139 -0
- google/genai/_interactions/_models.py +873 -0
- google/genai/_interactions/_qs.py +165 -0
- google/genai/_interactions/_resource.py +58 -0
- google/genai/_interactions/_response.py +847 -0
- google/genai/_interactions/_streaming.py +354 -0
- google/genai/_interactions/_types.py +276 -0
- google/genai/_interactions/_utils/__init__.py +79 -0
- google/genai/_interactions/_utils/_compat.py +61 -0
- google/genai/_interactions/_utils/_datetime_parse.py +151 -0
- google/genai/_interactions/_utils/_logs.py +40 -0
- google/genai/_interactions/_utils/_proxy.py +80 -0
- google/genai/_interactions/_utils/_reflection.py +57 -0
- google/genai/_interactions/_utils/_resources_proxy.py +39 -0
- google/genai/_interactions/_utils/_streams.py +27 -0
- google/genai/_interactions/_utils/_sync.py +73 -0
- google/genai/_interactions/_utils/_transform.py +472 -0
- google/genai/_interactions/_utils/_typing.py +172 -0
- google/genai/_interactions/_utils/_utils.py +437 -0
- google/genai/_interactions/_version.py +18 -0
- google/genai/_interactions/resources/__init__.py +34 -0
- google/genai/_interactions/resources/interactions.py +1350 -0
- google/genai/_interactions/types/__init__.py +107 -0
- google/genai/_interactions/types/allowed_tools.py +33 -0
- google/genai/_interactions/types/allowed_tools_param.py +35 -0
- google/genai/_interactions/types/annotation.py +42 -0
- google/genai/_interactions/types/annotation_param.py +42 -0
- google/genai/_interactions/types/audio_content.py +38 -0
- google/genai/_interactions/types/audio_content_param.py +45 -0
- google/genai/_interactions/types/audio_mime_type.py +25 -0
- google/genai/_interactions/types/audio_mime_type_param.py +27 -0
- google/genai/_interactions/types/code_execution_call_arguments.py +33 -0
- google/genai/_interactions/types/code_execution_call_arguments_param.py +32 -0
- google/genai/_interactions/types/code_execution_call_content.py +37 -0
- google/genai/_interactions/types/code_execution_call_content_param.py +37 -0
- google/genai/_interactions/types/code_execution_result_content.py +42 -0
- google/genai/_interactions/types/code_execution_result_content_param.py +41 -0
- google/genai/_interactions/types/content_delta.py +358 -0
- google/genai/_interactions/types/content_start.py +79 -0
- google/genai/_interactions/types/content_stop.py +35 -0
- google/genai/_interactions/types/deep_research_agent_config.py +33 -0
- google/genai/_interactions/types/deep_research_agent_config_param.py +32 -0
- google/genai/_interactions/types/document_content.py +36 -0
- google/genai/_interactions/types/document_content_param.py +43 -0
- google/genai/_interactions/types/dynamic_agent_config.py +44 -0
- google/genai/_interactions/types/dynamic_agent_config_param.py +33 -0
- google/genai/_interactions/types/error_event.py +46 -0
- google/genai/_interactions/types/file_search_result_content.py +46 -0
- google/genai/_interactions/types/file_search_result_content_param.py +46 -0
- google/genai/_interactions/types/function.py +38 -0
- google/genai/_interactions/types/function_call_content.py +39 -0
- google/genai/_interactions/types/function_call_content_param.py +39 -0
- google/genai/_interactions/types/function_param.py +37 -0
- google/genai/_interactions/types/function_result_content.py +52 -0
- google/genai/_interactions/types/function_result_content_param.py +54 -0
- google/genai/_interactions/types/generation_config.py +57 -0
- google/genai/_interactions/types/generation_config_param.py +59 -0
- google/genai/_interactions/types/google_search_call_arguments.py +29 -0
- google/genai/_interactions/types/google_search_call_arguments_param.py +31 -0
- google/genai/_interactions/types/google_search_call_content.py +37 -0
- google/genai/_interactions/types/google_search_call_content_param.py +37 -0
- google/genai/_interactions/types/google_search_result.py +35 -0
- google/genai/_interactions/types/google_search_result_content.py +43 -0
- google/genai/_interactions/types/google_search_result_content_param.py +44 -0
- google/genai/_interactions/types/google_search_result_param.py +35 -0
- google/genai/_interactions/types/image_content.py +41 -0
- google/genai/_interactions/types/image_content_param.py +48 -0
- google/genai/_interactions/types/image_mime_type.py +23 -0
- google/genai/_interactions/types/image_mime_type_param.py +25 -0
- google/genai/_interactions/types/interaction.py +165 -0
- google/genai/_interactions/types/interaction_create_params.py +212 -0
- google/genai/_interactions/types/interaction_event.py +37 -0
- google/genai/_interactions/types/interaction_get_params.py +46 -0
- google/genai/_interactions/types/interaction_sse_event.py +32 -0
- google/genai/_interactions/types/interaction_status_update.py +37 -0
- google/genai/_interactions/types/mcp_server_tool_call_content.py +42 -0
- google/genai/_interactions/types/mcp_server_tool_call_content_param.py +42 -0
- google/genai/_interactions/types/mcp_server_tool_result_content.py +52 -0
- google/genai/_interactions/types/mcp_server_tool_result_content_param.py +54 -0
- google/genai/_interactions/types/model.py +36 -0
- google/genai/_interactions/types/model_param.py +38 -0
- google/genai/_interactions/types/speech_config.py +35 -0
- google/genai/_interactions/types/speech_config_param.py +35 -0
- google/genai/_interactions/types/text_content.py +37 -0
- google/genai/_interactions/types/text_content_param.py +38 -0
- google/genai/_interactions/types/thinking_level.py +22 -0
- google/genai/_interactions/types/thought_content.py +41 -0
- google/genai/_interactions/types/thought_content_param.py +47 -0
- google/genai/_interactions/types/tool.py +100 -0
- google/genai/_interactions/types/tool_choice.py +26 -0
- google/genai/_interactions/types/tool_choice_config.py +28 -0
- google/genai/_interactions/types/tool_choice_config_param.py +29 -0
- google/genai/_interactions/types/tool_choice_param.py +28 -0
- google/genai/_interactions/types/tool_choice_type.py +22 -0
- google/genai/_interactions/types/tool_param.py +97 -0
- google/genai/_interactions/types/turn.py +76 -0
- google/genai/_interactions/types/turn_param.py +73 -0
- google/genai/_interactions/types/url_context_call_arguments.py +29 -0
- google/genai/_interactions/types/url_context_call_arguments_param.py +31 -0
- google/genai/_interactions/types/url_context_call_content.py +37 -0
- google/genai/_interactions/types/url_context_call_content_param.py +37 -0
- google/genai/_interactions/types/url_context_result.py +33 -0
- google/genai/_interactions/types/url_context_result_content.py +43 -0
- google/genai/_interactions/types/url_context_result_content_param.py +44 -0
- google/genai/_interactions/types/url_context_result_param.py +32 -0
- google/genai/_interactions/types/usage.py +106 -0
- google/genai/_interactions/types/usage_param.py +106 -0
- google/genai/_interactions/types/video_content.py +41 -0
- google/genai/_interactions/types/video_content_param.py +48 -0
- google/genai/_interactions/types/video_mime_type.py +36 -0
- google/genai/_interactions/types/video_mime_type_param.py +38 -0
- google/genai/_live_converters.py +31 -0
- google/genai/_tokens_converters.py +5 -0
- google/genai/batches.py +7 -0
- google/genai/client.py +223 -0
- google/genai/interactions.py +17 -0
- google/genai/live.py +4 -3
- google/genai/models.py +12 -0
- google/genai/tests/__init__.py +21 -0
- google/genai/tests/afc/__init__.py +21 -0
- google/genai/tests/afc/test_convert_if_exist_pydantic_model.py +309 -0
- google/genai/tests/afc/test_convert_number_values_for_function_call_args.py +63 -0
- google/genai/tests/afc/test_find_afc_incompatible_tool_indexes.py +240 -0
- google/genai/tests/afc/test_generate_content_stream_afc.py +530 -0
- google/genai/tests/afc/test_generate_content_stream_afc_thoughts.py +77 -0
- google/genai/tests/afc/test_get_function_map.py +176 -0
- google/genai/tests/afc/test_get_function_response_parts.py +277 -0
- google/genai/tests/afc/test_get_max_remote_calls_for_afc.py +130 -0
- google/genai/tests/afc/test_invoke_function_from_dict_args.py +241 -0
- google/genai/tests/afc/test_raise_error_for_afc_incompatible_config.py +159 -0
- google/genai/tests/afc/test_should_append_afc_history.py +53 -0
- google/genai/tests/afc/test_should_disable_afc.py +214 -0
- google/genai/tests/batches/__init__.py +17 -0
- google/genai/tests/batches/test_cancel.py +77 -0
- google/genai/tests/batches/test_create.py +78 -0
- google/genai/tests/batches/test_create_with_bigquery.py +113 -0
- google/genai/tests/batches/test_create_with_file.py +82 -0
- google/genai/tests/batches/test_create_with_gcs.py +125 -0
- google/genai/tests/batches/test_create_with_inlined_requests.py +255 -0
- google/genai/tests/batches/test_delete.py +86 -0
- google/genai/tests/batches/test_embedding.py +157 -0
- google/genai/tests/batches/test_get.py +78 -0
- google/genai/tests/batches/test_list.py +79 -0
- google/genai/tests/caches/__init__.py +17 -0
- google/genai/tests/caches/constants.py +29 -0
- google/genai/tests/caches/test_create.py +210 -0
- google/genai/tests/caches/test_create_custom_url.py +105 -0
- google/genai/tests/caches/test_delete.py +54 -0
- google/genai/tests/caches/test_delete_custom_url.py +52 -0
- google/genai/tests/caches/test_get.py +94 -0
- google/genai/tests/caches/test_get_custom_url.py +52 -0
- google/genai/tests/caches/test_list.py +68 -0
- google/genai/tests/caches/test_update.py +70 -0
- google/genai/tests/caches/test_update_custom_url.py +58 -0
- google/genai/tests/chats/__init__.py +1 -0
- google/genai/tests/chats/test_get_history.py +597 -0
- google/genai/tests/chats/test_send_message.py +844 -0
- google/genai/tests/chats/test_validate_response.py +90 -0
- google/genai/tests/client/__init__.py +17 -0
- google/genai/tests/client/test_async_stream.py +427 -0
- google/genai/tests/client/test_client_close.py +197 -0
- google/genai/tests/client/test_client_initialization.py +1687 -0
- google/genai/tests/client/test_client_requests.py +355 -0
- google/genai/tests/client/test_custom_client.py +77 -0
- google/genai/tests/client/test_http_options.py +178 -0
- google/genai/tests/client/test_replay_client_equality.py +168 -0
- google/genai/tests/client/test_retries.py +846 -0
- google/genai/tests/client/test_upload_errors.py +136 -0
- google/genai/tests/common/__init__.py +17 -0
- google/genai/tests/common/test_common.py +954 -0
- google/genai/tests/conftest.py +162 -0
- google/genai/tests/documents/__init__.py +17 -0
- google/genai/tests/documents/test_delete.py +51 -0
- google/genai/tests/documents/test_get.py +85 -0
- google/genai/tests/documents/test_list.py +72 -0
- google/genai/tests/errors/__init__.py +1 -0
- google/genai/tests/errors/test_api_error.py +417 -0
- google/genai/tests/file_search_stores/__init__.py +17 -0
- google/genai/tests/file_search_stores/test_create.py +66 -0
- google/genai/tests/file_search_stores/test_delete.py +64 -0
- google/genai/tests/file_search_stores/test_get.py +94 -0
- google/genai/tests/file_search_stores/test_import_file.py +112 -0
- google/genai/tests/file_search_stores/test_list.py +57 -0
- google/genai/tests/file_search_stores/test_upload_to_file_search_store.py +141 -0
- google/genai/tests/files/__init__.py +17 -0
- google/genai/tests/files/test_delete.py +46 -0
- google/genai/tests/files/test_download.py +85 -0
- google/genai/tests/files/test_get.py +46 -0
- google/genai/tests/files/test_list.py +72 -0
- google/genai/tests/files/test_upload.py +255 -0
- google/genai/tests/imports/test_no_optional_imports.py +28 -0
- google/genai/tests/interactions/__init__.py +0 -0
- google/genai/tests/interactions/test_integration.py +80 -0
- google/genai/tests/live/__init__.py +16 -0
- google/genai/tests/live/test_live.py +2177 -0
- google/genai/tests/live/test_live_music.py +362 -0
- google/genai/tests/live/test_live_response.py +163 -0
- google/genai/tests/live/test_send_client_content.py +147 -0
- google/genai/tests/live/test_send_realtime_input.py +268 -0
- google/genai/tests/live/test_send_tool_response.py +222 -0
- google/genai/tests/local_tokenizer/__init__.py +17 -0
- google/genai/tests/local_tokenizer/test_local_tokenizer.py +343 -0
- google/genai/tests/local_tokenizer/test_local_tokenizer_loader.py +235 -0
- google/genai/tests/mcp/__init__.py +17 -0
- google/genai/tests/mcp/test_has_mcp_tool_usage.py +89 -0
- google/genai/tests/mcp/test_mcp_to_gemini_tools.py +191 -0
- google/genai/tests/mcp/test_parse_config_for_mcp_sessions.py +201 -0
- google/genai/tests/mcp/test_parse_config_for_mcp_usage.py +130 -0
- google/genai/tests/mcp/test_set_mcp_usage_header.py +72 -0
- google/genai/tests/models/__init__.py +17 -0
- google/genai/tests/models/constants.py +8 -0
- google/genai/tests/models/test_compute_tokens.py +120 -0
- google/genai/tests/models/test_count_tokens.py +159 -0
- google/genai/tests/models/test_delete.py +107 -0
- google/genai/tests/models/test_edit_image.py +264 -0
- google/genai/tests/models/test_embed_content.py +94 -0
- google/genai/tests/models/test_function_call_streaming.py +442 -0
- google/genai/tests/models/test_generate_content.py +2502 -0
- google/genai/tests/models/test_generate_content_cached_content.py +132 -0
- google/genai/tests/models/test_generate_content_config_zero_value.py +103 -0
- google/genai/tests/models/test_generate_content_from_apikey.py +44 -0
- google/genai/tests/models/test_generate_content_http_options.py +40 -0
- google/genai/tests/models/test_generate_content_image_generation.py +143 -0
- google/genai/tests/models/test_generate_content_mcp.py +343 -0
- google/genai/tests/models/test_generate_content_media_resolution.py +97 -0
- google/genai/tests/models/test_generate_content_model.py +139 -0
- google/genai/tests/models/test_generate_content_part.py +821 -0
- google/genai/tests/models/test_generate_content_thought.py +76 -0
- google/genai/tests/models/test_generate_content_tools.py +1761 -0
- google/genai/tests/models/test_generate_images.py +191 -0
- google/genai/tests/models/test_generate_videos.py +759 -0
- google/genai/tests/models/test_get.py +104 -0
- google/genai/tests/models/test_list.py +233 -0
- google/genai/tests/models/test_recontext_image.py +189 -0
- google/genai/tests/models/test_segment_image.py +148 -0
- google/genai/tests/models/test_update.py +95 -0
- google/genai/tests/models/test_upscale_image.py +157 -0
- google/genai/tests/operations/__init__.py +17 -0
- google/genai/tests/operations/test_get.py +38 -0
- google/genai/tests/public_samples/__init__.py +17 -0
- google/genai/tests/public_samples/test_gemini_text_only.py +34 -0
- google/genai/tests/pytest_helper.py +229 -0
- google/genai/tests/shared/__init__.py +16 -0
- google/genai/tests/shared/batches/__init__.py +14 -0
- google/genai/tests/shared/batches/test_create_delete.py +57 -0
- google/genai/tests/shared/batches/test_create_get_cancel.py +56 -0
- google/genai/tests/shared/batches/test_list.py +40 -0
- google/genai/tests/shared/caches/__init__.py +14 -0
- google/genai/tests/shared/caches/test_create_get_delete.py +67 -0
- google/genai/tests/shared/caches/test_create_update_get.py +71 -0
- google/genai/tests/shared/caches/test_list.py +40 -0
- google/genai/tests/shared/chats/__init__.py +14 -0
- google/genai/tests/shared/chats/test_send_message.py +48 -0
- google/genai/tests/shared/chats/test_send_message_stream.py +50 -0
- google/genai/tests/shared/files/__init__.py +14 -0
- google/genai/tests/shared/files/test_list.py +41 -0
- google/genai/tests/shared/files/test_upload_get_delete.py +54 -0
- google/genai/tests/shared/models/__init__.py +14 -0
- google/genai/tests/shared/models/test_compute_tokens.py +41 -0
- google/genai/tests/shared/models/test_count_tokens.py +40 -0
- google/genai/tests/shared/models/test_edit_image.py +67 -0
- google/genai/tests/shared/models/test_embed.py +40 -0
- google/genai/tests/shared/models/test_generate_content.py +39 -0
- google/genai/tests/shared/models/test_generate_content_stream.py +54 -0
- google/genai/tests/shared/models/test_generate_images.py +40 -0
- google/genai/tests/shared/models/test_generate_videos.py +38 -0
- google/genai/tests/shared/models/test_list.py +37 -0
- google/genai/tests/shared/models/test_recontext_image.py +55 -0
- google/genai/tests/shared/models/test_segment_image.py +52 -0
- google/genai/tests/shared/models/test_upscale_image.py +52 -0
- google/genai/tests/shared/tunings/__init__.py +16 -0
- google/genai/tests/shared/tunings/test_create.py +46 -0
- google/genai/tests/shared/tunings/test_create_get_cancel.py +56 -0
- google/genai/tests/shared/tunings/test_list.py +39 -0
- google/genai/tests/tokens/__init__.py +16 -0
- google/genai/tests/tokens/test_create.py +154 -0
- google/genai/tests/transformers/__init__.py +17 -0
- google/genai/tests/transformers/test_blobs.py +71 -0
- google/genai/tests/transformers/test_bytes.py +15 -0
- google/genai/tests/transformers/test_duck_type.py +96 -0
- google/genai/tests/transformers/test_function_responses.py +72 -0
- google/genai/tests/transformers/test_schema.py +653 -0
- google/genai/tests/transformers/test_t_batch.py +286 -0
- google/genai/tests/transformers/test_t_content.py +160 -0
- google/genai/tests/transformers/test_t_contents.py +398 -0
- google/genai/tests/transformers/test_t_part.py +85 -0
- google/genai/tests/transformers/test_t_parts.py +87 -0
- google/genai/tests/transformers/test_t_tool.py +157 -0
- google/genai/tests/transformers/test_t_tools.py +195 -0
- google/genai/tests/tunings/__init__.py +16 -0
- google/genai/tests/tunings/test_cancel.py +39 -0
- google/genai/tests/tunings/test_end_to_end.py +106 -0
- google/genai/tests/tunings/test_get.py +67 -0
- google/genai/tests/tunings/test_list.py +75 -0
- google/genai/tests/tunings/test_tune.py +268 -0
- google/genai/tests/types/__init__.py +16 -0
- google/genai/tests/types/test_bytes_internal.py +271 -0
- google/genai/tests/types/test_bytes_type.py +152 -0
- google/genai/tests/types/test_future.py +101 -0
- google/genai/tests/types/test_optional_types.py +36 -0
- google/genai/tests/types/test_part_type.py +616 -0
- google/genai/tests/types/test_schema_from_json_schema.py +417 -0
- google/genai/tests/types/test_schema_json_schema.py +468 -0
- google/genai/tests/types/test_types.py +2903 -0
- google/genai/types.py +72 -0
- google/genai/version.py +1 -1
- {google_genai-1.54.0.dist-info → google_genai-1.55.0.dist-info}/METADATA +3 -1
- google_genai-1.55.0.dist-info/RECORD +345 -0
- google_genai-1.54.0.dist-info/RECORD +0 -41
- {google_genai-1.54.0.dist-info → google_genai-1.55.0.dist-info}/WHEEL +0 -0
- {google_genai-1.54.0.dist-info → google_genai-1.55.0.dist-info}/licenses/LICENSE +0 -0
- {google_genai-1.54.0.dist-info → google_genai-1.55.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
from ... import types
|
|
18
|
+
from ...chats import _validate_response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_validate_response_default_response():
|
|
22
|
+
response = types.GenerateContentResponse()
|
|
23
|
+
|
|
24
|
+
assert not _validate_response(response)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_validate_response_empty_content():
|
|
28
|
+
response = types.GenerateContentResponse(candidates=[])
|
|
29
|
+
|
|
30
|
+
assert not _validate_response(response)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_validate_response_empty_parts():
|
|
34
|
+
response = types.GenerateContentResponse(
|
|
35
|
+
candidates=[types.Candidate(content=types.Content(parts=[]))]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert not _validate_response(response)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_validate_response_empty_part():
|
|
42
|
+
response = types.GenerateContentResponse(
|
|
43
|
+
candidates=[types.Candidate(content=types.Content(parts=[types.Part()]))]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert not _validate_response(response)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_validate_response_part_with_empty_text():
|
|
50
|
+
response = types.GenerateContentResponse(
|
|
51
|
+
candidates=[
|
|
52
|
+
types.Candidate(content=types.Content(parts=[types.Part(text='')]))
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert not _validate_response(response)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_validate_response_part_with_text():
|
|
60
|
+
response = types.GenerateContentResponse(
|
|
61
|
+
candidates=[
|
|
62
|
+
types.Candidate(
|
|
63
|
+
content=types.Content(
|
|
64
|
+
parts=[types.Part(text='response from model')]
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert _validate_response(response)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_validate_response_part_with_function_call():
|
|
74
|
+
response = types.GenerateContentResponse(
|
|
75
|
+
candidates=[
|
|
76
|
+
types.Candidate(
|
|
77
|
+
content=types.Content(
|
|
78
|
+
parts=[
|
|
79
|
+
types.Part(
|
|
80
|
+
function_call=types.FunctionCall(
|
|
81
|
+
name='foo', args={'bar': 'baz'}
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert _validate_response(response)
|
|
@@ -0,0 +1,17 @@
|
|
|
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 the Google GenAI SDK."""
|
|
@@ -0,0 +1,427 @@
|
|
|
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
|
+
"""Tests for async stream."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import List
|
|
20
|
+
from unittest import mock
|
|
21
|
+
from unittest.mock import AsyncMock
|
|
22
|
+
from unittest.mock import MagicMock
|
|
23
|
+
from unittest.mock import patch
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import aiohttp
|
|
29
|
+
|
|
30
|
+
AIOHTTP_NOT_INSTALLED = False
|
|
31
|
+
except ImportError:
|
|
32
|
+
AIOHTTP_NOT_INSTALLED = True
|
|
33
|
+
aiohttp = mock.MagicMock()
|
|
34
|
+
|
|
35
|
+
import httpx
|
|
36
|
+
|
|
37
|
+
from ... import _api_client as api_client
|
|
38
|
+
from ... import Client
|
|
39
|
+
from ... import errors
|
|
40
|
+
from ... import types
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
EVENT_STREAM_DATA_WITH_ERROR = [
|
|
44
|
+
b'{"candidates":[{"content":{"parts":[{"text":"test"}],"role":"model"}}]}',
|
|
45
|
+
b'\n',
|
|
46
|
+
b'{"error":{"code":500,"message":"Error","status":"INTERNAL"}}',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MockHTTPXResponse(httpx.Response):
|
|
51
|
+
"""Mock httpx.Response class for testing."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, lines: List[str]):
|
|
54
|
+
self.aiter_lines = MagicMock()
|
|
55
|
+
self.aiter_lines.return_value.__aiter__ = MagicMock(
|
|
56
|
+
return_value=self._async_line_iterator(lines)
|
|
57
|
+
)
|
|
58
|
+
self.aclose = AsyncMock()
|
|
59
|
+
|
|
60
|
+
async def _async_line_iterator(self, lines: List[str]):
|
|
61
|
+
for line in lines:
|
|
62
|
+
yield line
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MockAIOHTTPResponse(aiohttp.ClientResponse):
|
|
66
|
+
|
|
67
|
+
def __init__(self, lines: List[str]):
|
|
68
|
+
self.content = MagicMock()
|
|
69
|
+
self.content.readline = AsyncMock()
|
|
70
|
+
# Simulate reading lines, each ending with newline bytes for readline behavior
|
|
71
|
+
self._read_data = b"\n".join(line.encode("utf-8") for line in lines) + b"\n"
|
|
72
|
+
self._read_pos = 0
|
|
73
|
+
self.content.readline.side_effect = self._async_read_line
|
|
74
|
+
self.release = MagicMock()
|
|
75
|
+
|
|
76
|
+
async def _async_read_line(self) -> bytes:
|
|
77
|
+
if self._read_pos >= len(self._read_data):
|
|
78
|
+
return b"" # End of stream
|
|
79
|
+
|
|
80
|
+
newline_pos = self._read_data.find(b"\n", self._read_pos)
|
|
81
|
+
if newline_pos == -1: # Should not happen with the appended '\n'
|
|
82
|
+
line = self._read_data[self._read_pos :]
|
|
83
|
+
self._read_pos = len(self._read_data)
|
|
84
|
+
return line
|
|
85
|
+
else:
|
|
86
|
+
line = self._read_data[self._read_pos : newline_pos + 1]
|
|
87
|
+
self._read_pos = newline_pos + 1
|
|
88
|
+
return line
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.fixture
|
|
92
|
+
def responses() -> api_client.HttpResponse:
|
|
93
|
+
return api_client.HttpResponse(headers={})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
requires_aiohttp = pytest.mark.skipif(
|
|
97
|
+
AIOHTTP_NOT_INSTALLED, reason="aiohttp is not installed, skipping test."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.fixture(autouse=True)
|
|
102
|
+
def reset_has_aiohttp():
|
|
103
|
+
yield
|
|
104
|
+
api_client.has_aiohttp = False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_invalid_response_stream_type(responses: api_client.HttpResponse):
|
|
108
|
+
"""Tests that an invalid response stream type raises an error."""
|
|
109
|
+
api_client.has_aiohttp = False
|
|
110
|
+
with pytest.raises(
|
|
111
|
+
TypeError,
|
|
112
|
+
match=(
|
|
113
|
+
"Expected self.response_stream to be an httpx.Response or"
|
|
114
|
+
" aiohttp.ClientResponse object"
|
|
115
|
+
),
|
|
116
|
+
):
|
|
117
|
+
|
|
118
|
+
async def run():
|
|
119
|
+
async for _ in responses._aiter_response_stream():
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
asyncio.run(run())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_httpx_simple_lines(responses: api_client.HttpResponse):
|
|
127
|
+
lines = ["hello", "world", "testing"]
|
|
128
|
+
mock_response = MockHTTPXResponse(lines)
|
|
129
|
+
responses.response_stream = mock_response
|
|
130
|
+
|
|
131
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
132
|
+
|
|
133
|
+
assert results == lines
|
|
134
|
+
mock_response.aiter_lines.assert_called_once()
|
|
135
|
+
mock_response.aclose.assert_called_once()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_httpx_data_prefix(responses: api_client.HttpResponse):
|
|
140
|
+
lines = ["data: { 'message': 'hello' }", "data: { 'status': 'ok' }"]
|
|
141
|
+
mock_response = MockHTTPXResponse(lines)
|
|
142
|
+
responses.response_stream = mock_response
|
|
143
|
+
|
|
144
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
145
|
+
|
|
146
|
+
assert results == ["{ 'message': 'hello' }", "{ 'status': 'ok' }"]
|
|
147
|
+
mock_response.aiter_lines.assert_called_once()
|
|
148
|
+
mock_response.aclose.assert_called_once()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_httpx_multiple_json_chunk(responses: api_client.HttpResponse):
|
|
153
|
+
lines = [
|
|
154
|
+
'{ "id": 1 }',
|
|
155
|
+
"",
|
|
156
|
+
'data: { "id": 2 }',
|
|
157
|
+
'data: { "id": 3 }',
|
|
158
|
+
]
|
|
159
|
+
mock_response = MockHTTPXResponse(lines)
|
|
160
|
+
responses.response_stream = mock_response
|
|
161
|
+
|
|
162
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
163
|
+
|
|
164
|
+
assert results == ['{ "id": 1 }', '{ "id": 2 }', '{ "id": 3 }']
|
|
165
|
+
mock_response.aiter_lines.assert_called_once()
|
|
166
|
+
mock_response.aclose.assert_called_once()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_httpx_incomplete_json_at_end(responses: api_client.HttpResponse):
|
|
171
|
+
lines = ['{ "partial": "data"'] # Missing closing brace
|
|
172
|
+
mock_response = MockHTTPXResponse(lines)
|
|
173
|
+
responses.response_stream = mock_response
|
|
174
|
+
|
|
175
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
176
|
+
|
|
177
|
+
# The remaining chunk is yielded
|
|
178
|
+
assert results == ['{ "partial": "data"']
|
|
179
|
+
mock_response.aiter_lines.assert_called_once()
|
|
180
|
+
mock_response.aclose.assert_called_once()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.mark.asyncio
|
|
184
|
+
async def test_httpx_empty_stream(responses: api_client.HttpResponse):
|
|
185
|
+
lines: List[str] = []
|
|
186
|
+
mock_response = MockHTTPXResponse(lines)
|
|
187
|
+
responses.response_stream = mock_response
|
|
188
|
+
|
|
189
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
190
|
+
|
|
191
|
+
assert results == []
|
|
192
|
+
mock_response.aiter_lines.assert_called_once()
|
|
193
|
+
mock_response.aclose.assert_called_once()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Async aiohttp
|
|
197
|
+
@requires_aiohttp
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
async def test_aiohttp_simple_lines(responses: api_client.HttpResponse):
|
|
200
|
+
api_client.has_aiohttp = True # Force aiohttp
|
|
201
|
+
lines = ["hello", "world", "testing"]
|
|
202
|
+
# Use the mock class that pretends to be aiohttp.ClientResponse
|
|
203
|
+
mock_response = MockAIOHTTPResponse(lines)
|
|
204
|
+
responses.response_stream = mock_response
|
|
205
|
+
|
|
206
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
207
|
+
|
|
208
|
+
assert results == lines
|
|
209
|
+
mock_response.content.readline.assert_any_call()
|
|
210
|
+
mock_response.release.assert_called_once()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@requires_aiohttp
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_aiohttp_data_prefix(responses: api_client.HttpResponse):
|
|
216
|
+
api_client.has_aiohttp = True # Force aiohttp
|
|
217
|
+
lines = ["data: { 'message': 'hello' }", "data: { 'status': 'ok' }"]
|
|
218
|
+
# Use the mock class that pretends to be aiohttp.ClientResponse
|
|
219
|
+
mock_response = MockAIOHTTPResponse(lines)
|
|
220
|
+
responses.response_stream = mock_response
|
|
221
|
+
|
|
222
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
223
|
+
|
|
224
|
+
assert results == ["{ 'message': 'hello' }", "{ 'status': 'ok' }"]
|
|
225
|
+
mock_response.content.readline.assert_any_call()
|
|
226
|
+
mock_response.release.assert_called_once()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@requires_aiohttp
|
|
230
|
+
@pytest.mark.asyncio
|
|
231
|
+
async def test_aiohttp_multiple_json_chunks(responses: api_client.HttpResponse):
|
|
232
|
+
api_client.has_aiohttp = True # Force aiohttp
|
|
233
|
+
lines = [
|
|
234
|
+
'{ "id": 1 }',
|
|
235
|
+
"", # empty line to check robustness
|
|
236
|
+
'data: { "id": 2 }',
|
|
237
|
+
'data: { "id": 3 }',
|
|
238
|
+
]
|
|
239
|
+
# Use the mock class that pretends to be aiohttp.ClientResponse
|
|
240
|
+
mock_response = MockAIOHTTPResponse(lines)
|
|
241
|
+
responses.response_stream = mock_response
|
|
242
|
+
|
|
243
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
244
|
+
|
|
245
|
+
assert results == ['{ "id": 1 }', '{ "id": 2 }', '{ "id": 3 }']
|
|
246
|
+
mock_response.content.readline.assert_any_call()
|
|
247
|
+
mock_response.release.assert_called_once()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@requires_aiohttp
|
|
251
|
+
@pytest.mark.asyncio
|
|
252
|
+
async def test_aiohttp_incomplete_json_at_end(
|
|
253
|
+
responses: api_client.HttpResponse,
|
|
254
|
+
):
|
|
255
|
+
api_client.has_aiohttp = True # Force aiohttp
|
|
256
|
+
lines = ['{ "partial": "data"'] # Missing closing brace
|
|
257
|
+
# Use the mock class that pretends to be aiohttp.ClientResponse
|
|
258
|
+
mock_response = MockAIOHTTPResponse(lines)
|
|
259
|
+
responses.response_stream = mock_response
|
|
260
|
+
|
|
261
|
+
results = [line async for line in responses._aiter_response_stream()]
|
|
262
|
+
|
|
263
|
+
assert results == ['{ "partial": "data"']
|
|
264
|
+
mock_response.content.readline.assert_any_call()
|
|
265
|
+
mock_response.release.assert_called_once()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def mock_response(chunks):
|
|
269
|
+
mock_stream = MagicMock(spec=httpx.SyncByteStream)
|
|
270
|
+
mock_stream.__iter__.return_value = chunks
|
|
271
|
+
return httpx.Response(
|
|
272
|
+
status_code=200,
|
|
273
|
+
stream=mock_stream,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@patch('httpx.Client.send')
|
|
278
|
+
def test_error_event_in_streamed_responses_bad_json(mock_send_method):
|
|
279
|
+
with_bad_json = [
|
|
280
|
+
b'{"candidates":[{"content":{"parts":[{"text":"test"}],"role":"model"}}]}',
|
|
281
|
+
b'\n',
|
|
282
|
+
b'{"error": bad_json}',
|
|
283
|
+
]
|
|
284
|
+
mock_send_method.return_value = mock_response(with_bad_json)
|
|
285
|
+
|
|
286
|
+
client = api_client.BaseApiClient(api_key='test_api_key')
|
|
287
|
+
stream = client.request_streamed('POST', 'models/gemini-2.5-flash', {})
|
|
288
|
+
|
|
289
|
+
chunk = next(stream)
|
|
290
|
+
assert chunk == types.HttpResponse(
|
|
291
|
+
headers={},
|
|
292
|
+
body=(
|
|
293
|
+
'{"candidates": [{"content": {"parts": [{"text": "test"}], "role":'
|
|
294
|
+
' "model"}}]}'
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
with pytest.raises(errors.UnknownApiResponseError):
|
|
299
|
+
next(stream)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@patch('httpx.Client.send')
|
|
303
|
+
def test_error_event_in_streamed_responses(mock_send_method):
|
|
304
|
+
mock_send_method.return_value = mock_response(EVENT_STREAM_DATA_WITH_ERROR)
|
|
305
|
+
|
|
306
|
+
client = api_client.BaseApiClient(api_key='test_api_key')
|
|
307
|
+
stream = client.request_streamed('POST', 'models/gemini-2.5-flash', {})
|
|
308
|
+
|
|
309
|
+
chunk = next(stream)
|
|
310
|
+
assert chunk == types.HttpResponse(
|
|
311
|
+
body=(
|
|
312
|
+
'{"candidates": [{"content": {"parts": [{"text": "test"}], "role":'
|
|
313
|
+
' "model"}}]}'
|
|
314
|
+
),
|
|
315
|
+
headers={},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
with pytest.raises(errors.ServerError):
|
|
319
|
+
next(stream)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@patch('httpx.Client.send')
|
|
323
|
+
def test_error_event_in_generate_content_stream(mock_send_method):
|
|
324
|
+
mock_send_method.return_value = mock_response(EVENT_STREAM_DATA_WITH_ERROR)
|
|
325
|
+
|
|
326
|
+
client = Client(api_key='test_api_key')
|
|
327
|
+
generated_response = client.models.generate_content_stream(
|
|
328
|
+
model='gemini-2.5-flash',
|
|
329
|
+
contents='Tell me a story in 300 words.',
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
chunk = next(generated_response)
|
|
333
|
+
assert chunk == types.GenerateContentResponse(
|
|
334
|
+
candidates=[
|
|
335
|
+
types.Candidate(
|
|
336
|
+
content=types.Content(
|
|
337
|
+
parts=[
|
|
338
|
+
types.Part(
|
|
339
|
+
text='test'
|
|
340
|
+
),
|
|
341
|
+
],
|
|
342
|
+
role='model'
|
|
343
|
+
)
|
|
344
|
+
),
|
|
345
|
+
],
|
|
346
|
+
sdk_http_response=types.HttpResponse(headers={})
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
with pytest.raises(errors.ServerError):
|
|
350
|
+
next(generated_response)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def _async_httpx_response(_):
|
|
354
|
+
mock_stream = MagicMock(spec=httpx.AsyncByteStream)
|
|
355
|
+
mock_stream.__aiter__.return_value = EVENT_STREAM_DATA_WITH_ERROR
|
|
356
|
+
mock_stream.aclose = AsyncMock()
|
|
357
|
+
return httpx.Response(
|
|
358
|
+
status_code=200,
|
|
359
|
+
stream=mock_stream,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@patch('httpx.AsyncBaseTransport')
|
|
364
|
+
@pytest.mark.asyncio
|
|
365
|
+
async def test_error_event_in_streamed_responses_async(mock_transport):
|
|
366
|
+
client = api_client.BaseApiClient(
|
|
367
|
+
api_key='test_api_key',
|
|
368
|
+
http_options=types.HttpOptions(
|
|
369
|
+
async_client_args={'transport': mock_transport}
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
mock_transport.handle_async_request = _async_httpx_response
|
|
373
|
+
mock_transport.aclose = AsyncMock()
|
|
374
|
+
|
|
375
|
+
resp = await client.async_request_streamed(
|
|
376
|
+
'POST', 'models/gemini-2.5-flash', {'key': 'value'}
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
chunk = await anext(resp)
|
|
380
|
+
assert chunk == types.HttpResponse(
|
|
381
|
+
headers={},
|
|
382
|
+
body=(
|
|
383
|
+
'{"candidates": [{"content": {"parts": [{"text": "test"}], "role":'
|
|
384
|
+
' "model"}}]}'
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
with pytest.raises(errors.ServerError):
|
|
389
|
+
await anext(resp)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@patch('httpx.AsyncBaseTransport')
|
|
393
|
+
@pytest.mark.asyncio
|
|
394
|
+
async def test_error_event_in_generate_content_stream_async(mock_transport):
|
|
395
|
+
client = Client(
|
|
396
|
+
api_key='test_api_key',
|
|
397
|
+
http_options=types.HttpOptions(
|
|
398
|
+
async_client_args={'transport': mock_transport}
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
mock_transport.handle_async_request = _async_httpx_response
|
|
402
|
+
mock_transport.aclose = AsyncMock()
|
|
403
|
+
|
|
404
|
+
generated_response = await client.aio.models.generate_content_stream(
|
|
405
|
+
model='gemini-2.5-flash',
|
|
406
|
+
contents='Tell me a story in 300 words.',
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
chunk = await anext(generated_response)
|
|
410
|
+
assert chunk == types.GenerateContentResponse(
|
|
411
|
+
candidates=[
|
|
412
|
+
types.Candidate(
|
|
413
|
+
content=types.Content(
|
|
414
|
+
parts=[
|
|
415
|
+
types.Part(
|
|
416
|
+
text='test'
|
|
417
|
+
),
|
|
418
|
+
],
|
|
419
|
+
role='model'
|
|
420
|
+
)
|
|
421
|
+
),
|
|
422
|
+
],
|
|
423
|
+
sdk_http_response=types.HttpResponse(headers={})
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
with pytest.raises(errors.ServerError):
|
|
427
|
+
await anext(generated_response)
|