Repository navigation
[BUG] Responses API streaming drops tool calls when available_functions is None, returning "" instead of the function-call list #7497
Description
Description
With api="responses" and stream=True, OpenAICompletion._handle_streaming_responses accumulates function calls from response.output_item.done events but only ever acts on them under if function_calls and available_functions:. There is no branch for the available_functions is None case, which is exactly the contract CrewAgentExecutor uses (it passes available_functions=None and executes the tools itself). The accumulated calls are discarded, the method falls through to the LLMCallCompletedEvent(LLM_CALL) path and returns the streamed text — an empty string for a pure tool-call turn.
The non-streaming sibling _handle_responses handles this correctly at the same call site (if function_calls and not available_functions: ... return function_calls), as does the chat-completions streaming path. Only the Responses streaming handlers are missing the branch.
Root cause: lib/crewai/src/crewai/llms/providers/openai/completion.py:1436 (sync) and :1573 (async twin _ahandle_streaming_responses).
Steps to Reproduce
- Configure
LLM(model="gpt-5.5", api="responses", stream=True)on an agent that has at least one tool. - Let
CrewAgentExecutorinvoke the LLM. It calls withtools=openai_toolsandavailable_functions=None(lib/crewai/src/crewai/agents/crew_agent_executor.py:542-543, and the same at:1340-1341), then requires a list back. - The model returns a
function_callitem. _call_responsesdispatches onself._effective_stream()into_handle_streaming_responses, which reaches the guard at line 1436 and drops the call.
Minimal handler-level reproduction (no network; the OpenAI client is faked):
from types import SimpleNamespace
from crewai.llms.providers.openai.completion import OpenAICompletion
FUNCTION_CALL_ITEM = SimpleNamespace(
type="function_call", id="fc_1", call_id="call_abc",
name="multiply", arguments='{"a": 17, "b": 23}', status="completed",
)
RESPONSE = SimpleNamespace(
id="resp_1", status="completed",
output=[FUNCTION_CALL_ITEM], output_text="",
usage=SimpleNamespace(input_tokens=10, output_tokens=5, total_tokens=15,
input_tokens_details=None, output_tokens_details=None),
)
def stream_events():
yield SimpleNamespace(type="response.created", response=RESPONSE)
yield SimpleNamespace(type="response.output_item.done", item=FUNCTION_CALL_ITEM)
yield SimpleNamespace(type="response.completed", response=RESPONSE)
class FakeResponses:
def __init__(self, streaming): self.streaming = streaming
def create(self, **kwargs):
return stream_events() if self.streaming else RESPONSE
class FakeClient:
def __init__(self, streaming): self.responses = FakeResponses(streaming)
def build(**kw):
return OpenAICompletion(model="gpt-5.5", api_key="sk-test", api="responses", **kw)
llm = build(stream=False)
llm._get_sync_client = lambda: FakeClient(False)
nonstream = llm._handle_responses(
params={"input": [{"role": "user", "content": "multiply"}]}, available_functions=None)
print("non-streaming ->", type(nonstream).__name__, nonstream)
llm = build(stream=True)
llm._get_sync_client = lambda: FakeClient(True)
streamed = llm._handle_streaming_responses(
params={"input": [{"role": "user", "content": "multiply"}]}, available_functions=None)
print("streaming ->", type(streamed).__name__, repr(streamed))Full script kept at /tmp/repro_openai_responses_stream_tools.py.
Expected behavior
For a pure tool-call turn, streaming should return the same value the non-streaming path returns: the list of function calls, so the caller (the executor) can run them.
Concrete basis, all in the same class:
- Non-streaming Responses sibling,
completion.py:1113:if function_calls and not available_functions:→ emitLLMCallCompletedEvent(call_type=LLMCallType.TOOL_CALL)andreturn function_calls. The async twin at:1260is identical. - Chat-completions streaming,
completion.py:2061-2062: comment "Without available_functions, return tool_calls so the caller (executor) handles execution", thenif message.tool_calls and not available_functions: ... return list(message.tool_calls); same block at:2499-2500in_finalize_streaming_response, whose docstring states: "Returns: Tool calls list when tools were invoked without available_functions, tool execution result when available_functions is provided, or the text response string." - The executor contract,
lib/crewai/src/crewai/agents/crew_agent_executor.py:551-561: it passesavailable_functions=None, then requiresisinstance(answer, list) and self._is_tool_call_list(answer)to run_handle_native_tool_calls. - The error surfaces from
lib/crewai/src/crewai/utilities/agent_utils.py("Invalid response from LLM call - None or empty."), or the agent silently returns a blank answer.
The streaming Responses handlers (:1436, :1573) use if function_calls and available_functions: with no falsy branch, so this path is the odd one out.
Screenshots/Code snippets
Verbatim output, verifier 1:
stream=False -> list [{'id': 'call_abc', 'name': 'multiply', 'arguments': '{"a": 17, "b": 23}'}] (create calls=1, tools sent=True)
stream=True -> str '' (create calls=1, tools sent=True)
stream=False executor gets: [{'id': 'call_abc', 'name': 'multiply', 'arguments': '{"a": 17, "b": 23}'}]
stream=True executor RAISES: ValueError: Invalid response from LLM call - None or empty.
(patched) stream=True -> list [{'id': 'call_abc', 'name': 'multiply', 'arguments': '{"a": 17, "b": 23}'}]
(patched) pytest tests/llms/openai tests/llms/test_tool_call_streaming.py: 204 passed, 4 failed, 1 skipped
Verbatim output, verifier 2:
================ stream=False ================
RESULT -> 'The answer is 391.'
create() calls: 2 | multiply() ran: [(17, 23)]
================ stream=True ================
RAISED -> ValueError Invalid response from LLM call - None or empty.
create() calls: 3 | multiply() ran: []
Operating System
Other (specify in additional context) — macOS 26.6.2 (Darwin 25.6.0)
Python Version
3.13
crewAI Version
1.15.21 (commit 9393a47)
crewAI Tools Version
Not installed / not involved in the reproduction (1.15.21 available in the same environment). The defect is in crewai core; no crewai-tools code is on the path.
Virtual Environment
Venv
Evidence
Environment: crewAI 1.15.21 @ 9393a47f313a0544db15693db9bcc48585f30ef5, openai 2.41.0, Python 3.13.15, macOS 26.6.2, uv venv.
An independent run on a clean checkout of the commit above gives:
non-streaming -> list [{'id': 'call_abc', 'name': 'multiply', 'arguments': '{"a": 17, "b": 23}'}]
streaming -> str ''
The _handle_responses / _handle_streaming_responses pair differs only in the missing not available_functions branch. Applying the mirror branch to both _handle_streaming_responses and _ahandle_streaming_responses makes the streaming case return the same list, with the existing openai provider tests unchanged in outcome.
Related reports found while checking for duplicates:
- Issue [BUG] Bedrock streaming drops native tool calls, so the tool never runs #7438 + PR fix(bedrock): return native tool calls from streaming when no available_functions #7439 — Bedrock streaming drops native tool calls (same defect class, different provider).
- PR Fix async/sync streaming to return tool calls when available_functions is None #4444 — chat-completions streaming returns tool calls when
available_functions is None; patches_handle_streaming_completion/_ahandle_streaming_completiononly. It does not touch_handle_streaming_responsesor_ahandle_streaming_responses, so the Responses path remains uncovered. - Closed [BUG] Azure streaming completions merge parallel tool calls into one corrupted call #7486 / [BUG] Bedrock streaming tool calls receive empty arguments ({}) — accumulated input is never folded back at contentBlockStop #7243 / [BUG] Async stream does not work with function calls #4442. No existing issue or PR covers the OpenAI Responses streaming path.
Possible Solution
Mirror the non-streaming sibling before the if function_calls and available_functions: block in both _handle_streaming_responses and _ahandle_streaming_responses: when function_calls is non-empty and available_functions is falsy, emit LLMCallCompletedEvent(call_type=LLMCallType.TOOL_CALL, response=function_calls, ...) and return function_calls — the same shape as completion.py:1113 and :2500. Plus a regression test covering the streaming Responses handler with available_functions=None.
Happy to open a PR with this approach.
Additional context
AI disclosure: this issue was authored by an AI agent. Per .github/CONTRIBUTING.md it carries the llm-generated label; I tried to apply it, but adding labels requires write access this account does not have (AddLabelsToLabelable permission error), so a maintainer will need to add it.
Activity
BlueX888 commentedon Sep 16, 2026
Reproduced this. Root cause: lib/crewai/src/crewai/llms/providers/openai/completion.py:1436 (sync _handle_streaming_responses) and :1573 (async _ahandle_streaming_responses) only act on the accumulated function_calls under 'if function_calls and available_functions:', so on the executor path (available_functions=None) the calls are discarded and the empty streamed text is returned in their place.
Minimal reproduction:
Observed output:
Happy to open a PR with this fix, or to be assigned if you prefer to take it.
github-actions commentedon Oct 16, 2026
This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.
github-actions commentedon Oct 21, 2026
This issue was closed because it has been stalled for 5 days with no activity.