crewai - ✅(Solved) Fix [BUG] Native tool calls are discarded if LLM returns a text response [5 pull requests, 5 comments, 3 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#4788Fetched 2026-04-08 00:40:20
View on GitHub
Comments
5
Participants
3
Timeline
32
Reactions
0
Author
Timeline (top)
cross-referenced ×12referenced ×12commented ×5labeled ×1

The Crew Agent Executor loop for models supporting native tools intentionally passes available_functions=None to get_llm_response in order to get the tool calls as list for external execution:

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/agents/crew_agent_executor.py#L512-L528

However, if the LLM response contains a text response in addition to the tool calls, this text response is returned instead of the tool calls, because of available_functions being None. The intention probably would be to follow the path 6) below, but 5) is executed instead.

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/llm.py#L1237-L1251

Fixing this might be as easy as switching 5) and 6), but I'm not sure if this breaks something else.

The plaintext response is passed back into the agent executor loop and handled as potential final answer, the tool calls are never executed.

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/agents/crew_agent_executor.py#L546-L557

Root Cause

However, if the LLM response contains a text response in addition to the tool calls, this text response is returned instead of the tool calls, because of available_functions being None. The intention probably would be to follow the path 6) below, but 5) is executed instead.

Fix Action

Fixed

PR fix notes

PR #4790: fix: prioritize tool calls over text when available_functions is None (fixes #4788)

Description (problem / solution / changelog)

fix: prioritize tool calls over text when available_functions is None

Summary

Fixes #4788. When LLMs like Anthropic return both text content and tool calls in the same response, the text was being returned instead of the tool calls — causing the executor to treat it as a final answer and silently discard the tool calls.

Root cause: CrewAgentExecutor._invoke_loop_native_tools intentionally passes available_functions=None to get_llm_response so that tool calls are returned as a raw list for the executor to handle. However, in LLM._handle_non_streaming_response (and its 3 sibling methods), the condition (not tool_calls or not available_functions) and text_response matched first when both conditions were true, returning text instead of the tool calls.

Fix: Reorder the priority checks in all 4 response handlers so that "tool calls present + no available_functions → return tool calls" is evaluated before the text response fallback:

  • _handle_non_streaming_response (sync, non-streaming)
  • _ahandle_non_streaming_response (async, non-streaming)
  • _handle_streaming_response (sync, streaming)
  • _ahandle_streaming_response (async, streaming)

Review & Testing Checklist for Human

  • Verify streaming handler changes are correct — the streaming paths (_handle_streaming_response at ~line 970 and _ahandle_streaming_response at ~line 1528) had different structure than non-streaming and the fix pattern differs slightly. The new tests only cover non-streaming paths. Consider testing with a streaming model that returns both text + tool calls (e.g., Anthropic via OpenRouter).
  • Check _ahandle_streaming_response broadened conditionif accumulated_tool_args and available_functions: was changed to if accumulated_tool_args:. Verify this doesn't cause issues when accumulated_tool_args is populated but available_functions is provided (existing tool execution path should still work via the inner if tool_calls_list and available_functions: check).
  • Edge case: empty tool_calls list — some providers may return tool_calls=[] (falsy). Verify the original behavior of falling through to text response is preserved in this case.
  • End-to-end test with Anthropic — reproduce the original issue by running a crew with native tool calls against an Anthropic model (e.g., openrouter/anthropic/claude-haiku-4.5) and confirm tools are now executed instead of text being treated as the final answer.

Notes

<!-- CURSOR_SUMMARY -->

[!NOTE] Medium Risk Changes LLM response return behavior so callers may now receive a list of tool calls instead of text in both streaming and non-streaming paths, which can affect downstream assumptions about return types. The logic is localized but touches core tool-calling flow across sync/async handlers.

Overview Fixes a bug where responses containing both text and tool calls could incorrectly return the text when available_functions is None, causing native tool-handling callers to miss tool execution.

Updates all LLM response handlers to prioritize returning tool calls when functions aren’t provided, including streaming where tool calls are accumulated from deltas (accumulated_tool_args) and returned to the caller; tool execution is now explicitly gated to cases where available_functions is provided.

Adds/updates tests to assert streaming returns accumulated tool calls (instead of "") and to cover sync/async non-streaming cases where tool calls should be returned over text, while preserving normal execution when available_functions exists.

<sup>Written by Cursor Bugbot for commit f13272e3a1ff59dc4a9594c7b64d997362c7f948. This will update automatically on new commits. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/llm.py (modified, +74/-22)
  • lib/crewai/tests/test_llm.py (modified, +176/-1)

PR #4795: fix: native tool calls discarded when LLM returns both text and tool calls

Description (problem / solution / changelog)

Root cause

When crew_agent_executor calls get_llm_response with available_functions=None (intentionally, to receive tool calls as a list for external execution), the condition in step 5 of LLM.call() was too broad:

# step 5 — BEFORE (buggy)
if (not tool_calls or not available_functions) and text_response:
    return text_response  # ← returned here, step 6 never reached

When the LLM returns both a text response and tool calls:

  • tool_calls = [ChatCompletionMessageToolCall(...)] → truthy
  • available_functions = None → falsy → not available_functions = True
  • text_response = some string → truthy
  • Result: condition is True, text is returned, tool calls are silently dropped ❌

Step 6 (if tool_calls and not available_functions: return tool_calls) is never reached.

Fix

Narrow the condition so text is only returned when there are genuinely no tool calls:

# step 5 — AFTER (fixed)
if not tool_calls and text_response:
    return text_response

Tool calls now always take priority when both a text response and tool calls are present. This matches the original intent documented in the comment for step 6.

Affected scenario

Reproduced with openrouter/anthropic/claude-haiku-4.5 and openrouter/anthropic/claude-sonnet-4.6 — models that sometimes emit a reasoning/text preamble alongside their tool call declarations.

Closes #4788

<!-- CURSOR_SUMMARY -->

[!NOTE] Medium Risk Changes response selection logic in LLM._handle_non_streaming_response, which can alter return types (string vs tool-call list) for some model outputs. Low implementation complexity, but impacts tool-execution flows that depend on this branching.

Overview Fixes a bug where _handle_non_streaming_response could return text_response and silently discard tool_calls when available_functions is None and the model returns both content and tool calls.

The tool-call branch is now evaluated before the text-response branch, ensuring callers that execute tools externally reliably receive the tool_calls list in this scenario.

<sup>Written by Cursor Bugbot for commit 7a656ae0046f707ac29e84bc06022bb01849c219. This will update automatically on new commits. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/llm.py (modified, +9/-6)

PR #4802: fix: prioritize tool calls over text when available_functions is None

Description (problem / solution / changelog)

Summary

Fixes #4788 - Native tool calls are discarded if LLM returns a text response alongside them.

When an LLM (e.g., Anthropic via OpenRouter/Bedrock) returns both a text response and tool calls in the same message, the previous logic in _handle_non_streaming_response returned the text response instead of the tool calls when available_functions was None. This caused the CrewAgentExecutor to treat the text as a final answer, silently discarding the tool calls that should have been executed.

Root cause

The condition at step 5 was:

if (not tool_calls or not available_functions) and text_response:
    return text_response

When tool_calls is truthy (tool calls exist), not available_functions is True (executor passes None), and text_response is truthy, this condition matches and returns the text — never reaching step 6 which would return the tool calls.

Fix

Swap the order of steps 5 and 6 so that tool calls are returned first when available_functions is None, before falling back to text response. The new step 6 now only returns text when there are genuinely no tool calls (not tool_calls).

This is a minimal, focused change (4 lines of logic) that preserves all existing behavior for:

  • Responses with only text (no tool calls) — still returns text
  • Responses with tool calls and available_functions provided — still executes tools
  • Responses with no text and no tool calls — falls through to step 8

Test plan

  • Verify LLM responses with both text + tool calls return tool calls when available_functions=None
  • Verify text-only responses still work correctly
  • Verify tool execution still works when available_functions is provided
  • Run existing test suite: pytest lib/crewai/tests/test_llm.py

🤖 Generated with Claude Code

<!-- CURSOR_SUMMARY -->

[!NOTE] Medium Risk Changes core LLM response selection logic and could affect downstream callers that previously received text in mixed text+tool responses, though the change is small and narrowly scoped to the available_functions=None case.

Overview Fixes non-streaming LLM response handling so that when a provider returns both tool_calls and text in the same message and available_functions is None, the method returns the tool_calls instead of prematurely treating the text as the final answer.

This reorders the decision logic in _handle_non_streaming_response to give tool calls priority (enabling executors to run native tools) while preserving the existing paths for text-only responses and for executing tools when available_functions is provided.

<sup>Written by Cursor Bugbot for commit 10cdfd6d0f8d465b9352b51b36319d0eb71507da. This will update automatically on new commits. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/llm.py (modified, +11/-7)

PR #4806: fix(llm): prevent text response from overriding native tool calls

Description (problem / solution / changelog)

Summary

  • When the LLM returns both a text response and tool_calls, the executor passes available_functions=None. The existing condition (not tool_calls or not available_functions) and text_response at step 5 matched because not available_functions was True, so the text response was returned and tool calls were silently discarded.
  • Fix: reorder the checks so that "tool_calls present but no available_functions" is evaluated before the text-return path. This ensures tool calls are returned to the caller (executor) for handling.
  • No new dependencies or breaking changes.

Test plan

  • Verify with an LLM that returns both text and tool_calls when available_functions=None -- tool calls should be returned, not text
  • Verify that when only text is returned (no tool_calls), behavior is unchanged
  • Verify that when available_functions is provided, tool execution still works normally

Fixes #4788

🤖 Generated with Claude Code

<!-- CURSOR_SUMMARY -->

[!NOTE] Medium Risk Changes LLM non-streaming return-path precedence so responses with tool_calls no longer fall back to text when available_functions is None, which may alter what downstream callers receive in this edge case.

Overview Prevents native tool_calls from being silently dropped when the LLM returns both text and tool calls but available_functions is not provided.

Reorders the non-streaming and async non-streaming response handling to return tool_calls first (and emit LLMCallType.TOOL_CALL completion events) before the text-return path, while keeping existing behavior when there are no tool calls or when tool execution is available.

<sup>Written by Cursor Bugbot for commit 33985614e0a5b973d7fea32b678408e3e5a3eaa4. This will update automatically on new commits. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/llm.py (modified, +32/-13)

PR #4819: Fix LiteLLM response_model and native tool call handling

Description (problem / solution / changelog)

This pull request fixes two related bugs in the LiteLLM integration used by the LLM wrapper:

  • When supports_function_calling() is true and a response_model is provided, the internal instructor path was used even when native tools were also passed. This caused custom agent tools to be ignored in favor of the structured output model.
  • Native tool calls could be dropped when the LLM returned both text and tool calls in the same response. In that case, the text branch won and the tool calls were never surfaced back to the executor.

Key changes:

  1. Only route through InternalInstructor when no tools are being passed. This ensures that when tools are provided, the call behaves like a normal tool-enabled completion and agent tools are preserved.

  2. Prefer tool calls over plain text when both are present. The non-streaming response handlers now return tool calls first when they exist, and only fall back to text when there are no tool calls.

These changes are applied to both the synchronous and asynchronous non-streaming handlers so that behavior is consistent across code paths.

Closes #4697. Closes #4788.

<!-- CURSOR_SUMMARY -->

[!NOTE] Medium Risk Changes LiteLLM non-streaming return semantics around response_model and tool-calling, which can affect downstream executors that previously received plain text instead of tool call objects. Risk is moderate due to altered control flow in a core LLM wrapper, but scoped to LiteLLM non-streaming sync/async handlers.

Overview Fixes LiteLLM non-streaming handling so response_model routing through InternalInstructor is skipped when native tools are provided, preserving tool-enabled completions.

Adjusts both sync and async non-streaming response handlers to prefer returning tool_calls when present but available_functions isn’t supplied (so callers can execute tools), and only fall back to returning text when no tool calls exist.

<sup>Written by Cursor Bugbot for commit f647049aa545f74414ca9e6e1475fab0aaa99728. This will update automatically on new commits. Configure here.</sup>

<!-- /CURSOR_SUMMARY -->

Changed files

  • lib/crewai/src/crewai/llm.py (modified, +16/-15)

Code Example

ModelResponse(id='redacted', created=1773061504, model='anthropic/claude-4.5-haiku-20251001', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='tool_calls', index=0, message=Message(content='I will search for the given query.', role='assistant', tool_calls=[ChatCompletionMessageToolCall(index=0, function=Function(arguments='{"mode": "redacted", "query": "redacted"}', name='code_search'), id='redacted', type='function')], function_call=None, provider_specific_fields={'refusal': None, 'reasoning': None}), provider_specific_fields={'native_finish_reason': 'tool_calls'})], usage=Usage(...), provider='Amazon Bedrock')
RAW_BUFFERClick to expand / collapse

Description

The Crew Agent Executor loop for models supporting native tools intentionally passes available_functions=None to get_llm_response in order to get the tool calls as list for external execution:

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/agents/crew_agent_executor.py#L512-L528

However, if the LLM response contains a text response in addition to the tool calls, this text response is returned instead of the tool calls, because of available_functions being None. The intention probably would be to follow the path 6) below, but 5) is executed instead.

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/llm.py#L1237-L1251

Fixing this might be as easy as switching 5) and 6), but I'm not sure if this breaks something else.

The plaintext response is passed back into the agent executor loop and handled as potential final answer, the tool calls are never executed.

https://github.com/crewAIInc/crewAI/blob/cd42bcf035c7e5b50ca6317da712d99394c75b44/lib/crewai/src/crewai/agents/crew_agent_executor.py#L546-L557

Steps to Reproduce

Sorry, I have no minimal reproduction example available at this time.

I ran a crew with native tool calls against "openrouter/anthropic/claude-haiku-4.5" and "openrouter/anthropic/claude-sonnet-4.6" with some custom tool definitions. This triggered LLM responses with both text response and ChatCompletionMessageToolCall definitions, which causes the described issue.

Expected behavior

Return the tool calls and execute them in the agent executor loop. Discard the text response content (I guess?).

Screenshots/Code snippets

See above

Operating System

Ubuntu 22.04

Python Version

3.12

crewAI Version

1.10.1

crewAI Tools Version

?

Virtual Environment

Venv

Evidence

LLM response with both text response and tool calls:

ModelResponse(id='redacted', created=1773061504, model='anthropic/claude-4.5-haiku-20251001', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='tool_calls', index=0, message=Message(content='I will search for the given query.', role='assistant', tool_calls=[ChatCompletionMessageToolCall(index=0, function=Function(arguments='{"mode": "redacted", "query": "redacted"}', name='code_search'), id='redacted', type='function')], function_call=None, provider_specific_fields={'refusal': None, 'reasoning': None}), provider_specific_fields={'native_finish_reason': 'tool_calls'})], usage=Usage(...), provider='Amazon Bedrock')

Possible Solution

See above

Additional context

See above

extent analysis

Fix Plan

To fix the issue, we need to modify the get_llm_response function to prioritize tool calls over text responses when available_functions is None.

  • Modify the get_llm_response function in llm.py to switch the order of conditions 5) and 6).
  • Update the crew_agent_executor.py to handle the new response format.

Example code:

# llm.py
def get_llm_response(...):
    # ...
    if available_functions is None and tool_calls:
        # 6) Return tool calls
        return tool_calls
    elif available_functions is None and text_response:
        # 5) Return text response (discarded in this case)
        return None
    # ...

Verification

To verify the fix, run the crew with native tool calls against the same models and tool definitions that triggered the issue. The tool calls should be executed in the agent executor loop, and the text response content should be discarded.

Extra Tips

  • Make sure to test the fix with different LLM responses and tool calls to ensure it works as expected.
  • Consider adding logging or debugging statements to monitor the response handling and tool call execution.
  • Review the code changes to ensure they do not introduce any regressions or break other functionality.

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

Return the tool calls and execute them in the agent executor loop. Discard the text response content (I guess?).

Still need to ship something?

×6

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

Back to top recommendations

TRENDING