Compare commits

...
Sign in to create a new pull request.

79 commits

Author SHA1 Message Date
pre-commit-ci[bot]
d6f1075812 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-13 03:13:46 +00:00
danielhanchen
657b3251f8 Upgrade setuptools and wheel in the auto-install command
The generated install command builds unsloth from git with
--no-build-isolation, so pip uses the environment's existing setuptools
rather than the pinned build-system requirement. On setuptools < 77 the
PEP 639 license string in pyproject.toml fails to validate and the
install aborts. Upgrade setuptools and wheel up front so the source
build always has PEP 639 support.
2026-06-13 03:13:03 +00:00
Daniel Han
a370521879 Merge branch 'main' into pip 2026-06-12 11:20:56 -07:00
Daniel Han
b712f9f557 Merge branch 'main' into pip 2026-06-12 07:37:43 -07:00
Daniel Han
667467a315 Merge branch 'main' into pip 2026-06-12 07:36:20 -07:00
Daniel Han
55075f691e Merge branch 'main' into pip 2026-06-12 06:55:57 -07:00
Daniel Han
0e929cddac Update pyproject.toml 2026-06-12 06:35:51 -07:00
Daniel Han
ecaf3dde2a Merge branch 'main' into pip 2026-06-12 06:35:37 -07:00
Daniel Han
fc9d82f6bf Update pyproject.toml 2026-06-11 09:22:35 -07:00
Daniel Han
4d2afc62a0 Merge branch 'main' into pip 2026-06-11 09:22:24 -07:00
Daniel Han
43c669e6ec Update pyproject.toml 2026-06-10 11:11:36 -07:00
Daniel Han
11b69c1051 Merge branch 'main' into pip 2026-06-10 11:11:29 -07:00
Daniel Han
39c7a4d290 Merge branch 'main' into pip 2026-06-03 07:30:11 -07:00
Daniel Han
ff1088af40 Update pyproject.toml 2026-06-03 06:36:36 -07:00
Daniel Han
6d3849b821 Merge branch 'main' into pip 2026-06-03 06:36:15 -07:00
Daniel Han
8e26a368e1 Merge branch 'main' into pip 2026-06-01 08:22:08 -07:00
Daniel Han
e31d4c6aea Update pyproject.toml 2026-05-31 07:11:09 -07:00
Daniel Han
5688072af6 Merge branch 'main' into pip 2026-05-31 07:10:55 -07:00
Daniel Han
d3ac7447eb Merge branch 'main' into pip 2026-05-26 07:26:37 -07:00
Daniel Han
2fdfe24fb1 Merge branch 'main' into pip 2026-05-24 07:11:32 -07:00
Daniel Han
e2215c9d11 Update pyproject.toml 2026-05-22 09:24:40 -07:00
Daniel Han
b11f13a710 Merge branch 'main' into pip 2026-05-22 09:24:32 -07:00
Daniel Han
66ae2d416c Merge branch 'main' into pip 2026-05-19 07:27:07 -07:00
Daniel Han
b0a35ddeab Merge branch 'main' into pip 2026-05-19 07:24:07 -07:00
Daniel Han
405add94a1 Update pyproject.toml 2026-05-19 07:00:38 -07:00
Daniel Han
64dc11faa2 Merge branch 'main' into pip 2026-05-19 07:00:31 -07:00
Daniel Han
07e2fccf38 Merge branch 'main' into pip 2026-05-18 08:43:14 -07:00
Daniel Han
d482382a92
Sync pyproject.toml fixes from main into pip (#5555)
- Add missing cu124onlytorch240 extras (xformers==0.0.28.post1)
- Add sentence-transformers to huggingface and colab-new extras
- Add triton extras group (split out from main deps)
- Add windows extras group
- Drop xformers<0.0.27 cap in colab-no-deps; gate on linux/win + x86_64
- Add [tool.ruff] config (target py311, lint select/ignore, extend-exclude)
- Add [tool.pytest.ini_options] testpaths = ["tests/security"]

Only PyPI-compatible (version-pin) entries copied; URL-based intel/amd/rocm/
flashattentiontorch* extras from main are intentionally skipped.
2026-05-18 06:39:41 -07:00
Daniel Han
342bbc2333 Merge branch 'main' into pip 2026-05-18 06:31:17 -07:00
Daniel Han
e1fe3be939 Update pyproject.toml 2026-05-18 06:30:03 -07:00
Daniel Han
d459f60458 Merge branch 'main' into pip 2026-05-18 06:26:54 -07:00
Daniel Han
2c4bf9ae35 Route CPU-only Linux x86_64 to ggml-org/llama.cpp prebuilts (#5302)
* Route CPU-only Linux x86_64 to ggml-org/llama.cpp prebuilts

setup.sh hard-coded _HELPER_RELEASE_REPO=unslothai/llama.cpp for every
non-Darwin host. unslothai/llama.cpp only publishes Linux CUDA bundles
(app-*-linux-x64-cuda*.tar.gz), so a CPU-only Linux host walked ~30
releases looking for a non-existent app-*-linux-x64-cpu asset, exited
the prebuilt planner with "no compatible Linux prebuilt asset was
found", and fell through to a source build. Free CI runners
(ubuntu-latest with no GPU) hit this on every install, and anyone
running Studio on a Linux laptop without an NVIDIA GPU paid the
~3 minute cmake+make cost on first install.

ggml-org publishes llama-<tag>-bin-ubuntu-x64.tar.gz on every release
and install_llama_prebuilt.py already knows how to fetch it: when
called with --published-repo ggml-org/llama.cpp, the Linux x86_64 +
not has_usable_nvidia branch in direct_upstream_release_plan picks up
that asset directly. The fix is purely on the routing side.

Tighten the gate so a Linux host routes to ggml-org only when it is
x86_64 and has no GPU detection tool installed (nvidia-smi, rocminfo,
amd-smi, hipconfig, hipinfo). Everything else stays on the current
path:

  - macOS: already on ggml-org, unchanged
  - Windows: already on ggml-org via setup.ps1, unchanged
  - Linux CUDA: nvidia-smi present -> unslothai/llama.cpp, unchanged
  - Linux ROCm: rocminfo / amd-smi / hipconfig / hipinfo present
                -> unslothai/llama.cpp -> source build with HIP,
                unchanged
  - Linux Intel / Vulkan / SYCL: no NVIDIA / AMD tools, hits the new
                ggml-org route, gets upstream CPU asset (same as
                today's source-build CPU output, ~3 min faster)
  - Linux arm64 / s390x: not x86_64 -> unslothai/llama.cpp ->
                source build, unchanged

* Tighten routing comment in studio/setup.sh
2026-05-06 06:22:56 +00:00
Daniel Han
a87a08610e Merge branch 'main' into pip 2026-05-06 00:27:15 +00:00
danielhanchen
41abe89041 Bump version to 2026.5.2
Cuts a new PyPI release that ships PR #5296: Studio chat history and
attachments work again with newer @assistant-ui/react, plus the pinned
assistant-ui surface and frontend package-lock.json so future installs
cannot drift back onto a broken bundle.
2026-05-06 00:25:04 +00:00
Daniel Han
d638d1bd6f Merge branch 'main' into pip 2026-05-06 00:24:48 +00:00
Daniel Han
2e3b2bdc27 Update pyproject.toml 2026-05-05 05:27:54 -07:00
Daniel Han
9cc539c1b4 Merge branch 'main' into pip 2026-05-05 05:27:25 -07:00
Daniel Han
973c7d80c2 Merge branch 'main' into pip 2026-04-23 06:58:47 -07:00
Daniel Han
c27f9b99e9 Merge branch 'main' into pip 2026-04-22 09:18:09 -07:00
Daniel Han
0c24d61708 Update pyproject.toml 2026-04-16 12:06:55 -07:00
Daniel Han
e9a2b5c010 Merge branch 'main' into pip 2026-04-16 12:06:45 -07:00
Daniel Han
e8355451ea Update pyproject.toml 2026-04-15 08:07:33 -07:00
Daniel Han
a7b4ae19ee Merge branch 'main' into pip 2026-04-15 08:07:11 -07:00
Daniel Han
9066946615 Merge branch 'main' into pip 2026-04-06 09:39:11 -07:00
Daniel Han
b2580ae32b Update pyproject.toml 2026-04-06 09:21:23 -07:00
Daniel Han
7ae9580ce6 Merge branch 'main' into pip 2026-04-06 09:21:07 -07:00
Daniel Han
9ad3b761ee Merge branch 'main' into pip 2026-04-03 15:02:41 -07:00
Daniel Han
8f721d28d6 Merge branch 'main' into pip 2026-04-02 12:28:40 -07:00
Daniel Han
396aa05ead Update pyproject.toml 2026-04-02 12:03:10 -07:00
Daniel Han
2297f73cad Merge branch 'main' into pip 2026-04-02 12:02:48 -07:00
Daniel Han
a728f7c308 Update pyproject.toml 2026-03-31 06:51:58 -07:00
Daniel Han
884152daee Merge branch 'main' into pip 2026-03-31 06:51:48 -07:00
Daniel Han
7437af2e44 Merge branch 'main' into pip 2026-03-27 08:42:05 -07:00
Daniel Han
67678d2c29 Merge branch 'main' into pip 2026-03-27 07:24:42 -07:00
Daniel Han
e568000a92 Update pyproject.toml 2026-03-27 07:23:42 -07:00
Daniel Han
6700dd60f0 Merge branch 'main' into pip 2026-03-27 07:23:27 -07:00
Daniel Han
abf578327c Merge branch 'main' into pip 2026-03-27 03:35:13 -07:00
Daniel Han
a4ae80cd6a Merge branch 'main' into pip 2026-03-25 09:40:21 -07:00
Daniel Han
ec47b2984d Merge branch 'main' into pip 2026-03-25 09:38:41 -07:00
Daniel Han
d9d1a63397 Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14
- Strip version constraints on Colab dep install
- Upgrade huggingface-hub>=1.0 if is_offline_mode is missing
- ensurepip fallback for uv venvs without pip
- Bump installer pins to 2026.3.14
2026-03-25 16:36:25 +00:00
Daniel Han
0bb6379aad Fix Colab huggingface-hub conflict and pip bootstrap on uv venvs
- colab.py / setup.sh: relax == pins to >= when installing studio.txt
  on Colab so huggingface-hub 0.36.2 does not clobber Colab's bundled
  version (which breaks transformers is_offline_mode import)
- install_python_stack.py: when uv is unavailable and pip is missing
  (uv-created venvs), bootstrap via ensurepip before attempting upgrade
- Bump version to 2026.3.14
2026-03-25 16:15:29 +00:00
Daniel Han
1c608e8ff7 Fix Colab dep install: relax == pins to >= to avoid breaking transformers
studio.txt pins huggingface-hub==0.36.2 and datasets==4.3.0 which
overwrite Colab's pre-installed versions and break its bundled
transformers (is_offline_mode was removed in newer huggingface-hub).

Relax == to >= in both colab.py and setup.sh Colab paths so pip keeps
existing compatible versions instead of force-upgrading.
2026-03-25 16:13:30 +00:00
Daniel Han
f9adf6834d Merge branch 'main' into pip 2026-03-25 09:04:26 -07:00
Daniel Han
1fd5853741 Merge branch 'main' into pip 2026-03-25 09:01:49 -07:00
Daniel Han
93a70fbe4e Merge branch 'main' into pip 2026-03-25 08:34:48 -07:00
Daniel Han
8b77451e75 Update pyproject.toml 2026-03-25 07:31:31 -07:00
Daniel Han
481f0618ff Merge branch 'main' into pip 2026-03-25 07:30:50 -07:00
Daniel Han
ddf6f6d1f9 fix(studio): remove litellm dep (quarantined on PyPI) (#4553)
litellm has been quarantined on PyPI due to a supply chain attack
in version 1.82.8 (malicious credential-stealing .pth file).
No versions are currently installable, which blocks
`unsloth studio setup` at step 8/11 (data-designer deps).

Remove litellm from the single-env data-designer requirements
so setup completes. litellm can be re-added once PyPI lifts the
quarantine.

Ref: https://github.com/BerriAI/litellm/issues/24512
2026-03-24 14:21:57 +00:00
Daniel Han
dddc9eac92 Merge branch 'main' into pip 2026-03-24 06:51:20 -07:00
Daniel Han
da81c94510 Merge branch 'main' into pip 2026-03-22 08:23:44 -07:00
Daniel Han
4c139503c1 Merge branch 'main' into pip 2026-03-22 06:14:40 -07:00
Daniel Han
9d1e3c38bc Merge branch 'main' into pip 2026-03-19 02:31:50 -07:00
Daniel Han
8bc26f4e1d Merge branch 'main' into pip 2026-03-18 11:13:18 -07:00
Daniel Han
044e67f5aa Merge branch 'main' into pip 2026-03-18 10:40:39 -07:00
Daniel Han
ba36c12240 Merge branch 'main' into pip 2026-03-18 09:10:55 -07:00
Daniel Han
0a5652281f Merge branch 'main' into pip 2026-03-18 08:33:28 -07:00
Daniel Han
bf63f79414 Merge branch 'main' into pip 2026-03-18 08:31:20 -07:00
Daniel Han
2973bea3d0 Merge branch 'main' into pip 2026-03-17 07:58:59 -07:00
Daniel Han
ddbbfe52cf Update pyproject.toml 2026-03-17 07:58:28 -07:00
462 changed files with 13541 additions and 5163 deletions

View file

@ -25,8 +25,30 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"unsloth_zoo>=2026.6.4",
"wheel>=0.42.0",
"packaging",
"torch>=2.4.0,<2.11.0",
"torchvision",
"numpy",
"tqdm",
"psutil",
"tyro",
"protobuf",
"xformers>=0.0.27.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"triton>=3.0.0 ; ('linux' in sys_platform)",
"triton-windows ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"sentencepiece>=0.2.0",
"datasets>=3.4.1,!=4.0.*,!=4.1.0,<4.4.0",
"accelerate>=0.34.1",
"peft>=0.18.0,!=0.11.0",
"huggingface_hub>=0.34.0",
"hf_transfer",
"diffusers",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0",
"trl>=0.18.2,!=0.19.0,<=0.24.0",
"typer",
"rich",
"pydantic",
"pyyaml",
"nest-asyncio",
@ -55,7 +77,6 @@ studio = [
"frontend/.git*",
"backend/requirements/**/*",
"backend/plugins/**/*",
"backend/assets/**/*.jinja",
"backend/core/data_recipe/oxc-validator/*.json",
"backend/core/data_recipe/oxc-validator/*.mjs",
]
@ -69,11 +90,11 @@ triton = [
"triton>=3.0.0 ; ('linux' in sys_platform)",
"triton-windows ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
huggingfacenotorch = [
huggingface = [
"unsloth_zoo>=2026.6.4",
"wheel>=0.42.0",
"packaging",
"torchvision",
"numpy",
"tqdm",
"psutil",
@ -89,12 +110,10 @@ huggingfacenotorch = [
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0",
"trl>=0.18.2,!=0.19.0,<=0.24.0",
"sentence-transformers",
]
huggingface = [
"unsloth[huggingfacenotorch]",
"unsloth_zoo>=2026.6.4",
"torchvision",
"unsloth[triton]",
"typer",
"pydantic",
"pyyaml",
"nest-asyncio",
]
windows = [
"unsloth[huggingface]",
@ -105,240 +124,126 @@ base = [
"unsloth[huggingface]",
]
cu118only = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.22.post7%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.22.post7%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.22.post7%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.22.post7 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121only = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.22.post7-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.22.post7-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.22.post7-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.22.post7 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch211 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.23 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch211 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.23 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch212 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23.post1%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23.post1%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.23.post1%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.23.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch212 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23.post1-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23.post1-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.23.post1-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.23.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch220 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.24%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.24%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.24%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.24 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch220 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.24-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.24-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.24-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers==0.0.24 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch230 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27%2Bcu118-cp312-cp312-manylinux2014_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.27 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch230 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.27-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.27-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.27-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.27-cp312-cp312-manylinux2014_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.27 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch240 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27.post2%2Bcu118-cp39-cp39-manylinux2014_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27.post2%2Bcu118-cp310-cp310-manylinux2014_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27.post2%2Bcu118-cp311-cp311-manylinux2014_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.27.post2%2Bcu118-cp312-cp312-manylinux2014_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.27.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch240 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post1-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post1-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post1-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post1-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.27.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu124onlytorch240 = [
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post1-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.28.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch250 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.28.post2-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.28.post2-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.28.post2-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.28.post2-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.28.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch250 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post2-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post2-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post2-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.28.post2-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.28.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu124onlytorch250 = [
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.28.post2-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.28.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch251 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post1-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post1-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post1-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post1-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.29.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu121onlytorch251 = [
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.29.post1-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.29.post1-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.29.post1-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu121/xformers-0.0.29.post1-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.29.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu124onlytorch251 = [
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post1-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.29.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch260 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post3-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post3-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post3-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.29.post3-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers==0.0.29.post3 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu124onlytorch260 = [
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu124/xformers-0.0.29.post3-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.29.post3 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch260 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.29.post3-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.29.post3 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch270 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.30-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.30 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch270 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.30-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.30 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch270 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp39-cp39-manylinux_2_28_x86_64.whl ; python_version=='3.9' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp310-cp310-manylinux_2_28_x86_64.whl ; python_version=='3.10' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp311-cp311-manylinux_2_28_x86_64.whl ; python_version=='3.11' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp312-cp312-manylinux_2_28_x86_64.whl ; python_version=='3.12' and ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp39-cp39-win_amd64.whl ; python_version=='3.9' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp310-cp310-win_amd64.whl ; python_version=='3.10' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp311-cp311-win_amd64.whl ; python_version=='3.11' and (sys_platform == 'win32')",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.30-cp312-cp312-win_amd64.whl ; python_version=='3.12' and (sys_platform == 'win32')",
"xformers==0.0.30 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch271 = [
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.31.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu118/xformers-0.0.31.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.31.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch271 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.31.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.31.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.31.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch271 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.31.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.31.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.31.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118onlytorch280 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.32.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.32.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.32.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch280 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.32.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.32.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.32.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch280 = [
"xformers @ https://download.pytorch.org/whl/cu129/xformers-0.0.32.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu129/xformers-0.0.32.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.32.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu130onlytorch280 = [
]
cu126onlytorch290 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch290 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu130onlytorch290 = [
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post1-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post1 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu130onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.33.post2 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu126onlytorch2100 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.34 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu128onlytorch2100 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.34 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu130onlytorch2100 = [
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
"xformers==0.0.34 ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
cu118 = [
"unsloth[huggingface]",
@ -352,22 +257,22 @@ cu121 = [
]
cu118-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu118onlytorch211]",
]
cu121-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch211]",
]
cu118-torch212 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu118onlytorch212]",
]
cu121-torch212 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch212]",
]
cu118-torch220 = [
@ -437,17 +342,17 @@ cu124-torch251 = [
]
cu118-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu118onlytorch260]",
]
cu124-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu124onlytorch260]",
]
cu126-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu126onlytorch260]",
]
cu118-torch270 = [
@ -557,16 +462,19 @@ conda = [
]
colab-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch211]",
]
flashattention = [
"packaging ; ('linux' in sys_platform)",
"ninja ; ('linux' in sys_platform)",
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
]
colab-ampere-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch211]",
"packaging",
"ninja",
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
"unsloth[flashattention]",
]
colab-torch220 = [
"unsloth[huggingface]",
@ -577,9 +485,7 @@ colab-ampere-torch220 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch220]",
"packaging",
"ninja",
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
"unsloth[flashattention]",
]
colab-new = [
"unsloth_zoo>=2026.6.4",
@ -598,6 +504,10 @@ colab-new = [
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[triton]",
"sentence-transformers",
"typer",
"pydantic",
"pyyaml",
"nest-asyncio",
]
colab-no-deps = [
"accelerate>=0.34.1",
@ -610,11 +520,6 @@ colab-no-deps = [
colab = [
"unsloth[cu121]",
]
flashattention = [
"packaging ; ('linux' in sys_platform)",
"ninja ; ('linux' in sys_platform)",
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
]
colab-ampere = [
"unsloth[colab-ampere-torch220]",
"unsloth[flashattention]",
@ -633,13 +538,13 @@ cu121-ampere = [
]
cu118-ampere-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu118onlytorch211]",
"unsloth[flashattention]",
]
cu121-ampere-torch211 = [
"unsloth[huggingface]",
"bitsandbytes==0.45.5",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu121onlytorch211]",
"unsloth[flashattention]",
]
@ -723,19 +628,19 @@ cu124-ampere-torch251 = [
]
cu118-ampere-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu118onlytorch260]",
"unsloth[flashattention]",
]
cu124-ampere-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu124onlytorch260]",
"unsloth[flashattention]",
]
cu126-ampere-torch260 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"bitsandbytes>=0.45.1",
"unsloth[cu126onlytorch260]",
"unsloth[flashattention]",
]
@ -797,7 +702,6 @@ cu130-ampere-torch280 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu130onlytorch280]",
"unsloth[flashattention]",
]
cu126-ampere-torch290 = [
"unsloth[huggingface]",
@ -844,464 +748,6 @@ cu130-ampere-torch2100 = [
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu130onlytorch2100]",
]
flashattentiontorch260abiFALSEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
flashattentiontorch260abiTRUEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiTRUE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiTRUE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiTRUE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiTRUE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiTRUE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
flashattentiontorch250abiFALSEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiFALSE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiFALSE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiFALSE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiFALSE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
flashattentiontorch250abiTRUEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiTRUE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiTRUE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiTRUE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiTRUE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.5cxx11abiTRUE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
flashattentiontorch240abiFALSEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiFALSE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiFALSE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiFALSE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiFALSE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
flashattentiontorch240abiTRUEcu12x = [
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiTRUE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiTRUE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiTRUE-cp311-cp311-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.11'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiTRUE-cp312-cp312-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.12'",
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.4cxx11abiTRUE-cp313-cp313-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.13'",
]
intelgputorch260 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.2.0-cp39-cp39-linux_x86_64.whl#sha256=147607f190a7d7aa24ba454def5977fbbfec792fdae18e4ed278cfec29b69271 ; ('linux' in sys_platform) and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.2.0-cp310-cp310-linux_x86_64.whl#sha256=23aa423fa1542afc34f67eb3ba8ef20060f6d1b3a4697eaeab22b11c92b30f2b ; ('linux' in sys_platform) and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.2.0-cp311-cp311-linux_x86_64.whl#sha256=bcfa995229bbfd9ffd8d6c8d9f6428d393e876fa6e23ee3c20e3c0d73ca75ca5 ; ('linux' in sys_platform) and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.2.0-cp312-cp312-linux_x86_64.whl#sha256=bd340903d03470708df3442438acb8b7e08087ab9e61fbe349b2872bf9257ab0 ; ('linux' in sys_platform) and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.2.0-cp313-cp313-linux_x86_64.whl#sha256=814dccc8a07159e6eca74bed70091bc8fea2d9dd87b0d91845f9f38cde62f01c ; ('linux' in sys_platform) and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.6.0%2Bxpu-cp39-cp39-linux_x86_64.whl#sha256=6a8adf6dc4c089406e8b3a7e58ab57a463bddf9b07130d2576e76eced43e92af ; ('linux' in sys_platform) and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.6.0%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=ff4561cbf07c83bbccaa0f6e9bb0e6dcf721bacd53c9c43c4eb0e7331b4792f9 ; ('linux' in sys_platform) and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.6.0%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=12005f66b810ddd3ab93f86c4522bcfdd412cbd27fc9d189b661ff7509bc5e8a ; ('linux' in sys_platform) and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.6.0%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=c4c5c67625cdacf35765c2b94e61fe166e3c3f4a14521b1212a59ad1b3eb0f2e ; ('linux' in sys_platform) and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.6.0%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=e6864f7a60a5ecc43d5d38f59a16e5dd132384f73dfd3a697f74944026038f7b ; ('linux' in sys_platform) and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch260 = [
"unsloth[intelgputorch260]"
]
intelgputorch270 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=749a7098492c6a27b356c97149a4a62973b953eae60bc1b6259260974f344913 ; ('linux' in sys_platform) and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=44362e80abd752471a08341093321955b066daa2cfb4810e73b8e3b240850f93 ; ('linux' in sys_platform) and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=faa6b8c945a837a080f641bc8ccc77a98fa66980dcd7e62e715fd853737343fd ; ('linux' in sys_platform) and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=40f6fb65b345dc9a61813abe7ac9a585f2c9808f414d140cc2a5f11f53ee063c ; ('linux' in sys_platform) and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=b22b4c02ec71b4bfc862ae3cdfd2871dc0b05d2b1802f5db2196e0f897d581e9 ; ('linux' in sys_platform) and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp39-cp39-win_amd64.whl#sha256=d4b738d7fa5100c1bd766f91614962828a4810eb57b4df92cd5214a83505a752 ; sys_platform == 'win32' and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp310-cp310-win_amd64.whl#sha256=143fe8a64d807bcdb7d81bbc062816add325570aa160448454ab6ded4a0a17a1 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp311-cp311-win_amd64.whl#sha256=a8025459ff325d6e3532eb5cf72519db1b178155e7d60aff6c56beb5968fc758 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp312-cp312-win_amd64.whl#sha256=0dd07e6d5b872e42e48f5ee140e609d4554ca3cc509d5bf509ac232267cf358e ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.0-cp313-cp313-win_amd64.whl#sha256=a936a18182d8e065a9933afc9a3ebbffadd38604969f87c493831214539fc027 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp39-cp39-linux_x86_64.whl#sha256=f8ee75e50fcbb37ed5b498299ca2264da99ab278a93fae2358e921e4a6e28273 ; ('linux' in sys_platform) and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=d6fdc342961d98fdcd9d03dfd491a3208bb5f7fbb435841f8f72ce9fdcd2d026 ; ('linux' in sys_platform) and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=74d07f9357df5cf2bf223ad3c84de16346bfaa0504f988fdd5590d3e177e5e86 ; ('linux' in sys_platform) and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=c806d44aa2ca5d225629f6fbc6c994d5deaac2d2cde449195bc8e3522ddd219a ; ('linux' in sys_platform) and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=25d8277b7f01d42e2e014ccbab57a2692b6ec4eff8dcf894eda1b297407cf97a ; ('linux' in sys_platform) and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp39-cp39-win_amd64.whl#sha256=046e85125266ae69c1a0d083e6c092f947ab4b6b41532c16bafe40dbced845df ; sys_platform == 'win32' and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=9ebaeffb82b0b3e39b6030927d3ebe0eb62a0e9045a3b2d7b0a9e7b15222c0db ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=356ba66cee127e7e2c942880bd50e03768306a4ea08d358a0f29c6eebfc4bc81 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=94739e665d9b4d5cd7af5f517cb6103f6f9fb421c095184609653a24524040f5 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=31df3cb674918e89bc8c532baa331dc84f4430e1f9c0ec379232db44cba78355 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch270 = [
"unsloth[intelgputorch270]"
]
intelgputorch280 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=ac4d8e33986b1c3c5e48151640539272b2187e83016985853111b46fb82c3c94 ; 'linux' in sys_platform and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=999fef4c1f711092b9d3086525920545df490de476ecebe899ffc777019ae17f ; 'linux' in sys_platform and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=57b09c8c492985ff6a27cd3a22b08e8f7b96b407bd8030967b6efbb9f63b80cf ; 'linux' in sys_platform and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=df4bb3282bac9a3b90231700077110d8680b338416de03c2b7c6133c9b602649 ; 'linux' in sys_platform and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=60da63c99ca827bdcb0df28e0298bf7d066dc607454c6d6176783cb4e79d838b ; 'linux' in sys_platform and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp39-cp39-win_amd64.whl#sha256=64aea8de349f3e2e0ebf4c24b011a8122531fdffda5776edaef45829cc241cf8 ; sys_platform == 'win32' and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp310-cp310-win_amd64.whl#sha256=ae573d255b257fdbed319a3440dc9d0a721e31160ab7f6eba1b2226e6a409a1d ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp311-cp311-win_amd64.whl#sha256=8e0ea4558e5776d8ddab0264310be9b26aee5641bcac0da023537556d4317b86 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp312-cp312-win_amd64.whl#sha256=4090dde07a4fffc34aaf855701a9db28e9fccb57b368ade520f1a0f8e811c878 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.4.0-cp313-cp313-win_amd64.whl#sha256=a33d0888f3c8df028a2d028842715837d0049524d6c06b9bb11869890a13601a ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp39-cp39-linux_x86_64.whl ; 'linux' in sys_platform and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp310-cp310-linux_x86_64.whl ; 'linux' in sys_platform and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp311-cp311-linux_x86_64.whl ; 'linux' in sys_platform and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp312-cp312-linux_x86_64.whl ; 'linux' in sys_platform and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp313-cp313-linux_x86_64.whl ; 'linux' in sys_platform and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp39-cp39-win_amd64.whl#sha256=f2f401276892428e4875cf1d8717c5cbab704b16fc594ccf23795e7b16549a99 ; sys_platform == 'win32' and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=125c60cd59d51b39581a7e9afcd4679bc3a6b8c1f9440b1bb502a23fdd60571e ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=47f1a57258cd460e80b38b2ed6744e31587ab77a96b4215bf59546cb4bab5cc0 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=0937d8943c145a83d9bafc6f80ef28971167817f9eda26066d33f72caf8a6646 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.8.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=e034aab1d71760dc80a731531be43673ffe15e99033b82d24e40d2e6d41bd8bf ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp39-cp39-manylinux_2_28_x86_64.whl#sha256=6e981c192045fc249c008441179ff237bb00174d818b875b0475730b63f0eaca ; 'linux' in sys_platform and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=e5ba4805969277175ebfd59cc717093528cc6e3ada89ac2725fc7a3c1fee6169 ; 'linux' in sys_platform and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=74c39c144104416bc4c5ad8c26ab0c169dc5cc6be58059e01bc3665dd0ef676f ; 'linux' in sys_platform and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=0acec355b80c3899841184084f365df336c508602812e34a44007b8b60d53af4 ; 'linux' in sys_platform and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=e2109ae773dad27b98ca17681044b4f876563c37f2382b75de3a371399edcff8 ; 'linux' in sys_platform and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp39-cp39-win_amd64.whl#sha256=5f7904e7048d414379bc8c1167260f1e84204f105db2d0a2f9c89e87ce1cf205 ; sys_platform == 'win32' and python_version == '3.9' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=005fca5e658ca8e37adb63c1a021c84f5e56dfa6cf0d601d89cfe40b9473f79f ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=c6d030f5361461550c0ff1339b5bca8585fc1e84fda2e64b6184e65a581e4f98 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=91aafd61864cdce27461cbec13ddbf28c1bc6494265a1e4b80131c64a3b7d18f ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.23.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=71dc4a6421742ed1e7f585b04a100ad53615c341fbccfbc255aefb38ea9091da ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch280 = [
"unsloth[intelgputorch280]"
]
intelgputorch290 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=c169a1de14c19673b17c751290d467fa282fc90fa5da4314b2e5cdab1f553146 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=013d9dd5d6479bd22983161f462e61c8dbe1d82e6730624a7a8d5945507eaa61 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=afc8cabfbf7ed51fd278d1e0f88d6afc157b0201bad4b99d681e4d542f9e66d4 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=0d24c1716088f2764d0d24c64227732195b6a42706c3c5fc89eeb4904bfa0818 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp310-cp310-win_amd64.whl#sha256=c83ab007311d9cfb6e809ee5a4587d99a9eef4be720b90da4f1aaa68b45139a0 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp311-cp311-win_amd64.whl#sha256=debf75348da8e8c7166b4d4a9b91d1508bb8d6581e339f79f7604b2e6746bacd ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp312-cp312-win_amd64.whl#sha256=97337a47425f1963a723475bd61037460e84ba01db4f87a1d662c3718ff6c47e ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp313-cp313-win_amd64.whl#sha256=2caf8138695f6abb023ecd02031a2611ba1bf8fff2f19802567cb2fadefe9e87 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=5afbe860ce991825a36b75706a523601087e414b77598ef0d9d3d565741c277d ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=607fe419c32d6e8e0556f745742e7cff1d0babce51f54be890e0c1422359c442 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=376bae584d89980b8e59934d248c38d5fa3b7d4687a4df1a19f4bc1d23dcc8c1 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=98d6a06dd7fb185874367b18bd609f05f16fdce4142a5980ca94461949965cd2 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=47cc68f631f65bd9c84924d052cd04dec7531023caa85e80345e9c94611c887d ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=d56c44ab4818aba57e5c7b628f422d014e0d507427170a771c5be85e308b0bc6 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=18cad93aaff76a01ce73aef6935ece7cfc03344b905592ec731446c44d44592b ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=579929cdc10a76800ead41289cac191ea36d1b16f5f501d3fc25607d4375cd83 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=cbfae2b79b7549fd368c2462fc8e94f8f26cc450782ee72138e908077c09a519 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=044fa36ef4b6b43edcd490b75c853fa4b3eb033c2bded29f8fbcf27734713c67 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=4b91e4bec1d740a6211f02578a79888550b73f3a4e1383035f8f6d72f587212c ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=88239e73ca37254bec84f29cd5887e10ff712de7edbbda3fbb3609cd6190d99e ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=19c7da8ca767d593e13a88a12bb08d06e34a673f6f26c2f9c191d60e81c02953 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=9bb0d1421c544ac8e2eca5b47daacaf54706dc9139c003aa5e77ee5f355c5931 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=6a5194bc736089606342d48a3f6822829b167617e9495d91d753dd1bd46fda18 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=da47a3ce2bb7f0301a31124668b5908f9b9e92d6241443de15a310ef9632fd83 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch290 = [
"unsloth[intelgputorch290]"
]
intelgputorch271 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=663ce21364096b268c6687f26f22862cb1001cae0c4ec9f98a0998415f99e2b0 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=dd92cc17000bad19f213b6a877d7f10cd71341b703cd188513ce9fff8d42e3dd ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=aa5c3ec21a89e967d1dfe61e3d5b1c1ae9620c871ed804771d3378d6a44066f2 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=d1c6f522e11112a311b1a61ba7b40b43ad8305675fa29153017ccb1ad0b6816d ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp310-cp310-win_amd64.whl#sha256=a5c16dcf449a9cb62bc3788f7ec45782bb3ead6edc2637a12b60ef0f8f45dc55 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp311-cp311-win_amd64.whl#sha256=bc2d76ffa4ceed5b38ae34b52dbff643442e1a44d52ca72d7cb520ca1950e9ae ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp312-cp312-win_amd64.whl#sha256=b09ca59ce52d6d27b1510df783cde222b703a71857a6fa953f1f155f9f50811a ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.3.1-cp313-cp313-win_amd64.whl#sha256=1260c4a4bad426b6cd3c8f3e1a21835381c6f217bf434bcb55fedec08a206dea ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=231c3fbd88a75d94de5ccbbb7f4f9a96cb3c58b3d891c2a1b469d38df95f9be6 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=78edcc27709dd819fc820f5eb9421bd10d3f3dcb14adb25ee60766c76f0e67f3 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=b443df40bc9cb7d648a9f8f9ed1d5c3a1203e561ebd0a61dd55fb8a58833d5ec ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=412b58ffcceebea399c9a1bcdb22896aa10385c2650a8c4f8a677fb11c49b448 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp310-cp310-win_amd64.whl#sha256=2591228dc2cb73c78daf24277c4449ba9474f94cd31938147249269fe89d05d6 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp311-cp311-win_amd64.whl#sha256=1aacb86e9a9684ffc8bde3db14b251d00df7019a9a434ec99a59076a2696325d ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp312-cp312-win_amd64.whl#sha256=9b65dc8562521b60d77aa653132bc03a19da0291318fcf919faa3f03080d8f7e ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.7.1%2Bxpu-cp313-cp313-win_amd64.whl#sha256=cd3669fee311bc3ee5501d696bf989226a6f2bf957d120a04881a07af05526d6 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=f8cdf6889c02b3166679eef661b68757ea7e99c314432c3d41dac3d2ed4a59d4 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=f7d15b65d52809745992e0001c25034f33ac01f2dff5248614e07b5d009a59b7 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=1ff1f98d70846352c7f56833bedab1a055ead27b11c120b8c719063ee0383554 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=f46945344ea911a70309231eaaf3b80c96f6646ce5515dc89aa94f94144e310e ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp310-cp310-win_amd64.whl#sha256=ecae9a02de769e2070d37388116beb407c3f0d60b8e65c1da1423f4eafee361a ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp311-cp311-win_amd64.whl#sha256=2914e62782431bebd6ad9a3b98a2b7311e448e84a7534bb7f35874b9279a17de ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp312-cp312-win_amd64.whl#sha256=5b462c156f4e2097e1e53649d3f298ce352fa4c5d1e6addd360375b10ebd6c67 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.22.1%2Bxpu-cp313-cp313-win_amd64.whl#sha256=fa87b3677cd1af67ce423004283c1bde80e3571f391182a3e89b485e18e3c70f ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch271 = [
"unsloth[intelgputorch271]"
]
intelgputorch291 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=c169a1de14c19673b17c751290d467fa282fc90fa5da4314b2e5cdab1f553146 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=013d9dd5d6479bd22983161f462e61c8dbe1d82e6730624a7a8d5945507eaa61 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=afc8cabfbf7ed51fd278d1e0f88d6afc157b0201bad4b99d681e4d542f9e66d4 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=0d24c1716088f2764d0d24c64227732195b6a42706c3c5fc89eeb4904bfa0818 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp310-cp310-win_amd64.whl#sha256=c83ab007311d9cfb6e809ee5a4587d99a9eef4be720b90da4f1aaa68b45139a0 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp311-cp311-win_amd64.whl#sha256=debf75348da8e8c7166b4d4a9b91d1508bb8d6581e339f79f7604b2e6746bacd ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp312-cp312-win_amd64.whl#sha256=97337a47425f1963a723475bd61037460e84ba01db4f87a1d662c3718ff6c47e ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"pytorch_triton_xpu @ https://download.pytorch.org/whl/pytorch_triton_xpu-3.5.0-cp313-cp313-win_amd64.whl#sha256=2caf8138695f6abb023ecd02031a2611ba1bf8fff2f19802567cb2fadefe9e87 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=fb7895c744132d6a8e56ce8434ae1d8355c9bda4e9f58832744ff742d6268eaf ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=da2604a9114a28de71ce654819424d20a246adf644d191ae160837df9731b79e ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=d5968d78d81c1d01efc1b3bf83d7da3d83161dcc3a9fcf91f500591db1c6c75d ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=b56d6b0d65863f370527e971dbfa046a5dd2a1f61cc95071db26c764f36e4dce ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp310-cp310-win_amd64.whl#sha256=2f318fb6a4bf1101cc17f35a5371f7c1768b41fceed03628397834e85b3edfdd ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp311-cp311-win_amd64.whl#sha256=c9cedc3fb099366b2e6c563df6578e323564b1b5d40ac27be73c674755343a1d ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp312-cp312-win_amd64.whl#sha256=bee9623254d0f95a1ca115dbd17e9a9d966fdb8ae123e2ada4a9eb2fb8d38db8 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.9.1%2Bxpu-cp313-cp313-win_amd64.whl#sha256=cd5c857da52a63c121561b30b0979e69ade70b575fd74e389787bc7c1ee2ac11 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=cc5272da2cb4554edf059eedd6d1f5ef2859033b0fb79d5dcb8e99a0697f3325 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=3c80d6a068c32fc4ebddb27953e03a0141bd0f10ca8730417cbc0e0748158285 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=8cf640a867cf270b3fda7a10002c29d3fc2ad6dfbd76404a8cdd820489adb04c ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=d9c59ee5ae3d0560f02401c8dfd8054d50813a8dbb5d33a8777de7d02f6fcb7b ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp310-cp310-win_amd64.whl#sha256=843ea7fcd8f5a22ebbc20d2d61d9eec7593821a0372eb8cabb73953d12ef6acf ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp311-cp311-win_amd64.whl#sha256=e5ff8a31d3c700f8dbac59697c8e32298a43ec059609ebc6ea7bab3eff6384e1 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp312-cp312-win_amd64.whl#sha256=8bae6d4c042f8d20818da4a5aa9109c6fbd6ec11bc422be152ce8adf9a7095bf ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.24.1%2Bxpu-cp313-cp313-win_amd64.whl#sha256=47059e290fc2a41ba78666ffcde102c436abf7ff8a34d200268b48c4fa0f9c45 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch291 = [
"unsloth[intelgputorch291]"
]
intelgputorch210 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp310-cp310-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp311-cp311-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp312-cp312-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.6.0-cp313-cp313-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=abb1d1ec1ac672bac0ff35420c965f2df0c636ef9d94e2a830e34578489d0a57 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=71ad2f82da0f41eaec159f39fc85854e27c2391efa91b373e550648a6f4aaad3 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=b473571d478912f92881cc13f15fa18f8463fb0fb8a068c96ed47a7d45a4da0a ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=3bc64a746ff25a93de140902c60c9e819d7413f5cea1e88d80999c27a5901e9c ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=ce50691ab3fb6301d9b7bb8b3834cf5fa7152a2b5f91fd24c5efdc601a25b780 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=cb9d37f21cb9fb7df67d62863f021c3144e8d8832b9ea8e8523ac308bc620ea1 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=3ad605be4728b6d3a28a44d07dd794b1a9e45551b0057815bf25eb2a6d6a56a7 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.10.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=2b4b56dd6c792aef82006904fa888692e3782e4ae5da27526801bad4898f05a5 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=7e1e7b170fcf7161c8499b67156c5a05462243626dc0974010791a0bab4378d3 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=bd6add201bd7628af70437292e1447abb368e0b5f4ff9abd334ae435efd44792 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=6ad2543496bc29e59d3dd614a94d09aa9870318aedb66045344fffddfedd2cf8 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=80269f37865fcd8b57f20e4786efae2200bfa2b2727926c3c7acc82f0e7d3548 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=6b9485ba85dcba4d196d6134d9c3332fb228fb2556416bf0450a64e8a472fcba ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=36cbaedf10f6412af5c89afd9aeea474e6a56a0050348ada8fabe1ecaf6b879e ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=738357d97468d75fe3d510ac37e65130f2787f81d9bbc1518898f7396dc3403f ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=1c4b44b36a557f7381e3076fb8843366742238648441d607c8d049c6da0f8886 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch210 = [
"unsloth[intelgputorch210]"
]
intelgputorch2110 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=2a1841138750f708ec017becbf8d357526f3fa350deee6553be5735ad66160a3 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=e85378f1fc1ea002271de2a35475b75008fa554b86ef9d3bc55be9c513a63b51 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=a6663ebe43e3c0d560ff774708632d7a75208ee64a291c1724ed5c16a92d1c72 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=08c8d43b2831faf9d6799480df2b45dde58102257aebd810d07a2ce18cd4e5df ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp310-cp310-win_amd64.whl#sha256=90fb8f767950a4ffca627faa7f86d9c697237ea4352d7e23505c5c9ed8e72216 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp311-cp311-win_amd64.whl#sha256=aa7de82f4265089e74f25a2701b7532e5c47d74224d877b61da1d66156e3f0c1 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp312-cp312-win_amd64.whl#sha256=5ba3a31c6e1b259ad2d924e1b50f72a78c6ebd7eb4f364473bbf93e144734e80 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.0-cp313-cp313-win_amd64.whl#sha256=e8b4caba9b2399ea4c7f9a2777042564dea5d6f9e586a2dcb015a4ce20f000f7 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp310-cp310-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp311-cp311-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp312-cp312-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.11.0%2Bxpu-cp313-cp313-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=6e634354b752b7366e8ad16b84f3e7e5863776a7ab448bbabae4fd36668dee7a ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=293169899f562ce473a58836dd024f0b1e72a347400278287ab393d1b04991e4 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=e204d14be6f0f84d5f0e6e9213556e80326c3ab682cac108bcbef340bf45297b ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=f134344006f0989a2d771554b7905fb05bd93d63b195e64626fde3495ec6f287 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=7e52729cb9736c66dc79a7f42de6b31db93b9161d3357fd34cfa33f5fe32b8ea ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=83a6130100c6b6750d8aa9fd29e5d0c53b1c85b1153b8ed4139aea54fc1892cc ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=03788e0e5a5b85a2f09d11f0263d579fcb0cf5623d8810149be0e37836c2738c ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.26.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=cb1da1d378ce440f7d1e0ed8cf21bd280d904ab25a55c9453f8377825818df74 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch2110 = [
"unsloth[intelgputorch2110]"
]
intelgputorch2120 = [
"unsloth_zoo[intelgpu]",
"unsloth[huggingfacenotorch]",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=844d981cb1b3948085e8cfa62c74de9f100259f6131959aa70be49123b88ae81 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=a16b1d00e94ad87d62af3512e390348b8656419598004100c56028bf494f086b ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=4e46e71e077cf483404a4c17ce40d71c5f0e13a81459139d4346ca427b1dd455 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl#sha256=4fdaed1bafc51d3a2834656a3420a6686a74ea226508765a49bf15d58ff3a930 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp310-cp310-win_amd64.whl#sha256=2778b46b22e9fa0916398db299a125027a1b2331c1173b3dd2b9e2cab6263a31 ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp311-cp311-win_amd64.whl#sha256=ad5b147d04ee0d40f3d4d32f85f5aa3a3beb6cd5799ca026d3d7f4afa3d9e24f ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp312-cp312-win_amd64.whl#sha256=d9482063af2a308543f23333e32edd738ea87cbb33ade68afda9ae0fd704ccd9 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"triton-xpu @ https://download.pytorch.org/whl/triton_xpu-3.7.1-cp313-cp313-win_amd64.whl#sha256=5d4d67f0deb1e851c01b293e602b8dcddad26ca2be61221cee3dc0e1aa0cdefd ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp310-cp310-linux_x86_64.whl#sha256=e8923cd1fe560472904b1461b745d2f1826bb9c1bc0808225d5f28a450e4d553 ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp311-cp311-linux_x86_64.whl#sha256=f7c082b2fc9b61def594d30ea57762dc4a8bc7111a9a9593953ed948de242e28 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp312-cp312-linux_x86_64.whl#sha256=f59decc04bec27862ed0197554a52370dbcba3e6892616d1fbce450e402bf2d5 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp313-cp313-linux_x86_64.whl#sha256=56f74e7c6c096e1a7ac215eb79ee590b764be3fbba8f4febc145bca47194a083 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=b9779b71457b5a916ae052ed2467c10273cae4862d469b191359173b2038c53e ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=7ef8e776c992e4e3ae007ebc108eb4f36b1d1dd9da97ecb308ab7fded89a2659 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=7f1d40febf2b8724adf4ff23866897d87478cc43de2a20f7776dc00be334c464 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torch @ https://download.pytorch.org/whl/xpu/torch-2.12.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=32770e2613df26e2c81ae64ea001b2ca12b8d152231285caff9b5f963a21ad75 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-manylinux_2_24_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_main/bitsandbytes-1.33.7.preview-py3-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp310-cp310-manylinux_2_28_x86_64.whl#sha256=0d517462caf6f5201c0d7c880f4ac431783c88fcc59b4587836da6c72a89509c ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp311-cp311-manylinux_2_28_x86_64.whl#sha256=4b6feada86aa0bd606904b05898b33538106120d8ed706ba11d0011046534cb8 ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=e231819be0f87829c2344c909c1f0db9d6ae7d6faefe644a526a1a01d0c18d98 ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp313-cp313-manylinux_2_28_x86_64.whl#sha256=8bc7d37515cea18af4c389d5fde58b1a9d76b015f2d87e4a7dc62ad50b1cc200 ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp310-cp310-win_amd64.whl#sha256=65dbb041057dddfe369f29cfaab63f75563621779a23a7b1e2c0ff8a84d4376a ; sys_platform == 'win32' and python_version == '3.10' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp311-cp311-win_amd64.whl#sha256=df647445365924d69fe3bb2a15a7edfe5b63ef91e4ae69af11d93582985237a4 ; sys_platform == 'win32' and python_version == '3.11' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp312-cp312-win_amd64.whl#sha256=b0db3df0d0d154d18ba988ab420f1da2549f9372113ff54ff66e4ae3c7fe3bd0 ; sys_platform == 'win32' and python_version == '3.12' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.27.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=c70850842068c43a0d50eaf139c25b6f6cc9b17a0dae70218c7e69edbee0bc80 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
intel-gpu-torch2120 = [
"unsloth[intelgputorch2120]"
]
intel = [
"unsloth[intelgputorch280]",
]
amd = [
"unsloth[huggingfacenotorch]",
"bitsandbytes>=0.49.1 ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64' or platform_machine == 'aarch64')",
"bitsandbytes>=0.49.1 ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
]
rocm702-torch280 = [
"unsloth[amd]",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/triton-3.4.0%2Brocm7.0.2.gitf9e5bf54-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/triton-3.4.0%2Brocm7.0.2.gitf9e5bf54-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/triton-3.4.0%2Brocm7.0.2.gitf9e5bf54-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torch-2.8.0%2Brocm7.0.2.lw.git245bf6ed-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torch-2.8.0%2Brocm7.0.2.lw.git245bf6ed-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torch-2.8.0%2Brocm7.0.2.lw.git245bf6ed-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torchvision-0.23.0%2Brocm7.0.2.git824e8c87-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torchvision-0.23.0%2Brocm7.0.2.git824e8c87-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0.2/torchvision-0.23.0%2Brocm7.0.2.git824e8c87-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
]
rocm72-torch291 = [
"unsloth[amd]",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/windows/rocm-rel-7.2/torch-2.9.1%2Brocmsdk20260116-cp312-cp312-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.12'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/windows/rocm-rel-7.2/torchvision-0.24.1%2Brocmsdk20260116-cp312-cp312-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.12'",
]
rocm711-torch291 = [
"unsloth[amd]",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.5.1%2Brocm7.1.1.gita272dfa8-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.5.1%2Brocm7.1.1.gita272dfa8-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.5.1%2Brocm7.1.1.gita272dfa8-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.5.1%2Brocm7.1.1.gita272dfa8-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.9.1%2Brocm7.1.1.lw.git351ff442-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.9.1%2Brocm7.1.1.lw.git351ff442-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.9.1%2Brocm7.1.1.lw.git351ff442-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.9.1%2Brocm7.1.1.lw.git351ff442-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.24.0%2Brocm7.1.1.gitb919bd0c-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.24.0%2Brocm7.1.1.gitb919bd0c-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.24.0%2Brocm7.1.1.gitb919bd0c-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.24.0%2Brocm7.1.1.gitb919bd0c-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
]
rocm72-torch2100 = [
"unsloth[amd]",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.6.0%2Brocm7.2.0.gitba5c1517-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.6.0%2Brocm7.2.0.gitba5c1517-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.6.0%2Brocm7.2.0.gitba5c1517-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.6.0%2Brocm7.2.0.gitba5c1517-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.10.0%2Brocm7.2.0.lw.gitb6ee5fde-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.10.0%2Brocm7.2.0.lw.gitb6ee5fde-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.10.0%2Brocm7.2.0.lw.gitb6ee5fde-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.10.0%2Brocm7.2.0.lw.gitb6ee5fde-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
]
rocm711-torch2100 = [
"unsloth[amd]",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.6.0%2Brocm7.1.1.gitba5c1517-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.6.0%2Brocm7.1.1.gitba5c1517-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.6.0%2Brocm7.1.1.gitba5c1517-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"triton @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/triton-3.6.0%2Brocm7.1.1.gitba5c1517-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.10.0%2Brocm7.1.1.lw.gitd9556b05-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.10.0%2Brocm7.1.1.lw.gitd9556b05-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.10.0%2Brocm7.1.1.lw.gitd9556b05-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torch @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torch-2.10.0%2Brocm7.1.1.lw.gitd9556b05-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp310-cp310-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.10' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
]
[project.urls]
homepage = "https://unsloth.ai"
@ -1310,7 +756,6 @@ repository = "https://github.com/unslothai/unsloth"
[tool.ruff]
target-version = "py311"
line-length = 100
force-exclude = true
extend-exclude = [
"*chat_templates.py",
@ -1343,4 +788,3 @@ ignore = [
# does NOT pick up the GPU-heavy tests under tests/python, tests/qlora,
# etc. The CI security job runs `pytest tests/security` explicitly.
testpaths = ["tests/security"]
pythonpath = ["."]

View file

@ -52,7 +52,9 @@ EXPECTED_NOISE_FILES = {
}
# File types where a quoted string can be a module specifier.
JS_LIKE_EXT = re.compile(r"\.(ts|tsx|js|jsx|mjs|cjs|html|htm|css|scss|sass|json|jsonc)$")
JS_LIKE_EXT = re.compile(
r"\.(ts|tsx|js|jsx|mjs|cjs|html|htm|css|scss|sass|json|jsonc)$"
)
# Files where JS import patterns could be a real module reference (.mdx is
# real ESM; .md code fences are not).
SCRIPT_LIKE_EXT = re.compile(r"\.(ts|tsx|js|jsx|mjs|cjs|mdx)$")
@ -249,7 +251,9 @@ def classify(pkg: str, file: str, content: str) -> str | None:
if is_script and re.search(rf"\bimport\(\s*['\"]{esc}{sub}['\"]\s*\)", content):
return "dynamic_import"
# require / require.resolve
if is_script and re.search(rf"\brequire(?:\.resolve)?\(\s*['\"]{esc}{sub}['\"]\s*\)", content):
if is_script and re.search(
rf"\brequire(?:\.resolve)?\(\s*['\"]{esc}{sub}['\"]\s*\)", content
):
return "require"
# Re-exports: `export * from`, `export { x } from`, `export type { Foo } from`.
if is_script and re.search(
@ -261,12 +265,16 @@ def classify(pkg: str, file: str, content: str) -> str | None:
# HTML script / link. Match pkg as a complete path segment so
# `/node_modules/foo-extra/...` is not treated as usage of `foo`.
html_pkg = rf"{esc}(?:/[^'\"#?]*)?(?=['\"#?])"
if is_html and re.search(rf"<script[^>]*src\s*=\s*['\"][^'\"]*/{html_pkg}", content):
if is_html and re.search(
rf"<script[^>]*src\s*=\s*['\"][^'\"]*/{html_pkg}", content
):
return "html_script"
if is_html and re.search(rf"<link[^>]*href\s*=\s*['\"][^'\"]*/{html_pkg}", content):
return "html_link"
# TypeScript triple-slash
if is_ts and re.search(rf"///\s*<reference\s+types\s*=\s*['\"]{esc}{sub}['\"]", content):
if is_ts and re.search(
rf"///\s*<reference\s+types\s*=\s*['\"]{esc}{sub}['\"]", content
):
return "tsc_triple_slash"
# new URL("pkg/...", import.meta.url)
if is_script and re.search(rf"\bnew\s+URL\(\s*['\"]{esc}{sub}['\"]", content):
@ -479,12 +487,18 @@ def _next_real_bin(words: list[str], idx: int) -> str | None:
if first in {"npx", "pnpx", "bunx"} and idx + 1 < len(words):
idx += 1
continue
if first in {"pnpm", "yarn"} and idx + 2 < len(words) and words[idx + 1] in {"exec", "dlx"}:
if (
first in {"pnpm", "yarn"}
and idx + 2 < len(words)
and words[idx + 1] in {"exec", "dlx"}
):
idx += 2
continue
# 3. Wrapper bin (cross-env, dotenv): skip its flags and env prefixes.
bin_token = first.removeprefix("./node_modules/.bin/").removeprefix("node_modules/.bin/")
bin_token = first.removeprefix("./node_modules/.bin/").removeprefix(
"node_modules/.bin/"
)
if bin_token in _SCRIPT_WRAPPERS and bin_token not in seen_wrappers:
seen_wrappers.add(bin_token)
idx += 1
@ -510,7 +524,9 @@ def _next_real_bin(words: list[str], idx: int) -> str | None:
return None
def scripts_bin_refs(head_pkg: dict, bin_to_pkg: dict[str, str]) -> dict[str, list[str]]:
def scripts_bin_refs(
head_pkg: dict, bin_to_pkg: dict[str, str]
) -> dict[str, list[str]]:
"""Return `{package_name: ['scripts.X: cmd', ...]}` for every package
referenced via its bin name in package.json scripts.
@ -566,7 +582,11 @@ def tsconfig_compiler_types_refs() -> set[str]:
if not isinstance(t, str):
continue
# `vite/client` resolves to the `vite` package.
pkg = t.split("/", 1)[0] if not t.startswith("@") else "/".join(t.split("/", 2)[:2])
pkg = (
t.split("/", 1)[0]
if not t.startswith("@")
else "/".join(t.split("/", 2)[:2])
)
out.add(pkg)
return out
@ -704,7 +724,9 @@ _file_lines_cache: dict[str, list[str]] = {}
def _read_file(path: str) -> list[str]:
if path not in _file_lines_cache:
try:
_file_lines_cache[path] = Path(path).read_text(errors = "replace").splitlines()
_file_lines_cache[path] = (
Path(path).read_text(errors = "replace").splitlines()
)
except (OSError, UnicodeDecodeError):
_file_lines_cache[path] = []
return _file_lines_cache[path]
@ -819,14 +841,18 @@ def find_types_runtime_usage(pkg: str, tsc_types: set[str]) -> list[Hit]:
def main() -> int:
p = argparse.ArgumentParser(description = __doc__, formatter_class = argparse.RawTextHelpFormatter)
p = argparse.ArgumentParser(
description = __doc__, formatter_class = argparse.RawTextHelpFormatter
)
p.add_argument(
"--base",
default = "origin/main",
help = "git ref to diff against (default: origin/main). "
"Examples: HEAD~1, main, a-tag, a-sha.",
)
p.add_argument("--base-pkg", help = "optional override: read base package.json from this path")
p.add_argument(
"--base-pkg", help = "optional override: read base package.json from this path"
)
p.add_argument(
"--base-lock",
help = "optional override: read base package-lock.json from this path. "
@ -918,7 +944,9 @@ def main() -> int:
print(f" - {w}")
print()
if missing_imports:
print(f"Imports without a matching package.json dep ({len(missing_imports)}):")
print(
f"Imports without a matching package.json dep ({len(missing_imports)}):"
)
for file, ln, spec in missing_imports[:20]:
print(f" - {file}:{ln} imports '{spec}'")
print()
@ -956,7 +984,9 @@ def main() -> int:
return 1
return 0
print(f"Checking {len(removed)} removed package(s) from studio/frontend/package.json")
print(
f"Checking {len(removed)} removed package(s) from studio/frontend/package.json"
)
print(f"Base: {args.base} Head: working tree")
print()
@ -980,7 +1010,9 @@ def main() -> int:
top = f"node_modules/{name}"
top_path = top if top in reachable_paths else None
nested = sorted(
p for p in reachable_paths if p != top and p.endswith(f"/node_modules/{name}")
p
for p in reachable_paths
if p != top and p.endswith(f"/node_modules/{name}")
)
return top_path, nested
@ -1026,7 +1058,9 @@ def main() -> int:
_print_hygiene()
if failures:
print(f"FAIL: {len(failures)} removed package(s) still referenced and not resolvable")
print(
f"FAIL: {len(failures)} removed package(s) still referenced and not resolvable"
)
for name, _ in failures:
print(f" - {name}")
return 1

View file

@ -38,7 +38,9 @@ HIGH = "HIGH"
class Finding:
__slots__ = ("severity", "name", "version", "kind", "detail")
def __init__(self, severity: str, name: str, version: str, kind: str, detail: str) -> None:
def __init__(
self, severity: str, name: str, version: str, kind: str, detail: str
) -> None:
self.severity = severity
self.name = name
self.version = version
@ -161,7 +163,9 @@ def diff_new_install_scripts(base_lock: dict, head_lock: dict) -> list[Finding]:
if key in base:
continue # pre-existing install-script dep; not in scope
name = head[key]
version = key[len(name) + 1 :] if key.startswith(name + "@") else "<unversioned>"
version = (
key[len(name) + 1 :] if key.startswith(name + "@") else "<unversioned>"
)
scripts = _fetch_registry_scripts(name, version)
if scripts:
detail = "; ".join(f"{h}={cmd!r}" for h, cmd in scripts.items())

View file

@ -123,7 +123,9 @@ def remove_redundant_passes(text: str) -> tuple[str, bool]:
lines = text.splitlines(keepends=True)
changed = False
for node in sorted(redundant, key=lambda item: (item.lineno, item.col_offset), reverse=True):
for node in sorted(
redundant, key=lambda item: (item.lineno, item.col_offset), reverse=True
):
start = node.lineno - 1
end = (node.end_lineno or node.lineno) - 1
if start >= len(lines):
@ -181,7 +183,11 @@ def remove_blank_after_short_import(text: str) -> tuple[str, bool]:
out: list[list[ast.stmt]] = []
for attr in ("body", "orelse", "finalbody"):
val = getattr(node, attr, None)
if isinstance(val, list) and val and all(isinstance(s, ast.stmt) for s in val):
if (
isinstance(val, list)
and val
and all(isinstance(s, ast.stmt) for s in val)
):
out.append(val)
return out
@ -199,7 +205,9 @@ def remove_blank_after_short_import(text: str) -> tuple[str, bool]:
j += 1
if j + 1 < len(suite): # an import block followed by another statement
last_imp, nxt = suite[j], suite[j + 1]
gap = range((last_imp.end_lineno or last_imp.lineno) + 1, nxt.lineno)
gap = range(
(last_imp.end_lineno or last_imp.lineno) + 1, nxt.lineno
)
nums = [n for n in gap if 1 <= n <= len(lines)]
if nums and all(lines[n - 1].strip() == "" for n in nums):
drop.update(nums)
@ -211,7 +219,13 @@ def remove_blank_after_short_import(text: str) -> tuple[str, bool]:
return "".join(kept), True
_STRING_TRIVIA = (tokenize.NL, tokenize.NEWLINE, tokenize.COMMENT, tokenize.INDENT, tokenize.DEDENT)
_STRING_TRIVIA = (
tokenize.NL,
tokenize.NEWLINE,
tokenize.COMMENT,
tokenize.INDENT,
tokenize.DEDENT,
)
_DEF_MIN_PARAMS_FOR_MULTILINE = 3 # signatures with < this many params stay one line

View file

@ -29,7 +29,9 @@ from pathlib import Path
try:
import yaml
except ImportError:
print("ERROR: PyYAML is required. Install with 'pip install pyyaml'", file = sys.stderr)
print(
"ERROR: PyYAML is required. Install with 'pip install pyyaml'", file = sys.stderr
)
sys.exit(2)
REPO_ROOT = Path(__file__).resolve().parents[1]
@ -133,7 +135,9 @@ def main() -> int:
)
if findings:
print("Workflow trigger lint failed with the following issues:", file = sys.stderr)
print(
"Workflow trigger lint failed with the following issues:", file = sys.stderr
)
for f in findings:
print(f" - {f}", file = sys.stderr)
return 1

View file

@ -459,7 +459,9 @@ def audit_npm_lockfile(path: Path) -> list[Finding]:
path = str(path),
package = key,
kind = "blocked-known-malicious",
detail = (f"{pkg_name}@{version} is on the BLOCKED_NPM_VERSIONS list"),
detail = (
f"{pkg_name}@{version} is on the BLOCKED_NPM_VERSIONS list"
),
)
)
@ -663,7 +665,9 @@ def main(argv: list[str] | None = None) -> int:
"--cargo-lockfile",
action = "append",
default = None,
help = ("Path to a Cargo.lock (repeatable). Default: studio/src-tauri/Cargo.lock."),
help = (
"Path to a Cargo.lock (repeatable). Default: studio/src-tauri/Cargo.lock."
),
)
parser.add_argument(
"--strict",

View file

@ -155,7 +155,9 @@ def convert_cell_to_python(source: str, *, allow_shell: bool = True) -> str:
cmd_lines.append(lines[i].strip())
full_cmd = "\n".join(cmd_lines)
result.extend(_emit_shell_command(indent, full_cmd, allow_shell = allow_shell))
result.extend(
_emit_shell_command(indent, full_cmd, allow_shell = allow_shell)
)
# %cd path -> os.chdir(path)
elif stripped.startswith("%cd "):
@ -278,7 +280,9 @@ def convert_notebook_to_script(
source_name = source
output_filename = filename.replace(".ipynb", ".py")
output_filename = output_filename.replace("(", "").replace(")", "").replace("-", "_")
output_filename = (
output_filename.replace("(", "").replace(")", "").replace("-", "_")
)
if output_dir:
output_path = os.path.join(output_dir, output_filename)
@ -297,7 +301,9 @@ def convert_notebook_to_script(
def main():
import argparse
class Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
class Formatter(
argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
pass
parser = argparse.ArgumentParser(
@ -311,8 +317,12 @@ Examples:
python notebook_to_python.py https://github.com/unslothai/notebooks/blob/main/nb/Oute_TTS_(1B).ipynb
""",
)
parser.add_argument("notebooks", nargs = "+", help = "Notebook files or URLs to convert.")
parser.add_argument("-o", "--output", dest = "output_dir", default = ".", help = "Output directory.")
parser.add_argument(
"notebooks", nargs = "+", help = "Notebook files or URLs to convert."
)
parser.add_argument(
"-o", "--output", dest = "output_dir", default = ".", help = "Output directory."
)
# Default True for backwards compat; pass --no-allow-shell for untrusted notebooks.
parser.add_argument(
"--allow-shell",

View file

@ -87,7 +87,9 @@ COLAB_ORACLE_FILES: dict[str, str] = {
"apt-list-gpu.txt": "colab_apt_list.gpu.txt",
"os-info-gpu.txt": "colab_os_info.gpu.txt",
}
COLAB_ORACLE_BASE_URL = "https://raw.githubusercontent.com/googlecolab/backend-info/main/"
COLAB_ORACLE_BASE_URL = (
"https://raw.githubusercontent.com/googlecolab/backend-info/main/"
)
# ----- Compat tables. PRs add rows as new releases land. ----- #
@ -187,7 +189,9 @@ def install_cells(nb: dict[str, Any]) -> list[tuple[int, str]]:
if first and first[0].strip().startswith("%%capture"):
out.append((i, src))
continue
if re.search(r"^[ \t]*!\s*(uv\s+)?pip\s+(install|uninstall)\b", src, re.MULTILINE):
if re.search(
r"^[ \t]*!\s*(uv\s+)?pip\s+(install|uninstall)\b", src, re.MULTILINE
):
out.append((i, src))
return out
@ -318,7 +322,9 @@ def parse_pip_line(line: str, line_no: int = 0) -> PipInvocation | None:
if t in ("install", "uninstall"):
continue
packages.append(t)
return PipInvocation(tool = tool, flags = flags, packages = packages, raw = line, line_no = line_no)
return PipInvocation(
tool = tool, flags = flags, packages = packages, raw = line, line_no = line_no
)
def _glue_line_continuations(text: str) -> list[tuple[int, str]]:
@ -403,7 +409,9 @@ def pypi_metadata(name: str, version: str) -> dict[str, Any] | None:
return data
def transitive_constraint(name: str, version: str, target: str) -> tuple[str | None, list[str]]:
def transitive_constraint(
name: str, version: str, target: str
) -> tuple[str | None, list[str]]:
"""Return (raw_specifier_string_or_None, list_of_(op,version) tuples)
for the constraint that `name==version` places on `target`.
"""
@ -477,7 +485,10 @@ def resolved_set(install_cell: str, colab: dict[str, str]) -> dict[str, str]:
out[sp.name] = ver
pinned.add(sp.name)
elif op == "<=" and sp.name not in pinned:
if sp.name not in upper_bounds or cmp_versions(ver, upper_bounds[sp.name]) < 0:
if (
sp.name not in upper_bounds
or cmp_versions(ver, upper_bounds[sp.name]) < 0
):
upper_bounds[sp.name] = ver
# Apply upper bounds where Colab's preinstall violates them.
for name, ub in upper_bounds.items():
@ -492,7 +503,9 @@ def resolved_set(install_cell: str, colab: dict[str, str]) -> dict[str, str]:
# ----- Rules ----- #
def rule_inst_001_git_plus(install_cell: str, file: str, cell_idx: int) -> list[Finding]:
def rule_inst_001_git_plus(
install_cell: str, file: str, cell_idx: int
) -> list[Finding]:
findings: list[Finding] = []
for inv in iter_pip_invocations(install_cell):
if any("git+" in p for p in inv.packages) or "git+" in inv.raw:
@ -680,7 +693,9 @@ def rule_inst_005_transformers_tokenizers(
_RE_DOUBLE_BANG = re.compile(r"^[ \t]*!{2,}\s*pip\b", re.MULTILINE)
def rule_inst_006_double_bang(install_cell: str, file: str, cell_idx: int) -> list[Finding]:
def rule_inst_006_double_bang(
install_cell: str, file: str, cell_idx: int
) -> list[Finding]:
findings: list[Finding] = []
for m in _RE_DOUBLE_BANG.finditer(install_cell):
line_no = install_cell.count("\n", 0, m.start()) + 1
@ -771,7 +786,9 @@ POLICY_CLAUSES_DEFAULT = [
]
def extract_policy_clauses(update_script: pathlib.Path) -> list[tuple[str, re.Pattern[str], Any]]:
def extract_policy_clauses(
update_script: pathlib.Path,
) -> list[tuple[str, re.Pattern[str], Any]]:
"""Best-effort scan of update_all_notebooks.py for canonical phrases;
falls back to POLICY_CLAUSES_DEFAULT (which we use directly today). The
permissive regexes avoid false positives on template rewords."""
@ -831,7 +848,11 @@ def cmd_drift(args: argparse.Namespace) -> int:
print(f"FAIL: {update_script} not found", file = sys.stderr)
return 2
# Stash any pre-existing dirty state, run the updater, diff, restore.
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd = nbdir).decode().strip()
head = (
subprocess.check_output(["git", "rev-parse", "HEAD"], cwd = nbdir)
.decode()
.strip()
)
subprocess.run(
["git", "-C", str(nbdir), "stash", "--include-untracked"],
check = False,
@ -932,7 +953,9 @@ def cmd_convert(args: argparse.Namespace) -> int:
hint = proc.stderr[-200:].strip(),
)
)
print(f"converted {len(notebooks) - len(failed)}/{len(notebooks)} notebooks to {out}")
print(
f"converted {len(notebooks) - len(failed)}/{len(notebooks)} notebooks to {out}"
)
_emit(failed)
return 0 if not failed else 1
@ -942,7 +965,11 @@ def cmd_convert(args: argparse.Namespace) -> int:
def cmd_lint(args: argparse.Namespace) -> int:
nbdir = pathlib.Path(args.notebooks_dir).resolve()
colab_path = pathlib.Path(args.colab_pin).resolve() if args.colab_pin else COLAB_FALLBACK_FILE
colab_path = (
pathlib.Path(args.colab_pin).resolve()
if args.colab_pin
else COLAB_FALLBACK_FILE
)
colab = parse_pip_freeze(colab_path)
if not colab:
print(
@ -982,9 +1009,13 @@ def cmd_lint(args: argparse.Namespace) -> int:
first_cell = cells[0][0] if cells else None
findings += rule_inst_003_peft_torchao(merged, oracle, rel, first_cell)
findings += rule_inst_004_torchcodec_torch(merged, oracle, rel, first_cell)
findings += rule_inst_005_transformers_tokenizers(merged, oracle, rel, first_cell)
findings += rule_inst_005_transformers_tokenizers(
merged, oracle, rel, first_cell
)
if not args.no_pypi:
findings += rule_inst_002_no_deps_transitive(merged, oracle, rel, first_cell)
findings += rule_inst_002_no_deps_transitive(
merged, oracle, rel, first_cell
)
findings += scan_user_cells(nb, rel)
_emit(findings)
return 0 if not any(f.severity == "error" for f in findings) else 1
@ -1159,7 +1190,9 @@ def cmd_colab_diff(args: argparse.Namespace) -> int:
print(f"::warning::colab-diff: could not fetch {url}: {e}")
continue
if not snap_path.exists():
print(f"::warning::colab-diff: no committed snapshot at {snap_path}; skipping")
print(
f"::warning::colab-diff: no committed snapshot at {snap_path}; skipping"
)
continue
snapshot_text = snap_path.read_text(encoding = "utf-8", errors = "replace")
parser = _COLAB_ORACLE_PARSERS[upstream_name]

View file

@ -768,7 +768,8 @@ def download_tarball(
written += len(chunk)
if written > max_bytes:
return dest, (
f"download exceeded cap {max_bytes} bytes " f"after {written} bytes"
f"download exceeded cap {max_bytes} bytes "
f"after {written} bytes"
)
h.update(chunk)
out.write(chunk)
@ -865,7 +866,11 @@ def safe_extract(
# each gets its own cap (both are bounded).
header = src.read(16)
is_binary = _looks_binary(name, header)
file_cap = HARD_MAX_BINARY_FILE_BYTES if is_binary else HARD_MAX_TEXT_FILE_BYTES
file_cap = (
HARD_MAX_BINARY_FILE_BYTES
if is_binary
else HARD_MAX_TEXT_FILE_BYTES
)
if declared > file_cap:
return (
f"member {name!r} declared size {declared} > "
@ -988,7 +993,9 @@ def scan_package_json(pkg: PackageEntry, rel: str, text: str) -> list[Finding]:
if isinstance(opt, dict):
for k, v in opt.items():
if isinstance(v, str) and (
v.startswith("github:") or v.startswith("git+") or v.startswith("git://")
v.startswith("github:")
or v.startswith("git+")
or v.startswith("git://")
):
findings.append(
Finding(
@ -1104,7 +1111,9 @@ def scan_text_blob(pkg: PackageEntry, rel: str, text: str) -> list[Finding]:
filename = rel,
pattern = "js-fetch-eval",
evidence = _evidence(text, _JS_FETCH_EVAL),
detail = ("Function/eval against base64-decoded payload (obfuscated dropper shape)"),
detail = (
"Function/eval against base64-decoded payload (obfuscated dropper shape)"
),
)
)
if _JS_ENV_TOKEN.search(text):
@ -1345,7 +1354,8 @@ def main(argv: list[str] | None = None) -> int:
if hard_errors or blocking:
if blocking:
print(
f"\n[scan-npm] FAIL: {len(blocking)} finding(s) " f"at or above {threshold}",
f"\n[scan-npm] FAIL: {len(blocking)} finding(s) "
f"at or above {threshold}",
file = sys.stderr,
)
return 1

View file

@ -293,7 +293,9 @@ RE_CRYPTO_THEFT = re.compile(
RE_PTH_IMPORT = re.compile(r"^\s*import\s+", re.MULTILINE)
# openssl CLI invocations via subprocess (encrypted exfiltration)
RE_OPENSSL_CLI = re.compile(r"\bopenssl\s+(enc|rand|rsautl|pkeyutl|genrsa|dgst|s_client)\b")
RE_OPENSSL_CLI = re.compile(
r"\bopenssl\s+(enc|rand|rsautl|pkeyutl|genrsa|dgst|s_client)\b"
)
# Write to /tmp then execute (staged dropper)
RE_TEMP_EXEC = re.compile(
@ -1224,13 +1226,15 @@ def iter_archive_files(archive_path: str):
# historically dereferenced them on extract.
if member.issym() or member.islnk():
print(
f" [WARN] {path.name}: refused link member " f"{member.name!r}",
f" [WARN] {path.name}: refused link member "
f"{member.name!r}",
file = sys.stderr,
)
continue
if member.isdev() or member.isfifo():
print(
f" [WARN] {path.name}: refused special member " f"{member.name!r}",
f" [WARN] {path.name}: refused special member "
f"{member.name!r}",
file = sys.stderr,
)
continue
@ -1432,7 +1436,9 @@ def download_packages(
env = env,
)
if proc.returncode != 0:
msg = f"pip download (with deps) failed: " f"{proc.stderr.strip()[:500]}"
msg = (
f"pip download (with deps) failed: " f"{proc.stderr.strip()[:500]}"
)
print(f" [ERROR] {msg}", file = sys.stderr)
download_errors.append(msg)
except subprocess.TimeoutExpired:
@ -1473,7 +1479,10 @@ def download_packages(
env = env,
)
if proc.returncode != 0:
msg = f"pip download failed for {spec}: " f"{proc.stderr.strip()[:500]}"
msg = (
f"pip download failed for {spec}: "
f"{proc.stderr.strip()[:500]}"
)
print(f" [ERROR] {msg}", file = sys.stderr)
download_errors.append(msg)
continue
@ -1499,7 +1508,9 @@ def _extract_pkg_name(spec: str) -> str:
"""Extract the package name from a pip spec string."""
m = _RE_NAME.match(spec)
return (
m.group(1) if m else spec.split("==")[0].split(">=")[0].split("<=")[0].split("[")[0].strip()
m.group(1)
if m
else spec.split("==")[0].split(">=")[0].split("<=")[0].split("[")[0].strip()
)
@ -1840,7 +1851,9 @@ def _run_fix(critical_pkgs: set[str], entries: list[dict], max_search: int) -> N
if git_entries:
for e in git_entries:
src = e["source_file"] or "CLI"
print(f" [SKIP] {pkg_name} is a git URL dep in {src}, cannot auto-update")
print(
f" [SKIP] {pkg_name} is a git URL dep in {src}, cannot auto-update"
)
changes_summary.append(f" SKIP {pkg_name} (git URL)")
continue
@ -1862,7 +1875,9 @@ def _run_fix(critical_pkgs: set[str], entries: list[dict], max_search: int) -> N
shutil.rmtree(dl_dir, ignore_errors = True)
if not current_ver:
print(f" [WARN] Cannot determine current version of {pkg_name}, skipping fix")
print(
f" [WARN] Cannot determine current version of {pkg_name}, skipping fix"
)
changes_summary.append(f" SKIP {pkg_name} (version unknown)")
continue
@ -1879,7 +1894,9 @@ def _run_fix(critical_pkgs: set[str], entries: list[dict], max_search: int) -> N
continue
print(f" [OK] {pkg_name}: {current_ver} -> {safe_ver}")
changes_summary.append(f" FIX {pkg_name}=={current_ver} -> {pkg_name}=={safe_ver}")
changes_summary.append(
f" FIX {pkg_name}=={current_ver} -> {pkg_name}=={safe_ver}"
)
# Update all occurrences in requirements files
file_updates: dict[str, dict[int, str]] = {}
@ -1926,7 +1943,9 @@ def _find_requirements_files(root: str) -> list[str]:
dirnames[:] = [
d
for d in dirnames
if not d.startswith(".") and d not in skip_dirs and not d.endswith(".egg-info")
if not d.startswith(".")
and d not in skip_dirs
and not d.endswith(".egg-info")
]
dirname = os.path.basename(dirpath)
for fname in sorted(filenames):
@ -1998,7 +2017,9 @@ def main() -> int:
print(f" {f}")
req_files.extend(found)
else:
print(f" [WARN] No requirements files found in {scan_dir}/", file = sys.stderr)
print(
f" [WARN] No requirements files found in {scan_dir}/", file = sys.stderr
)
# Build unified entry list: list of dicts with source tracking
entries: list[dict] = []

View file

@ -42,7 +42,9 @@ def _atomic_write_text(
REPO_ROOT = Path(__file__).resolve().parents[1]
BUILD_INFO_PATH = REPO_ROOT / "studio" / "backend" / "utils" / "_studio_release_build.py"
BUILD_INFO_PATH = (
REPO_ROOT / "studio" / "backend" / "utils" / "_studio_release_build.py"
)
BUILD_INFO_SUFFIX = "studio/backend/utils/_studio_release_build.py"
VERSION_RE = re.compile(r"^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.][0-9A-Za-z.-]*)?$")
GIT_DESCRIBE_SUFFIX_RE = re.compile(r"-\d+-g[0-9A-Fa-f]+(?:-dirty)?$")

View file

@ -74,7 +74,9 @@ def desired_key(name: str, versions: list[str]) -> str:
return f"{name}@{' || '.join(versions)}"
def compute_renames(policy: dict, lock_versions: dict[str, list[str]]) -> dict[str, str]:
def compute_renames(
policy: dict, lock_versions: dict[str, list[str]]
) -> dict[str, str]:
renames: dict[str, str] = {}
for key in policy:
name, rng = split_spec(key)
@ -93,7 +95,9 @@ def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description = __doc__)
mode = ap.add_mutually_exclusive_group(required = True)
mode.add_argument("--check", action = "store_true", help = "exit 1 if pins are stale")
mode.add_argument("--fix", action = "store_true", help = "rewrite package.json in place")
mode.add_argument(
"--fix", action = "store_true", help = "rewrite package.json in place"
)
ap.add_argument(
"--dir",
type = Path,
@ -105,20 +109,26 @@ def main(argv: list[str] | None = None) -> int:
pkg_path = args.dir / "package.json"
lock_path = args.dir / "package-lock.json"
if not pkg_path.exists() or not lock_path.exists():
print(f"sync-allow-scripts: nothing to do ({args.dir} has no package.json + lockfile)")
print(
f"sync-allow-scripts: nothing to do ({args.dir} has no package.json + lockfile)"
)
return 0
pkg = json.loads(pkg_path.read_text(encoding = "utf-8"))
policy = pkg.get("allowScripts")
if not isinstance(policy, dict) or not policy:
print("sync-allow-scripts: no allowScripts policy in package.json, nothing to do")
print(
"sync-allow-scripts: no allowScripts policy in package.json, nothing to do"
)
return 0
lock = json.loads(lock_path.read_text(encoding = "utf-8"))
renames = compute_renames(policy, script_versions_from_lock(lock))
if not renames:
print(f"sync-allow-scripts: {len(policy)} allowScripts entries in sync with the lockfile")
print(
f"sync-allow-scripts: {len(policy)} allowScripts entries in sync with the lockfile"
)
return 0
for old, new in renames.items():
@ -132,7 +142,9 @@ def main(argv: list[str] | None = None) -> int:
return 1
pkg["allowScripts"] = {renames.get(k, k): v for k, v in policy.items()}
pkg_path.write_text(json.dumps(pkg, indent = 2, ensure_ascii = False) + "\n", encoding = "utf-8")
pkg_path.write_text(
json.dumps(pkg, indent = 2, ensure_ascii = False) + "\n", encoding = "utf-8"
)
print(
f"sync-allow-scripts: re-pinned {len(renames)} entr{'y' if len(renames) == 1 else 'ies'} in {pkg_path}"
)

View file

@ -131,7 +131,8 @@ def _walk_yaml_diff(
"""Print a path-keyed summary of the first structural / scalar diff."""
if type(b) is not type(a):
print(
f" type-diff at {prefix or '/'}: " f"{type(b).__name__} -> {type(a).__name__}",
f" type-diff at {prefix or '/'}: "
f"{type(b).__name__} -> {type(a).__name__}",
)
return
if isinstance(b, dict):

View file

@ -161,7 +161,9 @@ class _Builder(ast.NodeVisitor):
def _visit_stmt(self, node: ast.AST, scope: Scope) -> None:
if isinstance(node, (ast.Import, ast.ImportFrom)):
star = isinstance(node, ast.ImportFrom) and any(a.name == "*" for a in node.names)
star = isinstance(node, ast.ImportFrom) and any(
a.name == "*" for a in node.names
)
if star:
scope.star_import = True
for alias in node.names:
@ -349,7 +351,9 @@ class _Builder(ast.NodeVisitor):
self._bind_args(node.args, child)
self._visit_expr(node.body, child)
return
if isinstance(node, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)):
if isinstance(
node, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)
):
child = Scope("comp", f"{scope.qualname}.<comp>", scope)
for i, gen in enumerate(node.generators):
# first iterable evaluates in the enclosing scope
@ -606,7 +610,9 @@ def compare(before_src: str, after_src: str, path: str) -> list[tuple[str, str]]
for scope, names in b["ambiguous"].items():
new = names - a["ambiguous"].get(scope, set())
for n in sorted(new):
findings.append(("WARN", f"{path}: AMBIGUOUS-BIND '{n}' import+non-import in {scope}"))
findings.append(
("WARN", f"{path}: AMBIGUOUS-BIND '{n}' import+non-import in {scope}")
)
# 6. TARGET-MISSING (informational): a scope stopped resolving to an import
# target. Real bugs are covered above; remaining cases are relocated code.
@ -618,7 +624,9 @@ def compare(before_src: str, after_src: str, path: str) -> list[tuple[str, str]]
if t in added_module_targets
else " [target not re-added here -> likely relocated/deleted]"
)
findings.append(("INFO", f"{path}: TARGET-MISSING {t} in scope {scope}{relocated}"))
findings.append(
("INFO", f"{path}: TARGET-MISSING {t} in scope {scope}{relocated}")
)
return findings
@ -763,7 +771,9 @@ def audit_files(paths: list[str]) -> int:
ok = n_err == 0 and n_fp == 0
print(
"\nAUDIT:",
"ROBUST (no crashes, no false positives vs pyflakes)" if ok else "NEEDS WORK (see above)",
"ROBUST (no crashes, no false positives vs pyflakes)"
if ok
else "NEEDS WORK (see above)",
)
return 0 if ok else 1
@ -799,12 +809,18 @@ def main() -> int:
blockers = [f for f in findings if f[0] == "BLOCKER"]
warns = [f for f in findings if f[0] == "WARN"]
infos = [f for f in findings if f[0] == "INFO"]
status = "CLEAN" if not blockers and not warns else ("BLOCKERS" if blockers else "WARNINGS")
status = (
"CLEAN"
if not blockers and not warns
else ("BLOCKERS" if blockers else "WARNINGS")
)
print(f"\n=== {path}: {status} ===")
for sev, m in blockers + warns + infos:
print(f" [{sev}] {m}")
any_blocker = any_blocker or bool(blockers)
print("\nOVERALL:", "FAIL (blockers found)" if any_blocker else "PASS (no blockers)")
print(
"\nOVERALL:", "FAIL (blockers found)" if any_blocker else "PASS (no blockers)"
)
return 1 if any_blocker else 0

View file

@ -108,7 +108,9 @@ def create_refresh_token(subject: str, *, desktop: bool = False) -> str:
return token
def refresh_access_token(refresh_token: str) -> Tuple[Optional[str], Optional[str], bool]:
def refresh_access_token(
refresh_token: str,
) -> Tuple[Optional[str], Optional[str], bool]:
"""
Validate a refresh token and issue a new access token.
@ -135,7 +137,9 @@ def reload_secret() -> None:
load_jwt_secret()
async def get_current_subject(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
async def get_current_subject(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> str:
"""Validate JWT and require the password-change flow to be completed."""
return await _get_current_subject(
credentials,

View file

@ -138,9 +138,13 @@ def get_connection() -> sqlite3.Connection:
);
"""
)
api_key_columns = {row["name"] for row in conn.execute("PRAGMA table_info(api_keys)")}
api_key_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(api_keys)")
}
if "is_internal" not in api_key_columns:
conn.execute("ALTER TABLE api_keys ADD COLUMN is_internal INTEGER NOT NULL DEFAULT 0")
conn.execute(
"ALTER TABLE api_keys ADD COLUMN is_internal INTEGER NOT NULL DEFAULT 0"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS app_secrets (
@ -154,9 +158,13 @@ def get_connection() -> sqlite3.Connection:
conn.execute(
"ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0"
)
refresh_columns = {row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)")}
refresh_columns = {
row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)")
}
if "is_desktop" not in refresh_columns:
conn.execute("ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0")
conn.execute(
"ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0"
)
conn.commit()
return conn

View file

@ -77,7 +77,9 @@ def _asset_name() -> Optional[Tuple[str, bool]]:
def _cache_path() -> Optional[Path]:
"""studio_bin_root()/cloudflared(.exe), or None if the studio home is unresolvable."""
try:
from utils.paths.storage_roots import studio_bin_root # lazy: backend-only import
from utils.paths.storage_roots import (
studio_bin_root,
) # lazy: backend-only import
except Exception:
return None
name = "cloudflared.exe" if sys.platform == "win32" else "cloudflared"
@ -262,7 +264,9 @@ class CloudflareTunnel:
if self.url is None:
self.error = "cloudflared exited before emitting a tunnel URL"
elif not self.ready:
self.error = "cloudflared exited before the tunnel connection registered"
self.error = (
"cloudflared exited before the tunnel connection registered"
)
self._url_event.set()
self._ready_event.set()

View file

@ -41,7 +41,12 @@ def get_colab_url(port: int = 8888) -> str:
try:
url = eval_js(f"google.colab.kernel.proxyPort({port})", timeout_sec = 10)
# Valid proxy URL is https:// and embeds the port.
if url and isinstance(url, str) and url.startswith("https://") and str(port) in url:
if (
url
and isinstance(url, str)
and url.startswith("https://")
and str(port) in url
):
return url.rstrip("/")
except Exception as e:
logger.info(f"Note: Could not get Colab URL (attempt {attempt + 1}/3: {e})")
@ -107,7 +112,9 @@ def _is_studio_healthy(port: int, timeout: float = 2.0) -> bool:
"""Return True if a Studio backend is already answering health checks on *port*."""
import urllib.request
try:
with urllib.request.urlopen(f"http://localhost:{port}/api/health", timeout = timeout):
with urllib.request.urlopen(
f"http://localhost:{port}/api/health", timeout = timeout
):
return True
except Exception:
return False
@ -179,7 +186,9 @@ def start(port: int = 8888):
# Fast path: Studio already running (cell re-run). Re-launching would collide on
# the port, so just re-show the link and iframe.
if _is_studio_healthy(port):
logger.info(f" Studio is already running on port {port} — reusing existing server.")
logger.info(
f" Studio is already running on port {port} — reusing existing server."
)
_show_and_embed(port)
try:
for _ in range(10000):
@ -202,7 +211,9 @@ def start(port: int = 8888):
logger.info(" Starting server...")
try:
app = run_server(host = "0.0.0.0", port = port, frontend_path = frontend_path, silent = True)
app = run_server(
host = "0.0.0.0", port = port, frontend_path = frontend_path, silent = True
)
except SystemExit as exc:
logger.error(f"❌ Unsloth Studio failed to start: {exc}")
return
@ -223,7 +234,9 @@ def start(port: int = 8888):
server_ready = False
for _ in range(40):
try:
with urllib.request.urlopen(f"http://localhost:{actual_port}/api/health", timeout = 1):
with urllib.request.urlopen(
f"http://localhost:{actual_port}/api/health", timeout = 1
):
server_ready = True
break
except Exception:

View file

@ -36,7 +36,9 @@ def _resolve_recipe_artifact_path(artifact_path: str) -> Path:
if not resolved.exists():
raise RecipeDatasetPublishError("Execution artifacts are no longer available.")
if not resolved.is_dir():
raise RecipeDatasetPublishError("Execution artifact path is not a dataset folder.")
raise RecipeDatasetPublishError(
"Execution artifact path is not a dataset folder."
)
return resolved

View file

@ -108,7 +108,9 @@ class Subscription:
event_id = self._next_id
body = json.dumps(event, separators = (",", ":"), ensure_ascii = False)
event_type = event.get("type") or "message"
return (f"id: {event_id}\n" f"event: {event_type}\n" f"data: {body}\n\n").encode("utf-8")
return (
f"id: {event_id}\n" f"event: {event_type}\n" f"data: {body}\n\n"
).encode("utf-8")
class JobManager:
@ -155,7 +157,9 @@ class JobManager:
job_id = uuid.uuid4().hex
self._job = Job(job_id = job_id, status = "pending", started_at = time.time())
self._job.progress_columns_total = llm_column_count
self._job.source_progress_estimated_total = _github_source_estimated_total(recipe)
self._job.source_progress_estimated_total = _github_source_estimated_total(
recipe
)
self._job.internal_api_key_id = internal_api_key_id
self._events.clear()
self._seq = 0
@ -182,7 +186,9 @@ class JobManager:
self._pump_thread = threading.Thread(target = self._pump_loop, daemon = True)
self._pump_thread.start()
self._emit({"type": EVENT_JOB_ENQUEUED, "ts": time.time(), "job_id": job_id})
self._emit(
{"type": EVENT_JOB_ENQUEUED, "ts": time.time(), "job_id": job_id}
)
return job_id
def cancel(self, job_id: str) -> bool:
@ -193,7 +199,9 @@ class JobManager:
if self._proc is None or not self._proc.is_alive():
return True
self._job.status = "cancelling"
self._emit({"type": EVENT_JOB_CANCELLING, "ts": time.time(), "job_id": job_id})
self._emit(
{"type": EVENT_JOB_CANCELLING, "ts": time.time(), "job_id": job_id}
)
try:
self._proc.terminate()
except (AttributeError, OSError):
@ -310,12 +318,16 @@ class JobManager:
if not parquet_dir.exists():
return {"error": f"dataset path missing: {parquet_dir}"}
return self._load_dataset_page(parquet_dir = parquet_dir, limit = limit, offset = offset)
return self._load_dataset_page(
parquet_dir = parquet_dir, limit = limit, offset = offset
)
except Exception as exc:
return {"error": f"dataset load failed: {exc}"}
@staticmethod
def _load_dataset_page(*, parquet_dir: Path, limit: int, offset: int) -> dict[str, Any]:
def _load_dataset_page(
*, parquet_dir: Path, limit: int, offset: int
) -> dict[str, Any]:
dataset_page = JobManager._load_dataset_page_with_duckdb(
parquet_dir = parquet_dir,
limit = limit,
@ -478,7 +490,9 @@ class JobManager:
self._job.error = self._job.error or "process exited"
self._job.finished_at = time.time()
event_type = (
EVENT_JOB_CANCELLED if self._job.status == "cancelled" else EVENT_JOB_ERROR
EVENT_JOB_CANCELLED
if self._job.status == "cancelled"
else EVENT_JOB_ERROR
)
self._emit(
{

View file

@ -119,7 +119,8 @@ def parse_log_message(msg: str) -> ParsedUpdate | None:
page_items = page_items,
rate_remaining = int(m.group("remaining")),
message = (
f"Scraping GitHub source: {repo} " f"{resource} page {page} (+{page_items})"
f"Scraping GitHub source: {repo} "
f"{resource} page {page} (+{page_items})"
),
),
)
@ -133,7 +134,9 @@ def parse_log_message(msg: str) -> ParsedUpdate | None:
source = "github",
status = "rate_limited",
retry_after_sec = seconds,
message = ("Waiting for GitHub rate limit. Studio will resume automatically."),
message = (
"Waiting for GitHub rate limit. Studio will resume automatically."
),
),
)
@ -161,7 +164,9 @@ def parse_log_message(msg: str) -> ParsedUpdate | None:
source = "github",
status = "rate_limited",
retry_after_sec = seconds,
message = ("Waiting for GitHub rate limit. Studio will resume automatically."),
message = (
"Waiting for GitHub rate limit. Studio will resume automatically."
),
),
)
@ -379,13 +384,15 @@ def _apply_source_progress(job: Job, progress: SourceProgress) -> None:
count_key = f"{progress.repo}:{progress.resource}"
if page_key not in job._source_seen_pages:
job._source_seen_pages.add(page_key)
job._source_counts[count_key] = int(job._source_counts.get(count_key, 0)) + int(
page_items or 0
)
job._source_counts[count_key] = int(
job._source_counts.get(count_key, 0)
) + int(page_items or 0)
fetched_items = sum(job._source_counts.values())
if fetched_items <= 0:
fetched_items = progress.fetched_items or (previous.fetched_items if previous else None)
fetched_items = progress.fetched_items or (
previous.fetched_items if previous else None
)
estimated_total = (
progress.estimated_total
@ -405,10 +412,14 @@ def _apply_source_progress(job: Job, progress: SourceProgress) -> None:
repo = progress.repo or (previous.repo if previous else None),
resource = progress.resource or (previous.resource if previous else None),
page = (
progress.page if progress.page is not None else (previous.page if previous else None)
progress.page
if progress.page is not None
else (previous.page if previous else None)
),
page_items = (
page_items if page_items is not None else (previous.page_items if previous else None)
page_items
if page_items is not None
else (previous.page_items if previous else None)
),
fetched_items = fetched_items,
estimated_total = estimated_total,
@ -439,7 +450,9 @@ def _compute_overall_progress(job: Job, column_progress: Progress) -> Progress:
if len(job._column_done) == 0:
done = current_done
else:
sum_done = sum(max(0, min(value, total_rows)) for value in job._column_done.values())
sum_done = sum(
max(0, min(value, total_rows)) for value in job._column_done.values()
)
done = int(sum_done / total_columns)
prev_done = int(job.progress.done or 0)

View file

@ -60,7 +60,9 @@ def _slugify_run_name(value: str) -> str:
return slug[:80].strip("-")
def _build_dataset_name(*, run_name: str | None, job_id: str, artifact_root: Path) -> str:
def _build_dataset_name(
*, run_name: str | None, job_id: str, artifact_root: Path
) -> str:
fallback = f"recipe_{job_id}"
slug = _slugify_run_name(run_name or "")
base_name = f"recipe_{slug}" if slug else fallback
@ -72,7 +74,9 @@ def _build_dataset_name(*, run_name: str | None, job_id: str, artifact_root: Pat
return candidate
def run_job_process(*, event_queue, recipe: dict[str, Any], run: dict[str, Any]) -> None:
def run_job_process(
*, event_queue, recipe: dict[str, Any], run: dict[str, Any]
) -> None:
"""Subprocess entrypoint. Sends events to `event_queue`."""
import os
@ -160,10 +164,14 @@ def run_job_process(*, event_queue, recipe: dict[str, Any], run: dict[str, Any])
}
)
else:
results = designer.create(builder, num_records = rows, dataset_name = dataset_name)
results = designer.create(
builder, num_records = rows, dataset_name = dataset_name
)
analysis = to_jsonable(results.load_analysis().model_dump(mode = "json"))
if merge_batches:
_merge_batches_to_single_parquet(results.artifact_storage.base_dataset_path)
_merge_batches_to_single_parquet(
results.artifact_storage.base_dataset_path
)
artifact_path = str(results.artifact_storage.base_dataset_path)
event_queue.put(
{

View file

@ -133,7 +133,11 @@ def _parse_oxc_spec(*, column: dict[str, Any]) -> OxcLocalCallableValidatorSpec
target_columns_raw = column.get("target_columns")
target_columns = (
[value.strip() for value in target_columns_raw if isinstance(value, str) and value.strip()]
[
value.strip()
for value in target_columns_raw
if isinstance(value, str) and value.strip()
]
if isinstance(target_columns_raw, list)
else []
)
@ -173,7 +177,9 @@ def _parse_oxc_validation_marker(fn_name: str) -> tuple[str, str, str]:
return "javascript", "syntax", "auto"
code_lang = parts[0] if parts[0] in _OXC_LANG_TO_NODE_LANG else "javascript"
mode = parts[1] if parts[1] in _OXC_VALIDATION_MODES else "syntax"
code_shape = parts[2] if len(parts) >= 3 and parts[2] in _OXC_CODE_SHAPES else "auto"
code_shape = (
parts[2] if len(parts) >= 3 and parts[2] in _OXC_CODE_SHAPES else "auto"
)
return code_lang, mode, code_shape
@ -194,7 +200,10 @@ def _build_oxc_validation_function(lang: str, validation_mode: str, code_shape:
code_values = (
["" for _ in range(row_count)]
if not code_column
else ["" if value is None else str(value) for value in df[code_column].tolist()]
else [
"" if value is None else str(value)
for value in df[code_column].tolist()
]
)
results = _run_oxc_batch(
@ -210,9 +219,7 @@ def _build_oxc_validation_function(lang: str, validation_mode: str, code_shape:
)
return pd.DataFrame(results)
_validator.__name__ = (
f"{OXC_VALIDATION_FN_MARKER}_{node_lang}_{mode.replace('+', '_')}_{normalized_code_shape}"
)
_validator.__name__ = f"{OXC_VALIDATION_FN_MARKER}_{node_lang}_{mode.replace('+', '_')}_{normalized_code_shape}"
return _validator
@ -292,13 +299,21 @@ def _run_oxc_batch(
warning_count_raw = item.get("warning_count")
out.append(
{
"is_valid": bool(is_valid_raw) if isinstance(is_valid_raw, bool) else False,
"error_count": int(error_count_raw) if isinstance(error_count_raw, int) else 0,
"is_valid": bool(is_valid_raw)
if isinstance(is_valid_raw, bool)
else False,
"error_count": int(error_count_raw)
if isinstance(error_count_raw, int)
else 0,
"error_message": str(message_raw or ""),
"severity": str(severity_raw) if isinstance(severity_raw, str) else None,
"severity": str(severity_raw)
if isinstance(severity_raw, str)
else None,
"code": str(code_raw) if isinstance(code_raw, str) else None,
"labels": labels_raw if isinstance(labels_raw, list) else [],
"codeframe": str(codeframe_raw) if isinstance(codeframe_raw, str) else None,
"codeframe": str(codeframe_raw)
if isinstance(codeframe_raw, str)
else None,
"warning_count": int(warning_count_raw)
if isinstance(warning_count_raw, int)
else 0,

View file

@ -22,7 +22,9 @@ def _encode_bytes_to_base64(value: bytes | bytearray) -> str:
return base64.b64encode(bytes(value)).decode("utf-8")
def _load_image_file_to_base64(path_value: str, *, base_path: str | None = None) -> str | None:
def _load_image_file_to_base64(
path_value: str, *, base_path: str | None = None
) -> str | None:
try:
path = Path(path_value)
candidates: list[Path] = []
@ -117,7 +119,9 @@ def _apply_data_designer_image_context_patch() -> None:
original_auto_resolve = ImageContext._auto_resolve_context_value
def _patched_auto_resolve(self: Any, context_value: Any, base_path: str | None) -> Any:
def _patched_auto_resolve(
self: Any, context_value: Any, base_path: str | None
) -> Any:
normalized = _normalize_image_context_value(context_value, base_path = base_path)
return original_auto_resolve(self, normalized, base_path)
@ -159,7 +163,9 @@ def _recipe_has_llm_columns(recipe: dict[str, Any]) -> bool:
return False
def _validate_recipe_runtime_support(recipe: dict[str, Any], model_providers: list[Any]) -> None:
def _validate_recipe_runtime_support(
recipe: dict[str, Any], model_providers: list[Any]
) -> None:
if _recipe_has_llm_columns(recipe) and not model_providers:
raise ValueError("Add a Provider connection block before running this recipe.")
@ -249,7 +255,9 @@ def build_config_builder(recipe: dict[str, Any]):
if key not in {"model_providers", "mcp_providers"}
}
recipe_core = _strip_frontend_model_config_metadata(recipe_core)
recipe_core, oxc_local_callable_specs = split_oxc_local_callable_validators(recipe_core)
recipe_core, oxc_local_callable_specs = split_oxc_local_callable_validators(
recipe_core
)
builder = DataDesignerConfigBuilder.from_config({"data_designer": recipe_core})
register_oxc_local_callable_validators(
builder = builder,
@ -320,10 +328,14 @@ def preview_recipe(
dataset = [to_jsonable(row) for row in raw_rows]
artifacts = (
None if results.processor_artifacts is None else to_jsonable(results.processor_artifacts)
None
if results.processor_artifacts is None
else to_jsonable(results.processor_artifacts)
)
analysis = (
None if results.analysis is None else to_jsonable(results.analysis.model_dump(mode = "json"))
None
if results.analysis is None
else to_jsonable(results.analysis.model_dump(mode = "json"))
)
return dataset, artifacts, analysis

View file

@ -58,7 +58,9 @@ def _apply_wsl_sudo_patch():
import unsloth_zoo.llama_cpp as llama_cpp_module
def _wsl_do_we_need_sudo(system_type = "debian"):
logger.info("WSL detected — skipping sudo check (build deps pre-installed by setup.sh)")
logger.info(
"WSL detected — skipping sudo check (build deps pre-installed by setup.sh)"
)
return False
llama_cpp_module.do_we_need_sudo = _wsl_do_we_need_sudo
@ -331,7 +333,9 @@ class ExportBackend:
output_path: Optional[str] = None
try:
if _IS_MLX:
mlx_save_method = "merged_4bit" if format_type == "4-bit (FP4)" else "merged_16bit"
mlx_save_method = (
"merged_4bit" if format_type == "4-bit (FP4)" else "merged_16bit"
)
else:
if format_type == "4-bit (FP4)":
save_method = "merged_4bit_forced"
@ -394,7 +398,9 @@ class ExportBackend:
private = private,
)
else:
hub_save_method = save_method if save_method is not None else "merged_16bit"
hub_save_method = (
save_method if save_method is not None else "merged_16bit"
)
self.current_model.push_to_hub_merged(
repo_id,
self.current_tokenizer,
@ -498,7 +504,9 @@ class ExportBackend:
else:
# Base model name from request or model config
base_model = (
base_model_id or self.current_model.config._name_or_path or "unknown"
base_model_id
or self.current_model.config._name_or_path
or "unknown"
)
hf_api = HfApi(token = hf_token)
@ -518,7 +526,9 @@ class ExportBackend:
extra = "unsloth",
)
card = ModelCard(content)
card.push_to_hub(repo_id, token = hf_token, commit_message = "Unsloth Model Card")
card.push_to_hub(
repo_id, token = hf_token, commit_message = "Unsloth Model Card"
)
if save_directory:
hf_api.upload_folder(
@ -581,7 +591,9 @@ class ExportBackend:
LLAMA_CPP_DEFAULT_DIR,
_resolve_local_convert_script, # noqa: F401
)
os.environ.setdefault("UNSLOTH_LLAMA_CPP_SCRIPTS_DIR", LLAMA_CPP_DEFAULT_DIR)
os.environ.setdefault(
"UNSLOTH_LLAMA_CPP_SCRIPTS_DIR", LLAMA_CPP_DEFAULT_DIR
)
except ImportError:
if not _LLAMA_CPP_SCRIPTS_WARNING_EMITTED:
logger.warning(
@ -609,12 +621,16 @@ class ExportBackend:
cwd = os.getcwd()
pre_existing_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf")))
pre_existing_subs = {d.name for d in Path(abs_save_dir).iterdir() if d.is_dir()}
pre_existing_subs = {
d.name for d in Path(abs_save_dir).iterdir() if d.is_dir()
}
# Avoid clobbering an existing user-owned model/ directory.
import uuid
_model_tmp = os.path.join(abs_save_dir, f"_tmp_model_{uuid.uuid4().hex[:8]}")
_model_tmp = os.path.join(
abs_save_dir, f"_tmp_model_{uuid.uuid4().hex[:8]}"
)
model_tmp_to_cleanup = _model_tmp
self.current_model.save_pretrained_gguf(
_model_tmp,
@ -623,11 +639,15 @@ class ExportBackend:
)
# Relocate the .gguf that convert_to_gguf wrote to cwd (repo root).
new_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf"))) - pre_existing_ggufs
new_ggufs = (
set(glob.glob(os.path.join(cwd, "*.gguf"))) - pre_existing_ggufs
)
for src in sorted(new_ggufs):
dest = os.path.join(abs_save_dir, os.path.basename(src))
shutil.move(src, dest)
logger.info(f"Relocated GGUF: {os.path.basename(src)}{abs_save_dir}/")
logger.info(
f"Relocated GGUF: {os.path.basename(src)}{abs_save_dir}/"
)
# Flatten GGUF files from subdirs created during this export.
for sub in list(Path(abs_save_dir).iterdir()):
@ -647,7 +667,10 @@ class ExportBackend:
if self.current_checkpoint:
ckpt = Path(self.current_checkpoint)
gguf_dir = ckpt.parent / f"{ckpt.name}_gguf"
if gguf_dir.is_dir() and gguf_dir.resolve() != Path(abs_save_dir).resolve():
if (
gguf_dir.is_dir()
and gguf_dir.resolve() != Path(abs_save_dir).resolve()
):
for src in gguf_dir.glob("*.gguf"):
dest = os.path.join(abs_save_dir, src.name)
shutil.move(str(src), dest)
@ -655,7 +678,9 @@ class ExportBackend:
# Also relocate Ollama Modelfile if present
modelfile = gguf_dir / "Modelfile"
if modelfile.is_file():
shutil.move(str(modelfile), os.path.join(abs_save_dir, "Modelfile"))
shutil.move(
str(modelfile), os.path.join(abs_save_dir, "Modelfile")
)
logger.info(f"Relocated Modelfile → {abs_save_dir}/")
shutil.rmtree(str(gguf_dir), ignore_errors = True)
logger.info(f"Cleaned up intermediate GGUF dir: {gguf_dir}")
@ -763,8 +788,12 @@ class ExportBackend:
repo_type = "model",
)
else:
self.current_model.push_to_hub(repo_id, token = hf_token, private = private)
self.current_tokenizer.push_to_hub(repo_id, token = hf_token, private = private)
self.current_model.push_to_hub(
repo_id, token = hf_token, private = private
)
self.current_tokenizer.push_to_hub(
repo_id, token = hf_token, private = private
)
logger.info(f"Adapter pushed successfully to {repo_id}")
return True, "LoRA adapter exported successfully", output_path

View file

@ -276,7 +276,9 @@ class ExportOrchestrator:
expected_type,
)
raise RuntimeError(f"Timeout waiting for '{expected_type}' response after {timeout}s")
raise RuntimeError(
f"Timeout waiting for '{expected_type}' response after {timeout}s"
)
def _drain_queue(self) -> list:
"""Drain all pending responses."""
@ -326,7 +328,9 @@ class ExportOrchestrator:
elif self._proc is not None:
self._shutdown_subprocess(timeout = 2)
logger.info("Spawning fresh export subprocess for '%s'", checkpoint_path)
logger.info(
"Spawning fresh export subprocess for '%s'", checkpoint_path
)
self._spawn_subprocess(sub_config)
try:
@ -438,7 +442,9 @@ class ExportOrchestrator:
},
)
def _run_export(self, export_type: str, params: dict) -> Tuple[bool, str, Optional[str]]:
def _run_export(
self, export_type: str, params: dict
) -> Tuple[bool, str, Optional[str]]:
"""Send an export command and wait for the result.
Returns ``(success, message, output_path)``. ``output_path`` is the on-disk
@ -500,7 +506,9 @@ class ExportOrchestrator:
finally:
self._export_active = False
def scan_checkpoints(self, outputs_dir: str = str(outputs_root())) -> List[Tuple[str, list]]:
def scan_checkpoints(
self, outputs_dir: str = str(outputs_root())
) -> List[Tuple[str, list]]:
"""Scan for checkpoints — runs locally, no ML imports."""
from utils.models.checkpoints import scan_checkpoints
return scan_checkpoints(outputs_dir = outputs_dir)

View file

@ -428,7 +428,9 @@ def run_export_process(*, cmd_queue: Any, resp_queue: Any, config: dict) -> None
import transformers
logger.info("Export subprocess loaded transformers %s", transformers.__version__)
logger.info(
"Export subprocess loaded transformers %s", transformers.__version__
)
except Exception as exc:
_send_response(
@ -529,7 +531,9 @@ def run_export_process(*, cmd_queue: Any, resp_queue: Any, config: dict) -> None
)
except Exception as exc:
logger.error("Error handling command '%s': %s", cmd_type, exc, exc_info = True)
logger.error(
"Error handling command '%s': %s", cmd_type, exc, exc_info = True
)
_send_response(
resp_queue,
{

View file

@ -36,7 +36,11 @@ def anthropic_tool_use_id(upstream_id = None) -> str:
"""Return an Anthropic-style tool_use id (prefix 'toolu_'). Reuses an
upstream id only if it already starts with 'toolu_'; otherwise mints a fresh
'toolu_<24 hex>'."""
if upstream_id and isinstance(upstream_id, str) and upstream_id.startswith("toolu_"):
if (
upstream_id
and isinstance(upstream_id, str)
and upstream_id.startswith("toolu_")
):
return upstream_id
return f"toolu_{uuid.uuid4().hex[:24]}"
@ -149,7 +153,9 @@ def anthropic_messages_to_openai(
tc = b.get("content", "")
if isinstance(tc, list):
tc = " ".join(
p["text"] for p in tc if isinstance(p, dict) and p.get("type") == "text"
p["text"]
for p in tc
if isinstance(p, dict) and p.get("type") == "text"
)
tool_results.append(
{
@ -432,7 +438,9 @@ class AnthropicStreamEmitter:
events.append(self._close_block())
# Reuse the id published in content_block_start; fall back to mapping
# the raw id only if no tool_start preceded this end.
tool_use_id = self._open_tool_use_id or anthropic_tool_use_id(event.get("tool_call_id", ""))
tool_use_id = self._open_tool_use_id or anthropic_tool_use_id(
event.get("tool_call_id", "")
)
self._open_tool_call_id = None
self._open_tool_use_id = None
self._open_tool_args_sent = False

View file

@ -77,7 +77,9 @@ class AudioCodecManager:
return
from snac import SNAC
self._snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device).eval()
self._snac_model = (
SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device).eval()
)
logger.info("Loaded SNAC codec (24kHz)")
def _load_bicodec(
@ -92,7 +94,9 @@ class AudioCodecManager:
# Clone SparkAudio/Spark-TTS for the sparktts package (HF model repos
# don't contain it)
spark_code_dir = os.path.join(os.path.dirname(model_repo_path or "."), "Spark-TTS")
spark_code_dir = os.path.join(
os.path.dirname(model_repo_path or "."), "Spark-TTS"
)
sparktts_pkg = os.path.join(spark_code_dir, "sparktts")
if not os.path.isdir(sparktts_pkg):
logger.info(f"Cloning SparkAudio/Spark-TTS to {spark_code_dir}...")
@ -175,7 +179,9 @@ class AudioCodecManager:
# ── Decoders ─────────────────────────────────────────────────
def decode_snac(self, generated_ids: torch.Tensor, device: str) -> Tuple[bytes, int]:
def decode_snac(
self, generated_ids: torch.Tensor, device: str
) -> Tuple[bytes, int]:
"""Decode SNAC tokens (Orpheus) into WAV bytes.
Finds the START_OF_SPEECH (128257) marker, extracts codes after it,
@ -188,7 +194,9 @@ class AudioCodecManager:
cropped = generated_ids[:, token_indices[1][-1] + 1 :]
else:
# Fall back to the entire output if the marker is missing
logger.warning("No START_OF_SPEECH token (128257) found — using full generated output")
logger.warning(
"No START_OF_SPEECH token (128257) found — using full generated output"
)
cropped = generated_ids
row = cropped[0]
@ -214,7 +222,8 @@ class AudioCodecManager:
layer_3.append(codes[7 * i + 6] - 24576)
snac_codes = [
torch.tensor(layer).unsqueeze(0).to(device) for layer in [layer_1, layer_2, layer_3]
torch.tensor(layer).unsqueeze(0).to(device)
for layer in [layer_1, layer_2, layer_3]
]
with torch.no_grad():
@ -241,12 +250,16 @@ class AudioCodecManager:
f"BiCodec decode: {len(global_matches)} global tokens, {len(semantic_matches)} semantic tokens"
)
if len(global_matches) < 10:
logger.info(f"BiCodec generated text (first 500 chars): {generated_text[:500]}")
logger.info(
f"BiCodec generated text (first 500 chars): {generated_text[:500]}"
)
if not semantic_matches:
raise ValueError("No bicodec_semantic tokens found in generated output")
semantic_ids = torch.tensor([int(t) for t in semantic_matches]).long().unsqueeze(0)
semantic_ids = (
torch.tensor([int(t) for t in semantic_matches]).long().unsqueeze(0)
)
# Speaker encoder expects exactly 32 global tokens (token_num=32);
# pad with zeros or truncate.

View file

@ -55,4 +55,6 @@ def apply_chat_template_for_generation(
break
if last_exc is not None:
raise last_exc
raise RuntimeError("apply_chat_template_for_generation: no attempt produced a result")
raise RuntimeError(
"apply_chat_template_for_generation: no attempt produced a result"
)

File diff suppressed because it is too large Load diff

View file

@ -260,7 +260,9 @@ class InferenceBackend:
if config.is_audio:
audio_type = config.audio_type
adapter_info = " (LoRA adapter)" if config.is_lora else ""
logger.info(f"Loading audio ({audio_type}) model{adapter_info}: {model_name}")
logger.info(
f"Loading audio ({audio_type}) model{adapter_info}: {model_name}"
)
log_gpu_memory(f"Before loading {model_name}")
if audio_type == "csm":
@ -294,7 +296,9 @@ class InferenceBackend:
from huggingface_hub import snapshot_download
local_dir = base_path.split("/")[-1]
repo_path = snapshot_download(base_path, local_dir = local_dir)
repo_path = snapshot_download(
base_path, local_dir = local_dir
)
abs_repo_path = os.path.abspath(repo_path)
logger.info(
@ -405,7 +409,9 @@ class InferenceBackend:
)
# Reject CPU/disk offload for audio models too
raise_if_offloaded(self.models[model_name]["model"], device_map, "Inference")
raise_if_offloaded(
self.models[model_name]["model"], device_map, "Inference"
)
self.models[model_name]["context_length"] = runtime_context_length(
self.models[model_name].get("model"),
max_seq_length,
@ -418,7 +424,9 @@ class InferenceBackend:
return True
model_type = "vision" if config.is_vision else "text"
adapter_info = " (LoRA adapter)" if self.models[model_name]["is_lora"] else ""
adapter_info = (
" (LoRA adapter)" if self.models[model_name]["is_lora"] else ""
)
logger.info(f"Loading {model_type} model{adapter_info}: {model_name}")
log_gpu_memory(f"Before loading {model_name}")
@ -442,10 +450,13 @@ class InferenceBackend:
from transformers import ProcessorMixin
if not (
isinstance(processor, ProcessorMixin) or hasattr(processor, "image_processor")
isinstance(processor, ProcessorMixin)
or hasattr(processor, "image_processor")
):
# LoRA adapters: use base model. Local merged exports: read base from export_metadata.json.
processor_source = config.base_model if config.is_lora else config.identifier
processor_source = (
config.base_model if config.is_lora else config.identifier
)
if not config.is_lora and config.is_local:
_meta_path = Path(config.path) / "export_metadata.json"
try:
@ -466,7 +477,9 @@ class InferenceBackend:
token = hf_token if hf_token and hf_token.strip() else None,
trust_remote_code = trust_remote_code,
)
logger.info(f"Loaded {type(processor).__name__} from {processor_source}")
logger.info(
f"Loaded {type(processor).__name__} from {processor_source}"
)
self.models[model_name]["model"] = model
self.models[model_name]["tokenizer"] = processor
@ -489,7 +502,9 @@ class InferenceBackend:
self.models[model_name]["model"] = model
self.models[model_name]["tokenizer"] = tokenizer
raise_if_offloaded(self.models[model_name]["model"], device_map, "Inference")
raise_if_offloaded(
self.models[model_name]["model"], device_map, "Inference"
)
self.models[model_name]["context_length"] = runtime_context_length(
self.models[model_name].get("model"),
max_seq_length,
@ -537,7 +552,11 @@ class InferenceBackend:
import sys as _sys
from utils.cache_cleanup import clear_unsloth_compiled_cache
_preserve = ["Unsloth*Trainer.py"] if _sys.platform in ("win32", "darwin") else None
_preserve = (
["Unsloth*Trainer.py"]
if _sys.platform in ("win32", "darwin")
else None
)
clear_unsloth_compiled_cache(preserve_patterns = _preserve)
logger.info(f"Model '{model_name}' successfully unloaded.")
@ -604,9 +623,13 @@ class InferenceBackend:
base_model_name = lora_config.base_model
# 1. Load the base model if not already in memory
if base_model_name not in self.models or not self.models[base_model_name].get("model"):
if base_model_name not in self.models or not self.models[
base_model_name
].get("model"):
logger.info(f"Base model '{base_model_name}' not loaded, loading now.")
base_config = ModelConfig.from_ui_selection(base_model_name, None, is_lora = False)
base_config = ModelConfig.from_ui_selection(
base_model_name, None, is_lora = False
)
if not self.load_model(
base_config,
max_seq_length,
@ -642,7 +665,9 @@ class InferenceBackend:
logger.error(traceback.format_exc())
return False, None, None
def load_adapter(self, base_model_name: str, adapter_path: str, adapter_name: str) -> bool:
def load_adapter(
self, base_model_name: str, adapter_path: str, adapter_name: str
) -> bool:
"""Load an adapter onto the model only if not already attached."""
model = self.models[base_model_name].get("model")
@ -713,12 +738,16 @@ class InferenceBackend:
)
model.base_model.disable_adapter_layers()
else:
logger.info(f"Compare mode: model '{base}' is not a PeftModel, already base")
logger.info(
f"Compare mode: model '{base}' is not a PeftModel, already base"
)
elif use_adapter is True:
# Re-enable LoRA layers -> adapter output.
if isinstance(model, (PeftModel, PeftModelForCausalLM)):
logger.info(f"Compare mode: enabling adapters on '{base}' for LoRA generation")
logger.info(
f"Compare mode: enabling adapters on '{base}' for LoRA generation"
)
model.base_model.enable_adapter_layers()
else:
logger.warning("use_adapter=true but model is not a PeftModel")
@ -726,11 +755,15 @@ class InferenceBackend:
elif isinstance(use_adapter, str):
# Enable adapters and set the named one active.
if isinstance(model, (PeftModel, PeftModelForCausalLM)):
logger.info(f"Compare mode: enabling adapter '{use_adapter}' on '{base}'")
logger.info(
f"Compare mode: enabling adapter '{use_adapter}' on '{base}'"
)
model.base_model.enable_adapter_layers()
self.set_active_adapter(base, use_adapter)
else:
logger.warning(f"use_adapter='{use_adapter}' but model is not a PeftModel")
logger.warning(
f"use_adapter='{use_adapter}' but model is not a PeftModel"
)
def generate_with_adapter_control(
self,
@ -902,7 +935,8 @@ class InferenceBackend:
processor = model_info.get("processor")
has_image_processing = processor is not None and (
isinstance(processor, ProcessorMixin) or hasattr(processor, "image_processor")
isinstance(processor, ProcessorMixin)
or hasattr(processor, "image_processor")
)
if has_image_processing:
yield from self._generate_vision_response(
@ -954,7 +988,9 @@ class InferenceBackend:
# Step 2: format with tokenizer.apply_chat_template().
if system_prompt:
template_messages = [{"role": "system", "content": system_prompt}] + messages
template_messages = [
{"role": "system", "content": system_prompt}
] + messages
else:
template_messages = messages
try:
@ -1069,7 +1105,9 @@ class InferenceBackend:
else:
# Text-only path for a vision model
formatted_prompt = self.format_chat_prompt(messages, system_prompt)
inputs = raw_tokenizer(formatted_prompt, return_tensors = "pt").to(model.device)
inputs = raw_tokenizer(formatted_prompt, return_tensors = "pt").to(
model.device
)
# Stream with TextIteratorStreamer + background thread
try:
@ -1356,7 +1394,9 @@ class InferenceBackend:
timeout = 0.2,
)
except Exception as e:
logger.warning(f"HarmonyTextStreamer init failed, falling back: {e}")
logger.warning(
f"HarmonyTextStreamer init failed, falling back: {e}"
)
streamer = TextIteratorStreamer(
tokenizer,
skip_prompt = True,
@ -1452,7 +1492,9 @@ class InferenceBackend:
cancel_event.set()
thread.join(timeout = 10)
if thread.is_alive():
logger.warning("Generation thread did not exit after cancel/join timeout")
logger.warning(
"Generation thread did not exit after cancel/join timeout"
)
if err.get("msg"):
yield f"Error: {err['msg']}"
@ -1527,12 +1569,21 @@ class InferenceBackend:
raise RuntimeError(f"Unknown audio_type: {audio_type}")
def _generate_snac(
self, model, tokenizer, text, temperature, top_p, max_new_tokens, repetition_penalty
self,
model,
tokenizer,
text,
temperature,
top_p,
max_new_tokens,
repetition_penalty,
):
"""Generate audio using SNAC codec (Orpheus)."""
device = model.device
start_token = torch.tensor([[128259]], device = device) # START_OF_HUMAN
end_tokens = torch.tensor([[128009, 128260]], device = device) # EOT, END_OF_HUMAN
end_tokens = torch.tensor(
[[128009, 128260]], device = device
) # EOT, END_OF_HUMAN
text_ids = tokenizer(text, return_tensors = "pt").input_ids.to(device)
input_ids = torch.cat([start_token, text_ids, end_tokens], dim = 1)
attention_mask = torch.ones_like(input_ids)
@ -1556,12 +1607,20 @@ class InferenceBackend:
inputs = processor(
f"[{speaker_id}]{text}", add_special_tokens = True, return_tensors = "pt"
).to(model.device)
audio_values = model.generate(**inputs, max_new_tokens = max_new_tokens, output_audio = True)
audio_values = model.generate(
**inputs, max_new_tokens = max_new_tokens, output_audio = True
)
return self._audio_codec_manager.decode_csm(audio_values)
def _generate_bicodec(self, model, tokenizer, text, temperature, top_k, max_new_tokens):
def _generate_bicodec(
self, model, tokenizer, text, temperature, top_k, max_new_tokens
):
"""Generate audio using BiCodec (Spark-TTS)."""
prompt = "<|task_tts|><|start_content|>" + text + "<|end_content|><|start_global_token|>"
prompt = (
"<|task_tts|><|start_content|>"
+ text
+ "<|end_content|><|start_global_token|>"
)
inputs = tokenizer([prompt], return_tensors = "pt").to(model.device)
generated = model.generate(
**inputs,
@ -1632,7 +1691,9 @@ class InferenceBackend:
def __init__(self, penalty: float):
self.penalty_last_n = 64
if not isinstance(penalty, float) or penalty <= 0:
raise ValueError(f"`penalty` has to be a positive float, but is {penalty}")
raise ValueError(
f"`penalty` has to be a positive float, but is {penalty}"
)
self.penalty = penalty
@torch.no_grad()
@ -1657,8 +1718,12 @@ class InferenceBackend:
)
return scores
generation_utils.RepetitionPenaltyLogitsProcessor = RepetitionPenaltyLogitsProcessorPatch
logger.info("Patched RepetitionPenaltyLogitsProcessor with 64-token window for OuteTTS")
generation_utils.RepetitionPenaltyLogitsProcessor = (
RepetitionPenaltyLogitsProcessorPatch
)
logger.info(
"Patched RepetitionPenaltyLogitsProcessor with 64-token window for OuteTTS"
)
def _apply_chat_template_for_generation(
self,
@ -1700,7 +1765,9 @@ class InferenceBackend:
logger.error("Tokenizer not loaded for active model")
return ""
chat_template_info = self.models[self.active_model_name].get("chat_template_info", {})
chat_template_info = self.models[self.active_model_name].get(
"chat_template_info", {}
)
tokenizer = self.models[self.active_model_name]["tokenizer"]
tokenizer = getattr(tokenizer, "tokenizer", tokenizer)
@ -1717,7 +1784,9 @@ class InferenceBackend:
if role in ["system", "user", "assistant"] and content.strip():
if role == last_role:
logger.debug(f"Skipping consecutive {role} message to maintain alternation")
logger.debug(
f"Skipping consecutive {role} message to maintain alternation"
)
continue
if role == "user":
@ -1733,7 +1802,9 @@ class InferenceBackend:
continue
if chat_messages and chat_messages[-1]["role"] == "assistant":
logger.debug("Removing final assistant message to ensure proper alternation")
logger.debug(
"Removing final assistant message to ensure proper alternation"
)
chat_messages.pop()
logger.info(f"Sending {len(chat_messages)} messages to tokenizer:")
@ -1748,7 +1819,10 @@ class InferenceBackend:
return formatted_prompt
except Exception as e:
error_msg = str(e).lower()
if "chat_template is not set" in error_msg or "no template argument" in error_msg:
if (
"chat_template is not set" in error_msg
or "no template argument" in error_msg
):
logger.info(
f"Base model detected - no built-in chat template available, using fallback formatting"
)
@ -1759,7 +1833,9 @@ class InferenceBackend:
)
if chat_template_info.get("has_template", False):
logger.info("Falling back to manual template formatting based on detected patterns")
logger.info(
"Falling back to manual template formatting based on detected patterns"
)
template_type = chat_template_info.get("format_type", "generic")
manual_prompt = self._format_chat_manual(
chat_messages,
@ -1772,7 +1848,9 @@ class InferenceBackend:
logger.info("Using generic chat formatting for base model")
return self._format_generic_template(chat_messages, {})
def _format_chat_manual(self, messages: list, template_type: str, special_tokens: dict) -> str:
def _format_chat_manual(
self, messages: list, template_type: str, special_tokens: dict
) -> str:
"""Manual chat-formatting fallback when the tokenizer template fails.
Args:
@ -1802,7 +1880,9 @@ class InferenceBackend:
for msg in messages:
role = msg["role"]
content = msg["content"]
formatted += f"<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>"
formatted += (
f"<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>"
)
formatted += "<|start_header_id|>assistant<|end_header_id|>\n\n"
return formatted
@ -1831,7 +1911,10 @@ class InferenceBackend:
formatted += f"[INST] {user_content} [/INST]"
if i + 1 < len(conversation) and conversation[i + 1]["role"] == "assistant":
if (
i + 1 < len(conversation)
and conversation[i + 1]["role"] == "assistant"
):
formatted += f" {conversation[i + 1]['content']}</s>"
i += 2
else:
@ -1965,7 +2048,9 @@ class InferenceBackend:
return text.strip()
def _load_chat_template_info(self, model_name: str):
if model_name not in self.models or not self.models[model_name].get("tokenizer"):
if model_name not in self.models or not self.models[model_name].get(
"tokenizer"
):
return
tokenizer = self.models[model_name]["tokenizer"]
@ -1983,7 +2068,9 @@ class InferenceBackend:
# Exact match first
model_name_lower = model_name.lower()
if model_name_lower in MODEL_TO_TEMPLATE_MAPPER:
chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[model_name_lower]
chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[
model_name_lower
]
logger.info(
f"Detected template '{chat_template_info['template_name']}' for {model_name} from mapper"
)
@ -1991,13 +2078,17 @@ class InferenceBackend:
# Partial match (for variants like model_name-bnb-4bit)
for key in MODEL_TO_TEMPLATE_MAPPER:
if key in model_name_lower or model_name_lower in key:
chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[key]
chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[
key
]
logger.info(
f"Detected template '{chat_template_info['template_name']}' for {model_name} (partial match)"
)
break
except Exception as e:
logger.warning(f"Could not detect template from mapper for {model_name}: {e}")
logger.warning(
f"Could not detect template from mapper for {model_name}: {e}"
)
try:
if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
@ -2006,7 +2097,10 @@ class InferenceBackend:
template_str = tokenizer.chat_template.lower()
if "start_header_id" in template_str and "end_header_id" in template_str:
if (
"start_header_id" in template_str
and "end_header_id" in template_str
):
chat_template_info["format_type"] = "llama3"
elif "[inst]" in template_str and "[/inst]" in template_str:
chat_template_info["format_type"] = "mistral"
@ -2033,7 +2127,9 @@ class InferenceBackend:
chat_template_info["special_tokens"] = special_tokens
else:
logger.info(f"No chat template found for {model_name}, will use generic formatting")
logger.info(
f"No chat template found for {model_name}, will use generic formatting"
)
except Exception as e:
logger.error(f"Error loading chat template info for {model_name}: {e}")
@ -2045,7 +2141,9 @@ class InferenceBackend:
f"Chat template loaded for {model_name}: {chat_template_info['format_type']} format"
)
else:
logger.info(f"No built-in chat template for {model_name}, will use generic formatting")
logger.info(
f"No built-in chat template for {model_name}, will use generic formatting"
)
def get_current_model(self) -> Optional[str]:
"""Currently active model name."""

File diff suppressed because it is too large Load diff

View file

@ -124,7 +124,9 @@ def is_managed_flag(flag: str) -> bool:
# from inherited extras so they can't last-wins-override an Apply that
# re-sets the same field.
_CONTEXT_FLAGS: frozenset[str] = frozenset({"-c", "--ctx-size"})
_CACHE_FLAGS: frozenset[str] = frozenset({"-ctk", "--cache-type-k", "-ctv", "--cache-type-v"})
_CACHE_FLAGS: frozenset[str] = frozenset(
{"-ctk", "--cache-type-k", "-ctv", "--cache-type-v"}
)
_SPEC_FLAGS: frozenset[str] = frozenset(
{
"--spec-default",
@ -170,11 +172,17 @@ _TENSOR_SPLIT_FLAGS: frozenset[str] = frozenset({"-ts", "--tensor-split"})
_SPLIT_SHADOWING_FLAGS: frozenset[str] = _SPLIT_MODE_FLAGS | _TENSOR_SPLIT_FLAGS
_SHADOWING_FLAGS: frozenset[str] = (
_CONTEXT_FLAGS | _CACHE_FLAGS | _SPEC_FLAGS | _TEMPLATE_FLAGS | _SPLIT_SHADOWING_FLAGS
_CONTEXT_FLAGS
| _CACHE_FLAGS
| _SPEC_FLAGS
| _TEMPLATE_FLAGS
| _SPLIT_SHADOWING_FLAGS
)
# Shadowing flags that take no value -- strip the flag only, not the next token.
_BOOLEAN_SHADOWING_FLAGS: frozenset[str] = frozenset({"--spec-default", "--jinja", "--no-jinja"})
_BOOLEAN_SHADOWING_FLAGS: frozenset[str] = frozenset(
{"--spec-default", "--jinja", "--no-jinja"}
)
def parse_ctx_override(args: Optional[Iterable[str]]) -> Optional[int]:
@ -201,16 +209,22 @@ def parse_ctx_override(args: Optional[Iterable[str]]) -> Optional[int]:
i += 1
else:
if i + 1 >= n or _flag_name(tokens[i + 1]) is not None:
raise ValueError(f"llama-server flag '{flag}' requires an integer value")
raise ValueError(
f"llama-server flag '{flag}' requires an integer value"
)
raw_value = tokens[i + 1]
i += 2
try:
value = int(str(raw_value).strip())
except ValueError as exc:
raise ValueError(f"llama-server flag '{flag}' requires an integer value") from exc
raise ValueError(
f"llama-server flag '{flag}' requires an integer value"
) from exc
if value < 0:
raise ValueError(f"llama-server flag '{flag}' requires a non-negative integer value")
raise ValueError(
f"llama-server flag '{flag}' requires a non-negative integer value"
)
override = value
return override
@ -226,7 +240,9 @@ def resolve_requested_ctx(args: Optional[Iterable[str]], fallback_n_ctx: int) ->
return override if override is not None else fallback_n_ctx
def _last_flag_value(args: Optional[Iterable[str]], flags: frozenset[str]) -> Optional[str]:
def _last_flag_value(
args: Optional[Iterable[str]], flags: frozenset[str]
) -> Optional[str]:
"""Return the last-wins string value among ``flags`` in extras, or None.
Handles both ``--flag=value`` and ``--flag value`` forms and raises if a
@ -295,7 +311,9 @@ def parse_split_mode_override(args: Optional[Iterable[str]]) -> Optional[str]:
return _last_flag_value(args, _SPLIT_MODE_FLAGS)
def resolve_tensor_parallel(args: Optional[Iterable[str]], fallback_tensor_parallel: bool) -> bool:
def resolve_tensor_parallel(
args: Optional[Iterable[str]], fallback_tensor_parallel: bool
) -> bool:
"""Return the tensor-parallel state load_model should treat as requested.
A user-supplied ``--split-mode`` in extras last-wins-overrides the

View file

@ -217,7 +217,9 @@ def _client(
auth = OAuth(mcp_url = url, token_storage = _oauth_store())
transport_cls = (
SSETransport if infer_transport_type_from_url(url) == "sse" else StreamableHttpTransport
SSETransport
if infer_transport_type_from_url(url) == "sse"
else StreamableHttpTransport
)
return Client(transport_cls(url = url, headers = headers or None, auth = auth))
@ -252,7 +254,9 @@ _probe_cooloff_until: dict[str, float] = {}
# endpoint/auth used to probe it (url, headers, oauth) or whether it's used at
# all (is_enabled). A rename does not. The update route's eviction and
# get_enabled_mcp_tools' mid-probe guard both key off this so they can't drift.
TOOL_CACHE_INVALIDATING_FIELDS = frozenset({"url", "headers_json", "use_oauth", "is_enabled"})
TOOL_CACHE_INVALIDATING_FIELDS = frozenset(
{"url", "headers_json", "use_oauth", "is_enabled"}
)
def get_cached_tools(server_id: str) -> Optional[list[dict]]:
@ -265,7 +269,11 @@ def cache_tools(server_id: str, tools: list[dict]) -> None:
def record_probe_failure(server_id: str, use_oauth: bool = False) -> None:
cooloff = OAUTH_FAILED_PROBE_COOLOFF_SECONDS if use_oauth else FAILED_PROBE_COOLOFF_SECONDS
cooloff = (
OAUTH_FAILED_PROBE_COOLOFF_SECONDS
if use_oauth
else FAILED_PROBE_COOLOFF_SECONDS
)
_probe_cooloff_until[server_id] = time.monotonic() + cooloff

View file

@ -60,14 +60,19 @@ def _enabled_from_spec(label: str, spec: dict) -> tuple[Optional[bool], Optional
return not disabled, None
def _parse_entry(name: str, spec: object) -> tuple[Optional[ParsedMcpEntry], Optional[str]]:
def _parse_entry(
name: str, spec: object
) -> tuple[Optional[ParsedMcpEntry], Optional[str]]:
label = str(name).strip()
if not label:
return None, "Server entry has an empty name."
if not isinstance(spec, dict):
return None, f"{label}: entry must be an object."
if _has_variable_reference(spec):
return None, f"{label}: VS Code variable references are not supported by import."
return (
None,
f"{label}: VS Code variable references are not supported by import.",
)
is_enabled, error = _enabled_from_spec(label, spec)
if error:
@ -91,8 +96,13 @@ def _parse_entry(name: str, spec: object) -> tuple[Optional[ParsedMcpEntry], Opt
if sandbox_enabled is not None and not isinstance(sandbox_enabled, bool):
return None, f"{label}: 'sandboxEnabled' must be true or false."
if sandbox_enabled:
return None, f"{label}: sandboxed stdio servers cannot be preserved by import."
unsupported = [field for field in _UNSUPPORTED_STDIO_FIELDS if spec.get(field) is not None]
return (
None,
f"{label}: sandboxed stdio servers cannot be preserved by import.",
)
unsupported = [
field for field in _UNSUPPORTED_STDIO_FIELDS if spec.get(field) is not None
]
if unsupported:
return None, f"{label}: import cannot preserve {', '.join(unsupported)}."
if spec.get("oauth") is not None:
@ -104,7 +114,10 @@ def _parse_entry(name: str, spec: object) -> tuple[Optional[ParsedMcpEntry], Opt
if env is not None and not isinstance(env, dict):
return None, f"{label}: 'env' must be an object."
if _has_null_value(env):
return None, f"{label}: null environment values are not supported by import."
return (
None,
f"{label}: null environment values are not supported by import.",
)
url = join_stdio_command([command, *(str(a) for a in args)])
headers = _coerce_str_dict(env) if env else None
return ParsedMcpEntry(label, url, headers, True, is_enabled = is_enabled), None
@ -120,12 +133,21 @@ def _parse_entry(name: str, spec: object) -> tuple[Optional[ParsedMcpEntry], Opt
field for field in _UNSUPPORTED_TIMEOUT_FIELDS if spec.get(field) is not None
]
if unsupported_timeout:
return None, f"{label}: import cannot preserve {', '.join(unsupported_timeout)}."
return (
None,
f"{label}: import cannot preserve {', '.join(unsupported_timeout)}.",
)
url_infers_sse = url.rstrip("/").endswith("/sse")
if entry_type == "sse" and not url_infers_sse:
return None, f"{label}: explicit SSE transport cannot be preserved for this URL."
return (
None,
f"{label}: explicit SSE transport cannot be preserved for this URL.",
)
if entry_type in _HTTP_REMOTE_TYPES and url_infers_sse:
return None, f"{label}: explicit HTTP transport cannot be preserved for this URL."
return (
None,
f"{label}: explicit HTTP transport cannot be preserved for this URL.",
)
oauth_raw = spec.get("oauth")
if oauth_raw is not None and not isinstance(oauth_raw, dict):
return None, f"{label}: 'oauth' must be an object."

View file

@ -296,7 +296,9 @@ class MLXInferenceBackend:
elif isinstance(content, list):
# Prepend image if not already present
has_image = any(
p.get("type") == "image" for p in content if isinstance(p, dict)
p.get("type") == "image"
for p in content
if isinstance(p, dict)
)
if not has_image:
content.insert(0, {"type": "image"})
@ -366,7 +368,9 @@ class MLXInferenceBackend:
preserve_thinking = preserve_thinking,
)
if prompt is None:
raise RuntimeError("apply_chat_template returned None — tokenizer may be incompatible")
raise RuntimeError(
"apply_chat_template returned None — tokenizer may be incompatible"
)
sampler = make_sampler(
temp = temperature,
@ -513,7 +517,9 @@ class MLXInferenceBackend:
**vlm_kwargs,
):
final_response = response
token_text = response.text if hasattr(response, "text") else str(response)
token_text = (
response.text if hasattr(response, "text") else str(response)
)
cumulative += token_text
yield cumulative
if cancel_event and cancel_event.is_set():

View file

@ -89,7 +89,9 @@ class InferenceOrchestrator:
atexit.register(self._cleanup)
logger.info("InferenceOrchestrator initialized (subprocess mode)")
threading.Thread(target = self._fetch_top_models, daemon = True, name = "top-models").start()
threading.Thread(
target = self._fetch_top_models, daemon = True, name = "top-models"
).start()
# ------------------------------------------------------------------
# Default models (top GGUFs fetched dynamically from HF)
@ -129,12 +131,14 @@ class InferenceOrchestrator:
if resp.status_code == 200:
models = resp.json()
# Top 40 GGUFs (deep pool for frontend infinite scroll)
gguf_ids = [m["id"] for m in models if m.get("id", "").upper().endswith("-GGUF")][
:40
]
gguf_ids = [
m["id"] for m in models if m.get("id", "").upper().endswith("-GGUF")
][:40]
# Top 40 non-GGUF hub models
hub_ids = [
m["id"] for m in models if not m.get("id", "").upper().endswith("-GGUF")
m["id"]
for m in models
if not m.get("id", "").upper().endswith("-GGUF")
][:40]
if gguf_ids:
self._top_gguf_cache = gguf_ids
@ -272,7 +276,8 @@ class InferenceOrchestrator:
"Try a smaller model, lower context length, or close other GPU-heavy apps."
)
return (
f"{message}{suffix} " f"Details: pid={pid}, signal={sig_name}, exitcode={exitcode}."
f"{message}{suffix} "
f"Details: pid={pid}, signal={sig_name}, exitcode={exitcode}."
)
return f"{message} Details: pid={pid}, exitcode={exitcode}."
@ -355,7 +360,8 @@ class InferenceOrchestrator:
)
raise RuntimeError(
f"Timeout waiting for '{expected_type}' response " f"(no activity for {timeout}s)"
f"Timeout waiting for '{expected_type}' response "
f"(no activity for {timeout}s)"
)
def _drain_queue(self) -> list:
@ -733,9 +739,13 @@ class InferenceOrchestrator:
# without re-entering the subprocess.
_tpl_info = model_info.get("chat_template_info")
if isinstance(_tpl_info, dict):
self.models[self.active_model_name]["chat_template_info"] = _tpl_info
self.models[self.active_model_name]["chat_template_info"] = (
_tpl_info
)
self.loading_models.discard(model_name)
logger.info("Model '%s' loaded successfully in subprocess", model_name)
logger.info(
"Model '%s' loaded successfully in subprocess", model_name
)
return True
else:
error = resp.get("error", "Failed to load model")
@ -1166,7 +1176,9 @@ class InferenceOrchestrator:
if resp is None:
if not self._ensure_subprocess_alive():
raise RuntimeError(self._subprocess_crash_message("audio generation"))
raise RuntimeError(
self._subprocess_crash_message("audio generation")
)
continue
rtype = resp.get("type", "")
@ -1258,7 +1270,9 @@ class InferenceOrchestrator:
# numpy array -> list for mp.Queue serialization
audio_data = (
audio_array.tolist() if hasattr(audio_array, "tolist") else list(audio_array)
audio_array.tolist()
if hasattr(audio_array, "tolist")
else list(audio_array)
)
cmd = {
@ -1288,7 +1302,10 @@ class InferenceOrchestrator:
if resp is None:
if not self._ensure_subprocess_alive():
yield ("Error: " + self._subprocess_crash_message("audio input generation"))
yield (
"Error: "
+ self._subprocess_crash_message("audio input generation")
)
return
continue

View file

@ -103,7 +103,9 @@ def _lookup(provider: str, model: str) -> Optional[dict[str, float]]:
return None
def calculate_cost(provider: str, model: str, usage: dict[str, Any]) -> dict[str, float]:
def calculate_cost(
provider: str, model: str, usage: dict[str, Any]
) -> dict[str, float]:
"""Return a per-turn USD cost breakdown (per-bucket + total).
Unknown model -> ``priced`` False and USD fields 0.0 (token counts still report).
@ -131,7 +133,8 @@ def calculate_cost(provider: str, model: str, usage: dict[str, Any]) -> dict[str
# Clamp >=0 so corrupted payloads can't produce a negative bill.
cache_creation = max(0, int(usage.get("cache_creation_input_tokens") or 0))
cache_read_native_present = (
"cache_read_input_tokens" in usage and usage.get("cache_read_input_tokens") is not None
"cache_read_input_tokens" in usage
and usage.get("cache_read_input_tokens") is not None
)
cache_read = max(0, int(usage.get("cache_read_input_tokens") or 0))
# Fall back to mirrored prompt_tokens_details only when native
@ -214,10 +217,14 @@ def calculate_cost(provider: str, model: str, usage: dict[str, Any]) -> dict[str
if cc_5m + cc_1h == 0 and cache_creation > 0:
# No breakdown -- assume default 5m pool.
cc_5m = cache_creation
out["cache_write_usd"] = (cc_5m / 1_000_000.0) * base * ANTHROPIC_CACHE_5M_WRITE_MULT + (
out["cache_write_usd"] = (
cc_5m / 1_000_000.0
) * base * ANTHROPIC_CACHE_5M_WRITE_MULT + (
cc_1h / 1_000_000.0
) * base * ANTHROPIC_CACHE_1H_WRITE_MULT
out["cache_read_usd"] = (cache_read / 1_000_000.0) * base * ANTHROPIC_CACHE_READ_MULT
out["cache_read_usd"] = (
(cache_read / 1_000_000.0) * base * ANTHROPIC_CACHE_READ_MULT
)
# Server-tool surcharges.
srv = usage.get("server_tool_use") or {}
if isinstance(srv, dict):
@ -234,7 +241,9 @@ def calculate_cost(provider: str, model: str, usage: dict[str, Any]) -> dict[str
if cache_read > 0:
non_cached_input = max(0, input_tokens - cache_read)
out["input_usd"] = (non_cached_input / 1_000_000.0) * base
out["cache_read_usd"] = (cache_read / 1_000_000.0) * base * OPENAI_CACHE_READ_MULT
out["cache_read_usd"] = (
(cache_read / 1_000_000.0) * base * OPENAI_CACHE_READ_MULT
)
# OpenAI server-tool surcharges arrive under `openai_tool_use`
# (normalised by the SSE finaliser from output items).
srv = usage.get("openai_tool_use") or {}

View file

@ -92,7 +92,9 @@ def _detect_render_html_tool_start(content: str) -> bool:
if not function_match and tool_call_index < 0:
return False
if function_match and (tool_call_index < 0 or function_match.start() < tool_call_index):
if function_match and (
tool_call_index < 0 or function_match.start() < tool_call_index
):
return function_match.group(1) == "render_html"
if tool_call_index >= 0:
@ -182,7 +184,9 @@ def run_safetensors_tool_loop(
# Forced first-pass RAG (mirrors the GGUF loop) so doc Qs don't lose to web_search.
from core.inference.tools import build_rag_autoinject
_auto = None if confirm_tool_calls else build_rag_autoinject(conversation, rag_scope)
_auto = (
None if confirm_tool_calls else build_rag_autoinject(conversation, rag_scope)
)
if _auto:
for _ev in _auto["events"]:
yield _ev
@ -201,7 +205,9 @@ def run_safetensors_tool_loop(
def _tool_succeeded(tool_name: str) -> bool:
key_prefix = f"{tool_name}:"
return any(
record.executed and not record.is_error and record.key.startswith(key_prefix)
record.executed
and not record.is_error
and record.key.startswith(key_prefix)
for record in tool_controller.history
)
@ -226,7 +232,9 @@ def run_safetensors_tool_loop(
final_attempt_done = True
active_tools = []
tool_protocol_active = not final_attempt_done and (unrestricted_tools or bool(active_tools))
tool_protocol_active = not final_attempt_done and (
unrestricted_tools or bool(active_tools)
)
tool_xml_signals = TOOL_XML_SIGNALS if tool_protocol_active else ()
detect_state = _state_buffering
@ -515,11 +523,15 @@ def run_safetensors_tool_loop(
conversation.append(assistant_msg)
assistant_appended = True
else:
assistant_msg.setdefault("tool_calls", []).append(decision.as_assistant_tool_call())
assistant_msg.setdefault("tool_calls", []).append(
decision.as_assistant_tool_call()
)
needs_confirm = bool(confirm_tool_calls)
approval_id = new_approval_id() if needs_confirm else ""
decision_slot = begin_tool_decision(session_id, approval_id) if needs_confirm else None
decision_slot = (
begin_tool_decision(session_id, approval_id) if needs_confirm else None
)
start_event = decision.tool_start_event()
start_event["approval_id"] = approval_id
start_event["awaiting_confirmation"] = needs_confirm

View file

@ -194,7 +194,11 @@ def parse_tool_calls_from_text(
for idx, fm in enumerate(func_starts):
func_name = fm.group(1)
body_start = fm.end()
next_func = func_starts[idx + 1].start() if idx + 1 < len(func_starts) else len(content)
next_func = (
func_starts[idx + 1].start()
if idx + 1 < len(func_starts)
else len(content)
)
end_tag = _TC_END_TAG_RE.search(content[body_start:])
if end_tag:
body_end = body_start + end_tag.start()

View file

@ -287,7 +287,9 @@ class ToolLoopController:
self._restrict_to_allowed = tools is not None
self._tools = [copy.deepcopy(dict(tool)) for tool in (tools or [])]
self._allowed_tool_names = {
name for name in (_tool_name_from_schema(tool) for tool in self._tools) if name
name
for name in (_tool_name_from_schema(tool) for tool in self._tools)
if name
}
self._auto_heal_tool_calls = auto_heal_tool_calls
self._one_shot_tools = one_shot_tools
@ -364,7 +366,9 @@ class ToolLoopController:
noop_result = noop,
)
def record_result(self, decision: ToolCallDecision, result: Any) -> ToolCallCompletion:
def record_result(
self, decision: ToolCallDecision, result: Any
) -> ToolCallCompletion:
"""Record a real tool execution and return model/frontend payload helpers."""
result_text = result if isinstance(result, str) else str(result)
failed = is_tool_error(result_text)

View file

@ -125,7 +125,9 @@ _BLOCKED_COMMANDS = (
)
_SHELL_SEPARATORS = frozenset({";", "&&", "||", "|", "&", "\n", "(", ")", "`", "{", "}"})
_SHELL_SEPARATORS = frozenset(
{";", "&&", "||", "|", "&", "\n", "(", ")", "`", "{", "}"}
)
# Bash keywords starting a new command position (then $cmd, do $cmd, etc.).
_SHELL_KEYWORDS_AS_SEP = frozenset({"then", "do", "else", "elif"})
# Wrappers whose next non-flag argument is the command Bash will exec.
@ -248,7 +250,9 @@ def _find_blocked_commands(command: str) -> set[str]:
tok_lower = token.lower()
# Match -c exactly, or combined flags ending in c (e.g. -lc, -xc)
is_unix_c = tok_lower == "-c" or (
tok_lower.startswith("-") and tok_lower.endswith("c") and not tok_lower.startswith("--")
tok_lower.startswith("-")
and tok_lower.endswith("c")
and not tok_lower.startswith("--")
)
is_win_c = tok_lower == "/c"
if not (is_unix_c or is_win_c) or i < 1 or i + 1 >= len(tokens):
@ -352,11 +356,18 @@ def _sandbox_preexec():
except (ValueError, OSError, AttributeError):
pass
try:
_resource.setrlimit(_resource.RLIMIT_FSIZE, (100 * 1024 * 1024, 100 * 1024 * 1024))
_resource.setrlimit(
_resource.RLIMIT_FSIZE, (100 * 1024 * 1024, 100 * 1024 * 1024)
)
except (ValueError, OSError):
pass
try:
as_bytes = int(os.environ.get("UNSLOTH_STUDIO_SANDBOX_AS_GB", "8")) * 1024 * 1024 * 1024
as_bytes = (
int(os.environ.get("UNSLOTH_STUDIO_SANDBOX_AS_GB", "8"))
* 1024
* 1024
* 1024
)
_resource.setrlimit(_resource.RLIMIT_AS, (as_bytes, as_bytes))
except (ValueError, OSError, AttributeError):
pass
@ -371,7 +382,9 @@ def _sandbox_preexec():
# when the parent's hard cap is below the request.
nofile = int(os.environ.get("UNSLOTH_STUDIO_SANDBOX_NOFILE", "16384"))
_soft_cur, hard_cur = _resource.getrlimit(_resource.RLIMIT_NOFILE)
target = nofile if hard_cur == _resource.RLIM_INFINITY else min(nofile, hard_cur)
target = (
nofile if hard_cur == _resource.RLIM_INFINITY else min(nofile, hard_cur)
)
_resource.setrlimit(_resource.RLIMIT_NOFILE, (target, target))
except (ValueError, OSError, AttributeError):
pass
@ -404,7 +417,9 @@ def _get_project_workdir(session_id: str) -> str | None:
from storage.studio_db import ensure_chat_project_workspace
project = ensure_chat_project_workspace(project_id)
except Exception:
logger.warning("Failed to resolve project sandbox for %s", session_id, exc_info = True)
logger.warning(
"Failed to resolve project sandbox for %s", session_id, exc_info = True
)
return None
if not project:
return None
@ -435,7 +450,9 @@ def _get_workdir(session_id: str | None = None) -> str:
workdir = project_workdir
elif session_id and _SESSION_ID_RE.match(session_id):
workdir = os.path.join(sandbox_root, session_id)
if not os.path.realpath(workdir).startswith(os.path.realpath(sandbox_root) + os.sep):
if not os.path.realpath(workdir).startswith(
os.path.realpath(sandbox_root) + os.sep
):
workdir = os.path.join(sandbox_root, "_invalid")
elif session_id:
workdir = os.path.join(sandbox_root, "_invalid")
@ -614,7 +631,9 @@ def _mcp_specs_for_server(server: dict, mcp_tools: list[dict]) -> list[dict]:
continue
# Duplicate tool names would also 400 OpenAI; drop dupes.
if name in seen_names:
logger.warning("Skipping duplicate MCP tool '%s' on '%s'.", raw_name, display)
logger.warning(
"Skipping duplicate MCP tool '%s' on '%s'.", raw_name, display
)
continue
seen_names.add(name)
specs.append(
@ -623,7 +642,8 @@ def _mcp_specs_for_server(server: dict, mcp_tools: list[dict]) -> list[dict]:
"function": {
"name": name,
"description": f"[{display}] {tool.get('description') or ''}".strip(),
"parameters": tool.get("inputSchema") or {"type": "object", "properties": {}},
"parameters": tool.get("inputSchema")
or {"type": "object", "properties": {}},
},
}
)
@ -643,7 +663,9 @@ async def get_enabled_mcp_tools() -> list[dict]:
# server gets re-probed -- and blocks the send for the full timeout -- on
# every message.
uncached = [
s for s in servers if get_cached_tools(s["id"]) is None and not in_failure_cooloff(s["id"])
s
for s in servers
if get_cached_tools(s["id"]) is None and not in_failure_cooloff(s["id"])
]
if uncached:
results = await asyncio.gather(
@ -731,7 +753,9 @@ def execute_tool(
``rag_scope``: hidden per-request RAG context the model never sees; consumed
by ``search_knowledge_base``.
"""
logger.info(f"execute_tool: name={name}, session_id={session_id}, timeout={timeout}")
logger.info(
f"execute_tool: name={name}, session_id={session_id}, timeout={timeout}"
)
effective_timeout = _EXEC_TIMEOUT if timeout is _TIMEOUT_UNSET else timeout
if name == "search_knowledge_base":
return _search_knowledge_base(arguments, rag_scope)
@ -765,9 +789,13 @@ def execute_tool(
timeout = effective_timeout,
)
if name == "python":
return _python_exec(arguments.get("code", ""), cancel_event, effective_timeout, session_id)
return _python_exec(
arguments.get("code", ""), cancel_event, effective_timeout, session_id
)
if name == "terminal":
return _bash_exec(arguments.get("command", ""), cancel_event, effective_timeout, session_id)
return _bash_exec(
arguments.get("command", ""), cancel_event, effective_timeout, session_id
)
return f"Unknown tool: {name}"
@ -875,7 +903,9 @@ def _last_user_text(conversation: list[dict]) -> str:
return ""
def build_rag_autoinject(conversation: list[dict], rag_scope: dict | None) -> dict | None:
def build_rag_autoinject(
conversation: list[dict], rag_scope: dict | None
) -> dict | None:
"""Pre-retrieve the latest user turn; if a hit clears the cosine floor return
``{"events": [...], "messages": [...]}`` to splice into the loop, else ``None``.
Toggle via ``rag_scope.autoinject`` (else env ``RAG_AUTOINJECT``); floor via
@ -971,7 +1001,9 @@ def build_rag_autoinject(conversation: list[dict], rag_scope: dict | None) -> di
"content": text,
},
]
logger.info("RAG auto-inject: %d passage(s) >= %.2f for %r", len(sources), floor, query[:80])
logger.info(
"RAG auto-inject: %d passage(s) >= %.2f for %r", len(sources), floor, query[:80]
)
return {"events": events, "messages": messages}
@ -1138,7 +1170,9 @@ def _fetch_page_text(
resp = opener.open(req, timeout = timeout)
except _HTTPError as e:
if e.code not in (301, 302, 303, 307, 308):
return f"Failed to fetch URL: HTTP {e.code} {getattr(e, 'reason', '')}"
return (
f"Failed to fetch URL: HTTP {e.code} {getattr(e, 'reason', '')}"
)
location = e.headers.get("Location")
if not location:
return "Failed to fetch URL: redirect missing Location header."
@ -1398,7 +1432,9 @@ def _check_signal_escape_patterns(code: str):
if func_name:
if func_name in ("signal.signal", "signal"):
if len(node.args) >= 1:
if _ast_name_matches(node.args[0], ("SIGALRM", "signal.SIGALRM")):
if _ast_name_matches(
node.args[0], ("SIGALRM", "signal.SIGALRM")
):
signal_tampering.append(
{
"type": "signal_handler_override",
@ -1408,7 +1444,9 @@ def _check_signal_escape_patterns(code: str):
)
elif func_name in ("signal.setitimer", "setitimer"):
if len(node.args) >= 1:
if _ast_name_matches(node.args[0], ("ITIMER_REAL", "signal.ITIMER_REAL")):
if _ast_name_matches(
node.args[0], ("ITIMER_REAL", "signal.ITIMER_REAL")
):
signal_tampering.append(
{
"type": "timer_manipulation",
@ -1461,7 +1499,9 @@ def _check_signal_escape_patterns(code: str):
else:
has_opaque_kwargs = True
cmd_kw_values = [v for k, v in expanded_kwargs.items() if k in _CMD_KWARGS]
cmd_kw_values = [
v for k, v in expanded_kwargs.items() if k in _CMD_KWARGS
]
all_call_args = list(node.args) + cmd_kw_values
blocked_in_args = _check_args_for_blocked(all_call_args)
@ -1471,7 +1511,9 @@ def _check_signal_escape_patterns(code: str):
{
"type": "shell_escape_dynamic",
"line": node.lineno,
"description": (f"{shell_func}() called with dynamic **kwargs"),
"description": (
f"{shell_func}() called with dynamic **kwargs"
),
}
)
elif blocked_in_args:
@ -1502,7 +1544,8 @@ def _check_signal_escape_patterns(code: str):
)
shell_node = expanded_kwargs.get("shell")
shell_safe = shell_node is None or (
isinstance(shell_node, ast.Constant) and shell_node.value is False
isinstance(shell_node, ast.Constant)
and shell_node.value is False
)
# Dynamic shell-exec args (chr/format/concat bypasses).
if (
@ -1515,10 +1558,15 @@ def _check_signal_escape_patterns(code: str):
if _extract_string_from_node(n) is not None:
return True
if isinstance(n, (ast.List, ast.Tuple)):
return all(_extract_string_from_node(e) is not None for e in n.elts)
return all(
_extract_string_from_node(e) is not None
for e in n.elts
)
return False
has_non_literal = any(not _is_safe_literal(a) for a in all_call_args)
has_non_literal = any(
not _is_safe_literal(a) for a in all_call_args
)
if has_non_literal:
shell_escapes.append(
{
@ -1775,7 +1823,9 @@ def _check_signal_escape_patterns(code: str):
"/etc/sudoers",
"/etc/ssh/",
)
_SENSITIVE_FILE_RE = re.compile(r"^/proc/(?:self|\d+)/(?:environ|cmdline|task/\d+/environ)$")
_SENSITIVE_FILE_RE = re.compile(
r"^/proc/(?:self|\d+)/(?:environ|cmdline|task/\d+/environ)$"
)
def _normalize_host(host: str) -> str:
if not host:
@ -1818,9 +1868,15 @@ def _check_signal_escape_patterns(code: str):
return True
if kw.arg == "data":
v = kw.value
if isinstance(v, ast.Call) and isinstance(v.func, ast.Name) and v.func.id == "open":
if (
isinstance(v, ast.Call)
and isinstance(v.func, ast.Name)
and v.func.id == "open"
):
return True
if isinstance(v, ast.Constant) and isinstance(v.value, (bytes, bytearray)):
if isinstance(v, ast.Constant) and isinstance(
v.value, (bytes, bytearray)
):
return True
return False
@ -1951,7 +2007,9 @@ def _check_signal_escape_patterns(code: str):
"""Whether the path argument resolves to a sandbox-local literal."""
if node is None:
return False
if isinstance(node, ast.Constant) and isinstance(node.value, (bytes, bytearray)):
if isinstance(node, ast.Constant) and isinstance(
node.value, (bytes, bytearray)
):
return True # inline bytes, no file access
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return _is_safe_relative_path(node.value)
@ -2037,7 +2095,11 @@ def _check_signal_escape_patterns(code: str):
)
# Direct sock.connect((host, port)) bypasses the FQ-prefix branch.
if isinstance(node.func, ast.Attribute) and node.func.attr == "connect" and node.args:
if (
isinstance(node.func, ast.Attribute)
and node.func.attr == "connect"
and node.args
):
a0 = node.args[0]
host_lit = None
if isinstance(a0, ast.Tuple) and a0.elts:
@ -2074,7 +2136,9 @@ def _check_signal_escape_patterns(code: str):
{
"type": "upload_blocked",
"line": getattr(node, "lineno", -1),
"description": ("Blocked: file upload disallowed in sandbox"),
"description": (
"Blocked: file upload disallowed in sandbox"
),
}
)
@ -2175,18 +2239,28 @@ def _check_code_safety(code: str) -> str | None:
if info.get("error"):
return None
reasons = [item.get("description", "") for item in info.get("signal_tampering", [])]
shell_reasons = [item.get("description", "") for item in info.get("shell_escapes", [])]
reasons = [
item.get("description", "") for item in info.get("signal_tampering", [])
]
shell_reasons = [
item.get("description", "") for item in info.get("shell_escapes", [])
]
exception_reasons = [
item.get("description", "") for item in info.get("exception_catching", [])
]
network_reasons = [item.get("description", "") for item in info.get("network_calls", [])]
network_reasons = [
item.get("description", "") for item in info.get("network_calls", [])
]
file_reasons = [
item.get("description", "") for item in info.get("sensitive_file_reads", [])
]
all_reasons = [
r
for r in reasons + shell_reasons + exception_reasons + network_reasons + file_reasons
for r in reasons
+ shell_reasons
+ exception_reasons
+ network_reasons
+ file_reasons
if r
]
if all_reasons:
@ -2266,7 +2340,9 @@ def _python_exec(
except OSError:
pass
try:
fd, tmp_path = tempfile.mkstemp(suffix = ".py", prefix = "studio_exec_", dir = workdir)
fd, tmp_path = tempfile.mkstemp(
suffix = ".py", prefix = "studio_exec_", dir = workdir
)
with os.fdopen(fd, "w") as f:
f.write(code)

View file

@ -89,7 +89,9 @@ def _build_model_config(config: dict):
return mc
def _get_hf_download_state(model_names: list[str] | None = None) -> tuple[int, bool] | None:
def _get_hf_download_state(
model_names: list[str] | None = None,
) -> tuple[int, bool] | None:
"""Return (total_bytes, has_incomplete) for the HF Hub cache, or None on error.
With *model_names*, only those models' ``blobs/`` dirs are checked (faster);
@ -240,10 +242,14 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
adapter_cfg = json.load(f)
training_method = adapter_cfg.get("unsloth_training_method")
if training_method == "lora" and load_in_4bit:
logger.info("adapter_config.json says lora — setting load_in_4bit=False")
logger.info(
"adapter_config.json says lora — setting load_in_4bit=False"
)
load_in_4bit = False
elif training_method == "qlora" and not load_in_4bit:
logger.info("adapter_config.json says qlora — setting load_in_4bit=True")
logger.info(
"adapter_config.json says qlora — setting load_in_4bit=True"
)
load_in_4bit = True
elif not training_method:
if (
@ -513,7 +519,9 @@ def _handle_generate_audio(backend, cmd: dict, resp_queue: Any) -> None:
)
def _handle_generate_audio_input(backend, cmd: dict, resp_queue: Any, cancel_event) -> None:
def _handle_generate_audio_input(
backend, cmd: dict, resp_queue: Any, cancel_event
) -> None:
"""Handle audio input generation (ASR/Whisper) — streams text tokens back."""
request_id = cmd.get("request_id", "")
@ -548,7 +556,9 @@ def _handle_generate_audio_input(backend, cmd: dict, resp_queue: Any, cancel_eve
for text_chunk in generator:
if cancel_event.is_set():
logger.info("Audio input generation cancelled for request %s", request_id)
logger.info(
"Audio input generation cancelled for request %s", request_id
)
break
_send_response(
@ -615,7 +625,9 @@ def _handle_unload(backend, cmd: dict, resp_queue: Any) -> None:
)
def run_inference_process(*, cmd_queue: Any, resp_queue: Any, cancel_event, config: dict) -> None:
def run_inference_process(
*, cmd_queue: Any, resp_queue: Any, cancel_event, config: dict
) -> None:
"""Subprocess entrypoint. Persistent — runs the command loop until shutdown.
Args:
@ -625,7 +637,9 @@ def run_inference_process(*, cmd_queue: Any, resp_queue: Any, cancel_event, conf
config: Initial configuration dict with model info.
"""
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["PYTHONWARNINGS"] = "ignore" # Suppress warnings at C-level before imports
os.environ["PYTHONWARNINGS"] = (
"ignore" # Suppress warnings at C-level before imports
)
if config.get("disable_xet"):
os.environ["HF_HUB_DISABLE_XET"] = "1"
@ -930,7 +944,9 @@ def run_inference_process(*, cmd_queue: Any, resp_queue: Any, cancel_event, conf
)
except Exception as exc:
logger.error("Error handling command '%s': %s", cmd_type, exc, exc_info = True)
logger.error(
"Error handling command '%s': %s", cmd_type, exc, exc_info = True
)
_send_response(
resp_queue,
{

View file

@ -26,14 +26,18 @@ def vision_endpoint() -> tuple[str, str] | None:
try:
from routes.inference import get_llama_cpp_backend
backend = get_llama_cpp_backend()
if getattr(backend, "is_loaded", False) and getattr(backend, "is_vision", False):
if getattr(backend, "is_loaded", False) and getattr(
backend, "is_vision", False
):
return backend.base_url, "local"
except Exception: # noqa: BLE001 - never let discovery break ingestion
return None
return None
def _caption_one(base_url: str, model: str, image_bytes: bytes, timeout: float) -> str | None:
def _caption_one(
base_url: str, model: str, image_bytes: bytes, timeout: float
) -> str | None:
import httpx
data_url = "data:image/png;base64," + base64.b64encode(image_bytes).decode("ascii")

View file

@ -27,7 +27,9 @@ class Chunk:
page_char_end: int
def _split(text: str, seps: tuple[str, ...], max_tokens: int, count: TokenCounter) -> list[str]:
def _split(
text: str, seps: tuple[str, ...], max_tokens: int, count: TokenCounter
) -> list[str]:
"""Recursively split into pieces each <= max_tokens (best effort). Pieces
rejoin to ``text`` exactly, so offsets are a running length."""
if count(text) <= max_tokens:
@ -41,7 +43,9 @@ def _split(text: str, seps: tuple[str, ...], max_tokens: int, count: TokenCounte
out: list[str] = []
for p in parts:
out.extend(
[p] if count(p) <= max_tokens else _split(p, seps[i + 1 :], max_tokens, count)
[p]
if count(p) <= max_tokens
else _split(p, seps[i + 1 :], max_tokens, count)
)
return [p for p in out if p]
n = max(1, max_tokens * 4)
@ -49,7 +53,11 @@ def _split(text: str, seps: tuple[str, ...], max_tokens: int, count: TokenCounte
def _merge(
pieces: list[str], starts: list[int], max_tokens: int, overlap: int, count: TokenCounter
pieces: list[str],
starts: list[int],
max_tokens: int,
overlap: int,
count: TokenCounter,
) -> list[tuple[str, int, int]]:
"""Greedy-merge pieces into <= max_tokens chunks with token overlap.
``starts[i]`` is ``pieces[i]``'s page char offset; returns
@ -106,7 +114,9 @@ def chunk_pages(
for piece in pieces:
starts.append(cursor)
cursor += len(piece)
for text, char_start, char_end in _merge(pieces, starts, max_tokens, overlap, count):
for text, char_start, char_end in _merge(
pieces, starts, max_tokens, overlap, count
):
out.append(
Chunk(
text = text,

View file

@ -31,7 +31,9 @@ CAPTION_TIMEOUT_S = float(os.environ.get("RAG_CAPTION_TIMEOUT_S", "30"))
EMBED_BACKEND = os.environ.get("RAG_EMBED_BACKEND", "auto")
# llama-server backend only. F16 over Q8_0: faster (no per-block dequant for this
# tiny model) and exact vs fp32, for ~30MB more on disk.
EMBED_GGUF_REPO = os.environ.get("RAG_EMBED_GGUF_REPO", "unsloth/bge-small-en-v1.5-GGUF")
EMBED_GGUF_REPO = os.environ.get(
"RAG_EMBED_GGUF_REPO", "unsloth/bge-small-en-v1.5-GGUF"
)
EMBED_GGUF_VARIANT = os.environ.get("RAG_EMBED_GGUF_VARIANT", "F16")
EMBED_DEVICE = os.environ.get("RAG_EMBED_DEVICE", "auto") # "auto" | "gpu" | "cpu"
EMBED_HOST = os.environ.get("RAG_EMBED_HOST", "127.0.0.1")

View file

@ -123,7 +123,9 @@ class LlamaServerBackend:
repo = config.EMBED_GGUF_REPO
token = os.environ.get("HF_TOKEN") or None
files = [f for f in list_repo_files(repo, token = token) if f.lower().endswith(".gguf")]
files = [
f for f in list_repo_files(repo, token = token) if f.lower().endswith(".gguf")
]
files = [f for f in files if "mmproj" not in f.lower()]
if not files:
raise RuntimeError(f"no .gguf file found in embedder repo {repo!r}")
@ -161,7 +163,9 @@ class LlamaServerBackend:
gpus = LlamaCppBackend._get_gpu_free_memory() # [(idx, free_mib)], honors CVD
return any(free >= LlamaServerBackend._MIN_GPU_FREE_MIB for _, free in gpus)
def _build_cmd(self, binary: str, model_path: str, port: int, *, use_gpu: bool) -> list[str]:
def _build_cmd(
self, binary: str, model_path: str, port: int, *, use_gpu: bool
) -> list[str]:
# No --embd-normalize (not in every build; we normalize in Python to match
# the ST path). --fit off: don't auto-resize ctx/offload to device memory.
cmd = [
@ -204,8 +208,12 @@ class LlamaServerBackend:
arch = platform.machine()
lib_dirs = [binary_dir]
for pattern in (
os.path.join(sys.prefix, "lib", "python*", "site-packages", "nvidia", "cu*", "lib"),
os.path.join(sys.prefix, "lib", "python*", "site-packages", "nvidia", "cudnn", "lib"),
os.path.join(
sys.prefix, "lib", "python*", "site-packages", "nvidia", "cu*", "lib"
),
os.path.join(
sys.prefix, "lib", "python*", "site-packages", "nvidia", "cudnn", "lib"
),
):
lib_dirs.extend(d for d in glob.glob(pattern) if os.path.isdir(d))
for cuda_lib in (
@ -382,7 +390,9 @@ class LlamaServerBackend:
raise RuntimeError(
f"llama-server embedder POST {path} -> {e.response.status_code}: {body}"
) from e
raise RuntimeError(f"llama-server embedder POST {path} failed after retry") from last_exc
raise RuntimeError(
f"llama-server embedder POST {path} failed after retry"
) from last_exc
def encode(
self,

View file

@ -108,7 +108,9 @@ def _run(
stored_path, max_figures = config.CAPTION_MAX_IMAGES
)
except Exception:
logger.warning("figure rendering failed for job %s", job_id, exc_info = True)
logger.warning(
"figure rendering failed for job %s", job_id, exc_info = True
)
figures = []
if figures:
_progress(conn, job_id, "captioning", 0.2)
@ -139,12 +141,16 @@ def _run(
from . import locators
regions = locators.pdf_regions_for_chunks(stored_path, pages, chunks)
except Exception:
logger.warning("pdf region location failed for job %s", job_id, exc_info = True)
logger.warning(
"pdf region location failed for job %s", job_id, exc_info = True
)
regions = None
_progress(conn, job_id, "storing", 0.9)
store.add_chunks(conn, scope, document_id, chunks, vectors, regions)
store.set_document_status(conn, document_id, "completed", num_chunks = len(chunks))
store.set_document_status(
conn, document_id, "completed", num_chunks = len(chunks)
)
_set_job(conn, job_id, status = "completed", stage = "done", progress = 1.0)
_emit(job_id, {"type": "complete", "num_chunks": len(chunks)})
@ -267,7 +273,9 @@ def get_job_status(job_id: str) -> dict | None:
"""Read the persisted ingestion job row (status / stage / progress / error)."""
conn = rag_db.get_connection()
try:
row = conn.execute("SELECT * FROM ingestion_jobs WHERE id=?", (job_id,)).fetchone()
row = conn.execute(
"SELECT * FROM ingestion_jobs WHERE id=?", (job_id,)
).fetchone()
return dict(row) if row else None
finally:
conn.close()

View file

@ -118,7 +118,9 @@ def _rects_from_words(page_words: list, indices: list[int], pw: float, ph: float
return out
def _regions_for_match(doc: Any, page_text: str, match: LocatorMatch) -> list[dict[str, Any]]:
def _regions_for_match(
doc: Any, page_text: str, match: LocatorMatch
) -> list[dict[str, Any]]:
try:
if match.page_index < 0 or match.page_index >= len(doc):
return []
@ -145,7 +147,9 @@ def _regions_for_match(doc: Any, page_text: str, match: LocatorMatch) -> list[di
return []
def pdf_regions_for_chunks(pdf_path: Path, pages: list, chunks: list) -> list[list[dict[str, Any]]]:
def pdf_regions_for_chunks(
pdf_path: Path, pages: list, chunks: list
) -> list[list[dict[str, Any]]]:
"""Region rects per chunk (parallel to ``chunks``), keyed off each chunk's
``source_page_index`` / ``page_char_start`` / ``page_char_end``. Non-PDFs and
failures yield [], never an exception."""

View file

@ -27,7 +27,10 @@ def retrieve_lexical(
k: int | None = None,
) -> list[Hit]:
k = k or config.TOP_K_LEXICAL
return [Hit(cid, s, lexical_score = s) for cid, s in store.search_lexical(conn, scope, query, k)]
return [
Hit(cid, s, lexical_score = s)
for cid, s in store.search_lexical(conn, scope, query, k)
]
def retrieve_dense(
@ -40,7 +43,9 @@ def retrieve_dense(
) -> list[Hit]:
k = k or config.TOP_K_DENSE
vec = embeddings.encode([query], model_name = model_name, normalize = True)[0]
return [Hit(cid, s, dense_score = s) for cid, s in store.search_dense(conn, scope, vec, k)]
return [
Hit(cid, s, dense_score = s) for cid, s in store.search_dense(conn, scope, vec, k)
]
def _rrf(rankings: list[list[Hit]], rrf_k: int, top_k: int) -> list[Hit]:
@ -48,13 +53,19 @@ def _rrf(rankings: list[list[Hit]], rrf_k: int, top_k: int) -> list[Hit]:
best: dict[str, Hit] = {}
for ranking in rankings:
for rank, hit in enumerate(ranking):
fused[hit.chunk_id] = fused.get(hit.chunk_id, 0.0) + 1.0 / (rrf_k + rank + 1)
fused[hit.chunk_id] = fused.get(hit.chunk_id, 0.0) + 1.0 / (
rrf_k + rank + 1
)
cur = best.get(hit.chunk_id)
if cur is None:
best[hit.chunk_id] = Hit(hit.chunk_id, 0.0, hit.lexical_score, hit.dense_score)
best[hit.chunk_id] = Hit(
hit.chunk_id, 0.0, hit.lexical_score, hit.dense_score
)
else:
cur.lexical_score = (
cur.lexical_score if cur.lexical_score is not None else hit.lexical_score
cur.lexical_score
if cur.lexical_score is not None
else hit.lexical_score
)
cur.dense_score = (
cur.dense_score if cur.dense_score is not None else hit.dense_score
@ -85,7 +96,9 @@ def retrieve_hybrid(
if mode == "dense":
return retrieve_dense(conn, scope, query, k, model_name = model_name)
lexical = retrieve_lexical(conn, scope, query, config.TOP_K_LEXICAL)
dense = retrieve_dense(conn, scope, query, config.TOP_K_DENSE, model_name = model_name)
dense = retrieve_dense(
conn, scope, query, config.TOP_K_DENSE, model_name = model_name
)
return _rrf([lexical, dense], config.RRF_K, k)

View file

@ -89,7 +89,10 @@ def delete_kb(conn: sqlite3.Connection, kb_id: str) -> None:
"""Delete a knowledge base and every document (+ chunks) under it."""
scope = kb_scope(kb_id)
doc_ids = [
r["id"] for r in conn.execute("SELECT id FROM documents WHERE scope=?", (scope,)).fetchall()
r["id"]
for r in conn.execute(
"SELECT id FROM documents WHERE scope=?", (scope,)
).fetchall()
]
for doc_id in doc_ids:
delete_document(conn, doc_id)
@ -170,7 +173,9 @@ def document_by_hash(conn: sqlite3.Connection, scope: str, sha256: str) -> str |
return row["id"] if row else None
def failed_documents_by_hash(conn: sqlite3.Connection, scope: str, sha256: str) -> list[dict]:
def failed_documents_by_hash(
conn: sqlite3.Connection, scope: str, sha256: str
) -> list[dict]:
rows = conn.execute(
"SELECT id, stored_path FROM documents WHERE scope=? AND sha256=? AND status='failed'",
(scope, sha256),

View file

@ -167,10 +167,14 @@ def search_for_autoinject(
mode = mode,
)
strong = [
h for h in hits if h.dense_score is not None and h.dense_score >= min_dense_score
h
for h in hits
if h.dense_score is not None and h.dense_score >= min_dense_score
][:k]
if not strong and hits and mode == "lexical":
probe = retrieval.retrieve_dense(conn, scope, query, 1, model_name = model_name)
probe = retrieval.retrieve_dense(
conn, scope, query, 1, model_name = model_name
)
if (
probe
and probe[0].dense_score is not None

View file

@ -83,7 +83,9 @@ def parse_tool_calls_from_text(content: str) -> list[dict]:
},
}
if isinstance(tc["function"]["arguments"], dict):
tc["function"]["arguments"] = json.dumps(tc["function"]["arguments"])
tc["function"]["arguments"] = json.dumps(
tc["function"]["arguments"]
)
tool_calls.append(tc)
except (json.JSONDecodeError, ValueError):
pass
@ -99,7 +101,11 @@ def parse_tool_calls_from_text(content: str) -> list[dict]:
func_name = fm.group(1)
body_start = fm.end()
# Boundaries: next <function= tag or </tool_call>
next_func = func_starts[idx + 1].start() if idx + 1 < len(func_starts) else len(content)
next_func = (
func_starts[idx + 1].start()
if idx + 1 < len(func_starts)
else len(content)
)
end_tag = _TC_END_TAG_RE.search(content[body_start:])
if end_tag:
body_end = body_start + end_tag.start()

View file

@ -159,7 +159,9 @@ def prepare_s3_dataset_download(
bucket/prefix contains no supported dataset files.
"""
if not boto3_available():
raise RuntimeError("S3 dataset loading requires boto3. Install it with: pip install boto3")
raise RuntimeError(
"S3 dataset loading requires boto3. Install it with: pip install boto3"
)
bucket = s3_config.get("bucket")
if not bucket:
@ -193,7 +195,9 @@ def prepare_s3_dataset_download(
local_path = _unique_local_path(target_dir, filename, used_paths)
download_kwargs = {}
if cancel_callback is not None:
download_kwargs["Callback"] = lambda _bytes: _raise_if_cancelled(cancel_callback)
download_kwargs["Callback"] = lambda _bytes: _raise_if_cancelled(
cancel_callback
)
client.download_file(bucket, key, local_path, **download_kwargs)
_raise_if_cancelled(cancel_callback)
local_files.append(local_path)

File diff suppressed because it is too large Load diff

View file

@ -42,7 +42,9 @@ _HF_TMP_CHECKPOINT_RE = re.compile(r"^tmp-checkpoint-\d+$")
def _sanitize_db_config(config: dict[str, Any]) -> dict[str, Any]:
db_config = {
k: v for k, v in config.items() if k not in {"hf_token", "wandb_token", "s3_config"}
k: v
for k, v in config.items()
if k not in {"hf_token", "wandb_token", "s3_config"}
}
s3_config = config.get("s3_config")
if hasattr(s3_config, "model_dump"):
@ -202,7 +204,9 @@ class TrainingBackend:
if self._pump_thread is not None and self._pump_thread.is_alive():
self._pump_thread.join(timeout = 5.0)
if self._pump_thread.is_alive():
logger.warning("Previous pump thread did not exit within 5s — refusing to start")
logger.warning(
"Previous pump thread did not exit within 5s — refusing to start"
)
return False
self._pump_thread = None
@ -254,7 +258,9 @@ class TrainingBackend:
"train_on_completions": kwargs.get("train_on_completions", False),
"finetune_vision_layers": kwargs.get("finetune_vision_layers", True),
"finetune_language_layers": kwargs.get("finetune_language_layers", True),
"finetune_attention_modules": kwargs.get("finetune_attention_modules", True),
"finetune_attention_modules": kwargs.get(
"finetune_attention_modules", True
),
"finetune_mlp_modules": kwargs.get("finetune_mlp_modules", True),
"enable_wandb": kwargs.get("enable_wandb", False),
"wandb_token": kwargs.get("wandb_token"),
@ -367,7 +373,9 @@ class TrainingBackend:
pass
# Update progress immediately for responsive UI.
self._progress.status_message = (
"Stopping training and saving checkpoint..." if save else "Cancelling training..."
"Stopping training and saving checkpoint..."
if save
else "Cancelling training..."
)
return True
@ -375,7 +383,9 @@ class TrainingBackend:
"""Force-kill the training subprocess so state can be reset immediately."""
with self._lock:
if self._proc is not None and self._proc.is_alive():
logger.info("Force-terminating training subprocess (pid=%s)", self._proc.pid)
logger.info(
"Force-terminating training subprocess (pid=%s)", self._proc.pid
)
self._proc.terminate()
proc = self._proc
cancelled = self._cancel_requested
@ -529,7 +539,8 @@ class TrainingBackend:
else:
self._progress.is_training = False
self._progress.error = (
self._progress.error or "Training process exited unexpectedly"
self._progress.error
or "Training process exited unexpectedly"
)
self._ensure_db_run_created()
@ -563,7 +574,9 @@ class TrainingBackend:
except (TypeError, ValueError):
logger.debug("Could not convert loss to float: %s", _raw_loss)
_safe_loss = None
_loss_is_nonfinite = _safe_loss is not None and not math.isfinite(_safe_loss)
_loss_is_nonfinite = _safe_loss is not None and not math.isfinite(
_safe_loss
)
if _loss_is_nonfinite:
# Drop the value rather than laundering it back to the last
# finite loss; clients see loss=None at this step so the NaN
@ -579,7 +592,9 @@ class TrainingBackend:
try:
_safe_lr = float(_raw_lr) if _raw_lr is not None else None
except (TypeError, ValueError):
logger.debug("Could not convert learning_rate to float: %s", _raw_lr)
logger.debug(
"Could not convert learning_rate to float: %s", _raw_lr
)
_safe_lr = None
if _safe_lr is not None and not math.isfinite(_safe_lr):
_safe_lr = None
@ -591,7 +606,9 @@ class TrainingBackend:
self._progress.loss = None
if _safe_lr is not None:
self._progress.learning_rate = _safe_lr
self._progress.total_steps = event.get("total_steps", self._progress.total_steps)
self._progress.total_steps = event.get(
"total_steps", self._progress.total_steps
)
self._progress.elapsed_seconds = event.get("elapsed_seconds")
self._progress.eta_seconds = event.get("eta_seconds")
self._progress.grad_norm = event.get("grad_norm")
@ -635,7 +652,9 @@ class TrainingBackend:
try:
eval_loss = float(eval_loss)
except (TypeError, ValueError):
logger.debug("Could not convert eval_loss to float: %s", eval_loss)
logger.debug(
"Could not convert eval_loss to float: %s", eval_loss
)
eval_loss = None
if step > 0 and eval_loss is not None and math.isfinite(eval_loss):
self.eval_loss_history.append(eval_loss)
@ -665,9 +684,12 @@ class TrainingBackend:
"job_id": self.current_job_id,
"model_name": self._db_config["model_name"],
"dataset_name": self._db_config.get("hf_dataset")
or next(iter(self._db_config.get("local_datasets") or []), "unknown"),
or next(
iter(self._db_config.get("local_datasets") or []), "unknown"
),
"config_json": _json.dumps(self._db_config),
"started_at": self._db_started_at or datetime.now(timezone.utc).isoformat(),
"started_at": self._db_started_at
or datetime.now(timezone.utc).isoformat(),
"total_steps": event.get("total_steps"),
}
elif (
@ -745,7 +767,9 @@ class TrainingBackend:
elif db_action == "update_total_steps":
try:
from storage.studio_db import update_run_total_steps
update_run_total_steps(db_action_kwargs["job_id"], db_action_kwargs["total_steps"])
update_run_total_steps(
db_action_kwargs["job_id"], db_action_kwargs["total_steps"]
)
self._db_total_steps_set = True
except Exception:
logger.warning("Failed to update total_steps in DB", exc_info = True)
@ -772,12 +796,15 @@ class TrainingBackend:
model_name = self._db_config["model_name"],
dataset_name = dataset_name,
config_json = _json.dumps(self._db_config),
started_at = self._db_started_at or datetime.now(timezone.utc).isoformat(),
started_at = self._db_started_at
or datetime.now(timezone.utc).isoformat(),
total_steps = self._progress.total_steps or None,
)
self._db_run_created = True
except Exception:
logger.warning("Failed to create DB run record for early failure", exc_info = True)
logger.warning(
"Failed to create DB run record for early failure", exc_info = True
)
def _finalize_run_in_db(
self,
@ -800,7 +827,10 @@ class TrainingBackend:
ended_at = datetime.now(timezone.utc).isoformat(),
final_step = self._progress.step,
final_loss = self._progress.loss
if (self._progress.loss is not None and math.isfinite(self._progress.loss))
if (
self._progress.loss is not None
and math.isfinite(self._progress.loss)
)
else None,
duration_seconds = self._progress.elapsed_seconds,
loss_sparkline = _json.dumps(sparkline),
@ -809,11 +839,17 @@ class TrainingBackend:
)
self._run_finalized = True
except Exception:
logger.warning("Failed to finalize run in DB (status=%s)", status, exc_info = True)
logger.warning(
"Failed to finalize run in DB (status=%s)", status, exc_info = True
)
def _flush_metrics_to_db(self) -> None:
"""Flush buffered metrics to the database and update live progress."""
if not self._metric_buffer or not self.current_job_id or not self._db_run_created:
if (
not self._metric_buffer
or not self.current_job_id
or not self._db_run_created
):
return
# Cap buffer to bound memory growth.
if len(self._metric_buffer) > 500:
@ -833,7 +869,10 @@ class TrainingBackend:
id = self.current_job_id,
step = self._progress.step,
loss = self._progress.loss
if (self._progress.loss is not None and math.isfinite(self._progress.loss))
if (
self._progress.loss is not None
and math.isfinite(self._progress.loss)
)
else None,
duration_seconds = self._progress.elapsed_seconds,
)
@ -951,7 +990,9 @@ class TrainingBackend:
else:
title = "Training Loss"
ax.set_title(title, fontsize = 11, fontweight = "bold", pad = 10, color = style["text"])
ax.set_title(
title, fontsize = 11, fontweight = "bold", pad = 10, color = style["text"]
)
ax.grid(True, alpha = 0.4, linestyle = "--", color = style["grid_color"])
ax.tick_params(colors = style["text"], which = "both")
ax.spines["top"].set_visible(False)

View file

@ -36,7 +36,8 @@ from typing import Any, Callable
if sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ:
try:
if os.path.exists("/dev/dxg") and any(
os.path.exists(_p + "/librocdxg.so") for _p in ("/opt/rocm/lib", "/opt/rocm/lib64")
os.path.exists(_p + "/librocdxg.so")
for _p in ("/opt/rocm/lib", "/opt/rocm/lib64")
):
os.environ["HSA_ENABLE_DXG_DETECTION"] = "1"
except Exception:
@ -54,7 +55,9 @@ from utils.wheel_utils import (
)
def _output_dir_from_resume_checkpoint(resume_from_checkpoint: str | None) -> str | None:
def _output_dir_from_resume_checkpoint(
resume_from_checkpoint: str | None,
) -> str | None:
if not resume_from_checkpoint:
return None
path = Path(resume_from_checkpoint)
@ -117,7 +120,9 @@ if sys.platform == "win32":
try:
if os.path.isdir(_default_root):
for _ver in sorted(os.listdir(_default_root), key = _ver_key, reverse = True):
for _ver in sorted(
os.listdir(_default_root), key = _ver_key, reverse = True
):
_bin = os.path.join(_default_root, _ver, "bin")
if os.path.isdir(_bin):
_candidates.append(_bin)
@ -258,7 +263,9 @@ def _install_package_wheel_first(
"(this may take several minutes)..."
)
else:
pypi_status_message = f"Installing {display_name} from PyPI for faster training..."
pypi_status_message = (
f"Installing {display_name} from PyPI for faster training..."
)
_send_status(event_queue, pypi_status_message)
@ -343,7 +350,8 @@ def _install_package_wheel_first(
)
_send_status(
event_queue,
f"{display_name} installation timed out after " f"{_run_kwargs.get('timeout')}s",
f"{display_name} installation timed out after "
f"{_run_kwargs.get('timeout')}s",
)
return False
@ -461,7 +469,9 @@ def _ensure_flash_linear_attention_unconditional(event_queue: Any) -> bool:
if os.getenv(_FLA_SKIP_ENV) == "1":
return False
if sys.platform == "win32":
logger.info("Skipping flash-linear-attention install: no prebuilt wheel for Windows")
logger.info(
"Skipping flash-linear-attention install: no prebuilt wheel for Windows"
)
return False
if sys.version_info < _FLA_MIN_PYTHON:
logger.info(
@ -536,7 +546,9 @@ def _ensure_flash_linear_attention_unconditional(event_queue: Any) -> bool:
)
except _sp.TimeoutExpired:
logger.warning("flash-linear-attention install timed out; continuing")
_send_status(event_queue, "flash-linear-attention install timed out; continuing")
_send_status(
event_queue, "flash-linear-attention install timed out; continuing"
)
return False
if result.returncode != 0:
@ -727,7 +739,10 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]:
# Arch attrs absent — fall back to device-name matching.
dev_lower = (getattr(props, "name", "") or "").lower()
is_unified = (
"890m" in dev_lower or "880m" in dev_lower or "8060s" in dev_lower or "8050s" in dev_lower
"890m" in dev_lower
or "880m" in dev_lower
or "8060s" in dev_lower
or "8050s" in dev_lower
)
return gcn_arch, is_unified
@ -770,7 +785,9 @@ def _run_pip(cmd: list[str], event_queue: Any, label: str) -> bool:
_send_status(event_queue, f"{label} install timed out; continuing")
return False
if result.returncode != 0:
logger.warning("%s install failed (continuing without it):\n%s", label, result.stdout)
logger.warning(
"%s install failed (continuing without it):\n%s", label, result.stdout
)
_send_status(event_queue, f"{label} install failed; continuing")
return False
return True
@ -871,7 +888,9 @@ def _ensure_tilelang_backend(event_queue: Any, model_name: str) -> None:
# UNSLOTH_STUDIO_SKIP_FAST_PATH_HOOKS=1 falls back to the substring path.
def _rebind_in_already_imported_modules(*, attr_name: str, old_obj: Any, new_obj: Any) -> int:
def _rebind_in_already_imported_modules(
*, attr_name: str, old_obj: Any, new_obj: Any
) -> int:
"""Rebind `attr_name -> new_obj` in every module that imported `old_obj`.
`from X import Y` creates a local binding that reassigning X.Y won't reach.
@ -944,7 +963,9 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
try:
ok = bool(install_fn(event_queue))
except Exception as exc:
logger.warning("%s install raised: %s; falling back to torch", gate_name, exc)
logger.warning(
"%s install raised: %s; falling back to torch", gate_name, exc
)
ok = False
logger.info("%s hook done; available=%s", gate_name, ok)
# post_available_fn handles "gate already True but ancillary kernel broken"
@ -953,7 +974,9 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
try:
post_available_fn(event_queue)
except Exception as exc:
logger.warning("%s post-available step raised: %s; continuing", gate_name, exc)
logger.warning(
"%s post-available step raised: %s; continuing", gate_name, exc
)
state["installed"] = True
return ok
@ -964,7 +987,9 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
def _fla_install(eq: Any) -> bool:
# FLA alone ~2.35x; +tilelang adds ~26%. tilelang is GDN-only (Qwen3.5 family).
if not _ensure_flash_linear_attention_unconditional(eq):
logger.info("FLA install did not produce an importable runtime; skipping TileLang")
logger.info(
"FLA install did not produce an importable runtime; skipping TileLang"
)
return False
if _model_wants_tilelang(model_name):
_ensure_tilelang_backend_unconditional(eq)
@ -979,7 +1004,10 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
# FLA imports; repair tilelang if missing or on the broken tvm-ffi list.
if not _model_wants_tilelang(model_name):
return
if _installed_tvm_ffi_version() not in _TVM_FFI_BROKEN_VERSIONS and _tilelang_importable():
if (
_installed_tvm_ffi_version() not in _TVM_FFI_BROKEN_VERSIONS
and _tilelang_importable()
):
return
_ensure_tilelang_backend_unconditional(eq)
@ -995,7 +1023,9 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
pypi_version = _CAUSAL_CONV1D_PACKAGE_VERSION,
filename_prefix = "causal_conv1d",
release_tag = _CAUSAL_CONV1D_RELEASE_TAG,
release_base_url = ("https://github.com/Dao-AILab/causal-conv1d/releases/download"),
release_base_url = (
"https://github.com/Dao-AILab/causal-conv1d/releases/download"
),
)
return bool(ok)
@ -1015,7 +1045,9 @@ def _install_fast_path_hooks(event_queue: Any, model_name: str) -> None:
rebound = _rebind_in_already_imported_modules(
attr_name = gate_name, old_obj = original, new_obj = wrapped
)
logger.info("Installed fast-path hook on %s (rebound %d modules)", gate_name, rebound)
logger.info(
"Installed fast-path hook on %s (rebound %d modules)", gate_name, rebound
)
def _should_try_runtime_flash_attn_install(max_seq_length: int) -> bool:
@ -1171,7 +1203,10 @@ def _resize_mlx_vlm_images(
image_layout = None,
):
if isinstance(value, list):
return [_resize_mlx_vlm_image(image, resize, image_layout = image_layout) for image in value]
return [
_resize_mlx_vlm_image(image, resize, image_layout = image_layout)
for image in value
]
return _resize_mlx_vlm_image(value, resize, image_layout = image_layout)
@ -1255,7 +1290,8 @@ def _normalize_mlx_studio_optimizer(value):
except KeyError:
supported = ", ".join(sorted(_MLX_STUDIO_OPTIM_MAP))
raise ValueError(
f"Unsupported optimizer for MLX training: {value!r}. " f"Supported values: {supported}."
f"Unsupported optimizer for MLX training: {value!r}. "
f"Supported values: {supported}."
)
@ -1277,7 +1313,9 @@ def _resolve_mlx_local_dataset_files(file_paths: list) -> list[str]:
all_files: list[str] = []
for dataset_file in file_paths or []:
file_path = (
dataset_file if os.path.isabs(dataset_file) else str(resolve_dataset_path(dataset_file))
dataset_file
if os.path.isabs(dataset_file)
else str(resolve_dataset_path(dataset_file))
)
file_path_obj = Path(file_path)
@ -1409,7 +1447,9 @@ def _run_mlx_training(event_queue, stop_queue, config):
raise NotImplementedError(message)
optim_name = _normalize_mlx_studio_optimizer(config.get("optim", "adamw_8bit"))
lr_scheduler_type = _normalize_mlx_studio_scheduler(config.get("lr_scheduler_type", "linear"))
lr_scheduler_type = _normalize_mlx_studio_scheduler(
config.get("lr_scheduler_type", "linear")
)
# ── 1. Load model ──
# Force text-only for non-image datasets even on vision-capable models
@ -1489,9 +1529,15 @@ def _run_mlx_training(event_queue, stop_queue, config):
finetune_language = config.get("finetune_language_layers", True)
finetune_attention = config.get("finetune_attention_modules", True)
finetune_mlp = config.get("finetune_mlp_modules", True)
finetune_vision = config.get("finetune_vision_layers", False) if is_vlm else False
finetune_vision = (
config.get("finetune_vision_layers", False) if is_vlm else False
)
if (finetune_attention or finetune_mlp) and not finetune_language and not finetune_vision:
if (
(finetune_attention or finetune_mlp)
and not finetune_language
and not finetune_vision
):
finetune_language = True
peft_kwargs["finetune_language_layers"] = finetune_language
@ -1524,7 +1570,9 @@ def _run_mlx_training(event_queue, stop_queue, config):
if len(file_paths) == 1:
p = Path(file_paths[0])
if p.is_dir() and ((p / "dataset_info.json").exists() or (p / "state.json").exists()):
if p.is_dir() and (
(p / "dataset_info.json").exists() or (p / "state.json").exists()
):
return load_from_disk(str(p))
all_files = _resolve_mlx_local_dataset_files(file_paths)
if not all_files:
@ -1612,7 +1660,9 @@ def _run_mlx_training(event_queue, stop_queue, config):
)
else:
errors = vlm_info.get("errors", [])
raise ValueError(f"VLM dataset format conversion failed: {'; '.join(errors)}")
raise ValueError(
f"VLM dataset format conversion failed: {'; '.join(errors)}"
)
if eval_dataset is not None:
ev_info = format_and_template_dataset(
eval_dataset,
@ -1757,7 +1807,11 @@ def _run_mlx_training(event_queue, stop_queue, config):
)
template_name = MODEL_TO_TEMPLATE_MAPPER.get(model_name.lower())
markers = TEMPLATE_TO_RESPONSES_MAPPER.get(template_name) if template_name else None
markers = (
TEMPLATE_TO_RESPONSES_MAPPER.get(template_name)
if template_name
else None
)
if markers:
trainer = train_on_responses_only(
trainer,
@ -1849,7 +1903,11 @@ def _run_mlx_training(event_queue, stop_queue, config):
"train/tokens_per_sec": tok_s,
"train/peak_gb": peak_gb,
"train/num_tokens": num_tokens,
**({"train/grad_norm": grad_norm} if grad_norm is not None else {}),
**(
{"train/grad_norm": grad_norm}
if grad_norm is not None
else {}
),
},
step = step,
)
@ -1872,7 +1930,9 @@ def _run_mlx_training(event_queue, stop_queue, config):
_send("progress", step = step, eval_loss = eval_loss)
if wandb_run is not None:
try:
wandb_run.log({"eval/loss": eval_loss, "eval/perplexity": perplexity}, step = step)
wandb_run.log(
{"eval/loss": eval_loss, "eval/perplexity": perplexity}, step = step
)
except Exception:
pass
if tb_writer is not None:
@ -2174,7 +2234,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")):
import shutil as _shutil
if not _shutil.which("hipinfo.exe"):
os.environ["PATH"] = _scripts_dir + os.pathsep + os.environ.get("PATH", "")
os.environ["PATH"] = (
_scripts_dir + os.pathsep + os.environ.get("PATH", "")
)
# BNB picks a rocm DLL from torch.version.hip, but AMD's Windows BNB
# wheel may ship a DLL whose suffix doesn't match. Detect the actual
@ -2215,7 +2277,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
# so later import fixes can still redetect or opt out. DLL
# with unparsable name -> seeded value or "72".
if _found_rocm_bnb:
_bnb_rocm_ver = _bnb_rocm_ver or os.environ.get("BNB_ROCM_VERSION") or "72"
_bnb_rocm_ver = (
_bnb_rocm_ver or os.environ.get("BNB_ROCM_VERSION") or "72"
)
os.environ["BNB_ROCM_VERSION"] = _bnb_rocm_ver
os.environ["UNSLOTH_BNB_ROCM_VERSION_SOURCE"] = "detected"
logger.info(
@ -2229,7 +2293,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
# the rocm version embedded in torch.__version__ when version.hip is
# unset (AMD SDK / Radeon wheels).
def _hip_ver_at_least(major: int, minor: int) -> bool:
_hip_str = getattr(getattr(_torch_for_rocm, "version", None), "hip", None)
_hip_str = getattr(
getattr(_torch_for_rocm, "version", None), "hip", None
)
if not _hip_str:
# Try the standard "+rocmX.Y.Z" embedded version first.
_ver_match = re.search(r"rocm(\d+)\.(\d+)", _build_version_for_rocm)
@ -2321,7 +2387,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
if prev < self.shape[0]:
a_tail = self[prev:].contiguous()
b_tail = (
mat2[-1].contiguous() if mat2.dim() == 3 else mat2.contiguous()
mat2[-1].contiguous()
if mat2.dim() == 3
else mat2.contiguous()
)
pieces.append(_t.mm(a_tail, b_tail))
result = (
@ -2491,7 +2559,11 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
def _on_progress(progress: TrainingProgress):
has_train_loss = progress.step > 0 and progress.loss is not None
has_eval_loss = progress.eval_loss is not None
if (progress.step == 0 and progress.total_steps > 0) or has_train_loss or has_eval_loss:
if (
(progress.step == 0 and progress.total_steps > 0)
or has_train_loss
or has_eval_loss
):
event_queue.put(
{
"type": "progress",
@ -2601,12 +2673,15 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
if dataset is None or trainer.should_stop:
if trainer.should_stop:
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
event_queue.put(
{"type": "complete", "output_dir": None, "ts": time.time()}
)
else:
event_queue.put(
{
"type": "error",
"error": trainer.training_progress.error or "Failed to load dataset",
"error": trainer.training_progress.error
or "Failed to load dataset",
"stack": "",
"ts": time.time(),
}
@ -2627,7 +2702,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
desc = getattr(bar, "desc", "") or ""
if total > 0 and n > 0 and desc:
pct = min(int(n * 100 / total), 100)
_send_status(event_queue, f"{desc.strip()} {pct}% ({n:,}/{total:,})")
_send_status(
event_queue, f"{desc.strip()} {pct}% ({n:,}/{total:,})"
)
except (AttributeError, ReferenceError):
pass
_tqdm_stop.wait(3)
@ -2655,7 +2732,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
)
if not success or trainer.should_stop:
if trainer.should_stop:
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
event_queue.put(
{"type": "complete", "output_dir": None, "ts": time.time()}
)
else:
error_msg = trainer.training_progress.error or "Failed to load model"
event_queue.put(
@ -2696,7 +2775,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
lora_r = config.get("lora_r", 128),
lora_alpha = config.get("lora_alpha", 32),
lora_dropout = config.get("lora_dropout", 0.0),
use_gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"),
use_gradient_checkpointing = config.get(
"gradient_checkpointing", "unsloth"
),
use_rslora = config.get("use_rslora", False),
use_loftq = config.get("use_loftq", False),
)
@ -2706,13 +2787,17 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
use_lora = True,
finetune_vision_layers = config.get("finetune_vision_layers", True),
finetune_language_layers = config.get("finetune_language_layers", True),
finetune_attention_modules = config.get("finetune_attention_modules", True),
finetune_attention_modules = config.get(
"finetune_attention_modules", True
),
finetune_mlp_modules = config.get("finetune_mlp_modules", True),
target_modules = config.get("target_modules"),
lora_r = config.get("lora_r", 16),
lora_alpha = config.get("lora_alpha", 16),
lora_dropout = config.get("lora_dropout", 0.0),
use_gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"),
use_gradient_checkpointing = config.get(
"gradient_checkpointing", "unsloth"
),
use_rslora = config.get("use_rslora", False),
use_loftq = config.get("use_loftq", False),
)
@ -2722,12 +2807,15 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
if not success or trainer.should_stop:
if trainer.should_stop:
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
event_queue.put(
{"type": "complete", "output_dir": None, "ts": time.time()}
)
else:
event_queue.put(
{
"type": "error",
"error": trainer.training_progress.error or "Failed to prepare model",
"error": trainer.training_progress.error
or "Failed to prepare model",
"stack": "",
"ts": time.time(),
}
@ -2783,7 +2871,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
ensure_dir(Path(tensorboard_dir))
# Start training directly — no inner thread, we ARE the subprocess.
dataset_display = config.get("hf_dataset", "") or config.get("uploaded_file", "") or ""
dataset_display = (
config.get("hf_dataset", "") or config.get("uploaded_file", "") or ""
)
_send_status(
event_queue,
f'Training "{model_name}"'
@ -2807,7 +2897,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
weight_decay = config.get("weight_decay", 0.001),
random_seed = config.get("random_seed", 3407),
packing = config.get("packing", False),
train_on_completions = False if is_cpt else config.get("train_on_completions", False),
train_on_completions = False
if is_cpt
else config.get("train_on_completions", False),
enable_wandb = config.get("enable_wandb", False),
wandb_project = config.get("wandb_project", "unsloth-training"),
wandb_token = config.get("wandb_token"),
@ -3068,7 +3160,9 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
if candidates:
all_files.extend(str(c) for c in candidates)
continue
raise ValueError(f"No supported data files in directory: {file_path_obj}")
raise ValueError(
f"No supported data files in directory: {file_path_obj}"
)
else:
all_files.append(file_path)
@ -3182,7 +3276,9 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
resume_from_checkpoint
)
if not output_dir:
output_dir = str(resolve_output_dir(f"{model_name.replace('/', '_')}_{int(time.time())}"))
output_dir = str(
resolve_output_dir(f"{model_name.replace('/', '_')}_{int(time.time())}")
)
output_dir = str(resolve_output_dir(output_dir))
num_epochs = config.get("num_epochs", 2)

View file

@ -61,14 +61,17 @@ async def list_cached_datasets(current_subject: str = Depends(get_current_subjec
@router.delete("/cached", response_model = DeleteCachedDatasetResponse)
async def delete_cached_dataset(
repo_id: str = Body(..., embed = True), current_subject: str = Depends(get_current_subject)
repo_id: str = Body(..., embed = True),
current_subject: str = Depends(get_current_subject),
):
return await cache_inventory.delete_cached_dataset_response(repo_id)
@router.get("/download-progress", response_model = DownloadProgressResponse)
async def get_dataset_download_progress(
repo_id: str = Query(..., description = "HuggingFace dataset repo ID, e.g. 'unsloth/LaTeX_OCR'"),
repo_id: str = Query(
..., description = "HuggingFace dataset repo ID, e.g. 'unsloth/LaTeX_OCR'"
),
expected_bytes: int = Query(0, description = "Expected total download size in bytes"),
hf_token: Optional[str] = Depends(get_hf_token),
current_subject: str = Depends(get_current_subject),
@ -89,9 +92,12 @@ async def download_dataset(
return await downloads.download_dataset_response(body, hf_token)
@router.post("/download/cancel", response_model = CancelDatasetDownloadResponse, status_code = 202)
@router.post(
"/download/cancel", response_model = CancelDatasetDownloadResponse, status_code = 202
)
async def cancel_dataset_download(
body: CancelDatasetDownloadRequest, current_subject: str = Depends(get_current_subject)
body: CancelDatasetDownloadRequest,
current_subject: str = Depends(get_current_subject),
):
return await downloads.cancel_dataset_download_response(body)

View file

@ -130,7 +130,9 @@ async def cancel_download_model(
@router.get("/download-status", response_model = DownloadJobStatus)
async def get_download_status(
repo_id: str = Query(..., description = "HuggingFace repo ID"),
gguf_variant: str = Query("", description = "Quantization variant (empty for safetensors)"),
gguf_variant: str = Query(
"", description = "Quantization variant (empty for safetensors)"
),
current_subject: str = Depends(get_current_subject),
):
return await downloads.get_download_status_response(repo_id, gguf_variant)
@ -147,7 +149,9 @@ async def get_active_downloads(
@router.get("/transport-status", response_model = TransportStatusResponse)
async def get_model_transport_status(
repo_id: str = Query(..., description = "HuggingFace repo ID"),
gguf_variant: str = Query("", description = "Quantization variant (empty for safetensors)"),
gguf_variant: str = Query(
"", description = "Quantization variant (empty for safetensors)"
),
hf_token: Optional[str] = Depends(get_hf_token),
current_subject: str = Depends(get_current_subject),
):

View file

@ -7,7 +7,9 @@ from pydantic import BaseModel, Field
from typing import List, Literal, Optional
DownloadJobState = Literal["idle", "running", "cancelling", "cancelled", "complete", "error"]
DownloadJobState = Literal[
"idle", "running", "cancelling", "cancelled", "complete", "error"
]
class DownloadModelRequest(BaseModel):

View file

@ -17,13 +17,19 @@ ModelRuntime = Literal["llama_cpp", "transformers", "adapter", "unknown"]
class GgufVariantDetail(BaseModel):
"""A single GGUF quantization variant in a HuggingFace repo."""
filename: str = Field(..., description = "GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')")
quant: str = Field(..., description = "Quantization label or internal GGUF variant key")
filename: str = Field(
..., description = "GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')"
)
quant: str = Field(
..., description = "Quantization label or internal GGUF variant key"
)
display_label: Optional[str] = Field(
None, description = "Optional user-facing label when quant is an internal key"
)
size_bytes: int = Field(0, description = "File size in bytes")
download_size_bytes: int = Field(0, description = "Total bytes needed to download this variant")
download_size_bytes: int = Field(
0, description = "Total bytes needed to download this variant"
)
downloaded: bool = Field(
False, description = "Whether this variant is already in the local HF cache"
)
@ -132,7 +138,9 @@ class LocalModelInfo(BaseModel):
class LocalModelListResponse(BaseModel):
"""Response schema for listing local/cached models."""
models_dir: str = Field(..., description = "Directory scanned for custom local models")
models_dir: str = Field(
..., description = "Directory scanned for custom local models"
)
hf_cache_dir: Optional[str] = Field(
None,
description = "HF cache root that was scanned",

View file

@ -12,7 +12,9 @@ from fastapi import HTTPException
from hub.utils.hf_cache_state import resolve_destructive_case_matches
def resolve_destructive_repo_ids(repo_id: str, candidates: Iterable[str], *, noun: str) -> set[str]:
def resolve_destructive_repo_ids(
repo_id: str, candidates: Iterable[str], *, noun: str
) -> set[str]:
"""Cache-dir repo ids a destructive op on *repo_id* may target.
Refuses with 409 on ambiguous case-only matches so a delete never removes

View file

@ -194,7 +194,9 @@ def _hf_datasets_cache_roots() -> list[Path]:
if hf_home:
_add(Path(hf_home).expanduser() / "datasets")
xdg_cache = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")).expanduser()
xdg_cache = Path(
os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")
).expanduser()
_add(xdg_cache / "huggingface" / "datasets")
return roots
@ -284,7 +286,11 @@ def _scan_hf_dataset_caches() -> list[dict]:
rev_id = getattr(rev, "commit_hash", None) or str(id(rev))
for f in rev.files:
blob_path = getattr(f, "blob_path", None)
key = str(blob_path) if blob_path else f"{rev_id}:{f.file_name}"
key = (
str(blob_path)
if blob_path
else f"{rev_id}:{f.file_name}"
)
unique_blobs[key] = int(f.size_on_disk or 0)
total_size = sum(unique_blobs.values())
key = repo_info.repo_id.lower()
@ -320,7 +326,9 @@ def _scan_hf_dataset_caches() -> list[dict]:
existing = seen_lower.get(key)
if _prefer_dataset_cache_row(row, existing):
seen_lower[key] = row
elif existing is not None and bool(existing.get("partial")) == bool(row.get("partial")):
elif existing is not None and bool(existing.get("partial")) == bool(
row.get("partial")
):
existing["size_bytes"] = max(existing["size_bytes"], row["size_bytes"])
existing["cache_path"] = existing.get("cache_path") or row.get("cache_path")
if (
@ -332,7 +340,9 @@ def _scan_hf_dataset_caches() -> list[dict]:
for row in _scan_processed_dataset_caches():
key = row["repo_id"].lower()
existing = seen_lower.get(key)
if existing is None or (bool(existing.get("partial")) and not bool(row.get("partial"))):
if existing is None or (
bool(existing.get("partial")) and not bool(row.get("partial"))
):
seen_lower[key] = row
else:
existing["size_bytes"] = max(existing["size_bytes"], row["size_bytes"])
@ -366,7 +376,9 @@ async def delete_cached_dataset_response(repo_id: str) -> dict:
if not _is_valid_repo_id(repo_id):
raise HTTPException(status_code = 400, detail = "Invalid repo_id format")
repo_key = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset")
repo_key = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "dataset"
)
if not downloads.registry.begin_delete(repo_key):
raise HTTPException(
status_code = 400,
@ -401,7 +413,9 @@ def _delete_cached_dataset_blocking(repo_id: str) -> dict:
if str(repo_info.repo_id) not in matched_repo_ids:
continue
try:
strategy = hf_cache.delete_revisions(*(rev.commit_hash for rev in repo_info.revisions))
strategy = hf_cache.delete_revisions(
*(rev.commit_hash for rev in repo_info.revisions)
)
strategy.execute()
deleted = True
except Exception as exc:
@ -430,7 +444,9 @@ def _delete_cached_dataset_blocking(repo_id: str) -> dict:
cache_purged = purge_repo_cache_dirs("dataset", repo_id)
partial_purged = purge_partial_repo("dataset", repo_id)
state_purged = download_manifest.purge_all_state_for_repo("dataset", repo_id) > 0
if not (deleted or processed_deleted or cache_purged or partial_purged or state_purged):
if not (
deleted or processed_deleted or cache_purged or partial_purged or state_purged
):
raise HTTPException(status_code = 404, detail = "Dataset not found in cache")
return {"status": "deleted", "repo_id": repo_id}

View file

@ -37,9 +37,7 @@ from hub.utils.snapshot_filters import (
logger = get_logger(__name__)
_dataset_size_cache: "OrderedDict[str, tuple[int, frozenset[str], bool, str, float]]" = (
OrderedDict()
)
_dataset_size_cache: "OrderedDict[str, tuple[int, frozenset[str], bool, str, float]]" = OrderedDict()
_dataset_size_neg_cache: "OrderedDict[tuple[str, str], float]" = OrderedDict()
_DATASET_SIZE_CACHE_MAX = 256
_DATASET_SIZE_POS_TTL = 60.0
@ -88,7 +86,9 @@ def get_dataset_snapshot_metadata_cached(
)
total = total_size_for_siblings(info.siblings)
hashes = blob_hashes_for_siblings(info.siblings)
restricted = bool(getattr(info, "private", False) or getattr(info, "gated", False))
restricted = bool(
getattr(info, "private", False) or getattr(info, "gated", False)
)
except Exception:
with _dataset_size_cache_lock:
_dataset_size_neg_cache[cache_key] = time.monotonic()
@ -132,7 +132,9 @@ async def get_dataset_download_progress_response(
)
def _dataset_status(key: str, *, repo_id: Optional[str] = None) -> DatasetDownloadJobStatus:
def _dataset_status(
key: str, *, repo_id: Optional[str] = None
) -> DatasetDownloadJobStatus:
state, error, generation = download_lifecycle.idle_status(
_registry,
key,
@ -154,7 +156,9 @@ async def download_dataset_response(
detail = f"Invalid repo_id: {repo_id!r}",
)
# Canonicalize so two different-cased paste-ins share one job + cache dir.
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "dataset"
)
key = _download_job_key(repo_id)
transport = download_lifecycle.resolve_transport(body.use_xet)
@ -211,7 +215,9 @@ async def cancel_dataset_download_response(body: CancelDatasetDownloadRequest) -
status_code = 400,
detail = f"Invalid repo_id: {repo_id!r}",
)
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "dataset"
)
key = _download_job_key(repo_id)
state = download_lifecycle.cancel_worker(
@ -224,21 +230,29 @@ async def cancel_dataset_download_response(body: CancelDatasetDownloadRequest) -
return {"repo_id": repo_id, "state": state}
async def get_dataset_download_status_response(repo_id: str) -> DatasetDownloadJobStatus:
async def get_dataset_download_status_response(
repo_id: str,
) -> DatasetDownloadJobStatus:
"""Return the latest state of a background dataset download job."""
repo_id = repo_id.strip()
if not _is_valid_repo_id(repo_id):
return DatasetDownloadJobStatus(state = "idle")
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "dataset"
)
return _dataset_status(_download_job_key(repo_id), repo_id = repo_id)
async def get_active_dataset_downloads_response(repo_id: str = "") -> ActiveDownloadsResponse:
async def get_active_dataset_downloads_response(
repo_id: str = "",
) -> ActiveDownloadsResponse:
repo_id = repo_id.strip()
if repo_id and not _is_valid_repo_id(repo_id):
return ActiveDownloadsResponse(downloads = [])
canonical_repo_id = (
await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset")
await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "dataset"
)
if repo_id
else None
)
@ -260,7 +274,9 @@ async def get_dataset_transport_status_response(repo_id: str) -> dict:
return {"has_partial": False, "last_transport": None, "resumable": False}
return {
"has_partial": has_active_incomplete_blobs("dataset", repo_id),
"last_transport": download_registry.read_active_transport_marker("dataset", repo_id),
"last_transport": download_registry.read_active_transport_marker(
"dataset", repo_id
),
"resumable": download_registry.is_resumable_partial("dataset", repo_id),
}

View file

@ -177,10 +177,14 @@ def _repo_file_matches_split(path: str, split: str) -> bool:
def _select_tier1_repo_file(
files: list[str], *, subset: Optional[str], train_split: str
) -> Optional[str]:
data_files = sorted(f for f in files if any(f.lower().endswith(ext) for ext in DATA_EXTS))
data_files = sorted(
f for f in files if any(f.lower().endswith(ext) for ext in DATA_EXTS)
)
if not data_files:
return None
tabular_files = [f for f in data_files if any(f.lower().endswith(ext) for ext in _TABULAR_EXTS)]
tabular_files = [
f for f in data_files if any(f.lower().endswith(ext) for ext in _TABULAR_EXTS)
]
candidates = tabular_files or data_files
if subset:
candidates = [f for f in candidates if _repo_file_matches_label(f, subset)]
@ -405,7 +409,9 @@ def check_format_response(
processed = format_dataset_preview(preview_slice)
preview_samples = _serialize_preview_rows(processed)
except Exception as e:
logger.warning(f"Processed preview generation failed (non-fatal): {e}")
logger.warning(
f"Processed preview generation failed (non-fatal): {e}"
)
preview_samples = _serialize_preview_rows(preview_slice)
else:
preview_samples = _serialize_preview_rows(preview_slice)
@ -416,7 +422,9 @@ def check_format_response(
if image_col and image_col in (result.get("columns") or []):
try:
sample_val = preview_slice[0][image_col]
if isinstance(sample_val, str) and sample_val.startswith(("http://", "https://")):
if isinstance(sample_val, str) and sample_val.startswith(
("http://", "https://")
):
url_warning = (
"This dataset contains image URLs instead of embedded images. "
"Images will be downloaded during training, which may be slow for large datasets."
@ -483,7 +491,8 @@ def ai_assist_mapping_response(
from hub.utils.llm_assist import llm_conversion_advisor
truncated = [
{col: str(s.get(col, ""))[:200] for col in request.columns} for s in request.samples[:5]
{col: str(s.get(col, ""))[:200] for col in request.columns}
for s in request.samples[:5]
]
result = llm_conversion_advisor(

View file

@ -223,7 +223,9 @@ def _stream_file_preview_slice(path: Path, preview_size: int):
return Dataset.from_list(rows), None
def _load_local_preview_slice(*, dataset_path: Path, train_split: str, preview_size: int):
def _load_local_preview_slice(
*, dataset_path: Path, train_split: str, preview_size: int
):
# Non-streaming loads take the cached builder lock; use the EACCES-safe wrapper.
from utils.datasets.cache_safe import load_dataset_cache_safe as load_dataset
@ -258,7 +260,9 @@ def _load_local_preview_slice(*, dataset_path: Path, train_split: str, preview_s
# Parquet/Arrow give a cheap exact total_rows via len()+select; JSON/CSV
# carry no such metadata, so stream them and report total_rows=None.
if suffix == ".parquet":
dataset = load_dataset("parquet", data_files = str(dataset_path), split = train_split)
dataset = load_dataset(
"parquet", data_files = str(dataset_path), split = train_split
)
total_rows = len(dataset)
preview_slice = dataset.select(range(min(preview_size, total_rows)))
return preview_slice, total_rows
@ -272,7 +276,9 @@ def _load_local_preview_slice(*, dataset_path: Path, train_split: str, preview_s
)
return preview
raise HTTPException(status_code = 400, detail = f"Unsupported file format: {dataset_path.suffix}")
raise HTTPException(
status_code = 400, detail = f"Unsupported file format: {dataset_path.suffix}"
)
def _sanitize_filename(filename: str) -> str:
@ -285,7 +291,10 @@ def _sanitize_filename(filename: str) -> str:
def _upload_too_large(size_bytes: int) -> HTTPException:
return HTTPException(
status_code = 413,
detail = (f"Upload is too large " f"({size_bytes:,} bytes; max {LOCAL_UPLOAD_MAX_BYTES:,})."),
detail = (
f"Upload is too large "
f"({size_bytes:,} bytes; max {LOCAL_UPLOAD_MAX_BYTES:,})."
),
)

View file

@ -26,8 +26,12 @@ def backend_dir() -> Path:
def resolve_transport(use_xet: bool) -> str:
transport = download_registry.TRANSPORT_XET if use_xet else download_registry.TRANSPORT_HTTP
unavailable_reason = download_registry.download_transport_unavailable_reason(transport)
transport = (
download_registry.TRANSPORT_XET if use_xet else download_registry.TRANSPORT_HTTP
)
unavailable_reason = download_registry.download_transport_unavailable_reason(
transport
)
if unavailable_reason is not None:
raise HTTPException(status_code = 400, detail = unavailable_reason)
return transport
@ -49,7 +53,9 @@ def spawn_worker(
shared ``.incomplete`` (e.g. bundled mmproj) is never deleted.
"""
cwd = backend_dir()
mode = download_registry.TRANSPORT_XET if use_xet else download_registry.TRANSPORT_HTTP
mode = (
download_registry.TRANSPORT_XET if use_xet else download_registry.TRANSPORT_HTTP
)
env = os.environ.copy()
if protected_blob_hashes:
env["UNSLOTH_PROTECTED_BLOB_HASHES"] = ",".join(sorted(protected_blob_hashes))
@ -73,7 +79,9 @@ def spawn_worker(
if hf_token:
env["HF_TOKEN"] = hf_token
existing_path = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = f"{cwd}{os.pathsep}{existing_path}" if existing_path else str(cwd)
env["PYTHONPATH"] = (
f"{cwd}{os.pathsep}{existing_path}" if existing_path else str(cwd)
)
return subprocess.Popen(
[
sys.executable,
@ -216,7 +224,9 @@ def finalize_worker_exit(
f"{label}: {stderr_text}"
)
else:
logger.info(f"{log_prefix} worker diagnostics for {label}: {stderr_text}")
logger.info(
f"{log_prefix} worker diagnostics for {label}: {stderr_text}"
)
logger.info(f"{log_prefix} complete: {label}")
# Defensive cleanup: the canonical clear is at download-start; this
# catches the rare case where that failed but the download succeeded.
@ -422,13 +432,18 @@ def idle_status(
def active_download_refs(
registry: download_registry.DownloadRegistry, repo_id: Optional[str], *, with_variant: bool
registry: download_registry.DownloadRegistry,
repo_id: Optional[str],
*,
with_variant: bool,
) -> list[ActiveDownload]:
downloads: list[ActiveDownload] = []
for ref in registry.active_job_refs(repo_id):
metadata = ref.metadata
if with_variant:
ref_repo_id = metadata.repo_id if metadata is not None else ref.key.split("::", 1)[0]
ref_repo_id = (
metadata.repo_id if metadata is not None else ref.key.split("::", 1)[0]
)
if metadata is not None:
variant = metadata.variant
else:

View file

@ -39,7 +39,9 @@ from hub.services.models.common import (
logger = get_logger(__name__)
_repo_size_cache: "OrderedDict[tuple[str, str], tuple[int, frozenset[str], float]]" = OrderedDict()
_repo_size_cache: "OrderedDict[tuple[str, str], tuple[int, frozenset[str], float]]" = (
OrderedDict()
)
_repo_size_neg_cache: "OrderedDict[tuple[str, str], float]" = OrderedDict()
_REPO_SIZE_CACHE_MAX = 256
_REPO_SIZE_POS_TTL = 60.0
@ -168,7 +170,9 @@ def _scan_cached_gguf() -> list[dict]:
continue
repo_id = repo_info.repo_id
total_size = _repo_gguf_size_bytes(repo_info)
has_variant_state, variant_state_size = _gguf_variant_state_summary(repo_id)
has_variant_state, variant_state_size = _gguf_variant_state_summary(
repo_id
)
if total_size == 0 and not has_variant_state:
continue
partial = hf_cache_scan.is_gguf_repo_partial(
@ -239,7 +243,9 @@ def _repo_non_gguf_model_payload(repo_info) -> _CachedNonGgufPayload:
has_transformers_safetensors = False
has_checkpoint = False
def _record_blob(target: dict[str, int], file_obj, rev_id: str, file_name: str) -> None:
def _record_blob(
target: dict[str, int], file_obj, rev_id: str, file_name: str
) -> None:
blob_path = getattr(file_obj, "blob_path", None)
size = int(file_obj.size_on_disk or 0)
key = str(blob_path) if blob_path else f"{rev_id}:{file_name}"
@ -367,7 +373,9 @@ def _cached_model_local_metadata(repo_path: Path) -> dict:
result["library_name"] = library_name.strip()
tags = card.get("tags")
if isinstance(tags, list):
clean_tags = [tag.strip() for tag in tags if isinstance(tag, str) and tag.strip()]
clean_tags = [
tag.strip() for tag in tags if isinstance(tag, str) and tag.strip()
]
if clean_tags:
result["tags"] = clean_tags
return result

View file

@ -84,7 +84,9 @@ def _is_model_directory(d: Path) -> bool:
return False
try:
has_config = (d / "config.json").exists() or (d / "adapter_config.json").exists()
has_config = (d / "config.json").exists() or (
d / "adapter_config.json"
).exists()
if not has_config:
return False
return any(_is_weight_file(f) for f in d.iterdir() if f.is_file())
@ -193,7 +195,9 @@ def _apply_format_aware_partial(
continue
# GGUF row-level transport is ambiguous (variants may differ); per-variant
# detail lives on GgufVariantDetail.partial_transport via the variants endpoint.
partial_transport = None if row.model_format == "gguf" else snapshot_partial_transport
partial_transport = (
None if row.model_format == "gguf" else snapshot_partial_transport
)
rewritten.append(
row.model_copy(
update = {
@ -217,7 +221,9 @@ def _weight_basename(name: str) -> str:
def _is_adapter_weight_name(name: str) -> bool:
lower = _weight_basename(name)
return lower.startswith("adapter_model") and lower.endswith((".safetensors", ".bin"))
return lower.startswith("adapter_model") and lower.endswith(
(".safetensors", ".bin")
)
def _is_transformers_safetensors_weight_name(name: str) -> bool:
@ -267,7 +273,9 @@ def _classify_non_gguf_model_format(
has_checkpoint_weights: bool,
trusted_hf_cache_repo: bool = False,
) -> Optional[ModelFormat]:
if has_safetensors and (has_config or (trusted_hf_cache_repo and has_transformers_safetensors)):
if has_safetensors and (
has_config or (trusted_hf_cache_repo and has_transformers_safetensors)
):
return "safetensors"
if has_adapter_config and has_adapter_weights:
return "adapter"
@ -278,7 +286,9 @@ def _classify_non_gguf_model_format(
def _is_main_gguf_filename(name: str) -> bool:
return (
_is_gguf_filename(name) and not _is_mmproj_filename(name) and not _is_mtp_drafter_path(name)
_is_gguf_filename(name)
and not _is_mmproj_filename(name)
and not _is_mtp_drafter_path(name)
)
@ -445,7 +455,8 @@ def _local_model_info(
),
load_id = load_id,
model_id = model_id,
display_name = display_name or (scan_path.stem if scan_path.is_file() else scan_path.name),
display_name = display_name
or (scan_path.stem if scan_path.is_file() else scan_path.name),
path = str(load_path),
size_bytes = max(0, int(size_bytes or 0)),
source = source,
@ -520,12 +531,17 @@ def _classify_local_path(
(scan_path / "adapter_config.json").is_file() if scan_path.is_dir() else False
)
adapter_config = _read_adapter_config(scan_path) if has_adapter_config else {}
adapter_base_model = _clean_optional_string(adapter_config.get("base_model_name_or_path"))
adapter_base_model = _clean_optional_string(
adapter_config.get("base_model_name_or_path")
)
adapter_type = _clean_optional_string(adapter_config.get("peft_type"))
training_method = _clean_optional_string(adapter_config.get("unsloth_training_method"))
training_method = _clean_optional_string(
adapter_config.get("unsloth_training_method")
)
has_adapter_weights = any(_is_adapter_weight_file(f) for f in files)
has_safetensors = any(
f.suffix.lower() == ".safetensors" and not _is_adapter_weight_file(f) for f in files
f.suffix.lower() == ".safetensors" and not _is_adapter_weight_file(f)
for f in files
)
has_transformers_safetensors = any(
_is_transformers_safetensors_weight_file(f) and not _is_adapter_weight_file(f)
@ -554,7 +570,9 @@ def _classify_local_path(
if f.suffix.lower() == ".safetensors" and not _is_adapter_weight_file(f)
)
else:
size_bytes = _sum_file_sizes(f for f in files if _is_checkpoint_weight_file(f))
size_bytes = _sum_file_sizes(
f for f in files if _is_checkpoint_weight_file(f)
)
rows.append(
_local_model_info(
scan_path = scan_path,

View file

@ -75,7 +75,9 @@ def _path_exists_or_symlink(path: Path) -> bool:
return False
def _repo_file_matches(target_repo, predicate) -> list[tuple[Path, Optional[Path], str]]:
def _repo_file_matches(
target_repo, predicate
) -> list[tuple[Path, Optional[Path], str]]:
matches: list[tuple[Path, Optional[Path], str]] = []
for rev in getattr(target_repo, "revisions", ()):
for f in getattr(rev, "files", ()):
@ -121,7 +123,11 @@ def _delete_gguf_variant_from_repos(
completed_hashes: set[str] = set()
for target_repo in target_repos:
repo_dir = Path(target_repo.repo_path) if getattr(target_repo, "repo_path", None) else None
repo_dir = (
Path(target_repo.repo_path)
if getattr(target_repo, "repo_path", None)
else None
)
matched = _repo_file_matches(
target_repo,
lambda name: _is_main_gguf_filename(name)
@ -233,7 +239,10 @@ def _loaded_id_matches_repo(loaded_id: str, repo_id: str) -> bool:
def _loaded_repo_variant_blocks_delete(
loaded_id: str, repo_id: str, delete_variant: Optional[str], loaded_variant: Optional[str]
loaded_id: str,
repo_id: str,
delete_variant: Optional[str],
loaded_variant: Optional[str],
) -> bool:
if not _loaded_id_matches_repo(loaded_id, repo_id):
return False
@ -256,7 +265,9 @@ def _llama_cpp_blocks_delete(repo_id: str, variant: Optional[str]) -> bool:
from routes.inference import get_llama_cpp_backend
backend = get_llama_cpp_backend()
except Exception as e:
logger.debug(f"llama.cpp backend unavailable during delete guard for {repo_id}: {e}")
logger.debug(
f"llama.cpp backend unavailable during delete guard for {repo_id}: {e}"
)
return False
loaded_id = backend.model_identifier
loaded_variant = getattr(backend, "hf_variant", None)
@ -283,7 +294,9 @@ def _inference_backend_blocks_delete(repo_id: str) -> bool:
from core.inference import get_inference_backend
backend = get_inference_backend()
except Exception as e:
logger.debug(f"Inference backend unavailable during delete guard for {repo_id}: {e}")
logger.debug(
f"Inference backend unavailable during delete guard for {repo_id}: {e}"
)
return False
active_name = backend.active_model_name
return bool(active_name) and _loaded_id_matches_repo(active_name, repo_id)
@ -316,7 +329,9 @@ async def delete_cached_model_response(
_inference_backend_blocks_delete(repo_id)
)
except Exception as e:
logger.warning(f"Load-state verification failed for {repo_id}; refusing delete: {e}")
logger.warning(
f"Load-state verification failed for {repo_id}; refusing delete: {e}"
)
raise HTTPException(
status_code = 503,
detail = _LOAD_STATE_UNVERIFIABLE_DETAIL,
@ -327,7 +342,9 @@ async def delete_cached_model_response(
detail = "Unload the model before deleting",
)
repo_key = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "model")
repo_key = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "model"
)
if not downloads.registry.begin_delete(repo_key, variant):
detail = (
f"Cancel the {variant} download before deleting it."
@ -336,7 +353,9 @@ async def delete_cached_model_response(
)
raise HTTPException(status_code = 400, detail = detail)
try:
return await asyncio.to_thread(_delete_cached_model_blocking, repo_id, variant, hf_token)
return await asyncio.to_thread(
_delete_cached_model_blocking, repo_id, variant, hf_token
)
finally:
downloads.registry.end_delete(repo_key, variant)
cache_inventory.invalidate_hf_cache_scans()
@ -375,18 +394,22 @@ def _delete_cached_model_blocking(
if not target_entries:
if variant is None:
cache_purged = purge_repo_cache_dirs("model", repo_id) or purge_partial_repo(
cache_purged = purge_repo_cache_dirs(
"model", repo_id
) or purge_partial_repo("model", repo_id)
state_purged = (
download_manifest.purge_all_state_for_repo("model", repo_id) > 0
)
state_purged = download_manifest.purge_all_state_for_repo("model", repo_id) > 0
if cache_purged or state_purged:
return {"status": "deleted", "repo_id": repo_id}
if variant:
incomplete_result = gguf_variants.delete_variant_incomplete_blobs_result(
repo_id,
variant,
hf_token,
companions = not sibling_active,
incomplete_result = (
gguf_variants.delete_variant_incomplete_blobs_result(
repo_id,
variant,
hf_token,
companions = not sibling_active,
)
)
if incomplete_result.unresolved:
raise HTTPException(
@ -422,7 +445,9 @@ def _delete_cached_model_blocking(
deleted_revisions = False
for hf_cache, repo_info in target_entries:
revision_hashes = [
rev.commit_hash for rev in repo_info.revisions if getattr(rev, "commit_hash", None)
rev.commit_hash
for rev in repo_info.revisions
if getattr(rev, "commit_hash", None)
]
if not revision_hashes:
continue

View file

@ -78,7 +78,9 @@ def _spawn_download_worker(
)
async def download_model_response(body: DownloadModelRequest, hf_token: Optional[str] = None):
async def download_model_response(
body: DownloadModelRequest, hf_token: Optional[str] = None
):
"""Start a background download for a HuggingFace model."""
repo_id = body.repo_id.strip()
if not _is_valid_repo_id(repo_id):
@ -87,7 +89,9 @@ async def download_model_response(body: DownloadModelRequest, hf_token: Optional
detail = f"Invalid repo_id: {repo_id!r}",
)
# Canonicalize so two different-cased paste-ins share one job + cache dir.
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "model")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "model"
)
variant = (body.gguf_variant or "").strip() or None
if variant is not None and not _is_valid_gguf_variant(variant):
@ -200,7 +204,9 @@ async def cancel_download_model_response(body: CancelDownloadRequest):
status_code = 400,
detail = f"Invalid repo_id: {repo_id!r}",
)
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "model")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "model"
)
variant = (body.gguf_variant or "").strip() or None
if variant is not None and not _is_valid_gguf_variant(variant):
raise HTTPException(
@ -219,12 +225,16 @@ async def cancel_download_model_response(body: CancelDownloadRequest):
return {"job_key": key, "state": state}
async def get_download_status_response(repo_id: str, gguf_variant: str = "") -> DownloadJobStatus:
async def get_download_status_response(
repo_id: str, gguf_variant: str = ""
) -> DownloadJobStatus:
"""Return the latest state of a background download job."""
repo_id = repo_id.strip()
if not _is_valid_repo_id(repo_id):
return DownloadJobStatus(state = "idle")
repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "model")
repo_id = await asyncio.to_thread(
resolve_cached_repo_id_case, repo_id, repo_type = "model"
)
variant = (gguf_variant or "").strip() or None
key = _download_job_key(repo_id, variant)
return _job_status(key, repo_id = repo_id, variant = variant)
@ -249,7 +259,9 @@ async def get_active_downloads_response(repo_id: str = "") -> ActiveDownloadsRes
)
def _variant_transport_status(repo_id: str, variant: str, hf_token: Optional[str]) -> dict:
def _variant_transport_status(
repo_id: str, variant: str, hf_token: Optional[str]
) -> dict:
incomplete_hashes = download_registry.incomplete_blob_hashes(
"model",
repo_id,
@ -281,13 +293,16 @@ def _variant_transport_status(repo_id: str, variant: str, hf_token: Optional[str
variant,
)
has_matching_incomplete = bool(
incomplete_hashes and variant_hashes and incomplete_hashes.intersection(variant_hashes)
incomplete_hashes
and variant_hashes
and incomplete_hashes.intersection(variant_hashes)
)
return {
"has_partial": has_partial,
"last_transport": last_transport,
"resumable": (
has_matching_incomplete and last_transport == download_registry.TRANSPORT_HTTP
has_matching_incomplete
and last_transport == download_registry.TRANSPORT_HTTP
),
}
@ -315,7 +330,9 @@ async def get_model_transport_status_response(
return _variant_transport_status(repo_id, variant, hf_token)
return {
"has_partial": has_active_incomplete_blobs("model", repo_id),
"last_transport": download_registry.read_active_transport_marker("model", repo_id),
"last_transport": download_registry.read_active_transport_marker(
"model", repo_id
),
"resumable": download_registry.is_resumable_partial("model", repo_id),
}
@ -359,7 +376,9 @@ async def get_gguf_download_progress_response(
if manifest is not None:
return (
sum(max(0, int(file.size or 0)) for file in manifest.expected_files),
frozenset(file.sha256 for file in manifest.expected_files if file.sha256),
frozenset(
file.sha256 for file in manifest.expected_files if file.sha256
),
)
return (
expected_total,

View file

@ -267,7 +267,12 @@ def _browse_relative_parts(requested_path: str, root: Path) -> Optional[list[str
parts = [part for part in rel_text.split(os.sep) if part not in ("", ".")]
altsep = os.altsep
for part in parts:
if part == ".." or "\x00" in part or os.sep in part or (altsep and altsep in part):
if (
part == ".."
or "\x00" in part
or os.sep in part
or (altsep and altsep in part)
):
return None
return parts
@ -460,7 +465,9 @@ def browse_folders_response(
# Parent is None at the FS root and when it would step outside the sandbox,
# so the up-row never 403s on click.
parent: Optional[str]
if target.parent == target or not _is_path_inside_allowlist(target.parent, allowed_roots):
if target.parent == target or not _is_path_inside_allowlist(
target.parent, allowed_roots
):
parent = None
else:
parent = str(target.parent)

View file

@ -49,9 +49,7 @@ from hub.utils.gguf_plan import (
logger = get_logger(__name__)
_VARIANT_HASH_CACHE: "OrderedDict[tuple[str, str, str, bool], tuple[frozenset[str], float]]" = (
OrderedDict()
)
_VARIANT_HASH_CACHE: "OrderedDict[tuple[str, str, str, bool], tuple[frozenset[str], float]]" = OrderedDict()
_VARIANT_REQUIREMENT_CACHE: "OrderedDict[tuple[str, str, str], tuple[_GgufVariantRequirement, float]]" = OrderedDict()
_VARIANT_REQUIREMENT_NEG_CACHE: "OrderedDict[tuple[str, str], float]" = OrderedDict()
_VARIANT_HASH_MAX = 512
@ -120,7 +118,9 @@ def _variant_requirement_neg_cache_clear(key: tuple[str, str]) -> None:
_VARIANT_REQUIREMENT_NEG_CACHE.pop(key, None)
def _variant_hash_cache_get(key: tuple[str, str, str, bool]) -> Optional[frozenset[str]]:
def _variant_hash_cache_get(
key: tuple[str, str, str, bool],
) -> Optional[frozenset[str]]:
with _VARIANT_HASH_LOCK:
cached = _VARIANT_HASH_CACHE.get(key)
if cached is None:
@ -133,7 +133,9 @@ def _variant_hash_cache_get(key: tuple[str, str, str, bool]) -> Optional[frozens
return hashes
def _variant_hash_cache_set(key: tuple[str, str, str, bool], hashes: frozenset[str]) -> None:
def _variant_hash_cache_set(
key: tuple[str, str, str, bool], hashes: frozenset[str]
) -> None:
with _VARIANT_HASH_LOCK:
_VARIANT_HASH_CACHE[key] = (hashes, time.monotonic())
_VARIANT_HASH_CACHE.move_to_end(key)
@ -141,7 +143,9 @@ def _variant_hash_cache_set(key: tuple[str, str, str, bool], hashes: frozenset[s
_VARIANT_HASH_CACHE.popitem(last = False)
def _variant_requirement_cache_get(key: tuple[str, str, str]) -> Optional[_GgufVariantRequirement]:
def _variant_requirement_cache_get(
key: tuple[str, str, str],
) -> Optional[_GgufVariantRequirement]:
with _VARIANT_HASH_LOCK:
cached = _VARIANT_REQUIREMENT_CACHE.get(key)
if cached is None:
@ -155,7 +159,9 @@ def _variant_requirement_cache_get(key: tuple[str, str, str]) -> Optional[_GgufV
def _variant_requirement_cache_set_many(
repo_id: str, hf_token: Optional[str], requirements: dict[str, _GgufVariantRequirement]
repo_id: str,
hf_token: Optional[str],
requirements: dict[str, _GgufVariantRequirement],
) -> None:
with _VARIANT_HASH_LOCK:
now = time.monotonic()
@ -167,7 +173,9 @@ def _variant_requirement_cache_set_many(
_VARIANT_REQUIREMENT_CACHE.popitem(last = False)
def _build_gguf_variant_requirements(siblings: list) -> dict[str, _GgufVariantRequirement]:
def _build_gguf_variant_requirements(
siblings: list,
) -> dict[str, _GgufVariantRequirement]:
return build_gguf_variant_plans(siblings)
@ -278,7 +286,11 @@ def gguf_variant_blob_hashes(
if requirement is None and allow_remote:
requirement = gguf_variant_requirements(repo_id, variant, hf_token)
if requirement is not None:
hashes = requirement.required_hashes if include_companions else requirement.main_hashes
hashes = (
requirement.required_hashes
if include_companions
else requirement.main_hashes
)
if hashes:
_variant_hash_cache_set(key, hashes)
return hashes
@ -300,7 +312,9 @@ def delete_variant_incomplete_blobs_result(
# With a sibling still downloading, ``companions=False`` keeps a shared mmproj
# from being unlinked out from under it; the repo's last delete reclaims it.
target_hashes = (
gguf_variant_blob_hashes(repo_id, variant, hf_token, include_companions = companions)
gguf_variant_blob_hashes(
repo_id, variant, hf_token, include_companions = companions
)
| extra_hashes
)
if not target_hashes:
@ -310,7 +324,9 @@ def delete_variant_incomplete_blobs_result(
incomplete_blob_hashes = set(),
variant_blob_hashes = frozenset(),
)
has_repo_partials = bool(download_registry.incomplete_blob_hashes("model", repo_id))
has_repo_partials = bool(
download_registry.incomplete_blob_hashes("model", repo_id)
)
return VariantIncompleteDeleteResult(
deleted = 0,
unresolved = has_variant_partial_state and has_repo_partials,
@ -440,7 +456,9 @@ async def get_gguf_variants_response(
)
try:
variants, has_vision, siblings = list_gguf_variants(repo_id, hf_token = hf_token)
variants, has_vision, siblings = list_gguf_variants(
repo_id, hf_token = hf_token
)
except Exception:
cached = list_gguf_variants_from_hf_cache(repo_id)
if cached is not None:
@ -555,17 +573,25 @@ async def get_gguf_variants_response(
partial_quants: set[str] = set()
partial_quant_transports: dict[str, Optional[str]] = {}
try:
incomplete_hashes = download_registry.incomplete_blob_hashes("model", repo_id)
incomplete_hashes = download_registry.incomplete_blob_hashes(
"model", repo_id
)
except Exception as e:
logger.warning(f"Failed to compute partial GGUF variants for {repo_id}: {e}")
logger.warning(
f"Failed to compute partial GGUF variants for {repo_id}: {e}"
)
incomplete_hashes = set()
scan_snapshot_dir = hf_cache_scan.resolve_snapshot_dir_for_scan("model", repo_id)
scan_snapshot_dir = hf_cache_scan.resolve_snapshot_dir_for_scan(
"model", repo_id
)
# Manifest + marker + main incomplete-blob check: catches variants whose
# download was cancelled or whose expected shards are missing/undersized.
for variant in variants:
try:
requirement = requirements_by_quant.get(variant.quant.lower())
variant_hashes = requirement.main_hashes if requirement is not None else None
variant_hashes = (
requirement.main_hashes if requirement is not None else None
)
if variant_hashes is None and incomplete_hashes:
variant_hashes = gguf_variant_blob_hashes(
repo_id,
@ -581,13 +607,16 @@ async def get_gguf_variants_response(
variant_blob_hashes = variant_hashes,
):
partial_quants.add(variant.quant)
partial_quant_transports[variant.quant] = _partial_transport_for_variant(
repo_id,
variant.quant,
partial_quant_transports[variant.quant] = (
_partial_transport_for_variant(
repo_id,
variant.quant,
)
)
except Exception as e:
logger.warning(
f"Manifest-based partial check failed for " f"{repo_id}/{variant.quant}: {e}"
f"Manifest-based partial check failed for "
f"{repo_id}/{variant.quant}: {e}"
)
if incomplete_hashes:
for variant in variants:
@ -597,7 +626,8 @@ async def get_gguf_variants_response(
# companion_hashes adds the MTP drafter (mmproj_hashes covers
# every mmproj precision in the repo, not just the planned one).
if (
(requirement.mmproj_hashes | requirement.companion_hashes) & incomplete_hashes
(requirement.mmproj_hashes | requirement.companion_hashes)
& incomplete_hashes
) and _filenames_cached(
requirement.main_filenames,
requirement.main_size_bytes,
@ -617,11 +647,15 @@ async def get_gguf_variants_response(
display_label = v.display_label,
size_bytes = v.size_bytes,
download_size_bytes = (
requirement.download_size_bytes if requirement is not None else v.size_bytes
requirement.download_size_bytes
if requirement is not None
else v.size_bytes
),
downloaded = _is_fully_downloaded(v) and not is_partial,
partial = is_partial,
partial_transport = (partial_quant_transports.get(v.quant) if is_partial else None),
partial_transport = (
partial_quant_transports.get(v.quant) if is_partial else None
),
)
return GgufVariantsResponse(

View file

@ -98,7 +98,9 @@ def _is_model_directory_for_scan(path: Path, *, entry_limit: int | None) -> bool
if entry_limit is None:
return _is_model_directory(path)
try:
has_config = (path / "config.json").exists() or (path / "adapter_config.json").exists()
has_config = (path / "config.json").exists() or (
path / "adapter_config.json"
).exists()
except OSError:
return False
return has_config and _has_immediate_model_weight(path)
@ -151,7 +153,9 @@ def _scan_models_dir(
break
try:
is_dir = child.is_dir()
is_gguf_file = not is_dir and child.suffix.lower() == ".gguf" and child.is_file()
is_gguf_file = (
not is_dir and child.suffix.lower() == ".gguf" and child.is_file()
)
if not is_dir and not is_gguf_file:
continue
has_model_files = is_gguf_file or _has_immediate_model_signal(child)
@ -190,7 +194,9 @@ def _hf_repo_dir_has_content(repo_dir: Path) -> bool:
return False
def _scan_hf_cache(cache_dir: Path, *, entry_limit: int | None = None) -> List[LocalModelInfo]:
def _scan_hf_cache(
cache_dir: Path, *, entry_limit: int | None = None
) -> List[LocalModelInfo]:
if not cache_dir.exists() or not cache_dir.is_dir():
return []
@ -228,7 +234,9 @@ def _scan_hf_cache(cache_dir: Path, *, entry_limit: int | None = None) -> List[L
repo_dir,
)
gguf_partial = hf_cache_scan.is_gguf_repo_partial(model_id, repo_dir)
has_gguf_variant_state, gguf_variant_state_size = _gguf_variant_state_summary(model_id)
has_gguf_variant_state, gguf_variant_state_size = _gguf_variant_state_summary(
model_id
)
snapshot_partial_transport = (
hf_cache_scan.partial_transport_for(
"model",
@ -311,7 +319,9 @@ def _scan_hf_cache(cache_dir: Path, *, entry_limit: int | None = None) -> List[L
return found
def _scan_lmstudio_dir(lm_dir: Path, *, entry_limit: int | None = None) -> List[LocalModelInfo]:
def _scan_lmstudio_dir(
lm_dir: Path, *, entry_limit: int | None = None
) -> List[LocalModelInfo]:
"""Scan an LM Studio models dir (``publisher/model-name`` folders of GGUFs, or top-level standalone GGUFs)."""
if not lm_dir.exists() or not lm_dir.is_dir():
return []
@ -427,7 +437,9 @@ def _resolve_allowed_models_dir(models_dir: str, allowed_roots: list[Path]) -> P
if not models_dir or not models_dir.strip():
raise ValueError("Directory not allowed")
requested = Path(os.path.realpath(os.path.expanduser(normalize_path(models_dir.strip()))))
requested = Path(
os.path.realpath(os.path.expanduser(normalize_path(models_dir.strip())))
)
if any(path_is_same_or_child(requested, root) for root in allowed_roots):
return requested
@ -510,7 +522,9 @@ async def _collect_models_from_default_sources(
and hf_default.resolve() != hf_cache_dir.resolve()
and hf_default.resolve() != legacy_hf.resolve()
):
local_models += await _scan_source("default HF cache", _scan_hf_cache, hf_default)
local_models += await _scan_source(
"default HF cache", _scan_hf_cache, hf_default
)
for lm_dir in lm_dirs:
local_models += await _scan_source("LM Studio", _scan_lmstudio_dir, lm_dir)
@ -612,7 +626,9 @@ def _dedupe_local_models(local_models: List[LocalModelInfo]) -> list[LocalModelI
)
async def list_local_models_response(models_dir: str = "./models") -> LocalModelListResponse:
async def list_local_models_response(
models_dir: str = "./models",
) -> LocalModelListResponse:
"""List local model candidates from every supported on-device source."""
hf_cache_dir = _resolve_hf_cache_dir()
legacy_hf = legacy_hf_cache_dir()

View file

@ -124,7 +124,9 @@ def _ollama_links_dir(ollama_dir: Path) -> Optional[Path]:
return None
def _make_ollama_blob_link(link_dir: Path, link_name: str, target: Path) -> Optional[str]:
def _make_ollama_blob_link(
link_dir: Path, link_name: str, target: Path
) -> Optional[str]:
"""Create a .gguf-named link to an Ollama blob: tries symlink then hardlink, skips the model if neither works (a full multi-GB copy would block the API). Idempotent."""
try:
link_dir.mkdir(parents = True, exist_ok = True)
@ -137,7 +139,9 @@ def _make_ollama_blob_link(link_dir: Path, link_name: str, target: Path) -> Opti
return None
link_path = _contained_link_path(link_dir, link_name)
if link_path is None:
logger.warning("Refusing unsafe Ollama link name %r under %s", link_name, link_dir)
logger.warning(
"Refusing unsafe Ollama link name %r under %s", link_name, link_dir
)
return None
try:
resolved = target.resolve()
@ -232,7 +236,9 @@ def _ollama_model_info_from_manifest(
model_type = cfg.get("model_type", "")
file_type = cfg.get("file_type", "")
except (json.JSONDecodeError, OSError) as e:
logger.debug("Could not parse Ollama config blob %s: %s", config_blob, e)
logger.debug(
"Could not parse Ollama config blob %s: %s", config_blob, e
)
layers = manifest.get("layers") or []
if not isinstance(layers, list):
@ -260,11 +266,17 @@ def _ollama_model_info_from_manifest(
model_blob = candidate
if materialize_links and model_link_dir is not None:
link_name = f"{safe_name}-{tag}{quant}.gguf"
gguf_link_path = _make_ollama_blob_link(model_link_dir, link_name, candidate)
gguf_link_path = _make_ollama_blob_link(
model_link_dir, link_name, candidate
)
elif materialize_links and media == "application/vnd.ollama.image.projector":
candidate = _ollama_blob_path(blobs_dir, digest)
if candidate is not None and _safe_is_file(candidate) and model_link_dir is not None:
if (
candidate is not None
and _safe_is_file(candidate)
and model_link_dir is not None
):
mmproj_name = f"{safe_name}-{tag}-mmproj.gguf"
_make_ollama_blob_link(model_link_dir, mmproj_name, candidate)

View file

@ -198,7 +198,9 @@ def test_delete_cached_dataset_absent_everywhere_raises_404(monkeypatch):
def test_check_format_rejects_invalid_path_as_400():
with pytest.raises(HTTPException) as exc_info:
formatting.check_format_response(CheckFormatRequest(dataset_name = "../../etc/passwd"))
formatting.check_format_response(
CheckFormatRequest(dataset_name = "../../etc/passwd")
)
assert exc_info.value.status_code == 400
@ -286,7 +288,9 @@ def test_dataset_claim_register_cancel_uses_registry_marker_owner(monkeypatch):
)
result = asyncio.run(
downloads.download_dataset_response(SimpleNamespace(repo_id = "Org/Data", use_xet = False))
downloads.download_dataset_response(
SimpleNamespace(repo_id = "Org/Data", use_xet = False)
)
)
assert result["state"] == "cancelled"
@ -328,7 +332,9 @@ def test_upload_dataset_response_writes_non_empty_file(monkeypatch, tmp_path):
payload = b'{"text":"hello"}\n'
monkeypatch.setattr(local, "DATASET_UPLOAD_DIR", tmp_path)
response = asyncio.run(local.upload_dataset_response(_Upload("../train.jsonl", payload)))
response = asyncio.run(
local.upload_dataset_response(_Upload("../train.jsonl", payload))
)
stored_path = Path(response.stored_path)
assert response.filename == "train.jsonl"

View file

@ -184,7 +184,9 @@ def test_make_ollama_blob_link_refuses_escaping_name(tmp_path):
blob.parent.mkdir(parents = True)
blob.write_bytes(b"weights")
escaped = ollama._make_ollama_blob_link(link_dir, "model-tag-../../../pwned.gguf", blob)
escaped = ollama._make_ollama_blob_link(
link_dir, "model-tag-../../../pwned.gguf", blob
)
assert escaped is None
assert not list(tmp_path.rglob("pwned.gguf"))
@ -200,7 +202,9 @@ def test_cached_gguf_scan_dedupes_and_excludes_mmproj_only(monkeypatch, tmp_path
[_file("Q4_K_M.gguf", 300), _file("Q8_0.gguf", 200)],
tmp_path / "large",
)
mmproj_only = _repo("Org/VisionAdapter", [_file("mmproj-F16.gguf", 900)], tmp_path / "mmproj")
mmproj_only = _repo(
"Org/VisionAdapter", [_file("mmproj-F16.gguf", 900)], tmp_path / "mmproj"
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
@ -241,7 +245,9 @@ def test_cached_gguf_scan_preserves_partial_flag(monkeypatch, tmp_path):
assert row["capabilities"]["can_chat"] is False
def test_cached_gguf_scan_includes_variant_state_without_completed_gguf(monkeypatch, tmp_path):
def test_cached_gguf_scan_includes_variant_state_without_completed_gguf(
monkeypatch, tmp_path
):
monkeypatch.setattr(state_dir, "cache_root", lambda: tmp_path / "state")
repo_path = tmp_path / "hub" / "models--Org--PartialGguf"
repo_path.mkdir(parents = True)
@ -257,7 +263,9 @@ def test_cached_gguf_scan_includes_variant_state_without_completed_gguf(monkeypa
[download_manifest.ExpectedFile(path = "model-Q4_K_M.gguf", size = 4096)],
"http",
)
assert download_manifest.write_cancel_marker("model", "Org/PartialGguf", "Q4_K_M", "http")
assert download_manifest.write_cancel_marker(
"model", "Org/PartialGguf", "Q4_K_M", "http"
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
@ -366,7 +374,9 @@ def test_gguf_variant_blob_hashes_skip_missing_rfilename(monkeypatch):
monkeypatch.setattr(
gguf_variants,
"_fetch_gguf_variant_requirements",
lambda _repo_id, _hf_token = None: gguf_variants._build_gguf_variant_requirements(siblings),
lambda _repo_id, _hf_token = None: gguf_variants._build_gguf_variant_requirements(
siblings
),
)
result = gguf_variants.gguf_variant_blob_hashes("Org/Malformed", "Q4_K_M", None)
@ -410,7 +420,9 @@ def test_download_gguf_variant_purges_only_main_quant_hashes(monkeypatch, tmp_pa
),
)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry,
@ -425,7 +437,8 @@ def test_download_gguf_variant_purges_only_main_quant_hashes(monkeypatch, tmp_pa
sys.modules,
"huggingface_hub",
SimpleNamespace(
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs) or str(tmp_path)
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs)
or str(tmp_path)
),
)
@ -441,12 +454,20 @@ def test_download_gguf_variant_purges_only_main_quant_hashes(monkeypatch, tmp_pa
},
)
]
assert [file.path for file in written[0][3]] == ["model-Q4_K_M.gguf", "mmproj-F16.gguf"]
assert snapshot_calls[0]["allow_patterns"] == ["model-Q4_K_M.gguf", "mmproj-F16.gguf"]
assert [file.path for file in written[0][3]] == [
"model-Q4_K_M.gguf",
"mmproj-F16.gguf",
]
assert snapshot_calls[0]["allow_patterns"] == [
"model-Q4_K_M.gguf",
"mmproj-F16.gguf",
]
assert verified == [("model", "Org/Vision", "Q4_K_M", str(tmp_path))]
def test_download_gguf_variant_manifest_resume_purges_only_main_quant_hashes(monkeypatch, tmp_path):
def test_download_gguf_variant_manifest_resume_purges_only_main_quant_hashes(
monkeypatch, tmp_path
):
prepare_calls = []
snapshot_calls = []
@ -484,12 +505,15 @@ def test_download_gguf_variant_manifest_resume_purges_only_main_quant_hashes(mon
"prepare_cache_for_transport",
lambda *args, **kwargs: prepare_calls.append((args, kwargs)) or 0,
)
monkeypatch.setattr(hf_download, "_verify_completed_download", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *_args, **_kwargs: None
)
monkeypatch.setitem(
sys.modules,
"huggingface_hub",
SimpleNamespace(
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs) or str(tmp_path)
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs)
or str(tmp_path)
),
)
@ -505,10 +529,15 @@ def test_download_gguf_variant_manifest_resume_purges_only_main_quant_hashes(mon
},
)
]
assert snapshot_calls[0]["allow_patterns"] == ["model-Q4_K_M.gguf", "mmproj-F16.gguf"]
assert snapshot_calls[0]["allow_patterns"] == [
"model-Q4_K_M.gguf",
"mmproj-F16.gguf",
]
def test_download_snapshot_recovers_manifest_after_metadata_fallback(monkeypatch, tmp_path):
def test_download_snapshot_recovers_manifest_after_metadata_fallback(
monkeypatch, tmp_path
):
metadata_calls = []
written = []
cleared = []
@ -518,11 +547,15 @@ def test_download_snapshot_recovers_manifest_after_metadata_fallback(monkeypatch
metadata_calls.append(True)
if len(metadata_calls) == 1:
raise RuntimeError("metadata down")
return SimpleNamespace(siblings = [SimpleNamespace(rfilename = "config.json", size = 12)])
return SimpleNamespace(
siblings = [SimpleNamespace(rfilename = "config.json", size = 12)]
)
monkeypatch.setattr(hf_download, "_model_info_with_retry", _metadata)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
@ -561,7 +594,9 @@ def test_download_dataset_continues_without_metadata_manifest(monkeypatch, tmp_p
monkeypatch.setattr(hf_download, "_dataset_info_with_retry", _metadata)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
@ -579,7 +614,8 @@ def test_download_dataset_continues_without_metadata_manifest(monkeypatch, tmp_p
sys.modules,
"huggingface_hub",
SimpleNamespace(
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs) or str(tmp_path)
snapshot_download = lambda **kwargs: snapshot_calls.append(kwargs)
or str(tmp_path)
),
)
@ -613,13 +649,17 @@ def test_download_snapshot_fails_when_metadata_unavailable_and_partial_remains(
monkeypatch.setattr(hf_download, "_model_info_with_retry", _metadata)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
)
monkeypatch.setattr(download_manifest, "clear_cancel_marker", lambda *_args: None)
monkeypatch.setattr(download_manifest, "read_manifest", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
download_manifest, "read_manifest", lambda *_args, **_kwargs: None
)
monkeypatch.setattr(
download_manifest, "write_manifest", lambda *args: written.append(args) or True
)
@ -719,7 +759,9 @@ def test_gguf_download_progress_fallback_logs_warning(monkeypatch):
assert kwargs == {}
def test_gguf_progress_counts_completed_mmproj_with_expected_bytes(monkeypatch, tmp_path):
def test_gguf_progress_counts_completed_mmproj_with_expected_bytes(
monkeypatch, tmp_path
):
"""A finished mmproj companion keeps counting toward progress once the caller
supplies expected bytes; resolving the variant requirement credits it."""
entry = tmp_path / "models--Org--Model-GGUF"
@ -1138,7 +1180,9 @@ def test_gguf_progress_scoped_hashes_exclude_sibling_quant(monkeypatch, tmp_path
assert result["downloaded_bytes"] == 5
def test_gguf_progress_unknown_hashes_does_not_count_foreign_blobs(monkeypatch, tmp_path):
def test_gguf_progress_unknown_hashes_does_not_count_foreign_blobs(
monkeypatch, tmp_path
):
# With a variant's hashes unresolved (metadata flaked, no manifest), the
# shared blobs/ dir's FINALIZED blobs must NOT be counted wholesale: a cached
# sibling quant (``siblinghash``) alongside is the "instant ~900 MB" bug.
@ -1190,7 +1234,9 @@ def test_gguf_progress_unknown_hashes_does_not_count_foreign_blobs(monkeypatch,
assert result["complete_on_disk"] is False
def test_gguf_progress_unknown_hashes_drops_unscoped_incomplete_blob(monkeypatch, tmp_path):
def test_gguf_progress_unknown_hashes_drops_unscoped_incomplete_blob(
monkeypatch, tmp_path
):
# With hashes unresolved, an .incomplete in the shared blobs/ dir can't be
# attributed to this variant (it may be a concurrent sibling's active write),
# so it is dropped, mirroring the finalized-blob guard. In production the
@ -1239,7 +1285,9 @@ def test_gguf_progress_unknown_hashes_drops_unscoped_incomplete_blob(monkeypatch
assert result["completed_bytes"] == 0 # finalized sibling still ignored
def test_gguf_progress_unknown_hashes_no_backward_dip_when_variant_finalizes(monkeypatch, tmp_path):
def test_gguf_progress_unknown_hashes_no_backward_dip_when_variant_finalizes(
monkeypatch, tmp_path
):
# Regression for the two-variant dip: with hashes unresolved, the first quant
# finalizes while the sibling still writes its .incomplete. The sibling's
# bytes used to leak into this numerator, dipping the bar ~99% -> ~78% for
@ -1310,7 +1358,9 @@ def test_hf_cache_model_file_probe_is_bounded(monkeypatch, tmp_path):
model.write_bytes(b"weights")
entries = [first, second, model]
monkeypatch.setattr(model_common.Path, "rglob", lambda _self, _pattern: iter(entries))
monkeypatch.setattr(
model_common.Path, "rglob", lambda _self, _pattern: iter(entries)
)
monkeypatch.setattr(model_common, "_HF_CACHE_MODEL_FILE_PROBE_LIMIT", 2)
bounded = model_common._iter_hf_cache_model_files(snapshot)
@ -1333,7 +1383,9 @@ def test_download_state_lookup_is_repo_case_insensitive(monkeypatch, tmp_path):
None,
[download_manifest.ExpectedFile(path = "config.json", size = 12)],
)
assert download_manifest.write_cancel_marker("model", "Owner/Repo", "Q4_K_M", "http")
assert download_manifest.write_cancel_marker(
"model", "Owner/Repo", "Q4_K_M", "http"
)
manifest = download_manifest.read_manifest("model", "owner/repo", None)
@ -1366,7 +1418,9 @@ def test_hf_cache_scan_fallback_row_uses_local_model_info_alias(monkeypatch, tmp
blobs_dir = repo_dir / "blobs"
blobs_dir.mkdir(parents = True)
(blobs_dir / "blob").write_bytes(b"content")
monkeypatch.setattr(local_inventory, "_classify_local_path", lambda *_args, **_kwargs: [])
monkeypatch.setattr(
local_inventory, "_classify_local_path", lambda *_args, **_kwargs: []
)
monkeypatch.setattr(
local_inventory.hf_cache_scan,
"is_snapshot_partial",
@ -1405,8 +1459,12 @@ def test_hf_cache_scan_uses_gguf_partial_row_for_variant_state(monkeypatch, tmp_
[download_manifest.ExpectedFile(path = "model-Q4_K_M.gguf", size = 8192)],
"http",
)
assert download_manifest.write_cancel_marker("model", "Org/PartialGguf", "Q4_K_M", "http")
monkeypatch.setattr(local_inventory, "_classify_local_path", lambda *_args, **_kwargs: [])
assert download_manifest.write_cancel_marker(
"model", "Org/PartialGguf", "Q4_K_M", "http"
)
monkeypatch.setattr(
local_inventory, "_classify_local_path", lambda *_args, **_kwargs: []
)
monkeypatch.setattr(
local_inventory.hf_cache_scan,
"is_snapshot_partial",
@ -1443,7 +1501,9 @@ def test_model_download_job_helpers_preserve_idle_shape():
assert status.error is None
def test_gguf_repo_partial_treats_completed_disk_variant_as_clean(monkeypatch, tmp_path):
def test_gguf_repo_partial_treats_completed_disk_variant_as_clean(
monkeypatch, tmp_path
):
monkeypatch.setattr(state_dir, "cache_root", lambda: tmp_path / "state")
snapshot = tmp_path / "cache" / "models--Org--Repo" / "snapshots" / "abc"
snapshot.mkdir(parents = True)
@ -1545,13 +1605,17 @@ def test_variant_partial_accepts_variant_filtered_legacy_hashes(monkeypatch, tmp
)
def test_gguf_variants_partial_marker_overrides_size_only_downloaded(monkeypatch, tmp_path):
def test_gguf_variants_partial_marker_overrides_size_only_downloaded(
monkeypatch, tmp_path
):
async def _run_inline(fn, *args, **kwargs):
return fn(*args, **kwargs)
monkeypatch.setattr(state_dir, "cache_root", lambda: tmp_path / "state")
monkeypatch.setattr(gguf_variants.asyncio, "to_thread", _run_inline)
assert download_manifest.write_cancel_marker("model", "Org/PartialRepo", "Q4_K_M", "http")
assert download_manifest.write_cancel_marker(
"model", "Org/PartialRepo", "Q4_K_M", "http"
)
snapshot = tmp_path / "cache" / "models--Org--PartialRepo" / "snapshots" / "rev0"
snapshot.mkdir(parents = True)
(snapshot / "model-Q4_K_M.gguf").write_bytes(b"x" * 100)
@ -1881,7 +1945,9 @@ def test_finalize_worker_exit_never_kills_a_healthy_worker(monkeypatch, tmp_path
)
def test_prepare_cache_for_transport_purges_only_requested_hashes(monkeypatch, tmp_path):
def test_prepare_cache_for_transport_purges_only_requested_hashes(
monkeypatch, tmp_path
):
root = tmp_path / "hub"
blobs = root / "models--Org--Repo" / "blobs"
blobs.mkdir(parents = True)
@ -1910,7 +1976,9 @@ def _vision_cache_root(monkeypatch, tmp_path):
return blobs
def test_prepare_cache_for_transport_purges_cross_transport_companion(monkeypatch, tmp_path):
def test_prepare_cache_for_transport_purges_cross_transport_companion(
monkeypatch, tmp_path
):
blobs = _vision_cache_root(monkeypatch, tmp_path)
companion = frozenset({"shared-mmproj"})
@ -1940,7 +2008,9 @@ def test_prepare_cache_for_transport_purges_cross_transport_companion(monkeypatc
assert not (blobs / "shared-mmproj.incomplete").exists()
def test_prepare_cache_for_transport_preserves_same_transport_companion(monkeypatch, tmp_path):
def test_prepare_cache_for_transport_preserves_same_transport_companion(
monkeypatch, tmp_path
):
blobs = _vision_cache_root(monkeypatch, tmp_path)
companion = frozenset({"shared-mmproj"})
@ -1995,7 +2065,9 @@ def test_prepare_cache_for_transport_protects_peer_companion(monkeypatch, tmp_pa
assert (blobs / "shared-mmproj.incomplete").exists()
def test_model_download_records_completed_baseline_for_new_gguf_variant(monkeypatch, tmp_path):
def test_model_download_records_completed_baseline_for_new_gguf_variant(
monkeypatch, tmp_path
):
async def _run_inline(fn, *args, **kwargs):
return fn(*args, **kwargs)
@ -2010,7 +2082,9 @@ def test_model_download_records_completed_baseline_for_new_gguf_variant(monkeypa
downloads.gguf_variants,
"gguf_variant_blob_hashes",
lambda _repo, _variant, _token = None, include_companions = True, **_kwargs: (
frozenset({"mainhash", "mmprojhash"}) if include_companions else frozenset({"mainhash"})
frozenset({"mainhash", "mmprojhash"})
if include_companions
else frozenset({"mainhash"})
),
)
monkeypatch.setattr(
@ -2053,7 +2127,9 @@ def test_model_download_records_completed_baseline_for_new_gguf_variant(monkeypa
registry = _Registry()
monkeypatch.setattr(downloads, "_registry", registry)
monkeypatch.setattr(downloads, "_spawn_download_worker", lambda *_args, **_kwargs: _Proc())
monkeypatch.setattr(
downloads, "_spawn_download_worker", lambda *_args, **_kwargs: _Proc()
)
asyncio.run(
downloads.download_model_response(
@ -2062,7 +2138,9 @@ def test_model_download_records_completed_baseline_for_new_gguf_variant(monkeypa
)
assert registry.claim_kwargs["blob_hashes"] == frozenset({"mainhash"})
assert registry.claim_kwargs["progress_blob_hashes"] == frozenset({"mainhash", "mmprojhash"})
assert registry.claim_kwargs["progress_blob_hashes"] == frozenset(
{"mainhash", "mmprojhash"}
)
assert registry.claim_kwargs["completed_baseline_bytes"] == 30
@ -2096,7 +2174,9 @@ def test_gguf_model_download_skips_completed_baseline_for_variant_resume_state(
downloads.gguf_variants,
"gguf_variant_blob_hashes",
lambda _repo, _variant, _token = None, include_companions = True, **_kwargs: (
frozenset({"mainhash", "mmprojhash"}) if include_companions else frozenset({"mainhash"})
frozenset({"mainhash", "mmprojhash"})
if include_companions
else frozenset({"mainhash"})
),
)
monkeypatch.setattr(
@ -2139,7 +2219,9 @@ def test_gguf_model_download_skips_completed_baseline_for_variant_resume_state(
registry = _Registry()
monkeypatch.setattr(downloads, "_registry", registry)
monkeypatch.setattr(downloads, "_spawn_download_worker", lambda *_args, **_kwargs: _Proc())
monkeypatch.setattr(
downloads, "_spawn_download_worker", lambda *_args, **_kwargs: _Proc()
)
asyncio.run(
downloads.download_model_response(
@ -2153,7 +2235,9 @@ def test_gguf_model_download_skips_completed_baseline_for_variant_resume_state(
def test_model_idle_status_uses_cancel_marker_after_restart(monkeypatch, tmp_path):
monkeypatch.setattr(state_dir, "cache_root", lambda: tmp_path)
monkeypatch.setattr(downloads, "_registry", download_registry.DownloadRegistry())
assert download_manifest.write_cancel_marker("model", "Owner/Repo", "Q4_K_M", "http")
assert download_manifest.write_cancel_marker(
"model", "Owner/Repo", "Q4_K_M", "http"
)
status = asyncio.run(downloads.get_download_status_response("owner/repo", "Q4_K_M"))
@ -2405,7 +2489,9 @@ def test_model_download_watcher_invalidates_hf_cache_scan(monkeypatch):
"_spawn_download_worker",
lambda *_args, **_kwargs: object(),
)
monkeypatch.setattr(downloads.download_lifecycle.threading, "Thread", _ImmediateThread)
monkeypatch.setattr(
downloads.download_lifecycle.threading, "Thread", _ImmediateThread
)
monkeypatch.setattr(
downloads.hf_cache_scan,
"invalidate_hf_cache_scans",
@ -2512,7 +2598,10 @@ def test_two_concurrent_same_repo_variants_both_complete(monkeypatch, tmp_path):
while time.monotonic() < deadline:
s4 = registry.get_job(key_q4).state
s8 = registry.get_job(key_q8).state
if s4 in download_registry.TERMINAL_STATES and s8 in download_registry.TERMINAL_STATES:
if (
s4 in download_registry.TERMINAL_STATES
and s8 in download_registry.TERMINAL_STATES
):
break
time.sleep(0.02)
@ -2628,7 +2717,9 @@ def test_snapshot_progress_filters_stale_blobs(monkeypatch, tmp_path):
assert result["expected_bytes"] == 140
def test_snapshot_progress_confirms_complete_only_with_verified_snapshot(monkeypatch, tmp_path):
def test_snapshot_progress_confirms_complete_only_with_verified_snapshot(
monkeypatch, tmp_path
):
entry = tmp_path / "models--Org--Model"
blobs = entry / "blobs"
snap = entry / "snapshots" / "rev0"
@ -2691,7 +2782,9 @@ def test_expected_files_from_snapshot_dir_records_relative_paths_and_sizes(tmp_p
assert all(f.sha256 is None for f in files)
def test_snapshot_progress_complete_with_manifest_synthesized_from_disk(monkeypatch, tmp_path):
def test_snapshot_progress_complete_with_manifest_synthesized_from_disk(
monkeypatch, tmp_path
):
"""A finished snapshot whose only manifest was synthesized from on-disk files
still verifies as complete, so a refresh finalizes it instead of capping at
99% and evicting it as gone."""
@ -2848,7 +2941,9 @@ def test_download_snapshot_writes_manifest_for_xet(monkeypatch, tmp_path):
),
)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
@ -2883,7 +2978,9 @@ def test_download_gguf_variant_writes_manifest_for_xet(monkeypatch, tmp_path):
),
)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
@ -2918,7 +3015,9 @@ def test_download_dataset_writes_manifest_for_xet(monkeypatch, tmp_path):
),
)
monkeypatch.setattr(
hf_download, "_verify_completed_download", lambda *args, **kwargs: verified.append(args)
hf_download,
"_verify_completed_download",
lambda *args, **kwargs: verified.append(args),
)
monkeypatch.setattr(
download_registry, "prepare_cache_for_transport", lambda *_args, **_kwargs: 0
@ -2956,7 +3055,9 @@ def test_dataset_status_includes_generation(monkeypatch):
lambda repo_id, **_kwargs: repo_id,
)
result = asyncio.run(dataset_downloads.get_dataset_download_status_response("Org/Data"))
result = asyncio.run(
dataset_downloads.get_dataset_download_status_response("Org/Data")
)
assert result.state == "running"
assert result.generation == 4

View file

@ -50,7 +50,9 @@ def _matches_label(snapshot: Path, path: Path, label: str) -> bool:
return label in rel
def dataset_snapshot_from_cache_path(local_path: Optional[str], repo_id: str) -> Optional[Path]:
def dataset_snapshot_from_cache_path(
local_path: Optional[str], repo_id: str
) -> Optional[Path]:
if not local_path or not repo_id:
return None
try:
@ -115,7 +117,9 @@ def cached_dataset_candidates(
) -> list[Path]:
try:
files = [
p for p in snapshot.rglob("*") if p.is_file() and p.name.lower().endswith(extensions)
p
for p in snapshot.rglob("*")
if p.is_file() and p.name.lower().endswith(extensions)
]
except OSError:
return []
@ -127,7 +131,9 @@ def cached_dataset_candidates(
def score(path: Path) -> tuple[int, int, str]:
rel = _rel_lower(snapshot, path)
subset_match = bool(subset_lower and _matches_label(snapshot, path, subset_lower))
subset_match = bool(
subset_lower and _matches_label(snapshot, path, subset_lower)
)
split_match = bool(split_lower and split_label_matches(rel, split_lower))
location_rank = 3
if split_match and (not subset_lower or subset_match):

View file

@ -23,7 +23,10 @@ def _column_names(dataset, sample: Optional[dict] = None) -> list[str]:
def _keyword_in_column(keyword: str, col_name: str) -> bool:
return re.search(r"\b" + re.escape(keyword) + r"\b", col_name, re.IGNORECASE) is not None
return (
re.search(r"\b" + re.escape(keyword) + r"\b", col_name, re.IGNORECASE)
is not None
)
def _unknown_dataset_format(
@ -165,14 +168,19 @@ def detect_custom_format_heuristic(dataset):
def has_keyword(col_name, keywords):
col_lower = col_name.lower()
col_normalized = col_lower.replace("_", "").replace("-", "").replace(" ", "")
return any(keyword in col_lower or keyword in col_normalized for keyword in keywords)
return any(
keyword in col_lower or keyword in col_normalized for keyword in keywords
)
def is_metadata(col_name):
col_lower = col_name.lower()
if col_lower in metadata_exact_match or col_lower in metadata_prefix_patterns:
return True
for pattern in metadata_prefix_patterns:
if col_lower.startswith(pattern.split("_")[0] + "_") and col_lower != pattern:
if (
col_lower.startswith(pattern.split("_")[0] + "_")
and col_lower != pattern
):
if "_" in col_lower:
prefix = col_lower.split("_")[0]
if prefix in ["generation", "pass", "inference"]:
@ -181,7 +189,11 @@ def detect_custom_format_heuristic(dataset):
def get_priority_score(col_name):
col_lower = col_name.lower()
return sum(score for pattern, score in priority_patterns.items() if pattern in col_lower)
return sum(
score
for pattern, score in priority_patterns.items()
if pattern in col_lower
)
def get_content_length(col_name):
try:
@ -195,7 +207,9 @@ def detect_custom_format_heuristic(dataset):
score = 10
if role_type == "user":
col_lower = col_name.lower()
if "task" in col_lower and not any(kw in col_lower for kw in user_words_high_priority):
if "task" in col_lower and not any(
kw in col_lower for kw in user_words_high_priority
):
score -= 15
score += get_priority_score(col_name)
if role_type in ["assistant", "user"]:
@ -219,12 +233,19 @@ def detect_custom_format_heuristic(dataset):
return score
content_columns = [col for col in all_columns if not is_metadata(col)]
assistant_potential = [col for col in content_columns if has_keyword(col, assistant_words)]
assistant_potential = [
col for col in content_columns if has_keyword(col, assistant_words)
]
user_potential = [col for col in content_columns if has_keyword(col, user_words)]
assistant_candidates = [
(col, score)
for col in assistant_potential
if (score := score_column(col, assistant_words, "assistant", len(assistant_potential))) > 0
if (
score := score_column(
col, assistant_words, "assistant", len(assistant_potential)
)
)
> 0
]
if assistant_candidates:
assistant_candidates.sort(key = lambda item: item[1], reverse = True)
@ -399,7 +420,9 @@ def detect_multimodal_dataset(dataset):
audio_columns.append(col_name)
modality_types.add("audio")
if audio_columns:
multimodal_columns = [col for col in multimodal_columns if col not in set(audio_columns)]
multimodal_columns = [
col for col in multimodal_columns if col not in set(audio_columns)
]
detected_text_col = None
if audio_columns:
@ -454,7 +477,9 @@ def detect_vlm_dataset_structure(dataset):
and isinstance(content[0], dict)
and "type" in content[0]
):
has_index = any("index" in item for item in content if isinstance(item, dict))
has_index = any(
"index" in item for item in content if isinstance(item, dict)
)
if has_index and "images" in column_names:
return {
"format": "vlm_messages_llava",
@ -463,7 +488,9 @@ def detect_vlm_dataset_structure(dataset):
"image_column": "images",
"text_column": None,
}
has_image = any("image" in item for item in content if isinstance(item, dict))
has_image = any(
"image" in item for item in content if isinstance(item, dict)
)
if has_image:
return {
"format": "vlm_messages",
@ -555,9 +582,9 @@ def detect_vlm_dataset_structure(dataset):
image_candidates = []
for col in column_names:
value = sample[col]
if any(_keyword_in_column(keyword, col) for keyword in image_keywords) or _is_image_value(
value
):
if any(
_keyword_in_column(keyword, col) for keyword in image_keywords
) or _is_image_value(value):
if hasattr(value, "size") and hasattr(value, "mode"):
score = 100
elif isinstance(value, dict) and ("bytes" in value or "path" in value):
@ -726,7 +753,9 @@ def _standardize_sharegpt_row(row: dict[str, Any], chat_column: str) -> dict[str
if not isinstance(message, dict):
continue
role = message.get("role") or message.get("from")
content = message.get("content") if "content" in message else message.get("value")
content = (
message.get("content") if "content" in message else message.get("value")
)
messages.append(
{
"role": _ROLE_MAP.get(str(role), str(role or "user")),

View file

@ -59,7 +59,9 @@ _LEGACY_MARKER_VERSION = 1
# Verbatim phrase the worker emits on a degraded completion and the download
# lifecycle escalates to a warning log. Shared so the emit and match stay coupled.
MANIFEST_DEGRADED_MARKER = "completed without a manifest so partial detection is degraded"
MANIFEST_DEGRADED_MARKER = (
"completed without a manifest so partial detection is degraded"
)
@dataclass(frozen = True)
@ -473,14 +475,18 @@ def _iter_variant_state_files(
yield _variant_from_state_file(entry, variant), entry
def iter_variant_manifests(repo_type: RepoType, repo_id: str) -> Iterator[tuple[str, Path]]:
def iter_variant_manifests(
repo_type: RepoType, repo_id: str
) -> Iterator[tuple[str, Path]]:
"""Yield (variant, manifest_path) for every variant-keyed manifest
written for this repo. Used by is_gguf_repo_partial to enumerate all
variants present on disk so the all-variants-broken gate can run."""
yield from _iter_variant_state_files(manifests_dir(), repo_type, repo_id)
def iter_variant_markers(repo_type: RepoType, repo_id: str) -> Iterator[tuple[str, Path]]:
def iter_variant_markers(
repo_type: RepoType, repo_id: str
) -> Iterator[tuple[str, Path]]:
"""Yield (variant, marker_path) for every variant-keyed cancel marker.
Companion to iter_variant_manifests: catches variants cancelled
before download-start ever wrote a manifest (very early failures)."""

View file

@ -113,7 +113,9 @@ def _worker_breadcrumb_path(key: str) -> Optional[Path]:
return parent / f"{safe}.json"
def write_worker_breadcrumb(key: str, pid: int, metadata: Optional["DownloadMetadata"]) -> None:
def write_worker_breadcrumb(
key: str, pid: int, metadata: Optional["DownloadMetadata"]
) -> None:
"""Record a live worker's PID so a restarted backend can reap it. Best
effort: a write failure only forfeits boot-time reaping for this worker,
still covered by the worker's own parent-death watchdog."""
@ -366,7 +368,9 @@ def _iter_active_snapshot_dirs(repo_type: str, repo_id: str) -> Iterator[Path]:
yield snapshot
def _manifest_verifies_against_active_cache(repo_type: str, repo_id: str, manifest) -> bool:
def _manifest_verifies_against_active_cache(
repo_type: str, repo_id: str, manifest
) -> bool:
from hub.utils import download_manifest
for snapshot_dir in _iter_active_snapshot_dirs(repo_type, repo_id):
if download_manifest.verify_against_disk(manifest, snapshot_dir).ok:
@ -374,7 +378,9 @@ def _manifest_verifies_against_active_cache(repo_type: str, repo_id: str, manife
return False
def _manifest_has_active_incomplete_blobs(repo_type: str, repo_id: str, manifest) -> bool:
def _manifest_has_active_incomplete_blobs(
repo_type: str, repo_id: str, manifest
) -> bool:
if not getattr(manifest, "variant", None):
return has_active_incomplete_blobs(repo_type, repo_id)
expected_hashes = frozenset(
@ -383,7 +389,9 @@ def _manifest_has_active_incomplete_blobs(repo_type: str, repo_id: str, manifest
if not expected_hashes:
return has_active_incomplete_blobs(repo_type, repo_id)
return bool(
incomplete_blob_hashes(repo_type, repo_id, active_only = True).intersection(expected_hashes)
incomplete_blob_hashes(repo_type, repo_id, active_only = True).intersection(
expected_hashes
)
)
@ -398,7 +406,9 @@ def _is_transport_marker_file(path: Path) -> bool:
# Matches ".transport", its tmps, and variant-scoped ".transport.gguf-*".
# Real HF cache entries (blobs/refs/snapshots/.no_exist) never start with
# ".transport.".
return path.name == TRANSPORT_MARKER_NAME or path.name.startswith(f"{TRANSPORT_MARKER_NAME}.")
return path.name == TRANSPORT_MARKER_NAME or path.name.startswith(
f"{TRANSPORT_MARKER_NAME}."
)
def _companion_marker_path(entry: Path) -> Path:
@ -516,9 +526,13 @@ def prepare_cache_for_transport(
total_purged += _purge_incomplete_blobs(entry, only_blob_hashes, protected)
else:
if _read_marker(entry, variant) != mode:
total_purged += _purge_incomplete_blobs(entry, only_blob_hashes, protected)
total_purged += _purge_incomplete_blobs(
entry, only_blob_hashes, protected
)
if companion_blob_hashes and _read_companion_marker(entry) != mode:
total_purged += _purge_incomplete_blobs(entry, companion_blob_hashes, protected)
total_purged += _purge_incomplete_blobs(
entry, companion_blob_hashes, protected
)
_write_marker(entry, mode, variant)
if has_companion:
_write_companion_marker(entry, mode)
@ -562,7 +576,9 @@ def purge_empty_marker_dir(
contents = list(entry.iterdir())
except OSError:
continue
if not contents or not all(_is_transport_marker_file(item) for item in contents):
if not contents or not all(
_is_transport_marker_file(item) for item in contents
):
continue
own_name = _marker_path(entry, variant).name
own_markers = [
@ -635,7 +651,9 @@ def incomplete_blob_hashes(
return out
def completed_blob_bytes(repo_type: str, repo_id: str, blob_hashes: frozenset[str]) -> int:
def completed_blob_bytes(
repo_type: str, repo_id: str, blob_hashes: frozenset[str]
) -> int:
"""Sum finalized blob bytes for *blob_hashes* in the active HF cache root.
A worker only writes to the active ``HF_HUB_CACHE`` root, so a baseline must
@ -658,7 +676,9 @@ def completed_blob_bytes(repo_type: str, repo_id: str, blob_hashes: frozenset[st
return total
def existing_blob_bytes(repo_type: str, repo_id: str, blob_hashes: frozenset[str]) -> int:
def existing_blob_bytes(
repo_type: str, repo_id: str, blob_hashes: frozenset[str]
) -> int:
"""Bytes already on disk (finalized + ``.incomplete``) for *blob_hashes* in
the active HF cache root. A blob is in exactly one state, so summing both
candidate names never double-counts. Used to size what a (possibly resumed)
@ -1046,7 +1066,9 @@ class DownloadRegistry:
return (metadata.variant or "").strip().lower() or None
return variant_from_key(key)
def _delete_blocked_by_active_locked(self, repo_id: str, variant: Optional[str]) -> bool:
def _delete_blocked_by_active_locked(
self, repo_id: str, variant: Optional[str]
) -> bool:
"""Whether an active download conflicts with deleting *repo_id*/*variant*.
A whole-repo delete (``variant is None``) conflicts with any active
@ -1107,7 +1129,9 @@ class DownloadRegistry:
if repo_key:
candidate_keys = list(self._repo_active.get(repo_key, set()))
else:
candidate_keys = [key for active in self._repo_active.values() for key in active]
candidate_keys = [
key for active in self._repo_active.values() for key in active
]
refs: list[ActiveDownloadRef] = []
for key in candidate_keys:
job = self._jobs.get(key)
@ -1231,7 +1255,9 @@ class DownloadRegistry:
try:
proc.wait(timeout = max(0.0, deadline - time.monotonic()))
except subprocess.TimeoutExpired:
logger.warning(f"shutdown: {kind} worker for {key} did not exit after kill")
logger.warning(
f"shutdown: {kind} worker for {key} did not exit after kill"
)
except Exception:
pass
# Mark only genuinely interrupted workers (rc != 0, or None on wait

View file

@ -143,7 +143,9 @@ def pick_best_gguf(filenames: list[str]) -> Optional[str]:
gguf_files = [
name
for name in filenames
if is_gguf_filename(name) and not is_mmproj_filename(name) and not is_mtp_drafter_path(name)
if is_gguf_filename(name)
and not is_mmproj_filename(name)
and not is_mtp_drafter_path(name)
]
if not gguf_files:
return None
@ -246,7 +248,9 @@ def iter_hf_cache_snapshots(repo_id: str):
yield from snapshots
def list_gguf_variants_from_hf_cache(repo_id: str) -> Optional[tuple[list[GgufVariantInfo], bool]]:
def list_gguf_variants_from_hf_cache(
repo_id: str,
) -> Optional[tuple[list[GgufVariantInfo], bool]]:
for snapshot in iter_hf_cache_snapshots(repo_id):
variants, has_vision = list_local_gguf_variants(str(snapshot))
if variants or has_vision:
@ -372,7 +376,9 @@ def list_gguf_variants(
has_vision = True
continue
quant = extract_quant_label(filename)
quant_totals[quant] = quant_totals.get(quant, 0) + int(getattr(sibling, "size", 0) or 0)
quant_totals[quant] = quant_totals.get(quant, 0) + int(
getattr(sibling, "size", 0) or 0
)
quant_first_file.setdefault(quant, filename)
for quant, total_size in quant_totals.items():

View file

@ -60,7 +60,9 @@ def expected_file_from_sibling(sibling) -> Optional[ExpectedFile]:
def is_companion_gguf_path(path: str) -> bool:
"""Companion (non-main) GGUF downloaded alongside a variant: the vision
mmproj or the separate MTP drafter (Gemma 4)."""
return is_gguf_filename(path) and (is_mmproj_filename(path) or is_mtp_drafter_path(path))
return is_gguf_filename(path) and (
is_mmproj_filename(path) or is_mtp_drafter_path(path)
)
def is_main_gguf_variant_path(path: str, variant: str) -> bool:
@ -81,7 +83,9 @@ def _gguf_rfilename(sibling) -> Optional[str]:
def mmproj_siblings(siblings: Sequence) -> list:
return [s for s in siblings if (name := _gguf_rfilename(s)) and is_mmproj_filename(name)]
return [
s for s in siblings if (name := _gguf_rfilename(s)) and is_mmproj_filename(name)
]
def preferred_mmproj_sibling(siblings: Sequence) -> Optional[object]:
@ -89,7 +93,11 @@ def preferred_mmproj_sibling(siblings: Sequence) -> Optional[object]:
if not candidates:
return None
return next(
(s for s in candidates if extract_quant_label(getattr(s, "rfilename")).upper() == "F16"),
(
s
for s in candidates
if extract_quant_label(getattr(s, "rfilename")).upper() == "F16"
),
candidates[0],
)
@ -106,7 +114,8 @@ def preferred_mtp_sibling(siblings: Sequence) -> Optional[object]:
(
s
for s in siblings
if (name := _gguf_rfilename(s)) and name.lower().rsplit("/", 1)[-1].startswith("mtp-")
if (name := _gguf_rfilename(s))
and name.lower().rsplit("/", 1)[-1].startswith("mtp-")
),
key = lambda s: getattr(s, "rfilename"),
)
@ -121,11 +130,17 @@ def build_gguf_variant_plans(siblings: Sequence) -> dict[str, GgufVariantPlan]:
for s in all_mmproj
if isinstance(getattr(s, "rfilename", None), str)
)
all_mmproj_hashes = frozenset(h for h in (sibling_sha256(s) for s in all_mmproj) if h)
all_mmproj_hashes = frozenset(
h for h in (sibling_sha256(s) for s in all_mmproj) if h
)
companion = preferred_mmproj_sibling(siblings)
companion_expected = expected_file_from_sibling(companion) if companion is not None else None
companion_expected = (
expected_file_from_sibling(companion) if companion is not None else None
)
mtp_sibling = preferred_mtp_sibling(siblings)
mtp_expected = expected_file_from_sibling(mtp_sibling) if mtp_sibling is not None else None
mtp_expected = (
expected_file_from_sibling(mtp_sibling) if mtp_sibling is not None else None
)
companions_expected = tuple(
file for file in (companion_expected, mtp_expected) if file is not None
)
@ -167,11 +182,17 @@ def plan_from_expected_files(
all_mmproj_hashes: frozenset[str] | None = None,
) -> GgufVariantPlan:
expected = tuple(expected_files)
main_files = tuple(file for file in expected if is_main_gguf_variant_path(file.path, variant))
companion_files = tuple(file for file in expected if is_companion_gguf_path(file.path))
main_files = tuple(
file for file in expected if is_main_gguf_variant_path(file.path, variant)
)
companion_files = tuple(
file for file in expected if is_companion_gguf_path(file.path)
)
# Manifest-resume fallback for the mmproj fields below: companion_files
# also holds the MTP drafter, so keep an mmproj-only view.
mmproj_files = tuple(file for file in companion_files if is_mmproj_filename(file.path))
mmproj_files = tuple(
file for file in companion_files if is_mmproj_filename(file.path)
)
main_hashes = frozenset(file.sha256 for file in main_files if file.sha256)
companion_hashes = frozenset(file.sha256 for file in companion_files if file.sha256)
required_hashes = frozenset(file.sha256 for file in expected if file.sha256)

View file

@ -66,7 +66,9 @@ def repo_cache_dir_name(repo_type: str, repo_id: str) -> str:
return f"{repo_type}s--{repo_id.replace('/', '--')}"
def resolve_destructive_case_matches(target: str, candidates: Iterable[str]) -> Optional[set[str]]:
def resolve_destructive_case_matches(
target: str, candidates: Iterable[str]
) -> Optional[set[str]]:
values = list(candidates)
exact = {candidate for candidate in values if candidate == target}
if exact:
@ -176,7 +178,9 @@ def iter_destructive_repo_cache_dirs(repo_type: str, repo_id: str) -> Iterator[P
folded_target = target.lower()
for root in hf_cache_roots():
try:
entries = [entry for entry in root.iterdir() if entry.name.lower() == folded_target]
entries = [
entry for entry in root.iterdir() if entry.name.lower() == folded_target
]
except OSError:
continue
matched_names = resolve_destructive_case_matches(

View file

@ -346,7 +346,11 @@ def _completed_gguf_variants(snapshot_dir: Optional[Path]) -> set[str]:
except OSError:
continue
rel = path.relative_to(snapshot_dir).as_posix()
if not is_gguf_filename(rel) or is_mmproj_filename(rel) or is_mtp_drafter_path(rel):
if (
not is_gguf_filename(rel)
or is_mmproj_filename(rel)
or is_mtp_drafter_path(rel)
):
continue
quant = extract_quant_label(rel)
split = _GGUF_SPLIT_RE.search(path.name)
@ -409,7 +413,8 @@ def is_snapshot_partial(
state_applies = _state_applies_to_repo_cache_dir(repo_cache_dir)
return _compose_partial(
lambda: state_applies and download_manifest.has_cancel_marker(repo_type, repo_id, None),
lambda: state_applies
and download_manifest.has_cancel_marker(repo_type, repo_id, None),
lambda: _snapshot_legacy_partial(repo_type, repo_id, repo_cache_dir),
lambda: _manifest_partial(
repo_type,
@ -441,7 +446,8 @@ def is_variant_partial(
state_applies = _state_applies_to_repo_cache_dir(repo_cache_dir)
return _compose_partial(
lambda: state_applies and download_manifest.has_cancel_marker("model", repo_id, variant),
lambda: state_applies
and download_manifest.has_cancel_marker("model", repo_id, variant),
lambda: bool(
incomplete_blob_hashes
and variant_blob_hashes

View file

@ -61,7 +61,9 @@ def _parse_json_response(text: str) -> Optional[dict[str, Any]]:
return parsed if isinstance(parsed, dict) else None
def _generate_with_backend(backend, messages: list[dict[str, str]], max_tokens: int) -> str:
def _generate_with_backend(
backend, messages: list[dict[str, str]], max_tokens: int
) -> str:
cumulative = ""
for chunk in backend.generate_chat_completion(
messages = messages,
@ -159,7 +161,9 @@ def _run_multi_pass_advisor(
return None
repo = os.environ.get("UNSLOTH_HELPER_MODEL_REPO", DEFAULT_HELPER_MODEL_REPO)
variant = os.environ.get("UNSLOTH_HELPER_MODEL_VARIANT", DEFAULT_HELPER_MODEL_VARIANT)
variant = os.environ.get(
"UNSLOTH_HELPER_MODEL_VARIANT", DEFAULT_HELPER_MODEL_VARIANT
)
backend = None
try:
from core.inference.llama_cpp import LlamaCppBackend
@ -179,7 +183,9 @@ def _run_multi_pass_advisor(
samples_text = _sample_text(columns, samples)
metadata_text = (
json.dumps(dataset_metadata, indent = 2, default = str)[:500] if dataset_metadata else "N/A"
json.dumps(dataset_metadata, indent = 2, default = str)[:500]
if dataset_metadata
else "N/A"
)
card_excerpt = (dataset_card or "")[:1200] or "N/A"
hints = _target_hints(model_name, model_type)
@ -284,7 +290,9 @@ def _run_multi_pass_advisor(
system_prompt = ""
if not pass1.get("is_conversational"):
user_cols = [col for col, role in column_roles.items() if role == "user"]
assistant_cols = [col for col, role in column_roles.items() if role == "assistant"]
assistant_cols = [
col for col, role in column_roles.items() if role == "assistant"
]
prompt_raw = _generate_with_backend(
backend,
[

View file

@ -189,7 +189,8 @@ def is_valid_repo_id(repo_id: str) -> bool:
if len(segments) not in (1, 2):
return False
return all(
segment not in ("", ".", "..") and _VALID_REPO_ID_SEGMENT.fullmatch(segment) is not None
segment not in ("", ".", "..")
and _VALID_REPO_ID_SEGMENT.fullmatch(segment) is not None
for segment in segments
)
@ -276,7 +277,9 @@ def _hf_hub_cache_dir() -> Path:
from huggingface_hub.constants import HF_HUB_CACHE
return Path(HF_HUB_CACHE)
except Exception as exc:
logger.debug("Could not read huggingface_hub HF_HUB_CACHE, using default: %s", exc)
logger.debug(
"Could not read huggingface_hub HF_HUB_CACHE, using default: %s", exc
)
return Path.home() / ".cache" / "huggingface" / "hub"
@ -403,7 +406,9 @@ def resolve_dataset_path(path_value: str) -> Path:
return path
except ValueError:
continue
raise ValueError(f"dataset path must be relative or under a dataset root: {raw!r}")
raise ValueError(
f"dataset path must be relative or under a dataset root: {raw!r}"
)
parts = [part for part in Path(normalized).parts if part not in ("", ".")]
if parts[:2] == ["assets", "datasets"]:

View file

@ -80,7 +80,9 @@ def repo_cache_basename(repo_type: RepoType, repo_id: str) -> str:
# wrong filename and a misclassified scanner row (the Literal only guards
# statically; dynamic/JSON-sourced values slip past it).
if repo_type not in _VALID_REPO_TYPES:
raise ValueError(f"repo_type must be one of {_VALID_REPO_TYPES}, got {repo_type!r}")
raise ValueError(
f"repo_type must be one of {_VALID_REPO_TYPES}, got {repo_type!r}"
)
return f"{repo_type}s--{repo_id.replace('/', '--')}".lower()

View file

@ -183,14 +183,17 @@ def _hf_token_arg(hf_token: str | None) -> HfTokenArg:
def _retry_metadata_fetch(repo_id: str, fetch, *, label: str):
for attempt, timeout in enumerate((_METADATA_REQUEST_TIMEOUT, _METADATA_RETRY_TIMEOUT)):
for attempt, timeout in enumerate(
(_METADATA_REQUEST_TIMEOUT, _METADATA_RETRY_TIMEOUT)
):
try:
return fetch(timeout)
except Exception as e:
if attempt == 1:
raise
print(
f"{label} request failed for {repo_id} " f"({type(e).__name__}: {e}); retrying.",
f"{label} request failed for {repo_id} "
f"({type(e).__name__}: {e}); retrying.",
file = sys.stderr,
)
time.sleep(_METADATA_RETRY_DELAY)
@ -439,7 +442,9 @@ def _recover_manifest_after_download(
)
sys.exit(1)
fallback_files = download_manifest.expected_files_from_snapshot_dir(Path(snapshot_path))
fallback_files = download_manifest.expected_files_from_snapshot_dir(
Path(snapshot_path)
)
if fallback_files and download_manifest.write_manifest(
repo_type,
repo_id,
@ -515,7 +520,9 @@ def _download_snapshot(repo_id: str, hf_token: str | None, mode: str) -> None:
snapshot_path,
mode,
fetch_info = lambda: _model_info_with_retry(repo_id, hf_token),
expected_files_from_info = lambda recovered: _snapshot_download_plan(recovered)[1],
expected_files_from_info = lambda recovered: _snapshot_download_plan(
recovered
)[1],
)
_verify_completed_download(
"model",
@ -538,12 +545,15 @@ def _gguf_variant_target_plan(
file = sys.stderr,
)
raise RuntimeError(
f"Metadata unavailable while resolving GGUF variant '{variant}' " f"for {repo_id}"
f"Metadata unavailable while resolving GGUF variant '{variant}' "
f"for {repo_id}"
) from e
return build_gguf_variant_plans(list(info.siblings)).get(variant.lower())
def _download_gguf_variant(repo_id: str, variant: str, hf_token: str | None, mode: str) -> None:
def _download_gguf_variant(
repo_id: str, variant: str, hf_token: str | None, mode: str
) -> None:
from huggingface_hub import snapshot_download
from hub.utils.download_registry import prepare_cache_for_transport
from hub.utils.hf_cache_state import has_active_incomplete_blobs

View file

@ -47,7 +47,9 @@ if sys.platform == "win32":
try:
if os.path.isdir(_default_root):
for _ver in sorted(os.listdir(_default_root), key = _ver_key, reverse = True):
for _ver in sorted(
os.listdir(_default_root), key = _ver_key, reverse = True
):
_bin = os.path.join(_default_root, _ver, "bin")
if os.path.isdir(_bin):
candidates.append(_bin)
@ -106,7 +108,9 @@ if sys.platform == "win32":
_all_vers_main: list[str] = []
for _pkg_dir in _bnb_spec.submodule_search_locations:
for _dll in _glob.glob(os.path.join(_pkg_dir, "libbitsandbytes_rocm*.dll")):
for _dll in _glob.glob(
os.path.join(_pkg_dir, "libbitsandbytes_rocm*.dll")
):
_found_rocm_bnb = True
_km = _re_bnb.search(
r"libbitsandbytes_rocm(\d+)\.dll", os.path.basename(_dll)
@ -124,7 +128,9 @@ if sys.platform == "win32":
# (HIP SDK on a CUDA/CPU box) must not force a ROCm backend onto a
# non-ROCm bitsandbytes, which raises at import. DLL unparsable -> "72".
if _found_rocm_bnb:
_bnb_rocm_ver_final = _bnb_rocm_ver or os.environ.get("BNB_ROCM_VERSION") or "72"
_bnb_rocm_ver_final = (
_bnb_rocm_ver or os.environ.get("BNB_ROCM_VERSION") or "72"
)
os.environ["BNB_ROCM_VERSION"] = _bnb_rocm_ver_final
os.environ["UNSLOTH_BNB_ROCM_VERSION_SOURCE"] = "detected"
_logging.getLogger(__name__).info(
@ -167,7 +173,9 @@ try:
configure_cpu_threads()
except ValueError as exc:
_raw = os.environ.get("UNSLOTH_CPU_THREADS")
raise SystemExit(f"Error: Invalid UNSLOTH_CPU_THREADS value {_raw!r}: {exc}") from None
raise SystemExit(
f"Error: Invalid UNSLOTH_CPU_THREADS value {_raw!r}: {exc}"
) from None
# Anaconda/conda-forge Python: seed platform._sys_version_cache before any
# library import triggers attrs -> rich -> structlog -> platform crash.
@ -219,7 +227,9 @@ def _read_studio_install_id() -> str:
/api/health emits "" and the launcher accepts any healthy backend.
Carries no install-path info (matters when Studio runs -H 0.0.0.0)."""
try:
token = (_STUDIO_ROOT_RESOLVED / "share" / "studio_install_id").read_text().strip()
token = (
(_STUDIO_ROOT_RESOLVED / "share" / "studio_install_id").read_text().strip()
)
except (OSError, ValueError):
return ""
return token if _STUDIO_INSTALL_ID_RE.fullmatch(token) else ""
@ -311,7 +321,9 @@ def get_unsloth_version() -> str:
except PackageNotFoundError:
pass
version_file = _Path(__file__).resolve().parents[2] / "unsloth" / "models" / "_utils.py"
version_file = (
_Path(__file__).resolve().parents[2] / "unsloth" / "models" / "_utils.py"
)
try:
for line in version_file.read_text(encoding = "utf-8").splitlines():
if line.startswith("__version__ = "):
@ -417,7 +429,9 @@ async def lifespan(app: FastAPI):
print(f"WARNING: {_msg}", flush = True)
except Exception as _probe_exc:
import structlog as _structlog
_structlog.get_logger(__name__).debug("llama.cpp startup probes failed: %s", _probe_exc)
_structlog.get_logger(__name__).debug(
"llama.cpp startup probes failed: %s", _probe_exc
)
from storage.studio_db import cleanup_orphaned_runs
@ -425,7 +439,9 @@ async def lifespan(app: FastAPI):
cleanup_orphaned_runs()
except Exception as exc:
import structlog
structlog.get_logger(__name__).warning("cleanup_orphaned_runs failed at startup: %s", exc)
structlog.get_logger(__name__).warning(
"cleanup_orphaned_runs failed at startup: %s", exc
)
_start_helper_precache_if_enabled()
@ -528,7 +544,9 @@ def _build_csp(script_nonce: "str | None" = None) -> str:
"https://*.googleusercontent.com wss://*.googleusercontent.com"
)
else:
connect_src = "'self' https://huggingface.co https://datasets-server.huggingface.co"
connect_src = (
"'self' https://huggingface.co https://datasets-server.huggingface.co"
)
return (
"default-src 'self'; "
@ -630,7 +648,11 @@ async def _send_411(send) -> None:
async def _send_413(send, total_bytes: int, max_bytes: int) -> None:
payload = _json_for_413.dumps(
{"detail": (f"Request body too large ({total_bytes:,} bytes; max {max_bytes:,}).")},
{
"detail": (
f"Request body too large ({total_bytes:,} bytes; max {max_bytes:,})."
)
},
).encode("utf-8")
await send(
{
@ -814,7 +836,9 @@ app.include_router(data_recipe_router, prefix = "/api/data-recipe", tags = ["dat
app.include_router(llama_router, prefix = "/api/llama", tags = ["llama"])
app.include_router(export_router, prefix = "/api/export", tags = ["export"])
app.include_router(rag_router, prefix = "/api/rag", tags = ["rag"])
app.include_router(training_history_router, prefix = "/api/train", tags = ["training-history"])
app.include_router(
training_history_router, prefix = "/api/train", tags = ["training-history"]
)
app.include_router(hub_inventory_router, prefix = "/api/hub", tags = ["hub"])
app.include_router(hub_datasets_router, prefix = "/api/hub/datasets", tags = ["hub"])
@ -856,7 +880,9 @@ async def health_check(request: Request):
from auth.authentication import get_current_subject as _gcs
from fastapi.security import HTTPAuthorizationCredentials
creds = HTTPAuthorizationCredentials(scheme = "Bearer", credentials = auth.split(" ", 1)[1])
creds = HTTPAuthorizationCredentials(
scheme = "Bearer", credentials = auth.split(" ", 1)[1]
)
# Must await: a bare coroutine is truthy and would skip the auth check
subject = await _gcs(creds)
except HTTPException:
@ -892,12 +918,16 @@ def studio_update_status(_current_subject: str = Depends(get_current_subject)):
"/api/studio/download-transport-capabilities",
response_model = TransportCapabilities,
)
def studio_download_transport_capabilities(_current_subject: str = Depends(get_current_subject)):
def studio_download_transport_capabilities(
_current_subject: str = Depends(get_current_subject),
):
return asdict(get_download_transport_capabilities())
@app.post("/api/shutdown")
async def shutdown_server(request: Request, current_subject: str = Depends(get_current_subject)):
async def shutdown_server(
request: Request, current_subject: str = Depends(get_current_subject)
):
"""Gracefully shut down the Unsloth Studio server.
Called by the frontend quit dialog so users can stop the server from the UI

View file

@ -24,13 +24,17 @@ class DesktopLoginRequest(BaseModel):
class RefreshTokenRequest(BaseModel):
"""Refresh token payload to obtain new access + refresh tokens."""
refresh_token: str = Field(..., description = "Refresh token from a previous login or refresh")
refresh_token: str = Field(
..., description = "Refresh token from a previous login or refresh"
)
class AuthStatusResponse(BaseModel):
"""Indicate whether the seeded admin auth flow is ready."""
initialized: bool = Field(..., description = "True if the auth database contains a login user")
initialized: bool = Field(
..., description = "True if the auth database contains a login user"
)
default_username: str = Field(
"unsloth",
description = "Default admin username for first-boot UI prefill.",
@ -71,7 +75,9 @@ class ApiKeyResponse(BaseModel):
id: int
name: str
key_prefix: str = Field(..., description = "First 8 characters after sk-unsloth- for display")
key_prefix: str = Field(
..., description = "First 8 characters after sk-unsloth- for display"
)
created_at: str
last_used_at: Optional[str] = None
expires_at: Optional[str] = None

View file

@ -101,7 +101,9 @@ class SeedInspectUploadRequest(BaseModel):
if not self.block_id:
raise ValueError("block_id is required when using file_ids")
if self.file_names is None or len(self.file_ids) != len(self.file_names):
raise ValueError("file_names must be provided and same length as file_ids")
raise ValueError(
"file_names must be provided and same length as file_ids"
)
if has_legacy:
if not self.filename:
raise ValueError("filename is required when using content_base64")

View file

@ -21,7 +21,11 @@ def _validate_save_directory(value: str) -> str:
if any(ch in raw for ch in ("\r", "\n")):
raise ValueError("save_directory may not contain control characters")
path = Path(raw).expanduser()
path_parts = (*path.parts, *PureWindowsPath(raw).parts, *raw.replace("\\", "/").split("/"))
path_parts = (
*path.parts,
*PureWindowsPath(raw).parts,
*raw.replace("\\", "/").split("/"),
)
if any(len(part) > 255 for part in path_parts if part not in ("", ".", "/", "\\")):
raise ValueError("save_directory path components must be <= 255 characters")
if (

View file

@ -26,7 +26,9 @@ class LoadRequest(BaseModel):
native_path_lease: Optional[str] = Field(
None, description = "Frontend-visible signed native path grant"
)
hf_token: Optional[str] = Field(None, description = "HuggingFace token for gated models")
hf_token: Optional[str] = Field(
None, description = "HuggingFace token for gated models"
)
max_seq_length: int = Field(
0,
ge = 0,
@ -49,7 +51,9 @@ class LoadRequest(BaseModel):
@field_validator("chat_template_override")
@classmethod
def normalize_blank_chat_template_override(cls, value: Optional[str]) -> Optional[str]:
def normalize_blank_chat_template_override(
cls, value: Optional[str]
) -> Optional[str]:
if value is not None and value.strip() == "":
return None
return value
@ -121,7 +125,9 @@ class ValidateModelRequest(BaseModel):
native_path_lease: Optional[str] = Field(
None, description = "Frontend-visible signed native path grant"
)
hf_token: Optional[str] = Field(None, description = "HuggingFace token for gated models")
hf_token: Optional[str] = Field(
None, description = "HuggingFace token for gated models"
)
gguf_variant: Optional[str] = Field(
None, description = "GGUF quantization variant (e.g. 'Q4_K_M')"
)
@ -136,7 +142,9 @@ class ValidateModelResponse(BaseModel):
valid: bool = Field(..., description = "Whether the model identifier looks valid")
message: str = Field(..., description = "Human-readable validation message")
identifier: Optional[str] = Field(None, description = "Resolved model identifier")
display_name: Optional[str] = Field(None, description = "Display name derived from identifier")
display_name: Optional[str] = Field(
None, description = "Display name derived from identifier"
)
is_gguf: bool = Field(False, description = "Whether this is a GGUF model (llama.cpp)")
is_lora: bool = Field(False, description = "Whether this is a LoRA adapter")
is_vision: bool = Field(False, description = "Whether this is a vision-capable model")
@ -154,10 +162,16 @@ class GenerateRequest(BaseModel):
temperature: float = Field(0.6, ge = 0.0, le = 2.0, description = "Sampling temperature")
top_p: float = Field(0.95, ge = 0.0, le = 1.0, description = "Top-p sampling")
top_k: int = Field(20, ge = -1, le = 100, description = "Top-k sampling")
max_new_tokens: int = Field(2048, ge = 1, le = 4096, description = "Maximum tokens to generate")
repetition_penalty: float = Field(1.0, ge = 1.0, le = 2.0, description = "Repetition penalty")
max_new_tokens: int = Field(
2048, ge = 1, le = 4096, description = "Maximum tokens to generate"
)
repetition_penalty: float = Field(
1.0, ge = 1.0, le = 2.0, description = "Repetition penalty"
)
presence_penalty: float = Field(0.0, ge = 0.0, le = 2.0, description = "Presence penalty")
image_base64: Optional[str] = Field(None, description = "Base64 encoded image for vision models")
image_base64: Optional[str] = Field(
None, description = "Base64 encoded image for vision models"
)
class LoadResponse(BaseModel):
@ -168,13 +182,19 @@ class LoadResponse(BaseModel):
display_name: str = Field(..., description = "Display name of the model")
is_vision: bool = Field(False, description = "Whether model is a vision model")
is_lora: bool = Field(False, description = "Whether model is a LoRA adapter")
is_gguf: bool = Field(False, description = "Whether model is a GGUF model (llama.cpp)")
is_gguf: bool = Field(
False, description = "Whether model is a GGUF model (llama.cpp)"
)
is_diffusion: bool = Field(
False, description = "Whether model is a block-diffusion model (DiffusionGemma)"
)
is_audio: bool = Field(False, description = "Whether model is a TTS audio model")
audio_type: Optional[str] = Field(None, description = "Audio codec type: snac, csm, bicodec, dac")
has_audio_input: bool = Field(False, description = "Whether model accepts audio input (ASR)")
audio_type: Optional[str] = Field(
None, description = "Audio codec type: snac, csm, bicodec, dac"
)
has_audio_input: bool = Field(
False, description = "Whether model accepts audio input (ASR)"
)
inference: dict = Field(
..., description = "Inference parameters (temperature, top_p, top_k, min_p)"
)
@ -274,7 +294,9 @@ class LoadProgressResponse(BaseModel):
0,
description = "Total bytes across all GGUF shards for the active model.",
)
fraction: float = Field(0.0, description = "bytes_loaded / bytes_total, clamped to 0..1.")
fraction: float = Field(
0.0, description = "bytes_loaded / bytes_total, clamped to 0..1."
)
class InferenceStatusResponse(BaseModel):
@ -287,17 +309,34 @@ class InferenceStatusResponse(BaseModel):
None,
description = "Loadable identifier for the active model.",
)
is_vision: bool = Field(False, description = "Whether the active model is a vision model")
is_gguf: bool = Field(False, description = "Whether the active model is a GGUF model (llama.cpp)")
is_diffusion: bool = Field(
False, description = "Whether the active model is a block-diffusion model (DiffusionGemma)"
is_vision: bool = Field(
False, description = "Whether the active model is a vision model"
)
is_gguf: bool = Field(
False, description = "Whether the active model is a GGUF model (llama.cpp)"
)
is_diffusion: bool = Field(
False,
description = "Whether the active model is a block-diffusion model (DiffusionGemma)",
)
gguf_variant: Optional[str] = Field(
None, description = "GGUF quantization variant (e.g. Q4_K_M)"
)
is_audio: bool = Field(
False, description = "Whether the active model is a TTS audio model"
)
audio_type: Optional[str] = Field(
None, description = "Audio codec type: snac, csm, bicodec, dac"
)
has_audio_input: bool = Field(
False, description = "Whether model accepts audio input (ASR)"
)
loading: List[str] = Field(
default_factory = list, description = "Models currently being loaded"
)
loaded: List[str] = Field(
default_factory = list, description = "Models currently loaded"
)
gguf_variant: Optional[str] = Field(None, description = "GGUF quantization variant (e.g. Q4_K_M)")
is_audio: bool = Field(False, description = "Whether the active model is a TTS audio model")
audio_type: Optional[str] = Field(None, description = "Audio codec type: snac, csm, bicodec, dac")
has_audio_input: bool = Field(False, description = "Whether model accepts audio input (ASR)")
loading: List[str] = Field(default_factory = list, description = "Models currently being loaded")
loaded: List[str] = Field(default_factory = list, description = "Models currently loaded")
inference: Optional[Dict[str, Any]] = Field(
None, description = "Recommended inference parameters for the active model"
)
@ -322,7 +361,9 @@ class InferenceStatusResponse(BaseModel):
supports_tools: bool = Field(
False, description = "Whether the active model supports tool calling"
)
context_length: Optional[int] = Field(None, description = "Context length of the active model")
context_length: Optional[int] = Field(
None, description = "Context length of the active model"
)
max_context_length: Optional[int] = Field(
None,
description = "Maximum context length currently available for the active model",
@ -651,7 +692,9 @@ class ChatCompletionRequest(BaseModel):
parallel_tool_calls: Optional[bool] = Field(
None, description = "Whether to enable parallel function calling during tool use."
)
seed: Optional[int] = Field(None, description = "Best-effort deterministic sampling seed.")
seed: Optional[int] = Field(
None, description = "Best-effort deterministic sampling seed."
)
stream_options: Optional[dict] = Field(
None,
description = 'Streaming options, e.g. {"include_usage": true} to emit a final usage chunk.',
@ -659,7 +702,9 @@ class ChatCompletionRequest(BaseModel):
# ── Unsloth extensions (ignored by standard OpenAI clients) ──
top_k: int = Field(20, ge = -1, le = 100, description = "[x-unsloth] Top-k sampling")
min_p: float = Field(0.01, ge = 0.0, le = 1.0, description = "[x-unsloth] Min-p sampling threshold")
min_p: float = Field(
0.01, ge = 0.0, le = 1.0, description = "[x-unsloth] Min-p sampling threshold"
)
repetition_penalty: float = Field(
1.0, ge = 1.0, le = 2.0, description = "[x-unsloth] Repetition penalty"
)
@ -935,7 +980,9 @@ class ChatCompletionRequest(BaseModel):
if not tc_id:
continue
function = tc.get("function")
function_name = function.get("name") if isinstance(function, dict) else None
function_name = (
function.get("name") if isinstance(function, dict) else None
)
if msg.name and function_name == msg.name:
name_match = (tc_id, asst_idx, tc_idx)
break
@ -1030,7 +1077,9 @@ class ChoiceDelta(BaseModel):
content: Optional[str] = None
OpenAIFinishReason = Literal["stop", "length", "tool_calls", "content_filter", "function_call"]
OpenAIFinishReason = Literal[
"stop", "length", "tool_calls", "content_filter", "function_call"
]
class ChunkChoice(BaseModel):
@ -1183,13 +1232,17 @@ class ResponsesFunctionCallInputItem(BaseModel):
"""
type: Literal["function_call"]
id: Optional[str] = Field(None, description = "Item id assigned by the server (e.g. fc_...)")
id: Optional[str] = Field(
None, description = "Item id assigned by the server (e.g. fc_...)"
)
call_id: str = Field(
...,
description = "Correlation id matching a function_call_output on the next turn.",
)
name: str
arguments: str = Field(..., description = "JSON string of the arguments the model produced.")
arguments: str = Field(
..., description = "JSON string of the arguments the model produced."
)
status: Optional[Literal["in_progress", "completed", "incomplete"]] = None
@ -1276,7 +1329,9 @@ class ResponsesRequest(BaseModel):
default = [],
description = "Input text or list of messages / function_call / function_call_output items",
)
instructions: Optional[str] = Field(None, description = "System / developer instructions")
instructions: Optional[str] = Field(
None, description = "System / developer instructions"
)
temperature: Optional[float] = Field(None, ge = 0.0, le = 2.0)
top_p: Optional[float] = Field(None, ge = 0.0, le = 1.0)
max_output_tokens: Optional[int] = Field(None, ge = 1)
@ -1364,7 +1419,9 @@ class ResponsesOutputFunctionCall(BaseModel):
id: str = Field(default_factory = lambda: f"fc_{uuid.uuid4().hex[:12]}")
call_id: str
name: str
arguments: str = Field(..., description = "JSON string of the arguments the model produced.")
arguments: str = Field(
..., description = "JSON string of the arguments the model produced."
)
status: Literal["completed", "in_progress", "incomplete"] = "completed"
@ -1477,12 +1534,16 @@ def _merge_anthropic_system(system: Any, additions: list[str]) -> Any:
if not additions:
return system
addition_blocks = [{"type": "text", "text": text} for text in additions if text.strip()]
addition_blocks = [
{"type": "text", "text": text} for text in additions if text.strip()
]
if not addition_blocks:
return system
if system is None:
return addition_blocks[0]["text"] if len(addition_blocks) == 1 else addition_blocks
return (
addition_blocks[0]["text"] if len(addition_blocks) == 1 else addition_blocks
)
if isinstance(system, str):
return "\n\n".join([system, *[block["text"] for block in addition_blocks]])
if isinstance(system, list):
@ -1561,7 +1622,9 @@ class AnthropicMessagesRequest(BaseModel):
normalized = dict(data)
normalized["messages"] = normalized_messages
normalized["system"] = _merge_anthropic_system(normalized.get("system"), system_additions)
normalized["system"] = _merge_anthropic_system(
normalized.get("system"), system_additions
)
return normalized
@ -1587,7 +1650,9 @@ class AnthropicResponseToolUseBlock(BaseModel):
input: dict
AnthropicResponseBlock = Union[AnthropicResponseTextBlock, AnthropicResponseToolUseBlock]
AnthropicResponseBlock = Union[
AnthropicResponseTextBlock, AnthropicResponseToolUseBlock
]
class AnthropicMessagesResponse(BaseModel):

View file

@ -53,5 +53,7 @@ class McpServerImportRequest(BaseModel):
class McpServerImportResult(BaseModel):
created: list[McpServerResponse] = Field(default_factory = list)
skipped: list[str] = Field(default_factory = list) # display names skipped as duplicates
skipped: list[str] = Field(
default_factory = list
) # display names skipped as duplicates
errors: list[str] = Field(default_factory = list)

View file

@ -12,7 +12,9 @@ ModelType = Literal["text", "vision", "audio", "embeddings"]
class CheckpointInfo(BaseModel):
"""Information about a discovered checkpoint directory."""
display_name: str = Field(..., description = "User-friendly checkpoint name (folder name)")
display_name: str = Field(
..., description = "User-friendly checkpoint name (folder name)"
)
path: str = Field(..., description = "Full path to the checkpoint directory")
loss: Optional[float] = Field(None, description = "Training loss at this checkpoint")
@ -61,23 +63,33 @@ class ModelDetails(BaseModel):
None, description = "Model identifier (alias for id, for backward compatibility)"
)
name: Optional[str] = Field(None, description = "Display name for the model")
config: Optional[Dict[str, Any]] = Field(None, description = "Model configuration dictionary")
config: Optional[Dict[str, Any]] = Field(
None, description = "Model configuration dictionary"
)
is_vision: bool = Field(False, description = "Whether model is a vision model")
is_embedding: bool = Field(
False, description = "Whether model is an embedding/sentence-transformer model"
)
is_lora: bool = Field(False, description = "Whether model is a LoRA adapter")
is_gguf: bool = Field(False, description = "Whether model is a GGUF model (llama.cpp format)")
is_gguf: bool = Field(
False, description = "Whether model is a GGUF model (llama.cpp format)"
)
is_mlx: bool = Field(
False, description = "Whether model is served via the MLX backend (Apple Silicon)"
)
is_audio: bool = Field(False, description = "Whether model is a TTS audio model")
audio_type: Optional[str] = Field(None, description = "Audio codec type: snac, csm, bicodec, dac")
has_audio_input: bool = Field(False, description = "Whether model accepts audio input (ASR)")
audio_type: Optional[str] = Field(
None, description = "Audio codec type: snac, csm, bicodec, dac"
)
has_audio_input: bool = Field(
False, description = "Whether model accepts audio input (ASR)"
)
model_type: Optional[ModelType] = Field(
None, description = "Collapsed model modality: text, vision, audio, or embeddings"
)
base_model: Optional[str] = Field(None, description = "Base model if this is a LoRA adapter")
base_model: Optional[str] = Field(
None, description = "Base model if this is a LoRA adapter"
)
max_position_embeddings: Optional[int] = Field(
None, description = "Maximum context length supported by the model"
)
@ -90,7 +102,9 @@ class LoRAInfo(BaseModel):
"""LoRA adapter or exported model information"""
display_name: str = Field(..., description = "Display name for the LoRA")
adapter_path: str = Field(..., description = "Path to the LoRA adapter or exported model")
adapter_path: str = Field(
..., description = "Path to the LoRA adapter or exported model"
)
base_model: Optional[str] = Field(None, description = "Base model identifier")
source: Optional[str] = Field(None, description = "'training' or 'exported'")
export_type: Optional[str] = Field(
@ -101,21 +115,29 @@ class LoRAInfo(BaseModel):
class LoRAScanResponse(BaseModel):
"""Response schema for scanning trained LoRA adapters"""
loras: List[LoRAInfo] = Field(default_factory = list, description = "List of found LoRA adapters")
loras: List[LoRAInfo] = Field(
default_factory = list, description = "List of found LoRA adapters"
)
outputs_dir: str = Field(..., description = "Directory that was scanned")
class ModelListResponse(BaseModel):
"""Response schema for listing models"""
models: List[ModelDetails] = Field(default_factory = list, description = "List of models")
default_models: List[str] = Field(default_factory = list, description = "List of default model IDs")
models: List[ModelDetails] = Field(
default_factory = list, description = "List of models"
)
default_models: List[str] = Field(
default_factory = list, description = "List of default model IDs"
)
class GgufVariantDetail(BaseModel):
"""A single GGUF quantization variant in a HuggingFace repo."""
filename: str = Field(..., description = "GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')")
filename: str = Field(
..., description = "GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')"
)
quant: str = Field(..., description = "Quantization label (e.g., 'Q4_K_M')")
size_bytes: int = Field(0, description = "File size in bytes")
downloaded: bool = Field(
@ -161,7 +183,9 @@ class LocalModelInfo(BaseModel):
class LocalModelListResponse(BaseModel):
"""Response schema for listing local/cached models."""
models_dir: str = Field(..., description = "Directory scanned for custom local models")
models_dir: str = Field(
..., description = "Directory scanned for custom local models"
)
hf_cache_dir: Optional[str] = Field(
None,
description = "HF cache root that was scanned",
@ -179,7 +203,9 @@ class LocalModelListResponse(BaseModel):
class AddScanFolderRequest(BaseModel):
"""Request body for adding a custom scan folder."""
path: str = Field(..., description = "Absolute or relative directory path to scan for models")
path: str = Field(
..., description = "Absolute or relative directory path to scan for models"
)
class ScanFolderInfo(BaseModel):

View file

@ -14,7 +14,9 @@ from pydantic import BaseModel, Field
class ProviderRegistryEntry(BaseModel):
"""A supported provider type with its default configuration."""
provider_type: str = Field(..., description = "Provider identifier (e.g. 'openai', 'mistral')")
provider_type: str = Field(
..., description = "Provider identifier (e.g. 'openai', 'mistral')"
)
display_name: str = Field(..., description = "Human-readable provider name")
base_url: str = Field(..., description = "Default API base URL")
default_models: list[str] = Field(
@ -42,7 +44,9 @@ class ProviderCreate(BaseModel):
"""Request to create a saved provider configuration."""
provider_type: str = Field(..., description = "Provider type from the registry")
display_name: str = Field(..., description = "User-chosen label (e.g. 'My OpenAI Key')")
display_name: str = Field(
..., description = "User-chosen label (e.g. 'My OpenAI Key')"
)
base_url: Optional[str] = Field(
None,
description = "Custom base URL (overrides registry default). Omit to use the default.",
@ -54,7 +58,9 @@ class ProviderUpdate(BaseModel):
display_name: Optional[str] = Field(None, description = "New display name")
base_url: Optional[str] = Field(None, description = "New base URL")
is_enabled: Optional[bool] = Field(None, description = "Enable or disable this provider")
is_enabled: Optional[bool] = Field(
None, description = "Enable or disable this provider"
)
class ProviderResponse(BaseModel):
@ -77,7 +83,9 @@ class ProviderModelInfo(BaseModel):
id: str = Field(..., description = "Model ID as expected by the provider API")
display_name: str = Field("", description = "Human-readable model name")
context_length: Optional[int] = Field(None, description = "Maximum context length in tokens")
context_length: Optional[int] = Field(
None, description = "Maximum context length in tokens"
)
owned_by: Optional[str] = Field(None, description = "Model owner/organization")

View file

@ -21,10 +21,16 @@ class TrainingStopResponse(BaseModel):
class TrainingMetricsResponse(BaseModel):
"""Response for training metrics history"""
loss_history: List[float] = Field(default_factory = list, description = "Loss values per step")
lr_history: List[float] = Field(default_factory = list, description = "Learning rate per step")
loss_history: List[float] = Field(
default_factory = list, description = "Loss values per step"
)
lr_history: List[float] = Field(
default_factory = list, description = "Learning rate per step"
)
step_history: List[int] = Field(default_factory = list, description = "Step numbers")
grad_norm_history: List[float] = Field(default_factory = list, description = "Gradient norm values")
grad_norm_history: List[float] = Field(
default_factory = list, description = "Gradient norm values"
)
grad_norm_step_history: List[int] = Field(
default_factory = list, description = "Step numbers for gradient norm values"
)

View file

@ -37,7 +37,9 @@ class S3Config(BaseModel):
bucket: str = Field(..., description = "S3 bucket name")
region: str = Field("us-east-1", description = "AWS region")
prefix: Optional[str] = Field(None, description = "Optional path prefix within bucket")
prefix: Optional[str] = Field(
None, description = "Optional path prefix within bucket"
)
access_key_id: Optional[str] = Field(
None,
alias = "accessKeyId",
@ -58,7 +60,9 @@ class S3Config(BaseModel):
def _check_credentials(self) -> "S3Config":
# Require either IAM role auth or a full key pair so credentials are
# never half-configured.
if not self.use_iam_role and not (self.access_key_id and self.secret_access_key):
if not self.use_iam_role and not (
self.access_key_id and self.secret_access_key
):
raise ValueError(
"s3_config requires either use_iam_role=True or both "
"access_key_id and secret_access_key"
@ -77,7 +81,9 @@ def _parse_lr(v: Any) -> float:
except (TypeError, ValueError):
raise ValueError(f"learning_rate must be parseable as float (got {v!r})")
if not (lr > 0.0):
raise ValueError(f"learning_rate must be > 0 (got {lr!r}); typical range is 1e-6 .. 1e-3")
raise ValueError(
f"learning_rate must be > 0 (got {lr!r}); typical range is 1e-6 .. 1e-3"
)
if lr >= _MAX_LR_VALUE:
raise ValueError(
f"learning_rate must be < 1.0 (got {lr!r}); "
@ -93,9 +99,11 @@ class TrainingStartRequest(BaseModel):
model_name: str = Field(
..., description = "Model identifier (e.g., 'unsloth/llama-3-8b-bnb-4bit')"
)
training_type: Literal["LoRA/QLoRA", "Full Finetuning", "Continued Pretraining"] = Field(
...,
description = "Training type: 'LoRA/QLoRA', 'Full Finetuning', or 'Continued Pretraining'",
training_type: Literal["LoRA/QLoRA", "Full Finetuning", "Continued Pretraining"] = (
Field(
...,
description = "Training type: 'LoRA/QLoRA', 'Full Finetuning', or 'Continued Pretraining'",
)
)
hf_token: Optional[str] = Field(None, description = "HuggingFace token")
load_in_4bit: bool = Field(True, description = "Load model in 4-bit quantization")
@ -110,7 +118,9 @@ class TrainingStartRequest(BaseModel):
)
# Dataset parameters
hf_dataset: Optional[str] = Field(None, description = "HuggingFace dataset identifier")
hf_dataset: Optional[str] = Field(
None, description = "HuggingFace dataset identifier"
)
local_datasets: List[str] = Field(
default_factory = list, description = "List of local dataset paths"
)
@ -120,8 +130,12 @@ class TrainingStartRequest(BaseModel):
format_type: str = Field(..., description = "Dataset format type")
subset: Optional[str] = None
train_split: Optional[str] = Field("train", description = "Training split name")
eval_split: Optional[str] = Field(None, description = "Eval split name. None = auto-detect")
eval_steps: float = Field(0.00, description = "Fraction of total steps between evals (0-1)")
eval_split: Optional[str] = Field(
None, description = "Eval split name. None = auto-detect"
)
eval_steps: float = Field(
0.00, description = "Fraction of total steps between evals (0-1)"
)
dataset_slice_start: Optional[int] = Field(
None, description = "Inclusive start row index for dataset slicing"
)
@ -150,7 +164,9 @@ class TrainingStartRequest(BaseModel):
if v is None:
raise ValueError("batch_size is required")
if v < 1 or v > _MAX_BATCH_SIZE:
raise ValueError(f"batch_size must be in [1, {_MAX_BATCH_SIZE}] (got {v!r})")
raise ValueError(
f"batch_size must be in [1, {_MAX_BATCH_SIZE}] (got {v!r})"
)
return v
@field_validator("gradient_accumulation_steps")
@ -160,7 +176,8 @@ class TrainingStartRequest(BaseModel):
return 1
if v < 1 or v > _MAX_GRAD_ACCUM:
raise ValueError(
f"gradient_accumulation_steps must be in [1, {_MAX_GRAD_ACCUM}] " f"(got {v!r})"
f"gradient_accumulation_steps must be in [1, {_MAX_GRAD_ACCUM}] "
f"(got {v!r})"
)
return v
@ -181,14 +198,18 @@ class TrainingStartRequest(BaseModel):
if v is None:
return v
if not isinstance(v, int) or v < 0 or v > _MAX_STEPS:
raise ValueError(f"max_steps must be a non-negative int <= {_MAX_STEPS} (got {v!r})")
raise ValueError(
f"max_steps must be a non-negative int <= {_MAX_STEPS} (got {v!r})"
)
return v
@field_validator("max_seq_length")
@classmethod
def _check_max_seq_length(cls, v: int) -> int:
if v is None or v < 1 or v > _MAX_SEQ_LENGTH:
raise ValueError(f"max_seq_length must be in [1, {_MAX_SEQ_LENGTH}] (got {v!r})")
raise ValueError(
f"max_seq_length must be in [1, {_MAX_SEQ_LENGTH}] (got {v!r})"
)
return v
@field_validator("vision_image_size", mode = "before")
@ -231,7 +252,8 @@ class TrainingStartRequest(BaseModel):
return v
if not isinstance(v, int) or v < 0 or v > _MAX_STEPS:
raise ValueError(
f"warmup_steps must be a non-negative int <= {_MAX_STEPS} " f"(got {v!r})"
f"warmup_steps must be a non-negative int <= {_MAX_STEPS} "
f"(got {v!r})"
)
return v
@ -267,7 +289,9 @@ class TrainingStartRequest(BaseModel):
except (TypeError, ValueError):
raise ValueError(f"weight_decay must be a number (got {v!r})")
if wd < 0 or wd > 10.0:
raise ValueError(f"weight_decay must be in [0, 10] (got {wd!r}); typical 0..0.1")
raise ValueError(
f"weight_decay must be in [0, 10] (got {wd!r}); typical 0..0.1"
)
return wd
@field_validator("lora_r")
@ -285,7 +309,9 @@ class TrainingStartRequest(BaseModel):
if v is None:
return 16
if v < 1 or v > _MAX_LORA_ALPHA:
raise ValueError(f"lora_alpha must be in [1, {_MAX_LORA_ALPHA}] (got {v!r})")
raise ValueError(
f"lora_alpha must be in [1, {_MAX_LORA_ALPHA}] (got {v!r})"
)
return v
@field_validator("lora_dropout")
@ -314,7 +340,9 @@ class TrainingStartRequest(BaseModel):
num_epochs: int = Field(1, description = "Number of training epochs")
learning_rate: str = Field("2e-4", description = "Learning rate")
batch_size: int = Field(1, description = "Batch size")
gradient_accumulation_steps: int = Field(1, description = "Gradient accumulation steps")
gradient_accumulation_steps: int = Field(
1, description = "Gradient accumulation steps"
)
warmup_steps: Optional[int] = Field(None, description = "Warmup steps")
warmup_ratio: Optional[float] = Field(None, description = "Warmup ratio")
max_steps: Optional[int] = Field(None, description = "Maximum training steps")
@ -342,19 +370,31 @@ class TrainingStartRequest(BaseModel):
lora_r: int = Field(16, description = "LoRA rank")
lora_alpha: int = Field(16, description = "LoRA alpha")
lora_dropout: float = Field(0.0, description = "LoRA dropout")
target_modules: List[str] = Field(default_factory = list, description = "Target modules for LoRA")
gradient_checkpointing: str = Field("", description = "Gradient checkpointing setting")
target_modules: List[str] = Field(
default_factory = list, description = "Target modules for LoRA"
)
gradient_checkpointing: str = Field(
"", description = "Gradient checkpointing setting"
)
use_rslora: bool = Field(False, description = "Use RSLoRA")
use_loftq: bool = Field(False, description = "Use LoftQ")
train_on_completions: bool = Field(False, description = "Train on completions only")
# Vision-specific LoRA parameters
finetune_vision_layers: bool = Field(False, description = "Finetune vision layers")
finetune_language_layers: bool = Field(False, description = "Finetune language layers")
finetune_attention_modules: bool = Field(False, description = "Finetune attention modules")
finetune_language_layers: bool = Field(
False, description = "Finetune language layers"
)
finetune_attention_modules: bool = Field(
False, description = "Finetune attention modules"
)
finetune_mlp_modules: bool = Field(False, description = "Finetune MLP modules")
is_dataset_image: bool = Field(False, description = "Whether the dataset contains image data")
is_dataset_audio: bool = Field(False, description = "Whether the dataset contains audio data")
is_dataset_image: bool = Field(
False, description = "Whether the dataset contains image data"
)
is_dataset_audio: bool = Field(
False, description = "Whether the dataset contains audio data"
)
is_embedding: bool = Field(
False, description = "Whether model is an embedding/sentence-transformer model"
)
@ -385,7 +425,9 @@ class TrainingStartRequest(BaseModel):
def _check_steps_or_epochs(self) -> "TrainingStartRequest":
# Each accepts 0 as "use the other"; both 0 means nothing to train.
if (self.max_steps is None or self.max_steps == 0) and self.num_epochs == 0:
raise ValueError("Either num_epochs or max_steps must be > 0; both cannot be 0.")
raise ValueError(
"Either num_epochs or max_steps must be > 0; both cannot be 0."
)
return self
@ -412,7 +454,9 @@ class TrainingStatus(BaseModel):
"error",
"stopped",
] = Field(..., description = "Current phase of training pipeline")
is_training_running: bool = Field(..., description = "True if training loop is actively running")
is_training_running: bool = Field(
..., description = "True if training loop is actively running"
)
eval_enabled: bool = Field(
False,
description = "True if evaluation dataset is configured for this training run",
@ -437,7 +481,9 @@ class TrainingProgress(BaseModel):
total_steps: int = Field(..., description = "Total training steps")
loss: Optional[float] = Field(None, description = "Current loss value")
learning_rate: Optional[float] = Field(None, description = "Current learning rate")
progress_percent: float = Field(..., description = "Progress percentage (0.0 to 100.0)")
progress_percent: float = Field(
..., description = "Progress percentage (0.0 to 100.0)"
)
epoch: Optional[float] = Field(None, description = "Current epoch")
elapsed_seconds: Optional[float] = Field(
None, description = "Time elapsed since training started"
@ -446,7 +492,9 @@ class TrainingProgress(BaseModel):
grad_norm: Optional[float] = Field(
None, description = "L2 norm of gradients, computed before gradient clipping"
)
num_tokens: Optional[int] = Field(None, description = "Total number of tokens processed so far")
num_tokens: Optional[int] = Field(
None, description = "Total number of tokens processed so far"
)
eval_loss: Optional[float] = Field(
None, description = "Eval loss from the most recent evaluation step"
)

Some files were not shown because too many files have changed in this diff Show more