[BUG] Responses API streaming drops tool calls when available_functions is None, returning "" instead of the function-call list
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 58.8k
- Forks
- 8.5k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 109
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 9393a47f313a0544db15693db9bcc48585f30ef5)
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 #7438 + PR #7439 — Bedrock streaming drops native tool calls (same defect class, different provider).
- PR #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 #7486 / #7243 / #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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/crewai/src/crewai/llms/providers/openai/completion.py at _handle_streaming_responses and _ahandle_streaming_responses, comparing them with _handle_responses and the chat-completions streaming path. Reproduce the pure tool-call case with available_functions=None, then add coverage for both streaming handlers and verify they return the function-call list and emit the existing tool-call completion event.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100