From 85c5804c8ef2f0a3ec246df088bf1c941adb5660 Mon Sep 17 00:00:00 2001 From: lorenzejay Date: Thu, 19 Feb 2026 10:54:01 -0800 Subject: [PATCH] ensure result_as_answer, hooks, and cache parodity --- .../src/crewai/agents/crew_agent_executor.py | 441 +++++++++++------- .../src/crewai/experimental/agent_executor.py | 64 ++- .../tests/agents/test_agent_executor.py | 50 ++ .../tests/agents/test_native_tool_calling.py | 133 ++++++ ...alling_tool_hook_parity_agent_kickoff.yaml | 264 +++++++++++ ...ve_tool_calling_tool_hook_parity_crew.yaml | 339 ++++++++++++++ 6 files changed, 1110 insertions(+), 181 deletions(-) create mode 100644 lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_agent_kickoff.yaml create mode 100644 lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_crew.yaml diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 2149cee1b..9148276ce 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -744,192 +744,297 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): for tool in self.original_tools or []: original_tools_by_name[sanitize_tool_name(tool.name)] = tool - # Reserve max-usage slots deterministically in call order. - # This prevents race conditions when multiple parallel calls target the same tool. - reserved_usage_by_tool: dict[str, int] = {} - execution_plan: list[tuple[str, str, str | dict[str, Any], Any | None, bool]] = [] - for call_id, func_name, func_args in parsed_calls: - original_tool = original_tools_by_name.get(func_name) - should_execute = True - if ( - original_tool - and getattr(original_tool, "max_usage_count", None) is not None - ): - current_usage = getattr(original_tool, "current_usage_count", 0) - reserved = reserved_usage_by_tool.get(func_name, 0) - if current_usage + reserved >= original_tool.max_usage_count: - should_execute = False - else: - reserved_usage_by_tool[func_name] = reserved + 1 - execution_plan.append( - (call_id, func_name, func_args, original_tool, should_execute) - ) - - assistant_message: LLMMessage = { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": call_id, - "type": "function", - "function": { - "name": func_name, - "arguments": func_args - if isinstance(func_args, str) - else json.dumps(func_args), - }, - } - for call_id, func_name, func_args, _, _ in execution_plan - ], - } - self.messages.append(assistant_message) - - def _execute_one( - idx: int, - call_id: str, - func_name: str, - func_args: str | dict[str, Any], - original_tool: Any | None, - should_execute: bool, - ) -> tuple[int, str, str, str, Any | None]: - if isinstance(func_args, str): - try: - args_dict = json.loads(func_args) - except json.JSONDecodeError: - args_dict = {} - else: - args_dict = func_args - - agent_key = ( - getattr(self.agent, "key", "unknown") if self.agent else "unknown" - ) - started_at = datetime.now() - crewai_event_bus.emit( - self, - event=ToolUsageStartedEvent( - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - ), - ) - - track_delegation_if_needed(func_name, args_dict, self.task) - - error_event_emitted = False - result: str = "Tool not found" - if not should_execute and original_tool: - result = ( - f"Tool '{func_name}' has reached its usage limit of " - f"{original_tool.max_usage_count} times and cannot be used anymore." + has_result_as_answer_in_batch = any( + bool( + original_tools_by_name.get(func_name) + and getattr( + original_tools_by_name.get(func_name), "result_as_answer", False + ) + ) + for _, func_name, _ in parsed_calls + ) + # Preserve sequential short-circuit behavior for result_as_answer tools. + if has_result_as_answer_in_batch: + logger.debug( + "Skipping parallel native execution because batch includes result_as_answer tool" + ) + else: + # Reserve max-usage slots deterministically in call order. + # This prevents race conditions when multiple parallel calls target the same tool. + reserved_usage_by_tool: dict[str, int] = {} + execution_plan: list[ + tuple[str, str, str | dict[str, Any], Any | None, bool] + ] = [] + for call_id, func_name, func_args in parsed_calls: + original_tool = original_tools_by_name.get(func_name) + should_execute = True + if ( + original_tool + and getattr(original_tool, "max_usage_count", None) is not None + ): + current_usage = getattr(original_tool, "current_usage_count", 0) + reserved = reserved_usage_by_tool.get(func_name, 0) + if current_usage + reserved >= original_tool.max_usage_count: + should_execute = False + else: + reserved_usage_by_tool[func_name] = reserved + 1 + execution_plan.append( + (call_id, func_name, func_args, original_tool, should_execute) ) - elif func_name in available_functions: - try: - raw_result = available_functions[func_name](**args_dict) - result = ( - str(raw_result) - if not isinstance(raw_result, str) - else raw_result - ) - except Exception as e: - result = f"Error executing tool: {e}" - if self.task: - self.task.increment_tools_errors() - crewai_event_bus.emit( - self, - event=ToolUsageErrorEvent( - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - error=e, - ), - ) - error_event_emitted = True - if not error_event_emitted: + assistant_message: LLMMessage = { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": call_id, + "type": "function", + "function": { + "name": func_name, + "arguments": func_args + if isinstance(func_args, str) + else json.dumps(func_args), + }, + } + for call_id, func_name, func_args, _, _ in execution_plan + ], + } + self.messages.append(assistant_message) + + def _execute_one( + idx: int, + call_id: str, + func_name: str, + func_args: str | dict[str, Any], + original_tool: Any | None, + should_execute: bool, + ) -> tuple[int, str, str, str, Any | None]: + if isinstance(func_args, str): + try: + args_dict = json.loads(func_args) + except json.JSONDecodeError: + args_dict = {} + else: + args_dict = func_args + + agent_key = ( + getattr(self.agent, "key", "unknown") + if self.agent + else "unknown" + ) + started_at = datetime.now() crewai_event_bus.emit( self, - event=ToolUsageFinishedEvent( - output=result, + event=ToolUsageStartedEvent( tool_name=func_name, tool_args=args_dict, from_agent=self.agent, from_task=self.task, agent_key=agent_key, - started_at=started_at, - finished_at=datetime.now(), ), ) - return idx, call_id, func_name, result, original_tool + track_delegation_if_needed(func_name, args_dict, self.task) - max_workers = min(8, len(parsed_calls)) - ordered_results: list[tuple[int, str, str, str, Any | None] | None] = [ - None - ] * len(parsed_calls) - with ThreadPoolExecutor(max_workers=max_workers) as pool: - futures = { - pool.submit( - _execute_one, - idx, - call_id, - func_name, - func_args, - original_tool, - should_execute, - ): idx - for idx, ( - call_id, - func_name, - func_args, - original_tool, - should_execute, - ) in enumerate(execution_plan) - } - for future in as_completed(futures): - idx = futures[future] - ordered_results[idx] = future.result() + structured_tool: CrewStructuredTool | None = None + for structured in self.tools or []: + if sanitize_tool_name(structured.name) == func_name: + structured_tool = structured + break - for record in ordered_results: - if record is None: - continue - _, call_id, func_name, result, original_tool = record - - tool_message: LLMMessage = { - "role": "tool", - "tool_call_id": call_id, - "name": func_name, - "content": result, - } - self.messages.append(tool_message) - - if self.agent and self.agent.verbose: - self._printer.print( - content=f"Tool {func_name} executed with result: {result[:200]}...", - color="green", + hook_blocked = False + before_hook_context = ToolCallHookContext( + tool_name=func_name, + tool_input=args_dict, + tool=structured_tool, # type: ignore[arg-type] + agent=self.agent, + task=self.task, + crew=self.crew, ) + before_hooks = get_before_tool_call_hooks() + try: + for hook in before_hooks: + hook_result = hook(before_hook_context) + if hook_result is False: + hook_blocked = True + break + except Exception as hook_error: + if self.agent.verbose: + self._printer.print( + content=f"Error in before_tool_call hook: {hook_error}", + color="red", + ) - if ( - original_tool - and hasattr(original_tool, "result_as_answer") - and original_tool.result_as_answer - ): - return AgentFinish( - thought="Tool result is the final answer", - output=result, - text=result, + from_cache = False + input_str = json.dumps(args_dict) if args_dict else "" + if self.tools_handler and self.tools_handler.cache: + cached_result = self.tools_handler.cache.read( + tool=func_name, input=input_str + ) + if cached_result is not None: + result = ( + str(cached_result) + if not isinstance(cached_result, str) + else cached_result + ) + from_cache = True + + error_event_emitted = False + result: str = "Tool not found" + if not should_execute and original_tool: + result = ( + f"Tool '{func_name}' has reached its usage limit of " + f"{original_tool.max_usage_count} times and cannot be used anymore." + ) + elif hook_blocked: + result = f"Tool execution blocked by hook. Tool: {func_name}" + elif from_cache: + pass + elif func_name in available_functions: + try: + raw_result = available_functions[func_name](**args_dict) + if self.tools_handler and self.tools_handler.cache: + should_cache = True + if ( + original_tool + and hasattr(original_tool, "cache_function") + and callable(original_tool.cache_function) + ): + should_cache = original_tool.cache_function( + args_dict, raw_result + ) + if should_cache: + self.tools_handler.cache.add( + tool=func_name, + input=input_str, + output=raw_result, + ) + result = ( + str(raw_result) + if not isinstance(raw_result, str) + else raw_result + ) + except Exception as e: + result = f"Error executing tool: {e}" + if self.task: + self.task.increment_tools_errors() + crewai_event_bus.emit( + self, + event=ToolUsageErrorEvent( + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + error=e, + ), + ) + error_event_emitted = True + + after_hook_context = ToolCallHookContext( + tool_name=func_name, + tool_input=args_dict, + tool=structured_tool, # type: ignore[arg-type] + agent=self.agent, + task=self.task, + crew=self.crew, + tool_result=result, ) + after_hooks = get_after_tool_call_hooks() + try: + for after_hook in after_hooks: + after_hook_result = after_hook(after_hook_context) + if after_hook_result is not None: + result = after_hook_result + after_hook_context.tool_result = result + except Exception as hook_error: + if self.agent.verbose: + self._printer.print( + content=f"Error in after_tool_call hook: {hook_error}", + color="red", + ) - reasoning_prompt = self._i18n.slice("post_tool_reasoning") - reasoning_message: LLMMessage = { - "role": "user", - "content": reasoning_prompt, - } - self.messages.append(reasoning_message) - return None + if not error_event_emitted: + crewai_event_bus.emit( + self, + event=ToolUsageFinishedEvent( + output=result, + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + started_at=started_at, + finished_at=datetime.now(), + ), + ) + + return idx, call_id, func_name, result, original_tool + + max_workers = min(8, len(parsed_calls)) + ordered_results: list[tuple[int, str, str, str, Any | None] | None] = [ + None + ] * len(parsed_calls) + with ThreadPoolExecutor(max_workers=max_workers) as pool: + futures = { + pool.submit( + _execute_one, + idx, + call_id, + func_name, + func_args, + original_tool, + should_execute, + ): idx + for idx, ( + call_id, + func_name, + func_args, + original_tool, + should_execute, + ) in enumerate(execution_plan) + } + for future in as_completed(futures): + idx = futures[future] + ordered_results[idx] = future.result() + + for record in ordered_results: + if record is None: + continue + _, call_id, func_name, result, original_tool = record + + tool_message: LLMMessage = { + "role": "tool", + "tool_call_id": call_id, + "name": func_name, + "content": result, + } + self.messages.append(tool_message) + + if self.agent and self.agent.verbose: + self._printer.print( + content=f"Tool {func_name} executed with result: {result[:200]}...", + color="green", + ) + + if ( + original_tool + and hasattr(original_tool, "result_as_answer") + and original_tool.result_as_answer + ): + return AgentFinish( + thought="Tool result is the final answer", + output=result, + text=result, + ) + + reasoning_prompt = self._i18n.slice("post_tool_reasoning") + reasoning_message: LLMMessage = { + "role": "user", + "content": reasoning_prompt, + } + self.messages.append(reasoning_message) + return None # Only process the FIRST tool call for sequential execution with reflection tool_call = tool_calls[0] diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 4c3799830..5724def3b 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -1,7 +1,7 @@ from __future__ import annotations -from concurrent.futures import ThreadPoolExecutor, as_completed from collections.abc import Callable, Coroutine +from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime import json import threading @@ -699,9 +699,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): "content": None, "tool_calls": tool_calls_to_report, } - if all( - type(tc).__qualname__ == "Part" for tc in pending_tool_calls - ): + if all(type(tc).__qualname__ == "Part" for tc in pending_tool_calls): assistant_message["raw_tool_call_parts"] = list(pending_tool_calls) self.state.messages.append(assistant_message) @@ -722,9 +720,9 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): pool.submit(self._execute_single_native_tool_call, tool_call): idx for idx, tool_call in enumerate(runnable_tool_calls) } - ordered_results: list[dict[str, Any] | None] = [ - None - ] * len(runnable_tool_calls) + ordered_results: list[dict[str, Any] | None] = [None] * len( + runnable_tool_calls + ) for future in as_completed(future_to_idx): idx = future_to_idx[future] ordered_results[idx] = future.result() @@ -732,10 +730,46 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): result for result in ordered_results if result is not None ] else: - execution_results = [ - self._execute_single_native_tool_call(tool_call) - for tool_call in runnable_tool_calls - ] + # Execute sequentially so result_as_answer tools can short-circuit + # immediately without running remaining calls. + for tool_call in runnable_tool_calls: + execution_result = self._execute_single_native_tool_call(tool_call) + call_id = cast(str, execution_result["call_id"]) + func_name = cast(str, execution_result["func_name"]) + result = cast(str, execution_result["result"]) + from_cache = cast(bool, execution_result["from_cache"]) + original_tool = execution_result["original_tool"] + + tool_message: LLMMessage = { + "role": "tool", + "tool_call_id": call_id, + "name": func_name, + "content": result, + } + self.state.messages.append(tool_message) + + # Log the tool execution + if self.agent and self.agent.verbose: + cache_info = " (from cache)" if from_cache else "" + self._printer.print( + content=f"Tool {func_name} executed with result{cache_info}: {result[:200]}...", + color="green", + ) + + if ( + original_tool + and hasattr(original_tool, "result_as_answer") + and original_tool.result_as_answer + ): + self.state.current_answer = AgentFinish( + thought="Tool result is the final answer", + output=result, + text=result, + ) + self.state.is_finished = True + return "tool_result_is_final" + + return "native_tool_completed" for execution_result in execution_results: call_id = cast(str, execution_result["call_id"]) @@ -843,7 +877,9 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): from_cache = False input_str = json.dumps(args_dict) if args_dict else "" if self.tools_handler and self.tools_handler.cache: - cached_result = self.tools_handler.cache.read(tool=func_name, input=input_str) + cached_result = self.tools_handler.cache.read( + tool=func_name, input=input_str + ) if cached_result is not None: result = ( str(cached_result) @@ -920,7 +956,9 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): # Convert to string for message result = ( - str(raw_result) if not isinstance(raw_result, str) else raw_result + str(raw_result) + if not isinstance(raw_result, str) + else raw_result ) except Exception as e: result = f"Error executing tool: {e}" diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index ff92bcd20..aa1f4ce30 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -586,3 +586,53 @@ class TestNativeToolExecution: assert elapsed < 0.8 assert isinstance(executor.state.current_answer, AgentFinish) assert executor.state.current_answer.output == "one" + + def test_execute_native_tool_result_as_answer_short_circuits_remaining_calls( + self, mock_dependencies + ): + executor = AgentExecutor(**mock_dependencies) + call_counts = {"slow_one": 0, "slow_two": 0} + + def slow_one() -> str: + call_counts["slow_one"] += 1 + time.sleep(0.2) + return "one" + + def slow_two() -> str: + call_counts["slow_two"] += 1 + time.sleep(0.2) + return "two" + + result_tool = Mock() + result_tool.name = "slow_one" + result_tool.result_as_answer = True + result_tool.max_usage_count = None + result_tool.current_usage_count = 0 + + executor.original_tools = [result_tool] + executor._available_functions = {"slow_one": slow_one, "slow_two": slow_two} + executor.state.pending_tool_calls = [ + { + "id": "call_1", + "function": {"name": "slow_one", "arguments": "{}"}, + }, + { + "id": "call_2", + "function": {"name": "slow_two", "arguments": "{}"}, + }, + ] + + started = time.perf_counter() + result = executor.execute_native_tool() + elapsed = time.perf_counter() - started + + assert result == "tool_result_is_final" + assert isinstance(executor.state.current_answer, AgentFinish) + assert executor.state.current_answer.output == "one" + assert call_counts["slow_one"] == 1 + assert call_counts["slow_two"] == 0 + assert elapsed < 0.35 + + tool_messages = [m for m in executor.state.messages if m.get("role") == "tool"] + assert len(tool_messages) == 1 + assert tool_messages[0]["tool_call_id"] == "call_1" diff --git a/lib/crewai/tests/agents/test_native_tool_calling.py b/lib/crewai/tests/agents/test_native_tool_calling.py index e2cfa4ff8..973178805 100644 --- a/lib/crewai/tests/agents/test_native_tool_calling.py +++ b/lib/crewai/tests/agents/test_native_tool_calling.py @@ -9,6 +9,7 @@ from __future__ import annotations import os import threading import time +from collections import Counter from unittest.mock import patch import pytest @@ -17,6 +18,8 @@ from pydantic import BaseModel, Field from crewai import Agent, Crew, Task from crewai.events import crewai_event_bus from crewai.events.types.tool_usage_events import ToolUsageFinishedEvent +from crewai.hooks import register_after_tool_call_hook, register_before_tool_call_hook +from crewai.hooks.tool_hooks import ToolCallHookContext from crewai.llm import LLM from crewai.tools.base_tool import BaseTool @@ -322,6 +325,136 @@ class TestOpenAINativeToolCalling: assert result is not None _assert_tools_overlapped() + @pytest.mark.vcr() + @pytest.mark.timeout(180) + def test_openai_parallel_native_tool_calling_tool_hook_parity_crew( + self, parallel_tools: list[BaseTool] + ) -> None: + ParallelProbe.reset() + _attach_parallel_probe_handler() + hook_calls: dict[str, list[dict[str, str]]] = {"before": [], "after": []} + + def before_hook(context: ToolCallHookContext) -> bool | None: + if context.tool_name.startswith("parallel_local_search_"): + hook_calls["before"].append( + { + "tool_name": context.tool_name, + "query": str(context.tool_input.get("query", "")), + } + ) + return None + + def after_hook(context: ToolCallHookContext) -> str | None: + if context.tool_name.startswith("parallel_local_search_"): + hook_calls["after"].append( + { + "tool_name": context.tool_name, + "query": str(context.tool_input.get("query", "")), + } + ) + return None + + register_before_tool_call_hook(before_hook) + register_after_tool_call_hook(after_hook) + + try: + agent = Agent( + role="Parallel Tool Agent", + goal="Use both tools exactly as instructed", + backstory="You follow tool instructions precisely.", + tools=parallel_tools, + llm=LLM(model="gpt-5-nano", temperature=1), + verbose=False, + max_iter=3, + ) + task = Task( + description=_parallel_prompt(), + expected_output="A one sentence summary of both tool outputs", + agent=agent, + ) + crew = Crew(agents=[agent], tasks=[task]) + result = crew.kickoff() + + assert result is not None + _assert_tools_overlapped() + + before_names = [call["tool_name"] for call in hook_calls["before"]] + after_names = [call["tool_name"] for call in hook_calls["after"]] + assert len(before_names) >= 3, "Expected before hooks for all parallel calls" + assert Counter(before_names) == Counter(after_names) + assert all(call["query"] for call in hook_calls["before"]) + assert all(call["query"] for call in hook_calls["after"]) + finally: + from crewai.hooks import ( + unregister_after_tool_call_hook, + unregister_before_tool_call_hook, + ) + + unregister_before_tool_call_hook(before_hook) + unregister_after_tool_call_hook(after_hook) + + @pytest.mark.vcr() + @pytest.mark.timeout(180) + def test_openai_parallel_native_tool_calling_tool_hook_parity_agent_kickoff( + self, parallel_tools: list[BaseTool] + ) -> None: + ParallelProbe.reset() + _attach_parallel_probe_handler() + hook_calls: dict[str, list[dict[str, str]]] = {"before": [], "after": []} + + def before_hook(context: ToolCallHookContext) -> bool | None: + if context.tool_name.startswith("parallel_local_search_"): + hook_calls["before"].append( + { + "tool_name": context.tool_name, + "query": str(context.tool_input.get("query", "")), + } + ) + return None + + def after_hook(context: ToolCallHookContext) -> str | None: + if context.tool_name.startswith("parallel_local_search_"): + hook_calls["after"].append( + { + "tool_name": context.tool_name, + "query": str(context.tool_input.get("query", "")), + } + ) + return None + + register_before_tool_call_hook(before_hook) + register_after_tool_call_hook(after_hook) + + try: + agent = Agent( + role="Parallel Tool Agent", + goal="Use both tools exactly as instructed", + backstory="You follow tool instructions precisely.", + tools=parallel_tools, + llm=LLM(model="gpt-5-nano", temperature=1), + verbose=False, + max_iter=3, + ) + result = agent.kickoff(_parallel_prompt()) + + assert result is not None + _assert_tools_overlapped() + + before_names = [call["tool_name"] for call in hook_calls["before"]] + after_names = [call["tool_name"] for call in hook_calls["after"]] + assert len(before_names) >= 3, "Expected before hooks for all parallel calls" + assert Counter(before_names) == Counter(after_names) + assert all(call["query"] for call in hook_calls["before"]) + assert all(call["query"] for call in hook_calls["after"]) + finally: + from crewai.hooks import ( + unregister_after_tool_call_hook, + unregister_before_tool_call_hook, + ) + + unregister_before_tool_call_hook(before_hook) + unregister_after_tool_call_hook(after_hook) + # ============================================================================= # Anthropic Provider Tests diff --git a/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_agent_kickoff.yaml b/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_agent_kickoff.yaml new file mode 100644 index 000000000..47dc51636 --- /dev/null +++ b/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_agent_kickoff.yaml @@ -0,0 +1,264 @@ +interactions: +- request: + body: '{"messages":[{"role":"system","content":"You are Parallel Tool Agent. You + follow tool instructions precisely.\nYour personal goal is: Use both tools exactly + as instructed"},{"role":"user","content":"\nCurrent Task: This is a tool-calling + compliance test. In your next assistant turn, emit exactly 3 tool calls in the + same response (parallel tool calls), in this order: 1) parallel_local_search_one(query=''latest + OpenAI model release notes''), 2) parallel_local_search_two(query=''latest Anthropic + model release notes''), 3) parallel_local_search_three(query=''latest Gemini + model release notes''). Do not call any other tools and do not answer before + those 3 tool calls are emitted. After the tool results return, provide a one + paragraph summary."}],"model":"gpt-5-nano","temperature":1,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"parallel_local_search_one","description":"Local + search tool #1 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_two","description":"Local + search tool #2 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_three","description":"Local + search tool #3 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}}]}' + headers: + User-Agent: + - X-USER-AGENT-XXX + accept: + - application/json + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + connection: + - keep-alive + content-length: + - '1748' + content-type: + - application/json + host: + - api.openai.com + x-stainless-arch: + - X-STAINLESS-ARCH-XXX + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - X-STAINLESS-OS-XXX + x-stainless-package-version: + - 1.83.0 + x-stainless-read-timeout: + - X-STAINLESS-READ-TIMEOUT-XXX + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-DB244zBgA66fzl8TNcIPRWoE4lDIQ\",\n \"object\": + \"chat.completion\",\n \"created\": 1771521916,\n \"model\": \"gpt-5-nano-2025-08-07\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_D2ojRWqkng6krQ51vWQEU8wR\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"parallel_local_search_one\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest OpenAI model release + notes\\\"}\"\n }\n },\n {\n \"id\": + \"call_v1tpTKw1sYcI75SWG1LCkAC3\",\n \"type\": \"function\",\n + \ \"function\": {\n \"name\": \"parallel_local_search_two\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest Anthropic model + release notes\\\"}\"\n }\n },\n {\n \"id\": + \"call_RrbyZClymnngoNLhlkQLLpwM\",\n \"type\": \"function\",\n + \ \"function\": {\n \"name\": \"parallel_local_search_three\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest Gemini model release + notes\\\"}\"\n }\n }\n ],\n \"refusal\": + null,\n \"annotations\": []\n },\n \"finish_reason\": \"tool_calls\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 343,\n \"completion_tokens\": + 855,\n \"total_tokens\": 1198,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 768,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" + headers: + CF-RAY: + - CF-RAY-XXX + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 19 Feb 2026 17:25:23 GMT + Server: + - cloudflare + Strict-Transport-Security: + - STS-XXX + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - X-CONTENT-TYPE-XXX + access-control-expose-headers: + - ACCESS-CONTROL-XXX + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - OPENAI-ORG-XXX + openai-processing-ms: + - '6669' + openai-project: + - OPENAI-PROJECT-XXX + openai-version: + - '2020-10-01' + set-cookie: + - SET-COOKIE-XXX + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - X-RATELIMIT-LIMIT-REQUESTS-XXX + x-ratelimit-limit-tokens: + - X-RATELIMIT-LIMIT-TOKENS-XXX + x-ratelimit-remaining-requests: + - X-RATELIMIT-REMAINING-REQUESTS-XXX + x-ratelimit-remaining-tokens: + - X-RATELIMIT-REMAINING-TOKENS-XXX + x-ratelimit-reset-requests: + - X-RATELIMIT-RESET-REQUESTS-XXX + x-ratelimit-reset-tokens: + - X-RATELIMIT-RESET-TOKENS-XXX + x-request-id: + - X-REQUEST-ID-XXX + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are Parallel Tool Agent. You + follow tool instructions precisely.\nYour personal goal is: Use both tools exactly + as instructed"},{"role":"user","content":"\nCurrent Task: This is a tool-calling + compliance test. In your next assistant turn, emit exactly 3 tool calls in the + same response (parallel tool calls), in this order: 1) parallel_local_search_one(query=''latest + OpenAI model release notes''), 2) parallel_local_search_two(query=''latest Anthropic + model release notes''), 3) parallel_local_search_three(query=''latest Gemini + model release notes''). Do not call any other tools and do not answer before + those 3 tool calls are emitted. After the tool results return, provide a one + paragraph summary."},{"role":"assistant","content":null,"tool_calls":[{"id":"call_D2ojRWqkng6krQ51vWQEU8wR","type":"function","function":{"name":"parallel_local_search_one","arguments":"{\"query\": + \"latest OpenAI model release notes\"}"}},{"id":"call_v1tpTKw1sYcI75SWG1LCkAC3","type":"function","function":{"name":"parallel_local_search_two","arguments":"{\"query\": + \"latest Anthropic model release notes\"}"}},{"id":"call_RrbyZClymnngoNLhlkQLLpwM","type":"function","function":{"name":"parallel_local_search_three","arguments":"{\"query\": + \"latest Gemini model release notes\"}"}}]},{"role":"tool","tool_call_id":"call_D2ojRWqkng6krQ51vWQEU8wR","name":"parallel_local_search_one","content":"[one] + latest OpenAI model release notes"},{"role":"tool","tool_call_id":"call_v1tpTKw1sYcI75SWG1LCkAC3","name":"parallel_local_search_two","content":"[two] + latest Anthropic model release notes"},{"role":"tool","tool_call_id":"call_RrbyZClymnngoNLhlkQLLpwM","name":"parallel_local_search_three","content":"[three] + latest Gemini model release notes"}],"model":"gpt-5-nano","temperature":1,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"parallel_local_search_one","description":"Local + search tool #1 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_two","description":"Local + search tool #2 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_three","description":"Local + search tool #3 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}}]}' + headers: + User-Agent: + - X-USER-AGENT-XXX + accept: + - application/json + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + connection: + - keep-alive + content-length: + - '2771' + content-type: + - application/json + cookie: + - COOKIE-XXX + host: + - api.openai.com + x-stainless-arch: + - X-STAINLESS-ARCH-XXX + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - X-STAINLESS-OS-XXX + x-stainless-package-version: + - 1.83.0 + x-stainless-read-timeout: + - X-STAINLESS-READ-TIMEOUT-XXX + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-DB24DjyYsIHiQJ7hHXob8tQFfeXBs\",\n \"object\": + \"chat.completion\",\n \"created\": 1771521925,\n \"model\": \"gpt-5-nano-2025-08-07\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"The three latest release-note references + retrieved encompass OpenAI, Anthropic, and Gemini, indicating that all three + major model families are actively updating their offerings. These notes typically + cover improvements to capabilities, safety measures, performance enhancements, + and any new APIs or features, suggesting a trend of ongoing refinement across + providers. If you\u2019d like, I can pull the full release notes or extract + and compare the key changes across the three sources.\",\n \"refusal\": + null,\n \"annotations\": []\n },\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 467,\n \"completion_tokens\": + 1437,\n \"total_tokens\": 1904,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 1344,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" + headers: + CF-RAY: + - CF-RAY-XXX + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 19 Feb 2026 17:25:35 GMT + Server: + - cloudflare + Strict-Transport-Security: + - STS-XXX + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - X-CONTENT-TYPE-XXX + access-control-expose-headers: + - ACCESS-CONTROL-XXX + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - OPENAI-ORG-XXX + openai-processing-ms: + - '10369' + openai-project: + - OPENAI-PROJECT-XXX + openai-version: + - '2020-10-01' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - X-RATELIMIT-LIMIT-REQUESTS-XXX + x-ratelimit-limit-tokens: + - X-RATELIMIT-LIMIT-TOKENS-XXX + x-ratelimit-remaining-requests: + - X-RATELIMIT-REMAINING-REQUESTS-XXX + x-ratelimit-remaining-tokens: + - X-RATELIMIT-REMAINING-TOKENS-XXX + x-ratelimit-reset-requests: + - X-RATELIMIT-RESET-REQUESTS-XXX + x-ratelimit-reset-tokens: + - X-RATELIMIT-RESET-TOKENS-XXX + x-request-id: + - X-REQUEST-ID-XXX + status: + code: 200 + message: OK +version: 1 diff --git a/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_crew.yaml b/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_crew.yaml new file mode 100644 index 000000000..9ce9bf06f --- /dev/null +++ b/lib/crewai/tests/cassettes/agents/TestOpenAINativeToolCalling.test_openai_parallel_native_tool_calling_tool_hook_parity_crew.yaml @@ -0,0 +1,339 @@ +interactions: +- request: + body: '{"trace_id": "e456cc10-ce7b-4e68-a2cc-ddb806a2e7b9", "execution_type": + "crew", "user_identifier": null, "execution_context": {"crew_fingerprint": null, + "crew_name": "crew", "flow_name": null, "crewai_version": "1.9.3", "privacy_level": + "standard"}, "execution_metadata": {"expected_duration_estimate": 300, "agent_count": + 0, "task_count": 0, "flow_method_count": 0, "execution_started_at": "2026-02-19T17:24:41.723158+00:00"}, + "ephemeral_trace_id": "e456cc10-ce7b-4e68-a2cc-ddb806a2e7b9"}' + headers: + Accept: + - '*/*' + Connection: + - keep-alive + Content-Length: + - '488' + Content-Type: + - application/json + User-Agent: + - X-USER-AGENT-XXX + X-Crewai-Organization-Id: + - 3433f0ee-8a94-4aa4-822b-2ac71aa38b18 + X-Crewai-Version: + - 1.9.3 + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + method: POST + uri: https://app.crewai.com/crewai_plus/api/v1/tracing/ephemeral/batches + response: + body: + string: '{"id":"a78f2aca-0525-47c7-8f37-b3fca0ad6672","ephemeral_trace_id":"e456cc10-ce7b-4e68-a2cc-ddb806a2e7b9","execution_type":"crew","crew_name":"crew","flow_name":null,"status":"running","duration_ms":null,"crewai_version":"1.9.3","total_events":0,"execution_context":{"crew_fingerprint":null,"crew_name":"crew","flow_name":null,"crewai_version":"1.9.3","privacy_level":"standard"},"created_at":"2026-02-19T17:24:41.989Z","updated_at":"2026-02-19T17:24:41.989Z","access_code":"TRACE-bd80d6be74","user_identifier":null}' + headers: + Connection: + - keep-alive + Content-Length: + - '515' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 19 Feb 2026 17:24:41 GMT + cache-control: + - no-store + content-security-policy: + - CSP-FILTERED + etag: + - ETAG-XXX + expires: + - '0' + permissions-policy: + - PERMISSIONS-POLICY-XXX + pragma: + - no-cache + referrer-policy: + - REFERRER-POLICY-XXX + strict-transport-security: + - STS-XXX + vary: + - Accept + x-content-type-options: + - X-CONTENT-TYPE-XXX + x-frame-options: + - X-FRAME-OPTIONS-XXX + x-permitted-cross-domain-policies: + - X-PERMITTED-XXX + x-request-id: + - X-REQUEST-ID-XXX + x-runtime: + - X-RUNTIME-XXX + x-xss-protection: + - X-XSS-PROTECTION-XXX + status: + code: 201 + message: Created +- request: + body: '{"messages":[{"role":"system","content":"You are Parallel Tool Agent. You + follow tool instructions precisely.\nYour personal goal is: Use both tools exactly + as instructed"},{"role":"user","content":"\nCurrent Task: This is a tool-calling + compliance test. In your next assistant turn, emit exactly 3 tool calls in the + same response (parallel tool calls), in this order: 1) parallel_local_search_one(query=''latest + OpenAI model release notes''), 2) parallel_local_search_two(query=''latest Anthropic + model release notes''), 3) parallel_local_search_three(query=''latest Gemini + model release notes''). Do not call any other tools and do not answer before + those 3 tool calls are emitted. After the tool results return, provide a one + paragraph summary.\n\nThis is the expected criteria for your final answer: A + one sentence summary of both tool outputs\nyou MUST return the actual complete + content as the final answer, not a summary."}],"model":"gpt-5-nano","temperature":1,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"parallel_local_search_one","description":"Local + search tool #1 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_two","description":"Local + search tool #2 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_three","description":"Local + search tool #3 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}}]}' + headers: + User-Agent: + - X-USER-AGENT-XXX + accept: + - application/json + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + connection: + - keep-alive + content-length: + - '1929' + content-type: + - application/json + host: + - api.openai.com + x-stainless-arch: + - X-STAINLESS-ARCH-XXX + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - X-STAINLESS-OS-XXX + x-stainless-package-version: + - 1.83.0 + x-stainless-read-timeout: + - X-STAINLESS-READ-TIMEOUT-XXX + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-DB23W8RBF6zlxweiHYGb6maVfyctt\",\n \"object\": + \"chat.completion\",\n \"created\": 1771521882,\n \"model\": \"gpt-5-nano-2025-08-07\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_sge1FXUkpmPEDe8nTOgn0tQG\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"parallel_local_search_one\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest OpenAI model release + notes\\\"}\"\n }\n },\n {\n \"id\": + \"call_z5jRPH4DQ7Wp3HdDUlZe8gGh\",\n \"type\": \"function\",\n + \ \"function\": {\n \"name\": \"parallel_local_search_two\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest Anthropic model + release notes\\\"}\"\n }\n },\n {\n \"id\": + \"call_DNlgqnadODDsyQkSuLcXZCX2\",\n \"type\": \"function\",\n + \ \"function\": {\n \"name\": \"parallel_local_search_three\",\n + \ \"arguments\": \"{\\\"query\\\": \\\"latest Gemini model release + notes\\\"}\"\n }\n }\n ],\n \"refusal\": + null,\n \"annotations\": []\n },\n \"finish_reason\": \"tool_calls\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 378,\n \"completion_tokens\": + 2456,\n \"total_tokens\": 2834,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 2368,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" + headers: + CF-RAY: + - CF-RAY-XXX + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 19 Feb 2026 17:25:02 GMT + Server: + - cloudflare + Strict-Transport-Security: + - STS-XXX + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - X-CONTENT-TYPE-XXX + access-control-expose-headers: + - ACCESS-CONTROL-XXX + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - OPENAI-ORG-XXX + openai-processing-ms: + - '19582' + openai-project: + - OPENAI-PROJECT-XXX + openai-version: + - '2020-10-01' + set-cookie: + - SET-COOKIE-XXX + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - X-RATELIMIT-LIMIT-REQUESTS-XXX + x-ratelimit-limit-tokens: + - X-RATELIMIT-LIMIT-TOKENS-XXX + x-ratelimit-remaining-requests: + - X-RATELIMIT-REMAINING-REQUESTS-XXX + x-ratelimit-remaining-tokens: + - X-RATELIMIT-REMAINING-TOKENS-XXX + x-ratelimit-reset-requests: + - X-RATELIMIT-RESET-REQUESTS-XXX + x-ratelimit-reset-tokens: + - X-RATELIMIT-RESET-TOKENS-XXX + x-request-id: + - X-REQUEST-ID-XXX + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are Parallel Tool Agent. You + follow tool instructions precisely.\nYour personal goal is: Use both tools exactly + as instructed"},{"role":"user","content":"\nCurrent Task: This is a tool-calling + compliance test. In your next assistant turn, emit exactly 3 tool calls in the + same response (parallel tool calls), in this order: 1) parallel_local_search_one(query=''latest + OpenAI model release notes''), 2) parallel_local_search_two(query=''latest Anthropic + model release notes''), 3) parallel_local_search_three(query=''latest Gemini + model release notes''). Do not call any other tools and do not answer before + those 3 tool calls are emitted. After the tool results return, provide a one + paragraph summary.\n\nThis is the expected criteria for your final answer: A + one sentence summary of both tool outputs\nyou MUST return the actual complete + content as the final answer, not a summary."},{"role":"assistant","content":null,"tool_calls":[{"id":"call_sge1FXUkpmPEDe8nTOgn0tQG","type":"function","function":{"name":"parallel_local_search_one","arguments":"{\"query\": + \"latest OpenAI model release notes\"}"}},{"id":"call_z5jRPH4DQ7Wp3HdDUlZe8gGh","type":"function","function":{"name":"parallel_local_search_two","arguments":"{\"query\": + \"latest Anthropic model release notes\"}"}},{"id":"call_DNlgqnadODDsyQkSuLcXZCX2","type":"function","function":{"name":"parallel_local_search_three","arguments":"{\"query\": + \"latest Gemini model release notes\"}"}}]},{"role":"tool","tool_call_id":"call_sge1FXUkpmPEDe8nTOgn0tQG","name":"parallel_local_search_one","content":"[one] + latest OpenAI model release notes"},{"role":"tool","tool_call_id":"call_z5jRPH4DQ7Wp3HdDUlZe8gGh","name":"parallel_local_search_two","content":"[two] + latest Anthropic model release notes"},{"role":"tool","tool_call_id":"call_DNlgqnadODDsyQkSuLcXZCX2","name":"parallel_local_search_three","content":"[three] + latest Gemini model release notes"},{"role":"user","content":"Analyze the tool + result. If requirements are met, provide the Final Answer. Otherwise, call the + next tool. Deliver only the answer without meta-commentary."}],"model":"gpt-5-nano","temperature":1,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"parallel_local_search_one","description":"Local + search tool #1 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_two","description":"Local + search tool #2 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}},{"type":"function","function":{"name":"parallel_local_search_three","description":"Local + search tool #3 for concurrency testing.","strict":true,"parameters":{"properties":{"query":{"description":"Search + query","title":"Query","type":"string"}},"required":["query"],"type":"object","additionalProperties":false}}}]}' + headers: + User-Agent: + - X-USER-AGENT-XXX + accept: + - application/json + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + connection: + - keep-alive + content-length: + - '3136' + content-type: + - application/json + cookie: + - COOKIE-XXX + host: + - api.openai.com + x-stainless-arch: + - X-STAINLESS-ARCH-XXX + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - X-STAINLESS-OS-XXX + x-stainless-package-version: + - 1.83.0 + x-stainless-read-timeout: + - X-STAINLESS-READ-TIMEOUT-XXX + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.3 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-DB23sY0Ahpd1yAgLZ882KkA50Zljx\",\n \"object\": + \"chat.completion\",\n \"created\": 1771521904,\n \"model\": \"gpt-5-nano-2025-08-07\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Results returned three items: the latest + OpenAI model release notes, the latest Anthropic model release notes, and + the latest Gemini model release notes.\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": + {\n \"prompt_tokens\": 537,\n \"completion_tokens\": 1383,\n \"total_tokens\": + 1920,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": + 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": + 1344,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n + \ \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" + headers: + CF-RAY: + - CF-RAY-XXX + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 19 Feb 2026 17:25:16 GMT + Server: + - cloudflare + Strict-Transport-Security: + - STS-XXX + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - X-CONTENT-TYPE-XXX + access-control-expose-headers: + - ACCESS-CONTROL-XXX + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - OPENAI-ORG-XXX + openai-processing-ms: + - '12339' + openai-project: + - OPENAI-PROJECT-XXX + openai-version: + - '2020-10-01' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - X-RATELIMIT-LIMIT-REQUESTS-XXX + x-ratelimit-limit-tokens: + - X-RATELIMIT-LIMIT-TOKENS-XXX + x-ratelimit-remaining-requests: + - X-RATELIMIT-REMAINING-REQUESTS-XXX + x-ratelimit-remaining-tokens: + - X-RATELIMIT-REMAINING-TOKENS-XXX + x-ratelimit-reset-requests: + - X-RATELIMIT-RESET-REQUESTS-XXX + x-ratelimit-reset-tokens: + - X-RATELIMIT-RESET-TOKENS-XXX + x-request-id: + - X-REQUEST-ID-XXX + status: + code: 200 + message: OK +version: 1