diff --git a/google/genai/_interactions/_base_client.py b/google/genai/_interactions/_base_client.py index c2e8232c3..71453670f 100644 --- a/google/genai/_interactions/_base_client.py +++ b/google/genai/_interactions/_base_client.py @@ -101,6 +101,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -578,8 +579,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/google/genai/_interactions/_compat.py b/google/genai/_interactions/_compat.py index 2ad0c9978..25b3af666 100644 --- a/google/genai/_interactions/_compat.py +++ b/google/genai/_interactions/_compat.py @@ -154,6 +154,7 @@ def model_dump( 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"): return model.model_dump( @@ -163,13 +164,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/google/genai/_interactions/_utils/_json.py b/google/genai/_interactions/_utils/_json.py new file mode 100644 index 000000000..d54cafcc3 --- /dev/null +++ b/google/genai/_interactions/_utils/_json.py @@ -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) diff --git a/google/genai/_interactions/types/image_content.py b/google/genai/_interactions/types/image_content.py index f775ba185..0f78a53dc 100644 --- a/google/genai/_interactions/types/image_content.py +++ b/google/genai/_interactions/types/image_content.py @@ -29,6 +29,8 @@ class ImageContent(BaseModel): type: Literal["image"] + caption: Optional[str] = None + data: Optional[str] = None """The image content.""" diff --git a/google/genai/_interactions/types/image_content_param.py b/google/genai/_interactions/types/image_content_param.py index cb247f318..951fa6dab 100644 --- a/google/genai/_interactions/types/image_content_param.py +++ b/google/genai/_interactions/types/image_content_param.py @@ -33,6 +33,8 @@ class ImageContentParam(TypedDict, total=False): type: Required[Literal["image"]] + caption: str + data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] """The image content."""