Skip to content

Gemma 3 models fail with TypeError when a tool returns a datetime or a Pydantic model (e.g. load_memory) #7667

Description

@roydonsequeira

🔴 Required Information

Describe the Bug:
Gemma and Gemma3Ollama turn every function response into text for Gemma 3 with a bare json.dumps(func_response.response) (src/google/adk/models/gemma_llm.py:289). A tool result is stored as the tool returned it (a non-dict result is wrapped as {"result": value}), so any value the json module cannot encode raises TypeError on the next model call and the run fails.

The built-in load_memory tool always hits this, because it returns a LoadMemoryResponse model. So does any tool whose result holds a datetime, bytes or a Pydantic model. The same call keeps the default ensure_ascii=True, so non-ASCII text in a tool result reaches the model as \uXXXX escapes instead of the text itself. LiteLlm sends the text unescaped (ensure_ascii=False).

Steps to Reproduce:

  1. Install google-adk 2.12.0 (also reproduced on main at d3247446) with litellm.
  2. Run the script under "Minimal Reproduction Code". It needs no network or API key: both adapters rewrite the tool result before calling the model.

Expected Behavior:
The tool result reaches Gemma 3 as JSON text, the way a Gemini request carries the same function response: a datetime as an ISO 8601 string, a Pydantic model as an object without its unset fields, bytes as base64, and non-ASCII text as is.

Observed Behavior:

Gemma: TypeError: Object of type datetime is not JSON serializable
Gemma3Ollama: TypeError: Object of type datetime is not JSON serializable
Invoking tool `get_booking` produced: `{"hotel": "ホテル東京"}`.

Environment Details:

  • ADK Library Version (pip show google-adk): 2.12.0, main at d3247446
  • Desktop OS: Windows 11
  • Python Version (python -V): Python 3.11.3

Model Information:

  • Are you using LiteLLM: Yes for Gemma3Ollama, No for Gemma
  • Which model is being used: gemma3:4b on Ollama 0.40.7 (ollama/gemma3:4b), gemma-3-4b-it in the offline script

🟡 Optional Information

Regression:
No. The conversion has used a bare json.dumps since Gemma support was added.

Logs:
The same failure with a real model: an Agent on Gemma3Ollama(model="ollama/gemma3:4b"), run through Runner.

A function tool whose result holds a datetime:

user: Who is the guest on booking B-17, and when did they check in?
  tool call: get_booking({'booking_id': 'B-17'})
  tool result: {'booking_id': 'B-17', 'guest': 'Zoë Müller', 'hotel': 'ホテル東京', 'checked_in_at': datetime.datetime(2026, 10, 22, 9, 30)}
  File ".../google/adk/models/gemma_llm.py", line 421, in generate_content_async
  File ".../google/adk/models/gemma_llm.py", line 67, in _move_function_calls_into_system_instruction
  File ".../google/adk/models/gemma_llm.py", line 289, in _convert_content_parts_for_gemma
TypeError: Object of type datetime is not JSON serializable

The built-in load_memory tool, with one remembered session in InMemoryMemoryService:

user: Which city do I live in? Look it up in your memory.
  tool call: load_memory({'query': 'Which city do I live in?'})
  tool result: {'result': LoadMemoryResponse(memories=[MemoryEntry(...)])}
TypeError: Object of type LoadMemoryResponse is not JSON serializable

Screenshots / Video:
N/A

Additional Context:
Gemma 4 is not affected; it uses native function calling through Gemini or LiteLlm.

Minimal Reproduction Code:

import asyncio
import datetime

from google.adk.models.gemma_llm import Gemma
from google.adk.models.gemma_llm import Gemma3Ollama
from google.adk.models.llm_request import LlmRequest
from google.genai import types


def request_with_tool_result(response):
  return LlmRequest(
      model="gemma-3-4b-it",
      contents=[
          types.Content(
              role="user",
              parts=[types.Part.from_text(text="Where is booking B-17?")],
          ),
          types.Content(
              role="user",
              parts=[
                  types.Part.from_function_response(
                      name="get_booking", response=response
                  )
              ],
          ),
      ],
      config=types.GenerateContentConfig(),
  )


async def main():
  booking = {
      "hotel": "ホテル東京",
      "checked_in_at": datetime.datetime(2026, 10, 22, 9, 30),
  }
  for model in (Gemma(), Gemma3Ollama(model="ollama/gemma3:4b")):
    try:
      # Both adapters rewrite the tool result into text before any network call.
      async for _ in model.generate_content_async(request_with_tool_result(booking)):
        pass
    except TypeError as e:
      print(f"{type(model).__name__}: TypeError: {e}")

  # Without the datetime the request goes through, but the tool result text
  # Gemma 3 reads is escaped.
  request = request_with_tool_result({"hotel": "ホテル東京"})
  await Gemma()._preprocess_request(request)
  print(request.contents[-1].parts[0].text)


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)

Activity

  1. added
    models[Component] This issue is related to model support
    on Oct 22, 2026
  2. surajksharma07 commented on Oct 22, 2026

    @surajksharma07
    Collaborator

    Reproduced this on 2.12.0 with both Gemma and Gemma3Ollama @roydonsequeira, including the load_memory case and the \uXXXX escaping.

    Running the payload through pydantic_core.to_jsonable_python(..., exclude_none=True, bytes_mode="base64", fallback=str) and then json.dumps(..., ensure_ascii=False) looks like a good workaround and it’s the same direction as your PR #7668. It worked for datetime, bytes and LoadMemoryResponse on my side.

    Could you check that it holds up in your setup too for example with None values and nested models in tool results before we move the PR forward?

    Also the CLA check on #7668 is failing so please sign it when you get a chance so the review can start.

  3. roydonsequeira commented on Oct 22, 2026

    @roydonsequeira
    Author

    Thanks for reproducing it. Both hold up. I compared the text Gemma 3 gets with the function response google-genai builds for the same payload in a Gemini request (_common.convert_to_dict, then encode_unserializable_types), and the two are identical for every None and nested-model payload I tried:

    • None values in the tool's own dicts and lists stay null, so a tool that returns nothing still renders as {"result": null}, exactly as on main. exclude_none only drops Pydantic model fields that are None, as the Gemini request does.
    • Nested models are serialized all the way down, whether they sit in another model, a list or a dataclass. Each level drops its None fields, and a datetime inside a nested dict comes out as an ISO 8601 string.

    For example, a Guest model with an unset email, an Address with an unset postcode, a list of addresses and a notes dict holding None and a datetime:

    {"result": {"name": "Zoë Müller", "address": {"city": "München"}, "previous_stays": [{"city": "東京", "postcode": "100-0005"}, {"city": "Paris"}], "notes": {"vip": null, "since": "2026-10-22T09:30:00"}}}
    

    I pushed 2132e8a to #7668. It adds a none_values case to test_preprocess_request_with_function_response_converts_values, and it fixes the docstring: it said unset model fields are dropped, but what gets dropped is fields that are None (custom_metadata={} is unset and stays). Nested models were already covered: the pydantic_model case goes four levels deep, from LoadMemoryResponse down to Part.

  4. surajksharma07 commented on Oct 22, 2026

    @surajksharma07
    Collaborator

    Ran the same cases against 2132e8a’s approach and they match on my side too @roydonsequeira. None in plain dicts stays null ({"result": null} unchanged), nested model None fields are dropped and your Guest/Address example and LoadMemoryResponse come out identical to what google-genai builds for the Gemini request.

    One small thing: None fields inside a dataclass are kept (e.g. {"city": "Rome", "note": null}) which is fine since the Gemini request does the same but the “each level drops its None fields” wording only really holds for Pydantic models.

    Set, Decimal and date also serialize cleanly now and unknown types fall back to str() instead of crashing so the change looks good.

    The only blocker left is the CLA check on #7668 which is still failing. Once that’s signed the workflows can be approved and the review can move ahead.

  5. roydonsequeira commented on Oct 22, 2026

    @roydonsequeira
    Author

    Good catch, thanks. Only Pydantic types drop their None fields, models and Pydantic dataclasses alike; a standard-library dataclass keeps them, as the Gemini request does. "Each level" was too broad.

    The CLA is signed now (Google Individual CLA with GitHub username roydonsequeira, 10:00 UTC), but the rescan link no longer starts a new cla/google run on #7668: the latest one, at 09:47 UTC, predates the signature. Could someone with access re-run the check?

  6. surajksharma07 commented on Oct 22, 2026

    @surajksharma07
    Collaborator

    Thanks for signing it @roydonsequeira. The cla/google check matches the commit author email not the GitHub username so please check that git log --format='%an <%ae>' -2 on the branch shows the email you signed with (or add that email to the Google account at cla.developers.google.com).

    If it matches pushing a new commit or an empty one (git commit --allow-empty -m "chore: re-trigger CLA" && git push) should start a fresh cla/google run. If the email is different amend the author on both commits and force-push.

    Also the PR description still says “unset model fields are dropped”. Could you change it to “None fields of Pydantic models and Pydantic dataclasses” to match the docstring you fixed in 2132e8a?

    Main (facd7dd) still has the bare json.dumps at gemma_llm.py:289 so #7668 is still needed. Once the CLA check passes the workflows can be approved and it can go to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

models[Component] This issue is related to model support

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions