crewai - ✅(Solved) Fix [BUG] output_pydantic model injected as native tool even when supports_function_calling() is False [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
crewAIInc/crewAI#4695Fetched 2026-04-08 00:40:38
View on GitHub
Comments
1
Participants
2
Timeline
9
Reactions
0
Timeline (top)
cross-referenced ×4labeled ×2referenced ×2commented ×1

When a Task has output_pydantic set and the LLM is a LiteLLM provider (Ollama) without function calling support. The output_pydantic Pydantic model is automatically injects as a native function/tool in the LiteLLM call, and forces the LLM to produce it immediately via tool_choice. As a result:

  • The LLM is forced to return the structured output immediately, without having gathered any data.
  • All fields come back empty or with placeholder values
  • There is no react system (with Action, Action Input and Observation like before)

Root Cause

When a Task has output_pydantic set and the LLM is a LiteLLM provider (Ollama) without function calling support. The output_pydantic Pydantic model is automatically injects as a native function/tool in the LiteLLM call, and forces the LLM to produce it immediately via tool_choice. As a result:

  • The LLM is forced to return the structured output immediately, without having gathered any data.
  • All fields come back empty or with placeholder values
  • There is no react system (with Action, Action Input and Observation like before)

Fix Action

Fixed

PR fix notes

PR #4698: fix: prevent response_model from being passed in ReAct flow when LLM lacks function calling

Description (problem / solution / changelog)

fix: don't pass response_model in ReAct flow for non-FC LLMs

Summary

Fixes #4695. When an LLM does not support function calling (supports_function_calling() returns False), the executor falls back to the ReAct text-based pattern. Previously, response_model (derived from task.output_pydantic) was still passed to get_llm_response in this path, which caused InternalInstructor to force structured output via instructor's TOOLS mode — before the agent could reason through Action/Observation cycles.

The fix sets response_model=None in both _invoke_loop_react() and _ainvoke_loop_react(). The output schema is already embedded in the prompt text for guidance (via build_task_prompt_with_schema), and the final conversion to pydantic/json happens downstream in task._export_output().

This also removes the now-dead if self.response_model is not None early-exit block from both methods, simplifying the ReAct response handling to always go through process_llm_response.

Changes to _invoke_loop / _ainvoke_loop routing

The routing logic was refactored to handle three cases explicitly:

ConditionRouteRationale
FC-capable + user-defined tools_invoke_loop_native_toolsUnchanged — native tool calling
FC-capable + no tools at all (self.tools empty) + response_model_invoke_loop_native_no_toolsSingle-shot structured output via instructor; no tools to iterate over
Everything else (non-FC, or FC with internal tools, or no response_model)_invoke_loop_reactReAct text-based loop; response_model=None to avoid forcing structured output

The check uses self.tools (which includes internal tools like delegation/human input) rather than self.original_tools (user-defined only), so agents with delegation enabled still use the ReAct loop even if they have no user-defined tools.

Updates since last revision

  • Used self.tools instead of self.original_tools for no-tools routing check (flagged by Bugbot): self.original_tools only includes user-defined tools, but self.tools also includes internal tools (delegation, human input). Using self.tools ensures agents with internal tools still use the ReAct loop for Action/Observation cycles.
  • Added test for FC+internal-tools scenario (total: 15 new tests).

Review & Testing Checklist for Human

  • Verify task._export_output() reliably converts ReAct string output to pydantic: The fix relies on downstream convert_to_model (which may re-call the LLM) to produce the pydantic output. The old code had an early-exit that accepted raw valid JSON matching the schema directly — this path is now removed. Test with a real non-FC LLM (e.g., Ollama) to confirm end-to-end pydantic output works.
  • Verify ReAct parser handles JSON-like final answers: Now all answers go through process_llm_response which expects the Final Answer: prefix. Confirm the prompt instructs the LLM to use this format, and that models don't produce raw JSON without the prefix.
  • Verify the self.tools vs self.original_tools distinction is correct: The routing uses not self.tools to check for truly no tools. Confirm that self.tools reliably includes all internal tools (delegation, human input, memory) and that there's no edge case where self.tools is empty but internal tools are still expected.
  • Verify the FC+no-tools+response_model path: _invoke_loop_native_no_tools does a single LLM call (no iteration loop). Confirm this is acceptable when an FC-capable LLM has output_pydantic but no tools.

Suggested manual test: Run a crew with output_pydantic set on a task, using an Ollama model (or any model where supports_function_calling() returns False), and confirm the agent completes the ReAct loop and produces valid pydantic output.

Notes

  • 15 new tests added covering sync, async, routing (including FC+no-tools+internal-tools edge cases), tool usage, and crew-level integration
  • All existing executor tests (sync + async) pass with no regressions (55 total tests passing)
  • Link to Devin session: https://app.devin.ai/sessions/fad74341a5d2401dab83634287fc10fd
  • Requested by: João
<!-- CURSOR_SUMMARY -->

[!NOTE] <sup>Cursor Bugbot is generating a summary for commit c5d438402f7750127ec5fa2a12da014a380cfde3. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/agents/crew_agent_executor.py (modified, +45/-73)
  • lib/crewai/tests/agents/test_react_output_pydantic.py (added, +510/-0)

Code Example

from crewai import LLM, Agent, Task, Crew
from crewai.tools import BaseTool
from pydantic import BaseModel

class MyOutput(BaseModel):
    name: str
    value: str

class MySearchTool(BaseTool):
    name: str = "my_search_tool"
    description: str = "Search for data"
    def _run(self, query: str) -> str:
        return '{"name": "Alice", "value": "42"}'

llm = LLM(model="ollama_chat/mistral-small3.2:24b", api_base="https://my-remote-ollama.example.com")

agent = Agent(role="Researcher", goal="Find data", backstory="...", llm=llm, tools=[MySearchTool()])
task = Task(
    description="Find the name and value",
    expected_output="Structured result",
    agent=agent,
    output_pydantic=MyOutput,
)
crew = Crew(agents=[agent], tasks=[task])
crew.kickoff() # Or with async

---

{
  "tools": [{"function": {"name": "MyOutput", .....}}],
  "tool_choice": {"type": "function", "function": {"name": "MyOutput"}}
}

---

self._client = instructor.from_litellm(completion) # always uses Mode.TOOLS by default
RAW_BUFFERClick to expand / collapse

Description

When a Task has output_pydantic set and the LLM is a LiteLLM provider (Ollama) without function calling support. The output_pydantic Pydantic model is automatically injects as a native function/tool in the LiteLLM call, and forces the LLM to produce it immediately via tool_choice. As a result:

  • The LLM is forced to return the structured output immediately, without having gathered any data.
  • All fields come back empty or with placeholder values
  • There is no react system (with Action, Action Input and Observation like before)

Steps to Reproduce

from crewai import LLM, Agent, Task, Crew
from crewai.tools import BaseTool
from pydantic import BaseModel

class MyOutput(BaseModel):
    name: str
    value: str

class MySearchTool(BaseTool):
    name: str = "my_search_tool"
    description: str = "Search for data"
    def _run(self, query: str) -> str:
        return '{"name": "Alice", "value": "42"}'

llm = LLM(model="ollama_chat/mistral-small3.2:24b", api_base="https://my-remote-ollama.example.com")

agent = Agent(role="Researcher", goal="Find data", backstory="...", llm=llm, tools=[MySearchTool()])
task = Task(
    description="Find the name and value",
    expected_output="Structured result",
    agent=agent,
    output_pydantic=MyOutput,
)
crew = Crew(agents=[agent], tasks=[task])
crew.kickoff() # Or with async
  1. Set supports_function_calling() to False (default for custom Ollama models).
  2. Assign a tool to the agent and set output_pydantic on the task.
  3. Run the crew.
  4. Observe that LiteLLM receives:
{
  "tools": [{"function": {"name": "MyOutput", .....}}],
  "tool_choice": {"type": "function", "function": {"name": "MyOutput"}}
}

Expected behavior

The agent must not receive any tools in the native format.

Screenshots/Code snippets

X

Operating System

Ubuntu 20.04

Python Version

3.11

crewAI Version

1.10.0

crewAI Tools Version

1.10.0

Virtual Environment

Venv

Evidence

X

Possible Solution

I search and found this in internal_instructor.py :

self._client = instructor.from_litellm(completion) # always uses Mode.TOOLS by default

But I was unable to fix this issue in local sorry

Additional context

X

extent analysis

Fix Plan

To fix the issue, we need to modify the internal_instructor.py file to not use Mode.TOOLS by default when creating the instructor client. We can achieve this by adding a conditional check to see if the LLM supports function calling.

  • Modify the internal_instructor.py file as follows:

if llm.supports_function_calling(): self._client = instructor.from_litellm(completion, mode=Mode.TOOLS) else: self._client = instructor.from_litellm(completion, mode=Mode.TEXT)

*   Alternatively, you can also modify the `supports_function_calling` method of the `LLM` class to return `False` for custom Ollama models:
    ```python
class LLM:
    # ...
    def supports_function_calling(self):
        if self.model.startswith("ollama_chat/"):
            return False
        # ...
  • If you cannot modify the internal_instructor.py file directly, you can try to override the supports_function_calling method of the LLM class when creating the llm object:

class CustomLLM(LLM): def supports_function_calling(self): return False

llm = CustomLLM(model="ollama_chat/mistral-small3.2:24b", api_base="https://my-remote-ollama.example.com")


### Verification
To verify that the fix worked, you can run the crew again and check the output of the LiteLLM. The output should no longer contain the `tools` and `tool_choice` fields with the `MyOutput` function.

### Extra Tips
*   Make sure to test the fix thoroughly to ensure that it does not introduce any new issues.
*   If you are using a custom Ollama model, you may need to modify the `supports_function_calling` method to return `False` for that specific model.
*   Consider opening a pull request to the crewAI repository to fix this issue for all users.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The agent must not receive any tools in the native format.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING