Commit graph

2,643 commits

Author SHA1 Message Date
Daniel Han
2e75b0131c Merge remote-tracking branch 'origin/main' into fold-integration
# Conflicts:
#	scripts/scan_packages_baseline.json
2026-07-07 01:46:07 +00:00
Daniel Han
660ef9aab7 Reconcile stale tests after stack fold: typed resolved-field roundtrip; drop dense-fbcache test superseded by video auto-quant 2026-07-07 01:32:08 +00:00
Daniel Han
de099eaecd Merge remote-tracking branch 'origin/video-hunyuan-gate' into fold-integration
# Conflicts:
#	studio/backend/core/inference/video.py
#	studio/backend/routes/models.py
#	studio/backend/tests/test_cached_gguf_routes.py
#	studio/frontend/src/features/images/images-page.tsx
#	studio/frontend/src/features/images/train/diffusion-train-panel.tsx
2026-07-07 01:15:20 +00:00
Daniel Han
c7822fa728 Merge remote-tracking branch 'origin/video-wan' into fold-integration 2026-07-07 01:08:50 +00:00
Daniel Han
341b876212 Merge remote-tracking branch 'origin/video-tab' into fold-integration 2026-07-07 01:08:50 +00:00
Daniel Han
590e1171e0 Merge remote-tracking branch 'origin/video-inference' into fold-integration 2026-07-07 01:08:50 +00:00
Daniel Han
6edb00aa34 Merge remote-tracking branch 'origin/diffusion-more-families' into fold-integration 2026-07-07 01:08:50 +00:00
Daniel Han
2e855a018d Merge remote-tracking branch 'origin/diffusion-auto-badges' into fold-integration
# Conflicts:
#	studio/backend/models/inference.py
#	studio/frontend/src/features/images/images-page.tsx
2026-07-07 01:08:43 +00:00
Daniel Han
d1fbe62aeb Merge remote-tracking branch 'origin/diffusion-auto-install' into fold-integration 2026-07-07 01:06:57 +00:00
Daniel Han
1081808ae7 Merge remote-tracking branch 'origin/diffusion-fp16-accum' into fold-integration 2026-07-07 01:06:57 +00:00
Daniel Han
826a31d7b6 Merge remote-tracking branch 'origin/diffusion-auto-policy' into fold-integration 2026-07-07 01:06:57 +00:00
Daniel Han
e1dd2dda6b Merge remote-tracking branch 'origin/diffusion-train-perf2' into fold-integration
# Conflicts:
#	studio/backend/core/training/diffusion_dit_trainer.py
#	studio/backend/core/training/diffusion_train_common.py
2026-07-07 01:06:42 +00:00
Daniel Han
2dbfd3c4a9 Merge remote-tracking branch 'origin/diffusion-krea2' into fold-integration
# Conflicts:
#	studio/backend/core/training/diffusion_train_common.py
2026-07-07 01:03:21 +00:00
Daniel Han
b52e7a5cc2 Merge remote-tracking branch 'origin/diffusion-train-tab-2' into fold-integration 2026-07-07 01:01:00 +00:00
Daniel Han
186f381bc6
Studio: diffusion UX polish and stronger auto policies (images + video) (#6885)
* Auto policies: deferred dense compile, video compile default, step cache and precision auto

Image dense loads with speed unset no longer sit at plain off: the load stays
bit-identical eager, and the 3rd generation in a session engages the default
compile profile plus the cuDNN attention upgrade mid-session (a one-off image
never pays the warmup, repeated use amortises it). Video dense loads resolve
straight to the default profile since a clip denoise amortises the compile
within a single run, and never to max.

Video also gains the image backend's tri-state auto policies: unset step cache
now decides from the default schedule and re-checks the actual step count per
generation, and unset precision (transformer_quant) hands the decision to the
hardware ladder instead of staying off. Memory badge reason now says plainly
that everything fits when no offload is planned.

* Rename Dtype to Precision, add the video Precision control, step cache Auto option

The images Advanced panel's Dtype row is now Precision (same control, clearer
name), and the video Advanced panel gains the matching Precision select wired
to the load route's existing transformer_quant field, gated to full-pipeline
loads the way the image control gates to GGUF. Step cache selects on both
pages gain an explicit Auto option as the default (the previous Off default
silently behaved as auto and never let anyone pin off), and the Speed and
Attention tooltips now state the deferred dense compile and the SageAttention
black-frame caveat.

* Model catalog: canonical diffusion model groups with device-aware routing

One canonical name per image/video model, its published artifacts (GGUF, FP8,
bnb-4bit, official BF16) as data, and pure routing helpers: suffix-stripped
canonical keys (owner-preserving; cross-owner merges only via explicit
aliases), group/artifact lookups, a flat back-compat options shim, load-spec
resolution replacing the pages' lookup tables, search matching over old ids
and format tokens, the GGUF fit ladder extracted from the variant expander,
and pickDefaultArtifact/pickDefaultQuant deciding what a bare group click
loads (downloaded first, then the best quality that fits 70 percent of VRAM,
GGUF as the safe fallback). Checked by npm run catalog:check, following the
i18n:check pattern.

* Picker: one canonical row per diffusion model with a format second level

The Images and Video pickers now render the curated catalog as one row per
model in Recommended: clicking loads the best artifact for the device (the
routed GGUF quant, a prequant FP8/bnb-4bit that fits, or the official BF16),
and a chevron opens the per-format list, with the GGUF row nesting the usual
quant expander. Live HF listing rows that belong to a group are deduplicated,
search collapses member repos into their group (old ids and format tokens
still match), and the On Device sections group cached member repos under the
same canonical name with the per-repo rows inside. Curated groups render from
the catalog rather than the HF listing, which finally surfaces LTX-2.3 in the
video Recommended list (its hub pipeline_tag is image-to-video, so the
text-to-video listing always missed it) and exposes the HunyuanVideo 720p
repack next to 480p.

Backend: /cached-models now tags trusted video-family repos text-to-video
instead of blanket text-to-image, and the pickers admit catalog-known
non-unsloth repos On Device, so cached Lightricks/Wan/Hunyuan pipelines
finally appear in the Video picker. Chat pickers pass no catalog and are
unchanged.

* Download formats, tab icons, plain-language train tips, 3-loop autoplay

The image Download button becomes a menu: PNG saves the original bytes with
the embedded recipe, JPEG and WebP re-encode client-side from the fetched
blob (JPEG flattened onto white). The video Download button gains MP4
(original, keeps audio), WebM and GIF; the latter two transcode server-side
from the stored MP4 via PyAV (VP9 realtime profile for WebM, ~12 fps adaptive
palette for GIF) behind a new gallery export route that 501s with a readable
message when a codec is missing.

Generated clips no longer loop forever: the player replays a clip three times
per selection, then pauses with controls up; a new generation or a refresh
gets its own three plays. The Create/Train tabs reuse the sidebar's New Chat
and Train icons (TestTubeOutlineIcon moved to a shared lib module), and every
Train tab helper text is now one plain sentence.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep the Create/Train tab icon and label on one line

TabsTrigger renders its children inside a plain inline span and the
Tailwind preflight gives svg display:block, so the HugeiconsIcon forced
the label onto a second line. Wrap icon plus label in their own
inline flex row inside each trigger.

* Strip -int8 and -nvfp4 prequant suffixes in the model catalog key

canonicalKeyFor already lowercases before matching, so -GGUF/-FP8 in any
case were covered; -int8 and -nvfp4 were not in the suffix table, so
such repos rendered as standalone rows in Recommended and On Device
instead of standardizing into their base-name group and routing through
pickDefaultArtifact. Added both suffixes plus case-insensitivity and
routing assertions to the catalog check.

* Standardize non-catalog picker rows to their base model name

The curated catalog already collapses its own groups, but hub listing
rows and cached repos outside the catalog (ERNIE-Image, FLUX.2-klein,
Qwen-Image-Edit-2509, FLUX.2-dev) still rendered raw ids with -GGUF /
-FP8 style suffixes in Recommended and On Device.

- model-catalog.ts: new stripArtifactSuffixesForDisplay, a
  case-preserving twin of canonicalKeyFor's stripping that keeps the
  owner prefix and original casing for display.
- pickers.tsx: recommended hub rows and the downloaded GGUF/model rows
  pass their labels through it when a catalog is present, so only the
  diffusion pickers change; chat rows keep raw ids. Click targets keep
  the full repo id, and the format badge still shows the artifact kind.
- Catalog check covers the new helper across GGUF/FP8/int8/nvfp4 in
  both cases plus no-op and suffix-only names.

* Offer official BF16/FP8 artifacts per model group and fix gallery label clipping

Model picker changes so groups are not limited to unsloth quant repos:

- model-catalog.ts: each image group that has an official vendor pipeline
  now carries its BF16 (official) artifact as the top (highest quality)
  entry - Tongyi-MAI/Z-Image-Turbo, Qwen/Qwen-Image, Qwen/Qwen-Image-2512,
  Qwen/Qwen-Image-Edit-2511, black-forest-labs/FLUX.1-dev, FLUX.1-schnell
  and FLUX.1-Kontext-dev. The LTX-2.3 video group now lists Lightricks'
  own bf16 and fp8 distilled single-file checkpoints alongside the GGUF.
  Resident sizes are set from the actual weight totals (FLUX ships a
  duplicate single-file that from_pretrained ignores, so FLUX bf16 is ~32
  GB not 54). The repos that used to be aliases are now real artifacts.
- The router already prefers the highest-quality artifact that fits the
  0.7 x GPU budget, so a datacenter GPU now defaults to official BF16
  while consumer GPUs still route to the fitting quant or GGUF. That is
  why bnb-4bit was the Z-Image-Turbo default before: it was the only
  non-GGUF artifact and it was already downloaded.
- diffusion.py: allowlist the four official image repos not previously
  trusted (qwen/qwen-image-2512, qwen/qwen-image-edit-2511,
  black-forest-labs/flux.1-schnell, flux.1-kontext-dev). All verified as
  safetensors-only diffusers model_index pipelines. The LTX-2.3
  checkpoints are already on the video trust list.
- catalog check: BF16-wins-on-datacenter, quant-wins-on-consumer, and the
  single-file load specs for the LTX-2.3 checkpoints.

Also fixes the video gallery thumbnail caption: the leading duration was
clipped by the rounded corner and selection border, so the strip now has
enough left/bottom padding to clear the curve.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* video gallery: guard export transcode against a stream-less clip

_transcode_webm and _transcode_gif indexed src.streams.video[0] before
checking the stream list, so a container with no video stream raised a bare
IndexError that the broad handlers then re-labeled as a missing libvpx or
decoder. Raise an explicit RuntimeError naming the real cause in both the
WebM and GIF paths.

* Studio: honor explicit attention/format choices, fix distilled-LTX defaults and On Device catalog routing

* Remove stray planning notes accidentally committed to the branch

* video: add transformerQuant to the load callback deps

handleLoad reads transformerQuant but omitted it from the useCallback dep array,
so after the user changes only Precision and then selects a model or clicks
Reapply, the memoized callback keeps the stale closure and loads the previous
precision. The image page's equivalent callback already lists it.

* model picker: honor the format filter when routing catalog clicks; add catalog rows to the roving list

- routedArtifactFor now scopes a group's artifacts to the active format filter
  (the same matchesFormatFilter predicate the visibility check uses) before
  pickDefaultArtifact, so a group shown only because it owns a GGUF no longer
  routes a click to a large non-GGUF download. Covers both the Recommended and
  On Device grouped paths.
- hubOptionKeys now includes the catalog-group, search-catalog-group, and grouped
  On Device row keys in exact render order, so arrow/Home/End roving reaches the
  catalog rows instead of giving them a duplicate missing id and skipping them.

* model picker: don't treat a partial base cache as downloaded

A partially-cached base repo (a cancelled download that left only some weights)
was counted as downloaded, so an On Device click routed to a fresh multi-GB
re-download instead of the complete GGUF. The picker's endpoint (/api/models/
cached-models) did not carry a partial flag at all, so a frontend-only guard
could not see it. Surface partial from that endpoint by reusing the hub inventory
scan's snapshot-partial detector, plumb it through CachedModelRepo (backend +
frontend types), and skip partial base repos when building the downloaded set.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* model picker + diffusion: drop partial/unloadable cached rows, skip defer-compile before a LoRA gen

- On Device (cached non-GGUF) rows filtered partial-download snapshots back in: sortedCachedModels
  gated on passesTaskGate + a groupForRepoId key match but, unlike downloadedSet, never checked
  c.partial, so an incomplete unsloth snapshot showed as a loadable On Device row (click errors or
  silently re-fetches multi-GB). It also admitted repos that only match the catalog by group KEY
  (a base / uncurated-quant sibling like Qwen/Qwen-Image-2512) which have no loadable artifact and
  dead-end at the trust gate. Add !c.partial and gate on artifactForRepoId (what loadSpecFor
  resolves) instead of groupForRepoId, so a cached row shows only when the backend can load it.

- Deferred speed-auto engaged the compile profile on the 3rd generation BEFORE _apply_loras. A
  compiled transformer rejects LoRA (supports_lora is False) and _apply_loras raises before its
  unchanged-selection no-op, so once compile engaged every LoRA generation on that load failed
  permanently. Skip the deferral when a LoRA is requested (compile and LoRA are mutually exclusive)
  and let it engage on a later LoRA-free generation.

* Scope the cached-model partial probe to the listed snapshot dir

list_cached_models builds each row from the largest/complete copy across HF cache
roots, but _cached_repo_partial probed is_snapshot_partial with no repo_cache_dir,
so the scan spanned every root: a stale .incomplete copy in one root would flag a
complete copy in another as partial and hide the usable model from the picker (the
click then routes to a re-download). Forward the winning snapshot's repo_path so all
three partial signals are scoped to that copy, matching the sibling inventory paths
(models/dataset cache_inventory, local_inventory).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Do not auto-route to gated repos, prefer complete cached copies, defer compile past attached LoRA, scope group expand keys

Four fixes:
- pickDefaultArtifact's not-downloaded ladder returned the gated BF16 FLUX.1-dev / Kontext-dev
  before the open GGUF on a large GPU, so a bare group click routed to a repo the user may lack
  license/token access to. Add a gated flag and skip gated artifacts in the not-downloaded ladder
  (an already-downloaded gated artifact is still returned).
- list_cached_models picked the largest duplicate cache copy and computed partial only on it, so a
  larger partial copy shadowed a smaller complete one; since partial rows are dropped from the
  picker the usable model vanished. Prefer completeness, then size.
- the deferred-speed compile engaged on a no-LoRA generation while an adapter from a prior
  generation was still attached, baking it into the compiled graph (the later unload is swallowed
  on a compiled pipe); also defer while adapters remain attached.
- routeGroupClick's GGUF fallback toggled the context-free canonicalId while the chevron toggles
  the context-scoped expandKey, leaving the format list un-collapsible in one context, dead in the
  other, and risking cross-context expansion; thread expandKey through.

* Guard video pipeline repos from deletion, drop the always-failing LTX FP8 artifact, prefer 720p Hunyuan

Three round-6 fixes:
- cached non-GGUF video repos now surface in the Video On-Device picker with the normal delete
  action, but /delete-cached only guarded chat + the Images engine, so a loaded/loading Wan / LTX /
  Hunyuan pipeline could have its HF snapshot removed from under it. Add a VideoBackend
  loading_repo_ids accessor and a video loaded/loading guard mirroring the Images one.
- the catalog advertised Lightricks/LTX-2.3-fp8 as loadable, but the LTX-2.3 loader refuses the
  official scaled-FP8 single file (.weight_scale/.input_scale) and points to GGUF/BF16, so a pick
  routed to a ~76 GB download that always fails on load. Remove the FP8 artifact.
- pickDefaultArtifact only sorts by format, so the HunyuanVideo group's 480p (listed first) beat
  the 720p even on GPUs where 720p fits the budget. List 720p first so the fit loop prefers it and
  falls back to 480p only on smaller cards.

* diffusion: add compute int8/fp8_dynamic text-encoder quant, wire into video

Add two torchao compute text-encoder quant modes to the diffusion precision
engine, alongside the existing layerwise fp8 and weight-only nvfp4:

- int8: per-token activation + per-channel weight (torch._int_mm), with per-layer
  keep-bf16 selection. int8 degrades on large encoders unless the most
  quant-sensitive decoder blocks stay bf16, so it engages only for families with
  a measured keep-bf16 schedule (qwen-image / qwen-image-edit keep first+last 6,
  flux.2-dev keeps first 3); a family without one falls back to fp8.
- fp8_dynamic: per-row fp8 compute (torch._scaled_mm), keeping the matmul in fp8
  on the tensor cores instead of upcasting each forward like the layerwise fp8.

The selective int8 caster reuses the committed transformer-quant factory
(_make_quant_config / make_filter_fn / exclude_tokens_for_scheme) plus a small
structural first/last-N block skip, so it depends only on committed APIs.

Wire text-encoder quant into the video backend, which previously loaded the
companion encoder (Gemma3 / UMT5 / Qwen2.5-VL) dense bf16 while quantising only
the DiT. text_encoder_quant is plumbed through the load request, validation, the
load chain, the resolved record, and status, mirroring the image backend; it
applies for every load kind (the encoder is dense regardless of how the DiT was
sourced). Widen the image and video load request Literals and add the video
status field.

Tests: int8 family-schedule routing and fp8 fallback, fp8_dynamic routing,
hardware gates (int8 sm_80+, fp8_dynamic sm_89+), the structural block selection,
the real int8 filter closure (keeps the first blocks plus the vision tower /
lm_head / T5 wo dense), and the video route threading and 422 validation.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* text-encoder quant: skip the torchao modes under offload (both backends)

quantize_text_encoders applied int8-with-schedule / fp8_dynamic / nvfp4 (all torchao) to the
text encoder regardless of the offload policy. An offload placement then moves the quantized encoder
with Module.to(), which torchao tensor subclasses reject (aten._has_compatible_shallow_copy_type is
unimplemented) -- a hard crash, the same one the DiT path already skips torchao quant under offload to
avoid. Add offload_active to quantize_text_encoders and skip the torchao modes when set; layerwise fp8
is not torchao and still streams under offload. Both the video and image loaders pass
offload_active = (offload policy != none).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* diffusion: skip non-bf16 linears for scaled_mm quant schemes

The fp8 / mxfp8 / nvfp4 schemes run on torch._scaled_mm and the fp4 / mx GEMMs,
which assert a bfloat16 input weight. On a mixed-precision DiT that keeps some
linears in fp32 for numerical stability (the Wan and Hunyuan video transformers
do this), quantize_ hits the first fp32 linear, raises, and the best-effort
wrapper swallows it to None, so the whole transformer stays dense with no error
and no speedup or memory saving.

Add a require_bf16 gate to make_filter_fn and pass it for the scaled_mm schemes
in quantize_transformer (and the fp8_dynamic text-encoder caster). The gate
skips non-bf16 linears so the scheme engages on the bf16 ones. int8 uses
torch._int_mm, which quantizes fp32/fp16 weights fine, so it leaves the gate off
and keeps its current coverage.

Verified on Wan2.2-TI2V-5B: fp8 and mxfp8 now quantize 303 linears via the
committed quantize_transformer path where they previously engaged 0.

* prequant builder: mirror the scaled-mm bf16 gate offline

The runtime DiT quantizer skips non-bf16 Linears for the scaled_mm schemes (fp8,
nvfp4, mxfp8) so the scheme engages on a mixed-precision transformer instead of
aborting on the first fp32 Linear. The offline prequant builder reused make_filter_fn
without that gate, so building an fp8/nvfp4/mxfp8 checkpoint for a mixed-precision DiT
(Wan, Hunyuan keep _keep_in_fp32_modules in fp32 even under torch_dtype=bf16) would hit
the same fp32 Linear and abort, breaking the builder's stated offline == runtime,
LPIPS-0 invariant. Thread require_bf16 = scheme in _SCALED_MM_SCHEMES through the builder,
record it in the checkpoint metadata, and verify it on load (mirrors the existing
exclude_name_tokens guard) so a future _SCALED_MM_SCHEMES change cannot silently load a
checkpoint built under the old filter.

* Keep nvfp4 fp32 linears quantised (bf16 gate is fp8/mxfp8 only)

Verified on torchao 0.17 / B200: fp8 per-row asserts 'PerRow quantization only
works for bfloat16 precision input weight' and mxfp8 asserts 'Only supporting bf16
out dtype', but NVFP4's high-precision conversion quantises an fp32 weight fine
(forward included). So the bf16 skip-gate must be fp8/mxfp8 only, not all scaled_mm
schemes -- otherwise nvfp4 leaves large fp32 projections dense, losing the intended
memory/speed gain. Rename _SCALED_MM_SCHEMES -> _REQUIRE_BF16_SCHEMES = (fp8, mxfp8)
and thread it through the runtime filter, the offline builder, and the loader
require_bf16 verification (offline == runtime preserved).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 17:48:39 -07:00
Daniel Han
c00c1e70c8
studio: tool calling for DeepSeek (R1/V3/V3.1), GLM 4.x, Kimi K2 on safetensors + MLX (#5624)
* studio: tool calling for Llama-3, Mistral, Gemma 4 on safetensors + MLX (#5615)

Adds tool calling for Llama-3, Mistral (pre-v11 + v11+ + [ARGS]), and Gemma 4 to the safetensors / transformers and MLX backends. Parser patched against llama.cpp / vLLM / SGLang per-family parsers and normalises to OpenAI shape. 96 targeted unit tests + cross-OS staging CI (ubuntu / macos-14 / windows) green on the multi-format probe.

* studio: tool-call healing parity between safetensors / MLX and GGUF

After the multi-format parser landed in #5615, the safetensors / MLX
agentic loop and the GGUF loop still differed on healing behaviour.
This commit closes the gaps in both directions so the two backends
react the same way to identical model output.

Changes:

1. core/inference/llama_cpp.py -- the GGUF BUFFERING state machine
   now wakes on every emission marker the shared parser knows. Was
   ("<tool_call>", "<function="); is now the five-tuple imported
   from core.inference.tool_call_parser (Qwen / Qwen3.5 / Llama-3
   <|python_tag|> / Mistral [TOOL_CALLS] / Gemma 4 <|tool_call>).
   Stream cleanup is delegated to the same shared strip_tool_markup
   so leaked markup from any family is removed from assistant
   content.

2. core/inference/llama_cpp.py -- per-tool canonical heal key. When
   a tool arguments field is a bare string and JSON parsing fails,
   the GGUF path now heals to {"code": raw_args} for python,
   {"command": raw_args} for terminal, and {"query": raw_args} for
   everything else. Was hard-coded to {"query": raw_args}, which
   silently routed every python / terminal emission through
   web_search. Mirrors safetensors_agentic._CANONICAL_HEAL_ARG.

3. core/inference/safetensors_agentic.py -- re-prompt on plan-
   without-action. When the model emits a short forward-looking
   intent ("I'll search for that", "Let me check", "First, I
   will...") and no tool call, the loop nudges the model to act
   instead of silently returning a plan-only answer. Up to
   _MAX_REPROMPTS=3 (matches GGUF). The intent regex, character
   cap, and instruction text are byte-identical to the GGUF path.
   The buffer-end fall-through is unified so a buffered intent
   emission that never exits the BUFFERING state still triggers
   the re-prompt.

4. core/inference/safetensors_agentic.py -- extra iteration slots
   for re-prompts. The loop now budgets max_tool_iterations +
   _MAX_REPROMPTS + 1 total iterations and tracks the tool-call
   count separately, so a stalling model can be nudged 3x without
   eating the caller's tool-call budget. Mirrors the _extra slot
   reservation in the GGUF path.

Tests (14 new safetensors-side units; 5 GGUF parity pins):

  TestLoopRePrompt                 -- intent-trigger, plain-answer,
                                      no-tools, cap-at-three, budget
                                      preserved, buffer-end intent.
  TestLoopCanonicalHealKey         -- python / terminal / unknown.
  TestGGUFSafetensorsHealingParity -- shared markers used, shared
                                      strip used, canonical heal keys
                                      identical, intent regex matches
                                      same phrases, _MAX_REPROMPTS
                                      equal on both backends.

All 110 targeted tests pass locally; the broader tool / inference /
model-config / sandbox / anthropic / mlx suites stay green.

Why this matters

Without this parity, Llama-3.2 / Mistral / Gemma 4 emissions on Mac
(MLX) and Linux-safetensors stop the agentic loop as soon as the
model says "Let me...", because the GGUF re-prompt logic never
existed on these backends. The two-marker GGUF BUFFERING tuple also
let non-Qwen tool emissions stream out as plain prose when
llama-server's structured channel did not pick them up. Both paths
now drain the same way, heal the same way, and re-prompt the same
way -- so a tool call that works on GGUF works identically on
safetensors / MLX.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fix tool-call parser bugs from gemini review on #5620

Three high-priority gemini findings on the tool-call parsing additions:

  1. unicode_escape on UTF-8 bytes corrupts non-ASCII literals
     (e.g.  becomes â\x9c¨). Replace with json.loads on a quoted
     string -- preserves emoji / CJK / RTL while still handling
     \n \t \uXXXX escapes.

  2. Llama-3 sentinel stripping is order-dependent. A leading
     `<|eot_id|><|begin_of_text|>` left `<|begin_of_text|>` behind
     because the loop had already passed that sentinel. Loop until
     no sentinel matches at the start.

  3. Mistral v11+ `[TOOL_CALLS] name { json }` regex uses non-greedy
     `\{.*?\}` which truncates at the first `}` of a nested JSON
     argument, leaking the tail (e.g. `}}`) into user-visible
     streamed text. Same problem for the v0.3 array pattern with
     nested brackets. Strip those with balanced brace/bracket
     scanning via a new `_strip_mistral_closed_calls` helper called
     from `strip_tool_markup`.

Also fix the inference routes' parallel `_TOOL_XML_RE`:

  - Same nested-JSON truncation in the Mistral patterns; route the
    strip through the parser's balanced-scan helper via a thin
    `_strip_tool_xml` wrapper that all existing callers now use.
  - Llama-3 `<|python_tag|>[^\n<]*` stopped at any `<`, leaking the
    tail of any tool call whose argument contained a literal `<`
    (queries, code snippets). Relax to `[^\n]*` which keeps the
    strip confined to the actual end-of-line.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: tool calling for DeepSeek (R1/V3/V3.1), GLM 4.x, Kimi K2

Adds three more emission-family parsers to tool_call_parser.py so the
shared safetensors / MLX / GGUF agentic loop covers the major open-
weight reasoning families. Patterns ported from llama.cpp
(common/chat-parser.cpp legacy pre-PEG branch), vLLM
(tool_parsers/deepseekv3*, glm4_moe, kimi_k2), and SGLang
(function_call/deepseekv31_detector, glm4_moe_detector, kimik2_detector).
All three references are MIT (llama.cpp) or Apache-2.0 (vLLM, SGLang).

Formats covered:

  DeepSeek R1     <|tool▁calls▁begin|><|tool▁call▁begin|>function
                  <|tool▁sep|>NAME\n```json\n{...}\n```<|tool▁call▁end|>
                  <|tool▁calls▁end|>
                  -- args wrapped in a Markdown json fence, ``function``
                  literal prefix per llama.cpp common_chat_parse_
                  deepseek_r1 (chat-parser.cpp:801-820)

  DeepSeek V3/V3.1
                  <|tool▁calls▁begin|><|tool▁call▁begin|>NAME
                  <|tool▁sep|>{json}<|tool▁call▁end|><|tool▁calls▁end|>
                  -- bare JSON, no code fence, no ``function`` prefix
                  per llama.cpp common_chat_parse_deepseek_v3_1
                  (chat-parser.cpp:822-879)

  GLM 4.5/4.6/4.7 <tool_call>NAME\n<arg_key>k1</arg_key>
                  \n<arg_value>v1</arg_value>...</tool_call>
                  -- strings raw, non-strings JSON-encoded per
                  chat_template.jinja; multi-call is back-to-back
                  blocks. Per llama.cpp common_chat_parse_glm_4_5
                  (chat-parser.cpp:1040-1052)

  Kimi K2         <|tool_calls_section_begin|><|tool_call_begin|>
                  functions.NAME:IDX<|tool_call_argument_begin|>{json}
                  <|tool_call_end|><|tool_calls_section_end|>
                  -- bare name recovered by stripping ``functions.``
                  prefix and ``:IDX`` suffix; full id preserved as
                  tool_calls[i].id so the roundtrip replays verbatim.
                  Per llama.cpp common_chat_parse_kimi_k2
                  (chat-parser.cpp:896-913)

Marker collisions

GLM uses the same ``<tool_call>`` opener as Qwen but with a bare
function name + ``<arg_key>`` body (Qwen has ``\s*{`` after the tag).
The dispatch keeps Qwen first; Qwen's _TC_JSON_START_RE returns no
matches on a GLM emission, so the fall-through to _parse_glm_tool_
calls handles it correctly. Existing Qwen tests confirm zero
regression.

Streaming buffer

TOOL_XML_SIGNALS extended from 5 markers to 12 so the BUFFERING state
machine wakes on every new family's section opener. Added the
DeepSeek alternative markers (ASCII underscores, short ``<|tool▁calls|>``
form) because real checkpoints emit those variants.

Strip patterns

_TOOL_CLOSED_PATS adds DeepSeek envelope (``<|tool▁calls▁begin|>...
<|tool▁calls▁end|>``) and Kimi section (``<|tool_calls_section_begin|>
...<|tool_calls_section_end|>``). _TOOL_ALL_PATS adds the same plus
the unclosed-tail variants so a truncated stream does not leak
markup.

Route gate

_detect_safetensors_features._PARSER_MARKERS grows to include
DeepSeek and Kimi markers plus ``<arg_key>`` (the unique GLM signal).
_TOOL_XML_RE (the route-layer markup-strip regex) gets DeepSeek and
Kimi closed-pair patterns. _TOOL_TEMPLATE_MARKERS in llama_cpp.py
adds ``message['role'] == 'tool'``, ``message['tool_calls']``, and
``tool_calls is defined`` so the classifier recognises DeepSeek's
subscripted-access template style (it has no top-level
``{% if tools %}`` block).

Tests (39 new):

  TestParserDeepSeek  (7) -- R1 fence, short-form opener, V3.1 bare,
                             multi-call, with-reasoning, strip,
                             signal-wakes-streaming
  TestParserGLM       (6) -- single, mixed types, multi-call,
                             unclosed-heal, no-Qwen-regression, strip
  TestParserKimi      (6) -- single, multi-call, dotted-name, unclosed,
                             strip, signal-wakes-streaming
  TestParserCrossFormatRouting (2) -- dispatch routing, signal coverage
  TestLoopBasic loop integration (3) -- DeepSeek / GLM / Kimi end-to-end
  Capability advertise (3) -- DeepSeek / GLM / Kimi templates flip
                             supports_tools=True

All 398 targeted tests pass locally (115 safetensors + 27 capability
+ rest of tool / inference / sandbox / model-config suites). Builds
on PR #5620 (parser + healing parity for Llama-3 / Mistral / Gemma 4);
will rebase cleanly onto main once #5620 lands. PR opened as draft -
do not merge until validated against real models for each family.

Sources

- llama.cpp common/chat-parser.cpp lines 801-913, 1040-1052 (MIT)
- vLLM vllm/tool_parsers/deepseekv31_tool_parser.py (Apache-2.0)
- vLLM vllm/tool_parsers/glm4_moe_tool_parser.py (Apache-2.0)
- vLLM vllm/tool_parsers/kimi_k2_tool_parser.py (Apache-2.0)
- SGLang python/sglang/srt/function_call/{deepseekv31,glm4_moe,kimik2}_
  detector.py (Apache-2.0)
- Live chat templates: deepseek-ai/DeepSeek-V3.1, zai-org/GLM-4.6,
  moonshotai/Kimi-K2-Instruct, unsloth/DeepSeek-V3-0324,
  unsloth/GLM-4.5-Air, unsloth/Kimi-K2-Instruct

* studio/routes: make python_tag strip multi-line aware

Earlier revisions of _TOOL_XML_RE in studio.backend.routes.inference
oscillated between two bug shapes:

  5615    r"<\|python_tag\|>[^\n<]*"   -- stopped at any literal "<"
                                         so code='if x < 10: pass'
                                         leaked '< 10: pass)' to the
                                         user.
  5620.1  r"<\|python_tag\|>[^\n]*"    -- single-line only; the second
                                         line of
                                         python.call(code="a\nb")
                                         leaked.

The full parser (_parse_llama3_python_tag) already handles both via
balanced-brace scanning, so the parsing path was fine; the LEAK was
in the streaming strip path that runs on every cumulative emission
while content is still arriving.

Switch to r"<\|python_tag\|>(?:[^<]|<(?!\|))*" so the strip consumes:

  * any character that is not a "<" (newlines, JSON, code, ...),
  * a "<" only when it is NOT followed by "|" (i.e. NOT a Llama-3
    sentinel start like <|eot_id|>, <|eom_id|>, <|begin_of_text|>).

This means:

  * code='if x < 10' stays inside the strip (5615 fix preserved),
  * multi-line code stays inside the strip (5620 round 2),
  * the strip terminates at the next Llama-3 sentinel so trailing
    assistant content survives.

Tests: TestRoutesPythonTagStrip (8 cases)
  pytest test_safetensors_tool_loop.py test_safetensors_capability_advertise.py
    -> 118 passed in 1.81s (was 110).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: review follow-ups for DeepSeek / GLM / Kimi tool calling

Four fixes addressing review of the parent commit:

1. GLM <arg_value> coercion: tighten the
   json.loads -> ast.literal_eval -> raw cascade to only deserialize
   when the body unambiguously looks like a JSON literal (object,
   array, JSON-encoded string, true/false/null, or numeric). Strings
   like ``True`` / ``None`` (Python literals, not JSON) and arbitrary
   prose now stay raw. The bare-numeric / bare-boolean ambiguity with
   string args remains an inherent limitation of the template without
   schema access -- documented in the new comment. Drops the ast
   import entirely (closes Gemini's :1036 suggestion).

2. Kimi K2 bare-counter ids (e.g. ``<|tool_call_begin|>3``) are now
   dropped rather than surfaced as a tool literally named "3". Matches
   vLLM behaviour; SGLang's schema-infer fallback is out of scope at
   the parse site. Real Kimi K2 emissions use ``functions.NAME:IDX``
   so this is the exception path.

3. Restore the elaborate ``<|python_tag|>(?:[^<]|<(?!\|))*`` clause in
   routes.inference._TOOL_XML_RE -- the simpler ``[^\n<]*`` form
   regressed PR #5620's multi-line / literal-``<`` python_tag fix.
   Restore ``TestRoutesPythonTagStrip`` (8 tests) adapted to call
   ``_TOOL_XML_RE.sub`` directly since the ``_strip_tool_xml`` helper
   was inlined this PR.

4. Add the spaced and backslash-escaped DeepSeek opener variants
   (``<|tool calls begin|>``, ``<|tool\_calls\_begin|>``) to
   ``TOOL_XML_SIGNALS`` for streaming-gate parity with
   ``_DEEPSEEK_BEGIN_RE``.

Also updates the llama.cpp / vLLM citations in the parser docstrings:
``common/chat-parser.cpp`` was split into ``common/chat.cpp`` +
``common/chat-peg-parser.cpp`` by llama.cpp PR #18675, and vLLM
moved the tool parsers from ``vllm/entrypoints/openai/tool_parsers/``
to ``vllm/tool_parsers/``. Pin to pre-refactor commit ``51fa458a92d6``
where the cited line numbers still resolve.

New regression tests in ``test_pr5624_regressions.py`` cover the GLM
coercion heuristic shapes, GLM literal-``<`` in arg_value, Kimi K2
dotted name, Kimi K2 bare-counter drop, DeepSeek V3.1 truncated
mid-stream, and routes-layer strip across all three new families.

Tests:
  pytest studio/backend/tests/test_safetensors_tool_loop.py
         studio/backend/tests/test_safetensors_capability_advertise.py
         studio/backend/tests/test_pr5624_regressions.py -q
  -> 170 passed in 1.91s

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: tighten verbose comments in tool-call parser sections

Comments were narrating what the code already says. Cut historical
"earlier revisions used X, then Y" narratives down to one-line WHY
notes where the footgun still matters (canonical heal-key parity,
balanced-brace vs non-greedy regex, ``(?:[^<]|<(?!\|))*`` over
``[^\n<]*``/``[^\n]*``). Drop section-header banners.

No behaviour change. Re-ran:
  pytest studio/backend/tests/test_safetensors_tool_loop.py \
         studio/backend/tests/test_safetensors_capability_advertise.py -q
  -> 118 passed.
Regression replay (parser + _coerce_arguments on the 5 #5615 inputs)
  -> 21/21.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: GLM 4.7 no-newline emission + Kimi multi-section parity

Two fixes surfaced by triple-confirm verification against the live
HF chat templates and upstream llama.cpp / vLLM / SGLang parsers.

1. GLM 4.7 silent drop
   ``zai-org/GLM-4.7/chat_template.jinja`` line 65 uses
   ``{{- '<tool_call>' + tc.name -}}`` which Jinja strips trailing
   whitespace from, so the first ``<arg_key>`` follows the function
   name with NO ``\n`` between them. Real emissions look like
   ``<tool_call>get_weather<arg_key>city</arg_key><arg_value>London
   </arg_value></tool_call>``. The previous ``_GLM_TC_OPEN_RE`` ended
   the name with ``\n`` so GLM-4.7 calls were silently dropped
   (parser returned ``[]``).

   Fix: relax the name terminator to a lookahead that accepts EITHER
   ``\n`` OR the next ``<arg_key>``:
       _GLM_TC_OPEN_RE = re.compile(
           r"<tool_call>\s*([^\n<{][^\n<]*?)\s*(?=\n|<arg_key>)"
       )
   The first-char restriction ``[^\n<{]`` still excludes Qwen's
   ``<tool_call>{json}`` form so the Qwen-vs-GLM dispatch remains
   mutually exclusive.

2. Kimi multi-section parity with vLLM / SGLang
   ``vllm/tool_parsers/kimi_k2_tool_parser.py`` and SGLang's
   ``kimik2_detector.py`` both use ``re.findall`` and so collect every
   ``<|tool_calls_section_begin|>...<|tool_calls_section_end|>`` block
   in a single stream. The previous implementation stopped at the
   first ``<|tool_calls_section_end|>``. Kimi K2 doesn't emit
   multi-section in practice, but parity is cheap.

   Fix: wrap the existing per-call body parser in an outer loop that
   advances past each ``<|tool_calls_section_end|>`` and continues to
   the next ``<|tool_calls_section_begin|>``. Body parsing extracted
   to ``_parse_kimi_section_body`` for clarity. Truncated final
   section is still surfaced via the existing in-body balanced-brace
   walk.

Verified independently against the live HF templates:
* GLM-4.7 emission constructed from the live template parses to the
  expected ``{name, arguments}`` shape.
* GLM-4.5 / 4.6 newline shape continues to parse (the lookahead also
  matches ``\n``).
* Qwen ``<tool_call>{json}`` still dispatches to the Qwen path -- the
  first-char restriction stops the GLM regex from biting JSON bodies.
* Kimi two-section stream surfaces both calls in order with full ids
  preserved.
* Bare-counter Kimi ids still drop.

Tests added in ``test_pr5624_regressions.py``:
* ``test_glm_4_7_no_newlines_between_name_and_arg_key``
* ``test_glm_4_7_no_newlines_multi_call``
* ``test_glm_4_7_does_not_break_qwen_path``
* ``test_kimi_two_sections_in_one_stream_both_parse``

  pytest studio/backend/tests/test_safetensors_tool_loop.py
         studio/backend/tests/test_safetensors_capability_advertise.py
         studio/backend/tests/test_pr5624_regressions.py -q
  -> 174 passed in 1.93s

  pytest studio/backend/tests/ -q -k 'not gpu and not llama_cpp_integration'
  -> 2038 passed, 15 failed (pre-existing CI gaps).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: parser robustness fixes for PR #5620

Three surgical extensions to the multi-format tool-call parser, each
covering a real fine-tune / template emission shape that the current
parser silently drops. No path narrows; all changes widen what is
accepted.

1. `_parse_tool_call_json` now accepts both `arguments` and
   `parameters` keys. A Hermes / Qwen `<tool_call>{json}</tool_call>`
   wrapper around a Llama-3.2 fine-tune that emits the `parameters`
   key was extracting the tool name and silently discarding the
   args, producing a working-shaped call with an empty payload. The
   bare-JSON and python_tag paths already accepted both keys; this
   path now matches them.

2. `_TC_FUNC_START_RE`, `_TC_PARAM_START_RE`, and `_TC_PARAM_CLOSE_RE`
   now also match the attribute form
   `<function name="..."><param name="...">v</param></function>` used
   by MiniCPM-5 and MiniMax-M2. Names land in either capture group,
   and `</param>` is accepted as a short close.

3. `_parse_llama3_bare_json` sentinel-strip now consumes the role
   label inserted between `<|start_header_id|>` and
   `<|end_header_id|>` by Meta's official Llama-3.x chat template.
   Without this, every assistant turn re-fed through the template
   prefix `<|start_header_id|>assistant<|end_header_id|>\n\n{json}`
   parsed to zero calls, so any history-with-tool-call round-trip
   in production silently dropped.

Tests in `studio/backend/tests/test_safetensors_tool_loop.py`:

* `TestParserRobustness::test_tool_call_json_accepts_parameters_key`
* `TestParserRobustness::test_function_xml_attribute_form`
* `TestParserRobustness::test_function_xml_attribute_form_multi_param`
* `TestParserRobustness::test_function_xml_legacy_equals_form_still_works`
  (regression guard for the existing `<function=name>` syntax)
* `TestParserRobustness::test_llama3_chat_template_round_trip`
* `TestParserRobustness::test_llama3_round_trip_all_roles`
* `TestParserRobustness::test_llama3_round_trip_with_eot_prefix`

`pytest studio/backend/tests/test_safetensors_tool_loop.py
        studio/backend/tests/test_safetensors_capability_advertise.py -q`
goes from 118 to 125 passed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Trim verbose comments in tool-call parser sections for PR #5624

Pure comment / docstring tightening on top of the GLM 4.7 + Kimi
multi-section fixes. No behavioural change.

* Drop multi-paragraph prelude and post-refactor citation chatter in
  the DeepSeek, GLM and Kimi parser docstrings; keep the shape and
  upstream-commit pin.
* Collapse ``parse_tool_calls_from_text``'s 9 per-family blocks into
  a single ordered loop with one combined comment.
* Tighten the GLM coercion, Kimi bare-counter and ``_TOOL_XML_RE``
  comments to one or two lines each.
* Same trim pass on ``_PARSER_MARKERS`` and the regression-test
  docstrings.

Tests:
  pytest studio/backend/tests/test_safetensors_tool_loop.py
         studio/backend/tests/test_safetensors_capability_advertise.py
         studio/backend/tests/test_pr5624_regressions.py -q
  -> 174 passed in 2.00s

* Fix O(N^2) DeepSeek V3.1 backtracking for PR #5624

Adversarial input ``<|tool▁calls▁begin|><|tool▁call▁begin|>fn<|tool▁sep|>``
followed by a long body that does NOT contain a closing brace caused
the V3 path's ``([^\n<]+?)<|tool▁sep|>`` regex to backtrack
quadratically: at each position the lazy quantifier extends one char
at a time looking for a sep that isn't there, taking ~19s on 50k
chars.

Replace the regex search with ``str.find`` on the sep marker plus a
left-walk to recover the name. ``str.find`` is O(N); the walk stops
on ``\n`` (turn boundary), ``<`` (start of a tag), or ``>`` (end of
an optional ``<|tool▁call▁begin|>`` prefix). Same observable
behaviour as the regex on every canonical input.

Tests:
  test_deepseek_v3_1_huge_truncated_body_is_linear (new) -- 50k chars
  must parse in &lt; 1s.
  pytest studio/backend/tests/test_safetensors_tool_loop.py
         studio/backend/tests/test_safetensors_capability_advertise.py
         studio/backend/tests/test_pr5624_regressions.py -q
  -> 175 passed in 1.97s
  pytest studio/backend/tests/ -q -k 'not gpu and not llama_cpp_integration'
  -> 2038 passed, 15 pre-existing failures unchanged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: terminate function-XML body at </function>, not just </tool_call>

`_parse_function_xml` was looking for `</tool_call>` (the Hermes
wrapper) as the body terminator. When a model emits a standalone
`<function=NAME><parameter=K>v</parameter></function>` followed by
explanatory prose (which models routinely do), no `</tool_call>` is
present, so the body extended to end-of-string and the trailing
prose leaked into the LAST parameter value.

Pre-existing on main (the legacy `<function=NAME>` form had this
bug too). Same affects PR #5620's new attribute-form
`<function name="NAME"><param name="K">v</param></function>`
emission used by MiniCPM-5 / MiniMax-M2.

Fix: `_TC_END_TAG_RE` now matches either `</tool_call>` OR
`</function>`. The existing `_TC_FUNC_CLOSE_RE` / `_TC_PARAM_CLOSE_RE`
strips are unchanged. Multi-call inputs still bound each function
at the next `<function=` start, so no over-eager consumption.

New tests:

* `test_function_xml_followed_by_prose` (legacy form + prose)
* `test_function_attribute_xml_followed_by_prose` (attribute form + prose)

Existing `test_code_with_embedded_xml` still passes (a parameter
value containing literal `<a></a>` is preserved because the
embedded close tag is `</a>`, not `</function>`).

`pytest studio/backend/tests/test_safetensors_tool_loop.py
        studio/backend/tests/test_safetensors_capability_advertise.py -q`
goes from 125 to 127 passed.

* Studio: tighten Llama-3.2 bare-JSON guard

A fuzz pass on PR #5811 turned up that ``_parse_llama3_bare_json``
accepted ``parameters`` as a string, contradicting the docstring's
"parameters or arguments is a dict" guard. Prose JSON like
``{"name":"foo","parameters":"a sentence"}`` would wrongly fire the
parser, which the agentic loop would then heal into a real
``foo(query="a sentence")`` call.

Same code lives on this branch, so the same fix applies here.

Tightened guard:

  - ``parameters`` must be a dict (Llama-3 spec).
  - ``arguments`` may be a dict, or a JSON-encoded string that
    decodes to a dict (OpenAI shape, e.g.
    ``"arguments":"{\"q\":\"x\"}"``). Plain non-JSON strings or
    JSON-strings of lists / scalars / null no longer pass.

Mirrors the fix landed in PR #5811 commit 615b8608. Adds the same
4 regression tests under TestParserMultiFormat.

Existing test suite stays green: 127 -> 131 passing.

* Studio: skip non-scalar args in python_tag JSON form

The JSON sub-path of ``_parse_llama3_python_tag`` was fabricating
``{"value": args}`` when the model emitted a non-dict / non-string
``arguments`` value (e.g. ``42``, ``[1,2,3]``, ``null``, ``true``).
This silently turned a malformed emission into a real tool call,
which the agentic loop would then execute with arguments the model
never intended.

Tightened: skip the call instead of fabricating. The same
behaviour now matches the bare-JSON guard tightened earlier
(strict-guard merge from PR #5620, inherited via merge here).

Added a regression test covering the four non-scalar shapes.
Pass count on this branch: 158 -> 159.

Sites in ``_parse_tool_call_json`` and ``_consume_mistral_call``
keep the existing looser behaviour for now; both are reached
only after explicit ``<tool_call>`` / ``[TOOL_CALLS]`` markers
so the false-positive surface there is much narrower.

* studio: fix safetensors tool-call parser gaps vs llama.cpp (Mistral CALL_ID / THINK, attribute-form signal)

Three GGUF-parity fixes to the safetensors tool-call parser, each matching
llama.cpp's reference behaviour:

- Mistral Small 3.2 emits [TOOL_CALLS]name[CALL_ID]<id>[ARGS]{json}. The
  parser stopped after the name on seeing [CALL_ID] (neither [ARGS] nor {),
  dropping the call. Skip an optional [CALL_ID]<id> segment in both the
  parse and strip paths. llama.cpp parses this (test-chat.cpp:4785).

- Magistral wraps reasoning in [THINK]...[/THINK]. A [TOOL_CALLS] inside the
  reasoning was parsed as a real call, producing a phantom call. Strip a
  leading [THINK] block before scanning so only the post-reasoning call
  counts (test-chat.cpp:2285); a literal [THINK] inside a later argument is
  left intact.

- The standalone MiniCPM-5 / MiniMax-M2 <function name="..."> attribute form
  parsed correctly but was absent from TOOL_XML_SIGNALS and the markup strip
  patterns, so the streaming safety-net parse was gated off (dropping the
  call) and markup leaked into displayed text. Add the signal and broaden
  the strip regexes.

Adds regression tests for all three.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fix GLM and Kimi K2 safetensors tool-call parser gaps vs llama.cpp

Four GGUF-parity fixes for the GLM and Kimi K2 families:

- GLM 4.7 zero-argument inline call <tool_call>name</tool_call> was dropped:
  the open-tag lookahead only allowed \n or <arg_key> after the name. Allow
  </tool_call> too so a no-arg call parses to empty args (vLLM / SGLang /
  llama.cpp all parse it).

- GLM string argument values were stripped, losing significant leading /
  trailing whitespace in code / diff arguments. Keep the raw value for the
  string fallback and only strip the copy used to probe for a JSON literal,
  matching vLLM glm4_moe which never strips string args.

- Kimi K2 calls emitted without the <|tool_calls_section_begin|> wrapper
  were dropped. llama.cpp makes the section optional (Kimi can call a tool
  straight after reasoning without opening a section); parse a bare
  <|tool_call_begin|> when no section is present.

- Kimi K2 malformed / truncated JSON in one call dropped every later call in
  the section. Skip the bad call and keep parsing so valid subsequent calls
  are recovered (vLLM parity).

Adds regression tests for all four.

* studio: fire safetensors tool calls for the bare-JSON (Llama-3.2) form

The agentic loop's streaming safety-net parse was gated on
has_tool_signal(), which is False for the Llama-3.1 / 3.2 bare-JSON tool
form {"name":..,"parameters":..} (no XML marker). Real tool calls were
therefore dropped: the loop logged "model planned without calling tools",
re-prompted three times, then gave up with zero tool calls, while GGUF's
llama-server parses the same emission natively.

Run parse_tool_calls_from_text() unconditionally in the safety net. The
parser is strict (only fires on a valid tool-call shape) so plain answers
are unaffected. Reproduced on a real unsloth/Llama-3.1-8B-Instruct run:
the model emits {"name":"web_search","parameters":{...}} which now
executes the tool instead of being re-prompted into a no-op.

Adds a loop regression test for the bare-JSON form.

* studio: fire safetensors tool calls for Gemma 4 (native template + stripped parser)

Gemma-4 safetensors fired no tools while its GGUF fired reliably. Three gaps:

- The Studio swaps in the Unsloth "gemma-4" chat template, which does not
  render the tools schema (the model's native template does), so the model
  never saw the tools. Fall back to the model's native template when the
  override template renders identically with and without tools. Same fix
  helps any family whose override template drops tools.
- skip_special_tokens strips the <|tool_call> wrapper and <|"|> string
  markers, so a streamed Gemma-4 call arrives as a bare call:NAME{k:v, ...}
  with unquoted values. Parse that form, keeping commas/braces inside a
  code or command value, normalising surrounding quotes, and stripping the
  leaked markup from the final answer.
- Without a grammar a small model can loop, repeating one call for the whole
  tool budget. Collapse exact-duplicate calls within a turn and force a final
  answer after a turn that made no new tool progress (llama-server's lazy
  grammar prevents this loop on the GGUF side).

Adds parser tests for the bare/stripped Gemma-4 form.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: complete strict-mode contract and fix parser import paths

Address review findings on the multi-format tool-call parser:

- Honor allow_incomplete=False in the remaining sub-parsers. The Llama-3
  <|python_tag|>NAME.call(...) parser, the pre-v11 Mistral [TOOL_CALLS] array
  parser, and the Gemma 4 <|tool_call> parser ignored strict mode, so a
  truncated call (missing closing paren, ], or <tool_call|>) was still healed
  and executed with Auto-Heal disabled. Thread strictness through and reject
  the unclosed forms, matching the JSON and function-XML paths.
- Drop the duplicate tool_call_parser import block in llama_cpp.py and the
  redundant un-aliased TOOL_XML_SIGNALS; only the _SHARED_TOOL_XML_SIGNALS
  alias is used as a value.
- Import _strip_mistral_closed_calls from core.inference.tool_call_parser in
  routes/inference.py instead of studio.backend.core... The self-contained
  run.py launch mode only puts studio/backend on sys.path, so the absolute
  package path raised ModuleNotFoundError on the server-tool strip path.

Add strict-mode regression tests for the truncated Llama-3 dot-call and the
unclosed Mistral array.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden DeepSeek/Kimi tool-call parsing and strip

Address review findings on the DeepSeek and Kimi parsers:

- Honor allow_incomplete=False for DeepSeek. An envelope with no closing
  <|tool▁calls▁end|> is truncated mid-stream; reject it in strict mode
  instead of healing the body out to EOF, matching the strict XML and Mistral
  paths.
- Do not skip a following tool call when the current call's end marker is
  missing. The DeepSeek V3 and Kimi loops advanced by searching forward for the
  next <|tool▁call▁end|> / <|tool_call_end|>, which could land on a later
  call's end marker and drop the call in between. Advance by the JSON end; the
  loop re-locates the next call marker from there.
- Strip truncated DeepSeek and Kimi section blocks in the route-level display
  regex. The patterns required the closing marker; add the end-of-text
  alternative so a block truncated by EOS does not leak raw markup to the UI.

Add regression tests for the truncated DeepSeek envelope, and for DeepSeek and
Kimi multi-call recovery when the first call's end marker is missing.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: preserve XML param indentation and alias Mistral array parameters

Two parser-correctness fixes found by auditing against the model chat templates
and the SGLang / vLLM reference parsers:

- Qwen3.5 XML parameter values lost their leading indentation. The chat template
  emits <parameter=k>\nVALUE\n</parameter>, but the parameter-start regex ate the
  wrapping newline AND the value's first-line indentation with a trailing \s*,
  then str.strip() removed the rest. Narrow the trailing class to horizontal
  whitespace only and trim exactly one wrapping newline (via _trim_param_value),
  preserving indentation in code/diff arguments. Matches SGLang's qwen3_coder
  detector. Applies to both _parse_function_xml (tool_call_parser.py) and the XML
  path in tool_healing.py.
- Mistral pre-v11 array objects keyed on parameters dropped their payload.
  _consume_mistral_call read only the arguments key; alias parameters the same way
  the JSON/XML paths and SGLang's base detector do.

Add regression tests for preserved multi-line indentation and the array
parameters alias.

* Studio: DeepSeek strip sync, Gemma nested args, GLM/Kimi strict mode

Parser-correctness fixes found by auditing DeepSeek/GLM/Kimi against vLLM,
SGLang, and the model chat templates:

- DeepSeek: the short <|tool▁calls|> opener (and the space / escaped-underscore
  spellings) was parsed but never stripped, so a short-opener envelope leaked raw
  markup to the UI. Share one opener alternation between _DEEPSEEK_BEGIN_RE and
  the strip patterns (and the route-level display regex) so a signal we parse can
  never be left un-stripped.
- Gemma wrapper-less stream: a nested object/array argument (loc:{city:NYC},
  labels:[bug,ui]) was kept as a literal string. Parse it recursively when the
  bare value is a balanced {} / [], falling back to the raw string for a
  truncated value.
- GLM and Kimi ignored allow_incomplete. With Auto-Heal off, a GLM block with no
  </tool_call>, a Kimi section with no <|tool_calls_section_end|>, or a Kimi call
  with no <|tool_call_end|> are truncated and must be rejected, matching the
  strict behavior of the JSON/XML/Mistral/DeepSeek paths and vLLM/SGLang.

Add regression tests for the short-opener strip, the Gemma nested args, and GLM /
Kimi strict-mode rejection.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: tighten tool-call parser comments

Make the comments in the multi-format tool-call parser and its callers succinct:
compress verbose docstrings/blocks to one or two lines, drop ones that restate the
code, and trim the tiny balanced-scanner helpers. Correctness rationale and
upstream provenance (SGLang/llama.cpp parity, the strict-mode / Auto-Heal
contract, whitespace-preservation, and the Unicode / full-width-pipe notes) are
kept in compact form.

Comment-only: no code or behavior change (verified with comment_tools.py check
--strip-docstrings; parser suite green).

* Studio: tighten DeepSeek/GLM/Kimi parser comments

Compress the comments added for the DeepSeek/GLM/Kimi parsers and the Gemma
wrapper-less helpers to one or two lines, keeping the upstream provenance
(llama.cpp 51fa458a92d6), the O(N^2) / strict-mode rationale, and the vLLM parity
notes intact.

Comment-only: no code or behavior change (verified with comment_tools.py check
--strip-docstrings; parser suite green).

* Studio: make DeepSeek R1 / GLM parsing linear and close routes strip gaps

Review follow-up for the DeepSeek/GLM/Kimi parser:

- DeepSeek R1 detection used a greedy ``([^\n]+)\n```json`` regex that backtracks
  O(N^2) on a fence-less truncated body; scan with str.find instead (mirrors the
  V3 path).
- GLM arg pairs used a lazy-group finditer that rescanned to EOF from each bare
  <arg_key> in an unclosed body (O(N^2)); walk pairs with str.find.
- The route display strip (_TOOL_XML_RE) accepted fewer DeepSeek openers than the
  parser (missed the space / escaped-underscore spellings) and missed bare
  section-less Kimi calls, so a call we parse could leak raw markup to the UI.
  Reuse the parser's shared _DEEPSEEK_OPEN_RE_SRC and add a bare-Kimi arm.

Add ReDoS-linearity regressions for the R1 and GLM paths, a positive R1
fenced-json parse test, and routes-strip tests for the space/escaped DeepSeek
openers and the bare Kimi call.

* Studio: fix test_mcp_servers _TOOL_XML_RE reconstruction after _DS_OPEN_SRC reuse

The routes strip fix made _TOOL_XML_RE reference the module-level
_DS_OPEN_SRC variable. test_mcp_servers reconstructs the regex by exec-ing
the extracted compile() source in a namespace that only defined _re, so it
raised NameError. Inject _DS_OPEN_SRC into that namespace, matching the same
fix already applied in test_tool_xml_strip.

* Studio: make Llama-3 .call and Mistral-array healing parsing linear

Two more O(n^2) ReDoS paths in the multi-format parser, both reachable from
the agentic loop on a long truncated body with no length cap:

- _LLAMA3_KV_RE.finditer over a .call(...) body retried at every offset of a
  long word run / unterminated quote (40K -> 14s). Replace with a hand-scan
  that reuses the same key/number/literal sub-regexes via anchored match and
  walks the string body by hand, so an unterminated quote is O(n). Verified
  byte-identical to the old regex over 200K fuzzed inputs.
- _parse_mistral_array healing ran _balanced_brace_end from every { in the
  body (20K -> 17s). Walk top-level objects, advancing past each balanced
  {...}; this also drops the phantom call the old scan emitted from a nested
  argument object.

Add adversarial-length linearity regressions plus positive .call kwargs and
unclosed-array recovery coverage.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strengthen #5624 regression assertions and strip-test harness guards

- test_strip_tool_markup_handles_deepseek_envelope used `A or B` where B was the
  preservation property the next line already asserts, masking the real check.
  Replace with an explicit assertion that the call name and args are stripped.
- The test_tool_xml_strip source-extraction harness reconstructs _TOOL_XML_RE and
  _strip_tool_xml_for_display from routes/inference.py via lazy regexes that could
  silently grab a shorter slice. Assert the extracted regex carries the DeepSeek /
  bare-Kimi arms and the helper body reached the _TOOL_XML_RE.sub call.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: honor strict mode in safety-net, keep empty Gemma args, strip attribute-form function XML

- safetensors safety-net parser now forwards allow_incomplete=auto_heal_tool_calls,
  matching the draining path, so a late incomplete tool call is not healed and
  executed when Auto-Heal is off.
- Gemma empty bare value ({k:}) now serialises as "" instead of invalid {"k":},
  which previously dropped the whole call.
- Route _TOOL_XML_RE also strips the <function name="..."> attribute form
  (MiniCPM-5 / MiniMax-M2) so it no longer leaks to the UI.

* Studio: linearize wrapper-less Gemma nested-arg parsing and correct parser provenance

- _gemma_parse_value/_gemma_parse_mapping/_gemma_parse_array now parse nested
  {}/[] in a single forward pass instead of pre-scanning each subtree with a
  balanced-brace walk and re-parsing it. Deeply nested wrapper-less Gemma args
  were O(n^2); they are now ~linear (and ~40x faster at depth 400).
- Correct the DeepSeek/GLM/Kimi provenance comments: the cited commit
  51fa458a92d6 is unrelated, and GLM/Kimi were never standalone
  common_chat_parse_* functions (llama.cpp uses common_chat_params_init_glm_4_5
  plus a generalized XML parser, PRs #15904 / #16932).
- Add tests: Gemma deep-nesting linearity, nested object/array preservation,
  same-turn distinct-call cap, and the native-template tool-render fallback.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: guard Gemma value parser against non-advancement and missing tokenizer

Addresses Gemini review:
- _gemma_parse_value now consumes one character when a stray }/]/, sits where a
  value is expected, so _gemma_parse_array can never stall at the same index on
  malformed input (a latent infinite loop).
- _render_with_native_template returns None when neither a tokenizer nor a
  processor is present instead of raising AttributeError.
- Tests for both.

* Studio: fix attribute-form function-XML literal close tag and zero-arg strict call

Addresses Codex review of the <function name="..."> attribute form in
_parse_function_xml (MiniCPM-5 / MiniMax-M2):
- End the call body at the LAST </function> / </tool_call> within the call's
  window, so a literal close tag inside a code/search argument (e.g.
  print("</function>")) is preserved instead of truncating the call.
- Accept a closed call with no parameters as a valid zero-argument call in strict
  mode (the function close is already required), instead of rejecting it as a
  truncated call.
- Tests for both, mirroring the legacy <function=...> coverage.

* Studio: drop scratch review/planning artifacts from the branch

* Studio: fix tool-call parser/loop review findings on the multi-format path

Address the live code-review findings on the safetensors/MLX + GGUF tool path:

- routes: include the attribute form <function name="..."> in the safetensors
  capability whitelist so MiniCPM-5 / MiniMax-M2 templates keep the tool pill
  (parser already handles the form; the post-filter wrongly suppressed it).
- safetensors loop: build the plan-without-action re-prompt from the active
  tools instead of a hardcoded web_search/python string, and gate it on
  auto_heal_tool_calls, matching the GGUF loop.
- safetensors loop: hold a leading bare-JSON object ({"name":..,"parameters":..})
  during BUFFERING until it closes, then drain it as a tool call instead of
  streaming the raw JSON to clients. The DRAINING/STREAMING resolvers still
  recover a plain JSON answer, so this can never drop content.
- parser: anchor the Llama-3 <|python_tag|>NAME.call(...) scan to the tag and
  chain ; -separated calls, so all semicolon-separated built-ins parse and a
  literal <|python_tag|>x.call(...) inside a JSON string argument no longer
  fires the wrong tool.
- parser: consume the optional trailing </s> after a named Mistral
  [TOOL_CALLS]name{json} call, mirroring the array shape.
- GGUF streaming strip: use the shared parser patterns (which know
  [TOOL_CALLS] and <|python_tag|>) so a textual tool call entering DRAINING is
  stripped instead of leaking the marker to streaming clients.
- routes: hoist the _strip_mistral_closed_calls import to module level.

Adds regression tests covering each fix; existing parser suite stays green.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: fix DeepSeek/GLM/Gemma tool-call review findings

Address the live code-review findings specific to the DeepSeek / GLM / Kimi
and native-template additions:

- parser: in strict mode (Auto-Heal off) require the per-call
  <|tool▁call|end|> terminator for DeepSeek V3 calls instead of executing on
  a bare balanced object closed only by the envelope end.
- parser: keep GLM string arguments that begin with a quote verbatim (drop
  the leading-quote case from the JSON-decode probe) so a quoted search query
  is not decoded down to its inner text.
- parser: reject a GLM call with an unclosed <arg_value> in strict mode, and
  under Auto-Heal keep the partial value rather than dropping it to a no-arg
  call.
- parser: add a balanced wrapper-less Gemma strip (call:NAME{...}) so a nested
  object/array argument is removed whole instead of leaving a trailing brace;
  run the balanced Mistral and Gemma strips on the streaming display paths too.
- safetensors loop: buffer a leading wrapper-less Gemma call:NAME{...} so it
  drains and executes instead of streaming the raw call text.
- inference: render the native-template fallback on a shallow tokenizer copy
  instead of mutating the shared tokenizer outside the generation lock, and
  load the native template from base_model for LoRA adapters.

Adds regression tests for each; existing parser suite stays green.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden multi-format tool-call detection from review findings

Apply five targeted fixes from the review pass over the multi-format tool
path:

- routes: route display strip delegates to _strip_tool_xml so Mistral
  [TOOL_CALLS] blocks with nested JSON are removed from streamed display
  text, not just the XML forms.
- tool_call_parser: skip function/parameter starts that fall inside an
  already-open parameter block (_inside_open_parameter) so nested example
  payloads are not mis-parsed as new calls; extract
  strip_llama3_leading_sentinels so the bare-JSON guard is shared.
- safetensors_agentic: probe bare JSON through strip_llama3_leading_sentinels
  before the balanced-brace check so a leaked header sentinel does not defeat
  the guard.
- tool_healing: allow dotted tool names in the Gemma wrapped start pattern.
- llama_cpp (GGUF): buffer wrapper-less Llama-3.2 {"name":..} calls that carry
  no XML signal, drain a complete object silently and hold an incomplete one,
  and run the end-of-stream safety net unconditionally so markerless calls are
  detected and never leak the raw JSON (including truncated fragments).

Adds regression tests for the GGUF bare-JSON streaming path and the Mistral
display strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: stop bare-JSON tool calls leaking at EOF, oversized, and into history

The second review pass flagged that the Llama-3.2 bare-JSON tool-call handling
still leaked raw JSON in several spots; ``strip_tool_markup`` only knows
XML/bracket markup, so the bare-JSON form survived it. Fix them symmetrically
across the safetensors and GGUF loops:

- Safetensors stream-end resolver now routes a held bare-JSON fragment to
  DRAINING (mirroring GGUF) so a truncated ``{"name":..`` cut off by the end of
  the stream is dropped instead of flushed as assistant content. The 7/10
  reviewer finding.
- Both loops now drain (suppress) an oversized still-open bare-JSON call once it
  passes ``_MAX_BARE_JSON_BUFFER`` instead of streaming the raw prefix, gated on
  a ``"name"`` key so a giant plain JSON answer still streams; a complete
  oversized call still executes via the safety net.
- Add a shared ``strip_leading_bare_json_call`` helper and apply it to the
  content kept for the assistant turn in both loops, so an executed bare-JSON
  call is not replayed as visible text or fed back as next-turn history.

Plain JSON answers without a ``"name"`` key are untouched throughout. Adds
regression tests for the EOF, oversized, and next-turn cases on both backends
plus unit tests for the helper.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: bound the Llama-3 python_tag strip on real control sentinels

The route display strip's <|python_tag|> arm ran to the next <| of any kind.
A tool-call argument carrying a literal <|...|> token (for example <|cite|>
inside a string value) truncated the strip early and leaked the call tail into
the visible response. Narrow the stop condition to the genuine Llama control
sentinels (eot_id, eom_id, python_tag, start/end_header_id, begin_of_text,
finetune_right_pad_id) so embedded markup and JSON are consumed while real
header/turn boundaries still bound the strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden GLM/Gemma parsing, cap GGUF textual calls, share native-template fallback

GLM 4.x parser walked a body pre-bounded by the first </tool_call>, so a string
argument containing a literal </tool_call> (e.g. code that prints it) was
truncated. Walk arg_key/arg_value pairs against the full content instead, since
each <arg_value> is delimited by its own </arg_value> and the call's real close
is the </tool_call> that precedes the next <arg_key>.

Add a truncated wrapper-less Gemma pattern (call:NAME{... with no closing brace)
to the markup strip so a call cut off mid-arguments does not leak raw into the
visible stream. It runs after the closed form, so a complete call keeps trailing
prose.

Cap and dedup tool calls parsed from the GGUF TEXTUAL fallback at
_MAX_TOOL_CALLS_PER_TURN, mirroring the safetensors loop. Structured
delta.tool_calls are grammar-bounded by llama-server, but text parsed straight
from content is not, so one runaway turn could fan out into dozens of
executions.

Extract the native-chat-template fallback into chat_template_helpers
(render_native_template / render_with_native_template_fallback) so the
transformers and MLX text backends share one implementation. The MLX text path
now applies it too, so an Unsloth override template that drops the tools schema
no longer silently stops MLX from advertising tools. The MLX VLM path renders
via the processor for image tokens and is intentionally left on its own render.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: gate markerless bare JSON on enabled tools and close parser/strip asymmetries

The Llama-3.2 custom_tools bare-JSON form has no marker, so any JSON object with a
name key was read as a tool call. An ordinary JSON answer like
{"name":"Alice","parameters":{"age":30}} was misclassified as a call to a
disabled tool and dropped from the visible response. Gate the markerless form on
the enabled tool names (threaded through parse_tool_calls_from_text and
strip_leading_bare_json_call, supplied by both streaming loops): an object whose
name is not an enabled tool is ordinary content. The marker-based forms keep
their name-agnostic behaviour (an explicit signal is a real call attempt), and
unrestricted mode stays ungated.

Also fix two parser/strip asymmetries the parser already tolerated:
- A literal </function> inside a parameter value (print("</function>")) truncated
  both the core and route strips at the first close, leaking the tail. Extend the
  strip to the call's real close (last </function> before the next opener),
  mirroring the parser, without merging separate calls.
- The single-object Mistral [TOOL_CALLS]{...} shape parsed but _strip_mistral_closed_calls
  left it, leaking the raw object into display. Strip the balanced object while
  keeping trailing prose, matching the array and name shapes.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: fix strip/parse symmetry and native-template token for DeepSeek/GLM/Kimi

Pass-3 review follow-ups on the multi-format tool parser:

- Bare Kimi call (<|tool_call_begin|>...<|tool_call_end|> with no section
  wrapper) is accepted by the parser, so add it to the closed strip patterns
  so the streaming (non-final) display strip removes it instead of leaking the
  markup mid-generation.
- Route display strip now also runs the wrapper-less Gemma cleanup, so a
  Gemma 4 call:NAME{..} no longer leaks into the visible answer.
- MLX model record carries base_model for a LoRA adapter so the native-template
  fallback loads the base repo template rather than the adapter's
  (often template-less) tokenizer.
- Native-template reload forwards the load-time HF token so a gated/private
  model's repo template can still be fetched (transformers and MLX text paths).
- GGUF end-of-stream bare-call heuristic is gated on the enabled tool names so a
  truncated ordinary JSON object ({"name":"Alice","age":) streams as the answer
  instead of being dropped as a tool call.

Adds regression tests for each case.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: gate GGUF bare-JSON suppression on enabled tools and fix python-tag exponent parsing

Pass-4 review follow-ups on the GGUF tool loop and Llama-3 parser:

- The GGUF bare-JSON suppression sites still keyed off a raw "name" substring,
  so an ordinary JSON answer whose name is not an enabled tool was dropped when
  it was truncated, oversized, or reached the no-tool DRAINING fallback (the
  parser, helper, and safetensors paths were already gated). All three sites now
  use the shared enabled-name gate, and a held bare-JSON buffer that turns out not
  to be an enabled call is shown as the answer instead of dropped at stream end.
- The Llama-3 python-tag numeric kwarg regex matched only the mantissa, so
  scientific notation was truncated to its leading digits (1e-3 parsed as 1) and a
  tool executed with the wrong value. The regex now accepts exponent and decimal
  forms, and the int/float classification keys off the exponent too.

Adds regression tests for the truncated / oversized disabled-name JSON cases (and
a counterpart that a truncated enabled call still does not leak) plus the
scientific-notation kwargs.

* Studio: drop accidentally committed async worker transcripts

Eight generated reviewer / async-worker transcripts were committed under
studio/backend/async_task_outputs/. They are not imported or referenced by any
code and carry only internal task state, so they should never ship in the repo.
Remove them and gitignore the directory so they cannot be re-added.

* Studio tools: gate safetensors bare-JSON drain, fix nested-name gate and function-XML strip

Pass-4 review follow-ups on the shared parser / safetensors loop:

- The safetensors oversized and end-of-stream bare-JSON drain branches keyed off
  a raw "name" substring, so a large or truncated ordinary JSON answer whose name
  is not an enabled tool was drained instead of streamed. Both now use the shared
  enabled-tool-name gate, matching the GGUF path.
- strip_leading_bare_json_call matched the first "name" anywhere, so a plain JSON
  answer with a nested name equal to an enabled tool ({"result":{"name":"web_search"}})
  was wrongly suppressed. It now extracts the TOP-LEVEL name only, walking past
  nested objects/arrays and keeping the text when a top-level value is truncated.
- The function-XML display strip used a regex negative-lookahead that stopped at a
  literal <function=...> opener inside a parameter value and then dropped the rest
  of the answer to EOF. A scan-based strip mirrors the parser (ignores openers
  inside an open <parameter> via _inside_open_parameter) and closes each call at its
  real </function>, so trailing assistant text after such a call survives.

Adds regression tests for each.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: keep tools prompt when native-template probe raises; make helper tests hermetic

Pass-4 review follow-ups on the native-template fallback:

- render_with_native_template_fallback re-renders the live template with tools=None
  to detect whether it dropped the schema. A template that requires tools can raise
  on that probe; that must not discard the already-valid tools prompt. The probe is
  now wrapped so any error returns the original formatted_prompt (transformers would
  otherwise fall back to manual formatting and lose the schema; MLX would let the
  exception escape).
- The native-template helper tests imported InferenceBackend just to reach the
  thin wrapper, which pulls in unsloth and its optional vllm package metadata. They
  now call the dependency-light render_native_template helper directly so they pass
  in a backend/test environment without vllm. Adds a probe-raises regression test.

* Tool parsing: 3.9 import safety, disabled-Auto-Heal contract, capability gate

Round-2 review follow-ups on the multi-format tool-call parser:

- tool_call_parser: add `from __future__ import annotations`. The module
  is dependency-light by design (external llama-server wrappers import it
  standalone) and the package targets python >=3.9, where its PEP 604
  `int | None` return annotations would raise TypeError on import.
- safetensors + GGUF drain fallback: gate the leading bare-JSON strip on
  auto_heal_tool_calls. With Auto-Heal off, a truncated enabled-name
  fragment that did not parse now stays visible, matching the XML strip
  in the same branch and the disabled-Auto-Heal contract. With Auto-Heal
  on it is still suppressed.
- safetensors capability gate: match the bare-JSON `{"name":` template
  marker with a whitespace/escape-tolerant regex so a pretty-printed
  `{ "name" :` or JSON-escaped `{\"name\":` template is not mis-classified
  as tool-less. The parser already accepts that whitespace via
  raw_decode, so the gate must too.

Regression tests added for each case.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* GLM tool-call display strip: treat literal close tag in arg value as data

Round-2 review follow-up on the GLM 4.x tool-call format.

The GLM call shape is <tool_call>NAME<arg_key>k</arg_key><arg_value>v
</arg_value>...</tool_call>. The parser was hardened to walk arg_key /
arg_value pairs so a literal </tool_call> inside an argument value (e.g.
print("</tool_call>")) is treated as data and the call's real close is the
</tool_call> that precedes the next <arg_key>. The display strips still used a
non-greedy <tool_call>.*?</tool_call> regex, which stopped at the literal and
leaked the call's tail into visible content and stale history.

Add _strip_glm_calls, a scan that mirrors the parser's close detection, and run
it before the regex arms in every strip pipeline: the core strip_tool_markup,
the route _strip_tool_xml display/history cleanup, and the safetensors + GGUF
streaming strips. Qwen / Hermes <tool_call>{json} has no NAME token after the
opener, so it is left to the regex arms unchanged.

Regression tests cover the literal-close-tag leak (core + route), normal GLM
calls, back-to-back GLM calls, zero-arg GLM, truncated GLM, and untouched Qwen.

* Tool parsing: symmetric "function" bare-JSON alias and route strip parity

Round-3 review follow-ups, all parser/strip symmetry fixes.

- Bare-JSON "function" alias: the markerless parser accepts a call name via
  obj.get("name") or obj.get("function"), but the strip/gates only knew "name",
  so a {"function":<enabled tool>} call executed while its raw JSON leaked. Teach
  _top_level_bare_json_name the alias (with "name" precedence and the same nested
  and truncated-name guards), and widen the guards in strip_leading_bare_json_call,
  the safetensors and GGUF _looks_like_enabled_bare_json gates, and the route
  capability marker regex.
- Route display/history cleanup: strip a tail-only </param> alias close (the
  parser accepts <param name="...">...</param>), and run the parser's guarded
  function-XML scan (_inside_open_parameter) before _TOOL_XML_RE so a literal
  nested <function=...></function> inside an argument value does not truncate the
  strip and leak the tail.

Regression tests added for each.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: fix DeepSeek strict recovery, Kimi dotted names, Gemma spaced streaming

Round 3 review fixes for the DeepSeek / GLM / Kimi tool-call parsing path.

- DeepSeek R1 and V3/V3.1 strict parsing (Auto-Heal off): when a call is
  truncated (missing closing fence or <tool_call_end> terminator), skip it
  and keep scanning for later well-formed calls instead of breaking out and
  dropping the rest of the envelope. This matches the Kimi strict parser's
  recovery behaviour.

- Kimi dotted tool names: keep the full name after stripping only the
  functions. prefix and :idx suffix, e.g. functions.mcp.server-list:0 stays
  mcp.server-list. The previous split on "." truncated dotted MCP names to
  their last segment. This matches current vLLM
  (tool_id.split(":")[0].removeprefix("functions.")) and SGLang
  (^(?:functions\.)?(?P<name>[\w.\-]+):(?P<index>\d+)$).

- Gemma wrapper-less call streaming: hold the whitespace-tolerant prefix
  (call : NAME) in the streaming suppression buffer, matching the parser's
  _GEMMA_BARE_TC_RE, so the spaced spelling split across chunks is buffered
  instead of leaking as visible text. Applied to both the safetensors and
  llama.cpp streaming paths.

- Remove dead _render_with_native_template method and the now-unused copy
  import from inference.py; the live path uses render_with_native_template_fallback.

Adds regression tests for DeepSeek R1/V3 strict recovery, Kimi full dotted
name preservation, and the Gemma spaced-call streaming suppression.

* Studio tools: honor tool budget in GGUF loop and guard function-XML streaming strip

Round 4 review fixes. Both are asymmetric-fix bugs where the final/steady path got a
guard the analogous streaming/loop path did not.

- GGUF tool-call budget: the safetensors loop counts real tool-call turns against
  max_tool_iterations (re-prompt stalls excepted), but the GGUF loop only bounded the
  turn count by the enlarged range (max_tool_iterations + _MAX_REPROMPTS). Since this
  PR raised _MAX_REPROMPTS from 1 to 3, a model that keeps making valid tool calls
  could run up to three extra tool rounds (with max_tool_iterations=1, four rounds
  instead of one). Add a _tool_iters_done counter that increments only when a tool
  actually executed in the turn, and stop once the caller's budget is spent so the
  post-loop final-answer nudge fires. A duplicate/disabled no-op turn is a correction
  turn (like a plan-without-action re-prompt) and does not consume budget, preserving
  the existing "already completed" re-prompt behavior.

- Streaming display strip: the final strip runs the guarded _strip_function_xml_calls
  scanner (a literal <function=...> inside a parameter value is data, not a nested
  call), but the GGUF and safetensors streaming strips still used only the open-ended
  regex arms. When a tool-call argument contained literal function markup, the regex
  tail ate everything to end-of-text and dropped the real trailing prose after the
  call's true </function>. Run the guarded scanner (and the balanced Mistral strip)
  before the regex arms in both streaming paths so streaming and final display agree.

Adds regression tests: GGUF valid tool calls respect max_tool_iterations, and the
streaming strip keeps trailing prose after a function-XML call with a literal marker.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: safetensors tool budget counts only executed turns (GGUF parity)

Follow-up to the GGUF budget fix. The safetensors loop charged max_tool_iterations
per non-re-prompt iteration (iteration + 1 - reprompt_count), so a duplicate/disabled
no-op turn spent a budget slot even though no tool ran. With a small cap this dropped
real work: for max_tool_iterations=2, a model that made a valid call, repeated it (an
internal no-op correction turn), then made a distinct valid call executed only the
first -- the third turn was sent with no tools and the distinct call was ignored.

Track whether a turn actually executed a tool (set on record_result) and count only
those turns against the cap, matching the GGUF loop. A duplicate/disabled no-op is a
correction turn -- like a plan-without-action re-prompt -- and no longer consumes
budget, so the model still gets its "already completed" nudge and another tool-enabled
turn. Adds a regression test for the small-cap duplicate-then-distinct-call flow.

* Studio tools: fix stale Kimi dotted-name regression test

test_pr5624_regressions.py still expected functions.my.tool:0 to resolve to the last
segment (tool). The parser now preserves the full dotted name (my.tool) after removing
only the functions. prefix and :idx suffix, matching current vLLM/SGLang so dotted MCP
names like mcp.server-list survive. Update the assertion, name, and module docstring to
the corrected contract (the raw id is still preserved on the call).

* Studio: render the reasoning block for safetensors and MLX like GGUF

enable_thinking chat templates (Qwen3/Qwen3.5/GLM) prefill an unclosed <think>
into the generation prompt, so the model emits only the closing </think> then
the answer. The safetensors/MLX chat stream emitted that as plain content, so
the reasoning showed inline with no collapsible thinking block, while GGUF
(which surfaces reasoning via reasoning_content) rendered one. This brings
safetensors and MLX to parity.

- _ResponsesReasoningExtractor gains a reasoning_prefilled mode that starts
  inside the reasoning block and splits on the first </think>; default False
  keeps GGUF and every existing caller byte-identical. It suppresses a stray
  re-emitted <think> and holds partial markers back across chunk boundaries.
- _sf_reasoning_prefill_mode gates the mode on reasoning being enabled for the
  request, an enable_thinking or enable_thinking_effort style, and the template
  actually using the standard <think>/</think> markers. Models with a bespoke
  reasoning channel (e.g. gemma's <|think|>/<|channel>) are excluded so their
  answer is never swallowed; gpt-oss (Harmony) and thinking-off requests are
  excluded too.
- sf_tool_stream and stream_chunks (the latter also serves MLX) feed text
  through the extractor, emitting reasoning_content then content deltas, with a
  per-turn reset in the tool loop and a flush before each tool_start; only the
  visible delta reaches the monitor reply. The two non-streaming drains split
  reasoning_content the same way.
- Tests: extractor prefilled mode (streaming and edge cases), the gate matrix
  including the gemma-style exclusion, and a route-replay of the tool-loop
  reasoning stream.

* Studio: render the reasoning block for safetensors and MLX like GGUF

enable_thinking chat templates (Qwen3/Qwen3.5/GLM) prefill an unclosed <think>
into the generation prompt, so the model emits only the closing </think> then
the answer. The safetensors/MLX chat stream emitted that as plain content, so
the reasoning showed inline with no collapsible thinking block, while GGUF
(which surfaces reasoning via reasoning_content) rendered one. This brings
safetensors and MLX to parity.

- _ResponsesReasoningExtractor gains a reasoning_prefilled mode that starts
  inside the reasoning block and splits on the first </think>; default False
  keeps GGUF and every existing caller byte-identical. It suppresses a stray
  re-emitted <think> and holds partial markers back across chunk boundaries.
- _sf_reasoning_prefill_mode gates the mode on reasoning being enabled for the
  request, an enable_thinking or enable_thinking_effort style, and the template
  actually using the standard <think>/</think> markers. Models with a bespoke
  reasoning channel (e.g. gemma's <|think|>/<|channel>) are excluded so their
  answer is never swallowed; gpt-oss (Harmony) and thinking-off requests are
  excluded too.
- sf_tool_stream and stream_chunks (the latter also serves MLX) feed text
  through the extractor, emitting reasoning_content then content deltas, with a
  per-turn reset in the tool loop and a flush before each tool_start; only the
  visible delta reaches the monitor reply. The two non-streaming drains split
  reasoning_content the same way.
- Tests: extractor prefilled mode (streaming and edge cases), the gate matrix
  including the gemma-style exclusion, and a route-replay of the tool-loop
  reasoning stream.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: don't force a tool re-prompt on a negated intent (safetensors parity)

The safetensors _INTENT_SIGNAL claimed to mirror GGUF but was missing the
negative lookahead, so a refusal like "I will not search the web for that"
matched the "i will" intent and triggered the plan-without-action re-prompt
(STOP... you MUST call a tool), overriding a valid no-tool answer. GGUF already
excludes not/never. Add the same (?!\s+(?:not|never)\b) lookahead so both
backends agree. Extends the intent parity test with negated refusals.

* studio: parse the outer envelope before DeepSeek/Kimi markers embedded in its args

parse_tool_calls_from_text ran the DeepSeek/Kimi marker pre-pass before the shared
<tool_call>/<function=...> parser. When a Qwen/Hermes call's argument contained
literal Kimi/DeepSeek markup (for example a user asking the model to explain that
syntax), the pre-pass matched the embedded marker and returned it, executing the
wrong tool and dropping the real call. Skip the pre-pass when a <tool_call> or
<function=...> envelope opens before the first DeepSeek/Kimi marker, so the shared
parser takes the outer call; a genuine marker-led call (no leading envelope) still
goes through the pre-pass. Tests for the embedded-marker case and the control.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: trim redundant comments (comment-only, AST-verified)

* Studio: trim redundant comments (comment-only, AST-verified)

* Studio: prevent Gemma tool-parser DoS on stray delimiters

_gemma_parse_value returned the input index unchanged when text[i] was a
stray delimiter (,}]), so the list and mapping caller loops that advance
on the returned index spun forever at 100% CPU on malformed input such as
[},]. Advance past the delimiter so parsing always terminates.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strip Magistral [THINK] reasoning from final display/history

strip_tool_markup removed [TOOL_CALLS] and <function> markup but left a
leading Magistral [THINK]...[/THINK] block intact, so its bracket-form
reasoning (not the <think> the reasoning channel renders) leaked into the
safetensors display and conversation history while GGUF/llama.cpp routes
it natively. Drop the leading reasoning block at end-of-turn (final=True)
via the existing _strip_mistral_reasoning helper; streaming is untouched.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: keep times in wrapper-less Gemma tool arguments

The wrapper-less Gemma value scanner used _GEMMA_KEY_RE = [\w.\-]+ for keys,
which also matches a digit-leading token, so a comma followed by a time or
ratio inside a value (call:web_search{query:meet at 10:00, 11:00 tomorrow})
was misread as a new 11: key, truncating the query and injecting a bogus
argument. Require keys to start with a letter or underscore, matching the
identifier-start rule the wrapped path already uses (_GEMMA_NEXT_KEY_RE).
Add a regression test.

* Studio: treat markers/close-tags inside tool-call arguments as data

Four parser correctness fixes where a valid argument string was mistaken for
structure:

- DeepSeek: find the envelope-end token outside JSON strings, so a query/code
  argument containing the literal token no longer truncates the body and drops
  the whole call.
- GLM: locate the real </arg_value> as the one whose next token is <arg_key> /
  </tool_call> / end, so a value containing a literal </arg_value> (or
  </tool_call>) is kept instead of executing the tool with corrupted arguments.
- Attribute-form <function name="..."> envelopes now count in the embedded-marker
  guard, so a DeepSeek/Kimi marker inside a parameter value does not hijack the
  outer call and run the wrong tool.
- Wrapper-less Gemma call:NAME{...} is gated on the enabled tool names (parse and
  display strip), mirroring the Llama bare-JSON gate, so a disabled/example name in
  prose is not stolen as a call and the real answer is preserved.

Add regression tests for each.

* Gate route Gemma wrapperless strip by enabled tools; make Kimi section-end search string-aware

Route-level display stripping now threads the enabled tool-name set into the
Gemma wrapperless-call strip, so prose that mentions a disabled tool
(call:foo{...}) is preserved while active tool calls are still stripped. This
mirrors the parser-level gate already used in tool_call_parser.

The Kimi section-end lookup now searches outside JSON string literals, so a
section-end marker appearing inside an argument string no longer triggers a
false truncation that drops a valid tool call.

* Run DeepSeek/Kimi pre-pass when a closed tool-call example precedes a real block

The marker pre-pass was skipped whenever any <tool_call>/<function> opener
appeared before the first DeepSeek/Kimi marker, even when that opener was a
CLOSED syntax example in prose that ends before the real block. In that case
parse_tool_calls_from_text skipped the DeepSeek/Kimi parsers and the genuine
tool call was dropped while a phantom tool named in the example ran instead.

Only treat a marker as embedded in a leading envelope when removing the closed
outer <tool_call>/<function> envelopes also removes every marker (the marker
actually sat inside one). A marker left standing is a real call, so the pre-pass
runs. The legitimate case of a marker inside a closed outer envelope's arguments
is preserved.

* Honor reasoning_effort none in safetensors prefill; strip Magistral reasoning while streaming

Two safetensors/MLX reasoning fixes surfaced in review:

_sf_reasoning_prefill_mode only checked enable_thinking, so an
enable_thinking_effort (GLM-5.2) request that disables thinking via
reasoning_effort=none (without enable_thinking=False) still began in
prefilled-<think> mode. A plain answer with no </think> was then swallowed
whole into reasoning_content and the visible response came back empty. Thread
reasoning_effort into the predicate and treat none as disabled, mirroring
_request_reasoning_kwargs.

strip_tool_markup_streaming stripped tool markup but not the leading Magistral
[THINK]...[/THINK] bracket block, so the raw chain-of-thought leaked into the
streamed safetensors content instead of the reasoning drawer (GGUF routes it
natively). Apply _strip_mistral_reasoning first, matching the final strip; an
unclosed [THINK] is held from the marker on so nothing flickers.

* Heal truncated outer tool envelopes and keep quoted Gemma args intact

Two follow-ups from review of the marker pre-pass and Gemma parsing:

The leading-envelope guard only removed CLOSED outer <tool_call>/<function>
envelopes before deciding whether a DeepSeek/Kimi marker was embedded, so a
truncated outer call missing its close tag (whose argument embeds a marker) was
treated as a standalone marker and the embedded sample ran instead of the
intended outer call being Auto-Healed. Decide on the last outer opener before the
marker and whether it closed before the marker instead, so a closed syntax
example still runs the pre-pass while a real closed-or-truncated outer call keeps
it.

The wrapper-less Gemma argument scan tracked bracket depth but not quotes, so a
quoted value containing a comma followed by a key-like token (a search query such
as "weather, location: Boston") was split mid-string, truncating the value and
fabricating an extra argument. Track quote state (with escapes) so the top-level
comma boundary is only taken outside quoted spans.

* Span outer envelopes to their real close when locating embedded markers

Locating the DeepSeek/Kimi marker relative to a leading outer envelope used the
FIRST close tag after the opener, so a literal </function> or </tool_call> inside
an argument value (for example python code that contains the text) was mistaken
for the envelope boundary. The marker after it was then treated as a standalone
call and the embedded sample ran instead of the intended outer call.

Match the closed outer envelopes with the shared patterns that already extend to
the real final close (a literal close inside a value is data), and treat a marker
that survives their removal as embedded only when a still-open (truncated) outer
opener precedes it, so Auto-Heal still repairs a truncated outer call. A closed
syntax example before a genuine block still runs the pre-pass.

* Span the tool_call outer envelope to its real close in the marker guard

The leading-envelope check reused the lazy <tool_call>.*?</tool_call> strip
pattern, so a Qwen/Hermes JSON argument containing a literal </tool_call> ended
the span early. A DeepSeek/Kimi sample later in that same string then survived
the closed-envelope removal, and the pre-pass executed the embedded call instead
of the outer <tool_call>. The <function> arm already spanned to its real close;
give <tool_call> the same real-close pattern (with the negative lookahead that
keeps back-to-back calls separate) so a literal close inside a value is data.

* Preserve no-tool Gemma prose and keep later R1 calls when healing a close

Two review follow-ups:

_gemma_strip_gate returned None when no tools were enabled, and None means
strip every markerless call:NAME{...} block, so a no-tool answer that documents
the syntax (or the Anthropic display path, which passes an empty tool list as
None) had that prose deleted. It is a display/history gate, so return the
enabled-name set instead -- an empty set when no tool is enabled, which strips
nothing because every call:NAME{...} is then prose.

The DeepSeek R1 heal path located the close fence with an unbounded forward
search, so when a first call had balanced JSON but omitted its fence the search
landed on a LATER call's terminator and pos advanced past that valid call,
dropping it. Match the close immediately after the JSON (whitespace-skipped) like
the strict path, and advance by just the JSON when it is absent, so a multi-call
turn keeps its later well-formed calls (heal is now a superset of strict).

* Resume wrapper-less Gemma scan past a consumed call's balanced body

The markerless call:NAME{...} scan used finditer, which resumes right after the
opening call: token, so a nested call:OTHER{...} mentioned inside the first
call's own quoted string argument (for example a web_search query that quotes the
Gemma tool syntax) was re-matched and returned as a spurious second tool call,
executing an unintended tool. Walk with a manual cursor that resumes after the
outer call's balanced body (brace matching already skips quoted braces), so a
call's arguments are never rescanned. Genuinely separate back-to-back calls and
disabled/example prose are unaffected.

* Mistral outer call wins over XML literals; align healer signals with its parser

Two follow-ups on the shared-parser ordering after the healing-passthrough
merge:
- A well-formed [TOOL_CALLS] call whose JSON arguments quote tool XML parsed
  the literal instead of the outer call (executing the wrong tool). When the
  first XML signal sits inside a leading balanced Mistral body it is argument
  data, so the Mistral parser now runs first; an XML signal before the trigger
  keeps the normal order, so a [TOOL_CALLS] literal inside an XML call's
  arguments still stays data.
- passthrough_healing buffered streams on the parser module's broadened signal
  list (now including <|python_tag|> and [TOOL_CALLS]) but promotes with
  core.tool_healing, which does not parse those forms: a streamed Mistral or
  Llama text call was held until finalization and flushed as prose. The healer
  keeps its own signal list limited to the formats it can promote, restoring
  immediate streaming for the rest.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: Gemma wrapper-less marker literals and quotes, GLM embedded close pair

- The Gemma fallback deferral now keys on an actual wrapped opener
  (_GEMMA_TC_RE), not the wrapper literal anywhere in content: a wrapper-less
  call whose argument merely mentions <|tool_call> has nothing tool_healing
  can parse, and deferring it lost the call entirely (not executed and
  stripped from display).
- New _gemma_body_brace_end boundary scanner honors single- and double-quoted
  strings like _gemma_parse_stripped_body, shared by parse and strip, so a
  quoted brace in a code argument (code:print('}')) no longer truncates the
  executed arguments or the strip span.
- _glm_value_close now requires a structural </arg_value> to sit at balanced
  quote state: the full pair </arg_value></tool_call> embedded inside a string
  literal is data, not an early close. When no candidate balances, the first
  token-valid close wins as before.

* Address review: leading envelopes win over rehearsed literals

- New _first_foreign_tool_signal shared by the leading-envelope guards adds
  <|python_tag|> to the protected signal set: the spelled-out literal inside a
  Mistral call's arguments (a query about Llama built-in tool syntax) executed
  the inner literal instead of the outer call.
- New _xml_signal_inside_leading_bare_json guard, sibling of the Mistral one:
  a leading bare-JSON call whose string argument quotes tool XML (a code value
  citing <function=...>) had the literal promoted by the shared XML pass
  before the bare-JSON parser ran.
- Magistral [THINK]...[/THINK] is dropped once at parse entry instead of only
  inside the Mistral parser, so a call rehearsed in the think block in a
  foreign format can no longer be promoted while the real call after the
  block is lost. Parse now agrees with the display strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: a disabled leading bare-JSON object keeps its literals as data

When the leading bare-JSON object is ordinary content (name not an enabled
tool), the guard proved the first tool signal sits inside it, so falling
through to the XML/python_tag passes promoted quoted string data as a real
call. Drop the object and parse only the tail: a real call after the object
still parses, nothing inside it can be promoted.

* Address review: apostrophes in raw Gemma values, GLM strict key contract, per-model template token

- Quote openers in the wrapper-less Gemma boundary and body scanners now
  require value-start context (after : { [ ( , =): an apostrophe inside an
  unquoted value (query:what's the weather) opened quote mode, swallowed the
  real closing brace, and lost the whole call on common contraction queries.
  Quoted values keep hiding delimiters as before.
- A GLM <arg_key> with no <arg_value> tag now rejects the call in strict
  mode, matching the unclosed-value contract, instead of executing the tool
  with the argument silently dropped; Auto-Heal keeps the lenient skip.
- The native-template fallback reads the hf_token stored on the model record
  instead of the instance-wide last-load token, so a later token-less load
  cannot break template fetches for a previously loaded gated model (both
  the transformers and MLX backends).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: Mistral literals inside leading JSON, whitespace-tolerant wrapped Gemma opener

- The leading bare-JSON guard now treats the [TOOL_CALLS] trigger as a
  foreign signal: the Mistral parser runs before the bare-JSON one, so a
  literal quoted inside the leading object's strings was promoted over the
  outer call (or over ordinary JSON content).
- tool_healing's wrapped Gemma opener tolerates whitespace around call and
  the colon: sampling drift emits call: name{ and call : name{, and
  rejecting those lost the call entirely because no fallback re-parses the
  wrapped form. Strict mode still requires the closing tag.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: DeepSeek/Kimi markers inside leading JSON and Mistral envelopes stay data

The DeepSeek/Kimi pre-pass runs before the outer-call parsers, and
_marker_inside_leading_envelope only protected XML envelopes: a marker
quoted inside a leading bare-JSON or Mistral call's argument strings was
promoted as a separate no-arg call and the real outer call dropped. The
guard now recognizes those two leading envelopes as well; standalone
DeepSeek/Kimi calls keep parsing.

* Address review: accept dotted Gemma argument keys in the key-quoting scanner

The scanner quoted keys of [alnum_-] only, so a dotted key (user.name:...)
was left unquoted, json.loads failed, and the whole wrapped call was lost
(parse empty, strip wipes the markup). Dots now match the parser's own
key/name charset.

* Address review: a real DeepSeek/Kimi call after a disabled leading JSON object still parses

DeepSeek/Kimi markers are foreign signals for the leading bare-JSON guard
too: a marker literal inside a disabled leading object made the envelope
guard skip the pre-pass for the whole message, so a real DeepSeek/Kimi call
after the object was dropped. Routing the case through the guard's
drop-and-parse-the-tail recursion reaches the real call while the literal
inside the object stays data.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: leading Mistral call owns the turn, dotted keys after bare values

- A LEADING parseable [TOOL_CALLS] call now runs the Mistral parser first
  unconditionally: literal XML in trailing prose after the call was promoted
  by the earlier shared XML pass, executing the quoted example instead of
  the real leading call. XML leading keeps the normal order.
- _GEMMA_NEXT_KEY_RE accepts dots so a dotted key after a bare value
  (query:foo,user.name:bob) ends the value at the comma instead of being
  swallowed into it, matching the round-earlier key-quoting charset.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: a leading wrapper-less Gemma call owns the turn

A quoted foreign literal inside a leading wrapper-less Gemma call's
argument (a query citing another tool syntax) was promoted by tool_healing
before the Gemma fallback ran, executing the quoted example and dropping
the outer call. New leading guard, sibling of the Mistral and bare-JSON
ones, gated on an enabled name since the form is markerless. Foreign markup
leading keeps the normal order.

* Fix merge resolution: restore both leading-guard test classes intact

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: markup quoted inside a nameless leading JSON answer stays data

The leading bare-JSON guard required a top-level name, so a structured JSON
answer quoting tool markup in its strings (a response_format turn
documenting a tool's syntax) had the literal promoted by the later passes.
A nameless leading object that parses as real JSON now routes through the
same decline-then-parse-the-tail path; non-JSON braced prose keeps the old
behaviour, and a real call after the answer still parses.

* Address review: JSON answers stay data, nested Gemma quotes, earliest envelope, no failure caching

- A whole-content JSON value is a structured answer: the markerless Gemma
  scan and its strip no longer promote or strip a quoted example of an
  enabled tool's syntax inside it.
- Nested stripped-stream Gemma values now unquote quoted string leaves
  recursively, so {loc:{city:"New York"}} hands the tool New York, matching
  the top-level coercion.
- The DeepSeek/Kimi pre-pass dispatches by earliest envelope opener, so a
  leading real call wins over a trailing example of the sibling format in
  either direction.
- A failed native-template fetch is no longer cached as no-template: the
  next call retries after the model record's token is fixed or a transient
  Hub error clears; only definitive loads are cached.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: closed calls precede the marker pre-pass, truncated Gemma scan stops, quoted nested delimiters

- A closed non-DeepSeek/Kimi call preceding the first DS/Kimi marker owns
  the turn: a trailing syntax example, or one quoted inside a wrapped Gemma
  argument, was promoted by the pre-pass and dropped the real leading call.
  Wrapped Gemma joins the outer-envelope pattern sets.
- An unbalanced wrapper-less Gemma call now stops the scan (mirroring the
  strip contract) instead of resuming inside its own argument text, where a
  quoted enabled call would be promoted.
- Raw-quoted strings in nested stripped-stream Gemma values hide delimiters,
  so {city:"New, York"} is one value instead of a split pair, returned
  unquoted like the top-level coercion.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: string-marker literals in wrapper-less args, mid-value quoted phrases

- The wrapper-less deferral guard no longer keys on the <|"|> literal: a
  real call whose argument merely mentions the string marker was deferred to
  tool_healing, which has no wrapped opener to parse, losing the call. The
  wrapped-opener check alone owns the deferral.
- Double quotes now also open at the start of a word, so a quoted phrase
  mid-value (query:find "weather, location: Boston", limit:3) hides its
  delimiters instead of splitting the value into garbage keys; apostrophes
  keep the value-start-only rule so contractions stay prose.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: strict GLM refuses in-quote close fallback, Gemma guard covers preambles

- _glm_value_close gains a strict flag: a truncated value whose only close
  candidates sit inside a string literal rejects the call in strict mode
  (Auto-Heal keeps the lenient partial), restoring the strict contract the
  quote-aware fallback had weakened.
- The leading wrapper-less Gemma guard no longer requires the call to open
  the response: a visible preamble before call:NAME{...} is the normal
  shape, and the quoted foreign literal inside the argument was promoted
  again in that shape. An enabled balanced call beginning before the first
  foreign signal owns it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: contextual GLM quote openers, disabled Gemma examples stay prose, JSON array answers

- The GLM value-close quote tracker uses the same contextual openers as the
  Gemma scanners (single quote after punctuation context, double quote also
  at word start), so strict mode accepts a normal apostrophe value again
  while still rejecting a truncated value whose only close candidates sit
  inside a string literal.
- A disabled wrapper-less Gemma call is prose by design, so a tool literal
  quoted inside it no longer promotes: the span is dropped for parsing and
  the tail parsed, mirroring the nameless-JSON guard.
- Leading JSON ARRAY answers join the leading-JSON envelope guard, so a
  marker quoted inside a structured array response stays data.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Align closed-envelope regression test with the document-order contract

The test asserted the pre-round-13 behavior (trailing DeepSeek/Kimi block
wins over a leading closed envelope) while the shipped rule is document
order: the leading closed call owns the turn. Rename the test and assert
the leading call so the suite matches the contract exercised by
test_leading_xml_call_wins_over_trailing_kimi_example.

* Parse a leading Llama-3.2 bare-JSON call before the markerless Gemma scan

The bare-JSON form only ever matches a leading call object, and document
order says that call owns the turn. Running the Gemma wrapper-less scan
first let an enabled call:NAME{...} snippet quoted inside the leading
call's string arguments steal the turn when the JSON was not the whole
content (trailing prose or a second ;-separated call), executing the
quoted tool instead of the real one. Reordering cannot take a leading
Gemma call's turn since that content never starts with an object brace.

* Leading-call ownership: Mistral trigger in Gemma guards, closed bare JSON before markers, depth-aware nested Gemma values

Three parser gaps against the document-order contract:

The wrapperless Gemma leading guards did not count [TOOL_CALLS] as a
foreign signal, so a leading Gemma call quoting a Mistral snippet in its
argument lost the turn to the quoted literal. Both the enabled-call and
disabled-example guards now include the trigger, matching the bare-JSON
guard's local inclusion.

_marker_inside_leading_envelope required the DeepSeek/Kimi marker to sit
inside the first closed bare-JSON or Mistral call. A marker after that
closed call (a trailing example or data in a later ;-chained call's
strings) now also defers to the leading call, the same inside-or-after
rule the closed XML envelope patterns already applied.

The nested Gemma primitive value scan split on every comma, corrupting
arguments like opts:{code:print(1,2),lang:py}. It now applies the same
paren/brace depth, contextual quote openers, and comma-only-before-a-key
mapping rule as the top-level scan.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Gemma leading guard: a closed enabled call preceding the signal owns the turn

The wrapperless Gemma guard only claimed the turn when the first foreign
signal sat inside the first enabled balanced call. When that call closed
before the signal (a second call quoting a Mistral or Kimi literal, or a
trailing prose example), the guard forfeited the turn and the foreign
parser promoted the quoted literal, dropping the real Gemma calls. Apply
the same inside-or-after ownership rule as the closed bare-JSON and
Mistral envelopes, gated on an enabled name so the name-agnostic legacy
path is unchanged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Marker guard: only an executable leading bare-JSON call owns the turn

The bare-JSON branch of the leading-envelope marker guard claimed the
turn for any NAMED leading object. A disabled-name object is prose by
design (the bare-JSON parser will not execute it), so deferring the
DeepSeek/Kimi pre-pass to it lost the real later call entirely. Gate the
ownership claim on the enabled set (or the name-agnostic None path). A
marker inside the disabled object's own strings stays data, matching the
tail-exclusion contract; a marker after it now falls through so the
pre-pass parses the real call. The Mistral branch stays ungated since
[TOOL_CALLS] parsing is never name-gated.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Gemma scan skips leading JSON answers; GLM heal bounds values at structural tags

Two fixes to the document-order data contracts:

The markerless Gemma scan only exempted whole-content JSON, so a leading
JSON answer followed by prose had an enabled call:NAME{...} snippet
inside its strings promoted to a real executed call and stripped from
the displayed answer. Both the parse and strip scans now start after a
balanced json-valid leading value span, keeping parse and strip
mirrored. Real calls after the answer still parse; mid-prose JSON gets
no exemption.

The GLM heal fallback for a missing closing arg_value tag took the
entire remainder as the value, executing markup-contaminated arguments
like city="NYC</tool_call>" and swallowing trailing prose. The healed
value now stops at the next arg_key or tool_call close and the pair walk
resumes there. EOF-truncated values keep the partial heal, strict mode
still rejects, and closed values holding a literal close tag in quotes
are untouched.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Compress docstrings in the multi-format tool parser to their contract essence

* Condense parser guard comments and test narration to contract essentials

* verify_import_hoist: exempt __future__ imports and same-diff relocations

Two false positives fired on this PR's refactor. A from __future__ import
is a compiler directive whose name never appears as a runtime load, so
HOISTED-IMPORT-UNUSED can never see it used, yet the file requires it for
PEP 604 annotations on Python 3.9. TARGET-CHANGED flagged the deliberate
move of the strip-pattern constants into core.inference.tool_call_parser
as a silent re-point even though the old module-level target was removed
and the new one added in the same diff. Both get narrow exemptions; a
re-point to a pre-existing target is still caught, and the self-test
negative controls all pass unchanged.

* Leading bare-JSON calls own the turn; function calls end at the first balanced close

The XML-signal guard for a leading bare-JSON call required the signal
strictly inside the object, so a trailing XML example stole the turn
from the leading call; it now applies the same inside-or-after rule as
the Mistral guard. Function-XML calls also ended at the LAST close tag,
which let prose after a closed call that mentions a literal close tag
get swallowed into the final parameter value; calls now end at the
first close tag that is not inside an open parameter, and the strip
mirrors the same rule so parse and strip agree.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Attribute-form calls end at the first balanced close; bare-JSON strip requires the call shape

The attribute form parser still kept the last close tag in the call
window, folding prose after a closed call into the final parameter
value. It now takes the first close not inside an open parameter, the
same rule the equals form and the strip already use.

The leading bare-JSON strip deleted any closed object whose top-level
name matched an enabled tool, including plain JSON answers the parser
correctly rejects as non-calls. The strip (and the drain gate that
delegates to it) now requires the parser's exact call shape, so answers
like {"name":"web_search","result":...} stream and display intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* False-alarm markers keep the answer; the bare-JSON strip consumes the whole chain

The trailing strip arms dropped everything from a bare marker to EOF,
so a normal answer that mentions [TOOL_CALLS] or another marker
literally was truncated (or fully swallowed when it started with the
literal) after the no-call drain fallback. Those arms now require a
call-shaped lookahead or marker-at-EOF before dropping; truncated real
calls still strip.

Chained bare-JSON turns executed both calls but stripped only the first
object, so the second call's raw JSON replayed into the next assistant
history message alongside the structured tool_calls. The strip now
consumes the entire chained run of call-shaped enabled objects while
non-call answers, disabled names, and trailing prose stay intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* DeepSeek and Kimi trailing strip arms require a call-shaped lookahead

Same false-alarm rule as the bare-word markers: a prose answer that
mentions a DeepSeek or Kimi marker literally keeps its tail, while
truncated real envelopes and bare end-of-text fragments still drop.

* Attribute-form containment, parameter-close-decides rule, preamble-tolerant Mistral guard, strict strip shape

Four document-order and containment fixes. A leading attribute-form
call now parses before the shared XML pass, so markup quoted in its
parameter stays data. The open-parameter scan lets the parameter's own
close tag decide, so any number of literal function closes inside one
value stay data, restoring the pre-close-scan behavior for multi-close
arguments. The leading-Mistral guard tolerates a visible preamble, with
the leading-bare-JSON guard running first so a trigger quoted inside a
leading JSON object stays data. The bare-JSON strip requires the
parser's top-level name in every mode, so nested-name JSON answers
survive name-agnostic stripping.

* Keep buffering long wrapper-less Gemma tool names instead of leaking the prefix

The streaming buffer stopped holding a call:NAME prefix at a fixed
32-char cap, so a Gemma wrapper-less call to a tool whose name exceeds
that (OpenAI allows 64 chars, MCP names run longer) streamed its raw
call:longname text as visible content before the end-of-turn parser
executed it. Hold the variable-length prefix while it still matches the
call: shape, bounded like the bare-JSON path and self-terminating into
prose, draining once the opening brace arrives.

* Keep prose that only mentions DeepSeek/Kimi markers in the route display strip

The route-level _TOOL_XML_RE DeepSeek/Kimi arms consumed from an opener up to
the end of text whenever the marker appeared, so an answer that merely refers
to a marker (for example "See <|tool_call_begin|> in the docs") had the rest
of the reply truncated. The parser-level _TOOL_ALL_PATS already gates these
arms with a call-shaped lookahead. Mirror it here so a marker is only stripped
when a real call follows it or it is a bare fragment at end of text.

* Tighten tool-calling parser and backend comments

* Pass trust_remote_code when reloading native tokenizers

The native-template fallback re-fetches a model's native chat template from
its repo when an Unsloth override template drops the tools schema. The
secondary AutoTokenizer.from_pretrained threaded hf_token but not
trust_remote_code, so for a model loaded with trust_remote_code=True whose
tokenizer repo carries custom code the reload raised, was swallowed, and the
request silently kept the tool-dropping prompt for a model that supports tools.

Store the loaded trust_remote_code on each backend's per-model info dict and
source it in render_native_template, so the reload re-uses exactly the consent
granted at load. For a LoRA adapter the reload targets the base model, whose
remote code was gated and loaded under the same stored flag, so re-passing it
executes no unconsented code. Falsy stored flag preserves the prior behaviour.

Adds a regression test that fails without the flag (custom-code reload raises,
returns None) and passes with it (tools-advertising native prompt returned).

* Treat <|python_tag|> as an outer marker envelope

A Llama-3 <|python_tag|> tool call (built-in NAME.call(...) or custom
{json} form) whose argument quotes a complete DeepSeek/Kimi example was
hijacked by the DeepSeek/Kimi marker pre-pass: the embedded example (for
example delete_all) executed instead of the real outer call. python_tag
is Llama-3's tool-call envelope, so a marker quoted inside its arguments
is data, the same as for <tool_call>, <function=...>, bare JSON, Mistral
and wrapper-less Gemma, which the guard already covers.

Add <|python_tag|> to _OUTER_ENVELOPE_OPEN_RE with a call-shaped
lookahead (mirroring the _TOOL_ALL_PATS python_tag arm) so the marker
pre-pass is suppressed when a python_tag call opens before the first
marker, while a bare prose <|python_tag|> mention is left untouched.

* Tighten tool-call parser comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
Co-authored-by: Daniel Han <info@unsloth.ai>
Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com>
2026-07-06 15:40:46 -07:00
Daniel Han
eb1ef44255
Studio: Gemma tool-call streaming follow-ups + nested-XML escape fix (#6476) (#6611)
* Quote-aware Gemma strip, symmetric unstarted cleanup, ReDoS anchor

Address review findings on the tool-strip and streaming paths:

- strip_tool_call_markup stripped Gemma-native spans with a plain regex that
  stops at the first <tool_call|>, so a literal close marker inside a
  <|"|>-quoted argument truncated the span and leaked its suffix into visible
  text. A brace/quote-aware _strip_gemma_native_spans now removes complete
  spans (keeping an incomplete one unless final), matching the parser's own
  balance logic.

- The Gemma close pattern this PR added (<\|tool_call>.*?<tool_call\|>) had no
  \Z fallback, so a run of unclosed markers backtracked from every open
  position (quadratic, and the streaming stripper re-scans per token). It is
  now anchored to (?:<tool_call|>|\Z) like routes/inference.py's _TOOL_XML_RE,
  linear with identical output on well-formed input.

- _SameTaskStreamingResponse added unstarted_cleanup for the OpenAI passthrough,
  but the local GGUF/safetensors streams that enter _TrackedCancel before
  returning only unregister in the generator finally, which never runs if the
  client disconnects before the body iterator starts, leaking cancel-registry
  entries. Each such stream now passes unstarted_cleanup to exit its tracker.

- __call__ reads _unstarted_cleanup via getattr so a response built through
  __new__ (the cancel-timing test) without __init__ does not raise
  AttributeError; the test also sets the attribute explicitly.

- Document that the verbatim /v1/chat/completions passthrough delegates
  <think>/<|tool_call> splitting to llama-server (--jinja, --reasoning-format
  auto) and is intentionally not re-parsed locally, noting the llama.cpp
  dependency.

Adds a regression test for the close-marker-inside-quoted-argument strip.

* Tighten comments on the tool-strip and streaming paths

Compress the verbose comment blocks added with the Gemma tool-call / streaming
work to crisp one or two liners, drop restatements of obvious code, and shorten
docstrings, keeping the load-bearing rationale (ReDoS anchor, quote-aware strip,
unstarted-cleanup, llama.cpp passthrough dependency). Code is unchanged
(verified comment-only via AST/ast signature, docstrings stripped).

* Harden Gemma parse/strip: span-aware XML fallback and quote-aware streaming

- Security: the XML fallback in parse_tool_calls_from_text scanned the whole
  content for <function=...> markers and only skipped those inside an open XML
  parameter, not those inside a collected JSON/Gemma candidate span. A balanced
  but unparsable Gemma call whose argument data contained XML tool markup
  (<|tool_call>call:outer{code:<function=terminal>...}<tool_call|>) therefore
  fell through to the fallback and returned an executable terminal call. The
  fallback now also excludes <function=> markers inside any candidate span,
  including ones that failed to parse.

- strip_tool_call_markup no longer skips the generic Gemma regex after running
  the quote-aware _strip_gemma_native_spans, so a closed Gemma span the helper
  cannot match (malformed, e.g. <|tool_call>{"name":"x"}<tool_call|>) is still
  stripped instead of leaking its opener and payload into visible text.

- _strip_gemma_native_spans stops at the first unbalanced start instead of
  re-scanning every later start to EOF, keeping it linear on a run of unclosed
  markers rather than quadratic.

- The GGUF and safetensors streaming strippers run _strip_gemma_native_spans
  before the regex patterns, so a well-formed streamed call whose quoted
  argument contains a literal close marker no longer leaks its suffix into
  incremental display.

Adds regression tests for the nested-XML escape and the malformed-span strip.

* Avoid remainder copy in _strip_gemma_native_spans

Match the Gemma close marker with re pos directly on the buffer instead
of slicing tail = text[brace_end + 1:] on every span. The streaming
strippers re-scan a growing cumulative buffer per token, so the per-span
remainder copy was quadratic. Behavior is unchanged.

* Exclude unclosed Gemma/JSON starts from the XML tool-call fallback

The nested-XML guard only skipped <function=> markers inside recorded
candidate spans, but a span is recorded only when the braces balance. An
unbalanced call such as <|tool_call>call:outer{code:<function=terminal>...
recorded no span, so the fallback still promoted the inner <function=> to
an executable terminal call. Treat unclosed JSON/Gemma starts as exclusion
spans through EOF before scanning. Standalone <function=> calls with no
preceding unclosed start still parse. Regression tests added.

* Skip doomed tool-strip passes to avoid quadratic rescans

The lazy closed-pair strip patterns (<tool_call>.*?</tool_call>,
<function=...>.*?</function>) rescan to EOF from every opener when their
close token is absent, which is O(n^2) and re-runs per streamed token. Add
strip_tool_patterns, which skips a pass whose close token is not present in
the text; output is identical to the per-pattern loop (verified by fuzz),
and a degenerate run drops from ~minutes to milliseconds. Used by
strip_tool_call_markup and the GGUF/safetensors streaming strippers.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Use full tool-call envelopes to close nested-XML escape variants

Key the parser and stripper off the full <|tool_call>...<tool_call|> /
<tool_call>...</tool_call> envelope (start to close marker, searched after
the braces; EOF if unclosed) instead of just the braces:

- XML between the closing brace and the close marker
  (call:outer{broken:{x}}<function=terminal>...<tool_call|>) is now inside
  the envelope, so the fallback no longer promotes it to a tool call.
- A balanced inner call inside an unclosed outer
  (call:outer{code:<|tool_call>call:terminal{...}<tool_call|>) is skipped
  via the envelope nested check, not just the XML fallback.
- strip_tool_call_markup searches for the close marker after the braces, so
  junk before <tool_call|> is stripped through the close and text after it is
  preserved instead of truncated to EOF; a no-close run stops early (linear).

Regression tests added; standalone XML and well-formed calls unaffected.

* Fix non-final Gemma strip and missing-close recovery for PR #6611

Split the nested-skip from the XML fallback exclusion: nesting is decided by
each marker's brace region, so a balanced call after one with a missing close
marker is recovered instead of being swallowed to EOF. Only the XML fallback
keeps the search-to-close envelope, so trailing nested markup still cannot
escape as an executable call.

Use a closed-only Gemma pattern in the non-final strip list so an incomplete
block is preserved (matching the JSON and function paths); the final list keeps
the close-or-EOF Gemma pattern in its original position, so streaming display
output is byte-for-byte unchanged.

Add regression tests for both cases.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Block gap-nested tool markers and fix XML strip order for PR #6611

Decide candidate nesting by a per-marker coverage region paired with a
per-format stack (a close after the braces pops the nearest still-open marker
of that format). A closed outer call now covers up to its own close marker, so a
JSON or Gemma tool marker smuggled between the outer braces and that close is
treated as data instead of being executed. An outer that balances but has no
close of its own covers only its brace region, so a later sibling after an
omitted close marker is still recovered (adjacent calls use an exclusive end
bound so the next call is not misread as nested).

Strip every closed pair (JSON, Gemma, function) before any to-EOF sweep, so a
closed function call whose parameter text contains a bare Gemma opener is
removed as a unit and the to-EOF sweep can no longer drop the visible text after
the close.

Add regression tests for both.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Strip closed tool blocks before the Gemma final sweep for PR #6611

The final display strip ran the quote-aware Gemma helper before the closed
JSON/function patterns. A closed <tool_call>...</tool_call> or
<function=...>...</function> block whose argument data held a call-form Gemma
opener (e.g. a "<|tool_call>call:t{" string) was read as an incomplete Gemma
span and truncated to EOF, dropping the block's close and any visible text after
it.

Strip closed JSON/function blocks first, so such a block is removed as a unit
before the helper runs. Centralize the final strip order in a shared
strip_tool_markup_final so strip_tool_call_markup and both streaming display
wrappers (safetensors, llama_cpp) stay in sync, and apply the same closed-block
pre-pass to the non-final path.

Add regression tests for the JSON and function variants.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Recover XML/JSON siblings after a close-less tool marker for PR #6611

Two fixes so the XML fallback and marker coverage recover a later valid call
after an earlier marker omits its close, matching the candidate loop:

Reuse the candidate marker-coverage in the XML fallback instead of a separate
search-to-close-or-EOF envelope. A balanced but close-less marker now covers
only its brace region there too, so a following <function=...> sibling is
recovered rather than filtered as nested data; an unbalanced marker still covers
to EOF and a closed one still covers through its close, so nested XML stays
blocked.

Ignore a close token that falls inside another call's balanced braces when
pairing closes in _marker_coverage. Such a token is that call's quoted argument
data, so it no longer pops an earlier close-less marker and extends its coverage
over a later valid sibling.

Add regression tests for both.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the closed-block strip pre-pass Gemma-span-aware

The final display strip ran the closed JSON/function regex pre-pass before
removing Gemma-native spans, so a literal <function=...> quoted inside a Gemma
argument plus any later </function> (a real call's close or even prose) was
deleted across the Gemma boundary. That mangled the Gemma close marker, the
quote-aware helper then saw an unclosed opener, and the whole visible tail
after the call was truncated.

The pre-pass now skips matches that start inside a complete Gemma span (that
text is the span's argument data) and resumes scanning at the end of the
covering span, so a real function-XML call after the Gemma call is still
stripped. The original ordering rationale is preserved: a Gemma opener inside
a JSON or function argument still cannot truncate that block, covered by
regression tests for both directions.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Trim comments in the Gemma streaming and strip pipeline to essentials

* Tighten comments in the Gemma strip and streaming disconnect paths

* Fold marker-collection comment to two lines

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 10:39:37 -07:00
Daniel Han
e9ea45b6a5
Studio: coerce tool_call arguments to dict before chat templating (fixes MLX tool follow-up error) (#6807)
* Studio: coerce tool_call arguments to dict before chat templating

Strict tool chat templates (e.g. mlx-community Qwen3.5 checkpoints) iterate
arguments.items() and raise "TypeError: Can only get item pairs from a mapping"
when a prior assistant tool call is re-rendered on the next turn. The agentic
loop stores arguments in the OpenAI JSON-string form (as_assistant_tool_call),
which is correct on the wire and for llama-server, but the transformers / MLX
paths apply_chat_template directly and hit the strict Jinja templates.

Normalize each assistant tool_call's function.arguments from a JSON string to a
dict inside apply_chat_template_for_generation (shared by both the MLX and
safetensors paths). A dict renders on strict and lenient templates alike;
non-JSON / non-dict values are left untouched, and the OpenAI-format
as_assistant_tool_call (used by the GGUF path + API responses) is unchanged.

Verified against the real mlx-community/Qwen3.5-2B-8bit template: string args
raised the tester's error, the fix renders cleanly, and the lenient
unsloth/Qwen3.5-0.8B template still works.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: make tool-arg coercion a string-first fallback (non-regressive)

Render the original OpenAI string-arg form first and only coerce arguments to a
dict when the template raises the mapping TypeError, instead of always coercing.
Any template that already renders is now byte-identical (a template that emits
arguments verbatim keeps the JSON string, not a Python dict repr).

Verified across Llama-3, Qwen2.5, Qwen3, Qwen3.5, Phi-3.5 (byte-identical) and
mlx-community/Qwen3.5-2B-8bit (strict -> fixed). Gemma-3 / Mistral tool-template
errors are unrelated (role alternation / tool-id length) and identical with or
without the change.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make core.inference package init lazy so dependency-light helpers import standalone

Importing any core.inference submodule ran the package __init__, which
eagerly imported orchestrator and llama_cpp; both pull loggers ->
structlog (and httpx), so a dependency-light helper like
chat_template_helpers dragged in the full heavy stack and its unit test
failed to collect in a backend env without structlog. Defer those
imports to attribute access via PEP 562 __getattr__, mirroring the lazy
pattern already in core/__init__.py. The re-exports resolve unchanged on
first access.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Retry dict-coercion for strict templates that raise non-TypeError

apply_chat_template_for_generation only retried the OpenAI JSON-string arguments
coercion when the first render raised TypeError (the arguments.items() form). The
bundled gemma-4.jinja instead rejects string arguments with raise_exception, which
surfaces as a Jinja error, so a second tool turn with string function.arguments
propagated and failed rather than retrying with the parsed dict.

Broaden the outer catch to Exception, still gated on there being a string arg to
normalize (normalized is messages -> re-raise), so unrelated template errors and
templates that already render are unaffected.

* Tighten comments in tool-call argument coercion helper and tests

* Tighten tool-call argument coercion comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 10:12:22 -07:00
Daniel Han
e9f49c62dd
studio: deterministic backend tool-calling wiring test (#6836)
* studio: deterministic backend tool-calling wiring test

Add a deterministic, download-free test that exercises the shared tool-calling
seam both inference backends use. InferenceBackend (transformers) and
MLXInferenceBackend both render the prompt through
apply_chat_template_for_generation(..., tools=...) and stream cumulative text
into run_safetensors_tool_loop. The existing test_safetensors_tool_loop.py
covers the parser and the loop state machine with fake generators but does not
cover the backend's own tool-injection seam, so a regression that drops the
tool schema before the tokenizer, or fails to feed a tool result back into
generation, would slip through.

The test drives that seam with fakes: a tokenizer that records the tools it is
handed, a canned tool-call generation, and a stub executor. It asserts the full
chain: tools reach the chat template, the loop parses the call, the tool is
dispatched once with the parsed arguments, the result is fed back, generation
re-enters, and the final answer streams after the tool result. It also guards
that the raw tool-call markup never leaks to the client as content.

The test imports no torch, unsloth, or mlx, so it runs in the portable Backend
CI alongside the tool-call parser tests and stays sub-second. Follow-up to the
parser test PRs #5620 and #5704.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: assert the tool result is fed back before the final turn

Strengthen the wiring test so single_turn records each turn's conversation and
the test asserts the tool result message is present in the conversation handed
to the final generation turn. Event ordering alone did not catch a loop that
stops appending the tool output before re-entering generation, because the fake
generation ignores the conversation; this closes that gap.

* studio: tighten comments in tool-calling wiring test

* studio: shorten comments in tool-calling wiring test

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 10:08:39 -07:00
Daniel Han
f38672da65
Studio: stop chat generation on the assistant-turn-end token (fixes Qwen3.5 loop) (#6804)
* Studio: stop chat generation on the assistant-turn-end token

A small chat model (e.g. Qwen3.5-0.8B) looped on the safetensors path: it emitted
a valid response or tool call, then ran past its turn and re-emitted the call,
hallucinating <|im_start|>user turns. Root cause: the model's tokenizer.eos_token
is synced to the config document terminator (<|endoftext|>, 248044) while chat
turns actually end with <|im_end|> (248046), so generate_stream's single
eos_token_id never stopped at the turn boundary.

Stop on every assistant-turn-end marker the vocab defines (tokenizer.eos plus
<|im_end|>, <|eot_id|>, <end_of_turn>, ...). Verified on the real weights: the
single-eos control loops (400 tokens) while the fixed set yields a clean 38-token
tool call and a clean answer from the tool result. No-op when eos is already the
turn-ender (the id just dedups).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: repair chat generation_config.eos_token_id at load time

Qwen3.5 / Qwen3.6 small chat checkpoints declare the chat turn-end as
tokenizer.eos_token (<|im_end|>) but ship config.eos_token_id = <|endoftext|>
and no generation_config.json (upstream shipped generation_config only on the
large chat models). So every .generate() path that reads generation_config -- the
vision path and tool loops, not just generate_stream -- never stops at the turn
boundary and loops.

At load time, when the tokenizer's own eos is a chat turn-end marker but
generation_config.eos_token_id omits it, add it. This fixes the config once for
all generation paths and complements the generate_stream turn-end stop. No-op for
base models (eos is a plain document terminator) and already-correct configs.
Verified on unsloth/Qwen3.5-0.8B: 248044 -> [248044, 248046].

* Studio: derive chat turn-end eos from the template, resolve once at load

Address PR review of the turn-end stop handling:
- Do not call tokenizer.get_vocab() per generation request (serializes the whole
  100k+ vocab). Resolve the turn-end tokens once at load and cache them on
  model_info; generate_stream reads the cache.
- Derive turn-end markers from the chat_template the model actually uses, not raw
  vocab membership, so a base/coder model that merely carries ChatML control
  tokens in a shared vocab is not stopped early, and a loader that synced
  tokenizer.eos to the document terminator is still covered.
- Skip harmony/gpt-oss templates: <|end|> there is an intra-message channel
  delimiter, not the turn end (dropped <|return|> from the marker list too).
- Move the logic to a dependency-light module (core.inference.chat_eos) so the
  unit test does not import the full unsloth/torch inference stack.

Verified on unsloth/Qwen3.5-0.8B (gen_config 248044 -> [248044, 248046], clean
38-token tool call with generation_config-only stopping), Phi-3.5 (adds <|end|>),
Llama-3 / Qwen3 (unchanged), and a harmony template (left untouched).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: refresh turn-end eos after the mapper installs its template

For a MODEL_TO_TEMPLATE_MAPPER model whose own tokenizer ships no
chat_template, the effective template is applied at generate time via
get_chat_template, but the turn-end eos ids were resolved once at load when
the template was still empty, so only the document eos was cached. Qwen2.5 /
Yi base checkpoints (eos <|endoftext|>, ChatML turns end with <|im_end|>)
then run past the assistant boundary in generate_stream and loop.

Re-resolve the turn-end eos from the now-templated tokenizer and refresh the
cached ids right after applying the mapper template, so generate_stream stops
at the ChatML turn end. Add a regression test.

* Studio: union turn-end eos refresh into load-time cache instead of overwriting

get_chat_template can return a different tokenizer whose vocab was remapped
(Gemma folds <end_of_turn> onto the eos id), while generate_stream re-reads the
original model_info tokenizer. Overwriting the cache with the refreshed set
dropped a valid load-time id (e.g. <end_of_turn>=107) and let generation run
past the real turn marker. Union the refresh into the existing cache so it can
only add ids, never drop a valid one. Add a regression test covering the
destructive-swap case the prior test missed.

* Studio: resolve refreshed turn-end ids on the generation tokenizer, add Gemma-4 marker

Two residual gaps in the turn-end eos refresh:

- For map_eos_token=True mapped templates (e.g. chatml on a Yi-6B base), get_chat_template
  returns a tokenizer whose vocab folds the turn-end token onto the document eos id, while
  generate_stream re-reads the original tokenizer. The refresh resolved ids on the returned
  tokenizer, so it stored the doc eos and missed the real turn-end id, and generation ran
  past the boundary. Read the turn-end marker strings from the mapped template but resolve
  their ids on the original generation tokenizer (new resolve_chat_turn_end_eos_ids_using).

- Add Gemma-4's <turn|> turn terminator to the marker allowlist; those templates keep a
  document eos so resolve otherwise missed the real turn marker.

Add regression tests for both.

* Fix turn-end detection for Starling, multi-variant and vision templates; keep tests collectable

The turn-end marker set missed OpenChat/Starling's barred <|end_of_turn|>
(distinct from Gemma's unbarred form), so Starling generations ran past
the assistant boundary. A dict/list chat_template (Hermes-3 style
default+tool_use variants) hit an early non-string return and skipped
detection; flatten and scan every variant. Vision models carry the
chat_template on the ProcessorMixin, not the unwrapped inner tokenizer,
so read markers from the template-carrying container while resolving ids
on the generation tokenizer.

The refresh test constructs the real backend, so it is guarded with a
module-level skip when unsloth/unsloth_zoo is absent (the lightweight
pytest matrix), and core.inference package init is made lazy so the
dependency-light chat_eos tests collect without the heavy stack.

* Studio: tighten chat turn-end eos comments

* Studio: condense chat turn-end eos comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 10:07:56 -07:00
Daniel Han
f0a5c52821
studio: tool calling + healing parity for Llama-3, Mistral, Gemma 4 on safetensors + MLX (#5620)
* studio: tool calling for Llama-3, Mistral, Gemma 4 on safetensors + MLX (#5615)

Adds tool calling for Llama-3, Mistral (pre-v11 + v11+ + [ARGS]), and Gemma 4 to the safetensors / transformers and MLX backends. Parser patched against llama.cpp / vLLM / SGLang per-family parsers and normalises to OpenAI shape. 96 targeted unit tests + cross-OS staging CI (ubuntu / macos-14 / windows) green on the multi-format probe.

* studio: tool-call healing parity between safetensors / MLX and GGUF

After the multi-format parser landed in #5615, the safetensors / MLX
agentic loop and the GGUF loop still differed on healing behaviour.
This commit closes the gaps in both directions so the two backends
react the same way to identical model output.

Changes:

1. core/inference/llama_cpp.py -- the GGUF BUFFERING state machine
   now wakes on every emission marker the shared parser knows. Was
   ("<tool_call>", "<function="); is now the five-tuple imported
   from core.inference.tool_call_parser (Qwen / Qwen3.5 / Llama-3
   <|python_tag|> / Mistral [TOOL_CALLS] / Gemma 4 <|tool_call>).
   Stream cleanup is delegated to the same shared strip_tool_markup
   so leaked markup from any family is removed from assistant
   content.

2. core/inference/llama_cpp.py -- per-tool canonical heal key. When
   a tool arguments field is a bare string and JSON parsing fails,
   the GGUF path now heals to {"code": raw_args} for python,
   {"command": raw_args} for terminal, and {"query": raw_args} for
   everything else. Was hard-coded to {"query": raw_args}, which
   silently routed every python / terminal emission through
   web_search. Mirrors safetensors_agentic._CANONICAL_HEAL_ARG.

3. core/inference/safetensors_agentic.py -- re-prompt on plan-
   without-action. When the model emits a short forward-looking
   intent ("I'll search for that", "Let me check", "First, I
   will...") and no tool call, the loop nudges the model to act
   instead of silently returning a plan-only answer. Up to
   _MAX_REPROMPTS=3 (matches GGUF). The intent regex, character
   cap, and instruction text are byte-identical to the GGUF path.
   The buffer-end fall-through is unified so a buffered intent
   emission that never exits the BUFFERING state still triggers
   the re-prompt.

4. core/inference/safetensors_agentic.py -- extra iteration slots
   for re-prompts. The loop now budgets max_tool_iterations +
   _MAX_REPROMPTS + 1 total iterations and tracks the tool-call
   count separately, so a stalling model can be nudged 3x without
   eating the caller's tool-call budget. Mirrors the _extra slot
   reservation in the GGUF path.

Tests (14 new safetensors-side units; 5 GGUF parity pins):

  TestLoopRePrompt                 -- intent-trigger, plain-answer,
                                      no-tools, cap-at-three, budget
                                      preserved, buffer-end intent.
  TestLoopCanonicalHealKey         -- python / terminal / unknown.
  TestGGUFSafetensorsHealingParity -- shared markers used, shared
                                      strip used, canonical heal keys
                                      identical, intent regex matches
                                      same phrases, _MAX_REPROMPTS
                                      equal on both backends.

All 110 targeted tests pass locally; the broader tool / inference /
model-config / sandbox / anthropic / mlx suites stay green.

Why this matters

Without this parity, Llama-3.2 / Mistral / Gemma 4 emissions on Mac
(MLX) and Linux-safetensors stop the agentic loop as soon as the
model says "Let me...", because the GGUF re-prompt logic never
existed on these backends. The two-marker GGUF BUFFERING tuple also
let non-Qwen tool emissions stream out as plain prose when
llama-server's structured channel did not pick them up. Both paths
now drain the same way, heal the same way, and re-prompt the same
way -- so a tool call that works on GGUF works identically on
safetensors / MLX.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fix tool-call parser bugs from gemini review on #5620

Three high-priority gemini findings on the tool-call parsing additions:

  1. unicode_escape on UTF-8 bytes corrupts non-ASCII literals
     (e.g.  becomes â\x9c¨). Replace with json.loads on a quoted
     string -- preserves emoji / CJK / RTL while still handling
     \n \t \uXXXX escapes.

  2. Llama-3 sentinel stripping is order-dependent. A leading
     `<|eot_id|><|begin_of_text|>` left `<|begin_of_text|>` behind
     because the loop had already passed that sentinel. Loop until
     no sentinel matches at the start.

  3. Mistral v11+ `[TOOL_CALLS] name { json }` regex uses non-greedy
     `\{.*?\}` which truncates at the first `}` of a nested JSON
     argument, leaking the tail (e.g. `}}`) into user-visible
     streamed text. Same problem for the v0.3 array pattern with
     nested brackets. Strip those with balanced brace/bracket
     scanning via a new `_strip_mistral_closed_calls` helper called
     from `strip_tool_markup`.

Also fix the inference routes' parallel `_TOOL_XML_RE`:

  - Same nested-JSON truncation in the Mistral patterns; route the
    strip through the parser's balanced-scan helper via a thin
    `_strip_tool_xml` wrapper that all existing callers now use.
  - Llama-3 `<|python_tag|>[^\n<]*` stopped at any `<`, leaking the
    tail of any tool call whose argument contained a literal `<`
    (queries, code snippets). Relax to `[^\n]*` which keeps the
    strip confined to the actual end-of-line.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio/routes: make python_tag strip multi-line aware

Earlier revisions of _TOOL_XML_RE in studio.backend.routes.inference
oscillated between two bug shapes:

  5615    r"<\|python_tag\|>[^\n<]*"   -- stopped at any literal "<"
                                         so code='if x < 10: pass'
                                         leaked '< 10: pass)' to the
                                         user.
  5620.1  r"<\|python_tag\|>[^\n]*"    -- single-line only; the second
                                         line of
                                         python.call(code="a\nb")
                                         leaked.

The full parser (_parse_llama3_python_tag) already handles both via
balanced-brace scanning, so the parsing path was fine; the LEAK was
in the streaming strip path that runs on every cumulative emission
while content is still arriving.

Switch to r"<\|python_tag\|>(?:[^<]|<(?!\|))*" so the strip consumes:

  * any character that is not a "<" (newlines, JSON, code, ...),
  * a "<" only when it is NOT followed by "|" (i.e. NOT a Llama-3
    sentinel start like <|eot_id|>, <|eom_id|>, <|begin_of_text|>).

This means:

  * code='if x < 10' stays inside the strip (5615 fix preserved),
  * multi-line code stays inside the strip (5620 round 2),
  * the strip terminates at the next Llama-3 sentinel so trailing
    assistant content survives.

Tests: TestRoutesPythonTagStrip (8 cases)
  pytest test_safetensors_tool_loop.py test_safetensors_capability_advertise.py
    -> 118 passed in 1.81s (was 110).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: tighten verbose comments in tool-call parser sections

Comments were narrating what the code already says. Cut historical
"earlier revisions used X, then Y" narratives down to one-line WHY
notes where the footgun still matters (canonical heal-key parity,
balanced-brace vs non-greedy regex, ``(?:[^<]|<(?!\|))*`` over
``[^\n<]*``/``[^\n]*``). Drop section-header banners.

No behaviour change. Re-ran:
  pytest studio/backend/tests/test_safetensors_tool_loop.py \
         studio/backend/tests/test_safetensors_capability_advertise.py -q
  -> 118 passed.
Regression replay (parser + _coerce_arguments on the 5 #5615 inputs)
  -> 21/21.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: parser robustness fixes for PR #5620

Three surgical extensions to the multi-format tool-call parser, each
covering a real fine-tune / template emission shape that the current
parser silently drops. No path narrows; all changes widen what is
accepted.

1. `_parse_tool_call_json` now accepts both `arguments` and
   `parameters` keys. A Hermes / Qwen `<tool_call>{json}</tool_call>`
   wrapper around a Llama-3.2 fine-tune that emits the `parameters`
   key was extracting the tool name and silently discarding the
   args, producing a working-shaped call with an empty payload. The
   bare-JSON and python_tag paths already accepted both keys; this
   path now matches them.

2. `_TC_FUNC_START_RE`, `_TC_PARAM_START_RE`, and `_TC_PARAM_CLOSE_RE`
   now also match the attribute form
   `<function name="..."><param name="...">v</param></function>` used
   by MiniCPM-5 and MiniMax-M2. Names land in either capture group,
   and `</param>` is accepted as a short close.

3. `_parse_llama3_bare_json` sentinel-strip now consumes the role
   label inserted between `<|start_header_id|>` and
   `<|end_header_id|>` by Meta's official Llama-3.x chat template.
   Without this, every assistant turn re-fed through the template
   prefix `<|start_header_id|>assistant<|end_header_id|>\n\n{json}`
   parsed to zero calls, so any history-with-tool-call round-trip
   in production silently dropped.

Tests in `studio/backend/tests/test_safetensors_tool_loop.py`:

* `TestParserRobustness::test_tool_call_json_accepts_parameters_key`
* `TestParserRobustness::test_function_xml_attribute_form`
* `TestParserRobustness::test_function_xml_attribute_form_multi_param`
* `TestParserRobustness::test_function_xml_legacy_equals_form_still_works`
  (regression guard for the existing `<function=name>` syntax)
* `TestParserRobustness::test_llama3_chat_template_round_trip`
* `TestParserRobustness::test_llama3_round_trip_all_roles`
* `TestParserRobustness::test_llama3_round_trip_with_eot_prefix`

`pytest studio/backend/tests/test_safetensors_tool_loop.py
        studio/backend/tests/test_safetensors_capability_advertise.py -q`
goes from 118 to 125 passed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: terminate function-XML body at </function>, not just </tool_call>

`_parse_function_xml` was looking for `</tool_call>` (the Hermes
wrapper) as the body terminator. When a model emits a standalone
`<function=NAME><parameter=K>v</parameter></function>` followed by
explanatory prose (which models routinely do), no `</tool_call>` is
present, so the body extended to end-of-string and the trailing
prose leaked into the LAST parameter value.

Pre-existing on main (the legacy `<function=NAME>` form had this
bug too). Same affects PR #5620's new attribute-form
`<function name="NAME"><param name="K">v</param></function>`
emission used by MiniCPM-5 / MiniMax-M2.

Fix: `_TC_END_TAG_RE` now matches either `</tool_call>` OR
`</function>`. The existing `_TC_FUNC_CLOSE_RE` / `_TC_PARAM_CLOSE_RE`
strips are unchanged. Multi-call inputs still bound each function
at the next `<function=` start, so no over-eager consumption.

New tests:

* `test_function_xml_followed_by_prose` (legacy form + prose)
* `test_function_attribute_xml_followed_by_prose` (attribute form + prose)

Existing `test_code_with_embedded_xml` still passes (a parameter
value containing literal `<a></a>` is preserved because the
embedded close tag is `</a>`, not `</function>`).

`pytest studio/backend/tests/test_safetensors_tool_loop.py
        studio/backend/tests/test_safetensors_capability_advertise.py -q`
goes from 125 to 127 passed.

* Studio: tighten Llama-3.2 bare-JSON guard

A fuzz pass on PR #5811 turned up that ``_parse_llama3_bare_json``
accepted ``parameters`` as a string, contradicting the docstring's
"parameters or arguments is a dict" guard. Prose JSON like
``{"name":"foo","parameters":"a sentence"}`` would wrongly fire the
parser, which the agentic loop would then heal into a real
``foo(query="a sentence")`` call.

Same code lives on this branch, so the same fix applies here.

Tightened guard:

  - ``parameters`` must be a dict (Llama-3 spec).
  - ``arguments`` may be a dict, or a JSON-encoded string that
    decodes to a dict (OpenAI shape, e.g.
    ``"arguments":"{\"q\":\"x\"}"``). Plain non-JSON strings or
    JSON-strings of lists / scalars / null no longer pass.

Mirrors the fix landed in PR #5811 commit 615b8608. Adds the same
4 regression tests under TestParserMultiFormat.

Existing test suite stays green: 127 -> 131 passing.

* studio: fix safetensors tool-call parser gaps vs llama.cpp (Mistral CALL_ID / THINK, attribute-form signal)

Three GGUF-parity fixes to the safetensors tool-call parser, each matching
llama.cpp's reference behaviour:

- Mistral Small 3.2 emits [TOOL_CALLS]name[CALL_ID]<id>[ARGS]{json}. The
  parser stopped after the name on seeing [CALL_ID] (neither [ARGS] nor {),
  dropping the call. Skip an optional [CALL_ID]<id> segment in both the
  parse and strip paths. llama.cpp parses this (test-chat.cpp:4785).

- Magistral wraps reasoning in [THINK]...[/THINK]. A [TOOL_CALLS] inside the
  reasoning was parsed as a real call, producing a phantom call. Strip a
  leading [THINK] block before scanning so only the post-reasoning call
  counts (test-chat.cpp:2285); a literal [THINK] inside a later argument is
  left intact.

- The standalone MiniCPM-5 / MiniMax-M2 <function name="..."> attribute form
  parsed correctly but was absent from TOOL_XML_SIGNALS and the markup strip
  patterns, so the streaming safety-net parse was gated off (dropping the
  call) and markup leaked into displayed text. Add the signal and broaden
  the strip regexes.

Adds regression tests for all three.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fire safetensors tool calls for the bare-JSON (Llama-3.2) form

The agentic loop's streaming safety-net parse was gated on
has_tool_signal(), which is False for the Llama-3.1 / 3.2 bare-JSON tool
form {"name":..,"parameters":..} (no XML marker). Real tool calls were
therefore dropped: the loop logged "model planned without calling tools",
re-prompted three times, then gave up with zero tool calls, while GGUF's
llama-server parses the same emission natively.

Run parse_tool_calls_from_text() unconditionally in the safety net. The
parser is strict (only fires on a valid tool-call shape) so plain answers
are unaffected. Reproduced on a real unsloth/Llama-3.1-8B-Instruct run:
the model emits {"name":"web_search","parameters":{...}} which now
executes the tool instead of being re-prompted into a no-op.

Adds a loop regression test for the bare-JSON form.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: complete strict-mode contract and fix parser import paths

Address review findings on the multi-format tool-call parser:

- Honor allow_incomplete=False in the remaining sub-parsers. The Llama-3
  <|python_tag|>NAME.call(...) parser, the pre-v11 Mistral [TOOL_CALLS] array
  parser, and the Gemma 4 <|tool_call> parser ignored strict mode, so a
  truncated call (missing closing paren, ], or <tool_call|>) was still healed
  and executed with Auto-Heal disabled. Thread strictness through and reject
  the unclosed forms, matching the JSON and function-XML paths.
- Drop the duplicate tool_call_parser import block in llama_cpp.py and the
  redundant un-aliased TOOL_XML_SIGNALS; only the _SHARED_TOOL_XML_SIGNALS
  alias is used as a value.
- Import _strip_mistral_closed_calls from core.inference.tool_call_parser in
  routes/inference.py instead of studio.backend.core... The self-contained
  run.py launch mode only puts studio/backend on sys.path, so the absolute
  package path raised ModuleNotFoundError on the server-tool strip path.

Add strict-mode regression tests for the truncated Llama-3 dot-call and the
unclosed Mistral array.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: preserve XML param indentation and alias Mistral array parameters

Two parser-correctness fixes found by auditing against the model chat templates
and the SGLang / vLLM reference parsers:

- Qwen3.5 XML parameter values lost their leading indentation. The chat template
  emits <parameter=k>\nVALUE\n</parameter>, but the parameter-start regex ate the
  wrapping newline AND the value's first-line indentation with a trailing \s*,
  then str.strip() removed the rest. Narrow the trailing class to horizontal
  whitespace only and trim exactly one wrapping newline (via _trim_param_value),
  preserving indentation in code/diff arguments. Matches SGLang's qwen3_coder
  detector. Applies to both _parse_function_xml (tool_call_parser.py) and the XML
  path in tool_healing.py.
- Mistral pre-v11 array objects keyed on parameters dropped their payload.
  _consume_mistral_call read only the arguments key; alias parameters the same way
  the JSON/XML paths and SGLang's base detector do.

Add regression tests for preserved multi-line indentation and the array
parameters alias.

* Studio: tighten tool-call parser comments

Make the comments in the multi-format tool-call parser and its callers succinct:
compress verbose docstrings/blocks to one or two lines, drop ones that restate the
code, and trim the tiny balanced-scanner helpers. Correctness rationale and
upstream provenance (SGLang/llama.cpp parity, the strict-mode / Auto-Heal
contract, whitespace-preservation, and the Unicode / full-width-pipe notes) are
kept in compact form.

Comment-only: no code or behavior change (verified with comment_tools.py check
--strip-docstrings; parser suite green).

* Studio: make Llama-3 .call and Mistral-array healing parsing linear

Two more O(n^2) ReDoS paths in the multi-format parser, both reachable from
the agentic loop on a long truncated body with no length cap:

- _LLAMA3_KV_RE.finditer over a .call(...) body retried at every offset of a
  long word run / unterminated quote (40K -> 14s). Replace with a hand-scan
  that reuses the same key/number/literal sub-regexes via anchored match and
  walks the string body by hand, so an unterminated quote is O(n). Verified
  byte-identical to the old regex over 200K fuzzed inputs.
- _parse_mistral_array healing ran _balanced_brace_end from every { in the
  body (20K -> 17s). Walk top-level objects, advancing past each balanced
  {...}; this also drops the phantom call the old scan emitted from a nested
  argument object.

Add adversarial-length linearity regressions plus positive .call kwargs and
unclosed-array recovery coverage.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: honor strict mode in safety-net, keep empty Gemma args, strip attribute-form function XML

- safetensors safety-net parser now forwards allow_incomplete=auto_heal_tool_calls,
  matching the draining path, so a late incomplete tool call is not healed and
  executed when Auto-Heal is off.
- Gemma empty bare value ({k:}) now serialises as "" instead of invalid {"k":},
  which previously dropped the whole call.
- Route _TOOL_XML_RE also strips the <function name="..."> attribute form
  (MiniCPM-5 / MiniMax-M2) so it no longer leaks to the UI.

* Studio: fix attribute-form function-XML literal close tag and zero-arg strict call

Addresses Codex review of the <function name="..."> attribute form in
_parse_function_xml (MiniCPM-5 / MiniMax-M2):
- End the call body at the LAST </function> / </tool_call> within the call's
  window, so a literal close tag inside a code/search argument (e.g.
  print("</function>")) is preserved instead of truncating the call.
- Accept a closed call with no parameters as a valid zero-argument call in strict
  mode (the function close is already required), instead of rejecting it as a
  truncated call.
- Tests for both, mirroring the legacy <function=...> coverage.

* Studio: fix tool-call parser/loop review findings on the multi-format path

Address the live code-review findings on the safetensors/MLX + GGUF tool path:

- routes: include the attribute form <function name="..."> in the safetensors
  capability whitelist so MiniCPM-5 / MiniMax-M2 templates keep the tool pill
  (parser already handles the form; the post-filter wrongly suppressed it).
- safetensors loop: build the plan-without-action re-prompt from the active
  tools instead of a hardcoded web_search/python string, and gate it on
  auto_heal_tool_calls, matching the GGUF loop.
- safetensors loop: hold a leading bare-JSON object ({"name":..,"parameters":..})
  during BUFFERING until it closes, then drain it as a tool call instead of
  streaming the raw JSON to clients. The DRAINING/STREAMING resolvers still
  recover a plain JSON answer, so this can never drop content.
- parser: anchor the Llama-3 <|python_tag|>NAME.call(...) scan to the tag and
  chain ; -separated calls, so all semicolon-separated built-ins parse and a
  literal <|python_tag|>x.call(...) inside a JSON string argument no longer
  fires the wrong tool.
- parser: consume the optional trailing </s> after a named Mistral
  [TOOL_CALLS]name{json} call, mirroring the array shape.
- GGUF streaming strip: use the shared parser patterns (which know
  [TOOL_CALLS] and <|python_tag|>) so a textual tool call entering DRAINING is
  stripped instead of leaking the marker to streaming clients.
- routes: hoist the _strip_mistral_closed_calls import to module level.

Adds regression tests covering each fix; existing parser suite stays green.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden multi-format tool-call detection from review findings

Apply five targeted fixes from the review pass over the multi-format tool
path:

- routes: route display strip delegates to _strip_tool_xml so Mistral
  [TOOL_CALLS] blocks with nested JSON are removed from streamed display
  text, not just the XML forms.
- tool_call_parser: skip function/parameter starts that fall inside an
  already-open parameter block (_inside_open_parameter) so nested example
  payloads are not mis-parsed as new calls; extract
  strip_llama3_leading_sentinels so the bare-JSON guard is shared.
- safetensors_agentic: probe bare JSON through strip_llama3_leading_sentinels
  before the balanced-brace check so a leaked header sentinel does not defeat
  the guard.
- tool_healing: allow dotted tool names in the Gemma wrapped start pattern.
- llama_cpp (GGUF): buffer wrapper-less Llama-3.2 {"name":..} calls that carry
  no XML signal, drain a complete object silently and hold an incomplete one,
  and run the end-of-stream safety net unconditionally so markerless calls are
  detected and never leak the raw JSON (including truncated fragments).

Adds regression tests for the GGUF bare-JSON streaming path and the Mistral
display strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: stop bare-JSON tool calls leaking at EOF, oversized, and into history

The second review pass flagged that the Llama-3.2 bare-JSON tool-call handling
still leaked raw JSON in several spots; ``strip_tool_markup`` only knows
XML/bracket markup, so the bare-JSON form survived it. Fix them symmetrically
across the safetensors and GGUF loops:

- Safetensors stream-end resolver now routes a held bare-JSON fragment to
  DRAINING (mirroring GGUF) so a truncated ``{"name":..`` cut off by the end of
  the stream is dropped instead of flushed as assistant content. The 7/10
  reviewer finding.
- Both loops now drain (suppress) an oversized still-open bare-JSON call once it
  passes ``_MAX_BARE_JSON_BUFFER`` instead of streaming the raw prefix, gated on
  a ``"name"`` key so a giant plain JSON answer still streams; a complete
  oversized call still executes via the safety net.
- Add a shared ``strip_leading_bare_json_call`` helper and apply it to the
  content kept for the assistant turn in both loops, so an executed bare-JSON
  call is not replayed as visible text or fed back as next-turn history.

Plain JSON answers without a ``"name"`` key are untouched throughout. Adds
regression tests for the EOF, oversized, and next-turn cases on both backends
plus unit tests for the helper.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: bound the Llama-3 python_tag strip on real control sentinels

The route display strip's <|python_tag|> arm ran to the next <| of any kind.
A tool-call argument carrying a literal <|...|> token (for example <|cite|>
inside a string value) truncated the strip early and leaked the call tail into
the visible response. Narrow the stop condition to the genuine Llama control
sentinels (eot_id, eom_id, python_tag, start/end_header_id, begin_of_text,
finetune_right_pad_id) so embedded markup and JSON are consumed while real
header/turn boundaries still bound the strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: gate markerless bare JSON on enabled tools and close parser/strip asymmetries

The Llama-3.2 custom_tools bare-JSON form has no marker, so any JSON object with a
name key was read as a tool call. An ordinary JSON answer like
{"name":"Alice","parameters":{"age":30}} was misclassified as a call to a
disabled tool and dropped from the visible response. Gate the markerless form on
the enabled tool names (threaded through parse_tool_calls_from_text and
strip_leading_bare_json_call, supplied by both streaming loops): an object whose
name is not an enabled tool is ordinary content. The marker-based forms keep
their name-agnostic behaviour (an explicit signal is a real call attempt), and
unrestricted mode stays ungated.

Also fix two parser/strip asymmetries the parser already tolerated:
- A literal </function> inside a parameter value (print("</function>")) truncated
  both the core and route strips at the first close, leaking the tail. Extend the
  strip to the call's real close (last </function> before the next opener),
  mirroring the parser, without merging separate calls.
- The single-object Mistral [TOOL_CALLS]{...} shape parsed but _strip_mistral_closed_calls
  left it, leaking the raw object into display. Strip the balanced object while
  keeping trailing prose, matching the array and name shapes.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: gate GGUF bare-JSON suppression on enabled tools and fix python-tag exponent parsing

Pass-4 review follow-ups on the GGUF tool loop and Llama-3 parser:

- The GGUF bare-JSON suppression sites still keyed off a raw "name" substring,
  so an ordinary JSON answer whose name is not an enabled tool was dropped when
  it was truncated, oversized, or reached the no-tool DRAINING fallback (the
  parser, helper, and safetensors paths were already gated). All three sites now
  use the shared enabled-name gate, and a held bare-JSON buffer that turns out not
  to be an enabled call is shown as the answer instead of dropped at stream end.
- The Llama-3 python-tag numeric kwarg regex matched only the mantissa, so
  scientific notation was truncated to its leading digits (1e-3 parsed as 1) and a
  tool executed with the wrong value. The regex now accepts exponent and decimal
  forms, and the int/float classification keys off the exponent too.

Adds regression tests for the truncated / oversized disabled-name JSON cases (and
a counterpart that a truncated enabled call still does not leak) plus the
scientific-notation kwargs.

* Studio tools: gate safetensors bare-JSON drain, fix nested-name gate and function-XML strip

Pass-4 review follow-ups on the shared parser / safetensors loop:

- The safetensors oversized and end-of-stream bare-JSON drain branches keyed off
  a raw "name" substring, so a large or truncated ordinary JSON answer whose name
  is not an enabled tool was drained instead of streamed. Both now use the shared
  enabled-tool-name gate, matching the GGUF path.
- strip_leading_bare_json_call matched the first "name" anywhere, so a plain JSON
  answer with a nested name equal to an enabled tool ({"result":{"name":"web_search"}})
  was wrongly suppressed. It now extracts the TOP-LEVEL name only, walking past
  nested objects/arrays and keeping the text when a top-level value is truncated.
- The function-XML display strip used a regex negative-lookahead that stopped at a
  literal <function=...> opener inside a parameter value and then dropped the rest
  of the answer to EOF. A scan-based strip mirrors the parser (ignores openers
  inside an open <parameter> via _inside_open_parameter) and closes each call at its
  real </function>, so trailing assistant text after such a call survives.

Adds regression tests for each.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tool parsing: 3.9 import safety, disabled-Auto-Heal contract, capability gate

Round-2 review follow-ups on the multi-format tool-call parser:

- tool_call_parser: add `from __future__ import annotations`. The module
  is dependency-light by design (external llama-server wrappers import it
  standalone) and the package targets python >=3.9, where its PEP 604
  `int | None` return annotations would raise TypeError on import.
- safetensors + GGUF drain fallback: gate the leading bare-JSON strip on
  auto_heal_tool_calls. With Auto-Heal off, a truncated enabled-name
  fragment that did not parse now stays visible, matching the XML strip
  in the same branch and the disabled-Auto-Heal contract. With Auto-Heal
  on it is still suppressed.
- safetensors capability gate: match the bare-JSON `{"name":` template
  marker with a whitespace/escape-tolerant regex so a pretty-printed
  `{ "name" :` or JSON-escaped `{\"name\":` template is not mis-classified
  as tool-less. The parser already accepts that whitespace via
  raw_decode, so the gate must too.

Regression tests added for each case.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tool parsing: symmetric "function" bare-JSON alias and route strip parity

Round-3 review follow-ups, all parser/strip symmetry fixes.

- Bare-JSON "function" alias: the markerless parser accepts a call name via
  obj.get("name") or obj.get("function"), but the strip/gates only knew "name",
  so a {"function":<enabled tool>} call executed while its raw JSON leaked. Teach
  _top_level_bare_json_name the alias (with "name" precedence and the same nested
  and truncated-name guards), and widen the guards in strip_leading_bare_json_call,
  the safetensors and GGUF _looks_like_enabled_bare_json gates, and the route
  capability marker regex.
- Route display/history cleanup: strip a tail-only </param> alias close (the
  parser accepts <param name="...">...</param>), and run the parser's guarded
  function-XML scan (_inside_open_parameter) before _TOOL_XML_RE so a literal
  nested <function=...></function> inside an argument value does not truncate the
  strip and leak the tail.

Regression tests added for each.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: honor tool budget in GGUF loop and guard function-XML streaming strip

Round 4 review fixes. Both are asymmetric-fix bugs where the final/steady path got a
guard the analogous streaming/loop path did not.

- GGUF tool-call budget: the safetensors loop counts real tool-call turns against
  max_tool_iterations (re-prompt stalls excepted), but the GGUF loop only bounded the
  turn count by the enlarged range (max_tool_iterations + _MAX_REPROMPTS). Since this
  PR raised _MAX_REPROMPTS from 1 to 3, a model that keeps making valid tool calls
  could run up to three extra tool rounds (with max_tool_iterations=1, four rounds
  instead of one). Add a _tool_iters_done counter that increments only when a tool
  actually executed in the turn, and stop once the caller's budget is spent so the
  post-loop final-answer nudge fires. A duplicate/disabled no-op turn is a correction
  turn (like a plan-without-action re-prompt) and does not consume budget, preserving
  the existing "already completed" re-prompt behavior.

- Streaming display strip: the final strip runs the guarded _strip_function_xml_calls
  scanner (a literal <function=...> inside a parameter value is data, not a nested
  call), but the GGUF and safetensors streaming strips still used only the open-ended
  regex arms. When a tool-call argument contained literal function markup, the regex
  tail ate everything to end-of-text and dropped the real trailing prose after the
  call's true </function>. Run the guarded scanner (and the balanced Mistral strip)
  before the regex arms in both streaming paths so streaming and final display agree.

Adds regression tests: GGUF valid tool calls respect max_tool_iterations, and the
streaming strip keeps trailing prose after a function-XML call with a literal marker.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio tools: safetensors tool budget counts only executed turns (GGUF parity)

Follow-up to the GGUF budget fix. The safetensors loop charged max_tool_iterations
per non-re-prompt iteration (iteration + 1 - reprompt_count), so a duplicate/disabled
no-op turn spent a budget slot even though no tool ran. With a small cap this dropped
real work: for max_tool_iterations=2, a model that made a valid call, repeated it (an
internal no-op correction turn), then made a distinct valid call executed only the
first -- the third turn was sent with no tools and the distinct call was ignored.

Track whether a turn actually executed a tool (set on record_result) and count only
those turns against the cap, matching the GGUF loop. A duplicate/disabled no-op is a
correction turn -- like a plan-without-action re-prompt -- and no longer consumes
budget, so the model still gets its "already completed" nudge and another tool-enabled
turn. Adds a regression test for the small-cap duplicate-then-distinct-call flow.

* Studio: render the reasoning block for safetensors and MLX like GGUF

enable_thinking chat templates (Qwen3/Qwen3.5/GLM) prefill an unclosed <think>
into the generation prompt, so the model emits only the closing </think> then
the answer. The safetensors/MLX chat stream emitted that as plain content, so
the reasoning showed inline with no collapsible thinking block, while GGUF
(which surfaces reasoning via reasoning_content) rendered one. This brings
safetensors and MLX to parity.

- _ResponsesReasoningExtractor gains a reasoning_prefilled mode that starts
  inside the reasoning block and splits on the first </think>; default False
  keeps GGUF and every existing caller byte-identical. It suppresses a stray
  re-emitted <think> and holds partial markers back across chunk boundaries.
- _sf_reasoning_prefill_mode gates the mode on reasoning being enabled for the
  request, an enable_thinking or enable_thinking_effort style, and the template
  actually using the standard <think>/</think> markers. Models with a bespoke
  reasoning channel (e.g. gemma's <|think|>/<|channel>) are excluded so their
  answer is never swallowed; gpt-oss (Harmony) and thinking-off requests are
  excluded too.
- sf_tool_stream and stream_chunks (the latter also serves MLX) feed text
  through the extractor, emitting reasoning_content then content deltas, with a
  per-turn reset in the tool loop and a flush before each tool_start; only the
  visible delta reaches the monitor reply. The two non-streaming drains split
  reasoning_content the same way.
- Tests: extractor prefilled mode (streaming and edge cases), the gate matrix
  including the gemma-style exclusion, and a route-replay of the tool-loop
  reasoning stream.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: don't force a tool re-prompt on a negated intent (safetensors parity)

The safetensors _INTENT_SIGNAL claimed to mirror GGUF but was missing the
negative lookahead, so a refusal like "I will not search the web for that"
matched the "i will" intent and triggered the plan-without-action re-prompt
(STOP... you MUST call a tool), overriding a valid no-tool answer. GGUF already
excludes not/never. Add the same (?!\s+(?:not|never)\b) lookahead so both
backends agree. Extends the intent parity test with negated refusals.

* Studio: trim redundant comments (comment-only, AST-verified)

* Studio: prevent Gemma tool-parser DoS on stray delimiters

_gemma_parse_value returned the input index unchanged when text[i] was a
stray delimiter (,}]), so the list and mapping caller loops that advance
on the returned index spun forever at 100% CPU on malformed input such as
[},]. Advance past the delimiter so parsing always terminates.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strip Magistral [THINK] reasoning from final display/history

strip_tool_markup removed [TOOL_CALLS] and <function> markup but left a
leading Magistral [THINK]...[/THINK] block intact, so its bracket-form
reasoning (not the <think> the reasoning channel renders) leaked into the
safetensors display and conversation history while GGUF/llama.cpp routes
it natively. Drop the leading reasoning block at end-of-turn (final=True)
via the existing _strip_mistral_reasoning helper; streaming is untouched.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Honor reasoning_effort none in safetensors prefill; strip Magistral reasoning while streaming

Two safetensors/MLX reasoning fixes surfaced in review:

_sf_reasoning_prefill_mode only checked enable_thinking, so an
enable_thinking_effort (GLM-5.2) request that disables thinking via
reasoning_effort=none (without enable_thinking=False) still began in
prefilled-<think> mode. A plain answer with no </think> was then swallowed
whole into reasoning_content and the visible response came back empty. Thread
reasoning_effort into the predicate and treat none as disabled, mirroring
_request_reasoning_kwargs.

strip_tool_markup_streaming stripped tool markup but not the leading Magistral
[THINK]...[/THINK] bracket block, so the raw chain-of-thought leaked into the
streamed safetensors content instead of the reasoning drawer (GGUF routes it
natively). Apply _strip_mistral_reasoning first, matching the final strip; an
unclosed [THINK] is held from the marker on so nothing flickers.

* Mistral outer call wins over XML literals; align healer signals with its parser

Two follow-ups on the shared-parser ordering after the healing-passthrough
merge:
- A well-formed [TOOL_CALLS] call whose JSON arguments quote tool XML parsed
  the literal instead of the outer call (executing the wrong tool). When the
  first XML signal sits inside a leading balanced Mistral body it is argument
  data, so the Mistral parser now runs first; an XML signal before the trigger
  keeps the normal order, so a [TOOL_CALLS] literal inside an XML call's
  arguments still stays data.
- passthrough_healing buffered streams on the parser module's broadened signal
  list (now including <|python_tag|> and [TOOL_CALLS]) but promotes with
  core.tool_healing, which does not parse those forms: a streamed Mistral or
  Llama text call was held until finalization and flushed as prose. The healer
  keeps its own signal list limited to the formats it can promote, restoring
  immediate streaming for the rest.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: leading envelopes win over rehearsed literals

- New _first_foreign_tool_signal shared by the leading-envelope guards adds
  <|python_tag|> to the protected signal set: the spelled-out literal inside a
  Mistral call's arguments (a query about Llama built-in tool syntax) executed
  the inner literal instead of the outer call.
- New _xml_signal_inside_leading_bare_json guard, sibling of the Mistral one:
  a leading bare-JSON call whose string argument quotes tool XML (a code value
  citing <function=...>) had the literal promoted by the shared XML pass
  before the bare-JSON parser ran.
- Magistral [THINK]...[/THINK] is dropped once at parse entry instead of only
  inside the Mistral parser, so a call rehearsed in the think block in a
  foreign format can no longer be promoted while the real call after the
  block is lost. Parse now agrees with the display strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: a disabled leading bare-JSON object keeps its literals as data

When the leading bare-JSON object is ordinary content (name not an enabled
tool), the guard proved the first tool signal sits inside it, so falling
through to the XML/python_tag passes promoted quoted string data as a real
call. Drop the object and parse only the tail: a real call after the object
still parses, nothing inside it can be promoted.

* Address review: Mistral literals inside leading JSON, whitespace-tolerant wrapped Gemma opener

- The leading bare-JSON guard now treats the [TOOL_CALLS] trigger as a
  foreign signal: the Mistral parser runs before the bare-JSON one, so a
  literal quoted inside the leading object's strings was promoted over the
  outer call (or over ordinary JSON content).
- tool_healing's wrapped Gemma opener tolerates whitespace around call and
  the colon: sampling drift emits call: name{ and call : name{, and
  rejecting those lost the call entirely because no fallback re-parses the
  wrapped form. Strict mode still requires the closing tag.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: accept dotted Gemma argument keys in the key-quoting scanner

The scanner quoted keys of [alnum_-] only, so a dotted key (user.name:...)
was left unquoted, json.loads failed, and the whole wrapped call was lost
(parse empty, strip wipes the markup). Dots now match the parser's own
key/name charset.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: leading Mistral call owns the turn, dotted keys after bare values

- A LEADING parseable [TOOL_CALLS] call now runs the Mistral parser first
  unconditionally: literal XML in trailing prose after the call was promoted
  by the earlier shared XML pass, executing the quoted example instead of
  the real leading call. XML leading keeps the normal order.
- _GEMMA_NEXT_KEY_RE accepts dots so a dotted key after a bare value
  (query:foo,user.name:bob) ends the value at the comma instead of being
  swallowed into it, matching the round-earlier key-quoting charset.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: markup quoted inside a nameless leading JSON answer stays data

The leading bare-JSON guard required a top-level name, so a structured JSON
answer quoting tool markup in its strings (a response_format turn
documenting a tool's syntax) had the literal promoted by the later passes.
A nameless leading object that parses as real JSON now routes through the
same decline-then-parse-the-tail path; non-JSON braced prose keeps the old
behaviour, and a real call after the answer still parses.

* Compress docstrings in the multi-format tool parser to their contract essence

* verify_import_hoist: exempt __future__ imports and same-diff relocations

Two false positives fired on this PR's refactor. A from __future__ import
is a compiler directive whose name never appears as a runtime load, so
HOISTED-IMPORT-UNUSED can never see it used, yet the file requires it for
PEP 604 annotations on Python 3.9. TARGET-CHANGED flagged the deliberate
move of the strip-pattern constants into core.inference.tool_call_parser
as a silent re-point even though the old module-level target was removed
and the new one added in the same diff. Both get narrow exemptions; a
re-point to a pre-existing target is still caught, and the self-test
negative controls all pass unchanged.

* Leading bare-JSON calls own the turn; function calls end at the first balanced close

The XML-signal guard for a leading bare-JSON call required the signal
strictly inside the object, so a trailing XML example stole the turn
from the leading call; it now applies the same inside-or-after rule as
the Mistral guard. Function-XML calls also ended at the LAST close tag,
which let prose after a closed call that mentions a literal close tag
get swallowed into the final parameter value; calls now end at the
first close tag that is not inside an open parameter, and the strip
mirrors the same rule so parse and strip agree.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Attribute-form calls end at the first balanced close; bare-JSON strip requires the call shape

The attribute form parser still kept the last close tag in the call
window, folding prose after a closed call into the final parameter
value. It now takes the first close not inside an open parameter, the
same rule the equals form and the strip already use.

The leading bare-JSON strip deleted any closed object whose top-level
name matched an enabled tool, including plain JSON answers the parser
correctly rejects as non-calls. The strip (and the drain gate that
delegates to it) now requires the parser's exact call shape, so answers
like {"name":"web_search","result":...} stream and display intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* False-alarm markers keep the answer; the bare-JSON strip consumes the whole chain

The trailing strip arms dropped everything from a bare marker to EOF,
so a normal answer that mentions [TOOL_CALLS] or another marker
literally was truncated (or fully swallowed when it started with the
literal) after the no-call drain fallback. Those arms now require a
call-shaped lookahead or marker-at-EOF before dropping; truncated real
calls still strip.

Chained bare-JSON turns executed both calls but stripped only the first
object, so the second call's raw JSON replayed into the next assistant
history message alongside the structured tool_calls. The strip now
consumes the entire chained run of call-shaped enabled objects while
non-call answers, disabled names, and trailing prose stay intact.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Attribute-form containment, parameter-close-decides rule, preamble-tolerant Mistral guard, strict strip shape

Four document-order and containment fixes. A leading attribute-form
call now parses before the shared XML pass, so markup quoted in its
parameter stays data. The open-parameter scan lets the parameter's own
close tag decide, so any number of literal function closes inside one
value stay data, restoring the pre-close-scan behavior for multi-close
arguments. The leading-Mistral guard tolerates a visible preamble, with
the leading-bare-JSON guard running first so a trigger quoted inside a
leading JSON object stays data. The bare-JSON strip requires the
parser's top-level name in every mode, so nested-name JSON answers
survive name-agnostic stripping.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Let a leading <|python_tag|> call own the turn over quoted XML literals

The leading-call ownership contract (a leading executable call owns the turn;
foreign markup quoted in its string arguments or trailing prose stays data) was
enforced for the bare-JSON, Mistral and attribute-form leading calls but not
for the Llama-3 <|python_tag|> form. The shared tool_healing XML pass runs
before _parse_llama3_python_tag and does not recognise <|python_tag|>, so a
<function=...> / <tool_call> / [TOOL_CALLS] literal quoted inside a
<|python_tag|> .call(...) string argument (or its JSON parameters) was promoted
and the wrong tool executed. Well-formed single-format examples:

  <|python_tag|>web_search.call(query="... <function=foo> ...")  ->  foo
  <|python_tag|>python.call(code="<function=render_html>..</function>")  ->  render_html

both returned the phantom inner tool instead of the real leading call.

Add a leading-<|python_tag|> guard mirroring the other leading-call guards:
when the tag is the first tool signal, parse it before tool_healing so quoted
foreign markup stays data. A foreign signal before the tag keeps normal
document order. Added TestPythonTagOuterOverXmlLiteral (7 cases).

* studio: tighten tool-calling comments to be shorter and clearer

* studio: shorten tool-format comments in changed files

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
Co-authored-by: Daniel Han <info@unsloth.ai>
Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com>
2026-07-06 10:06:06 -07:00
Daniel Han
57259ded0a Match Wan generation-defaults key as a name segment, not a raw substring 2026-07-06 15:53:52 +00:00
Daniel Han
e8a407bf6e Load Wan VAE in fp32 via per-component dtype dict; fix A14B 720p preset 2026-07-06 15:29:34 +00:00
Daniel Han
46e2cf5dee
studio: label RAM and VRAM readouts as GiB not GB (#6895)
The live resource monitor and GPU readouts derive memory from binary byte
counts (bytes / 1024**3 for torch and psutil, MiB / 1024 for the nvidia-smi
path), which is GiB, but the UI labeled the values "GB". On a B200 this
showed "178.35 GB" for a card whose nvidia-smi total is 183359 MiB
(179 GiB), so it looked like memory was missing.

Relabel the measured RAM and VRAM readouts to GiB across the floating
monitor, the resources tab, the studio live GPU panel, the hub header, the
about tab and the onboarding summary. The numeric values are unchanged, so
the training GPU selection and memory-fit logic that read the same fields
are unaffected. Disk stays labeled GB because the backend reports it in
decimal GB (bytes / 1e9), and model file sizes and download progress keep
their decimal GB labels to match Hugging Face.
2026-07-06 08:27:26 -07:00
pre-commit-ci[bot]
a8494e25f3 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 15:24:16 +00:00
Daniel Han
0f2f4334e2 Reject dense DiT precisions on a CUDA-absent host before eviction; stabilize family-info tests
The start-route preflight caught the bf16-GPU and int8-torchao requirements but not the dense
precisions' CUDA requirement: on a GPU-less host bf16_unsupported_reason exempts CPU-only, so a
bf16/fp8 (or int8-with-torchao) DiT request passed the preflight, evicted resident workloads, then
raised only in the trainer child. Add the dense-mode CUDA gate mirroring _resolve_base_precision so
the doomed run is rejected up front. Also pin bf16_unsupported_reason in the two positive-path
family-info tests so they are deterministic across GPU types (a non-bf16 CUDA box would otherwise
empty every DiT family's advertised modes).
2026-07-06 15:22:58 +00:00
pre-commit-ci[bot]
f6cdcd3f12 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 15:00:27 +00:00
Daniel Han
c6f21a11fd Do not advertise a video GGUF arch that has no registered family
_arch_to_task mapped every arch in _VIDEO_GGUF_ARCHS to text-to-video, so a cached wan GGUF was
surfaced in the Video picker even though no Wan VideoFamily is registered in this build, and
selecting it 400s on load. Gate the video task on detect_video_family actually resolving the
arch: an unregistered video arch (wan here) falls to the unsupported bucket (hidden from chat and
the Video/Images pickers) until its family lands, so the picker never offers a GGUF that cannot
load. Correct in both stacked video PRs (ltxv resolves; wan resolves only once its family exists).
2026-07-06 14:59:51 +00:00
Daniel Han
790b41f262 Honor the resolved family default when no identifier names a video variant
default_video_generation_params fell back to a hardcoded LTX 40/4.0 when no identifier matched.
A Wan model loaded from an opaque local path under an explicit family_override (none of
gguf_filename / repo_id / base_repo carrying a wan token) then ran 40/4.0 instead of Wan's 50/5.0.
Add a fallback param and pass the resolved family's own default_steps/default_guidance from both
call sites, so the fallback tracks the actual family.
2026-07-06 14:52:11 +00:00
Daniel Han
b6b507c48b Key the auto step-cache on the pipe's default strength when the request omits it
The auto FBCache policy keyed on the full step count whenever strength was omitted, but the
loader only passes the strength kwarg when it is set, so an img2img/inpaint pipe then runs its
OWN signature default (< 1, e.g. FluxImg2ImgPipeline's 0.6). FBCache would engage on the full
28 steps while the pipe actually denoises ~16, degrading the image on exactly the short
trajectory the policy exists to keep uncached. Thread the pipe's signature default into the
policy via a new effective_request_strength helper (unit-tested), so the effective denoise count
matches what the pipe runs.
2026-07-06 14:49:47 +00:00
pre-commit-ci[bot]
092dae8f58 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 14:41:49 +00:00
Daniel Han
26aeaf52ff Resolve picked GGUFs from every cache root; hide the dead Reapply button on refresh
Two follow-ups to the video-tab review fixes:
- the cached-hub arch fallback only probed the active HF cache, but the cached-gguf picker
  scans the active, legacy, and default cache roots; a GGUF cached in a non-active root was
  offered yet 400d on load. Probe all three roots.
- on a mount/refresh with a model already resident, status.loaded made the Reapply button render
  but lastLoad (set only by our own loads) was null, so clicking it silently did nothing. Track a
  session reapply descriptor and hide the button when it is absent rather than offer a dead control.
2026-07-06 14:41:11 +00:00
Daniel Han
bb937296e7 Guard in-flight video loads from deletion, resolve cached hub GGUFs by arch, seed gen controls
Three video-tab fixes:
- delete-cached refused a loaded video repo but not one a background load is still downloading
  (status().loaded is False in that window); add VideoBackend.loading_repo_ids() mirroring the
  image backend and check it in the route so deleting mid-download can no longer yank blobs.
- the cached-gguf picker tags a cached HUB GGUF by its general.architecture, but the loader's
  arch fallback only read a LOCAL file, so an opaquely-named cached hub LTX GGUF the picker
  offered 400d on load; read the arch from the cached blob (network-free) too.
- on a mount with a model already loaded (refresh / load from another client) steps and guidance
  stuck at the pre-load default, so a base checkpoint silently generated a degraded clip; seed
  them from the backend-authoritative status.defaults once per newly-loaded model.
2026-07-06 14:14:51 +00:00
pre-commit-ci[bot]
d86c4c70e6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 14:05:24 +00:00
Daniel Han
908cb86b8d Do not widen the dense prefetch under a forced-offload memory policy
_dense_quant_prefetch_needed widened the prefetch to pull the base repo's transformer/
shards whenever a dense-quant candidate resolved, but balanced/low_vram (and the legacy
cpu_offload flag) force load_pipeline onto offload unconditionally in plan_diffusion_memory,
so its re-plan never flips to OFFLOAD_NONE and the dense build never runs. The offloaded GGUF
path then never touches those shards, so the widened prefetch only wastes a multi-GB download,
and a disk-full on that begin_load pull has no GGUF fallback (unlike the in-load_pipeline dense
failure). Mirror plan_diffusion_memory's definite-offload gates so the prefetch stays scoped.
2026-07-06 14:04:47 +00:00
Daniel Han
9ab68825ee Give Krea-2-Raw its undistilled 52-step recipe instead of the distilled default
Krea-2-Raw is in _TRUSTED_NON_GGUF_REPOS, so it is inference-loadable, but the generic
"krea" generation-defaults key matched it too and applied Turbo's distilled 8-step / no-CFG
recipe, producing degraded output on the undistilled base. Add a more specific krea-2-raw key
(52 steps, guidance 3.5 per the model card) ahead of the generic one, in both the backend
table and the frontend MODEL_DEFAULTS so the Studio UI and the OpenAI images route agree.
2026-07-06 14:00:39 +00:00
Daniel Han
aa54a062ec Gate DiT training on functional torchao for explicit int8; hide always-400 DiT modes on non-bf16 GPUs
The start route preflight only rejected non-bf16 GPUs; an explicit int8 request on
a host with a missing or stub torchao passed the preflight, evicted resident GPU
workloads, then died in the trainer child (its int8 base quantizer has no fallback).
Fold both gates into training_precision_preflight_error so int8-without-torchao fails
fast before eviction. Also empty the advertised DiT precision_modes (and surface the
reason in vram_note, drop compile) whenever the bf16 preflight would reject the family,
so /info never offers an nf4 DiT option the route always 400s.
2026-07-06 13:35:43 +00:00
pre-commit-ci[bot]
cc71be93f2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 12:21:32 +00:00
Daniel Han
2bc6f6879a Wan: fp32 VAE decode, correct bf16-resident DiT sizes, TI2V /32 snap
Verified against diffusers 0.39 source + the HF configs/safetensors headers:
- The Wan VAE decodes in float32 (WanPipeline loads AutoencoderKLWan at torch.float32
  while the pipe runs bf16); the loader cast every component to bf16, degrading every
  clip. Add vae_force_fp32 (both Wan families) and pin pipe.vae back to fp32 after build.
- The Wan transformers ship FP32 on disk (safetensors headers are F32; A14B index =
  57.15 GB per expert = 14.3B x 4, TI2V = 20.0 GB = 5B x 4), so bf16_components_gb held
  the fp32 on-disk sums (114.3 / 20.0) instead of the documented bf16-resident sizes.
  Halve to 57.2 (two A14B experts) and 10.0 (TI2V), so the plan no longer over-budgets
  the DiTs ~2x and forces needless offload on an 80 GB GPU.
- TI2V-5B's VAE is 16x spatial (vae/config.json), so WanPipeline floors H/W to 16*2 = 32.
  Snap TI2V to /32 (was /16) so the recorded size matches the generated clip (a 720
  request was recorded but rendered at 704). A14B keeps /16 (Wan2.1 8x VAE).
2026-07-06 12:20:56 +00:00
Daniel Han
cb6737cbb8
Auto Xet to HTTP download fallback in from_pretrained; share Studio's fallback via unsloth_zoo (#6638) 2026-07-06 05:13:25 -07:00
pre-commit-ci[bot]
37ed4efaa6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 11:59:39 +00:00
Daniel Han
5e9b90f19c Guard loaded video GGUFs from deletion; resolve local GGUF family by arch
- delete_cached_model guarded chat (llama.cpp/transformers) and Images but not the
  Video backend, so a loaded video GGUF (which shares the On-Device GGUF delete UI)
  could be removed from under a live pipeline. Add the mirror guard on
  get_video_backend().status().
- The Video picker admits a local GGUF by its general.architecture, but the loader
  detected the family only from path/name tokens, so a renamed ltxv file (e.g.
  model.gguf) was offered yet failed validate_load_request with a 400. _detect_load_family
  now falls back to reading the arch (its string is a family alias) when name detection
  misses, so the loader accepts exactly what the picker offered; an unsupported video arch
  (wan) still resolves to None and 400s as before.
2026-07-06 11:59:03 +00:00
Daniel Han
89e5f69d90 Don't prefetch dense shards for a prequant load; surface resolved provenance
- _dense_quant_prefetch_needed widened the transformer/ prefetch to pull the base
  repo's full dense bf16 shards even when a prequant checkpoint is configured
  (candidate.prequant), contradicting its own docstring. That both defeats the
  prequant download savings and can hard-fail begin_load on a disk-full (no GGUF
  fallback there). Only widen for a real dense build (candidate is not None and
  not candidate.prequant).
- DiffusionStatusResponse declared no 'resolved' field, so Pydantic's default
  extra='ignore' silently dropped the per-control auto-policy provenance the
  backend records (build_resolved_record / state.resolved) -- the plumbing never
  reached any client. Declare the field so it round-trips.
2026-07-06 11:52:32 +00:00
Daniel Han
4f00222c2e Reserve the fp8 bf16 footprint even when the cache estimate is absent
For a narrow (fp8) Ideogram-4 base the planner reserves the family's known bf16
component total, but the reservation was gated on model_dense_mib being non-None.
On a first-time load an empty blob cache (or a best-effort download probe that
swallowed a transient HF error) leaves model_dense_mib None, so the guard skipped
the reservation exactly when it was needed: the planner then read 'size unknown ->
stay resident' and the ~54 GB pipeline OOMed a card that offload would have fit.
family_bf16_components_gb is a network-free constant, so reserve it whenever the
cache signal is absent (use it directly when None, else take the max).
2026-07-06 11:48:23 +00:00
Daniel Han
a1114bfdc3 Align LR-schedule copy with the diffusion two-card chart layout
DiffusionCharts deliberately renders only Training Loss + Gradient Norm (the LR
curve is the deterministic schedule the user picked), but the settings copy and two
comments still promised a live LR chart. Reword them so the UI no longer references a
chart that was intentionally dropped.
2026-07-06 11:21:06 +00:00
Daniel Han
2d974219bf Deploy Krea adapters on Turbo and use its distilled recipe over the API
- DiffusionFamily gains deploy_base_repo (krea/Krea-2-Turbo): deploying a LoRA
  trained on Raw now previews it on Turbo, not the non-distilled Raw checkpoint.
  Scoped to a same-precision override so it never turns an nf4 train base into a
  larger bf16 deploy load; exposed through family_train_infos -> the Train UI's
  onDeployClick / historical-run deploy resolve the deploy base.
- _GENERATION_DEFAULTS gains a Krea entry (8 steps, 0 CFG) so the OpenAI
  /v1/images/generations route matches the Create UI's documented distilled recipe
  instead of falling through to the generic (9, 0.0).
2026-07-06 11:16:25 +00:00
pre-commit-ci[bot]
cc6d7c96a6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 11:04:54 +00:00
Daniel Han
6bd3e87c6f Gate DiT training precision: deny fp8 for Qwen, gate explicit int8 on torchao, gate advertised dense modes + route on bf16
- normalized() + family_train_infos() mirror the inference fp8 deny for
  Qwen-Image (activation outliers exceed fp8's range and corrupt the trained
  result); int8 stays allowed and the UI no longer advertises fp8 for it.
- _resolve_base_precision() gates an explicit int8 on a FUNCTIONAL torchao, the
  same gate auto and /info already apply, so a missing/stub torchao fails fast
  instead of silently loading dense with compile disabled.
- train_precision_modes() gates the dense modes (bf16/int8/fp8/auto) on
  torch.cuda.is_bf16_supported(), so a non-bf16 CUDA GPU (T4/V100/RTX 20xx) is
  offered only nf4 instead of a start that evicts resident models and then fails.
- start_diffusion_training preflights bf16 support for the DiT families BEFORE
  _free_gpu_for_diffusion_training(), so any DiT start (nf4 included, since the
  trainer requires bf16 unconditionally on CUDA) fails fast without eviction.
2026-07-06 11:04:07 +00:00
pre-commit-ci[bot]
e6bd256ad6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-06 10:50:45 +00:00
Daniel Han
311d9da637 diffusion: disk-gate the dense-quant prefetch, keep Speed=off bit-exact
- _dense_quant_prefetch_needed widened the prefetch to pull the base repo's bf16 transformer/ shards
  whenever a dense-quant scheme could resolve, with no disk check. On the offload path that can fill
  the cache volume mid-download and hard-fail the load in a spot unload/cancel cannot preempt, instead
  of the disk guard falling back to running the GGUF as-is (the Dtype hint's documented disk fallback).
  Defer to resolve_dense_quant_candidate, the same disk-aware resolver load_pipeline re-plans against,
  so the prefetch widens only when the dense build would really run.
- An explicit Speed=off (bit-exact) load with an unset dtype was promoted to auto-quant by the Dtype
  default, silently engaging int8/fp8 + compile and breaking the bit-exact request (an auto DEFAULT
  overriding an EXPLICIT control). Suppress the auto-dtype default when speed is explicitly off, in both
  load_pipeline and the prefetch.
2026-07-06 10:50:09 +00:00