Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/google/adk/integrations/openai/_openai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

from pydantic import BaseModel
from pydantic import Field
from pydantic_core import to_jsonable_python
from typing_extensions import override

from . import _openai_common
Expand Down Expand Up @@ -115,6 +116,22 @@ def _part_to_openai_content(
return ""


def _encode_tool_result_value(value: Any) -> Any:
"""Converts a tool result value that json cannot encode, like a datetime.

Datetimes become ISO 8601 text, bytes become base64 text and Pydantic
models become dicts of their non-None fields, as in a Gemini request.
Values Pydantic cannot convert fall back to their str().
"""
return to_jsonable_python(
value,
by_alias=False,
exclude_none=True,
bytes_mode="base64",
serialize_unknown=True,
)


def _content_to_openai_messages(
content: types.Content,
) -> list[dict[str, object]]:
Expand Down Expand Up @@ -144,7 +161,10 @@ def _content_to_openai_messages(
"role": "tool",
"tool_call_id": part.function_response.id or "",
"content": (
json.dumps(part.function_response.response)
json.dumps(
part.function_response.response,
default=_encode_tool_result_value,
)
if part.function_response.response is not None
else ""
),
Expand Down
79 changes: 79 additions & 0 deletions tests/unittests/integrations/openai/test_openai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import contextlib
import datetime
import json
import logging
import os
Expand All @@ -26,12 +27,15 @@
from google.adk.integrations.openai._openai_llm import _serialize_system_instruction
from google.adk.integrations.openai._openai_llm import _usage_metadata
from google.adk.integrations.openai._openai_llm import OpenAILlm
from google.adk.memory.memory_entry import MemoryEntry
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.tools.load_memory_tool import LoadMemoryResponse
from google.genai import types
from google.genai.types import Content
from google.genai.types import Part
from openai import AsyncOpenAI
from pydantic import BaseModel
import pytest


Expand Down Expand Up @@ -125,6 +129,81 @@ def test_content_to_openai_messages_with_empty_response():
assert messages[0]["content"] == ""


class _Order(BaseModel):
id: int
shipped: datetime.date
note: str | None = None


@pytest.mark.parametrize(
"response, want_content",
[
pytest.param(
{"last_login": datetime.datetime(2026, 11, 9, 10, 0)},
'{"last_login": "2026-11-09T10:00:00"}',
id="datetime",
),
pytest.param(
{"thumbnail": b"\x89PNG"},
'{"thumbnail": "iVBORw=="}',
id="bytes",
),
pytest.param(
{"result": _Order(id=7, shipped=datetime.date(2026, 11, 10))},
'{"result": {"id": 7, "shipped": "2026-11-10"}}',
id="pydantic_model",
),
pytest.param(
{
"result": LoadMemoryResponse(
memories=[
MemoryEntry(
content=types.Content(
role="user",
parts=[types.Part(text="My cat is Tom.")],
),
author="user",
)
]
)
},
'{"result": {"memories": [{"content": {"parts": [{"text": "My cat'
' is Tom."}], "role": "user"}, "custom_metadata": {}, "author":'
' "user"}]}}',
id="load_memory",
),
pytest.param(
{"city": "Zürich", "forecast": {"rain": None, "temps": [1.5, -2]}},
'{"city": "Z\\u00fcrich", "forecast": {"rain": null, "temps": [1.5,'
" -2]}}",
id="plain_json_unchanged",
),
],
)
def test_content_to_openai_messages_encodes_tool_result_values(
response, want_content
):
"""Tool results that json cannot encode directly are still sent."""
from google.adk.integrations.openai._openai_llm import _content_to_openai_messages

content = types.Content(
role="user",
parts=[
types.Part(
function_response=types.FunctionResponse(
id="call_123", name="lookup", response=response
)
)
],
)
messages = _content_to_openai_messages(content)
assert messages == [{
"role": "tool",
"tool_call_id": "call_123",
"content": want_content,
}]


@pytest.mark.asyncio
async def test_generate_content_async():
with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "test_key"}):
Expand Down
Loading