feat(phase1): add image storage utilities

- Create backend/services/image_storage.py with 4 core functions:
  - sanitize_filename(): remove unsafe chars, limit to 255 chars, convert to lowercase
  - get_unique_filename(): handle collisions with UUID suffix (format: {name}_{uuid8}_{variant}.jpg)
  - ensure_image_directories(): create /images/ root and category subdirs on startup
  - save_image(): save bytes to /images/{category}/{filename}, returns relative path
- Create comprehensive test suite (22 tests) covering all functionality
- Integrate ensure_image_directories() into FastAPI startup event
- Directory structure: /images/{category}/{filename}
- Collision handling: auto-suffix with UUID if filename exists
- All tests passing, pathlib.Path for safe operations
This commit is contained in:
2026-04-20 21:57:26 +03:00
parent 39fab336ba
commit ea49cd6e4a
4487 changed files with 1305959 additions and 4 deletions

View File

@@ -0,0 +1,26 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Google Gen AI SDK"""
from . import interactions
from . import types
from . import version
from .client import Client
__version__ = version.__version__
__all__ = ['Client']

View File

@@ -0,0 +1,55 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import typing
from ._mcp_utils import mcp_to_gemini_tools
from .types import FunctionCall, Tool
if typing.TYPE_CHECKING:
from mcp import types as mcp_types
from mcp import ClientSession
class McpToGenAiToolAdapter:
"""Adapter for working with MCP tools in a GenAI client."""
def __init__(
self,
session: "mcp.ClientSession", # type: ignore # noqa: F821
list_tools_result: "mcp_types.ListToolsResult", # type: ignore
) -> None:
self._mcp_session = session
self._list_tools_result = list_tools_result
async def call_tool(
self, function_call: FunctionCall
) -> "mcp_types.CallToolResult": # type: ignore
"""Calls a function on the MCP server."""
name = function_call.name if function_call.name else ""
arguments = dict(function_call.args) if function_call.args else {}
return typing.cast(
"mcp_types.CallToolResult",
await self._mcp_session.call_tool(
name=name,
arguments=arguments,
),
)
@property
def tools(self) -> list[Tool]:
"""Returns a list of Google GenAI tools."""
return mcp_to_gemini_tools(self._list_tools_result.tools)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Utilities for the API Modules of the Google Gen AI SDK."""
from typing import Optional
from . import _api_client
class BaseModule:
def __init__(self, api_client_: _api_client.BaseApiClient):
self._api_client = api_client_
@property
def vertexai(self) -> Optional[bool]:
return self._api_client.vertexai

View File

@@ -0,0 +1,325 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import inspect
import sys
import types as builtin_types
import typing
from typing import _GenericAlias, Any, Callable, get_args, get_origin, Literal, Optional, Union # type: ignore[attr-defined]
import pydantic
from . import _extra_utils
from . import types
if sys.version_info >= (3, 10):
VersionedUnionType = builtin_types.UnionType
else:
VersionedUnionType = typing._UnionGenericAlias # type: ignore[attr-defined]
__all__ = [
'_py_builtin_type_to_schema_type',
'_raise_for_unsupported_param',
'_handle_params_as_deferred_annotations',
'_add_unevaluated_items_to_fixed_len_tuple_schema',
'_is_builtin_primitive_or_compound',
'_is_default_value_compatible',
'_parse_schema_from_parameter',
'_get_required_fields',
]
_py_builtin_type_to_schema_type = {
str: types.Type.STRING,
int: types.Type.INTEGER,
float: types.Type.NUMBER,
bool: types.Type.BOOLEAN,
list: types.Type.ARRAY,
dict: types.Type.OBJECT,
None: types.Type.NULL,
}
def _raise_for_unsupported_param(
param: inspect.Parameter, func_name: str, exception: Union[Exception, type[Exception]]
) -> None:
raise ValueError(
f'Failed to parse the parameter {param} of function {func_name} for'
' automatic function calling.Automatic function calling works best with'
' simpler function signature schema, consider manually parsing your'
f' function declaration for function {func_name}.'
) from exception
def _handle_params_as_deferred_annotations(param: inspect.Parameter, annotation_under_future: dict[str, Any], name: str) -> inspect.Parameter:
"""Catches the case when type hints are stored as strings."""
if isinstance(param.annotation, str):
param = param.replace(annotation=annotation_under_future[name])
return param
def _add_unevaluated_items_to_fixed_len_tuple_schema(
json_schema: dict[str, Any]
) -> dict[str, Any]:
if (
json_schema.get('maxItems')
and (
json_schema.get('prefixItems')
and len(json_schema['prefixItems']) == json_schema['maxItems']
)
and json_schema.get('type') == 'array'
):
json_schema['unevaluatedItems'] = False
return json_schema
def _is_builtin_primitive_or_compound(
annotation: inspect.Parameter.annotation, # type: ignore[valid-type]
) -> bool:
return annotation in _py_builtin_type_to_schema_type.keys()
def _is_default_value_compatible(
default_value: Any, annotation: inspect.Parameter.annotation # type: ignore[valid-type]
) -> bool:
# None type is expected to be handled external to this function
if _is_builtin_primitive_or_compound(annotation):
return isinstance(default_value, annotation)
if (
isinstance(annotation, _GenericAlias)
or isinstance(annotation, builtin_types.GenericAlias)
or isinstance(annotation, VersionedUnionType)
):
origin = get_origin(annotation)
if origin in (Union, VersionedUnionType): # type: ignore[comparison-overlap]
return any(
_is_default_value_compatible(default_value, arg)
for arg in get_args(annotation)
)
if origin is dict: # type: ignore[comparison-overlap]
return isinstance(default_value, dict)
if origin is list: # type: ignore[comparison-overlap]
if not isinstance(default_value, list):
return False
# most tricky case, element in list is union type
# need to apply any logic within all
# see test case test_generic_alias_complex_array_with_default_value
# a: typing.List[int | str | float | bool]
# default_value: [1, 'a', 1.1, True]
return all(
any(
_is_default_value_compatible(item, arg)
for arg in get_args(annotation)
)
for item in default_value
)
if origin is Literal: # type: ignore[comparison-overlap]
return default_value in get_args(annotation)
# return False for any other unrecognized annotation
return False
def _parse_schema_from_parameter( # type: ignore[return]
api_option: Literal['VERTEX_AI', 'GEMINI_API'],
param: inspect.Parameter,
func_name: str,
) -> types.Schema:
"""parse schema from parameter.
from the simplest case to the most complex case.
"""
schema = types.Schema()
default_value_error_msg = (
f'Default value {param.default} of parameter {param} of function'
f' {func_name} is not compatible with the parameter annotation'
f' {param.annotation}.'
)
if _is_builtin_primitive_or_compound(param.annotation):
if param.default is not inspect.Parameter.empty:
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
schema.type = _py_builtin_type_to_schema_type[param.annotation]
return schema
if (
isinstance(param.annotation, VersionedUnionType)
# only parse simple UnionType, example int | str | float | bool
# complex UnionType will be invoked in raise branch
and all(
(_is_builtin_primitive_or_compound(arg) or arg is type(None))
for arg in get_args(param.annotation)
)
):
schema.type = _py_builtin_type_to_schema_type[dict]
schema.any_of = []
unique_types = set()
for arg in get_args(param.annotation):
if arg.__name__ == 'NoneType': # Optional type
schema.nullable = True
continue
schema_in_any_of = _parse_schema_from_parameter(
api_option,
inspect.Parameter(
'item', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=arg
),
func_name,
)
if (
schema_in_any_of.model_dump_json(exclude_none=True)
not in unique_types
):
schema.any_of.append(schema_in_any_of)
unique_types.add(schema_in_any_of.model_dump_json(exclude_none=True))
if len(schema.any_of) == 1: # param: list | None -> Array
schema.type = schema.any_of[0].type
schema.any_of = None
if (
param.default is not inspect.Parameter.empty
and param.default is not None
):
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
return schema
if isinstance(param.annotation, _GenericAlias) or isinstance(
param.annotation, builtin_types.GenericAlias
):
origin = get_origin(param.annotation)
args = get_args(param.annotation)
if origin is dict:
schema.type = _py_builtin_type_to_schema_type[dict]
if param.default is not inspect.Parameter.empty:
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
return schema
if origin is Literal:
if not all(isinstance(arg, str) for arg in args):
raise ValueError(
f'Literal type {param.annotation} must be a list of strings.'
)
schema.type = _py_builtin_type_to_schema_type[str]
schema.enum = list(args)
if param.default is not inspect.Parameter.empty:
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
return schema
if origin is list:
schema.type = _py_builtin_type_to_schema_type[list]
schema.items = _parse_schema_from_parameter(
api_option,
inspect.Parameter(
'item',
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=args[0],
),
func_name,
)
if param.default is not inspect.Parameter.empty:
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
return schema
if origin is Union:
schema.any_of = []
schema.type = _py_builtin_type_to_schema_type[dict]
unique_types = set()
for arg in args:
# The first check is for NoneType in Python 3.9, since the __name__
# attribute is not available in Python 3.9
if type(arg) is type(None) or (
hasattr(arg, '__name__') and arg.__name__ == 'NoneType'
): # Optional type
schema.nullable = True
continue
schema_in_any_of = _parse_schema_from_parameter(
api_option,
inspect.Parameter(
'item',
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=arg,
),
func_name,
)
if (
len(param.annotation.__args__) == 2
and type(None) in param.annotation.__args__
): # Optional type
for optional_arg in param.annotation.__args__:
if (
hasattr(optional_arg, '__origin__')
and optional_arg.__origin__ is list
):
# Optional type with list, for example Optional[list[str]]
schema.items = schema_in_any_of.items
if (
schema_in_any_of.model_dump_json(exclude_none=True)
not in unique_types
):
schema.any_of.append(schema_in_any_of)
unique_types.add(schema_in_any_of.model_dump_json(exclude_none=True))
if len(schema.any_of) == 1: # param: Union[List, None] -> Array
schema.type = schema.any_of[0].type
schema.any_of = None
if (
param.default is not None
and param.default is not inspect.Parameter.empty
):
if not _is_default_value_compatible(param.default, param.annotation):
raise ValueError(default_value_error_msg)
schema.default = param.default
return schema
# all other generic alias will be invoked in raise branch
if (
# for user defined class, we only support pydantic model
_extra_utils.is_annotation_pydantic_model(param.annotation)
):
if (
param.default is not inspect.Parameter.empty
and param.default is not None
):
schema.default = param.default
schema.type = _py_builtin_type_to_schema_type[dict]
schema.properties = {}
for field_name, field_info in param.annotation.model_fields.items():
schema.properties[field_name] = _parse_schema_from_parameter(
api_option,
inspect.Parameter(
field_name,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=field_info.annotation,
),
func_name,
)
schema.required = _get_required_fields(schema)
return schema
_raise_for_unsupported_param(param, func_name, ValueError)
def _get_required_fields(schema: types.Schema) -> Optional[list[str]]:
if not schema.properties:
return None
return [
field_name
for field_name, field_schema in schema.properties.items()
if not field_schema.nullable and field_schema.default is None
]

View File

@@ -0,0 +1,26 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Base transformers for Google GenAI SDK."""
import base64
# Some fields don't accept url safe base64 encoding.
# We shouldn't use this transformer if the backend adhere to Cloud Type
# format https://cloud.google.com/docs/discovery/type-format.
# TODO(b/389133914,b/390320301): Remove the hack after backend fix the issue.
def t_bytes(data: bytes) -> str:
if not isinstance(data, bytes):
return data
return base64.b64encode(data).decode('ascii')

View File

@@ -0,0 +1,50 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
from typing import Optional
from .types import HttpOptions
_default_base_gemini_url = None
_default_base_vertex_url = None
def set_default_base_urls(
gemini_url: Optional[str], vertex_url: Optional[str]
) -> None:
"""Overrides the base URLs for the Gemini API and Vertex AI API."""
global _default_base_gemini_url, _default_base_vertex_url
_default_base_gemini_url = gemini_url
_default_base_vertex_url = vertex_url
def get_base_url(
vertexai: bool,
http_options: Optional[HttpOptions] = None,
) -> Optional[str]:
"""Returns the default base URL based on the following priority.
1. Base URLs set via HttpOptions.
2. Base URLs set via the latest call to setDefaultBaseUrls.
3. Base URLs set via environment variables.
"""
if http_options and http_options.base_url:
return http_options.base_url
if vertexai:
return _default_base_vertex_url or os.getenv('GOOGLE_VERTEX_BASE_URL')
else:
return _default_base_gemini_url or os.getenv('GOOGLE_GEMINI_BASE_URL')

View File

@@ -0,0 +1,847 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Common utilities for the SDK."""
import base64
import collections.abc
import datetime
import enum
import functools
import logging
import re
import typing
from typing import Any, Callable, FrozenSet, Optional, Union, get_args, get_origin
import uuid
import warnings
import pydantic
from pydantic import alias_generators
from typing_extensions import TypeAlias
logger = logging.getLogger('google_genai._common')
StringDict: TypeAlias = dict[str, Any]
class ExperimentalWarning(Warning):
"""Warning for experimental features."""
def set_value_by_path(
data: Optional[dict[Any, Any]], keys: list[str], value: Any
) -> None:
"""Examples:
set_value_by_path({}, ['a', 'b'], v)
-> {'a': {'b': v}}
set_value_by_path({}, ['a', 'b[]', c], [v1, v2])
-> {'a': {'b': [{'c': v1}, {'c': v2}]}}
set_value_by_path({'a': {'b': [{'c': v1}, {'c': v2}]}}, ['a', 'b[]', 'd'], v3)
-> {'a': {'b': [{'c': v1, 'd': v3}, {'c': v2, 'd': v3}]}}
"""
if value is None:
return
for i, key in enumerate(keys[:-1]):
if key.endswith('[]'):
key_name = key[:-2]
if data is not None and key_name not in data:
if isinstance(value, list):
data[key_name] = [{} for _ in range(len(value))]
else:
raise ValueError(
f'value {value} must be a list given an array path {key}'
)
if isinstance(value, list) and data is not None:
for j, d in enumerate(data[key_name]):
set_value_by_path(d, keys[i + 1 :], value[j])
else:
if data is not None:
for d in data[key_name]:
set_value_by_path(d, keys[i + 1 :], value)
return
elif key.endswith('[0]'):
key_name = key[:-3]
if data is not None and key_name not in data:
data[key_name] = [{}]
if data is not None:
set_value_by_path(data[key_name][0], keys[i + 1 :], value)
return
if data is not None:
data = data.setdefault(key, {})
if data is not None:
existing_data = data.get(keys[-1])
# If there is an existing value, merge, not overwrite.
if existing_data is not None:
# Don't overwrite existing non-empty value with new empty value.
# This is triggered when handling tuning datasets.
if not value:
pass
# Don't fail when overwriting value with same value
elif value == existing_data:
pass
# Instead of overwriting dictionary with another dictionary, merge them.
# This is important for handling training and validation datasets in tuning.
elif isinstance(existing_data, dict) and isinstance(value, dict):
# Merging dictionaries. Consider deep merging in the future.
existing_data.update(value)
else:
raise ValueError(
f'Cannot set value for an existing key. Key: {keys[-1]};'
f' Existing value: {existing_data}; New value: {value}.'
)
else:
if (
keys[-1] == '_self'
and isinstance(data, dict)
and isinstance(value, dict)
):
data.update(value)
else:
data[keys[-1]] = value
def get_value_by_path(
data: Any, keys: list[str], *, default_value: Any = None
) -> Any:
"""Examples:
get_value_by_path({'a': {'b': v}}, ['a', 'b'])
-> v
get_value_by_path({'a': {'b': [{'c': v1}, {'c': v2}]}}, ['a', 'b[]', 'c'])
-> [v1, v2]
"""
if keys == ['_self']:
return data
for i, key in enumerate(keys):
if not data:
return default_value
if key.endswith('[]'):
key_name = key[:-2]
if key_name in data:
return [
get_value_by_path(d, keys[i + 1 :], default_value=default_value)
for d in data[key_name]
]
else:
return default_value
elif key.endswith('[0]'):
key_name = key[:-3]
if key_name in data and data[key_name]:
return get_value_by_path(
data[key_name][0], keys[i + 1 :], default_value=default_value
)
else:
return default_value
else:
if key in data:
data = data[key]
elif isinstance(data, BaseModel) and hasattr(data, key):
data = getattr(data, key)
else:
return default_value
return data
def move_value_by_path(data: Any, paths: dict[str, str]) -> None:
"""Moves values from source paths to destination paths.
Examples:
move_value_by_path(
{'requests': [{'content': v1}, {'content': v2}]},
{'requests[].*': 'requests[].request.*'}
)
-> {'requests': [{'request': {'content': v1}}, {'request': {'content':
v2}}]}
"""
for source_path, dest_path in paths.items():
source_keys = source_path.split('.')
dest_keys = dest_path.split('.')
# Determine keys to exclude from wildcard to avoid cyclic references
exclude_keys = set()
wildcard_idx = -1
for i, key in enumerate(source_keys):
if key == '*':
wildcard_idx = i
break
if wildcard_idx != -1 and len(dest_keys) > wildcard_idx:
# Extract the intermediate key between source and dest paths
# Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*']
# We want to exclude 'request'
for i in range(wildcard_idx, len(dest_keys)):
key = dest_keys[i]
if key != '*' and not key.endswith('[]') and not key.endswith('[0]'):
exclude_keys.add(key)
# Move values recursively
_move_value_recursive(data, source_keys, dest_keys, 0, exclude_keys)
def _move_value_recursive(
data: Any,
source_keys: list[str],
dest_keys: list[str],
key_idx: int,
exclude_keys: set[str],
) -> None:
"""Recursively moves values from source path to destination path."""
if key_idx >= len(source_keys):
return
key = source_keys[key_idx]
if key.endswith('[]'):
# Handle array iteration
key_name = key[:-2]
if key_name in data and isinstance(data[key_name], list):
for item in data[key_name]:
_move_value_recursive(
item, source_keys, dest_keys, key_idx + 1, exclude_keys
)
elif key == '*':
# Handle wildcard - move all fields
if isinstance(data, dict):
# Get all keys to move (excluding specified keys)
keys_to_move = [
k
for k in list(data.keys())
if not k.startswith('_') and k not in exclude_keys
]
# Collect values to move
values_to_move = {k: data[k] for k in keys_to_move}
# Set values at destination
for k, v in values_to_move.items():
# Build destination keys with the field name
new_dest_keys = []
for dk in dest_keys[key_idx:]:
if dk == '*':
new_dest_keys.append(k)
else:
new_dest_keys.append(dk)
set_value_by_path(data, new_dest_keys, v)
# Delete from source
for k in keys_to_move:
del data[k]
else:
# Navigate to next level
if key in data:
_move_value_recursive(
data[key], source_keys, dest_keys, key_idx + 1, exclude_keys
)
def maybe_snake_to_camel(snake_str: str, convert: bool = True) -> str:
"""Converts a snake_case string to CamelCase, if convert is True."""
if not convert:
return snake_str
return re.sub(r'_([a-zA-Z])', lambda match: match.group(1).upper(), snake_str)
def convert_to_dict(obj: object, convert_keys: bool = False) -> Any:
"""Recursively converts a given object to a dictionary.
If the object is a Pydantic model, it uses the model's `model_dump()` method.
Args:
obj: The object to convert.
convert_keys: Whether to convert the keys from snake case to camel case.
Returns:
A dictionary representation of the object, a list of objects if a list is
passed, or the object itself if it is not a dictionary, list, or Pydantic
model.
"""
if isinstance(obj, pydantic.BaseModel):
return convert_to_dict(obj.model_dump(exclude_none=True), convert_keys)
elif isinstance(obj, dict):
return {
maybe_snake_to_camel(key, convert_keys): convert_to_dict(value)
for key, value in obj.items()
}
elif isinstance(obj, list):
return [convert_to_dict(item, convert_keys) for item in obj]
else:
return obj
def _is_struct_type(annotation: type) -> bool:
"""Checks if the given annotation is list[dict[str, typing.Any]]
or typing.List[typing.Dict[str, typing.Any]].
This maps to Struct type in the API.
"""
outer_origin = get_origin(annotation)
outer_args = get_args(annotation)
if outer_origin is not list: # Python 3.9+ normalizes list
return False
if not outer_args or len(outer_args) != 1:
return False
inner_annotation = outer_args[0]
inner_origin = get_origin(inner_annotation)
inner_args = get_args(inner_annotation)
if inner_origin is not dict: # Python 3.9+ normalizes to dict
return False
if not inner_args or len(inner_args) != 2:
# dict should have exactly two type arguments
return False
# Check if the dict arguments are str and typing.Any
key_type, value_type = inner_args
return key_type is str and value_type is typing.Any
def _remove_extra_fields(model: Any, response: dict[str, object]) -> None:
"""Removes extra fields from the response that are not in the model.
Mutates the response in place.
"""
key_values = list(response.items())
for key, value in key_values:
# Need to convert to snake case to match model fields names
# ex: UsageMetadata
alias_map = {
field_info.alias: key for key, field_info in model.model_fields.items()
}
if key not in model.model_fields and key not in alias_map:
response.pop(key)
continue
key = alias_map.get(key, key)
annotation = model.model_fields[key].annotation
# Get the BaseModel if Optional
if typing.get_origin(annotation) is Union:
annotation = typing.get_args(annotation)[0]
# if dict, assume BaseModel but also check that field type is not dict
# example: FunctionCall.args
if isinstance(value, dict) and typing.get_origin(annotation) is not dict:
_remove_extra_fields(annotation, value)
elif isinstance(value, list):
if _is_struct_type(annotation):
continue
for item in value:
# assume a list of dict is list of BaseModel
if isinstance(item, dict):
_remove_extra_fields(typing.get_args(annotation)[0], item)
T = typing.TypeVar('T', bound='BaseModel')
def _pretty_repr(
obj: Any,
*,
indent_level: int = 0,
indent_delta: int = 2,
max_len: int = 100,
max_items: int = 5,
depth: int = 6,
visited: Optional[FrozenSet[int]] = None,
) -> str:
"""Returns a representation of the given object."""
if visited is None:
visited = frozenset()
obj_id = id(obj)
if obj_id in visited:
return '<... Circular reference ...>'
if depth < 0:
return '<... Max depth ...>'
visited = frozenset(list(visited) + [obj_id])
indent = ' ' * indent_level
next_indent_str = ' ' * (indent_level + indent_delta)
if isinstance(obj, pydantic.BaseModel):
cls_name = obj.__class__.__name__
items = []
# Sort fields for consistent output
fields = sorted(type(obj).model_fields)
for field_name in fields:
field_info = type(obj).model_fields[field_name]
if not field_info.repr: # Respect Field(repr=False)
continue
try:
value = getattr(obj, field_name)
except AttributeError:
continue
if value is None:
continue
value_repr = _pretty_repr(
value,
indent_level=indent_level + indent_delta,
indent_delta=indent_delta,
max_len=max_len,
max_items=max_items,
depth=depth - 1,
visited=visited,
)
items.append(f'{next_indent_str}{field_name}={value_repr}')
if not items:
return f'{cls_name}()'
return f'{cls_name}(\n' + ',\n'.join(items) + f'\n{indent})'
elif isinstance(obj, str):
if '\n' in obj:
escaped = obj.replace('"""', '\\"\\"\\"')
# Indent the multi-line string block contents
return f'"""{escaped}"""'
return repr(obj)
elif isinstance(obj, bytes):
if len(obj) > max_len:
return f"{repr(obj[:max_len-3])[:-1]}...'"
return repr(obj)
elif isinstance(obj, collections.abc.Mapping):
if not obj:
return '{}'
# Check if the next level of recursion for keys/values will exceed the depth limit.
if depth <= 0:
item_count_str = f"{len(obj)} item{'s' if len(obj) != 1 else ''}"
return f'{{<... {item_count_str} at Max depth ...>}}'
if len(obj) > max_items:
return f'<dict len={len(obj)}>'
items = []
try:
sorted_keys = sorted(obj.keys(), key=str)
except TypeError:
sorted_keys = list(obj.keys())
for k in sorted_keys:
v = obj[k]
k_repr = _pretty_repr(
k,
indent_level=indent_level + indent_delta,
indent_delta=indent_delta,
max_len=max_len,
max_items=max_items,
depth=depth - 1,
visited=visited,
)
v_repr = _pretty_repr(
v,
indent_level=indent_level + indent_delta,
indent_delta=indent_delta,
max_len=max_len,
max_items=max_items,
depth=depth - 1,
visited=visited,
)
items.append(f'{next_indent_str}{k_repr}: {v_repr}')
return f'{{\n' + ',\n'.join(items) + f'\n{indent}}}'
elif isinstance(obj, (list, tuple, set)):
return _format_collection(
obj,
indent_level=indent_level,
indent_delta=indent_delta,
max_len=max_len,
max_items=max_items,
depth=depth,
visited=visited,
)
else:
# Fallback to standard repr, indenting subsequent lines only
raw_repr = repr(obj)
# Replace newlines with newline + indent
return raw_repr.replace('\n', f'\n{next_indent_str}')
def _format_collection(
obj: Any,
*,
indent_level: int,
indent_delta: int,
max_len: int,
max_items: int,
depth: int,
visited: FrozenSet[int],
) -> str:
"""Formats a collection (list, tuple, set)."""
if isinstance(obj, list):
brackets = ('[', ']')
internal_obj = obj
elif isinstance(obj, tuple):
brackets = ('(', ')')
internal_obj = list(obj)
elif isinstance(obj, set):
internal_obj = list(obj)
if obj:
brackets = ('{', '}')
else:
brackets = ('set(', ')')
else:
raise ValueError(f'Unsupported collection type: {type(obj)}')
if not internal_obj:
return brackets[0] + brackets[1]
# If the call to _pretty_repr for elements will have depth < 0
if depth <= 0:
item_count_str = f"{len(internal_obj)} item{'s'*(len(internal_obj)!=1)}"
return f'{brackets[0]}<... {item_count_str} at Max depth ...>{brackets[1]}'
indent = ' ' * indent_level
next_indent_str = ' ' * (indent_level + indent_delta)
elements = []
num_to_show = min(len(internal_obj), max_items)
for i in range(num_to_show):
elem = internal_obj[i]
elements.append(
next_indent_str
+ _pretty_repr(
elem,
indent_level=indent_level + indent_delta,
indent_delta=indent_delta,
max_len=max_len,
max_items=max_items,
depth=depth - 1,
visited=visited,
)
)
if len(internal_obj) > max_items:
elements.append(
f'{next_indent_str}<... {len(internal_obj) - max_items} more items ...>'
)
return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
class BaseModel(pydantic.BaseModel):
model_config = pydantic.ConfigDict(
alias_generator=alias_generators.to_camel,
populate_by_name=True,
from_attributes=True,
protected_namespaces=(),
extra='forbid',
# This allows us to use arbitrary types in the model. E.g. PIL.Image.
arbitrary_types_allowed=True,
ser_json_bytes='base64',
val_json_bytes='base64',
ignored_types=(typing.TypeVar,),
)
@pydantic.model_validator(mode='before')
@classmethod
def _check_field_type_mismatches(cls, data: Any) -> Any:
"""Check for type mismatches and warn before Pydantic processes the data."""
# Handle both dict and Pydantic model inputs
if not isinstance(data, (dict, pydantic.BaseModel)):
return data
for field_name, field_info in cls.model_fields.items():
if isinstance(data, dict):
value = data.get(field_name)
else:
value = getattr(data, field_name, None)
if value is None:
continue
expected_type = field_info.annotation
origin = get_origin(expected_type)
if origin is Union:
args = get_args(expected_type)
non_none_types = [arg for arg in args if arg is not type(None)]
if len(non_none_types) == 1:
expected_type = non_none_types[0]
if (isinstance(expected_type, type) and
get_origin(expected_type) is None and
issubclass(expected_type, pydantic.BaseModel) and
isinstance(value, pydantic.BaseModel) and
not isinstance(value, expected_type)):
logger.warning(
f"Type mismatch in {cls.__name__}.{field_name}: "
f"expected {expected_type.__name__}, got {type(value).__name__}"
)
return data
def __repr__(self) -> str:
try:
return _pretty_repr(self)
except Exception:
return super().__repr__()
@classmethod
def _from_response(
cls: typing.Type[T],
*,
response: dict[str, object],
kwargs: dict[str, object],
) -> T:
# To maintain forward compatibility, we need to remove extra fields from
# the response.
# We will provide another mechanism to allow users to access these fields.
# For Agent Engine we don't want to call _remove_all_fields because the
# user may pass a dict that is not a subclass of BaseModel.
# If more modules require we skip this, we may want a different approach
should_skip_removing_fields = (
kwargs is not None
and 'config' in kwargs
and kwargs['config'] is not None
and isinstance(kwargs['config'], dict)
and 'include_all_fields' in kwargs['config']
and kwargs['config']['include_all_fields']
)
if not should_skip_removing_fields:
_remove_extra_fields(cls, response)
validated_response = cls.model_validate(response)
return validated_response
def to_json_dict(self) -> dict[str, object]:
return self.model_dump(exclude_none=True, mode='json')
class CaseInSensitiveEnum(str, enum.Enum):
"""Case insensitive enum."""
@classmethod
def _missing_(cls, value: Any) -> Any:
try:
return cls[value.upper()] # Try to access directly with uppercase
except KeyError:
try:
return cls[value.lower()] # Try to access directly with lowercase
except KeyError:
warnings.warn(f'{value} is not a valid {cls.__name__}')
try:
# Creating a enum instance based on the value
# We need to use super() to avoid infinite recursion.
unknown_enum_val = super().__new__(cls, value)
unknown_enum_val._name_ = str(value) # pylint: disable=protected-access
unknown_enum_val._value_ = value # pylint: disable=protected-access
return unknown_enum_val
except:
return None
def timestamped_unique_name() -> str:
"""Composes a timestamped unique name.
Returns:
A string representing a unique name.
"""
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
unique_id = uuid.uuid4().hex[0:5]
return f'{timestamp}_{unique_id}'
def encode_unserializable_types(data: dict[str, object]) -> dict[str, object]:
"""Converts unserializable types in dict to json.dumps() compatible types.
This function is called in models.py after calling convert_to_dict(). The
convert_to_dict() can convert pydantic object to dict. However, the input to
convert_to_dict() is dict mixed of pydantic object and nested dict(the output
of converters). So they may be bytes in the dict and they are out of
`ser_json_bytes` control in model_dump(mode='json') called in
`convert_to_dict`, as well as datetime deserialization in Pydantic json mode.
Returns:
A dictionary with json.dumps() incompatible type (e.g. bytes datetime)
to compatible type (e.g. base64 encoded string, isoformat date string).
"""
processed_data: dict[str, object] = {}
if not isinstance(data, dict):
return data
for key, value in data.items():
if isinstance(value, bytes):
processed_data[key] = base64.urlsafe_b64encode(value).decode('ascii')
elif isinstance(value, datetime.datetime):
processed_data[key] = value.isoformat()
elif isinstance(value, dict):
processed_data[key] = encode_unserializable_types(value)
elif isinstance(value, list):
if all(isinstance(v, bytes) for v in value):
processed_data[key] = [
base64.urlsafe_b64encode(v).decode('ascii') for v in value
]
if all(isinstance(v, datetime.datetime) for v in value):
processed_data[key] = [v.isoformat() for v in value]
else:
processed_data[key] = [encode_unserializable_types(v) for v in value]
else:
processed_data[key] = value
return processed_data
def experimental_warning(
message: str,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Experimental warning, only warns once."""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
warning_done = False
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
nonlocal warning_done
if not warning_done:
warning_done = True
warnings.warn(
message=message,
category=ExperimentalWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator
def _normalize_key_for_matching(key_str: str) -> str:
"""Normalizes a key for case-insensitive and snake/camel matching."""
return key_str.replace('_', '').lower()
def align_key_case(
target_dict: StringDict, update_dict: StringDict
) -> StringDict:
"""Aligns the keys of update_dict to the case of target_dict keys.
Args:
target_dict: The dictionary with the target key casing.
update_dict: The dictionary whose keys need to be aligned.
Returns:
A new dictionary with keys aligned to target_dict's key casing.
"""
aligned_update_dict: StringDict = {}
target_keys_map = {
_normalize_key_for_matching(key): key for key in target_dict.keys()
}
for key, value in update_dict.items():
normalized_update_key = _normalize_key_for_matching(key)
if normalized_update_key in target_keys_map:
aligned_key = target_keys_map[normalized_update_key]
else:
aligned_key = key
if isinstance(value, dict) and isinstance(
target_dict.get(aligned_key), dict
):
aligned_update_dict[aligned_key] = align_key_case(
target_dict[aligned_key], value
)
elif isinstance(value, list) and isinstance(
target_dict.get(aligned_key), list
):
# Direct assign as we treat update_dict list values as golden source.
aligned_update_dict[aligned_key] = value
else:
aligned_update_dict[aligned_key] = value
return aligned_update_dict
def recursive_dict_update(
target_dict: StringDict, update_dict: StringDict
) -> None:
"""Recursively updates a target dictionary with values from an update dictionary.
We don't enforce the updated dict values to have the same type with the
target_dict values except log warnings.
Users providing the update_dict should be responsible for constructing correct
data.
Args:
target_dict (dict): The dictionary to be updated.
update_dict (dict): The dictionary containing updates.
"""
# Python SDK http request may change in camel case or snake case:
# If the field is directly set via setv() function, then it is camel case;
# otherwise it is snake case.
# Align the update_dict key case to target_dict to ensure correct dict update.
aligned_update_dict = align_key_case(target_dict, update_dict)
for key, value in aligned_update_dict.items():
if (
key in target_dict
and isinstance(target_dict[key], dict)
and isinstance(value, dict)
):
recursive_dict_update(target_dict[key], value)
elif key in target_dict and not isinstance(target_dict[key], type(value)):
logger.warning(
f"Type mismatch for key '{key}'. Existing type:"
f' {type(target_dict[key])}, new type: {type(value)}. Overwriting.'
)
target_dict[key] = value
else:
target_dict[key] = value
def is_duck_type_of(obj: Any, cls: type[pydantic.BaseModel]) -> bool:
"""Checks if an object has all of the fields of a Pydantic model.
This is a duck-typing alternative to `isinstance` to solve dual-import
problems. It returns False for dictionaries, which should be handled by
`isinstance(obj, dict)`.
Args:
obj: The object to check.
cls: The Pydantic model class to duck-type against.
Returns:
True if the object has all the fields defined in the Pydantic model, False
otherwise.
"""
if isinstance(obj, dict) or not hasattr(cls, 'model_fields'):
return False
# Check if the object has all of the Pydantic model's defined fields.
all_matched = all(hasattr(obj, field) for field in cls.model_fields)
if not all_matched and isinstance(obj, pydantic.BaseModel):
# Check the other way around if obj is a Pydantic model.
# Check if the Pydantic model has all of the object's defined fields.
try:
obj_private = cls()
all_matched = all(hasattr(obj_private, f) for f in type(obj).model_fields)
except ValueError:
return False
return all_matched

View File

@@ -0,0 +1,679 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Extra utils depending on types that are shared between sync and async modules."""
import asyncio
import inspect
import io
import logging
import sys
import typing
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
import mimetypes
import os
import pydantic
from . import _common
from . import _mcp_utils
from . import _transformers as t
from . import errors
from . import types
from ._adapters import McpToGenAiToolAdapter
if sys.version_info >= (3, 10):
from types import UnionType
else:
UnionType = typing._UnionGenericAlias # type: ignore[attr-defined]
if typing.TYPE_CHECKING:
from mcp import ClientSession as McpClientSession
from mcp.types import Tool as McpTool
else:
McpClientSession: typing.Type = Any
McpTool: typing.Type = Any
try:
from mcp import ClientSession as McpClientSession
from mcp.types import Tool as McpTool
except ImportError:
McpClientSession = None
McpTool = None
_DEFAULT_MAX_REMOTE_CALLS_AFC = 10
logger = logging.getLogger('google_genai.models')
def _create_generate_content_config_model(
config: types.GenerateContentConfigOrDict,
) -> types.GenerateContentConfig:
if isinstance(config, dict):
return types.GenerateContentConfig(**config)
else:
return config
def _get_gcs_uri(
src: Union[str, types.BatchJobSourceOrDict]
) -> Optional[str]:
"""Extracts the first GCS URI from the source, if available."""
if isinstance(src, str) and src.startswith('gs://'):
return src
elif isinstance(src, dict) and src.get('gcs_uri'):
return src['gcs_uri'][0] if src['gcs_uri'] else None
elif isinstance(src, types.BatchJobSource) and src.gcs_uri:
return src.gcs_uri[0] if src.gcs_uri else None
return None
def _get_bigquery_uri(
src: Union[str, types.BatchJobSourceOrDict]
) -> Optional[str]:
"""Extracts the BigQuery URI from the source, if available."""
if isinstance(src, str) and src.startswith('bq://'):
return src
elif isinstance(src, dict) and src.get('bigquery_uri'):
return src['bigquery_uri']
elif isinstance(src, types.BatchJobSource) and src.bigquery_uri:
return src.bigquery_uri
return None
def format_destination(
src: Union[str, types.BatchJobSource],
config: Optional[types.CreateBatchJobConfig] = None,
) -> types.CreateBatchJobConfig:
"""Formats the destination uri based on the source uri for Vertex AI."""
if config is None:
config = types.CreateBatchJobConfig()
unique_name = None
if not config.display_name:
unique_name = _common.timestamped_unique_name()
config.display_name = f'genai_batch_job_{unique_name}'
if not config.dest:
gcs_source_uri = _get_gcs_uri(src)
bigquery_source_uri = _get_bigquery_uri(src)
if gcs_source_uri and gcs_source_uri.endswith('.jsonl'):
config.dest = f'{gcs_source_uri[:-6]}/dest'
elif bigquery_source_uri:
unique_name = unique_name or _common.timestamped_unique_name()
config.dest = f'{bigquery_source_uri}_dest_{unique_name}'
return config
def find_afc_incompatible_tool_indexes(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> list[int]:
"""Checks if the config contains any AFC incompatible tools.
A `types.Tool` object that contains `function_declarations` is considered a
non-AFC tool for this execution path.
Args:
config: The GenerateContentConfig to check for incompatible tools.
Returns:
A list of indexes of the incompatible tools in the config.
"""
if not config:
return []
config_model = _create_generate_content_config_model(config)
incompatible_tools_indexes: list[int] = []
if not config_model or not config_model.tools:
return incompatible_tools_indexes
for index, tool in enumerate(config_model.tools):
if not isinstance(tool, types.Tool):
continue
if tool.function_declarations:
incompatible_tools_indexes.append(index)
if tool.mcp_servers:
incompatible_tools_indexes.append(index)
return incompatible_tools_indexes
def get_function_map(
config: Optional[types.GenerateContentConfigOrDict] = None,
mcp_to_genai_tool_adapters: Optional[
dict[str, McpToGenAiToolAdapter]
] = None,
is_caller_method_async: bool = False,
) -> dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]]:
"""Returns a function map from the config."""
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]] = {}
if not config:
return function_map
config_model = _create_generate_content_config_model(config)
if config_model.tools:
for tool in config_model.tools:
if callable(tool):
if inspect.iscoroutinefunction(tool) and not is_caller_method_async:
raise errors.UnsupportedFunctionError(
f'Function {tool.__name__} is a coroutine function, which is not'
' supported for automatic function calling. Please manually'
f' invoke {tool.__name__} to get the function response.'
)
function_map[tool.__name__] = tool
if mcp_to_genai_tool_adapters:
if not is_caller_method_async:
raise errors.UnsupportedFunctionError(
'MCP tools are not supported in synchronous methods.'
)
for tool_name, _ in mcp_to_genai_tool_adapters.items():
if function_map.get(tool_name):
raise ValueError(
f'Tool {tool_name} is already defined for the request.'
)
function_map.update(mcp_to_genai_tool_adapters)
return function_map
def convert_number_values_for_dict_function_call_args(
args: _common.StringDict,
) -> _common.StringDict:
"""Converts float values in dict with no decimal to integers."""
return {
key: convert_number_values_for_function_call_args(value)
for key, value in args.items()
}
def convert_number_values_for_function_call_args(
args: Union[dict[str, object], list[object], object],
) -> Union[dict[str, object], list[object], object]:
"""Converts float values with no decimal to integers."""
if isinstance(args, float) and args.is_integer():
return int(args)
if isinstance(args, dict):
return {
key: convert_number_values_for_function_call_args(value)
for key, value in args.items()
}
if isinstance(args, list):
return [
convert_number_values_for_function_call_args(value) for value in args
]
return args
def is_annotation_pydantic_model(annotation: Any) -> bool:
try:
return inspect.isclass(annotation) and issubclass(
annotation, pydantic.BaseModel
)
# for python 3.10 and below, inspect.isclass(annotation) has inconsistent
# results with versions above. for example, inspect.isclass(dict[str, int]) is
# True in 3.10 and below but False in 3.11 and above.
except TypeError:
return False
def convert_if_exist_pydantic_model(
value: Any, annotation: Any, param_name: str, func_name: str
) -> Any:
if isinstance(value, dict) and is_annotation_pydantic_model(annotation):
try:
return annotation(**value)
except pydantic.ValidationError as e:
raise errors.UnknownFunctionCallArgumentError(
f'Failed to parse parameter {param_name} for function'
f' {func_name} from function call part because function call argument'
f' value {value} is not compatible with parameter annotation'
f' {annotation}, due to error {e}'
)
if isinstance(value, list) and get_origin(annotation) == list:
item_type = get_args(annotation)[0]
return [
convert_if_exist_pydantic_model(item, item_type, param_name, func_name)
for item in value
]
if isinstance(value, dict) and get_origin(annotation) == dict:
_, value_type = get_args(annotation)
return {
k: convert_if_exist_pydantic_model(v, value_type, param_name, func_name)
for k, v in value.items()
}
# example 1: typing.Union[int, float]
# example 2: int | float equivalent to UnionType[int, float]
if get_origin(annotation) in (Union, UnionType):
for arg in get_args(annotation):
if (
(get_args(arg) and get_origin(arg) is list)
or isinstance(value, arg)
or (isinstance(value, dict) and is_annotation_pydantic_model(arg))
):
try:
return convert_if_exist_pydantic_model(
value, arg, param_name, func_name
)
# do not raise here because there could be multiple pydantic model types
# in the union type.
except pydantic.ValidationError:
continue
# if none of the union type is matched, raise error
raise errors.UnknownFunctionCallArgumentError(
f'Failed to parse parameter {param_name} for function'
f' {func_name} from function call part because function call argument'
f' value {value} cannot be converted to parameter annotation'
f' {annotation}.'
)
# the only exception for value and annotation type to be different is int and
# float. see convert_number_values_for_function_call_args function for context
if isinstance(value, int) and annotation is float:
return value
if not isinstance(value, annotation):
raise errors.UnknownFunctionCallArgumentError(
f'Failed to parse parameter {param_name} for function {func_name} from'
f' function call part because function call argument value {value} is'
f' not compatible with parameter annotation {annotation}.'
)
return value
def convert_argument_from_function(
args: _common.StringDict, function: Callable[..., Any]
) -> _common.StringDict:
signature = inspect.signature(function)
func_name = function.__name__
converted_args = {}
for param_name, param in signature.parameters.items():
if param_name in args:
converted_args[param_name] = convert_if_exist_pydantic_model(
args[param_name],
param.annotation,
param_name,
func_name,
)
return converted_args
def invoke_function_from_dict_args(
args: _common.StringDict, function_to_invoke: Callable[..., Any]
) -> Any:
converted_args = convert_argument_from_function(args, function_to_invoke)
try:
return function_to_invoke(**converted_args)
except Exception as e:
raise errors.FunctionInvocationError(
f'Failed to invoke function {function_to_invoke.__name__} with'
f' converted arguments {converted_args} from model returned function'
f' call argument {args} because of error {e}'
)
async def invoke_function_from_dict_args_async(
args: _common.StringDict, function_to_invoke: Callable[..., Any]
) -> Any:
converted_args = convert_argument_from_function(args, function_to_invoke)
try:
return await function_to_invoke(**converted_args)
except Exception as e:
raise errors.FunctionInvocationError(
f'Failed to invoke function {function_to_invoke.__name__} with'
f' converted arguments {converted_args} from model returned function'
f' call argument {args} because of error {e}'
)
def get_function_response_parts(
response: types.GenerateContentResponse,
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
) -> list[types.Part]:
"""Returns the function response parts from the response."""
func_response_parts = []
if (
response.candidates is not None
and isinstance(response.candidates[0].content, types.Content)
and response.candidates[0].content.parts is not None
):
for part in response.candidates[0].content.parts:
if not part.function_call:
continue
func_name = part.function_call.name
if func_name is not None and part.function_call.args is not None:
func = function_map[func_name]
args = convert_number_values_for_dict_function_call_args(
part.function_call.args
)
func_response: _common.StringDict
try:
if not isinstance(func, McpToGenAiToolAdapter):
func_response = {
'result': invoke_function_from_dict_args(args, func)
}
except Exception as e: # pylint: disable=broad-except
func_response = {'error': str(e)}
func_response_part = types.Part.from_function_response(
name=func_name, response=func_response
)
func_response_parts.append(func_response_part)
return func_response_parts
async def get_function_response_parts_async(
response: types.GenerateContentResponse,
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
) -> list[types.Part]:
"""Returns the function response parts from the response."""
func_response_parts = []
if (
response.candidates is not None
and isinstance(response.candidates[0].content, types.Content)
and response.candidates[0].content.parts is not None
):
for part in response.candidates[0].content.parts:
if not part.function_call:
continue
func_name = part.function_call.name
if func_name is not None and part.function_call.args is not None:
func = function_map[func_name]
args = convert_number_values_for_dict_function_call_args(
part.function_call.args
)
func_response: _common.StringDict
try:
if isinstance(func, McpToGenAiToolAdapter):
mcp_tool_response = await func.call_tool(
types.FunctionCall(name=func_name, args=args)
)
if mcp_tool_response.isError:
func_response = {'error': mcp_tool_response}
else:
func_response = {'result': mcp_tool_response}
elif inspect.iscoroutinefunction(func):
func_response = {
'result': await invoke_function_from_dict_args_async(args, func)
}
else:
func_response = {
'result': await asyncio.to_thread(
invoke_function_from_dict_args, args, func
)
}
except Exception as e: # pylint: disable=broad-except
func_response = {'error': str(e)}
func_response_part = types.Part.from_function_response(
name=func_name, response=func_response
)
func_response_parts.append(func_response_part)
return func_response_parts
def should_disable_afc(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> bool:
"""Returns whether automatic function calling is enabled."""
if not config:
return False
config_model = _create_generate_content_config_model(config)
# If max_remote_calls is less or equal to 0, warn and disable AFC.
if (
config_model
and config_model.automatic_function_calling
and config_model.automatic_function_calling.maximum_remote_calls
is not None
and int(config_model.automatic_function_calling.maximum_remote_calls) <= 0
):
logger.warning(
'max_remote_calls in automatic_function_calling_config'
f' {config_model.automatic_function_calling.maximum_remote_calls} is'
' less than or equal to 0. Disabling automatic function calling.'
' Please set max_remote_calls to a positive integer.'
)
return True
# Default to enable AFC if not specified.
if (
not config_model.automatic_function_calling
or config_model.automatic_function_calling.disable is None
):
return False
if (
config_model.automatic_function_calling.disable
and config_model.automatic_function_calling.maximum_remote_calls
is not None
# exclude the case where max_remote_calls is set to 10 by default.
and 'maximum_remote_calls'
in config_model.automatic_function_calling.model_fields_set
and int(config_model.automatic_function_calling.maximum_remote_calls) > 0
):
logger.warning(
'`automatic_function_calling.disable` is set to `True`. And'
' `automatic_function_calling.maximum_remote_calls` is a'
' positive number'
f' {config_model.automatic_function_calling.maximum_remote_calls}.'
' Disabling automatic function calling. If you want to enable'
' automatic function calling, please set'
' `automatic_function_calling.disable` to `False` or leave it unset,'
' and set `automatic_function_calling.maximum_remote_calls` to a'
' positive integer or leave'
' `automatic_function_calling.maximum_remote_calls` unset.'
)
return config_model.automatic_function_calling.disable
def get_max_remote_calls_afc(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> int:
if not config:
return _DEFAULT_MAX_REMOTE_CALLS_AFC
"""Returns the remaining remote calls for automatic function calling."""
if should_disable_afc(config):
raise ValueError(
'automatic function calling is not enabled, but SDK is trying to get'
' max remote calls.'
)
config_model = _create_generate_content_config_model(config)
if (
not config_model.automatic_function_calling
or config_model.automatic_function_calling.maximum_remote_calls is None
):
return _DEFAULT_MAX_REMOTE_CALLS_AFC
return int(config_model.automatic_function_calling.maximum_remote_calls)
def raise_error_for_afc_incompatible_config(config: Optional[types.GenerateContentConfig]
) -> None:
"""Raises an error if the config is not compatible with AFC."""
if (
not config
or not config.tool_config
or not config.tool_config.function_calling_config
):
return
afc_config = config.automatic_function_calling
disable_afc_config = afc_config.disable if afc_config else False
stream_function_call = (
config.tool_config.function_calling_config.stream_function_call_arguments
)
if stream_function_call and not disable_afc_config:
raise ValueError(
'Running in streaming mode with stream_function_call_arguments'
' enabled, this feature is not compatible with automatic function'
' calling (AFC). Please set config.automatic_function_calling.disable'
' to True to disable AFC or leave config.tool_config.'
' function_calling_config.stream_function_call_arguments to be empty'
' or set to False to disable streaming function call arguments.'
)
def should_append_afc_history(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> bool:
if not config:
return True
config_model = _create_generate_content_config_model(config)
if not config_model.automatic_function_calling:
return True
return not config_model.automatic_function_calling.ignore_call_history
def parse_config_for_mcp_usage(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> Optional[types.GenerateContentConfig]:
"""Returns a parsed config with an appended MCP header if MCP tools or sessions are used."""
if not config:
return None
config_model = _create_generate_content_config_model(config)
# Create a copy of the config model with the tools field cleared since some
# tools may not be pickleable.
config_model_copy = config_model.model_copy(update={'tools': None})
config_model_copy.tools = config_model.tools
if config_model.tools and _mcp_utils.has_mcp_tool_usage(config_model.tools):
if config_model_copy.http_options is None:
config_model_copy.http_options = types.HttpOptions(headers={})
if config_model_copy.http_options.headers is None:
config_model_copy.http_options.headers = {}
_mcp_utils.set_mcp_usage_header(config_model_copy.http_options.headers)
return config_model_copy
async def parse_config_for_mcp_sessions(
config: Optional[types.GenerateContentConfigOrDict] = None,
) -> tuple[
Optional[types.GenerateContentConfig],
dict[str, McpToGenAiToolAdapter],
]:
"""Returns a parsed config with MCP sessions converted to GenAI tools.
Also returns a map of MCP tools to GenAI tool adapters to be used for AFC.
"""
mcp_to_genai_tool_adapters: dict[str, McpToGenAiToolAdapter] = {}
parsed_config = parse_config_for_mcp_usage(config)
if not parsed_config:
return None, mcp_to_genai_tool_adapters
# Create a copy of the config model with the tools field cleared as they will
# be replaced with the MCP tools converted to GenAI tools.
parsed_config_copy = parsed_config.model_copy(update={'tools': None})
if parsed_config.tools:
parsed_config_copy.tools = []
for tool in parsed_config.tools:
if McpClientSession is not None and isinstance(tool, McpClientSession):
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
tool, await tool.list_tools()
)
# Extend the config with the MCP session tools converted to GenAI tools.
parsed_config_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
for genai_tool in mcp_to_genai_tool_adapter.tools:
if genai_tool.function_declarations:
for function_declaration in genai_tool.function_declarations:
if function_declaration.name:
if mcp_to_genai_tool_adapters.get(function_declaration.name):
raise ValueError(
f'Tool {function_declaration.name} is already defined for'
' the request.'
)
mcp_to_genai_tool_adapters[function_declaration.name] = (
mcp_to_genai_tool_adapter
)
else:
parsed_config_copy.tools.append(tool)
return parsed_config_copy, mcp_to_genai_tool_adapters
def append_chunk_contents(
contents: Union[types.ContentListUnion, types.ContentListUnionDict],
chunk: types.GenerateContentResponse,
) -> Union[types.ContentListUnion, types.ContentListUnionDict]:
"""Appends the contents of the chunk to the contents list and returns it."""
if chunk is not None and chunk.candidates is not None:
chunk_content = chunk.candidates[0].content
contents = t.t_contents(contents) # type: ignore[assignment]
if isinstance(contents, list) and chunk_content is not None:
contents.append(chunk_content) # type: ignore[arg-type]
return contents
def prepare_resumable_upload(
file: Union[str, os.PathLike[str], io.IOBase],
user_http_options: Optional[types.HttpOptionsOrDict] = None,
user_mime_type: Optional[str] = None,
) -> tuple[
types.HttpOptions,
int,
str,
]:
"""Prepares the HTTP options, file bytes size and mime type for a resumable upload.
This function inspects a file (from a path or an in-memory object) to
determine its size and MIME type. It then constructs the necessary HTTP
headers and options required to initiate a resumable upload session.
"""
size_bytes = None
mime_type = user_mime_type
if isinstance(file, io.IOBase):
if mime_type is None:
raise ValueError(
'Unknown mime type: Could not determine the mimetype for your'
' file\n please set the `mime_type` argument'
)
if hasattr(file, 'mode'):
if 'b' not in file.mode:
raise ValueError('The file must be opened in binary mode.')
offset = file.tell()
file.seek(0, os.SEEK_END)
size_bytes = file.tell() - offset
file.seek(offset, os.SEEK_SET)
else:
fs_path = os.fspath(file)
if not fs_path or not os.path.isfile(fs_path):
raise FileNotFoundError(f'{file} is not a valid file path.')
size_bytes = os.path.getsize(fs_path)
if mime_type is None:
mime_type, _ = mimetypes.guess_type(fs_path)
if mime_type is None:
raise ValueError(
'Unknown mime type: Could not determine the mimetype for your'
' file\n please set the `mime_type` argument'
)
http_options: types.HttpOptions
if user_http_options:
if isinstance(user_http_options, dict):
user_http_options = types.HttpOptions(**user_http_options)
http_options = user_http_options
http_options.api_version = ''
http_options.headers = {
'Content-Type': 'application/json',
'X-Goog-Upload-Protocol': 'resumable',
'X-Goog-Upload-Command': 'start',
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
}
else:
http_options = types.HttpOptions(
api_version='',
headers={
'Content-Type': 'application/json',
'X-Goog-Upload-Protocol': 'resumable',
'X-Goog-Upload-Command': 'start',
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
},
)
if isinstance(file, (str, os.PathLike)):
if http_options.headers is None:
http_options.headers = {}
http_options.headers['X-Goog-Upload-File-Name'] = os.path.basename(file)
return http_options, size_bytes, mime_type

View File

@@ -0,0 +1,120 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import typing as _t
from . import types
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
from ._utils import file_from_path
from ._client import (
Client,
Stream,
Timeout,
Transport,
AsyncClient,
AsyncStream,
RequestOptions,
GeminiNextGenAPIClient,
AsyncGeminiNextGenAPIClient,
)
from ._models import BaseModel
from ._version import __title__, __version__
from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse
from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS
from ._exceptions import (
APIError,
ConflictError,
NotFoundError,
APIStatusError,
RateLimitError,
APITimeoutError,
BadRequestError,
APIConnectionError,
AuthenticationError,
InternalServerError,
PermissionDeniedError,
UnprocessableEntityError,
APIResponseValidationError,
GeminiNextGenAPIClientError,
)
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
from ._client_adapter import GeminiNextGenAPIClientAdapter, AsyncGeminiNextGenAPIClientAdapter
__all__ = [
"types",
"__version__",
"__title__",
"NoneType",
"Transport",
"ProxiesTypes",
"NotGiven",
"NOT_GIVEN",
"not_given",
"Omit",
"omit",
"GeminiNextGenAPIClientError",
"APIError",
"APIStatusError",
"APITimeoutError",
"APIConnectionError",
"APIResponseValidationError",
"BadRequestError",
"AuthenticationError",
"PermissionDeniedError",
"NotFoundError",
"ConflictError",
"UnprocessableEntityError",
"RateLimitError",
"InternalServerError",
"Timeout",
"RequestOptions",
"Client",
"AsyncClient",
"Stream",
"AsyncStream",
"GeminiNextGenAPIClient",
"AsyncGeminiNextGenAPIClient",
"file_from_path",
"BaseModel",
"DEFAULT_TIMEOUT",
"DEFAULT_MAX_RETRIES",
"DEFAULT_CONNECTION_LIMITS",
"DefaultHttpxClient",
"DefaultAsyncHttpxClient",
"DefaultAioHttpClient",
"AsyncGeminiNextGenAPIClientAdapter",
"GeminiNextGenAPIClientAdapter"
]
if not _t.TYPE_CHECKING:
from ._utils._resources_proxy import resources as resources
_setup_logging()
# Update the __module__ attribute for exported symbols so that
# error messages point to this module instead of the module
# it was originally defined in, e.g.
# google.genai._interactions._exceptions.NotFoundError -> google.genai._interactions.NotFoundError
__locals = locals()
for __name in __all__:
if not __name.startswith("__"):
try:
__locals[__name].__module__ = "google.genai._interactions"
except (TypeError, AttributeError):
# Some of our exported symbols are builtins which we can't set attributes for.
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,600 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, Mapping
from typing_extensions import Self, override
import httpx
from . import _exceptions
from ._qs import Querystring
from ._types import (
Omit,
Headers,
Timeout,
NotGiven,
Transport,
ProxiesTypes,
RequestOptions,
not_given,
)
from ._utils import is_given
from ._compat import cached_property
from ._models import FinalRequestOptions
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError
from ._base_client import (
DEFAULT_MAX_RETRIES,
SyncAPIClient,
AsyncAPIClient,
)
from ._client_adapter import GeminiNextGenAPIClientAdapter, AsyncGeminiNextGenAPIClientAdapter
if TYPE_CHECKING:
from .resources import webhooks, interactions
from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
from .resources.interactions import InteractionsResource, AsyncInteractionsResource
__all__ = [
"Timeout",
"Transport",
"ProxiesTypes",
"RequestOptions",
"GeminiNextGenAPIClient",
"AsyncGeminiNextGenAPIClient",
"Client",
"AsyncClient",
]
class GeminiNextGenAPIClient(SyncAPIClient):
# client options
api_key: str | None
api_version: str
client_adapter: GeminiNextGenAPIClientAdapter | None
def __init__(
self,
*,
api_key: str | None = None,
api_version: str | None = "v1beta",
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
# Configure a custom httpx client.
# We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
# See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
http_client: httpx.Client | None = None,
client_adapter: GeminiNextGenAPIClientAdapter | None = None,
# Enable or disable schema validation for data returned by the API.
# When enabled an error APIResponseValidationError is raised
# if the API responds with invalid data for the expected schema.
#
# This parameter may be removed or changed in the future.
# If you rely on this feature, please open a GitHub issue
# outlining your use-case to help us decide if it should be
# part of our public interface in the future.
_strict_response_validation: bool = False,
) -> None:
"""Construct a new synchronous GeminiNextGenAPIClient client instance.
This automatically infers the `api_key` argument from the `GEMINI_API_KEY` environment variable if it is not provided.
"""
if api_key is None:
api_key = os.environ.get("GEMINI_API_KEY")
self.api_key = api_key
if api_version is None:
api_version = "v1beta"
self.api_version = api_version
if base_url is None:
base_url = os.environ.get("GEMINI_NEXT_GEN_API_BASE_URL")
if base_url is None:
base_url = f"https://generativelanguage.googleapis.com"
self.client_adapter = client_adapter
super().__init__(
version=__version__,
base_url=base_url,
max_retries=max_retries,
timeout=timeout,
http_client=http_client,
custom_headers=default_headers,
custom_query=default_query,
_strict_response_validation=_strict_response_validation,
)
self._default_stream_cls = Stream
@cached_property
def interactions(self) -> InteractionsResource:
from .resources.interactions import InteractionsResource
return InteractionsResource(self)
@cached_property
def webhooks(self) -> WebhooksResource:
from .resources.webhooks import WebhooksResource
return WebhooksResource(self)
@cached_property
def with_raw_response(self) -> GeminiNextGenAPIClientWithRawResponse:
return GeminiNextGenAPIClientWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> GeminiNextGenAPIClientWithStreamedResponse:
return GeminiNextGenAPIClientWithStreamedResponse(self)
@property
@override
def qs(self) -> Querystring:
return Querystring(array_format="comma")
@property
@override
def auth_headers(self) -> dict[str, str]:
api_key = self.api_key
if api_key is None:
return {}
return {"x-goog-api-key": api_key}
@property
@override
def default_headers(self) -> dict[str, str | Omit]:
return {
**super().default_headers,
**self._custom_headers,
}
@override
def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
if headers.get("Authorization") or custom_headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit):
return
if self.api_key and headers.get("x-goog-api-key"):
return
if custom_headers.get("x-goog-api-key") or isinstance(custom_headers.get("x-goog-api-key"), Omit):
return
raise TypeError(
'"Could not resolve authentication method. Expected the api_key to be set. Or for the `x-goog-api-key` headers to be explicitly omitted"'
)
@override
def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
if not self.client_adapter or not self.client_adapter.is_vertex_ai():
return options
headers = options.headers or {}
has_auth = headers.get("Authorization") or headers.get("x-goog-api-key") # pytype: disable=attribute-error
if has_auth:
return options
adapted_headers = self.client_adapter.get_auth_headers()
if adapted_headers:
options.headers = {
**adapted_headers,
**headers
}
return options
def copy(
self,
*,
api_key: str | None = None,
api_version: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
set_default_query: Mapping[str, object] | None = None,
client_adapter: GeminiNextGenAPIClientAdapter | None = None,
_extra_kwargs: Mapping[str, Any] = {},
) -> Self:
"""
Create a new client instance re-using the same options given to the current client with optional overriding.
"""
if default_headers is not None and set_default_headers is not None:
raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
if default_query is not None and set_default_query is not None:
raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
headers = self._custom_headers
if default_headers is not None:
headers = {**headers, **default_headers}
elif set_default_headers is not None:
headers = set_default_headers
params = self._custom_query
if default_query is not None:
params = {**params, **default_query}
elif set_default_query is not None:
params = set_default_query
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
api_version=api_version or self.api_version,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
max_retries=max_retries if is_given(max_retries) else self.max_retries,
default_headers=headers,
default_query=params,
client_adapter=self.client_adapter or client_adapter,
**_extra_kwargs,
)
# Alias for `copy` for nicer inline usage, e.g.
# client.with_options(timeout=10).foo.create(...)
with_options = copy
def _get_api_version_path_param(self) -> str:
return self.api_version
@override
def _make_status_error(
self,
err_msg: str,
*,
body: object,
response: httpx.Response,
) -> APIStatusError:
if response.status_code == 400:
return _exceptions.BadRequestError(err_msg, response=response, body=body)
if response.status_code == 401:
return _exceptions.AuthenticationError(err_msg, response=response, body=body)
if response.status_code == 403:
return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
if response.status_code == 404:
return _exceptions.NotFoundError(err_msg, response=response, body=body)
if response.status_code == 409:
return _exceptions.ConflictError(err_msg, response=response, body=body)
if response.status_code == 422:
return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
if response.status_code == 429:
return _exceptions.RateLimitError(err_msg, response=response, body=body)
if response.status_code >= 500:
return _exceptions.InternalServerError(err_msg, response=response, body=body)
return APIStatusError(err_msg, response=response, body=body)
class AsyncGeminiNextGenAPIClient(AsyncAPIClient):
# client options
api_key: str | None
api_version: str
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None
def __init__(
self,
*,
api_key: str | None = None,
api_version: str | None = "v1beta",
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
# Configure a custom httpx client.
# We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
# See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
http_client: httpx.AsyncClient | None = None,
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None = None,
# Enable or disable schema validation for data returned by the API.
# When enabled an error APIResponseValidationError is raised
# if the API responds with invalid data for the expected schema.
#
# This parameter may be removed or changed in the future.
# If you rely on this feature, please open a GitHub issue
# outlining your use-case to help us decide if it should be
# part of our public interface in the future.
_strict_response_validation: bool = False,
) -> None:
"""Construct a new async AsyncGeminiNextGenAPIClient client instance.
This automatically infers the `api_key` argument from the `GEMINI_API_KEY` environment variable if it is not provided.
"""
if api_key is None:
api_key = os.environ.get("GEMINI_API_KEY")
self.api_key = api_key
if api_version is None:
api_version = "v1beta"
self.api_version = api_version
if base_url is None:
base_url = os.environ.get("GEMINI_NEXT_GEN_API_BASE_URL")
if base_url is None:
base_url = f"https://generativelanguage.googleapis.com"
self.client_adapter = client_adapter
super().__init__(
version=__version__,
base_url=base_url,
max_retries=max_retries,
timeout=timeout,
http_client=http_client,
custom_headers=default_headers,
custom_query=default_query,
_strict_response_validation=_strict_response_validation,
)
self._default_stream_cls = AsyncStream
@cached_property
def interactions(self) -> AsyncInteractionsResource:
from .resources.interactions import AsyncInteractionsResource
return AsyncInteractionsResource(self)
@cached_property
def webhooks(self) -> AsyncWebhooksResource:
from .resources.webhooks import AsyncWebhooksResource
return AsyncWebhooksResource(self)
@cached_property
def with_raw_response(self) -> AsyncGeminiNextGenAPIClientWithRawResponse:
return AsyncGeminiNextGenAPIClientWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncGeminiNextGenAPIClientWithStreamedResponse:
return AsyncGeminiNextGenAPIClientWithStreamedResponse(self)
@property
@override
def qs(self) -> Querystring:
return Querystring(array_format="comma")
@property
@override
def auth_headers(self) -> dict[str, str]:
api_key = self.api_key
if api_key is None:
return {}
return {"x-goog-api-key": api_key}
@property
@override
def default_headers(self) -> dict[str, str | Omit]:
return {
**super().default_headers,
**self._custom_headers,
}
@override
def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
if headers.get("Authorization") or custom_headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit):
return
if self.api_key and headers.get("x-goog-api-key"):
return
if custom_headers.get("x-goog-api-key") or isinstance(custom_headers.get("x-goog-api-key"), Omit):
return
raise TypeError(
'"Could not resolve authentication method. Expected the api_key to be set. Or for the `x-goog-api-key` headers to be explicitly omitted"'
)
@override
async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
if not self.client_adapter or not self.client_adapter.is_vertex_ai():
return options
headers = options.headers or {}
has_auth = headers.get("Authorization") or headers.get("x-goog-api-key") # pytype: disable=attribute-error
if has_auth:
return options
adapted_headers = await self.client_adapter.async_get_auth_headers()
if adapted_headers:
options.headers = {
**adapted_headers,
**headers
}
return options
def copy(
self,
*,
api_key: str | None = None,
api_version: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
set_default_query: Mapping[str, object] | None = None,
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None = None,
_extra_kwargs: Mapping[str, Any] = {},
) -> Self:
"""
Create a new client instance re-using the same options given to the current client with optional overriding.
"""
if default_headers is not None and set_default_headers is not None:
raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
if default_query is not None and set_default_query is not None:
raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
headers = self._custom_headers
if default_headers is not None:
headers = {**headers, **default_headers}
elif set_default_headers is not None:
headers = set_default_headers
params = self._custom_query
if default_query is not None:
params = {**params, **default_query}
elif set_default_query is not None:
params = set_default_query
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
api_version=api_version or self.api_version,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
max_retries=max_retries if is_given(max_retries) else self.max_retries,
default_headers=headers,
default_query=params,
client_adapter=self.client_adapter or client_adapter,
**_extra_kwargs,
)
# Alias for `copy` for nicer inline usage, e.g.
# client.with_options(timeout=10).foo.create(...)
with_options = copy
def _get_api_version_path_param(self) -> str:
return self.api_version
@override
def _make_status_error(
self,
err_msg: str,
*,
body: object,
response: httpx.Response,
) -> APIStatusError:
if response.status_code == 400:
return _exceptions.BadRequestError(err_msg, response=response, body=body)
if response.status_code == 401:
return _exceptions.AuthenticationError(err_msg, response=response, body=body)
if response.status_code == 403:
return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
if response.status_code == 404:
return _exceptions.NotFoundError(err_msg, response=response, body=body)
if response.status_code == 409:
return _exceptions.ConflictError(err_msg, response=response, body=body)
if response.status_code == 422:
return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
if response.status_code == 429:
return _exceptions.RateLimitError(err_msg, response=response, body=body)
if response.status_code >= 500:
return _exceptions.InternalServerError(err_msg, response=response, body=body)
return APIStatusError(err_msg, response=response, body=body)
class GeminiNextGenAPIClientWithRawResponse:
_client: GeminiNextGenAPIClient
def __init__(self, client: GeminiNextGenAPIClient) -> None:
self._client = client
@cached_property
def interactions(self) -> interactions.InteractionsResourceWithRawResponse:
from .resources.interactions import InteractionsResourceWithRawResponse
return InteractionsResourceWithRawResponse(self._client.interactions)
@cached_property
def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse:
from .resources.webhooks import WebhooksResourceWithRawResponse
return WebhooksResourceWithRawResponse(self._client.webhooks)
class AsyncGeminiNextGenAPIClientWithRawResponse:
_client: AsyncGeminiNextGenAPIClient
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
self._client = client
@cached_property
def interactions(self) -> interactions.AsyncInteractionsResourceWithRawResponse:
from .resources.interactions import AsyncInteractionsResourceWithRawResponse
return AsyncInteractionsResourceWithRawResponse(self._client.interactions)
@cached_property
def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse:
from .resources.webhooks import AsyncWebhooksResourceWithRawResponse
return AsyncWebhooksResourceWithRawResponse(self._client.webhooks)
class GeminiNextGenAPIClientWithStreamedResponse:
_client: GeminiNextGenAPIClient
def __init__(self, client: GeminiNextGenAPIClient) -> None:
self._client = client
@cached_property
def interactions(self) -> interactions.InteractionsResourceWithStreamingResponse:
from .resources.interactions import InteractionsResourceWithStreamingResponse
return InteractionsResourceWithStreamingResponse(self._client.interactions)
@cached_property
def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse:
from .resources.webhooks import WebhooksResourceWithStreamingResponse
return WebhooksResourceWithStreamingResponse(self._client.webhooks)
class AsyncGeminiNextGenAPIClientWithStreamedResponse:
_client: AsyncGeminiNextGenAPIClient
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
self._client = client
@cached_property
def interactions(self) -> interactions.AsyncInteractionsResourceWithStreamingResponse:
from .resources.interactions import AsyncInteractionsResourceWithStreamingResponse
return AsyncInteractionsResourceWithStreamingResponse(self._client.interactions)
@cached_property
def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse:
from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse
return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks)
Client = GeminiNextGenAPIClient
AsyncClient = AsyncGeminiNextGenAPIClient

View File

@@ -0,0 +1,48 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from abc import ABC, abstractmethod
__all__ = [
"GeminiNextGenAPIClientAdapter",
"AsyncGeminiNextGenAPIClientAdapter"
]
class BaseGeminiNextGenAPIClientAdapter(ABC):
@abstractmethod
def is_vertex_ai(self) -> bool:
...
@abstractmethod
def get_project(self) -> str | None:
...
@abstractmethod
def get_location(self) -> str | None:
...
class AsyncGeminiNextGenAPIClientAdapter(BaseGeminiNextGenAPIClientAdapter):
@abstractmethod
async def async_get_auth_headers(self) -> dict[str, str] | None:
...
class GeminiNextGenAPIClientAdapter(BaseGeminiNextGenAPIClientAdapter):
@abstractmethod
def get_auth_headers(self) -> dict[str, str] | None:
...

View File

@@ -0,0 +1,241 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
from typing_extensions import Self, Literal, TypedDict
import pydantic
from pydantic.fields import FieldInfo
from ._types import IncEx, StrBytesIntFloat
_T = TypeVar("_T")
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
# --------------- Pydantic v2, v3 compatibility ---------------
# Pyright incorrectly reports some of our functions as overriding a method when they don't
# pyright: reportIncompatibleMethodOverride=false
PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
if TYPE_CHECKING:
def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001
...
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001
...
def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001
...
def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001
...
def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001
...
def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001
...
def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001
...
else:
# v1 re-exports
if PYDANTIC_V1:
from pydantic.typing import (
get_args as get_args,
is_union as is_union,
get_origin as get_origin,
is_typeddict as is_typeddict,
is_literal_type as is_literal_type,
)
from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
else:
from ._utils import (
get_args as get_args,
is_union as is_union,
get_origin as get_origin,
parse_date as parse_date,
is_typeddict as is_typeddict,
parse_datetime as parse_datetime,
is_literal_type as is_literal_type,
)
# refactored config
if TYPE_CHECKING:
from pydantic import ConfigDict as ConfigDict
else:
if PYDANTIC_V1:
# TODO: provide an error message here?
ConfigDict = None
else:
from pydantic import ConfigDict as ConfigDict
# renamed methods / properties
def parse_obj(model: type[_ModelT], value: object) -> _ModelT:
if PYDANTIC_V1:
return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
else:
return model.model_validate(value)
def field_is_required(field: FieldInfo) -> bool:
if PYDANTIC_V1:
return field.required # type: ignore
return field.is_required()
def field_get_default(field: FieldInfo) -> Any:
value = field.get_default()
if PYDANTIC_V1:
return value
from pydantic_core import PydanticUndefined
if value == PydanticUndefined:
return None
return value
def field_outer_type(field: FieldInfo) -> Any:
if PYDANTIC_V1:
return field.outer_type_ # type: ignore
return field.annotation
def get_model_config(model: type[pydantic.BaseModel]) -> Any:
if PYDANTIC_V1:
return model.__config__ # type: ignore
return model.model_config
def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
if PYDANTIC_V1:
return model.__fields__ # type: ignore
return model.model_fields
def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
if PYDANTIC_V1:
return model.copy(deep=deep) # type: ignore
return model.model_copy(deep=deep)
def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
if PYDANTIC_V1:
return model.json(indent=indent) # type: ignore
return model.model_dump_json(indent=indent)
class _ModelDumpKwargs(TypedDict, total=False):
by_alias: bool
def model_dump(
model: pydantic.BaseModel,
*,
exclude: IncEx | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
kwargs: _ModelDumpKwargs = {}
if by_alias is not None:
kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
**kwargs,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
if PYDANTIC_V1:
return model.parse_obj(data) # pyright: ignore[reportDeprecated]
return model.model_validate(data)
# generic models
if TYPE_CHECKING:
class GenericModel(pydantic.BaseModel): ...
else:
if PYDANTIC_V1:
import pydantic.generics
class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
else:
# there no longer needs to be a distinction in v2 but
# we still have to create our own subclass to avoid
# inconsistent MRO ordering errors
class GenericModel(pydantic.BaseModel): ...
# cached properties
if TYPE_CHECKING:
cached_property = property
# we define a separate type (copied from typeshed)
# that represents that `cached_property` is `set`able
# at runtime, which differs from `@property`.
#
# this is a separate type as editors likely special case
# `@property` and we don't want to cause issues just to have
# more helpful internal types.
class typed_cached_property(Generic[_T]):
func: Callable[[Any], _T]
attrname: str | None
def __init__(self, func: Callable[[Any], _T]) -> None: ...
@overload
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...
@overload
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ...
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self:
raise NotImplementedError()
def __set_name__(self, owner: type[Any], name: str) -> None: ...
# __set__ is not defined at runtime, but @cached_property is designed to be settable
def __set__(self, instance: object, value: _T) -> None: ...
else:
from functools import cached_property as cached_property
typed_cached_property = cached_property

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import httpx
RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response"
OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
# default timeout is 1 minute
DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
DEFAULT_MAX_RETRIES = 2
DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
INITIAL_RETRY_DELAY = 0.5
MAX_RETRY_DELAY = 8.0

View File

@@ -0,0 +1,123 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal
import httpx
__all__ = [
"BadRequestError",
"AuthenticationError",
"PermissionDeniedError",
"NotFoundError",
"ConflictError",
"UnprocessableEntityError",
"RateLimitError",
"InternalServerError",
]
class GeminiNextGenAPIClientError(Exception):
pass
class APIError(GeminiNextGenAPIClientError):
message: str
request: httpx.Request
body: object | None
"""The API response body.
If the API responded with a valid JSON structure then this property will be the
decoded result.
If it isn't a valid JSON structure then this will be the raw response.
If there was no response associated with this error then it will be `None`.
"""
def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002
super().__init__(message)
self.request = request
self.message = message
self.body = body
class APIResponseValidationError(APIError):
response: httpx.Response
status_code: int
def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None:
super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body)
self.response = response
self.status_code = response.status_code
class APIStatusError(APIError):
"""Raised when an API response has a status code of 4xx or 5xx."""
response: httpx.Response
status_code: int
def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None:
super().__init__(message, response.request, body=body)
self.response = response
self.status_code = response.status_code
class APIConnectionError(APIError):
def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None:
super().__init__(message, request, body=None)
class APITimeoutError(APIConnectionError):
def __init__(self, request: httpx.Request) -> None:
super().__init__(message="Request timed out.", request=request)
class BadRequestError(APIStatusError):
status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride]
class AuthenticationError(APIStatusError):
status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride]
class PermissionDeniedError(APIStatusError):
status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride]
class NotFoundError(APIStatusError):
status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride]
class ConflictError(APIStatusError):
status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride]
class UnprocessableEntityError(APIStatusError):
status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride]
class RateLimitError(APIStatusError):
status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride]
class InternalServerError(APIStatusError):
pass

View File

@@ -0,0 +1,139 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# mypy: ignore-errors
from __future__ import annotations
import io
import os
import pathlib
from typing import overload
from typing_extensions import TypeGuard
import anyio
from ._types import (
FileTypes,
FileContent,
RequestFiles,
HttpxFileTypes,
Base64FileInput,
HttpxFileContent,
HttpxRequestFiles,
)
from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
def is_file_content(obj: object) -> TypeGuard[FileContent]:
return (
isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
)
def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
if not is_file_content(obj):
prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
raise RuntimeError(
f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
) from None
@overload
def to_httpx_files(files: None) -> None: ...
@overload
def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
if files is None:
return None
if is_mapping_t(files):
files = {key: _transform_file(file) for key, file in files.items()}
elif is_sequence_t(files):
files = [(key, _transform_file(file)) for key, file in files]
else:
raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
return files
def _transform_file(file: FileTypes) -> HttpxFileTypes:
if is_file_content(file):
if isinstance(file, os.PathLike):
path = pathlib.Path(file)
return (path.name, path.read_bytes())
return file
if is_tuple_t(file):
return (file[0], read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
def read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return pathlib.Path(file).read_bytes()
return file
@overload
async def async_to_httpx_files(files: None) -> None: ...
@overload
async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
if files is None:
return None
if is_mapping_t(files):
files = {key: await _async_transform_file(file) for key, file in files.items()}
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
return files
async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
if is_file_content(file):
if isinstance(file, os.PathLike):
path = anyio.Path(file)
return (path.name, await path.read_bytes())
return file
if is_tuple_t(file):
return (file[0], await async_read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
async def async_read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return await anyio.Path(file).read_bytes()
return file

View File

@@ -0,0 +1,888 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# mypy: ignore-errors
from __future__ import annotations
import os
import inspect
import weakref
from typing import (
IO,
TYPE_CHECKING,
Any,
Type,
Union,
Generic,
TypeVar,
Callable,
Iterable,
Optional,
AsyncIterable,
cast,
)
from datetime import date, datetime
from typing_extensions import (
List,
Unpack,
Literal,
ClassVar,
Protocol,
Required,
ParamSpec,
TypedDict,
TypeGuard,
final,
override,
runtime_checkable,
)
import pydantic
from pydantic.fields import FieldInfo
from ._types import (
Body,
IncEx,
Query,
ModelT,
Headers,
Timeout,
NotGiven,
AnyMapping,
HttpxRequestFiles,
)
from ._utils import (
PropertyInfo,
is_list,
is_given,
json_safe,
lru_cache,
is_mapping,
parse_date,
coerce_boolean,
parse_datetime,
strip_not_given,
extract_type_arg,
is_annotated_type,
is_type_alias_type,
strip_annotated_type,
)
from ._compat import (
PYDANTIC_V1,
ConfigDict,
GenericModel as BaseGenericModel,
get_args,
is_union,
parse_obj,
get_origin,
is_literal_type,
get_model_config,
get_model_fields,
field_get_default,
)
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
__all__ = ["BaseModel", "GenericModel"]
_T = TypeVar("_T")
_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
P = ParamSpec("P")
@runtime_checkable
class _ConfigProtocol(Protocol):
allow_population_by_field_name: bool
class BaseModel(pydantic.BaseModel):
if PYDANTIC_V1:
@property
@override
def model_fields_set(self) -> set[str]:
# a forwards-compat shim for pydantic v2
return self.__fields_set__ # type: ignore
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
extra: Any = pydantic.Extra.allow # type: ignore
else:
model_config: ClassVar[ConfigDict] = ConfigDict(
extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
)
def to_dict(
self,
*,
mode: Literal["json", "python"] = "python",
use_api_names: bool = True,
exclude_unset: bool = True,
exclude_defaults: bool = False,
exclude_none: bool = False,
warnings: bool = True,
) -> dict[str, object]:
"""Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
By default, fields that were not set by the API will not be included,
and keys will match the API response, *not* the property names from the model.
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
Args:
mode:
If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`.
If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)`
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
exclude_none: Whether to exclude fields that have a value of `None` from the output.
warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2.
"""
return self.model_dump(
mode=mode,
by_alias=use_api_names,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
warnings=warnings,
)
def to_json(
self,
*,
indent: int | None = 2,
use_api_names: bool = True,
exclude_unset: bool = True,
exclude_defaults: bool = False,
exclude_none: bool = False,
warnings: bool = True,
) -> str:
"""Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation).
By default, fields that were not set by the API will not be included,
and keys will match the API response, *not* the property names from the model.
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
Args:
indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2`
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that have the default value.
exclude_none: Whether to exclude fields that have a value of `None`.
warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2.
"""
return self.model_dump_json(
indent=indent,
by_alias=use_api_names,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
warnings=warnings,
)
@override
def __str__(self) -> str:
# mypy complains about an invalid self arg
return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
# Override the 'construct' method in a way that supports recursive parsing without validation.
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
@classmethod
@override
def construct( # pyright: ignore[reportIncompatibleMethodOverride]
__cls: Type[ModelT],
_fields_set: set[str] | None = None,
**values: object,
) -> ModelT:
m = __cls.__new__(__cls)
fields_values: dict[str, object] = {}
config = get_model_config(__cls)
populate_by_name = (
config.allow_population_by_field_name
if isinstance(config, _ConfigProtocol)
else config.get("populate_by_name")
)
if _fields_set is None:
_fields_set = set()
model_fields = get_model_fields(__cls)
for name, field in model_fields.items():
key = field.alias
if key is None or (key not in values and populate_by_name):
key = name
if key in values:
fields_values[name] = _construct_field(value=values[key], field=field, key=key)
_fields_set.add(name)
else:
fields_values[name] = field_get_default(field)
extra_field_type = _get_extra_fields_type(__cls)
_extra = {}
for key, value in values.items():
if key not in model_fields:
parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
if PYDANTIC_V1:
_fields_set.add(key)
fields_values[key] = parsed
else:
_extra[key] = parsed
object.__setattr__(m, "__dict__", fields_values)
if PYDANTIC_V1:
# init_private_attributes() does not exist in v2
m._init_private_attributes() # type: ignore
# copied from Pydantic v1's `construct()` method
object.__setattr__(m, "__fields_set__", _fields_set)
else:
# these properties are copied from Pydantic's `model_construct()` method
object.__setattr__(m, "__pydantic_private__", None)
object.__setattr__(m, "__pydantic_extra__", _extra)
object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
return m
if not TYPE_CHECKING:
# type checkers incorrectly complain about this assignment
# because the type signatures are technically different
# although not in practice
model_construct = construct
if PYDANTIC_V1:
# we define aliases for some of the new pydantic v2 methods so
# that we can just document these methods without having to specify
# a specific pydantic version as some users may not know which
# pydantic version they are currently using
@override
def model_dump(
self,
*,
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
Args:
mode: The mode in which `to_python` should run.
If mode is 'json', the output will only contain JSON serializable types.
If mode is 'python', the output may contain non-JSON-serializable Python objects.
include: A set of fields to include in the output.
exclude: A set of fields to exclude from the output.
context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that are set to their default value.
exclude_none: Whether to exclude fields that have a value of `None`.
exclude_computed_fields: Whether to exclude computed fields.
While this can be useful for round-tripping, it is usually recommended to use the dedicated
`round_trip` parameter instead.
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
fallback: A function to call when an unknown value is encountered. If not provided,
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
"""
if mode not in {"json", "python"}:
raise ValueError("mode must be either 'json' or 'python'")
if round_trip != False:
raise ValueError("round_trip is only supported in Pydantic v2")
if warnings != True:
raise ValueError("warnings is only supported in Pydantic v2")
if context is not None:
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
if exclude_computed_fields != False:
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
@override
def model_dump_json(
self,
*,
indent: int | None = None,
ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
Generates a JSON representation of the model using Pydantic's `to_json` method.
Args:
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.
include: Field(s) to include in the JSON output. Can take either a string or set of strings.
exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings.
by_alias: Whether to serialize using field aliases.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that have the default value.
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to use serialization/deserialization between JSON and class instance.
warnings: Whether to show any warnings that occurred during serialization.
Returns:
A JSON string representation of the model.
"""
if round_trip != False:
raise ValueError("round_trip is only supported in Pydantic v2")
if warnings != True:
raise ValueError("warnings is only supported in Pydantic v2")
if context is not None:
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
if ensure_ascii != False:
raise ValueError("ensure_ascii is only supported in Pydantic v2")
if exclude_computed_fields != False:
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
exclude=exclude,
by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
if PYDANTIC_V1:
type_ = cast(type, field.outer_type_) # type: ignore
else:
type_ = field.annotation # type: ignore
if type_ is None:
raise RuntimeError(f"Unexpected field type is None for {key}")
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
if PYDANTIC_V1:
# TODO
return None
schema = cls.__pydantic_core_schema__
if schema["type"] == "model":
fields = schema["schema"]
if fields["type"] == "model-fields":
extras = fields.get("extras_schema")
if extras and "cls" in extras:
# mypy can't narrow the type
return extras["cls"] # type: ignore[no-any-return]
return None
def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
if is_union(type_):
for variant in get_args(type_):
if is_basemodel(variant):
return True
return False
return is_basemodel_type(type_)
def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]:
origin = get_origin(type_) or type_
if not inspect.isclass(origin):
return False
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
def build(
base_model_cls: Callable[P, _BaseModelT],
*args: P.args,
**kwargs: P.kwargs,
) -> _BaseModelT:
"""Construct a BaseModel class without validation.
This is useful for cases where you need to instantiate a `BaseModel`
from an API response as this provides type-safe params which isn't supported
by helpers like `construct_type()`.
```py
build(MyModel, my_field_a="foo", my_field_b=123)
```
"""
if args:
raise TypeError(
"Received positional arguments which are not supported; Keyword arguments must be used instead",
)
return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
"""Loose coercion to the expected type with construction of nested values.
Note: the returned value from this function is not guaranteed to match the
given type.
"""
return cast(_T, construct_type(value=value, type_=type_))
def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
"""Loose coercion to the expected type with construction of nested values.
If the given value does not match the expected type then it is returned as-is.
"""
# store a reference to the original type we were given before we extract any inner
# types so that we can properly resolve forward references in `TypeAliasType` annotations
original_type = None
# we allow `object` as the input type because otherwise, passing things like
# `Literal['value']` will be reported as a type error by type checkers
type_ = cast("type[object]", type_)
if is_type_alias_type(type_):
original_type = type_ # type: ignore[unreachable]
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
if metadata is not None and len(metadata) > 0:
meta: tuple[Any, ...] = tuple(metadata)
elif is_annotated_type(type_):
meta = get_args(type_)[1:]
type_ = extract_type_arg(type_, 0)
else:
meta = tuple()
# we need to use the origin class for any types that are subscripted generics
# e.g. Dict[str, object]
origin = get_origin(type_) or type_
args = get_args(type_)
if is_union(origin):
try:
return validate_type(type_=cast("type[object]", original_type or type_), value=value)
except Exception:
pass
# if the type is a discriminated union then we want to construct the right variant
# in the union, even if the data doesn't match exactly, otherwise we'd break code
# that relies on the constructed class types, e.g.
#
# class FooType:
# kind: Literal['foo']
# value: str
#
# class BarType:
# kind: Literal['bar']
# value: int
#
# without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then
# we'd end up constructing `FooType` when it should be `BarType`.
discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta)
if discriminator and is_mapping(value):
variant_value = value.get(discriminator.field_alias_from or discriminator.field_name)
if variant_value and isinstance(variant_value, str):
variant_type = discriminator.mapping.get(variant_value)
if variant_type:
return construct_type(type_=variant_type, value=value)
# if the data is not valid, use the first variant that doesn't fail while deserializing
for variant in args:
try:
return construct_type(value=value, type_=variant)
except Exception:
continue
raise RuntimeError(f"Could not convert data into a valid instance of {type_}")
if origin == dict:
if not is_mapping(value):
return value
_, items_type = get_args(type_) # Dict[_, items_type]
return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
if (
not is_literal_type(type_)
and inspect.isclass(origin)
and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
):
if is_list(value):
return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
if is_mapping(value):
if issubclass(type_, BaseModel):
return type_.construct(**value) # type: ignore[arg-type]
return cast(Any, type_).construct(**value)
if origin == list:
if not is_list(value):
return value
inner_type = args[0] # List[inner_type]
return [construct_type(value=entry, type_=inner_type) for entry in value]
if origin == float:
if isinstance(value, int):
coerced = float(value)
if coerced != value:
return value
return coerced
return value
if type_ == datetime:
try:
return parse_datetime(value) # type: ignore
except Exception:
return value
if type_ == date:
try:
return parse_date(value) # type: ignore
except Exception:
return value
return value
@runtime_checkable
class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails
DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
```py
class Foo(BaseModel):
type: Literal['foo']
```
Will result in field_name='type'
"""
field_alias_from: str | None
"""The name of the discriminator field in the API response, e.g.
```py
class Foo(BaseModel):
type: Literal['foo'] = Field(alias='type_from_api')
```
Will result in field_alias_from='type_from_api'
"""
mapping: dict[str, type]
"""Mapping of discriminator value to variant type, e.g.
{'foo': FooVariant, 'bar': BarVariant}
"""
def __init__(
self,
*,
mapping: dict[str, type],
discriminator_field: str,
discriminator_alias: str | None,
) -> None:
self.mapping = mapping
self.field_name = discriminator_field
self.field_alias_from = discriminator_alias
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
cached = DISCRIMINATOR_CACHE.get(union)
if cached is not None:
return cached
discriminator_field_name: str | None = None
for annotation in meta_annotations:
if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None:
discriminator_field_name = annotation.discriminator
break
if not discriminator_field_name:
return None
mapping: dict[str, type] = {}
discriminator_alias: str | None = None
for variant in get_args(union):
variant = strip_annotated_type(variant)
if is_basemodel_type(variant):
if PYDANTIC_V1:
field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
if not field_info:
continue
# Note: if one variant defines an alias then they all should
discriminator_alias = field_info.alias
if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
for entry in get_args(annotation):
if isinstance(entry, str):
mapping[entry] = variant
else:
field = _extract_field_schema_pv2(variant, discriminator_field_name)
if not field:
continue
# Note: if one variant defines an alias then they all should
discriminator_alias = field.get("serialization_alias")
field_schema = field["schema"]
if field_schema["type"] == "literal":
for entry in cast("LiteralSchema", field_schema)["expected"]:
if isinstance(entry, str):
mapping[entry] = variant
if not mapping:
return None
details = DiscriminatorDetails(
mapping=mapping,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
DISCRIMINATOR_CACHE.setdefault(union, details)
return details
def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
schema = model.__pydantic_core_schema__
if schema["type"] == "definitions":
schema = schema["schema"]
if schema["type"] != "model":
return None
schema = cast("ModelSchema", schema)
fields_schema = schema["schema"]
if fields_schema["type"] != "model-fields":
return None
fields_schema = cast("ModelFieldsSchema", fields_schema)
field = fields_schema["fields"].get(field_name)
if not field:
return None
return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast]
def validate_type(*, type_: type[_T], value: object) -> _T:
"""Strict validation that the given value matches the expected type"""
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
return cast(_T, parse_obj(type_, value))
return cast(_T, _validate_non_model_type(type_=type_, value=value))
def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
"""Add a pydantic config for the given type.
Note: this is a no-op on Pydantic v1.
"""
setattr(typ, "__pydantic_config__", config) # noqa: B010
# our use of subclassing here causes weirdness for type checkers,
# so we just pretend that we don't subclass
if TYPE_CHECKING:
GenericModel = BaseModel
else:
class GenericModel(BaseGenericModel, BaseModel):
pass
if not PYDANTIC_V1:
from pydantic import TypeAdapter as _TypeAdapter
_CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
if TYPE_CHECKING:
from pydantic import TypeAdapter
else:
TypeAdapter = _CachedTypeAdapter
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
return TypeAdapter(type_).validate_python(value)
elif not TYPE_CHECKING: # TODO: condition is weird
class RootModel(GenericModel, Generic[_T]):
"""Used as a placeholder to easily convert runtime types to a Pydantic format
to provide validation.
For example:
```py
validated = RootModel[int](__root__="5").__root__
# validated: 5
```
"""
__root__: _T
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
model = _create_pydantic_model(type_).validate(value)
return cast(_T, model.__root__)
def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
return RootModel[type_] # type: ignore
class FinalRequestOptionsInput(TypedDict, total=False):
method: Required[str]
url: Required[str]
params: Query
headers: Headers
max_retries: int
timeout: float | Timeout | None
files: HttpxRequestFiles | None
idempotency_key: str
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
@final
class FinalRequestOptions(pydantic.BaseModel):
method: str
url: str
params: Query = {}
headers: Union[Headers, NotGiven] = NotGiven()
max_retries: Union[int, NotGiven] = NotGiven()
timeout: Union[float, Timeout, None, NotGiven] = NotGiven()
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
extra_json: Union[AnyMapping, None] = None
if PYDANTIC_V1:
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
arbitrary_types_allowed: bool = True
else:
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
def get_max_retries(self, max_retries: int) -> int:
if isinstance(self.max_retries, NotGiven):
return max_retries
return self.max_retries
def _strip_raw_response_header(self) -> None:
if not is_given(self.headers):
return
if self.headers.get(RAW_RESPONSE_HEADER):
self.headers = {**self.headers}
self.headers.pop(RAW_RESPONSE_HEADER)
# override the `construct` method so that we can run custom transformations.
# this is necessary as we don't want to do any actual runtime type checking
# (which means we can't use validators) but we do want to ensure that `NotGiven`
# values are not present
#
# type ignore required because we're adding explicit types to `**values`
@classmethod
def construct( # type: ignore
cls,
_fields_set: set[str] | None = None,
**values: Unpack[FinalRequestOptionsInput],
) -> FinalRequestOptions:
kwargs: dict[str, Any] = {
# we unconditionally call `strip_not_given` on any value
# as it will just ignore any non-mapping types
key: strip_not_given(value)
for key, value in values.items()
}
if PYDANTIC_V1:
return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
return super().model_construct(_fields_set, **kwargs)
if not TYPE_CHECKING:
# type checkers incorrectly complain about this assignment
model_construct = construct

View File

@@ -0,0 +1,168 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
from typing_extensions import Literal, get_args
from ._types import NotGiven, not_given
from ._utils import flatten
_T = TypeVar("_T")
ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
NestedFormat = Literal["dots", "brackets"]
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"]
Params = Mapping[str, Data]
class Querystring:
array_format: ArrayFormat
nested_format: NestedFormat
def __init__(
self,
*,
array_format: ArrayFormat = "repeat",
nested_format: NestedFormat = "brackets",
) -> None:
self.array_format = array_format
self.nested_format = nested_format
def parse(self, query: str) -> Mapping[str, object]:
# Note: custom format syntax is not supported yet
return parse_qs(query)
def stringify(
self,
params: Params,
*,
array_format: ArrayFormat | NotGiven = not_given,
nested_format: NestedFormat | NotGiven = not_given,
) -> str:
return urlencode(
self.stringify_items(
params,
array_format=array_format,
nested_format=nested_format,
)
)
def stringify_items(
self,
params: Params,
*,
array_format: ArrayFormat | NotGiven = not_given,
nested_format: NestedFormat | NotGiven = not_given,
) -> list[tuple[str, str]]:
opts = Options(
qs=self,
array_format=array_format,
nested_format=nested_format,
)
return flatten([self._stringify_item(key, value, opts) for key, value in params.items()])
def _stringify_item(
self,
key: str,
value: Data,
opts: Options,
) -> list[tuple[str, str]]:
if isinstance(value, Mapping):
items: list[tuple[str, str]] = []
nested_format = opts.nested_format
for subkey, subvalue in value.items():
items.extend(
self._stringify_item(
# TODO: error if unknown format
f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]",
subvalue,
opts,
)
)
return items
if isinstance(value, (list, tuple)):
array_format = opts.array_format
if array_format == "comma":
return [
(
key,
",".join(self._primitive_value_to_str(item) for item in value if item is not None),
),
]
elif array_format == "repeat":
items = []
for item in value:
items.extend(self._stringify_item(key, item, opts))
return items
elif array_format == "indices":
items = []
for i, item in enumerate(value):
items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
return items
elif array_format == "brackets":
items = []
key = key + "[]"
for item in value:
items.extend(self._stringify_item(key, item, opts))
return items
else:
raise NotImplementedError(
f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
)
serialised = self._primitive_value_to_str(value)
if not serialised:
return []
return [(key, serialised)]
def _primitive_value_to_str(self, value: PrimitiveData) -> str:
# copied from httpx
if value is True:
return "true"
elif value is False:
return "false"
elif value is None:
return ""
return str(value)
_qs = Querystring()
parse = _qs.parse
stringify = _qs.stringify
stringify_items = _qs.stringify_items
class Options:
array_format: ArrayFormat
nested_format: NestedFormat
def __init__(
self,
qs: Querystring = _qs,
*,
array_format: ArrayFormat | NotGiven = not_given,
nested_format: NestedFormat | NotGiven = not_given,
) -> None:
self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format
self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format

View File

@@ -0,0 +1,58 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import anyio
if TYPE_CHECKING:
from ._client import GeminiNextGenAPIClient, AsyncGeminiNextGenAPIClient
class SyncAPIResource:
_client: GeminiNextGenAPIClient
def __init__(self, client: GeminiNextGenAPIClient) -> None:
self._client = client
self._get = client.get
self._post = client.post
self._patch = client.patch
self._put = client.put
self._delete = client.delete
self._get_api_list = client.get_api_list
def _sleep(self, seconds: float) -> None:
time.sleep(seconds)
class AsyncAPIResource:
_client: AsyncGeminiNextGenAPIClient
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
self._client = client
self._get = client.get
self._post = client.post
self._patch = client.patch
self._put = client.put
self._delete = client.delete
self._get_api_list = client.get_api_list
async def _sleep(self, seconds: float) -> None:
await anyio.sleep(seconds)

View File

@@ -0,0 +1,850 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
import os
import inspect
import logging
import datetime
import functools
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Union,
Generic,
TypeVar,
Callable,
Iterator,
AsyncIterator,
cast,
overload,
)
from typing_extensions import Awaitable, ParamSpec, override, get_origin
import anyio
import httpx
import pydantic
from ._types import NoneType
from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
from ._models import BaseModel, is_basemodel
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
from ._exceptions import APIResponseValidationError, GeminiNextGenAPIClientError
if TYPE_CHECKING:
from ._models import FinalRequestOptions
from ._base_client import BaseClient
P = ParamSpec("P")
R = TypeVar("R")
_T = TypeVar("_T")
_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]")
_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]")
log: logging.Logger = logging.getLogger(__name__)
class BaseAPIResponse(Generic[R]):
_cast_to: type[R]
_client: BaseClient[Any, Any]
_parsed_by_type: dict[type[Any], Any]
_is_sse_stream: bool
_stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None
_options: FinalRequestOptions
http_response: httpx.Response
retries_taken: int
"""The number of retries made. If no retries happened this will be `0`"""
def __init__(
self,
*,
raw: httpx.Response,
cast_to: type[R],
client: BaseClient[Any, Any],
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
options: FinalRequestOptions,
retries_taken: int = 0,
) -> None:
self._cast_to = cast_to
self._client = client
self._parsed_by_type = {}
self._is_sse_stream = stream
self._stream_cls = stream_cls
self._options = options
self.http_response = raw
self.retries_taken = retries_taken
@property
def headers(self) -> httpx.Headers:
return self.http_response.headers
@property
def http_request(self) -> httpx.Request:
"""Returns the httpx Request instance associated with the current response."""
return self.http_response.request
@property
def status_code(self) -> int:
return self.http_response.status_code
@property
def url(self) -> httpx.URL:
"""Returns the URL for which the request was made."""
return self.http_response.url
@property
def method(self) -> str:
return self.http_request.method
@property
def http_version(self) -> str:
return self.http_response.http_version
@property
def elapsed(self) -> datetime.timedelta:
"""The time taken for the complete request/response cycle to complete."""
return self.http_response.elapsed
@property
def is_closed(self) -> bool:
"""Whether or not the response body has been closed.
If this is False then there is response data that has not been read yet.
You must either fully consume the response body or call `.close()`
before discarding the response to prevent resource leaks.
"""
return self.http_response.is_closed
@override
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>"
)
def _parse(self, *, to: type[_T] | None = None) -> R | _T:
cast_to = to if to is not None else self._cast_to
# unwrap `TypeAlias('Name', T)` -> `T`
if is_type_alias_type(cast_to):
cast_to = cast_to.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
if cast_to and is_annotated_type(cast_to):
cast_to = extract_type_arg(cast_to, 0)
origin = get_origin(cast_to) or cast_to
if self._is_sse_stream:
if to:
if not is_stream_class_type(to):
raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}")
return cast(
_T,
to(
cast_to=extract_stream_chunk_type(
to,
failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]",
),
response=self.http_response,
client=cast(Any, self._client),
options=self._options,
),
)
if self._stream_cls:
return cast(
R,
self._stream_cls(
cast_to=extract_stream_chunk_type(self._stream_cls),
response=self.http_response,
client=cast(Any, self._client),
options=self._options,
),
)
stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls)
if stream_cls is None:
raise MissingStreamClassError()
return cast(
R,
stream_cls(
cast_to=cast_to,
response=self.http_response,
client=cast(Any, self._client),
options=self._options,
),
)
if cast_to is NoneType:
return cast(R, None)
response = self.http_response
if cast_to == str:
return cast(R, response.text)
if cast_to == bytes:
return cast(R, response.content)
if cast_to == int:
return cast(R, int(response.text))
if cast_to == float:
return cast(R, float(response.text))
if cast_to == bool:
return cast(R, response.text.lower() == "true")
if origin == APIResponse:
raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
if inspect.isclass(origin) and issubclass(origin, httpx.Response):
# Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response
# and pass that class to our request functions. We cannot change the variance to be either
# covariant or contravariant as that makes our usage of ResponseT illegal. We could construct
# the response class ourselves but that is something that should be supported directly in httpx
# as it would be easy to incorrectly construct the Response object due to the multitude of arguments.
if cast_to != httpx.Response:
raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
return cast(R, response)
if (
inspect.isclass(
origin # pyright: ignore[reportUnknownArgumentType]
)
and not issubclass(origin, BaseModel)
and issubclass(origin, pydantic.BaseModel)
):
raise TypeError(
"Pydantic models must subclass our base model type, e.g. `from google.genai._interactions import BaseModel`"
)
if (
cast_to is not object
and not origin is list
and not origin is dict
and not origin is Union
and not issubclass(origin, BaseModel)
):
raise RuntimeError(
f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}."
)
# split is required to handle cases where additional information is included
# in the response, e.g. application/json; charset=utf-8
content_type, *_ = response.headers.get("content-type", "*").split(";")
if not content_type.endswith("json"):
if is_basemodel(cast_to):
try:
data = response.json()
except Exception as exc:
log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc)
else:
return self._client._process_response_data(
data=data,
cast_to=cast_to, # type: ignore
response=response,
)
if self._client._strict_response_validation:
raise APIResponseValidationError(
response=response,
message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.",
body=response.text,
)
# If the API responds with content that isn't JSON then we just return
# the (decoded) text without performing any parsing so that you can still
# handle the response however you need to.
return response.text # type: ignore
data = response.json()
return self._client._process_response_data(
data=data,
cast_to=cast_to, # type: ignore
response=response,
)
class APIResponse(BaseAPIResponse[R]):
@overload
def parse(self, *, to: type[_T]) -> _T: ...
@overload
def parse(self) -> R: ...
def parse(self, *, to: type[_T] | None = None) -> R | _T:
"""Returns the rich python representation of this response's data.
For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
You can customise the type that the response is parsed into through
the `to` argument, e.g.
```py
from google.genai._interactions import BaseModel
class MyModel(BaseModel):
foo: str
obj = response.parse(to=MyModel)
print(obj.foo)
```
We support parsing:
- `BaseModel`
- `dict`
- `list`
- `Union`
- `str`
- `int`
- `float`
- `httpx.Response`
"""
cache_key = to if to is not None else self._cast_to
cached = self._parsed_by_type.get(cache_key)
if cached is not None:
return cached # type: ignore[no-any-return]
if not self._is_sse_stream:
self.read()
parsed = self._parse(to=to)
if is_given(self._options.post_parser):
parsed = self._options.post_parser(parsed)
self._parsed_by_type[cache_key] = parsed
return parsed
def read(self) -> bytes:
"""Read and return the binary response content."""
try:
return self.http_response.read()
except httpx.StreamConsumed as exc:
# The default error raised by httpx isn't very
# helpful in our case so we re-raise it with
# a different error message.
raise StreamAlreadyConsumed() from exc
def text(self) -> str:
"""Read and decode the response content into a string."""
self.read()
return self.http_response.text
def json(self) -> object:
"""Read and decode the JSON response content."""
self.read()
return self.http_response.json()
def close(self) -> None:
"""Close the response and release the connection.
Automatically called if the response body is read to completion.
"""
self.http_response.close()
def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]:
"""
A byte-iterator over the decoded response content.
This automatically handles gzip, deflate and brotli encoded responses.
"""
for chunk in self.http_response.iter_bytes(chunk_size):
yield chunk
def iter_text(self, chunk_size: int | None = None) -> Iterator[str]:
"""A str-iterator over the decoded response content
that handles both gzip, deflate, etc but also detects the content's
string encoding.
"""
for chunk in self.http_response.iter_text(chunk_size):
yield chunk
def iter_lines(self) -> Iterator[str]:
"""Like `iter_text()` but will only yield chunks for each line"""
for chunk in self.http_response.iter_lines():
yield chunk
class AsyncAPIResponse(BaseAPIResponse[R]):
@overload
async def parse(self, *, to: type[_T]) -> _T: ...
@overload
async def parse(self) -> R: ...
async def parse(self, *, to: type[_T] | None = None) -> R | _T:
"""Returns the rich python representation of this response's data.
For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
You can customise the type that the response is parsed into through
the `to` argument, e.g.
```py
from google.genai._interactions import BaseModel
class MyModel(BaseModel):
foo: str
obj = response.parse(to=MyModel)
print(obj.foo)
```
We support parsing:
- `BaseModel`
- `dict`
- `list`
- `Union`
- `str`
- `httpx.Response`
"""
cache_key = to if to is not None else self._cast_to
cached = self._parsed_by_type.get(cache_key)
if cached is not None:
return cached # type: ignore[no-any-return]
if not self._is_sse_stream:
await self.read()
parsed = self._parse(to=to)
if is_given(self._options.post_parser):
parsed = self._options.post_parser(parsed)
self._parsed_by_type[cache_key] = parsed
return parsed
async def read(self) -> bytes:
"""Read and return the binary response content."""
try:
return await self.http_response.aread()
except httpx.StreamConsumed as exc:
# the default error raised by httpx isn't very
# helpful in our case so we re-raise it with
# a different error message
raise StreamAlreadyConsumed() from exc
async def text(self) -> str:
"""Read and decode the response content into a string."""
await self.read()
return self.http_response.text
async def json(self) -> object:
"""Read and decode the JSON response content."""
await self.read()
return self.http_response.json()
async def close(self) -> None:
"""Close the response and release the connection.
Automatically called if the response body is read to completion.
"""
await self.http_response.aclose()
async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
"""
A byte-iterator over the decoded response content.
This automatically handles gzip, deflate and brotli encoded responses.
"""
async for chunk in self.http_response.aiter_bytes(chunk_size):
yield chunk
async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]:
"""A str-iterator over the decoded response content
that handles both gzip, deflate, etc but also detects the content's
string encoding.
"""
async for chunk in self.http_response.aiter_text(chunk_size):
yield chunk
async def iter_lines(self) -> AsyncIterator[str]:
"""Like `iter_text()` but will only yield chunks for each line"""
async for chunk in self.http_response.aiter_lines():
yield chunk
class BinaryAPIResponse(APIResponse[bytes]):
"""Subclass of APIResponse providing helpers for dealing with binary data.
Note: If you want to stream the response data instead of eagerly reading it
all at once then you should use `.with_streaming_response` when making
the API request, e.g. `.with_streaming_response.get_binary_response()`
"""
def write_to_file(
self,
file: str | os.PathLike[str],
) -> None:
"""Write the output to the given file.
Accepts a filename or any path-like object, e.g. pathlib.Path
Note: if you want to stream the data to the file instead of writing
all at once then you should use `.with_streaming_response` when making
the API request, e.g. `.with_streaming_response.get_binary_response()`
"""
with open(file, mode="wb") as f:
for data in self.iter_bytes():
f.write(data)
class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]):
"""Subclass of APIResponse providing helpers for dealing with binary data.
Note: If you want to stream the response data instead of eagerly reading it
all at once then you should use `.with_streaming_response` when making
the API request, e.g. `.with_streaming_response.get_binary_response()`
"""
async def write_to_file(
self,
file: str | os.PathLike[str],
) -> None:
"""Write the output to the given file.
Accepts a filename or any path-like object, e.g. pathlib.Path
Note: if you want to stream the data to the file instead of writing
all at once then you should use `.with_streaming_response` when making
the API request, e.g. `.with_streaming_response.get_binary_response()`
"""
path = anyio.Path(file)
async with await path.open(mode="wb") as f:
async for data in self.iter_bytes():
await f.write(data)
class StreamedBinaryAPIResponse(APIResponse[bytes]):
def stream_to_file(
self,
file: str | os.PathLike[str],
*,
chunk_size: int | None = None,
) -> None:
"""Streams the output to the given file.
Accepts a filename or any path-like object, e.g. pathlib.Path
"""
with open(file, mode="wb") as f:
for data in self.iter_bytes(chunk_size):
f.write(data)
class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]):
async def stream_to_file(
self,
file: str | os.PathLike[str],
*,
chunk_size: int | None = None,
) -> None:
"""Streams the output to the given file.
Accepts a filename or any path-like object, e.g. pathlib.Path
"""
path = anyio.Path(file)
async with await path.open(mode="wb") as f:
async for data in self.iter_bytes(chunk_size):
await f.write(data)
class MissingStreamClassError(TypeError):
def __init__(self) -> None:
super().__init__(
"The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `google.genai._interactions._streaming` for reference",
)
class StreamAlreadyConsumed(GeminiNextGenAPIClientError):
"""
Attempted to read or stream content, but the content has already
been streamed.
This can happen if you use a method like `.iter_lines()` and then attempt
to read th entire response body afterwards, e.g.
```py
response = await client.post(...)
async for line in response.iter_lines():
... # do something with `line`
content = await response.read()
# ^ error
```
If you want this behaviour you'll need to either manually accumulate the response
content or call `await response.read()` before iterating over the stream.
"""
def __init__(self) -> None:
message = (
"Attempted to read or stream some content, but the content has "
"already been streamed. "
"This could be due to attempting to stream the response "
"content more than once."
"\n\n"
"You can fix this by manually accumulating the response content while streaming "
"or by calling `.read()` before starting to stream."
)
super().__init__(message)
class ResponseContextManager(Generic[_APIResponseT]):
"""Context manager for ensuring that a request is not made
until it is entered and that the response will always be closed
when the context manager exits
"""
def __init__(self, request_func: Callable[[], _APIResponseT]) -> None:
self._request_func = request_func
self.__response: _APIResponseT | None = None
def __enter__(self) -> _APIResponseT:
self.__response = self._request_func()
return self.__response
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
if self.__response is not None:
self.__response.close()
class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]):
"""Context manager for ensuring that a request is not made
until it is entered and that the response will always be closed
when the context manager exits
"""
def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None:
self._api_request = api_request
self.__response: _AsyncAPIResponseT | None = None
async def __aenter__(self) -> _AsyncAPIResponseT:
self.__response = await self._api_request
return self.__response
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
if self.__response is not None:
await self.__response.close()
def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]:
"""Higher order function that takes one of our bound API methods and wraps it
to support streaming and returning the raw `APIResponse` object directly.
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]:
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
kwargs["extra_headers"] = extra_headers
make_request = functools.partial(func, *args, **kwargs)
return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request))
return wrapped
def async_to_streamed_response_wrapper(
func: Callable[P, Awaitable[R]],
) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]:
"""Higher order function that takes one of our bound API methods and wraps it
to support streaming and returning the raw `APIResponse` object directly.
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]:
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
kwargs["extra_headers"] = extra_headers
make_request = func(*args, **kwargs)
return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request))
return wrapped
def to_custom_streamed_response_wrapper(
func: Callable[P, object],
response_cls: type[_APIResponseT],
) -> Callable[P, ResponseContextManager[_APIResponseT]]:
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
and wraps the method to support streaming and returning the given response class directly.
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]:
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
kwargs["extra_headers"] = extra_headers
make_request = functools.partial(func, *args, **kwargs)
return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request))
return wrapped
def async_to_custom_streamed_response_wrapper(
func: Callable[P, Awaitable[object]],
response_cls: type[_AsyncAPIResponseT],
) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]:
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
and wraps the method to support streaming and returning the given response class directly.
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]:
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
kwargs["extra_headers"] = extra_headers
make_request = func(*args, **kwargs)
return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request))
return wrapped
def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]:
"""Higher order function that takes one of our bound API methods and wraps it
to support returning the raw `APIResponse` object directly.
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]:
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
kwargs["extra_headers"] = extra_headers
return cast(APIResponse[R], func(*args, **kwargs))
return wrapped
def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]:
"""Higher order function that takes one of our bound API methods and wraps it
to support returning the raw `APIResponse` object directly.
"""
@functools.wraps(func)
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]:
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
kwargs["extra_headers"] = extra_headers
return cast(AsyncAPIResponse[R], await func(*args, **kwargs))
return wrapped
def to_custom_raw_response_wrapper(
func: Callable[P, object],
response_cls: type[_APIResponseT],
) -> Callable[P, _APIResponseT]:
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
and wraps the method to support returning the given response class directly.
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT:
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
kwargs["extra_headers"] = extra_headers
return cast(_APIResponseT, func(*args, **kwargs))
return wrapped
def async_to_custom_raw_response_wrapper(
func: Callable[P, Awaitable[object]],
response_cls: type[_AsyncAPIResponseT],
) -> Callable[P, Awaitable[_AsyncAPIResponseT]]:
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
and wraps the method to support returning the given response class directly.
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
"""
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]:
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
kwargs["extra_headers"] = extra_headers
return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs))
return wrapped
def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type:
"""Given a type like `APIResponse[T]`, returns the generic type variable `T`.
This also handles the case where a concrete subclass is given, e.g.
```py
class MyResponse(APIResponse[bytes]):
...
extract_response_type(MyResponse) -> bytes
```
"""
return extract_type_var_from_base(
typ,
generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)),
index=0,
)

View File

@@ -0,0 +1,359 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py
from __future__ import annotations
import json
import inspect
from types import TracebackType
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast
from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable
import httpx
from ._utils import extract_type_var_from_base
if TYPE_CHECKING:
from ._client import GeminiNextGenAPIClient, AsyncGeminiNextGenAPIClient
from ._models import FinalRequestOptions
_T = TypeVar("_T")
class Stream(Generic[_T]):
"""Provides the core interface to iterate over a synchronous stream response."""
response: httpx.Response
_options: Optional[FinalRequestOptions] = None
_decoder: SSEBytesDecoder
def __init__(
self,
*,
cast_to: type[_T],
response: httpx.Response,
client: GeminiNextGenAPIClient,
options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
def __next__(self) -> _T:
return self._iterator.__next__()
def __iter__(self) -> Iterator[_T]:
for item in self._iterator:
yield item
def _iter_events(self) -> Iterator[ServerSentEvent]:
yield from self._decoder.iter_bytes(self.response.iter_bytes())
def __stream__(self) -> Iterator[_T]:
cast_to = cast(Any, self._cast_to)
response = self.response
process_data = self._client._process_response_data
iterator = self._iter_events()
try:
for sse in iterator:
if sse.data.startswith("[DONE]"):
break
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
response.close()
def __enter__(self) -> Self:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()
def close(self) -> None:
"""
Close the response and release the connection.
Automatically called if the response body is read to completion.
"""
self.response.close()
class AsyncStream(Generic[_T]):
"""Provides the core interface to iterate over an asynchronous stream response."""
response: httpx.Response
_options: Optional[FinalRequestOptions] = None
_decoder: SSEDecoder | SSEBytesDecoder
def __init__(
self,
*,
cast_to: type[_T],
response: httpx.Response,
client: AsyncGeminiNextGenAPIClient,
options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
async def __anext__(self) -> _T:
return await self._iterator.__anext__()
async def __aiter__(self) -> AsyncIterator[_T]:
async for item in self._iterator:
yield item
async def _iter_events(self) -> AsyncIterator[ServerSentEvent]:
async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
yield sse
async def __stream__(self) -> AsyncIterator[_T]:
cast_to = cast(Any, self._cast_to)
response = self.response
process_data = self._client._process_response_data
iterator = self._iter_events()
try:
async for sse in iterator:
if sse.data.startswith("[DONE]"):
break
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
await response.aclose()
async def __aenter__(self) -> Self:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
await self.close()
async def close(self) -> None:
"""
Close the response and release the connection.
Automatically called if the response body is read to completion.
"""
await self.response.aclose()
class ServerSentEvent:
def __init__(
self,
*,
event: str | None = None,
data: str | None = None,
id: str | None = None,
retry: int | None = None,
) -> None:
if data is None:
data = ""
self._id = id
self._data = data
self._event = event or None
self._retry = retry
@property
def event(self) -> str | None:
return self._event
@property
def id(self) -> str | None:
return self._id
@property
def retry(self) -> int | None:
return self._retry
@property
def data(self) -> str:
return self._data
def json(self) -> Any:
return json.loads(self.data)
@override
def __repr__(self) -> str:
return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})"
class SSEDecoder:
_data: list[str]
_event: str | None
_retry: int | None
_last_event_id: str | None
def __init__(self) -> None:
self._event = None
self._data = []
self._last_event_id = None
self._retry = None
def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
for chunk in self._iter_chunks(iterator):
# Split before decoding so splitlines() only uses \r and \n
for raw_line in chunk.splitlines():
line = raw_line.decode("utf-8")
sse = self.decode(line)
if sse:
yield sse
def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]:
"""Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
data = b""
for chunk in iterator:
for line in chunk.splitlines(keepends=True):
data += line
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
yield data
data = b""
if data:
yield data
async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
async for chunk in self._aiter_chunks(iterator):
# Split before decoding so splitlines() only uses \r and \n
for raw_line in chunk.splitlines():
line = raw_line.decode("utf-8")
sse = self.decode(line)
if sse:
yield sse
async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
"""Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
data = b""
async for chunk in iterator:
for line in chunk.splitlines(keepends=True):
data += line
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
yield data
data = b""
if data:
yield data
def decode(self, line: str) -> ServerSentEvent | None:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
if not line:
if not self._event and not self._data and not self._last_event_id and self._retry is None:
return None
sse = ServerSentEvent(
event=self._event,
data="\n".join(self._data),
id=self._last_event_id,
retry=self._retry,
)
# NOTE: as per the SSE spec, do not reset last_event_id.
self._event = None
self._data = []
self._retry = None
return sse
if line.startswith(":"):
return None
fieldname, _, value = line.partition(":")
if value.startswith(" "):
value = value[1:]
if fieldname == "event":
self._event = value
elif fieldname == "data":
self._data.append(value)
elif fieldname == "id":
if "\0" in value:
pass
else:
self._last_event_id = value
elif fieldname == "retry":
try:
self._retry = int(value)
except (TypeError, ValueError):
pass
else:
pass # Field is ignored.
return None
@runtime_checkable
class SSEBytesDecoder(Protocol):
def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
...
def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
"""Given an async iterator that yields raw binary data, iterate over it & yield every event encountered"""
...
def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]:
"""TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`"""
origin = get_origin(typ) or typ
return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream))
def extract_stream_chunk_type(
stream_cls: type,
*,
failure_message: str | None = None,
) -> type:
"""Given a type like `Stream[T]`, returns the generic type variable `T`.
This also handles the case where a concrete subclass is given, e.g.
```py
class MyStream(Stream[bytes]):
...
extract_stream_chunk_type(MyStream) -> bytes
```
"""
from ._base_client import Stream, AsyncStream
return extract_type_var_from_base(
stream_cls,
index=0,
generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)),
failure_message=failure_message,
)

View File

@@ -0,0 +1,285 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from os import PathLike
from typing import (
IO,
TYPE_CHECKING,
Any,
Dict,
List,
Type,
Tuple,
Union,
Mapping,
TypeVar,
Callable,
Iterable,
Iterator,
Optional,
Sequence,
AsyncIterable,
)
from typing_extensions import (
Set,
Literal,
Protocol,
TypeAlias,
TypedDict,
SupportsIndex,
overload,
override,
runtime_checkable,
)
import httpx
import pydantic
from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport
if TYPE_CHECKING:
from ._models import BaseModel
from ._response import APIResponse, AsyncAPIResponse
Transport = BaseTransport
AsyncTransport = AsyncBaseTransport
Query = Mapping[str, object]
Body = object
AnyMapping = Mapping[str, object]
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]]
ProxiesTypes = Union[str, Proxy, ProxiesDict]
if TYPE_CHECKING:
Base64FileInput = Union[IO[bytes], PathLike[str]]
FileContent = Union[IO[bytes], bytes, PathLike[str]]
else:
Base64FileInput = Union[IO[bytes], PathLike]
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
# Used for sending raw binary data / streaming data in request bodies
# e.g. for file uploads without multipart encoding
BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
FileTypes = Union[
# file (or bytes)
FileContent,
# (filename, file (or bytes))
Tuple[Optional[str], FileContent],
# (filename, file (or bytes), content_type)
Tuple[Optional[str], FileContent, Optional[str]],
# (filename, file (or bytes), content_type, headers)
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
# duplicate of the above but without our custom file support
HttpxFileContent = Union[IO[bytes], bytes]
HttpxFileTypes = Union[
# file (or bytes)
HttpxFileContent,
# (filename, file (or bytes))
Tuple[Optional[str], HttpxFileContent],
# (filename, file (or bytes), content_type)
Tuple[Optional[str], HttpxFileContent, Optional[str]],
# (filename, file (or bytes), content_type, headers)
Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]],
]
HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]]
# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT
# where ResponseT includes `None`. In order to support directly
# passing `None`, overloads would have to be defined for every
# method that uses `ResponseT` which would lead to an unacceptable
# amount of code duplication and make it unreadable. See _base_client.py
# for example usage.
#
# This unfortunately means that you will either have
# to import this type and pass it explicitly:
#
# from google.genai._interactions import NoneType
# client.get('/foo', cast_to=NoneType)
#
# or build it yourself:
#
# client.get('/foo', cast_to=type(None))
if TYPE_CHECKING:
NoneType: Type[None]
else:
NoneType = type(None)
class RequestOptions(TypedDict, total=False):
headers: Headers
max_retries: int
timeout: float | Timeout | None
params: Query
extra_json: AnyMapping
idempotency_key: str
follow_redirects: bool
# Sentinel class used until PEP 0661 is accepted
class NotGiven:
"""
For parameters with a meaningful None value, we need to distinguish between
the user explicitly passing None, and the user not passing the parameter at
all.
User code shouldn't need to use not_given directly.
For example:
```py
def create(timeout: Timeout | None | NotGiven = not_given): ...
create(timeout=1) # 1s timeout
create(timeout=None) # No timeout
create() # Default timeout behavior
```
"""
def __bool__(self) -> Literal[False]:
return False
@override
def __repr__(self) -> str:
return "NOT_GIVEN"
not_given = NotGiven()
# for backwards compatibility:
NOT_GIVEN = NotGiven()
class Omit:
"""
To explicitly omit something from being sent in a request, use `omit`.
```py
# as the default `Content-Type` header is `application/json` that will be sent
client.post("/upload/files", files={"file": b"my raw file content"})
# you can't explicitly override the header as it has to be dynamically generated
# to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
client.post(..., headers={"Content-Type": "multipart/form-data"})
# instead you can remove the default `application/json` header by passing omit
client.post(..., headers={"Content-Type": omit})
```
"""
def __bool__(self) -> Literal[False]:
return False
omit = Omit()
@runtime_checkable
class ModelBuilderProtocol(Protocol):
@classmethod
def build(
cls: type[_T],
*,
response: Response,
data: object,
) -> _T: ...
Headers = Mapping[str, Union[str, Omit]]
class HeadersLikeProtocol(Protocol):
def get(self, __key: str) -> str | None: ...
HeadersLike = Union[Headers, HeadersLikeProtocol]
ResponseT = TypeVar(
"ResponseT",
bound=Union[
object,
str,
None,
"BaseModel",
List[Any],
Dict[str, Any],
Response,
ModelBuilderProtocol,
"APIResponse[Any]",
"AsyncAPIResponse[Any]",
],
)
StrBytesIntFloat = Union[str, bytes, int, float]
# Note: copied from Pydantic
# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79
IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]]
PostParser = Callable[[Any], Any]
@runtime_checkable
class InheritsGeneric(Protocol):
"""Represents a type that has inherited from `Generic`
The `__orig_bases__` property can be used to determine the resolved
type variable for a given base class.
"""
__orig_bases__: tuple[_GenericAlias]
class _GenericAlias(Protocol):
__origin__: type[object]
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
follow_redirects: bool
_T_co = TypeVar("_T_co", covariant=True)
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
#
# Note: index() and count() methods are intentionally omitted to allow pyright to properly
# infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@overload
def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
# deserializing our custom sequence type
SequenceNotStr = Sequence

View File

@@ -0,0 +1,80 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from ._path import path_template as path_template
from ._sync import asyncify as asyncify
from ._proxy import LazyProxy as LazyProxy
from ._utils import (
flatten as flatten,
is_dict as is_dict,
is_list as is_list,
is_given as is_given,
is_tuple as is_tuple,
json_safe as json_safe,
lru_cache as lru_cache,
is_mapping as is_mapping,
is_tuple_t as is_tuple_t,
is_iterable as is_iterable,
is_sequence as is_sequence,
coerce_float as coerce_float,
is_mapping_t as is_mapping_t,
removeprefix as removeprefix,
removesuffix as removesuffix,
extract_files as extract_files,
is_sequence_t as is_sequence_t,
required_args as required_args,
coerce_boolean as coerce_boolean,
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
maybe_coerce_boolean as maybe_coerce_boolean,
maybe_coerce_integer as maybe_coerce_integer,
)
from ._compat import (
get_args as get_args,
is_union as is_union,
get_origin as get_origin,
is_typeddict as is_typeddict,
is_literal_type as is_literal_type,
)
from ._typing import (
is_list_type as is_list_type,
is_union_type as is_union_type,
extract_type_arg as extract_type_arg,
is_iterable_type as is_iterable_type,
is_required_type as is_required_type,
is_sequence_type as is_sequence_type,
is_annotated_type as is_annotated_type,
is_type_alias_type as is_type_alias_type,
strip_annotated_type as strip_annotated_type,
extract_type_var_from_base as extract_type_var_from_base,
)
from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator
from ._transform import (
PropertyInfo as PropertyInfo,
transform as transform,
async_transform as async_transform,
maybe_transform as maybe_transform,
async_maybe_transform as async_maybe_transform,
)
from ._reflection import (
function_has_argument as function_has_argument,
assert_signatures_in_sync as assert_signatures_in_sync,
)
from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime

View File

@@ -0,0 +1,61 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# mypy: ignore-errors
from __future__ import annotations
import sys
import typing_extensions
from typing import Any, Type, Union, Literal, Optional
from datetime import date, datetime
from typing_extensions import get_args as _get_args, get_origin as _get_origin
from .._types import StrBytesIntFloat
from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime
_LITERAL_TYPES = {Literal, typing_extensions.Literal}
def get_args(tp: type[Any]) -> tuple[Any, ...]:
return _get_args(tp)
def get_origin(tp: type[Any]) -> type[Any] | None:
return _get_origin(tp)
def is_union(tp: Optional[Type[Any]]) -> bool:
if sys.version_info < (3, 10):
return tp is Union # type: ignore[comparison-overlap]
else:
import types
return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap]
def is_typeddict(tp: Type[Any]) -> bool:
return typing_extensions.is_typeddict(tp)
def is_literal_type(tp: Type[Any]) -> bool:
return get_origin(tp) in _LITERAL_TYPES
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
return _parse_date(value)
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
return _parse_datetime(value)

View File

@@ -0,0 +1,151 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
without the Pydantic v1 specific errors.
"""
from __future__ import annotations
import re
from typing import Dict, Union, Optional
from datetime import date, datetime, timezone, timedelta
from .._types import StrBytesIntFloat
date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
time_expr = (
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
)
date_re = re.compile(f"{date_expr}$")
datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
EPOCH = datetime(1970, 1, 1)
# if greater than this, the number is in ms, if less than or equal it's in seconds
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
MS_WATERSHED = int(2e10)
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
MAX_NUMBER = int(3e20)
def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
if isinstance(value, (int, float)):
return value
try:
return float(value)
except ValueError:
return None
except TypeError:
raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
if seconds > MAX_NUMBER:
return datetime.max
elif seconds < -MAX_NUMBER:
return datetime.min
while abs(seconds) > MS_WATERSHED:
seconds /= 1000
dt = EPOCH + timedelta(seconds=seconds)
return dt.replace(tzinfo=timezone.utc)
def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
if value == "Z":
return timezone.utc
elif value is not None:
offset_mins = int(value[-2:]) if len(value) > 3 else 0
offset = 60 * int(value[1:3]) + offset_mins
if value[0] == "-":
offset = -offset
return timezone(timedelta(minutes=offset))
else:
return None
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
"""
Parse a datetime/int/float/string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
Raise ValueError if the input is well formatted but not a valid datetime.
Raise ValueError if the input isn't well formatted.
"""
if isinstance(value, datetime):
return value
number = _get_numeric(value, "datetime")
if number is not None:
return _from_unix_seconds(number)
if isinstance(value, bytes):
value = value.decode()
assert not isinstance(value, (float, int))
match = datetime_re.match(value)
if match is None:
raise ValueError("invalid datetime format")
kw = match.groupdict()
if kw["microsecond"]:
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
tzinfo = _parse_timezone(kw.pop("tzinfo"))
kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
kw_["tzinfo"] = tzinfo
return datetime(**kw_) # type: ignore
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
"""
Parse a date/int/float/string and return a datetime.date.
Raise ValueError if the input is well formatted but not a valid date.
Raise ValueError if the input isn't well formatted.
"""
if isinstance(value, date):
if isinstance(value, datetime):
return value.date()
else:
return value
number = _get_numeric(value, "date")
if number is not None:
return _from_unix_seconds(number).date()
if isinstance(value, bytes):
value = value.decode()
assert not isinstance(value, (float, int))
match = date_re.match(value)
if match is None:
raise ValueError("invalid date format")
kw = {k: int(v) for k, v in match.groupdict().items()}
try:
return date(**kw)
except ValueError:
raise ValueError("invalid date format") from None

View File

@@ -0,0 +1,50 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
from typing import Any
from datetime import datetime
from typing_extensions import override
import pydantic
from .._compat import model_dump
def openapi_dumps(obj: Any) -> bytes:
"""
Serialize an object to UTF-8 encoded JSON bytes.
Extends the standard json.dumps with support for additional types
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
"""
return json.dumps(
obj,
cls=_CustomEncoder,
# Uses the same defaults as httpx's JSON serialization
ensure_ascii=False,
separators=(",", ":"),
allow_nan=False,
).encode()
class _CustomEncoder(json.JSONEncoder):
@override
def default(self, o: Any) -> Any:
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, pydantic.BaseModel):
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
return super().default(o)

View File

@@ -0,0 +1,40 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import logging
logger: logging.Logger = logging.getLogger("google.genai._interactions")
httpx_logger: logging.Logger = logging.getLogger("httpx")
def _basic_config() -> None:
# e.g. [2023-10-05 14:12:26 - google.genai._interactions._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK"
logging.basicConfig(
format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def setup_logging() -> None:
env = os.environ.get("GEMINI_NEXT_GEN_API_LOG")
if env == "debug":
_basic_config()
logger.setLevel(logging.DEBUG)
httpx_logger.setLevel(logging.DEBUG)
elif env == "info":
_basic_config()
logger.setLevel(logging.INFO)
httpx_logger.setLevel(logging.INFO)

View File

@@ -0,0 +1,142 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
import re
from typing import (
Any,
Mapping,
Callable,
)
from urllib.parse import quote
# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
def _quote_path_segment_part(value: str) -> str:
"""Percent-encode `value` for use in a URI path segment.
Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
"""
# quote() already treats unreserved characters (letters, digits, and -._~)
# as safe, so we only need to add sub-delims, ':', and '@'.
# Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
return quote(value, safe="!$&'()*+,;=:@")
def _quote_query_part(value: str) -> str:
"""Percent-encode `value` for use in a URI query string.
Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
"""
return quote(value, safe="!$'()*+,;:@/?")
def _quote_fragment_part(value: str) -> str:
"""Percent-encode `value` for use in a URI fragment.
Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
"""
return quote(value, safe="!$&'()*+,;=:@/?")
def _interpolate(
template: str,
values: Mapping[str, Any],
quoter: Callable[[str], str],
) -> str:
"""Replace {name} placeholders in `template`, quoting each value with `quoter`.
Placeholder names are looked up in `values`.
Raises:
KeyError: If a placeholder is not found in `values`.
"""
# re.split with a capturing group returns alternating
# [text, name, text, name, ..., text] elements.
parts = _PLACEHOLDER_RE.split(template)
for i in range(1, len(parts), 2):
name = parts[i]
if name not in values:
raise KeyError(f"a value for placeholder {{{name}}} was not provided")
val = values[name]
if val is None:
parts[i] = "null"
elif isinstance(val, bool):
parts[i] = "true" if val else "false"
else:
parts[i] = quoter(str(values[name]))
return "".join(parts)
def path_template(template: str, /, **kwargs: Any) -> str:
"""Interpolate {name} placeholders in `template` from keyword arguments.
Args:
template: The template string containing {name} placeholders.
**kwargs: Keyword arguments to interpolate into the template.
Returns:
The template with placeholders interpolated and percent-encoded.
Safe characters for percent-encoding are dependent on the URI component.
Placeholders in path and fragment portions are percent-encoded where the `segment`
and `fragment` sets from RFC 3986 respectively are considered safe.
Placeholders in the query portion are percent-encoded where the `query` set from
RFC 3986 §3.3 is considered safe except for = and & characters.
Raises:
KeyError: If a placeholder is not found in `kwargs`.
ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
"""
# Split the template into path, query, and fragment portions.
fragment_template: str | None = None
query_template: str | None = None
rest = template
if "#" in rest:
rest, fragment_template = rest.split("#", 1)
if "?" in rest:
rest, query_template = rest.split("?", 1)
path_template = rest
# Interpolate each portion with the appropriate quoting rules.
path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
# Reject dot-segments (. and ..) in the final assembled path. The check
# runs after interpolation so that adjacent placeholders or a mix of static
# text and placeholders that together form a dot-segment are caught.
# Also reject percent-encoded dot-segments to protect against incorrectly
# implemented normalization in servers/proxies.
for segment in path_result.split("/"):
if _DOT_SEGMENT_RE.match(segment):
raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
result = path_result
if query_template is not None:
result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
if fragment_template is not None:
result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
return result

View File

@@ -0,0 +1,80 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Generic, TypeVar, Iterable, cast
from typing_extensions import override
T = TypeVar("T")
class LazyProxy(Generic[T], ABC):
"""Implements data methods to pretend that an instance is another instance.
This includes forwarding attribute access and other methods.
"""
# Note: we have to special case proxies that themselves return proxies
# to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz`
def __getattr__(self, attr: str) -> object:
proxied = self.__get_proxied__()
if isinstance(proxied, LazyProxy):
return proxied # pyright: ignore
return getattr(proxied, attr)
@override
def __repr__(self) -> str:
proxied = self.__get_proxied__()
if isinstance(proxied, LazyProxy):
return proxied.__class__.__name__
return repr(self.__get_proxied__())
@override
def __str__(self) -> str:
proxied = self.__get_proxied__()
if isinstance(proxied, LazyProxy):
return proxied.__class__.__name__
return str(proxied)
@override
def __dir__(self) -> Iterable[str]:
proxied = self.__get_proxied__()
if isinstance(proxied, LazyProxy):
return []
return proxied.__dir__()
@property # type: ignore
@override
def __class__(self) -> type: # pyright: ignore
try:
proxied = self.__get_proxied__()
except Exception:
return type(self)
if issubclass(type(proxied), LazyProxy):
return type(proxied)
return proxied.__class__
def __get_proxied__(self) -> T:
return self.__load__()
def __as_proxied__(self) -> T:
"""Helper method that returns the current proxy, typed as the loaded object"""
return cast(T, self)
@abstractmethod
def __load__(self) -> T: ...

View File

@@ -0,0 +1,57 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
import inspect
from typing import Any, Callable
def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
"""Returns whether or not the given function has a specific parameter"""
sig = inspect.signature(func)
return arg_name in sig.parameters
def assert_signatures_in_sync(
source_func: Callable[..., Any],
check_func: Callable[..., Any],
*,
exclude_params: set[str] = set(),
) -> None:
"""Ensure that the signature of the second function matches the first."""
check_sig = inspect.signature(check_func)
source_sig = inspect.signature(source_func)
errors: list[str] = []
for name, source_param in source_sig.parameters.items():
if name in exclude_params:
continue
custom_param = check_sig.parameters.get(name)
if not custom_param:
errors.append(f"the `{name}` param is missing")
continue
if custom_param.annotation != source_param.annotation:
errors.append(
f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}"
)
continue
if errors:
raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))

View File

@@ -0,0 +1,39 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
from typing import Any
from typing_extensions import override
from ._proxy import LazyProxy
class ResourcesProxy(LazyProxy[Any]):
"""A proxy for the `google.genai._interactions.resources` module.
This is used so that we can lazily import `google.genai._interactions.resources` only when
needed *and* so that users can just import `google.genai._interactions` and reference `google.genai._interactions.resources`
"""
@override
def __load__(self) -> Any:
import importlib
mod = importlib.import_module("google.genai._interactions.resources")
return mod
resources = ResourcesProxy().__as_proxied__()

View File

@@ -0,0 +1,27 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any
from typing_extensions import Iterator, AsyncIterator
def consume_sync_iterator(iterator: Iterator[Any]) -> None:
for _ in iterator:
...
async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None:
async for _ in iterator:
...

View File

@@ -0,0 +1,73 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
import asyncio
import functools
from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
import anyio
import sniffio
import anyio.to_thread
T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")
async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
return await asyncio.to_thread(func, *args, **kwargs)
return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
)
# inspired by `asyncer`, https://github.com/tiangolo/asyncer
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
positional and keyword arguments.
Usage:
```python
def blocking_func(arg1, arg2, kwarg1=None):
# blocking code
return result
result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
```
## Arguments
`function`: a blocking regular callable (e.g. a function)
## Return
An async function that takes the same positional and keyword arguments as the
original one, that when called runs the same original function in a thread worker
and returns the result.
"""
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
return await to_thread(function, *args, **kwargs)
return wrapper

View File

@@ -0,0 +1,472 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from __future__ import annotations
import io
import base64
import pathlib
from typing import Any, Mapping, TypeVar, cast
from datetime import date, datetime
from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints
import anyio
import pydantic
from ._utils import (
is_list,
is_given,
lru_cache,
is_mapping,
is_iterable,
is_sequence,
)
from .._files import is_base64_file_input
from ._compat import get_origin, is_typeddict
from ._typing import (
is_list_type,
is_union_type,
extract_type_arg,
is_iterable_type,
is_required_type,
is_sequence_type,
is_annotated_type,
strip_annotated_type,
)
_T = TypeVar("_T")
# TODO: support for drilling globals() and locals()
# TODO: ensure works correctly with forward references in all cases
PropertyFormat = Literal["iso8601", "base64", "custom"]
class PropertyInfo:
"""Metadata class to be used in Annotated types to provide information about a given type.
For example:
class MyParams(TypedDict):
account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')]
This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API.
"""
alias: str | None
format: PropertyFormat | None
format_template: str | None
discriminator: str | None
def __init__(
self,
*,
alias: str | None = None,
format: PropertyFormat | None = None,
format_template: str | None = None,
discriminator: str | None = None,
) -> None:
self.alias = alias
self.format = format
self.format_template = format_template
self.discriminator = discriminator
@override
def __repr__(self) -> str:
return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')"
def maybe_transform(
data: object,
expected_type: object,
) -> Any | None:
"""Wrapper over `transform()` that allows `None` to be passed.
See `transform()` for more details.
"""
if data is None:
return None
return transform(data, expected_type)
# Wrapper over _transform_recursive providing fake types
def transform(
data: _T,
expected_type: object,
) -> _T:
"""Transform dictionaries based off of type information from the given type, for example:
```py
class Params(TypedDict, total=False):
card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
transformed = transform({"card_id": "<my card ID>"}, Params)
# {'cardID': '<my card ID>'}
```
Any keys / data that does not have type information given will be included as is.
It should be noted that the transformations that this function does are not represented in the type system.
"""
transformed = _transform_recursive(data, annotation=cast(type, expected_type))
return cast(_T, transformed)
@lru_cache(maxsize=8096)
def _get_annotated_type(type_: type) -> type | None:
"""If the given type is an `Annotated` type then it is returned, if not `None` is returned.
This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]`
"""
if is_required_type(type_):
# Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]`
type_ = get_args(type_)[0]
if is_annotated_type(type_):
return type_
return None
def _maybe_transform_key(key: str, type_: type) -> str:
"""Transform the given `data` based on the annotations provided in `type_`.
Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata.
"""
annotated_type = _get_annotated_type(type_)
if annotated_type is None:
# no `Annotated` definition for this type, no transformation needed
return key
# ignore the first argument as it is the actual type
annotations = get_args(annotated_type)[1:]
for annotation in annotations:
if isinstance(annotation, PropertyInfo) and annotation.alias is not None:
return annotation.alias
return key
def _no_transform_needed(annotation: type) -> bool:
return annotation == float or annotation == int
def _transform_recursive(
data: object,
*,
annotation: type,
inner_type: type | None = None,
) -> object:
"""Transform the given data against the expected type.
Args:
annotation: The direct type annotation given to the particular piece of data.
This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
the list can be transformed using the metadata from the container type.
Defaults to the same value as the `annotation` argument.
"""
from .._compat import model_dump
if inner_type is None:
inner_type = annotation
stripped_type = strip_annotated_type(inner_type)
origin = get_origin(stripped_type) or stripped_type
if is_typeddict(stripped_type) and is_mapping(data):
return _transform_typeddict(data, stripped_type)
if origin == dict and is_mapping(data):
items_type = get_args(stripped_type)[1]
return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
if (
# List[T]
(is_list_type(stripped_type) and is_list(data))
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
# Sequence[T]
or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
):
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
# intended as an iterable, so we don't transform it.
if isinstance(data, dict):
return cast(object, data)
inner_type = extract_type_arg(stripped_type, 0)
if _no_transform_needed(inner_type):
# for some types there is no need to transform anything, so we can get a small
# perf boost from skipping that work.
#
# but we still need to convert to a list to ensure the data is json-serializable
if is_list(data):
return data
return list(data)
return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
if is_union_type(stripped_type):
# For union types we run the transformation against all subtypes to ensure that everything is transformed.
#
# TODO: there may be edge cases where the same normalized field name will transform to two different names
# in different subtypes.
for subtype in get_args(stripped_type):
data = _transform_recursive(data, annotation=annotation, inner_type=subtype)
return data
if isinstance(data, pydantic.BaseModel):
return model_dump(data, exclude_unset=True, mode="json")
annotated_type = _get_annotated_type(annotation)
if annotated_type is None:
return data
# ignore the first argument as it is the actual type
annotations = get_args(annotated_type)[1:]
for annotation in annotations:
if isinstance(annotation, PropertyInfo) and annotation.format is not None:
return _format_data(data, annotation.format, annotation.format_template)
return data
def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
if isinstance(data, (date, datetime)):
if format_ == "iso8601":
return data.isoformat()
if format_ == "custom" and format_template is not None:
return data.strftime(format_template)
if format_ == "base64" and is_base64_file_input(data):
binary: str | bytes | None = None
if isinstance(data, pathlib.Path):
binary = data.read_bytes()
elif isinstance(data, io.IOBase):
binary = data.read()
if isinstance(binary, str): # type: ignore[unreachable]
binary = binary.encode()
if not isinstance(binary, bytes):
raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
return base64.b64encode(binary).decode("ascii")
return data
def _transform_typeddict(
data: Mapping[str, object],
expected_type: type,
) -> Mapping[str, object]:
result: dict[str, object] = {}
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
# we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
type_ = annotations.get(key)
if type_ is None:
# we do not have a type annotation for this field, leave it as is
result[key] = value
else:
result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_)
return result
async def async_maybe_transform(
data: object,
expected_type: object,
) -> Any | None:
"""Wrapper over `async_transform()` that allows `None` to be passed.
See `async_transform()` for more details.
"""
if data is None:
return None
return await async_transform(data, expected_type)
async def async_transform(
data: _T,
expected_type: object,
) -> _T:
"""Transform dictionaries based off of type information from the given type, for example:
```py
class Params(TypedDict, total=False):
card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
transformed = transform({"card_id": "<my card ID>"}, Params)
# {'cardID': '<my card ID>'}
```
Any keys / data that does not have type information given will be included as is.
It should be noted that the transformations that this function does are not represented in the type system.
"""
transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type))
return cast(_T, transformed)
async def _async_transform_recursive(
data: object,
*,
annotation: type,
inner_type: type | None = None,
) -> object:
"""Transform the given data against the expected type.
Args:
annotation: The direct type annotation given to the particular piece of data.
This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
the list can be transformed using the metadata from the container type.
Defaults to the same value as the `annotation` argument.
"""
from .._compat import model_dump
if inner_type is None:
inner_type = annotation
stripped_type = strip_annotated_type(inner_type)
origin = get_origin(stripped_type) or stripped_type
if is_typeddict(stripped_type) and is_mapping(data):
return await _async_transform_typeddict(data, stripped_type)
if origin == dict and is_mapping(data):
items_type = get_args(stripped_type)[1]
return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
if (
# List[T]
(is_list_type(stripped_type) and is_list(data))
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
# Sequence[T]
or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
):
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
# intended as an iterable, so we don't transform it.
if isinstance(data, dict):
return cast(object, data)
inner_type = extract_type_arg(stripped_type, 0)
if _no_transform_needed(inner_type):
# for some types there is no need to transform anything, so we can get a small
# perf boost from skipping that work.
#
# but we still need to convert to a list to ensure the data is json-serializable
if is_list(data):
return data
return list(data)
return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
if is_union_type(stripped_type):
# For union types we run the transformation against all subtypes to ensure that everything is transformed.
#
# TODO: there may be edge cases where the same normalized field name will transform to two different names
# in different subtypes.
for subtype in get_args(stripped_type):
data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype)
return data
if isinstance(data, pydantic.BaseModel):
return model_dump(data, exclude_unset=True, mode="json")
annotated_type = _get_annotated_type(annotation)
if annotated_type is None:
return data
# ignore the first argument as it is the actual type
annotations = get_args(annotated_type)[1:]
for annotation in annotations:
if isinstance(annotation, PropertyInfo) and annotation.format is not None:
return await _async_format_data(data, annotation.format, annotation.format_template)
return data
async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
if isinstance(data, (date, datetime)):
if format_ == "iso8601":
return data.isoformat()
if format_ == "custom" and format_template is not None:
return data.strftime(format_template)
if format_ == "base64" and is_base64_file_input(data):
binary: str | bytes | None = None
if isinstance(data, pathlib.Path):
binary = await anyio.Path(data).read_bytes()
elif isinstance(data, io.IOBase):
binary = data.read()
if isinstance(binary, str): # type: ignore[unreachable]
binary = binary.encode()
if not isinstance(binary, bytes):
raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
return base64.b64encode(binary).decode("ascii")
return data
async def _async_transform_typeddict(
data: Mapping[str, object],
expected_type: type,
) -> Mapping[str, object]:
result: dict[str, object] = {}
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
# we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
type_ = annotations.get(key)
if type_ is None:
# we do not have a type annotation for this field, leave it as is
result[key] = value
else:
result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
return result
@lru_cache(maxsize=8096)
def get_type_hints(
obj: Any,
globalns: dict[str, Any] | None = None,
localns: Mapping[str, Any] | None = None,
include_extras: bool = False,
) -> dict[str, Any]:
return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)

View File

@@ -0,0 +1,172 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# mypy: ignore-errors
from __future__ import annotations
import sys
import typing
import typing_extensions
from typing import Any, TypeVar, Iterable, cast
from collections import abc as _c_abc
from typing_extensions import (
TypeIs,
Required,
Annotated,
get_args,
get_origin,
)
from ._utils import lru_cache
from .._types import InheritsGeneric
from ._compat import is_union as _is_union
def is_annotated_type(typ: type) -> bool:
return get_origin(typ) == Annotated
def is_list_type(typ: type) -> bool:
return (get_origin(typ) or typ) == list
def is_sequence_type(typ: type) -> bool:
origin = get_origin(typ) or typ
return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence
def is_iterable_type(typ: type) -> bool:
"""If the given type is `typing.Iterable[T]`"""
origin = get_origin(typ) or typ
return origin == Iterable or origin == _c_abc.Iterable
def is_union_type(typ: type) -> bool:
return _is_union(get_origin(typ))
def is_required_type(typ: type) -> bool:
return get_origin(typ) == Required
def is_typevar(typ: type) -> bool:
# type ignore is required because type checkers
# think this expression will always return False
return type(typ) == TypeVar # type: ignore
_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,)
if sys.version_info >= (3, 12):
_TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType)
def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
"""Return whether the provided argument is an instance of `TypeAliasType`.
```python
type Int = int
is_type_alias_type(Int)
# > True
Str = TypeAliasType("Str", str)
is_type_alias_type(Str)
# > True
```
"""
return isinstance(tp, _TYPE_ALIAS_TYPES)
# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
@lru_cache(maxsize=8096)
def strip_annotated_type(typ: type) -> type:
if is_required_type(typ) or is_annotated_type(typ):
return strip_annotated_type(cast(type, get_args(typ)[0]))
return typ
def extract_type_arg(typ: type, index: int) -> type:
args = get_args(typ)
try:
return cast(type, args[index])
except IndexError as err:
raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err
def extract_type_var_from_base(
typ: type,
*,
generic_bases: tuple[type, ...],
index: int,
failure_message: str | None = None,
) -> type:
"""Given a type like `Foo[T]`, returns the generic type variable `T`.
This also handles the case where a concrete subclass is given, e.g.
```py
class MyResponse(Foo[bytes]):
...
extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes
```
And where a generic subclass is given:
```py
_T = TypeVar('_T')
class MyResponse(Foo[_T]):
...
extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes
```
"""
cls = cast(object, get_origin(typ) or typ)
if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains]
# we're given the class directly
return extract_type_arg(typ, index)
# if a subclass is given
# ---
# this is needed as __orig_bases__ is not present in the typeshed stubs
# because it is intended to be for internal use only, however there does
# not seem to be a way to resolve generic TypeVars for inherited subclasses
# without using it.
if isinstance(cls, InheritsGeneric):
target_base_class: Any | None = None
for base in cls.__orig_bases__:
if base.__origin__ in generic_bases:
target_base_class = base
break
if target_base_class is None:
raise RuntimeError(
"Could not find the generic base class;\n"
"This should never happen;\n"
f"Does {cls} inherit from one of {generic_bases} ?"
)
extracted = extract_type_arg(target_base_class, index)
if is_typevar(extracted):
# If the extracted type argument is itself a type variable
# then that means the subclass itself is generic, so we have
# to resolve the type argument from the class itself, not
# the base class.
#
# Note: if there is more than 1 type argument, the subclass could
# change the ordering of the type arguments, this is not currently
# supported.
return extract_type_arg(typ, index)
return extracted
raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}")

View File

@@ -0,0 +1,438 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# mypy: ignore-errors
from __future__ import annotations
import os
import re
import inspect
import functools
from typing import (
Any,
Tuple,
Mapping,
TypeVar,
Callable,
Iterable,
Sequence,
cast,
overload,
)
from pathlib import Path
from datetime import date, datetime
from typing_extensions import TypeGuard
import sniffio
from .._types import Omit, NotGiven, FileTypes, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
_MappingT = TypeVar("_MappingT", bound=Mapping[str, object])
_SequenceT = TypeVar("_SequenceT", bound=Sequence[object])
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
def flatten(t: Iterable[Iterable[_T]]) -> list[_T]:
return [item for sublist in t for item in sublist]
def extract_files(
# TODO: this needs to take Dict but variance issues.....
# create protocol type ?
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '<array>', 'data'].
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
files.extend(_extract_items(query, path, index=0, flattened_key=None))
return files
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
except IndexError:
if not is_given(obj):
# no value was provided - we can safely ignore
return []
# cyclical import
from .._files import assert_is_file_content
# We have exhausted the path, return the entry we found.
assert flattened_key is not None
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
for entry in obj:
assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
files.append((flattened_key + "[]", cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
return [(flattened_key, cast(FileTypes, obj))]
index += 1
if is_dict(obj):
try:
# Remove the field if there are no more dict keys in the path,
# only "<array>" traversal markers or end.
if all(p == "<array>" for p in path[index:]):
item = obj.pop(key)
else:
item = obj[key]
except KeyError:
# Key was not present in the dictionary, this is not indicative of an error
# as the given path may not point to a required field. We also do not want
# to enforce required fields as the API may differ from the spec in some cases.
return []
if flattened_key is None:
flattened_key = key
else:
flattened_key += f"[{key}]"
return _extract_items(
item,
path,
index=index,
flattened_key=flattened_key,
)
elif is_list(obj):
if key != "<array>":
return []
return flatten(
[
_extract_items(
item,
path,
index=index,
flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
)
for item in obj
]
)
# Something unexpected was passed, just ignore it.
return []
def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
return not isinstance(obj, NotGiven) and not isinstance(obj, Omit)
# Type safe methods for narrowing types with TypeVars.
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
# however this cause Pyright to rightfully report errors. As we know we don't
# care about the contained types we can safely use `object` in its place.
#
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
# `is_*` is for when you're dealing with an unknown input
# `is_*_t` is for when you're narrowing a known union type to a specific subset
def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]:
return isinstance(obj, tuple)
def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]:
return isinstance(obj, tuple)
def is_sequence(obj: object) -> TypeGuard[Sequence[object]]:
return isinstance(obj, Sequence)
def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]:
return isinstance(obj, Sequence)
def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]:
return isinstance(obj, Mapping)
def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]:
return isinstance(obj, Mapping)
def is_dict(obj: object) -> TypeGuard[dict[object, object]]:
return isinstance(obj, dict)
def is_list(obj: object) -> TypeGuard[list[object]]:
return isinstance(obj, list)
def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
def deepcopy_minimal(item: _T) -> _T:
"""Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
- mappings, e.g. `dict`
- list
This is done for performance reasons.
"""
if is_mapping(item):
return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
if is_list(item):
return cast(_T, [deepcopy_minimal(entry) for entry in item])
return item
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
if size == 0:
return ""
if size == 1:
return seq[0]
if size == 2:
return f"{seq[0]} {final} {seq[1]}"
return delim.join(seq[:-1]) + f" {final} {seq[-1]}"
def quote(string: str) -> str:
"""Add single quotation marks around the given string. Does *not* do any escaping."""
return f"'{string}'"
def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]:
"""Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function.
Useful for enforcing runtime validation of overloaded functions.
Example usage:
```py
@overload
def foo(*, a: str) -> str: ...
@overload
def foo(*, b: bool) -> str: ...
# This enforces the same constraints that a static type checker would
# i.e. that either a or b must be passed to the function
@required_args(["a"], ["b"])
def foo(*, a: str | None = None, b: bool | None = None) -> str: ...
```
"""
def inner(func: CallableT) -> CallableT:
params = inspect.signature(func).parameters
positional = [
name
for name, param in params.items()
if param.kind
in {
param.POSITIONAL_ONLY,
param.POSITIONAL_OR_KEYWORD,
}
]
@functools.wraps(func)
def wrapper(*args: object, **kwargs: object) -> object:
given_params: set[str] = set()
for i, _ in enumerate(args):
try:
given_params.add(positional[i])
except IndexError:
raise TypeError(
f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given"
) from None
for key in kwargs.keys():
given_params.add(key)
for variant in variants:
matches = all((param in given_params for param in variant))
if matches:
break
else: # no break
if len(variants) > 1:
variations = human_join(
["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants]
)
msg = f"Missing required arguments; Expected either {variations} arguments to be given"
else:
assert len(variants) > 0
# TODO: this error message is not deterministic
missing = list(set(variants[0]) - given_params)
if len(missing) > 1:
msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}"
else:
msg = f"Missing required argument: {quote(missing[0])}"
raise TypeError(msg)
return func(*args, **kwargs)
return wrapper # type: ignore
return inner
_K = TypeVar("_K")
_V = TypeVar("_V")
@overload
def strip_not_given(obj: None) -> None: ...
@overload
def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ...
@overload
def strip_not_given(obj: object) -> object: ...
def strip_not_given(obj: object | None) -> object:
"""Remove all top-level keys where their values are instances of `NotGiven`"""
if obj is None:
return None
if not is_mapping(obj):
return obj
return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)}
def coerce_integer(val: str) -> int:
return int(val, base=10)
def coerce_float(val: str) -> float:
return float(val)
def coerce_boolean(val: str) -> bool:
return val == "true" or val == "1" or val == "on"
def maybe_coerce_integer(val: str | None) -> int | None:
if val is None:
return None
return coerce_integer(val)
def maybe_coerce_float(val: str | None) -> float | None:
if val is None:
return None
return coerce_float(val)
def maybe_coerce_boolean(val: str | None) -> bool | None:
if val is None:
return None
return coerce_boolean(val)
def removeprefix(string: str, prefix: str) -> str:
"""Remove a prefix from a string.
Backport of `str.removeprefix` for Python < 3.9
"""
if string.startswith(prefix):
return string[len(prefix) :]
return string
def removesuffix(string: str, suffix: str) -> str:
"""Remove a suffix from a string.
Backport of `str.removesuffix` for Python < 3.9
"""
if string.endswith(suffix):
return string[: -len(suffix)]
return string
def file_from_path(path: str) -> FileTypes:
contents = Path(path).read_bytes()
file_name = os.path.basename(path)
return (file_name, contents)
def get_required_header(headers: HeadersLike, header: str) -> str:
lower_header = header.lower()
if is_mapping_t(headers):
# mypy doesn't understand the type narrowing here
for k, v in headers.items(): # type: ignore
if k.lower() == lower_header and isinstance(v, str):
return v
# to deal with the case where the header looks like Stainless-Event-Id
intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize())
for normalized_header in [header, lower_header, header.upper(), intercaps_header]:
value = headers.get(normalized_header)
if value:
return value
raise ValueError(f"Could not find {header} header")
def get_async_library() -> str:
try:
return sniffio.current_async_library()
except Exception:
return "false"
def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]:
"""A version of functools.lru_cache that retains the type signature
for the wrapped function arguments.
"""
wrapper = functools.lru_cache( # noqa: TID251
maxsize=maxsize,
)
return cast(Any, wrapper) # type: ignore[no-any-return]
def json_safe(data: object) -> object:
"""Translates a mapping / sequence recursively in the same fashion
as `pydantic` v2's `model_dump(mode="json")`.
"""
if is_mapping(data):
return {json_safe(key): json_safe(value) for key, value in data.items()}
if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)):
return [json_safe(item) for item in data]
if isinstance(data, (datetime, date)):
return data.isoformat()
return data

View File

@@ -0,0 +1,18 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from ..version import __version__ as __version__
__title__ = "google.genai._interactions"

View File

@@ -0,0 +1,48 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .webhooks import (
WebhooksResource,
AsyncWebhooksResource,
WebhooksResourceWithRawResponse,
AsyncWebhooksResourceWithRawResponse,
WebhooksResourceWithStreamingResponse,
AsyncWebhooksResourceWithStreamingResponse,
)
from .interactions import (
InteractionsResource,
AsyncInteractionsResource,
InteractionsResourceWithRawResponse,
AsyncInteractionsResourceWithRawResponse,
InteractionsResourceWithStreamingResponse,
AsyncInteractionsResourceWithStreamingResponse,
)
__all__ = [
"InteractionsResource",
"AsyncInteractionsResource",
"InteractionsResourceWithRawResponse",
"AsyncInteractionsResourceWithRawResponse",
"InteractionsResourceWithStreamingResponse",
"AsyncInteractionsResourceWithStreamingResponse",
"WebhooksResource",
"AsyncWebhooksResource",
"WebhooksResourceWithRawResponse",
"AsyncWebhooksResourceWithRawResponse",
"WebhooksResourceWithStreamingResponse",
"AsyncWebhooksResourceWithStreamingResponse",
]

View File

@@ -0,0 +1,135 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from .tool import Tool as Tool
from .turn import Turn as Turn
from .model import Model as Model
from .usage import Usage as Usage
from .content import Content as Content
from .webhook import Webhook as Webhook
from .function import Function as Function
from .annotation import Annotation as Annotation
from .tool_param import ToolParam as ToolParam
from .turn_param import TurnParam as TurnParam
from .error_event import ErrorEvent as ErrorEvent
from .interaction import Interaction as Interaction
from .model_param import ModelParam as ModelParam
from .usage_param import UsageParam as UsageParam
from .content_stop import ContentStop as ContentStop
from .image_config import ImageConfig as ImageConfig
from .text_content import TextContent as TextContent
from .url_citation import URLCitation as URLCitation
from .allowed_tools import AllowedTools as AllowedTools
from .audio_content import AudioContent as AudioContent
from .content_delta import ContentDelta as ContentDelta
from .content_param import ContentParam as ContentParam
from .content_start import ContentStart as ContentStart
from .file_citation import FileCitation as FileCitation
from .image_content import ImageContent as ImageContent
from .speech_config import SpeechConfig as SpeechConfig
from .video_content import VideoContent as VideoContent
from .function_param import FunctionParam as FunctionParam
from .place_citation import PlaceCitation as PlaceCitation
from .signing_secret import SigningSecret as SigningSecret
from .thinking_level import ThinkingLevel as ThinkingLevel
from .webhook_config import WebhookConfig as WebhookConfig
from .thought_content import ThoughtContent as ThoughtContent
from .annotation_param import AnnotationParam as AnnotationParam
from .document_content import DocumentContent as DocumentContent
from .tool_choice_type import ToolChoiceType as ToolChoiceType
from .generation_config import GenerationConfig as GenerationConfig
from .google_maps_result import GoogleMapsResult as GoogleMapsResult
from .image_config_param import ImageConfigParam as ImageConfigParam
from .text_content_param import TextContentParam as TextContentParam
from .tool_choice_config import ToolChoiceConfig as ToolChoiceConfig
from .url_citation_param import URLCitationParam as URLCitationParam
from .url_context_result import URLContextResult as URLContextResult
from .allowed_tools_param import AllowedToolsParam as AllowedToolsParam
from .audio_content_param import AudioContentParam as AudioContentParam
from .file_citation_param import FileCitationParam as FileCitationParam
from .image_content_param import ImageContentParam as ImageContentParam
from .speech_config_param import SpeechConfigParam as SpeechConfigParam
from .video_content_param import VideoContentParam as VideoContentParam
from .webhook_list_params import WebhookListParams as WebhookListParams
from .webhook_ping_params import WebhookPingParams as WebhookPingParams
from .dynamic_agent_config import DynamicAgentConfig as DynamicAgentConfig
from .google_search_result import GoogleSearchResult as GoogleSearchResult
from .place_citation_param import PlaceCitationParam as PlaceCitationParam
from .webhook_config_param import WebhookConfigParam as WebhookConfigParam
from .function_call_content import FunctionCallContent as FunctionCallContent
from .interaction_sse_event import InteractionSSEEvent as InteractionSSEEvent
from .thought_content_param import ThoughtContentParam as ThoughtContentParam
from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
from .webhook_list_response import WebhookListResponse as WebhookListResponse
from .webhook_ping_response import WebhookPingResponse as WebhookPingResponse
from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams
from .document_content_param import DocumentContentParam as DocumentContentParam
from .interaction_get_params import InteractionGetParams as InteractionGetParams
from .function_result_content import FunctionResultContent as FunctionResultContent
from .generation_config_param import GenerationConfigParam as GenerationConfigParam
from .interaction_start_event import InteractionStartEvent as InteractionStartEvent
from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
from .file_search_call_content import FileSearchCallContent as FileSearchCallContent
from .google_maps_call_content import GoogleMapsCallContent as GoogleMapsCallContent
from .google_maps_result_param import GoogleMapsResultParam as GoogleMapsResultParam
from .tool_choice_config_param import ToolChoiceConfigParam as ToolChoiceConfigParam
from .url_context_call_content import URLContextCallContent as URLContextCallContent
from .url_context_result_param import URLContextResultParam as URLContextResultParam
from .interaction_create_params import InteractionCreateParams as InteractionCreateParams
from .interaction_status_update import InteractionStatusUpdate as InteractionStatusUpdate
from .deep_research_agent_config import DeepResearchAgentConfig as DeepResearchAgentConfig
from .dynamic_agent_config_param import DynamicAgentConfigParam as DynamicAgentConfigParam
from .file_search_result_content import FileSearchResultContent as FileSearchResultContent
from .google_maps_call_arguments import GoogleMapsCallArguments as GoogleMapsCallArguments
from .google_maps_result_content import GoogleMapsResultContent as GoogleMapsResultContent
from .google_search_call_content import GoogleSearchCallContent as GoogleSearchCallContent
from .google_search_result_param import GoogleSearchResultParam as GoogleSearchResultParam
from .interaction_complete_event import InteractionCompleteEvent as InteractionCompleteEvent
from .url_context_call_arguments import URLContextCallArguments as URLContextCallArguments
from .url_context_result_content import URLContextResultContent as URLContextResultContent
from .code_execution_call_content import CodeExecutionCallContent as CodeExecutionCallContent
from .function_call_content_param import FunctionCallContentParam as FunctionCallContentParam
from .google_search_call_arguments import GoogleSearchCallArguments as GoogleSearchCallArguments
from .google_search_result_content import GoogleSearchResultContent as GoogleSearchResultContent
from .mcp_server_tool_call_content import MCPServerToolCallContent as MCPServerToolCallContent
from .code_execution_call_arguments import CodeExecutionCallArguments as CodeExecutionCallArguments
from .code_execution_result_content import CodeExecutionResultContent as CodeExecutionResultContent
from .function_result_content_param import FunctionResultContentParam as FunctionResultContentParam
from .file_search_call_content_param import FileSearchCallContentParam as FileSearchCallContentParam
from .google_maps_call_content_param import GoogleMapsCallContentParam as GoogleMapsCallContentParam
from .mcp_server_tool_result_content import MCPServerToolResultContent as MCPServerToolResultContent
from .url_context_call_content_param import URLContextCallContentParam as URLContextCallContentParam
from .deep_research_agent_config_param import DeepResearchAgentConfigParam as DeepResearchAgentConfigParam
from .file_search_result_content_param import FileSearchResultContentParam as FileSearchResultContentParam
from .google_maps_call_arguments_param import GoogleMapsCallArgumentsParam as GoogleMapsCallArgumentsParam
from .google_maps_result_content_param import GoogleMapsResultContentParam as GoogleMapsResultContentParam
from .google_search_call_content_param import GoogleSearchCallContentParam as GoogleSearchCallContentParam
from .url_context_call_arguments_param import URLContextCallArgumentsParam as URLContextCallArgumentsParam
from .url_context_result_content_param import URLContextResultContentParam as URLContextResultContentParam
from .code_execution_call_content_param import CodeExecutionCallContentParam as CodeExecutionCallContentParam
from .google_search_call_arguments_param import GoogleSearchCallArgumentsParam as GoogleSearchCallArgumentsParam
from .google_search_result_content_param import GoogleSearchResultContentParam as GoogleSearchResultContentParam
from .mcp_server_tool_call_content_param import MCPServerToolCallContentParam as MCPServerToolCallContentParam
from .code_execution_call_arguments_param import CodeExecutionCallArgumentsParam as CodeExecutionCallArgumentsParam
from .code_execution_result_content_param import CodeExecutionResultContentParam as CodeExecutionResultContentParam
from .mcp_server_tool_result_content_param import MCPServerToolResultContentParam as MCPServerToolResultContentParam
from .webhook_rotate_signing_secret_params import WebhookRotateSigningSecretParams as WebhookRotateSigningSecretParams
from .webhook_rotate_signing_secret_response import (
WebhookRotateSigningSecretResponse as WebhookRotateSigningSecretResponse,
)

View File

@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from .._models import BaseModel
from .tool_choice_type import ToolChoiceType
__all__ = ["AllowedTools"]
class AllowedTools(BaseModel):
"""The configuration for allowed tools."""
mode: Optional[ToolChoiceType] = None
"""The mode of the tool choice."""
tools: Optional[List[str]] = None
"""The names of the allowed tools."""

View File

@@ -0,0 +1,35 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import TypedDict
from .._types import SequenceNotStr
from .tool_choice_type import ToolChoiceType
__all__ = ["AllowedToolsParam"]
class AllowedToolsParam(TypedDict, total=False):
"""The configuration for allowed tools."""
mode: ToolChoiceType
"""The mode of the tool choice."""
tools: SequenceNotStr[str]
"""The names of the allowed tools."""

View File

@@ -0,0 +1,28 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Union
from typing_extensions import Annotated, TypeAlias
from .._utils import PropertyInfo
from .url_citation import URLCitation
from .file_citation import FileCitation
from .place_citation import PlaceCitation
__all__ = ["Annotation"]
Annotation: TypeAlias = Annotated[Union[URLCitation, FileCitation, PlaceCitation], PropertyInfo(discriminator="type")]

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import TypeAlias
from .url_citation_param import URLCitationParam
from .file_citation_param import FileCitationParam
from .place_citation_param import PlaceCitationParam
__all__ = ["AnnotationParam"]
AnnotationParam: TypeAlias = Union[URLCitationParam, FileCitationParam, PlaceCitationParam]

View File

@@ -0,0 +1,59 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["AudioContent"]
class AudioContent(BaseModel):
"""An audio content block."""
type: Literal["audio"]
channels: Optional[int] = None
"""The number of audio channels."""
data: Optional[str] = None
"""The audio content."""
mime_type: Optional[
Literal[
"audio/wav",
"audio/mp3",
"audio/aiff",
"audio/aac",
"audio/ogg",
"audio/flac",
"audio/mpeg",
"audio/m4a",
"audio/l16",
"audio/opus",
"audio/alaw",
"audio/mulaw",
]
] = None
"""The mime type of the audio."""
rate: Optional[int] = None
"""The sample rate of the audio."""
uri: Optional[str] = None
"""The URI of the audio."""

View File

@@ -0,0 +1,64 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["AudioContentParam"]
class AudioContentParam(TypedDict, total=False):
"""An audio content block."""
type: Required[Literal["audio"]]
channels: int
"""The number of audio channels."""
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""The audio content."""
mime_type: Literal[
"audio/wav",
"audio/mp3",
"audio/aiff",
"audio/aac",
"audio/ogg",
"audio/flac",
"audio/mpeg",
"audio/m4a",
"audio/l16",
"audio/opus",
"audio/alaw",
"audio/mulaw",
]
"""The mime type of the audio."""
rate: int
"""The sample rate of the audio."""
uri: str
"""The URI of the audio."""
set_pydantic_config(AudioContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["CodeExecutionCallArguments"]
class CodeExecutionCallArguments(BaseModel):
"""The arguments to pass to the code execution."""
code: Optional[str] = None
"""The code to be executed."""
language: Optional[Literal["python"]] = None
"""Programming language of the `code`."""

View File

@@ -0,0 +1,32 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, TypedDict
__all__ = ["CodeExecutionCallArgumentsParam"]
class CodeExecutionCallArgumentsParam(TypedDict, total=False):
"""The arguments to pass to the code execution."""
code: str
"""The code to be executed."""
language: Literal["python"]
"""Programming language of the `code`."""

View File

@@ -0,0 +1,39 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
from .code_execution_call_arguments import CodeExecutionCallArguments
__all__ = ["CodeExecutionCallContent"]
class CodeExecutionCallContent(BaseModel):
"""Code execution content."""
id: str
"""Required. A unique ID for this specific tool call."""
arguments: CodeExecutionCallArguments
"""Required. The arguments to pass to the code execution."""
type: Literal["code_execution_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,46 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .code_execution_call_arguments_param import CodeExecutionCallArgumentsParam
__all__ = ["CodeExecutionCallContentParam"]
class CodeExecutionCallContentParam(TypedDict, total=False):
"""Code execution content."""
id: Required[str]
"""Required. A unique ID for this specific tool call."""
arguments: Required[CodeExecutionCallArgumentsParam]
"""Required. The arguments to pass to the code execution."""
type: Required[Literal["code_execution_call"]]
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(CodeExecutionCallContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,41 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["CodeExecutionResultContent"]
class CodeExecutionResultContent(BaseModel):
"""Code execution result content."""
call_id: str
"""Required. ID to match the ID from the function call block."""
result: str
"""Required. The output of the code execution."""
type: Literal["code_execution_result"]
is_error: Optional[bool] = None
"""Whether the code execution resulted in an error."""
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,48 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["CodeExecutionResultContentParam"]
class CodeExecutionResultContentParam(TypedDict, total=False):
"""Code execution result content."""
call_id: Required[str]
"""Required. ID to match the ID from the function call block."""
result: Required[str]
"""Required. The output of the code execution."""
type: Required[Literal["code_execution_result"]]
is_error: bool
"""Whether the code execution resulted in an error."""
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(CodeExecutionResultContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,69 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Union
from typing_extensions import Annotated, TypeAlias
from .._utils import PropertyInfo
from .text_content import TextContent
from .audio_content import AudioContent
from .image_content import ImageContent
from .video_content import VideoContent
from .thought_content import ThoughtContent
from .document_content import DocumentContent
from .function_call_content import FunctionCallContent
from .function_result_content import FunctionResultContent
from .file_search_call_content import FileSearchCallContent
from .google_maps_call_content import GoogleMapsCallContent
from .url_context_call_content import URLContextCallContent
from .file_search_result_content import FileSearchResultContent
from .google_maps_result_content import GoogleMapsResultContent
from .google_search_call_content import GoogleSearchCallContent
from .url_context_result_content import URLContextResultContent
from .code_execution_call_content import CodeExecutionCallContent
from .google_search_result_content import GoogleSearchResultContent
from .mcp_server_tool_call_content import MCPServerToolCallContent
from .code_execution_result_content import CodeExecutionResultContent
from .mcp_server_tool_result_content import MCPServerToolResultContent
__all__ = ["Content"]
Content: TypeAlias = Annotated[
Union[
TextContent,
ImageContent,
AudioContent,
DocumentContent,
VideoContent,
ThoughtContent,
FunctionCallContent,
CodeExecutionCallContent,
URLContextCallContent,
MCPServerToolCallContent,
GoogleSearchCallContent,
FileSearchCallContent,
GoogleMapsCallContent,
FunctionResultContent,
CodeExecutionResultContent,
URLContextResultContent,
GoogleSearchResultContent,
MCPServerToolResultContent,
FileSearchResultContent,
GoogleMapsResultContent,
],
PropertyInfo(discriminator="type"),
]

View File

@@ -0,0 +1,427 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Dict, List, Union, Optional
from typing_extensions import Literal, Annotated, TypeAlias
from .._utils import PropertyInfo
from .._models import BaseModel
from .annotation import Annotation
from .text_content import TextContent
from .image_content import ImageContent
from .google_maps_result import GoogleMapsResult
from .url_context_result import URLContextResult
from .google_search_result import GoogleSearchResult
from .google_maps_call_arguments import GoogleMapsCallArguments
from .url_context_call_arguments import URLContextCallArguments
from .google_search_call_arguments import GoogleSearchCallArguments
from .code_execution_call_arguments import CodeExecutionCallArguments
__all__ = [
"ContentDelta",
"Delta",
"DeltaText",
"DeltaImage",
"DeltaAudio",
"DeltaDocument",
"DeltaVideo",
"DeltaThoughtSummary",
"DeltaThoughtSummaryContent",
"DeltaThoughtSignature",
"DeltaFunctionCall",
"DeltaCodeExecutionCall",
"DeltaURLContextCall",
"DeltaGoogleSearchCall",
"DeltaMCPServerToolCall",
"DeltaFileSearchCall",
"DeltaGoogleMapsCall",
"DeltaFunctionResult",
"DeltaFunctionResultResultFunctionResultSubcontentList",
"DeltaCodeExecutionResult",
"DeltaURLContextResult",
"DeltaGoogleSearchResult",
"DeltaMCPServerToolResult",
"DeltaMCPServerToolResultResultFunctionResultSubcontentList",
"DeltaFileSearchResult",
"DeltaFileSearchResultResult",
"DeltaGoogleMapsResult",
"DeltaTextAnnotation",
]
class DeltaText(BaseModel):
text: str
type: Literal["text"]
class DeltaImage(BaseModel):
type: Literal["image"]
data: Optional[str] = None
mime_type: Optional[
Literal[
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
]
] = None
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
"""The resolution of the media."""
uri: Optional[str] = None
class DeltaAudio(BaseModel):
type: Literal["audio"]
channels: Optional[int] = None
"""The number of audio channels."""
data: Optional[str] = None
mime_type: Optional[
Literal[
"audio/wav",
"audio/mp3",
"audio/aiff",
"audio/aac",
"audio/ogg",
"audio/flac",
"audio/mpeg",
"audio/m4a",
"audio/l16",
"audio/opus",
"audio/alaw",
"audio/mulaw",
]
] = None
rate: Optional[int] = None
"""The sample rate of the audio."""
uri: Optional[str] = None
class DeltaDocument(BaseModel):
type: Literal["document"]
data: Optional[str] = None
mime_type: Optional[Literal["application/pdf"]] = None
uri: Optional[str] = None
class DeltaVideo(BaseModel):
type: Literal["video"]
data: Optional[str] = None
mime_type: Optional[
Literal[
"video/mp4",
"video/mpeg",
"video/mpg",
"video/mov",
"video/avi",
"video/x-flv",
"video/webm",
"video/wmv",
"video/3gpp",
]
] = None
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
"""The resolution of the media."""
uri: Optional[str] = None
DeltaThoughtSummaryContent: TypeAlias = Annotated[Union[TextContent, ImageContent], PropertyInfo(discriminator="type")]
class DeltaThoughtSummary(BaseModel):
type: Literal["thought_summary"]
content: Optional[DeltaThoughtSummaryContent] = None
"""A new summary item to be added to the thought."""
class DeltaThoughtSignature(BaseModel):
type: Literal["thought_signature"]
signature: Optional[str] = None
"""Signature to match the backend source to be part of the generation."""
class DeltaFunctionCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
arguments: Dict[str, object]
name: str
type: Literal["function_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaCodeExecutionCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
arguments: CodeExecutionCallArguments
"""The arguments to pass to the code execution."""
type: Literal["code_execution_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaURLContextCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
arguments: URLContextCallArguments
"""The arguments to pass to the URL context."""
type: Literal["url_context_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaGoogleSearchCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
arguments: GoogleSearchCallArguments
"""The arguments to pass to Google Search."""
type: Literal["google_search_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaMCPServerToolCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
arguments: Dict[str, object]
name: str
server_name: str
type: Literal["mcp_server_tool_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaFileSearchCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
type: Literal["file_search_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaGoogleMapsCall(BaseModel):
id: str
"""Required. A unique ID for this specific tool call."""
type: Literal["google_maps_call"]
arguments: Optional[GoogleMapsCallArguments] = None
"""The arguments to pass to the Google Maps tool."""
signature: Optional[str] = None
"""A signature hash for backend validation."""
DeltaFunctionResultResultFunctionResultSubcontentList: TypeAlias = Annotated[
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
]
class DeltaFunctionResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: Union[List[DeltaFunctionResultResultFunctionResultSubcontentList], str, object]
type: Literal["function_result"]
is_error: Optional[bool] = None
name: Optional[str] = None
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaCodeExecutionResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: str
type: Literal["code_execution_result"]
is_error: Optional[bool] = None
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaURLContextResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[URLContextResult]
type: Literal["url_context_result"]
is_error: Optional[bool] = None
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaGoogleSearchResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[GoogleSearchResult]
type: Literal["google_search_result"]
is_error: Optional[bool] = None
signature: Optional[str] = None
"""A signature hash for backend validation."""
DeltaMCPServerToolResultResultFunctionResultSubcontentList: TypeAlias = Annotated[
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
]
class DeltaMCPServerToolResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: Union[List[DeltaMCPServerToolResultResultFunctionResultSubcontentList], str, object]
type: Literal["mcp_server_tool_result"]
name: Optional[str] = None
server_name: Optional[str] = None
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaFileSearchResultResult(BaseModel):
"""The result of the File Search."""
custom_metadata: Optional[List[object]] = None
"""User provided metadata about the FileSearchResult."""
class DeltaFileSearchResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[DeltaFileSearchResultResult]
type: Literal["file_search_result"]
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaGoogleMapsResult(BaseModel):
call_id: str
"""Required. ID to match the ID from the function call block."""
type: Literal["google_maps_result"]
result: Optional[List[GoogleMapsResult]] = None
"""The results of the Google Maps."""
signature: Optional[str] = None
"""A signature hash for backend validation."""
class DeltaTextAnnotation(BaseModel):
type: Literal["text_annotation"]
annotations: Optional[List[Annotation]] = None
"""Citation information for model-generated content."""
Delta: TypeAlias = Annotated[
Union[
DeltaText,
DeltaImage,
DeltaAudio,
DeltaDocument,
DeltaVideo,
DeltaThoughtSummary,
DeltaThoughtSignature,
DeltaFunctionCall,
DeltaCodeExecutionCall,
DeltaURLContextCall,
DeltaGoogleSearchCall,
DeltaMCPServerToolCall,
DeltaFileSearchCall,
DeltaGoogleMapsCall,
DeltaFunctionResult,
DeltaCodeExecutionResult,
DeltaURLContextResult,
DeltaGoogleSearchResult,
DeltaMCPServerToolResult,
DeltaFileSearchResult,
DeltaGoogleMapsResult,
DeltaTextAnnotation,
],
PropertyInfo(discriminator="type"),
]
class ContentDelta(BaseModel):
delta: Delta
"""The delta content data for a content block."""
event_type: Literal["content.delta"]
index: int
event_id: Optional[str] = None
"""
The event_id token to be used to resume the interaction stream, from this event.
"""

View File

@@ -0,0 +1,67 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import TypeAlias
from .text_content_param import TextContentParam
from .audio_content_param import AudioContentParam
from .image_content_param import ImageContentParam
from .video_content_param import VideoContentParam
from .thought_content_param import ThoughtContentParam
from .document_content_param import DocumentContentParam
from .function_call_content_param import FunctionCallContentParam
from .function_result_content_param import FunctionResultContentParam
from .file_search_call_content_param import FileSearchCallContentParam
from .google_maps_call_content_param import GoogleMapsCallContentParam
from .url_context_call_content_param import URLContextCallContentParam
from .file_search_result_content_param import FileSearchResultContentParam
from .google_maps_result_content_param import GoogleMapsResultContentParam
from .google_search_call_content_param import GoogleSearchCallContentParam
from .url_context_result_content_param import URLContextResultContentParam
from .code_execution_call_content_param import CodeExecutionCallContentParam
from .google_search_result_content_param import GoogleSearchResultContentParam
from .mcp_server_tool_call_content_param import MCPServerToolCallContentParam
from .code_execution_result_content_param import CodeExecutionResultContentParam
from .mcp_server_tool_result_content_param import MCPServerToolResultContentParam
__all__ = ["ContentParam"]
ContentParam: TypeAlias = Union[
TextContentParam,
ImageContentParam,
AudioContentParam,
DocumentContentParam,
VideoContentParam,
ThoughtContentParam,
FunctionCallContentParam,
CodeExecutionCallContentParam,
URLContextCallContentParam,
MCPServerToolCallContentParam,
GoogleSearchCallContentParam,
FileSearchCallContentParam,
GoogleMapsCallContentParam,
FunctionResultContentParam,
CodeExecutionResultContentParam,
URLContextResultContentParam,
GoogleSearchResultContentParam,
MCPServerToolResultContentParam,
FileSearchResultContentParam,
GoogleMapsResultContentParam,
]

View File

@@ -0,0 +1,38 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .content import Content
from .._models import BaseModel
__all__ = ["ContentStart"]
class ContentStart(BaseModel):
content: Content
"""The content of the response."""
event_type: Literal["content.start"]
index: int
event_id: Optional[str] = None
"""
The event_id token to be used to resume the interaction stream, from this event.
"""

View File

@@ -0,0 +1,34 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["ContentStop"]
class ContentStop(BaseModel):
event_type: Literal["content.stop"]
index: int
event_id: Optional[str] = None
"""
The event_id token to be used to resume the interaction stream, from this event.
"""

View File

@@ -0,0 +1,43 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["DeepResearchAgentConfig"]
class DeepResearchAgentConfig(BaseModel):
"""Configuration for the Deep Research agent."""
type: Literal["deep-research"]
collaborative_planning: Optional[bool] = None
"""Enables human-in-the-loop planning for the Deep Research agent.
If set to true, the Deep Research agent will provide a research plan in its
response. The agent will then proceed only if the user confirms the plan in the
next turn. Relevant issue: b/482352502.
"""
thinking_summaries: Optional[Literal["auto", "none"]] = None
"""Whether to include thought summaries in the response."""
visualization: Optional[Literal["off", "auto"]] = None
"""Whether to include visualizations in the response."""

View File

@@ -0,0 +1,42 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, Required, TypedDict
__all__ = ["DeepResearchAgentConfigParam"]
class DeepResearchAgentConfigParam(TypedDict, total=False):
"""Configuration for the Deep Research agent."""
type: Required[Literal["deep-research"]]
collaborative_planning: bool
"""Enables human-in-the-loop planning for the Deep Research agent.
If set to true, the Deep Research agent will provide a research plan in its
response. The agent will then proceed only if the user confirms the plan in the
next turn. Relevant issue: b/482352502.
"""
thinking_summaries: Literal["auto", "none"]
"""Whether to include thought summaries in the response."""
visualization: Literal["off", "auto"]
"""Whether to include visualizations in the response."""

View File

@@ -0,0 +1,38 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["DocumentContent"]
class DocumentContent(BaseModel):
"""A document content block."""
type: Literal["document"]
data: Optional[str] = None
"""The document content."""
mime_type: Optional[Literal["application/pdf"]] = None
"""The mime type of the document."""
uri: Optional[str] = None
"""The URI of the document."""

View File

@@ -0,0 +1,45 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["DocumentContentParam"]
class DocumentContentParam(TypedDict, total=False):
"""A document content block."""
type: Required[Literal["document"]]
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""The document content."""
mime_type: Literal["application/pdf"]
"""The mime type of the document."""
uri: str
"""The URI of the document."""
set_pydantic_config(DocumentContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,43 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import TYPE_CHECKING, Dict
from typing_extensions import Literal
from pydantic import Field as FieldInfo
from .._models import BaseModel
__all__ = ["DynamicAgentConfig"]
class DynamicAgentConfig(BaseModel):
"""Configuration for dynamic agents."""
type: Literal["dynamic"]
if TYPE_CHECKING:
# Some versions of Pydantic <2.8.0 have a bug and dont allow assigning a
# value to this field, so for compatibility we avoid doing it at runtime.
__pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
# Stub to indicate that arbitrary properties are accepted.
# To access properties that are not valid identifiers you can use `getattr`, e.g.
# `getattr(obj, '$type')`
def __getattr__(self, attr: str) -> object: ...
else:
__pydantic_extra__: Dict[str, object]

View File

@@ -0,0 +1,28 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, Required, TypedDict
__all__ = ["DynamicAgentConfigParam"]
class DynamicAgentConfigParam(TypedDict, total=False, extra_items=object): # type: ignore[call-arg]
"""Configuration for dynamic agents."""
type: Required[Literal["dynamic"]]

View File

@@ -0,0 +1,45 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["ErrorEvent", "Error"]
class Error(BaseModel):
"""Error message from an interaction."""
code: Optional[str] = None
"""A URI that identifies the error type."""
message: Optional[str] = None
"""A human-readable error message."""
class ErrorEvent(BaseModel):
event_type: Literal["error"]
error: Optional[Error] = None
"""Error message from an interaction."""
event_id: Optional[str] = None
"""
The event_id token to be used to resume the interaction stream, from this event.
"""

View File

@@ -0,0 +1,47 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["FileCitation"]
class FileCitation(BaseModel):
"""A file citation annotation."""
type: Literal["file_citation"]
document_uri: Optional[str] = None
"""The URI of the file."""
end_index: Optional[int] = None
"""End of the attributed segment, exclusive."""
file_name: Optional[str] = None
"""The name of the file."""
source: Optional[str] = None
"""Source attributed for a portion of the text."""
start_index: Optional[int] = None
"""Start of segment of the response that is attributed to this source.
Index indicates the start of the segment, measured in bytes.
"""

View File

@@ -0,0 +1,46 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, Required, TypedDict
__all__ = ["FileCitationParam"]
class FileCitationParam(TypedDict, total=False):
"""A file citation annotation."""
type: Required[Literal["file_citation"]]
document_uri: str
"""The URI of the file."""
end_index: int
"""End of the attributed segment, exclusive."""
file_name: str
"""The name of the file."""
source: str
"""Source attributed for a portion of the text."""
start_index: int
"""Start of segment of the response that is attributed to this source.
Index indicates the start of the segment, measured in bytes.
"""

View File

@@ -0,0 +1,35 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["FileSearchCallContent"]
class FileSearchCallContent(BaseModel):
"""File Search content."""
id: str
"""Required. A unique ID for this specific tool call."""
type: Literal["file_search_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,42 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["FileSearchCallContentParam"]
class FileSearchCallContentParam(TypedDict, total=False):
"""File Search content."""
id: Required[str]
"""Required. A unique ID for this specific tool call."""
type: Required[Literal["file_search_call"]]
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(FileSearchCallContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,45 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["FileSearchResultContent", "Result"]
class Result(BaseModel):
"""The result of the File Search."""
custom_metadata: Optional[List[object]] = None
"""User provided metadata about the FileSearchResult."""
class FileSearchResultContent(BaseModel):
"""File Search result content."""
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[Result]
"""Required. The results of the File Search."""
type: Literal["file_search_result"]
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,52 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union, Iterable
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["FileSearchResultContentParam", "Result"]
class Result(TypedDict, total=False):
"""The result of the File Search."""
custom_metadata: Iterable[object]
"""User provided metadata about the FileSearchResult."""
class FileSearchResultContentParam(TypedDict, total=False):
"""File Search result content."""
call_id: Required[str]
"""Required. ID to match the ID from the function call block."""
result: Required[Iterable[Result]]
"""Required. The results of the File Search."""
type: Required[Literal["file_search_result"]]
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(FileSearchResultContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,38 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["Function"]
class Function(BaseModel):
"""A tool that can be used by the model."""
type: Literal["function"]
description: Optional[str] = None
"""A description of the function."""
name: Optional[str] = None
"""The name of the function."""
parameters: Optional[object] = None
"""The JSON Schema for the function's parameters."""

View File

@@ -0,0 +1,41 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Dict, Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["FunctionCallContent"]
class FunctionCallContent(BaseModel):
"""A function tool call content block."""
id: str
"""Required. A unique ID for this specific tool call."""
arguments: Dict[str, object]
"""Required. The arguments to pass to the function."""
name: str
"""Required. The name of the tool to call."""
type: Literal["function_call"]
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,48 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Dict, Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["FunctionCallContentParam"]
class FunctionCallContentParam(TypedDict, total=False):
"""A function tool call content block."""
id: Required[str]
"""Required. A unique ID for this specific tool call."""
arguments: Required[Dict[str, object]]
"""Required. The arguments to pass to the function."""
name: Required[str]
"""Required. The name of the tool to call."""
type: Required[Literal["function_call"]]
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(FunctionCallContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,37 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, Required, TypedDict
__all__ = ["FunctionParam"]
class FunctionParam(TypedDict, total=False):
"""A tool that can be used by the model."""
type: Required[Literal["function"]]
description: str
"""A description of the function."""
name: str
"""The name of the function."""
parameters: object
"""The JSON Schema for the function's parameters."""

View File

@@ -0,0 +1,51 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Union, Optional
from typing_extensions import Literal, Annotated, TypeAlias
from .._utils import PropertyInfo
from .._models import BaseModel
from .text_content import TextContent
from .image_content import ImageContent
__all__ = ["FunctionResultContent", "ResultFunctionResultSubcontentList"]
ResultFunctionResultSubcontentList: TypeAlias = Annotated[
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
]
class FunctionResultContent(BaseModel):
"""A function tool result content block."""
call_id: str
"""Required. ID to match the ID from the function call block."""
result: Union[List[ResultFunctionResultSubcontentList], str, object]
"""The result of the tool call."""
type: Literal["function_result"]
is_error: Optional[bool] = None
"""Whether the tool call resulted in an error."""
name: Optional[str] = None
"""The name of the tool that was called."""
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,55 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union, Iterable
from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .text_content_param import TextContentParam
from .image_content_param import ImageContentParam
__all__ = ["FunctionResultContentParam", "ResultFunctionResultSubcontentList"]
ResultFunctionResultSubcontentList: TypeAlias = Union[TextContentParam, ImageContentParam]
class FunctionResultContentParam(TypedDict, total=False):
"""A function tool result content block."""
call_id: Required[str]
"""Required. ID to match the ID from the function call block."""
result: Required[Union[Iterable[ResultFunctionResultSubcontentList], str, object]]
"""The result of the tool call."""
type: Required[Literal["function_result"]]
is_error: bool
"""Whether the tool call resulted in an error."""
name: str
"""The name of the tool that was called."""
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(FunctionResultContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,64 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Union, Optional
from typing_extensions import Literal, TypeAlias
from .._models import BaseModel
from .image_config import ImageConfig
from .speech_config import SpeechConfig
from .thinking_level import ThinkingLevel
from .tool_choice_type import ToolChoiceType
from .tool_choice_config import ToolChoiceConfig
__all__ = ["GenerationConfig", "ToolChoice"]
ToolChoice: TypeAlias = Union[ToolChoiceType, ToolChoiceConfig]
class GenerationConfig(BaseModel):
"""Configuration parameters for model interactions."""
image_config: Optional[ImageConfig] = None
"""Configuration for image interaction."""
max_output_tokens: Optional[int] = None
"""The maximum number of tokens to include in the response."""
seed: Optional[int] = None
"""Seed used in decoding for reproducibility."""
speech_config: Optional[List[SpeechConfig]] = None
"""Configuration for speech interaction."""
stop_sequences: Optional[List[str]] = None
"""A list of character sequences that will stop output interaction."""
temperature: Optional[float] = None
"""Controls the randomness of the output."""
thinking_level: Optional[ThinkingLevel] = None
"""The level of thought tokens that the model should generate."""
thinking_summaries: Optional[Literal["auto", "none"]] = None
"""Whether to include thought summaries in the response."""
tool_choice: Optional[ToolChoice] = None
"""The tool choice configuration."""
top_p: Optional[float] = None
"""The maximum cumulative probability of tokens to consider when sampling."""

View File

@@ -0,0 +1,66 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union, Iterable
from typing_extensions import Literal, TypeAlias, TypedDict
from .._types import SequenceNotStr
from .thinking_level import ThinkingLevel
from .tool_choice_type import ToolChoiceType
from .image_config_param import ImageConfigParam
from .speech_config_param import SpeechConfigParam
from .tool_choice_config_param import ToolChoiceConfigParam
__all__ = ["GenerationConfigParam", "ToolChoice"]
ToolChoice: TypeAlias = Union[ToolChoiceType, ToolChoiceConfigParam]
class GenerationConfigParam(TypedDict, total=False):
"""Configuration parameters for model interactions."""
image_config: ImageConfigParam
"""Configuration for image interaction."""
max_output_tokens: int
"""The maximum number of tokens to include in the response."""
seed: int
"""Seed used in decoding for reproducibility."""
speech_config: Iterable[SpeechConfigParam]
"""Configuration for speech interaction."""
stop_sequences: SequenceNotStr[str]
"""A list of character sequences that will stop output interaction."""
temperature: float
"""Controls the randomness of the output."""
thinking_level: ThinkingLevel
"""The level of thought tokens that the model should generate."""
thinking_summaries: Literal["auto", "none"]
"""Whether to include thought summaries in the response."""
tool_choice: ToolChoice
"""The tool choice configuration."""
top_p: float
"""The maximum cumulative probability of tokens to consider when sampling."""

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from .._models import BaseModel
__all__ = ["GoogleMapsCallArguments"]
class GoogleMapsCallArguments(BaseModel):
"""The arguments to pass to the Google Maps tool."""
queries: Optional[List[str]] = None
"""The queries to be executed."""

View File

@@ -0,0 +1,31 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import TypedDict
from .._types import SequenceNotStr
__all__ = ["GoogleMapsCallArgumentsParam"]
class GoogleMapsCallArgumentsParam(TypedDict, total=False):
"""The arguments to pass to the Google Maps tool."""
queries: SequenceNotStr[str]
"""The queries to be executed."""

View File

@@ -0,0 +1,39 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
from .google_maps_call_arguments import GoogleMapsCallArguments
__all__ = ["GoogleMapsCallContent"]
class GoogleMapsCallContent(BaseModel):
"""Google Maps content."""
id: str
"""Required. A unique ID for this specific tool call."""
type: Literal["google_maps_call"]
arguments: Optional[GoogleMapsCallArguments] = None
"""The arguments to pass to the Google Maps tool."""
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,46 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .google_maps_call_arguments_param import GoogleMapsCallArgumentsParam
__all__ = ["GoogleMapsCallContentParam"]
class GoogleMapsCallContentParam(TypedDict, total=False):
"""Google Maps content."""
id: Required[str]
"""Required. A unique ID for this specific tool call."""
type: Required[Literal["google_maps_call"]]
arguments: GoogleMapsCallArgumentsParam
"""The arguments to pass to the Google Maps tool."""
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(GoogleMapsCallContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from .._models import BaseModel
__all__ = ["GoogleMapsResult", "Place", "PlaceReviewSnippet"]
class PlaceReviewSnippet(BaseModel):
"""
Encapsulates a snippet of a user review that answers a question about
the features of a specific place in Google Maps.
"""
review_id: Optional[str] = None
"""The ID of the review snippet."""
title: Optional[str] = None
"""Title of the review."""
url: Optional[str] = None
"""A link that corresponds to the user review on Google Maps."""
class Place(BaseModel):
name: Optional[str] = None
"""Title of the place."""
place_id: Optional[str] = None
"""The ID of the place, in `places/{place_id}` format."""
review_snippets: Optional[List[PlaceReviewSnippet]] = None
"""
Snippets of reviews that are used to generate answers about the features of a
given place in Google Maps.
"""
url: Optional[str] = None
"""URI reference of the place."""
class GoogleMapsResult(BaseModel):
"""The result of the Google Maps."""
places: Optional[List[Place]] = None
"""The places that were found."""
widget_context_token: Optional[str] = None
"""Resource name of the Google Maps widget context token."""

View File

@@ -0,0 +1,39 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from typing_extensions import Literal
from .._models import BaseModel
from .google_maps_result import GoogleMapsResult
__all__ = ["GoogleMapsResultContent"]
class GoogleMapsResultContent(BaseModel):
"""Google Maps result content."""
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[GoogleMapsResult]
"""Required. The results of the Google Maps."""
type: Literal["google_maps_result"]
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,46 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union, Iterable
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .google_maps_result_param import GoogleMapsResultParam
__all__ = ["GoogleMapsResultContentParam"]
class GoogleMapsResultContentParam(TypedDict, total=False):
"""Google Maps result content."""
call_id: Required[str]
"""Required. ID to match the ID from the function call block."""
result: Required[Iterable[GoogleMapsResultParam]]
"""Required. The results of the Google Maps."""
type: Required[Literal["google_maps_result"]]
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(GoogleMapsResultContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,66 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Iterable
from typing_extensions import TypedDict
__all__ = ["GoogleMapsResultParam", "Place", "PlaceReviewSnippet"]
class PlaceReviewSnippet(TypedDict, total=False):
"""
Encapsulates a snippet of a user review that answers a question about
the features of a specific place in Google Maps.
"""
review_id: str
"""The ID of the review snippet."""
title: str
"""Title of the review."""
url: str
"""A link that corresponds to the user review on Google Maps."""
class Place(TypedDict, total=False):
name: str
"""Title of the place."""
place_id: str
"""The ID of the place, in `places/{place_id}` format."""
review_snippets: Iterable[PlaceReviewSnippet]
"""
Snippets of reviews that are used to generate answers about the features of a
given place in Google Maps.
"""
url: str
"""URI reference of the place."""
class GoogleMapsResultParam(TypedDict, total=False):
"""The result of the Google Maps."""
places: Iterable[Place]
"""The places that were found."""
widget_context_token: str
"""Resource name of the Google Maps widget context token."""

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from .._models import BaseModel
__all__ = ["GoogleSearchCallArguments"]
class GoogleSearchCallArguments(BaseModel):
"""The arguments to pass to Google Search."""
queries: Optional[List[str]] = None
"""Web search queries for the following-up web search."""

View File

@@ -0,0 +1,31 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import TypedDict
from .._types import SequenceNotStr
__all__ = ["GoogleSearchCallArgumentsParam"]
class GoogleSearchCallArgumentsParam(TypedDict, total=False):
"""The arguments to pass to Google Search."""
queries: SequenceNotStr[str]
"""Web search queries for the following-up web search."""

View File

@@ -0,0 +1,42 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
from .google_search_call_arguments import GoogleSearchCallArguments
__all__ = ["GoogleSearchCallContent"]
class GoogleSearchCallContent(BaseModel):
"""Google Search content."""
id: str
"""Required. A unique ID for this specific tool call."""
arguments: GoogleSearchCallArguments
"""Required. The arguments to pass to Google Search."""
type: Literal["google_search_call"]
search_type: Optional[Literal["web_search", "image_search", "enterprise_web_search"]] = None
"""The type of search grounding enabled."""
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,49 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .google_search_call_arguments_param import GoogleSearchCallArgumentsParam
__all__ = ["GoogleSearchCallContentParam"]
class GoogleSearchCallContentParam(TypedDict, total=False):
"""Google Search content."""
id: Required[str]
"""Required. A unique ID for this specific tool call."""
arguments: Required[GoogleSearchCallArgumentsParam]
"""Required. The arguments to pass to Google Search."""
type: Required[Literal["google_search_call"]]
search_type: Literal["web_search", "image_search", "enterprise_web_search"]
"""The type of search grounding enabled."""
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(GoogleSearchCallContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from .._models import BaseModel
__all__ = ["GoogleSearchResult"]
class GoogleSearchResult(BaseModel):
"""The result of the Google Search."""
search_suggestions: Optional[str] = None
"""Web content snippet that can be embedded in a web page or an app webview."""

View File

@@ -0,0 +1,42 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List, Optional
from typing_extensions import Literal
from .._models import BaseModel
from .google_search_result import GoogleSearchResult
__all__ = ["GoogleSearchResultContent"]
class GoogleSearchResultContent(BaseModel):
"""Google Search result content."""
call_id: str
"""Required. ID to match the ID from the function call block."""
result: List[GoogleSearchResult]
"""Required. The results of the Google Search."""
type: Literal["google_search_result"]
is_error: Optional[bool] = None
"""Whether the Google Search resulted in an error."""
signature: Optional[str] = None
"""A signature hash for backend validation."""

View File

@@ -0,0 +1,49 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union, Iterable
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
from .google_search_result_param import GoogleSearchResultParam
__all__ = ["GoogleSearchResultContentParam"]
class GoogleSearchResultContentParam(TypedDict, total=False):
"""Google Search result content."""
call_id: Required[str]
"""Required. ID to match the ID from the function call block."""
result: Required[Iterable[GoogleSearchResultParam]]
"""Required. The results of the Google Search."""
type: Required[Literal["google_search_result"]]
is_error: bool
"""Whether the Google Search resulted in an error."""
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""A signature hash for backend validation."""
set_pydantic_config(GoogleSearchResultContentParam, {"arbitrary_types_allowed": True})

View File

@@ -0,0 +1,29 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import TypedDict
__all__ = ["GoogleSearchResultParam"]
class GoogleSearchResultParam(TypedDict, total=False):
"""The result of the Google Search."""
search_suggestions: str
"""Web content snippet that can be embedded in a web page or an app webview."""

View File

@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["ImageConfig"]
class ImageConfig(BaseModel):
"""The configuration for image interaction."""
aspect_ratio: Optional[
Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1"]
] = None
image_size: Optional[Literal["1K", "2K", "4K", "512"]] = None

View File

@@ -0,0 +1,32 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing_extensions import Literal, TypedDict
__all__ = ["ImageConfigParam"]
class ImageConfigParam(TypedDict, total=False):
"""The configuration for image interaction."""
aspect_ratio: Literal[
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1"
]
image_size: Literal["1K", "2K", "4K", "512"]

View File

@@ -0,0 +1,45 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
from typing_extensions import Literal
from .._models import BaseModel
__all__ = ["ImageContent"]
class ImageContent(BaseModel):
"""An image content block."""
type: Literal["image"]
data: Optional[str] = None
"""The image content."""
mime_type: Optional[
Literal[
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
]
] = None
"""The mime type of the image."""
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
"""The resolution of the media."""
uri: Optional[str] = None
"""The URI of the image."""

View File

@@ -0,0 +1,50 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from typing import Union
from typing_extensions import Literal, Required, Annotated, TypedDict
from .._types import Base64FileInput
from .._utils import PropertyInfo
from .._models import set_pydantic_config
__all__ = ["ImageContentParam"]
class ImageContentParam(TypedDict, total=False):
"""An image content block."""
type: Required[Literal["image"]]
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
"""The image content."""
mime_type: Literal[
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
]
"""The mime type of the image."""
resolution: Literal["low", "medium", "high", "ultra_high"]
"""The resolution of the media."""
uri: str
"""The URI of the image."""
set_pydantic_config(ImageContentParam, {"arbitrary_types_allowed": True})

Some files were not shown because too many files have changed in this diff Show More