110 KiB
Security Policy
Supported Versions
We provide security updates for the following versions:
- Latest minor version: Active support (e.g., v0.20.x)
- Previous minor versions: Bug-fix support only
- Versions older than 3 minor versions: No support
Example:
- v0.53.0 -- Full support (latest)
- v0.52.0 -- Full support
- v0.51.0 -- Full support
- v0.50.0 -- Full support
- v0.49.0 -- Full support
- v0.48.0 -- Bug-fix support only
- v0.47.0-v0.47.x -- Bug-fix support only
- v0.46.x and below -- No support
Reporting a Vulnerability
Do not open a public issue or pull request for security vulnerabilities.
Instead, use GitHub Security Advisories to report privately, or email vpn.alpamys@gmail.com with:
- Description: A clear explanation of the vulnerability
- Steps to Reproduce: How to trigger or demonstrate the issue
- Affected Versions: Which Soup versions are impacted
- Suggested Fix (optional): Any proposed solutions
- Contact Info: Your email for follow-up (optional)
What to Include
To: vpn.alpamys@gmail.com
Subject: Security Vulnerability Report: [Brief Title]
Description:
[Explain the vulnerability in detail]
Affected Component:
[e.g., data/loader.py, trainer/sft.py, etc.]
Steps to Reproduce:
1. [Step 1]
2. [Step 2]
3. ...
Impact:
[What could go wrong? Data exposure? RCE? DoS?]
Suggested Fix (optional):
[Your proposed solution, if any]
Response Timeline
- Initial Response: Within 48 hours
- Assessment: 1-3 business days
- Fix Development: Varies by severity
- Patch Release: As soon as possible after fix verification
- Public Disclosure: Coordinated with reporter (typically 90 days after patch release)
Severity Levels
- Critical: Remote code execution, data exposure, complete compromise (patch within 24-48 hours)
- High: Authentication bypass, privilege escalation, denial of service (patch within 1 week)
- Medium: Information disclosure, partial compromise (patch within 2 weeks)
- Low: Minor issues with limited impact (patch in next regular release)
Security Best Practices
When using Soup, follow these practices to stay secure:
1. Keep Soup Updated
pip install --upgrade soup-cli
2. Protect API Keys
Never commit API keys or secrets to version control. Use environment variables:
export HUGGINGFACE_TOKEN=your_token_here
export WANDB_API_KEY=your_key_here
soup train
3. Validate Data
- Only use trusted datasets
- Verify checksums for large datasets
- Inspect data for malicious content before training
4. Model Permissions
- Be cautious when downloading models from untrusted sources
- Use model hub providers with verified publishers (HuggingFace, Meta, etc.)
- Keep track of which models you've fine-tuned and their base model sources
5. GPU/Compute Safety
- Run on isolated machines if training on sensitive data
- Clear cache and temporary files after training
- Don't share fine-tuned models containing sensitive information
Known Vulnerabilities
We maintain a log of known security issues and their fixes. This will be updated as issues are discovered and resolved.
Current Status
No known critical vulnerabilities in current releases.
Security Hardening History
-
v0.10.10: Bearer token auth on Web UI, CORS restrictions, path traversal protection, SSRF prevention, max_tokens limits, supply-chain pinning (llama.cpp b5270), deprecated CLI secret flags
-
v0.12.0: experiment_name path traversal validation, GaLore parameter type enforcement
-
v0.13.0: Batch inference max_tokens capped at 16384, trust_remote_code warning
-
v0.14.0: Plaintext loader UTF-8 encoding, MoE config validation (moe_aux_loss_coeff ge=0, moe_lora boolean only)
-
v0.14.3: Data validate auto-detects format, Web UI
--show-tokenflag + auth token documented -
v0.15.0:
rope_scaling_typeLiteral constraint,max_lengthbounds (ge=64, le=1048576), FSDP config key allowlist, Liger Kernel exception handling narrowed -
v0.16.0:
embedding_lossLiteral constraint,embedding_margingt=0 validation, ONNX export without trust_remote_code (with warning), TensorRT export subprocess list args (no shell injection), speculative decoding SSRF-protected (URL blocked) with warning panel, vLLM speculative model URL validation -
v0.17.0: Server data generation provider SSRF validation (scheme whitelist + localhost-only HTTP), audio model trust_remote_code warning panel, audio file path traversal protection (resolved paths confined to audio_dir), SGLang backend trust_remote_code warning panel
-
v0.18.0: Ollama deploy GGUF path traversal protection +
.ggufextension validation, model name validation (no path separators/null bytes), subprocess list args (no shell injection), Modelfile parameter key allowlist + value newline/null sanitization, overwrite warning panel -
v0.19.0: Custom eval JSONL schema validation + 10k task cap, regex scoring ReDoS guard, judge API SSRF protection + API key isolation, human eval local-only terminal UI + 10k prompt cap, leaderboard read-only SQLite queries
-
v0.20.0: Ollama provider localhost-only validation (remote blocked), Anthropic provider API key from env only (never CLI arg), vLLM provider SSRF protection (scheme whitelist + localhost-only HTTP), output path traversal protection (
..blocked), configurable rate limiting (--requests-per-minute) -
v0.21.0: Migrate input/output path traversal protection (resolve + relative_to(cwd)), Unsloth .ipynb AST-only parsing (no exec/eval), recipes output path traversal protection, NEFTune config bounded (ge=0.0, le=50.0)
-
v0.22.0: Multi-adapter serving path traversal protection (resolve + relative_to(cwd)), adapter name validation (alphanumeric + hyphens only), unknown adapter returns 404 (not 500)
-
v0.23.0: AWQ/GPTQ calibration data path traversal protection (resolve + relative_to(cwd)), AWQ/GPTQ output path stays under cwd, curriculum_buckets bounded (ge=1, le=20), AWQ/GPTQ trust_remote_code warning panel
-
v0.24.0: HF download trust_remote_code=False + warning panel, HF download output path sanitized (Path.name), download --samples capped at 1M, dataset registry name validation (no path separators/null bytes), registry path traversal protection, loss_watchdog threshold le=100 + patience le=1000, freeze_layers le=1000
-
v0.24.1: AWQ/GPTQ output path traversal validation moved before import check (previously unreachable when autoawq/auto-gptq not installed), Windows Unicode fix for Rich console output (replaced non-ASCII symbols with ASCII equivalents)
-
v0.24.2: Chat proxy SSRF protection (localhost-only HTTP, HTTPS for remote), chat proxy max_tokens capped at 16384 + temperature/top_p bounded, chat proxy Bearer token auth required, XSS prevention (HTML-escape before markdown render), runs compare max 5 runs, config from-form validates via load_config_from_string, SSE read endpoints no auth (GET)
-
v0.25.0: Tool-calling JSON-only parsing (no eval), RLVR math_verify regex-extracted numerics (no eval), code_exec 5s timeout + 512MB RLIMIT on POSIX + ephemeral cwd + socket patch +
python -I -S+ 10KB output cap, verifiable_domain Literal constraint, LoRA PEFT mutual exclusion (DoRA/VeRA/OLoRA), data augment path containment + caps, forgetting_detection bounds, checkpoint_intelligence bounds + symlink refusal, autopilot path containment (realpath + commonpath) + goal Literal + GPU/time budget bounds, MLX trainers no trust_remote_code -
v0.26.0 — Registry: name/tag validation (alphanumeric +
_-.only, null-byte rejected, name ≤128 / tag ≤64 chars), artifact path containment (defaultenforce_cwd=Trueviaos.path.realpath + commonpath, stored path is realpath), SQL LIKE wildcard escaping (%and_escaped withESCAPE '\\'insearch()and prefixresolve()), DB 600 perms on POSIX, lineage indirect-cycle detection (BFS ancestor walk before insert), CLI Rich markup escaped everywhere,resolve()raisesAmbiguousRefErroron ambiguous prefix (no silent None) -
v0.26.0 — Eval Gate: suite path via shared
utils/paths.is_under_cwdcontainment,regression_threshold[0.0, 1.0],every_n_epochs[1, 100],on_regressionLiteral ("stop"/"warn"/"continue"),GateTask.tasks/promptsnull-byte rejection,judge_modelURL scheme allowlist (ollama://,https://,http://localhost/http://127.0.0.1) — SSRF hardening, callback fails-safe: structured errors treated as regressions underon_regression="stop" -
v0.26.0 — Trace-to-Preference: input/output path containment via shared
is_under_cwd, trace line cap 100,000,--format/--signalLiteral validation, PII warning panel before every run, JSON-only parsing (no eval), malformed JSON lines skipped silently -
v0.26.0 — Quant-Lobotomy:
--before/--after/--tasksall containment-checked,registry://refs support optionalkindsfilter to avoid picking the wrong artifact, format Literal validated -
v0.26.0 — Soup Cans: Manifest format version pinned to 1; name alphanumeric+
_-.; author max 128 chars, no null bytes/newlines; created_at must parse viadatetime.fromisoformat; description max 4096; DataRef URL HTTPS-only; hf_dataset regex-validated; tar extraction usesfilter="data"on Python 3.12+, fallback only onTypeError/AttributeError(notTarError); manual symlink/hardlink rejection +commonpathcheck; 100 MB size cap on pack + fork; dunder-key (__*__) and null-byte rejection in fork modifications to prevent prototype pollution; inspect/read_config refuse paths outside cwd -
v0.27.0 — Multi-GPU Mastery:
--gpusbounds (reject bool, non-digit, zero, negative, values aboveMAX_GPU_COUNT=128);--gpus autoon 0-GPU host prints explicit yellow warning (no silent no-op); Rich markup escaped on--configpath before embedding in the multi-GPU advice Panel;accelerate launchargv assembled viashlex.quoteper element (copy-pasted command safe against crafted paths);build_accelerate_argvvalidatesnum_processes >= 1,mixed_precisionLiteral (no/fp16/bf16/fp8),num_machinesbounded[1, 256]; ZeRO++ integer literals (int(1e9)not float) so DeepSpeed strict JSON validator accepts;validate_fsdp2_compile_configrequires FSDP + CUDA + transformers + torch>=2.2/accelerate>=0.27; DeepSpeed-MII stub exits non-zero to prevent silent mis-start;validate_pipeline_configenforcespipeline_stages >= 2+ CUDA +gpu_count >= stages;pipeline_stagesPydantic bounds[1, 16];parallelismLiteraldata|pipeline; NCCL env (NCCL_P2P_DISABLE/NCCL_IB_DISABLE/NCCL_NVLS_ENABLE) applied viaos.environ.setdefaultonly — user/launcher overrides are never stomped -
v0.28.0 — Training Speed & Memory:
quantization_aware: Union[bool, Literal["fp8"]]rejects arbitrary strings (onlytrue/false/"fp8"); FP8 path requires CUDA + Hopper+ SM capability + transformers backend;gradient_checkpointing: Union[bool, Literal["selective","medium","full","auto"]]rejects unknown tier strings and returns only HF-supported keys (no private markers leak intoTrainingArguments.gradient_checkpointing_kwargs);activation_offloadingLiteralcpu|disk, scratchsave_dircontainment-enforced via sharedutils/paths.is_under_cwdbefore disk writes,torch.load(weights_only=True)prevents arbitrary Python deserialization on reload, TOCTOU closed betweenmkstempandtorch.saveby holding the fd open, best-effort cleanup on context exit (handles SIGKILL mid-backward);kernel_picker.pick_best_kernelraisesValueErrorwhen all candidates lack a finitetime_ms(prevents silent promotion of an untimed combo); Cut CE architecture detector matches on last path component only (sodeepseek-ai/...-phi-...org-prefix does not trigger a Phi patch on a DeepSeek model);build_cross_doc_masknumpy-vectorised to avoid O(seq_length²) pure-Python fill atmax_lengthbound (1M);@model_validatorrequirespacking=truewhenpacking_cross_doc_attn_mask=true(prevents silent no-op);SoupConfig._validate_v028_speed_memory_sft_onlyrejectsuse_cut_ce/quantization_aware="fp8"/kernel_auto_compose/activation_offloadingon non-SFT tasks — prevents legacy int8-QAT wrapper from crashing on the string"fp8"and prevents silent no-ops on DPO/GRPO/KTO/etc. (multi-trainer wiring tracked for v0.28.1) -
v0.29.0 — HF Hub Deep Integration:
HF_ENDPOINTSSRF-hardened — scheme allowlist (http/https), null-byte rejection,0.0.0.0explicitly rejected, plain-HTTP only permitted for loopback (localhost/127.0.0.1/::1), RFC1918 / link-local / cloud-metadata (169.254.x) IPs rejected viaipaddress.ip_address; repo ID regex[A-Za-z0-9][A-Za-z0-9._-]{0,95}per component, ≤200 chars total, null-byte / whitespace /../ leading-/rejection (applied topush --repo,train --push-as,data push --hf-dataset,deploy hf-space --model/--space); collection slugowner/slug-hashregex-validated, ≤256 chars; HF token resolution single-sourced inutils/hf.resolve_token(env > cached login), explicit non-printable tokens rejected,push --tokenflag deprecated with yellow warning;soup push --modelconfined to cwd viais_under_cwd(prevents craftedsoup.yaml output:from uploading system files); auto-push checkpointallow_patternsrestricts uploaded files to*.safetensors/*.bin/*.pt/*.json/tokenizer*/trainer_state.json/training_args.bin/README.md(keeps.envand source files out of auto-pushed branches);prepare_hf_resumeenforces cwd containment and passeslocal_dir_use_symlinks=False(defeats symlink-based FS escape on olderhuggingface_hub); commit messages stripped to first line and capped at 200 chars (prevents multi-line injection into public HF commit history);_render_eval_scorecardneutralises|/[/]/(/)/!/newlines/tabs/</>in task names and non-numeric scores;data_lineageHTML-escaped (defeats XSS on HF Hub README viewer);render_space_templatevalidatesmodel_repoviavalidate_repo_idbefore substitution into renderedapp.py(crafted repo id cannot inject Python code);HFPushCallbackuses sticky_repo_failedflag to short-circuit retries after hard failure (no log spam, no wasted API calls);add_to_collectionprefers HfHubHTTPError 409 detection over string-match for duplicate handling -
v0.30.0 — Inference Excellence: OTLP endpoint SSRF-hardened matching v0.29.0
HF_ENDPOINT(scheme allowlist,0.0.0.0rejected, RFC1918 / link-local / cloud-metadata viaipaddress.ip_address, plain HTTP loopback-only);pick_draft_modelrejects URL-scheme target names (http:///https:///file://), null bytes, names >200 chars;validate_regex_patternlength-capped at 2048 + null-byte rejection + must compile;validate_json_schemadict shape + 64KB serialised cap + requiredtypefield;--json-schemafile path confined to cwd via sharedutils/paths.is_under_cwd;--structured-output jsonrequires--json-schema(fail-fast prevents silent no-op); FastAPI/v1/adapters/activate/{name}pattern^[a-zA-Z0-9][a-zA-Z0-9\-]*$enforced before handler runs; activate/deactivate state protected bythreading.Lock(no race on concurrent hot-swap);/v1/adaptersresponse omits filesystem paths (names + active flag only); CORS on transformers backend restricted to loopback origins (^https?://(localhost|127\.0\.0\.1)(:\d+)?$) since hot-swap endpoints mutate state without auth;auto_quant.Candidatename regex + score[0.0, 1.0]+ finite-float check + non-negative latency;pick_bestgenerator-safe (materialises to list) so error-message count is accurate; OTel span context usescontextlib.ExitStackso__exit__sees real exception info (spans correctly marked error on HTTPException);record_latencyalways runs infinallyso tail-latency percentiles include failure paths;build_traceridempotent — only installs provider when current isProxyTracerProvider/NoOpTracerProvider(preserves operator-supplied instrumentation) -
v0.32.0 — Training Stability & Auto-Tuning:
--find-lr-outputcontainment via sharedutils/paths.is_under_cwd(prevents writes outside cwd);save_lr_finder_reportrejects NaN / Infinity floats inlrs/lossesand serialises withallow_nan=False(keeps the report parser-safe);compute_lr_schedulerejects non-positivestart_lr, inverted ranges, andnum_stepsoutside[2, 10_000];pick_mixed_precisionrejects empty / null-byte / >200-char model names and resolves multi-version quirks (qwen2.5vsqwen2,phi-3.5vsphi-3) by longest-substring-first iteration so an added family can never accidentally make a more-specific entry dead code;compute_warmup_stepsclamps to[10, 1000]with aratio==0.0short-circuit matching HF Trainer's "no warmup" convention;SpikeRecoveryStrategyis@dataclass(frozen=True)(post-construction mutation cannot bypass validation),max_attempts ∈ [1, 10],lr_decay ∈ (0, 1),min_lr > 0; cross-validator_validate_spike_recovery_requires_watchdogrejectsloss_spike_recovery=true, loss_watchdog=falseat config-load (fails fast instead of never triggering);convergence_window ∈ [5, 10_000],convergence_rel_tol ∈ (0, 1],recommend_actionreusesdetect_plateauso plateau heuristic stays single-source-of-truth;GradAccumMonitor.recommend()caps doubledaccumatMAX_ACCUM=1024so a runaway advisory loop cannot blow up DataLoader prefetch;generate_configvalidates BOTH the YAML output path AND the embeddeddecisions["output"]field viais_under_cwd(closes the gap where a crafteddecisions["output"]="../../etc"would have silently propagated into the rendered YAML) -
v0.34.0 — Observability & Dev UX:
.crashbundle generator (utils/crash.py) recursively redactshf_*/sk-*/Bearer …token-shaped strings in any capturedconfigand metric tail before serialisation, so a.crashfile shared on a public GitHub issue cannot leak credentials;output_diris reduced toos.path.basenameso$HOMEdoesn't leak;write_crash_bundleusesos.path.realpath + commonpathfor cwd containment (Windows-safe; raisesValueErrornotPermissionErrorso callers cannot silently swallow withexcept OSError); filename appendssecrets.token_hex(4)so two crashes in the same UTC second don't collide; bundle truncated toMAX_BUNDLE_BYTES=1_000_000.train.pycrash-write surfaces failures to the user (no silent missing-bundle).profiling.pyresolve_trace_pathrejects empty /./..///\\/ null-byterun_id(closes theoutput_dir/profiles/../trace.jsonescape) and usesos.path.realpath + is_under_cwd; profiles dir is created only on successful torch import (no stale empty dirs on torch-less CI).tracker.get_runLIKE-prefix match escapes%/_/\\and usesESCAPE '\\'so a craftedrun_idcannot widen the match (mirrors v0.26.0 registry policy). Lazy schema migration (_ensure_schema) tolerates the "duplicate column" race when two CLI processes start simultaneously on a fresh DB (fork-based multi-GPU training, TUI auto-refresh).runs.py show/replay/cleanswitched userrun_idrendering tomarkup_escapeand switchedcleancontainment from brokenPath.resolve() + relative_to()to project-standardos.path.realpath + is_under_cwd.tui_app.pylazy-importsExperimentTrackerandmarkup_escapes every DB-sourced string before passing into Textual widgets so a crafted base_model / experiment_name cannot inject[bold red]…[/]markup.run_cost.estimate_run_cost_usdrejectsboolinnum_gpus(bool is a subclass of int — same defence as v0.30.0Candidate.__post_init__); duration clamped to[0, 1 year]; unknown GPU returnsNoneso callers render—instead of fabricating$0.00.log_level.parse_log_levelrejects non-string + null-byte input. -
v0.33.0 — Live Wire: RLVR
code_exec_rewardadds OS-level isolation (Linux best-effortos.unshare(CLONE_NEWUSER|CLONE_NEWNET|CLONE_NEWPID), macOSsandbox-execwith default-denyMACOS_SANDBOX_PROFILEnarrowed to a 3-namemach-lookupallowlist to prevent DNS / NSURLSession bypass of(deny network*));prune_checkpointsswitches to TOCTOU-safeos.lstat + S_ISLNK+shutil.rmtree(onerror=_abort_on_symlink)so a symlink encountered mid-walk aborts rather than escapes;run_gatewraps each task scorer in a typedtry/exceptso backend failures producescore=None, error=str(exc)(never silentscore=1.0);_parse_judge_urlremoves the barehttp://catch-all (defence-in-depth after the Pydantic GateTask validator);soup can runrequires--yesor explicit consent callback and raisesValueError(notPermissionError, which is anOSErrorsubclass that broadexceptblocks would swallow); GGUFrglobresult for ollama deploy isrealpath+commonpathchecked against extract_dir (prevents symlink escape from a crafted can);DeployTarget.pathvalidator normalises mixed\\//separators before splitting (closes a Windows..bypass);CAN_FORMAT_VERSION1→2 (additive — v1 still loads);soup can publishvalidatesrepo_idviautils/hf.validate_repo_id, resolves token viaresolve_token, sanitises commit messages (first-line, 200-char cap), uses HTTPS-only HfApi;_write_spike_recovery_hintaddsis_under_cwdcontainment check onargs.output_dirfrom raw HFTrainingArguments;lookup_entry_by_output_diremitsResourceWarningwhen 1000-row scan limit is hit (no silent miss);CrossDocCollatorno longer mutates input feature dicts (HF Dataset rows are cached and reused — mutation broke subsequent batches);Candidaterejectsboolinscore/latency_ms(was sneaking pastintisinstance check);evaluate_candidatelatency mean now divides by completed prompts (excludes crashed) so a broken candidate isn't artificially fast;auto_quant.run_auto_quant_pickersoft-falls-back to highest-scored candidate when no candidate clearsmin_score(server still binds);build_logits_processorsreturns[]when neitheroutlinesnorlm-format-enforceris installed (server degrades to free-form rather than 500); MII server uses loopback-only CORS, max_tokens cap [1, 16384], stream rejection, generic 500 with no stack-trace leak;os.execvpauto-reexec uses list args (no shell), all forwarded flags pre-validated;cleanup_extract_dirusesos.path.commonpath(Windows-safe) instead ofstartswith;_run_subprocesscatchesTimeoutExpiredand returns rc=124 (coreutils convention) instead of an unhandled traceback; neweval_resultsandtensorrtartifact kinds inRegistryStore._VALID_KINDS -
v0.53.0 — Quant Menu II (UD GGUFs + KV cache + NVFP4 + LF parity + save formats): 6 schema-only Parts; live wiring deferred to v0.53.1. Every new validator follows the project's established hardening policy: closed allowlists (
UD_GGUF_FORMATS,IQ_GGUF_FORMATS,APPLE_ARM_GGUF_FORMATS,KV_CACHE_TYPES,MERGE_SAVE_FORMATS,TORCHAO_PTQ_SCHEMES) asfrozensetso registries cannot be mutated;_GGUF_METADATA/_KV_CACHE_METADATA/_MERGE_METADATA/_TORCHAO_METADATAwrapped inMappingProxyType;_LOWER_INDEXfor GGUF lookup is alsoMappingProxyType-wrapped (replaces O(N) walk with O(1) lookup — code-review MEDIUM fix). All string validators reject non-string / bool / empty / null-byte / oversize with case-insensitive normalisation (matches v0.41.0validate_optimizer_name/ v0.51.0validate_hub_namepolicy);validate_torchao_schemeis INTENTIONALLY case-sensitive (PyTorch class names —torchao.quantize_looks them up by exact name) with the asymmetry documented at both validators (security-review LOW fix).validate_calibration_data_path+validate_quant_config_pathare shape-only at this release; their docstrings name the exact controls a v0.53.1 CLI dispatch contributor MUST add (os.path.realpath+os.path.commonpathcwd containment,os.lstat+stat.S_ISLNKsymlink rejection beforeopen(), existence check,yaml.safe_load-only for quant configs) — closes the security-review MEDIUM "documentation gap at trust boundary" finding. SoupConfig cross-validators:_validate_fp8_attention_compat(requiresquantization_aware='fp8'BEFORE the MLX gate so the more actionable error fires first — code-review MEDIUM fix);_validate_nvfp4_compat(non-MLX +modality='text'; Blackwell SM ≥ 12.0 runtime check fires at trainer construction);_validate_unsloth_bnb_4bit_compat(requiresbackend='unsloth'+quantization='4bit');_validate_bnb_4bit_double_quant(requiresquantization='4bit'— rejectsnone/8bit/Quant-Menu);_validate_llm_int8_alias(assertsquantization='8bit', deliberately disjoint from v0.41.0load_in_8bitaliasing);_validate_quantize_ref_reward(extended ref-task allowlist{dpo, ipo, simpo, orpo, bco, kto, preference, grpo, ppo}per code-review HIGH fix — first-cut omitted grpo + kto + ppo which all have reference policies);_validate_kv_cache_type_supported(onlyfp8gated to non-MLX in v0.53.0; q8_0/bf16/f16 pass-through documented at validator site so v0.53.1 contributor sees the gate immediately).requires_hopperreads from_KV_CACHE_METADATAspec — single source of truth so adding a Hopper-only type means flipping the spec field only (code-review MEDIUM fix). All 7 new bool fields share_validate_v053_bool_fieldsfield_validator(mode='before')that rejects bool-as-int with explicitTypeError("v0.53.0 flag must be bool")and passesNonethrough to Pydantic'sdefault=Falserather than silently coercing it (python-review MEDIUM fix —fp8_attention: nullin YAML now surfaces as a "valid boolean" ValidationError instead of masquerading asFalse). Known limitations: (1) Every live wiring is deferred to v0.53.1 —export_advanced_gguf,apply_kv_cache_type,apply_fp8_attention,apply_nvfp4,merge_4bit,export_torchaoall raiseNotImplementedErrorwith explicitv0.53.1markers. (2)validate_calibration_data_path+validate_quant_config_pathare shape-only this release; CLI dispatch in v0.53.1 MUST add cwd-containment + TOCTOU symlink rejection. (3)kv_cache_typeMLX permissive policy: onlyfp8is rejected, the other three pass-through; v0.53.1 may narrow further. (4) Hopper SM-capability check is runtime-only — schema acceptskv_cache_type='fp8'+fp8_attention=truewithout GPU probe. (5) NVFP4 + Blackwell (SM ≥ 12.0) check is runtime-only. (6)bnb_4bit_use_double_quantonly gated againstquantization, not againstquantization_aware— the latter combination is already rejected by v0.28.0 Quant-Menu + QAT cross-validator. (7)llm_int8is an assertion not an aliaser — diverges from v0.41.0load_in_8bitdesign on purpose. (v0.53.0) -
v0.52.0 — Modality II (TTS + Distillation + BitNet + EBFT-GDPO + MoE quant + reasoning_effort): 7 schema-only Parts; live trainer / loss / export wiring deferred to v0.52.1. Every new validator follows the project's established hardening policy: closed allowlist (
SUPPORTED_TTS_FAMILIES,CLASSIFIER_TASKS,DIVERGENCES,BITNET_QUANT_FORMATS,BITNET_EXPORT_FORMATS,EBFT_VARIANTS,GDPO_VARIANTS,MOE_EXPERT_QUANT_FORMATS,REASONING_EFFORT_LEVELS, per-family_FAMILY_EMOTIONS) wrapped infrozenset/MappingProxyTypeso registries cannot be mutated at runtime;validate_*helpers reject non-string / bool / empty / null-byte / oversize / unknown inputs with case-insensitive normalisation (matches v0.41.0validate_optimizer_name/ v0.50.0grpo_variant/ v0.51.0hubpolicy); float validators (validate_distill_temperature,validate_ebft_temperature) gate onmath.isfiniteto reject NaN AND±inf(matches v0.32.0save_lr_finder_reportpolicy).field_validator(mode="before")onnum_labels(security-review HIGH fix) rejectsboolbefore Pydantic'sge=1coercion silently treatsTrueas1. Field validator onreasoning_effortroutes through the sharedvalidate_reasoning_efforthelper so the schema and runtime validator agree on what's accepted (security-review MEDIUM fix). SoupConfig cross-validators:_validate_tts_compat(requirestask='tts'+modality='audio_out'+ non-MLX backend; per-family emotion allowlist via_FAMILY_EMOTIONS),_validate_classifier_compat(with lazy-import early-return — code-review HIGH fix — so SFT hot path doesn't pay import cost; requiresnum_labelson classifier tasks; rejects classifier-only fields outside the task family with named offenders),_validate_distill_compat(requiresteacher_modelwhentask='distill'; rejects distill-only fields outside the task),_validate_bitnet_compat(gates to non-MLX + text-modality + task ∈ {sft, pretrain, dpo}),_validate_ebft_compat+_validate_gdpo_compat(task-family gates),_validate_moe_expert_quant_compat(requiresmoe_lora=trueto prevent silent no-op),_validate_reasoning_effort_task_gate(code-review HIGH fix — rejectsreasoning_effort+train_on_eotoutside the SFT-family task set with named offenders; mirrors v0.50.0 GRPO stability task-gate policy). PublicDIVERGENCESfrozenset is derived from_DIVERGENCE_ALIASESso adding a new alias updates both the accepted-input set and the error message in lockstep (review fix LOW).validate_bitnet_exportenforces a closed-allowlist canonical form forsoup export --format <bitnet|tq1_0>, both of which are CLI-registered with a yellow advisory panel +Exit(0)stub (no artifact written until v0.52.1 — the format flag is accepted so existing scripts pinned to v0.52.0 will not break). 6 new YAML recipes appended (5 TTS + Falcon-E BitNet) — every entry is exercised bytests/test_v0520.pyforload_config_from_stringround-trip +_no_null_or_whitespacemodel-id check (mirrors v0.51.0 review-fix LOW). Known limitations: (1) Every live trainer / loss / export path is deferred to v0.52.1 —build_tts_trainer,build_classifier_trainer,build_distill_trainer,build_bitnet_trainer,export_bitnet_gguf,apply_ebft_loss,apply_gdpo_loss,apply_moe_expert_quantall raiseNotImplementedErrorwith explicitv0.52.1markers; schema accepts every new task / quant / variant + the CLI stub forsoup export --format bitnet/tq1_0prints a deferred-advisory panel and exits 0. (2)modality='audio_out'accepted on non-TTS tasks — design choice this release so future audio-output tasks (ASR / V2A) can reuse it; today's runtime trainer dispatch must checktask == 'tts'to avoid silent routing into the deferred TTS path. (3) Oute emotion allowlist is a tight 6-entry subset (neutral / happy / sad / angry / calm / excited); operators wanting custom emotions will need a v0.52.1 patch to extendOUTE_EMOTIONS. (4)is_bitnet_modelis best-effort heuristic over name prefixes (bitnet,falcon-e,1bitllm,onebit); a BitNet checkpoint published under an org without any of those prefixes returns False. This is detection, not gating — the trainer wrapper (v0.52.1) loads the model regardless of the heuristic. (5)quantization='bitnet_1.58'gated to task ∈ {sft, pretrain, dpo} — extending to GRPO / PPO / RewardModel requires upstream onebitllms RL kernels not yet shipped. (v0.52.0) -
v0.51.0 — Model Catalog Expansion + Alternative Model Hubs: 5 release Parts. New
soup_cli/utils/hubs.pyships closed allowlistSUPPORTED_HUBS = frozenset({hf, modelscope, modelers})+ threeMappingProxyType-wrapped registries (_HUB_DEFAULT_ENDPOINTS/_HUB_ENDPOINT_ENV/_HUB_PACKAGE) so the registry cannot be mutated at runtime (matches v0.36.0_REGISTRYpolicy).validate_hub_namerejects non-string / bool / empty / null-byte / >32-char / unknown with case-insensitive normalisation (matches v0.41.0validate_optimizer_namepolicy).validate_hub_endpointis the SSRF kernel — full parity with v0.29.0utils/hf.resolve_endpoint: scheme allowlist (http/httpsonly), null-byte rejection, control-character / CRLF rejection added in v0.51.0 as a defence-in-depth review fix (defends against URL-as-HTTP-header injection if the URL ever flows into a raw HTTP client),0.0.0.0explicitly rejected, plain HTTP only for loopback{localhost, 127.0.0.1, ::1}, RFC1918 / link-local / cloud-metadata IPs (169.254.x) rejected viaipaddress.ip_addressfor plain HTTP.resolve_endpoint(hub, *, env=None)looks up the per-hub env var (HF_ENDPOINT/MODELSCOPE_ENDPOINT/MODELERS_ENDPOINT) and runs the override throughvalidate_hub_endpoint; default endpoints are baked-in HTTPS URLs.is_hfrejectsboolexplicitly (review fix HIGH — bool is a subclass of int and would have silently fallen throughhub.lower() == "hf"→False, which happens to be correct by accident but violates the contract; matches v0.30.0Candidate/ v0.34.0estimate_run_cost_usdpolicy).TrainingConfig.hub: Literal["hf","modelscope","modelers"]field gets afield_validator(mode="before")_normalize_hubthat delegates tovalidate_hub_namesohub: HFin YAML normalises to"hf"(review fix HIGH — first-cut had Pydantic Literal exact-match whilevalidate_hub_namewas case-insensitive, breaking the v0.41.0validate_optimizer_name/ v0.50.0grpo_variant/rollout_backendpolicy of agreement between schema and shared validator). SoupConfig_validate_hub_supportedcross-validator rejectshub != 'hf'onbackend == 'mlx'with a distinct error message (review fix HIGH —mlx-lmonly downloads from HF Hub; without this gate abackend: mlx+hub: modelscopeconfig would silently pass schema load and fail at runtime with a confusingmlx-lmerror). 26 new YAML recipes appended tosoup_cli/recipes/catalog.py— every entry is exercised bytests/test_v0510.pyviaload_config_from_stringround-trip +yaml.safe_load(no Python tags / no template injection / no credential leak in the YAML strings) + a_no_null_or_whitespacemodel-id check that rejects empty path components (review fix LOW — first-cut allowed"/name"leading-slash IDs to pass). Two non-<N>Bsizestrings ("image"/"ocr"/"moe"/"medium") were normalised to"N/A"(review fix MEDIUM —search_recipes(size=…)would silently miss those entries, and the autopilot VRAM estimator could not parse them). Known limitations: (1) Live downloader / uploader / push integration deferred to v0.51.1 —TrainingConfig.hubschema lock-in ships now (Literal accept + MLX cross-validator + case-normalisation), butsoup data download --hub modelscopeandsoup push --hub modelersstill route through the existing HF Hub code path; the actualmodelscope-sdk/openmind-hubadapters are the v0.51.1 deliverable. Same stub-then-live pattern as v0.27.0 MII / v0.37.0 multipack / v0.50.0 GRPO Plus. (2) Speculative / aspirationalbasemodel IDs in some Part A/C recipes — the catalog ships entries foropenai/gpt-oss-{20,120}b,THUDM/glm-5,Qwen/Qwen-Image,deepseek-ai/DeepSeek-OCR,PaddlePaddle/PaddleOCR-VL,google/embeddinggemma-300mso users have ready-made recipes the moment those repos go live (matches the plan's "match Unsloth's day-zero coverage" directive). Recipes for not-yet-published repos will surface a clear HF Hub 404 when the user runssoup train --recipe <name>. (3) DNS-resolved private hostnames not blocked —validate_hub_endpointonly rejects literal RFC1918 / link-local IP addresses; a hostname likecorp-proxy.internalthat DNS-resolves to a private IP is accepted at validation time (mirrors the v0.29.0HF_ENDPOINTpolicy — DNS resolution is intentionally not performed in this local-tool threat model). (v0.51.0) -
v0.50.0 — GRPO Plus (RL parity): 22 features across 5 Parts shipped as schema-only (closed allowlists + Pydantic validators + NotImplementedError stubs for live wiring deferred to v0.50.1). All new validators follow the project's bool-rejection-before-int policy (matches v0.30.0
Candidate); closed-allowlistvalidate_grpo_variant/validate_rollout_backendreject non-string / bool / empty / null-byte / oversize / unknown inputs with actionable error messages and case-insensitive normalisation.validate_grpo_deltais bool-first /math.isfinite/(0, 1]bounded (matches v0.32.0save_lr_finder_report/ v0.41.0 Part Blr_groupspolicy). New_VARIANT_METADATA(Part A) and_BACKEND_METADATA(Part C) areMappingProxyType-wrapped frozen-dataclass registries (matches v0.36.0_REGISTRY/ v0.41.0_OPTIMIZER_PACKAGESpolicy). Security-review fixes: (1)grpo_deltaschema gets an explicitfield_validator(mode='after')callingmath.isfinite— Pydantic'sgt=0, le=1bounds only incidentally reject NaN (sinceNaN > 0is False); the explicit validator prevents a future Pydantic change from regressing the guard. (2)validate_long_context_grpo_compatadds null-byte rejection ontaskANDbackendstrings + aboolguard onuse_ring_attention(parity withvalidate_grpo_variant/validate_rollout_backend). (3)validate_vllm_sleep_mode_compatadds null-byte rejection onbackend. Code-review HIGH fixes: (4)_validate_grpo_stability_task_gatenow includesgrpo_fp16in the GRPO-only-fields list — previously a user could silently setgrpo_fp16: trueontask='sft'and have it no-op. (5)_validate_vllm_sleep_modenow requirestask='grpo'(sleep mode is a between-rollouts feature, meaningless on SFT) and rejects with atask='grpo'message. TDD-review HIGH fixes: (6) new_reject_bool_on_grpo_numericsfield_validator on every Part D numeric field +grpo_deltaexplicitly rejectsboolbefore Pydantic'sTrue→1coercion (matches v0.30.0 / v0.41.0 Part B / v0.43.0 Part B policy). Known limitations: (1) Every live loss kernel / launcher (apply_variant_loss,apply_vllm_sleep_mode,launch_rollout,build_prm_trainer) raisesNotImplementedErrorwith explicitv0.50.1markers — same stub-then-live pattern as v0.27.0 MII / v0.37.0 multipack / v0.41.0 LLaMA Pro / v0.45.0 plugins / v0.48.0 curriculum / v0.49.0 LongLoRA. (2)long_context_grporequires Tiled MLP (v0.56.0 Part A) to actually run; the schema gate ships now so v0.50.0 configs are stable. (3)vision_grpo=truedoes not check whether the base model is actually a VLM — upstream trainer surfaces that error loudly. (4) The 7 stability knobs schema-validate but none are wired into a live callback in this release;replay_buffer_size,defer_rerolling, andskip_zero_advantageare pure schema lock-ins. (v0.50.0) -
v0.49.0 — Long Context & Architecture: 4 release Parts ship YaRN RoPE scaling, Dynamic NTK hardening, LongLoRA S² shifted-sparse attention (schema-only gate), and full Llama 3.1 NTK-aware scaling. Security review hardened the public boundary of
soup_cli/utils/long_context.py::get_rope_scaling_config—target_length/original_length/yarn_factornow rejectbool/ NaN / Inf / non-positive at entry so a direct caller bypassing Pydantic cannot emit{factor: NaN}into HF model configs (matches v0.30.0Candidate/ v0.34.0estimate_run_cost_usd/ v0.41.0 Part Blr_groupspolicy).scale_inv_freq_llama3rejectsboolon every numeric parameter (inv_freq/scale_factor/low_freq_factor/high_freq_factor/old_context_len) — first-cut only guardedinv_freq.yarn_get_mscalenow raises on non-finite input (review fix LOW — first-cut silently clamped NaN/Inf to identity, hiding the misconfig from callers).detect_llama3_rope_in_configuses explicitis Noneinstead of theoridiom when probingrope.get("type")— falsy-but-set values no longer silently fall through torope_type(matches v0.40.6 review-fix policy). Newsoup_cli/utils/longlora.pyshipsis_llama_modelwith a word-boundary regex(?:^|[^a-z0-9])(?:code)?-?llama(?:-?\d+(?:\.\d+)?)?(?:[^a-z0-9]|$)— substring"llama"inside an unrelated identifier (e.g.my-llama-style-finetune) does NOT match; null-byte rejection onmodel_name(matches v0.39.0is_gemma4_model/ v0.44.0is_llama4_modelpolicy); 512-char cap returnsFalserather than raising (bounded scan time)._LLAMA_REGEXhas no nested quantifiers / overlapping alternation and is ReDoS-bounded by the 512-char cap.validate_longlora_compatemits distinct error messages per failure mode (mlx vs other non-transformers backends — matches v0.34.0 review-fix policy on distinct actionable rejections).TrainingConfigfield_validator(mode='before')rejectsboolon the four yarn fields before Pydantic'sgt/lecoercion silently treatsTrueas1.0. SoupConfig_validate_longlora_compatinvokesvalidate_longlora_compatat config load so a misconfiguredsoup.yamlfails fast with an actionable message rather than silently no-opping at trainer construction time (mirrors v0.39.0 ReLoRA / v0.48.0 curriculum_dynamic schema-gate policy). Known limitations: (1) LongLoRA liveLlamaAttention.forwardoverride deferred to v0.49.1 —apply_longlora_forward_overrideraisesNotImplementedErrorwith the v0.49.1 marker; the schema gate ships now so misconfigured runs cannot reach trainer construction. (2) LongLoRA architecture allowlist is Llama 1/2/3.x + CodeLlama only; Mistral / Qwen / Phi expansion tracked for v0.49.1+. (3) Llama 3.1 NTK auto-detect helperdetect_llama3_rope_in_configships but is not yet wired intoapply_long_context_config; trainer wiring can pick it up when needed. -
v0.48.0 — Adaptive Training (BETA): 2 release Parts ship a dynamic curriculum re-weighter and a Bayesian data-mixing optimiser. New
soup_cli/utils/curriculum_dynamic.pyships frozenDynamicCurriculumPolicywith bounded fields (num_buckets ∈ [1, 20],recompute_every_n_steps ∈ [1, 100_000],floor ∈ (0, 1/num_buckets],temperature > 0); bool-rejected on every numeric input (matches v0.30.0Candidate/ v0.34.0estimate_run_cost_usdpolicy);math.isfiniteon every float (matches v0.32.0 / v0.47.0 policy).compute_bucket_weightswater-fill design (review fix HIGH — first-cut had a trailingw/sum(w)renorm that could push elements sitting exactly at the floor below the floor when accumulated float error left the sum slightly > 1.0; renorm removed because softmax already sums to 1.0 so water-fill output also sums to 1.0).validate_distributed_curriculumcross-validator rejectsenabled=Truewithworld_size > 1unless the caller attests anall_reducehook is registered — DDP/grad-accum footgun: divergent per-rank stats without coordination silently desynchronise the sampler. SoupConfig cross-validators rejectcurriculum_dynamic=trueon mlx backend (HF Trainer-callback specific) and on non-SFT/pretrain tasks with distinct error messages (matches v0.34.0 review-fix policy).render_curveandparse_history_jsonlenforce_MAX_HISTORY_ROWS = 100_000DoS cap (review fix MEDIUM — first-cut had no cap, an attacker-controlled JSONL with 10M rows would have OOM'd the process). Newsoup runs curriculum-curveCLI:is_under_cwdcontainment,os.lstat + S_ISLNKrejection (TOCTOU defence — mirrors v0.33.0 #22 / v0.43.0 Part C / v0.44.0 Part B / v0.45.0 Part E / v0.46.0 Part A policy), 50 MB file-size cap + 100k-line streaming cap (review fix HIGH — without these,--history /path/to/giant.jsonlwould read the file into memory unbounded), null-byte rejection on tracker-suppliedoutput_dir(defence-in-depth beforeos.path.join). Newsoup_cli/utils/data_mix.pyships frozenMixCandidatewith simplex constraint (sum(weights) == 1.0 ± 1e-6), finite eval_loss (math.isfinite),_MAX_LOSS = 1e6sanity cap, bool / negative rejection on every numeric.validate_datasetsenforces 2-32 entries (review fix MEDIUM — first-cut had anot rawempty-only guard that fell through to the realpath loop for single-entry inputs, surfacing a less-actionable error after path resolution),is_under_cwdcontainment, symlink rejection, dedup, null-byte / oversize / non-string rejection.parse_budgetaccepts digits + optionals/m/hsuffix bounded to[60s, 24h].run_mix_optimizerper-candidate proxy exceptions are logged at DEBUG +opt.tell(_MAX_LOSS)+continue(review fix MEDIUM — first-cut raisedRuntimeErroron the first proxy failure, breaking the documentedpartial=Truecontract; isolation policy mirrors v0.33.0 #47CrossDocCollatorand v0.40.3judge_filter_pairs);KeyboardInterrupt/SystemExitre-raised; NaN proxy returns logged + skipped.render_mix_recipe_yamlrejects newlines / null bytes / >4096-char dataset paths (defends against YAML key injection — mirrors v0.46.0 Part Arender_recipe_yamlpolicy).write_mix_recipeis atomic viatempfile.mkstemp + os.replace;is_under_cwdcontainment + ≤4096-char path +os.lstat + S_ISLNKrejection (TOCTOU) + overwrite-required gate.load_mix_recipeos.lstatis wrapped intry/except OSError(review fix HIGH — first-cut calledos.lstatbare afteros.path.lexists, leaving a TOCTOU race where path disappearance between the two calls would raise an unhandledOSErrorto the user); 256 KB file cap;yaml.safe_loadonly. Newsoup data mix --optimize / --applyTyper command with mutually-exclusive modes. Known limitations: (1) Live HF Trainer callback for dynamic curriculum deferred to v0.48.1 — schema gates, math kernel, and visualiser ship; the callback wiring withtorch.distributed.all_reduceof per-bucket stats lands in v0.48.1. (2) Multi-trainer expansion (DPO/GRPO/etc.) deferred to v0.48.1 — schema rejects non-SFT/pretrain because per-sample loss semantics differ enough that bucket-level uncertainty does not transfer cleanly. (3) Live proxy training loop forsoup data mixdeferred to v0.48.1 — CLI ships with a synthetic offline proxy (quadratic penalty around uniform mixture). (4)scikit-optimizeintegration deferred to v0.48.1 —OptimizerProtocolducktype is the integration point; default fallback is a deterministic Dirichlet sampler. (5) Both features shipBETA:-prefixed in CLI help and field descriptions until reference-benchmark validation lands in v0.48.1. -
v0.47.0 — Data Forge: 2 release Parts ship a synthetic data pipeline with full provenance + a data-quality scorecard. New
soup_cli/utils/data_forge.pyships frozenForgePlan/ProvenanceRecord/ForgeRowdataclasses + a closedVALID_TASKS = ("sft", "preference", "tool")allowlist.chunk_documentparagraph splitter has_MAX_DOC_CHARS = 4 MiBcap, null-byte rejection, and bool-as-int reject onmax_chunk_chars(matches v0.30.0Candidatepolicy)._validate_float_unitusesmath.isfiniteto reject NaN AND ±Inf BEFORE the[0, 1]bounds check (review fix HIGH — first-cut acceptedfloat("nan")becausenan < 0.0isFalse, silently disabling active-pruning; mirrors v0.32.0save_lr_finder_reportand v0.41.0 Part Bparse_lr_groupspolicy).discover_documentsdoesis_under_cwdcontainment (review fix MEDIUM — first-cut only enforced containment frombuild_forge_plan, leaving direct callers unprotected; matches v0.42.0discover_*policy), rejects symlinked directories viaos.lstat + stat.S_ISLNK, restricts to a closed_DOC_EXTENSIONS = {.txt, .md, .json, .jsonl}allowlist, caps at_MAX_DOCS = 10_000, and skips dotfiles.synthesise_forge_rowsrunschunk → judge(prompt) → score_uncertainty → ForgeRowwith judge-exception swallow at DEBUG (matches v0.33.0 #47CrossDocCollatorand v0.40.3judge_filter_pairspolicy — no silent crash on a single bad judge call).write_forge_dataset+write_provenanceare atomic viatempfile.mkstemp+os.replace(mirrors v0.43.0 Part Dcopy_bundle_to); both writers call_check_write_pathwhich enforcesis_under_cwd+ ≤4096-char cap +os.lstat + stat.S_ISLNKrejection at the target (TOCTOU defence — mirrors v0.33.0 #22 / v0.43.0 Part C / v0.44.0 Part B / v0.45.0 Part E / v0.46.0 policy); rejects non-ForgeRowelements (review fix MEDIUM — first-cut wouldTypeErrormid-write leaving partial state). Newsoup_cli/utils/data_score.pyshipsBENCHMARKSMappingProxyType(6 names — mmlu / gsm8k / humaneval / truthfulqa / arc / hellaswag) and frozenScoreReportwithlanguagesexposed asMappingProxyTypeto prevent caller mutation.ngram_setcapsn ∈ [1, 32], text at_MAX_TEXT_CHARS = 1 MiB, rejects bool-as-int. ReDoS-hardened PII regexes —_PII_PATTERNSrewritten in the security review (review fix HIGH × 2): phone pattern flattened to remove nested(?:\+\d{1,3}[\s.-]?)?(?:\(?\d{2,4}\)?[\s.-]?)?(catastrophic backtracking on near-miss inputs) → flat alternation with digit-count post-filter ≥ 7; credit-card rewritten from\b(?:\d[ -]?){13,19}\b(exponential backtracking on a 13-digit-space pattern ending withx) to anchored\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{1,7}\bwith hard digit caps;_PII_SCAN_CAP = 50_000chars enforced on every input viadetect_piipre-truncation._require_strrejects null bytes (review fix MEDIUM — first-cut only checked length, leaving the data_score validators inconsistent with the data_forge_validate_str)._require_unit_floatusesmath.isfiniteto reject NaN AND ±Inf (matchesdata_forgepolicy).decontaminate_rowsdocuments that it uses one-sided containment ratio|inter| / |b|rather than symmetric Jaccard (review fix MEDIUM — first-cut docstring said "Jaccard" but formula was containment; renamed for accuracy so future maintainers don't accidentally swap the denominator).compute_scorecardper-rowtry/except ValueErrorblocks now log at DEBUG (review fix MEDIUM — first-cut was silentpass, violating v0.33.0 #47 and v0.40.3 logging policy).load_jsonl_rows+write_jsonl_rowsenforceis_under_cwdcontainment,_MAX_FILE_BYTES = 1 GiBcap,_MAX_ROWS = 1_000_000row cap,os.lstat + stat.S_ISLNKrejection on both input and output, atomic write viatempfile.mkstemp+os.replace._read_rowsand_write_rowsCLI helpers have fullList[Mapping[str, Any]]/Iterable[Mapping[str, Any]]type annotations (review fix HIGH — first-cut haddef _read_rows(path: str):with no return type, breaking downstream type checking).decontaminate_textsparameter isOptional[Mapping[str, Sequence[str]]] = None(review fix HIGH — first-cut hadMapping[...] = None # type: ignore[assignment]masking the type error).import mathandimport tempfilemoved to module top-level (review fix MEDIUM — first-cut had stdlib imports inside function bodies, violating the project's lazy-import policy which applies only to heavy ML deps). Newsoup_cli/commands/data_forge.pycollapses the duplicatediscover_documentscall (review fix MEDIUM — first-cut called the helper twice, introducing a TOCTOU window whereplan.num_docscould disagree with the actual doc list). Newsoup_cli/commands/data_score.py--benchmarksallowlist-validated againstBENCHMARKSkeys with Rich-escaped error messages. Known limitations: (1) Live judge providers (Ollama / Anthropic / vLLM via--judge-provider) deferred to v0.47.1 —soup data forgeships with a deterministic offline echo stub; stub-then-live pattern matches v0.27.0 MII / v0.37.0 multipack / v0.46.0 Part A. (2) Decontamination benchmark corpora not bundled —soup data decontaminate --benchmarks mmluvalidates the flag but operates on an empty corpus; operator-supplied--benchmark-filelands in v0.47.1. (3) Llama-Guard-3-1B toxicity classifier + FineWeb-Edu educational classifier + full Presidio PII +langdetect/fastTextship behind[data-pro]extras in v0.47.1. (4) Provenance manifest stores absolute realpath insource_doc(security review M4) — operators sharing manifests should redact paths; kept as-is for audit-trail completeness, mirrors v0.34.0crash.pydesign tension between traceability and$HOMEleak prevention. -
v0.46.0 — Deploy & Agent Autopilot: 2 release Parts ship a deploy-target picker and an Agent Forge for spec-driven tool-calling SFT datasets. New
soup_cli/utils/deploy_autopilot.pyships a 10-profileMappingProxyType-wrapped catalog withDeployProfile@dataclass(frozen=True)and closed allowlists onruntime(transformers / vllm / sglang / mlx / ollama / lm-studio / executorch),quant(none / 4bit / 8bit / gptq / awq / fp8 / mxfp4 / hqq:Nbit),peft(lora / dora / qlora / full)._makefactory rejects non-kebab-case names, bool-as-int onrecommended_max_length, out-of-bounds[64, 1_048_576], null-byte / >512-char description+notes.render_recipe_yamlrejects empty / null-byte / newline / >200-charbaseand >4096-charoutput_dir(defends against a crafted--base "evil\ntraining: { epochs: 9999 }"injecting YAML keys into the rendered recipe).render_deploy_scriptusesshlex.quoteonmodel_pathand rejects newline / NUL / >4096-char.write_recipe/write_deploy_scriptenforceis_under_cwdcontainment, ≤4096-char path cap, andos.lstat + stat.S_ISLNKrejection at the write target (TOCTOU defence — mirrors v0.33.0 #22 / v0.43.0 Part C / v0.44.0 Part B / v0.45.0 Part E policy).soup deploy autopilotpanel passes every profile field throughrich.markup.escape(matches v0.43.0 Part BTournamentpolicy — defends against markup injection if a future profile carries[blink]in its description). Newsoup_cli/utils/agent_forge.pyparses OpenAPI 3.x / MCP server manifests / GraphQL introspection JSON into a canonicalEndpointfrozen dataclass — every parser routes synthesisedpaththrough_validate_path(non-empty single-line NUL-free ≤1024 chars), so a manifest withname="evil\nhost"is rejected with a warning (review fix HIGH — the v0.46.0 first-cut stored the rawf"mcp://{name}"without validation, allowing newline injection into theEndpoint.pathfield).$refstrings in OpenAPI are left opaque (no external resolution — defends against file-read SSRF that a fully-resolving parser would expose)._MAX_ENDPOINTS=10_000,_MAX_SPEC_BYTES=5*1024*1024,_MAX_ROWS_PER_ENDPOINT=32,_MAX_DESCRIPTION=512.load_spec_fileenforcesis_under_cwdcontainment +os.lstat + stat.S_ISLNKrejection BEFORErealpathresolution (review fix MEDIUM — the v0.46.0 first-cut orderedisfile(real)before the lstat check, which followed the symlink) + 5 MiB cap +yaml.safe_loadonly (neveryaml.load).write_datasetis atomic viatempfile.mkstemp+os.replace(review fix HIGH — replaces a v0.46.0 first-cut directopen(real, "w")loop that would leave a partial JSONL file on mid-streamTypeError; mirrors v0.43.0 Part Dcopy_bundle_topolicy). Symlink rejection at the dataset target.endpoint_to_rowsrejects bool / out-of-boundsexamples_per_endpoint(∈ [1, 32]). Newsoup_cli/commands/agent.py:synthRich table passes every cell throughrich.markup.escape(review fix HIGH — defends against spec-controlled markup injection throughep.path);trainvalidates--baseand--output-dirfor NUL / newline / >4096-char BEFORE embedding into the rendered YAML recipe string (review fix CRITICAL — defends against YAML key injection where--base $'evil\\ntraining: { epochs: 9999 }'would smuggle injected training keys);evalenforces predictions pathis_under_cwdcontainment +os.lstat + stat.S_ISLNKrejection +_MAX_PRED_LINES=1_000_000DoS cap (review fix HIGH — v0.46.0 first-cut had no line cap, a multi-GB predictions file would have iterated unbounded). Known limitations: (1) Live Quant-Lobotomy auto-measure deferred to v0.46.1 — autopilot writes the canonical PEFT+quant combo per profile but does not yet measure OK/MINOR/MAJOR via v0.26.0 Quant-Lobotomy Checker. (2) ExecuTorch packaging deferred to v0.54.0 —iphone-16/pixel-9recipes are plan-only. (3)soup agent trainis plan-only — prints the plannedsoup traininvocation rather than re-entering Typer in-process (same design as v0.44.0soup quantize). (4)soup agent evalis heuristic — scores tool-name match + arguments-key validity only; live RLVRcode_execsandbox scoring deferred to v0.46.1. (5)$refresolution in OpenAPI specs is intentionally not done (file-read SSRF defence); users wanting full resolution should runopenapi-spec-validatorupstream. (6) MCP / GraphQL non-HTTP sentinel methods (invoke/query/mutation) are stored onEndpoint.methodwithout going through_validate_method(documented design intent —_HTTP_METHODScovers HTTP-only). -
v0.45.0 — Plugin System & Ecosystem Wins: 5 release Parts ship the plugin / hook system + 4 ecosystem-integration schemas. New
soup_cli/plugins/__init__.pyregistry: kebab-case plugin name regex^[a-z0-9][a-z0-9\-]{0,39}$, semver-ish version regex,_MAX_PLUGINS=64,_MAX_TEMPLATES_PER_PLUGIN=32,_MAX_MODEL_GROUPS_PER_PLUGIN=32,_MAX_NAME_ENTRY_LEN=128. Re-registering with a different(version, plugin object, templates, model_groups, description)is rejected (review fix HIGH — first-cut omitteddescriptionfrom the conflict check, allowing silent description override).load_plugins()swallows per-plugin exceptions at WARNING level so one broken plugin cannot crash the CLI.soup plugins enable / disableraiseKeyErrorfor unknown names (caller-distinguishable from validation errors). All user-controlled output passes throughrich.markup.escape. Newutils/anthropic_messages.pyenforces_MAX_MESSAGES=1024, per-message_MAX_CONTENT_LEN=1MiB,max_tokenscap mirrors v0.30.0 inference-server_MAX_TOKENS_CAP=16384;toolrole with structured (list) content is concatenated into a singletool_resulttext block instead of silently dropped (review fix MEDIUM — first-cut emitted""for any non-str content);from_anthropicuses.get("role")defensively aftervalidate_anthropic_payload(review fix MEDIUM —KeyErrorsurface guard). Newutils/server_tools.pyships closedSUPPORTED_TOOLS = frozenset({python, bash, web_search})allowlist +WebSearchConfigwithdomain_allowlist(≤64 entries, default empty = deny all) +rate_limit_per_minute ∈ [1, 600].is_domain_allowedstrips:portsuffix before matching (review fix MEDIUM — without this,Host: api.example.com:443would silently fail to match the bareapi.example.comallowlist entry, causing legitimate denials) AND rejects IPv6 literals ([::1]→ False — IPv6 should never match a domain allowlist).validate_domainrejects null-byte / whitespace /// oversize.pythonandbashreuse the v0.25.0 RLVR sandbox (5s timeout, RLIMIT_AS/CPU on POSIX, ephemeral cwd, socket patch); live HTTP endpoints land in v0.45.1. Newutils/ngram_spec.pyNgramSpecConfigis@dataclass(frozen=True)withn ∈ [1, 8],num_draft_tokens ∈ [1, 32],prompt_lookup_max ∈ [0, 1MiB]; bool rejection on every numeric field (matches v0.30.0Candidate/ v0.34.0estimate_run_cost_usd/ v0.42.0make_preprocess_cache_keypolicy). Newutils/integrations.py15-entry catalog wrapped inMappingProxyType;IntegrationSpecis frozen so the catalog cannot be mutated at runtime. Newutils/trainer_plugins.py6-entry allowlist (MappingProxyType);validate_trainer_plugin_listacceptsSequence[str](review fix MEDIUM — first-cut annotated asTuple[str, ...]) but explicitly rejects barestrargument so a caller passing"grokfast"directly doesn't iterate per-character. Newutils/recipe_dag.pyparses Data Recipe DAG with closedNODE_KINDS = frozenset({seed, llm_text, code, judge, validator, sampler}); Kahn's topological sort usescollections.deque+ sorted-on-arrival (review fix HIGH — first-cut had O(N² log N)queue.sort()inside the BFS body); cycle / self-loop / dangling-edge / duplicate-edge / duplicate-node-name / unknown-kind rejection with specific error messages._MAX_NODES=256,_MAX_EDGES=1024,_MAX_FILE_BYTES=1MiB.parse_recipe_yamlusesyaml.safe_load.load_recipe_yamlenforcesis_under_cwdcontainment ANDos.lstat + stat.S_ISLNKsymlink rejection at the recipe path (review fix MEDIUM — TOCTOU defence; mirrors v0.33.0 #22 / v0.43.0 Part C / v0.44.0 Part B policy; without this, a symlink under cwd pointing at/etc/passwdwould resolve throughrealpathand load arbitrary files).RecipeDAGandRecipeNodeare@dataclass(frozen=True);RecipeNode.configisMappingProxyType-wrapped so caller mutation post-parse cannot affect the validated topology.parse_recipedoes NOT mutate input dicts (matches v0.33.0 #47CrossDocCollatorimmutability policy). Known limitations: (1) Plugin trainer-callback wiring deferred to v0.45.1 —register_pluginaccepts hook objects anddiscover_hookslists implementations, but no HF Trainer callback emitspre_train/post_train/pre_step/post_stepyet. (2) Anthropic/v1/messagesendpoint deferred to v0.45.1. (3) Server-side tool HTTP endpoints (python / bash / web_search) deferred to v0.45.1. (4) n-gram speculative-decoding live engine wiring deferred to v0.45.1. (5) External integrations are a catalog, not launchers —IntegrationSpecdescribes 15 ecosystem targets but nosoup deploy <target>command auto-detects + invokes their CLIs (tracked for v0.46.0 Deploy Autopilot). (6) Advanced trainer plugins are an allowlist, not callbacks —validate_trainer_plugin_listaccepts[grokfast, spectrum, ...]at schema validation, but no trainer wraps the upstream callbacks yet (live wiring per plugin in v0.45.1). (7) Data Recipe DAG runner deferred —soup data recipevalidates topology and prints planned topological order; node-by-node execution against a local model is the v0.45.1 deliverable. -
v0.44.0 — Live Dashboard & UX: 21 features across 4 Parts. New
soup_cli/utils/qr_url.pybuild_phone_urlenforces a scheme allowlist (http/https), loopback-only HTTP (RFC1918 / link-local / cloud-metadata IPs implicitly rejected becausehost not in _LOOPBACK_HOSTSraises), IPv6 literal auto-bracketing per RFC 3986, and token routing through the URL query string?token=…(review fix HIGH — the v0.44.0 first-cut put it in the URL fragment#token=…which never reaches the server). Token shape regex^[A-Za-z0-9_\-]{16,128}$matchessecrets.token_urlsafe(N)output. Newcommands/fetch.pyenforcesis_under_cwdon--outputAND addsos.lstat + S_ISLNKsymlink rejection at the write target regardless of--force(review fix HIGH — TOCTOU defence; without this, force=True would follow a pre-placed symlink and overwrite a file outside cwd; mirrors v0.33.0 #22 / v0.40.2 #51 / v0.43.0 Part C policy). Bundled-source path isos.path.commonpath-checked againstfetch_examples_dir()(review fix HIGH — defends against a future catalog entry containing..that would escape the bundled directory).fetch_examples_dir()usesos.path.realpathnotabspath(review fix MED — symlinked installs resolve to the real package root, not the symlink's parent). Newutils/checkpoint_trigger.pywrite_triggeraddsos.lstat + S_ISLNKrejection on the trigger target (review fix MED — symlink at<output_dir>/.checkpoint_nowcould redirect the write to any user-writable path). Trigger path stays under cwd via sharedis_under_cwd. Newcommands/llama.pyproxies to llama.cpp binaries with_LLAMA_ENV_ALLOWLISTfrozenset child-env filter (review fix MED) —HF_TOKEN/OPENAI_API_KEY/ANTHROPIC_API_KEY/ allSOUP_*are dropped before exec; onlyPATH/HOME/USER/USERPROFILE/TMP/TEMP/ locale + llama.cpp-recognisedLLAMA_CPP_HOME/GGML_*/OMP_NUM_THREADSare forwarded. Subprocess uses list args (no shell). Closed allowlist_SUBCOMMAND_TO_BINARY: MappingProxyTyperejects unknown subcommands;_validate_argrejects null-byte / newline / carriage-return / >1024-char per arg;_MAX_ARGS=64cap. Newutils/onboarding.pyrender_onboarding_yamlenforcesis_under_cwdon theoutputfield at render time (review fix MED — without this, a crafted answer dict likeoutput: ../../etc/cron.d/xwould silently propagate into the rendered YAML). Literal allowlist ontask(8 entries) andquantization(4bit/8bit/none);epochs ∈ [1, 10]; null-byte rejection on every string with field-named error message. Newutils/sweep_config.pyenforces a strict scalar allowlist (str/int/float/bool) on every param value (review fix LOW — without this,params: {lr: [{__class__: evil}]}would smuggle non-scalar YAML through to runtime);_MAX_FILE_BYTES=256KB,_MAX_PARAM_KEYS=32,_MAX_VALUES_PER_KEY=64,_MAX_PARAM_KEY_LEN=128.SweepSpec.paramsisMappingProxyType[str, Tuple[Any, ...]]for genuinefrozen=Trueimmutability (review fix HIGH —frozen=Trueon aDict[str, List[Any]]field only prevents reassignment, not mutation). Same fix applied toConsolidationPlan.shard_files: Tuple[str, ...]andDelinearizePlan.weight_files: Tuple[str, ...]. Newutils/gpu_monitor.pyparse_nvidia_smi_csvrejects null-byte in GPU name field; subprocess uses list args + 5s timeout +OSError/TimeoutExpiredswallowed (never raises).detect_apple_siliconrewritten to use cleanplatform.system()/platform.machine()checks (review fix HIGH — the v0.44.0 first-cut had a parser-priority bugif X if Y else Z:that produced the right answer by coincidence on every platform but would silently break on any future branch refactor). Newutils/sse_train_stream.pyTrainEventvalidatestypeagainst a closed_VALID_TYPES = frozenset({metric, status, log, eval}), rejects non-finitets, rejects null-byte / >1024-charmessage.to_payloadfilters output to a closed_ALLOWED_KEYSallowlist (defence-in-depth against payload key drift from refactors).format_sse_frameemits the standard W3C SSE wire formatdata: {json}\n\nwithensure_ascii=False. Newutils/tool_outputs.pyToolOutputsBufferis thread-safe (threading.Lock) backed bycollections.deque(maxlen=_MAX_RECORDS=1000)(review fix MED — replaces an O(N)self.records[-N:]list slice that triggered GC pressure on overflow); validates name (no null-byte, ≤128 chars), bool rejection onstarted_ts/duration_ms(matches project bool-as-int policy),successmust be exactlyboolnotint, output preview truncated to 4096 chars.ToolCallTimer.__exit__returnsFalseexplicitly per project readability policy (does not suppress exceptions). Newutils/llama_server_timings.pyparse_timingstolerates missing keys (defaults to None), clampskv_cache_pctto[0, 100], rejects bool / non-finite / negative ints in numeric coercion.format_kv_barvalidateswidth ∈ (0, 200]. Newutils/tail_latency.pyenforcesMAX_SAMPLES=1_000_000DoS cap on every iteration; bool rejection + non-finite rejection + alpha bounds. Newsoup_cli/ui/plugins/__init__.pyregistry: tab name regex^[a-z0-9][a-z0-9\-]{0,30}$,_MAX_TABS=32cap,list_tabs()returnsMappingProxyTypedefensive view,load_plugins()swallows per-plugin exceptions at WARNING level so one broken plugin doesn't crash the whole UI._NAME_DISALLOWEDinutils/shortcuts.pyisfrozenset(review fix LOW — was mutablesetat module level). Newutils/ui_env.pyhost validator restricts to[a-zA-Z0-9.-:]≤253 chars + null-byte rejection; port[1, 65535]; key length cap 256 + null-byte rejection. Known limitations: (1) Live monitoring SSE endpoint deferred — schema + frame formatter ship, FastAPI route in v0.44.1. (2) Phone visibility flag wiring (soup ui --public --auth-token+ QR print at startup) deferred to v0.44.1. (3)soup merge-sharded-fsdp-weightsandsoup delinearize-llama4are plan-only — torch-side runtime in v0.44.1. (4)soup llama <subcommand>does not bundle llama.cpp; the binary must be on PATH (shutil.which) and an actionableFileNotFoundErrorfires when missing. (5)soup serve --reasoning-parser <name>allowlist ships, runtime hook in v0.44.1. (6) Apple Siliconsoup monitorprints a yellow advisory and exits withnvidia-smi unavailablerc=1 —powermetricsparser in v0.44.1. -
v0.43.0 — Tracker & Eval Pro: 18 features across 4 Parts. New
soup_cli/utils/trackers.pyships an immutable_REPORT_TO_BACKENDSMappingProxyTypeallowlist (wandb/tensorboard/mlflow/swanlab/trackio/none);validate_tracker_namerejects non-string / empty / null-byte / >32-char inputs. PostHog telemetry is opt-IN (default OFF) viaSOUP_TELEMETRY=1env var;build_telemetry_payloadschema is closed-key (soup_version / command / python major.minor / os / arch / duration) — no model names, dataset paths, or config contents leak in the payload. Live PostHog network code deferred to v0.43.1. New--trackerflag onsoup trainmutually exclusive with--wandb/--tensorboardviaresolve_report_to.tracker_backend = trackercapture incommands/train.pydefends against the Typer parameter being shadowed by the localExperimentTracker()instance (review fix HIGH #1 — silent--trackerdrop regression).nlg_metrics.pyis pure-math: BLEU_lcs_lengthallocates a fresh row per outer iter (review fix HIGH #2 — first-cut had a double-buffer re-zero that destroyed the last row).effective_tokens_per_secondreturnsNoneon non-positive wall_clock (no fabrication).eval/calibrate.pyCalibrationReportis@dataclass(frozen=True)withFrozenInstanceErrortest (matches v0.32.0 / v0.39.0 / v0.41.0 frozen-dataclass policy);run_calibrationcaps prompts at 10_000 (DoS defence).eval/arena.pyTournamentenforces 256-model cap, 1M-match cap,MappingProxyTypeview onratingsso external callers cannot mutate Elo state (LOW review fix)._validate_model_namerejects null-byte / >128-char AND Rich markup metacharacters[/](security review fix LOW #5 — leaderboard markup-injection defence).update_elorejects bool / non-finite / NaNk.eval/benchmarks_v0_43.pyshipsNEW_BENCHMARKS_V0_43 = frozenset({"ceval","cmmlu","aider_polyglot"})+MappingProxyType-wrapped metadata; live Aider Polyglot runner deferred to v0.43.1.utils/profiling_v0_43.pyresolve_snapshot_pathrejects null-byte /./../ path separators inrun_id;base_dirrejects null-byte / absolute /..segments before realpath join (security review fix HIGH #1) — closes a Windows short-name path escape.memory_snapshot_contextnarrows theRuntimeErrorcatch to therecord()entry call only (review fix HIGH #2 — wide catch would have triggeredgenerator already executingon user-body RuntimeError); yieldsNonewhen torch / CUDA /_record_memory_historyAPI missing.nccl_bandwidth_checkrejects bool / non-finite / negativemeasured_gb_per_sec; reference table isMappingProxyType-wrapped.utils/vscode_setup.pybuild_launch_jsonrejects null-byte / newline / >512-charconfig_pathso a crafted argument cannot inject Python args into the generated JSON.write_vscode_launchusesos.lstat + S_ISLNKsymlink rejection at the target path regardless offorce(security review fix MEDIUM #3 — TOCTOU defence; without this, force=True would follow a pre-placed symlink and overwrite a file outside cwd; mirrors v0.33.0 #22 / v0.40.2 #51 policy).utils/demo_bundles.pycopy_bundle_tovalidates every JSONL line viajson.loads, enforces 50 MB cap, stages writes to<target>.tmpwithos.lstat + S_ISLNKrejection (security review fix HIGH #2) andos.replace-atomic rename on success — mid-stream rejection never leaves a partial file. Newsoup data demoTyper command:--outputcontainment-checked via sharedis_under_cwd. CLI output prints_esc(written)to defend against Rich markup injection through a crafted output path (review fix LOW)._lcs_lengthalgorithmic correctness fix (review fix HIGH #2) + BLEU geometric-mean policy fix (returns 0 on any zero precision unlesssmooth=True) (review fix HIGH #1). Allmathimports moved to module-level (PEP 8 / project lazy-import policy: only torch / transformers / peft / trl / mlx are lazy). Known limitations: (1) MLflow / SwanLab / Trackio live integration deferred to v0.43.1 — schema accepts the names and threads into HFTrainingArguments.report_tobut Soup does not bundle the upstream packages; HF Trainer raises a generic ImportError when the package is absent. (2) PostHog telemetry deferred — payload schema +SOUP_TELEMETRY=1env var ship, but no network code;build_telemetry_payloadis purely a schema-locking stub (mirrors v0.27.0 MII / v0.37.0 multipack / v0.42.0 fsspec stub-then-live pattern). (3) Aider Polyglot live runner deferred —aider_polyglotis in the benchmark allowlist but live wiring requires the upstreamaider-chatpackage + Docker. (4)soup doctor --ncclmeasurement CLI surface deferred —nccl_bandwidth_checkaccepts a measured value and classifies it, but does not yet measure viatorch.distributed.all_reduce. (5)examples/data/fixtures shipped at the repo root (not as package data insidesoup_cli/);_bundle_source_pathderivesrepo_rootviastr(importlib.resources.files("soup_cli"))which is editable-install + wheel friendly but may break in zipapp / namespace-package installs. -
v0.42.0 — Data Pipeline Pro: closes the data-tooling gap with Axolotl + LlamaFactory in 18 features across 6 Parts. New
soup_cli/utils/data_pipeline.pyships an immutable_REMOTE_SCHEMESMappingProxyTypeallowlist (s3 / gs / gcs / az / abfs / abfss / oci);validate_remote_urirejects userinfo, fragments, AND query strings (the latter is SSRF-adjacent — fsspec backends interpret?endpoint_url=…as configuration overrides; a crafteds3://bucket/x?endpoint_url=https://attacker.comwould bypass otherwise-trusted infrastructure). Bucket regex^[a-zA-Z0-9][a-zA-Z0-9._\-]{0,62}$(1-63 chars, leading alnum) matches RFC 3986 + S3/GCS naming. Path length cap 2048; null-byte rejection on every string-shaped input; bool rejected before int isinstance check on every numeric input (buffer_size/shards/image_pixels/video_maxlen/num_datasets).parse_interleavereturns frozenInterleaveSpecdataclass; per-probmath.isfinite(rejects NaN AND ±inf, matches v0.32.0 / v0.41.0 policy); sum-to-1 ±1e-6; max 32 datasets.validate_new_tokenscaps lists at 10_000 entries with no duplicates, per-token <= 256 chars, null-byte rejected; returns a defensive copy.validate_prompt_strategyenforces amodule.path:function_nameregex with per-side 128 char cap (total <= 260).make_preprocess_cache_keyrejects bool / null-byte / non-string / non-positive on every parameter; uses\x1funit-separator delimiter.detect_ingest_formatrejects null-byte / empty / unsupported extensions. New schema validators onDataConfig:video_dir/tokenized_pathuse sharedis_under_cwdcontainment so a crafted YAML likedata.video_dir: ../../etcfails at config load (review-fix M1; distinct fromimage_dir/audio_dirlegacy policy).train_on_promptmutually exclusive withtrain_on_responses_only(matches v0.36.0 loss-mask exclusivity policy).interleavefield has its ownfield_validatorsodata.interleave: 99fails at config load (review-fix L3) AND bareinterleave: probsis rejected at schema load with the actionable "use {strategy: probs, probs: [...]} dict form" message instead of getting a confusing late error (review-fix HIGH #2). Image-pixel validator threadsinfo.field_nameso error messages name the actual field that tripped (review-fix M3). Bucket regex relaxed from{1,62}to{0,62}so 1-character bucket names (per S3/GCS spec) are no longer falsely rejected (review-fix HIGH #1)._convert_videovalidatesvideois non-empty string with null-byte rejection and 2048-char cap (review-fix H2)._convert_prmtype-checksprompt(non-empty string) + everycompletions[i](string) + everylabels[i](bool, not int — matches v0.30.0Candidatepolicy) and caps at 10_000 steps._convert_multimodalvalidates content-parttypeagainst the closed allowlist{text, image, audio, video}. Newsoup data preprocess <config>CLI:--configcontainment-checked viais_under_cwdBEFOREload_config(review-fix HIGH #1) so a crafted argument like../../etc/passwd.yamlcannot be opened. Newsoup data ingest <file>CLI:os.lstat + stat.S_ISLNKsymlink rejection on the input file (TOCTOU defence, mirrors v0.33.0 #22prune_checkpointspolicy);is_under_cwdcontainment on input + output. Known limitations: (1) fsspec live loaders deferred to v0.42.1 (schema gate fires now, runtime ImportError advisory); (2) AOT preprocess live tokenize loop deferred to v0.42.1 (cache-key emit + path-plan only); (3) custom prompt-strategy runtime invocation deferred to v0.42.1 (regex shape validation only); (4) PRM live trainer wiring deferred to v0.50 per original roadmap; (5) markdown ingest does not split on headings (the docstring's heading-split claim was aspirational); (6)_convert_video/_convert_multimodalvalidate path/url null-byte and length but do NOT verify file existence (matches v0.40.5reward_modelpolicy — file-existence is the trainer's responsibility). -
v0.41.0 — Optimizer & PEFT Zoo: closes the optimizer-breadth gap with LlamaFactory + Axolotl. New
soup_cli/utils/optimizer_zoo.pyships a closedSUPPORTED_OPTIMIZERSfrozenset(HF-native + bnb-backed + 14 v0.41.0 additions: BAdam / APOLLO / Adam-mini / lomo / adalomo / grokadamw / schedule_free_adamw / schedule_free_sgd / muon / dion / came_pytorch / ao_adamw_{fp8,4bit,8bit}).validate_optimizer_namerejects non-string / empty / null-byte / >64-char inputs; lower-cases the name for deterministic lookup (matches v0.30.0pick_draft_modelpolicy)._OPTIMIZER_PACKAGESwrapped intypes.MappingProxyTypeso the registry cannot be mutated at runtime (matches v0.36.0_REGISTRYpolicy).is_new_v0_41_optimizeris non-string-safe (returns False rather than raising). Newsoup_cli/utils/lr_groups.pyparsestraining.lr_groupsaccepting list-of-pairs / list-of-dicts /{pattern: lr}mapping; capped atMAX_LR_GROUPS=32; per-pattern non-empty string ≤256 chars + null-byte rejection +re.compilevalidation + best-effort ReDoS probe (compiled.search("a"*128)catches catastrophic-backtracking patterns); per-LR(0.0, 1.0]bounds +math.isfinite(rejects NaN AND±inf) + bool rejection (matches v0.30.0Candidatepolicy). Duplicate patterns rejected.LrGroupis@dataclass(frozen=True)(matches v0.32.0SpikeRecoveryStrategypolicy).lr_groups_from_schemaconverts the canonical stored shapeList[Dict]into runtimeList[LrGroup](closes the schema-to-runtime type gap that the code review caught).build_optimizer_param_groupsrejects bool / non-positivebase_lrat runtime (defence-in-depth). Newsoup_cli/utils/loftq_init.pyexposesvalidate_loftq_iter(∈ [1, 10], bool rejected) andvalidate_loftq_bits(∈ {2, 4, 8}, bool rejected) withbuild_loftq_configlazy-importingpeft.LoftQConfig(ImportError carries actionablepip install --upgrade pefthint). Newsoup_cli/utils/block_expansion.pyvalidators reject bool onexpand_layers/freeze_trainable_layersand usefield_validator(mode="before")so Pydantic'sge/ledoes not silently coerceTrueto1(matches v0.30.0 / v0.34.0 / v0.36.0 / v0.40.6 bool-as-int hardening policy)._count_layersuseshasattr(layers, "__len__")instead oftry/except TypeErrorso legitimate__len__bugs surface loudly. SchemaLoraConfig.init_strategyLiteral extended to{"random", "pissa", "olora", "loftq"}; cross-validator rejects loftq + use_dora / use_vera.TrainingConfig.load_in_8bit/load_in_16bituseis Truepolicy (matches v0.34.0 / v0.39.0 / v0.40.6is Noneover falsy guards) — explicitFalseis treated as "no preference" not as "off"; mutually-exclusive both-True rejected; combining alias=True with explicit Quant Menu format raises rather than silently overriding. The alias-drivenquantizationrewrite uses directself.quantization = ...assignment (NOTobject.__setattr__— code review caught that the latter would silently bypass any future field validator onquantization).expand_model_blocksraisesNotImplementedErrorwith a v0.41.1 marker on non-zero block counts (mirrors v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu / v0.39.0 ReLoRA stub-then-live pattern). Known limitations: (1) LLaMA Pro live wiring deferred to v0.41.1; (2) Mixture-of-Depths (use_mod=True) live patch deferred to v0.41.1; (3) optimizer dependency check is advisory at trainer construction time, not at schema-load (CI environments often lack optional optimizer packages); (4)load_in_8bit/load_in_16bitrewritequantizationonly when set toTrue— explicitFalseis intentionally a no-op so a YAML withload_in_8bit: falseandquantization: 4bitstill trains in 4-bit. -
v0.40.6 — ReLoRA + surgical PEFT non-SFT: closes the v0.39.0 known gap by extending the ReLoRA callback (v0.39.0 Part B) and the surgical PEFT patches (v0.39.0 Part D — Gemma4
ClippableLinear->nn.Linearswap, fused-MoE 3-D expert dropout strip) from SFT-only to all 11 non-SFT transformer-backend trainers (DPO / GRPO / KTO / ORPO / SimPO / IPO / PPO / RewardModel / Pretrain / Embedding / BCO). New shared modulesoup_cli/utils/peft_wiring.pyexposes three helpers:apply_pre_lora_patches(model, base)(Gemma4-gated, runs BEFOREget_peft_modelso PEFT'starget_modulesmatcher sees the swappednn.Linear),apply_post_lora_patches(model)(3-D MoE expert dropout strip, runs AFTER LoRA injection — architecture-detected viaweight.ndim == 3inside the helper, safe to call unconditionally),attach_relora_callback(trainer, tcfg)(returnsTrue/False; usesif relora_steps is None: return Falseper project policy so a schema-bypassing caller passingrelora_steps=0surfaces as a loudReLoRAPolicyValueError rather than a silent skip). SFT migrates to the same helpers in the same release (centralisation invariant): every trainer file calls only the helpers, eliminating the v0.39.0 inline copy insft.py.SoupConfig._validate_relora_supported_tasksremoves thetask != "sft"rejection branch; MLX backend rejection retained with a distinct error message (callback is HF Trainer-specific). Source-level grep matrix intests/test_v0406_part_a.pyproves all 12 transformer-backend trainers (sft + 11 non-SFT) callapply_pre_lora_patchesBEFOREget_peft_modelBEFOREapply_post_lora_patches, plus behavioural unit tests for each helper (Gemma4 happy path + exception swallow, post-LoRA strip happy path + exception swallow, ReLoRA policy field forwarding, schema-gate matrix covering every transformer task plus thepreferencedispatcher withpreference_loss='dpo'). Defence-in-depth carry-over:peft_wiringswallows broadExceptionfrom each upstream patch at DEBUG level (matches v0.39.0 Part D best-effort design);%sformatting onexc(notrepr) so$HOME-prefixed paths cannot leak (matches v0.34.0crash.pyredaction policy); the underlyingapply_gemma4_clippable_patchandstrip_lora_dropout_for_3d_expertsalready validate model_name (null bytes, length) and are duck-typed via v0.39.0 review fixes. Known limitations: (1) Multi-modal trainers (vision/audio paths insft.py) inherit ReLoRA + surgical patches because they share the SFT trainer wrapper, but the surgical patches are best-effort (try/except DEBUG-logged) — a Gemma4 vision model is unlikely in practice; if encountered, the patch attempt may noisy-log without applying. (2) The schema gate now accepts every transformer-backend task withrelora_steps, but real-world correctness on RLHF (PPO / RewardModel) is unverified — ReLoRA was originally validated on SFT/causal-LM training; rejection-sampling-style RL loops may interact unexpectedly with periodic LoRA pruning + optimizer reset. Tracked as a community QA item; the schema does not gate on this since the upstream paper does not preclude RL use. (3)apply_post_lora_patchesswallows exceptions at DEBUG, consistent with the v0.39.0 best-effort design — a real PEFT-side breakage instrip_lora_dropout_for_3d_expertswould silently no-op on a non-MoE model where the strip is also a no-op, so the silent fallback is acceptable. -
v0.40.5 — Quant Menu non-SFT: closes the v0.38.0 known gap by extending the seven Quant Menu formats (gptq / awq / hqq:Nbit / aqlm / eetq / mxfp4 / fp8) from SFT-only to all 11 transformer-backend trainers (DPO / GRPO / KTO / ORPO / SimPO / IPO / PPO / RewardModel / Pretrain / Embedding / BCO).
SoupConfig._validate_quant_menu_supported_tasksremoves thetask != "sft"rejection branch; MLX backend rejection retained with distinct message;modality != "text"rejection retained for vision/audio (multi-modal Quant Menu is tracked for a follow-up). Each non-SFT_setup_transformersreplaces its inline BNB-only branch with a call tobuild_quantization_config_for_loader(tcfg=tcfg, base=cfg.base, console=console)— same pattern assft.py:420-440, no remainingBitsAndBytesConfig(load_in_4bit=True ...)literal in any non-SFT trainer (source-level invariant test intests/test_v0405_part_a.py). The kbit-prep tuple is widened from("4bit", "8bit")to("4bit", "8bit", "mxfp4")so the BNB MXFP4 path runs throughprepare_model_for_kbit_training._load_reward_model(module-level helper inppo.py) accepts an optionaltcfg=Nonekwarg — when set, the reward model is loaded with the same Quant Menu config as the policy, defending against silent fp16 OOM on a GPTQ/AWQ/HQQ policy run. PPO call sites at_create_reward_model+_setup_rewardboth forwardtcfg=tcfg. Defence-in-depth: newTrainingConfig.reward_modelfield validator rejects null bytes and caps length at 512 chars at config-load (matches the policy applied tocfg.base); the Quant Menu loader's per-call null-byte check in_check_local_markerremains as the runtime backstop. Known limitations: (1) vision/audio modality + Quant Menu still rejected by the modality gate —_setup_vision_transformers/_setup_audio_transformersretain inlineBitsAndBytesConfigblocks because they need vision-specific kwargs the unified loader does not yet thread; (2) Autopilot's quantization picker still recommends only4bit/8bit/none— Quant Menu format awareness deferred; (3)tcfg.reward_modelis null-byte and length-validated at schema load but not path-containment-checked (is_under_cwd) — consistent with howcfg.baseis treated, both can be HF repo IDs or absolute local paths. -
v0.40.4 — trust_remote_code multi-trainer + multipack live: closes the v0.36.0 #63 known gap by extending the
--trust-remote-codeopt-in across every non-SFT trainer wrapper (DPO / GRPO / KTO / ORPO / SimPO / IPO / PPO / RewardModel / Pretrain / Embedding / BCO + the unifiedPreferenceTrainerWrapperdispatcher) and the 5 standalone commands (soup diff,soup export,soup merge,soup infer,soup data generate). Pattern (15 sites): each__init__resolves once viamodel_requires_trust_remote_code(config.base) or False+resolve_trust_remote_code(...)and storesself._trust_remote_code— everyfrom_pretrainedcall site now reads the resolved value (no remainingtrust_remote_code=Trueliteral in any trainer file; source-level invariant test intests/test_v0404_part_a.py).commands/train.pyno longer carries the v0.36.0sft_kwargssplit;trust_remote_codeis part of the unifiedtrainer_kwargsdict that flows to every trainer regardless of task._load_reward_model(module-level helper inppo.py) accepts atrust_remote_code: boolparameter and resolves internally — design intent is that the helper is independently safe to call from outsidePPOTrainerWrapper.PreferenceTrainerWrapperdispatcher forwards the raw bool to the inner DPO/SimPO/ORPO/IPO/BCO wrapper kwargs at both_build_innerand_build_multi_objectivesites; the resolver fires inside the inner wrapper at construction time._export_onnx/_export_tensorrt/_export_awq/_export_gptqand_merge_adapterhelpers all gain atrust_remote_code: bool = Falseparameter threaded from the Typer flag. Multipack live HF Trainer wiring (#65) lands via a newget_train_dataloaderoverride onmake_multipack_trainer_classthat installsMultipackBatchSampler(real_batches=False)(yields flatlist[int]per pack — DataLoader-compatible) as the DataLoader'sbatch_sampler=. The override forwardsargs.dataloader_drop_last/dataloader_num_workers/dataloader_pin_memoryfromTrainingArguments._get_train_sampleroverride stays as a defensive no-op fallback that ALWAYS delegates to super (review-fix: a multipacklist[list[int]]from this method would cause a shape mismatch if any HF eval / prediction loop bypassesget_train_dataloader). The state-presence guard switched from falsy (not max_seq) to explicitis None+not lengths(defensively rejects only-None and empty-list cases — non-positive ints already rejected upstream byattach_multipack_state). Falls back tosuper().get_train_dataloader()when state is missing OR whentrain_datasetis unset (defence-in-depth so the subclass remains safe to instantiate). Known limitations: (1)multipack: truerequires the dataset to exposeinput_ids(preferred) orlengthper row — un-tokenized text-only datasets trigger the v0.40.3 all-zeros WARNING and theMultipackBatchSamplerwill reject the run. (2) The DataLoader override does NOT thread FSDP / DeepSpeed parallelism env hints fromsuper().get_train_dataloader(), so distributedmultipack: trueruns are still untested under FSDP / ZeRO; tracked for v0.40.5+ paired with v0.42.0 multi-GPU work. (3)_live_lr_sweep_from_configincommands/train.pystill hardcodestrust_remote_code=Falsefor the LR sweep's internal model load — defensive but means--find-lrcannot consume custom-code models even with the user opt-in (defence-in-depth, not a bypass). (4) Each non-SFT trainer's__init__repeats the resolver block (10 sites) — code-quality refactor candidate (single shared helper) deferred to a future patch to keep the v0.40.4 diff focused on the gap closure. -
v0.40.3 — Stub-to-live: New
soup_cli/utils/batch_probe.py:make_cuda_probe_fnbuilds a CUDA probe closure that runs ONE forward+backward+step on a synthetic batch per candidate;model.zero_grad(set_to_none=True)runs BEFORE forward (defends against the synthetic backward accumulating into the live training model's grad buffers — matches v0.35.0 #45benchmark_kernel_combospolicy); intermediateids/attn/labels/outputsaredel-ed beforeloss.backward()so peak VRAM reflects a realistic training step;boolrejected onbatch_sizeandmax_length;max_length < 8rejected;torch.cuda.OutOfMemoryErrorreturns False, other exceptions propagate; returnsNone(no-op) on non-CUDA / no-torch / missing model or tokenizer. Newsoup_cli/utils/multipack_trainer.py:make_multipack_trainer_classislru_cached so two calls with the samebase_clsreturn the same subclass (consistentisinstance, pickle-safe);attach_multipack_staterejectsboolonmax_seq_len/batch_size/seedand rejects emptylengths;lengths_from_datasetlogs WARNING when every row produces 0 (loud-fail mirrors v0.37.0 multipack arch allowlist — prevents silent NaN-loss footgun);_get_train_sampleroverride accepts*args, **kwargsfor HF >= 4.41 signature compat. Live wiring of the sampler into SFT / Pretrain trainer wrappers is deferred to v0.40.4 — adversarial 5th-pass review surfaced aSampler[int]vslist[list[int]]shape mismatch with HF Trainer's DataLoader; the wrappers currently print a yellow advisory and fall back to the standard sampler whenmultipack: true. Newsoup_cli/data/traces/quality.py:judge_filter_pairsreuses v0.19.0JudgeEvaluatorSSRF protections; threshold rejectsbool/ NaN / out-of-[0,1];_MAX_BATCH=100_000cap applied via lazyitertools.islice(never fully materialises a malicious / pathological generator); per-pair backend exceptions caught and logged at DEBUG (matches v0.33.0 #47CrossDocCollatorpolicy — never silently crash the harvest);judge_providervalidated againstVALID_PROVIDERSallowlist at the CLI boundary BEFORE constructor, with a Rich-escape error on mismatch. Newsoup_cli/monitoring/trace_logger.py:TraceLogWriteris thread-safe (singlethreading.Lock— multi-worker--workers 4documented as a single-process limitation); path containment via sharedis_under_cwd; null-byte / empty / non-string path rejected;cap_mbbounds[1, 10000]with explicitboolrejection; rotation: whencurrent + extra > cap_bytes, rename to<path>.1(one backup retained); symlink at the backup path is rejected viaos.lstat + stat.S_ISLNK(matches v0.33.0 #22 TOCTOU policy) — defends against pre-placed<log>.1 -> /etc/cron.d/xoverwrite. Secret redaction: prompt + response strings passed through_SECRET_REmatchinghf_*(≥8),sk-*(≥16), andBearer …(≥8) — replaces matches with<redacted>before serialisation (mirrors v0.34.0crash.pypolicy).--trace-logconstructor error messages incommands/serve.pyarerich.markup.escaped before printing so a crafted path name cannot inject Rich markup. Unserialisable entries dropped silently; disk-full / OSError on write never crashes the request handler (passive log). Known limitations: live CUDA probe is wired in SFT only; multipack live wiring covers SFT+Pretrain only;TraceLogWriterretains exactly ONE backup file (operators wanting longer retention should use external rotation); custom HF Space templates from v0.40.2 still always create the Space withspace_sdk="gradio"(tracked for v0.40.4+). -
v0.40.2 — Quick polish + carry-overs: New
soup_cli/utils/hf_space.py:render_custom_template_direnforcesis_under_cwdcontainment on the template directory;validate_repo_idruns BEFORE{MODEL_REPO}substitution (matches v0.29.0 Part F policy); per-file 256 KB cap (matches v0.39.0 Part E template-size policy); onlyapp.py/README.md/requirements.txtare read (closed allowlist — no path-from-user-data). Symlinks rejected viaos.lstat + stat.S_ISLNKand non-regular files (FIFO / device) also rejected (matches v0.33.0 #22 prune_checkpoints TOCTOU policy) — defends against<template_dir>/app.py -> /etc/passwd._find_highest_local_checkpointreadsoutput_dirafter caller'sis_under_cwdvalidation (inprepare_hf_resume) and silently drops non-directories + OSError.prepare_hf_resumeskips the snapshot download when localcheckpoint-N >= remote checkpoint-N(saves bandwidth and never overwrites a fresher local checkpoint).commands/data.py:register_datacontainment switched fromPath.resolve() + relative_to()to sharedis_under_cwd(Windows 8.3 short-name safety per CLAUDE.md project rule); same fix applied tocommands/bench.pyprompts-file containment.commands/infer.py:--outputnow containment-checked viais_under_cwd(late-evaluated after model+input validation so pre-existingtmp_pathtest contracts keep working).commands/quickstart.py:--outputvalidates target dir viais_under_cwdbeforemkdir(parents=True); rejects out-of-cwd targets with friendly message.commands/runs.py:_filter_runs_by_cwdusesos.path.realpath + commonpath, catches(ValueError, OSError)so cross-drive paths on Windows (D:\runsvsC:\project) drop silently rather than crash.monitoring/display.py:format_gate_rowuses explicittask.get("passed") is Trueso a missing"passed"field renders neutrally instead of as a false-y red ✗.commands/infer.py:_resolve_model_sourceheuristic for HF-id-vs-local-path: only falls through to HF when value is NOT path-like (no./,/,\\,~, no Windows drive letter, non-empty); path-like-but-missing raisesFileNotFoundErrorso users see actionable errors instead of confusing HF download attempts. Known limitation: custom HF Space templates always create the Space withspace_sdk="gradio"regardless of the suppliedapp.py(no--sdkflag in this release; combine--template streamlit-chatwith the inline registry for Streamlit Spaces). Tracked for v0.40.3+. -
v0.40.1 — QA Hardening:
soup_cli/utils/encoding.force_utf8_stdioreconfigures Windows stdout/stderr to UTF-8 before any Rich Console is constructed;os.environ.setdefault("PYTHONIOENCODING", "utf-8")preserves user override;(OSError, ValueError, AttributeError)swallowed on detached streams; POSIX no-op.SoupConfig._remap_root_level_misplaced_keys(model_validator, mode='before') migrates root-levellora:intotraining.loraso nested validators (includinglora.init_strategy: Literal["random","pissa","olora"]) actually fire — closes a footgun where the misplaced key was silently dropped. Caller's dict is never mutated (shallow-copy policy mirroring v0.33.0 #47 / v0.40.0 Part B).PreferenceTrainerWrapper._build_multi_objectivereplaces the v0.40.0NotImplementedErrorstub with a primary-loss approximation;validate_weight_compatrejects BCO mixed with paired losses at runtime (data-format incompatible).combine_lossesrejects empty weights, propagates NaN loudly (no silent zeroing), and rejectsboolweight values (matches v0.30.0Candidatepolicy)._probe_cache_param_countrejects empty / null-byte model names before path construction (mirrors v0.26.0 registry / v0.39.0 ReLoRAPolicy policy).commands/doctorflagstransformers ≥ 5.0.0as INCOMPATIBLE via_MAX_EXCLUSIVEtable;_version_geparses leading-int chunks so5.0.0.dev0correctly trips the cap._detect_gpu_hw_without_torch_cudacallsnvidia-smivia argv list (no shell), 5s timeout,OSError/TimeoutExpiredcaught; GPU label fromnvidia-smistdout isrich.markup.escaped before embedding in Rich-markup string (a real GPU name likeNVIDIA Quadro [T4]cannot break or inject markup)._detect_dual_python_interpretersusesos.path.realpath(notPath.resolve()) for Windows 8.3 short-name compat._pick_quickstart_modelswaps TinyLlama-1.1B → SmolLM2-135M whentotal_memory ≤ 6 GB(prevents step-0 OOM on RTX 3050 4 GB / similar)._live_lr_sweep_from_configswitched brokenload_localimport toload_raw_data(previously always silently fell back to a static placeholder curve).commands/migraterejects.jsonlinput (with first-line{sniff) with exit-2 friendly error;.jsonl-only suffix gate prevents false-positives on.ipynbnotebooks.commands/eval custom -ois now honored independently of--attach-to-registry; loop-shadow regression whereoutput = generate_fn(...)overwrote the CLI option fixed (variable renamed toresponse)._load_jsonlswitched fromutf-8toutf-8-sigso PowerShellOut-File -Encoding utf8-produced JSONL no longer fails first-row parse. Known limitation:--trust-remote-codeopt-in surface still excludes 10 non-SFT trainers + 5 commands (v0.36.0 #63 carry-over). -
v0.40.0 — Preference Variety: New
task='bco'(Binary Classifier Optimization) andtask='preference'(unified dispatcher). New schema fields:bco_beta(gt=0),preference_loss: Literal[dpo,simpo,orpo,ipo,bco]|None,preference_loss_weights: Optional[Dict[str,float]],dpo_beta_schedule: Literal[linear,cosine,exponential]|None,dpo_beta_end: float, gt=0|None,dpo_ref_regen_epochs: int [1,1000]|None. Cross-validators:_validate_preference_dispatcherrejects setting eitherpreference_lossorpreference_loss_weightsoutsidetask='preference'(closes ordering-dependency between Part B/D validators);_validate_dpo_variants_supported_tasksgates β-schedule + ref-regen to DPO-family tasks (dpo,ipo, orpreference+preference_loss in {dpo, ipo}); rejected on mlx backend with distinct error message (matches v0.34.0 distinct-reason policy);_validate_preference_loss_weightsenforces 2–5 entries (single-entry rejected with actionable message pointing at scalarpreference_loss), key allowlist{dpo, simpo, orpo, ipo, bco}, explicit null-byte rejection on keys (matches v0.39.0 rank_pattern policy), per-value bounds(0, 1], weights must sum to 1.0 (±1e-6), mutually exclusive with scalarpreference_loss, rejected on mlx backend.compute_beta_at_steprejectsboolonstepandtotal_steps(project bool-as-int policy from v0.30.0).BetaScheduleCallbackresolvestotal_stepslazily inon_train_beginso the schedule sees the realstate.max_stepspopulated by HF Trainer (closes a first-cut silent-no-op bug where total_steps=0 emitted beta_end for every step).RefModelRegenCallback._regenerateusesstrict=Trueonload_state_dictand logs at WARNING on mismatch (closes a first-cut silent partial-copy hazard where strict=False could produce a hybrid old-base + new-LoRA reference); epoch 0 regen suppressed (avoids copying untrained student); trainer.betaassignment swallow narrowed toAttributeErroronly.PreferenceTrainerWrapper._make_inner_cfgusesmodel_copy(notmodel_dump+model_validate) so re-validation never sees an inconsistent intermediate state and the caller'scfgis never mutated (mirrors v0.33.0 #47 immutability policy)._split_dpo_rows_to_bcoskipped-row count emitted at DEBUG so production silent-degradation is inspectable (mirrors v0.33.0 #47 CrossDocCollator policy). Multi-objective live runtime weighted-loss combination is deferred to v0.40.1:PreferenceTrainerWrapper.setupraisesNotImplementedErrorwith a friendly message naming the deferred-version follow-up (mirrors v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu / v0.39.0 ReLoRA stub-then-live pattern). Known limitation:BCOTrainerWrapper._setup_transformersstill hardcodestrust_remote_code=True(v0.36.0 #63 known-gap family carry-over across non-SFT trainers). -
v0.39.0 — LoRA Quality:
LoraConfig.init_strategy: Literal["random","pissa","olora"]rejects unknown strategies; PiSSA + DoRA / VeRA combinations rejected at config-load.model_validator(mode="before")alignsuse_olora=True→init_strategy="olora"via dict-copy (no caller mutation; matches v0.33.0 #47 immutability policy).rank_pattern/alpha_pattern: Optional[Dict[str, int]]capped at 256 keys × value (0, 1024], rejectsbool(subclass ofint— matches v0.30.0Candidatepolicy), null bytes in keys, empty keys; cross-validator rejects withuse_vera=True.ReLoRAPolicyis@dataclass(frozen=True)(post-construction mutation raisesFrozenInstanceError); bounds:steps ∈ [1, 1e7],warmup_ratio ∈ [0, 1],prune_ratio ∈ (0, 1)(strict — prevents zero-everything footgun).magnitude_prune_tensorstrict0 < prune_ratio < 1rejection, non-Tensor input raisesTypeError, empty / single-element tensor short-circuits (avoidskthvalue(_, 0)runtime crash)._validate_relora_supported_taskscross-validator rejectsrelora_stepswithtask != "sft"andbackend=mlxwith distinct error messages (matches v0.34.0 distinct-reason policy); multi-trainer expansion deferred to v0.39.1.is_gemma4_modeluses a word-boundary regex ((?:^|[^a-z0-9])gemma-?4(?:[^a-z0-9]|$)) so"ungemma4ed"/"my-gemma4ish"no longer over-match; null-byte rejection onmodel_name.apply_gemma4_clippable_patchweight-copy fallback logs at DEBUG instead of silent random-init; the patch is gated byis_gemma4_model(cfg.base)insft.pybefore invocation so non-Gemma4 trainings never traverse the module tree.apply_surgical_patchesrejects empty / null-bytemodel_namewithValueError.templates/load_templatecontainment: filename re-validated via_validate_name(rejects..///\\/null/empty);os.path.realpath + os.path.commonpathcontainment check on the resolved path against_templates_dir()so a tamperedmanifest.jsoncannot read files outside the package directory (mirrors v0.26.0 registry policy). Tampered-manifestValueErrorfrom_validate_namecaught and falls back to inline (no propagating exception). 256 KB file-size cap. InlineTEMPLATEScarries an explicit deprecation comment pointing at the canonical YAML registry;tests/test_templates_yaml.pyasserts byte-equality of all 16 inline ↔ YAML pairs to prevent silent drift. Planned removal: v0.41.0+. -
v0.38.0 — Quant Menu:
TrainingConfig.quantizationLiteral extended withgptq/awq/hqq:1bit..hqq:8bit(nohqq:7bit— HQQ doesn't support it) /aqlm/eetq/mxfp4/fp8; Pydantic rejects every other string at config-load.validate_gptq_checkpointandvalidate_awq_checkpointprobe local paths forquantize_config.json/quant_config.json; HF repo IDs fall through; null-byte rejection + non-stringTypeErroron the ref._validate_prequantized_no_qatrejects every pre-quantized format combined withquantization_aware(int8 QAT or'fp8') — pre-quantized weights carry their own scale and QAT/FP8 prepare would silently corrupt them (mirrors LlamaFactoryquantization.py:117/199/211)._validate_bnb_quant_storage_only_with_4bitrejectsbnb_4bit_quant_storageon every non-BNB-4bit format (silent no-op otherwise); allowed dtypes:Literal["uint8", "float16", "bfloat16", "float32"]._validate_quant_menu_supported_tasksrestricts the new formats totask='sft'onbackend='transformers'in v0.38.0 with distinct MLX-backend vs unsupported-task error messages (matches v0.34.0 distinct-reason policy).check_quant_distributed_compathard-fails HQQ/EETQ/AQLM × {FSDP, ZeRO-3} (sourced from LlamaFactoryquantization.py:199/211plus AQLM dequant constraints); warning-tier (not error) for BNB-4bit + FSDP withoutbnb_4bit_quant_storageso users see the silent perf cliff; unknownquantizationraisesValueError(no silent pass) and the check is wired intocommands/train.pystartup.parse_hqq_bitsrejects unsupported bit-rates and malformedhqq:strings before any kernel build. -
v0.37.0 — Multipack:
validate_multipack_architectureraisesValueErroron unknown arch (loud-fail vs Axolotl's silent-miss footgun); 18-arch frozen allowlist (Llama 3.x / Qwen 2/3 / Mistral / Gemma 2/3 / Phi 3/4 / DeepSeek V2/V3 / Mixtral / Falcon / StableLM / SmolLM2). FFD packer caps at_MAX_FFD_ITEMS=1_000_000(algorithm is O(N²) worst-case — defence against adversarial dataset DoS);boolrejection on every numeric input (max_len, per-elementlengths,batch_max_len,batch_size,seed,max_seq_length) matches v0.30.0+Candidatepolicy; generator-input materialisation prevents silent empty-bin output when validation exhausts the iterator.MultipackBatchSamplerrejects emptylengths, non-positivebatch_max_len/batch_size, items larger thanbatch_max_len.build_multipack_sampler_for_lengthsrejectstcfg.batch_size="auto"with actionable message (must be resolved upstream)._validate_multipack_packing_exclusivecross-validator onTrainingConfigrejects bothmultipackandpackingset;_validate_multipack_supported_tasksonSoupConfigrestricts multipack tosft/pretrainontransformersbackend with distinct error messages for MLX backend vs unsupported task (matches v0.34.0 distinct-reason policy).build_4d_attention_maskcaps allocations at_MAX_MASK_ELEMENTS=2³¹cells (~8GB float32) — defence againstmax_length=1M×batch_size=8OOM;tag_sub_sequencescapped at_MAX_BOUNDARY_SEGMENTS=1_000_000. Mask builder rejects non-floating dtypes (was silentnp.finfoValueError), non-2Dseq_pos_ids, negative segment IDs; padding (id=0) tokens are fully masked, including diagonal, so softmax is well-defined.select_packing_strategyrejects non-boolflash_attn_available.JinjaTemplateAnalyzerparses chat templates viaEnvironment.parseonly — never renders, so a crafted soup.yaml cannot trigger SSRF / filesystem reads; 128KB template cap, null-byte rejection,TemplateSyntaxErrorre-raised asValueError.DEFAULT_MESSAGE_FIELDSis afrozenset(runtime-immutable);JinjaTemplateAnalyzer.message_fieldsreturns a defensive copy. -
v0.36.0 — Correctness First:
--trust-remote-codeopt-in replaces 9 unconditionaltrust_remote_code=Truecall sites acrosssoup train/chat/serve/data download/eval auto;KNOWN_SAFE_PREFIXESallowlist (15 first-party orgs) suppresses warning panel for trusted repos;model_requires_trust_remote_codeprobes localconfig.jsonforauto_map(HF Hub repo IDs returnNone/unknown — HF still raises loudly when custom code is actually needed);resolve_trust_remote_coderaisesValueErrorwith actionable message when model needs custom code but the user did not opt in. Chat-template hardening:DataConfig.chat_templatevalidator rejects null bytes, oversize (>64KB), AND filesystem-touching Jinja directives ({% include %},{% import %},{% from %},{% macro %},{% extends %}— both whitespace-control variants); empty string normalised toNone;_REGISTRYwrapped inMappingProxyType(callers cannot mutate);apply_chat_template_overrideemits yellow advisory when active so users knowsoup pushwill persist the override intotokenizer_config.json. SFT silent f-string fallback (f"{role}: {content}") replaced with hardValueError— produced wrong loss labels for years on tokenizers without a chat template. Loss-mask fallback passesadd_special_tokens=Falseto incremental tokenize calls so HF cannot double-prepend BOS at front of each render (consistent prefix-delta walk); narrowed exception catch in_apply_template_with_maskfrom(TypeError, ValueError)toTypeErroronly so a malformed messages list propagates instead of falling through to the loose path.SOUP_BATCH_CACHE_PATHenv override containment-checked viaos.path.realpath + commonpathagainst~/cwd/tempfile.gettempdir(); out-of-bounds values fall through to safe default; cache file gets best-effort0o600perms after atomic rename (matches v0.26.0 registry.db policy).make_cache_keyrejectsboolin numeric inputs (matches v0.30.0Candidatepolicy). Documented limitation: non-SFT trainers (DPO/GRPO/KTO/ORPO/SimPO/IPO/PPO/RewardModel/Pretrain/Embedding) andcommands/{diff,export,merge,infer,generate}.pystill hardcodetrust_remote_code=True— v0.36.x patch follow-up.
Security Scanning
- All code is scanned with
rufffor style and common issues - Dependencies are regularly updated to patch known CVEs
- GitHub's dependency scanning alerts us to vulnerable dependencies
- We use GitHub Actions CI/CD for continuous integration
Dependency Updates
We actively monitor and update dependencies:
- Major dependency updates: Tested in PR before merging
- Security patches: Applied immediately and released as patch versions
- Deprecated dependencies: Replaced proactively
Coming Soon
- Automated dependency scanning
- SBOM (Software Bill of Materials) for each release
- Third-party security audit (after 1.0.0 release)
Questions?
If you have security questions (not vulnerability reports) or need clarification:
- Open a GitHub Discussion tagged
security - Open a GitHub Issue (non-vulnerability inquiries)
- Check our CONTRIBUTING.md for general support
License
This Security Policy is provided under the Apache-2.0 license, same as the Soup project.
Last Updated: April 2026
For the latest version of this policy, visit: https://github.com/MakazhanAlpamys/Soup/blob/main/SECURITY.md