v0.75.1 is a backport, not a cut of main. Its tree is tag v0.75.0 plus 25 cherry-picks, one
test adaptation and the release commits. main was 186 commits past v0.75.0 when this release was
cut, so releasing from main would have shipped an entire minor's worth of unreleased work under a
patch number. The diff of v0.75.0...v0.75.1 is exactly what these notes describe.
What's New
-
A layer-streamed LoRA adapter saved as ZERO tensors under peft 0.21.0 (#1005, fixed in #1010).
peft 0.21 selects an adapter's tensors by the prefixes it reads offmodel.named_modules(), where
0.20 filtered by thelora_substring. The streaming wrapper's module and parameter names carried
.inner.while itsstate_dict()keys were canonical, sotrainer.save_model(), every
save_stepscheckpoint andget_peft_model_state_dict()returned nothing — and nothing raised.
Any adapter saved withstream_layers: trueon peft>=0.21 before this fix holds zero tensors (a
40-byteadapter_model.safetensors) and cannot be recovered: re-train it. Naming only; nothing
on the streamed forward path changes. -
soup runs clean --keep-weightsdid the opposite of what it said on Click 8.1 (#1057). The
option was a bare flag with aTruedefault, which Click 8.1 treats as a toggle, so typing
--keep-weightsparsed asFalseand deleted whole non-best checkpoints. It is now the paired
--keep-weights/--no-keep-weights: the default and--keep-weightskeep weights on every Click
version, and--no-keep-weightsis the one way to delete them. A repo-wide ratchet now fails any
True-default flag that has no negative form. -
Cloud runs keep their outputs and their cleanup (#1058, with the follow-up #1073 in #1078).
soup train --cloud modalran training in an ephemeral container while the local entrypoint
printed "download checkpoints to<dir>" with nothing to download; outputs now live on the named
soup-outputsvolume, are committed in afinally, and are downloaded with destination
containment. On Lambda, Ctrl+C during--cloud-submitcould kill the lifecycle controller while it
was inside thefinallythat terminates the paid instance; the parent now keeps waiting across the
interrupt, and the follow-up closes the two remaining windows (a signal betweenPopenreturning
and the guarded region being entered, and one landing on the notice write). -
Hardening across eight areas — see Security below.
Install / Upgrade
pip install --upgrade soup-cli
# or with the training stack
pip install --upgrade "soup-cli[train]"Security
This release is the fix for security advisory
GHSA-63h4-gvp4-r26g,
which has the full detail, the affected version range of each issue, and upgrade guidance. Summary:
- Credentials sent to the wrong service.
soup push --hub modelscope|modelersresolved the
Hugging Face token for every hub and presented it to that third party (0.53.10-0.75.0). A judge
URL of the formhttps://<host>/<model>was classified as the OpenAI provider for every host,
soOPENAI_API_KEYwas sent as a Bearer token to it (0.33.0-0.75.0). Each hub now authenticates
only with its own credential, and onlyapi.openai.comreceivesOPENAI_API_KEY. soup adapters verify --public-keyaccepted unsigned adapters (0.71.2-0.75.0): the trusted
key was consulted only inside the ed25519 branch, so a record with any other backend verified
with exit code 0 even under--strict.- The Web UI rendered dataset and run content as HTML (0.7.0-0.75.0) and sent no
Content-Security-Policy. All rendering now goes through one escaping template, inline handlers
are gone, and the UI sends a CSP withoutunsafe-inlinefor scripts plusX-Content-Type-Options,
Referrer-PolicyandX-Frame-Options; Chart.js is pinned with Subresource Integrity. soup servetool and adapter routes checked neitherHostnorOrigin(tool routes
0.53.7-0.75.0, adapter routes 0.30.0-0.75.0), so a web page the operator visited could reach
them. They now answer 421 for a foreign Host and 403 for a foreign Origin, and the tool token is
compared in constant time. Inference routes are deliberately unchanged so a reverse proxy can
still front them.
Also (added 2026-09-25): with--tool-auth-tokenset,GET /v1/adapters,
POST /v1/adapters/activate/{name}andPOST /v1/adapters/deactivateignored the token in
0.71.27-0.75.0, so on a non-loopback bind anyone who could reach the port could list adapters and
switch the one being served. They now require it, so a client that listed adapters without the
token gets 401.soup mcp serve --allow-executedid not pin every approved input (0.73.3-0.75.0): several
config paths could change between plan approval and execution, the one-active-execution check read
only the 50 newest runs, and a bookkeeping failure after spawn left a child running unsupervised.- Config regexes could hang loading or training (
lr_groups0.41.0-0.75.0,
unfrozen_parameters0.71.23-0.75.0). The old guard was itself the problem — it ran the untrusted
pattern against a 128-character probe. It is replaced by a structural check that parses the
pattern and never matches with it. - Credential option values were written to the audit log (0.71.3-0.75.0) whenever they did not
start withhf_,sk-orBearer. Masking is now by option name. - Reading an untrusted
.canarchive could exhaust memory through YAML alias expansion or
unbounded member reads, and manifest fields could emit terminal control sequences
(0.26.0-0.75.0).
Rotate these credentials
- Ran
soup push --hub modelscopeor--hub modelerson 0.53.10-0.75.0 while logged in to Hugging
Face or withHF_TOKENset? Rotate your Hugging Face token at
https://huggingface.co/settings/tokens. - Had a judge URL on 0.33.0-0.75.0 pointing anywhere other than
api.openai.comwhile
OPENAI_API_KEYwas set? Rotate that key. - Passed
--tool-auth-token,--auth-tokenor--api-keyon 0.71.3-0.75.0 and collect your audit
log anywhere? Rotate those tokens and purge the log.
Found by a full-repository review with OpenCodeReview, verified by the maintainer.
Known Limitations
- 26 tests fail on the maintainer's dev box and none of them is a regression. All are the
NF4/bitsandbytes/streaming files, and they fail with identical IDs on a cleanv0.75.0
git-archive copy — the control that makes "not a regression" checkable. The underlying divergence
is #776 (two bitsandbytes 4-bit compute paths disagreeing on sm_120); CI cannot see it because
those modules areskipif(not torch.cuda.is_available())and GitHub runners have no GPU. - The POSIX SIGINT test for the Lambda controller runs on CI only. It needs a real process group
and a real signal, so it is skipped on Windows. The platform-independent half (a stderr whose
first write raisesKeyboardInterrupt) runs everywhere. - The Web UI token still lives in
sessionStorage. The escaping work removes the sinks that
could read it from the page and the CSP forbids inline script and eval, but the storage choice
itself is unchanged. - Three regex sinks still have no complexity check:
eval/custom.py,utils/diagnose/format.py
andutils/recipe_run.py. The check shipped where config-supplied patterns reach peft and the
trainers; these three take patterns from other inputs and were deliberately left out of a
security release rather than swept in. soup can run's extracted config still goes through the generic loader, bounded now by the
pre-runread_configcheck, but the loader itself is unchanged.
Release record
- Gate record: no gate. Nothing in this release was decided by a new measurement; every item is a
correctness or hardening fix with its own test, so no file was added tobenchmarks/. Stated here
rather than skipped silently. - Preprint: unchanged. No measured number the preprint states moves, and its scope does not
change — nothing here touches layer streaming, its mechanism or its architecture list. - Suite: the authoritative signal for the tagged tree is the CI matrix on
d1b28dd2, 14/14 green.
Locally the backport ran 21518 passed / 26 failed on Windows + Python 3.12; the same 26 fail with
identical IDs on a cleanv0.75.0copy, which is the control that makes "zero regressions"
checkable. 500 test files, 21734 collected (5 deselected), up from 475 / 21264 at v0.75.0.
Contributors
Every change in this patch release is the maintainer's own work, so there is no contributor list this
time. v0.75.0 — the release this one patches — carried 60 pull requests from 22 people outside the
maintainer, and they are credited in its notes.