Skip to content

fix: prevent output_pydantic from injecting tool schema when LLM lacks function calling#5085

Open
giulio-leone wants to merge 2 commits intocrewAIInc:mainfrom
giulio-leone:fix/output-pydantic-no-function-calling
Open

fix: prevent output_pydantic from injecting tool schema when LLM lacks function calling#5085
giulio-leone wants to merge 2 commits intocrewAIInc:mainfrom
giulio-leone:fix/output-pydantic-no-function-calling

Conversation

@giulio-leone
Copy link
Copy Markdown
Contributor

Summary

Fixes #4695

When a Task has output_pydantic set and the LLM does not support native function calling (e.g., Ollama models), the executor correctly falls back to the ReAct text-based loop. However, response_model was still forwarded to the LLM call, causing InternalInstructor to inject the Pydantic schema as a tool via instructor.from_litellm(completion) in Mode.TOOLS mode — which models without function-calling support cannot handle.

Root Cause

_invoke_loop_react() and _ainvoke_loop_react() both passed self.response_model to get_llm_response() / aget_llm_response(). This triggered the instructor path in LLM._process_call_params() (line 1153), which wraps the LLM with instructor.from_litellm(completion) using the default Mode.TOOLS — injecting the Pydantic model as a native tool regardless of whether the model actually supports function calling.

Fix

Pass response_model=None in both _invoke_loop_react() and _ainvoke_loop_react(). The task-level convert_to_model() in task.py (line 1021-1040) already handles text → Pydantic conversion after the ReAct loop finishes, so instructor-based extraction is unnecessary and harmful in this code path.

Changes

  • lib/crewai/src/crewai/agents/crew_agent_executor.py: Set response_model=None in both sync and async ReAct loops
  • lib/crewai/tests/agents/test_react_response_model.py: Added 2 regression tests:
    • Verifies response_model=None is passed to get_llm_response in ReAct mode
    • Verifies _invoke_loop_react is selected (not native tools) when function calling is unsupported

Test Results

  • 2 new tests pass ✅
  • 29 existing sync executor tests pass ✅
  • No regressions

Note

Medium Risk
Changes how output_pydantic is handled in the ReAct execution path, which can affect structured output behavior for models without native tool/function calling. Risk is mitigated by added regression tests covering the selection of ReAct mode and ensuring response_model is not forwarded.

Overview
Prevents output_pydantic from being forwarded to the LLM call when the executor falls back to the ReAct text loop, by forcing response_model=None in both sync and async ReAct invocations so non-function-calling models don’t receive an injected tool schema.

Adds regression tests ensuring ReAct mode is chosen when supports_function_calling() is false and that get_llm_response is called with response_model=None in this path.

Written by Cursor Bugbot for commit 16be9ac. This will update automatically on new commits. Configure here.

⚠️ This reopens #4828 which was accidentally closed due to fork deletion.

giulio-leone and others added 2 commits March 21, 2026 15:07
…s function calling

When a Task has output_pydantic set and the LLM does NOT support native
function calling (e.g., Ollama models), the executor falls back to the
ReAct text-based loop. Previously, response_model was still forwarded to
the LLM call, causing instructor to inject the Pydantic schema as a
tool/function — something models without function-calling support cannot
handle.

Root cause: _invoke_loop_react() and _ainvoke_loop_react() passed
self.response_model to get_llm_response(), which triggered
InternalInstructor in Mode.TOOLS mode regardless of model capabilities.

Fix: Pass response_model=None in ReAct mode. The task-level
convert_to_model() already handles text → Pydantic conversion after the
loop finishes, so instructor-based extraction is unnecessary and harmful
in this path.

Closes crewAIInc#4695
Keep the no-function-calling ReAct path string-only after forcing\nresponse_model=None, and add focused sync/async coverage for the\ndirect-JSON path so the fallback parser is only used when needed.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] output_pydantic model injected as native tool even when supports_function_calling() is False

1 participant