From 08cf75ce4b9008933d54a648e4c533d5c27952f0 Mon Sep 17 00:00:00 2001 From: Concedo <39025047+LostRuins@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:26:20 +0800 Subject: [PATCH] jinja tool calls for qwen3.5 and glm --- koboldcpp.py | 92 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/koboldcpp.py b/koboldcpp.py index bae1e832e..648f585d5 100755 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -2928,17 +2928,78 @@ def is_ipv6_supported(): except Exception: return False -def detect_toolcall_tags(text: str): #for use with jinja tool responses, detect the tool call tag if present, we'll use that to split. - if text is None: - return None +def toolcall_to_normalized_json(text): #convert weird formats into standard tool call json text = text.strip() - m = re.match(r'<\s*([A-Za-z_][\w\-]*)\b[^>]*>', text, re.DOTALL) # match first opening tag - if not m: - return None - tag = m.group(1) - if re.search(rf'\s*$', text, re.DOTALL): # ensure the string ends with the matching closing tag - return tag - return None + def parse_qwen35(text: str) -> str: + fn_match = re.search(r"", text) + if not fn_match: + return text + fn_name = fn_match.group(1).strip() + params = {} + param_blocks = re.findall(r"(.*?)", text, re.DOTALL) + for key, value in param_blocks: + params[key.strip()] = value.strip() + return json.dumps({"name": fn_name, "arguments": params}) + def parse_glm(text: str) -> str: + text = text.strip() + # Extract function name: it's the first thing before any + fn_match = re.match(r"^\s*([^\<\s]+)", text) + if not fn_match: + return text + fn_name = fn_match.group(1).strip() + # Extract all key/value pairs + keys = re.findall(r"(.*?)", text) + values = re.findall(r"(.*?)", text) + params = {} + for i in range(min(len(keys), len(values))): + params[keys[i].strip()] = values[i].strip() + return json.dumps({"name": fn_name, "arguments": params}) + + #if we are already valid JSON, return + check_ok = extract_json_from_string(text) + if check_ok and len(check_ok)>0: + return text #is valid JSON or parsable + + # handle glm with args + if "" in text and "" in text: + return parse_glm(text) + + # handle qwen3.5 + if "", ""), + ("", ""), + ("<|tool_call_begin|>", "<|tool_call_end|>"), + ("<|tool▁call▁begin|>", "<|tool▁call▁end|>") + ] + found = False + for start, end in tcpairs: + pattern = re.escape(start) + r"(.*?)" + re.escape(end) + matches = re.findall(pattern, text, flags=re.DOTALL) + if matches: + found = True + for match in matches: + normalizedtc = toolcall_to_normalized_json(match.strip()) + sub_tool_calls = extract_json_from_string(normalizedtc) + tool_calls.extend(sub_tool_calls) + break + # fallback ONLY if no tags were found at all + if not found: + tool_calls = extract_json_from_string(text) + return tool_calls def format_jinja(messages, tools, chat_template_kwargs=None): try: @@ -4222,16 +4283,7 @@ class KcppServerRequestHandler(http.server.SimpleHTTPRequestHandler): using_openai_tools = genparams.get('using_openai_tools', False) if using_openai_tools: # first, check and potentially segment multiple tags for multi-tool calls - toolcalltagfound = detect_toolcall_tags(recvtxt) - if not toolcalltagfound: - tool_calls = extract_json_from_string(recvtxt) - else: # we found tool call tags, split to extract all internal stuff - splitting_str = recvtxt.replace(f"<{toolcalltagfound}>", "").replace(f"", "") - chunks = [x.strip() for x in splitting_str.split("") if x] - chunks = [x for x in chunks if x] - for chunk in chunks: #for each potential toolcall, add it to the pile - sub_tool_calls = extract_json_from_string(chunk) - tool_calls.extend(sub_tool_calls) + tool_calls = repack_toolcall_tags(recvtxt) if tool_calls and len(tool_calls)>0: tool_calls = [normalize_tool_call(obj) for obj in tool_calls] for tc in tool_calls: