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: