Skip to content

fix(models): stop Gemma 3 failing on datetime or Pydantic tool results - #7668

Open
roydonsequeira wants to merge 2 commits into
google:mainfrom
roydonsequeira:fix/gemma3-tool-result-json
Open

roydonsequeira wants to merge 2 commits into
google:mainfrom
roydonsequeira:fix/gemma3-tool-result-json

Conversation

@roydonsequeira

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:
Gemma and Gemma3Ollama render each function response as text for Gemma 3 with a bare json.dumps(func_response.response). A tool result is stored as the tool returned it, so a datetime, bytes or a Pydantic model raised TypeError on the next model call and failed the run. The built-in load_memory tool always hit this, because it returns a LoadMemoryResponse. The default ensure_ascii=True also sent non-ASCII text in tool results to the model as \uXXXX escapes.

Solution:
Serialize the payload with pydantic_core.to_jsonable_python, the way a Gemini request carries the same function response: unset model fields are dropped (as google-genai does with exclude_none=True), bytes are base64-encoded, and values Pydantic cannot serialize fall back to their str(). json.dumps(..., ensure_ascii=False) then keeps non-ASCII text as is. Payloads that already were plain JSON render exactly as before, so the existing tests are unchanged.

The change is in GemmaFunctionCallingMixin's content conversion, which both Gemma and Gemma3Ollama use. Gemma 4 uses native function calling and is not affected.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

test_preprocess_request_with_function_response_converts_values in tests/unittests/models/test_gemma_llm.py covers non-ASCII text, a datetime, bytes and a LoadMemoryResponse. All four cases fail on main (three with TypeError, one with escaped text) and pass with the fix.

$ pytest tests/unittests/models/test_gemma_llm.py
31 passed in 3.46s

Full suite on Windows 11, Python 3.11 (pytest tests/unittests -n 4): 73 failed, 20749 passed, 107 skipped. Every one of the 73 also fails on unmodified main in this environment, all outside models/ (Windows-specific failures such as test_cli_deploy_to_cloud_run.py and test_samples.py). test_gemma_llm.py also passes on Python 3.12, 3.13 and 3.14.

Manual End-to-End (E2E) Tests:

An Agent on Gemma3Ollama(model="ollama/gemma3:4b") (Ollama 0.40.7), run through Runner with InMemorySessionService. Each run prints the tool-result text that reaches the model.

Function tool whose result holds a datetime and non-ASCII text:

# main
user: Who is the guest on booking B-17, and when did they check in?
  tool call: get_booking({'booking_id': 'B-17'})
  FAILED: TypeError: Object of type datetime is not JSON serializable

# this change
user: Who is the guest on booking B-17, and when did they check in?
  tool call: get_booking({'booking_id': 'B-17'})
  [sent to model] Invoking tool `get_booking` produced: `{"booking_id": "B-17", "guest": "Zoë Müller", "hotel": "ホテル東京", "checked_in_at": "2026-10-22T09:30:00"}`.
concierge: Zoë Müller checked in on 2026-10-22 at 09:30:00.

Built-in load_memory with one remembered session in InMemoryMemoryService:

# main
user: Which city do I live in? Look it up in your memory.
  tool call: load_memory({'query': 'Which city do I live in?'})
  FAILED: TypeError: Object of type LoadMemoryResponse is not JSON serializable

# this change
user: Which city do I live in? Look it up in your memory.
  tool call: load_memory({'query': 'Which city do I live in?'})
  [sent to model] Invoking tool `load_memory` produced: `{"result": {"memories": [{"content": {"parts": [{"text": "I live in München and my favourite dish is 寿司."}], "role": "user"}, "custom_metadata": {}, "id": "f2eedd24-719f-497f-87be-78ba493f0797", "author": "user", "timestamp": "2026-10-22T11:14:28.255772"}]}}`.
assistant: I live in München.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

The formatters, linters and repository checks from .pre-commit-config.yaml pass on both files, and mypy reports no errors in gemma_llm.py, as on main.

Gemma and Gemma3Ollama rendered each function response as text with a
bare json.dumps, but a tool result holds whatever the tool returned. A
datetime, bytes or a Pydantic model, such as the LoadMemoryResponse of
the built-in load_memory tool, raised TypeError on the next model call
and failed the run, and non-ASCII text reached the model as \uXXXX
escapes.

Serialize the payload with to_jsonable_python the way a Gemini request
carries a function response, and keep non-ASCII text as is.

Fixes google#7667
@google-cla

google-cla Bot commented Oct 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

A None in the tool's own dicts and lists stays null in the text Gemma 3
reads, as it does in a Gemini request; exclude_none only drops Pydantic
model fields that are None. Pin that with a test case, and correct the
docstring, which described the dropped fields as unset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants