Skip to content

fix: native tool calls broken over OpenAI Responses API#6515

Open
theCyberTech wants to merge 7 commits into
mainfrom
fix/native-tool-call-responses-api-shape
Open

fix: native tool calls broken over OpenAI Responses API#6515
theCyberTech wants to merge 7 commits into
mainfrom
fix/native-tool-call-responses-api-shape

Conversation

@theCyberTech

@theCyberTech theCyberTech commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Native tool calling was silently broken for any LLM(api="responses") setup with tools attached — three separate, compounding bugs in the same code path:

  • is_tool_call_list() / extract_tool_call_info() (crewai/utilities/agent_utils.py) didn't recognize the Responses API's flat {"id", "name", "arguments"} tool-call dict shape (only Chat-Completions/Anthropic/Bedrock/Gemini shapes were handled). A genuine tool call was misclassified as a plain text answer, so the agent returned the raw unexecuted tool-call JSON as its final output instead of calling the tool.
  • Even after fixing the classification, extract_tool_call_info()'s dict branch only read an "input" key for arguments, never "arguments" — so the tool would have executed with empty args.
  • _prepare_responses_params() (crewai/llms/providers/openai/completion.py) passed Chat-Completions-shaped tool_calls/tool-role messages straight through as Responses API input items. The Responses API has no equivalent shape for those — it expects standalone function_call / function_call_output items — so any multi-turn tool-calling conversation over api="responses" 400'd on the second turn.

Reproduced against a real OpenAI-compatible self-hosted model server (vLLM) with a tool-using agent. Before the fix: the agent returned the tool-call JSON as its "answer" without ever invoking the tool. After: the agent calls the tool, gets the real result, and completes the task correctly across multiple turns.

Test plan

  • Added regression tests for is_tool_call_list() / extract_tool_call_info() covering the Responses API shape, confirmed they fail against pre-fix code and pass against the fix, with no regressions in the existing 77-test suite (tests/utilities/test_agent_utils.py)
  • Added regression tests for _convert_message_to_responses_input_items() / _prepare_responses_params() covering assistant tool_calls, tool-role results, and a full multi-turn conversation, confirmed they fail against pre-fix code and pass against the fix, with no regressions in tests/llms/openai/test_openai.py (101 passed) or tests/llms/openai_compatible/
  • End-to-end manual verification: ran a tool-using agent against a real Responses-API-compatible local model server, confirmed the tool executes with correct arguments and the task completes with real extracted output

Note

Medium Risk
Touches core LLM/agent tool paths for api="responses"; changes are targeted with regression tests and should not affect Chat Completions or other providers.

Overview
Fixes native tool calling when using LLM(api="responses") with tools—previously agents could return raw tool-call JSON instead of executing tools, or fail on follow-up turns.

Responses request shaping: _prepare_responses_params now maps Chat-Completions-style history (assistant tool_calls, tool role results) into Responses API function_call / function_call_output input items via _convert_message_to_responses_input_items, instead of forwarding keys the API rejects on multi-turn tool loops.

Agent tool-call handling: is_tool_call_list and extract_tool_call_info recognize the Responses API’s flat {"name", "arguments", ...} tool-call dicts (not only nested function / Bedrock input shapes), so calls are classified and parsed with real arguments.

Regression tests cover message conversion, multi-turn conversation shape, and the new dict formats in test_openai.py and test_agent_utils.py.

Reviewed by Cursor Bugbot for commit 6450d67. Bugbot is set up for automated code reviews on this repo. Configure here.

theCyberTech and others added 3 commits July 11, 2026 18:12
…tool loop

is_tool_call_list() and extract_tool_call_info() only recognized
Chat-Completions-style ({"function": {...}}), Anthropic-style
({"name", "input"}), and Gemini-style tool-call shapes. The Responses
API's function_call output items are flat dicts shaped
{"id", "name", "arguments"} with no nested "function" key and no
"input" key, so they matched none of the checks.

This caused is_tool_call_list() to misclassify a genuine tool call as
a plain text answer, so the native tool loop returned the raw
tool-call list as the agent's final output instead of executing the
tool. Even after recognizing the shape, extract_tool_call_info() would
have passed an empty arguments dict, since it only read "input" for
the dict fallback.

Verified against LLM(api="responses") with tools attached: the agent
now correctly executes the tool with the parsed arguments instead of
returning the unexecuted tool-call JSON as its answer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Regression tests for is_tool_call_list() and extract_tool_call_info()
against the Responses API's flat {"id", "name", "arguments"} dict
shape, alongside existing Chat-Completions and Bedrock/Anthropic
shapes to confirm no regression there.

Confirmed these tests fail against the pre-fix version of
agent_utils.py (3 failures matching exactly the Responses API cases)
and pass against the fix in 37087b7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… API input items

_prepare_responses_params() passed non-system messages straight through
as the Responses API "input" array without converting them. That's
fine for plain user/assistant text (matches the API's lenient "easy
input message" shape), but Chat-Completions-style assistant messages
carrying "tool_calls" and "tool"-role messages have no equivalent
shape in the Responses API - it expects standalone "function_call" and
"function_call_output" input items instead. Sending the raw
Chat-Completions shapes gets rejected with a 400 (union-type
validation failure against every Responses API input item variant).

This broke every multi-turn tool-calling conversation over
api="responses" that doesn't rely on auto_chain/previous_response_id
(i.e. the common case: resending full history each turn instead of
referencing server-side state).

Added _convert_message_to_responses_input_items() to translate:
  - assistant + tool_calls -> one function_call item per call
  - tool role               -> function_call_output item
  - everything else         -> passed through unchanged

Verified against a real multi-turn tool-calling run: the agent now
completes the full conversation and returns the actual extracted
answer instead of erroring on the second turn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 11:13
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0b45e88-5806-469b-b7d3-92f443bf5dd6

📥 Commits

Reviewing files that changed from the base of the PR and between f6640b1 and 9e06006.

📒 Files selected for processing (1)
  • lib/crewai/src/crewai/llms/providers/openai/completion.py

📝 Walkthrough

Walkthrough

OpenAI Responses API preparation now converts Chat Completions tool-call messages into Responses API input items. Shared utilities recognize flat Responses API tool-call objects, with regression tests covering conversion, parsing, and multi-turn conversations.

Changes

Responses API tool-call compatibility

Layer / File(s) Summary
Responses input conversion
lib/crewai/src/crewai/llms/providers/openai/completion.py, lib/crewai/tests/llms/openai/test_openai.py
Assistant tool calls and tool results are converted into function_call and function_call_output input items, with single-turn and multi-turn regression tests.
Tool-call shape parsing
lib/crewai/src/crewai/utilities/agent_utils.py, lib/crewai/tests/utilities/test_agent_utils.py
Tool-call detection and argument extraction support flat Responses API objects alongside existing nested and alternative provider formats, with corresponding tests.

Sequence Diagram(s)

sequenceDiagram
  participant ChatMessages
  participant OpenAICompletion
  participant ResponsesAPIInput
  ChatMessages->>OpenAICompletion: Chat Completions messages
  OpenAICompletion->>OpenAICompletion: Convert tool calls and tool results
  OpenAICompletion->>ResponsesAPIInput: function_call and function_call_output items
Loading

Suggested reviewers: joaomdmoura, lucasgomide, mattatcha

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: native tool calls for OpenAI Responses API.
Description check ✅ Passed The description accurately matches the changes by explaining the tool-call parsing and Responses API conversion fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/native-tool-call-responses-api-shape

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes native tool-calling when using LLM(api="responses") by teaching CrewAI to correctly recognize and parse Responses API tool-call shapes and by converting Chat-Completions-style tool-call message turns into valid Responses API input items for multi-turn conversations.

Changes:

  • Extend is_tool_call_list() / extract_tool_call_info() to support Responses API’s flat {"id","name","arguments"} tool-call dict shape.
  • Add conversion logic so assistant tool_calls turns and tool role results become function_call / function_call_output input items for Responses API requests.
  • Add regression tests covering both the Responses API tool-call shape parsing and multi-turn input conversion behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
lib/crewai/src/crewai/utilities/agent_utils.py Recognize/parse Responses API flat tool-call dicts (arguments support, tool-call list detection).
lib/crewai/src/crewai/llms/providers/openai/completion.py Convert Chat-Completions tool-call turns into Responses API function_call / function_call_output input items.
lib/crewai/tests/utilities/test_agent_utils.py Regression tests for Responses API tool-call list detection and tool call info extraction.
lib/crewai/tests/llms/openai/test_openai.py Regression tests for converting tool-call conversations into valid Responses API inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/crewai/src/crewai/utilities/agent_utils.py Outdated
Comment thread lib/crewai/src/crewai/llms/providers/openai/completion.py
theCyberTech and others added 2 commits July 11, 2026 19:30
_convert_message_to_responses_input_items() was annotated to return
list[dict[str, Any]], but the passthrough branch returns the LLMMessage
argument unchanged. Lists are invariant in mypy, so a bare LLMMessage
(TypedDict) isn't assignable into a list[dict[str, Any]] return - this
was flagged by CI's type-checker job across all Python versions.

Widened the return type (and the local list built in the tool_calls
branch) to list[dict[str, Any] | LLMMessage], matching what the
function actually returns.

Confirmed with a local mypy run and the full openai/agent_utils test
suites (176 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@joaomdmoura joaomdmoura left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the diagnosis is right and I verified the core fix against the framework's own message flow before reviewing:

  • The flat tool-call dicts come from _extract_function_calls_from_response(), which maps the SDK's item.call_id into "id" — so extract_tool_call_info() picks up the correct correlation id, and the converter's function_call.call_idfunction_call_output.call_id pairing round-trips correctly.
  • Both history writers (agent_executor.py / agent_utils.py) always emit CC-shaped tool_calls with content: None and string arguments, so the converter's assumptions match everything the framework itself produces.
  • _prepare_responses_params() is the single choke point for sync and async, so streaming is covered too.
  • The new is_tool_call_list / extract_tool_call_info branches are additive and can't change CC/Bedrock/Anthropic/Gemini outcomes.

Test quality is good: exact-shape assertions, negative-space checks (no leftover CC-only keys), and still-recognized cases for the other provider shapes.

Requesting changes for two small hardening items (inline) — both matter precisely because this PR's target is OpenAI-compatible servers, where shape drift is the norm. One nit inline as well.

On the broadened is_tool_call_list classification (any dict with name+arguments): I'd accept it as-is — it's the same risk class as the pre-existing Bedrock name+input check, and only provider paths that return Python lists are exposed. Tightening isn't possible without changing the producer; if we want it airtight later, add "type": "function_call" to _extract_function_calls_from_response()'s dicts and check it — follow-up, not a blocker.

Missing test coverage matching the two inline items: assistant message with both content and tool_calls, and dict-valued arguments reaching the converter.

"""
role = message.get("role")

if role == "assistant" and message.get("tool_calls"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assistant text is dropped when a message carries both content and tool_calls.

This branch returns only function_call items, discarding any assistant text. The framework's own history writers always set content: None, so no in-framework impact — but this is a general converter on the public LLM.call(messages=...) path, and Chat-Completions-style histories from external callers legitimately carry assistant text alongside tool calls ("I'll fetch that page…"). That text now silently disappears from the model's context.

Cheap fix — emit the text item first:

if role == "assistant" and message.get("tool_calls"):
    items: list[dict[str, Any] | LLMMessage] = []
    if message.get("content"):
        items.append({"role": "assistant", "content": message["content"]})
    for tool_call in message["tool_calls"]:
        ...

Plus a test for the combined shape.

Comment thread lib/crewai/src/crewai/llms/providers/openai/completion.py Outdated
Comment thread lib/crewai/src/crewai/llms/providers/openai/completion.py
Comment thread lib/crewai/src/crewai/utilities/agent_utils.py
@github-actions github-actions Bot added size/L and removed size/M labels Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/tests/llms/openai/test_openai.py`:
- Around line 1009-1037: Update
test_openai_responses_api_preserves_assistant_content_with_tool_calls to include
a realistic top-level id on the fixture’s tool call, then assert that the
generated Responses API function_call preserves that exact ID as call_id instead
of only checking a generated call_ prefix. Keep the existing assistant content
and arguments assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 789a2591-bbd4-4854-8c91-b67fbcd45cfb

📥 Commits

Reviewing files that changed from the base of the PR and between 6450d67 and f6640b1.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/llms/providers/openai/completion.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/tests/llms/openai/test_openai.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/src/crewai/utilities/agent_utils.py

Comment thread lib/crewai/tests/llms/openai/test_openai.py
@theCyberTech theCyberTech requested a review from joaomdmoura July 13, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants