[Bugfix][Tool Parser] poolside_v1: accept tool calls without newline after function name (#47311)

Signed-off-by: Joe Rowell <[email protected]>
This commit is contained in:
Joe Rowell
2026-07-02 12:08:37 -07:00
committed by GitHub
parent e392bf7a68
commit 258f8de91f
2 changed files with 103 additions and 1 deletions
@@ -220,6 +220,108 @@ def test_responses_extract_tool_calls_with_flat_tools() -> None:
assert args["content"] == content
def _weather_tool() -> dict[str, Any]:
return {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
def _build_weather_request(*, tool_choice: str) -> ChatCompletionRequest:
return ChatCompletionRequest.model_validate(
{
"model": "poolside-test",
"messages": [{"role": "user", "content": "weather in Paris?"}],
"tools": [_weather_tool()],
"tool_choice": tool_choice,
}
)
def test_no_newline_after_name_non_streaming() -> None:
request = _build_weather_request(tool_choice="auto")
parser = _make_parser(request)
model_output = (
"<tool_call>get_weather"
"<arg_key>city</arg_key><arg_value>Paris</arg_value>"
"</tool_call>"
)
result = parser.extract_tool_calls(model_output, request)
assert result.tools_called
assert len(result.tool_calls) == 1
assert result.tool_calls[0].function.name == "get_weather"
args = json.loads(result.tool_calls[0].function.arguments)
assert args == {"city": "Paris"}
def test_newline_after_name_still_parses_non_streaming() -> None:
request = _build_weather_request(tool_choice="auto")
parser = _make_parser(request)
model_output = (
"<tool_call>get_weather\n"
"<arg_key>city</arg_key>\n<arg_value>Paris</arg_value>\n"
"</tool_call>"
)
result = parser.extract_tool_calls(model_output, request)
assert result.tools_called
assert result.tool_calls[0].function.name == "get_weather"
args = json.loads(result.tool_calls[0].function.arguments)
assert args == {"city": "Paris"}
def test_no_newline_after_name_streaming() -> None:
request = _build_weather_request(tool_choice="auto")
parser = _make_parser(request)
model_output = (
"<tool_call>get_weather"
"<arg_key>city</arg_key><arg_value>Paris</arg_value>"
"</tool_call>"
)
name = ""
args = ""
prev = ""
for ch in model_output:
cur = prev + ch
delta = parser.extract_tool_calls_streaming(
previous_text=prev,
current_text=cur,
delta_text=ch,
previous_token_ids=[],
current_token_ids=[],
delta_token_ids=[],
request=request,
)
prev = cur
if delta is None:
continue
for tc in delta.tool_calls or []:
if tc.function is None:
continue
if tc.function.name:
name += tc.function.name
if tc.function.arguments:
args += tc.function.arguments
assert name == "get_weather"
assert json.loads(args) == {"city": "Paris"}
def _stream_partial_start_token(request: ResponsesRequest):
parser = _make_parser(request)
delta = parser.tool_call_start_token[0]
+1 -1
View File
@@ -76,7 +76,7 @@ class PoolsideV1ToolParser(ToolParser):
self.func_call_regex = re.compile(r"<tool_call>.*?</tool_call>", re.DOTALL)
self.func_detail_regex = re.compile(
r"<tool_call>([^\n]*)\n(.*)</tool_call>", re.DOTALL
r"<tool_call>\s*([^\n<]+?)\s*\n?\s*(<arg_key>.*?)?</tool_call>", re.DOTALL
)
self.func_arg_regex = re.compile(
r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>", re.DOTALL