Skip to content

[BUG] Gemini LLM Tool Calling Fails: tools parameter is null in API request with CrewAI v0.121.0 & LiteLLM v1.70.4 #2895

Closed
@geantendormi76

Description

@geantendormi76

Description

Hi CrewAI Team,

We are encountering a persistent issue where our Agent, configured with a Google Gemini LLM (via crewai.LLM), fails to make tool calls. The Agent gets stuck in the "Thinking..." phase, and our LLMCallStartedEvent listener consistently shows that the tools parameter in the API request to Gemini is null.

We have conducted extensive debugging and believe the issue lies in how CrewAI (v0.121.0) and LiteLLM (v1.70.4) handle the transmission of structured tool definitions to the Gemini API.

Below is a detailed breakdown of the bug, our environment, and our findings:

1. Describe the Bug

When using CrewAI v0.121.0 with Google Gemini LLM (e.g., gemini/gemini-2.0-flash via crewai.LLM), custom tools configured for an Agent are not being correctly passed to the underlying Gemini API. Specifically, when inspecting the LLMCallStartedEvent, the event.__dict__['tools'] field (which should contain the structured tool definitions for the LLM API call) is consistently null. This prevents the Gemini LLM from recognizing or utilizing its native function calling capabilities, causing the Agent to get stuck in the "Thinking..." phase without generating any Action: or Action Input:.

This issue persists despite extensive Prompt engineering, including:

  • Ensuring all Pydantic args_schema fields for custom tools have non-null description and no default values.
  • Adding model_config = ConfigDict(extra='forbid') to Pydantic args_schema models.
  • Manually embedding a standard JSON Schema representation of args_schema within the tool.description attribute passed to the LLM in the text prompt.
  • Providing clear, ReAct-style tool invocation examples within the Task description.

2. Environment and Library Versions

  • CrewAI: 0.121.0
  • crewai-tools: (Please specify your version, e.g., 0.x.y)
  • LiteLLM: 1.70.4
  • google-generativeai (SDK): 0.8.5
  • Pydantic: v2.11.5
  • Python: (Please specify your Python version, e.g., 3.10.x or 3.12.x)
  • Operating System: WSL2 (Ubuntu)
  • Network: Accessing Gemini API via a SOCKS5 proxy (socks5h://127.0.0.1:10808), with HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY environment variables correctly set. Necessary SOCKS support libraries (pysocks, httpx[socks]) are installed.
  • LLM Configuration in CrewAI:
    from crewai import LLM
    gemini_llm = LLM(
        model="gemini/gemini-2.0-flash", 
        api_key=os.getenv("GEMINI_API_KEY"), 
        temperature=0.1, 
        max_tokens=2048
    )

3. Steps to Reproduce

  • Define a custom tool inheriting from crewai.tools.BaseTool with a Pydantic args_schema.
  • Configure an Agent with this custom tool and the Gemini LLM instance.
  • Define a Task that explicitly guides the Agent to use this custom tool.
  • Instantiate a Crew and kickoff() the task.
  • Implement LLMCallStartedEvent and LLMCallCompletedEvent listeners to inspect the data sent to and received from the LLM.

4. Expected Behavior

The LLMCallStartedEvent.event.__dict__['tools'] field (or equivalent attribute holding the structured tool definitions passed to litellm.completion) should contain a list of tool definitions formatted兼容 Gemini API (e.g., a list of dictionaries, each with a function_declarations key). The Gemini LLM should then be able to recognize these tools and, when appropriate, return a response containing tool_calls to invoke the custom tool.

5. Actual Behavior & Logs

  • The Agent gets stuck in the "Thinking..." phase indefinitely.
  • The LLMCallStartedEvent.event.__dict__['tools'] field is consistently null.
  • The system message in the prompt sent to the LLM (observed via LLMCallStartedEvent.messages) does correctly show the tool's name and a JSON Schema representation of its arguments (due to manual embedding in the tool's description attribute), but this text-based schema is not a substitute for the API-level tools parameter.
  • Crucially, a direct test using litellm.completion() (outside of CrewAI) with the same Gemini model, API key, and a manually constructed tools parameter containing the Pydantic-derived JSON schema for the custom tool, successfully passes the tool definition to the Gemini API. LiteLLM's debug logs (Final returned optional params: {'tools': [{'function_declarations': [...]}]}) confirm this. The Gemini API, in this direct LiteLLM test, recognizes the tool (though it may still opt to respond with text asking for more clarity before calling it, which is acceptable LLM behavior). This indicates LiteLLM itself is capable of handling and transmitting the tool schema correctly when provided with it.

Log Snippet from LLMCallStartedEvent in CrewAI:

// event.__dict__
{
  "timestamp": "2025-05-24 01:32:09.566129",
  "type": "llm_call_started",
  // ... other fields ...
  "messages": [
    {
      "role": "system",
      "content": "You are 信息检索专家...\nTool Name: HybridRAGQueryTool\nTool Arguments (JSON Schema):\n```json\n{\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"query\": {\n      \"description\": \"用户提出的原始查询文本。\",\n      \"title\": \"Query\",\n      \"type\": \"string\"\n    },\n    // ... other params ...\n  },\n  \"required\": [\"query\", ...],\n  \"title\": \"QueryRequest\",\n  \"type\": \"object\"\n}\n```\nTool Description: 【核心RAG工具】...\n\nIMPORTANT: Use the following format in your response..."
    },
    {
      "role": "user",
      "content": "\nCurrent Task: 用户查询是:'公司2024年第一季度的销售额是多少?'..."
    }
  ],
  "tools": null, // <--- THIS IS THE CORE PROBLEM
  "callbacks": [
    "<crewai.utilities.token_counter_callback.TokenCalcHandler object at 0x...>"
  ],
  "available_functions": null
}

6. Suspected Cause

The issue appears to stem from how CrewAI v0.121.0 (possibly in conjunction with LiteLLM v1.70.4) processes the Agent's tools list and prepares the tools parameter for the litellm.completion call when the target LLM is Gemini. Despite the google-generativeai package being installed and Pydantic schemas being well-defined (without default values and with ConfigDict(extra='forbid')), the structured tool definitions are not making it into the final API request's tools field.

This could be due to:

  • An internal bug in CrewAI's LLM class or its tool handling logic specific to Gemini.
  • An incorrect or missing step in the conversion of CrewAI BaseTool instances (and their Pydantic args_schema) into the format LiteLLM expects for Gemini's function_declarations.
  • A misconfiguration or missing parameter in the crewai.LLM instantiation for Gemini that is necessary to enable full tool/function calling support via the API.

7. Request for Assistance

  • Could you please investigate why the tools parameter is null in the LLMCallStartedEvent when using Gemini with CrewAI v0.121.0 and LiteLLM v1.70.4, even when a direct LiteLLM call with a manually constructed tools parameter works?
  • Is there a known issue or a specific configuration required in CrewAI or its LLM class to ensure Pydantic args_schema from BaseTool instances are correctly translated into the tools (containing function_declarations) parameter for Gemini API calls via LiteLLM?
  • Are there any working examples specifico to CrewAI v0.121.0+ and Gemini (gemini/gemini-2.0-flash or gemini-pro) that demonstrate successful native tool/function calling where the API request's tools field is correctly populated?

Thank you for your time and assistance with this critical issue.

Steps to Reproduce

Prerequisites:

  1. Python environment with the following packages installed (versions reflect our testing environment):
    • crewai==0.121.0
    • crewai-tools==0.45.0 (请将 0.45.0 替换为您实际使用的 crewai-tools 版本,可以通过 pip show crewai-tools 查看)
    • litellm==1.70.4
    • google-generativeai==0.8.5
    • pydantic==2.11.5
    • python-dotenv
    • httpx[socks]
    • pysocks
  2. A valid GEMINI_API_KEY environment variable set.
  3. (Optional, for full reproduction of our setup) A SOCKS5 proxy running locally on 127.0.0.1:10808 that can access Google Gemini APIs. Environment variables HTTP_PROXY, HTTPS_PROXY, ALL_PROXY set to socks5h://127.0.0.1:10808. Note: The core tools: null issue seems independent of the proxy, as direct LiteLLM calls with tools work through the proxy.
  4. An MCP proxy server (like mcpo) and a simple RAG MCP service. For a truly minimal example to demonstrate the tools: null issue, this MCP dependency can be mocked out in the tool's _run method to just return a string, as the problem occurs before tool execution.

Code to Reproduce (Minimal Example Focus):

  • pydantic_models.py:

    from pydantic import BaseModel, Field, ConfigDict
    from typing import List, Dict, Any, Optional
    
    class QueryRequest(BaseModel):
        model_config = ConfigDict(extra='forbid')
        query: str = Field(description="用户提出的原始查询文本。")
        top_k_vector: int = Field(description="期望检索的向量搜索结果数量。")
        top_k_kg: int = Field(description="期望检索的知识图谱结果数量。")
        top_k_bm25: int = Field(description="期望检索的 BM25 关键词搜索结果数量。")
  • custom_crewai_tools.py (Simplified for MRE):

    import json
    from typing import Type, ClassVar
    from pydantic import BaseModel
    from crewai.tools import BaseTool
    from pydantic_models import QueryRequest # Assuming pydantic_models.py is in the same directory
    
    class GeminiJSONSchemaBaseTool(BaseTool):
        _original_description: str = ""
        def __init__(self, **kwargs):
            if 'description' in kwargs: self._original_description = kwargs['description']
            super().__init__(**kwargs)
            if hasattr(self, 'args_schema') and self.args_schema: self._reformat_description_with_json_schema()
            elif self._original_description:
                self.description = f"Tool Name: {self.name}\nTool Arguments (JSON Schema):\n```json\nNo arguments required.\n```\nTool Description: {self._original_description}"
    
        def _reformat_description_with_json_schema(self):
            schema_str = "No arguments required."
            if self.args_schema:
                try:
                    if isinstance(self.args_schema, type) and issubclass(self.args_schema, BaseModel):
                        schema_dict = self.args_schema.model_json_schema(); schema_str = json.dumps(schema_dict, indent=2, ensure_ascii=False)
                    else: schema_str = str(self.args_schema)
                except Exception as e: print(f"Error generating JSON schema for {self.name}: {e}"); schema_str = str(self.args_schema)
            self.description = f"Tool Name: {self.name}\nTool Arguments (JSON Schema):\n```json\n{schema_str}\n```\nTool Description: {self._original_description}"
    
    class MinimalRAGTool(GeminiJSONSchemaBaseTool): # Changed from BaseMCPTool for MRE
        name: str = "MinimalRAGQueryTool"
        args_schema: Type[BaseModel] = QueryRequest
    
        def __init__(self, **kwargs):
            original_tool_description = "A minimal RAG tool for testing."
            super().__init__(name=self.name, description=original_tool_description, args_schema=self.args_schema, **kwargs)
    
        def _run(self, query: str, top_k_vector: int, top_k_kg: int, top_k_bm25: int) -> str:
            print(f"--- MinimalRAGTool Executed with query: {query} ---")
            return json.dumps({"status": "success", "final_answer": f"Mocked RAG result for '{query}'"})
  • run_agent_mre.py (Minimal Reproducible Example):

    import os
    import json
    from crewai import Agent, Task, Crew, Process, LLM
    from crewai.utilities.events import base_event_listener, CrewAIEventsBus
    from crewai.utilities.events.llm_events import LLMCallStartedEvent, LLMCallCompletedEvent
    from custom_crewai_tools import MinimalRAGTool # Assuming custom_crewai_tools.py is in the same directory
    from dotenv import load_dotenv
    
    load_dotenv()
    
    GEMINI_MODEL_NAME = "gemini/gemini-2.0-flash"
    GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
    
    if not GEMINI_API_KEY:
        print("CRITICAL ERROR: GEMINI_API_KEY not set.")
        exit(1)
    
    try:
        llm_for_agent = LLM(model=GEMINI_MODEL_NAME, api_key=GEMINI_API_KEY, temperature=0.1, max_tokens=2048)
        print(f"Agent LLM configured: {GEMINI_MODEL_NAME}")
    except Exception as e:
        print(f"CRITICAL ERROR: Failed to configure LLM: {e}"); exit(1)
    
    minimal_rag_tool = MinimalRAGTool()
    
    researcher_agent = Agent(
        role='信息检索专家 (MRE)',
        goal='使用 MinimalRAGQueryTool 回答查询。',
        backstory='一个用于复现工具调用问题的AI助手。',
        llm=llm_for_agent,
        tools=[minimal_rag_tool],
        verbose=True,
        allow_delegation=False
    )
    
    research_task = Task(
        description="用户查询是:'{query}'。请使用 `MinimalRAGQueryTool` 工具进行搜索。严格按以下格式调用:\n```\nThought: 我需要用 MinimalRAGQueryTool 搜索 '{query}'。\nAction: MinimalRAGQueryTool\nAction Input: {{\"query\": \"{query}\", \"top_k_vector\": 3, \"top_k_kg\": 2, \"top_k_bm25\": 3}}\n```",
        expected_output="工具的JSON字符串输出。",
        agent=researcher_agent,
    )
    
    test_crew = Crew(agents=[researcher_agent], tasks=[research_task], process=Process.sequential, verbose=True)
    
    event_bus = CrewAIEventsBus()
    @event_bus.on(LLMCallStartedEvent)
    def on_llm_call_started(source: any, event: LLMCallStartedEvent):
        print("\n===== LLM Call Started (MRE) =====")
        print(f"Source: {type(source).__name__}")
        model_name = getattr(event, 'model', 'Unknown')
        if hasattr(event, '__dict__') and model_name == 'Unknown': event_dict = event.__dict__; model_name = event_dict.get('model_name', event_dict.get('model', 'Unknown'))
        print(f"Model: {model_name}")
        print("Messages: ", json.dumps(event.messages, indent=2, ensure_ascii=False) if isinstance(event.messages, list) else event.messages)
        print("Tools in API Call: ", json.dumps(event.tools, indent=2, ensure_ascii=False) if hasattr(event, 'tools') and event.tools else "None or Not Present")
        print("Event Dict Tools: ", json.dumps(event.__dict__.get('tools'), indent=2, default=str) if hasattr(event, '__dict__') else "No __dict__ or no tools key")
        print("================================\n")
    
    @event_bus.on(LLMCallCompletedEvent)
    def on_llm_call_completed(source: any, event: LLMCallCompletedEvent):
        print("\n===== LLM Call Completed (MRE) =====")
        # ... (similar logging as in your original run_agent.py) ...
        print("==================================\n")
        
    print("--- Starting MRE Crew ---")
    result = test_crew.kickoff(inputs={'query': 'Test query for MRE'})
    print("\n--- MRE Crew Finished ---")
    print("Final Result (MRE):", result.raw if hasattr(result, 'raw') else result)

Steps to run the MRE:

  1. Save the above code blocks into pydantic_models.py, custom_crewai_tools.py (using MinimalRAGTool), and run_agent_mre.py respectively in the same directory.
  2. Create a .env file with GEMINI_API_KEY="YOUR_KEY".
  3. Ensure SOCKS5 proxy is set up in the terminal if needed.
  4. Run python run_agent_mre.py.
  5. Observe the console output.

Expected Result for MRE:
The LLMCallStartedEvent log should show the Tools in API Call (or Event Dict Tools) field populated with the schema for MinimalRAGQueryTool.

Actual Result for MRE:
The Tools in API Call (or Event Dict Tools) field in LLMCallStartedEvent is null. The agent gets stuck in "Thinking...".

Expected behavior

The LLMCallStartedEvent.event.__dict__['tools'] field (or an equivalent attribute holding the structured tool definitions passed to litellm.completion) should contain a list of tool definitions formatted for the Gemini API (e.g., a list of dictionaries, each with a function_declarations key).

The Gemini LLM should then be able to recognize these tools and, when appropriate, return a response containing tool_calls to invoke the custom tool, or at least a ReAct formatted text output indicating a tool call. The Agent should proceed to execute the tool.

Screenshots/Code snippets

{
  "role": "user",
  "content": "\nCurrent Task:

\u7528\u6237\u67e5\u8be2\u662f\uff1a'\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d
\u662f\u591a\u5c11\uff1f'\u3002\u4f60\u7684\u4efb\u52a1\u662f\u4f7f\u7528HybridRAGQueryTool\u5de5\u5177\u
5bf9\u7528\u6237\u67e5\u8be2\u8fdb\u884c\u6df7\u5408RAG\u641c\u7d22\u3002
\u4f60\u5fc5\u987b\u8c03\u7528\u8fd9\u4e2a\u5de5\u5177\u6765\u5b8c\u6210\u4efb\u52a1\u3002\u4ee5\u4e0b\u662
f\u8c03\u7528\u5de5\u5177\u7684\u7cbe\u786e\u793a\u4f8b\uff0c\u8bf7\u4e25\u683c\u9075\u5faa\u6b64\u683c\u5f
0f\uff1a\n\n\nThought: \u6211\u9700\u8981\u5bf9\u7528\u6237\u67e5\u8be2 '\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d\u662f\u591a\u5c11\uff1f' \u8fdb\u884c\u6df7\u5408RAG\u641c\u7d22\uff0c\u4ee5\u83b7\u53d6\u76f8\u5173\u4fe1\u606f\u3002\nAction: HybridRAGQueryTool\nAction Input: {{\"query\": \"\u516c\u53f82024\u5e74\u7b2c\u4e00\u5b63\u5ea6\u7684\u9500\u552e\u989d\u662f\u591a\u5c11\uff1f\", \"top_k_vector\": 5, \"top_k_kg\": 3, \"top_k_bm25\": 5}}\n\n\n\u8bf7\u52a1\u5fc5\u5c06 Action Input
\u4e25\u683c\u683c\u5f0f\u5316\u4e3a JSON
\u5b57\u7b26\u4e32\uff0c\u5e76\u786e\u4fdd\u5176\u4e2d\u7684\u53c2\u6570\u4e0e\u5de5\u5177\u7684
args_schema
\u5339\u914d\u3002\n\u4f60\u7684\u6700\u7ec8\u8f93\u51fa\uff08\u6307\u4f60\u8c03\u7528\u5de5\u5177\u540e\u7
684\u771f\u5b9e
Observation\uff09\u5c06\u662fRAG\u5de5\u5177\u7684\u539f\u59cb\u8f93\u51faJSON\u5b57\u7b26\u4e32\uff0c\u4e0
d\u8fdb\u884c\u4efb\u4f55\u4fee\u6539\u6216\u89e3\u91ca\u3002\u6ce8\u610f\uff1aHybridRAGQueryTool
\u7684\u8f93\u51fa\uff08\u771f\u5b9e\u7684
Observation\uff09\u4f1a\u662f\u4e00\u4e2aJSON\u5b57\u7b26\u4e32\uff0c\u5305\u542b 'status'
\u5b57\u6bb5\u3002\u5982\u679c 'status' \u662f 'error' \u6216
'clarification_needed'\uff0c\u8bf7\u5c06\u9519\u8bef\u4fe1\u606f\u6216\u6f84\u6e05\u8bf7\u6c42\u4f5c\u4e3a
u4f60\u7684\u601d\u8003\u7ed3\u679c\u7684\u4e00\u90e8\u5206\uff0c\u5e76\u505c\u6b62\u8fdb\u4e00\u6b65\u7684
\u56de\u7b54\u751f\u6210\uff0c\u4f8b\u5982\uff1a'Thought:
\u5de5\u5177\u8fd4\u56de\u4e86\u6f84\u6e05\u8bf7\u6c42\uff0c\u6211\u9700\u8981\u62a5\u544a\u7ed9\u7528\u623
7\u3002\nFinal Answer: \u7cfb\u7edf\u9700\u8981\u6f84\u6e05\uff1a[\u6f84\u6e05\u95ee\u9898]'\u3002\n\nThis
is the expected criteria for your final answer:
\u6df7\u5408RAG\u5de5\u5177\u7684\u539f\u59cb\u8f93\u51faJSON\u5b57\u7b26\u4e32\uff0c\u53ef\u80fd\u5305\u54
2b 'status', 'final_answer', 'clarification_question', \u6216 'error_message' \u7b49\u5b57\u6bb5\u3002\nyou
MUST return the actual complete content as the final answer, not a summary.\n\nBegin! This is VERY
important to you, use the tools available and give your best Final Answer, your job depends on
it!\n\nThought:"
}
],
"tools": null,
"callbacks": [
"<crewai.utilities.token_counter_callback.TokenCalcHandler object at 0x7ff336a374a0>"
],
"available_functions": null
}

============================================

🚀 Crew: crew
└── 📋 Task: 50be96fc-ef6f-4afc-94e9-b738123b35df
Status: Executing Task...
└── 🧠 Thinking...

Operating System

Ubuntu 22.04

Python Version

3.12

crewAI Version

0.121.0

crewAI Tools Version

0.45.0

Virtual Environment

Venv

Evidence

Key evidence is the LLMCallStartedEvent log showing event.dict['tools'] as null, consistently across multiple tests, even after ensuring Pydantic schemas are correctly defined without default values and with ConfigDict(extra='forbid').
A direct LiteLLM call (outside CrewAI, using litellm.completion) with a manually constructed tools parameter (containing function_declarations for the same tool schema) does show LiteLLM attempting to pass these tool definitions to the Gemini API (visible in LiteLLM's Final returned optional params). This strongly suggests the issue lies within CrewAI's preparation or passing of tool definitions to LiteLLM when targeting Gemini.

Please see the "Description" and "Steps to Reproduce" sections for detailed logs and MRE code.

Possible Solution

None

Additional context

We have been through extensive debugging, including:

  • Verifying API key validity and network connectivity (SOCKS5 proxy is in use and confirmed working for direct LiteLLM calls to Gemini for text generation, and also for LiteLLM calls with tools when tools are manually constructed and passed to litellm.completion).
  • Ensuring all Pydantic args_schema fields have non-null description, no default values, and use ConfigDict(extra='forbid').
  • Manually embedding a standard JSON Schema representation of args_schema within the tool.description attribute (this successfully appears in the text prompt but does not solve the tools: null API parameter issue).
  • Providing very explicit ReAct examples in Task descriptions.
  • Updating all relevant libraries (crewai, litellm, google-generativeai, pydantic, httpx[socks], pysocks) to their latest or recent stable versions.
    The core issue remains that the structured tool definitions are not being passed at the API call level (tools: null), preventing Gemini from utilizing its native function calling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions