Commit graph

92 commits

Author SHA1 Message Date
Daniel Han
36df317293 Trim the comments across the diffusion backend
Comment-only pass over the Python this PR touches: drop what the code already
says, collapse multi-line explanations that still read on one line, and keep
the reasoning that is not recoverable from the code. No code, docstring
semantics or behaviour changes; verified with an AST comparison against the
previous revision, and the backend suite is unchanged (same 37 environment
failures as before: the API integration tests that need a live keyed server,
the flash-attn install hooks, and the GPU memory fields).
2026-07-26 20:31:19 +00:00
Daniel Han
9eed2bdfa3 Fix quantized-load LoRA bake, prequant family exclusions and outpaint canvas
Six review findings across the Images page and model scanning:

- The quantized (int8/fp8) load path can only attach LoRA adapters
  before quantization, but the frontend load request had no loras field,
  so every generation after such a load was rejected and each reload
  repeated it. Send the selection with the load.
- build_prequant_checkpoint passed no family to the scheme exclusions
  while recording the family in metadata, so a Qwen int8 artifact baked
  the short-M text-stream linears and was then rejected wholesale by the
  loader's family-keyed check.
- Registering a bare single-file checkpoint directory produced no On
  Device row even though the images loader can load it; only its parent
  worked. Admit that shape when nothing else matched.
- Unload left the Reapply target set, so the repair path was skipped and
  Reapply reloaded the ejected model. Clear it, as the video page does.
- Both FLUX.2 bases were trusted for training but not inference, so
  Deploy to Create rejected every FLUX.2 adapter.
- Outpaint allocated the grown canvas before downscaling, exceeding the
  browser canvas area cap on a large photo; an over-cap canvas is
  unusable, so Extend silently posted a fully transparent image and
  mask. Scale the source first.
2026-07-26 08:12:01 +00:00
michaelhan
bfe6f542ce Merge origin/main into image-generation (PR #6763)
Resolve the drift between PR #6763 and current main:

- deletion: main moved cached-model deletion into hub/services/models/deletion.py,
  so the PR's Images/Video in-use guards move there too as _diffusion_blocks_delete
  and _video_blocks_delete, keeping main's fail-closed 503 contract.
- llama_keepwarm: take main's rewrite, re-apply the PR's image/video inference
  suffixes so a generation in flight blocks an idle unload.
- routes/training: keep main's sidecar-swap 409 and resume_source_run_id, run
  start_training in the worker thread the PR's unload hook needs.
- model picker: main renamed components/assistant-ui/model-selector ->
  features/model-picker/... and rewrote pickers.tsx, so the PR's picker work is
  ported onto main's version (task/catalog props, task gating of hub + cached +
  local rows, single-device expanderGpuGb, fine-tuned section hidden when scoped)
  rather than reverting main's pinned-models and per-model-config work.
- images/video pages: imports repointed at the new model-selector path.
- tests: delete-guard tests retargeted at the deletion service.

Typecheck, i18n parity and model-catalog checks pass.
2026-07-25 00:34:38 -07:00
Souravrajvi0
8c975fcbaf
fix: pin torchcodec for torch 2.10 and warn on ABI mismatch (#7299)
* fix: pin torchcodec for torch 2.10 and warn on ABI mismatch

Add unsloth[audio] extra with torchcodec>=0.10.0,<0.11.0 and emit a
clear warning when installed torchcodec minors disagree with torch
(unslothai/unsloth#7225).

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

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

* fix(packaging): address Codex review on torchcodec/torch 2.10 compat (#7299)

- Postpone annotations so import_fixes loads on Python 3.9
- Align TORCH_TORCHCODEC matrix with upstream (2.9: 0.8/0.9, 2.8: 0.6/0.7)
- Fix mismatch hint upper bound (<0.11.0) and gate audio-torch210 suggestion
- Split audio extra per torch minor; gate torch210 pin behind python>=3.10
- Bundle audio-torch210 only in *-torch2100 install extras

* fix(security): refresh openai CRITICAL scan baseline hashes (#7299)

openai package code drift reopened five CRITICAL findings in the
extras pip-scan-packages shard (C2 loop body hashes + IMDS/network
evidence). Update the reviewed allowlist evidence/hashes so CI gates
on new findings only, not benign SDK churn.

* chore: retrigger CI after baseline refresh (#7299)

* chore: touch scan baseline comment to retrigger security audit (#7299)

* Guard torchcodec version parsing so bad version strings cannot break import

* Bundle audio pin into intel-gpu-torch210 and guard the mismatch warning

* Tighten comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-07-23 19:12:52 -07:00
Daniel Han
b0d6131567
Security audit: refresh scan baselines for current dependency set (#7362)
The pip scan-packages extras shard has been red on main because openai
2.47.0 changed the code inside five previously baselined findings, so
their evidence hashes no longer matched the allowlist. The hf-stack
shard was about to go red the same way: unsloth-zoo 2026.7.5 changed
two baselined test files.

All seven reopened findings were re-verified against the exact resolved
archives before re-baselining:

- openai/_base_client.py: while True in SyncPage.iter_pages, the
  pagination iterator.
- openai/auth/_workload.py: Azure IMDS and GCP metadata token
  providers for the documented workload identity federation feature.
- openai/resources/{beta/responses,realtime,responses}: while True in
  websocket __aiter__ event loops; the loop bodies gained reconnect
  handling in 2.47.0, which is what shifted the hashes.
- unsloth-zoo tests/test_vision_collator_audio.py: asserts that an
  inline /tmp/a.wav path is passed through by the audio collator.
- unsloth-zoo tests/test_gemma4_forced_float32_ple_dtype.py:
  compile()/exec() of the project's own generated Gemma4 PLE cast
  helper source in tests.

No existing entries were removed. All three shards now exit 0 locally
against the same requirement sets CI uses.
2026-07-23 02:02:41 -07:00
Michael Han
d5cf96d628
Studio: add local speech-to-text dictation engine (#7095)
* Studio: add Voice settings tab (dictation, dictionary, read aloud)

New Voice tab in Settings, placed just before About:

- Dictation: microphone picker, browser STT engine, recognition language,
  and an inline mic test with a live transcript
- Dictation dictionary: entries rewrite matching speech to their exact
  spelling and casing, applied in both dictation paths
- Recent dictations: last 20 final transcripts with copy and clear, so
  text can be recovered if it lands in the wrong place
- Read aloud: optional button on assistant responses with two engines,
  curated system voices (novelty and legacy voices filtered, quality
  ranked, capped at 20) or the TTS audio model loaded in Unsloth via
  /audio/generate (e.g. Orpheus), plus speed, pitch, volume and preview

Settings persist in localStorage (unsloth_voice_settings) and are read
at call time so changes apply without reloading the runtime. Adds en
keys plus the tab label for ja, zh-CN and pt-BR.

* Studio: drop the single option STT engine select, rename TTS option

The STT engine dropdown only had one entry, so it added noise without
giving a real choice. The engine row can come back once local STT
models land. Also renames the TTS engine option Unsloth TTS model to
Load TTS model to make the action clearer.

* Studio: harden Voice settings against edge cases found in simulation

Simulated the feature across Chromium, Firefox and WebKit plus node
level unit runs and backend contract checks. Fixes from the findings:

- Dictionary rewrite used a replacement string, so entries containing
  dollar patterns corrupted transcripts (A$$AP became A$AP, $& injected
  the match). Switched to the callback form of String.replace
- Persisted voice settings now validate types on hydration: non string
  micDeviceId, dictationLanguage and ttsVoiceURI, and non boolean
  ttsEnabled fall back to defaults instead of flowing into the UI
- Dictionary entries are trimmed, capped at 120 chars and re-sanitized
  on hydration
- The Test dictation panel now falls back to the default microphone
  when the saved device is unplugged, matching the composer adapter

Test coverage: 46 unit assertions (dictionary regex edge cases across
unicode, word boundaries and injection, voice curation for simulated
macOS, Windows and Linux voice inventories, corrupt storage merge),
13 backend contract checks against /audio/generate on an isolated
instance, and 60 browser assertions across the three engines covering
rendering, degradation without SpeechRecognition, curation in a real
DOM, dictionary persistence with unicode and dollar entries, the
no-model preview error path and corrupt localStorage recovery.

* Studio: address Voice settings review feedback

Verified each review comment before acting. Confirmed and fixed:

- Editing a dictionary entry was broken in two ways: the store trimmed
  on every keystroke so spaces could not be typed, and clearing the
  field deleted the entry and unmounted the input mid edit. Updates now
  keep the raw value and a blur commit trims or removes the entry
- The unplugged mic fallback checked instanceof DOMException, but a
  cross browser probe showed Firefox and WebKit throw
  OverconstrainedError objects that are not DOMExceptions, so the
  fallback never fired there. Matching on the error name now
- When the browser ended a dictation test on its own (silence timeout),
  the mic stream stayed open. All recognition end paths now stop the
  tracks and save the transcript through a single finalize path
- The studio TTS audio element now releases its WAV data URL as soon as
  playback ends, fails or is cancelled
- Allow microphone now reports insecure contexts (no mediaDevices)
  accurately instead of claiming access was blocked
- Voice tab copy moved into i18n keys per src/i18n/AGENTS.md, so locale
  overlays can translate it; en is the baseline and parity passes
- unsloth_voice_settings added to the Reset all local preferences key
  list so voice preferences obey the reset
- Non default microphones note that the system default is used when the
  browser speech engine cannot bind a specific device, since browsers
  without the start(track) overload ignore the argument silently

Re-ran the full simulation set after the changes: 46 unit assertions,
13 backend contract checks and 60 browser assertions across Chromium,
Firefox and WebKit all pass, plus a dedicated browser probe for the
dictionary editing behavior.

* Studio: use the chat mic icon in Voice settings for consistency

The Voice tab and its buttons used the hugeicons Mic02 glyph while the
chat composer uses a custom filled mic. Extract that composer icon into
a shared lib/mic-icon component, drop the duplicate inline copies in
thread.tsx and shared-composer.tsx, and use it for the Voice tab icon
and the tab's mic buttons so the microphone looks the same everywhere.

* Studio: address second round of Voice settings review feedback

Verified each new comment against the current code first. One item was
already fixed in the previous round (recording transcripts when the
browser ends a dictation test on its own). Confirmed and fixed:

- The microphone row showed a picker with generic names when browsers
  enumerate unlabeled devices before permission, leaving no way to
  grant access from the row. It now branches on whether labels are
  visible and shows Allow microphone otherwise
- Compare chat dictation ignored the selected microphone. It now opens
  the chosen device with the same fallback rules as the main adapter,
  passes the track to recognition where supported and releases the
  stream when recognition ends
- Closing the Voice tab cancelled the shared speechSynthesis even when
  read aloud was playing a chat message. Cleanup now only cancels when
  the tab owns an active preview
- Double clicking Start test could race two recognizers and leak the
  first stream. A starting flag set before the getUserMedia await makes
  start reentrancy safe
- Turning off the read aloud setting mid playback removed the only stop
  control. The stop button now renders whenever a message is speaking
- When an engine lacks the start(track) overload, both dictation paths
  now release the selected device stream before retrying with the
  default microphone instead of holding it open
- Read aloud support no longer requires Web Speech synthesis: the
  Unsloth TTS engine only needs audio playback, so it stays available
  in WebViews without speechSynthesis, with a clear error if the system
  engine is chosen there

Not addressed here: cancelling in flight backend TTS generation on
stop. The route runs generation in a worker thread without a
cancellation path, which is shared pre existing behavior with audio
chat generation and belongs in a backend change.

All suites re-run green: 46 unit, 13 backend contract and 60 browser
matrix assertions across Chromium, Firefox and WebKit, plus probes for
the unlabeled device branch and the double click race.

* Studio: drop empty and duplicate voiceURIs so the Voice tab never renders a crashing Select item

* Studio: guard dictation mic lifecycle in Voice test and Compare composer

Release a microphone opened after the component unmounts, and stop Compare
dictation on a permission or security failure instead of silently recording
from the default device, matching the main chat adapter.

* Studio: fix dictation and read-aloud lifecycle edge cases in Voice settings

- Join final dictation chunks with a space so recorded transcripts do not merge words
- Ignore a stale recognizer onend so a quick stop then restart is not torn down
- Use previewingRef so a double click on TTS preview does not orphan the first request
- Keep the read-aloud stop control visible when a new run starts while a message is spoken
- Stop the dictionary remove button from deleting an adjacent entry on a blur then click race

* Studio: trim redundant Voice settings comments

* Studio: fix Voice preview and Compare dictation edge cases

- Only cancel the shared speechSynthesis for a system-voice preview, so stopping
  a Studio preview no longer stops an unrelated chat read-aloud
- Release the Studio preview audio and its WAV data URL on normal completion
- Iterate every finalized result in Compare dictation so batched phrases are kept
- Cap persisted recent dictations to the last 20 on hydration

* Studio: use clipboard fallback for recents and release failed preview audio

- Copy recent dictations via the copyToClipboard helper so the execCommand
  fallback works in Safari and insecure http LAN contexts
- Release the Studio preview audio when play() rejects, not just on ended/error

* Studio: add local speech-to-text dictation engine

Add an offline dictation engine that transcribes with a local faster-whisper
model, alongside the existing browser (Web Speech) engine. The browser engine
streams audio to Apple or Google speech services and needs internet; the new
engine runs on the server, works offline, and drives any chat model without
evicting it (it loads in the backend process, separate from the model
subprocess). It also gives Firefox dictation, which has no Web Speech support.

Backend: a lazily-loaded, kept-warm faster-whisper sidecar and three routes
under /api/inference/audio (stt/status, stt/load, transcribe). faster-whisper
is torch-free, so this does not disturb the existing model stack.

Frontend: a Dictation engine setting (browser or local model), a curated model
picker with sizes, and MediaRecorder capture posted to the transcribe route.
The model warms automatically when the engine is selected, with live status.

* Studio: stream local STT transcription as you speak

Local dictation showed nothing until you stopped, because the whole clip was
transcribed once on stop. Now the growing recording is re-transcribed on a
fast pass every second and emitted as live interim text, with an accurate
final pass on stop. Partial recordings decode fine, and the model refines
earlier words as more audio arrives.

Adds an interim flag to the transcribe route (beam 1, no VAD) for the fast
preview pass; the final stop uses the accurate path.

* Studio: make local dictation stop instant and reliable

Stopping local dictation waited for a final network transcription before the
session ended, so the stop button did not flip and a second click ended the
session early and dropped the text. Now stop commits the live transcript
immediately, releases the mic at once, and ignores a second stop while
finalizing. Previews run more often so the committed text is current.

* Studio: record local dictation in short clips for reliable streaming

Re-transcribing a growing buffer every second got slower as it grew, flooded
the backend, showed stale words, and could leave the stop button stuck waiting
on a backlog. Record short independent clips instead and transcribe each once,
appending the text as you speak. Work per clip is bounded, so stopping is
prompt (with a hard timeout as a safety net) and long dictations stay smooth.

* Studio: dictate then transcribe once on stop, ChatGPT style

Local STT dictation streamed by re-transcribing the growing clip, which
was quadratic and saturated the backend (multi-second lag), and stop only
halted the recorder without releasing the mic, so it kept recording. Record
the microphone continuously, release it the instant the user stops, and
transcribe the whole clip once. Stopping is immediate and the transcript
lands in about a second. Also add the tiny model for the fastest option.

* Studio: surface dictation and read-aloud failures instead of failing silently

- Compare dictation reports microphone and speech-recognition errors via toast,
  reusing the main chat adapter's describeMediaError and describeSpeechError
- Read-aloud toasts genuine model or synthesis failures while ignoring cancellations

* Studio: ChatGPT-style recording bar for dictation

Clicking the mic now drops the composer into a dedicated recording bar
with a live waveform, a discard (X) and a confirm (tick), instead of a
plain stop button. The tick stops recording and transcribes the clip;
the X throws the recording away and keeps whatever text was already in
the composer. The model adapter taps the mic with an analyser to drive
the waveform, and the router tracks the live session so the X can cancel
it without transcribing.

* Studio: transcribe dictation while speaking, ChatGPT layout

Match ChatGPT's recording layout: the bar now renders in place of the
input with the left plus button kept, the waveform in the middle, and
the discard and confirm buttons together on the right.

Cut the post-confirm delay by transcribing in the background as the user
talks. The audio is split at natural pauses (voice-activity detection off
the same analyser that drives the waveform) and each clip is transcribed
as it is cut, so confirming only has to finish the short final tail. The
model is also warmed when recording starts so the first run never pays a
cold load.

* Studio: ChatGPT waveform, hide tools while dictating, faster STT

Make the recording UI read like ChatGPT: the waveform is now a dense row
of round dots that rise into thin centered bars, and while dictating only
the plus button shows, with the mode badge and tool toggles hidden so the
bar is just the waveform and controls.

Speed up transcription: decode greedily (beam_size=1), which is several
times faster on CPU with negligible accuracy loss on short dictation
clips, and cap background segments at 6s so the final tail after confirm
stays short.

* Studio: finish ChatGPT voice bar and low-latency STT

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

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

* Studio: full-width waveform with a timer that freezes on stop

Use the full-width waveform for the recording bar: brighter, bigger bars
that advance on a fixed cadence (keeping peaks between advances) so they
glide instead of racing by, inset from the composer edges. Keep a visible
timer and the green confirm button, matching the ChatGPT reference, and
freeze the timer and waveform the moment the user confirms.

* Studio: fix multilingual local dictation

* Studio: speed up dictation and release local STT

* Studio: harden dictation finalization and STT decoding

* Studio: restore Firefox dictation fallback

* Studio: add dictation history manager

* Studio: manage speech model downloads

* Studio: remove em dash from voice model label

* Studio: move dictation history into Voice

* Studio: source local STT from Unsloth Whisper models

Point the dictation STT sidecar and its Model Hub download entries at
Unsloth's Hugging Face Whisper repos (small, large-v3-turbo, large-v3)
and run them through Transformers, so Studio only ever downloads
Unsloth-uploaded weights. Drop faster-whisper and the Systran/mobiuslabs
repos; keep the Model Hub as the only download path via local_files_only,
and keep PyAV for audio decoding.

Device selection uses float16 on CUDA and float32 on MPS and CPU, since
Whisper's decoder is unstable in float16 on MPS and repeats tokens.

Shorten the model picker labels to name plus download size and update the
STT tests for the new backend.

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

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

* Studio: smooth dictation waveform and keep pill height

* Studio: align STT model dropdown width and tidy voice copy

* Studio: guide to local engine when browser dictation is offline

* Studio: clarify voice section and STT model copy

* Studio: keep STT warm with training-aware eviction

* Harden STT lifecycle and browser compatibility

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

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

* Fix model discovery test lint

* Harden cross-browser microphone errors

* Harden cross-browser microphone errors

* Surface voice test recognition errors and fall back to Studio TTS

- Voice test now toasts non-abort speech-recognition failures instead of
  ending silently, matching the main and Compare dictation paths.
- Read-aloud routes to the backend model when the runtime lacks Web Speech
  synthesis (audio-only WebView), so it no longer errors immediately.

* Fix reviewed STT lifecycle races

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

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

* Fix read-aloud fallback controls

* Guard read-aloud stop when deleting a non-speaking message

aui.message().stopSpeaking() throws unless this message is the one being
read aloud, so calling it unconditionally rejected the delete handler before
the message was removed. Only stop speech when this message is speaking.

* Cap recent dictation transcript length before persisting

Recent dictations only limited entry count, so a long transcript stored the
full text in the persisted voice settings and a few could exceed the
localStorage quota, throwing synchronously from the uncaught dictation cleanup
path. Truncate each entry on save and on hydration, matching the dictionary cap.

* Studio: keep dictation mic clickable and guide to local model

Register the dictation adapter unconditionally so the mic stays enabled
for any engine and starts working right after switching to the local
model on an already-open thread.

When the browser engine cannot run (Firefox, Brave, non-secure origins),
clicking the mic shows a toast that points to the local speech-to-text
model instead of leaving a disabled button. The toast stacks its action
below the text with a fully rounded button.

* Studio: add bottom padding below the dictation guidance toast button

* Studio: increase bottom padding under the dictation toast button

* Studio: add bottom padding inside the dictation toast button

* Studio: add five Whisper defaults and custom model search

Add private UnslothAI Tiny and Base mirrors to the curated local STT choices while keeping Small as the default. Let users search or paste a Transformers-compatible Whisper repository and validate it end to end.

Keep short dictations in one clip to avoid repeated padded encoder work, then split longer recordings near Whisper's 30-second boundary.

Update hidden model filters and tests, including the CPU-only CI runtime stub for PyAV.

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

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

* Studio: use public Unsloth Whisper repositories

Point the Tiny and Base dictation defaults to the public unsloth repositories and remove the private mirror references from model filtering and tests.

* Studio: update Whisper download sizes

Reflect the cleaned public Tiny and Base repositories in the curated model labels.

* Studio: right-align STT model size, fix dropdown wheel scroll, refresh sizes

- Show the download size on the right of each model row so long names
  like Whisper Large v3 Turbo no longer hide it
- Update curated Whisper sizes to the safetensors weights actually
  downloaded: Tiny 151 MB, Base 290 MB, Small 967 MB
- Drive the model list scroll from a wheel handler so the mouse wheel
  scrolls it inside the Settings dialog, not just the scrollbar
- Add a search icon and shorten the placeholder to Search model

* Studio: do not search when a dictation model is picked, shrink repo label

- Treat the filled-in model text as a selection, not a query, so choosing
  a model no longer kicks off a Hugging Face search
- Make the repository line under each model name smaller

* Studio: tighten dictation model and local engine descriptions

* Studio: keep model display on pick instead of the query, shrink row text

- Guard the combobox input so selecting a model shows its name and does
  not echo the typed query back or start a search
- Map the item label to the friendly display so picks fill the field
- Reduce the model name and size text in each row

* Studio: show only the model name in the dictation field, shrink size label

- Drop the download size from the search field; the name alone is shown
  once a model is selected, with sizes kept in the dropdown list
- Reduce the size label text in each row

* Studio: clarify the dictation model description

* Studio: drop Hugging Face from the dictation model description

* Studio: move the dictation dictionary to its own Manage subpage

- Replace the inline entry list with a Manage row, matching Dictation
  history, so a long dictionary no longer crowds Voice settings
- Add a DictationDictionaryView subpage that holds the entry editor

* Studio: match STT field font, use best voice for System default

- Bump the dictation model field text to text-sm so it matches the
  engine dropdown next to it
- Resolve the System default read-aloud voice to the top curated voice
  instead of the browser default, which is a robotic legacy voice on macOS

* Studio: rerank read-aloud voices and drop duplicate voice entries

- Rank by vendor quality, then the user's locale, then a preferred list of
  natural voices, so the best voice leads instead of the first alphabetically
- Collapse voices that macOS reports twice under one name and language

* Studio: fold dictionary and recents into the dictation section

- Drop the separate Dictation dictionary and Recent dictations headings;
  their Manage rows now sit under Dictation, split by the row divider
- Shorten the custom spellings description

* Studio: add search and sort to dictation history

- Filter saved dictations by text with a search field
- Sort by newest, oldest, or A to Z; show a no-matches message
- Keep Clear all available regardless of the current filter

* Studio: settle cancelled STT loads before training and fix dictation review items

Wait for a cancelled STT load to exit and release its memory before
reporting it freed for training, so the loader cannot still be inside
from_pretrained()/.to(device) holding VRAM when the training subprocess
starts. A load that finishes before observing the cancel now gets
unloaded so the memory is actually reclaimed.

Clear the accelerator cache before the CPU fallback in load() so a failed
CUDA/MPS load does not strand reserved VRAM once the sidecar is marked
CPU-resident.

Send the saved Hugging Face token when polling STT download progress so a
gated or private repo resolves and shows the correct Load/Downloaded
state instead of reporting missing.

Mark the composer Dictate button as type="button" so clicking it does not
also submit the draft when the composer already has text or attachments.

* Studio: pin dictation settings per session and close STT startup races

Capture the STT model and language when a dictation session starts and
pass them to every queued segment and the warm-up load, so changing the
model or language mid-recording no longer transcribes the same clip with
the wrong model or a model that is not downloaded.

Check the local runtime at the top of transcribe(), before the model
cache lookup and the bounded audio decode, so a server missing PyTorch or
Transformers returns 501 up front instead of decoding a long clip first.

Treat the training startup window as active for STT device selection.
start_training frees VRAM in before_spawn but only assigns _proc later, so
a concurrent STT load could take the GPU that was just cleared. A startup
flag now reports training active from the free until the process is live,
forcing those loads to CPU; a finally clears it on every exit.

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

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

* Studio: stub the STT runtime check in transcribe orchestration tests

transcribe() now verifies the local runtime up front, so the unit tests
that exercise transcription orchestration must treat the runtime as
present to keep passing where PyTorch, Transformers, and PyAV are not
installed. Stub ensure_stt_available in the shared fixture and restore
the real check in the availability and load-rejection tests.

* Harden custom Whisper dictation models

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

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

* Add whisper.cpp dictation engine with per-engine downloads and history rework

Engines
- New GGML STT sidecar that runs a managed whisper-server subprocess with
  idle unload, plus a pinned static build script (scripts/build_whisper_cpp.sh)
- Dictation engine picker now offers Browser, Local transcription
  (whisper.cpp), and Local transcription (Transformers)
- Both local engines serve the same five curated Whisper models and download
  them directly with byte-level progress reported by /audio/stt/status
- Models auto load on selection and when their download finishes
- Unload and training admission account for both engines

Benchmarks (Apple Silicon, greedy, warm, same checkpoints)
- whisper.cpp transcribes 2.4x to 5x faster than Transformers and loads in
  about 0.45s vs 0.86s for Whisper Small
- whisper.cpp GGUF path is unchanged by the Transformers addition
  (load 0.445s -> 0.444s, short clip 0.391s -> 0.347s, long 1.197s -> 1.129s)

Voice settings UI
- Plain curated model select replaces the searchable combobox
- Single download progress bar with transfer rate for both engines
- Dictation history now stores every dictation with Show more pagination,
  a top Clear history action, and links back to the chat it was spoken into
- Archived chats dialog gets the same pagination
- Delete dialog offers deleting a dictation together with its chat

Tests: 88 backend STT tests pass, including new snapshot download coverage.
Frontend typecheck, lint, i18n parity, and production build pass.

* Merge local engines into one option and source GGML models from unslothai

Engine selection
- The dictation engine dropdown is back to two choices: Browser and Local
  transcription. The selected model decides the backend: curated ids run
  GGML checkpoints through whisper.cpp, searched Hugging Face repositories
  run safetensors through Transformers
- Model picker lists the curated models and searches Hugging Face for other
  Whisper repositories, validating them before selection. The trigger is a
  plain button so the selection never renders inside a text input
- /audio/stt/status accepts a model query param so downloaded state works
  for custom repositories; the engine param on load, transcribe, and
  download routes is derived from the model everywhere

Model source
- Curated GGML checkpoints now download from the Unsloth-hosted
  unslothai/whisper-*-GGUF repositories (one repo per model) instead of
  ggerganov/whisper.cpp; cache lookups, progress totals, and in-flight blob
  tracking are per-model

Fixes
- Voice settings and dictation history were not persisting: the quota-safe
  localStorage wrapper was declared after the store that uses it, so the
  persist storage factory failed silently. Every settings write also threw
  mid-click, which kept the model picker popover from closing on selection
- is_model_downloaded now verifies config, preprocessor config, and real
  weight files instead of trusting an offline snapshot lookup, so a partial
  download left by an aborted fetch shows the Download button instead of
  failing to load
- Removed whisper.cpp mentions from user-facing text: the ready status
  shows Loaded instead of the runtime name, picker rows show the source
  repository, and runtime error messages say local transcription runtime

Verified with automated browser sessions and live API checks: selection
closes the picker with no page errors, persisted settings hydrate on
reload, a stale partial snapshot triggers download then loads on MPS and
transcribes, and curated models download from the unslothai repos. 88
backend STT tests, typecheck, lint, i18n parity, and build pass.

* Skip the duplicate source line for custom models in the STT picker

A custom repository's display name is its id, so search results and the
appended current selection rendered the same string twice. The source
line now only renders when it differs from the name; curated rows keep
their name, unslothai source repository, and download size.

* Verify every shard of a sharded checkpoint in the downloaded check

A snapshot holding one of N shards (or a corrupt shard index) passed the
downloaded check and then failed at load. When model.safetensors.index.json
exists, every shard in its weight map must now be present. Found by
simulation; covered by a regression test.

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

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

* Rename stale _starting references in the pump resilience tests

The startup flag on TrainingBackend was renamed to _spawn_in_progress but
two tests added alongside it still asserted on the old name, failing the
Python 3.11 to 3.13 CI jobs.

* Make the selected model row clearly highlighted in the STT picker

The current selection was a faint background tint. It now uses the accent
background with a medium weight name. Two line rows use a small corner
radius; single line custom repo rows keep the pill shape.

* Address review feedback on STT snapshot checks, VRAM release, and dictation UX

Verify snapshot completeness in the load preflight so a partial download
fails before the audio is decoded, for curated and custom repos alike.
Drop the failed accelerator traceback before the CPU retry so the cache
clear can actually release that memory. Keep unloading the GGUF sidecar
after cancelling an in-flight Transformers load; both engines can hold
memory at once. Allow Auto language with English-only .en checkpoints,
matching the backend which sends no forced language. Keep the discard
button usable while a transcription is pending so a slow or hung request
cannot trap the composer in dictation mode. Stop linking Compare and
settings test dictations to the unrelated active single chat thread.

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

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

* Move the CPU retry out of the exception handler

On Python 3.10 the interpreter exception state keeps its own reference
to the traceback, so dropping it from the caught exception was not
enough to release the failed accelerator load during the retry. Leaving
the handler before clearing the cache works on every supported version.

* Address review feedback on session handoff, chat pinning, and server lifetime

Starting a dictation from a second entry point now cancels the session
it replaces, so the old recording cannot keep the microphone open or
save a transcript with no discard button pointing at it. The linked
chat is pinned when recording starts, so switching threads while a
transcription finalizes cannot relink the transcript to the newly
opened chat. whisper-server is now bound to Studio's lifetime like the
other long-lived children: PDEATHSIG on Linux, the parent job object on
Windows, and pid adoption so the shutdown sweep reaps it; before this
it survived a Ctrl+C exit as an orphan still holding the model.

* Remove the dictation mic test from Voice settings

The composer dictate button covers the same check, so the test row, its
transcript panel, the unsupported fallback row, and their strings and
search entry are gone.

* Studio STT: gate GGUF whisper-server on training and fix dictation retry and dictionary edits

GGUF (whisper.cpp) sidecar:
- Launch whisper-server with --no-gpu while training is active, mirroring the Transformers sidecar's CPU device choice, so a mid-training dictation cannot reclaim the VRAM training just freed.
- Report is_loading() during whisper-server startup so training VRAM admission accounts for the accelerator memory it is about to bind.
- Require PyAV in is_available() so /audio/stt/status reports the engine unavailable when uploads cannot be decoded, instead of loading fine and then 501ing at transcription.
- Reject a missing model before decoding audio, matching the Transformers download preflight.

Voice settings:
- The download Retry button now restarts the download; the sidecar error is sticky until a new start(), so re-polling alone never cleared it.

Dictation dictionary:
- Tabbing from an emptied entry to its remove button no longer commit-splices the row first, which shifted indices and deleted the wrong entry.

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

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

* Studio STT: fix curated GGUF whisper filenames to match hosted repos

The unslothai/whisper-*-GGUF repos host the checkpoint as whisper-<id>.bin,
not ggml-<id>.bin, so every curated dictation download and cached-path
lookup 404'd and the whisper.cpp engine could never load a model. Point
GGML_STT_MODELS at the real filenames and guard the naming with a test.

* Studio STT: validate a custom dictation repo before downloading it

The Transformers STT engine accepts an arbitrary owner/model repo, but the
download route handed it straight to snapshot_download, pulling a possibly large
non-Whisper repository into the shared HF cache. Confirm the repo is a Whisper
checkpoint first with the existing metadata-only validate_remote_model (no
weights); curated ids short-circuit and the GGUF engine (curated-only) is
unaffected. A non-Whisper repo now 422s before any download.

* Studio STT: preempt a still-loading GGUF server for training admission

A whisper-server still in its startup window binds accelerator memory but has no
loaded_model yet, so training admission could miss it and launch into an OOM.
Make the GGUF startup cancellable (cancel_pending_load signals an abort event and
terminates the starting process without the load lock; _wait_for_server observes
it and raises SttLoadCancelledError; wait_for_load_to_settle blocks on the lock
until the killed server is reaped), and always fold the GGUF sidecar into the
resident-STT summary so a resident Transformers model cannot mask a loading GGUF
server. free_stt_model_for_training now cancels an in-flight load and waits for it
to settle before training claims the memory.

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

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

* Studio STT: fall back to Transformers when whisper-server is absent

A curated dictation model (including the default small) hard-pinned the GGUF
engine, but standard installs do not ship whisper-server, so every recording
501'd instead of using the Transformers engine that serves the same checkpoint
-- the GGUF sidecar's own documented contract. Add _resolve_serving_stt_engine:
a GGUF request for a curated id (the only ids GGUF accepts, all Transformers-
servable) downgrades to Transformers when whisper-server is unavailable, applied
consistently to download, load and transcribe (not unload, which targets a
specific engine). The Voice tab likewise falls back to the Transformers status so
the model is not shown unavailable and download is not blocked.

* Studio STT: hide custom Whisper caches from the legacy model pickers

The legacy /cached-models (and /cached-gguf) routes called is_hidden_model with
only the owner/model id, which cannot reach the config-based Whisper check, so a
downloaded custom (non-curated) Whisper checkpoint was still offered as a chat
model. Pass the cached snapshot path so _path_is_whisper_model inspects the repo
config and hides it, matching the discovery route.

* Studio STT: hide GGUF dictation repos, lock-free status, unload fallback, split training eviction

- Hide the curated GGUF dictation repos (unslothai/whisper-*-GGUF) from the chat
  model inventory and pickers, backend and frontend. Only their Transformers
  safetensors companions were hidden; the GGUF repos use a different org and a
  -GGUF suffix and carry a raw .bin with no whisper config.json, so they leaked
  into chat pickers.
- Make the GGUF sidecar loaded_model/device accessors lock-free, mirroring the
  Transformers sidecar. transcribe() holds self._lock across the whole inference
  call, so /audio/stt status polls and training admission previously blocked
  behind an in-flight transcription.
- stt_unload resolves through the serving resolver: a "gguf" pick on a host
  without whisper-server is served by the Transformers fallback, so unload must
  target that engine or the resident model is never freed. Unload also attempts
  every engine even if one raises, so a failure freeing one backend no longer
  skips the other.
- free_stt_model_for_training frees the Transformers and GGUF sidecars under
  independent exception boundaries so a failure unloading one no longer skips
  the other before training claims the memory.

Adds tests/test_stt_review_fixes.py covering all four.

* Studio STT: resolve Auto dictation language for the model engine + snapshot process liveness

- The model dictation adapter sent the raw setting (the literal "auto") to the
  backend, while the browser engine resolves Auto via resolveDictationLanguage.
  A batch of non-English voice notes came back mostly English on Auto. Add
  resolveModelDictationLanguage: only the literal "auto" is resolved to a
  concrete locale, gated so it becomes a language the model AND Whisper can
  honor (mirroring the backend's known-whisper-languages set); an explicit
  language, or a locale Whisper cannot honor, stays unchanged/auto-detect. Wire
  it into both adapter call sites.
- GgmlSttSidecar._process_alive() read self._process twice; a concurrent
  unload() nulls it under the lock while loaded_model/device read lock-free, so
  a null between the two reads called None.poll(). Snapshot once. Adds a
  deterministic regression test.

* studio: tighten comments and docstrings in the dictation modules

* studio: harden dictation model downloads, GGML readiness, and recording paths

Address review findings on the STT dictation feature:

- build_whisper_cpp.sh refuses to delete a whisper.cpp tree under a custom
  Studio home unless it carries the Studio ownership marker, matching the
  setup.sh policy, and marks trees it creates
- _snapshot_is_complete validates every shard of a sharded PyTorch
  (pytorch_model.bin.index.json) checkpoint like the safetensors path, and
  requires tokenizer assets (tokenizer.json or vocab.json + merges.txt)
- custom-repo downloads pin the revision resolved at validation time and
  restrict snapshot_download to the model/tokenizer/config/preprocessor file
  classes Studio loads
- the GGML sidecar holds its port reservation until just before spawning
  whisper-server and only accepts readiness from a responder that both looks
  like whisper.cpp's server and belongs to the still-running managed child,
  probing twice, so mic audio cannot be posted to a foreign local process
- the recording adapter transcribes every non-empty segment; the RMS meter
  only shapes segment boundaries and can no longer discard quiet speech
- Compare-pane dictation can cancel a pending transcription on second click,
  with the button relabeled while finalizing
- localStorage quota recovery halves the dictation history until the save
  fits, so small histories shrink too
- the System default TTS voice resolves to the platform default voice
- new dictation UI imports go through the chat and hub feature barrels

Regression tests cover the build-script gate, sharded PyTorch and tokenizer
completeness, revision pinning and allow patterns, and the whisper-server
readiness probe.

* Fix STT download and voice picker follow-ups

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

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

* Add dictation button regression coverage

* Studio: prebuilt whisper.cpp via the shared llama.cpp install core, slim bundles paired to the llama prebuilt (#7294)

* Studio STT: add prebuilt whisper.cpp (whisper-server) installer

New install_whisper_prebuilt.py downloads a per-platform whisper-server
bundle published by the unslothai/whisper.cpp prebuilt CI into the managed
whisper.cpp dir (build/bin/whisper-server) so local dictation needs no
compiler. Mirrors install_node_prebuilt.py / install_llama_prebuilt.py:
host + backend detection, sha256 pins (whisper_prebuilt_pins.json) as the
trust anchor, staging + install lock + atomic swap, traversal-safe extract,
co-located shared libs (RUNPATH=$ORIGIN), an UNSLOTH_WHISPER_PREBUILT_INFO.json
marker with idempotent "already matches", and exit codes 0/1/2/3. Not wired
into setup yet; the pins ship empty so every asset fails closed until the
first fork release is published and its digests are reviewed in.

* Studio STT: install prebuilt whisper.cpp during setup and update

Add a fail-open whisper.cpp block to setup.sh after the llama.cpp section so
`unsloth studio update` (and a fresh install) fetch the prebuilt whisper-server
into the managed whisper.cpp dir the sidecar discovers. It skips a user-set
WHISPER_SERVER_PATH/UNSLOTH_WHISPER_CPP_PATH, honors UNSLOTH_SKIP_WHISPER_INSTALL,
forwards the resolved ROCm gfx, and never aborts setup: a busy install keeps the
existing runtime, and an unavailable prebuilt stays quiet (source build is opt-in
via UNSLOTH_WHISPER_FORCE_COMPILE) since Transformers STT and browser dictation
remain. Register UNSLOTH_WHISPER_PREBUILT_INFO.json as Studio-owned evidence.

* Studio STT: harden whisper-server child env + WSL ROCm detection

- Sidecar spawns whisper-server with a scrubbed child env that prepends the
  binary dir (co-located GPU libs) to the loader path, and on WSL2 ROCm loads
  the system HIP first (HSA_ENABLE_DXG_DETECTION=1) so a bundle's bare-metal HIP
  does not segfault on /dev/dxg. Secret-bearing vars are dropped from the child.
- find_whisper_server_binary now requires an executable, not just a file.
- Installer rocm probe passes HSA_ENABLE_DXG_DETECTION and falls back to
  /opt/rocm/bin/rocminfo so a WSL ROCm host is not misdetected as CPU-only;
  gfx parsing skips the gfx000 CPU agent and generic ISA lines.
- Tests for the child env (secret scrub, lib dir, WSL HIP precedence), the
  executable check, and the WSL rocm detection.

* Studio STT: in-app whisper.cpp prebuilt update stack + ship pins in the wheel

Mirror the llama.cpp update stack for the whisper.cpp prebuilt so Studio can
detect and install a newer whisper-server release from inside the app:
- backend/utils/whisper_cpp_freshness.py: read UNSLOTH_WHISPER_PREBUILT_INFO.json
  and compare the installed release against the newest unslothai/whisper.cpp
  release. Whisper tags are v<upstream>-unsloth.<N>, so is_behind compares a
  (major, minor, patch, serial) key with a strict downgrade guard; 24h cache;
  fail-open.
- backend/utils/whisper_cpp_update.py: run install_whisper_prebuilt.py to fetch
  and atomically swap the newest bundle, unloading the warm GGUF sidecar first.
- backend/routes/whisper.py mounted at /api/whisper (update-status + update).
- pyproject: add whisper_prebuilt_pins.json to studio package-data so the
  installer's trust anchor ships in the wheel (it is a data file, not a .py
  module, so package discovery alone does not include it; node_prebuilt_pins.json
  is listed for the same reason). Without this a pip-installed wheel had no pins
  and the prebuilt install aborted to Transformers STT.
Adds test_whisper_cpp_freshness.py (version parser, is_behind matrix + downgrade
guard, marker layouts, stale decision, fail-open).

* Studio STT: verify whisper prebuilts via the release checksum index, like llama.cpp

Re-align the whisper.cpp prebuilt installer to install_llama_prebuilt.py's trust
model: instead of a committed whisper_prebuilt_pins.json, verify every download
against the release's own whisper-prebuilt-sha256.json checksum index, fetched
from the same GitHub release.

- parse_release_checksums / fetch_release_checksums / expected_sha256_for replace
  the pins layer. The index is validated for schema/component and that its
  release_tag matches the resolved release; an asset absent from it, a release
  that does not publish it, or a manifest sha256 that disagrees with it all fail
  closed to a source build.
- resolve_release_tag now resolves the newest published release at runtime (or an
  explicit --published-release-tag), matching llama and the freshness check;
  removed the pinned-default and the UNSLOTH_WHISPER_ALLOW_UNVERIFIED opt-in.
- Delete studio/whisper_prebuilt_pins.json and drop its pyproject package-data
  entry (nothing to ship now, same as llama which has no committed pins).
- Adds test_install_whisper_prebuilt_checksums.py (index parser, fail-closed on
  uncovered asset, tampered-manifest guard, newest-release resolution).

This is a same-origin checksum (integrity, not authenticity), identical to the
llama.cpp installer; pair releases with GitHub artifact attestations for provenance.

* Resolve whisper prebuilt release via the download host (no GitHub API)

Mirror install_llama_prebuilt.py's fast path: resolve the release tag from
the releases/latest redirect and fetch the manifest + checksum index from
constructed releases/download URLs, so the common install path makes zero
api.github.com calls (unauthenticated api.github.com is capped at 60 req/hour
per IP; the download host is not). Fall back to the GitHub API only on a 404,
malformed asset, or tag mismatch.

* Studio STT: coverage-aware whisper prebuilt selection via a shared core

whisper's select_artifact returned the first os/arch/backend manifest match and
ignored the SM-coverage fields the release manifest already carries, so a
Blackwell B200 (sm_100) was served cuda12-legacy (sms 50-61) -- runnable only via
forward PTX JIT. install_llama_prebuilt.py on the same host correctly picks
cuda13-newer.

Extract the coverage-aware selection into a shared, component-agnostic core under
studio/backend/utils/prebuilt/ (selection + GPU host-capability detection), lifted
from llama's linux_cuda_choice_from_release / _artifact_covers_sms / _sm_range and
generalised over a normalised artifact. whisper's HostInfo now records the GPU
compute caps + driver CUDA version (honoring CUDA_VISIBLE_DEVICES), and
select_artifact routes CUDA/ROCm through the shared selector: every visible SM
must be covered, the tightest-covering profile wins (Blackwell-aware runtime-line
ordering), ROCm matches the gfx target exactly, and an uncovered GPU falls back to
the CPU bundle. CPU/Metal/Vulkan keep first-match. The resolver JSON, exit codes,
and "already matches" contract are unchanged.

On the B200 the installer now resolves cuda13-newer, matching llama.

* Studio STT: gate whisper CUDA selection on the on-disk runtime, like llama

The prebuilt CUDA bundles are dynamically linked and intentionally do NOT ship
libcudart/libcublas -- they load the same runtime the host already has. So the
driver's advertised CUDA version is only an upper bound: a cuda13 bundle still
needs cuda13 runtime libraries present on disk. Port llama's on-disk runtime
scan (detected_linux_runtime_lines / detected_windows_runtime_lines) into the
shared core and intersect it with the driver-compatible lines in
select_cuda_attempts. A host with a cuda13 driver but only cuda12 runtime (e.g.
torch-cuda12) now correctly gets a cuda12 bundle instead of an unloadable cuda13
one; a host with no CUDA runtime at all falls back to CPU.

Fixes a glob bug in the port (any(Path(d).glob(p) for d in dirs) tests generator
truthiness, not a match) that made every major report present; add a real
filesystem test that exercises the scan.

* studio: harden shared prebuilt core to full llama parity

Apply the review findings on the shared coverage-aware prebuilt-consumer
core so whisper.cpp selection is exactly equivalent to the llama.cpp path.

hosts.py: port llama's CUDA_VISIBLE_DEVICES handling. A GPU hidden by an
index/UUID selector now reports has_usable_nvidia False instead of staying
usable, via supports_explicit_visible_device_matching plus the physical /
explicit-match branches, and _select_visible_rows now matches rows the way
llama does (index or UUID, gpu- prefix optional) and skips unmatched tokens
rather than keeping all rows. Adds the Linux /proc/driver/nvidia/gpus
fallback and has_physical_nvidia. Adds parse_macos_version.

runtime_libs.py: the Linux on-disk scan now requires the exact libcudart /
libcublas SONAME (libcudart.so.13), not a libcudart.so.13* glob, so a bare
versioned file without the SONAME symlink no longer counts as loadable.
Hardens the ldconfig parse against an empty left-hand side.

selection.py: fix the Blackwell/torch reordering so it keys on the covering
runtime lines (falls through to the torch preference when the covering lines
were filtered out), matching linux_cuda_choice_from_release. Corrects the
compatible_runtime_lines_for_driver docstring: the bundles do not ship the
CUDA runtime, so the driver version is only an upper bound and the caller
must intersect with the on-disk scan.

install_whisper_prebuilt.py: enforce a macOS artifact's min_os (new
HostInfo.macos_version) so a bundle that cannot load on the host OS version
is dropped. Keep resolver stdout to only the JSON line by leaving logs on
stderr in --resolve-prebuilt mode, and map an unexpected probe failure to
prebuilt_available False instead of a traceback.

Tests: new host-probe suite for the visible-device logic, exact-SONAME
runtime-scan cases, macOS min_os filtering, resolver stdout-only-JSON,
exit-code mapping, and the repo key.

* studio: fix whisper prebuilt selection + launch parity gaps from review

A parallel review surfaced integration defects where the whisper path could
select or launch a bundle that cannot run on a concrete host. Each is fixed to
match install_llama_prebuilt.py.

macOS min_os: the manifest labels macOS requirements as macos-<version>
(e.g. macos-14.0), which the version parser could not read, so the guard was a
no-op and a macOS-13 host would install the macos-14 Metal bundle. Strip the
platform prefix before parsing.

ROCm gfx detection: _detect_rocm_gfx returned the first gfx token and ignored
HIP_VISIBLE_DEVICES / ROCR_VISIBLE_DEVICES / CUDA_VISIBLE_DEVICES. Since exact
ROCm matching treats that token as the active GPU, a mixed APU + dGPU host
(gfx1151 + gfx1100) with HIP_VISIBLE_DEVICES=1 installed the wrong archive. Route
through a shared pick_rocm_gfx_target (lifted from llama) that parses per-GPU
sections and honors the visibility vars (empty / -1 -> no AMD GPU).

--rocm-gfx override: recording the arch without setting has_rocm left the host on
its CUDA/CPU path so the ROCm bundle was never picked. --rocm-gfx now implies
has_rocm and clears NVIDIA state, like llama's _apply_host_overrides.

CUDA launch env: a CUDA bundle ships the ggml CUDA backend but not
libcudart/libcublas, and the sidecar launch env exposed only the bundle dir, so
on a host whose CUDA runtime lives only in the PyTorch wheels the selection would
gate cuda usable but the server could not load it. Add the CUDA-from-PyTorch
runtime dirs to the child loader path for CUDA bundles (bundle dir still first),
mirroring binary_env.

Also normalize a manifest artifact's supported_sms defensively (parity with
llama's parser) and document that blackwell_min_toolkit_for_caps is retained for
the Phase B llama Windows path.

Not changed (verified parity, not defects): Linux/Windows min_os is enforced
nowhere in llama (macOS only); the resolver is optimistic about the checksum
index and the install path verifies.

* studio: tighten prebuilt-core code comments

* studio: lift shared prebuilt installer core out of the whisper installer

* studio: reuse the llama.cpp prebuilt installer machinery for whisper

* studio: unify llama and whisper prebuilt installers on a shared descriptor core

* studio: consolidate prebuilt installer tests into the shared core suite

Grow tests/studio/install/test_prebuilt_core.py from 62 to 164 tests so every
component-agnostic behavior runs against both descriptors: the full seven
profile CUDA release matrix (multi-GPU, on-disk runtime gating, shuffle
stability, missing SM metadata, dotted SM normalization, no-driver fallback
policy), the ROCm gfx family matrix, macOS min_os gating and its helper,
backend resolution incl. cpu-fallback precedence and Intel-mac auto detect,
checksum-index non-object and plain-lookup cases, the tar symlink/hardlink
extraction guards moved from the llama suite, and the compute-cap, visible
device, runtime-line and Blackwell helper value tables moved verbatim from
the llama characterization suites.

Delete only tests whose exact behavior the master now asserts for the same
component: 40 pure-alias helper cases in test_selection_logic.py (replaced by
value-identical master tables plus an alias-identity pin), 6 extraction moves
and the master-absorbed zip-symlink case in the llama logic suite, 3 routing
twins in test_rocm_support.py already pinned byte-for-byte in
test_selection_logic.py, the 2 Blackwell helper tables in the backend resolve
suite, 28 whisper logic tests and 10 whisper checksum tests re-asserted by
the master whisper parameterization. Wrapper wiring pins, the llama release
plan dialect, fingerprints and every llama-only behavior stay untouched.

* studio: dedupe sidecar and update helpers into the backend prebuilt package

* studio: chain whisper.cpp prebuilt updates onto the llama.cpp update flow

* studio: consume paired slim whisper prebuilts via the llama ggml runtime

* studio: serve every whisper backend from slim prebuilts

* studio: drop the whisper fat per-accelerator selection chain

unslothai/whisper.cpp releases are slim-only from v1.9.1-unsloth.2: one
ggml-less bundle per os/arch, paired to the llama.cpp prebuilt that provides
every ggml backend. Delete the whisper-side fat CUDA/ROCm/metal/vulkan
selection glue; keep slim selection + pairing, link_ggml_runtime, and one
legacy shape, the published fat CPU bundle of an explicitly pinned pre-slim
release. Exit 2 now reads as prebuilt unavailable (whisper never source
builds); setup already treats it that way.

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

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

* Wire libomp runtime DLL alongside ggml in slim whisper installs

llama's clang-built windows-arm64 ggml-base.dll imports
libomp140.aarch64.dll, shipped in the llama bundle but not a system DLL.
Without it next to whisper-server.exe the loader fails with
STATUS_DLL_NOT_FOUND before main. MSVC x64 links vcomp140.dll from
System32 and Linux ggml uses system libgomp.so.1, so only windows-arm64
was affected. The empty-runtime guard still requires a real ggml
library; libomp alone is not a pairing.

* studio: drop whisper-side fat-selection support structure

Slim whisper bundles are selected per os/arch only; all accelerator
capability comes from the installed llama.cpp prebuilt, whose installer
already did the coverage-aware selection. Remove the machinery that only
existed to pick among fat per-accelerator whisper bundles:

- prebuilt_core: delete the generic CUDA/ROCm coverage selection
  (select_cuda_artifact, select_rocm_artifact, ArtifactView adapters,
  detected_cuda_runtime_lines, the exact-SONAME linux probe) that no
  shipped component routes through; llama keeps its own selection chain
  and whisper shadows select_artifact with the slim-only version.
  select_artifact is now a plain os/arch/backend first-match.
- install_whisper_prebuilt: drop the HostInfo CUDA fields
  (compute_caps, driver_cuda_version, torch_runtime_line) and the torch
  runtime probe that populated them; nothing reachable reads them, and
  the resolver payload sources runtime_line from the artifact.
- whisper_cpp_update: delete the standalone start_update job worker;
  whisper applies only run as the chained phase of the combined
  llama+whisper update. The status payload keeps its job field (idle).
- routes/whisper: drop the progress logger that could never fire.
- tests: remove tests of the deleted paths and tests duplicating the
  descriptor-parameterized core suite or the llama freshness suite.

Contracts unchanged: resolver JSON keys, exit codes, marker fields,
pairing logs, and the pinned pre-slim fat CPU escape hatch.

* Address review feedback on the whisper prebuilt update and install paths

- Pin the chained whisper phase to the release the freshness check
  offered, so the download-host latest pointer cannot reinstall an
  older build in a loop
- Wire the whisper prebuilt install into setup.ps1 (Windows setup
  previously skipped it entirely)
- Treat a non-executable server or missing wired ggml libraries as a
  broken install instead of reporting already matches
- Keep whisper sidecar reloads out of the job-level reload flag and
  resync chat state after a partial chained update that unloaded llama
- Repoint home and profile vars for the whisper-server subprocess at a
  managed scratch dir and drop credential-store pointers
- Clear the prebuilt marker before the opt-in source build overwrite
- Write the prebuilt marker with explicit utf-8 encoding

* Tighten comments in the whisper prebuilt consumer

* Harden the Windows whisper setup phase and the chained update edges

- setup.ps1: honor WHISPER_SERVER_PATH / UNSLOTH_WHISPER_CPP_PATH /
  UNSLOTH_SKIP_WHISPER_INSTALL, run the custom-home ownership guard
  before the atomic install, and forward the release-tag pin and ROCm
  hints like setup.sh
- sidecar: a cpu-selected install launches whisper-server with --no-gpu
  (slim wiring links every llama backend, so the flag is what keeps a
  deliberate CPU choice off the GPU)
- chained update: leave whisper unpinned on macOS (the llama phase can
  walk back there, and a newest-tag pin could be an impossible pairing
  on every retry) and treat installer exit 2 as kept-existing-runtime
  instead of failing the combined job
- job.to_tag now comes only from the llama phase, so a whisper-only
  round cannot report a llama update that never ran

* Fix slim whisper runtime follow-ups

* Address remaining whisper update reviews

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

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

* Address remaining prebuilt update reviews

* Fix remaining chained update reviews

* Fix remaining whisper runtime review edges

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

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

---------

Co-authored-by: danielhanchen <unslothai@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>

---------

Co-authored-by: danielhanchen <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-07-23 01:39:03 -07:00
Daniel Han
f2f41bf9b1
Baseline two benign unsloth-zoo test-file findings in scan_packages (#7325)
The enforcing pip scan-packages hf-stack shard fails on two CRITICAL
staged-dropper findings in unsloth-zoo test files:
tests/test_mlx_save_export_regressions.py and
tests/test_vision_collator_audio.py. Both are false positives: the
combination heuristic matches a /tmp path literal alongside unrelated
subprocess/import references in the same file, but those are mocked test
fixtures (monkeypatch.setattr on subprocess, asserted /tmp path strings),
not droppers. Add both to the reviewed allowlist so the gate stops
red-failing on legitimate test code. The scan then exits 0 on both the
hf-stack shard and a direct unsloth-zoo scan.
2026-07-22 03:52:32 -07:00
Michael Han
6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00
Daniel Han
030524ae8e
security: refresh the fastapi C2-loop baseline entry for the current release (#7223)
The pip scan-packages studio shard is red on main and on every open PR:
the baselined fastapi finding (the benign SSE keepalive `while True:`
loop in fastapi/routing.py, reviewed and suppressed long ago) records
its evidence at L586 with the span digest of the fastapi release current
at baseline time. The latest fastapi shifts that loop to L587 and its
span digest with it, so the evidence hash no longer matches and the
scanner reports the finding as new, failing the shard with one
unsuppressed CRITICAL.

Re-reviewed the flagged code in the current release before refreshing:
L587 is the same keepalive loop inside the streaming response machinery,
not a beacon. Only the one entry's evidence and evidence_hash change.

Verified with the scanner itself: `scan_packages.py fastapi
--no-baseline` reproduces the exact CI evidence string, and with the
updated baseline the same scan exits 0 with the finding suppressed as
1 CRITICAL baselined.
2026-07-19 00:34:13 -07:00
Daniel Han
a440ee748a Extend the fp8 TE quant to HiDream's Llama text_encoder_4
The generic quantize_text_encoders pass only covers text_encoder.._3, so
HiDream's HEAVIEST encoder (Llama-3.1-8B TE4, 16.1 GB bf16) always stayed
dense. TE4 is assembled separately (hidream_te4_kwargs), so the fp8 path
now lives there: when the requested TE quant is layerwise fp8 and the
device/family qualify, TE4 prefers the hosted pre-cast checkpoint
(unsloth/HiDream-I1-Full-FP8, 8.6 GB) and falls back to dense-load-then-
cast; a mid-pass cast failure reloads a fresh dense encoder instead of
shipping partial state. The pre-cast loader and builder gain
config_subfolder/config_overrides for standalone encoder repos whose
config sits at the root and whose pipeline needs forward flags
(output_hidden_states/attentions).

Verified on B200: bit-identity 291 tensors (225 fp8, 0 mismatches),
hosted checkpoint engages through the real backend (marker + status fp8),
load 24.3 s vs 48.0 s dense, LPIPS 0.133 mean over 3 same-seed pairs vs
the dense-TE render (gate 0.25), non-black frames.
2026-07-18 07:55:24 +00:00
Daniel Han
645f3858d1 Fix pre-cast TE checkpoint loading and engagement reporting
Two bugs found while building the hosted checkpoints:

- The builder recorded torch.__version__ (a TorchVersion object) in the
  checkpoint metadata, so torch.load(weights_only=True) rejected every
  artifact and the loader silently fell back to the dense download.
  Record plain strings.
- Re-applying the layerwise fp8 cast to an injected pre-cast encoder
  raised on the duplicate hook registration, making quantize_text_encoders
  report the engaged cast as failed (status showed no TE quant while the
  encoder ran fp8). _cast_fp8 now returns early when the hooks are
  already installed.

Also corrects the LTX TE size note: Gemma3-12B stored fp32 (~49 GB), not 27B.
2026-07-18 06:52:46 +00:00
Daniel Han
4e9aab60ae Add the pre-cast text-encoder checkpoint builder
Applies the runtime layerwise fp8 storage cast to a model's dense text
encoder once and saves the cast state dict with baked metadata (format
tag, base_model_id, family, scheme, component, te_class, versions) in
the layout diffusion_te_prequant.py validates. Resolves the encoder
class from the checkpoint's config.architectures so the recorded
te_class matches what the pipeline instantiates. CPU-runnable: the cast
touches storage dtypes only.
2026-07-18 06:24:48 +00:00
Daniel Han
744b59f04a
scan_packages: baseline sentencepiece dup2 finding after upstream reindent (#7120)
The supply-chain scan gates on non-baselined CRITICAL/HIGH findings. A newer
sentencepiece release reindented the stdout/stderr fd-redirect helper in
sentencepiece/__init__.py (the os.dup2 pair the heuristic flags as a
reverse/bind-shell pattern), moving it from L1221/L1226 to L772/L777 and
changing its leading indentation.

The baseline key is (package, package-relative file, check, evidence_hash),
where evidence_hash is over the matched code with the L<NN>: markers stripped
but the code's own indentation preserved. The reindent therefore changed the
hash (bba233.. -> 65b5a11c..), so the existing entry no longer suppressed the
finding and it resurfaced as a blocking CRITICAL in the hf-stack and studio
scan legs.

Add the new indentation variant to the allowlist. The calls are sentencepiece
redirecting stdout/stderr file descriptors to capture its C++ logs, not a
shell; no socket or networking is involved. The old L1221 entry is kept so
both versions stay covered.
2026-07-14 01:38:50 -07:00
Daniel Han
53912b2f99 Studio: tighten image-generation fix comments and docstrings 2026-07-13 13:32:28 +00:00
Daniel Han
5eef2f4003 Studio: preserve foreign gallery files, force safetensors on remote ControlNets, and close dataset/seed/GPU gaps
Gallery clear/delete now scope to Studio-owned files: image_gallery and
video_gallery skip PNGs / MP4s without a readable recipe (a hand-dropped or
orphan file the listing already hides), so clear() and a guessed-id delete no
longer destroy files the gallery never surfaced.

Remote ControlNets now force use_safetensors: a bare owner/name reaches
from_pretrained without the base trust gate, and the Hub scan fails open when
unavailable, so requiring safetensors closes the pickle deserialization vector.

POSIX uninstall now stops resident sd-server / sd-cli under an owned sd.cpp root
before removing the tree (marker-gated), mirroring the Windows stop-before-delete
scan; a live native server no longer survives unlinking its binary.

Diffusion dataset containment: the training-start read path and the discovery
picker route bare names through the protected resolver, so a symlinked dataset
is rejected / not advertised like the caption/delete routes already do. Uploads
gain the inference decode guard (oversized real images 400 before OOMing the
trainer) and dataset upload/caption/delete/import are blocked with 409 while a
diffusion run is active.

JSONL readers (trainer + routes) tolerate non-object JSON and invalid UTF-8
instead of raising AttributeError / 500.

LoRA family compatibility is enforced in the shared resolver, not only the
picker, so a direct API client cannot apply a mismatched-family adapter.

GPU arbiter gains release_if so the image/video unload idle-check and release
are atomic against a concurrent same-owner load's registration. Native batch
recipes persist the base batch_seed and restore replays from it, so a native
batch_index>0 image no longer advances its seed twice.

FLUX.2-klein selects its sd.cpp text encoder by variant (4B -> Qwen3-4B,
9B -> Qwen3-8B) instead of the single family default.
2026-07-13 10:02:42 +00:00
Daniel Han
53668c9f66 Studio: include marker-owned custom sd.cpp roots in the uninstall stop scan; harden Pester install against the nuget.exe PSGallery bootstrap 2026-07-13 07:26:18 +00:00
Daniel Han
e0ef488f47 Tighten comments and docstrings added by the image-generation fixes 2026-07-13 05:29:09 +00:00
Daniel Han
7ec9e77a5d Studio: fix diffusion install ownership, dataset upload atomicity, gallery pagination, and teardown races
install_sd_cpp_prebuilt: only write the .unsloth-studio-owned marker when the
install created the target directory or it was empty. Adopting a pre-existing,
unowned, non-empty directory (a user's own stable-diffusion.cpp checkout) made
it eligible for the uninstaller's recursive delete.

routes/training upload: make the multi-file promotion transactional. Back up
each displaced original and roll every destination back on any failure, so a
mid-loop rename error can no longer partially overwrite the live dataset.

routes/training _resolve_dataset_folder: reject a symlinked dataset directory
and prove the resolved folder stays under the datasets root, so image
read/caption/delete cannot escape the root through a link.

routes/training delete: escape glob metacharacters in the thumbnail filename so
deleting an image named like [ab].png removes only its own thumbnails.

image_gallery / video_gallery listing: filter records against the response
schema inside the pager via a valid callback, so offset/limit/has_more all count
over accepted records. A leading schema-invalid record no longer returns an
empty page with has_more=true and stalls infinite scroll at offset 0.

image_gallery / video_gallery save: publish via a temp file plus atomic rename
(the sidecar is the video pair's commit marker) and clean up on failure, so a
partial write never surfaces a truncated PNG or strands an orphan MP4.

diffusion_train_common discovery: treat an empty caption sidecar as a metadata
tombstone that still falls through to the dreambooth instance prompt, so
clearing every metadata caption no longer fails with no captioned images found.

diffusion backend unload: wait for an in-flight denoise to exit before tearing
down process-wide patches and state, mirroring the load path.

diffusion_engine_router: serialize the whole check/unload/publish transition so
a concurrent selection cannot return the engine being unloaded.

uninstall.ps1: gate the default sd.cpp process stop on the owner marker so a
user's own sd-server is not terminated for a directory we then keep.
2026-07-13 01:22:54 +00:00
Daniel Han
4cc35aa87a Tighten comments in the image stack tests and scripts 2026-07-12 12:21:14 +00:00
Daniel Han
1a2dc57c4a scripts: baseline the gguf 0.19.0 HF download helper for the studio scan shard
The studio dependency spec resolves gguf 0.19.0, whose gguf/utility.py
legitimately sends an HF_TOKEN Authorization header from the authenticated
Hugging Face download helper used by convert_hf_to_gguf; main's baseline
entry covers a different gguf version so the evidence hash differs.
Verified locally: the full studio shard scan exits 0 with the updated
baseline and zero unsuppressed CRITICAL or HIGH findings.
2026-07-11 16:50:19 +00:00
Daniel Han
57f08ebfb2 Merge remote-tracking branch 'origin/main' into ig_merge 2026-07-11 15:15:54 +00:00
Daniel Han
9fa6fd40e1
scripts: refresh scan_packages allowlist baseline (#7078)
Some checks are pending
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
New releases of huggingface-hub (1.23.0) and openai (2.45.0) shifted or
added polling loops that the C2 polling/beaconing check flags, failing
all three pip scan-packages shards (studio 1, hf-stack 1, extras 3 new
CRITICAL findings) org-wide including on main.

Regenerated with scan_packages.py --write-baseline per CI shard (same
shard-to-requirements mapping and --with-deps as security-audit.yml)
and merged. All entries were manually reviewed at the resolved versions:

- huggingface-hub hf_api.py: create_repo 409-concurrency retry loop
  body changed in 1.23.0; refreshed evidence hash. The loop POSTs to
  the canonical Hub endpoint and retries only on a specific conflict
  error. Benign client retry.
- openai beta/threads/runs/runs.py: create_and_poll run-status helper
  refactored in 2.45.0 (Assistants deprecation annotations); refreshed
  evidence hash. Documented polling helper against api.openai.com.
- openai beta/responses/responses.py: new beta websocket client whose
  __aiter__ yields server events until the connection closes. New
  entry; standard event-stream iterator, not beaconing.
- openai resources/responses/responses.py: evidence line number
  refreshed only, hash unchanged.

The two dropped entries are the pre-refactor hashes of the same two
loops above; they no longer occur at the resolved versions. Verified
locally: all three shards exit 0 with 0 unsuppressed CRITICAL/HIGH
(hf-stack 120, studio 151, extras 99 suppressed).
2026-07-11 08:15:43 -07:00
Daniel Han
d439508853 Reset the step cache before every bench generation to match production
The image bench drove pipe() directly with no cache reset between the
warmup and measured prompts, while the production backend clears the
FBCache residuals before each generation. diffusers keys those residuals
on the long-lived transformer and never resets them itself, so step 0 of
each measured prompt compared its first-block residual against the
previous prompt's final one, a state production never runs. Mirror the
backend's _reset_step_cache (best-effort, no-op for uncached configs and
for SDXL's unet) inside _generate so both warmup and measured passes
start clean, and note that pre-fix FBCache rows may overstate results.
2026-07-11 07:55:01 +00:00
Daniel Han
de2f22df2b perf(image): compile numeric parity, cache-hook compile arming, FBCache toggle crash fix, TE fp8 zero-row guard
Applies the video round-2 accuracy findings to the image diffusion stack and fixes
two real image-path bugs found while measuring. All numbers B200, production
settings (family default steps/guidance, 1024px, seed 42, 4 fixed prompts), LPIPS
(AlexNet) via the new scripts/image_speedmem_bench.py, which drives the production
lever functions in the loader's own order.

- inductor precision parity: emulate_precision_casts=True on the regional-compile
  path (fused pointwise kernels keep fp32 intermediates where eager rounds to bf16
  between ops). Pairwise LPIPS of the compiled tier vs the same-stack eager tier:
  Qwen-Image 0.019 to 0.006 at identical speed (72.4 vs 72.5 ms/step), FLUX.1-dev
  0.046 to 0.029 at +2% step time (69.8 vs 68.3, reproduced), FLUX.2-klein-4B
  0.018 to 0.017 at identical speed. Snapshot/restored with the other process-wide
  backend flags so an off load never inherits it.
- cache x compile composition: re-point each cache hook's fn_ref.original_forward
  at a torch.compile'd wrapper of the same bound method (armed only where the
  speed layer compiled the block; restored before every disable_cache and before
  the partial-hook cleanup). Qwen-Image FBCache computed steps 91.8 to 71.2 ms
  (back at the uncached compiled rate), 1.21x end to end (7.36 to 6.06 s per 4
  images); FLUX.1-dev already traced through its FBCache hook and is measured
  neutral (same-process armed vs unarmed latents bit-identical). Skip counts
  within noise (13 vs 11 of 76; pairwise LPIPS 0.005).
- FBCache mid-session toggle crash: diffusers 0.39 caches the HookRegistry child
  list on first cache_context use, so an uncached generation followed by a
  20+-step generation (the auto toggle path) enabled hooks the context never
  reached and crashed with "No context is set" (reproduced live on FLUX.1-dev).
  Invalidate the stale child cache after every enable_cache.
- TE fp8_dynamic zero-row guard: torchao per-row fp8 derives a per-output-channel
  scale from the row amax, so an all-zero weight row is 0/0 = NaN. SDXL's
  text_encoder_2 (OpenCLIP bigG) ships exactly such a row, and every explicit
  fp8_dynamic SDXL render came out black; keep zero-row Linears dense (LPIPS
  0.976 black to 0.096 working). Other families' encoders have no such rows and
  are byte-identical.
- No AUTO TE quant exists on the image branch (text_encoder_quant defaults dense,
  explicit-only), so the video round's auto-dense retune has no image analogue;
  the explicit lever's cost is now measured (TE fp8_dynamic alone, LPIPS vs
  bit-exact: Qwen-Image 0.038, FLUX.1-dev 0.084, SDXL 0.096; no speed win, VRAM
  -6.5 GB on Qwen-Image) for the docs.

Tests: 96 passing across the cache/speed/precision suites (11 new arming, 2
child-registry, 2 zero-row, 4 inductor-flag); ruff clean.
2026-07-10 16:07:46 +00:00
Daniel Han
b9ebfe089b Merge remote-tracking branch 'origin/main' into ig_merge
# Conflicts:
#	scripts/scan_packages_baseline.json
2026-07-10 06:28:04 +00:00
Daniel Han
b5dca66cb1
scripts: refresh scan_packages allowlist baseline (#7032)
* scripts: refresh scan_packages allowlist baseline

Regenerate scripts/scan_packages_baseline.json against the current
resolved dependency set so the blocking pip scan-packages gate matches
what the scanner now finds. Refreshes evidence hashes for benign
findings whose code shifted lines (unsloth-zoo mlx loader, gguf/mlx
test /tmp fixtures) and adds two mainstream-library entries that were
newly surfaced (torch inductor codecache base64+subprocess compile
cache, torch testing common_utils socket import). Stale entries whose
matching code changed and no longer triggers are dropped.

All entries remain CRITICAL/HIGH findings manually judged benign;
matched on (package, file, check, evidence_hash).

* ci(security-audit): re-run scan when the allowlist baseline changes

The security-audit pull_request trigger listed the scanners but not
their allowlist baselines, so a baseline-only edit never re-ran the
scan that consumes it. A refreshed baseline could therefore merge
without CI confirming its evidence hashes match what the scanner finds.
Add scan_packages_baseline.json and scan_npm_packages_baseline.json to
the paths filter so baseline changes are validated on their own PR.
2026-07-09 04:52:30 -07:00
Daniel Han
27511f30ee Merge origin/main into image-generation
Resolve the app-sidebar.tsx conflict: main refactored the chat-export dropdown to a
format-based CHAT_EXPORT_OPTIONS + dynamic-import exportConversationByFormat dispatcher,
which the merged body already uses. Keep main's dispatcher and drop the branch's static
export imports; keep TestTubeOutlineIcon imported from the shared @/lib/hugeicons-derived
module (also used by images-page) rather than main's duplicate inline definition. The
branch's Images and Video nav items are preserved.
2026-07-08 01:57:07 +00:00
Daniel Han
ffa8a56391 Merge image-generation bug fixes (#6872)
Fold PR #6872's image-generation fixes into the branch, deduped against the
round-12 dataset-upload and gallery integrity work already on image-generation.

Fixes carried forward from #6872:
- fp8 single-file transformer memory estimate: an fp8 checkpoint loads with no
  quantization_config and diffusers upcasts it to bf16 (~2x resident), so budget
  it accordingly in _plan_memory and estimate_safetensors_dense_mib.
- dense-quant OOM-evict preflight: when the GGUF fits resident but the dense bf16
  transformer this path materializes does not, skip the fast path up front rather
  than evict the current pipeline and OOM in finalization. Combined with the
  existing offload->resident candidate re-plan so both the family-table estimate
  and the on-disk shard measurement gate engagement (unified on the
  transformer_resident_override_mib plan override).
- ControlNet: evict the previous module and its from_pipe wrapper before loading a
  new one so swapping ControlNets within a base-model load cannot accumulate to OOM.
- ControlNet union_control_mode: raise on an unknown control type instead of
  silently defaulting to canny.
- edit-family mask rejection: raise instead of silently dropping a mask on an
  image-editing model that has no inpaint pipeline.
- companion cache: walk the snapshot dir and exclude transformer/ so the
  dense-quant prefetch's cached shards do not inflate the companion total and
  wrongly force offload.
- training: drop piecewise_constant from the LR scheduler enum and force bf16 for
  fp16-incompatible families.
- dataset upload: batch-atomic staging with the same-stem duplicate guard.
- images page: guard negative-prompt restore on guidance>0, clear stale ControlNet
  selection on restore, and revert an optimistic quant label when a pipeline load
  never starts.
- uninstall (sh + ps1): keep the owner-marker guard on sd.cpp removal.

Conflicts resolved in favour of image-generation's evolved memory system,
loadSpecFor catalog, and stop-and-save (lora_path) run detection; #6872's fp8 and
dense-preflight fixes carried forward on top. All affected backend tests pass
(test_diffusion_backend, test_diffusion_training, test_diffusion_lora_trainer,
test_video_gallery, test_diffusion_controlnet).
2026-07-07 16:14:06 +00:00
Daniel Han
6519fbfad5 Keep an unowned default-mode sd.cpp checkout on uninstall
The custom-root sd.cpp removal requires the .unsloth-studio-owned marker (written by
install_sd_cpp_prebuilt) before deleting, so a user's own stable-diffusion.cpp checkout
beside a custom root is kept. The default-mode removal of ~/.unsloth/stable-diffusion.cpp
was unconditional, so a user who keeps their own checkout at that path (or points
UNSLOTH_SD_CPP_PATH there), or a pre-marker Studio build, would have it deleted on
uninstall. Guard the default-mode removal on the same owner marker in both uninstall.sh
and uninstall.ps1. Extends the sd.cpp uninstall shell test with owned (removed) and
unowned (kept) default-mode cases.
2026-07-07 10:22:40 +00:00
Leo Borcherding
296cacb5a1
ROCm-on-WSL: support discrete Radeon (RDNA 3/4) in WSL, not just Strix Halo (#6915)
* WSL ROCm: generalize ROCm-on-WSL bootstrap from Strix-only to any RDNA arch

install_rocm_wsl_strixhalo.sh hardcoded gfx1151, so its verify step died on
discrete Radeon cards even though the ROCm + librocdxg setup is arch-agnostic.
Auto-detect the GPU arch from rocminfo (override via UNSLOTH_WSL_GFX), verify any
GPU agent enumerates over DXG, and map the arch to AMD's per-arch wheel family for
the optional smoke test (injecting librocdxg into torch/lib so torch's bundled
ROCr finds the DXG bridge). Verified on gfx1200 (Radeon RX 9060 XT) in WSL2 +
Ubuntu 24.04 -- torch.cuda now enumerates the GPU.

* WSL ROCm: trigger the ROCm-on-WSL bootstrap for discrete Radeon GPUs too

_maybe_bootstrap_rocm_wsl only fired for Strix APUs (matched via /proc/cpuinfo,
which discrete cards don't appear in). Add _wsl_amd_gpu_name() -- queries the
Windows host via WMI -- and broaden the trigger gate plus the 'already-usable
ROCm' rocminfo check from gfx1151-only to any real GPU agent (gfxNNNN, excluding
the gfx11-generic fallback ISA). The generalized bootstrap then auto-detects the
arch. Enables 'curl install.sh | sh' to set up ROCm-on-WSL on discrete Radeon RX
7000/9000 in WSL2 + Ubuntu 24.04, not just Strix Halo/Point.

* WSL ROCm: address review -- filter generic ISA in bootstrap, bound the host GPU query

- install_rocm_wsl_strixhalo.sh: exclude the gfx11-generic fallback ISA in arch
  detection (grep -v generic), matching install.sh's rocminfo check, so a generic
  agent listed before the real one can't be picked as the arch.
- install.sh: wrap the powershell.exe Win32_VideoController query in _run_bounded
  (10s timeout) so an unstable WSL-interop / busy host can't hang the installer.

* WSL ROCm: harden arch-detect + librocdxg copy under set -eo pipefail (review)

- _detected_gfx: append '|| true' so a no-GPU rocminfo (empty pipeline, non-zero
  under pipefail) doesn't abort the assignment before the '[ -z ]' branch prints
  the diagnostic + die message.
- smoke-test librocdxg copy: gate on '[ -d "$_tlib" ]' instead of '[ -n ]' so a
  non-directory value can't make cp rename librocdxg to 'lib'.

* WSL ROCm: address Codex review (gfx000, 24.04 reroute for discrete, test locator)

- Exclude gfx000 (the CPU agent) from the WSL 'usable ROCm' check and the bootstrap
  arch-detect: match gfx[1-9] (nonzero arch), so a partial ROCm install that only
  reports the CPU ISA no longer short-circuits the librocdxg setup. (P2)
- Reuse the Ubuntu-24.04 reroute for discrete Radeon: broaden
  _maybe_reroute_strixhalo_to_2404's gate with the same _wsl_amd_gpu_name (WMI)
  fallback, so a discrete card on 26.04 reroutes to a 24.04 distro like Strix does
  instead of falling to CPU. Moved _wsl_amd_gpu_name above the reroute and made it
  self-contained + 10s-bounded (it runs before _run_bounded is defined). (P2)
- Update TestInstallShDropinPersistence to locate the gate by its unique
  '!/generic/' clause now that the gfx1151 literal is gone. (P1)

* Condense ROCm-on-WSL comments in install.sh and bootstrap helper

* Guard WSL reroute from NVIDIA hybrid hosts and fix GFX-override pipefail check

* Honor CUDA_VISIBLE_DEVICES-hidden NVIDIA in the WSL reroute guard

* Reuse _has_usable_nvidia_gpu in the WSL reroute guard

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-07-07 02:29:37 -07:00
Daniel Han
c399aabd5d Merge remote-tracking branch 'origin/main' into fold-integration
# Conflicts:
#	scripts/scan_packages_baseline.json
2026-07-07 05:52:16 +00:00
Daniel Han
d36aeb54f3 Studio: classify local video pipelines, harden video preflight, and gate custom-root sd.cpp removal on ownership
- _local_model_task now tags a local diffusers pipeline that resolves to a video
  family (LTX / Wan / Hunyuan) as text-to-video, mirroring the cached-repo
  _cached_repo_task, so supported local video pipelines surface in the Video
  On-Device picker instead of being routed to the Images picker where the image
  loader rejects them. Gated on _local_is_diffusers so only a real loadable
  pipeline dir reaches the video check.
- Video validate_load_request now rejects a local pipeline pick whose directory has
  no model_index.json before the GPU handoff, mirroring the image loader, so a bad
  local pipeline can no longer evict the resident model and only then fail deep in
  from_pretrained.
- The custom/env-mode uninstall now removes a sibling stable-diffusion.cpp only when
  it carries the Studio owner marker. install_sd_cpp_prebuilt writes the canonical
  .unsloth-studio-owned marker on install; uninstall.sh and uninstall.ps1 keep any
  unowned checkout (a user's own git clone of stable-diffusion.cpp beside a custom
  Studio root is no longer deleted). A pre-marker Studio build is left behind rather
  than a user file removed.

Adds regression tests: local video pipeline tagged text-to-video (and a video-named
non-pipeline dir stays untagged so it can never trigger a doomed pipeline load), the
video local-pipeline preflight rejection, the install ownership marker, and the
uninstall keeping an unowned sibling while removing an owned one.
2026-07-07 03:15:07 +00:00
Daniel Han
c2a7b78f6b
Studio: exclude mlx-lm 0.31.3 (broke gemma4/qwen3_5 QK-norm load on Apple Silicon) (#6803)
* Studio: exclude mlx-lm 0.31.3 (broke gemma4/qwen3_5 QK-norm load)

mlx-lm 0.31.3 regressed the QK-norm archs: its strict load_weights rejects the
q_norm/k_norm tensors with "Received N parameters not in model", so gemma4 and
qwen3_5 checkpoints fail to load. Studio installs the MLX stack unpinned at
latest, which pulls 0.31.3. Verified on a real macos-14 runner: gemma4 fails to
load on 0.31.3 but loads and generates coherently on 0.31.2 and on git-main
(future 0.31.4). See mlx-lm #1242.

Exclude just that release (!=0.31.3) in the installer and the self-heal floor so
--upgrade still resolves to the newest good build, and treat an already-installed
0.31.3 as unsatisfied so the self-heal replaces it.

* Studio MLX: cover fresh-install path + robust bad-version compare

Address PR review:
- Fresh install.sh (Apple Silicon) runs the base 'uv pip install unsloth' with
  SKIP_STUDIO_BASE=1, skipping the guarded MLX-stack step, so transitive
  resolution could still pull mlx-lm 0.31.3. install.sh already exports
  UV_OVERRIDE -> overrides-darwin-arm64.txt before that install, so exclude
  mlx-lm 0.31.3 there too; this also strengthens the self-heal (same override).
- Match the known-bad version with parsed packaging.Version so 0.31.3 == 0.31.3.0
  (trailing-zero normalization) instead of raw string equality.

* Studio: exclude mlx-lm 0.31.3 on the fresh Apple Silicon install too

The overrides file only applies via UV_OVERRIDE when it exists relative to the
script, which is not true for a curl-piped install, and the guarded MLX step in
install_python_stack.py is skipped there (SKIP_STUDIO_BASE=1). So the base
install could still resolve the transitive mlx-lm to the broken 0.31.3. Append
mlx-lm!=0.31.3 to the base install on Apple Silicon (empty elsewhere), so the
fresh path pins away from 0.31.3 without waiting for the runtime self-heal.

* Studio: exclude mlx-lm 0.31.3 on the migrated install; keep the >=0.22.0 floor

The with-deps migrated install did not append ${_MLX_LM_EXCLUDE_ARG:-}, so a
curl-piped Apple Silicon migration (no repo overrides file, UV_OVERRIDE unset)
could resolve mlx-lm 0.31.3 transitively. Append the exclusion there, matching
the fresh install path. The no-torch migration is left alone since --no-deps
never resolves mlx-lm (same as the fresh no-torch path).

Also restore the >=0.22.0 floor in overrides-darwin-arm64.txt: a uv override
replaces the transitive constraint, so a bare !=0.31.3 could let the resolver
drop below the supported minimum that mlx_repair.py enforces at runtime.

* Triage huggingface_hub 1.22.0 / fastapi / multiprocess scanner false positives

The scan-packages gate red-failed on all three shards after transitive deps
bumped. Every new CRITICAL is a benign false positive, verified against upstream:

- huggingface_hub 1.22.0 added _sandbox.py for the remote HF sandbox feature.
  Its job-startup bootstrap string (fetch sbx-server into the container /tmp and
  exec it) and the SandboxPool host-reservation loop trip the staged-dropper and
  C2-loop heuristics; that script runs inside a remote HF container, not on the
  user machine. The bump also re-hashed the already-reviewed benign polling loops
  in hf_api.py and utils/_http.py. The PyPI artifact is byte-identical to the
  official v1.22.0 tag.
- fastapi 0.139.0 routing.py re-hashed the websocket keepalive while-True loop;
  byte-identical to upstream 0.139.0.
- multiprocess 0.70.19 forkserver.py and tests/__init__.py re-hashed the AF_UNIX
  fork-server IPC and fd-inheritance tests; genuine uqfoundation release, local
  IPC not network.

Added 7 reviewed allowlist entries (no blind regenerate). All three shards
(hf-stack, studio, extras) exit 0 locally.

* Tighten mlx-lm 0.31.3 exclusion comments

* Trim mlx-lm 0.31.3 exclusion comments
2026-07-06 19:40:06 -07:00
Daniel Han
fb94a79337 Studio: fix dataset upload data loss, caption over-count, and custom-root sd.cpp uninstall
- Diffusion dataset upload now streams each file into a sibling temp file and
  atomically os.replace()s it into place only after the whole file is written and
  within the size cap. A mid-batch 413 (or any abort) removes the temp, never an
  example already stored under the same name, so re-uploading a too-large batch can
  no longer truncate or delete a previously uploaded image.
- _diffusion_dataset_summary counts an image as captioned only when it resolves to a
  non-empty caption via the same sidecar-over-metadata precedence the trainer uses. An
  empty (tombstone) sidecar shadows a metadata row and makes the trainer skip the
  image, so counting it over-reported caption_count and mislabeled an effectively
  uncaptioned dataset as captioned.
- uninstall.sh/.ps1 now remove a custom/env-mode Studio's native diffusion build that
  installs beside the root as a stable-diffusion.cpp sibling (find_sd_cpp_binary
  resolves it from the Studio home's parent), guarded by the same unsafe-path check,
  and stop processes locking the default-mode stable-diffusion.cpp before removing it.

Adds regression tests for the upload data-loss and caption-count paths and a hermetic
shell test for the custom-root stable-diffusion.cpp removal.
2026-07-07 02:16:56 +00:00
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
233949cc9c
scan_packages: baseline transitive-dep drift in the supply-chain scan (#6917)
The pip scan-packages gate (SCAN_ENFORCE=1) blocks on non-baselined
CRITICAL/HIGH findings. Recent upstream releases of transitive
dependencies added new files/loops that trip the pattern scanner, so all
three shards (extras, hf-stack, studio) red-failed on legitimate library
code. Add the 7 reviewed findings to scripts/scan_packages_baseline.json.

Each entry is genuine upstream code from the official PyPI archive:

- huggingface-hub huggingface_hub/_sandbox.py (staged dropper + C2 loop):
  the HF Jobs sandbox bootstrap string and its host-pool reservation
  loop. New in huggingface_hub 1.x (pulled via huggingface_hub>=0.34.0).
- huggingface-hub huggingface_hub/hf_api.py, utils/_http.py (C2 loop):
  standard polling / retry while True loops.
- fastapi fastapi/routing.py (C2 loop): websocket receive loop.
- fastmcp-slim fastmcp/cli/apps_dev.py (fs enum + network): the FastMCP
  dev CLI (PrefectHQ) making httpx/socket calls.
- cffi cffi/_cffi_gen_src.py (compile + exec): cffi generating and
  running C extension source, its core purpose.

Additive only: no existing baseline entry is changed or removed. Verified
by re-running the scanner over the full closure on Python 3.12.13 (the CI
interpreter); it now exits 0 with only MEDIUM findings remaining.
2026-07-06 18:34:18 -07: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
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
oobabooga
9d8bf002f8 Merge remote-tracking branch 'origin/image-generation' into fix/imggen-review-bugs
# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/training/diffusion_train_common.py
#	studio/backend/routes/training.py
#	studio/backend/tests/test_diffusion_dataset_api.py
2026-07-06 09:16:30 -03:00
Daniel Han
dfa21001a7 Fail video quality gate on candidate frame-count mismatch
A candidate clip that decodes to fewer frames than the reference was
compared only over the shared prefix, so good early frames could PASS a
truncated or corrupt render. Record both frame counts and gate a mismatch
as FAIL, matching the harness contract that the requested shape is held
fixed. Extend the selftest with a truncated-clip case.
2026-07-05 07:46:42 +00:00
Daniel Han
f313cfd7e5 Security audit: baseline the new huggingface-hub Sandboxes findings
The latest huggingface-hub release added the Sandboxes feature. Its
bootstrap (_sandbox.py) fetches the static sbx-server binary into /tmp with
an Authorization header and marks it executable, which is exactly the
staged-dropper pattern the scanner hunts, and three while True polling loops
in _sandbox.py / hf_api.py / utils/_http.py match the beaconing heuristic.
All four verified against the official huggingface/huggingface_hub
repository: the snippet is the documented sandbox server injection and the
loops are deadline-style job and sandbox polling. Entries generated with
--write-baseline and reviewed line by line; scan_packages.py huggingface-hub
now exits 0 with the four findings suppressed.
2026-07-05 07:40:45 +00:00
Daniel Han
c35802bb12 Gate empty decodes and NaN audio as FAIL in the video quality script
An empty or corrupt clip decode crashed clip_metrics on frame indexing; it now
returns a zero-frame FAIL record. A NaN candidate audio RMS compared False
against every threshold and slipped past the silence trip-wire; NaN now counts
as a collapse.
2026-07-05 00:13:52 +00:00
Daniel Han
84420ac637 Merge branch 'video-wan' into video-hunyuan-gate 2026-07-04 18:58:27 +00:00
Daniel Han
dfeb0438e8 Baseline huggingface-hub 1.22.0 sandbox and retry-loop scanner findings
The 1.22.0 release adds _sandbox.py (the client for HF Jobs sandboxes,
including the bootstrap that downloads HF's own sandbox server binary)
and the scanner flags it plus three long-standing while True retry and
pagination loops as CRITICAL. Reviewed all four against the upstream
repo and the 1.22.0 wheel: legitimate library code. Entries generated
with scan_packages.py --write-baseline and verified to suppress with
exit 0.
2026-07-04 18:58:16 +00:00
oobabooga
756c715721 Trim the verbose comments added by the fixes 2026-07-04 13:25:08 -03:00
pre-commit-ci[bot]
3ebec8e037 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-04 14:47:26 +00:00
Daniel Han
f77fa80007 Add HunyuanVideo-1.5 family and the video quality gate
HunyuanVideo-1.5 (the 8.3B DiT with Qwen2.5-VL + ByT5 text encoders) loads
through the hunyuanvideo-community Diffusers repacks; tencent's own repo is
the original non-diffusers layout and cannot load as a pipeline, so only the
community 480p/720p t2v repos are trusted. Two pipeline quirks, both verified
against pipeline_hunyuan_video1_5.py in diffusers 0.39, shape the wiring:

- __call__ takes no guidance kwarg: CFG lives on the pipeline's guider
  component (ClassifierFreeGuidance, shipped at scale 6.0). The family gains
  guidance_via_guider and generate() writes the requested scale onto
  pipe.guider instead of passing cfg_kwarg, which the pipeline would reject.
- __call__ has no callback_on_step_end: progress and cancellation fall back
  to a scheduler.step wrapper (one call per denoise step), installed for the
  duration of the call and always restored. Cancellation unwinds the loop by
  raising through the wrapper and surfaces the same cancelled sentinel the
  callback path uses.

The VAE compresses 16x spatial / 4x temporal, so sizes snap to /16 and frame
counts to 4k+1. The transformer declares _repeated_blocks and CacheMixin, so
the regional compile profile and the step cache both apply unchanged.

scripts/video_quality.py is the video accuracy gate, the analogue of
scripts/diffusion_quality.py with the same pure-numpy PSNR/SSIM math so image
and video budgets compare: fixed prompt/seed/shape, one short clip per
candidate against a reference clip, per-frame SSIM/PSNR over sampled frames,
a temporal-consistency deviation (motion-energy series error, catching
flicker SSIM alone misses), black-frame/NaN collapse checks, an audio RMS
silence trip-wire for LTX-2, and wall time + peak VRAM per candidate.
Verdicts map the standing budget: ssim >= 0.75 passes, >= 0.50 warns,
anything lower or any collapse fails. --selftest runs the metric path on
synthetic clips with no GPU or model.
2026-07-04 14:22:10 +00:00
oobabooga
e5c63cdfff Fix diffusion training validation, dataset upload atomicity, and LoRA error mapping 2026-07-04 03:17:12 -03:00
Daniel Han
4562b537f8
Studio diffusion: fix FP8 transformer quant producing noise (per-row scaling) (#6772)
* Studio diffusion: cross-platform device policy, fp16 guard, lock split, validate-before-evict

Phase 1 of porting the richer diffusion stack onto the image-generation backend.

- Add a compartmentalized device/dtype policy module (diffusion_device.py)
  resolving CUDA/ROCm/XPU/MPS/CPU with capability flags. Keeps the NVIDIA
  capability-based bf16 choice; ROCm and XPU are isolated; MPS uses bf16 or
  fp32, never a silent fp16 that renders a black image.
- Add a per-family fp16_incompatible flag (Z-Image) and promote a resolved
  float16 to float32 for those families so they do not produce black images.
- Split the backend locks: a generation holds only _generate_lock, so status,
  unload, and a new load are never blocked by a long denoise. Add per-generation
  cancellation via callback_on_step_end so an eviction or a superseding load
  preempts a running generation; a replacement load waits for it to stop before
  allocating, so two pipelines never sit in VRAM at once.
- Validate a load request before the GPU handoff so an unloadable pick never
  evicts a working chat model, and reject missing local paths up front.
- Add CPU-only tests for the device policy, dtype guard, lock split and
  cancellation, and validate-before-evict, plus a GPU benchmark/regression
  script (scripts/diffusion_bench.py) measuring latency, peak VRAM, and PSNR
  against a saved reference.

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

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

* Studio diffusion (Phase 2A): measured-budget memory planner + offload/VAE policy

Add a lean, backend-agnostic memory policy that picks a CPU-offload policy and
VAE tiling/slicing from measured free device memory vs the model's estimated
resident footprint, then applies it to the built pipeline. auto stays resident
when the model fits (byte-identical to the prior resident path), and falls to
whole-module offload when tight; fast/balanced/low_vram are explicit overrides.
Sequential submodule offload is unreliable for GGUF transformers on diffusers
0.38, so it falls back to whole-module offload and status reports the policy
actually engaged.

Verified on Z-Image-Turbo Q4_K_M (B200): auto reproduces the resident image with
no VRAM/latency regression (PSNR inf); balanced/low_vram cut generation peak VRAM
47.9% (15951 -> 8318 MB) with byte-identical output, at the expected latency cost.

73 prior + 35 new CPU tests pass.

* Studio diffusion (Phase 2D): streamed block-level offload + functional VAE tiling

Add a streamed 'group' offload tier (diffusers apply_group_offloading, block_level,
use_stream) that keeps the transformer flowing through the GPU a few blocks at a
time while the text encoder / VAE stay resident, and fix VAE tiling to drive the
VAE submodule (pipelines like Z-Image expose enable_tiling on pipe.vae, not the
pipeline). apply_memory_plan now returns the (policy, tiling) actually engaged so
status never overstates either, and group falls back to whole-module offload when
the transformer can't be streamed.

Measured on Z-Image (B200), all lossless (PSNR inf vs resident): balanced/group
cuts generation peak VRAM 32% (15951 -> 10840 MB) at near-resident speed (2.07 ->
2.99s); low_vram/model cuts it 48% (-> 8318 MB) but is slower (7.99s). Mode names
now match that tradeoff: balanced = stream the transformer, low_vram = offload
every component. auto picks group when the companions fit resident, else model.

112 CPU tests pass.

* Studio diffusion (Phase 5): image quality-vs-quant accuracy harness

Add scripts/diffusion_quality.py, the accuracy analogue of the KLD workflow: hold
prompt + seed fixed, render a grid with a reference quant (default BF16), then render
each candidate quant and measure drift from the reference. Records mean PSNR + SSIM
(pure-numpy, no skimage/scipy) and optional CLIP text-alignment + image-similarity
(transformers, --clip), plus file size, latency, and peak VRAM, then prints a
quality-vs-cost table and recommends the smallest quant within a quality budget.
--selftest validates the metrics on synthetic images with no GPU or model.

Verified on Z-Image (B200): the table degrades monotonically with quant size
(Q8 -> Q4 -> Q2: PSNR 21.7 -> 15.5, SSIM 0.82 -> 0.61), while CLIP-text stays flat
(~0.34) -- quantization erodes fine detail far more than prompt adherence.

* Studio diffusion (Phase 3): opt-in speed layer (channels_last / compile / TF32)

Add a speed_mode knob (off by default, so the render path stays bit-identical):
default applies channels_last VAE + regional torch.compile of the denoiser's
repeated block where eligible; max also enables TF32 matmul and fused QKV. Regional
compile is gated off for the GGUF transformer (dequantises per-op) and for families
flagged not compile-friendly (a new supports_torch_compile flag, False for Z-Image),
so it activates automatically only once a non-GGUF bf16 transformer is loaded. Speed
optims run before placement/offload, per the diffusers composition order. status now
reports speed_mode + the optims actually engaged.

Verified on Z-Image (B200): default -> ['channels_last'], max -> ['channels_last',
'tf32'], compile correctly skipped for GGUF; generation works in every mode.

121 CPU tests pass.

* Studio diffusion (Phase 2B): opt-in fp8 text-encoder layerwise casting

Add a text_encoder_fp8 knob that casts the companion text encoder(s) to fp8 (e4m3)
storage via diffusers apply_layerwise_casting, upcasting per layer to the bf16
compute dtype while normalisations and embeddings stay full precision. Applied
before placement, gated to CUDA + bf16, best-effort (a failure leaves the encoder
dense). status reports which encoders were cast.

Verified on Z-Image (B200, balanced/group mode where the encoder stays resident):
generation peak VRAM dropped 37% (10840 -> 6791 MB, below the lowest-VRAM offload)
at near-resident speed. It is a memory-vs-quality tradeoff, not free -- ~20 dB PSNR
vs the bf16 encoder, a larger shift than one transformer quant step -- so it is off
by default and documented as such, with the Phase 5 harness to size the cost.

127 CPU tests pass.

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

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

* Studio diffusion (Phase 2C): NVFP4 text-encoder quant (+ generalise fp8 knob)

Generalise the text-encoder precision knob from a fp8 bool to text_encoder_quant
(fp8 | nvfp4). nvfp4 quantises the companion text encoder to 4-bit via torchao
NVFP4 weight-only (two-level microscaling) on Blackwell's FP4 tensor cores; fp8
stays the broader-hardware path (cc>=8.9). Both are gated, best-effort, and run
before placement; status reports the mode actually engaged. This is the lean
realisation of GGUF-native text-encoder quant: 4-bit on the encoder without the
3045-line port.

Verified on Z-Image (B200, balanced/group where the encoder stays resident), vs the
bf16 encoder: nvfp4 cut generation peak VRAM 48% (10840 -> 5593 MB, the lowest TE
option, below whole-model offload) at near-fp8 quality (16.4 vs 17.1 dB PSNR), and
both quants ran faster than bf16. A memory-vs-quality tradeoff (off by default);
size it per model with the Phase 5 quality harness. diffusion_bench gains
--text-encoder-quant.

129 CPU tests pass.

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

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

* Studio diffusion (Phase 4): native stable-diffusion.cpp engine for CPU/Mac

Adds the CPU / Apple-Silicon tier of the two-engine strategy, mirroring the
chat backend's llama.cpp shell-out. Diffusers stays the default on CUDA / ROCm
/ XPU; this covers the hardware diffusers serves poorly, consuming the same
split GGUF assets Studio already curates.

- sd_cpp_args.py: pure sd-cli command builder. Maps the family to its
  text-encoder flag (Z-Image Qwen3 to --llm, Qwen-Image to --qwen2vl, FLUX.1
  CLIP-L + T5), and the diffusers memory policy (none/group/model/sequential)
  to sd.cpp's offload flags (--offload-to-cpu / --clip-on-cpu / --vae-on-cpu /
  --vae-tiling / --diffusion-fa), so one user knob drives both engines.
- sd_cpp_engine.py: SdCppEngine over a located sd-cli. find_sd_cpp_binary()
  with the same precedence as the llama finder (env override, then the Studio
  install root, then in-tree, then PATH), an is_available/version probe, and a
  one-shot subprocess generate that streams progress and returns the PNG.
  runtime_env() prepends the binary's directory to the platform library path
  so a prebuilt's bundled libstable-diffusion.so resolves.
  select_diffusion_engine() is the pure routing decision (GPU backends to
  diffusers, CPU/MPS to native when present).
- install_sd_cpp_prebuilt.py: resolve + download the per-host prebuilt
  (macOS-arm64/Metal, Linux x86_64 CPU, Vulkan/ROCm/Windows variants) into the
  Studio install root. resolve_release_asset() is a pure, unit-tested
  host-to-asset matrix.
- scripts/sd_cpp_smoke.py: end-to-end native generation harness.

Tests (CPU-only, subprocess/filesystem stubbed): 49 new across args, engine,
routing, runtime env, and the installer resolver. Full diffusion suite 166
passing.

Verified on a B200 box: built sd-cli (CUDA) and the prebuilt (CPU) both
generate Z-Image-Turbo Q4_K end to end through SdCppEngine: balanced (group
offload, 5.0s gen), low_vram (full CPU offload + VAE tiling, 13.4s), and the
dynamically-linked CPU prebuilt (50.4s on CPU), all producing coherent images.

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

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

* Studio diffusion (Phase 6): img2img / inpaint / edit / LoRA / upscale on the native engine

Builds on Phase 4's native stable-diffusion.cpp engine, extending it from
text-to-image to the wider feature surface, since sd.cpp supports all of these
through the binary already. Pure command-builder additions plus one engine
method, so the txt2img path is unchanged.

- sd_cpp_args.py: SdCppGenParams gains image-conditioning fields. init_img +
  strength make a run img2img, adding mask makes it inpaint, ref_images drives
  FLUX-Kontext / Qwen-Image-Edit style editing (repeated --ref-image), and
  lora_dir + the <lora:name:weight> prompt syntax select LoRAs. New
  SdCppUpscaleParams + build_sd_cpp_upscale_command for the ESRGAN upscale run
  mode (input image + esrgan model, no prompt / text encoders).
- sd_cpp_engine.py: the subprocess runner is factored into a shared _run() so
  generate() (now carrying the conditioning flags) and a new upscale() reuse
  the same streaming / error / output-check path.
- scripts/sd_cpp_smoke.py: --task {txt2img,img2img,upscale} with --init-img /
  --strength / --upscale-model / --upscale-repeats.

Tests: 10 new across the img2img / inpaint / edit / LoRA flag construction, the
upscale builder and its validation, and the engine's img2img + upscale paths.
Full diffusion suite 176 passing.

Verified on a B200 box through SdCppEngine: img2img (Z-Image-Turbo Q4_K, the
init image conditioned at strength 0.6, 4.8s) and ESRGAN upscale
(512x512 -> 2048x2048 via RealESRGAN_x4plus_anime_6B, 2.7s), both producing
coherent images. Video and the diffusers-path feature wiring are deferred.

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

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

* Studio diffusion (Phase 7): accuracy-preserving speed pass

Re-review of the diffusion stack (#6675/#6679/#6680) surfaced one real accuracy
bug and a dead-on-arrival speed path; this fixes both and adds the lossless /
near-lossless wins, all measured on a B200.

Correctness:
- TF32 global-state leak (fix). speed_mode=max flipped torch.backends.*.allow_tf32
  process-wide and never restored them, so a later `off` load silently inherited
  TF32 and was no longer bit-identical. Added snapshot_backend_flags /
  restore_backend_flags (TF32 + cudnn.benchmark), captured before the speed layer
  runs and restored on unload. Verified: load max -> unload -> load off is now
  byte-identical (PSNR inf) to a fresh off.
- sd-cli timeout could hang forever. _run() blocked in `for line in stdout` and
  only checked the timeout after EOF, so a child stuck in model load / GPU init
  with no output ignored the timeout. Drained stdout on a reader thread with a
  wall-clock deadline. Added a silent-hang regression test.

Speed (diffusers path), near-lossless, opt-in tiers:
- Regional torch.compile now runs on the GGUF transformer. The is_gguf gate (and
  Z-Image's supports_torch_compile=False) were stale: compile_repeated_blocks
  compiles and runs ~2.2x faster on the GGUF Z-Image transformer on
  torch 2.9.1 / diffusers 0.38 (the per-op dequant stays eager, the rest of the
  block compiles). Measured: off 1.80s -> default 0.82s/gen (+54.7%), PSNR 37.7 dB
  vs eager -- far above the Q4 quant noise floor (~21 dB), so it does not move
  output quality. Gate relaxed; default tier delivers it.
- cudnn.benchmark added to the default tier (autotunes the fixed-shape VAE convs).
- torch.inference_mode() around the pipeline call (lossless, strictly faster than
  the no_grad diffusers uses internally).

Memory path:
- VAE tiling (not bit-identical >1MP) restricted to the model/sequential/CPU tiers;
  the balanced (group) tier keeps exact slicing only, so it is now bit-identical to
  the resident image (verified PSNR inf) and slightly faster.
- Group offload adds non_blocking + record_stream on the CUDA stream path to
  overlap each block's H2D copy with compute (lossless; gated on the installed
  diffusers signature so older versions still work).

Native (sd.cpp) path:
- native_speed_flags: a first-class speed knob (default -> --diffusion-fa, a
  near-lossless CUDA win that was previously only added on offload tiers; max also
  -> --diffusion-conv-direct). conv-direct stays opt-in: measured +45% on CUDA, so
  it is never auto-on. Engine generate() merges it, de-duped against offload flags.

Default profile: a GGUF model with no explicit speed_mode now resolves to the
`default` profile (resolve_speed_mode), since compile's perturbation sits below the
quantisation noise floor and so does not reduce quality versus the dense reference;
out of the box a GGUF Z-Image generation drops from 1.80s to 0.81s. Dense models
stay `off` / bit-identical, and an explicit speed_mode -- including "off" -- is
always honored, so the byte-identical path remains one flag away and is the
regression reference.

Tooling: scripts/compile_probe.py (eager vs compiled GGUF probe), scripts/
perf_verify.py (the B200 verification above), and diffusion_bench.py gains
--speed-mode so the speed tiers are benchmarkable.

Tests: 183 passing (was 166); new coverage for the backend-flag snapshot/restore,
GGUF compile eligibility, the balanced tiling/slicing split, native_speed_flags +
the engine de-dup, and the sd-cli silent-hang timeout.

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

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

* Studio diffusion (Phase 7): max tier uses max-autotune-no-cudagraphs + engine/lever benchmarks

The opt-in `max` speed tier now compiles the repeated block with
mode=max-autotune-no-cudagraphs (dynamic=False) instead of the default mode:
Triton autotuning for GEMM/conv-heavier models, gated to the tier where a longer
cold compile is acceptable. CUDA-graph modes (reduce-overhead / max-autotune) are
deliberately avoided -- both crash on the regionally-compiled block (its static
output buffer is overwritten across denoise steps), measured.

Adds two reproducible benchmarks used to validate the optimization research:
- scripts/compare_engines.py: PyTorch (diffusers GGUF) vs native sd.cpp head-to-head.
- scripts/leverage_probe.py: coordinate_descent_tuning + FirstBlockCache probes.

Measured on B200 (Z-Image Q4_K_M, 1024px, 8 steps): default compile 0.80s/gen;
coordinate_descent_tuning 0.79s (within noise, already covered by max-autotune);
FirstBlockCache does not run on Z-Image (diffusers 0.38 block-detection / Dynamo).

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

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

* Studio diffusion (Phase 8): opt-in fast transformer (torchao int8/fp8/fp4 on a dense source)

Add an opt-in transformer_quant mode that loads the dense bf16 transformer and
torchao-quantises it onto the low-precision tensor cores, instead of the GGUF
transformer (which dequantises to bf16 per matmul and so runs at bf16 rate). On a
B200 (Z-Image-Turbo, 1024px/8 steps): auto picks fp8 at 0.614s vs GGUF+compile's
0.823s (1.34x), int8 0.626s (1.32x), both at lower LPIPS than GGUF's own 4-bit floor.

GGUF+compile stays the low-memory default and the fallback. The mode is gated on
CUDA + bf16 + resident VRAM headroom (the dense load peaks ~21GB vs GGUF's 13GB);
any unsupported arch/scheme, OOM, or quant failure falls back to GGUF with a logged
reason. auto picks the best scheme per GPU via a real quantise+matmul smoke probe
(Blackwell nvfp4/fp8/mxfp8, Ada/Hopper fp8, Ampere int8); a min-features filter skips
the tiny projections that crash int8's torch._int_mm. New module mirrors
diffusion_precision.py; quant runs before compile before placement.

184 -> tests pass; new test_diffusion_transformer_quant.py plus backend/route
coverage. scripts/diffusion_bench.py gains --transformer-quant; scripts/quant_probe.py
is the standalone torchao lever probe.

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

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

* Studio diffusion (Phase 8): consumer-GPU tuning - lock fp8 fast accumulate, prefer fp8 over mxfp8, reject 2:4 sparsity

Consumer Blackwell halves tensor-core throughput on FP32 accumulate (fp8 419 vs 838
TFLOPS with FP16 accumulate; bf16 209), so:
- fp8 config locks use_fast_accum=True (Float8MMConfig). torchao already defaults it on;
  pinning it guards consumer cards against a default change. On B200 it is identical
  speed and slightly better quality (LPIPS 0.050 vs 0.091).
- the Blackwell auto ladder prefers fp8 over mxfp8 (measured faster + more accurate).

2:4 semi-structured sparsity evaluated and rejected (scripts/sparse_accum_probe.py):
2:4 magnitude-prune + fp8 gives LPIPS 0.858 (broken image) with no fine-tune, the
cuSPARSELt kernel errors on torch 2.9, and it does not compose with torch.compile
(our main ~2x). Documented as a dead end, not shipped.

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

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

* Studio diffusion (Phase 8): add fp8 fast-accum overflow verification probe

scripts/fp8_overflow_check.py hooks every quantised linear during a real Z-Image
generation and reports max-abs + non-finite counts for use_fast_accum True vs False.
Confirms fast accumulation is an accumulation-precision knob, not an overflow one:
across 276 linears, including Z-Image's ~1.0e6 activation peaks (which overflow FP16),
0 non-finite elements and identical max-abs for both modes.

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

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

* Studio diffusion (Phase 8): detect consumer vs data-center GPU for fp8 accumulate, with user override

Consumer/workstation GPUs (GDDR) halve fp8 FP32-accumulate throughput, so they want
fast (FP16) accumulate; data-center HBM parts (B200/H100/A100/L40) are not nerfed and
prefer the higher-precision FP32 accumulate. Add _is_consumer_gpu() (token-exact match
on the device name per NVIDIA's GPU list, so workstation A4000 != data-center A40;
GeForce/TITAN and unknown default to consumer) and gate the fp8 use_fast_accum on it.

Measured: fast accumulate is ~2x on consumer Blackwell and ~8% on B200 (0.608 vs 0.665s),
no overflow, quality below the quant noise floor. So the default leans to accuracy on
data-center; a new request field transformer_quant_fast_accum (null=auto, true/false=force)
lets the operator override per load (scripts/diffusion_bench.py --fp8-fast-accum auto|on|off).

187 diffusion tests pass (+ consumer detection, _resolve_fast_accum, and the override
threading).

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

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

* Studio diffusion (Phase 8): add NVFP4 probe documenting it is not yet a win on torch 2.9

scripts/nvfp4_probe.py measures NVFP4 via torchao on the real Z-Image transformer.
Finding (B200, 1024px/8 steps): NVFP4 is a torchao feature and DOES run with
use_triton_kernel=False (the default triton path needs the missing MSLK library), but
only at bf16-compile rate (0.667s vs fp8 0.592s) -- it dequantises FP4->bf16 rather than
using the FP4 tensor cores. The real FP4 speedup needs MSLK or torch>=2.11 + torchao's
CUTLASS FP4 GEMM. The smoke probe (default triton=True) already keeps NVFP4 out of auto
on this env, so auto correctly stays on fp8; NVFP4 activates automatically once fast.

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

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

* Studio diffusion (Phase 8): prefer fp8 over nvfp4 in Blackwell auto ladder

Validated NVFP4 on torch 2.11 + torchao CUTLASS FP4 in an isolated env. The FP4
tensor-core GEMM is genuinely active there (a 16384^3 GEMM hits ~3826 TFLOPS,
2.52x bf16 and 1.37x fp8), but it only beats fp8 on very large GEMMs. At the
diffusion transformer's shapes (hidden ~3072, MLP ~12288, M~4096) NVFP4 is both
slower (0.81x fp8 end to end on Z-Image 1024px) and less accurate (LPIPS 0.166
vs fp8's 0.044). Reorder the Blackwell auto ladder to fp8 before nvfp4 so auto is
correct even on a future MSLK-equipped box; nvfp4 stays an explicit opt-in. Add
scripts/nvfp4_t211_probe.py (extension diagnostics + GEMM micro + end-to-end).

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

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

* Studio diffusion (Phase 9): pre-quantized transformer loading

The Phase 8 fast transformer_quant path materialises the dense bf16 transformer on
the GPU and torchao-quantises it in place, so its load peak is ~2x GGUF's (~21 vs
13.4 GB) plus a ~12 GB download. Add a pre-quantized branch: quantise once offline
(scripts/build_prequant_checkpoint.py) and at runtime build the transformer skeleton
on the meta device (accelerate.init_empty_weights) and load_state_dict(assign=True)
the quantized weights, so the dense bf16 never touches the GPU.

Measured (B200, Z-Image fp8): full-pipeline GPU load peak 21.2 -> 14.6 GB (matching
GGUF's 13.4), on-disk 12 -> 6.28 GB, output bit-identical (LPIPS 0.0). It is the same
torchao config + min_features filter the runtime path uses, applied ahead of time.

New core/inference/diffusion_prequant.py (resolve_prequant_source +
load_prequantized_transformer, best-effort, lazy imports). diffusion.py
_load_dense_quant_pipeline tries the pre-quant source first and falls back to the
dense materialise+quantise path, then to GGUF, so the default is unchanged.
DiffusionLoadRequest gains transformer_prequant_path; DiffusionFamily gains an empty
prequant_repos map for hosted checkpoints (hosting deferred). Hermetic CPU tests for
the resolver, the meta-init+assign loader, and the backend branch selection +
fallbacks; GPU verification via scripts/verify_prequant_backend.py.

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

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

* Studio diffusion (Phase 10): attention-backend selection

Add a selectable attention kernel via the diffusers set_attention_backend
dispatcher. Attention is memory-bandwidth bound, so a better kernel is an
end-to-end win orthogonal to the linear-weight quantisation (it speeds the QK/PV
matmuls torchao never touches) and composes with torch.compile.

auto picks the best exact backend for the device: cuDNN fused attention
(_native_cudnn) on NVIDIA when a speed profile is active, measured ~1.18x
end-to-end on a B200 (Z-Image 1024px/8 steps) with LPIPS ~0.004 vs the default
(below the compile/quant noise floor); native SDPA elsewhere and when speed=off
(so off stays bit-identical). Explicit native/cudnn/flash/flash3/flash4/sage/
xformers/aiter are honored, and an unavailable kernel falls back to the default
rather than failing the load.

New core/inference/diffusion_attention.py (normalize + per-device select + apply,
best-effort, lazy imports). Set on pipe.transformer BEFORE compile in load_pipeline;
attention_backend threads through begin_load / load_pipeline / status like the other
load knobs. New request field attention_backend + status field. Hermetic CPU tests
for normalize / select policy / apply fallback, plus route threading + 422. Measured
via scripts/perf_levers_probe.py.

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

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

* Studio diffusion (Phase 11): prefer int8 on consumer GPUs in the auto ladder

Consumer / workstation GPUs halve fp8 (and fp16/bf16) FP32-accumulate tensor-core
throughput, while int8 runs at full rate (int32 accumulate is not nerfed). Public
benchmarks (SDNQ across RTX 3090/4090/5090, AMD, Intel) confirm int8 via torch._int_mm
is as fast or faster than fp8 on every consumer part, and the only path on pre-Ada
consumer cards without fp8 tensor cores. So when transformer_quant=auto, reorder the
arch tier to put int8 first on a consumer/workstation GPU (detected by the existing
_is_consumer_gpu name heuristic), while data-center HBM parts keep fp8 first.

Pure ladder reorder via _prefer_consumer_scheme; no new flags. Verified non-regression
on a B200 (still picks fp8). Hermetic tests for consumer Blackwell/Ada/workstation
(-> int8) and data-center Ada/Hopper/Blackwell (-> fp8).

* Studio diffusion (Phase 12): First-Block-Cache step caching for many-step DiT

Add opt-in step caching (First-Block-Cache) for the diffusion transformer. Across
denoise steps a DiT's output settles, so once the first block's residual barely
changes the remaining blocks are skipped and their cached output reused. diffusers
ships it natively (FirstBlockCacheConfig + transformer.enable_cache, with the
standalone apply_first_block_cache hook as a fallback).

Measured on Flux.1-dev (28 steps, 1024px): ~1.4x on top of torch.compile (2.83 ->
2.03s) at LPIPS ~0.08 vs the no-cache output, well inside the quality bar.

OFF by default and a per-load opt-in: the win scales with step count, so it is for
many-step models (Flux / Qwen-Image) and pointless for few-step distilled models
(e.g. Z-Image-Turbo at ~8 steps), where a single skipped step is a large fraction
of the trajectory. It composes with regional compile only with fullgraph=False (the
cache's per-step decision is a torch.compiler.disable graph break), which the speed
layer now switches to automatically when a cache is engaged. Best-effort: a model
whose block signature the hook does not recognise is caught and the load proceeds
uncached.

- new core/inference/diffusion_cache.py: normalize_transformer_cache + apply_step_cache
  (enable_cache / apply_first_block_cache fallback; threshold auto-raised for a
  quantised transformer per ParaAttention's fp8 guidance; lazy diffusers import).
- diffusion_speed.py: apply_speed_optims takes cache_active; compile drops fullgraph
  when a cache is engaged.
- diffusion.py: apply_step_cache before compile; thread transformer_cache /
  transformer_cache_threshold through begin_load -> load_pipeline and report the
  engaged mode in status().
- models/inference.py + routes/inference.py: transformer_cache (off | fbcache) and
  transformer_cache_threshold request fields, engaged mode in the status response.
- hermetic tests for normalisation, the enable_cache / hook-fallback paths, threshold
  selection, and best-effort failure handling, plus route threading + validation.
- scripts/fbcache_flux_probe.py: the Flux validation probe (latency / speedup / VRAM /
  LPIPS vs the compiled no-cache baseline).

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

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

* Studio diffusion (Phase 14): fix int8 dense quant on Flux / Qwen (skip M=1 modulation linears)

The opt-in dense int8 transformer path crashed on Flux.1 and Qwen-Image with
'torch._int_mm: self.size(0) needs to be greater than 16, but got 1'. int8 dynamic quant
goes through torch._int_mm, which requires the activation row count M > 16. A DiT's AdaLN
modulation projections (Flux norm1.linear 3072->18432, Qwen img_mod.1 / txt_mod.1, Flux.2
*_modulation.linear) and its timestep / guidance / pooled-text conditioning embedders are
computed once from the [batch, dim] conditioning vector (M = batch = 1), not per token, so
they hit _int_mm at M=1 and crash. Their feature dims are large, so the existing
min_features filter did not exclude them.

Fix: the int8 filter now also skips any Linear whose fully-qualified name matches a
modulation / conditioning-embedder token (norm, _mod, modulation, timestep_embed,
guidance_embed, time_text_embed, pooled). These layers run at M=1 once per block and are a
negligible share of the FLOPs, so int8 keeps the full speedup on the attention / FFN layers
(M = sequence length). fp8 / nvfp4 / mxfp8 use scaled_mm, which has no M>16 limit and
quantises these layers fine, so the exclusion is int8-only. Sequence embedders
(context_embedder / x_embedder / txt_in, M = seq) are deliberately not excluded -- note
'context_embedder' contains the substring 'text_embed', which is why the token is the
specific 'time_text_embed', not 'text_embed'.

Measured on a B200 (1024px, transformer_quant=int8 + speed=default), int8 now runs on every
supported model and is the fastest dense path on Flux/Qwen (int8 runs full-rate vs fp8's
FP32-accumulate): FLUX.1-dev 9.62s eager -> 1.98s (4.86x, vs fp8 2.15s), Qwen-Image -> 1.87s
(5.57x, vs fp8 2.09s), FLUX.1-schnell -> 0.41s (3.59x). Z-Image and Flux.2-klein (already
working) are unchanged.

- diffusion_transformer_quant.py: add _INT8_EXCLUDE_NAME_TOKENS; make_filter_fn takes
  exclude_name_tokens; quantize_transformer passes it for int8 only.
- hermetic test that the int8 filter excludes the modulation / embedder linears (and keeps
  attention / FFN / sequence-embedder linears), while fp8 keeps them.
- scripts/int8_linear_probe.py: the meta-device probe used to enumerate each transformer's
  Linear layers and derive the exclusion list.

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

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

* Studio diffusion (Phase 15): build int8 pre-quantized checkpoints (skip M=1 modulation linears)

The prequant-checkpoint builder applied the dense quant filter without the int8-only
M=1 modulation / conditioning-embedder exclusion the runtime path uses, so a built int8
checkpoint baked those projections as int8 and crashed (torch._int_mm needs M>16) at the
first denoise step on Flux / Qwen. Factor the scheme->exclusion decision into a shared
exclude_tokens_for_scheme() used by both the runtime quantise path and the offline builder
so they can never drift, and apply it in build_prequant_checkpoint.py. int8 prequant now
produces a working checkpoint on every supported model, giving int8 (the consumer-preferred
scheme) the same ~2x load-VRAM and download reduction fp8 already had.

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

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

* Studio diffusion (Phase 16): route no-GPU loads to the native sd.cpp engine

When no CUDA/ROCm/XPU GPU is available, route diffusion load/generate to the
native stable-diffusion.cpp engine instead of diffusers, with diffusers as the
guaranteed fallback. On CPU sd.cpp is 1.4-2.8x faster and uses 1.5-2.2x less RAM.

- diffusion_engine_router: centralised engine selection (built on the existing
  select_diffusion_engine), env opt-outs, MPS gating, recorded fallback reason.
- sd_cpp_backend (SdCppDiffusionBackend): the diffusers backend method surface
  backed by sd-cli, with lazy binary install, registry-driven asset fetch,
  step-progress parsing, and cancellation.
- diffusion_families: per-family single-file VAE + text-encoder asset mapping.
- sd_cpp_engine: cancellation support (process-group kill + SdCppCancelled).
- routes/inference + gpu_arbiter: drive the active engine via the router; the
  API now reports the active engine and any fallback reason.
- tests for the backend, router, route selection, and cancellation.

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

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

* Phase 16 review fixes: engine-switch unload, sd.cpp error mapping, per-image seeds, Qwen sampler

Address review feedback on #6724:
- engine router: unload the engine being deactivated on a switch, so the old
  model is not left resident-but-unreachable (the evictor only targets the active
  engine).
- generate route: sd.cpp execution errors (nonzero exit / timeout / missing
  output) now map to 500, not 409 (which only means not-loaded / cancelled).
- native batch: return per-image seeds and persist the actual seed for each image
  so every batch image is reproducible.
- Qwen-Image native path: apply --sampling-method euler --flow-shift 3 per the
  stable-diffusion.cpp docs; other families keep sd-cli defaults.
- honor speed_mode (native --diffusion-fa) and, off-CPU, memory_mode/cpu_offload
  offload flags on the native load instead of hardcoding them off.
- fail the load when the sd-cli binary is present but not runnable (version()
  now returns None on exec error / nonzero exit).
- size estimate: only treat the transformer asset as a possible local path.

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

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

* Studio diffusion (Phase 9): gate request-supplied local prequant paths behind operator opt-in

load_prequantized_transformer ends in torch.load(weights_only=False), which executes
arbitrary code from the pickle. The transformer_prequant_path load-request field reached
that unpickle for any local file an authenticated caller named, so a request could trigger
remote code execution. Refuse the source.kind=='path' branch unless the operator sets
UNSLOTH_ALLOW_LOCAL_PREQUANT_PATH=1; the first-party hosted-repo checkpoint stays trusted
and unaffected. Document the requirement on the API field and add gate tests.

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

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

* Studio diffusion (Phase 10): reset the global attention backend on native, gate arch-specific kernels, accept sdpa

- apply_attention_backend now restores the native default when no backend is requested or a
  kernel fails. diffusers keeps a process-wide active attention backend that
  set_attention_backend updates, and a fresh transformer's processors follow it, so a load
  that wanted native could silently inherit a backend (e.g. cuDNN) an earlier speed-profile
  load pinned, breaking the bit-identical/off guarantee.
- select_attention_backend drops flash3/flash4 up front when the CUDA capability is below
  Hopper/Blackwell. diffusers only checks the kernels package at set time, so an explicit
  request on the wrong card set fine then crashed mid-generation; it now falls back to native.
- Add the sdpa alias to the attention_backend Literal so an API request with sdpa (already a
  valid alias of native) is accepted instead of 422-rejected by Pydantic.
- Drop the dead replace('-','_') normalization (no alias uses dashes/underscores).
- perf_levers_probe.py output dir is now relative to the script, not a hardcoded path.

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

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

* Studio diffusion (Phase 12): only engage FBCache on context-aware transformers; quantized threshold for GGUF

- apply_step_cache now engages only via the transformer's native enable_cache (the diffusers
  CacheMixin path), which exists exactly when the pipeline wraps the transformer call in a
  cache_context. The standalone apply_first_block_cache fallback installed on non-CacheMixin
  transformers too (e.g. Z-Image), whose pipeline opens no cache_context, so the load reported
  transformer_cache=fbcache and then the first generation crashed inside the hook. Such a model
  now runs uncached per the best-effort contract.
- GGUF transformers are quantized (the default Studio load path), so they now use the higher
  quantized FBCache threshold when the caller leaves it unset, instead of the dense default
  that could keep the cache from triggering.
- fbcache_flux_probe.py: compile cached runs with fullgraph=False (FBCache is a graph break, so
  fullgraph=True failed warmup and silently measured an eager cached run); output dir is now
  relative to the script, not a hardcoded path.

* Studio diffusion (Phase 11): keep professional RTX cards on the fp8 ladder

_is_consumer_gpu treated professional parts (RTX PRO 6000 Blackwell, RTX 6000 Ada) as
consumer because their names carry no datacenter token, so the auto ladder moved int8 ahead
of fp8 and the fp8 path chose fast accumulate for them. The rest of the backend already
classifies these as datacenter/professional (llama_cpp.py _DATACENTER_GPU_RE), so detect the
same RTX PRO 6000 / RTX 6000 Ada markers here and keep fp8 first with precise accumulate.

Also fix the consumer-Blackwell test to use compute capability (10, 0) instead of (12, 0).

* Studio diffusion (Phase 8): tolerate missing torch.float8_e4m3fn in the mxfp8 config

Accessing torch.float8_e4m3fn raises AttributeError on a torch build without it (not just
TypeError on older torchao), which would break the mxfp8 config helper instead of falling
back to the default. Catch both so the fallback is robust.

quant_probe.py: same AttributeError fallback; run LPIPS on CPU so the scorer never holds
CUDA memory during the per-row VRAM probe; output dir relative to the script.

* Studio diffusion (Phase 7): robust backend-flag snapshot/restore and restore on failed speeded load

- snapshot_backend_flags reads each flag defensively (getattr + hasattr), so a build/platform
  missing one (no cuda.matmul on CPU/MPS) still captures the rest instead of skipping the
  whole snapshot. restore_backend_flags restores each flag independently so one failure can't
  leave the others leaked process-wide.
- load_pipeline restores the flags (and clears the GPU cache) when the build fails after
  apply_speed_optims mutated the process-wide flags but before _state captured them for unload
  to restore -- otherwise a failed default/max load left cudnn.benchmark/TF32 on and
  contaminated later off generations.

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

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

* Studio diffusion (Phase 4): enforce the sd-cli timeout while reading output

Iterating proc.stdout directly blocks until the stream closes, so a sd-cli that hangs
without producing output (or without closing stdout) would never reach proc.wait and the
wall-clock timeout was silently bypassed. Drain stdout on a daemon thread and wait on the
PROCESS, so the main thread always enforces the timeout and kills a hung process (which
closes the pipe and ends the reader). Add a test that times out even when stdout blocks,
and make the no-binary test hermetic so a host-installed sd-cli can't leak in.

* Studio diffusion (Phase 14): guard the int8 exclusion filter against a None fqn

The filter callback can be invoked without a module name, so fqn.lower() would raise
AttributeError on None. Fall back to an empty name (nothing matches the exclusion tokens,
so the linear is kept) instead of crashing the quantise pass.

* Studio diffusion (Phase 16) review fixes: native engine robustness

- sd_cpp_backend: stop truncating explicit seeds to 53 bits (mask to int64);
  a large requested seed was silently collapsed (2**53 -> 0) and distinct seeds
  aliased to the same image. Random seeds stay 53-bit (JS-safe).
- sd_cpp_backend: sanitize empty/whitespace hf_token to None so HfApi/hf_hub
  fall back to anonymous instead of failing auth on a blank token.
- sd_cpp_backend: a superseding load now cancels the in-flight generation, so the
  old sd-cli can no longer return/persist an image from the previous model.
- diffusion_engine_router: run the previous engine's unload() OUTSIDE the lock so a
  slow 10+ GB free / CUDA sync does not block engine selection.
- diffusion_engine_router: probe sd-cli runnability (version()) before committing to
  native, so a present-but-unrunnable binary falls back to diffusers at selection.
- diffusion_device: resolve a torch-free CPU target when torch is unavailable, so a
  CPU-only install can still reach the native sd.cpp engine instead of failing load.
- tests updated for the runnability probe + a not-runnable fallback case.

* Studio diffusion (Phase 9) review fixes: prequant safety + validation

- SECURITY: a request-supplied local pre-quant path is now unpickled only when it
  resolves inside an operator-configured ALLOWLIST of directories
  (UNSLOTH_ALLOW_LOCAL_PREQUANT_PATH = dir[:dir...]). The previous boolean opt-in,
  once enabled for one trusted checkpoint, allowed torch.load(weights_only=False) on
  any path a load request named (arbitrary code execution). realpath() blocks symlink
  escapes; a bare on/off toggle is no longer a wildcard.
- Validate the checkpoint's min_features against the runtime Linear filter, so a
  checkpoint that quantised a different layer set is rejected instead of silently
  loading a model that mismatches the dense path while reporting the same scheme.
- Tolerant base_model_id compare (exact or same final path/repo segment), so a local
  path or fork of the canonical base is accepted instead of falling back to dense.
- _has_meta_tensors uses any(chain(...)) (no intermediate lists).
- prequant verify/probe scripts use repo-relative paths (+ env overrides), not the
  author's absolute /mnt paths.
- tests: allowlist-dir opt-in, outside-allowlist refusal, min_features mismatch, fork tail.

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

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

* Studio diffusion (Phase 7) review fixes: offload fallback + bench scripts

- diffusion_memory: when group offload is unavailable and the plan falls back to
  whole-module offload, enable VAE tiling (the group plan left it off, but the fallback
  is the low-VRAM path where the decode spike can OOM). Covers both the group and
  sequential fallback branches.
- perf_verify: include the balanced-vs-off PSNR in the pass/fail condition, so a
  balanced bit-identity regression actually fails the check instead of exiting 0.
- compare_engines: --vae/--llm default to None (were author-absolute /mnt paths), and
  the load-progress poll has a 30 min deadline instead of looping forever on a hang.
- test for the group->model fallback enabling VAE tiling.

* Studio diffusion (Phase 8) review fixes: quant compile + nvfp4 path

- diffusion: a torchao-quantized transformer is committed only compiled. A dense model
  resolves to speed_mode=off, which would run the quant eager (~30x slower than the GGUF
  it replaced), so when transformer_quant engaged and speed resolved to off, promote to
  default (regional compile); warn loudly if compile still does not engage.
- diffusion_transformer_quant: build the nvfp4 config with use_triton_kernel=False so the
  CUTLASS FP4 path is used (torchao defaults to the Triton kernel, which needs MSLK);
  otherwise the smoke probe fails on CUTLASS-only Blackwell and silently drops to GGUF.
- nvfp4_probe: repo-relative output dir + --out-dir (was an author-absolute /mnt path).
- test asserts the eager-quant -> default-compile promotion.

* Studio diffusion (Phase 10) review fixes: attention gating + probe isolation

- diffusion_attention: gate the auto cuDNN-attention upgrade on SM80+; on pre-Ampere
  NVIDIA (T4/V100) cuDNN fused SDPA is accepted at set time but fails at first generation,
  so auto now stays on native SDPA there.
- diffusion_attention: _active_attention_backend handles get_active_backend() returning an
  enum/None (not a tuple); the old  unpack always raised and was swallowed, so
  the native-restore short-circuit never fired.
- perf_levers_probe: free the resident pipe on a skipped (attn/fbcache) variant; run LPIPS
  on CPU so it isn't charged to every variant's peak VRAM; reset force_fuse_int_mm_with_mul
  so the inductor_flags variant doesn't leak into later compiled rows.
- tests for the SM80 cuDNN gate.

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

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

* Studio diffusion (Phase 4) review fixes: sd.cpp installer + engine hardening

- install_sd_cpp_prebuilt: download the release archive with urlopen + an explicit
  timeout + copyfileobj (urlretrieve has no timeout and hangs on a stalled socket);
  extract through a per-member containment check (Zip-Slip guard); expanduser the
  --install-dir so a tilde path is not taken literally; and on Windows CUDA also fetch
  the separately-published cudart runtime DLL archive so sd-cli.exe can start.
- sd_cpp_engine: find_sd_cpp_binary honors UNSLOTH_STUDIO_HOME / STUDIO_HOME like the
  installer, so a custom-root install is discovered without UNSLOTH_SD_CPP_PATH; start
  sd-cli with the parent-death child_popen_kwargs so it is not orphaned on a backend
  crash; reap the SIGKILLed child (proc.wait) so a cancel/timeout does not leave a zombie.
- tests: Zip-Slip rejection, normal extraction, studio-home discovery.

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

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

* Studio diffusion (Phase 4) review round 2: collect sd-cli batch outputs

Codex review: when batch_count > 1, stable-diffusion.cpp's save_results() writes
the numbered files <stem>_<idx><suffix> (base_0.png, base_1.png, ...) instead of
the literal --output path. SdCppEngine.generate checked only the literal path, so
a batch generation would exit 0 and then raise 'no image' (or return a stale
file). generate now returns the literal path when present and otherwise falls
back to the numbered siblings; single-image behavior is unchanged.

Test: a fake sd-cli that writes img_0.png/img_1.png (not img.png) is collected
without error.

* Studio diffusion (Phase 6) review round 2: img2img source dims + upscale repeats

Codex review on the native engine arg builder:

- build_sd_cpp_command emitted --width/--height unconditionally, so an
  img2img/inpaint/edit run that left dims unset forced a 1024x1024 resize/crop of
  the input. width/height are now Optional (None = unset): an image-conditioned
  run (init_img or ref_images) with unset dims omits the flags so sd.cpp derives
  the size from the input image (set_width_and_height_if_unset); a plain txt2img
  run with unset dims keeps the prior 1024x1024 default; explicit dims are always
  honored. width/height are read only by the builder, so the type change is local.

- build_sd_cpp_upscale_command used a truthiness guard (params.repeats and ...)
  that silently swallowed repeats=0 into sd-cli's default of one pass, turning an
  explicit no-op into a real upscale. It now rejects repeats < 1 with ValueError
  and emits the flag for any explicit value != 1.

Tests: img2img unset dims omit width/height (init_img and ref_images), explicit
dims emitted, txt2img keeps 1024; upscale rejects repeats=0 and omits the flag at
the default. (Two pre-existing binary-discovery tests fail only because a real
sd-cli is installed in this dev environment; unrelated to this change.)

* Studio diffusion (Phase 9) review round 2: correct prequant allowlist doc

Codex review: the transformer_prequant_path field description still told operators
to enable local checkpoints with UNSLOTH_ALLOW_LOCAL_PREQUANT_PATH=1, but the
prior security fix made that variable a directory allowlist -- _allowed_prequant_roots
deliberately drops bare on/off toggle tokens (1/true/yes/...). An operator
following the documented =1 would have every transformer_prequant_path request
silently refused. The description now states it must name one or more allowlisted
directories and that a bare on/off value is not accepted.

Test: asserts the field help references UNSLOTH_ALLOW_LOCAL_PREQUANT_PATH, does
not say =1, and describes an allowlist/directory (guards against doc drift).

* Studio diffusion (Phase 10) review round 2: cudnn/flash3 gating + registry reset

Codex review on attention-backend selection:

- Explicit attention_backend=cudnn skipped the SM80 gate that auto applies, so on
  pre-Ampere NVIDIA (T4 SM75 / V100 SM70) it set fine then crashed at the first
  generation with no fallback. select_attention_backend now applies
  _cudnn_attention_supported() to an explicit cuDNN request too.

- flash3 used a minimum-only capability gate (>= SM90), so an explicit flash3 on a
  Blackwell B200 (SM100) passed and then failed at generation -- FlashAttention 3
  is a Hopper-SM90 rewrite with no Blackwell kernel. The arch gate is now a
  (min, max-exclusive) range: flash3 is SM9x-only, flash4 stays SM100+.

- apply_attention_backend's success path left diffusers' process-wide active
  backend pinned to the kernel it set; a later component whose processors are
  unconfigured (backend None) would inherit it. It now resets the global registry
  to native after a successful per-transformer set (the transformer keeps its own
  backend), best-effort. Also fixed _active_attention_backend: get_active_backend()
  returns a (name, fn) tuple, so the prior code stringified the tuple and never
  matched a name, defeating the native-restore short-circuit.

Tests: explicit cudnn dropped below SM80; flash3 dropped on SM100 and allowed on
SM90; global registry reset after a successful set; _active_attention_backend
reads the tuple return.

* Studio diffusion (Phase 11) review round 2: keep GH200/B300 on the fp8 ladder

Codex review: _DATACENTER_GPU_TOKENS omitted GH200 (Grace-Hopper) and B300
(Blackwell Ultra), though it has the distinct GB200/GB300 superchip tokens. So
_is_consumer_gpu returned True for 'NVIDIA GH200 480GB' / 'NVIDIA B300', and the
auto ladder moved int8 ahead of fp8 on those data-center parts -- contradicting
llama_cpp.py's datacenter regex, which lists both. Added GH200 and B300 so they
are treated as data-center class and keep the intended fp8-first behavior.

Test: extends the datacenter parametrize with 'NVIDIA B300' and
'NVIDIA GH200 480GB' (now _is_consumer_gpu False).

* Studio diffusion (Phase 14) review round 2: apply int8 M=1 exclusion in the builder

Codex review: the M=1 modulation/embedder exclusion was wired only into the dense
runtime quantiser; the offline builder scripts/build_prequant_checkpoint.py called
make_filter_fn(min_features) with no exclusion. So an int8 prequant checkpoint
quantised the AdaLN modulation and conditioning-embedder linears, and loading it
via transformer_prequant_path (the load path only loads already-quantised tensors,
it can't re-skip them) reintroduced the torch._int_mm M=1 crash this phase fixes
for the runtime path.

Extracted int8_exclude_name_tokens(scheme) as the single source of truth (int8 ->
the M=1 exclusion, every other scheme -> none) and use it in both the runtime
quantiser and the builder, so a prequant artifact's quantised-layer set always
matches the runtime. fp8/fp4/mx artifacts are byte-identical (empty exclusion).

Test: int8_exclude_name_tokens returns the exclusion for int8 and () for
fp8/nvfp4/mxfp8.

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

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

* Studio diffusion (Phase 16) review round 2: native CPU arbiter, status offload, load race

Codex review on the native-engine routing:

- The /images/load route took the GPU arbiter (acquire_for(DIFFUSION) -> evict chat)
  unconditionally after engine selection. A native sd.cpp load on a pure-CPU host
  never touches the GPU, so that needlessly tore down the resident chat model. The
  handoff is now gated: diffusers always takes it, a force-native sd.cpp load on a
  CUDA/XPU/MPS box still takes it, but a native sd.cpp load on a CPU host skips it.

- sd_cpp status() hardcoded offload_policy 'none' / cpu_offload False even when
  _run_load computed real offload flags (balanced/low_vram/cpu_offload off-CPU), so
  the setting was unverifiable. status now derives them from state.offload_flags
  (still 'none' on CPU, where the flags are empty).

- _run_load committed the new state without cancelling/waiting on a generation that
  started during the (slow) asset download, so a stale sd-cli run against the OLD
  model could finish afterward and persist an image from the previous model once the
  new load reported ready. The commit now signals the in-flight cancel and waits on
  _generate_lock before swapping _state (taken only at commit, so the download never
  serialises against generation), mirroring the diffusers load path.

Tests: CPU native load skips the arbiter while a GPU native load takes it; status
reports offload active when flags are set; _run_load cancels and waits for an
in-flight generation before committing.

* Studio diffusion (Phase 14) review round 2: align helper name with the stack

Rename the int8 exclusion helper to exclude_tokens_for_scheme, matching the
identical helper already present higher in the diffusion stack (Phase 16). The
helper definition, the runtime quantiser call, and the offline builder are now
byte-identical to that version, so the two branches no longer introduce a
divergent name for the same single-source-of-truth and the stack merges without
a conflict on this fix. No behavior change.

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

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

* Studio diffusion: eager patches + torch.compile cache speed phase

Adds the opt-in speed path for the GGUF diffusion transformer behind a
selectable speed mode (default off, so output is unchanged until a profile
is chosen):

- diffusion_eager_patches.py: shared eager fast-paths (channels_last,
  attention/backend selection, fused norms and QKV) installed at load and
  rolled back on unload or failed load.
- diffusion_compile_cache.py / diffusion_gguf_compile.py: a persistent
  torch.compile cache and the GGUF-transformer compile wiring.
- diffusion_arch_patches.py: architecture-specific patches.
- diffusion_patch_backend.py: shared install/restore plumbing.
- diffusion_speed.py: speed-profile planning.

Tests for each module plus the benchmarking and probe scripts used to
measure speed, memory, and accuracy of the path.

* Studio diffusion: image workflows (safetensors, image-conditioned, editing) + Images UI

Backend:
- Load non-GGUF safetensors models: full bnb-4bit pipelines and single-file
  fp8 transformers, gated to the unsloth org plus a curated allowlist.
- Image-conditioned workflows built with Pipeline.from_pipe so they reuse the
  loaded transformer/VAE/text-encoder with no extra VRAM: img2img, inpaint,
  outpaint, and a hires-fix upscale pass.
- Instruction editing as its own family kind (Qwen-Image-Edit-2511,
  FLUX.1-Kontext-dev) and FLUX.2-klein reference conditioning (single and
  multi-reference) plus klein inpaint.
- Auto-resize odd-sized inputs to a multiple of 16 (and resize the matched
  mask) so img2img/inpaint/edit no longer reject non-/16 uploads. Bound the
  decoded image size and cap upscale output to avoid OOM on large inputs.
- Fixes: from_pipe defaulting to a float32 recast that crashed torchao
  quantized transformers; image-conditioned calls forcing the slider size
  onto the input image. Native sd.cpp engine rejects image-conditioned and
  reference requests it cannot serve.

Frontend:
- Redesigned Images page with capability-gated workflow tabs (Create,
  Transform, Inpaint, Extend, Upscale, Reference, Edit), a brush mask editor,
  client-side outpaint, and a multi-reference picker.
- Advanced options moved to a right-docked panel mirroring Chat: closed by
  default, toggled by a single fixed top-bar button that stays in place.

sd.cpp installer: pin the release, verify each download's sha256, add a
download timeout, and make the source repo configurable for a future mirror.

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

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

* Studio Images: correct the Advanced panel comment (closed by default, fixed toggle)

* Studio diffusion: fix FP8 transformer quant producing noise (per-row scaling)

FP8 dense-quant produced pure noise on models with extreme activation
outliers (z-image), while INT8 was fine. Root cause: torchao's default
fp8 granularity is per-TENSOR, so z-image's MLP activation outliers
(~6.6e4) force a tensor-wide scale that pushes every normal value below
fp8 resolution and the denoise collapses to noise. INT8 dynamic is
per-token by default, which is why it was unaffected.

Fix: request PerRow granularity (per-token activation + per-output-channel
weight) for the fp8 config, confining each outlier to its own row. The
per-row scaled_mm is probed by _smoke_probe, so an arch or build without
it falls through the ladder to int8.

Validated on B200 (z-image, 1024px, 8 steps): per-tensor fp8 = noise,
per-row fp8 = matches bf16; int8 unaffected. Adds a regression test
asserting the fp8 config carries PerRow granularity.

* Studio: do not force diffusers pipelines cross-tagged gguf into the GGUF variant expander

Some diffusers image repos (e.g. unsloth/Qwen-Image-2512-unsloth-bnb-4bit) carry a
stray "gguf" tag on the Hub but ship no .gguf files. The model search classified
them as GGUF from the bare tag, so the picker rendered the GGUF variant expander,
which then dead-ended at "No GGUF variants found." Trust the bare gguf tag only when
the repo is not a diffusers pipeline; the -GGUF name suffix and real gguf metadata
(populated via expand=gguf) remain authoritative, so genuine GGUF repos are unaffected.

* Studio Images: load non-curated unsloth/on-device diffusers repos instead of no-op

handleModelSelect only loaded curated safetensors ids and GGUF variant picks; any other
non-GGUF pick (an on-device diffusers folder, or a future unsloth diffusers image repo
surfaced by search) silently did nothing. Treat such a pick as a full diffusers pipeline
load when the id is unsloth-hosted or on-device (the backend infers the family + base repo
and gates loads to unsloth/* or local paths), and show a clear message otherwise instead
of silently ignoring the click. Curated and GGUF paths are unchanged.

* Studio Images: keep curated safetensors models in Recommended after download

The curated bnb-4bit / fp8 diffusion rows were filtered out of the Images picker's
Recommended list once cached (curatedSafetensorsRows dropped anything in downloadedSet),
so they vanished from the picker after the first load and could only be found by typing an
exact search. The row already renders a downloaded badge, matching how GGUF Recommended
rows stay visible when cached. Drop the exclusion so the curated safetensors always list.

* Studio Images: clarify the GGUF transformer-quant Advanced control

Renamed the confusing "Transformer quant / GGUF default" control to "GGUF speed mode"
with an "Off (run the GGUF)" default, and reworded the hint to state plainly that FP8/INT8/
FP4 load the FULL base model (larger download + more VRAM) rather than re-packing the GGUF,
falling back to the GGUF if it can't fit. Behavior unchanged; labels/hint only.

* Studio Images: list on-device unsloth diffusion models in the picker

The Images picker's On Device tab hid every non-GGUF cached repo whenever a
task filter was active, so downloaded unsloth diffusion pipelines (bnb-4bit
and FP8 safetensors) never showed up there. List cached repos that pass the
task gate, limited under a filter to unsloth-hosted ones so base repos (which
fail the diffusion load trust gate) don't appear only to dead-end on click.
Chat behavior is unchanged: the task gate still drops image repos there.

* Studio: hide single-file image checkpoints from the chat model picker

The chat picker treats a cached repo as an image model, and hides it, only
when it ships a diffusers model_index.json. Single-file, ComfyUI, and
ControlNet image checkpoints (an FP8 Qwen-Image, a z-image safetensors, a
Qwen-Image ControlNet) carry none, so they surfaced as loadable chat models.
Fall back to resolving the repo id against the known diffusion families, the
same resolver the Images backend loads from, so these checkpoints are tagged
text-to-image and stay in the Images picker only.

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

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

* Studio Images: add the FLUX.2-dev model family

Loading unsloth/FLUX.2-dev-GGUF failed because detect_family knew only the
Qwen3-based FLUX.2-klein, so FLUX.2-dev (the full, Mistral-based Flux2Pipeline)
resolved to nothing and the load errored. Add a flux.2-dev family: Flux2Pipeline
+ Flux2Transformer2DModel over the black-forest-labs/FLUX.2-dev base repo (gated,
reachable with an HF token), with its FLUX.2 32-channel VAE and Mistral text
encoder wired for the sd-cli path from the open Comfy-Org/flux2-dev mirror.
text-to-image only: diffusers 0.38 ships no Flux2 img2img / inpaint pipeline for
dev. Frontend gets sensible dev defaults (28 steps, guidance 4), distinct from
klein's turbo defaults. Verified live: GGUF load resolves the family + gated base
repo and generates a real 1024x1024 image on GPU.

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

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

* Studio Images: clearer error for an unsupported diffusion model

When a repo id resolves to no diffusion family the load raised 'Could not infer a
diffusion family... Pass family_override (z-image)', which points at an unrelated
family and doesn't say what is supported. Replace it with a message that lists the
supported families (from a new supported_family_names helper) and notes that video
models and image models whose diffusers transformer has no single-file loader are
not supported. Applies to both the diffusers and native sd.cpp load paths. Also
refreshes two stale family-registry comments that still called FLUX.2-dev omitted.

* [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

* [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

* [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

* [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

* [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

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

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

* Remove stray async task scratch outputs committed by mistake

* fp8 prequant: reject stale per-tensor checkpoints

The per-row fp8 fix only applies when quantising dense on the fly; a supplied
transformer_prequant_path (or a future hosted fp8 repo) bypassed it, so an fp8
checkpoint baked with the old per-tensor layout still loaded and reproduced the
z-image noise failure. Stamp the fp8 granularity into the checkpoint metadata at
build time and require per-row at load time, so a stale artifact is rejected and
the loader falls back to rebuilding/re-quantising. The gate is fp8-only (int8 and
the others are unaffected). Adds regression tests.

* Diffusion: guard trust check against OSError and validate conditioning inputs

- _is_trusted_diffusion_repo: wrap Path.exists() so a repo id with invalid
  characters (or a bare owner/name id) can't raise OSError; treat any failure as
  not-a-local-path and fall through to the unsloth/ allowlist. validate_load_request
  still raises the clear FileNotFoundError for a genuinely missing local pick.
- generate(): reject mask_image / upscale / reference_images supplied without an
  input image, and reject reference_images on a family that does not support
  reference conditioning, instead of silently degrading to txt2img / img2img.

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

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

* Address Codex review findings on the image-workflows PR

Keep diffusion.py importable without torch: the compile/arch patch modules
import torch at module level, so import them lazily at their load/unload
call sites instead of at module load. This restores the torchless contract
so get_diffusion_backend() works on a CPU/native sd.cpp install.

Match family reject keywords and aliases as whole path/name segments, not
raw substrings, so an unrelated word like edited, edition, or kontextual no
longer misroutes or hides a valid base image model, while supported edit
families (Qwen-Image-Edit, FLUX Kontext) still resolve. Mirror the same
segment matching in the picker task filter.

Route FLUX.2-dev native guidance through --guidance like the other FLUX
families rather than --cfg-scale. Reject native upscale requests that have
no input image. Read image header dimensions and reject over-limit inputs
before decoding pixels, so a crafted small-payload image cannot spike
memory. Reject an upscale that would shrink the source below its input
size. Validate the model_kind against the filename extension before the
GPU handoff. Estimate a local diffusers pipeline's size from its on-disk
weights so auto memory planning does not skip offload and OOM. Report
workflows: [txt2img] from the native backend status so the Create tab
stays enabled for a loaded native model. Clamp the outpaint canvas to the
backend's 4096px decode limit.

Adds regression tests for segment matching and kind/extension validation.

* Address further Codex findings on the image-workflows PR

- Persist the actual output image size in the gallery recipe instead of the
  request sliders: Transform/Inpaint/Edit derive the size from the uploaded
  image, Extend grows the canvas, and Upscale resizes it, so the sliders
  recorded (and later restored) the wrong dimensions for those workflows.
- Reject a remote '*-GGUF' repo loaded as a full pipeline (no single-file
  name) in validate_load_request, so the unloadable pick fails before chat is
  evicted rather than deep in from_pretrained.
- Only publish an image-conditioned from_pipe wrapper to the shared aux cache
  when the load is still current: from_pipe runs under the generate lock but
  not the state lock, so an unload racing its construction could otherwise
  cache a wrapper over torn-down modules that a later load would reuse.
- Verify the Windows CUDA runtime archive checksum before extracting it, like
  the main sd-cli archive, so a corrupt or tampered runtime is rejected rather
  than extracted next to the binary.

* [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>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-07-03 13:51:26 -03:00