Skip to content

[BUG] Responses API streaming drops tool calls when available_functions is None, returning "" instead of the function-call list #7497

Description

@BlueX888

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

  1. Configure LLM(model="gpt-5.5", api="responses", stream=True) on an agent that has at least one tool.
  2. Let CrewAgentExecutor invoke the LLM. It calls with tools=openai_tools and available_functions=None (lib/crewai/src/crewai/agents/crew_agent_executor.py:542-543, and the same at :1340-1341), then requires a list back.
  3. The model returns a function_call item.
  4. _call_responses dispatches on self._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: → emit LLMCallCompletedEvent(call_type=LLMCallType.TOOL_CALL) and return function_calls. The async twin at :1260 is identical.
  • Chat-completions streaming, completion.py:2061-2062: comment "Without available_functions, return tool_calls so the caller (executor) handles execution", then if message.tool_calls and not available_functions: ... return list(message.tool_calls); same block at :2499-2500 in _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 passes available_functions=None, then requires isinstance(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:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions