From 4bef85e363ed668d6bf3b2efc5bcc61380dfbe9f Mon Sep 17 00:00:00 2001 From: Om Chauhan Date: Tue, 7 Apr 2026 10:08:04 +0530 Subject: [PATCH 1/4] added custom_tools support for OpenAI adapters --- src/pipecat/adapters/schemas/tools_schema.py | 6 +- .../adapters/services/open_ai_adapter.py | 10 +- .../services/open_ai_realtime_adapter.py | 10 +- .../services/open_ai_responses_adapter.py | 9 +- tests/test_function_calling_adapters.py | 128 ++++++++++++++++++ 5 files changed, 153 insertions(+), 10 deletions(-) diff --git a/src/pipecat/adapters/schemas/tools_schema.py b/src/pipecat/adapters/schemas/tools_schema.py index e3940f6cb..4ccf2fb81 100644 --- a/src/pipecat/adapters/schemas/tools_schema.py +++ b/src/pipecat/adapters/schemas/tools_schema.py @@ -21,10 +21,12 @@ class AdapterType(Enum): """Supported adapter types for custom tools. Parameters: - GEMINI: Google Gemini adapter - currently the only service supporting custom tools. + GEMINI: Google Gemini adapter. + OPENAI: OpenAI adapter (Chat Completions and Responses API). """ - GEMINI = "gemini" # that is the only service where we are able to add custom tools for now + GEMINI = "gemini" + OPENAI = "openai" class ToolsSchema: diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index 37adcfc9a..3729745d3 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -17,7 +17,7 @@ from openai.types.chat import ( ) from pipecat.adapters.base_llm_adapter import BaseLLMAdapter -from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema from pipecat.processors.aggregators.llm_context import ( LLMContext, LLMContextMessage, @@ -96,7 +96,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): "tool_choice": context.tool_choice, } - def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[ChatCompletionToolParam]: + def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]: """Convert function schemas to OpenAI's function-calling format. Args: @@ -107,10 +107,14 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): with ChatCompletion API. """ functions_schema = tools_schema.standard_tools - return [ + formatted_standard_tools = [ ChatCompletionToolParam(type="function", function=func.to_default_dict()) for func in functions_schema ] + custom_openai_tools = [] + if tools_schema.custom_tools: + custom_openai_tools = tools_schema.custom_tools.get(AdapterType.OPENAI, []) + return formatted_standard_tools + custom_openai_tools def get_messages_for_logging(self, context: LLMContext) -> List[Dict[str, Any]]: """Get messages from a universal LLM context in a format ready for logging about OpenAI. diff --git a/src/pipecat/adapters/services/open_ai_realtime_adapter.py b/src/pipecat/adapters/services/open_ai_realtime_adapter.py index 64684e038..0481d1bd4 100644 --- a/src/pipecat/adapters/services/open_ai_realtime_adapter.py +++ b/src/pipecat/adapters/services/open_ai_realtime_adapter.py @@ -15,7 +15,7 @@ from loguru import logger from pipecat.adapters.base_llm_adapter import BaseLLMAdapter from pipecat.adapters.schemas.function_schema import FunctionSchema -from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema from pipecat.processors.aggregators.llm_context import LLMContext, LLMContextMessage from pipecat.services.openai.realtime import events @@ -236,4 +236,10 @@ class OpenAIRealtimeLLMAdapter(BaseLLMAdapter): List of function definitions in OpenAI Realtime format. """ functions_schema = tools_schema.standard_tools - return [self._to_openai_realtime_function_format(func) for func in functions_schema] + formatted_standard_tools = [ + self._to_openai_realtime_function_format(func) for func in functions_schema + ] + custom_openai_tools = [] + if tools_schema.custom_tools: + custom_openai_tools = tools_schema.custom_tools.get(AdapterType.OPENAI, []) + return formatted_standard_tools + custom_openai_tools diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py index 320bd11ef..e0f0f9c32 100644 --- a/src/pipecat/adapters/services/open_ai_responses_adapter.py +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -13,7 +13,7 @@ from openai._types import NotGiven as OpenAINotGiven from openai.types.responses import FunctionToolParam, ResponseInputItemParam from pipecat.adapters.base_llm_adapter import BaseLLMAdapter -from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema from pipecat.processors.aggregators.llm_context import ( LLMContext, LLMContextMessage, @@ -106,7 +106,7 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam return params - def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[FunctionToolParam]: + def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]: """Convert function schemas to Responses API function tool format. Args: @@ -128,7 +128,10 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam if "description" in d: tool["description"] = d["description"] result.append(tool) - return result + custom_openai_tools = [] + if tools_schema.custom_tools: + custom_openai_tools = tools_schema.custom_tools.get(AdapterType.OPENAI, []) + return result + custom_openai_tools def get_messages_for_logging(self, context: LLMContext) -> List[Dict[str, Any]]: """Get messages from context in a format ready for logging. diff --git a/tests/test_function_calling_adapters.py b/tests/test_function_calling_adapters.py index 348754f47..306cde69e 100644 --- a/tests/test_function_calling_adapters.py +++ b/tests/test_function_calling_adapters.py @@ -15,6 +15,7 @@ from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter from pipecat.adapters.services.open_ai_realtime_adapter import OpenAIRealtimeLLMAdapter +from pipecat.adapters.services.open_ai_responses_adapter import OpenAIResponsesLLMAdapter class TestFunctionAdapters(unittest.TestCase): @@ -176,6 +177,133 @@ class TestFunctionAdapters(unittest.TestCase): tools_def.custom_tools = {AdapterType.GEMINI: [search_tool]} assert GeminiLLMAdapter().to_provider_tools_format(tools_def) == expected + def test_openai_adapter_with_custom_tools(self): + """Test OpenAI adapter appends custom tools.""" + tool_search = {"type": "tool_search"} + expected = [ + ChatCompletionToolParam( + type="function", + function={ + "name": "get_weather", + "description": "Get the weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city, e.g. San Francisco", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use.", + }, + }, + "required": ["location", "format"], + }, + }, + ), + tool_search, + ] + tools_def = self.tools_def + tools_def.custom_tools = {AdapterType.OPENAI: [tool_search]} + assert OpenAILLMAdapter().to_provider_tools_format(tools_def) == expected + + def test_openai_responses_adapter_with_custom_tools(self): + """Test OpenAI Responses adapter appends custom tools.""" + tool_search = {"type": "tool_search"} + expected = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city, e.g. San Francisco", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use.", + }, + }, + "required": ["location", "format"], + }, + "strict": None, + }, + tool_search, + ] + tools_def = self.tools_def + tools_def.custom_tools = {AdapterType.OPENAI: [tool_search]} + assert OpenAIResponsesLLMAdapter().to_provider_tools_format(tools_def) == expected + + def test_openai_responses_adapter(self): + """Test OpenAI Responses adapter format transformation.""" + expected = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city, e.g. San Francisco", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use.", + }, + }, + "required": ["location", "format"], + }, + "strict": None, + } + ] + assert OpenAIResponsesLLMAdapter().to_provider_tools_format(self.tools_def) == expected + + def test_openai_realtime_adapter_with_custom_tools(self): + """Test OpenAI Realtime adapter appends custom tools.""" + tool_search = {"type": "tool_search"} + expected = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city, e.g. San Francisco", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use.", + }, + }, + "required": ["location", "format"], + }, + }, + tool_search, + ] + tools_def = self.tools_def + tools_def.custom_tools = {AdapterType.OPENAI: [tool_search]} + assert OpenAIRealtimeLLMAdapter().to_provider_tools_format(tools_def) == expected + + def test_openai_adapter_ignores_other_adapter_custom_tools(self): + """Test that OpenAI adapter ignores custom tools for other adapters.""" + tools_def = self.tools_def + tools_def.custom_tools = {AdapterType.GEMINI: [{"google_search": {}}]} + result = OpenAILLMAdapter().to_provider_tools_format(tools_def) + assert len(result) == 1 + def test_bedrock_adapter(self): """Test AWS Bedrock adapter format transformation.""" expected = [ From 1443dfb070dd8756c813e292eec0fec39669f0b4 Mon Sep 17 00:00:00 2001 From: Om Chauhan Date: Tue, 7 Apr 2026 10:12:39 +0530 Subject: [PATCH 2/4] added changelog --- changelog/4248.added.md | 1 + src/pipecat/adapters/schemas/tools_schema.py | 2 +- tests/test_function_calling_adapters.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog/4248.added.md diff --git a/changelog/4248.added.md b/changelog/4248.added.md new file mode 100644 index 000000000..72a127c2f --- /dev/null +++ b/changelog/4248.added.md @@ -0,0 +1 @@ +- Added `AdapterType.OPENAI` to `ToolsSchema`, enabling custom OpenAI-specific tools (e.g. `tool_search`) in Chat Completions,Responses, and Realtime adapters. diff --git a/src/pipecat/adapters/schemas/tools_schema.py b/src/pipecat/adapters/schemas/tools_schema.py index 4ccf2fb81..1c1ba0dd3 100644 --- a/src/pipecat/adapters/schemas/tools_schema.py +++ b/src/pipecat/adapters/schemas/tools_schema.py @@ -22,7 +22,7 @@ class AdapterType(Enum): Parameters: GEMINI: Google Gemini adapter. - OPENAI: OpenAI adapter (Chat Completions and Responses API). + OPENAI: OpenAI adapter (Chat Completions, Responses, and Realtime API). """ GEMINI = "gemini" diff --git a/tests/test_function_calling_adapters.py b/tests/test_function_calling_adapters.py index 306cde69e..67df0c7e9 100644 --- a/tests/test_function_calling_adapters.py +++ b/tests/test_function_calling_adapters.py @@ -299,10 +299,10 @@ class TestFunctionAdapters(unittest.TestCase): def test_openai_adapter_ignores_other_adapter_custom_tools(self): """Test that OpenAI adapter ignores custom tools for other adapters.""" + expected = OpenAILLMAdapter().to_provider_tools_format(self.tools_def) tools_def = self.tools_def tools_def.custom_tools = {AdapterType.GEMINI: [{"google_search": {}}]} - result = OpenAILLMAdapter().to_provider_tools_format(tools_def) - assert len(result) == 1 + assert OpenAILLMAdapter().to_provider_tools_format(tools_def) == expected def test_bedrock_adapter(self): """Test AWS Bedrock adapter format transformation.""" From fc3307bc635e9c70784aa57fa2517634300ea302 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 10 Apr 2026 10:15:39 -0400 Subject: [PATCH 3/4] Use OpenAI SDK types for tool params in adapters and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are TypedDicts (plain dicts at runtime), so no behavioral change — just more descriptive type hints for readers. Use ToolParam instead of FunctionToolParam for the Responses adapter to reflect that custom non-function tools are supported. Use ChatCompletionToolParam instead of Any for the completions adapter return type. Update tests to use typed params in expected values. --- .../adapters/services/open_ai_adapter.py | 2 +- .../services/open_ai_responses_adapter.py | 6 +++--- tests/test_function_calling_adapters.py | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index 3729745d3..db9f3ee2f 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -96,7 +96,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]): "tool_choice": context.tool_choice, } - def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]: + def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[ChatCompletionToolParam]: """Convert function schemas to OpenAI's function-calling format. Args: diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py index e0f0f9c32..a58b26c91 100644 --- a/src/pipecat/adapters/services/open_ai_responses_adapter.py +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -10,7 +10,7 @@ import copy from typing import Any, Dict, List, Optional, TypedDict from openai._types import NotGiven as OpenAINotGiven -from openai.types.responses import FunctionToolParam, ResponseInputItemParam +from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam from pipecat.adapters.base_llm_adapter import BaseLLMAdapter from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema @@ -25,7 +25,7 @@ class OpenAIResponsesLLMInvocationParams(TypedDict, total=False): """Context-based parameters for invoking OpenAI Responses API.""" input: List[ResponseInputItemParam] - tools: List[FunctionToolParam] | OpenAINotGiven + tools: List[ToolParam] | OpenAINotGiven instructions: str @@ -106,7 +106,7 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam return params - def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[Any]: + def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[ToolParam]: """Convert function schemas to Responses API function tool format. Args: diff --git a/tests/test_function_calling_adapters.py b/tests/test_function_calling_adapters.py index 67df0c7e9..616f2ea1e 100644 --- a/tests/test_function_calling_adapters.py +++ b/tests/test_function_calling_adapters.py @@ -7,6 +7,8 @@ import unittest from openai.types.chat import ChatCompletionToolParam +from openai.types.responses.function_tool_param import FunctionToolParam +from openai.types.responses.tool_search_tool_param import ToolSearchToolParam from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.tools_schema import AdapterType, ToolsSchema @@ -213,11 +215,11 @@ class TestFunctionAdapters(unittest.TestCase): """Test OpenAI Responses adapter appends custom tools.""" tool_search = {"type": "tool_search"} expected = [ - { - "type": "function", - "name": "get_weather", - "description": "Get the weather in a given location", - "parameters": { + FunctionToolParam( + type="function", + name="get_weather", + description="Get the weather in a given location", + parameters={ "type": "object", "properties": { "location": { @@ -232,9 +234,9 @@ class TestFunctionAdapters(unittest.TestCase): }, "required": ["location", "format"], }, - "strict": None, - }, - tool_search, + strict=None, + ), + ToolSearchToolParam(type="tool_search"), ] tools_def = self.tools_def tools_def.custom_tools = {AdapterType.OPENAI: [tool_search]} From ef4dcca4f1be9b7fcf707e28de6b5e7b68af6922 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 10 Apr 2026 10:23:13 -0400 Subject: [PATCH 4/4] Update changelog to describe user-facing custom_tools support --- changelog/4248.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4248.added.md b/changelog/4248.added.md index 72a127c2f..b48a6a432 100644 --- a/changelog/4248.added.md +++ b/changelog/4248.added.md @@ -1 +1 @@ -- Added `AdapterType.OPENAI` to `ToolsSchema`, enabling custom OpenAI-specific tools (e.g. `tool_search`) in Chat Completions,Responses, and Realtime adapters. +- `ToolsSchema` now accepts `custom_tools` for OpenAI LLM services (`OpenAILLMService`, `OpenAIResponsesLLMService`, `OpenAIResponsesHttpLLMService`, and `OpenAIRealtimeLLMService`), letting you pass provider-specific tools like `tool_search` alongside standard function tools.