soup-cli 0.40.0__tar.gz → 0.40.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. {soup_cli-0.40.0 → soup_cli-0.40.2}/CONTRIBUTING.md +1 -1
  2. {soup_cli-0.40.0 → soup_cli-0.40.2}/PKG-INFO +16 -9
  3. {soup_cli-0.40.0 → soup_cli-0.40.2}/README.md +14 -7
  4. {soup_cli-0.40.0 → soup_cli-0.40.2}/SECURITY.md +4 -1
  5. {soup_cli-0.40.0 → soup_cli-0.40.2}/pyproject.toml +2 -2
  6. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/__init__.py +1 -1
  7. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/autopilot/analyzer.py +56 -3
  8. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cli.py +27 -10
  9. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/bench.py +23 -10
  10. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/data.py +78 -19
  11. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/deploy.py +25 -4
  12. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/doctor.py +138 -10
  13. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/eval.py +24 -11
  14. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/history.py +23 -0
  15. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/infer.py +65 -5
  16. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/init.py +15 -3
  17. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/migrate.py +29 -0
  18. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/quickstart.py +95 -9
  19. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/recipes.py +22 -0
  20. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/runs.py +39 -1
  21. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/train.py +7 -2
  22. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/config/schema.py +32 -0
  23. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/loader.py +6 -1
  24. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/monitoring/display.py +44 -1
  25. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/monitoring/hf_push.py +42 -0
  26. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/preference.py +70 -11
  27. soup_cli-0.40.2/soup_cli/utils/encoding.py +43 -0
  28. soup_cli-0.40.2/soup_cli/utils/hf_space.py +94 -0
  29. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/log_level.py +13 -0
  30. soup_cli-0.40.2/soup_cli/utils/preference_combine.py +184 -0
  31. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_data_sample.py +4 -2
  32. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_pissa_init.py +25 -0
  33. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_preference_multi.py +5 -7
  34. soup_cli-0.40.2/tests/test_preference_multi_runtime.py +249 -0
  35. soup_cli-0.40.2/tests/test_v0401_part_c.py +182 -0
  36. soup_cli-0.40.2/tests/test_v0401_part_d.py +125 -0
  37. soup_cli-0.40.2/tests/test_v0401_part_e.py +84 -0
  38. soup_cli-0.40.2/tests/test_v0402_part_a.py +370 -0
  39. soup_cli-0.40.2/tests/test_v0402_part_b.py +251 -0
  40. soup_cli-0.40.2/tests/test_windows_encoding.py +121 -0
  41. {soup_cli-0.40.0 → soup_cli-0.40.2}/.dockerignore +0 -0
  42. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/FUNDING.yml +0 -0
  43. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  44. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  45. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  46. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/pull_request_template.md +0 -0
  47. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/workflows/ci.yml +0 -0
  48. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/workflows/docker.yml +0 -0
  49. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/workflows/publish.yml +0 -0
  50. {soup_cli-0.40.0 → soup_cli-0.40.2}/.github/workflows/recipe-validation.yml +0 -0
  51. {soup_cli-0.40.0 → soup_cli-0.40.2}/.gitignore +0 -0
  52. {soup_cli-0.40.0 → soup_cli-0.40.2}/CODEOWNERS +0 -0
  53. {soup_cli-0.40.0 → soup_cli-0.40.2}/CODE_OF_CONDUCT.md +0 -0
  54. {soup_cli-0.40.0 → soup_cli-0.40.2}/Dockerfile +0 -0
  55. {soup_cli-0.40.0 → soup_cli-0.40.2}/LICENSE +0 -0
  56. {soup_cli-0.40.0 → soup_cli-0.40.2}/NOTICE +0 -0
  57. {soup_cli-0.40.0 → soup_cli-0.40.2}/docker-compose.yml +0 -0
  58. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/README.md +0 -0
  59. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/dpo_chat.yaml +0 -0
  60. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/dpo_example.yaml +0 -0
  61. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/grpo_reasoning.yaml +0 -0
  62. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/rlhf_step1_sft.yaml +0 -0
  63. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/rlhf_step2_reward.yaml +0 -0
  64. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/rlhf_step3_ppo.yaml +0 -0
  65. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/sft_basic.yaml +0 -0
  66. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/configs/vision_llama.yaml +0 -0
  67. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/data/alpaca_tiny.jsonl +0 -0
  68. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/data/chat_preferences.jsonl +0 -0
  69. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/data/dpo_sample.jsonl +0 -0
  70. {soup_cli-0.40.0 → soup_cli-0.40.2}/examples/data/reasoning_math.jsonl +0 -0
  71. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup.png +0 -0
  72. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/__main__.py +0 -0
  73. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/autopilot/__init__.py +0 -0
  74. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/autopilot/decisions.py +0 -0
  75. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/autopilot/generate_config.py +0 -0
  76. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/__init__.py +0 -0
  77. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/pack.py +0 -0
  78. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/publish.py +0 -0
  79. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/run.py +0 -0
  80. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/schema.py +0 -0
  81. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/unpack.py +0 -0
  82. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/cans/verify.py +0 -0
  83. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/__init__.py +0 -0
  84. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/adapters.py +0 -0
  85. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/autopilot.py +0 -0
  86. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/can.py +0 -0
  87. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/chat.py +0 -0
  88. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/cost.py +0 -0
  89. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/diff.py +0 -0
  90. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/export.py +0 -0
  91. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/generate.py +0 -0
  92. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/merge.py +0 -0
  93. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/profile.py +0 -0
  94. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/push.py +0 -0
  95. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/registry.py +0 -0
  96. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/serve.py +0 -0
  97. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/sweep.py +0 -0
  98. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/tui.py +0 -0
  99. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/ui.py +0 -0
  100. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/commands/why.py +0 -0
  101. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/config/__init__.py +0 -0
  102. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/config/loader.py +0 -0
  103. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/__init__.py +0 -0
  104. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/augment.py +0 -0
  105. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/chat_templates.py +0 -0
  106. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/collators.py +0 -0
  107. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/formats.py +0 -0
  108. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/loss_mask.py +0 -0
  109. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/providers/__init__.py +0 -0
  110. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/providers/_utils.py +0 -0
  111. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/providers/anthropic.py +0 -0
  112. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/providers/ollama.py +0 -0
  113. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/providers/vllm.py +0 -0
  114. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/sft_format.py +0 -0
  115. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/__init__.py +0 -0
  116. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/code.py +0 -0
  117. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/conversation.py +0 -0
  118. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/preference.py +0 -0
  119. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/qa.py +0 -0
  120. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/reasoning.py +0 -0
  121. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/tool_calling.py +0 -0
  122. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/templates/verifiable.py +0 -0
  123. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/traces/__init__.py +0 -0
  124. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/traces/pair_builder.py +0 -0
  125. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/traces/parsers.py +0 -0
  126. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/data/validator.py +0 -0
  127. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/__init__.py +0 -0
  128. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/checkpoint_intelligence.py +0 -0
  129. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/custom.py +0 -0
  130. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/forgetting.py +0 -0
  131. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/gate.py +0 -0
  132. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/human.py +0 -0
  133. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/judge.py +0 -0
  134. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/leaderboard.py +0 -0
  135. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/eval/quant_check.py +0 -0
  136. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/experiment/__init__.py +0 -0
  137. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/experiment/tracker.py +0 -0
  138. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/migrate/__init__.py +0 -0
  139. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/migrate/axolotl.py +0 -0
  140. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/migrate/common.py +0 -0
  141. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/migrate/llamafactory.py +0 -0
  142. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/migrate/unsloth.py +0 -0
  143. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/monitoring/__init__.py +0 -0
  144. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/monitoring/callback.py +0 -0
  145. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/recipes/__init__.py +0 -0
  146. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/recipes/catalog.py +0 -0
  147. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/registry/__init__.py +0 -0
  148. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/registry/attach.py +0 -0
  149. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/registry/diff.py +0 -0
  150. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/registry/hashing.py +0 -0
  151. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/registry/store.py +0 -0
  152. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/__init__.py +0 -0
  153. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/audio.yaml +0 -0
  154. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/bco.yaml +0 -0
  155. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/chat.yaml +0 -0
  156. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/code.yaml +0 -0
  157. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/embedding.yaml +0 -0
  158. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/ipo.yaml +0 -0
  159. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/kto.yaml +0 -0
  160. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/longcontext.yaml +0 -0
  161. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/manifest.json +0 -0
  162. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/medical.yaml +0 -0
  163. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/moe.yaml +0 -0
  164. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/orpo.yaml +0 -0
  165. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/pretrain.yaml +0 -0
  166. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/reasoning.yaml +0 -0
  167. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/rlhf.yaml +0 -0
  168. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/simpo.yaml +0 -0
  169. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/tool-calling.yaml +0 -0
  170. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/templates/vision.yaml +0 -0
  171. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/__init__.py +0 -0
  172. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/bco.py +0 -0
  173. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/dpo.py +0 -0
  174. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/embedding.py +0 -0
  175. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/grpo.py +0 -0
  176. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/ipo.py +0 -0
  177. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/kto.py +0 -0
  178. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/mlx_dpo.py +0 -0
  179. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/mlx_grpo.py +0 -0
  180. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/mlx_routing.py +0 -0
  181. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/mlx_sft.py +0 -0
  182. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/orpo.py +0 -0
  183. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/ppo.py +0 -0
  184. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/pretrain.py +0 -0
  185. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/reward_model.py +0 -0
  186. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/rewards.py +0 -0
  187. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/sft.py +0 -0
  188. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/trainer/simpo.py +0 -0
  189. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/tui_app.py +0 -0
  190. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/__init__.py +0 -0
  191. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/app.py +0 -0
  192. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/static/app.js +0 -0
  193. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/static/index.html +0 -0
  194. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/static/logo.png +0 -0
  195. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/static/logo.svg +0 -0
  196. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/ui/static/style.css +0 -0
  197. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/__init__.py +0 -0
  198. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/activation_offload.py +0 -0
  199. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/auto_quant.py +0 -0
  200. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/batch_probe.py +0 -0
  201. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/constants.py +0 -0
  202. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/convergence.py +0 -0
  203. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/crash.py +0 -0
  204. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/cross_doc_attn.py +0 -0
  205. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/curriculum.py +0 -0
  206. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/cut_ce.py +0 -0
  207. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/deepspeed.py +0 -0
  208. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/dpo_variants.py +0 -0
  209. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/errors.py +0 -0
  210. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/flash_attn.py +0 -0
  211. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/fp8.py +0 -0
  212. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/freeze.py +0 -0
  213. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/fsdp.py +0 -0
  214. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/galore.py +0 -0
  215. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/gpu.py +0 -0
  216. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/grad_accum.py +0 -0
  217. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/gradient_ckpt.py +0 -0
  218. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/hf.py +0 -0
  219. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/jinja_analyzer.py +0 -0
  220. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/kernel_picker.py +0 -0
  221. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/launcher.py +0 -0
  222. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/liger.py +0 -0
  223. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/long_context.py +0 -0
  224. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/lr_finder.py +0 -0
  225. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/metrics.py +0 -0
  226. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/mii.py +0 -0
  227. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/mixed_precision.py +0 -0
  228. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/mlx.py +0 -0
  229. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/moe.py +0 -0
  230. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/multipack.py +0 -0
  231. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/multipack_sampler.py +0 -0
  232. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/neat_packing.py +0 -0
  233. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/ollama.py +0 -0
  234. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/paths.py +0 -0
  235. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/peft_builder.py +0 -0
  236. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/peft_patches.py +0 -0
  237. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/pipeline.py +0 -0
  238. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/profiler.py +0 -0
  239. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/profiling.py +0 -0
  240. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/qat.py +0 -0
  241. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/quality.py +0 -0
  242. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/quant_menu.py +0 -0
  243. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/registry.py +0 -0
  244. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/relora.py +0 -0
  245. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/replay.py +0 -0
  246. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/ring_attention.py +0 -0
  247. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/run_cost.py +0 -0
  248. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/sglang.py +0 -0
  249. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/spec_pairing.py +0 -0
  250. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/spike_recovery.py +0 -0
  251. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/structured_output.py +0 -0
  252. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/topology.py +0 -0
  253. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/tracing.py +0 -0
  254. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/trust_remote.py +0 -0
  255. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/unsloth.py +0 -0
  256. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/v028_features.py +0 -0
  257. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/vllm.py +0 -0
  258. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/warmup.py +0 -0
  259. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_cli/utils/why.py +0 -0
  260. {soup_cli-0.40.0 → soup_cli-0.40.2}/soup_logo_svg.svg +0 -0
  261. {soup_cli-0.40.0 → soup_cli-0.40.2}/templates/chat.yaml +0 -0
  262. {soup_cli-0.40.0 → soup_cli-0.40.2}/templates/code.yaml +0 -0
  263. {soup_cli-0.40.0 → soup_cli-0.40.2}/templates/medical.yaml +0 -0
  264. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/__init__.py +0 -0
  265. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/conftest.py +0 -0
  266. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_adapters.py +0 -0
  267. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_advanced_peft.py +0 -0
  268. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_assistant_mask.py +0 -0
  269. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_audio.py +0 -0
  270. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_auto_tuning.py +0 -0
  271. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_autopilot.py +0 -0
  272. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_awq_gptq_export.py +0 -0
  273. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_batch_probe.py +0 -0
  274. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_bco.py +0 -0
  275. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_bench.py +0 -0
  276. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_bugfixes.py +0 -0
  277. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_callback.py +0 -0
  278. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_cans.py +0 -0
  279. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_chat.py +0 -0
  280. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_chat_template.py +0 -0
  281. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_cli.py +0 -0
  282. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_cli_subprocess.py +0 -0
  283. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_config.py +0 -0
  284. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_cost.py +0 -0
  285. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_crash_reporter.py +0 -0
  286. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_curriculum.py +0 -0
  287. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_data.py +0 -0
  288. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_data_augment.py +0 -0
  289. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_data_split.py +0 -0
  290. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_data_tools.py +0 -0
  291. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_dataset_hub.py +0 -0
  292. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_dataset_registry.py +0 -0
  293. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_deepspeed.py +0 -0
  294. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_deploy_ollama.py +0 -0
  295. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_diff.py +0 -0
  296. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_display.py +0 -0
  297. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_doctor.py +0 -0
  298. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_dpo_example.py +0 -0
  299. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_dpo_variants.py +0 -0
  300. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_embedding.py +0 -0
  301. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_errors.py +0 -0
  302. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_eval.py +0 -0
  303. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_eval_gate.py +0 -0
  304. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_eval_platform.py +0 -0
  305. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_export.py +0 -0
  306. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_formats.py +0 -0
  307. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_fp8_recipe.py +0 -0
  308. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_freeze_training.py +0 -0
  309. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_generate.py +0 -0
  310. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_gpu.py +0 -0
  311. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_grpo.py +0 -0
  312. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_hf_integration.py +0 -0
  313. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_infer.py +0 -0
  314. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_inference_advanced.py +0 -0
  315. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_init.py +0 -0
  316. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ipo.py +0 -0
  317. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_jinja_analyzer.py +0 -0
  318. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_kto.py +0 -0
  319. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_loader.py +0 -0
  320. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_log_level.py +0 -0
  321. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_loss_watchdog.py +0 -0
  322. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_merge.py +0 -0
  323. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_migrate.py +0 -0
  324. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_mlx_backend.py +0 -0
  325. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_moe.py +0 -0
  326. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_multi_adapter.py +0 -0
  327. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_multi_gpu.py +0 -0
  328. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_multipack_config.py +0 -0
  329. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_multipack_invariants.py +0 -0
  330. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_multipack_sampler.py +0 -0
  331. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_neat_packing.py +0 -0
  332. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_neftune_rslora.py +0 -0
  333. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_onnx_tensorrt_export.py +0 -0
  334. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_orpo.py +0 -0
  335. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_packing.py +0 -0
  336. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_a_wave1.py +0 -0
  337. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_a_wave2.py +0 -0
  338. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_b.py +0 -0
  339. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_c.py +0 -0
  340. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_d.py +0 -0
  341. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_e.py +0 -0
  342. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_part_f_hardening.py +0 -0
  343. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_peft_methods.py +0 -0
  344. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_peft_patches.py +0 -0
  345. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_performance.py +0 -0
  346. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ppo.py +0 -0
  347. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_preference_dispatcher.py +0 -0
  348. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_pretrain.py +0 -0
  349. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_profile.py +0 -0
  350. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_profiling.py +0 -0
  351. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_progress.py +0 -0
  352. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_push.py +0 -0
  353. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_qat.py +0 -0
  354. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_quality_filter.py +0 -0
  355. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_quant_check.py +0 -0
  356. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_quant_menu.py +0 -0
  357. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_quickstart.py +0 -0
  358. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_rank_pattern.py +0 -0
  359. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_recipes.py +0 -0
  360. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_recipes_v031.py +0 -0
  361. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_registry.py +0 -0
  362. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_relora.py +0 -0
  363. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_replay.py +0 -0
  364. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_resume.py +0 -0
  365. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_rlvr.py +0 -0
  366. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_run_cost.py +0 -0
  367. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_runs.py +0 -0
  368. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_serve.py +0 -0
  369. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_server_generate.py +0 -0
  370. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_sglang_serve.py +0 -0
  371. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_simpo.py +0 -0
  372. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_smoke_train.py +0 -0
  373. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_speculative_decoding.py +0 -0
  374. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_sweep.py +0 -0
  375. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_synth_data_pro.py +0 -0
  376. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_templates_yaml.py +0 -0
  377. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_tensorboard.py +0 -0
  378. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_tool_calling.py +0 -0
  379. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_trace_to_pref.py +0 -0
  380. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_tracker.py +0 -0
  381. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_trainer_coverage_v035.py +0 -0
  382. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_trainer_init.py +0 -0
  383. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_training_intelligence.py +0 -0
  384. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_training_speed.py +0 -0
  385. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_trust_remote_code.py +0 -0
  386. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_tui.py +0 -0
  387. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ui.py +0 -0
  388. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ui_chat.py +0 -0
  389. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ui_config_builder.py +0 -0
  390. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ui_live_monitor.py +0 -0
  391. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_ui_metrics.py +0 -0
  392. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_unsloth.py +0 -0
  393. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_validator.py +0 -0
  394. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_vision.py +0 -0
  395. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_vllm_serve.py +0 -0
  396. {soup_cli-0.40.0 → soup_cli-0.40.2}/tests/test_why.py +0 -0
@@ -111,7 +111,7 @@ soup_cli/
111
111
  templates/ - 17 built-in soup.yaml templates (YAML + manifest.json) with load_template loader (v0.39.0, +bco v0.40.0)
112
112
  ui/ - Web UI (FastAPI + HTML/JS SPA)
113
113
 
114
- tests/ - Test suite (136 files, 4656 tests)
114
+ tests/ - Test suite (143 files, 4756 tests)
115
115
  examples/ - Real-world config examples and datasets
116
116
  ```
117
117
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soup-cli
3
- Version: 0.40.0
3
+ Version: 0.40.2
4
4
  Summary: Fine-tune LLMs in one command. No SSH, no config hell.
5
5
  Project-URL: Homepage, https://github.com/MakazhanAlpamys/Soup
6
6
  Project-URL: Repository, https://github.com/MakazhanAlpamys/Soup
@@ -27,7 +27,7 @@ Requires-Dist: pydantic>=2.0.0
27
27
  Requires-Dist: pyyaml>=6.0
28
28
  Requires-Dist: rich>=13.0.0
29
29
  Requires-Dist: torch>=2.0.0
30
- Requires-Dist: transformers>=4.36.0
30
+ Requires-Dist: transformers<5.0.0,>=4.36.0
31
31
  Requires-Dist: trl>=0.7.0
32
32
  Requires-Dist: typer<0.21.0,>=0.9.0
33
33
  Provides-Extra: audio
@@ -100,6 +100,7 @@ Description-Content-Type: text/markdown
100
100
  </p>
101
101
 
102
102
  <p align="center">
103
+ <a href="https://trysoup.dev">Website</a> &middot;
103
104
  <a href="#quick-start">Quick Start</a> &middot;
104
105
  <a href="#features">Features</a> &middot;
105
106
  <a href="#data-tools">Data Tools</a> &middot;
@@ -115,6 +116,7 @@ Description-Content-Type: text/markdown
115
116
  <img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="Apache-2.0 License">
116
117
  <a href="https://github.com/MakazhanAlpamys/Soup/actions"><img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/MakazhanAlpamys/65fdc943f85f3b2c46ecddb415c2b779/raw/soup_tests.json" alt="Tests"></a>
117
118
  <a href="https://github.com/MakazhanAlpamys/Soup/actions"><img src="https://github.com/MakazhanAlpamys/Soup/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
119
+ <a href="https://trysoup.dev"><img src="https://img.shields.io/badge/website-trysoup.dev-blue" alt="Website"></a>
118
120
  </p>
119
121
 
120
122
  ---
@@ -129,15 +131,20 @@ soup train
129
131
 
130
132
  ## What's New
131
133
 
132
- Latest highlights only. Full history: [GitHub Releases](https://github.com/MakazhanAlpamys/Soup/releases).
133
134
 
134
- **v0.40.0 Preference Variety**: BCO trainer + a unified preference-loss surface so DPO / SimPO / ORPO / IPO / BCO live behind one config knob. Adds two opt-in DPO controls (β-schedule + ref-model regen) and a forward-looking multi-objective preference-loss surface.
135
+ Latest highlights only. Full history: [GitHub Releases](https://github.com/MakazhanAlpamys/Soup/releases).
135
136
 
136
- - **BCO Trainer** set `task: bco` for Binary Classifier Optimization. Same input format as DPO (`prompt + chosen + rejected`); rows are internally split to TRL's BCO unpaired schema. New `training.bco_beta` (default 0.1, gt=0). Template: `soup init --template bco`.
137
- - **Unified preference dispatcher** — set `task: preference` + `training.preference_loss: dpo|simpo|orpo|ipo|bco` to pick the loss without renaming your task. Legacy `task: dpo`, `task: simpo`, etc remain first-class — the new surface is additive, not a breaking collapse. Useful for hyperparameter sweeps over the loss type itself.
138
- - **KL-controlled DPO variants** — anneal β over training with `training.dpo_beta_schedule: linear|cosine|exponential` + `training.dpo_beta_end`. Periodically refresh the frozen reference model with the current student via `training.dpo_ref_regen_epochs: 2`. Both gated to DPO-family tasks (`dpo`, `ipo`, or `preference` with `preference_loss in {dpo, ipo}`); transformers backend only.
139
- - **Multi-objective preference loss** — define `training.preference_loss_weights: {dpo: 0.7, bco: 0.3}` to blend losses. 2–5 entries, weights must sum to 1. Schema-level surface ships now; live runtime weighted-loss combination deferred to v0.40.1 (`PreferenceTrainerWrapper.setup` raises `NotImplementedError` with a friendly message until then — same stub-then-live pattern as v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu / v0.39.0 ReLoRA).
140
- - **Net +118 tests** (4538 4656) across BCO trainer + dispatcher + β schedule math + ref-model regen TOCTOU + multi-objective schema bounds.
137
+ **v0.40.2Quick polish + carry-overs**: closes 3 originally-scheduled GitHub issues (#36 eval-gate dashboard row, #50 smarter `--hf-resume`, #51 custom HF Space templates) plus 7 v0.40.1 long-tail UX papercuts.
138
+
139
+ - **`--hf-resume` prefers local newer** — `prepare_hf_resume` checks the highest local `checkpoint-N` against the highest remote branch and skips the snapshot download when the local copy is newer or equal. Saves bandwidth and never overwrites a fresher local checkpoint with stale Hub state.
140
+ - **Eval-gate dashboard row** — pure formatter `format_gate_row(state)` lives in `soup_cli/monitoring/display.py` and renders a one-liner like `Gate: helpfulness 7.8 | math 0.82 (-0.06) | STOP` for the live training panel. Hidden when eval-gate is disabled.
141
+ - **`soup deploy hf-space --template-dir <path>`** supply your own `app.py` + `README.md` (+ optional `requirements.txt`) instead of the built-in `gradio-chat` / `streamlit-chat`. Containment-checked, repo-id substitution validated *before* render, 256 KB cap per file, symlinks rejected.
142
+ - **`soup quickstart --output DIR`** — route data, config, and run dir under any directory you choose (default keeps backwards-compat in cwd).
143
+ - **`soup runs --cwd-only`** — restrict the listing to runs whose `output_dir` is under the current directory; the global `~/.soup/experiments.db` view is still default.
144
+ - **`soup infer` / `soup bench` accept HF ids** — when the local model path is missing AND the value isn't path-like (no `./`, `/`, `~`, `C:\`), it falls through to a HuggingFace download via `transformers.from_pretrained`. Path-like-but-missing surfaces a friendly `FileNotFoundError`.
145
+ - **CLI flag aliases** — `data filter --min-coherence` (alias for `--coherence`); `data split --train` accepted (informational, train is the implicit remainder); `data register / unregister` accept positional `<name>` and `<path>` alongside the `--name` / `--path` options, with conflict detection.
146
+ - **`--log-level` plumbing complete** — `apply_logging_level` now sets the root logger so third-party libraries (transformers / peft / trl) actually respect QUIET and DEBUG. The four tiers no longer produce byte-identical output.
147
+ - **+36 net new tests** across the new helpers, security-fix follow-ups (path-containment in `register_data`, `bench.py`, `infer.py --output`, symlink-reject in custom Space templates), and Windows cross-drive edge cases.
141
148
 
142
149
  ## Why Soup?
143
150
 
@@ -9,6 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
+ <a href="https://trysoup.dev">Website</a> &middot;
12
13
  <a href="#quick-start">Quick Start</a> &middot;
13
14
  <a href="#features">Features</a> &middot;
14
15
  <a href="#data-tools">Data Tools</a> &middot;
@@ -24,6 +25,7 @@
24
25
  <img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="Apache-2.0 License">
25
26
  <a href="https://github.com/MakazhanAlpamys/Soup/actions"><img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/MakazhanAlpamys/65fdc943f85f3b2c46ecddb415c2b779/raw/soup_tests.json" alt="Tests"></a>
26
27
  <a href="https://github.com/MakazhanAlpamys/Soup/actions"><img src="https://github.com/MakazhanAlpamys/Soup/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
28
+ <a href="https://trysoup.dev"><img src="https://img.shields.io/badge/website-trysoup.dev-blue" alt="Website"></a>
27
29
  </p>
28
30
 
29
31
  ---
@@ -38,15 +40,20 @@ soup train
38
40
 
39
41
  ## What's New
40
42
 
41
- Latest highlights only. Full history: [GitHub Releases](https://github.com/MakazhanAlpamys/Soup/releases).
42
43
 
43
- **v0.40.0 Preference Variety**: BCO trainer + a unified preference-loss surface so DPO / SimPO / ORPO / IPO / BCO live behind one config knob. Adds two opt-in DPO controls (β-schedule + ref-model regen) and a forward-looking multi-objective preference-loss surface.
44
+ Latest highlights only. Full history: [GitHub Releases](https://github.com/MakazhanAlpamys/Soup/releases).
44
45
 
45
- - **BCO Trainer** set `task: bco` for Binary Classifier Optimization. Same input format as DPO (`prompt + chosen + rejected`); rows are internally split to TRL's BCO unpaired schema. New `training.bco_beta` (default 0.1, gt=0). Template: `soup init --template bco`.
46
- - **Unified preference dispatcher** — set `task: preference` + `training.preference_loss: dpo|simpo|orpo|ipo|bco` to pick the loss without renaming your task. Legacy `task: dpo`, `task: simpo`, etc remain first-class — the new surface is additive, not a breaking collapse. Useful for hyperparameter sweeps over the loss type itself.
47
- - **KL-controlled DPO variants** — anneal β over training with `training.dpo_beta_schedule: linear|cosine|exponential` + `training.dpo_beta_end`. Periodically refresh the frozen reference model with the current student via `training.dpo_ref_regen_epochs: 2`. Both gated to DPO-family tasks (`dpo`, `ipo`, or `preference` with `preference_loss in {dpo, ipo}`); transformers backend only.
48
- - **Multi-objective preference loss** — define `training.preference_loss_weights: {dpo: 0.7, bco: 0.3}` to blend losses. 2–5 entries, weights must sum to 1. Schema-level surface ships now; live runtime weighted-loss combination deferred to v0.40.1 (`PreferenceTrainerWrapper.setup` raises `NotImplementedError` with a friendly message until then — same stub-then-live pattern as v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu / v0.39.0 ReLoRA).
49
- - **Net +118 tests** (4538 4656) across BCO trainer + dispatcher + β schedule math + ref-model regen TOCTOU + multi-objective schema bounds.
46
+ **v0.40.2Quick polish + carry-overs**: closes 3 originally-scheduled GitHub issues (#36 eval-gate dashboard row, #50 smarter `--hf-resume`, #51 custom HF Space templates) plus 7 v0.40.1 long-tail UX papercuts.
47
+
48
+ - **`--hf-resume` prefers local newer** — `prepare_hf_resume` checks the highest local `checkpoint-N` against the highest remote branch and skips the snapshot download when the local copy is newer or equal. Saves bandwidth and never overwrites a fresher local checkpoint with stale Hub state.
49
+ - **Eval-gate dashboard row** — pure formatter `format_gate_row(state)` lives in `soup_cli/monitoring/display.py` and renders a one-liner like `Gate: helpfulness 7.8 | math 0.82 (-0.06) | STOP` for the live training panel. Hidden when eval-gate is disabled.
50
+ - **`soup deploy hf-space --template-dir <path>`** supply your own `app.py` + `README.md` (+ optional `requirements.txt`) instead of the built-in `gradio-chat` / `streamlit-chat`. Containment-checked, repo-id substitution validated *before* render, 256 KB cap per file, symlinks rejected.
51
+ - **`soup quickstart --output DIR`** — route data, config, and run dir under any directory you choose (default keeps backwards-compat in cwd).
52
+ - **`soup runs --cwd-only`** — restrict the listing to runs whose `output_dir` is under the current directory; the global `~/.soup/experiments.db` view is still default.
53
+ - **`soup infer` / `soup bench` accept HF ids** — when the local model path is missing AND the value isn't path-like (no `./`, `/`, `~`, `C:\`), it falls through to a HuggingFace download via `transformers.from_pretrained`. Path-like-but-missing surfaces a friendly `FileNotFoundError`.
54
+ - **CLI flag aliases** — `data filter --min-coherence` (alias for `--coherence`); `data split --train` accepted (informational, train is the implicit remainder); `data register / unregister` accept positional `<name>` and `<path>` alongside the `--name` / `--path` options, with conflict detection.
55
+ - **`--log-level` plumbing complete** — `apply_logging_level` now sets the root logger so third-party libraries (transformers / peft / trl) actually respect QUIET and DEBUG. The four tiers no longer produce byte-identical output.
56
+ - **+36 net new tests** across the new helpers, security-fix follow-ups (path-containment in `register_data`, `bench.py`, `infer.py --output`, symlink-reject in custom Space templates), and Windows cross-drive edge cases.
50
57
 
51
58
  ## Why Soup?
52
59
 
@@ -9,7 +9,8 @@ We provide security updates for the following versions:
9
9
  - **Versions older than 3 minor versions:** No support
10
10
 
11
11
  Example:
12
- - v0.40.0-0.40.x -- Full support (latest)
12
+ - v0.40.2 -- Full support (latest)
13
+ - v0.40.0-v0.40.x -- Full support
13
14
  - v0.39.0-0.39.x -- Bug-fix support only
14
15
  - v0.38.0-0.38.x -- Bug-fix support only
15
16
  - v0.37.x and below -- No support
@@ -143,6 +144,8 @@ No known critical vulnerabilities in current releases.
143
144
  - **v0.32.0 — Training Stability & Auto-Tuning**: `--find-lr-output` containment via shared `utils/paths.is_under_cwd` (prevents writes outside cwd); `save_lr_finder_report` rejects NaN / Infinity floats in `lrs` / `losses` and serialises with `allow_nan=False` (keeps the report parser-safe); `compute_lr_schedule` rejects non-positive `start_lr`, inverted ranges, and `num_steps` outside `[2, 10_000]`; `pick_mixed_precision` rejects empty / null-byte / >200-char model names and resolves multi-version quirks (`qwen2.5` vs `qwen2`, `phi-3.5` vs `phi-3`) by longest-substring-first iteration so an added family can never accidentally make a more-specific entry dead code; `compute_warmup_steps` clamps to `[10, 1000]` with a `ratio==0.0` short-circuit matching HF Trainer's "no warmup" convention; `SpikeRecoveryStrategy` is `@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_watchdog` rejects `loss_spike_recovery=true, loss_watchdog=false` at config-load (fails fast instead of never triggering); `convergence_window ∈ [5, 10_000]`, `convergence_rel_tol ∈ (0, 1]`, `recommend_action` reuses `detect_plateau` so plateau heuristic stays single-source-of-truth; `GradAccumMonitor.recommend()` caps doubled `accum` at `MAX_ACCUM=1024` so a runaway advisory loop cannot blow up DataLoader prefetch; `generate_config` validates BOTH the YAML output path AND the embedded `decisions["output"]` field via `is_under_cwd` (closes the gap where a crafted `decisions["output"]="../../etc"` would have silently propagated into the rendered YAML)
144
145
  - **v0.34.0 — Observability & Dev UX**: `.crash` bundle generator (`utils/crash.py`) recursively redacts `hf_*` / `sk-*` / `Bearer …` token-shaped strings in any captured `config` and metric tail before serialisation, so a `.crash` file shared on a public GitHub issue cannot leak credentials; `output_dir` is reduced to `os.path.basename` so `$HOME` doesn't leak; `write_crash_bundle` uses `os.path.realpath + commonpath` for cwd containment (Windows-safe; raises `ValueError` not `PermissionError` so callers cannot silently swallow with `except OSError`); filename appends `secrets.token_hex(4)` so two crashes in the same UTC second don't collide; bundle truncated to `MAX_BUNDLE_BYTES=1_000_000`. `train.py` crash-write surfaces failures to the user (no silent missing-bundle). `profiling.py` `resolve_trace_path` rejects empty / `.` / `..` / `/` / `\\` / null-byte `run_id` (closes the `output_dir/profiles/../trace.json` escape) and uses `os.path.realpath + is_under_cwd`; profiles dir is created only on successful torch import (no stale empty dirs on torch-less CI). `tracker.get_run` LIKE-prefix match escapes `%` / `_` / `\\` and uses `ESCAPE '\\'` so a crafted `run_id` cannot 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/clean` switched user `run_id` rendering to `markup_escape` and switched `clean` containment from broken `Path.resolve() + relative_to()` to project-standard `os.path.realpath + is_under_cwd`. `tui_app.py` lazy-imports `ExperimentTracker` and `markup_escape`s 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_usd` rejects `bool` in `num_gpus` (bool is a subclass of int — same defence as v0.30.0 `Candidate.__post_init__`); duration clamped to `[0, 1 year]`; unknown GPU returns `None` so callers render `—` instead of fabricating `$0.00`. `log_level.parse_log_level` rejects non-string + null-byte input.
145
146
  - **v0.33.0 — Live Wire**: RLVR `code_exec_reward` adds OS-level isolation (Linux best-effort `os.unshare(CLONE_NEWUSER|CLONE_NEWNET|CLONE_NEWPID)`, macOS `sandbox-exec` with default-deny `MACOS_SANDBOX_PROFILE` narrowed to a 3-name `mach-lookup` allowlist to prevent DNS / NSURLSession bypass of `(deny network*)`); `prune_checkpoints` switches to TOCTOU-safe `os.lstat + S_ISLNK` + `shutil.rmtree(onerror=_abort_on_symlink)` so a symlink encountered mid-walk aborts rather than escapes; `run_gate` wraps each task scorer in a typed `try/except` so backend failures produce `score=None, error=str(exc)` (never silent `score=1.0`); `_parse_judge_url` removes the bare `http://` catch-all (defence-in-depth after the Pydantic GateTask validator); `soup can run` requires `--yes` or explicit consent callback and raises `ValueError` (not `PermissionError`, which is an `OSError` subclass that broad `except` blocks would swallow); GGUF `rglob` result for ollama deploy is `realpath+commonpath` checked against extract_dir (prevents symlink escape from a crafted can); `DeployTarget.path` validator normalises mixed `\\`/`/` separators before splitting (closes a Windows `..` bypass); `CAN_FORMAT_VERSION` 1→2 (additive — v1 still loads); `soup can publish` validates `repo_id` via `utils/hf.validate_repo_id`, resolves token via `resolve_token`, sanitises commit messages (first-line, 200-char cap), uses HTTPS-only HfApi; `_write_spike_recovery_hint` adds `is_under_cwd` containment check on `args.output_dir` from raw HF `TrainingArguments`; `lookup_entry_by_output_dir` emits `ResourceWarning` when 1000-row scan limit is hit (no silent miss); `CrossDocCollator` no longer mutates input feature dicts (HF Dataset rows are cached and reused — mutation broke subsequent batches); `Candidate` rejects `bool` in `score`/`latency_ms` (was sneaking past `int` isinstance check); `evaluate_candidate` latency mean now divides by *completed* prompts (excludes crashed) so a broken candidate isn't artificially fast; `auto_quant.run_auto_quant_picker` soft-falls-back to highest-scored candidate when no candidate clears `min_score` (server still binds); `build_logits_processors` returns `[]` when neither `outlines` nor `lm-format-enforcer` is 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.execvp` auto-reexec uses list args (no shell), all forwarded flags pre-validated; `cleanup_extract_dir` uses `os.path.commonpath` (Windows-safe) instead of `startswith`; `_run_subprocess` catches `TimeoutExpired` and returns rc=124 (coreutils convention) instead of an unhandled traceback; new `eval_results` and `tensorrt` artifact kinds in `RegistryStore._VALID_KINDS`
147
+ - **v0.40.2 — Quick polish + carry-overs**: New `soup_cli/utils/hf_space.py:render_custom_template_dir` enforces `is_under_cwd` containment on the template directory; `validate_repo_id` runs 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); only `app.py` / `README.md` / `requirements.txt` are read (closed allowlist — no path-from-user-data). Symlinks rejected via `os.lstat + stat.S_ISLNK` and 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_checkpoint` reads `output_dir` after caller's `is_under_cwd` validation (in `prepare_hf_resume`) and silently drops non-directories + OSError. `prepare_hf_resume` skips the snapshot download when local `checkpoint-N >= remote checkpoint-N` (saves bandwidth and never overwrites a fresher local checkpoint). `commands/data.py:register_data` containment switched from `Path.resolve() + relative_to()` to shared `is_under_cwd` (Windows 8.3 short-name safety per CLAUDE.md project rule); same fix applied to `commands/bench.py` prompts-file containment. `commands/infer.py:--output` now containment-checked via `is_under_cwd` (late-evaluated after model+input validation so pre-existing `tmp_path` test contracts keep working). `commands/quickstart.py:--output` validates target dir via `is_under_cwd` before `mkdir(parents=True)`; rejects out-of-cwd targets with friendly message. `commands/runs.py:_filter_runs_by_cwd` uses `os.path.realpath + commonpath`, catches `(ValueError, OSError)` so cross-drive paths on Windows (`D:\runs` vs `C:\project`) drop silently rather than crash. `monitoring/display.py:format_gate_row` uses explicit `task.get("passed") is True` so a missing `"passed"` field renders neutrally instead of as a false-y red ✗. `commands/infer.py:_resolve_model_source` heuristic 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 raises `FileNotFoundError` so users see actionable errors instead of confusing HF download attempts. Known limitation: custom HF Space templates always create the Space with `space_sdk="gradio"` regardless of the supplied `app.py` (no `--sdk` flag in this release; combine `--template streamlit-chat` with the inline registry for Streamlit Spaces). Tracked for v0.40.3+.
148
+ - **v0.40.1 — QA Hardening**: `soup_cli/utils/encoding.force_utf8_stdio` reconfigures 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-level `lora:` into `training.lora` so nested validators (including `lora.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_objective` replaces the v0.40.0 `NotImplementedError` stub with a primary-loss approximation; `validate_weight_compat` rejects BCO mixed with paired losses at runtime (data-format incompatible). `combine_losses` rejects empty weights, propagates NaN loudly (no silent zeroing), and rejects `bool` weight values (matches v0.30.0 `Candidate` policy). `_probe_cache_param_count` rejects empty / null-byte model names before path construction (mirrors v0.26.0 registry / v0.39.0 ReLoRAPolicy policy). `commands/doctor` flags `transformers ≥ 5.0.0` as INCOMPATIBLE via `_MAX_EXCLUSIVE` table; `_version_ge` parses leading-int chunks so `5.0.0.dev0` correctly trips the cap. `_detect_gpu_hw_without_torch_cuda` calls `nvidia-smi` via argv list (no shell), 5s timeout, `OSError` / `TimeoutExpired` caught; GPU label from `nvidia-smi` stdout is `rich.markup.escape`d before embedding in Rich-markup string (a real GPU name like `NVIDIA Quadro [T4]` cannot break or inject markup). `_detect_dual_python_interpreters` uses `os.path.realpath` (not `Path.resolve()`) for Windows 8.3 short-name compat. `_pick_quickstart_model` swaps TinyLlama-1.1B → SmolLM2-135M when `total_memory ≤ 6 GB` (prevents step-0 OOM on RTX 3050 4 GB / similar). `_live_lr_sweep_from_config` switched broken `load_local` import to `load_raw_data` (previously always silently fell back to a static placeholder curve). `commands/migrate` rejects `.jsonl` input (with first-line `{` sniff) with exit-2 friendly error; `.jsonl`-only suffix gate prevents false-positives on `.ipynb` notebooks. `commands/eval custom -o` is now honored independently of `--attach-to-registry`; loop-shadow regression where `output = generate_fn(...)` overwrote the CLI option fixed (variable renamed to `response`). `_load_jsonl` switched from `utf-8` to `utf-8-sig` so PowerShell `Out-File -Encoding utf8`-produced JSONL no longer fails first-row parse. Known limitation: `--trust-remote-code` opt-in surface still excludes 10 non-SFT trainers + 5 commands (v0.36.0 #63 carry-over).
146
149
  - **v0.40.0 — Preference Variety**: New `task='bco'` (Binary Classifier Optimization) and `task='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_dispatcher` rejects setting either `preference_loss` or `preference_loss_weights` outside `task='preference'` (closes ordering-dependency between Part B/D validators); `_validate_dpo_variants_supported_tasks` gates β-schedule + ref-regen to DPO-family tasks (`dpo`, `ipo`, or `preference` + `preference_loss in {dpo, ipo}`); rejected on mlx backend with distinct error message (matches v0.34.0 distinct-reason policy); `_validate_preference_loss_weights` enforces 2–5 entries (single-entry rejected with actionable message pointing at scalar `preference_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 scalar `preference_loss`, rejected on mlx backend. `compute_beta_at_step` rejects `bool` on `step` and `total_steps` (project bool-as-int policy from v0.30.0). `BetaScheduleCallback` resolves `total_steps` lazily in `on_train_begin` so the schedule sees the real `state.max_steps` populated by HF Trainer (closes a first-cut silent-no-op bug where total_steps=0 emitted beta_end for every step). `RefModelRegenCallback._regenerate` uses `strict=True` on `load_state_dict` and 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 `.beta` assignment swallow narrowed to `AttributeError` only. `PreferenceTrainerWrapper._make_inner_cfg` uses `model_copy` (not `model_dump`+`model_validate`) so re-validation never sees an inconsistent intermediate state and the caller's `cfg` is never mutated (mirrors v0.33.0 #47 immutability policy). `_split_dpo_rows_to_bco` skipped-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.setup` raises `NotImplementedError` with 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_transformers` still hardcodes `trust_remote_code=True` (v0.36.0 #63 known-gap family carry-over across non-SFT trainers).
147
150
  - **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")` aligns `use_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], rejects `bool` (subclass of `int` — matches v0.30.0 `Candidate` policy), null bytes in keys, empty keys; cross-validator rejects with `use_vera=True`. `ReLoRAPolicy` is `@dataclass(frozen=True)` (post-construction mutation raises `FrozenInstanceError`); bounds: `steps ∈ [1, 1e7]`, `warmup_ratio ∈ [0, 1]`, `prune_ratio ∈ (0, 1)` (strict — prevents zero-everything footgun). `magnitude_prune_tensor` strict `0 < prune_ratio < 1` rejection, non-Tensor input raises `TypeError`, empty / single-element tensor short-circuits (avoids `kthvalue(_, 0)` runtime crash). `_validate_relora_supported_tasks` cross-validator rejects `relora_steps` with `task != "sft"` and `backend=mlx` with distinct error messages (matches v0.34.0 distinct-reason policy); multi-trainer expansion deferred to v0.39.1. `is_gemma4_model` uses a word-boundary regex (`(?:^|[^a-z0-9])gemma-?4(?:[^a-z0-9]|$)`) so `"ungemma4ed"` / `"my-gemma4ish"` no longer over-match; null-byte rejection on `model_name`. `apply_gemma4_clippable_patch` weight-copy fallback logs at DEBUG instead of silent random-init; the patch is gated by `is_gemma4_model(cfg.base)` in `sft.py` before invocation so non-Gemma4 trainings never traverse the module tree. `apply_surgical_patches` rejects empty / null-byte `model_name` with `ValueError`. `templates/load_template` containment: filename re-validated via `_validate_name` (rejects `..`/`/`/`\\`/null/empty); `os.path.realpath + os.path.commonpath` containment check on the resolved path against `_templates_dir()` so a tampered `manifest.json` cannot read files outside the package directory (mirrors v0.26.0 registry policy). Tampered-manifest `ValueError` from `_validate_name` caught and falls back to inline (no propagating exception). 256 KB file-size cap. Inline `TEMPLATES` carries an explicit deprecation comment pointing at the canonical YAML registry; `tests/test_templates_yaml.py` asserts byte-equality of all 16 inline ↔ YAML pairs to prevent silent drift. Planned removal: v0.41.0+.
148
151
  - **v0.38.0 — Quant Menu**: `TrainingConfig.quantization` Literal extended with `gptq` / `awq` / `hqq:1bit`..`hqq:8bit` (no `hqq:7bit` — HQQ doesn't support it) / `aqlm` / `eetq` / `mxfp4` / `fp8`; Pydantic rejects every other string at config-load. `validate_gptq_checkpoint` and `validate_awq_checkpoint` probe local paths for `quantize_config.json` / `quant_config.json`; HF repo IDs fall through; null-byte rejection + non-string `TypeError` on the ref. `_validate_prequantized_no_qat` rejects every pre-quantized format combined with `quantization_aware` (int8 QAT or `'fp8'`) — pre-quantized weights carry their own scale and QAT/FP8 prepare would silently corrupt them (mirrors LlamaFactory `quantization.py:117/199/211`). `_validate_bnb_quant_storage_only_with_4bit` rejects `bnb_4bit_quant_storage` on every non-BNB-4bit format (silent no-op otherwise); allowed dtypes: `Literal["uint8", "float16", "bfloat16", "float32"]`. `_validate_quant_menu_supported_tasks` restricts the new formats to `task='sft'` on `backend='transformers'` in v0.38.0 with distinct MLX-backend vs unsupported-task error messages (matches v0.34.0 distinct-reason policy). `check_quant_distributed_compat` hard-fails HQQ/EETQ/AQLM × {FSDP, ZeRO-3} (sourced from LlamaFactory `quantization.py:199/211` plus AQLM dequant constraints); warning-tier (not error) for BNB-4bit + FSDP without `bnb_4bit_quant_storage` so users see the silent perf cliff; unknown `quantization` raises `ValueError` (no silent pass) and the check is wired into `commands/train.py` startup. `parse_hqq_bits` rejects unsupported bit-rates and malformed `hqq:` strings before any kernel build.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "soup-cli"
7
- version = "0.40.0"
7
+ version = "0.40.2"
8
8
  description = "Fine-tune LLMs in one command. No SSH, no config hell."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -27,7 +27,7 @@ dependencies = [
27
27
  "pydantic>=2.0.0",
28
28
  "pyyaml>=6.0",
29
29
  "torch>=2.0.0",
30
- "transformers>=4.36.0",
30
+ "transformers>=4.36.0,<5.0.0",
31
31
  "peft>=0.7.0",
32
32
  "trl>=0.7.0",
33
33
  "datasets>=2.14.0",
@@ -1,3 +1,3 @@
1
1
  """Soup CLI — Fine-tune LLMs in one command."""
2
2
 
3
- __version__ = "0.40.0"
3
+ __version__ = "0.40.2"
@@ -91,12 +91,65 @@ _MODEL_SIZE_RE = re.compile(r"(\d+(?:\.\d+)?)\s*[Bb]")
91
91
 
92
92
 
93
93
  def _guess_params_from_name(name: str) -> float:
94
- """Extract the parameter count (in billions) from a model name."""
94
+ """Extract the parameter count (in billions) from a model name.
95
+
96
+ v0.40.1 Part C / C3 — fall back to **1B** (not 7B) when the name has no
97
+ embedded size hint. The previous 7.0 default made tiny models like
98
+ ``tiny-gpt2`` (5 MB) fail VRAM-budget checks on machines that could
99
+ trivially train them. We also recognise the ``-Mm`` (millions) format
100
+ used by SmolLM2 / Phi-3 family. Probes for a local safetensors index
101
+ (cached HF snapshot) before falling back.
102
+ """
95
103
  match = _MODEL_SIZE_RE.search(name)
96
104
  if match:
97
105
  return float(match.group(1))
98
- # Conservative default
99
- return 7.0
106
+ # Recognise <N>m / <N>M for sub-billion models (e.g. SmolLM2-135M,
107
+ # tiny-gpt2). Convert to billions.
108
+ m_match = re.search(r"(\d+(?:\.\d+)?)\s*[Mm](?![a-zA-Z])", name)
109
+ if m_match:
110
+ return float(m_match.group(1)) / 1000.0
111
+ # Probe local HF cache for safetensors index size if we can.
112
+ cache_size = _probe_cache_param_count(name)
113
+ if cache_size is not None:
114
+ return cache_size
115
+ # Conservative default — small assumption (yellow advisory should fire).
116
+ return 1.0
117
+
118
+
119
+ def _probe_cache_param_count(name: str) -> Optional[float]:
120
+ """Best-effort: read parameter count from cached safetensors index.
121
+
122
+ Looks at ``~/.cache/huggingface/hub/models--<owner>--<repo>/snapshots/*/
123
+ model.safetensors.index.json`` and returns ``total_size / 4 / 1e9`` (fp32
124
+ bytes per param). Returns ``None`` if not found.
125
+
126
+ v0.40.1 review fix — reject empty / null-byte names (project policy
127
+ mirroring v0.26.0 registry / v0.39.0 ReLoRAPolicy) before constructing
128
+ the cache path.
129
+ """
130
+ if not isinstance(name, str) or not name or "\x00" in name:
131
+ return None
132
+ try:
133
+ from pathlib import Path as _Path
134
+
135
+ owner_repo = name.replace("/", "--")
136
+ cache = _Path.home() / ".cache" / "huggingface" / "hub" / f"models--{owner_repo}"
137
+ if not cache.is_dir():
138
+ return None
139
+ for idx in cache.rglob("model.safetensors.index.json"):
140
+ try:
141
+ import json as _json
142
+ data = _json.loads(idx.read_text(encoding="utf-8"))
143
+ total_bytes = data.get("metadata", {}).get("total_size")
144
+ if isinstance(total_bytes, (int, float)) and total_bytes > 0:
145
+ # Assume fp32 storage (4 bytes/param) — generous upper
146
+ # bound; bf16/fp16 cuts it in half.
147
+ return float(total_bytes) / 4.0 / 1e9
148
+ except (OSError, ValueError):
149
+ continue
150
+ except Exception: # noqa: BLE001
151
+ return None
152
+ return None
100
153
 
101
154
 
102
155
  def analyze_model(name: str, params_b: Optional[float] = None) -> ModelProfile:
@@ -2,11 +2,20 @@
2
2
 
3
3
  import sys
4
4
 
5
- import typer
6
- from rich.console import Console
5
+ # UTF-8 stdio bootstrap (v0.40.1 Part A) — must run before any Rich console
6
+ # is constructed. On Windows, reconfigures sys.stdout/stderr to UTF-8 so β /
7
+ # ✓ / box-drawing chars don't crash with UnicodeEncodeError on cp1251/cp1252.
8
+ # POSIX: no-op.
9
+ from soup_cli.utils.encoding import force_utf8_stdio
7
10
 
8
- from soup_cli import __version__
9
- from soup_cli.commands import (
11
+ force_utf8_stdio()
12
+ _utf8_bootstrap_done = True
13
+
14
+ import typer # noqa: E402
15
+ from rich.console import Console # noqa: E402
16
+
17
+ from soup_cli import __version__ # noqa: E402
18
+ from soup_cli.commands import ( # noqa: E402
10
19
  adapters,
11
20
  autopilot,
12
21
  bench,
@@ -34,15 +43,15 @@ from soup_cli.commands import (
34
43
  train,
35
44
  ui,
36
45
  )
37
- from soup_cli.commands import doctor as doctor_cmd
38
- from soup_cli.commands import quickstart as quickstart_cmd
39
- from soup_cli.commands import (
46
+ from soup_cli.commands import doctor as doctor_cmd # noqa: E402
47
+ from soup_cli.commands import quickstart as quickstart_cmd # noqa: E402
48
+ from soup_cli.commands import ( # noqa: E402
40
49
  tui as tui_cmd,
41
50
  )
42
- from soup_cli.commands import (
51
+ from soup_cli.commands import ( # noqa: E402
43
52
  why as why_cmd,
44
53
  )
45
- from soup_cli.utils.constants import GITHUB_URL
54
+ from soup_cli.utils.constants import GITHUB_URL # noqa: E402
46
55
 
47
56
  console = Console()
48
57
 
@@ -217,7 +226,11 @@ def main(
217
226
  """Soup — fine-tune LLMs in one command."""
218
227
  global _verbose, _log_level
219
228
  _verbose = verbose
220
- from soup_cli.utils.log_level import parse_log_level, setup_logging
229
+ from soup_cli.utils.log_level import (
230
+ apply_logging_level,
231
+ parse_log_level,
232
+ setup_logging,
233
+ )
221
234
 
222
235
  try:
223
236
  tier = parse_log_level(log_level)
@@ -226,6 +239,10 @@ def main(
226
239
  raise typer.Exit(code=2) from exc
227
240
  _log_level = tier.value
228
241
  setup_logging(tier)
242
+ # v0.40.2 N1/G2: also push the tier into the root logger so third-party
243
+ # libraries (transformers / peft / trl) respect QUIET / DEBUG. Without
244
+ # this, all four levels were producing nearly-identical output.
245
+ apply_logging_level(tier)
229
246
 
230
247
 
231
248
  def run():
@@ -43,13 +43,24 @@ def bench(
43
43
  """Run an inference benchmark (speed and memory) on a loaded model."""
44
44
  import torch
45
45
 
46
- from soup_cli.commands.infer import _generate, _load_model
46
+ from soup_cli.commands.infer import _generate, _load_model, _resolve_model_source
47
47
  from soup_cli.utils.gpu import detect_device
48
48
 
49
- model_path = Path(model)
50
- if not model_path.exists():
51
- console.print(f"[red]Model not found: {model_path}[/]")
52
- raise typer.Exit(1)
49
+ # Resolve local-path-or-HF-id (#N7).
50
+ try:
51
+ model_kind, model_ref = _resolve_model_source(model)
52
+ except FileNotFoundError as exc:
53
+ console.print(
54
+ f"[red]{exc}[/]\n"
55
+ "[dim]If you meant a HuggingFace repo, use the form "
56
+ "'owner/repo-name' (no leading './').[/]"
57
+ )
58
+ raise typer.Exit(1) from exc
59
+ model_path = Path(model_ref)
60
+ if model_kind == "hf":
61
+ console.print(
62
+ f"[dim]Local path not found; treating {model_ref!r} as a HF repo id.[/]"
63
+ )
53
64
 
54
65
  device, _ = detect_device()
55
66
 
@@ -61,15 +72,17 @@ def bench(
61
72
 
62
73
  if prompts_file:
63
74
  import json
64
- p_path = Path(prompts_file).resolve()
75
+ import os as _os
65
76
 
66
- try:
67
- p_path.relative_to(Path.cwd())
68
- except ValueError:
77
+ from soup_cli.utils.paths import is_under_cwd
78
+
79
+ if not is_under_cwd(prompts_file):
69
80
  console.print(
70
- f"[red]Security Error:[/] Path {p_path} is outside the current working directory."
81
+ "[red]Security Error:[/] Prompts file must stay under the "
82
+ "current working directory."
71
83
  )
72
84
  raise typer.Exit(1)
85
+ p_path = Path(_os.path.realpath(prompts_file))
73
86
 
74
87
  if not p_path.is_file():
75
88
  console.print(f"[red]Prompts file not found:[/] {p_path}")
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import random
7
7
  from pathlib import Path
8
+ from typing import Optional
8
9
 
9
10
  import typer
10
11
  from rich.console import Console
@@ -325,7 +326,7 @@ def filter_data(
325
326
  help="Max perplexity threshold (rows above this are removed)",
326
327
  ),
327
328
  coherence: float = typer.Option(
328
- None, "--coherence",
329
+ None, "--coherence", "--min-coherence",
329
330
  help="Min coherence threshold 0.0-1.0 (rows below this are removed)",
330
331
  ),
331
332
  perplexity_model: str = typer.Option(
@@ -746,8 +747,11 @@ def sample_data(
746
747
  sampled = _sample_random(data, sample_count, seed=seed)
747
748
 
748
749
  # Resolve output path (with path traversal protection on explicit --output)
750
+ # v0.40.1 Part E — include the strategy in the default filename so
751
+ # successive `random` / `diverse` / `hard` runs don't silently overwrite
752
+ # each other.
749
753
  if output is None:
750
- out_path = file_path.parent / f"{file_path.stem}_sampled.jsonl"
754
+ out_path = file_path.parent / f"{file_path.stem}_sampled_{strategy}.jsonl"
751
755
  else:
752
756
  out_path = Path(output).resolve()
753
757
  cwd = Path.cwd().resolve()
@@ -777,6 +781,13 @@ def split_data(
777
781
  None, "--test",
778
782
  help="Test split: percentage (default) or absolute count (with --absolute)",
779
783
  ),
784
+ train: int = typer.Option(
785
+ None, "--train",
786
+ help=(
787
+ "Train split (informational; the train remainder is implied by "
788
+ "--val + --test). Accepted for command parity."
789
+ ),
790
+ ),
780
791
  absolute: bool = typer.Option(
781
792
  False, "--absolute",
782
793
  help="Treat --val/--test as absolute sample counts instead of percentages",
@@ -1458,56 +1469,104 @@ def _load_augment_provider(provider: str, rpm: int):
1458
1469
 
1459
1470
  @app.command(name="register")
1460
1471
  def register_data(
1461
- name: str = typer.Option(..., "--name", "-n", help="Dataset name"),
1462
- path: str = typer.Option(..., "--path", "-p", help="Path to dataset file"),
1472
+ name_arg: Optional[str] = typer.Argument(
1473
+ None, help="Dataset name (positional alternative to --name)",
1474
+ ),
1475
+ path_arg: Optional[str] = typer.Argument(
1476
+ None, help="Path to dataset file (positional alternative to --path)",
1477
+ ),
1478
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Dataset name"),
1479
+ path: Optional[str] = typer.Option(
1480
+ None, "--path", "-p", help="Path to dataset file"
1481
+ ),
1463
1482
  fmt: str = typer.Option(
1464
1483
  "auto", "--format", "-f",
1465
1484
  help="Dataset format: alpaca, sharegpt, chatml, dpo, kto, auto",
1466
1485
  ),
1467
1486
  ):
1468
- """Register a local dataset by name for use in soup.yaml."""
1487
+ """Register a local dataset by name for use in soup.yaml.
1488
+
1489
+ Accepts both ``--name X --path Y`` and positional ``X Y``.
1490
+ """
1469
1491
  from soup_cli.utils.registry import register_dataset
1470
1492
 
1471
- # Path traversal protection
1472
- resolved = Path(path).resolve()
1473
- cwd = Path.cwd().resolve()
1474
- try:
1475
- resolved.relative_to(cwd)
1476
- except ValueError:
1493
+ # Resolve positional vs option, with conflict detection
1494
+ final_name = name if name is not None else name_arg
1495
+ final_path = path if path is not None else path_arg
1496
+ if final_name is None or final_path is None:
1497
+ console.print(
1498
+ "[red]Provide both name and path "
1499
+ "(positional `<name> <path>` or `--name --path`).[/]"
1500
+ )
1501
+ raise typer.Exit(2)
1502
+ if name is not None and name_arg is not None and name != name_arg:
1503
+ console.print("[red]Conflict: --name and positional name differ.[/]")
1504
+ raise typer.Exit(2)
1505
+ if path is not None and path_arg is not None and path != path_arg:
1506
+ console.print("[red]Conflict: --path and positional path differ.[/]")
1507
+ raise typer.Exit(2)
1508
+
1509
+ # Path traversal protection — use os.path.realpath + commonpath
1510
+ # (project standard; Windows 8.3 short-name safe).
1511
+ import os as _os
1512
+
1513
+ from soup_cli.utils.paths import is_under_cwd
1514
+
1515
+ if not is_under_cwd(final_path):
1477
1516
  console.print(
1478
1517
  "[red]Dataset path must be under the current working directory.[/]"
1479
1518
  )
1480
1519
  raise typer.Exit(1)
1520
+ resolved = Path(_os.path.realpath(final_path))
1481
1521
 
1482
1522
  registry_path = _get_registry_path()
1483
1523
 
1484
1524
  try:
1485
- register_dataset(name, str(resolved), fmt, registry_path=registry_path)
1525
+ register_dataset(final_name, str(resolved), fmt, registry_path=registry_path)
1486
1526
  except ValueError as exc:
1487
1527
  console.print(f"[red]{exc}[/]")
1488
1528
  raise typer.Exit(1)
1489
1529
 
1490
1530
  console.print(
1491
- f"[green]Registered dataset '[bold]{name}[/bold]'[/]\n"
1492
- f" Path: {path}\n"
1531
+ f"[green]Registered dataset '[bold]{final_name}[/bold]'[/]\n"
1532
+ f" Path: {final_path}\n"
1493
1533
  f" Format: {fmt}"
1494
1534
  )
1495
1535
 
1496
1536
 
1497
1537
  @app.command(name="unregister")
1498
1538
  def unregister_data(
1499
- name: str = typer.Option(..., "--name", "-n", help="Dataset name to remove"),
1539
+ name_arg: Optional[str] = typer.Argument(
1540
+ None, help="Dataset name (positional alternative to --name)",
1541
+ ),
1542
+ name: Optional[str] = typer.Option(
1543
+ None, "--name", "-n", help="Dataset name to remove"
1544
+ ),
1500
1545
  ):
1501
- """Remove a dataset from the local registry."""
1546
+ """Remove a dataset from the local registry.
1547
+
1548
+ Accepts both ``--name X`` and positional ``X``.
1549
+ """
1502
1550
  from soup_cli.utils.registry import unregister_dataset
1503
1551
 
1552
+ final_name = name if name is not None else name_arg
1553
+ if final_name is None:
1554
+ console.print(
1555
+ "[red]Provide a dataset name "
1556
+ "(positional `<name>` or `--name`).[/]"
1557
+ )
1558
+ raise typer.Exit(2)
1559
+ if name is not None and name_arg is not None and name != name_arg:
1560
+ console.print("[red]Conflict: --name and positional name differ.[/]")
1561
+ raise typer.Exit(2)
1562
+
1504
1563
  registry_path = _get_registry_path()
1505
- removed = unregister_dataset(name, registry_path=registry_path)
1564
+ removed = unregister_dataset(final_name, registry_path=registry_path)
1506
1565
 
1507
1566
  if removed:
1508
- console.print(f"[green]Removed dataset '{name}' from registry.[/]")
1567
+ console.print(f"[green]Removed dataset '{final_name}' from registry.[/]")
1509
1568
  else:
1510
- console.print(f"[red]Dataset '{name}' not found in registry.[/]")
1569
+ console.print(f"[red]Dataset '{final_name}' not found in registry.[/]")
1511
1570
  raise typer.Exit(1)
1512
1571
 
1513
1572
 
@@ -435,6 +435,15 @@ def hf_space(
435
435
  "-t",
436
436
  help=f"Space template: {', '.join(HF_SPACE_TEMPLATES.keys())}",
437
437
  ),
438
+ template_dir: Optional[str] = typer.Option(
439
+ None,
440
+ "--template-dir",
441
+ help=(
442
+ "Custom template directory (overrides --template). "
443
+ "Must contain app.py + README.md, optionally requirements.txt. "
444
+ "Use {MODEL_REPO} placeholder for substitution."
445
+ ),
446
+ ),
438
447
  private: bool = typer.Option(
439
448
  False, "--private", help="Create the Space as private",
440
449
  ),
@@ -461,7 +470,7 @@ def hf_space(
461
470
  except ValueError as exc:
462
471
  console.print(f"[red]Invalid --space repo id:[/] {exc}")
463
472
  raise typer.Exit(1) from exc
464
- if template not in HF_SPACE_TEMPLATES:
473
+ if template_dir is None and template not in HF_SPACE_TEMPLATES:
465
474
  console.print(
466
475
  f"[red]Unknown template: {template}[/]\n"
467
476
  f"Available: {', '.join(HF_SPACE_TEMPLATES.keys())}"
@@ -485,16 +494,28 @@ def hf_space(
485
494
 
486
495
  # --- Render template files ---
487
496
  try:
488
- files = render_space_template(template, model_repo=model)
497
+ if template_dir is not None:
498
+ from soup_cli.utils.hf_space import render_custom_template_dir
499
+ files = render_custom_template_dir(template_dir, model_repo=model)
500
+ # Custom templates default to gradio SDK unless requirements
501
+ # imply otherwise; we record gradio for create_repo space_sdk.
502
+ sdk = "gradio"
503
+ else:
504
+ files = render_space_template(template, model_repo=model)
505
+ sdk = HF_SPACE_TEMPLATES[template]["sdk"]
489
506
  except ValueError as exc:
490
507
  console.print(f"[red]Template render failed:[/] {exc}")
491
508
  raise typer.Exit(1) from exc
509
+ except FileNotFoundError as exc:
510
+ console.print(f"[red]Template directory error:[/] {exc}")
511
+ raise typer.Exit(1) from exc
492
512
 
513
+ template_label = template_dir if template_dir is not None else template
493
514
  console.print(
494
515
  Panel(
495
516
  f"Space: [bold]{space}[/]\n"
496
517
  f"Model: [bold]{model}[/]\n"
497
- f"Template: [bold]{template}[/]\n"
518
+ f"Template: [bold]{template_label}[/]\n"
498
519
  f"Private: [bold]{private}[/]",
499
520
  title="Deploy HuggingFace Space",
500
521
  )
@@ -514,7 +535,7 @@ def hf_space(
514
535
  try:
515
536
  api.create_repo(
516
537
  repo_id=space, repo_type="space",
517
- space_sdk=HF_SPACE_TEMPLATES[template]["sdk"],
538
+ space_sdk=sdk,
518
539
  private=private, exist_ok=True,
519
540
  )
520
541
  for in_repo_name, content in files.items():