Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
348 lines (319 loc) · 15.8 KB
/
Copy pathpyproject.toml
File metadata and controls
348 lines (319 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# NOSONAR text:S8565 — Sonar wants a deterministic uv.lock / poetry.lock.
# ForgeLM does not use uv or poetry; supply-chain determinism is provided
# instead by (a) bounded dependency ranges in [project.dependencies], (b)
# pip-audit nightly gating in .github/workflows/nightly.yml, (c) per-OS
# CycloneDX SBOM emission in publish.yml. A pip-tools workflow producing
# requirements.lock for development environments is tracked under
# F-PR29-A6-lock-file in docs/roadmap/risks-and-decisions.md (v0.6.x).
[build-system]
# setuptools floor is a SECURITY floor, not a feature floor: 83.0.0 is the
# first release carrying the fix for PYSEC-2026-3447, which the nightly
# supply-chain scan (pip-audit) fails closed on. Raise it here so a
# build-from-source never provisions a vulnerable build toolchain.
requires = ["setuptools>=83.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "forgelm"
version = "0.11.1rc1"
description = "Config-driven LLM fine-tuning with safety evaluation, EU AI Act compliance, 6 alignment methods, and one-command bundled quickstart templates."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "Apache-2.0" }
authors = [{ name = "ForgeLM Contributors" }]
keywords = [
"llm", "fine-tuning", "lora", "qlora", "dpo", "rlhf",
"transformers", "peft", "trl", "safety", "eu-ai-act",
"deepspeed", "alignment", "machine-learning",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
]
dependencies = [
"torch>=2.4.0,<3.0.0",
# transformers 5.x requires torch>=2.4. PyTorch Foundation never published
# >=2.3 wheels for x86_64 Darwin, so Intel Macs (x86_64) can no longer
# install ForgeLM's core dependency stack — that platform was dropped with
# the transformers-5 migration (v0.9.0). Apple Silicon, Linux and Windows
# are unaffected. The previous `numpy<2; darwin x86_64` ABI-guard marker is
# gone with the platform it protected (torch 2.4+ ships against NumPy 2.x).
# 5.10.1 is the first installable release past three advisories that reach ForgeLM's own load and save path from
# a Hub repo: CVE-2026-4372 (fixed 5.3.0), CVE-2026-5241 (a LightGlue config.json overrides trust_remote_code;
# fixed 5.5.0) and CVE-2026-9856 (chat_template names used unsanitised as file names by save_pretrained; fixed
# 5.10.0, which PyPI yanked, hence 5.10.1). It also needs huggingface_hub>=1.5.0.
"transformers>=5.10.1,<6.0.0",
"peft>=0.19.0,<1.0.0",
"datasets>=4.7.0,<6.0.0",
"accelerate>=1.4.0,<2.0.0",
"pyyaml>=6.0.1,<7.0.0",
"pydantic>=2.0.0,<3.0.0",
"trl>=1.0.0,<2.0.0",
"tensorboard>=2.15.0,<3.0.0",
"huggingface_hub>=1.5.0,<3.0.0",
"requests>=2.32.2,<3.0.0",
# requests-toolbelt is required by forgelm._http for the HostHeaderSSLAdapter
# used to close the DNS-rebinding TOCTOU window in safe_post / safe_get
# (issue #14). Small (~30KB) and from the same requests ecosystem, so it's
# a hard dep rather than an optional extra — SSRF hardening must always be
# active, not opt-in.
"requests-toolbelt>=1.0.0,<2.0.0",
]
[project.optional-dependencies]
# QLoRA via bitsandbytes 4-bit/8-bit quantization (Linux-only for most setups).
qlora = [
"bitsandbytes>=0.42.0,<1.0.0; sys_platform == 'linux'",
]
# Unsloth fused-kernel acceleration for SFT/DPO (Linux-only).
unsloth = [
"unsloth>=2024.3.0; sys_platform == 'linux'",
]
# Benchmarking — lm-eval-harness for OpenLLM-style tasks.
eval = [
"lm-eval>=0.4.4,<1.0.0",
]
# Experiment tracking — Weights & Biases run logging.
# Historically named `tracking` when W&B was the only supported backend;
# kept under that name because renaming it would break every existing
# `pip install forgelm[tracking]`. MLflow lives in its own extra below.
tracking = [
"wandb>=0.16.0",
]
# Experiment tracking via MLflow. `training.report_to: "mlflow"` is an
# accepted config value and transformers ships the MLflowCallback, but the
# callback imports mlflow at runtime — so without this extra a run that asks
# for mlflow tracking dies at trainer construction. Split out rather than
# folded into `tracking` because mlflow pulls a large dependency tree that a
# W&B-only user should not have to install.
tracking-mlflow = [
"mlflow>=2.9.0",
]
# Multi-GPU/multi-node training via DeepSpeed (ZeRO-1/2/3).
# DeepSpeed publishes no Windows wheels, so an unmarked requirement sends
# `pip install forgelm[distributed]` on Windows into a source build needing
# MSVC + the CUDA toolkit, which typically fails and takes the whole install
# down with it — including the parts that would have worked. The marker makes
# Windows degrade the way `qlora` and `unsloth` already do: the install
# succeeds, DeepSpeed is simply absent, and the operator meets the project's
# standard optional-dependency contract (ImportError with an install hint) only
# if they actually reach a ZeRO/FSDP path. Failing the whole install for a
# backend the user may not use is the worse trade.
distributed = [
"deepspeed>=0.14.0; sys_platform != 'win32'",
]
# Model merging (TIES / DARE / SLERP / linear) is implemented natively on
# state dicts via the core peft + torch deps — no extra packages are required
# (mergekit was declared here but never imported anywhere; it carried its own
# tight transformers/torch pins that could downgrade a training env for zero
# benefit). Kept as a no-op extra so `pip install forgelm[merging]` still resolves.
merging = []
# GGUF export. forgelm export runs llama.cpp's convert_hf_to_gguf.py, which no
# llama-cpp-python wheel puts where ForgeLM searches: point FORGELM_GGUF_CONVERTER
# at the copy in a llama.cpp checkout (docs/usermanuals/en/getting-started/installation.md).
# Produces f16 and q8_0 GGUF directly. K-quants (q4_k_m etc.) require a
# separate llama-quantize step; see docs/usermanuals/en/deployment/gguf-export.md for the full flow.
# Linux and macOS only; Windows builds are available but less reliable.
export = [
"llama-cpp-python>=0.2.90; sys_platform != 'win32'",
]
# Document ingestion — PDF/DOCX/EPUB/TXT → SFT-ready JSONL.
# pypdf is pure-Python (cross-platform); ebooklib + beautifulsoup4 parse
# EPUB containers; langdetect powers the audit's top-3 language stat.
# xxhash is an optional non-cryptographic 64-bit hash that materially speeds
# up the simhash backend; we fall back to BLAKE2b when it is not present
# so a bare install still works.
# OCR is intentionally out of scope (handle externally with Tesseract).
ingestion = [
"pypdf>=4.0.0,<7.0.0",
"python-docx>=1.0.0,<2.0.0",
"ebooklib>=0.18,<1.0.0",
"beautifulsoup4>=4.12.0,<5.0.0",
"langdetect>=1.0.9,<2.0.0",
"xxhash>=3.0.0,<5.0.0",
]
# Optional MinHash LSH near-duplicate detector for >50K-row corpora.
# Default audit path stays simhash + LSH banding; this extra unlocks
# `forgelm audit --dedup-method minhash --jaccard-threshold 0.85` for
# operators auditing larger datasets where MinHash's recall/precision
# trade-off is the industry standard (NeMo Curator, Dolma, RedPajama).
ingestion-scale = [
"datasketch>=1.6.0,<3.0.0",
]
# Optional Presidio ML-NER PII detector. Adds the unstructured-identifier
# categories (person / organization / location) that the regex detector
# inherently misses. Heavyweight install (≈ 50 MB spaCy model on first
# analyzer build), so opt-in only — the regex set already covers the
# GDPR-mandated structured identifiers (email, phone, IBAN, credit card,
# national IDs).
ingestion-pii-ml = [
"presidio-analyzer>=2.2.0,<3.0.0",
]
# Rich terminal rendering for the `forgelm chat` REPL.
# Falls back to plain text output when not installed.
chat = [
"rich>=13.0.0",
]
# Developer tooling — test runner, coverage, linter/formatter.
dev = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
# pytest-randomly shuffles test order per session so order-dependent
# bugs (e.g. the v0.5.7 round-3 sys.modules.pop pollution cascade)
# surface in CI instead of waiting for a Python micro-bump or runner
# change to flip collection order. Run locally via
# ``pytest --randomly-seed=<n>`` to reproduce a failure.
"pytest-randomly>=3.15.0,<6.0.0",
# Bounded on purpose. ruff 0.16.0 began formatting Python blocks *inside
# Markdown*, taking the format check from 277 files to 562 — on an unchanged
# tree that is 32 newly "unformatted" documents and a red lint job nobody
# caused. ci.yml installed `ruff` unpinned, so the breakage would have
# arrived with whatever the runner resolved that morning. Adopting a
# formatter major is a reviewed decision with a diff attached, not an ambush;
# tests/test_packaging.py pins this constraint against ci.yml's copy.
"ruff>=0.4.0,<0.16.0",
# packaging supplies Version() for semantic (not lexical) version comparison
# in tools/check_deprecation_targets.py and tests/test_config.py — "v1.10.0"
# sorts below "v1.2.0" as a string, which would silently invert the
# "removal target is still ahead of the shipping version" gate. Declared here
# rather than in [project].dependencies because nothing under forgelm/
# imports it: it is dev/CI tooling only. It also arrives transitively via
# transformers/huggingface_hub, so this entry exists to make the import
# declared rather than accidental (no undeclared imports).
"packaging>=23.0",
# tomllib is stdlib only on 3.11+; the 3.10 CI runner needs the tomli
# backport so the dev-only guards that parse pyproject.toml
# (``tools/check_notebook_pins.py``, ``tools/check_deprecation_targets.py``)
# and their test modules can run. Never imported by the runtime path.
"tomli>=2.0.0; python_version < '3.11'",
# mypy backs the public-surface type gate that
# ``docs/design/library_api.md`` has documented since Phase 36 as "a new CI
# step": ``mypy --strict --follow-imports=silent forgelm/__init__.py
# forgelm/_version.py``. No such step existed, mypy was not installed
# anywhere, and the command failed when finally run. Scoped to the two
# public-surface modules on purpose — ``--strict`` over ``trainer.py`` would
# mean typing the whole transformers/trl surface, which is not this
# project's to fix (that argument is made in library_api.md Q3, and the
# broader package's internal typing debt is tracked as a budget in
# docs/roadmap/risks-and-decisions.md rather than blocking this gate).
"mypy>=1.8.0,<2.0.0",
# bandit is in the gauntlet CLAUDE.md/AGENTS.md/CONTRIBUTING.md publish, so
# it must be installable from the extra CONTRIBUTING tells a contributor to
# install. It was only in [security], so the documented block was
# unrunnable in the documented venv. The <2.0.0 cap matches the [security]
# copy and exists because a scanner major can redden a blocking gate with no
# code change on this side.
"bandit[toml]>=1.7.0,<2.0.0",
]
# ISO 27001 / SOC 2 alignment supply-chain security tooling.
# Operators install via `pip install forgelm[security]` to run the same
# pip-audit (CVE) + bandit (static-analysis) checks ForgeLM's nightly +
# CI workflows enforce. Both are dev/CI tooling, never imported by
# the runtime path.
security = [
"pip-audit>=2.7.0,<3.0.0",
"bandit[toml]>=1.7.0,<2.0.0",
]
[project.urls]
Homepage = "https://github.com/HodeTech/ForgeLM"
Documentation = "https://github.com/HodeTech/ForgeLM/tree/main/docs/guides"
Repository = "https://github.com/HodeTech/ForgeLM"
Issues = "https://github.com/HodeTech/ForgeLM/issues"
Changelog = "https://github.com/HodeTech/ForgeLM/blob/main/CHANGELOG.md"
[project.scripts]
forgelm = "forgelm.cli:main"
[tool.setuptools]
package-dir = {"" = "."}
[tool.setuptools.packages.find]
where = ["."]
include = ["forgelm*"]
# Bundle quickstart template assets (config.yaml, data.jsonl, README.md) into
# the wheel so `forgelm quickstart` works after a `pip install` rather than
# only from a source checkout.
[tool.setuptools.package-data]
"forgelm.templates" = [
"*.md",
"*/*.yaml",
"*/*.jsonl",
"*/*.md",
]
# Phase 19: PEP 561 type-hint distribution marker so downstream
# `mypy --strict` / pyright consumers see in-source type hints without
# a separate stubs package. Empty file at forgelm/py.typed.
"forgelm" = ["py.typed"]
# Phase 36: bundled default safety probes so `forgelm safety-eval
# --default-probes` works after a wheel install.
"forgelm.safety_prompts" = ["*.jsonl"]
# F1 (review-cycle 3): schema-derived wizard defaults shipped as
# package data so ``forgelm/wizard/_state.py`` reads them via
# ``importlib.resources`` from a wheel install (otherwise the file
# wouldn't survive ``pip install`` and the wizard would fall back
# to its hardcoded sentinels). Generated by
# ``tools/generate_wizard_defaults.py``; CI guard at
# ``tools/check_wizard_defaults_sync.py``.
"forgelm.wizard" = ["_defaults.json"]
[tool.pytest.ini_options]
# F-P8-C-20: --strict-markers turns any undefined/typo'd @pytest.mark into
# an error instead of a silent no-op. The marker roster is kept to what is
# actually applied (the four aspirational unit/integration/smoke/slow
# markers had zero usages and documented a selection that never existed).
addopts = "--cov=forgelm --cov-report=term-missing --cov-fail-under=40 --strict-markers"
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]
markers = [
"fixture_drift: tests sensitive to upstream pricing/dataset drift; excluded from the release matrix via `pytest -m 'not fixture_drift'` in publish.yml",
# A sibling of fixture_drift rather than a reuse of it: fixture_drift means
# "the expected value may have moved upstream", this means "the assertion is
# only true when `pip install -e .` is what put forgelm on sys.path".
# tools/check_import_origin.py FAILS by design against a non-editable
# install, so its CLI-level tests cannot pass in publish.yml's
# `pip install dist/*.whl` matrix. Deselected there via the same
# `-m 'not ...'` expression. Do NOT weaken the guard to make them pass —
# the guard is right and the assumption baked into the test is what is
# environment-bound. See tests/test_check_import_origin.py::TestCli.
"requires_editable_install: assertions that hold only under `pip install -e .`; excluded from the release matrix via `pytest -m 'not requires_editable_install'` in publish.yml",
]
[tool.ruff]
target-version = "py310"
line-length = 120
[tool.ruff.lint]
select = ["F", "E9", "W", "B", "I"]
ignore = ["B008", "B905"]
# Pylint config (consumed by Codacy's pylint plugin). Pylint's default
# max-line-length is 100, but the project formatter is ruff @ 120 (see
# [tool.ruff] above). Without this alignment, ruff format reflows long
# f-strings back to single lines that pylint then flags — a no-win.
# Pinning pylint to 120 keeps the two tools in agreement.
[tool.pylint.format]
max-line-length = 120
# Bandit static-analysis config (consumed by Codacy + standalone bandit).
# - tests/ is excluded from the broader scan; we still scan the package itself.
# - No `skips` entry: B101 (assert_used) is left enabled repo-wide so any
# stray `assert` in production source under `forgelm/` is flagged. Tests
# are already out of scope via `exclude_dirs`, so an explicit B101 skip
# would only mask real findings in product code. If a genuine in-product
# assert is ever needed (e.g. invariant guard the type checker depends on),
# tag the line with `# nosec B101 — <rationale>` rather than re-adding a
# global skip.
[tool.bandit]
exclude_dirs = ["tests", ".venv", "build", "dist"]
[tool.coverage.run]
source = ["forgelm"]
omit = ["forgelm/templates/__init__.py"]
[tool.coverage.report]
fail_under = 40
show_missing = true
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.",
"raise ImportError",
"except ImportError",
]