Commit graph

304 commits

Author SHA1 Message Date
dependabot[bot]
2ba8756401
Bump setuptools in the python-security group across 1 directory
Bumps the python-security group with 1 update in the / directory: [setuptools](https://github.com/pypa/setuptools).


Updates `setuptools` from 80.9.0 to 83.0.0
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.9.0...v83.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 83.0.0
  dependency-type: direct:development
  dependency-group: python-security
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 06:40:01 +00:00
Michael Han
d74d03d350
Show release notes in the update popup, sourced from CHANGELOG.md (#7432)
* Show release notes in the update popup, sourced from CHANGELOG.md

The update banner only linked out to the online changelog, so there was no
way to see what an update contains before taking it.

Add CHANGELOG.md at the repo root as the source of release notes. Studio
reads it from the default branch, so editing the file updates the popup
without a release or rebuild, and falls back to the copy bundled in the
install when the repo is unreachable.

Notes are matched to one exact version. The popup asks for the version it is
offering and gets that section or nothing, so an older release's notes can
never appear next to a newer update. When there is no match the popup links
out to the online changelog instead.

The collapsed popup previews the top bullets with the leading sentence
highlighted; "Show release notes" expands the full notes in a scrollable
panel. Applies to both the browser and desktop banners, and the desktop
updater's own release body is used when CHANGELOG.md has no matching section.

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

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

* Address review: fence matching, nested bullets, BOM, updater notes field

Track the opening fence marker and length so a ``` sample inside a ````
block does not close it early and let the sample's heading be indexed as a
real release.

Preserve list indentation in the preview and take only top-level bullets, so
nested detail no longer consumes the four headline slots.

Strip a UTF-8 BOM before parsing. An editor on Windows can leave one on the
first line, which hid a section whose heading started the file.

Read `notes`/`pub_date` from latest.json in the manual Linux updater path,
with aliases for the older `body`/`date`. The workflow publishes Tauri's
field names, so the manual path's release body was always empty. Also loop
the preview tag strip until stable for CodeQL js/incomplete-multi-character
-sanitization; the value renders as text, so this is defence in depth.

* Address review: bare fence closers, HTML comments, underscores, notes URL

A closing fence must carry nothing after the delimiter, so a ```` line with
trailing text inside a ```` block is content rather than the end of it. Both
the parser and the preview extractor follow that rule now.

Skip headings inside HTML comments. A commented-out section is not rendered
by Markdown, so it must not be indexed as a release.

Strip only paired emphasis and park code spans first, so identifiers keep
their underscores: UNSLOTH_DISABLE_UPDATE_CHECK was previewing as
UNSLOTHDISABLEUPDATECHECK.

Prefer the caller's release URL over the API's generic changelog link, so the
desktop fallback points at the release page for the version being offered.

Look at the repo-root CHANGELOG.md before the packaging snapshot, and remove
the snapshot after build.sh, so an edited root file is never shadowed by a
stale copy.

Also nudge the notes container radius from 16px to 14px.

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

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

* Address review: comparison operators, hidden comments, remote failures

Require a name character after "<" when stripping tags. A bullet reading
"Support Python <3.15 and >3.9" previewed as "Support Python 3.9", because
the operators were consumed as if they were a tag.

Track HTML comments while collecting preview lines. A commented-out bullet
was previewed as a published change even though Markdown never renders it.

Report a remote lookup failure whenever nothing matched. The bundled
changelog cannot know a version newer than the install, so discarding the
error made an offline lookup read as "no notes were published". The hook now
treats a reported failure as its retryable error state.

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

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

* Address review: code-span delimiters, stale notes, retry past cached failures

Treat an HTML comment delimiter inside inline code as literal. A note reading
"Type `<!--` to begin a comment" put the parser into comment state, so every
release below it was swallowed into the entry above and became unfindable.
Applied to the preview extractor too.

Return no notes while the offered version differs from the fetched one. On
the render where the version changes, the hook still held the previous
release's notes, which the panel would show for a frame.

Let retry bypass a cached remote failure via a refresh flag on the endpoint.
Failures are cached for five minutes, so the visible Retry action could not
recover until the TTL expired. A cached success is still reused, so retries
cannot hammer the remote.

* Address review: CommonMark indentation, desktop release notes link

Allow up to three leading spaces on release headings and fences, and treat
four as indented code. An indented heading was unreachable and its notes were
appended to the release above, while an indented backtick line opened a fence
that swallowed later headings.

Link desktop release notes to the release page for the offered version on
every platform. The existing URL is built only in manual Linux package mode,
so in-app updates on macOS, Windows and AppImage fell back to the generic
changelog. The install button keeps using the manual URL.

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

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

* Address review: wrapped prose, autolinks, abbreviations in the preview

Accumulate contiguous prose lines into one preview item. A paragraph wrapped
across source lines renders as one block but previewed as three fragments,
which also ate the four-item limit.

Keep Markdown autolinks. <https://example.com/notes> was stripped as if it
were a tag, so "See <https://example.com/notes> for details" previewed as
"See for details".

Do not split the lead sentence at an abbreviation. "Supports several formats,
e.g. GGUF and Safetensors." highlighted only up to "e.g." and dimmed the
actual change; known abbreviations and single initials are skipped now.

* Address review: park code spans first, skip indented code blocks

Park code spans before any other inline transformation. Tags, links, images
and emphasis inside a span are literal, but the strips ran first, so "Use
`<button>` for actions" previewed as "Use for actions".

Skip lines inside an indented code block when collecting bullets. A "- pip
install ..." line in a four-space-indented block became the headline and
pushed out the real prose, though Markdown renders it as code. Continuation
lines of an open bullet are unaffected.

* Studio: skip raw HTML blocks when reading release notes

A <pre>, <script>, <style> or <textarea> block renders literally, so a
sample '## 9.9.9' heading inside one was indexed as a release and cut the
real section's body short. The preview had the same gap and listed sample
bullets as notes.

Both readers now track type 1 HTML blocks and skip their contents. Blocks
open only at the start of a line, so a tag named mid-sentence stays inline
text, and <details> is type 6 so its Markdown still parses.

* Studio: read HTML blocks the way CommonMark renders them

A fence inside a <pre> block was treated as a real fence, so the block's
closing tag was swallowed and every release below it disappeared. Raw HTML
state is now checked before fences, in both readers.

Type 6 and 7 blocks (<details>, <div>, a bare tag on its own line) run to
the next blank line, so a heading pressed against the opening tag is not a
release either. Type 7 cannot interrupt a paragraph, so prose followed by a
bare tag is unaffected.

Checked against a CommonMark reference: 20000 generated well-formed
changelogs now agree exactly on which headings are releases, and every
previewed note is text the renderer really shows.

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

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

* Studio: restore preview types dropped in the scanner refactor

The previous commit's refactor removed the Bullet and preview item
interfaces, so tsc -b failed and every job that builds the frontend
stopped there.

* Studio: fix release-notes preview and packaging review findings

Preview: a code span now closes on a run of the same length, so a note
containing backticks keeps them; thematic breaks no longer take a preview
slot; a quoted list is example output, so it stays out of the headline
bullets and is only used when a section has none of its own.

Popup: a failed lookup keeps the changelog link beside Retry, which the web
banner always offered before, and the desktop popup waits briefly for the
auto-auth token instead of recording a failure the user has to clear.

Packaging: the changelog snapshot is made by the build backend, so
python -m build, pip install . and sdist builds all ship the offline copy,
not only build.sh.

* Studio: scope the changelog fallback and hide staged sections

Installed, the levels above studio/ are site-packages, so a stray
CHANGELOG.md left there by another package outranked the bundled
snapshot. Those levels are now searched only when a checkout marker
(pyproject.toml or .git) is present, so a source checkout still serves
the editable file.

A section staged as only an HTML comment renders as nothing but was
reported as matched, leaving an empty notes surface. Notes that render
nothing now read as unpublished, so the popup links out instead.

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

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

* Studio: cover the remaining raw block forms and repository links

Parser and preview: processing instructions, declarations and CDATA are
literal like <pre>, so a sample heading or bullet inside one is no longer
read as a release. ATX headings now need a space or tab after the hashes,
matching CommonMark, so a pasted non-breaking space no longer truncates
the release above it.

Popup: the notes region follows the viewport and the card scrolls as a
backstop, so a window under about 430px high no longer pushes the title
and dismiss control off screen. Relative links in the notes resolve against
the repository instead of Studio's origin, where the renderer blocked them.

* Studio: reference-style images, empty previews and version queries

Reference definitions now resolve against the raw host when the label is
used as an image, so ![alt][arch] loads the file instead of its HTML page
on GitHub. Labels are matched the way CommonMark compares them, and a
reference written inside a fenced block does not count.

Notes that preview as nothing, such as a lone command block, no longer
leave an empty muted strip in the collapsed popup; expanding still shows
them. A version query that cannot parse is rejected up front rather than
looked up and reported as no notes.

* Studio: Markdown scanning fixes across the release notes path

Code spans are now scanned rather than matched by pattern, so a run of
backticks closes only on a run of the same length. The preview and the
link resolver share that scanner, so a link inside `a``b [x](y.md)`
stays literal in both.

Also: a closing fence may carry only spaces or tabs, so a delimiter with a
non-breaking space after it stays code in all three scanners; escaped
parentheses in a link target resolve to the literal path instead of being
mangled; the collapsed preview decodes entities the way the expanded view
renders them, while code spans stay literal; and release notes are fetched
through authFetch so an expired access token is refreshed and retried.

* Changelog: real 2026.7.5 notes, led by the AMD release

Fills the section the popup reads with the actual headline changes, so the
collapsed preview shows real content instead of placeholder notes. Leads with
AMD support and covers the 23 July update: RDNA2 and Gorgon Halo, Strix Halo
detection, RDNA4 and ROCm failure recovery, 2x faster unified memory loading,
whisper.cpp dictation, and rollback environment cleanup.

* Studio: fix release-notes text handling found by adversarial testing

Line endings are normalised first: a CRLF body from the desktop updater no
longer hides fences, so a code sample cannot become a headline bullet, and
lone CR text splits into bullets.

Preview: reference links and images render as their text, a definition line
renders as nothing, parentheses in a destination no longer truncate the
sentence, escaped punctuation stays literal, and a fence indented into a
list item is treated as the block it is.

Links: a badge resolves both its image and its outer link, indented code and
code spans that cross a line are left alone, a definition cannot interrupt a
paragraph, and image alt text no longer decides a label's host.

Also: an escaped backtick cannot open a code span, park sentinels in the
source cannot swap content, two in-flight requests for one version resolve
in order, and repeated bullets no longer share a React key.

Comment scanning no longer rescans code spans per delimiter and span lookup
is a binary search: the worst inputs measured drop from 96ms to 1ms at the
20k cap, and from 544ms to 15ms at 200k.

* Studio: parser and fetch fixes found by adversarial testing

A comment marker written in prose no longer swallows the rest of the file.
Only a comment that starts a line opens a block; one written mid-sentence is
inline HTML and hides its own line at most. This was the worst case found:
a single stray marker made every release below it unreachable and served
their notes under the newer version's heading.

Also in the parser: a closing delimiter takes its whole line, so a heading
glued after it is not a release; an exact heading is never shadowed by a
zero-padded one; setext headings are release boundaries; any heading, rule
or definition ends a paragraph; and the code-span guard is a linear scan
rather than a backtracking pattern, so 20k backticks parse in a millisecond
instead of over a minute.

Fetching: one deadline for the whole response with chunked reads, so a
trickling server cannot hold a worker for minutes, waiters give up instead
of queueing behind a stalled fetch, and identity encoding is requested so a
compressing proxy cannot produce mojibake notes. Truncated notes close an
open fence.

UI: images and the renderer's own link dialog are held inside the card,
which the shared preview's blanket max-width reset had let escape, and only
the notes region scrolls so the dismiss control stays reachable on a short
viewport.

The developer update override no longer beats the documented opt-out, and
its value has to parse as a version.

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

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

* Studio: CommonMark paragraph and block rules across the notes path

Setext detection now requires plain paragraph text above the underline. A
list item followed by --- is a list and a rule, not a heading: reading it as
one discarded the bullet and every note after it.

A backtick fence whose info string holds a backtick is not a fence, so such
a line no longer swallows the releases below it in the parser, the preview
and the link resolver.

Preview: only an ordered list starting at 1 interrupts a paragraph, an
unresolved reference keeps its brackets, a comment written mid-sentence
hides its own line at most instead of the rest of the document, a raw block
closer takes its whole line, and a code span closer after a backslash still
closes, since escapes do not apply inside a span.

Links: raw HTML blocks are literal, an escaped opener is not a link, and a
definition under a heading is a definition.

The overlay stack is capped to the viewport and both overlays can give up
height, so a long download list no longer pushes the update card's title and
dismiss control off screen.

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

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

* Studio: desktop notes by backend version, desktop stack cap, fetch budget

latest.json now publishes the backend release the desktop build pins, and
both desktop paths carry it: the manual metadata check through Rust, and the
in-app updater through the raw metadata it already exposes. The popup looks
release notes up by that version, so desktop stops asking CHANGELOG.md for
an app SemVer it never contains and falling back to the generic installer
text. Metadata without the field still parses and behaves as before.

The desktop overlay stack is capped to the viewport like the browser one,
since the download panel shares it and the card's own cap cannot see a
sibling.

The fetch budget now bounds each read, not just the gap between reads. Slow
headers followed by a slow body held a worker for 5.6s against a 3s budget;
it is 3.0s now, and a timeout is reported as one.

* Studio: keep list-nested headings out of the release index

A `## <version>` heading indented to a list item's content column is inside
that item in CommonMark, not a release boundary. Reading it as one truncated
the real release and indexed a version that does not exist.

parse_changelog now tracks the open list items by the column their content
starts at, and only counts a heading left of that column. Supporting rules,
each checked against markdown-it (commonmark preset): a marker needs
whitespace after it, so `2.0` stays a setext version; an item interrupts a
paragraph only when it has content, and an ordered one only when it starts at
1; an empty item takes one blank line; a dedented fence, break or heading
closes the item; and `- ## 2.0` is a heading inside the item.

* Studio: whole-paragraph setext headings, uppercase declarations, escaped marks

Three CommonMark conformance fixes on the notes path, each checked against
markdown-it (commonmark preset).

A setext heading is the whole paragraph above the underline, so a heading that
wraps kept its version only on the first line while the parser read the last:
`2026.7.5 - Release` over `July 25` left that release unindexed and its notes
unreachable. The parser now tracks every line of the open paragraph, including
lazy continuations, and stops at whatever really interrupts it: a quote marker,
a bullet, or an ordered marker starting at 1.

A type 4 HTML block needs an uppercase letter after `<!`, so prose mentioning
`<!note` was hiding every release below it until the next `>`.

In the link resolver, `\![alt][label]` renders as a link, so its definition
resolves to the file's page on GitHub rather than the raw-content host.

* Studio: the preview needs the uppercase declaration rule too

The backend parser stopped treating `<!note` as an HTML block, but the
collapsed preview still did, so prose mentioning one emptied the preview of
every bullet below it while the expanded notes rendered them. A shipped test
now pins the two to the same rule.

* Treat an empty HTML comment as closed and always release the changelog fetch flag

<!--> and <!---> are complete comments in CommonMark: the closer overlaps the
opener, so searching for --> past the opener never found it and the scanner
stayed in comment state for the rest of the file. An empty comment used as a
section marker hid every release below it, in both the backend parser and the
frontend preview.

get_remote_changelog cleared its single-flight flag only after except Exception,
so a BaseException stranded it and every later caller waited out the full
deadline for the life of the process. Move the release into a finally.

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

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

* Compare resolved changelog paths instead of a hardcoded checkout name

The ordering assertion matched the string suffix /unsloth/CHANGELOG.md, so it
raised StopIteration in any checkout not literally named unsloth, and on
Windows the separator is a backslash so the suffix never matched there either.
Both are unrelated to the ordering under test. Verified failing on
ubuntu-24.04, macos-14-arm64 and windows-2025 alike, and passing after.

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

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

* Scan backtick runs once instead of rescanning the suffix per opener

Every unmatched opener rescanned the rest of the line and the outer loop then
advanced by a single run, so a line of runs of 1, 2, 3 ... backticks was
quadratic: 321 KB took 7.688s, and release notes are reparsed on every popup
request, so one malformed remote changelog could tie up backend workers across
installed clients. Collect the runs in one pass and walk a cursor per run
length, since a length that runs out of partners stays out. Same 321 KB now
takes 0.013s and 5 MB takes 0.205s. Verified identical output against the old
implementation on 30000 randomized lines.

* Read type 6 and 7 HTML containers in the link resolver too

The resolver masked only type 1 blocks (pre, script, style, textarea), while
the backend parser and the collapsed preview already apply the type 6 and 7
rules, so the three disagreed on the same notes. A <details> or <div> with no
blank line inside is a type 6 block whose contents render verbatim, so two
things went wrong there: a relative link was rewritten into text the reader
sees literally, and a fence inside the block was taken for a real fence, which
silently stopped every link below it from resolving. A blank line, not the
closing tag, ends these blocks, so the common '<div align="center">' followed
by a blank line still holds Markdown and still resolves.

* Mask comments before fences, split only on Markdown line endings, stage the snapshot

Three separate reports, all confirmed against head.

The link resolver tracked no comment state, so a fence delimiter hidden inside
an HTML comment was read as a real fence. The fence then stayed open and every
visible line below was classified as code, so none of its links resolved: one
commented-out draft containing a stray backtick run silently broke the rest of
the notes. Comments are masked now, but only outside a fence, since fenced
content is literal and a comment opener in it is not one. Commented ranges join
the code spans, so a link the reader cannot see is not rewritten either.
Verified with 9 cases under node; 2 fail on the previous file.

str.splitlines also breaks on U+2028, U+2029, NEL, vertical tab and form feed,
none of which end a line in CommonMark. A separator sitting in prose ahead of
"## 9.9.9" made the parser index a release that renders nowhere and truncate
the notes above it: measured, the version list went from 2.0, 9.9.9, 1.0 to
2.0, 1.0 and the 2.0 body stopped being cut at the separator.

The build wrote the snapshot beside the checked-in sources, so a PEP 517 build
against an immutable checkout (Nix, Bazel, a read-only container mount) raised
PermissionError before build_py started and produced no wheel at all. The
source-tree copy is best effort now and the wheel takes its copy from the
staging directory. Reproduced both ways against a read-only package dir.

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

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

* Use the backend's heading and quote marker rules in the preview

An ATX heading needs an ASCII space or tab after the marker, which is exactly
what _HEADING_PATTERN requires. The \s class also matches a non-breaking space,
so prose beginning "## Important change" with one was classified as a heading
and discarded by collectBullets, and a prose-only release then had no collapsed
preview at all rather than a wrong one.

A blockquote marker takes at most three leading spaces, like every other marker
in this file. Accepting any run let an indented code sample containing
"> - sample output" shed its indentation and enter the collector, so a release
with no real bullets showed code as its summary.

Both reproduced under node against the real module: the two cases fail on the
previous file and pass now, with a real heading, a real quoted bullet and an
ordinary bullet unchanged.

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

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

* Collect preview reference labels only from lines that can be definitions

A definition-shaped line inside an indented code block or a deep fence is
literal text, so CommonMark leaves a later "[Beta] support" unresolved with its
brackets showing. The pre-scan ran over every line regardless, so the label was
recorded and toPlainText stripped the brackets: the collapsed preview claimed a
resolved reference the expanded notes do not have.

It now skips the same code the collector pass skips. A real definition takes at
most three spaces of indentation, so the indent test cannot reject one, which
the second case checks. Reproduced under node: the indented-code definition
resolved "Beta support" before and keeps its brackets now.

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

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

* Let a document-level HTML block close an open list item

CommonMark HTML blocks of types 1 to 6 interrupt a paragraph, so a "<div>" to
the left of an open list item closes it and a following one-to-three-space
indented "## 2.0" is a real document heading. Two things stopped that: the block
opener was blanked before the list tracker saw it, so it read as a blank line,
and _may_be_lazy treated it as ordinary text that could continue the item's
paragraph. The item therefore stayed open and the release below the block was
swallowed entirely.

The opener's indentation is now taken before it is hidden, the way a fence
opener's already was, and an HTML block opener is no longer a candidate for lazy
continuation. Type 7 cannot interrupt a paragraph and is deliberately excluded,
since after_paragraph is the only state this helper is asked about.

Measured on the reported shape: the version list went from 3.0, 1.0 to
3.0, 2.0, 1.0. The test also pins the two cases that must not change, an
indented heading genuinely nested in an item and an ordinary lazy continuation,
both of which still suppress the heading.

* Let the download panel shrink inside the capped overlay stack

The bottom-right stack is capped to the viewport, but a flex item defaults to
min-height:auto, so the download panel's outer wrapper could not shrink below
its own content. min-h-0 had been added to the nested panel and not to this
wrapper, so on a short viewport the cap was absorbed by the update card, whose
header and actions are fixed, instead of by the download list, which scrolls.

Only the shared-stack branch takes it. Standalone is positioned fixed and is not
a flex item at all.

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

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

* Tighten release notes comments

Shorten the comments and docs added with the update popup release notes
so each explains its line in as few words as possible. Comments only, no
behaviour change.

* Measure release-notes indentation from the container

CommonMark measures a block's indentation from its container, not from the
left margin (spec 0.31.2 sections 4.4 and 5.2). The three changelog scanners
measured from the margin in different places, so they disagreed with the
renderer and with each other.

Under "- Details:" the content column is 2, so a four-space line is two
columns in: a paragraph holding a link. The link resolver read it as an
indented code block and left the destination relative, so it resolved against
Studio's own origin instead of the repository.

At document level the same four spaces really are code, and a top-level
bullet is not indented enough to continue the block. The preview promoted an
indented line that looked like a fence opener to a list-contained fence, so
with no later closer every bullet below it was skipped and the collapsed
popup lost its summary.

A fence is scoped to its container too: with no closing line it runs to the
end of the containing block, not the end of the document (section 4.5). A
dedented "## 2.0" closes the list item the fence sits in, so it is a real
release heading. Document-wide fence state kept the block open, so one
missing closing line hid every release below it.

Both frontend scanners now read their list columns from one module ported
from the backend's own tracker, which keeps the three in step.

Two smaller fixes ride along. A release body written as a GFM table rendered
as a grid but previewed as its raw "| Change | Detail | | --- | --- |"
delimiters, so table rows are now dropped from the collapsed summary the way
a code block already is. The comment scanner restarted its code-span search
at the first span for every opener, so a line of N spans and N openers cost N
squared: a 203 KiB line, well inside the 2 MiB the fetcher accepts, took 10.9s
and now takes 41ms.

Differential fuzzing against a CommonMark reference implementation puts the
parser's heading mismatches at 11 of 14275 documents, down from 617, and the
link resolver's at 147 of 6000, down from 217.

* Keep Retry reachable when the release notes fetch fails

The panel took fallbackMarkdown for every response that did not match, error
included, so markdown was always truthy on desktop and the error branch that
carries the Retry button was unreachable. The fallback there is the updater's
static install blurb, not this release's notes, so a transient failure showed
"Download the Apple Silicon .dmg" where the notes should be, with no way to ask
again until the cache expired.

The hook already separates the two: a reported failure is error and retryable,
"no section for this version" is ready and is not. The fallback now applies only
to the second, which is the case its prop documents.

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

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

* Scope an unclosed comment to its block and end a release on a bare ##

Two CommonMark rules the changelog scanners read too strictly.

An HTML block only opens when the line itself begins with a comment marker
(spec 0.31.2 section 4.6, type 2). One written mid-sentence is inline raw HTML
and, unclosed, is ordinary text. The link resolver carried the open state to
every line below instead, so a note reading "- Type <!-- to begin a comment"
masked the relative links under it and they resolved against Studio's own
origin rather than the repository. maskComments now separates the block form
from the inline one and skips an opener sitting inside a code span, the way
_strip_comments and stripCommentSpans already do. The spans are scanned only
once an opener turns up, so a line without one costs what it did before.

An ATX heading's opening sequence may also be followed by the end of the line
(section 4.2), so a bare ## is an empty level-two heading. Both heading
patterns required whitespace after the hashes, so everything below such a line
stayed inside the release above it and the popup could show unrelated notes
under that version. An empty heading carries no version, so it ends the release
without indexing one of its own.

Differential runs against markdown-it-py: section bodies 7769 to 0 mismatches
over 36069 generated documents, comment-heavy link resolution 705 to 53 over
6000, and previews leaking a bare marker as headline text 22484 to 0 over
40000. The residual link cases are all one shape, a comment block opened inside
a list item that outlives the item, which the fence tracker scopes and the
comment tracker does not, in all three scanners alike.

* Give a hidden comment its own column and balance link destinations

A comment is an HTML block, so one written at the margin under a bullet is not
indented enough to continue that item and closes the list. All three scanners
blanked the line before list tracking saw it, which reads as a blank line and
leaves the item open, so a release heading below it looked like nested item
content and the new release merged into the one above. A hidden line now keeps
its own column through _hidden_structure and hiddenStructure, and only its
column, since the text a comment or a raw block hides is not Markdown and must
not open a list of its own. A line inside a block already open is that block's
content and still keeps nothing.

A link destination may hold parentheses while they balance, so [x]((draft).md)
points at (draft).md. The resolver stopped at the first paren, matched an empty
destination and left the markdown alone, so the link resolved against Studio's
own origin. The balanced form counts only while a closing paren or a title
still ends the link, so the stray paren in [x](a(b.md) stays the closer the way
CommonMark reads it rather than being swallowed into a link across lines.

* Scope paragraph state to the container a line is written in

Two lines the parser read as block starts are lazy paragraph text, so the
list they were written under closed early and the heading indented to the
item's content column was indexed as a release the renderer never shows.

A setext underline may never be a lazy continuation line (spec 0.31.2
section 4.3), so `===` written left of an open item is more of that item's
paragraph. Rejecting every underline-shaped line ended the list there. A row
of three dashes is still a thematic break, which does end it.

Lazy continuation runs the other way too: a marker written outside a
blockquote is not text of the quote's paragraph, so `2. item` under `> quote`
opens a list even though an ordered marker past 1 may not interrupt a
paragraph. Paragraph state is now scoped to its container: a quote line
leaves open only the quote's own paragraph, an underline needs one in its own
container, a definition ends one only when there is none to continue, and a
line four columns past its container is code, which may not interrupt.

The frontend pair reads the same tracker, so both scanners now carry the
quote state and a fence inside a list item ends with the item in the preview
the way it already did on the backend.

Measured against markdown-it-py (CommonMark 0.31.2) over 264k generated
documents: 3368 sections now match the renderer, none regressed, and every
list and quote corpus is exact.

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

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

* Read a fence and an HTML block from the container it opens in

A block is measured from its container and not from the left margin (spec
0.31.2 sections 4.5 and 5.2), but the link resolver's fence, raw HTML and type
6 expressions all started at the margin, so a fence behind a quote marker and
one three columns under a nested bullet opened nothing. The sample inside was
then read as prose, and a relative link written in a code block or a details
body was rewritten into text the reader is shown verbatim. Matching runs of
backticks hid some of it by accident, since the code span scanner pairs them
across lines, but a tilde fence, a closer of a different length and every HTML
block went through. Each line is now read from the container it is written in,
which the list tracker already knew, and a block is scoped to that container
the way a fence inside an item already was: a line to the left of the item, or
outside the quote, ends the block along with it, and a bare quote marker is
the blank line that ends a type 6 block.

A destination holds parentheses while they balance, and a path may nest them,
so [x](((draft)).md) points at ((draft)).md. One nesting level was all the
expression allowed, so anything deeper fell through to the plain form, matched
an empty destination and left the link resolving against Studio's own origin.
The pairs are unrolled to the 32 levels cmark counts, and the balanced form is
still gated on a closer following it, so the stray paren in [x](a(b.md) stays
the closer the way CommonMark reads it rather than inventing a link across
lines.

Measured against markdown-it-py (CommonMark 0.31.2) over 66k generated
documents, comparing the rendered HTML rather than the destinations alone:
7286 documents in the parenthesis corpus and 313 in the container corpus now
match the renderer, and the link and definition corpora are unchanged. One
container document regresses, where closing the HTML block correctly exposes
an unrelated gap of its own: a link reference definition still leaves a
paragraph open, so the indented line below it reads as prose rather than as
code. The list tracker still matches the backend on every step, the repo's own
CHANGELOG resolves identically, and the pathological inputs measure the same.

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

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

* Read a block from the item its marker opens, and let a comment reach its paragraph

Four things the three changelog scanners read differently from a renderer.

A fence written straight after a list marker is the item's own first content,
measured from the column that content starts, so "- ```md" opens one. All three
scanners matched the whole line and saw nothing, so the code sample below it was
prose: the resolver rewrote a destination the reader sees verbatim, and the
preview offered the info string as a headline bullet. A shared itemContent /
_item_content reads past a marker that really opens an item, capping the padding
the way the list tracker caps it so an over-indented line is still indented code.
An HTML block opener is read the same way, and its marker survives into the
structural line so the item it opens is still tracked.

An HTML block holds no lazy continuation line, so one opened on an item's
continuation line ends where the item does, exactly as a fence there already
did. The backend and the preview ended it only on a blank line, so it ran past
the item and swallowed the next release heading, which made those notes
unreachable and dropped every bullet below it from the collapsed popup. A raw
block inside an item ends on a blank line too, which is where cmark puts it.

A comment written mid-sentence is inline raw HTML belonging to the paragraph
around it, so its "-->" may arrive on a later line of that same paragraph. Ending
it at its own line left a backtick inside it pairing with a real one below, which
hid a following link from the resolver, and left the preview quoting text the
popup body does not show. A shared commentClosesBelow answers whether the closer
arrives before the paragraph breaks; where it does not, the opener stays the
ordinary text a renderer shows, so a note that merely mentions "<!--" still hides
nothing.

Only ASCII punctuation is escapable, so the backslash in "docs\alpha.md" is a
character of the path. Dropping every backslash rewrote it to a path that does
not exist, and a URL parser reads what survives as a separator, so a Windows or
namespaced path pointed at the wrong file either way. The destination expression
now escapes only punctuation, which also means a space still ends a destination:
"[x](a b.md)" and "[x](a(b.md)" are not links, so their paths are left alone
rather than half-rewritten. A destination that runs out of line still resolves,
since its closer is on the line below.

Fuzzed against markdown-it (CommonMark 0.31.2) over 20k-document corpora, with
the whole rewritten document rendered and compared, not just its destinations.
Release headings: 117 to 16 on containers, 88 to 10 on markers, 17 to 12,
nothing new anywhere. Link destinations: 8823 to 104 on markers, 114 to 98 on
comments, nothing new. Whole-document renders: 9271 to 220, 5116 to 245, 1265 to
671. The Python and TypeScript list trackers still agree over 26861 steps, and
itemContent and hiddenStructure agree over another 6335. 321 KB of unmatched
backticks still measures the same.

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

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

* Let a definition follow a definition, and read a comment from the item it opens in

Three CommonMark conformance fixes in the changelog scanners.

A link reference definition is a block of its own that may not interrupt a
paragraph, so it opens none either: definitions are allowed to run
consecutively (spec 0.31.2 section 4.7). The link resolver counted one as
paragraph text, so every definition after the first fell outside the set of
lines a definition may start on and kept its relative destination, which then
resolved against Studio's own origin. The backend already read the line this
way.

The guard asking whether a `-->` is reachable from an opener read any line
whose first character was punctuation as the start of a new block. A `-->`
written on a line of its own is how a multiline comment is ordinarily closed,
and a wrapped line may open with emphasis, so neither counted as more of the
paragraph carrying the comment. The comment never closed and the collapsed
popup showed the author's internal note to the reader. It now tests for a
block that may actually interrupt a paragraph.

A comment is an HTML block too (section 4.6, type 2), so one written as a list
item's first content opens inside that item exactly as a fence written there
does. All three scanners looked for the opener at the margin of the line as
written, so a marker in front of it hid the block: the resolver rewrote a
destination inside raw HTML, which Streamdown then shows the reader as a
literal URL, and the preview quoted the hidden note back at them as though the
bullet were Markdown. The opener is now read from the item's content, the
marker survives into the structural line so the item it opens is still
tracked, and the block is scoped to that item the way a fence there is.

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

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

* Tighten the release notes comments without losing the reasons they record

---------

Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-28 21:26:43 -07:00
Daniel Han
fa95054399
Gate the torchcodec audio extras to platforms that have a wheel (#7587) 2026-07-28 20:56:11 -07:00
Leo Borcherding
411cb86d62
amd: require bitsandbytes>=0.50.0 in the amd extra (fixes ROCm 4-bit NaNs) (#7535)
* amd: require bitsandbytes>=0.50.0 in the amd extra

bnb <= 0.49.2 NaNs at decode shape on every AMD GPU. The ROCm 4-bit GEMV
fix (bnb PR #1887) first ships in 0.50.0, on PyPI since 2026-07-24, so the
old >=0.49.1 floor could still resolve the broken range.

Mirrors the same change made on the pip release branch in #7278.

* amd: cite the 0.50.0 ROCm work accurately in the bnb floor comment

The comment credited bnb PR #1887 as "the ROCm 4-bit GEMV fix" for every
AMD GPU. #1887 decouples blocksize from warp size and fixes a hardcoded
warp size of 32 in kgemm_4bit_inference_naive, which is a CDNA problem by
construction. The RDNA-side work is #1979 (fused 4-bit SIMT GEMM) and
#2012 (RDNA3/4 workgroup resonance). All three first ship in 0.50.0, so
the >=0.50.0 floor is unchanged; only the justification was wrong.

* amd: raise the installer bitsandbytes fallback floors to 0.50.0

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

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

* amd: stop reporting the bitsandbytes PyPI fallback as broken

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

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

* Tighten AMD bnb floor comments

* Keep the amd extra citation and the AMD install guide reference

* amd: do not promise aarch64 a ROCm 4-bit backend it never gets

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

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

* amd: fall back to the PyPI bitsandbytes floor on Windows ROCm too

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

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

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-28 18:12:26 -07:00
Daniel Han
1781770bee
Studio: detect an interrupted dependency install instead of launching a backend that cannot import (#7492)
* Studio: detect an interrupted dependency install instead of launching a backend that cannot import

An installer killed part-way leaves a venv with a working CLI but without
studio.txt's dependencies. Nothing recorded that, so three separate places all
reported it healthy:

- the desktop preflight probed only `unsloth -h` (typer + rich) and a hardcoded
  desktop-capabilities dict, neither of which touches studio.backend, so it
  returned ManagedReady and spawned a backend that died on `import structlog`;
- setup.sh's fast path compared the installed unsloth version against PyPI,
  which matches on a half-built venv because unsloth is installed early, so
  `unsloth studio update` printed "up to date" and repaired nothing;
- start_managed_repair calls that update and then re-checks with the same blind
  probes, so Repair reported success without fixing anything.

install_python_stack.py now clears a completion manifest before the dependency
pass and writes it only after the final step. `unsloth studio verify-install`
and desktop-capabilities' new studio_install_ok field read it, the preflight
turns a false answer into ManagedStale so auto-repair runs, and setup.sh /
setup.ps1 gain an escape hatch next to the existing anyio one.

Separately, the wheel ships studio/ and studio.backend* but declared none of
their dependencies, so `unsloth train`, `export`, `chat`, `inference` and
`studio` all ended in a rich traceback after a plain pip install. structlog is
the only hard module-level import that chain reaches once starlette's
annotation-only import moves under TYPE_CHECKING, so it becomes a core
dependency and the rest of the server stack becomes a [studio] extra mirroring
studio.txt. The CLI import sites now report missing dependencies as a sentence
with two remedies.

Fixes #4701, #5260, #7147

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

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

* Match the trimmed comments merged on the pip branch

* Put the install manifest in the preflight fingerprint for PR #7492

The capability cache keyed the venv on pyvenv.cfg, uv.lock, requirements.txt,
the interpreter and site-packages/unsloth_cli/commands/studio.py, none of which
a repair touches when it only reinstalls studio.txt. So an entry cached while
the install was healthy stayed valid after the manifest was dropped, and the
probe returned Ready on exactly the half-built venv this is meant to catch.

* Address the review findings on PR #7492

Fail the install when the completion manifest cannot be written, instead of
exiting 0 without the record every later check requires, which is a repair
loop by construction.

Compare the version of the package the manifest names, so `studio update
--package X` does not read as a permanent version change.

Read the manifest from the venv that owns it when the CLI runs outside the
managed venv, and drop the dependency verdict in that case: the walk ran
against the wrong interpreter and says nothing about that venv.

Name the import that actually failed. `unsloth train` reaches torch through
the same guard, and the studio extra does not carry it, so recommending that
extra alone left the command failing in the same place.

* Declare click, which typer stopped providing, for PR #7492

unsloth_cli/commands/start.py imports click at module scope and
unsloth_cli/__init__.py imports that module, so every unsloth command needs
it. typer carried click through 0.19 and dropped it in 0.27, and the declared
floor is typer>=0.12.0, so a fresh resolve gets no click. On the published
wheel it still arrives because huggingface_hub requires click<9,>=8.4.2, which
is luck rather than a declaration. A wheel built from this branch's
dependency list has neither, and every command dies at import.

Verified: before, `unsloth --help` on a fresh venv raised ModuleNotFoundError
for click; after, it exits 0. The drift test now covers it.

* Keep a running backend from the previous app version manageable

The manageability bump gated two unrelated things through one constant. For
the managed CLI probe 2 is right: a CLI reporting 1 cannot answer
studio_install_ok. For a RUNNING backend it is wrong, because a process
already started cannot change what it reports, so bumping studio/backend/main.py
in lockstep does not help one the previous app version spawned.

That backend is proven ours by root id and ownership token, but
lifecycle_control_block_reason returned Unmanageable, and that branch never
calls adopt_verified_backend. has_owned_backend() stays false, so Repair falls
into block_external_conflict, which finds the same process and refuses: the app
could no longer stop a backend it owns the token for. The same regression in
backend.rs turned a terminal-launched same-root server from AttachedReady into
ExternalConflict.

Split the constant: DESKTOP_BACKEND_MANAGEABILITY_VERSION = 1 for the two
live-backend probes, DESKTOP_MANAGEABILITY_VERSION = 2 for the CLI probe. Every
real gate (protocol, auth, ownership, desktop-login, MIN_DESKTOP_BACKEND_VERSION)
is untouched, so an old backend still reaches OwnedStale, adopt, stop, repair.

Also stop the installer when the stale manifest cannot be removed. Windows
raises on a read-only or locked file, and the pass would then run behind a
marker that still names this version and these digests, so a run killed
part-way would verify as complete.

* Answer for the managed venv, not the one the CLI happens to run in

The guard matched ModuleNotFoundError.name, an import name, against
missing_requirements(), which returns distribution names. So a missing PyJWT
printed 'pip install jwt', and jwt, docx and fitz are each a real but unrelated
PyPI project (fitz is a neuroimaging workflow tool), so following the advice
installed the wrong package and left the backend just as broken. Map the import
to its distribution before deciding, and never offer the import itself.

install_state() verified the caller's own prefix. The wheel ships studio/, so a
CLI installed outside the managed venv always finds its own copy of the helper
first, and a healthy managed install reported studio_install_incomplete with a
missing list copied from the wrong venv. Selecting the root is not enough:
_installed_version() reads the running interpreter and req_root defaults to the
caller's studio.txt, so both checks still answered for the wrong venv. Hand
verify_install() that venv's own metadata, enumerated through
Distribution.discover(context = ...path), which does not fall back to sys.path.
The candidate order is untouched, so shadowed-tree detection is unchanged.

setup.ps1 replaces pip, torch and triton before install_python_stack.py runs,
so the manifest it drops is not dropped before the first mutation. A run killed
in between kept a marker that still verifies while torch was half-replaced;
drop it at the top of the dependency pass instead. setup.sh is unaffected, the
stack is the first thing its pass runs, and a test now pins both.

pip uninstall rewrites nothing that was fingerprinted, and cache_matches
re-reads the cached studio_install_ok rather than re-checking, so a venv that
lost a studio.txt package kept being served the healthy verdict. Fold a sorted
hash of the installed dist-info names into the marker hash.

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

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

* A missing manifest helper is a torn install, not an old one

studio/install_manifest.py ships in the same wheel as _studio_deps.py, so
nothing legitimately has one without the other: a CLI predating both never
reaches this code, and the desktop already calls such a CLI stale on
desktop_manageability_version.

Returning ok=true there reported a healthy install for a tree the package
update had half replaced, and the preflight then launched a backend whose
own run.py could be just as absent. Report it incomplete so repair runs.

* Tighten comments across the install-detection changes

* Validate Studio dependency readiness

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-07-28 10:57:20 +02: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
d49d23ffab Update pyproject.toml 2026-07-23 02:31:21 -07:00
Daniel Han
4fedb51b73
unsloth start/run: tool-call flags, positional model, and grouped help (#7328)
* unsloth start/run: tool-call flags, positional model, grouped help

Expose the existing tool-call controls as first-class CLI flags on both
unsloth run and unsloth start, add positional model detection with a GGUF
quant default, and group --help into rich panels.

Flags (unsloth run): --enable-tool-call-healing/--disable-tool-call-healing
(default on), --enable-tool-call-nudging/--disable-tool-call-nudging
(default on). Resolved before any re-exec and written to the existing env
controls (UNSLOTH_DISABLE_TOOL_CALL_HEALING, UNSLOTH_TOOL_CALL_NUDGE) so the
in-venv server reads them at import; an omitted flag respects a value the
parent already set.

Flags (unsloth start): --enable-tools/--disable-tools (default off, passthrough),
plus the same healing/nudging flags (default on). start conveys them to the
auto-started run via the child env and the tools flag, so it stays correct even
if run re-execs into an older Studio venv.

Positional model: a leading org/name(:variant) token routes to --model when
--model is absent, without stealing an option value or an agent passthrough arg.
A bare GGUF repo with no variant defaults to UD-Q4_K_XL for the unsloth namespace
and Q4_K_M elsewhere, applied only on the fresh auto-serve path so attaching to a
loaded model never reloads.

Help is grouped into rich panels (Model / Server / Session for start; Model /
Server and network / Tool calls / Advanced for run) so --help reads cleanly.

Adds unit coverage for the helpers, the start command-and-env forwarding, the
positional/quant defaulting, and the run env resolution.

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

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

* Positional model: reuse _is_hub_model_id so local dirs and paths are not stolen

Route a bare org/name positional to --model only when it resolves as a hub id
(via the existing _is_hub_model_id, which rejects local paths and existing
dirs), so an OpenCode project dir like owner/repo is left for the agent. Apply
the same guard to the auto-serve GGUF quant default so a local -GGUF path is
not forced to a quant it may not contain.

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

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

* unsloth start: typer floor, drop redundant GGUF quant default, respect inherited tool-call env

- Require typer>=0.12.0. The rich_help_panel options added here crash at import
  on typer<0.6, and the dependency was previously unbounded.
- Stop forcing a default GGUF quant for a bare org/name-GGUF on auto-serve. The
  server's own quant preference already picks UD-Q4_K_XL for Unsloth uploads and
  Q4_K_M otherwise, and falls back when that exact quant is missing, so forcing a
  fixed variant broke external repos that only publish Q5_K_M/Q8_0.
- Make the healing/nudging start flags tri-state so an omitted flag keeps an
  operator's inherited UNSLOTH_DISABLE_TOOL_CALL_HEALING / UNSLOTH_TOOL_CALL_NUDGE
  instead of overwriting it with the start defaults.

* Fix start passthrough and inherited tool settings

---------

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-23 01:44:57 -07:00
Daniel Han
968e6230a0
Unsloth start: add local subagents for Claude Code, Codex, OpenCode and Pi (#7326)
Bring the local-subagent support onto main. The original change (#7316) merged
into the stacked pr/daniel-unsloth-start-audit branch rather than main, and #7313
reached main via squash, so these files never landed on main.

Adds --as-subagent for claude, codex, opencode and pi: the parent agent keeps its
own cloud model while a locally served GGUF is registered as a delegated subagent,
using ephemeral per-session config that never touches the user's real agent config.
2026-07-22 04:34:58 -07:00
Daniel Han
691aebd567 AMD 2026-07-20 07:07:30 -07:00
Daniel Han
a53aae046d Versioning 2026-07-15 11:20:22 -07:00
Wasim Yousef Said
1bf3509fea
Fix agent workspace isolation and Hermes one-shot resume (#7103)
* Fix coding agent workspace and resume handling

* Handle attached Hermes flags and OpenClaw paths

* Add Codex model metadata catalog

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

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

* Fix Codex reasoning summary metadata

* Preserve Hermes hook approval on resumed one-shots

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-15 15:06:03 +02:00
Daniel Han
38ea267124 Versioning 2026-07-08 06:51:58 -07:00
Daniel Han
10d8f985a2 Versioning 2026-07-07 06:19:25 -07:00
Daniel Han
f62c26e63d
Fix stale xformers and flash-attn wheel URLs (#4213)
Co-authored-by: Jeffrey Cruz <jeffrey.cruz@me.com>
2026-06-29 18:45:26 -03:00
Yuwen Hu
27b66b2efe
Fix outdated triton-xpu 3.7.1 sha256 hashes in intel-gpu-torch2120 extra (#6629) 2026-06-29 18:38:30 -03:00
Nilay
e5cf956601
Studio: shareable per-checkpoint preview links (#6486)
* checkpoint preview endpoint

* harden new preview endpoints

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

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

* address review

* Studio preview: pin adapter, guard streaming submit, robust copy-link

Harden the public per-checkpoint preview surface:

- Pin use_adapter=True in the preview payload sanitizer. Otherwise an
  unauthenticated /p caller can POST use_adapter=false, which calls
  disable_adapter_layers() on the shared in-memory model without restoring
  it; since load_model skips reloads for the same checkpoint, every later
  visitor (the page never sends the field) keeps getting base-model output
  instead of the fine-tuned checkpoint. Forcing it on also re-enables a
  previously disabled adapter and no-ops on merged checkpoints.
- Ignore preview-page submits while a response is streaming. The send
  button was disabled but the Enter handler still called requestSubmit(),
  so a second request could start before the first reply landed in msgs and
  reorder the chat history. Both the keydown and submit handlers now honor
  the disabled button.
- Keep the cloudflare-URL polling loop alive across transient startup fetch
  errors instead of letting one rejection halt it.
- Build the copy-link from a backend preview_ref (output dir relative to
  outputs_root, gated on previewability and the two-segment /p route limit)
  so a nested output dir no longer copies a basename-only link that 404s.
  Expose preview_ref on training run summaries.

Add route-level security tests (path traversal, payload sanitization,
asset containment, CSP header, HTML title escaping, streaming lock held
until drained) and preview_ref unit tests.

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

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

* Studio preview: Safari-safe submit and adapter pin only for LoRA

Follow-ups from cross-browser and route simulations:

- Preview page: send the message from a shared send() helper called by both
  the form submit and the Enter key, instead of form.requestSubmit(). The
  latter throws on Safari < 16 and older iOS, which broke Enter-to-send there.
  Verified across Chromium, Firefox and WebKit with Playwright.
- Only pin use_adapter=True when the resolved checkpoint is a LoRA adapter
  (adapter_config.json present); for a merged checkpoint strip it to None.
  A merged model has no adapter to toggle, so forcing it on only produced a
  per-request "not a PeftModel" warning. The cross-request base-model
  contamination fix still holds for LoRA previews.

Add a merged-checkpoint test asserting use_adapter is stripped to None.

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

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

* Studio preview: trim verbose comments

Tighten comments across the preview routes, page, checkpoint helpers, and tests
to short single-line notes; drop ones that just restate the code. No behavior
change (verified comment/docstring-only with comment_tools.py check).

* Harden preview routes for PR #6486

- Return a generic 400 detail on a rejected preview path so the public /p
  route never echoes the absolute install path (the real reason is logged
  server-side instead).
- Strip confirm_tool_calls, session_id and rag_scope in the preview payload
  sanitizer so the public surface stays inert regardless of the tool gate.
- Use Path.is_relative_to for the asset containment check, matching the rest
  of the codebase.
- Add img-src 'self' and font-src 'self' to the preview page CSP.
- Preview page: on a mid-stream error keep the streamed text, flag the break,
  and restore the prompt so the user can retry; drop the unused --font-sans var.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-06-24 06:31:53 -07:00
Daniel Han
c7c353d740
Pin isolated Node.js installer to committed sha256 digests (#6625)
* Pin isolated Node.js installer to committed sha256 digests

The isolated Node installer verified each downloaded archive only against
SHASUMS256.txt fetched from the same nodejs.org origin as the archive, so a
compromised CDN or TLS path could serve a malicious archive plus a matching
checksum and gain code execution when the extracted node is run during the
npm floor check and version probe.

Anchor trust in studio/node_prebuilt_pins.json, a committed manifest of
per-arch sha256 digests, and verify archives against it. The default channel
installs the pinned version and never fetches the remote SHASUMS. Unpinned
lts, latest, or explicit versions fail closed via UnpinnedNodeRefused unless
UNSLOTH_NODE_ALLOW_UNVERIFIED=1, and the refusal is not swallowed by the
keep-existing-on-transient-failure path. Ship the manifest in package-data.

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

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

* Address review nits on the pinned Node installer

- Drop the unused npm_min_major field from node_prebuilt_pins.json; the floor is
  the NPM_MIN_MAJOR module constant and the dead field could silently drift.
- Reword the unpinned-refusal message so it does not tell a user already on the
  default to install it, and point the "add a pin" hint at the exact asset.
- Decode the opt-in SHASUMS body with errors="replace" so a non-UTF8 response
  yields a clean PrebuiltFallback instead of an uncaught UnicodeDecodeError.
- Tests: assert the refusal message (guards the main() catch order, not just the
  exit code), cover malformed-manifest parsing, and drive the opt-in remote-SHASUMS
  path end to end through install_prebuilt.

* Tighten comments in the pinned Node installer

Collapse multi-line rationale comments to single lines, drop docstrings on the
obvious internal helpers (load_pins, pinned_sha256), and shorten the manifest
note. Comments/docstrings only; verified code-unchanged via AST comparison.

* Address Codex review: verify pins on existing installs; tomllib fallback

- existing_install_matches now takes an expected_sha and the short-circuit passes
  the committed pin, so a version-matching but non-pinned or tampered install (e.g.
  from the old remote-SHASUMS path) is re-verified instead of kept. An unpinned
  target without opt-in no longer short-circuits on an existing install; it falls
  through to the UnpinnedNodeRefused fail-closed path.
- The package-data test uses pytest.importorskip(tomllib/tomli) so it does not
  ModuleNotFoundError on the supported 3.9/3.10 interpreters.

* Make the transient-failure keep-existing path pin-aware

The previous commit added the pinned-digest check to the existing-install
short-circuit but not to the post-download-failure fallback, which still kept any
runnable same-version install via existing_install_usable(). A same-version
install whose recorded sha256 is not the pin could therefore be kept on a
transient download failure, the exact artifact the short-circuit rejects. Refuse
to keep a same-version pin-mismatched install there too; a different usable
version is still kept for offline resilience.

* Bump pinned default Node to the current 24 LTS (24.18.0)

Node 24 LTS moved to 24.18.0; since the default channel now resolves straight to
the manifest, a frozen 24.17.0 would downgrade fresh installs and make
UNSLOTH_NODE_VERSION=lts refuse the current LTS as unpinned. Update default_version
and all six per-arch digests (verified against the official SHASUMS256.txt), and
point the test INDEX/short-circuit fixtures at the new LTS.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-24 05:47:58 -07:00
Daniel Han
c7eaaaeaef Versioning 2026-06-22 08:58:48 -07:00
Daniel Han
936fe199e7 Update pyproject.toml 2026-06-18 10:28:47 -07:00
Daniel Han
b32ef2ef86 Bug fixes 2026-06-13 04:43:27 -07:00
Daniel Han
ebe3bbd041 Versioning 2026-06-12 06:27:38 -07:00
Daniel Han
a70146df0f
Studio: bundle Gemma 4 chat templates (E2B/E4B + larger) and auto-apply to unsloth/gemma-4-*-GGUF (#6245)
* Studio: override chat template for unsloth/gemma-4-*-GGUF with bundled gemma-4.jinja

The chat templates baked into the shipped unsloth/gemma-4-*-GGUF quants predate
Google's gemma-4 chat-template PR #118 and lack the preserve_thinking flag, so
Studio cannot surface the "Preserve thinking" toggle for Gemma 4. Bundle the updated
template and override the embedded one at llama-server launch via --chat-template-file,
scoped to the gemma-4 GGUF family, so users do not need to re-download any quant.

- Add studio/backend/assets/chat_templates/gemma-4.jinja (PR #118 based;
  preserve_thinking defaults false, the one deliberate divergence from upstream).
- Add core/inference/chat_templates.py: gemma-4 GGUF matcher plus an
  effective-override resolver (explicit user template still wins).
- Wire the resolver into routes/inference.py ahead of the reload-dedup check and
  both load_model calls so the live backend and the incoming request compare against
  the same template text (no spurious reloads).
- Default preserve_thinking off in the launch-time chat_template_kwargs so direct
  API callers match the UI default.
- Ship the asset via package-data and add unit tests.

* Studio: ship E2B/E4B edge variant of the bundled Gemma 4 template

Google ships two distinct gemma-4 chat templates: E2B and E4B omit the empty
"<|channel>thought<channel|>" block on enable_thinking=false, while the
12b/26B-A4B/31B family emits it (confirmed against google/gemma-4-E2B-it,
-E4B-it, -12b-it, -26B-A4B-it, -31B-it; the two families differ only in that
one block). The single PR #118 based template followed the larger-model
behavior, which is wrong for the E2B/E4B GGUFs this feature most targets.

- Add studio/backend/assets/chat_templates/gemma-4-edge.jinja: identical to
  gemma-4.jinja minus the empty-thought-block, matching E2B/E4B behavior.
- Route unsloth/gemma-4-E2B-it-GGUF and -E4B-it-GGUF to the edge template;
  12b/26B-A4B/31B keep gemma-4.jinja.
- Extend tests for the edge matcher, per-family routing, and the empty-thought
  block difference (off for edge, on for standard).

* Studio: address review feedback on the gemma-4 template override

- Normalize owner-less shorthand model ids in the template matcher: a bare
  "gemma-4-E2B-it-GGUF" is canonicalized to "unsloth/" the same way
  ModelConfig.from_identifier does, so shorthand loads still get the override
  (and the preserve_thinking capability) instead of falling back to the
  embedded template.
- Scope the test's module stubs with unittest.mock.patch.dict instead of
  sys.modules.setdefault, and only stub deps that are missing, so the global
  module registry is not polluted for tests that run afterwards.
- Guard the Jinja render tests with pytest.importorskip("jinja2") so the suite
  stays runnable in minimal Studio environments where jinja2 is not present.
- Add tests for shorthand resolution.

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

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

* Studio: address 10-reviewer P1 findings on the gemma-4 template override

- /status no longer surfaces Studio's auto-applied bundled template as a
  user-authored chat_template_override. The frontend adopts that field as
  editable state and would otherwise re-send the gemma-4 template as an explicit
  override for a later, unrelated model. /status now reports None when the live
  override equals the model's auto-resolved bundled template.
- When a bundled family template is in effect, strip an inherited
  --chat-template-file from llama_extra_args too (not only when the raw request
  set chat_template_override). Otherwise a stale inherited template, appended
  last, shadows the bundled one while Studio reports the bundled template's
  capabilities.
- Write the temp chat-template file as UTF-8 explicitly, and keep the bundled
  templates ASCII (replaced em dashes), so non-UTF-8 Windows locales cannot raise
  UnicodeEncodeError or emit a mis-encoded template. Added an ASCII guard test.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-12 05:49:39 -07:00
Daniel Han
ddaa8a9cb5 Update pyproject.toml 2026-06-11 09:19:10 -07:00
Nilay
f64c3c8aba
Studio: add unsloth chat CLI command (#6170)
* Studio: add `unsloth chat` CLI command

Interactive chat REPL on the shared Studio backend: trained-model picker
when no model is given, /think and /compare toggles (adapter toggle on
CUDA, side-by-side base-model load on MLX), markdown streaming, and
connect-if-running Studio server mode so models stay warm across
sessions and are shared with the UI.

* fix settings

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

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

* fix error handling and compare base precision

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

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

* Fix chat CLI backend imports and GGUF drafter loading

* Hide split thinking tags in chat CLI streams

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-06-11 16:09:34 +01:00
Daniel Han
5f11182634 Update pyproject.toml 2026-06-10 11:10:51 -07:00
Daniel Han
3ce187da02
Formatting: ruff line-length 100, kwarg-spacing passes, drop blank after short local imports (#6079)
Raise ruff line-length to 100 and extend the local pre-commit format pipeline (def-signature magic-comma normalization, short multi-line assert collapse, kwarg '=' spacing, blank-line-after-short-import removal, adjacent string-literal / f-string+plain merge, redundant-pass pruning). Every transform re-checks the file AST and is dropped if it would differ; the whole-repo reformat is verified AST-identical per file and idempotent.
2026-06-08 04:24:13 -07:00
Daniel Han
08d02610d9 Versioning 2026-06-03 06:35:55 -07:00
Daniel Han
2e1580f70b Versioning 2026-05-31 06:29:11 -07:00
Daniel Han
18c300197c API fixes + pypi 2026-05-22 09:16:53 -07:00
Daniel Han
07c03777b8 Versioning 2026-05-19 07:00:04 -07:00
Daniel Han
aa374319d1 Versioning 2026-05-18 05:29:52 -07:00
Daniel Han
e20bbeff9a
intel-gpu: pin unsloth_zoo>=2026.5.2 via huggingfacenotorch (#5499)
Fixes unslothai/unsloth#5494: installing any intel-gpu-torch* extra
without also pulling `huggingface` or `colab-new` lets the resolver
silently fall back to a stale unsloth_zoo (2026.3.6 in the original
report) because no version floor is enforced on
`unsloth_zoo[intelgpu]` in those blocks.

unsloth_zoo 2026.5.2 (just released) also relaxes its own torch upper
bound from <2.11.0 to <2.13.0, which is what unblocks the resolver for
the intel-gpu-torch2110 and intel-gpu-torch2120 extras shipped in
#5484.

Adding the floor in `huggingfacenotorch` propagates it to every
extra that includes the HF-without-torch base: amd, huggingface, and
all nine intelgputorch* blocks. Single line, single source of truth.

Requires unsloth_zoo 2026.5.2 to be on PyPI for end-to-end resolution.
2026-05-17 01:40:20 -07:00
Daniel Han
f7069b242c intel-gpu: add xpu extras for torch 2.7.1 / 2.9.1 / 2.11.0 / 2.12.0
Builds on the intelgputorch210 fix in the previous commit by also
filling in the four other intel-gpu-torch* wheel stacks that PyTorch
has published but Unsloth was not yet exposing:

* intelgputorch271 / intel-gpu-torch271:
    torch 2.7.1+xpu  with pytorch-triton-xpu 3.3.1
* intelgputorch291 / intel-gpu-torch291:
    torch 2.9.1+xpu  with pytorch-triton-xpu 3.5.0
* intelgputorch2110 / intel-gpu-torch2110:
    torch 2.11.0+xpu with triton-xpu 3.7.0
* intelgputorch2120 / intel-gpu-torch2120:
    torch 2.12.0+xpu with triton-xpu 3.7.1

Each torch + triton pair was cross-checked against the wheel's PEP 658
.metadata sidecar to confirm the upstream Requires-Dist matches what
this block pins. Verified via `uv pip compile` across cp310/cp311/cp312/
cp313 on Linux x86_64 and Windows AMD64.
2026-05-16 06:18:38 -07:00
stoicAI1776
e87c0dd418 fix(pyproject): use triton-xpu 3.6.0 for intelgputorch210 (#4931) 2026-05-16 06:18:38 -07:00
Daniel Han
ef9f672fe8
security: NOT affected by Mini Shai-Hulud (May-12 wave) -- forward-looking hardening only (#5397)
* scripts/scan_*: add Mini Shai-Hulud May-12 IOC strings and pin-blocklists

Append the May-12 2026 wave indicators (git-tanstack.com, transformers.pyz,
/tmp/transformers.pyz, "With Love TeamPCP", "We've been online over 2 hours")
to all three scanner IOC tables, add BLOCKED_NPM_VERSIONS (42 TanStack pkgs,
4 opensearch versions, 3 squawk pkgs) in scan_npm_packages.py and
lockfile_supply_chain_audit.py (kept byte-identical), add BLOCKED_PYPI_VERSIONS
(guardrails-ai 0.10.1, mistralai 2.4.6, lightning 2.6.2/2.6.3) plus
RE_MAY12_IOC wiring across check_py_file/check_shell_file/check_workflow_file
in scan_packages.py. The npm orchestrator and the lockfile auditor now
short-circuit on a blocked entry before fetching the tarball, and the
PyPI download pipeline drops blocked specs before pip download is invoked.

* tests/security: regression suite for supply-chain scanners

Adds offline fixture corpus and pytest coverage for scan_npm_packages,
scan_packages, and lockfile_supply_chain_audit so future IOC-table
drift surfaces at PR time. Pytest scope narrowed to tests/security so
GPU smoke tests are not picked up by default.

* ci(security-audit): drop continue-on-error on pip-scan and npm-scan jobs

Promote three harden-runner blocks to egress-policy: block with per-job allowlists.
Add tests-security job running pytest tests/security as a hard gate.

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

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

* scripts: harden third-party downloads, pip resolver pins, atomic writes

Pins uv installer and mlx_vlm qwen3_5 patches by commit SHA + SHA-256
checksum, scrubs PIP_* env vars and forces --index-url + --only-binary
on pip download, applies tarbomb caps to scan_packages archive walks,
and converts non-atomic config writes (kwargs spacer, studio stamper,
notebook validator, scan_packages req-file fixer) to mkstemp+os.replace.

Also adds host allowlist to notebook_to_python downloader, threads an
--allow-shell flag through its shell=True emission with reviewer warning
comments, locks both MLX installer scripts to set -euo pipefail, and
extends CODEOWNERS so colab snapshot data files require notebook-owner
review.

* ci(workflows): harden release-desktop / smoke / notebooks workflows

Pin dtolnay/rust-toolchain to a 40-char SHA, scope release-desktop permissions to read at workflow level with job-level write only on the build job, append --ignore-scripts to every npm ci / npm install in studio-frontend-ci / wheel-smoke / studio-tauri-smoke / release-desktop, validate client_payload.ref shape via an env-var-isolated regex on every notebooks-ci job, and add step-security/harden-runner in audit mode as the first step of release-desktop and mlx-ci.

* scripts: promote silent scanner failures to non-zero exit codes
scan_packages now returns 2 on pip-download failure and emits a CRITICAL archive_corrupted finding on truncated wheels/sdists.
notebook_to_python exits 1 on per-notebook failures; notebook_validator wraps the stash/pop in try/finally; lockfile audit rejects bare UNSLOTH_LOCKFILE_AUDIT_SKIP=1 with a loud GitHub Actions warning.

* Add npm cooldown + new-install-script gate + Dependabot cooldown

Pins min-release-age=7 (npm 11.10+) in repo-root and studio/frontend
.npmrc, adds scripts/check_new_install_scripts.py to fail PRs that
add a postinstall dep, ships a new security-audit job for npm audit
signatures plus the diff, and extends .github/dependabot.yml with
cooldown stanzas. Pin @tanstack/react-router to 1.169.9 per GHSA-
g7cv-rxg3-hmpx; lockfile regen deferred until that release lands on
npm. tests/security gains 4 new tests; full suite 26/26 green.

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

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

* ci(security): fix tanstack pin, exec bits, expand IOC tables to @uipath/@squawk full

- Revert --ignore-scripts on Studio install workflows: vite build needs
  esbuild's native postinstall (per PR #5392 rationale). Keep
  --ignore-scripts on security-audit.yml's standalone npm audit job.
- Pin @tanstack/react-router to the actual published 1.169.2 (was a
  forward-looking 1.169.9 that does not exist on npm; broke npm ci).
- Drop redundant repo-root .npmrc; studio/frontend/.npmrc covers the
  only npm project today (root cooldown re-instate via dependabot.yml).
- Restore exec bits on 7 files my filesystem stripped during cherry-pick.
- Expand BLOCKED_NPM_VERSIONS with full safedep.io + Aikido enumeration:
  22 @squawk/* packages with 5 versions each (110 entries; previously
  3 entries with 1 version each), and 66 @uipath/* packages (entirely
  missing before). Mirror in scripts/lockfile_supply_chain_audit.py.

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

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

* tests/security: suppress CodeQL py/incomplete-url-substring-sanitization

The two flagged 'X' in Y assertions are NOT URL sanitization checks.
They verify our scanner WROTE a known IOC literal into its stdout /
Finding.evidence, which is the opposite of an attack surface --
matching the scanner's output is precisely what catches the worm.
Inline lgtm[] suppression with a 4-line rationale comment above each.

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

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

* scripts/scan_*: expand IOC tables with Aikido full 169-pkg enumeration

Per Aikido 2026-05-12 disclosure (373 malicious package-version entries
across 169 npm package names), add to BLOCKED_NPM_VERSIONS:

  - @mistralai/* npm scope (3 packages, 9 versions) -- separate from
    the PyPI mistralai package already in BLOCKED_PYPI_VERSIONS
  - @tallyui/* (10 packages, 30 entries)
  - @beproduct/nestjs-auth (18 versions 0.1.2..0.1.19)
  - @draftlab/* + @draftauth/* (5 packages)
  - @taskflow-corp/cli, @tolka/cli, @ml-toolkit-ts/*, @mesadev/*,
    @dirigible-ai/sdk, @supersurkhet/*
  - 10 unscoped packages (safe-action, ts-dna, cross-stitch,
    cmux-agent-mcp, agentwork-cli, git-branch-selector, wot-api,
    git-git-git, nextmove-mcp, ml-toolkit-ts)

Also add to KNOWN_IOC_STRINGS / NPM_IOC_STRINGS:

  - router_init.js SHA-256 ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
  - tanstack_runner.js SHA-256 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96
  - bun run tanstack_runner.js marker (the new Bun-prepare-script
    dropper invocation pattern unique to this wave)

Total: 170 packages, 401 versions blocklisted. Studio lockfile still
scans clean (0 findings, 0 hard errors).

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

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

* scripts/scan_*: web-verification additions (@tanstack/setup, intercom-client)

Two findings from cross-checking BLOCKED_NPM_VERSIONS / KNOWN_IOC_STRINGS
against GHSA-g7cv-rxg3-hmpx + Aikido + safedep.io + Socket + Semgrep.

  - Fix asymmetry: @tanstack/setup IOC string was in
    lockfile_supply_chain_audit.py's NPM_IOC_STRINGS but missing from
    scan_npm_packages.py's KNOWN_IOC_STRINGS. The literal is the malicious
    optional-dependency name used by the May-12 TanStack wave; no
    legitimate npm package of this name exists.

  - Add intercom-client@7.0.4: the npm counterpart of the lightning
    2.6.2/2.6.3 PyPI compromise (Apr-30 wave). Same threat actor
    (TeamPCP). Confirmed by Semgrep, Aikido, OX Security, Resecurity,
    Kodem. Safe version is 7.0.3 and earlier.

Total BLOCKED_NPM_VERSIONS: 171 packages / 402 versions. Both files
remain byte-identical. Studio lockfile still scans clean.

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

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

* ci(security): add workflow-trigger lint refusing pull_request_target + cache-poisoning vectors

The two patterns that together powered GHSA-g7cv-rxg3-hmpx (TanStack
Mini Shai-Hulud) are now gated at PR time:

  1. pull_request_target -- the worm chain started with a fork PR that
     ran in the base-repo context. Every workflow in this repo today
     uses 'pull_request' (safe); the lint refuses any new
     pull_request_target additions outright. workflow_run is
     restricted, allowed only with an explicit allow-comment.

  2. Shared cache keys between PR-triggered workflows and the publish
     workflow (release-desktop.yml). The TanStack attack chain poisoned
     a shared Actions cache from a fork PR; the legitimate release
     workflow then restored the poisoned cache. The lint refuses any
     cache key that appears in both a PR-triggered workflow and a
     workflow_dispatch-only / publish workflow.

Current tree is clean: 0 pull_request_target, 0 workflow_run, 0
PR-publish cache-key collisions across all 24 workflows. The lint
locks that invariant in place.

Files:
  + scripts/lint_workflow_triggers.py (~200 LOC, stdlib + PyYAML)
  + tests/security/test_lint_workflow_triggers.py (5 tests covering
    current-tree pass, pull_request_target reject, workflow_run
    restricted, justified workflow_run accept, cache-key collision
    reject)
  ~ .github/workflows/security-audit.yml: new workflow-trigger-lint
    job, no continue-on-error, harden-runner block-mode, PyYAML only
    runtime dep.

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

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

* security: fix tests-security CI job + CodeQL false-positives

Two CI failures on the prior push:

1. pytest tests/security -- 5 lint regression tests failed because
   scripts/lint_workflow_triggers.py imports PyYAML which is not in
   the bare runner's Python env. Added pyyaml==6.0.2 to the pip
   install step alongside pytest. (29 scanner tests already passed.)

2. CodeQL py/incomplete-url-substring-sanitization fired on two
   test assertions that check the scanner WROTE the IOC literal
   to its own stdout/stderr. The rule pattern-matches on
   `"<host>" in <var>` and cannot distinguish a URL sanitizer from
   a regression-test evidence check. Previous `# lgtm[...]` inline
   suppressions were detached from the operator when pre-commit
   reformatted the assert across multiple lines. Rebuilt the IOC
   literals at runtime (`"git-tanstack." + "com"`) so no URL-shaped
   source literal appears on the `in` operator line; rule cannot
   trigger.

Verified locally: `pytest tests/security -v` -> 34 passed in 2.70s.

* security(studio): defensive .npmrc cooldown aliases + save-exact

Two additions to studio/frontend/.npmrc to harden the existing
`min-release-age=7` (Mini Shai-Hulud defence):

1. `minimum-release-age=10080` (minutes) -- defensive alias for the
   same 7-day floor. Some npm versions / wrappers consult one key but
   not the other; setting both prevents a single upstream setting-name
   parse change from silently disabling the cooldown. The two keys
   MUST agree (do not let them drift).

2. `save-exact=true` -- refuses to write back `^x.y.z` ranges into
   package.json when a maintainer runs `npm install <pkg>` locally.
   Does NOT rewrite already-present ranges; stops NEW carets from
   creeping into the manifest as patch-version footguns.

Verified: pytest tests/security -> 34 passed in 2.63s.

* chore(dependabot): remove dead bun entry for /studio/frontend

`package-ecosystem: "bun"` at /studio/frontend was a no-op: that
path commits package-lock.json, not bun.lock / bun.lockb, so
Dependabot's bun ecosystem silently skipped it. The actual
behaviour is unchanged -- the npm entry below the cargo block
already owns npm_and_yarn security advisories for /studio/frontend
with `open-pull-requests-limit: 0` (version-update PRs suppressed,
security PRs flow through).

This commit:

  - Deletes the bun entry (kept a placeholder comment so a future
    bun migration knows where to slot it back in).
  - Rewrites the npm /studio/frontend entry comment to explain the
    real intent: lockfile is the authoritative pin, .npmrc
    `min-release-age=7` already blocks fresh tarballs at install
    time, dependabot only needs to surface security advisories.

No functional change: same set of dependabot PRs as before (zero
version updates, security advisories grouped weekly with cooldown).

Verified: pytest tests/security -> 34 passed in 2.67s; YAML
parses cleanly via PyYAML.

* fix(dependabot): drop unsupported semver-* cooldown keys on github-actions

Dependabot's validator rejected the config with:

  The property '#/updates/0/cooldown/semver-minor-days' is not
  supported for the package ecosystem 'github-actions'.
  The property '#/updates/0/cooldown/semver-patch-days' is not
  supported for the package ecosystem 'github-actions'.

The `semver-minor-days` / `semver-patch-days` cooldown knobs are
only valid for semver-aware ecosystems (npm, cargo, etc.). The
github-actions ecosystem pins via git tags / SHAs, not semver, so
only `default-days` is honored. Pre-existing bug on main; surfaced
on this PR because the prior commit re-validated the file.

Behaviour: github-actions PRs now respect the 7-day cooldown floor
(was already the intent), without the no-op semver bands.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-13 04:58:12 -07:00
Manan Shah
d65149795b
feat(studio): MLX training tab on Apple Silicon (LoRA / full FT, VLM, export) (#5265)
* Add Apple Silicon MLX routing

Rewrite __init__.py: detect MLX on macOS arm64 before any torch imports
Extract original GPU init to _gpu_init.py (unchanged)
MLX path imports FastMLXModel from unsloth_zoo, skips all GPU code
GPU path unchanged: from ._gpu_init import *

* Add Apple Silicon MLX routing

- Rewrite __init__.py: detect MLX on macOS arm64 before any torch imports
- Extract original GPU init to _gpu_init.py (unchanged)
- MLX path imports FastMLXModel from unsloth_zoo, skips all GPU code
- GPU path unchanged: from ._gpu_init import *

* mlx with studio

* mlx with studio

* updating temporary install.sh

* updating temporary install.sh

* adding t_v5 path

* adding t_v5 path

* fixing vision training

* fixing vision training

* adding chat

* adding chat

* minor

* minor

* Adding export and fixing training issues, inference with lora adaptors

* Adding export and fixing training issues, inference with lora adaptors

* fix: MLX worker pass load_in_4bit, override is_vlm based on dataset, streaming for VLM

* fix: MLX worker pass load_in_4bit, override is_vlm based on dataset, streaming for VLM

* Merge mlx-apple-silicon into main

* update install.sh to point to main branch

* update install.sh to point to main branch

* fix: export returns 3 values (success, message, output_path) matching upstream worker

* fix: export returns 3 values (success, message, output_path) matching upstream worker

* fix(mlx): show training-process peak memory in Studio UI, not system-wide

Studio UI was showing ~95 GB during MLX training because get_gpu_utilization
read "In use system memory" from IORegistry's AGXAccelerator — system-wide
GPU memory across all processes (training + backend + browser + Display).

Now the trainer's mx.get_peak_memory value is forwarded through the
progress event and surfaced via /api/train/hardware while training is
active. Falls back to the system-wide reading when training is not running.

* fix(mlx): show training-process peak memory in Studio UI, not system-wide

Studio UI was showing ~95 GB during MLX training because get_gpu_utilization
read "In use system memory" from IORegistry's AGXAccelerator — system-wide
GPU memory across all processes (training + backend + browser + Display).

Now the trainer's mx.get_peak_memory() value is forwarded through the
progress event and surfaced via /api/train/hardware while training is
active. Falls back to the system-wide reading when training is not running.

* fix(mlx): make is_bfloat16_supported detect M1/M2 (no native bf16)

M1 and M2 chips emulate bf16 in software on the GPU, causing 40-70%
slower prefill compared to native fp16. M3+ have native bf16 (macOS
Sonoma+ MPSGraph). Replaces the always-True stub with chip-aware
detection via mx.device_info.

* fix(mlx): make is_bfloat16_supported() detect M1/M2 (no native bf16)

M1 and M2 chips emulate bf16 in software on the GPU, causing 40-70%
slower prefill compared to native fp16. M3+ have native bf16 (macOS
Sonoma+ MPSGraph). Replaces the always-True stub with chip-aware
detection via mx.device_info().

* feat(mlx): wire training_type="Full Finetuning" through MLX worker

Compute use_lora from the UI's training_type before loading the model,
pass full_finetuning=not use_lora to FastMLXModel.from_pretrained, and
let the existing 'if use_lora' branch skip get_peft_model. Matches the
GPU worker's flow.

* feat(mlx): wire training_type="Full Finetuning" through MLX worker

Compute use_lora from the UI's training_type before loading the model,
pass full_finetuning=not use_lora to FastMLXModel.from_pretrained, and
let the existing 'if use_lora' branch skip get_peft_model. Matches the
GPU worker's flow.

* fix(mlx): pass save_method='merged_16bit' from Studio's export page

Previously the MLX path called save_pretrained_merged with no
save_method, which fell through to a no-op that didn't actually fuse
LoRA into the base. Now Studio's "Merged Model" export properly
fuses LoRA + dequantizes any 4-bit base to bf16, matching the GPU
behavior for the same UI option.

* fix(mlx): pass save_method='merged_16bit' from Studio's export page

Previously the MLX path called save_pretrained_merged() with no
save_method, which fell through to a no-op that didn't actually fuse
LoRA into the base. Now Studio's "Merged Model" export properly
fuses LoRA + dequantizes any 4-bit base to bf16, matching the GPU
behavior for the same UI option.

* fix(studio): pass private to MLX push, return 3-tuples consistently

MLX push_to_hub branch now forwards private=private (matches GPU)
Existing 2-tuple early-returns ('repo_id+token required', 'PEFT model
needed') were tripping the route's 3-tuple unpack. Added a None
output_path so the unpack always succeeds.

* fix(studio): pass private to MLX push, return 3-tuples consistently

- MLX push_to_hub branch now forwards private=private (matches GPU)
- Existing 2-tuple early-returns ('repo_id+token required', 'PEFT model
  needed') were tripping the route's 3-tuple unpack. Added a None
  output_path so the unpack always succeeds.

* studio wirings

* studio wirings

* Merge pull request #5 from Manan17/feat/quant_config

studio wirings

* fix(mlx): wire train_on_completions for VLM via per-template lookup

Mirror the GPU worker: stop excluding VLMs and stop hardcoding
template detection. Look up the model in MODEL_TO_TEMPLATE_MAPPER and
fetch the per-template instruction/response markers from
TEMPLATE_TO_RESPONSES_MAPPER. The frontend already force-disables
train_on_completions for vision+image and audio cases, so backend
just trusts the flag.

* fix(mlx): wire train_on_completions for VLM via per-template lookup

Mirror the GPU worker: stop excluding VLMs and stop hardcoding
template detection. Look up the model in MODEL_TO_TEMPLATE_MAPPER and
fetch the per-template instruction/response markers from
TEMPLATE_TO_RESPONSES_MAPPER. The frontend already force-disables
train_on_completions for vision+image and audio cases, so backend
just trusts the flag.

* wire in lora rslora, init lora weights, random_state

* wire in lora rslora, init lora weights, random_state

* loftq studio error message fix

* loftq studio error message fix

* handle unknown optim and lr scheduler

* handle unknown optim and lr scheduler

* Merge pull request #6 from Manan17/update/peftkwargs

Update/peftkwargs

* feat(mlx): pass finetune_language/attention/mlp/vision flags to FastMLXModel

Studio's four UI checkboxes now actually flow through to MLX get_peft_model
(which was just updated in unsloth-zoo to honor them). Also drops the
incorrect train_projector wiring that tied projector LoRA to the
attn/mlp flags — those are language-side toggles, not projector toggles.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* feat(mlx): pass finetune_language/attention/mlp/vision flags to FastMLXModel

Studio's four UI checkboxes now actually flow through to MLX get_peft_model
(which was just updated in unsloth-zoo to honor them). Also drops the
incorrect train_projector wiring that tied projector LoRA to the
attn/mlp flags — those are language-side toggles, not projector toggles.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* feat(mlx,ux): auto-imply finetune_language_layers when user picks attn/mlp

UI guardrail. The four checkboxes (vision/language/attention/MLP) carry
"scope × module-type" semantics that aren't obvious — picking just
"Attention modules" + "MLP modules" without "Language layers" naturally
reads as "fine-tune attn/mlp" but our backend reads it as "fine-tune
attn/mlp modules in *no* tower" → empty target_modules → zero
trainable params → crash inside value_and_grad.

If user selected attn or mlp module types but no layer scope, default
to language scope. Power users can still explicitly choose
language=False, vision=True if they want vision-only fine-tuning of
attn/mlp.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* feat(mlx,ux): auto-imply finetune_language_layers when user picks attn/mlp

UI guardrail. The four checkboxes (vision/language/attention/MLP) carry
"scope × module-type" semantics that aren't obvious — picking just
"Attention modules" + "MLP modules" without "Language layers" naturally
reads as "fine-tune attn/mlp" but our backend reads it as "fine-tune
attn/mlp modules in *no* tower" → empty target_modules → zero
trainable params → crash inside value_and_grad.

If user selected attn or mlp module types but no layer scope, default
to language scope. Power users can still explicitly choose
language=False, vision=True if they want vision-only fine-tuning of
attn/mlp.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* fix(mlx): wire top_k, repetition_penalty, and VLM top_p through to mlx-lm/mlx-vlm

Inference UI sliders for top_k and repetition_penalty had no effect on
MLX, and VLM top_p was also silently dropped. Plus a latent pre-existing
bug: mlx_vlm.generate_step expects temperature= (long form), but we
were passing temp= which silently fell into **kwargs — every VLM chat
was effectively greedy regardless of the temperature slider.

Text path (_generate_text):
make_sampler now receives top_k in addition to temp/top_p
make_logits_processors built and forwarded when repetition_penalty is
non-trivial (skip when 0.0/1.0 to avoid pointless overhead)

VLM path (_generate_vlm):
Pass top_p, top_k, repetition_penalty as kwargs (mlx_vlm.stream_generate
forwards them to generate_step's sampler/logits_processor builders)
Rename temp= → temperature= so it's actually consumed

Verified end-to-end with a smoke test on Qwen2.5-0.5B-Instruct (text) and
Qwen2.5-VL-3B-Instruct (VLM): each of {greedy, top_p=0.5, top_k=10,
rep_pen=1.5} now produces a distinct output, proving the parameters
reach the sampler.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* fix(mlx): wire top_k, repetition_penalty, and VLM top_p through to mlx-lm/mlx-vlm

Inference UI sliders for top_k and repetition_penalty had no effect on
MLX, and VLM top_p was also silently dropped. Plus a latent pre-existing
bug: mlx_vlm.generate_step expects temperature= (long form), but we
were passing temp= which silently fell into **kwargs — every VLM chat
was effectively greedy regardless of the temperature slider.

Text path (_generate_text):
- make_sampler now receives top_k in addition to temp/top_p
- make_logits_processors built and forwarded when repetition_penalty is
  non-trivial (skip when 0.0/1.0 to avoid pointless overhead)

VLM path (_generate_vlm):
- Pass top_p, top_k, repetition_penalty as kwargs (mlx_vlm.stream_generate
  forwards them to generate_step's sampler/logits_processor builders)
- Rename temp= → temperature= so it's actually consumed

Verified end-to-end with a smoke test on Qwen2.5-0.5B-Instruct (text) and
Qwen2.5-VL-3B-Instruct (VLM): each of {greedy, top_p=0.5, top_k=10,
rep_pen=1.5} now produces a distinct output, proving the parameters
reach the sampler.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* feat(mlx): map format_type to MLX save_method, reuse local save dir for hub push

export_merged_model: format_type="4-bit (FP4)" → save_method="merged_4bit"
(was hardcoded merged_16bit, ignoring the UI choice).
Both export_merged_model and export_base_model now pass save_directory=
to push_to_hub_merged so it reuses the just-written local folder
instead of re-saving under a relative "username/model" directory.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

* feat(mlx): map format_type to MLX save_method, reuse local save dir for hub push

- export_merged_model: format_type="4-bit (FP4)" → save_method="merged_4bit"
  (was hardcoded merged_16bit, ignoring the UI choice).
- Both export_merged_model and export_base_model now pass save_directory=
  to push_to_hub_merged so it reuses the just-written local folder
  instead of re-saving under a relative "username/model" directory.

Co-Authored-By: Manan17 <shahmanan170602@gmail.com>

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

* restore install

* restore install

* fix(mlx): restore FastVisionModel as a distinct class

unsloth/__init__.py was assigning `FastVisionModel = FastLanguageModel`
right after defining `class FastVisionModel(FastLanguageModel)` with a
`for_training` static method. The alias erased the class binding, so
the documented `FastVisionModel.for_training(model)` call from upstream
Unsloth's VLM notebooks raised `AttributeError` on MLX.

Remove the offending alias. `FastVisionModel` is now a real subclass of
`FastLanguageModel` again — inherits `from_pretrained` /
`get_peft_model` / `for_inference`, exposes `for_training` as a no-op
pass-through (no-op because MLX doesn't have a train/eval mode flag;
the call exists purely for GPU/MLX notebook parity).

Verified end-to-end: Qwen3-VL-2B + LaTeX_OCR LoRA + vision LoRA via
FastVisionModel.from_pretrained → get_peft_model → for_training →
MLXTrainer.train runs 10 steps cleanly (loss 1.10 → 0.12, no NaNs,
peak 5.89 GB).

Studio's path (FastLanguageModel.from_pretrained for any repo,
auto-detect VLM in the loader) is unaffected. Tier-1 review finding #8.

* fix(mlx): restore FastVisionModel as a distinct class

unsloth/__init__.py was assigning `FastVisionModel = FastLanguageModel`
right after defining `class FastVisionModel(FastLanguageModel)` with a
`for_training` static method. The alias erased the class binding, so
the documented `FastVisionModel.for_training(model)` call from upstream
Unsloth's VLM notebooks raised `AttributeError` on MLX.

Remove the offending alias. `FastVisionModel` is now a real subclass of
`FastLanguageModel` again — inherits `from_pretrained` /
`get_peft_model` / `for_inference`, exposes `for_training` as a no-op
pass-through (no-op because MLX doesn't have a train/eval mode flag;
the call exists purely for GPU/MLX notebook parity).

Verified end-to-end: Qwen3-VL-2B + LaTeX_OCR LoRA + vision LoRA via
FastVisionModel.from_pretrained → get_peft_model → for_training →
MLXTrainer.train() runs 10 steps cleanly (loss 1.10 → 0.12, no NaNs,
peak 5.89 GB).

Studio's path (FastLanguageModel.from_pretrained for any repo,
auto-detect VLM in the loader) is unaffected. Tier-1 review finding #8.

* Studio: harden MLX training and export, restore GPU init guards

Studio export
Restore Tuple[bool, str, Optional[str]] contract on export_merged_model,
export_base_model, export_gguf, and export_lora_adapter, populating
output_path on successful local saves so routes/worker/CLI/frontend
details.output_path is non-empty again.
Lift the GPU save_method assignment out of the local-save branch so
Hub-only merged exports (save_directory='', push_to_hub=True) no longer
hit UnboundLocalError on the push branch.
For MLX merged and base hub-only export, stage to a tempfile.TemporaryDirectory
before push_to_hub_merged instead of passing save_directory=''.
Source _IS_MLX from unsloth instead of recomputing the platform check
(single source of truth, also enforces mlx-package availability).

Studio MLX training/inference
Pass token=hf_token into FastMLXModel.from_pretrained for gated/private
models, matching the inference path.
Strip hf_token and wandb_token from wandb.init(config=...) so secrets
do not leak into the W&B run config.
Replace load_from_disk(local_datasets[0]) with the existing
UnslothTrainer._resolve_local_files / _loader_for_files helpers so
uploaded JSON/JSONL/CSV/Parquet files train through the normal datasets
loader (load_from_disk still used for HF save_to_disk directories).
Make the dataset slice helper inclusive at the end and treat 0 as a real
index instead of "unset", matching the GPU and embedding paths.
Add a status_message -> message alias inside _send so the existing parent
pump (training.py) renders MLX status updates instead of blanks.
Forward min_p through generate_chat_response into _generate_text /
_generate_vlm and into make_sampler / vlm_kwargs so the sampling control
is no longer a no-op on MLX.
Wrap unsloth_zoo.mlx_loader / mlx_trainer imports with a clearer
ImportError pointing users at install.sh for Apple Silicon.
Exit the MLX stop-polling thread on EOFError/OSError instead of
busy-looping when the queue/pipe is permanently closed (one-line
why-safe rationale inline).

Studio frontend
ParamsSection subscribes to platform deviceType via the Zustand hook so
the gradient checkpointing dropdown re-renders after the async device
fetch completes.

Studio hardware
get_gpu_utilization MLX branch now reads _read_apple_gpu_stats once and
derives VRAM totals from psutil, removing the second ioreg subprocess
per utilization poll.

Unsloth core
Restore the os.geteuid == 0 guard around the CUDA ldconfig recovery
that was lost when GPU initialization moved into _gpu_init.py, plus the
non-root manual-fix warning branch. Non-root CUDA users no longer shell
out to ldconfig at import time.
Load dataprep/raw_text via importlib so the MLX import path no longer
pulls torch in through dataprep/__init__.py -> synthetic.py.
FastVisionModel.from_pretrained overrides the inherited delegator only
to inject text_only=False; this is an extension, not a duplication, and
is needed so VLM checkpoint loads keep the vision tower.
Wrap the MLX-branch unsloth_zoo import with a clearer ImportError.

* Studio: regression tests for MLX training/export and GPU init ldconfig guard

tests/python/test_gpu_init_ldconfig_guard.py asserts the geteuid root
check still wraps the ldconfig recovery and the non-root branch warns
bnb users; AST + source-text inspection so the test runs without torch.
tests/studio/test_export_output_path_contract.py covers the
Tuple[bool, str, Optional[str]] return contract on every export method,
the output_path assignment after successful local save, the Hub-only
GPU save_method binding fix, the MLX hub-only TemporaryDirectory
staging, and the single-source `_IS_MLX` import from unsloth.
tests/studio/test_mlx_training_worker_behaviors.py covers token
forwarding to FastMLXModel.from_pretrained, wandb config secret
stripping, file-aware local dataset loading, status_message ->
message aliasing, inclusive slice semantics, EOFError/OSError stop
thread exit, and the friendly mlx_loader / mlx_trainer ImportError.

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

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

* fix(mlx): cap inference memory + release wired on unload + tame worker pre-pin

Three memory-hardening fixes for Studio's MLX path:

1. Inference applies the same Metal caps as the trainer.
   load_model previously only called set_wired_limit(100% of recommended)
   with no upper memory_limit, leaving large VLM checkpoints unbounded
   during the loader allocation. Add _configure_memory_limits() that sets
   memory_limit to 85% of recommended and wired_limit to min(recommended,
   memory_limit) — matching MLXTrainer's defaults so behavior is the same
   whether the user trains or just runs inference.

2. unload_model releases pinned memory back to the OS — but only when
   the cache is empty. Without this, pinned wired bytes stayed allocated
   to MLX after the model was gone, starving other apps. The release is
   guarded on `not self.models` so unloading one of several cached
   models doesn't un-pin weights still in use.

3. Worker pre-cap is conservative instead of aggressive.
   The previous pre-pin set_wired_limit(100% of recommended) competed
   with MLXTrainer's later more conservative cap. Replace with the same
   85%-memory / min(rec, memory) pair that the trainer applies later
   (idempotent re-apply). Bounds the model load + LoRA setup window
   without over-pinning.

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

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

* tests/studio: regression tests for the _IS_MLX dispatch gate

Two gates drive every MLX-vs-CUDA dispatch decision in Studio:

  1. unsloth._IS_MLX in unsloth/__init__.py — evaluated once at import
     time, read by Studio worker code to choose the GPU vs MLX trainer
     and inference paths. Defined as
        Darwin AND arm64 AND find_spec("mlx") is not None.

  2. utils.hardware.detect_hardware() — runtime probe with priority
     CUDA > XPU > MLX > CPU. The MLX branch is reached only when both
     CUDA and XPU are unavailable and the host is Apple Silicon and
     mlx is importable.

Neither gate had a direct test. Adds tests/studio/test_is_mlx_dispatch_gate.py
with six tests:

  test_is_mlx_gate_uses_three_required_predicates
      AST-walks unsloth/__init__.py and asserts the _IS_MLX assignment
      is a BoolOp(And) of platform.system()=="Darwin",
      platform.machine()=="arm64", and find_spec("mlx") is not None.
      Catches accidental rewrites that drop a predicate.

  test_is_mlx_gate_true_on_apple_silicon_with_mlx_present
      Spoofs platform to Darwin/arm64, injects a fake mlx module so
      find_spec returns a real ModuleSpec, re-evaluates the gate
      expression. Verifies it flips True under the exact conditions
      Studio expects.

  test_is_mlx_gate_false_when_mlx_missing
      Spoofs Apple Silicon but with mlx absent. Verifies the gate stays
      False (so a Mac without mlx installed does not pretend to have
      MLX support).

  test_is_mlx_gate_false_on_non_apple_silicon
      Canary on the actual Linux+CUDA / AMD / Intel test host: the gate
      must remain False regardless of whether mlx happens to be
      importable. Protects existing GPU users from accidental MLX
      hijack when MLX support evolves.

  test_detect_hardware_picks_mlx_when_only_apple_silicon_available
      Forces torch.cuda and torch.xpu off, spoofs Apple Silicon, injects
      fake mlx and mlx.core. detect_hardware() must return DeviceType.MLX.

  test_detect_hardware_picks_cuda_on_real_host
      Canary: on a real CUDA host detect_hardware() must return
      DeviceType.CUDA. Protects against the MLX branch shadowing CUDA
      dispatch on NVIDIA / AMD ROCm hosts.

Uses the same monkeypatch.setitem(sys.modules, ...) fake-mlx pattern as
the existing test_mlx_inference_backend.py — no new test infrastructure,
no real mlx install required.

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

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

* Add AGPL-3.0 SPDX header to Studio MLX regression tests

Four Studio MLX test files shipped without an SPDX-License-Identifier:

  studio/backend/tests/test_mlx_training_worker_config.py
  tests/studio/test_mlx_training_worker_behaviors.py
  tests/studio/test_export_output_path_contract.py
  tests/studio/test_is_mlx_dispatch_gate.py

They sit in or alongside studio/backend/, which is governed by
studio/LICENSE.AGPL-3.0, and exercise AGPL Studio code. Add the same
"# SPDX-License-Identifier: AGPL-3.0-only" header that's already on
test_mlx_inference_backend.py so the license declaration matches
the code under test rather than defaulting to the repo-root
Apache-2.0.

* Wrap MLX submodule imports with friendly install hint

The _IS_MLX block at the top of unsloth/__init__.py already catches the
missing-package case with a friendly install hint, but the follow-up
"from unsloth_zoo.mlx_trainer import ..." and "from unsloth_zoo.mlx_loader import ..."
lines run unguarded. An Apple Silicon user who has unsloth-zoo installed
but on an older version (e.g. the current PyPI release, before the MLX
modules ship) sees a raw ImportError on the submodule rather than the
hint that points at install.sh.

Wrap the two submodule imports in the same try/except shape so the
friendly install message fires whether the package is missing entirely
or just predates the MLX submodules. No-op once both packages release
together; smooths the transitional window where unsloth/main has merged
but unsloth-zoo on PyPI has not.

---------

Co-authored-by: DoubleMathew <mmathew23@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-05-05 23:54:58 -07:00
Daniel Han
be874c72e6 Update pyproject.toml 2026-05-05 05:06:30 -07:00
Daniel Han
b09aa82a3a
Studio: add github_repo seed reader and GitHub Support Bot recipe (#5169)
* Studio: add github_repo seed reader and GitHub Support Bot recipe

Adds a first-party Data Designer seed reader that scrapes GitHub issues,
pull requests, and commits from one or more repositories via the GraphQL
API, and a learning recipe (GitHub Support Bot) that turns those rows into
synthetic support Q&A pairs for fine-tuning.

Backend (new plugin studio/backend/plugins/data-designer-github-repo-seed):
* GitHubRepoSeedSource config: repos, token (falls back to GH_TOKEN /
  GITHUB_TOKEN env var), item_types (issues / pulls / commits),
  per-resource limit (0 means all), max_comments_per_item.
* Rate-limit-aware GraphQL client (GitHubClient + RepoScraper) shared
  across repos; flattens each item into a uniform row with columns
  item_type, repo, number, title, body, state, author, created_at,
  closed_at, url, labels, comments.
* Registered via the data_designer.plugins entry point.

Frontend:
* New seed_github block variant so the seed node card shows
  "GitHub repositories" instead of the generic "Document file"
  placeholder, with its own icon and inline summary (repo count +
  item-type list).
* Rewritten seed dialog github_repo form: repos textarea pre-filled with
  unslothai/unsloth + unslothai/unsloth-zoo, password input for the GH
  token, items-per-repo number with an "All" toggle, and the noisier
  options (item types, max comments, include comments) tucked under an
  Advanced collapsible.
* Local model auto-load on Run: if a recipe uses an is_local provider
  and the inference server is not already serving that model, the
  executions hook calls /api/inference/load first. Removes the "open
  /chat to load a model" prerequisite that users kept tripping on.
* Honor the recipe's run.rows value in the Run dialog (previously the
  store reset to 5 regardless of what the template shipped).

Recipe (studio/frontend/src/features/data-recipes/learning-recipes/
github-support-bot.json):
* Defaults to the Local Model provider + unsloth/gemma-4-E2B-it-GGUF.
* Scrapes unslothai/unsloth and unslothai/unsloth-zoo, issues and pulls,
  up to 100 items per resource.
* Two LLM blocks: normalized_question (llm-text) rewrites each thread
  into a clean support question, support_answer (llm-structured)
  produces JSON with answer / diagnosis_questions / cites / confidence.
* Run defaults to 10 rows for a quick smoke test.

Verified end-to-end on a running Studio: card renders, source-data
dialog is pre-populated, All toggle disables the limit input, the
recipe executes and produces rows against a loaded local GGUF.

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

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

* fix: improve GitHub recipe support

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

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

* Studio: speed up GitHub scraper and harden the support-bot recipe

Addresses a perf issue found while demoing the github_repo seed reader:

Scraper is too slow at scale. The PRs GraphQL query pulls deeply nested
fields (reviewThreads, reviews, commits, timelineItems, etc.) so the
page size was pinned at 3 to stay under GitHub's node-count ceiling. 100
PRs meant 34 serial round trips. Added lighter query variants
(PRS_PAGE_QUERY_LIGHT, ISSUES_PAGE_QUERY_LIGHT) that drop the fields the
Studio flatten layer does not use (it only reads title, body, state,
author, labels, comments). With the light query PR pages can safely go
to 25 per page and issues to 50. The plugin scraper now passes
light=True to RepoScraper so Studio always uses the fast path; the heavy
query remains available for other callers.

Recipe defaults are now demo-ready with production knobs called out:
- max_parallel_requests: 1 and max_tokens: 800 so small local models
  stay stable when running the support_answer structured column.
- support_answer prompt trimmed to 80-200 words so gemma-4-E2B GGUF can
  actually comply with the schema. The canonical 150-300 word codex
  prompt is still documented in the node3 markdown note for
  production upgrades.

* Studio: rename GitHub recipe to 'GitHub Scraper' and add Easy mode

Changes the recipe framing from a single-purpose 'Support Bot' pipeline
to a general-purpose scraper that produces {user_request,
grounded_response} training pairs. Aligns with the canonical
github_data_gatherer dataset (11 enrichment tasks mirrored in pr_requests_20
/ issue_requests_20 on the input side and explain_pr / issue_fix_plan /
issue_solution on the output side).

Recipe JSON changes:
- columns[0] renamed normalized_question -> user_request, prompt now
  inverts a GitHub thread into a realistic user ask instead of
  normalising it.
- columns[1] renamed support_answer -> coauthor_response, emits
  {response, followups, cites, task, confidence} and branches on
  issue vs PR thread type.
- Notes rewritten to document the 11-task catalog and the canonical
  production prompt to paste in for a full dataset backfill.

Frontend: Easy mode for github_repo recipes. The drag-and-drop canvas is
hidden behind an 'Advanced' tab; Easy mode is the default for any recipe
whose seed_source_type is github_repo. The Easy form reuses the existing
GithubRepoSeedForm (promoted to exported), adds a rows input bound to
previewRows, a model field bound to the model_config, and a single Run
button that calls runPreview() directly (no modal). Non-github recipes
see the same Editor / Runs tabs as before.

View mode persists per-recipe-id in localStorage under
recipe-studio:view-mode:<recipeId>.

* Studio: auto-detect server GH_TOKEN and widen Easy-mode detection

The GitHub seed form now fetches /api/data-recipe/seed/github/env-token
on mount and, when the server exposes a GH_TOKEN / GITHUB_TOKEN env var
and the token field is blank, shows a small 'Using server env var' badge
and swaps the placeholder text. The token value itself is never returned
to the UI.

Widens Easy-mode detection in recipe-studio-page.tsx so that recipes
saved before ui.seed_source_type was persisted also get the Easy tab:
falls back to recipe.seed_config.source.seed_type, which is always
present for github_repo seeds.

* fix: polish GitHub recipe UI

* Studio: default llama-server --threads to -1 (auto)

Previously we passed --threads only when the caller set an explicit
value, which meant llama-server fell back to its internal default.
That default has varied across llama.cpp builds (some versions use
hardware concurrency including hyperthreads, which hurts throughput on
CPU-heavy inference). Always passing --threads -1 pins the behaviour
to llama.cpp's auto-detect (physical cores).

Caller-supplied n_threads still wins when non-None.

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

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

* Studio: auto-switch Easy mode to Runs pane on run start

Easy mode had no progress island or canvas overlay, so after clicking Run
the only visible state was the button label flipping to "Running..." while
the screen otherwise stayed identical. This reads as stuck even though the
job is progressing.

Wire an onExecutionStart callback from recipe-studio-page.tsx through to
useRecipeExecutions so that when a run is kicked off from easy mode, the
page flips to the executions view where the Runs sidebar, progress bar,
rate/ETA panel, and live log are rendered. Advanced/editor mode keeps its
existing behavior and stays on the canvas (it already has the floating
ExecutionProgressIsland).

* fix: clean up GitHub scraper layout

* Studio: forward llm-structured output_format as llama-server response_format

Local GGUF runs of llm-structured columns used to generate the full
max_tokens budget before the prompt-level "return JSON in a ```json
fence" instruction got parsed. Small models (e.g. gemma-4-E2B-it)
routinely broke format, so each row took ~65s and frequently failed
with "No parsable JSON structure within ```json markdown fence".

For any local-provider model_config referenced by an llm-structured
column, clone the model_config and inject response_format into the
clone's inference_parameters. Uses llama.cpp server's flat shape
(tools/server/README.md):

    {"type": "json_schema", "schema": <output_format>}

Not the OpenAI-nested form; data_designer's OpenAI adapter forwards
response_format verbatim via facade._COMPLETION_REQUEST_FIELDS, and
llama-server's documented schema path expects the flat variant.

The clone is per (model_alias, column) so:
- llm-text / llm-judge columns that share the same alias keep
  free-form sampling.
- Each structured column gets its own schema, so columns with
  different output_formats don't collide.

Effect on gemma-4-E2B-it demos: every row parses cleanly, and the
model terminates immediately after the closing brace instead of
running to max_tokens. Net wall-clock is usually faster even though
grammar-constrained sampling is slightly slower per token.

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

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

* Studio: flip Easy to Runs pane before validation scrape, not after

Previously onExecutionStart fired inside runExecution, which runs AFTER
validateRecipe() -- and validation re-invokes the seed reader. For the
github_repo reader that is a full GraphQL scrape, so the user sat on a
"Running..." button with an otherwise unchanged Easy form for 10-15s
before anything moved.

Call onExecutionStart at the top of runWithValidation, right after we
have a payload to send. The view flips immediately; ensureLocalModelLoaded
+ validateRecipe now run against the Runs pane instead of a frozen Easy
form. runExecution still calls onExecutionStart downstream, but the
callback is idempotent (the page's easy -> executions guard skips the
second call), so no behaviour change for runs that pass validation.

If validation fails the toast + runErrors path still fires; the Easy
form's error banner still reads runErrors when the user switches back.

* Studio: unify data-recipe workflow auth on sk-unsloth-* keys

The previous commit (a61b4cc9) assumed storage.create_api_key(..., internal=True)
and storage.revoke_internal_api_key(key_id) existed, but those helpers were
only in the working tree, never committed. Recipe runs in local-model mode
were therefore crashing with 500 when _inject_local_providers tried to mint
a workflow key. This commit ships the missing pieces.

auth/storage.py:
- api_keys schema gains is_internal INTEGER DEFAULT 0 (with a guarded
  ALTER TABLE migration so existing auth.db files upgrade in place).
- create_api_key takes an internal=False kwarg; internal keys are flagged
  so they can be hidden from user-facing listings.
- list_api_keys takes include_internal=False so UIs never see workflow keys.
- New revoke_internal_api_key(key_id): id-only revoke for keys minted by
  non-user subjects (the JobManager does not know a username).

core/data_recipe/jobs/manager.py:
- JobManager.start accepts internal_api_key_id and stores it on Job so
  lifecycle handlers can revoke eagerly.
- _handle_event revokes on EVENT_JOB_COMPLETED / _ERROR / _CANCELLED.
- _pump_loop subprocess-died fallback also retires the key so a crashed
  worker cannot leak a live sk-unsloth-* beyond its TTL.
- Revocation is best-effort (swallow exceptions) -- the 24h TTL is the
  safety net if storage hiccups.

core/data_recipe/jobs/types.py:
- Job dataclass gains internal_api_key_id: int | None = None.

Replaces the bespoke 24h JWT path that jobs.py used to mint for local
providers. One mint/revoke/verify surface for every API key the server
issues, and revocation is now eager (seconds, not 24h) instead of TTL-only.

* Studio: plug workflow-key leak on unexpected create_job errors

Review follow-up on the sk-unsloth-* workflow-key lifecycle in
create_job. Previously the revoke handlers wrapped mgr.start(...) but
only caught RuntimeError and ValueError, and get_job_manager() sat
outside the try block entirely. Any other exception type (TypeError
from a mismatched kwarg, OSError from the queue write, etc.) would
bubble up to FastAPI and leave the minted key live until its 24h TTL.

Fix: one try block covers both get_job_manager() and mgr.start(), with
a trailing except Exception that revokes and re-raises. The
RuntimeError -> 409 and ValueError -> 400 paths are unchanged so
specific client-facing status codes still surface. Revocation is still
best-effort (_revoke_internal_api_key_safe swallows errors) because we
never want revoke failures to mask the original crash.

Severity is low -- the key can't bootstrap longer access and the 24h
TTL bounds the window -- but the reviewer's point stands: eager revoke
on every failure path is the right invariant.

* Studio: nest response_format under extra_body so pydantic accepts it

The previous commit dropped response_format at the top level of a cloned
model_config's inference_parameters, which BuilderConfig rejected with:

  ValidationError: Extra inputs are not permitted [type=extra_forbidden]
  data_designer.model_configs.1.inference_parameters.response_format

data_designer's BaseInferenceParams is a pydantic model with extra=forbid
and only a fixed set of fields (temperature, top_p, max_tokens,
max_parallel_requests, timeout, extra_body). The pass-through path for
anything the schema doesn't know about is `extra_body`, which the
OpenAI SDK spreads into the chat-completions request body at the top
level -- which is exactly where llama-server reads response_format from.

Inject under extra_body (merging with any existing extra_body contents)
so the clone validates. llama-server still receives
{"type": "json_schema", "schema": <output_format>} at the top level of
the request body, which is the flat shape llama.cpp's server expects.

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

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

* Studio: forward response_format to llama-server and fence-wrap the reply

Two-part fix for the llm-structured data-recipe path:

(1) The /v1/chat/completions proxy was dropping response_format. The
route's passthrough branch only triggered on tools / tool messages, so
requests carrying a JSON schema fell into the non-passthrough GGUF path
which calls generate_chat_completion (no response_format kwarg). The
schema never reached llama-server, so guided decoding was a no-op and
the model emitted free-form text that happened to parse a fraction of
the time. Widen the passthrough trigger and teach _build_passthrough_payload
to forward response_format so llama-server's GBNF grammar actually runs.

Guided decoding does not require supports_tools, so split the condition:
a request is now passthrough-routed if it carries tools/tool messages
(existing behavior) OR carries response_format (new). The vision guard,
streaming fork, and tools-choice defaulting are unchanged.

(2) data_designer's llm-structured parser looks for a ```json ... ```
markdown fence and discards anything else. Guided decoding emits only
the JSON object (the GBNF grammar has no fence tokens), so a
100%-valid schema-constrained run still ended up 0 ok / N failed with
"No parsable JSON structure within ```json markdown fence". In
_openai_passthrough_non_streaming, wrap each choice's content in the
expected fence when the caller asked for guided decoding. Already-fenced
content is left alone so other clients that prefer raw JSON are not
affected; the wrap is scoped to requests that carried response_format.

Net effect on the GitHub Support Bot recipe on a local GGUF: schema
actually binds during sampling, content arrives wrapped in the fence
data_designer expects, and generation terminates immediately after the
closing brace instead of running out to max_tokens.

* Studio: Easy mode runs a full run, capped at the user's row count

Easy mode used to call runPreview, which produces a test run: no
artifact persisted, reduced progress tracking, and framed in the Runs
pane as "Test run". The whole point of the form is to let a user kick
off a real dataset build with one click, so wire it to runFull instead
and bind the Rows input to fullRows (not previewRows).

runFull requires a non-empty fullRunName. The Easy form has no run-name
input, so seed a default on mount whenever Easy is active and
fullRunName is still empty. Uses `<recipe name> <iso-timestamp>` so
each Easy run gets a stable-ish default that still sorts chronologically
in the Runs pane. User can override it from the Advanced run dialog
before clicking Run.

Rename GithubScraperEasyView's rows props from previewRows/setPreviewRows
to rows/setRows so the view stays agnostic to which hook state the page
chooses to bind. Loading indicator now follows fullLoading.

* Studio: clamp GitHub scrape page size and memoize the materialization

Two wins for the "before Generating fires" gap on small previews:

(1) scrape_{issues,prs,commits} hardcoded per_page (50 / 25 / 100) and
only checked the trial limit AFTER the page was written, so a 1-row
Easy run still asked GitHub for a full 50-issue + 25-PR page, wrote
them all to JSONL, and then stopped because total_new already exceeded
the trial cap. Cap per_page at min(page_cap, trial_limit) so
github_limit=1 actually asks for first:1.

(2) GitHubRepoSeedReader.get_dataset_uri used to scrape fresh on every
invocation. data_designer calls the seed reader multiple times per
recipe job (validation, preview, per-column sampling), so a 2-repo
Easy preview ran the full GraphQL scrape three times back-to-back,
burning ~15s of dead air before any LLM generation began.

Added a module-level in-process cache keyed on
(repos, item_types, limit, include_comments, max_comments_per_item,
sha256(token)[:16]) that stores the JSONL path of the first
materialization. Subsequent calls with the same signature return the
cached path, guarded by a staleness check that drops the entry if the
file was tmp-cleaned. Raw token values never land in the key.

Net effect on a 1-row Easy run, 2 repos, limit=1: 2 GraphQL round
trips instead of ~12, and the first-to-Generating gap collapses from
~15s to roughly 2-3s.

* Studio: make Easy mode Rows input editable instead of snapping to 1

The Rows to generate input used type="number" with value bound directly
to the rows state and an onChange that coerced any non-positive parse
result back to 1. The moment the user pressed backspace to clear the
field, the parent re-rendered with value=1 and the caret jumped, making
it impossible to change the value without arrowing the browser's +/-
spinner.

Switch to a text input with inputMode="numeric" and pattern="[0-9]*"
(so mobile still shows a numeric keyboard, and the browser drops the
spinner buttons the user did not want). Add a local rowsText buffer so
the field can hold transient empty / partial digit strings while
editing without fighting the parent state; the canonical rows value
only advances when the buffer parses to a valid integer in [1, 10000],
and onBlur clamps back to 1 or 10000 if the user left it out of range.

No behavior change for valid numeric edits - the downstream runFull()
still sees a clean positive integer.

* Studio: expand dataset cells horizontally by column on click

Click a long cell to expand that whole column. Click again to collapse.
Replaces the prior row-level vertical expansion which made it hard to
compare cells across columns. State is scoped per execution and per
column; the row itself is no longer a click target.

* Studio: force expanded dataset column to grow wide enough to read

* Studio: disable thinking for local recipe inference and plumb the kwarg

Reasoning-capable models (gemma-3n, qwen3.5, etc.) emit a
<think>...</think> preamble ahead of the answer by default, which
roughly doubles the generated token count per row on a local GGUF
and pushes the actual answer past data_designer's json-fence regex
on llm-structured columns. Recipes want the terse answer, not the
scratchpad.

Two halves of the fix:

(1) routes/data_recipe/jobs.py: when _inject_local_providers walks
the recipe's model_configs to point them at the local endpoint, also
stash chat_template_kwargs={"enable_thinking": false} under each
config's inference_parameters.extra_body. OpenAI SDK spreads
extra_body into the top-level request body, so llama-server and the
Studio /v1/chat/completions route both see it.

(2) routes/inference.py: the chat-completions route previously
dropped chat_template_kwargs on the floor because the whitelist
body builder only forwarded known fields.

    - At the top of openai_chat_completions, lift
      chat_template_kwargs.enable_thinking from payload.model_extra
      onto the typed payload.enable_thinking field when the caller
      did not set the latter, so the non-passthrough GGUF path's
      generate_chat_completion(...) call honors the override.
    - Teach _build_passthrough_payload to forward a
      chat_template_kwargs dict, and have _build_openai_passthrough_body
      derive that dict from payload.enable_thinking so
      response_format requests (structured columns) also land at
      llama-server with the reasoning preamble suppressed.

Net effect on a 10-row support-bot run with gemma-4-E2B-it-GGUF:
responses arrive without <think> tags, wall-clock per call drops
roughly in half, and structured columns stop leaking reasoning
tokens through the GBNF-constrained output.

* Studio: update GitHub Support Bot learning recipe with maintainer layout

Replace the template with the hand-laid-out export from the maintainer
so note nodes ship with real x/y positions (scattered around the
graph instead of all stacked at x=480) and the edges / canvas pan look
correct on first load. Also picks up the maintainer's prompt tweaks and
output schema names (coauthor_response / user_request / followups / task /
cites / confidence).

Diff is mostly ui.nodes positions and prompt bodies; runtime shape is
unchanged (seed_config / columns still target model_1 against the Local
Model provider).

* Studio: auto-size dataset sample columns; wide text gets a wide column

Drop the per-column click-to-expand toggle and the 180-char truncation.
Every column now renders its full value. Columns with long text get a
min-w of 48rem so the text is readable without wrapping into a tall
block; narrow-content columns get a 12rem min-w. The table wrapper
already has overflow-x-auto, so wide-column totals cause a horizontal
scrollbar instead of cramming everything into the viewport.

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

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

* fix GitHub scrape progress

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

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

* add resetApiBase export for test setup

* Studio: rename github-support-bot output columns to User / Assistant

Previously emitted user_request and coauthor_response, which did not
match the canonical User / Assistant chat-pair shape that downstream
SFT consumers expect. Renamed the columns in the recipe JSON (columns,
UI node ids, edges, notes, prompt Jinja refs) and the matching copy in
the learning-recipes index, data-recipes-page, and easy view.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: wasimysaid <wasimysdev@gmail.com>
2026-04-24 12:02:03 -07:00
Daniel Han
d20b306755 Versioning 2026-04-16 12:06:10 -07:00
Daniel Han
ba387e2c8f Update pyproject.toml 2026-04-15 08:06:30 -07:00
Daniel Han
4c83e3540e Update 2026-04-06 09:20:17 -07:00
Daniel Han
8ffd5826f2 Gemma-4 2026-04-02 11:59:37 -07:00
Daniel Han
cfeb8c3245 Versioning 2026-03-31 06:51:34 -07:00
Daniel Han
a0bca759f3
Fix editable install scanning 6,500+ node_modules dirs (#4697)
* fix: scope packages.find to prevent node_modules namespace scanning

The packages.find section had no include filter, so setuptools'
find_namespace_packages discovered all directories as potential Python
packages -- including the 6,557 directories inside
studio/frontend/node_modules/ after the frontend build step.

This caused the editable install overlay step to run 20,000+ glob
operations across 6,619 "packages", which on fast NVMe takes ~5s but
on slower disks can take 7+ minutes.

Adding an explicit include filter scopes discovery to only the packages
we actually ship (unsloth, unsloth_cli, studio, studio.backend), dropping
from 6,619 to 58 discovered packages and the editable build time from
5.4s to 1.2s.

Also removes the broken kernels/moe exclude (used "/" instead of "."
notation so it never matched) and adds a node_modules exclude as a
safety net.

* fix: use precise node_modules exclude patterns

Use "*.node_modules" and "*.node_modules.*" instead of "*.node_modules*"
to avoid accidentally excluding valid packages that might contain
"node_modules" as a substring in their name.
2026-03-30 02:40:29 -07:00
Daniel Han
844a816ed0 Update pyproject.toml 2026-03-27 07:14:03 -07:00
Daniel Han
3108750bb0
Remove duplicate frontend assets from wheel to reduce package size (#4567)
The wheel currently ships frontend/public/, frontend/src/, and
frontend/*.lock alongside frontend/dist/. These are build-time inputs
that Vite already copies into dist/ during the build step:

- public/ is copied verbatim into dist/ by vite build (28.6 MB duplicate)
- src/ is TSX source compiled into dist/assets/*.js (2.1 MB, not used at runtime)
- *.lock files are package manager lockfiles (0.9 MB, not used at runtime)

The backend only serves from frontend/dist/ (see main.py setup_frontend
and run.py frontend_path). Nothing references public/ or src/ at runtime.

This drops the wheel from ~62.7 MB to ~31 MB.
2026-03-24 23:48:49 -07:00
Daniel Han
fac6f7887e Versioning 2026-03-24 06:50:36 -07:00
Daniel Han
9c95148045
Fix tool call parsing, add tool outputs panel and UI improvements (#4416)
* Add elapsed timer to tool status pill in Studio

Show a count-up seconds timer (0s, 1s, 2s, ...) next to the tool status
text in the composer area. Helps users gauge how long a tool call (web
search, code execution) has been running. Timer resets when a new tool
starts and disappears when all tools finish.

* Fix tool call parsing, add tool outputs panel and reasoning copy button

Backend:
- Rewrite tool call XML parser to use balanced-brace JSON extraction
  instead of greedy regex, fixing truncation on nested braces in
  code/JSON arguments
- Handle optional closing tags (</tool_call>, </function>, </parameter>)
  that models frequently omit
- Support bare <function=...> tags without <tool_call> wrapper
- Strip tool call markup from streamed content so raw XML never leaks
  into the chat UI
- Use a persistent ~/studio_sandbox/ working directory for tool
  execution so files persist across calls within a session
- Emit tool_start/tool_end SSE events so the frontend can display
  tool inputs and outputs

Frontend:
- Add collapsible "Tool Outputs" panel below assistant messages showing
  each tool call's input and output with copy buttons
- Add copy button to reasoning blocks
- Add elapsed timer to tool status pill
- Update project URLs in pyproject.toml (http -> https, add docs link)

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

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

* Add interactive HTML preview with fullscreen toggle for code blocks

HTML code fences now render an interactive sandboxed iframe preview
below the syntax-highlighted code, similar to how SVG fences show
an image preview. The iframe uses sandbox="allow-scripts" to allow
JavaScript execution while blocking access to the parent page.

Includes a fullscreen toggle (enlarge/minimize button) that expands
the preview into a viewport overlay, dismissible via button, Escape
key, or backdrop click. A streaming placeholder prevents partial
HTML from rendering mid-stream.

* Add tool call settings: auto-heal toggle, max iterations, timeout

Add three user-configurable tool call settings to the Studio Settings panel:

- Auto Heal Tool Calls: toggle to control fallback XML parsing of malformed
  tool calls from model output (default: on)
- Max Tool Calls Per Message: slider 0-40 + Max to cap tool call iterations
  per message (default: 10)
- Max Tool Call Duration: slider 1-30 minutes + Max to set per-tool-call
  execution timeout (default: 5 minutes)

All settings persist to localStorage and flow through the full stack:
frontend store -> API request -> Pydantic model -> route -> llama_cpp -> tools.

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

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

* Fix tool call timeout: respect no-limit and apply to web search

- Use a sentinel to distinguish timeout=None (no limit) from the default
  (300s). Previously None was silently replaced with _EXEC_TIMEOUT.
- Pass the configured timeout to DDGS() for web searches so the setting
  applies uniformly to all tool types.

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

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

* Add input validation bounds and per-thread sandbox isolation

- Add ge=0 constraint to max_tool_calls_per_message (rejects negative values)
- Add ge=1 constraint to tool_call_timeout (minimum 1 second)
- Thread session_id from frontend through backend to tool execution
- Scope sandbox directories per conversation: ~/studio_sandbox/{thread_id}/
- Backwards compatible: API callers without session_id use ~/studio_sandbox/

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

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

* Fix non-monotonic streaming and Python temp script path

- Split tool markup stripping into closed-only (mid-stream) and full
  (final flush) to prevent cumulative text from shrinking mid-stream
- Enforce monotonicity: only emit when cleaned text grows, so the
  proxy's delta logic (cumulative[len(prev_text):]) never breaks
- Place Python temp scripts in the sandbox workdir instead of /tmp so
  sys.path[0] points to the sandbox and cross-call imports work

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

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

* Sanitize session_id to prevent path traversal in sandbox

Strip path separators and parent-dir references from session_id before
using it as a directory name. Verify the resolved path stays under
~/studio_sandbox/ as a second guard.

* feat(chat): proper assistant-ui tool call UIs with sources

Replace custom metadata-based ToolOutputsGroup with native assistant-ui
tool-call content parts. Backend SSE tool_start/tool_end events now emit
proper { type: "tool-call" } parts from the adapter, enabling per-tool
UIs registered via tools.by_name in MessagePrimitive.Parts.

- Web search: Globe icon, Source badges with favicons, auto-collapse
  when LLM starts responding
- Python: Code icon, syntax-highlighted code via Streamdown/shiki,
  output block with copy
- Terminal: Terminal icon, command in trigger, output with copy
- ToolGroup wraps consecutive tool calls (skips for single calls)
- Sources component renders URL badges at end of message
- Flattened code block CSS (single border, no nested boxes)

* fix(inference): respect empty enabled_tools allowlist

`if payload.enabled_tools:` is falsy for [], falling through to
ALL_TOOLS. Use `is not None` so an explicit empty list disables
all tools as intended.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Shine1i <wasimysdev@gmail.com>
2026-03-18 08:28:02 -07:00
Daniel Han
e9fa12acd3 Update pyproject.toml 2026-03-18 07:26:40 -07:00