Skip to content

Expose standalone tool healing utilities#5583

Merged
danielhanchen merged 1 commit into
unslothai:mainfrom
alkinun:feature/tool-healing-core
May 19, 2026
Merged

Expose standalone tool healing utilities#5583
danielhanchen merged 1 commit into
unslothai:mainfrom
alkinun:feature/tool-healing-core

Conversation

@alkinun

@alkinun alkinun commented May 18, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Added a lightweight unsloth_core.tool_healing module for parsing and stripping model-emitted tool-call markup without importing the training stack.
  • Added compatibility exports under unsloth.core.tool_healing for existing Unsloth import paths.
  • Updated Studio llama.cpp inference to use the shared helpers when available.
  • Added a backend-local fallback so studio/backend remains importable when launched as a standalone tree without the repo root on PYTHONPATH.
  • Added regression tests for JSON tool calls, Ollama-style keys, function-style markup, no-stack imports, and standalone Studio backend imports.
  • Updated the Studio Playwright default-model expectation to match the current DEFAULT_MODELS_GGUF[0] (unsloth/Qwen3.6-27B-MTP-GGUF).

Why

External inference servers can reuse tool healing without importing torch, transformers, or the full Unsloth training runtime.

Closes #5502

Tests

  • python -m pytest tests/python/test_tool_healing_public_api.py -q
  • python - <<'PY' ... assert EXPECTED_DEFAULT == DEFAULT_MODELS_GGUF[0] ... PY
  • python -m py_compile unsloth_core/tool_healing.py unsloth_core/__init__.py unsloth/core/tool_healing.py unsloth/core/__init__.py studio/backend/core/tool_healing.py studio/backend/core/inference/llama_cpp.py tests/python/test_tool_healing_public_api.py tests/studio/playwright_chat_ui.py

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@alkinun alkinun force-pushed the feature/tool-healing-core branch from 59e03f6 to f95db43 Compare May 18, 2026 17:55
@alkinun

alkinun commented May 18, 2026

Copy link
Copy Markdown
Collaborator Author

Supersedes #5571. The branch has been rebased onto current main and squashed to one clean commit.

@alkinun alkinun force-pushed the feature/tool-healing-core branch from 08e1efc to 11288f4 Compare May 18, 2026 20:05
@alkinun alkinun changed the title feature/tool-healing-core: Expose tool healing utilities Expose standalone tool healing utilities May 18, 2026
@alkinun alkinun force-pushed the feature/tool-healing-core branch from 11288f4 to 6d4e358 Compare May 18, 2026 20:16
@danielhanchen danielhanchen force-pushed the feature/tool-healing-core branch from 6d4e358 to c5c7d87 Compare May 19, 2026 08:08
@danielhanchen

Copy link
Copy Markdown
Member

Pushed a follow-up commit on top of your branch addressing the review:

  1. Rebased onto current main. The playwright_chat_ui.py hunk in this PR landed at roughly the same time as studio: read Playwright default model from defaults.py without importing it #5595, which added an ast.literal_eval helper for exactly the no-torch / no-structlog Playwright lane. Resolved the conflict by keeping studio: read Playwright default model from defaults.py without importing it #5595's helper rather than reintroducing the hardcoded literal.
  2. tests/python/test_tool_healing_public_api.py
    • The test_studio_backend_llama_cpp_imports_without_unsloth_core test imported core.inference.llama_cpp, which transitively pulls structlog via core/inference/__init__.py -> orchestrator. That made the test fail in any minimal CI lane. Renamed to test_studio_backend_local_tool_healing_fallback and switched to importing core.tool_healing directly. That tests the actual claim (the fallback module works when unsloth_core is blocked) without dragging in the orchestrator.
    • Replaced cwd="/tmp" with tempfile.gettempdir() so the test runs on Windows.
  3. studio/backend/core/tool_healing.py now mirrors heal_tool_call_content. The fallback's public surface is identical to the canonical module so anyone reaching the fallback path does not silently lose that helper.
  4. tests/python/test_tool_healing_duplicates_in_sync.py pins TOOL_CALL_MARKUP_SIGNALS, parse_tool_calls_from_text, strip_tool_call_markup, and heal_tool_call_content outputs between the canonical and fallback modules so the duplicate cannot drift.
  5. pyproject.toml: dropped the redundant "unsloth_core*" from setuptools.packages.find since the existing "unsloth*" glob already matches it.

One thing worth calling out in the description: I ran the new module against the previous inline parser in studio/backend/core/inference/llama_cpp.py on a corpus of 40 inputs. 31 are bit-for-bit identical; the other 9 are improvements (the old code emitted arguments as raw None/list/int/bool in some paths, which violates the OpenAI Chat Completions spec, and it appended tool calls with empty/null name). The Ollama-style tool_name / tool_arguments support is the documented new behaviour. None of the diffs are regressions, but it is worth a sentence in the PR body so consumers know to expect strictly-string arguments and that empty/null-name calls are now dropped instead of forwarded.

Local run (Python 3.14, no torch / transformers / structlog): all 11 tests pass.

@alkinun

alkinun commented May 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks!

@danielhanchen danielhanchen force-pushed the feature/tool-healing-core branch from 798f5ef to bc38756 Compare May 19, 2026 11:00
@danielhanchen

Copy link
Copy Markdown
Member

Reworked and squashed the branch to a single commit.

Dropping the new top-level unsloth_core namespace was the main goal here. Sibling packages we ship live under unsloth-zoo or under studio.backend.*; introducing a brand new top-level Python package on top of that just for one stdlib helper file is more surface area than it is worth, especially when the helper is conceptually a piece of llama.cpp inference logic.

The new home is studio/backend/core/tool_healing.py. The full import chain studio.backend.core is already stdlib-only at import time (the __init__.py there is lazy via __getattr__), so from studio.backend.core.tool_healing import parse_tool_calls_from_text, strip_tool_call_markup pulls zero heavy modules and satisfies #5502 without the new package.

What this commit does:

  1. Verbatim extraction. The regexes (_TOOL_CLOSED_PATS, _TOOL_ALL_PATS, _TC_JSON_START_RE, _TC_FUNC_START_RE, _TC_END_TAG_RE, _TC_FUNC_CLOSE_RE, _TC_PARAM_START_RE, _TC_PARAM_CLOSE_RE) and the parser / stripper bodies are byte-for-byte identical to the inline code that previously lived in llama_cpp.py. The 9 behavioural deltas the previous revision introduced (Ollama-style tool_name / tool_arguments keys, JSON-stringify-any-non-string arguments, empty/null-name drop, heal_tool_call_content) are all reverted. This PR is a pure code move; no behaviour change.
  2. llama_cpp.py consumes the helpers via a normal relative import. _parse_tool_calls_from_text becomes a one-line delegate; the _strip_tool_markup closure keeps the if not auto_heal_tool_calls: return text fast path and delegates the substantive work. The inline _TOOL_XML_SIGNALS = ("<tool_call>", "<function=") if auto_heal_tool_calls else () and the any(s in ... for s in _TOOL_XML_SIGNALS) guards stay exactly as they were on main.
  3. AST-deterministic verification test. tests/python/test_tool_healing_extraction_is_exact.py asserts the regex literals are byte-identical to the OLD inline source via direct re.compile(...) equality, asserts the function shapes via ast.parse walks, and runs a 16-input corpus through both the extracted helper and an in-test reference implementation copied verbatim from the OLD source. Any future edit must keep them in lock-step or this test fails. 5/5 tests pass on Python 3.14 in a minimal env with no torch / transformers / structlog / httpx / numpy installed.
  4. Cleanup. unsloth_core/, unsloth/core/, tests/python/test_tool_healing_public_api.py, and tests/python/test_tool_healing_duplicates_in_sync.py are gone. The redundant "unsloth_core*" glob in pyproject.toml is gone too. The Playwright change from the original branch was dropped during the rebase (it would have undone studio: read Playwright default model from defaults.py without importing it #5595).

Final diff: 3 files, +485 / -136. PR is single-commit and mergeable.

Move the inline tool-call XML parser and stripper out of
studio/backend/core/inference/llama_cpp.py into a new
studio/backend/core/tool_healing.py so external inference servers
(llama-server wrappers, llama-swap, custom shims) can reuse the same
logic without importing the inference orchestrator, structlog, httpx,
or anything from torch / transformers / unsloth.

Closes unslothai#5502.

What this PR does:

- New file studio/backend/core/tool_healing.py contains the regex
  constants (_TOOL_CLOSED_PATS, _TOOL_ALL_PATS, _TC_JSON_START_RE,
  _TC_FUNC_START_RE, _TC_END_TAG_RE, _TC_FUNC_CLOSE_RE,
  _TC_PARAM_START_RE, _TC_PARAM_CLOSE_RE), parse_tool_calls_from_text,
  and strip_tool_call_markup. The regexes and function bodies are
  byte-for-byte the same as the previous inline implementation in
  llama_cpp.py; only the @staticmethod decorator and the closure-only
  `if not auto_heal_tool_calls: return text` short-circuit are dropped
  (the latter stays in the caller as a fast path when healing is off).
- studio/backend/core/inference/llama_cpp.py now imports the regexes
  and helpers from .tool_healing. LlamaCppBackend._parse_tool_calls_from_text
  becomes a one-line delegate; the _strip_tool_markup closure keeps the
  auto_heal_tool_calls fast path and delegates the work.
- Helper module imports cleanly without torch, transformers, structlog,
  httpx, or numpy. studio.backend.core itself is already stdlib-only
  at import time (lazy __getattr__), so `from
  studio.backend.core.tool_healing import parse_tool_calls_from_text,
  strip_tool_call_markup` is the lightweight import path issue unslothai#5502
  asked for.

No behaviour change for existing Studio paths. parse_tool_calls_from_text
and strip_tool_call_markup produce the same OpenAI-shape output the
old inline code produced for every input.

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
@danielhanchen danielhanchen force-pushed the feature/tool-healing-core branch from c43537f to 1e75cfb Compare May 19, 2026 12:00
@danielhanchen

Copy link
Copy Markdown
Member

Thanks a lot! I removed unsloth_core since it'll sadly cause more folders / files - folks can still now use the tool healing though from a separate file

@danielhanchen danielhanchen merged commit f747108 into unslothai:main May 19, 2026
4 of 27 checks passed
rsd-darshan pushed a commit to rsd-darshan/unsloth that referenced this pull request Jun 3, 2026
…nslothai#5583)

Move the inline tool-call XML parser and stripper out of
studio/backend/core/inference/llama_cpp.py into a new
studio/backend/core/tool_healing.py so external inference servers
(llama-server wrappers, llama-swap, custom shims) can reuse the same
logic without importing the inference orchestrator, structlog, httpx,
or anything from torch / transformers / unsloth.

Closes unslothai#5502.

What this PR does:

- New file studio/backend/core/tool_healing.py contains the regex
  constants (_TOOL_CLOSED_PATS, _TOOL_ALL_PATS, _TC_JSON_START_RE,
  _TC_FUNC_START_RE, _TC_END_TAG_RE, _TC_FUNC_CLOSE_RE,
  _TC_PARAM_START_RE, _TC_PARAM_CLOSE_RE), parse_tool_calls_from_text,
  and strip_tool_call_markup. The regexes and function bodies are
  byte-for-byte the same as the previous inline implementation in
  llama_cpp.py; only the @staticmethod decorator and the closure-only
  `if not auto_heal_tool_calls: return text` short-circuit are dropped
  (the latter stays in the caller as a fast path when healing is off).
- studio/backend/core/inference/llama_cpp.py now imports the regexes
  and helpers from .tool_healing. LlamaCppBackend._parse_tool_calls_from_text
  becomes a one-line delegate; the _strip_tool_markup closure keeps the
  auto_heal_tool_calls fast path and delegates the work.
- Helper module imports cleanly without torch, transformers, structlog,
  httpx, or numpy. studio.backend.core itself is already stdlib-only
  at import time (lazy __getattr__), so `from
  studio.backend.core.tool_healing import parse_tool_calls_from_text,
  strip_tool_call_markup` is the lightweight import path issue unslothai#5502
  asked for.

No behaviour change for existing Studio paths. parse_tool_calls_from_text
and strip_tool_call_markup produce the same OpenAI-shape output the
old inline code produced for every input.

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Decouple Tool Healing from Unsloth for use in external inference servers

2 participants