Skip to content

Unintended user message injection breaks tool calling with LiteLLM + OpenAI/Azure #4249

@GitMarco27

Description

@GitMarco27

Describe the bug

When using LiteLLM with OpenAI / AzureOpenAI models, after tool calls, an user message "Handle the requests as specified in the System Instruction." is injected into the conversation history. This message triggers OpenAI's prompt injection safety guards and causes the request to fail.

openai.BadRequestError: Error code: 400 - {'error': {'message': "The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766", 'type': None, 'param': 'prompt', 'code': 'content_filter', 'status': 400, 'innererror': {'code': 'ResponsibleAIPolicyViolation', 'content_filter_result': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': True, 'detected': True}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}}}

Root Cause Analysis

The bug originates from an interaction between two functions in lite_llm.py:

  1. _part_has_payload() does not consider function_response as a valid payload for the model:
def _part_has_payload(part: types.Part) -> bool:
  """Checks whether a Part contains usable payload for the model."""
  if part.text:
    return True
  if part.inline_data and part.inline_data.data:
    return True
  if part.file_data and (part.file_data.file_uri or part.file_data.data):
    return True
  return False
  1. _append_fallback_user_content_if_missing() iterates through llm_request.contents looking for a user Content with payload. Tool responses (function_response) are represented as Content with role='user'. Since _part_has_payload() returns False for function_response parts, the function incorrectly appends a fallback text part.
def _append_fallback_user_content_if_missing(
    llm_request: LlmRequest,
) -> None:
  """Ensures there is a user message with content for LiteLLM backends.

  Args:
    llm_request: The request that may need a fallback user message.
  """
  for content in reversed(llm_request.contents):
    if content.role == "user":
      parts = content.parts or []
      if any(_part_has_payload(part) for part in parts):
        return
      if not parts:
        content.parts = []
      content.parts.append(
          types.Part.from_text(
              text="Handle the requests as specified in the System Instruction."
          )
      )
      return
  llm_request.contents.append(
      types.Content(
          role="user",
          parts=[
              types.Part.from_text(
                  text=(
                      "Handle the requests as specified in the System"
                      " Instruction."
                  )
              ),
          ],
      )
  )
  1. _content_to_message_param() then processes this Content. The newly added text part becomes a non_tool_part, which triggers the branch:
if tool_messages and non_tool_parts:
    follow_up = await _content_to_message_param(
        types.Content(role=content.role, parts=non_tool_parts),
        provider=provider,
    )
    follow_up_messages = (
        follow_up if isinstance(follow_up, list) else [follow_up]
    )
    return tool_messages + follow_up_messages

This creates an extra user message with the fallback text "Handle the requests as specified in the System Instruction.", which OpenAI's safety systems flag as potential prompt injection.

Previous Behavior

In previous versions, _content_to_message_param handled this by returning early when tool_messages existed, so the new user part was never really provided to the model invocation:

if tool_messages:
    return tool_messages if len(tool_messages) > 1 else tool_messages[0]

This prevented the fallback text from being converted into a follow-up user message.


To Reproduce

  1. Install google-adk>1.22.1
  2. Create an agent with at least one tool using LiteLLM with an OpenAI model
  3. Trigger a conversation that results in a tool call
  4. Observe the LLM request includes an extra user message: "Handle the requests as specified in the System Instruction."
  5. With OpenAI models, this triggers prompt injection safety guards

Minimal reproduction code

import asyncio
import os

from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from google.genai.types import GenerateContentConfig

load_dotenv()


def get_litellm_model() -> LiteLlm:
    deployment_id = os.getenv("AZURE_OPENAI_DEPLOYMENT_ID")
    return LiteLlm(
        model=f"azure/{deployment_id}",
        stream=True,
    )


async def add(a: float, b: float) -> float:
    return a + b


def create_simple_agent() -> Agent:
    model = get_litellm_model()

    instructions = """
    You are a helpful mathematical assistant with access to a calculator tool.
    """

    agent = Agent(
        name="Claudia",
        model=model,
        generate_content_config=GenerateContentConfig(temperature=0.0),
        instruction=instructions,
        description="A simple agent that can perform basic mathematical calculations",
        tools=[add],
    )

    return agent


async def run_agent_single_query(query: str):
    agent = create_simple_agent()
    session_service = InMemorySessionService()

    runner = Runner(
        agent=agent,
        app_name="SimpleADKApp",
        session_service=session_service,
        auto_create_session=True,
    )

    user_id = "user_123"
    session_id = "session_001"

    content = types.Content(role="user", parts=[types.Part(text=query)])

    response_text = ""
    async for event in runner.run_async(
        user_id=user_id,
        session_id=session_id,
        new_message=content,
    ):
        if event.content and event.content.parts:
            response_text = event.content.parts[0].text or ""

    print(f"Agent Response:\n{response_text}\n")
    return response_text


async def main():
    await run_agent_single_query("Ciao! Quanto fa 3 + 3?")


if __name__ == "__main__":
    asyncio.run(main())

Error / Stacktrace

litellm.exceptions.ContentPolicyViolationError: litellm.BadRequestError: litellm.ContentPolicyViolationError: litellm.ContentPolicyViolationError: AzureException - The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766

Expected behavior

Content objects containing only function_response parts should NOT have the fallback text appended. The _part_has_payload() function should return True for parts that have function_response, recognizing that tool responses are valid payload.


Proposed fix

  1. Add a function_response check in _part_has_payload() in order to avoid appending this fallback for function responses:
def _part_has_payload(part: types.Part) -> bool:
  """Checks whether a Part contains usable payload for the model."""
  if part.text:
    return True
  if part.inline_data and part.inline_data.data:
    return True
  if part.file_data and (part.file_data.file_uri or part.file_data.data):
    return True
  if part.function_response:
    return True
  return False
  1. Remove _append_fallback_user_content_if_missing: avoid adding a fallback user message at all as this is unexpected, dangerous and really similar to a silent failure.

Desktop

  • OS: Ubuntu 20.04.6 LTS
  • Python version: 3.12.11
  • ADK version: > 1.22.1

Model Information

  • Are you using LiteLLM: Yes
  • Which model is being used: OpenAI models via LiteLLM (gpt-4.1)

Additional context

  1. The bug specifically affects the flow:

    • User sends message -> Model responds with tool call -> Tool executes -> Tool response (function_response) is added to history -> _append_fallback_user_content_if_missing incorrectly modifies this Content -> Extra user message is created
  2. This issue may not manifest with Gemini models accessed directly (without LiteLLM) because they may handle the message format differently.

  3. This bug forbids the use of any tool calls in conjunction with LiteLLM and OpenAI models and so does not allow me to use the latest releases of Google ADK

Metadata

Metadata

Assignees

Labels

models[Component] Issues related to model support

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions