diff --git a/google/genai/_api_client.py b/google/genai/_api_client.py index e6202815e..3ef7c774f 100644 --- a/google/genai/_api_client.py +++ b/google/genai/_api_client.py @@ -250,7 +250,7 @@ def __init__( dict[str, str], httpx.Headers, 'CIMultiDictProxy[str]', - CaseInsensitiveDict, + CaseInsensitiveDict[str], ], response_stream: Union[Any, str] = None, byte_stream: Union[Any, bytes] = None, @@ -351,10 +351,14 @@ def _iter_response_stream(self) -> Iterator[str]: chunk = '' balance = 0 data_buffer: list[str] = [] + response_stream: Iterator[str] if isinstance(self.response_stream, httpx.Response): response_stream = self.response_stream.iter_lines() else: - response_stream = self.response_stream.iter_lines(decode_unicode=True) + response_stream = ( + line.decode('utf-8') if isinstance(line, bytes) else line + for line in self.response_stream.iter_lines(decode_unicode=True) + ) for line in response_stream: if not line: if data_buffer: diff --git a/google/genai/_interactions/_files.py b/google/genai/_interactions/_files.py index 7f209ee4e..53118c3a8 100644 --- a/google/genai/_interactions/_files.py +++ b/google/genai/_interactions/_files.py @@ -39,7 +39,9 @@ def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: - return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + return isinstance(obj, bytes) or isinstance(obj, io.IOBase) or isinstance( + obj, os.PathLike + ) def is_file_content(obj: object) -> TypeGuard[FileContent]: diff --git a/google/genai/_interactions/_types.py b/google/genai/_interactions/_types.py index 309e981f9..c955b18b7 100644 --- a/google/genai/_interactions/_types.py +++ b/google/genai/_interactions/_types.py @@ -71,10 +71,10 @@ ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] ProxiesTypes = Union[str, Proxy, ProxiesDict] if TYPE_CHECKING: - Base64FileInput = Union[IO[bytes], PathLike[str]] + Base64FileInput = Union[IO[bytes], bytes, PathLike[str]] FileContent = Union[IO[bytes], bytes, PathLike[str]] else: - Base64FileInput = Union[IO[bytes], PathLike] + Base64FileInput = Union[IO[bytes], bytes, PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. diff --git a/google/genai/_interactions/_utils/_transform.py b/google/genai/_interactions/_utils/_transform.py index bb7db1865..7de242209 100644 --- a/google/genai/_interactions/_utils/_transform.py +++ b/google/genai/_interactions/_utils/_transform.py @@ -259,7 +259,9 @@ def _format_data(data: object, format_: PropertyFormat, format_template: str | N if format_ == "base64" and is_base64_file_input(data): binary: str | bytes | None = None - if isinstance(data, pathlib.Path): + if isinstance(data, bytes): + binary = data + elif isinstance(data, pathlib.Path): binary = data.read_bytes() elif isinstance(data, io.IOBase): binary = data.read() @@ -268,7 +270,7 @@ def _format_data(data: object, format_: PropertyFormat, format_template: str | N binary = binary.encode() if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + raise RuntimeError(f"Could not read bytes from {data!r}; Received {type(binary)}") return base64.b64encode(binary).decode("ascii") @@ -425,7 +427,9 @@ async def _async_format_data(data: object, format_: PropertyFormat, format_templ if format_ == "base64" and is_base64_file_input(data): binary: str | bytes | None = None - if isinstance(data, pathlib.Path): + if isinstance(data, bytes): + binary = data + elif isinstance(data, pathlib.Path): binary = await anyio.Path(data).read_bytes() elif isinstance(data, io.IOBase): binary = data.read() @@ -434,7 +438,7 @@ async def _async_format_data(data: object, format_: PropertyFormat, format_templ binary = binary.encode() if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + raise RuntimeError(f"Could not read bytes from {data!r}; Received {type(binary)}") return base64.b64encode(binary).decode("ascii") diff --git a/google/genai/_local_tokenizer_loader.py b/google/genai/_local_tokenizer_loader.py index 0f6edc3ad..1c821bb1b 100644 --- a/google/genai/_local_tokenizer_loader.py +++ b/google/genai/_local_tokenizer_loader.py @@ -18,7 +18,7 @@ import hashlib import os import tempfile -from typing import Optional, cast +from typing import Optional import uuid import requests # type: ignore @@ -75,7 +75,7 @@ def _load_file(file_url_path: str) -> bytes: """Loads file bytes from the given file url path.""" resp = requests.get(file_url_path) resp.raise_for_status() - return cast(bytes, resp.content) + return resp.content def _is_valid_model(*, model_data: bytes, expected_hash: str) -> bool: diff --git a/google/genai/tests/interactions/test_base64_inputs.py b/google/genai/tests/interactions/test_base64_inputs.py new file mode 100644 index 000000000..d87e0ffe4 --- /dev/null +++ b/google/genai/tests/interactions/test_base64_inputs.py @@ -0,0 +1,81 @@ +# Copyright 2026 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 base64 + +import pytest + +from ..._interactions._utils import async_maybe_transform +from ..._interactions._utils import maybe_transform +from ..._interactions._utils._json import openapi_dumps +from ..._interactions.types import interaction_create_params + + +@pytest.mark.parametrize( + 'content_type,mime_type', + [ + ('image', 'image/png'), + ('audio', 'audio/wav'), + ('video', 'video/mp4'), + ('document', 'application/pdf'), + ], +) +def test_media_input_bytes_are_base64_encoded(content_type, mime_type): + body = maybe_transform( + { + 'input': { + 'type': content_type, + 'data': b'media-bytes', + 'mime_type': mime_type, + }, + 'model': 'gemini-2.5-flash', + }, + interaction_create_params.CreateModelInteractionParamsNonStreaming, + ) + + assert body['input']['data'] == base64.b64encode(b'media-bytes').decode( + 'ascii' + ) + assert openapi_dumps(body) + + +@pytest.mark.parametrize( + 'content_type,mime_type', + [ + ('image', 'image/png'), + ('audio', 'audio/wav'), + ('video', 'video/mp4'), + ('document', 'application/pdf'), + ], +) +@pytest.mark.asyncio +async def test_media_input_bytes_are_base64_encoded_async( + content_type, mime_type +): + body = await async_maybe_transform( + { + 'input': { + 'type': content_type, + 'data': b'media-bytes', + 'mime_type': mime_type, + }, + 'model': 'gemini-2.5-flash', + }, + interaction_create_params.CreateModelInteractionParamsNonStreaming, + ) + + assert body['input']['data'] == base64.b64encode(b'media-bytes').decode( + 'ascii' + ) + assert openapi_dumps(body)