Fix OpenAI tool compatibility (#509)

* Fix OpenAI tool compatibility; add agent mode for OpenAI compatible servers like Llama.cpp; add testcase to verify completeness
This commit is contained in:
Piotr Wilkin (ilintar) authored and GitHub committed 2025-08-22 01:30:02 +02:00
1 parent 395fcc4126
commit b299422699
5 files changed
+61 -3

No files matched your search

+2
View File
@@ -552,6 +552,8 @@ Choose the context that best matches the type of integration you are using.
When launching Serena, specify the context using `--context <context-name>`.
Note that for cases where parameter lists are specified (e.g. Claude Desktop), you must add two parameters to the list.
If you are using a local server (such as Llama.cpp) which requires you to use OpenAI-compatible tool descriptions, use context `oaicompat-agent` instead of `agent`.
#### Modes
Modes further refine Serena's behavior for specific types of tasks or interaction styles. Multiple modes can be active simultaneously, allowing you to combine their effects. Modes influence the system prompt and can also alter the set of available tools by excluding certain ones.
+11 -2
View File
@@ -38,7 +38,7 @@ def configure_logging(*args, **kwargs) -> None: # type: ignore
# patch the logging configuration function in fastmcp, because it's hard-coded and broken
server.configure_logging = configure_logging
server.configure_logging = configure_logging # type: ignore
@dataclass
@@ -106,6 +106,15 @@ class SerenaMCPFactory:
# ---- simplify anyOf/oneOf if they only differ by integer/number ----
for key in ("oneOf", "anyOf"):
if key in node and isinstance(node[key], list):
# Special case: anyOf or oneOf with "type X" and "null"
if len(node[key]) == 2:
types = [sub.get("type") for sub in node[key]]
if "null" in types:
non_null_type = next(t for t in types if t != "null")
if isinstance(non_null_type, str):
node["type"] = non_null_type
node.pop(key, None)
continue
simplified = []
changed = False
for sub in node[key]:
@@ -333,7 +342,7 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory):
@asynccontextmanager
async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]:
openai_tool_compatible = self.context.name in ["chatgpt", "codex"]
openai_tool_compatible = self.context.name in ["chatgpt", "codex", "oaicompat-agent"]
self._set_mcp_tools(mcp_server, openai_tool_compatible=openai_tool_compatible)
log.info("MCP server lifetime setup complete")
yield
@@ -0,0 +1,8 @@
description: All tools except InitialInstructionsTool for agent context, uses OpenAI compatible tool definitions
prompt: |
You are running in agent context where the system prompt is provided externally. You should use symbolic
tools when possible for code understanding and modification.
excluded_tools:
- initial_instructions
tool_description_overrides: {}
+1 -1
View File
@@ -25,7 +25,7 @@ class BaseMockTool(Tool):
"""A mock Tool class for testing."""
def __init__(self):
super().__init__(MockAgent())
super().__init__(MockAgent()) # type: ignore
class BasicTool(BaseMockTool):
+39
View File
@@ -0,0 +1,39 @@
import logging
import pytest
from serena.config.serena_config import SerenaConfig
from serena.mcp import SerenaMCPFactorySingleProcess
from serena.tools.tools_base import ToolRegistry
@pytest.mark.parametrize("context", ("chatgpt", "codex", "oaicompat-agent"))
def test_all_tool_parameters_have_type(context):
"""
For every tool exposed by Serena, ensure that the generated
Open‑AI schema contains a ``type`` entry for each parameter.
"""
cfg = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR)
registry = ToolRegistry()
cfg.included_optional_tools = tuple(registry.get_tool_names_optional())
factory = SerenaMCPFactorySingleProcess(context=context)
# Initialize the agent so that the tools are available
factory._instantiate_agent(cfg, [])
tools = list(factory._iter_tools())
for tool in tools:
mcp_tool = factory.make_mcp_tool(tool, openai_tool_compatible=True)
params = mcp_tool.parameters
# Collect any parameter that lacks a type
issues = []
print(f"Checking tool {tool}")
if "properties" not in params:
issues.append(f"Tool {tool.get_name()!r} missing properties section")
else:
for pname, prop in params["properties"].items():
if "type" not in prop:
issues.append(f"Tool {tool.get_name()!r} parameter {pname!r} missing 'type'")
if issues:
raise AssertionError("\n".join(issues))