[Bugfix][Tool Parser] poolside_v1: accept tool calls without newline after function name (#47311)
Signed-off-by: Joe Rowell <joerowell4@gmail.com>
Joe Rowell committed
Jul 2, 2026 at 21:08 UTC
258f8de91f99b40a4dfb234e55a09e9493c6ece8
2 files changed
+103
-1
tests/tool_parsers/test_poolside_v1_tool_parser.py
+102
@@ -220,6 +220,108 @@ def test_responses_extract_tool_calls_with_flat_tools() -> None:
220
assert args["content"] == content
221
222
223
+def _weather_tool() -> dict[str, Any]:
224
+ return {
225
+ "type": "function",
226
+ "function": {
227
+ "name": "get_weather",
228
+ "description": "Get the weather for a city",
229
+ "parameters": {
230
+ "type": "object",
231
+ "properties": {"city": {"type": "string"}},
232
+ "required": ["city"],
233
+ },
234
+ },
235
+ }
236
+
237
+
238
+def _build_weather_request(*, tool_choice: str) -> ChatCompletionRequest:
239
+ return ChatCompletionRequest.model_validate(
240
+ {
241
+ "model": "poolside-test",
242
+ "messages": [{"role": "user", "content": "weather in Paris?"}],
243
+ "tools": [_weather_tool()],
244
+ "tool_choice": tool_choice,
245
+ }
246
+ )
247
+
248
+
249
+def test_no_newline_after_name_non_streaming() -> None:
250
+ request = _build_weather_request(tool_choice="auto")
251
+ parser = _make_parser(request)
252
+
253
+ model_output = (
254
+ "<tool_call>get_weather"
255
+ "<arg_key>city</arg_key><arg_value>Paris</arg_value>"
256
+ "</tool_call>"
257
+ )
258
+
259
+ result = parser.extract_tool_calls(model_output, request)
260
+
261
+ assert result.tools_called
262
+ assert len(result.tool_calls) == 1
263
+ assert result.tool_calls[0].function.name == "get_weather"
264
+ args = json.loads(result.tool_calls[0].function.arguments)
265
+ assert args == {"city": "Paris"}
266
+
267
+
268
+def test_newline_after_name_still_parses_non_streaming() -> None:
269
+ request = _build_weather_request(tool_choice="auto")
270
+ parser = _make_parser(request)
271
+
272
+ model_output = (
273
+ "<tool_call>get_weather\n"
274
+ "<arg_key>city</arg_key>\n<arg_value>Paris</arg_value>\n"
275
+ "</tool_call>"
276
+ )
277
+
278
+ result = parser.extract_tool_calls(model_output, request)
279
+
280
+ assert result.tools_called
281
+ assert result.tool_calls[0].function.name == "get_weather"
282
+ args = json.loads(result.tool_calls[0].function.arguments)
283
+ assert args == {"city": "Paris"}
284
+
285
+
286
+def test_no_newline_after_name_streaming() -> None:
287
+ request = _build_weather_request(tool_choice="auto")
288
+ parser = _make_parser(request)
289
+
290
+ model_output = (
291
+ "<tool_call>get_weather"
292
+ "<arg_key>city</arg_key><arg_value>Paris</arg_value>"
293
+ "</tool_call>"
294
+ )
295
+
296
+ name = ""
297
+ args = ""
298
+ prev = ""
299
+ for ch in model_output:
300
+ cur = prev + ch
301
+ delta = parser.extract_tool_calls_streaming(
302
+ previous_text=prev,
303
+ current_text=cur,
304
+ delta_text=ch,
305
+ previous_token_ids=[],
306
+ current_token_ids=[],
307
+ delta_token_ids=[],
308
+ request=request,
309
+ )
310
+ prev = cur
311
+ if delta is None:
312
+ continue
313
+ for tc in delta.tool_calls or []:
314
+ if tc.function is None:
315
+ continue
316
+ if tc.function.name:
317
+ name += tc.function.name
318
+ if tc.function.arguments:
319
+ args += tc.function.arguments
320
+
321
+ assert name == "get_weather"
322
+ assert json.loads(args) == {"city": "Paris"}
323
+
324
+
325
def _stream_partial_start_token(request: ResponsesRequest):
326
parser = _make_parser(request)
327
delta = parser.tool_call_start_token[0]
vllm/tool_parsers/poolside_v1_tool_parser.py
+1
-1
@@ -76,7 +76,7 @@ class PoolsideV1ToolParser(ToolParser):
76
77
self.func_call_regex = re.compile(r"<tool_call>.*?</tool_call>", re.DOTALL)
78
self.func_detail_regex = re.compile(
79
- r"<tool_call>([^\n]*)\n(.*)</tool_call>", re.DOTALL
79
+ r"<tool_call>\s*([^\n<]+?)\s*\n?\s*(<arg_key>.*?)?</tool_call>", re.DOTALL
80
)
81
self.func_arg_regex = re.compile(
82
r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>", re.DOTALL