diff --git a/docs/server.md b/docs/server.md
index 4d396dc..9f6bd0a 100644
--- a/docs/server.md
+++ b/docs/server.md
@@ -472,7 +472,7 @@ Rerank documents based on a query.
## Tool Calling Support
-OpenArc supports OpenAI-compatible tool calling. Tools are parsed from model output using regex pattern matching for JSON objects containing `name` and `arguments` fields.
+OpenArc supports OpenAI-compatible tool calling. Tools are parsed from model output using Hermes-style `...` tags containing JSON with `name` and `arguments` fields.
Tool calls are detected in streaming and non-streaming modes:
- **Streaming**: Tool calls are detected incrementally and streamed as structured chunks
@@ -480,26 +480,20 @@ Tool calls are detected in streaming and non-streaming modes:
### Parser Implementation
-The `parse_tool_calls()` function searches for JSON objects in the model's text output and converts them to OpenAI-compatible tool call format.
+The `parse_tool_calls()` function extracts payloads from `...` tags in the model's text output and converts them to OpenAI-compatible tool call format.
**Input Format (Model Output):**
-The parser expects JSON objects embedded in the text with the following structure:
+The parser expects Hermes-style tagged payloads with the following structure:
```json
-{
- "name": "function_name",
- "arguments": {
- "arg1": "value1",
- "arg2": "value2"
- }
-}
+{"name":"function_name","arguments":{"arg1":"value1","arg2":"value2"}}
```
**Input to the parser from a model:**
```
-The user wants to know the weather. {"name": "get_weather", "arguments": {"location": "San Francisco", "units": "celsius"}} I'll check that for you.
+The user wants to know the weather. {"name":"get_weather","arguments":{"location":"San Francisco","units":"celsius"}} I'll check that for you.
```
**Output Format (OpenAI-Compatible):**
@@ -521,8 +515,9 @@ Parser returns a list of tool call objects in OpenAI format:
**Parser Behavior:**
-- Searches for JSON objects using regex pattern: `\{(?:[^{}]|(?:\{[^{}]*\}))*\}`
-- Validates that each JSON object contains both `name` and `arguments` fields
+- Extracts JSON payloads from `...` tags
+- Supports an EOS fallback when a `` start tag appears without a closing ``
+- Validates that each payload contains both `name` and `arguments` fields
- Generates unique IDs in format `call_{24-char-hex}`
- Converts `arguments` to JSON string (required by OpenAI format)
- Returns `None` if no valid tool calls are found
diff --git a/src/server/main.py b/src/server/main.py
index da0ff35..a8fefa8 100644
--- a/src/server/main.py
+++ b/src/server/main.py
@@ -6,7 +6,6 @@
import json
import logging
import os
-import re
import time
import traceback
import uuid
@@ -214,24 +213,7 @@ def parse_tool_calls(text: str) -> Optional[List[Dict[str, Any]]]:
if tool_calls:
return tool_calls
- # Backward compatibility for plain JSON tool call outputs without tags.
- pattern = r"\{(?:[^{}]|(?:\{[^{}]*\}))*\}"
- for match in re.findall(pattern, text, re.DOTALL):
- try:
- data = json.loads(match)
- if isinstance(data, dict) and "name" in data and "arguments" in data:
- tool_calls.append({
- "id": f"call_{uuid.uuid4().hex[:24]}",
- "type": "function",
- "function": {
- "name": str(data.get("name", "")),
- "arguments": _format_tool_call_arguments(data.get("arguments", {})),
- },
- })
- except json.JSONDecodeError:
- continue
-
- return tool_calls if tool_calls else None
+ return None
#===============================================================#
# OpenArc internal
diff --git a/src/tests/test_tool_call_parser_unit.py b/src/tests/test_tool_call_parser_unit.py
index 5d5daf0..a6f53c9 100644
--- a/src/tests/test_tool_call_parser_unit.py
+++ b/src/tests/test_tool_call_parser_unit.py
@@ -49,6 +49,14 @@ def test_parse_tool_calls_supports_missing_closing_tag_until_eos() -> None:
assert json.loads(tool_calls[0]["function"]["arguments"]) == {"query": "vLLM"}
+def test_parse_tool_calls_rejects_plain_json_without_tool_call_tags() -> None:
+ text = '{"name":"search","arguments":{"query":"legacy"}}'
+
+ tool_calls = server_main.parse_tool_calls(text)
+
+ assert tool_calls is None
+
+
@pytest.mark.asyncio
async def test_openai_chat_completions_non_streaming_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
class _Workers: