Skip to content

[LiteLLM] role='tool_responses' for Gemma-4 models breaks tool calling on LM Studio (strict OpenAI role validation) #6482

Description

@armandokeller

Describe the Bug

_content_to_message_param in google/adk/models/lite_llm.py forces
role="tool_responses" for the tool-result message whenever the model
name matches _is_gemma4_model (regex gemma-?4), regardless of which
serving backend is actually handling the request:

tool_role = "tool_responses" if _is_gemma4_model(model) else "tool"

This was introduced for Ollama (#5650) and broadened to also match
hyphenated gemma-4 names to cover vLLM/llama.cpp (#6334). Both of
those fixes assume every llama.cpp-family server passes the role
value straight through to the model's chat template without validating
it against the standard OpenAI role enum.

LM Studio does not — its OpenAI-compatible endpoint validates
messages[].role strictly and rejects tool_responses outright, even
though LM Studio also serves GGUF models via a llama.cpp-based runtime.
The very backend #6334 was broadened to support ("vLLM, llama.cpp")
turns out not to be uniform: LM Studio's own compatibility layer sits
in front of llama.cpp and enforces the standard role set before the
model's chat template ever sees the message.

Net effect: any ADK agent with at least one tool, running a Gemma-4
model through LiteLLM against LM Studio, fails on the very first
tool-result turn.

Steps to Reproduce

  1. Load any Gemma-4-named model in LM Studio (tested with
    google/gemma-4-e4b) and start its local server
    (http://127.0.0.1:1234/v1, OpenAI-compatible).
  2. Run the following minimal ADK agent, which has a single trivial
    tool unrelated to any specific application:
import asyncio
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import InMemoryRunner
from google.adk.tools.function_tool import FunctionTool
from google.genai import types

def roll_dice() -> dict:
    """Roll a six-sided die.

    Returns:
        A dict with the result.
    """
    return {"result": 4}

model = LiteLlm(
    model="openai/google/gemma-4-e4b",
    api_base="http://127.0.0.1:1234/v1",
    api_key="not-needed",
)
agent = Agent(name="t", model=model, instruction="Be helpful.", tools=[FunctionTool(roll_dice)])

async def main():
    runner = InMemoryRunner(agent, app_name="t")
    session = await runner.session_service.create_session(app_name="t", user_id="u")
    message = types.Content(role="user", parts=[types.Part(text="Roll the dice for me")])
    async for event in runner.run_async(user_id="u", session_id=session.id, new_message=message):
        if event.is_final_response() and event.content and event.content.parts:
            print(event.content.parts[0].text)

asyncio.run(main())

Expected Behavior

The agent calls roll_dice, receives the result, and produces a final
text response.

Observed Behavior

The first request (tool declaration only, no prior tool call) succeeds
normally and the model correctly emits a tool_calls response. The
second request — sent after the tool runs, carrying the tool result —
is rejected by LM Studio:

2026-07-25 21:51:45 [ERROR]
 [google/gemma-4-e4b] Invalid 'content': 'content' field must be a string or an array of objects.

LM Studio's own debug log shows the exact outgoing message that trips
this:

{
  "role": "assistant",
  "tool_calls": [
    {
      "type": "function",
      "id": "200618556",
      "function": { "name": "roll_dice", "arguments": "{}" }
    }
  ]
},
{
  "role": "tool_responses",
  "tool_call_id": "200618556",
  "content": "{\"result\": 4}"
}

role: "tool_responses" is not a role LM Studio's OpenAI-compatible
validator accepts (only the standard system/user/assistant/tool
set is), so the request as a whole is rejected. The reported error text
("content field must be...") is misleading — the actual problem is
the unrecognized role value, not the content field.

Environment

  • google-adk: 2.5.0
  • litellm: 1.93.0
  • LM Studio: OpenAI-compatible server at /v1, google/gemma-4-e4b
    (GGUF, llama.cpp-based runtime)
  • Python: 3.13
  • OS: Windows

Additional Context

Related: #5650 (Ollama, original fix), #6334 (broadened to hyphenated
gemma-4 naming for vLLM/llama.cpp). This report is about a case
neither of those covers: a llama.cpp-based server (LM Studio) that
does validate the OpenAI role enum strictly, unlike whatever
vLLM/llama.cpp setup #6334 was tested against.

Possible directions for a fix (not prescriptive, just what occurred to
us):

  • Make the tool-result role a LiteLlm constructor option instead of
    an unconditional model-name-based inference, so callers can opt out
    per backend.
  • Detect the actual serving backend (e.g. via a capability probe, or a
    custom_llm_provider/base-url heuristic) rather than matching on
    model name alone, since "serves a gemma-4-named GGUF" does not
    imply "accepts a non-standard role value."
  • At minimum, catch the resulting BadRequestError and retry once with
    the standard role="tool", so a stricter backend degrades instead of
    hard-failing.

Happy to provide more logs or test a patch against our setup if useful.

Metadata

Metadata

Labels

models[Component] This issue is related to model supportrequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions