fix: guard output_pydantic injection when LLM lacks function calling#4862
fix: guard output_pydantic injection when LLM lacks function calling#4862gambletan wants to merge 1 commit intocrewAIInc:mainfrom
Conversation
…on calling When a Task has output_pydantic set and the LLM does not support function calling (e.g., Ollama models), the pydantic schema was being injected as a native tool via response_model, forcing the LLM to produce structured output immediately via tool_choice without gathering data first. This fix adds a guard to only set response_model to output_pydantic/output_json when the LLM supports function calling. When function calling is not supported, the existing text-based conversion path in converter.py handles structured output correctly. Fixes crewAIInc#4695
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| hasattr(self.llm, "supports_function_calling") | ||
| and callable(getattr(self.llm, "supports_function_calling", None)) | ||
| and self.llm.supports_function_calling() | ||
| ) |
There was a problem hiding this comment.
New method duplicates existing method's core logic
Low Severity
_llm_supports_function_calling() duplicates the first three conditions of _supports_native_tool_calling(). The existing method could be refactored to delegate: return self._llm_supports_function_calling() and len(tools) > 0. Additionally, the condition use_native_tool_calling or self._llm_supports_function_calling() in create_agent_executor is logically equivalent to just self._llm_supports_function_calling() (by absorption: (A∧B)∨A = A), making the use_native_tool_calling or part misleadingly redundant.
Additional Locations (1)
alvinttang
left a comment
There was a problem hiding this comment.
Review
This PR addresses a real problem: when output_pydantic is set on a task and the LLM lacks function calling support (e.g., some Ollama models), the pydantic schema gets injected as a tool, forcing the LLM to produce structured output immediately without first using the agent's actual tools.
Issues and suggestions:
-
_llm_supports_function_calling()is fragile: The method checkshasattr+callable+ calls the method. Butsupports_function_calling()onLLMis determined by looking up the model in LiteLLM's model info, and it can raise exceptions (e.g., if the model isn't in LiteLLM's registry). This helper doesn't handle exceptions — alitellm.exceptions.BadRequestErrororKeyErrorfrom the model registry lookup would propagate up and crash. Consider wrapping in a try/except that defaults toTrue(safe fallback: assume function calling is supported, preserving existing behavior). -
Inconsistency between
create_agent_executorand_update_executor_parameters: Increate_agent_executor, the guard isuse_native_tool_calling or self._llm_supports_function_calling(). In_update_executor_parameters, it's onlyself._llm_supports_function_calling()— theuse_native_tool_callingcheck is missing. Ifuse_native_tool_callingwas True in the first method, shouldn't it also be respected in the update path? This could cause the response model to be dropped on executor parameter updates even when native tool calling was initially enabled. -
Relationship with PR #4856: This PR and #4856 both fix issues around
output_pydantic+ tools interaction but from different angles. PR #4856 fixes the LLM-level short-circuit inInternalInstructor. This PR guards at the agent level. They seem complementary but could interact: if both are merged, and the LLM does support function calling, the response_model will be passed through (this PR allows it) but tools will also be passed (PR #4856 ensures they aren't dropped). Worth verifying they compose correctly. -
No tests: This is a behavioral change to core agent executor creation with no test coverage. At minimum, a unit test should verify that when
supports_function_calling()returnsFalse,response_modelisNone(and when it returnsTrue, it's set). Mocking the LLM'ssupports_function_callingmethod would make this straightforward. -
task.response_modelalways passes through: This is correct — an explicitresponse_modelset by the user should always be respected. Good design choice.
The fix direction is right, but the implementation needs hardening around error handling in _llm_supports_function_calling() and consistency between the two code paths.


Summary
Fixes #4695
When a Task has
output_pydanticset and the LLM does not support function calling (e.g., Ollama models via LiteLLM), the pydantic schema is incorrectly injected as a native tool viaresponse_model. This forces the LLM to produce structured output immediately viatool_choicewithout first gathering data through the agent's tools, resulting in empty/placeholder values.Changes
_llm_supports_function_calling()helper method onAgentto check LLM function calling support without requiring a tools listcreate_agent_executor()to only passoutput_pydantic/output_jsonasresponse_modelwhen the LLM supports function calling_update_executor_parameters()with the same guardtask.response_model(explicit user override) is always respected regardless of function calling supportHow it works
When
supports_function_calling()returnsFalse:response_modelis set toNone(unlesstask.response_modelis explicitly set)converter.pyhandles converting the result to the pydantic modelWhen
supports_function_calling()returnsTrue:output_pydantic/output_jsonis passed asresponse_modelTest plan
output_pydantictask + agent tools -- verify the agent uses ReAct loop and tools before producing structured outputoutput_pydantic-- verify existing behavior is unchangedtask.response_modelis always passed through regardless of function calling supportNote
Medium Risk
Changes how
response_modelis set for task execution, which can alter structured-output and tool-calling behavior across LLM providers. Risk is moderate because it affects agent executor configuration but is narrowly scoped to guarding schema injection based on function-calling support.Overview
Prevents
Task.output_pydantic/output_jsonfrom being passed asresponse_modelwhen the underlying LLM does not support native function calling, avoiding forced structured output that can bypass tool usage.Adds an
Agent._llm_supports_function_calling()helper and updates bothcreate_agent_executor()and_update_executor_parameters()to only setresponse_modelfromoutput_pydantic/output_jsonwhen function calling is supported (while always honoring an explicittask.response_model).Written by Cursor Bugbot for commit 0ca3977. This will update automatically on new commits. Configure here.