-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
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:
_part_has_payload()does not considerfunction_responseas 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_append_fallback_user_content_if_missing()iterates throughllm_request.contentslooking for auserContent with payload. Tool responses (function_response) are represented asContentwithrole='user'. Since_part_has_payload()returnsFalseforfunction_responseparts, 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."
)
),
],
)
)_content_to_message_param()then processes this Content. The newly added text part becomes anon_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_messagesThis 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
- Install
google-adk>1.22.1 - Create an agent with at least one tool using LiteLLM with an OpenAI model
- Trigger a conversation that results in a tool call
- Observe the LLM request includes an extra user message:
"Handle the requests as specified in the System Instruction." - 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
- Add a
function_responsecheck 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- 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
-
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_missingincorrectly modifies this Content -> Extra user message is created
- User sends message -> Model responds with tool call -> Tool executes -> Tool response (function_response) is added to history ->
-
This issue may not manifest with Gemini models accessed directly (without LiteLLM) because they may handle the message format differently.
-
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