"""Nemotron Cascade-2 tool protocol (XML in ChatML content). Tercet-R is trained on `nvidia/Nemotron-Cascade-2-SFT-Data`, which inlines available tools, calls, and results as `` / `` / `` in message text. That is not OpenAI `tool_calls` JSON and not this tokenizer's unused `<|tool_call|>` / `<|tool_response|>` specials. This module matches NVIDIA's Cascade-2 chat template: https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/blob/main/chat_template.jinja """ from __future__ import annotations import json import re import sys from dataclasses import dataclass from typing import Any TOOL_CALL_OPEN = "" TOOL_CALL_CLOSE = "" TOOL_RESPONSE_OPEN = "" TOOL_RESPONSE_CLOSE = "" TOOLS_OPEN = "" TOOLS_CLOSE = "" THINK_OPEN = "" THINK_CLOSE = "" CHATML_START_RE = re.compile(r"^<\|im_start\|>[A-Za-z]+\n") CHATML_END_RE = re.compile(r"\n?<\|im_end\|>\s*$") TOOL_CALL_BLOCK_RE = re.compile( rf"{re.escape(TOOL_CALL_OPEN)}(.*?){re.escape(TOOL_CALL_CLOSE)}", re.DOTALL, ) FUNCTION_BLOCK_RE = re.compile( r"\s]+)>(.*?)", re.DOTALL, ) PARAMETER_BLOCK_RE = re.compile( r"\s]+)>\n?(.*?)\n?", re.DOTALL, ) FUNCTION_NAME_RE = re.compile(r"\s]+)") INFERENCE_ROLE_MAP = { "system": "system", "user": "user", "human": "user", "assistant": "assistant", "gpt": "assistant", "tool": "tool", "function": "tool", } TOOLS_PREAMBLE = "# Tools\n\nYou have access to the following functions:\n\n" TOOL_CALL_INSTRUCTIONS = ( "\n\nIf you choose to call a function ONLY reply in the following format " "with NO suffix:\n\n" "\n" "\n" "\n" "value_1\n" "\n" "\n" "This is the value for the second parameter\n" "that can span\n" "multiple lines\n" "\n" "\n" "\n\n" "\n" "Reminder:\n" "- Function calls MUST follow the specified format: an inner " " block must be nested within " " XML tags\n" "- Required parameters MUST be specified\n" "- You may provide optional reasoning for your function call in natural " "language BEFORE the function call, but NOT after\n" "- If there is no function call available, answer the question like " "normal with your current knowledge and do not tell the user about " "function calls\n" "" ) @dataclass(frozen=True) class ParsedToolCall: name: str arguments: dict[str, Any] raw: str def message_text(raw_content: Any) -> str: if raw_content is None: return "" if isinstance(raw_content, str): return raw_content if isinstance(raw_content, list): parts: list[str] = [] for item in raw_content: if isinstance(item, str): parts.append(item) continue if not isinstance(item, dict): continue part_type = item.get("type") if part_type in {None, "text", "input_text", "output_text"}: text = item.get("text") if isinstance(text, str): parts.append(text) return "".join(parts) return str(raw_content) def strip_leaked_chatml(content: str) -> str: text = content.strip() while True: match = CHATML_START_RE.match(text) if match is None: break text = text[match.end() :] text = CHATML_END_RE.sub("", text) return text.strip() def has_tools_block(content: str) -> bool: return TOOLS_OPEN in content def wrap_tool_response(content: str) -> str: text = strip_leaked_chatml(content) if TOOL_RESPONSE_OPEN in text: return text return f"{TOOL_RESPONSE_OPEN}\n\n\n{text}\n{TOOL_RESPONSE_CLOSE}" def _xml_value(value: Any) -> str: if isinstance(value, dict) or ( isinstance(value, (list, tuple)) and not isinstance(value, (str, bytes)) ): return json.dumps(value, ensure_ascii=False) if value is True or value is False or value is None: return str(value) return str(value) def _render_extra_keys(payload: dict[str, Any], handled: set[str]) -> str: chunks: list[str] = [] for key, value in payload.items(): if key in handled: continue chunks.append(f"\n<{key}>{_xml_value(value)}") return "".join(chunks) def _unwrap_tool(raw_tool: Any) -> dict[str, Any]: if not isinstance(raw_tool, dict): raise ValueError("Each tool must be an object") if isinstance(raw_tool.get("function"), dict): tool = dict(raw_tool["function"]) else: tool = dict(raw_tool) name = tool.get("name") if not isinstance(name, str) or not name.strip(): raise ValueError("Tool is missing a function name") tool["name"] = name.strip() return tool def coerce_tools(raw_tools: Any) -> list[dict[str, Any]]: if raw_tools is None: return [] if isinstance(raw_tools, str): text = raw_tools.strip() if not text: return [] raw_tools = loads_json(text) if isinstance(raw_tools, dict): raw_tools = [raw_tools] if not isinstance(raw_tools, list): raise ValueError("tools must be a list of function specs") return [_unwrap_tool(item) for item in raw_tools] def render_function_schema(tool: dict[str, Any]) -> str: chunks = [f"\n\n{tool['name']}"] description = tool.get("description") if isinstance(description, str) and description.strip(): chunks.append(f"\n{description.strip()}") chunks.append("\n") parameters = tool.get("parameters") properties: dict[str, Any] = {} if isinstance(parameters, dict): raw_properties = parameters.get("properties") if isinstance(raw_properties, dict): properties = raw_properties for param_name, raw_fields in properties.items(): fields = raw_fields if isinstance(raw_fields, dict) else {} chunks.append("\n") chunks.append(f"\n{param_name}") if "type" in fields: chunks.append(f"\n{_xml_value(fields['type'])}") if isinstance(fields.get("description"), str) and fields["description"].strip(): chunks.append( f"\n{fields['description'].strip()}" ) if "enum" in fields: chunks.append(f"\n{_xml_value(fields['enum'])}") chunks.append( _render_extra_keys( fields, {"name", "type", "description", "enum"}, ) ) chunks.append("\n") chunks.append( _render_extra_keys(parameters, {"type", "properties", "required"}) ) if "required" in parameters: chunks.append(f"\n{_xml_value(parameters['required'])}") chunks.append("\n") chunks.append( _render_extra_keys( tool, {"type", "name", "description", "parameters"}, ) ) chunks.append("\n") return "".join(chunks) def render_available_tools(raw_tools: Any) -> str: tools = coerce_tools(raw_tools) if not tools: return "" body = "".join(render_function_schema(tool) for tool in tools) return ( f"{TOOLS_PREAMBLE}{TOOLS_OPEN}{body}\n{TOOLS_CLOSE}" f"{TOOL_CALL_INSTRUCTIONS}" ) def inject_available_tools(system_content: str, raw_tools: Any) -> str: block = render_available_tools(raw_tools) if not block: return system_content if has_tools_block(system_content): return system_content if not system_content.strip(): return block return f"{system_content.rstrip()}\n\n{block}" def parse_json_int(text: str) -> int | str: """Keep oversized JSON integers as strings. Python 3.12 refuses to convert integers longer than ``sys.get_int_max_str_digits()`` (default 4300). Tool-call payloads sometimes embed hashes or blobs as bare JSON numbers; those must not abort packing. """ digits = text.lstrip("+-") limit = sys.get_int_max_str_digits() if limit and len(digits) > limit: return text return int(text) def loads_json(text: str) -> Any: return json.loads(text, parse_int=parse_json_int) # Models sometimes close a JSON string with `".}` instead of `"}`. _TRAILING_STRING_PERIOD_RE = re.compile(r'"\s*\.(?=\s*[}\],])') _TRAILING_COMMA_RE = re.compile(r",\s*(?=[}\]])") _JSON_NAME_RE = re.compile(r'"name"\s*:\s*"([^"]+)"') _JSON_QUERY_RE = re.compile(r'"query"\s*:\s*"([^"]*)"') def repair_jsonish(text: str) -> str: repaired = _TRAILING_STRING_PERIOD_RE.sub('"', text.strip()) return _TRAILING_COMMA_RE.sub("", repaired) def loads_jsonish(text: str) -> Any: candidates = (text.strip(), repair_jsonish(text), text.strip().replace("'", '"')) seen: set[str] = set() last_error: Exception | None = None for candidate in candidates: if not candidate or candidate in seen: continue seen.add(candidate) try: return loads_json(candidate) except (json.JSONDecodeError, ValueError, RecursionError) as error: last_error = error if last_error is not None: raise last_error raise json.JSONDecodeError("Empty JSON", text, 0) def extract_json_tool_fields(text: str) -> tuple[str, dict[str, Any]] | None: name_match = _JSON_NAME_RE.search(text) if name_match is None: return None arguments: dict[str, Any] = {} query_match = _JSON_QUERY_RE.search(text) if query_match is not None: arguments["query"] = query_match.group(1) return name_match.group(1).strip(), arguments def _json_ready_int(value: int) -> int | str: try: json.dumps(value) except ValueError: previous = sys.get_int_max_str_digits() sys.set_int_max_str_digits(0) try: return str(value) finally: sys.set_int_max_str_digits(previous) return value def json_ready(value: Any) -> Any: """Coerce Python literals into JSON-serialisable values. Some SFT sources store tool arguments as Python literals. `ast.literal_eval` turns `{1, 2}` into a `set` and `...` into `Ellipsis`, which later `json.dumps` calls reject. Oversized ints are stored as decimal strings. """ if value is None or isinstance(value, (bool, float, str)): return value if isinstance(value, int): return _json_ready_int(value) if value is Ellipsis: return None if isinstance(value, dict): return {str(key): json_ready(item) for key, item in value.items()} if isinstance(value, (set, frozenset)): items = [json_ready(item) for item in value] try: return sorted( items, key=lambda item: json.dumps(item, sort_keys=True, default=str), ) except TypeError: return items if isinstance(value, tuple): return [json_ready(item) for item in value] if isinstance(value, list): return [json_ready(item) for item in value] if isinstance(value, bytes): try: return value.decode("utf-8") except UnicodeDecodeError: return list(value) try: json.dumps(value) return value except (TypeError, ValueError): return str(value) def parse_argument_value(raw: str) -> Any: text = raw.strip() if not text: return "" try: return loads_json(text) except (json.JSONDecodeError, ValueError, RecursionError): return text def parse_arguments_payload(raw: Any) -> dict[str, Any]: if raw is None: return {} ready = json_ready(raw) if isinstance(ready, dict): return ready if isinstance(ready, str): text = ready.strip() if not text: return {} try: loaded = loads_json(text) except (json.JSONDecodeError, ValueError, RecursionError): return {"value": ready} loaded = json_ready(loaded) if isinstance(loaded, dict): return loaded return {"value": loaded} return {"value": ready} def format_tool_call_xml(name: str, arguments: dict[str, Any]) -> str: chunks = [f"{TOOL_CALL_OPEN}\n\n"] for key, value in arguments.items(): chunks.append(f"\n{_xml_value(value)}\n\n") chunks.append(f"\n{TOOL_CALL_CLOSE}\n") return "".join(chunks) def format_tool_calls_xml(raw_tool_calls: Any) -> str: if not raw_tool_calls: return "" if not isinstance(raw_tool_calls, list): raise ValueError("tool_calls must be a list") chunks: list[str] = [] for raw_call in raw_tool_calls: if not isinstance(raw_call, dict): raise ValueError("Each tool_call must be an object") payload = raw_call.get("function") if isinstance(raw_call.get("function"), dict) else raw_call if not isinstance(payload, dict): raise ValueError("tool_call is missing a function object") name = payload.get("name") if not isinstance(name, str) or not name.strip(): raise ValueError("tool_call is missing a function name") arguments = parse_arguments_payload(payload.get("arguments")) chunks.append(format_tool_call_xml(name.strip(), arguments)) return "".join(chunks) def parse_json_tool_inner(inner: str) -> ParsedToolCall | None: text = inner.strip() if not text: return None payload: Any | None try: payload = loads_jsonish(text) except (json.JSONDecodeError, ValueError, RecursionError): payload = None if isinstance(payload, dict): nested = payload.get("function") source = nested if isinstance(nested, dict) else payload name = source.get("name") if not isinstance(name, str) or not name.strip(): name = payload.get("name") if isinstance(name, str) and name.strip(): arguments = parse_arguments_payload( source.get( "arguments", source.get("parameters", payload.get("arguments")), ) ) return ParsedToolCall(name=name.strip(), arguments=arguments, raw="") extracted = extract_json_tool_fields(repair_jsonish(text)) if extracted is None: return None name, arguments = extracted return ParsedToolCall(name=name, arguments=arguments, raw="") def parse_tool_calls(text: str) -> list[ParsedToolCall]: calls: list[ParsedToolCall] = [] for block in TOOL_CALL_BLOCK_RE.finditer(text): inner = block.group(1) raw = block.group(0).strip() found_xml = False for function in FUNCTION_BLOCK_RE.finditer(inner): found_xml = True name = function.group(1).strip() arguments: dict[str, Any] = {} for parameter in PARAMETER_BLOCK_RE.finditer(function.group(2)): arguments[parameter.group(1).strip()] = parse_argument_value( parameter.group(2) ) calls.append( ParsedToolCall( name=name, arguments=json_ready(arguments), raw=raw, ) ) if found_xml: continue parsed = parse_json_tool_inner(inner) if parsed is not None: calls.append( ParsedToolCall(name=parsed.name, arguments=parsed.arguments, raw=raw) ) return calls def openai_tool_calls_from_text(text: str) -> list[dict[str, Any]]: encoded: list[dict[str, Any]] = [] for index, call in enumerate(parse_tool_calls(text)): encoded.append( { "id": f"call_{index}_{call.name}", "type": "function", "function": { "name": call.name, "arguments": json.dumps(call.arguments, ensure_ascii=False), }, } ) return encoded def assistant_message_content(message: dict[str, Any]) -> str: reasoning = message.get("reasoning_content") content = message_text(message.get("content")) if isinstance(reasoning, str) and reasoning.strip(): content = f"{THINK_OPEN}\n{reasoning.strip()}\n{THINK_CLOSE}\n{content}" tool_xml = format_tool_calls_xml(message.get("tool_calls")) if tool_xml: if content.strip(): return f"{content.rstrip()}\n{tool_xml}" return tool_xml return content def _flush_tool_group( group: list[str], messages: list[dict[str, str]], ) -> None: if not group: return messages.append({"role": "user", "content": "\n".join(group)}) group.clear() def prepare_inference_messages( raw_messages: list[dict[str, Any]], *, tools: Any | None = None, ) -> list[dict[str, str]]: if not raw_messages: raise ValueError("Chat history cannot be empty") prepared: list[dict[str, str]] = [] pending_tool_results: list[str] = [] for index, raw_message in enumerate(raw_messages): if not isinstance(raw_message, dict): raise ValueError(f"Unsupported chat message at index {index}") raw_role = raw_message.get("role") if not isinstance(raw_role, str): raise ValueError(f"Unsupported chat role at index {index}: {raw_role!r}") role = INFERENCE_ROLE_MAP.get(raw_role.strip().lower()) if role is None: raise ValueError(f"Unsupported chat role at index {index}: {raw_role!r}") if role == "assistant": content = assistant_message_content(raw_message) else: content = message_text(raw_message.get("content")) if role == "tool": content = wrap_tool_response(content) if not content.strip(): raise ValueError(f"Chat content at index {index} must be non-empty") pending_tool_results.append(content) continue _flush_tool_group(pending_tool_results, prepared) if not content.strip(): if role == "system": continue raise ValueError(f"Chat content at index {index} must be non-empty") if role == "system": content = strip_leaked_chatml(content) prepared.append({"role": role, "content": content}) _flush_tool_group(pending_tool_results, prepared) tools_block = render_available_tools(tools) if tools_block: if prepared and prepared[0]["role"] == "system": prepared[0] = { "role": "system", "content": inject_available_tools(prepared[0]["content"], tools), } else: prepared.insert(0, {"role": "system", "content": tools_block}) if not prepared: raise ValueError("Chat history cannot be empty") return prepared