google-genai 1.14.0__py3-none-any.whl → 1.16.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 CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -15,9 +15,11 @@
15
15
 
16
16
  """Google Gen AI SDK"""
17
17
 
18
+ from . import version
18
19
  from .client import Client
19
- from . import version
20
+ from .live import live_ephemeral_connect
21
+
20
22
 
21
23
  __version__ = version.__version__
22
24
 
23
- __all__ = ['Client']
25
+ __all__ = ['Client', 'live_ephemeral_connect']
@@ -0,0 +1,55 @@
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 typing
17
+
18
+ from ._mcp_utils import mcp_to_gemini_tools
19
+ from .types import FunctionCall, Tool
20
+
21
+ if typing.TYPE_CHECKING:
22
+ from mcp import types as mcp_types
23
+ from mcp import ClientSession
24
+
25
+
26
+ class McpToGenAiToolAdapter:
27
+ """Adapter for working with MCP tools in a GenAI client."""
28
+
29
+ def __init__(
30
+ self,
31
+ session: "mcp.ClientSession", # type: ignore # noqa: F821
32
+ list_tools_result: "mcp_types.ListToolsResult", # type: ignore
33
+ ) -> None:
34
+ self._mcp_session = session
35
+ self._list_tools_result = list_tools_result
36
+
37
+ async def call_tool(
38
+ self, function_call: FunctionCall
39
+ ) -> "mcp_types.CallToolResult": # type: ignore
40
+ """Calls a function on the MCP server."""
41
+ name = function_call.name if function_call.name else ""
42
+ arguments = dict(function_call.args) if function_call.args else {}
43
+
44
+ return typing.cast(
45
+ "mcp_types.CallToolResult",
46
+ await self._mcp_session.call_tool(
47
+ name=name,
48
+ arguments=arguments,
49
+ ),
50
+ )
51
+
52
+ @property
53
+ def tools(self) -> list[Tool]:
54
+ """Returns a list of Google GenAI tools."""
55
+ return mcp_to_gemini_tools(self._list_tools_result.tools)
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import math
32
32
  import os
33
33
  import ssl
34
34
  import sys
35
+ import threading
35
36
  import time
36
37
  from typing import Any, AsyncIterator, Optional, Tuple, Union
37
38
  from urllib.parse import urlparse
@@ -71,14 +72,14 @@ def _append_library_version_headers(headers: dict[str, str]) -> None:
71
72
  'user-agent' in headers
72
73
  and version_header_value not in headers['user-agent']
73
74
  ):
74
- headers['user-agent'] += f' {version_header_value}'
75
+ headers['user-agent'] = f'{version_header_value} ' + headers['user-agent']
75
76
  elif 'user-agent' not in headers:
76
77
  headers['user-agent'] = version_header_value
77
78
  if (
78
79
  'x-goog-api-client' in headers
79
80
  and version_header_value not in headers['x-goog-api-client']
80
81
  ):
81
- headers['x-goog-api-client'] += f' {version_header_value}'
82
+ headers['x-goog-api-client'] = f'{version_header_value} ' + headers['x-goog-api-client']
82
83
  elif 'x-goog-api-client' not in headers:
83
84
  headers['x-goog-api-client'] = version_header_value
84
85
 
@@ -376,10 +377,12 @@ class BaseApiClient:
376
377
  # credentials. This is crucial for thread safety when multiple coroutines
377
378
  # might be accessing the credentials at the same time.
378
379
  try:
379
- self._auth_lock = asyncio.Lock()
380
+ self._sync_auth_lock = threading.Lock()
381
+ self._async_auth_lock = asyncio.Lock()
380
382
  except RuntimeError:
381
383
  asyncio.set_event_loop(asyncio.new_event_loop())
382
- self._auth_lock = asyncio.Lock()
384
+ self._sync_auth_lock = threading.Lock()
385
+ self._async_auth_lock = asyncio.Lock()
383
386
 
384
387
  # Handle when to use Vertex AI in express mode (api key).
385
388
  # Explicit initializer arguments are already validated above.
@@ -519,25 +522,26 @@ class BaseApiClient:
519
522
 
520
523
  def _access_token(self) -> str:
521
524
  """Retrieves the access token for the credentials."""
522
- if not self._credentials:
523
- self._credentials, project = _load_auth(project=self.project)
524
- if not self.project:
525
- self.project = project
526
-
527
- if self._credentials:
528
- if self._credentials.expired or not self._credentials.token:
529
- # Only refresh when it needs to. Default expiration is 3600 seconds.
530
- _refresh_auth(self._credentials)
531
- if not self._credentials.token:
525
+ with self._sync_auth_lock:
526
+ if not self._credentials:
527
+ self._credentials, project = _load_auth(project=self.project)
528
+ if not self.project:
529
+ self.project = project
530
+
531
+ if self._credentials:
532
+ if self._credentials.expired or not self._credentials.token:
533
+ # Only refresh when it needs to. Default expiration is 3600 seconds.
534
+ _refresh_auth(self._credentials)
535
+ if not self._credentials.token:
536
+ raise RuntimeError('Could not resolve API token from the environment')
537
+ return self._credentials.token # type: ignore[no-any-return]
538
+ else:
532
539
  raise RuntimeError('Could not resolve API token from the environment')
533
- return self._credentials.token # type: ignore[no-any-return]
534
- else:
535
- raise RuntimeError('Could not resolve API token from the environment')
536
540
 
537
541
  async def _async_access_token(self) -> Union[str, Any]:
538
542
  """Retrieves the access token for the credentials asynchronously."""
539
543
  if not self._credentials:
540
- async with self._auth_lock:
544
+ async with self._async_auth_lock:
541
545
  # This ensures that only one coroutine can execute the auth logic at a
542
546
  # time for thread safety.
543
547
  if not self._credentials:
@@ -551,7 +555,7 @@ class BaseApiClient:
551
555
  if self._credentials:
552
556
  if self._credentials.expired or not self._credentials.token:
553
557
  # Only refresh when it needs to. Default expiration is 3600 seconds.
554
- async with self._auth_lock:
558
+ async with self._async_auth_lock:
555
559
  if self._credentials.expired or not self._credentials.token:
556
560
  # Double check that the credentials expired before refreshing.
557
561
  await asyncio.to_thread(_refresh_auth, self._credentials)
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -269,7 +269,7 @@ def _parse_schema_from_parameter(
269
269
  raise ValueError(
270
270
  f'Failed to parse the parameter {param} of function {func_name} for'
271
271
  ' automatic function calling.Automatic function calling works best with'
272
- ' simpler function signature schema,consider manually parse your'
272
+ ' simpler function signature schema, consider manually parsing your'
273
273
  f' function declaration for function {func_name}.'
274
274
  )
275
275
 
google/genai/_common.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Google LLC
1
+ # Copyright 2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -24,14 +24,30 @@ from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
24
24
  import pydantic
25
25
 
26
26
  from . import _common
27
+ from . import _mcp_utils
27
28
  from . import errors
28
29
  from . import types
30
+ from ._adapters import McpToGenAiToolAdapter
31
+
29
32
 
30
33
  if sys.version_info >= (3, 10):
31
34
  from types import UnionType
32
35
  else:
33
36
  UnionType = typing._UnionGenericAlias # type: ignore[attr-defined]
34
37
 
38
+ if typing.TYPE_CHECKING:
39
+ from mcp import ClientSession as McpClientSession
40
+ from mcp.types import Tool as McpTool
41
+ else:
42
+ McpClientSession: typing.Type = Any
43
+ McpTool: typing.Type = Any
44
+ try:
45
+ from mcp import ClientSession as McpClientSession
46
+ from mcp.types import Tool as McpTool
47
+ except ImportError:
48
+ McpClientSession = None
49
+ McpTool = None
50
+
35
51
  _DEFAULT_MAX_REMOTE_CALLS_AFC = 10
36
52
 
37
53
  logger = logging.getLogger('google_genai.models')
@@ -78,10 +94,13 @@ def format_destination(
78
94
 
79
95
  def get_function_map(
80
96
  config: Optional[types.GenerateContentConfigOrDict] = None,
97
+ mcp_to_genai_tool_adapters: Optional[
98
+ dict[str, McpToGenAiToolAdapter]
99
+ ] = None,
81
100
  is_caller_method_async: bool = False,
82
- ) -> dict[str, Callable[..., Any]]:
101
+ ) -> dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]]:
83
102
  """Returns a function map from the config."""
84
- function_map: dict[str, Callable[..., Any]] = {}
103
+ function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]] = {}
85
104
  if not config:
86
105
  return function_map
87
106
  config_model = _create_generate_content_config_model(config)
@@ -95,6 +114,17 @@ def get_function_map(
95
114
  f' invoke {tool.__name__} to get the function response.'
96
115
  )
97
116
  function_map[tool.__name__] = tool
117
+ if mcp_to_genai_tool_adapters:
118
+ if not is_caller_method_async:
119
+ raise errors.UnsupportedFunctionError(
120
+ 'MCP tools are not supported in synchronous methods.'
121
+ )
122
+ for tool_name, _ in mcp_to_genai_tool_adapters.items():
123
+ if function_map.get(tool_name):
124
+ raise ValueError(
125
+ f'Tool {tool_name} is already defined for the request.'
126
+ )
127
+ function_map.update(mcp_to_genai_tool_adapters)
98
128
  return function_map
99
129
 
100
130
 
@@ -247,7 +277,7 @@ async def invoke_function_from_dict_args_async(
247
277
 
248
278
  def get_function_response_parts(
249
279
  response: types.GenerateContentResponse,
250
- function_map: dict[str, Callable[..., Any]],
280
+ function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
251
281
  ) -> list[types.Part]:
252
282
  """Returns the function response parts from the response."""
253
283
  func_response_parts = []
@@ -267,9 +297,10 @@ def get_function_response_parts(
267
297
  )
268
298
  func_response: dict[str, Any]
269
299
  try:
270
- func_response = {
271
- 'result': invoke_function_from_dict_args(args, func)
272
- }
300
+ if not isinstance(func, McpToGenAiToolAdapter):
301
+ func_response = {
302
+ 'result': invoke_function_from_dict_args(args, func)
303
+ }
273
304
  except Exception as e: # pylint: disable=broad-except
274
305
  func_response = {'error': str(e)}
275
306
  func_response_part = types.Part.from_function_response(
@@ -278,9 +309,10 @@ def get_function_response_parts(
278
309
  func_response_parts.append(func_response_part)
279
310
  return func_response_parts
280
311
 
312
+
281
313
  async def get_function_response_parts_async(
282
314
  response: types.GenerateContentResponse,
283
- function_map: dict[str, Callable[..., Any]],
315
+ function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
284
316
  ) -> list[types.Part]:
285
317
  """Returns the function response parts from the response."""
286
318
  func_response_parts = []
@@ -300,7 +332,15 @@ async def get_function_response_parts_async(
300
332
  )
301
333
  func_response: dict[str, Any]
302
334
  try:
303
- if inspect.iscoroutinefunction(func):
335
+ if isinstance(func, McpToGenAiToolAdapter):
336
+ mcp_tool_response = await func.call_tool(
337
+ types.FunctionCall(name=func_name, args=args)
338
+ )
339
+ if mcp_tool_response.isError:
340
+ func_response = {'error': mcp_tool_response}
341
+ else:
342
+ func_response = {'result': mcp_tool_response}
343
+ elif inspect.iscoroutinefunction(func):
304
344
  func_response = {
305
345
  'result': await invoke_function_from_dict_args_async(args, func)
306
346
  }
@@ -401,3 +441,71 @@ def should_append_afc_history(
401
441
  if not config_model.automatic_function_calling:
402
442
  return True
403
443
  return not config_model.automatic_function_calling.ignore_call_history
444
+
445
+
446
+ def parse_config_for_mcp_usage(
447
+ config: Optional[types.GenerateContentConfigOrDict] = None,
448
+ ) -> Optional[types.GenerateContentConfig]:
449
+ """Returns a parsed config with an appended MCP header if MCP tools or sessions are used."""
450
+ if not config:
451
+ return None
452
+ config_model = _create_generate_content_config_model(config)
453
+ # Create a copy of the config model with the tools field cleared since some
454
+ # tools may not be pickleable.
455
+ config_model_copy = config_model.model_copy(update={'tools': None})
456
+ config_model_copy.tools = config_model.tools
457
+ if config_model.tools and _mcp_utils.has_mcp_tool_usage(config_model.tools):
458
+ if config_model_copy.http_options is None:
459
+ config_model_copy.http_options = types.HttpOptions(headers={})
460
+ if config_model_copy.http_options.headers is None:
461
+ config_model_copy.http_options.headers = {}
462
+ _mcp_utils.set_mcp_usage_header(config_model_copy.http_options.headers)
463
+
464
+ return config_model_copy
465
+
466
+
467
+ async def parse_config_for_mcp_sessions(
468
+ config: Optional[types.GenerateContentConfigOrDict] = None,
469
+ ) -> tuple[
470
+ Optional[types.GenerateContentConfig],
471
+ dict[str, McpToGenAiToolAdapter],
472
+ ]:
473
+ """Returns a parsed config with MCP sessions converted to GenAI tools.
474
+
475
+ Also returns a map of MCP tools to GenAI tool adapters to be used for AFC.
476
+ """
477
+ mcp_to_genai_tool_adapters: dict[str, McpToGenAiToolAdapter] = {}
478
+ parsed_config = parse_config_for_mcp_usage(config)
479
+ if not parsed_config:
480
+ return None, mcp_to_genai_tool_adapters
481
+ # Create a copy of the config model with the tools field cleared as they will
482
+ # be replaced with the MCP tools converted to GenAI tools.
483
+ parsed_config_copy = parsed_config.model_copy(update={'tools': None})
484
+ if parsed_config.tools:
485
+ parsed_config_copy.tools = []
486
+ for tool in parsed_config.tools:
487
+ if McpClientSession is not None and isinstance(tool, McpClientSession):
488
+ mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
489
+ tool, await tool.list_tools()
490
+ )
491
+ # Extend the config with the MCP session tools converted to GenAI tools.
492
+ parsed_config_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
493
+ for genai_tool in mcp_to_genai_tool_adapter.tools:
494
+ if genai_tool.function_declarations:
495
+ for function_declaration in genai_tool.function_declarations:
496
+ if function_declaration.name:
497
+ if mcp_to_genai_tool_adapters.get(function_declaration.name):
498
+ raise ValueError(
499
+ f'Tool {function_declaration.name} is already defined for'
500
+ ' the request.'
501
+ )
502
+ mcp_to_genai_tool_adapters[function_declaration.name] = (
503
+ mcp_to_genai_tool_adapter
504
+ )
505
+ if McpClientSession is not None:
506
+ parsed_config_copy.tools.extend(
507
+ tool
508
+ for tool in parsed_config.tools
509
+ if not isinstance(tool, McpClientSession)
510
+ )
511
+ return parsed_config_copy, mcp_to_genai_tool_adapters