# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Strict-mode (Auto-Heal disabled) tool-call parsing.
With ``allow_incomplete=False`` the parser must accept a well-formed
``...`` call even when the model appends prose
after the closing tag -- matching the JSON-style ``...`` path,
which already tolerates trailing text -- while still rejecting genuinely
truncated calls that never close.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
if _BACKEND_DIR not in sys.path:
sys.path.insert(0, _BACKEND_DIR)
from core.inference.tool_call_parser import parse_tool_calls_from_text
def _only(text: str) -> dict:
calls = parse_tool_calls_from_text(text, allow_incomplete = False)
assert len(calls) == 1, f"expected exactly one call, got {len(calls)}: {calls!r}"
fn = calls[0]["function"]
return {"name": fn["name"], "arguments": json.loads(fn["arguments"])}
class TestFunctionStyleTrailingText:
def test_closed_function_with_trailing_prose_is_accepted(self):
text = (
"weather london"
" Let me check that for you."
)
call = _only(text)
assert call == {"name": "web_search", "arguments": {"query": "weather london"}}
def test_closed_function_with_trailing_whitespace_is_accepted(self):
text = "cats \n\n"
call = _only(text)
assert call == {"name": "web_search", "arguments": {"query": "cats"}}
def test_closed_function_without_trailing_text_still_parses(self):
text = "cats"
call = _only(text)
assert call == {"name": "web_search", "arguments": {"query": "cats"}}
def test_multi_param_with_trailing_prose(self):
text = (
"ls -la"
"home running it now"
)
call = _only(text)
assert call == {
"name": "terminal",
"arguments": {"command": "ls -la", "workdir": "home"},
}
def test_code_value_containing_literal_close_tag_is_preserved(self):
# The real closing is the last one; the literal inside
# the code argument must survive (rfind, not the first match).
text = (
""
'print("")'
" all done"
)
call = _only(text)
assert call == {"name": "python", "arguments": {"code": 'print("")'}}
def test_incomplete_function_without_close_is_still_rejected(self):
text = "weather london"
assert parse_tool_calls_from_text(text, allow_incomplete = False) == []
def test_param_without_close_tag_is_rejected_in_strict_mode(self):
# Closing present, but the single parameter never closes.
text = "weather london"
assert parse_tool_calls_from_text(text, allow_incomplete = False) == []
class TestParityWithJsonStyle:
def test_json_tool_call_with_trailing_prose_is_accepted(self):
text = (
'{"name":"web_search","arguments":{"query":"weather london"}}'
" Let me check that for you."
)
calls = parse_tool_calls_from_text(text, allow_incomplete = False)
assert len(calls) == 1
assert calls[0]["function"]["name"] == "web_search"
def test_function_and_json_styles_agree_on_trailing_text(self):
q = "weather london"
func = parse_tool_calls_from_text(
f"{q} trailing",
allow_incomplete = False,
)
js = parse_tool_calls_from_text(
f'{{"name":"web_search","arguments":{{"query":"{q}"}}}} trailing',
allow_incomplete = False,
)
assert len(func) == len(js) == 1
assert json.loads(func[0]["function"]["arguments"]) == {"query": q}
assert json.loads(js[0]["function"]["arguments"]) == {"query": q}
class TestHealingPathUnaffected:
def test_auto_heal_still_repairs_unclosed_function(self):
text = "cats"
calls = parse_tool_calls_from_text(text, allow_incomplete = True)
assert len(calls) == 1
assert calls[0]["function"]["name"] == "web_search"