Repository navigation
feat: Phase 10.5 Quickstart Layer (v0.4.5) - #9
Conversation
Closes the "trained, now what?" gap with five new modules: - forgelm/inference.py: shared generation primitives (load_model, generate, generate_stream, logit_stats, adaptive_sample) used by chat, judge, synthetic - forgelm/chat.py: interactive REPL with streaming, /reset /save /temperature /system slash commands, rich optional rendering, optional Llama Guard routing - forgelm/fit_check.py: VRAM pre-flight advisor — architecture via AutoConfig, formula-based estimation, FITS/TIGHT/OOM/UNKNOWN verdict + recommendations - forgelm/export.py: GGUF export wrapping llama-cpp-python converter; adapter merge; 6 quant levels; SHA-256 appended to model_integrity.json - forgelm/deploy.py: deployment config generation for ollama/vllm/tgi/hf-endpoints CLI: forgelm chat / export / deploy subcommands + --fit-check flag; full backward compat with existing flat CLI. All subcommands work without --config. Tests: 89 new tests across 5 modules; full suite 427 passed, 34 skipped. Docs: CHANGELOG, README, roadmap, quickstart, usage reference updated. Version: 0.3.1rc1 → 0.4.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- forgelm/chat.py: remove unused `rich.text.Text` import - forgelm/inference.py: remove unused `math` import (auto-fixed) - tests/test_safety_advanced.py: remove unused `result` assignment (F841) - tests/test_cli_phase10.py: remove unused _run_deploy_cmd/_run_export_cmd imports - tests/test_deploy.py: remove unused pytest import, sort imports (I001) - tests/test_export.py: remove unused subprocess/pytest imports, sort imports - tests/test_fit_check.py: remove unused types import - tests/test_inference.py: sort import block (I001), remove unused importlib/math - tests/test_oom_recovery.py: remove unused generate_training_manifest import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_oom_recovery: rename capture_manifest arg cfg→config to match generate_training_manifest(config=...) keyword call signature - test_safety_advanced: patch transformers.pipeline instead of forgelm.safety.pipeline — pipeline is a local import inside run_safety_evaluation so no module-level attribute exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- roadmap.md: remove redundant "Phase 12 (reserved)" row; fix Phase 10.5 Mermaid color (was green/done, still planned → blue); update doc map labels to reflect completed vs planned status - completed-phases.md: add Phase 10 to summary table; replace 650-line Phase 10-13 duplicate content with a concise ✅ entry for Phase 10; remove Phase 11-13 content (each has its own dedicated file) - phase-12-quickstart.md: update heading from "Phase 12" to "Phase 10.5"; add file-name note explaining why filename stays as phase-12-* - phase-11-data-ingestion.md: fix delivery target from "v0.4.5 or v0.5.0" to v0.5.0 (consistent with roadmap.md and releases.md) - releases.md: remove duplicate --- separator; clarify v0.3.1rc1 status as folded into v0.4.0 rather than a standalone release candidate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat: emit warning instead of silent no-op in _safety_hint - cli: add --output-format/--quiet/--log-level to chat/export/deploy subparsers using argparse.SUPPRESS so they don't override main parser values; remove redundant inner KeyboardInterrupt handler in chat - deploy: parameterize cloud vendor for HF Endpoints config; fix vLLM comment generation to drop trailing colon for None values - export: produce f16 GGUF for K-quants with explicit warning (two-step llama-quantize required); add FORGELM_GGUF_CONVERTER env var override; add subprocess timeout (3600s); cleanup merged adapter dir in finally; remove duplicate llama_cpp import - fit_check: use free_bytes (not total) for available_gb; drop unused arch parameter from _build_recommendations - inference: propagate exceptions from background generate thread; raise ValueError when adapter passed to Unsloth backend; warn when Unsloth receives load_in_4bit=False; switch 8-bit loading to BitsAndBytesConfig - README: add 5 Phase 10 modules to project structure - completed-phases: update scope note to Phase 1-10 - tests: fix tautological assertion in test_export; update test_fit_check to expect free_bytes; mock signatures accept timeout kwarg - pyproject: clarify export extra comment re: K-quant two-step flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat: emit safety warning once per session in __init__ instead of every turn; remove unused _safety_hint method; route all streaming output and welcome banner through self._print/_print_inline so injected output_fn captures everything - inference: forward load_in_4bit and load_in_8bit to _load_unsloth (signature now accepts both as keyword args) and pass them to FastLanguageModel.from_pretrained; warn on conflicting flags - deploy: drop deprecated top-level "version" key from generated docker-compose.yaml - fit_check: discard unused total_bytes via underscore unpack; simplify tautological 4-bit recommendation check; compute adapter parameter count as an integer directly to avoid float round-trip via GB - README: update stale test count (145+/11 files → 427+/30 files) - tests: simplify chat KI test (drop redundant MagicMock indirection); assert chat --help output documents model_path; replace manual cwd management in deploy test with monkeypatch.chdir; split tautological assertion in fit_check hypothetical-mode test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical: - export: K-quant requests now write to a .f16.gguf intermediate path and ExportResult.quant reports "f16" (the actual file) instead of the requested K-quant. model_integrity.json SHA-256 stays honest — audit chain no longer claims a hash that doesn't match the file. Required llama-quantize follow-up step is logged with the exact path. - inference: TextIteratorStreamer now has timeout=60 and the streaming loop catches queue.Empty. If model.generate() raises before the queue is populated (OOM, dtype mismatch, invalid input ids), the captured worker exception is re-raised instead of blocking the chat REPL forever. High: - chat: remove unimplemented --safety flag (was a no-op after round 2; CLAUDE.md "stub tagged production" anti-pattern). Welcome banner no longer claims "Safety: on". - chat: token output now goes through _print_inline_raw which rich-escapes model tokens before rendering ([red]ALERT[/red] in training data was a low-grade prompt-injection vector). Rich console buffer is force-flushed so streaming UX doesn't appear frozen. - fit_check: size-hint matching uses word-bounded regex so "llama-13b" no longer falls back to the 3B architecture profile in --offline mode. - deploy: tgi/ollama targets now reject non-local model paths up front; HF Hub IDs no longer silently produce broken docker volumes / Modelfiles the user only discovers at deploy time. Medium: - export: adapter merge forces torch_dtype=float16 (CPU bf16 falls back to fp32 and stalls on large models); full converter stderr/stdout logged separately from the 500-char ExportResult.error so debug context is preserved. - inference: fallback prompt template (no chat_template) now appends "assistant: " to mimic add_generation_prompt=True; raw base models stop echoing "user: ..." continuations. - chat: /save persists the system prompt as the first JSONL message so saved transcripts are replayable. - fit_check: breakdown includes estimation_caveat noting ±30% typical error and unmodeled factors (FlashAttention, MoE, sliding window). Low: - cli: comment explaining the outer KeyboardInterrupt guard around the chat dispatch covers Ctrl-C during model load / banner render. - CHANGELOG: document the K-quant two-step requirement under v0.4.0. Tests: - test_kquant_produces_f16_intermediate: K-quant request → result.quant=f16 and .f16.gguf path on disk. - test_tgi_rejects_non_local_path / test_ollama_rejects_non_local_path: HF Hub IDs return success=False with an actionable error. - test_export mocks now read --outfile from the cmd args so the new K-quant rerouting is tested transparently. Existing ollama/tgi tests create real model directories under tmp_path. 430 passed, 34 skipped (3 new tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes ~94 of 104 SonarCloud findings. Behavior is unchanged; remaining critical findings are cognitive-complexity refactors that need their own focused PRs. Wave 1 — quick wins ------------------- Security: - nightly.yml / publish.yml: drop the workflow-level write permission and declare read-only at workflow scope; only the jobs that need write (notify-failure: issues; publish-testpypi/pypi: id-token) keep it. Unused parameters: - compliance.export_compliance_artifacts: drop the unused `config` arg. Manifest already has the config-derived data; updates 5 call sites (cli.py, trainer.py, test_compliance.py, test_integration_smoke.py). - model._apply_moe_expert_quantization: drop unused `num_experts`. - trainer.ForgeTrainer: drop unused `final_path` from _run_benchmark_if_configured / _run_safety_if_configured / _run_judge_if_configured / _export_compliance_if_needed; drop the unused `metrics` from _run_benchmark_if_configured. Updates the 4 call sites in train(). Constructor → literal: - model._apply_lora: lora_kwargs uses a dict literal. - trainer._get_common_training_kwargs / _build_trainer: kwargs and trainer_kwargs use dict literals. - inference.generate / generate_stream: gen_kwargs uses dict literals (separately flagged by SonarCloud). Duplicate literals → constants: - trainer: extract _EVT_REVERT_TRIGGERED for the audit event name used three times around the auto-revert paths. - wizard: extract _PREFERENCE_COLUMNS_HINT for the dpo/simpo/orpo dataset-format hint. PyTorch dim= explicit: - merging._slerp_merge: switch to torch.linalg.vector_norm so SLERP's scalar-magnitude semantics are explicit (previously torch.norm with no dim). - merging._ties_merge_tensor: pass dim=None to torch.quantile. Misc: - merging: rename unused `missing` to `_` in load_state_dict unpack. - wizard.run_wizard: return type now Optional[str] (it already returned None in the "defer" branch). - wizard: replace nested ternary for strategy_str with if/elif/else. - config.DistributedConfig: rephrase inline comment to avoid string-literals-that-look-like-code. - compliance._sanitize_md: type hint now Optional[str] (the body already handled None). Wave 2 — float == pytest.approx ------------------------------- Mechanical migration of ~60 assertions across 13 test files. These were exact comparisons against literal config defaults / dataclass fields (no FP arithmetic) so behavior is unchanged, but pytest.approx silences SonarCloud's float-equality rule and is the conventional pattern. Files: test_alignment, test_benchmark, test_compliance, test_config, test_cost_estimation, test_galore, test_integration_smoke, test_judge_functions, test_long_context, test_safety_advanced, test_synthetic, test_trainer, test_webhook. Three files needed `import pytest` added (test_compliance, test_long_context, test_webhook). False positives left as-is: - runtime_smoke.py:88 — already re-raises on non-zero exit; SystemExit(0) is the documented success path. - test_safety_advanced.py:235 — `result =` was already removed in round-2. - notebooks/kto_binary_feedback.ipynb — low-priority duplicate literal. Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the SonarCloud cognitive-complexity findings (≥15 → all <15) without changing behavior. Each function was decomposed into private helpers with single responsibilities; 430 tests still pass with no regressions. Small (CC 16–24): - webhook._send → _resolve_url + _post_payload + _mask - synthetic.generate → _generate_one + _write_output - synthetic._call_file_teacher → _load_file_responses - model._apply_moe_expert_quantization → _is_frozen_expert_weight + _recast_expert_weight - model._freeze_unselected_experts → _parse_selected_experts + _expert_index_in_name - judge.run_judge_evaluation → _load_eval_prompts + _load_local_judge + _generate_response + _clip_judge_score + _save_judge_results - trainer._collect_resource_usage → _collect_gpu_info + _train_runtime_seconds + _resolve_cost_per_hour Medium (CC 29–32): - benchmark.run_benchmark → _extract_task_score + _parse_results + _save_benchmark_json (plus _ACC_METRIC_KEYS constant) - model_card.generate_model_card → _build_metrics_table + _build_benchmark_section + _build_safety_section + _format_method + _build_extra_tags - model.get_model_and_tokenizer → _load_unsloth + _load_tokenizer + _device_map_for + _build_model_kwargs + _apply_moe_config + _build_lora_config Large (CC 39–66): - cli._run_dry_run → _galore_dry_run_fields + _evaluation_dry_run_fields + _compliance_dry_run_fields + _build_dry_run_result - config._validate_consistency → _warn_general_consistency + _warn_high_risk_compliance + _validate_trainer_type + _validate_galore + _validate_distributed - cli.main → _dispatch_subcommand + _maybe_run_wizard + _load_config_or_exit + _apply_offline_flag + _maybe_run_no_train_mode + _run_training_pipeline - trainer.train → _measure_baseline_loss + _apply_benchmark_result + _apply_resource_usage + _apply_safety_result + _apply_judge_result + _finalize_artifacts + _handle_human_approval_gate + _run_training_pipeline (the orchestrator stays a thin try/except) - wizard.run_wizard → _collect_rope_scaling + _collect_neftune_alpha + _collect_webhook_config + _collect_safety_config + _collect_evaluation_config + _collect_compliance_config + _save_config_to_file + _print_wizard_summary - data._make_batch_processor → _process_text_format + _process_messages_format + _format_user_assistant_row + _process_user_assistant_format - data.prepare_dataset → _apply_mix_ratio + _merge_extra_datasets + _ensure_validation_split + _validate_trainer_columns + _shuffle_and_passthrough + _passthrough_for_trainer + _passthrough_multimodal + _format_sft_dataset - safety.run_safety_evaluation → _load_safety_prompts + _generate_safety_responses + _release_model_from_gpu + _classify_one_response + _classify_responses + _evaluate_safety_gates + _save_safety_results Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verified findings: - CHANGELOG: "afterwards" → "afterward" (American English consistency) - CHANGELOG: v0.3.1rc1 release date corrected to 2026-03-28 (matches the actual `bump version to 0.3.1rc1` commit 7f827c0); v0.4.0 gains a Breaking section documenting that export_compliance_artifacts dropped its unused `config` parameter - README: stale test-count line updated to "430 passed (+34 skipped) across 30 test files" - fit_check: regex lookbehind now blocks `.` so size-hint matching doesn't classify "1.7b" as the 7B architecture (also caught Llama-3.2 fractional sizes) - fit_check._build_recommendations: drop unused `quant` param + matching call site (body uses `m.load_in_4bit` directly) - merging: restore missing/unexpected unpack from load_state_dict and log both directions of mismatch — useful when diagnosing a bad merge - test_benchmark: replace `abs(... - 0.60) < 0.01` with `pytest.approx(0.60, abs=0.01)` for consistency - test_cli_phase10: estimated_gb assertion uses pytest.approx instead of exact float equality Skipped: - test_safety_advanced patch target — reviewer suggestion is incorrect. `safety.py` does NOT have a top-level `from transformers import pipeline`; the import lives inside `run_safety_evaluation`'s body, so `forgelm.safety.pipeline` isn't a valid attribute. The current `patch("transformers.pipeline", ...)` is the right target. Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three actionable findings + one config-level suppression for the bulk B101 noise: - pyproject.toml: add a [tool.bandit] section with exclude_dirs=["tests", ...] and skips=["B101"]. Codacy's Bandit pass was reporting "use of assert" on every test assertion (~40 hits); pytest *requires* asserts, so the right fix is config, not per-call suppression. - chat.py: narrow the bare `except Exception` around `_console.file.flush()` to `(OSError, AttributeError)` — the actual failure modes (closed stdout, console without `.file`). Closes Bandit B110. - export.py: annotate the `subprocess.run(cmd, ...)` call with `# noqa: S603 # nosec B603` plus a comment explaining why it's safe (list-form, no shell=True, paths under user control). Closes B603. - test_deploy.py: replace the hardcoded "/tmp/Modelfile" string in test_deploy_result_dataclass with `tmp_path / "Modelfile"`. Closes B108 on that one flagged line; other /tmp references in tests/ are now covered by the Bandit exclude_dirs. Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI failed on tests that don't run in the local sandbox (require torch + transformers). Two regressions slipped through Wave 1.2: - tests/test_moe_functions.py: drop the `num_experts` argument from the two `_apply_moe_expert_quantization(model, 4)` calls; the function signature is now single-arg. - tests/test_oom_recovery.py: drop the `final_path` positional from the two `trainer._export_compliance_if_needed(...)` calls; the method signature is now `(metrics, result)`. No production-code change. ruff clean; the local tests that *do* run still pass (430), and the affected suites collect cleanly under skip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, gating) Real-bug fixes: - judge: replace the score=0 failure sentinel with score=None across _parse_judge_json / _call_api_judge / _call_local_judge. The old value was clipped up to 1.0 by _clip_judge_score and silently lowered the average. _clip_judge_score now passes None through; run_judge_evaluation averages only valid scores and reports the failure count separately. judge_results.json gains a per-row "judge_failed" boolean. - safety: mark classifier-crash rows as low_confidence=True (with a classifier_error message) so they show up in the manual-review surface instead of disappearing into the unsafe bucket. - safety: drop the misleading `del model` from _release_model_from_gpu (it only dropped the local arg binding, not the caller's reference). Document the contract in the docstring; the caller now sets its own `model = None` after calling the helper so VRAM can actually be reclaimed. - trainer: fuzzy GPU pricing match now sorts by descending name length so more-specific entries beat shorter substrings (e.g. "NVIDIA H100" wins over "NVIDIA H1"). - trainer: _apply_benchmark_result now respects evaluation.auto_revert (mirroring the safety/judge gates). Benchmark failure with auto_revert= False is recorded on TrainResult but no longer destroys saved artifacts. - webhook: _mask now redacts userinfo and query strings via urlparse, returning scheme://host plus a single safe path segment. Removed resp.text from the HTTP-error log so receiver-echoed payloads can no longer leak secrets — only status code is surfaced. Cleanups / nits also applied: - config: drop the redundant `self.lora and ...` truthiness check (lora is a required pydantic model, always truthy). - data: _shuffle_and_passthrough and _format_sft_dataset only shuffle the "train" split now — validation/test stay in source order so eval metrics line up sample-by-sample across runs. - data: _process_user_assistant_format distinguishes "wrong schema" (raise) from "empty batch" (return empty list) by checking key presence instead of column truthiness. - model_card: _build_metrics_table filters any namespaced key (anything with `/`) so safety/, resource/, judge/, benchmark/ each stay in their dedicated sections instead of cluttering the metrics table. - synthetic: log a warning on empty teacher response; skip the trailing api_delay after the last prompt; _load_file_responses now warns when seed_file is unset/missing and debug-logs malformed lines with line numbers so users have something to grep for. - wizard: _save_config_to_file no longer retries the same path on OSError (which would re-raise the same error when the user typed the literal fallback name). Falls back to a timestamped file in the user's home directory instead. - cli: extract _report_training_error to centralize the json-vs-human-log split for both ImportError and Exception paths. Extract _add_common_subparser_flags to deduplicate the --quiet / --log-level / --output-format triples across chat/export/deploy. Tests: - test_judge_functions: update three assertions for the new None sentinel (test_invalid_json_returns_none_sentinel, test_empty_string, test_api_timeout). Behavior change is intentional — score=0 was hiding failures in the average. Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral regression) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…izard CC) Workflow security: - nightly.yml / publish.yml: drop the workflow-level `contents: read` permission and declare it per-job. Each job now spells out exactly the scopes it needs (test jobs: read; notify-failure: read+issues; publish- testpypi/pypi: read+id-token). export.py: - Extract `_CONVERTER_SCRIPT` constant (was the literal "convert_hf_to_ gguf.py" repeated 3×). - Rename the `format=` keyword-only parameter to `format_=` so it no longer shadows the builtin. Updates `cli._run_export_cmd` and `tests/test_export.py::test_unsupported_format_returns_failure` to the new keyword. - Refactor `export_model` (CC 25 → well below 15) by extracting: * `_validate_export_request` — early rejection for bad format/quant * `_resolve_kquant_path` — K-quant → .f16.gguf rerouting * `_build_converter_command` — argv assembly * `_run_converter` — subprocess invocation + error reporting * `_cleanup_merged_dir` — temporary adapter-merge cleanup deploy.py: - `generate_deploy_config` had 18 parameters (limit 13). Group the HF-Endpoints knobs (task/instance_size/instance_type/region/framework/ vendor) into a new `HFEndpointsOptions` dataclass and the sampling defaults (temperature/top_k/top_p) into `SamplingOptions`. Net 18→11. - Update `cli._run_deploy_cmd` to pass `hf_endpoints=HFEndpointsOptions (vendor=...)` and the test `test_json_roundtrip_for_hf_endpoints` similarly. CHANGELOG gains a Breaking entry for the new signature. wizard.py: - `run_wizard` was still over the CC threshold after Wave 4. Extract: * `_select_model` — model picker (POPULAR_MODELS + custom) * `_derive_strategy_flags` — decode the 4-way strategy menu * `_parse_trainer_type` — return (trainer_type, dataset_format_hint) * `_collect_galore_config` — the GaLore-specific interactive branch tests/test_inference.py: - Hoist the inline `_Tensor`/`_Scalar` stub classes (and their helper ops `softmax`/`log`/`sum`) to module scope. `_make_torch_stub` now just builds the torch module (CC 36 → well under threshold). - Five test methods that did `torch_stub, _Tensor, _ = _make_torch_stub()` now do `torch_stub = _make_torch_stub()` — the local-variable PascalCase warnings disappear because `_Tensor` is no longer rebound in test scope. - Replace the unused `tok` in `test_adapter_is_merged` with `_`. tests/test_export.py: - `test_no_error_when_manifest_missing` no longer hardcodes `/tmp/model.gguf`; uses `tmp_path / "model.gguf"`. CHANGELOG: document the two breaking-API changes (export_model `format=` → `format_=`; generate_deploy_config grouped dataclasses). Verification: - ruff format && ruff check: clean - pytest tests/: 430 passed, 34 skipped (no behavioral regression) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… judge edges)
Real bugs:
- fit_check: when galore_enabled=True, the previous code added
adapter_gb on top of galore_gb and sized optimizer_state_gb against
the LoRA adapter — double-counting and wrong baseline. The two
strategies are mutually exclusive: under GaLore we now derive
projected_params = num_params * (galore_rank / hidden_size) and use
THAT as trainable_params for _optimizer_state_gb; adapter_gb stays 0.
Under LoRA we keep the original adapter sizing and skip galore_gb.
- fit_check: quant detection used to read bnb_4bit_compute_dtype even
when load_in_4bit was False, mis-tagging plain bf16 runs. Now picks
storage dtype: load_in_8bit → "8bit"; otherwise checks
m.torch_dtype / m.config.torch_dtype with a small fp16/bf16/fp32 map;
bnb_4bit_compute_dtype is consulted only when load_in_4bit=True.
- judge: an entire run with all-failures used to fall through to
"Average judge score (0.00) below minimum (X.XX)". Now we detect
``not valid_scores`` first and emit a distinct failure_reason
("No valid judge scores (all N/M parses/requests failed)").
Type / cleanup:
- judge: JudgeResult.scores typed as List[Optional[float]] with a
comment documenting the None sentinel — it was already storing None
but the annotation said List[float].
- merging: drop the redundant dim=None on torch.quantile; flat is
already 1-D after .flatten() so the default behavior is correct.
- export: rename the keyword-only parameter format_ → output_format
(matches the convention discussed in review); update cli call site
and test.
Verification:
- ruff format && ruff check: clean
- pytest tests/: 430 passed, 34 skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three security notices flagged by Codacy after the round-7 refactor shifted line numbers: - export.py:23 (B404, "Consider possible security implications associated with the subprocess module"): annotate the import line with `# noqa: S404 # nosec B404` and a short comment pointing reviewers to the safe-usage rationale on _run_converter. - export.py:271-272 (B603/S603, S607): the multi-line subprocess.run call was already noqa'd on its opening line, but Codacy was re-flagging the `cmd,` continuation line on its own. Add the suppression on both lines (S603/B603 and S607/B607 covered) and expand the inline comment to spell out the no-shell, absolute-path invariant. Skipped (Codacy "Possibly found usage of AI tooling: PyTorch" notices on export.py:131, fit_check.py:240, inference.py 10x, merging.py:175): this is Codacy's AI/ML library-usage *policy* signal, not a security or quality issue. ForgeLM is an LLM fine-tuning toolkit; using PyTorch is unavoidable. No code change. Verification: - ruff format && ruff check: clean - pytest tests/test_export.py: 18 passed (no behavioral change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notebooks: - New: notebooks/post_training_workflow.ipynb. End-to-end walkthrough of the four Phase 10 CLI commands (`forgelm --fit-check` / `chat` / `export` / `deploy`) plus the programmatic `forgelm.inference` API. Existing training notebooks deliberately left alone — their inline `model.generate(...)` is a fast in-memory sanity check; the new toolchain reloads from disk and is post-training scope. - quickstart_sft.ipynb: append a "Next Steps — Phase 10 Post-Training Toolchain" section pointing into the new notebook. Roadmap close-out: - roadmap.md: split "Latest release" (v0.4.0, shipped 2026-04-26) from "Current milestone" (v0.4.5 / Phase 10.5 Quickstart). Previously both pointed at v0.4.0, which was misleading once the release went live. - releases.md: correct release date 2026-04-25 → 2026-04-26 (matches the actual PyPI publish + GitHub Release timestamps); link to the GitHub release page; refresh the chat feature row to reflect the round-2 removal of the unimplemented `--safety` flag and the round-6 rich-markup escape; refresh export to call out the K-quant honest- intermediate flow; refresh fit-check to mention LoRA⊕GaLore mutual exclusion and word-bounded size-hint matching; refresh deploy to mention HF-Hub-ID rejection for tgi/ollama. - completed-phases.md: already accurate (Phase 10 row shows ✅ + v0.4.0). Tooling: - .gitignore: drop .claude/scheduled_tasks.lock and .claude/state/ — Claude Code runtime artifacts that shouldn't be tracked.
One-command bundled templates with opinionated defaults — primary community-growth driver. `forgelm quickstart customer-support` renders a config the existing trainer accepts unchanged, copies a small seed dataset, optionally runs training, and drops into chat with the result. Module: forgelm/quickstart.py — Template registry (frozen dataclass), auto_select_model() GPU-aware downsizing (≥10 GB → primary, otherwise fallback ≤2B; explicit no-gpu-detected reason without CUDA), and run_quickstart() orchestrator that substitutes model + dataset paths into the bundled YAML. CLI: forgelm quickstart <template> with --list (text + JSON), --model / --dataset overrides, --dry-run, --no-chat. Wizard now opens with "Start from a template?" — Yes routes to quickstart, No falls through to the existing 8-step flow. Same code paths, same YAML schema. 5 templates under forgelm/templates/ (all QLoRA 4-bit, rank=8, batch=1): - customer-support: Qwen2.5-7B ↔ SmolLM2-1.7B SFT, 60 examples - code-assistant: Qwen2.5-Coder-7B ↔ SmolLM2-1.7B SFT, 60 examples - domain-expert: BYOD (empty data + walkthrough README) - medical-qa-tr: Qwen2.5-7B Turkish medical Q&A with disclaimers - grpo-math: Qwen2.5-Math-7B ↔ -1.5B GRPO, 40 prompts LICENSES.md catalogs all bundled seed datasets (CC-BY-SA 4.0, author-original) and documents how to contribute new templates. pyproject.toml [tool.setuptools.package-data] bundles *.yaml / *.jsonl / *.md under forgelm.templates into the wheel. Tests: tests/test_quickstart.py — 40 tests, GPU-independent via _detect_available_vram_gb mock. Includes a regression test that loads every generated YAML through load_config() — strongest guard against template drift the moment a config schema changes. Nightly CI runs `quickstart --dry-run` + `--config <out> --dry-run` for every bundled template. Docs: roadmap.md/-tr.md + phase-12-quickstart.md + releases.md mark Phase 10.5 as Done; CHANGELOG and README updated; docs/guides/ quickstart.md gets an "Option 0: One-Command Quickstart Template" section. Version bumped to 0.4.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 24 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a Quickstart Layer (v0.4.5): bundled one-command templates shipped in the wheel, Changes
Sequence Diagram(s)sequenceDiagram
participant User as User / CLI
participant CLI as forgelm.cli
participant QS as forgelm.quickstart
participant Train as Trainer Process
participant Config as Config Loader
User->>CLI: forgelm quickstart <template> [--dry-run/overrides]
CLI->>QS: run_quickstart(template, overrides)
QS->>QS: detect GPU VRAM / auto_select_model
QS->>QS: load template config.yaml & patch model/data
QS->>QS: copy bundled data.jsonl (if present)
QS->>QS: write timestamped YAML + audit JSONL -> return QuickstartResult
alt dry-run
CLI-->>User: print summary / JSON
else normal
CLI->>Train: spawn subprocess: forgelm --config <generated.yaml>
Train->>Config: load generated YAML
Train-->>CLI: exit status
CLI->>Train: optionally spawn: forgelm chat <model_dir>
Train-->>User: interactive chat
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 minor |
| Security | 2 critical |
🟢 Metrics 361 complexity · 20 duplication
Metric Results Complexity 361 Duplication 20
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request introduces the Quickstart Layer and Post-Training toolchain, adding bundled templates, an interactive chat REPL, GGUF export, VRAM estimation, and deployment config generation. It also includes security hardening for webhooks and Markdown templates, along with refactoring for better modularity and OOM recovery. Feedback focuses on enhancing robustness, specifically by ensuring directory creation for saved chat history, handling interrupts in the quickstart CLI, guarding against missing configuration sections, enforcing strictness in dataset zipping, and ensuring inference mode is used in background generation threads.
I am having trouble creating individual review comments. Click here to see my feedback.
forgelm/chat.py (181)
The /save command will fail if the provided path includes non-existent parent directories. It's safer to ensure the directory structure exists before attempting to write the file.
import os
if os.path.dirname(path):
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:forgelm/cli.py (909-911)
The quickstart subcommand should handle KeyboardInterrupt gracefully, similar to the chat subcommand, to avoid showing a traceback when the user cancels the operation (especially during the long-running training phase).
elif command == "quickstart":
try:
_run_quickstart_cmd(args, getattr(args, "output_format", "text"))
except KeyboardInterrupt:
pass
sys.exit(EXIT_SUCCESS)
forgelm/config.py (472)
Accessing self.lora.r without checking if self.lora is None could lead to an AttributeError if the LoRA configuration section is omitted from the YAML. While other parts of the codebase might assume its presence, it's safer to guard this check.
if self.lora and self.lora.r > 0:
forgelm/data.py (121)
Using zip without strict=True can lead to silent data truncation if the System, User, and Assistant columns have mismatched lengths. Enabling strict mode ensures that such inconsistencies are caught early.
for s, u, a in zip(sys_texts, user_texts, asst_texts, strict=True)
forgelm/inference.py (288-290)
The background generation thread should run within torch.inference_mode() to disable gradient tracking and reduce memory overhead, consistent with the non-streaming generate implementation.
def _gen_thread() -> None:
import torch
try:
with torch.inference_mode():
model.generate(**gen_kwargs)
- chat.py /save: create parent directories before writing transcript so user-supplied paths with non-existent parents don't error mid-write. - cli.py quickstart subcommand: catch KeyboardInterrupt so Ctrl-C during the (long) training subprocess exits cleanly without a traceback, matching the chat subcommand. - config.py GaLore-with-LoRA log: guard `self.lora.r` access behind a None check so a YAML omitting the lora section doesn't AttributeError. - data.py User/Assistant zip: use strict=True so mismatched column lengths fail loudly instead of silently truncating training data. - inference.py generate_stream: wrap the background generation thread in torch.inference_mode() to disable gradient tracking, matching the non-streaming generate() implementation. - cli.py quickstart subprocess: explicit security-rationale comment on subprocess import — argv-list form (not shell=True) means no shell is invoked, defusing recurring Codacy B404/B603 advisories. All findings verified against current code before fixing. No behavior changes for the happy path; failure modes get clearer or safer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
The 3 tests in TestGrpoRewardCallable patch trl.GRPOTrainer, which mock.patch resolves via getattr before swapping. Newer trl releases lazy-load the GRPO module, and that lazy import fails on torch versions that don't expose torch.distributed.fsdp.FSDPModule (a moving target in the torch ↔ trl pairing). The patch itself raises AttributeError before our test code runs, surfacing as a false fail in nightly compatibility CI. Probe trl.GRPOTrainer at module load and skip the whole class when it can't be reached. Tests still run on environments with a compatible torch+trl pairing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/roadmap-tr.md (1)
22-38:⚠️ Potential issue | 🟡 MinorMermaid diagram is stale relative to the updated status table.
The table now references "Faz 10.5" (with target
v0.4.5) and adds a new "Faz 14" (targetv0.5.1), but the diagram still labels the quickstart node asP12 Quickstart Katmanıand omits Faz 14 entirely. Also, the AI summary mentions a styling change forP105that is not present in the diagram code below.Consider relabeling
P12→P105(Faz 10.5) and adding a Faz 14 node so the visual matches the new "Bir bakışta durum" table.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/roadmap-tr.md` around lines 22 - 38, Update the Mermaid diagram so it matches the status table: rename node P12 (label "Quickstart Katmanı") to P105 (Faz 10.5) and change its target tag to v0.4.5, add a new node for Faz 14 (e.g., P14 with label "Faz 14" and target v0.5.1) and wire the edges the same way P12 was connected (P10 → P105, P11 → P105, P105 → P13), and apply the intended style change mentioned in the summary for P105 (the same fill/stroke scheme used for other Faz 10.x nodes).
🧹 Nitpick comments (6)
forgelm/chat.py (1)
182-184: Directory pre-creation is fine; theif parent:guard is effectively dead.Because
os.path.abspath(path)always resolves to an absolute path,os.path.dirname(...)is never an empty string, so the guard never short-circuits. Not a bug — keep it for clarity if you prefer, or drop it. The surroundingOSErrorhandler already coversmakedirsfailures, so error paths are correct.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/chat.py` around lines 182 - 184, The guard "if parent:" before creating directories is dead because os.path.dirname(os.path.abspath(path)) never yields empty string; remove the conditional and call os.makedirs(parent, exist_ok=True) unconditionally (or if you prefer keep it, replace it with an explanatory comment) — update the block around where parent is computed (using os.path.dirname and os.path.abspath) in forgelm/chat.py so only the necessary os.makedirs(parent, exist_ok=True) remains and rely on the existing OSError handling for failures.forgelm/config.py (1)
472-479: Theself.lora andguard is unreachable:lorais a required field onForgeConfig.
ForgeConfig.lora: LoraConfigModel(line 373) is required, soself.lorais always truthy after validation — the new existence check never short-circuits. This is harmless, but if the goal was to handle a missinglorasection thenlorashould be madeOptional[LoraConfigModel] = NoneonForgeConfigfor the guard to actually do anything. Otherwise drop the redundant check.♻️ Option A — drop the dead guard
- if self.lora and self.lora.r > 0: + if self.lora.r > 0: logger.info( "GaLore (gradient rank=%d) enabled alongside LoRA (adapter rank=%d). "🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/config.py` around lines 472 - 479, The conditional "if self.lora and self.lora.r > 0:" contains a redundant existence check because ForgeConfig.lora is required (type LoraConfigModel), so remove the dead "self.lora and" guard and change the condition to "if self.lora.r > 0:" (keeping the logger message that references self.training.galore_rank and self.lora.r); alternatively, if you intentionally want lora to be optional, change the ForgeConfig.lora type to Optional[LoraConfigModel] = None and keep the original guard—pick one approach and apply it consistently.forgelm/wizard.py (1)
318-378: Consider extracting the template-pick step.
_maybe_run_quickstart_template()interleaves the prompt, the digit/name parsing, the BYOD handling, and the orchestrator call in one function — and combined with the new branching inrun_wizard()it pushes cognitive complexity over the SonarCloud threshold (flagged on line 381). Extracting the template-resolution block (lines 337‑354) into a small helper such as_pick_template_name(names) -> Optional[str]keeps each piece focused and quiets the static analysis warning without changing behavior.Also, a small UX nit (no fix required): when the user types a digit that is out of range (e.g., "99"), the function silently falls through to the full 8‑step wizard rather than re-prompting. If that wasn't intentional, a single re-prompt would be friendlier than dumping into the long flow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/wizard.py` around lines 318 - 378, Extract the template selection/parsing logic inside _maybe_run_quickstart_template into a small helper function named _pick_template_name(names) -> Optional[str] that accepts the list of template names and returns the chosen name or None; move the block that reads raw = _prompt(...), parses digits/indices and checks membership in TEMPLATES into _pick_template_name and call it from _maybe_run_quickstart_template (keep the same return semantics so behavior is unchanged). Ensure the helper uses the same prompt text and default (names[0]) and preserves the current checks (raw.isdigit() -> index bounds -> names[idx-1], elif raw in TEMPLATES -> raw, else None) so SonarCloud cognitive complexity is reduced without changing UX; do not change BYOD handling or exception behavior.forgelm/quickstart.py (3)
199-205: Address Sonar nits in_detect_available_vram_gb.
freeis unused (Sonarpython:S1481). Rename to_to silence the warning and signal intent.♻️ Proposed fix
- free, total = torch.cuda.mem_get_info() + _, total = torch.cuda.mem_get_info()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/quickstart.py` around lines 199 - 205, In _detect_available_vram_gb, rename the unused variable free returned by torch.cuda.mem_get_info() to _, so the tuple unpacking becomes "_, total = torch.cuda.mem_get_info()" to silence the Sonar python:S1481 unused-variable warning and signal intent while preserving logic that uses total for the VRAM capacity calculation; keep the existing exception handling and logger.debug intact.
318-330: Unusedcolorparameter onformat_template_list.The
colorkeyword-only argument is accepted but never read — Sonarpython:S1172. Either drop it (no caller uses it today) or wire it up to ANSI rendering. Dropping is the lower-risk option since the public callers (forgelm/cli.py::_run_quickstart_cmd) invoke it without arguments.♻️ Proposed fix
-def format_template_list(*, color: bool = False) -> str: +def format_template_list() -> str: """Render the template registry as a human-readable list."""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/quickstart.py` around lines 318 - 330, Remove the unused keyword-only parameter `color` from the function signature of format_template_list and update any internal references if present; ensure callers like _run_quickstart_cmd in forgelm/cli.py continue to call format_template_list() without arguments (no changes needed there because they don't pass color), and run tests/lint to confirm the Sonar S1172 warning is resolved.
282-285: Deferconfig_target.parent.mkdiruntil after dataset resolution succeeds.Today, calling
run_quickstart("domain-expert")withoutdataset_overridecreates./configs/(or the user's--outputparent directory) and only then raisesValueError("does not bundle a dataset"). The user is left with an empty directory they didn't ask for. Move the mkdir to just before the YAML write, or perform dataset resolution first.♻️ Proposed reorder
config_target = Path(output_path) if output_path else _default_output_path(template_name) - config_target.parent.mkdir(parents=True, exist_ok=True) - dataset_path, dataset_notes = _resolve_dataset(template, dataset_override, config_target.parent) cfg = _materialize_config(template, chosen_model, dataset_path) + config_target.parent.mkdir(parents=True, exist_ok=True) with open(config_target, "w", encoding="utf-8") as f: yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False)Note:
_resolve_datasetalready callsscratch_dir.mkdir(...)itself before copying the bundled JSONL, so the bundled-data path keeps working.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/quickstart.py` around lines 282 - 285, The current code creates the output parent directory via config_target.parent.mkdir(...) before calling _resolve_dataset(...), which leaves an empty directory on failure; move the call to config_target.parent.mkdir(parents=True, exist_ok=True) so it runs only after dataset resolution succeeds (i.e., after dataset_path, dataset_notes = _resolve_dataset(template, dataset_override, config_target.parent)) and immediately before the YAML/config write step, or remove the early mkdir entirely since _resolve_dataset() creates its own scratch dir when using bundled data; update references to config_target, output_path, _default_output_path, and dataset_override accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/nightly.yml:
- Around line 63-67: The loop in the nightly workflow intentionally omits the
BYOD template "domain-expert" because template_assets() (in
forgelm/quickstart.py) raises a ValueError for templates without bundled
datasets; add an inline comment inside the for tpl loop explaining that
domain-expert is BYOD and therefore excluded to prevent confusion, and also
update the PR summary text to accurately say "smoke tests 4 of 5 templates"
instead of "all 5 templates".
In `@CHANGELOG.md`:
- Line 7: The changelog has duplicate dates for release headings "## [0.4.5] —
2026-04-25" and "## [0.4.0] — 2026-04-25"; verify the correct release date for
the 0.4.5 entry and update the "## [0.4.5] — 2026-04-25" header to the correct
date (likely "2026-04-26") so it matches the PR/tag date, or correct the 0.4.0
date if that one is wrong, ensuring the header text for the affected version(s)
exactly matches the intended release date.
In `@docs/roadmap.md`:
- Around line 16-18: The roadmap has inconsistent release dates between the two
entries: the "v0.4.5" line shows 2026-04-25 while the "v0.4.0" line shows
2026-04-26, which is impossible; update the date for the "v0.4.5" entry (the
`v0.4.5` string in docs/roadmap.md) to the correct shipping date (likely
2026-04-26) so the timeline reads chronologically, or if v0.4.0 is wrong update
the `v0.4.0` date instead—edit the text around the `v0.4.5` / `v0.4.0` entries
to make the dates consistent.
In `@docs/roadmap/releases.md`:
- Line 61: The release entry header "v0.4.5 — \"Quickstart Layer\" (2026-04-25)"
has a date earlier than the v0.4.0 entry and appears to be a typo; update the
header in releases.md so the date is consistent (e.g., change "(2026-04-25)" to
"(2026-04-26)" or set the correct published date), or if the version is wrong
adjust the version string to the intended one, ensuring the header "v0.4.5 —
\"Quickstart Layer\"" and its date match the PR/release timeline.
In `@forgelm/cli.py`:
- Around line 854-878: The spawned subprocesses (train_cmd and chat_cmd) don’t
inherit top-level CLI flags from args, so propagate relevant flags (e.g.
--output-format, --quiet, --log-level, --offline) into both train_cmd and
chat_cmd before calling subprocess.run; for each flag on args, append the
appropriate argv form (use flag-only for booleans like --quiet/--offline when
True, and flag + value for options like --output-format and --log-level) to the
lists so the child processes run with the same settings as the parent (locate
where train_cmd and chat_cmd are built near the call to
_load_config_for_quickstart_chat and update them accordingly).
- Around line 866-874: The code currently hardcodes "final_model" when building
final_model_dir; instead load the generated YAML via
_load_config_for_quickstart_chat(result.config_path) into a config dict and
derive the path using the config's output_dir and training.final_model_dir
(falling back to "final_model" if the field is missing). Replace the current
final_model_dir = Path(_load_config_for_quickstart_chat(result.config_path)) /
"final_model" with logic that reads config["output_dir"] and
config.get("training", {}).get("final_model_dir", "final_model") (mirroring how
_build_dry_run_result constructs the trainer output path) so auto-chat will find
the model when templates override final_model_dir.
In `@forgelm/templates/grpo-math/data.jsonl`:
- Line 26: Q26's prompt "Three numbers add to 60. The first is twice the second,
and the third is 5 more than the second..." yields a fractional second number
(13.75); change the constants so the answer is an integer — for example replace
"60" with "65" in the prompt so the equation 4x+5=65 gives x=15 and the final
line remains "Answer: <number>" for exact-match extraction.
In `@forgelm/templates/LICENSES.md`:
- Around line 30-31: The contributing checklist requires bundled template
configs to set gradient_checkpointing: true, but the added templates
(medical-qa-tr/config.yaml, code-assistant/config.yaml,
domain-expert/config.yaml, etc.) do not set training.gradient_checkpointing;
either add gradient_checkpointing: true under the training: section of each
bundled template config mentioned so the 12 GB-fit claim is accurate, or update
the checklist wording in LICENSES.md to remove the strict requirement (e.g., say
"rely on trainer default unless you need to override"); pick one approach and
apply it consistently across the listed templates and the checklist.
In `@forgelm/templates/medical-qa-tr/data.jsonl`:
- Line 12: Update the assistant message string in the JSON object that answers
"Yüksek ateş nasıl düşürülür?" to correct the typo "sertboyun" to two words
"sert boyun" so the model trains on the proper Turkish phrase; locate the
assistant content field inside the JSON entry (the object where
"role":"assistant" contains the response text) and replace the single-word
"sertboyun" with "sert boyun" preserving the rest of the sentence and
punctuation.
- Line 17: The assistant's phrase "üzerini temiz bir bez veya ileri tabakanın
yapışmadığı pansumanla örtün" is awkward and should be replaced with a concise
Turkish phrasing meaning "non‑stick dressing"; update the assistant message
content (the string starting with "Hafif (1. derece) yanıkta:") to read
something like "... üzerini temiz bir bez veya yara yüzeyine yapışmayan
pansumanla örtün." ensuring the rest of the sentence and the final safety
reminder ("Tıbbi acil durumlarda 112'yi arayın veya bir sağlık profesyoneline
danışın.") remain unchanged.
In `@forgelm/wizard.py`:
- Around line 357-361: The BYOD branch in _maybe_run_quickstart_template
currently treats an empty dataset_path as a silent fallback which causes
run_wizard to start the generic 8-step flow; change this to re-prompt the user
(or abort with a clear message) when template.bundled_dataset is false and the
user submits an empty path: replace the single _prompt call for dataset_path
with a loop that calls _prompt repeatedly (showing the same prompt and an
explicit "cancel" or empty-to-abort option) until a non-empty path is provided
or the user explicitly cancels, and if cancelled raise/return a clear sentinel
(or error) rather than returning None so run_wizard can handle it appropriately;
reference: template.bundled_dataset, chosen, _prompt,
_maybe_run_quickstart_template, run_wizard, and the BYOD template like
"domain-expert".
---
Outside diff comments:
In `@docs/roadmap-tr.md`:
- Around line 22-38: Update the Mermaid diagram so it matches the status table:
rename node P12 (label "Quickstart Katmanı") to P105 (Faz 10.5) and change its
target tag to v0.4.5, add a new node for Faz 14 (e.g., P14 with label "Faz 14"
and target v0.5.1) and wire the edges the same way P12 was connected (P10 →
P105, P11 → P105, P105 → P13), and apply the intended style change mentioned in
the summary for P105 (the same fill/stroke scheme used for other Faz 10.x
nodes).
---
Nitpick comments:
In `@forgelm/chat.py`:
- Around line 182-184: The guard "if parent:" before creating directories is
dead because os.path.dirname(os.path.abspath(path)) never yields empty string;
remove the conditional and call os.makedirs(parent, exist_ok=True)
unconditionally (or if you prefer keep it, replace it with an explanatory
comment) — update the block around where parent is computed (using
os.path.dirname and os.path.abspath) in forgelm/chat.py so only the necessary
os.makedirs(parent, exist_ok=True) remains and rely on the existing OSError
handling for failures.
In `@forgelm/config.py`:
- Around line 472-479: The conditional "if self.lora and self.lora.r > 0:"
contains a redundant existence check because ForgeConfig.lora is required (type
LoraConfigModel), so remove the dead "self.lora and" guard and change the
condition to "if self.lora.r > 0:" (keeping the logger message that references
self.training.galore_rank and self.lora.r); alternatively, if you intentionally
want lora to be optional, change the ForgeConfig.lora type to
Optional[LoraConfigModel] = None and keep the original guard—pick one approach
and apply it consistently.
In `@forgelm/quickstart.py`:
- Around line 199-205: In _detect_available_vram_gb, rename the unused variable
free returned by torch.cuda.mem_get_info() to _, so the tuple unpacking becomes
"_, total = torch.cuda.mem_get_info()" to silence the Sonar python:S1481
unused-variable warning and signal intent while preserving logic that uses total
for the VRAM capacity calculation; keep the existing exception handling and
logger.debug intact.
- Around line 318-330: Remove the unused keyword-only parameter `color` from the
function signature of format_template_list and update any internal references if
present; ensure callers like _run_quickstart_cmd in forgelm/cli.py continue to
call format_template_list() without arguments (no changes needed there because
they don't pass color), and run tests/lint to confirm the Sonar S1172 warning is
resolved.
- Around line 282-285: The current code creates the output parent directory via
config_target.parent.mkdir(...) before calling _resolve_dataset(...), which
leaves an empty directory on failure; move the call to
config_target.parent.mkdir(parents=True, exist_ok=True) so it runs only after
dataset resolution succeeds (i.e., after dataset_path, dataset_notes =
_resolve_dataset(template, dataset_override, config_target.parent)) and
immediately before the YAML/config write step, or remove the early mkdir
entirely since _resolve_dataset() creates its own scratch dir when using bundled
data; update references to config_target, output_path, _default_output_path, and
dataset_override accordingly.
In `@forgelm/wizard.py`:
- Around line 318-378: Extract the template selection/parsing logic inside
_maybe_run_quickstart_template into a small helper function named
_pick_template_name(names) -> Optional[str] that accepts the list of template
names and returns the chosen name or None; move the block that reads raw =
_prompt(...), parses digits/indices and checks membership in TEMPLATES into
_pick_template_name and call it from _maybe_run_quickstart_template (keep the
same return semantics so behavior is unchanged). Ensure the helper uses the same
prompt text and default (names[0]) and preserves the current checks
(raw.isdigit() -> index bounds -> names[idx-1], elif raw in TEMPLATES -> raw,
else None) so SonarCloud cognitive complexity is reduced without changing UX; do
not change BYOD handling or exception behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 83988864-811c-412d-b3ec-f7392d3a225d
📒 Files selected for processing (31)
.github/workflows/nightly.ymlCHANGELOG.mdREADME.mddocs/guides/quickstart.mddocs/roadmap-tr.mddocs/roadmap.mddocs/roadmap/phase-12-quickstart.mddocs/roadmap/releases.mdforgelm/__init__.pyforgelm/chat.pyforgelm/cli.pyforgelm/config.pyforgelm/data.pyforgelm/inference.pyforgelm/quickstart.pyforgelm/templates/LICENSES.mdforgelm/templates/__init__.pyforgelm/templates/code-assistant/config.yamlforgelm/templates/code-assistant/data.jsonlforgelm/templates/customer-support/config.yamlforgelm/templates/customer-support/data.jsonlforgelm/templates/domain-expert/README.mdforgelm/templates/domain-expert/config.yamlforgelm/templates/grpo-math/config.yamlforgelm/templates/grpo-math/data.jsonlforgelm/templates/medical-qa-tr/config.yamlforgelm/templates/medical-qa-tr/data.jsonlforgelm/wizard.pypyproject.tomltests/test_grpo_reward.pytests/test_quickstart.py
…nitpicks) Verified each finding against current code; applied real fixes only. Dates / consistency - CHANGELOG.md, docs/roadmap.md, roadmap-tr.md, roadmap/releases.md, roadmap/phase-12-quickstart.md: v0.4.5 date corrected from 2026-04-25 to 2026-04-26 (chronologically after v0.4.0 PyPI publish). - docs/roadmap-tr.md: Mermaid diagram updated to match the EN version — adds Faz 10.5 (P105) + Faz 14 (P14) nodes with the same fill/ stroke scheme used for completed phases. CI / wiring - .github/workflows/nightly.yml: inline comment explaining that the smoke loop covers 4 of 5 templates (domain-expert is BYOD and is covered separately by the pytest unit test). - forgelm/cli.py: quickstart subcommand now propagates top-level flags (--output-format, --quiet, --log-level) into both the train and chat subprocesses so the child inherits the operator's logging preferences. - forgelm/cli.py: the auto-launched chat REPL now reads training.final_model_dir from the generated YAML instead of hardcoding "final_model" — so templates that override it are still found. Renames _load_config_for_quickstart_chat -> _load_quickstart_train_paths and returns (output_dir, final_subdir). UX - forgelm/wizard.py: BYOD branch in _maybe_run_quickstart_template used to silently fall through to the full 8-step flow when the user hit enter at the dataset prompt. Now re-prompts in a loop until a real path is provided or the user types "cancel". - forgelm/templates/LICENSES.md: contributing checklist no longer hard-requires `gradient_checkpointing: true` in template configs; trainer.py auto-enables it when CUDA is available, so explicit setting is only for overrides. Data correctness - forgelm/templates/grpo-math/data.jsonl Q26: changed "add to 60" to "add to 65" so the equation 4x+5=65 yields x=15 (integer) instead of 13.75 (fractional, breaks `Answer: <number>` exact-match). - forgelm/templates/medical-qa-tr/data.jsonl line 12: fixed Turkish typo "sertboyun" -> "sert boyun" (two words). - forgelm/templates/medical-qa-tr/data.jsonl line 17: replaced awkward "ileri tabakanın yapışmadığı pansumanla" with the natural Turkish phrasing "yara yüzeyine yapışmayan pansumanla". Nitpicks - forgelm/chat.py: removed dead `if parent:` guard around makedirs. os.path.dirname(os.path.abspath(path)) is always non-empty; the conditional was unreachable. - forgelm/config.py: reverted previous defensive `self.lora and self.lora.r > 0` to `self.lora.r > 0`. ForgeConfig.lora is a required field (LoraConfigModel, no Optional, no default), so the None check was dead code. - forgelm/quickstart.py: renamed `free, total =` to `_, total =` in _detect_available_vram_gb to silence Sonar S1481 unused-variable. - forgelm/quickstart.py: removed unused `color` keyword-only param from format_template_list — no caller passes it. Skipped (low-value) - wizard.py helper extraction for SonarCloud cognitive-complexity nitpick — the function is already linear and well-commented; the proposed split adds an indirection without removing complexity. - quickstart.py mkdir reordering — current placement is correct for the dataset_override path, where _resolve_dataset() returns early without creating the directory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al gold-answer correctness trl.GRPOTrainer requires reward_funcs to be set. The bundled grpo-math quickstart used to ship without grpo_reward_model, which crashed at GRPOTrainer init with TypeError. Two reward callables are now wired additively (TRL sums them): - combined_format_length_reward (forgelm/grpo_rewards.py): always-on baseline. 0.8 * format-match (Answer: <value> regex) + 0.2 * length shaping. Gradient-rich from step 0; teaches output structure even when no ground truth exists. - _math_reward_fn (forgelm/trainer.py): correctness signal, appended only when the dataset carries a `gold_answer` field. Strips common units, compares with exact-string match then numeric tolerance. The bundled grpo-math seed now has a `gold_answer` per row, so the template trains end-to-end with both signals out of the box. When grpo_reward_model is set, the classifier callable replaces both built-ins (operator opted into a learned reward). config.py docstring, docs/guides/alignment.md "Reward selection", template config.yaml header all updated to describe the additive behavior. CLAUDE.md registers the new grpo_rewards module. Tests: - test_grpo_format_reward.py — pure-Python tests for the format/length callables + trainer integration (skipped on torch/trl version mismatch via the existing grpo_patchable probe). - test_grpo_math_reward.py — pure-Python tests for normalize/match/ reward_fn helpers. - test_grpo_reward.py — updated existing GRPO trainer wiring tests: no-reward-model + no-gold-answers now expects [combined] (was raise); gold_answer present now expects [combined, _math_reward_fn] (was [_math_reward_fn] only).
✅ Actions performedReview triggered.
|
1 similar comment
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
forgelm/quickstart.py (1)
367-447:⚠️ Potential issue | 🟡 Minor
dry_runstill produces on-disk side effects (per-run dir, audit event, dataset copy, YAML).Lines 396, 398-413, 415, 425-427 all execute regardless of
dry_run. The flag is purely informational here and only gates the trainer subprocess inforgelm.cli. Either tighten the docstring to make that explicit (e.g. "Hint forwarded to the CLI; this function always materializes config + dataset"), or short-circuit_resolve_dataset/_materialize_config/the YAML write whendry_run=Trueso a true preview leaves no artifacts. Same concern as the prior review on this hunk — flagging again because the behavior is unchanged in this revision.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/quickstart.py` around lines 367 - 447, The quickstart function currently performs filesystem side effects even when dry_run=True; change it to short-circuit before any on-disk work: after deciding chosen_model/selection_reason, if dry_run is True skip calling _append_audit_event, skip creating config_target.parent, skip _resolve_dataset and _materialize_config and the YAML write (and do not perform the dataset copy), and return a QuickstartResult that reflects the chosen_model/selection_reason with config_path and dataset_path set to None (or a clearly non-written marker) and started_training=False; update any logfile message to reflect dry-run state. Reference symbols: quickstart, _append_audit_event, config_target, _resolve_dataset, _materialize_config, QuickstartResult, and the YAML write block.
🧹 Nitpick comments (4)
forgelm/wizard.py (1)
326-378: Unusedtemplate_nameparameter — drop it or surface it in prompts.
_resolve_byod_dataset_pathacceptstemplate_namebut never uses it (SonarCloud confirmed). Either remove it from the signature and the caller, or thread it into the prompt/error messages so users see which template they're providing data for (e.g.,"Path to your dataset JSONL for '{template_name}'…").♻️ Suggested fix (drop the parameter)
-def _resolve_byod_dataset_path(template_name: str) -> Optional[str]: +def _resolve_byod_dataset_path() -> Optional[str]: """Prompt the user for a BYOD dataset path and validate it.- dataset_path = _resolve_byod_dataset_path(chosen) + dataset_path = _resolve_byod_dataset_path()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/wizard.py` around lines 326 - 378, The function _resolve_byod_dataset_path currently takes an unused parameter template_name; remove this unused parameter from the function signature and its docstring and update every caller to stop passing template_name (or, alternatively, if you prefer to keep the context, thread template_name into the user-facing strings—e.g., include it in the prompt and error messages inside _resolve_byod_dataset_path—so the parameter is actually used). Ensure you update the function definition, adjust all invocations to match the new signature (or keep the same calls if you choose to use the parameter), and revise the docstring to reflect the final behavior.tests/test_cli_helpers.py (1)
35-44: Redundant filter assertions — the index check below is the real guard.Lines 38-39 build a comprehension that filters by membership in
("--log-level", "DEBUG")and compares to the same list. This passes vacuously even if the two tokens were not adjacent (and would also pass if there were spurious copies). The contiguous-pair assertions at lines 41-44 already cover this correctly; consider dropping the comprehension lines to avoid the false sense of coverage.♻️ Suggested simplification
def test_inherited_flags_propagates_log_level_to_both() -> None: args = _ns(log_level="DEBUG") train_flags, chat_flags = _build_quickstart_inherited_flags(args) - assert ["--log-level", "DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")] - assert ["--log-level", "DEBUG"] == [flag for flag in chat_flags if flag in ("--log-level", "DEBUG")] - # Also assert the pair appears contiguously in both lists. + # Assert the --log-level/DEBUG pair appears contiguously in both lists. assert "--log-level" in train_flags assert train_flags[train_flags.index("--log-level") + 1] == "DEBUG" assert "--log-level" in chat_flags assert chat_flags[chat_flags.index("--log-level") + 1] == "DEBUG"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_cli_helpers.py` around lines 35 - 44, Remove the redundant membership-comprehension assertions in test_inherited_flags_propagates_log_level_to_both that check ["--log-level", "DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")] and the equivalent for chat_flags; they are vacuous and duplicate coverage already provided by the contiguous-pair checks. Keep the calls to _ns and _build_quickstart_inherited_flags and retain the explicit contiguous assertions that verify train_flags and chat_flags contain "--log-level" followed immediately by "DEBUG" (using train_flags.index("--log-level") + 1 and chat_flags.index("--log-level") + 1). Ensure no other assertions are removed so the test still validates adjacency and presence.forgelm/trainer.py (2)
99-120: Minor:zip(..., strict=False)will silently drop completions whengold_answerlength differs.If TRL's per-sample column passthrough ever yields a
gold_answerlist shorter thancompletions(custom data collator, prompt-only fallback, etc.), the trailing completions silently get no reward signal instead of surfacing the mismatch. Given the contract is supposed to be aligned,strict=Truewould catch wiring regressions earlier without changing happy-path behavior.♻️ Suggested change
- for completion, gold in zip(completions, golds, strict=False): + for completion, gold in zip(completions, golds, strict=True):Worth confirming TRL always passes per-sample columns aligned with
completions. If it doesn't (e.g., for prompts without agold_answercell),strict=Truewould raise — in which case the currentstrict=Falseis the right call and this comment can be ignored.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/trainer.py` around lines 99 - 120, In _math_reward_fn the zip(completions, golds, strict=False) call can silently drop completions when gold_answer is shorter; change it to zip(completions, golds, strict=True) inside _math_reward_fn so mismatched lengths raise immediately and surface wiring regressions (keep the rest of the logic using _ANSWER_PATTERN, _normalize_answer and _answers_match unchanged); if there is a valid case where golds may be missing for some completions, instead explicitly handle that case before zipping (e.g., fill missing golds or branch) rather than relying on strict=False.
45-81: Optional: bare"m"in_REWARD_STRIP_TOKENScan over-strip non-unit suffixes.The bare
"m"token combined withendswith/startswithwill strip a trailingmfrom any answer string that happens to end with one (e.g., a hypothetical"Answer: them"collapses to"the"). It's safe for the curated grpo-math seed dataset but brittle if a future template reuses this normalizer with less constrained outputs.Consider gating unit stripping behind a word-boundary or whitespace check (e.g., only strip when preceded by whitespace or a digit) so the helper can be reused safely outside the math template. Non-blocking for this PR.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/trainer.py` around lines 45 - 81, The token list _REWARD_STRIP_TOKENS and function _normalize_answer can over-strip the bare "m" from arbitrary strings; change the stripping logic to only remove "m" when it appears as a unit token at a word boundary (e.g., preceded/followed by whitespace or a digit) rather than any suffix/prefix match. Update _normalize_answer to check surrounding characters (or use a simple regex word-boundary check) before trimming a unit token like "m" so you still strip genuine units (e.g., "5 m" or "5m") but avoid trimming letters inside words like "them"; keep the existing two-pass loop and other tokens unchanged and only add the boundary check for single-letter unit tokens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@forgelm/cli.py`:
- Around line 831-848: The missing-TEMPLATE branch currently logs an error to
stderr and exits directly; update it to mirror the run_quickstart exception path
by checking the same output_format variable and, if output_format == "json",
print a JSON error envelope (e.g., {"success": False, "error": "<message>"} ) to
stdout instead of using logger.error, then call sys.exit(EXIT_CONFIG_ERROR);
modify the args.template check near the top of the quickstart invocation so it
uses the output_format-aware behavior, keeping the same error message text when
not in JSON mode.
In `@forgelm/templates/grpo-math/config.yaml`:
- Around line 46-47: Update the inline comment that currently says "Renamed from
grpo_max_new_tokens in v0.4.6" to reflect the shipped Quickstart Layer version
(v0.4.5) or make it version-agnostic; specifically edit the comment immediately
above the grpo_max_completion_length field (which replaced grpo_max_new_tokens)
to read either "Renamed from grpo_max_new_tokens in v0.4.5" or "Renamed from
grpo_max_new_tokens (v0.4.5+)" so the note matches the package/versioning in
pyproject.toml and docs/roadmap.md.
In `@tests/test_quickstart_subprocess.py`:
- Around line 105-159: The assertions that check for absolute paths use
POSIX-specific startswith("/") and will fail on Windows; update the two checks
(the cfg_value check in test_quickstart_subprocess.py and the model_path check
in test_chat_subprocess_uses_absolute_model_path) to use a platform-agnostic
absolute-path test such as os.path.isabs(cfg_value) or
pathlib.Path(cfg_value).is_absolute() (and similarly for model_path) so the
tests validate "absolute" on all OSes while still keeping the existing endswith
checks for the filename.
---
Duplicate comments:
In `@forgelm/quickstart.py`:
- Around line 367-447: The quickstart function currently performs filesystem
side effects even when dry_run=True; change it to short-circuit before any
on-disk work: after deciding chosen_model/selection_reason, if dry_run is True
skip calling _append_audit_event, skip creating config_target.parent, skip
_resolve_dataset and _materialize_config and the YAML write (and do not perform
the dataset copy), and return a QuickstartResult that reflects the
chosen_model/selection_reason with config_path and dataset_path set to None (or
a clearly non-written marker) and started_training=False; update any logfile
message to reflect dry-run state. Reference symbols: quickstart,
_append_audit_event, config_target, _resolve_dataset, _materialize_config,
QuickstartResult, and the YAML write block.
---
Nitpick comments:
In `@forgelm/trainer.py`:
- Around line 99-120: In _math_reward_fn the zip(completions, golds,
strict=False) call can silently drop completions when gold_answer is shorter;
change it to zip(completions, golds, strict=True) inside _math_reward_fn so
mismatched lengths raise immediately and surface wiring regressions (keep the
rest of the logic using _ANSWER_PATTERN, _normalize_answer and _answers_match
unchanged); if there is a valid case where golds may be missing for some
completions, instead explicitly handle that case before zipping (e.g., fill
missing golds or branch) rather than relying on strict=False.
- Around line 45-81: The token list _REWARD_STRIP_TOKENS and function
_normalize_answer can over-strip the bare "m" from arbitrary strings; change the
stripping logic to only remove "m" when it appears as a unit token at a word
boundary (e.g., preceded/followed by whitespace or a digit) rather than any
suffix/prefix match. Update _normalize_answer to check surrounding characters
(or use a simple regex word-boundary check) before trimming a unit token like
"m" so you still strip genuine units (e.g., "5 m" or "5m") but avoid trimming
letters inside words like "them"; keep the existing two-pass loop and other
tokens unchanged and only add the boundary check for single-letter unit tokens.
In `@forgelm/wizard.py`:
- Around line 326-378: The function _resolve_byod_dataset_path currently takes
an unused parameter template_name; remove this unused parameter from the
function signature and its docstring and update every caller to stop passing
template_name (or, alternatively, if you prefer to keep the context, thread
template_name into the user-facing strings—e.g., include it in the prompt and
error messages inside _resolve_byod_dataset_path—so the parameter is actually
used). Ensure you update the function definition, adjust all invocations to
match the new signature (or keep the same calls if you choose to use the
parameter), and revise the docstring to reflect the final behavior.
In `@tests/test_cli_helpers.py`:
- Around line 35-44: Remove the redundant membership-comprehension assertions in
test_inherited_flags_propagates_log_level_to_both that check ["--log-level",
"DEBUG"] == [flag for flag in train_flags if flag in ("--log-level", "DEBUG")]
and the equivalent for chat_flags; they are vacuous and duplicate coverage
already provided by the contiguous-pair checks. Keep the calls to _ns and
_build_quickstart_inherited_flags and retain the explicit contiguous assertions
that verify train_flags and chat_flags contain "--log-level" followed
immediately by "DEBUG" (using train_flags.index("--log-level") + 1 and
chat_flags.index("--log-level") + 1). Ensure no other assertions are removed so
the test still validates adjacency and presence.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 80595dc0-309c-476f-91cc-8faa22169936
📒 Files selected for processing (21)
.github/workflows/nightly.ymlconfig_template.yamldocs/guides/alignment.mddocs/reference/configuration.mddocs/roadmap-tr.mddocs/roadmap.mddocs/roadmap/phase-12-quickstart.mdforgelm/cli.pyforgelm/config.pyforgelm/grpo_rewards.pyforgelm/quickstart.pyforgelm/templates/grpo-math/config.yamlforgelm/trainer.pyforgelm/wizard.pytests/test_alignment.pytests/test_cli_helpers.pytests/test_grpo_math_reward.pytests/test_packaging.pytests/test_quickstart_compat.pytests/test_quickstart_subprocess.pytests/test_trainer_grpo_config.py
✅ Files skipped from review due to trivial changes (2)
- config_template.yaml
- docs/reference/configuration.md
🚧 Files skipped from review as they are similar to previous changes (1)
- .github/workflows/nightly.yml
…ict zip + 'm' boundary, version note, dead nits
Seven small findings, each verified against current code before fixing:
CLI / template error path consistency (forgelm/cli.py):
- The missing-TEMPLATE branch in _run_quickstart_cmd used logger.error
+ sys.exit. The run_quickstart exception handler immediately above it
emits a JSON envelope when output_format == "json". Aligned both
branches on the same shape so machine-readable callers always get a
parsable JSON error, not a stderr log line.
Cross-platform absolute-path test (tests/test_quickstart_subprocess.py):
- The two argv-absolute-path assertions used .startswith("/") which
fails on Windows where absolute paths look like "C:\\...". Switched
to os.path.isabs() so the test validates the property on every OS
while keeping the trailing-name endswith() checks.
Reward correctness (forgelm/trainer.py):
- _math_reward_fn used zip(..., strict=False), which silently truncates
to the shorter of completions / gold_answer. A wiring regression
(mismatched batch sizes) would surface as low reward instead of an
exception. Switched to strict=True with a defensive early-return
when gold_answer is missing entirely (the trainer wires this fn only
when the dataset has gold_answer; the early-return is belt-and-suspenders).
- _normalize_answer's _REWARD_STRIP_TOKENS list contains bare "m" for
meters, but with no boundary check it would shave the trailing "m"
off ordinary words like "them" or "method". Added a boundary check
for single-letter alpha tokens: strip only when the adjacent char is
a digit or whitespace. Multi-char and non-alpha tokens (kg, km/h, $)
are unchanged.
Template version note (forgelm/templates/grpo-math/config.yaml):
- The inline "Renamed from grpo_max_new_tokens in v0.4.6" comment was
off-by-one; the rename actually shipped in v0.4.5. Corrected.
Wizard helper signature (forgelm/wizard.py):
- _resolve_byod_dataset_path took an unused template_name parameter.
The function never references it; user-facing strings are generic
"this template" wording. Removed the parameter and updated the caller.
Test cleanup (tests/test_cli_helpers.py):
- test_inherited_flags_propagates_log_level_to_both had a vacuous
list-comprehension equality check that duplicated the contiguous-pair
check below it. Removed; the contiguous-pair assertion is the real
contract.
New regression tests (tests/test_grpo_math_reward.py):
- 7 parametrized cases for the "m" word-boundary rule (them / method /
metric ton must NOT strip; "5 m" / "5m" / "m 5" / "m5" MUST strip).
- test_missing_gold_answer_kwarg_returns_zeros (defensive early-return).
- test_mismatched_lengths_raises (strict=True surfaces the regression).
Skipped finding: "dry_run inert" (per-run dataset materialization).
This was rejected in a prior round and the rationale stands — the
existing test test_quickstart_dry_run_writes_yaml_and_exits_clean
asserts the config file IS written on dry_run, and the CLI message
"To start training: forgelm --config ..." promises the operator that
the file is on disk and ready. Changing dry_run to a pure-preview mode
would break both. If a true preview mode is needed it should be a
separate flag (e.g. --preview), not a dry_run semantic shift.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
forgelm/trainer.py (1)
162-180: Heads-up:bool(first["gold_answer"])treats falsy column values as "no gold".For string golds (the bundled templates), this is fine and
test_empty_gold_answer_treated_as_missingeven pins it. But if a future dataset stores numeric golds where the first row happens to be0(or0.0,False,None), the math reward gets silently disabled even though the column exists. Since the rest of the pipeline normalizes everything via_normalize_answer(gold)and string-equality, you might prefer probing for column presence only:♻️ Optional: presence-based probe
- first = train[0] - if isinstance(first, dict): - return "gold_answer" in first and bool(first["gold_answer"]) + first = train[0] + if isinstance(first, dict): + return "gold_answer" in firstYou'd need to update
test_empty_gold_answer_treated_as_missingaccordingly, or keep the current "empty string = missing" semantics if that's the deliberate UX.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/trainer.py` around lines 162 - 180, The detection in _dataset_has_gold_answers treats falsy gold values (e.g., 0, 0.0, False, None) as "no gold" by using bool(first["gold_answer"]); change the probe to check for key presence instead (e.g., if isinstance(first, dict): return "gold_answer" in first) so existence is detected regardless of value, and then update or adjust test_empty_gold_answer_treated_as_missing to reflect the new presence-based semantics (or explicitly special-case empty-string-as-missing if you want to keep the current UX).forgelm/cli.py (1)
798-921: Dispatcher logic is solid — the JSON-mode missing-template error, abs-path argv, inherited flags, and YAML-drivenfinal_model_dirlookup all line up with the prior review feedback.SonarCloud reports cognitive complexity 19 (>15) on this function. Optional split: pull the train-and-chat tail (lines 876–921) into a
_run_quickstart_train_then_chat(args, result, output_format)helper if you want to silence it without behavior change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/cli.py` around lines 798 - 921, Function _run_quickstart has high cognitive complexity; extract the training-and-chat tail into a new helper to simplify the dispatcher. Create a helper named _run_quickstart_train_then_chat(args, result, output_format) that contains everything after the dry_run check starting at the subprocess training invocation through the final sys.exit(EXIT_SUCCESS), move the subprocess logic (train_cmd, subprocess.run, train_rc handling), the args.no_chat check, the _load_quickstart_train_paths/final_model_dir check, and the chat subprocess logic into that helper, and call it from _run_quickstart with the same arguments so behavior, exit codes, logging, and inherited flags (from _build_quickstart_inherited_flags) remain identical; ensure you import subprocess in the helper or keep the existing import and preserve the same checks/comments and return/exit semantics.tests/test_quickstart_subprocess.py (1)
121-160: Patchingpathlib.Path.is_dirglobally is broad but acceptable here — the assertions on argv shape and absolute-path semantics remain precise.Worth noting if this test starts becoming flaky as the quickstart codepath grows: a more surgical
patch.object(Path, "is_dir", ...)scoped to a constructed instance, or a realfinal_modeldirectory undertmp_path, would reduce the blast radius. Not a blocker.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_quickstart_subprocess.py` around lines 121 - 160, The test test_chat_subprocess_uses_absolute_model_path currently patches pathlib.Path.is_dir globally via patch("pathlib.Path.is_dir", return_value=True); to reduce blast radius and make the test less brittle, change that to a more surgical approach by patching the Path class method via patch.object(Path, "is_dir", return_value=True) or (alternatively) create an actual final_model directory under tmp_path and point the patched _load_quickstart_train_paths to that tmp_path-based directory; update references in the test (the patch call and the mocked return_value for _load_quickstart_train_paths used by main) so the Path.is_dir override is scoped to Path and/or the created tmp_path/final_model instance rather than globally patching pathlib.Path.is_dir.forgelm/wizard.py (1)
326-378: Path resolution doesn't match the docstring —.expanduser()alone keeps relative paths relative.Lines 329–330 promise "local paths are returned as resolved absolute paths," but line 360 only does
Path(dataset_path).expanduser(). A user input of./mydata.jsonlis validated and then returned asmydata.jsonl— still relative. Today the train subprocess inherits the parent cwd so this works by accident, but anything that later changes cwd between argv build and exec (or any non-cli consumer of the generated YAML) will silently miss the dataset.Either tighten the impl to actually return absolute, or weaken the docstring.
♻️ Suggested fix (resolve to absolute, matching the docstring)
- resolved = Path(dataset_path).expanduser() + resolved = Path(dataset_path).expanduser().resolve() if not resolved.is_file():🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/wizard.py` around lines 326 - 378, The docstring promises absolute paths but the code only calls Path(dataset_path).expanduser() and returns that (still relative); update _resolve_byod_dataset_path so after expanding user (~) you check existence on that Path, then call resolved = resolved.resolve() (or resolved.resolve(strict=True)) to convert it to an absolute path before returning; i.e., change the block around resolved = Path(...).expanduser() to first validate resolved.is_file(), then replace the returned value with str(resolved.resolve()) so callers always get an absolute path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@forgelm/cli.py`:
- Around line 893-895: The code currently propagates the raw child return code
(train_rc) via logger.error and sys.exit, which can leak signal-derived codes
(e.g., 137, 139); change the logic around the train_rc check so that if train_rc
!= 0 you log the code but map/clamp any value outside the allowed contract
(0,1,2,3,4) to the canonical EXIT_TRAINING_ERROR constant (likely named
EXIT_TRAINING_ERROR or value 2) before calling sys.exit; update the block that
references train_rc, logger.error and sys.exit to compute an exit_code =
train_rc if train_rc in (0,1,2,3,4) else EXIT_TRAINING_ERROR, log accordingly,
and call sys.exit(exit_code).
---
Nitpick comments:
In `@forgelm/cli.py`:
- Around line 798-921: Function _run_quickstart has high cognitive complexity;
extract the training-and-chat tail into a new helper to simplify the dispatcher.
Create a helper named _run_quickstart_train_then_chat(args, result,
output_format) that contains everything after the dry_run check starting at the
subprocess training invocation through the final sys.exit(EXIT_SUCCESS), move
the subprocess logic (train_cmd, subprocess.run, train_rc handling), the
args.no_chat check, the _load_quickstart_train_paths/final_model_dir check, and
the chat subprocess logic into that helper, and call it from _run_quickstart
with the same arguments so behavior, exit codes, logging, and inherited flags
(from _build_quickstart_inherited_flags) remain identical; ensure you import
subprocess in the helper or keep the existing import and preserve the same
checks/comments and return/exit semantics.
In `@forgelm/trainer.py`:
- Around line 162-180: The detection in _dataset_has_gold_answers treats falsy
gold values (e.g., 0, 0.0, False, None) as "no gold" by using
bool(first["gold_answer"]); change the probe to check for key presence instead
(e.g., if isinstance(first, dict): return "gold_answer" in first) so existence
is detected regardless of value, and then update or adjust
test_empty_gold_answer_treated_as_missing to reflect the new presence-based
semantics (or explicitly special-case empty-string-as-missing if you want to
keep the current UX).
In `@forgelm/wizard.py`:
- Around line 326-378: The docstring promises absolute paths but the code only
calls Path(dataset_path).expanduser() and returns that (still relative); update
_resolve_byod_dataset_path so after expanding user (~) you check existence on
that Path, then call resolved = resolved.resolve() (or
resolved.resolve(strict=True)) to convert it to an absolute path before
returning; i.e., change the block around resolved = Path(...).expanduser() to
first validate resolved.is_file(), then replace the returned value with
str(resolved.resolve()) so callers always get an absolute path.
In `@tests/test_quickstart_subprocess.py`:
- Around line 121-160: The test test_chat_subprocess_uses_absolute_model_path
currently patches pathlib.Path.is_dir globally via patch("pathlib.Path.is_dir",
return_value=True); to reduce blast radius and make the test less brittle,
change that to a more surgical approach by patching the Path class method via
patch.object(Path, "is_dir", return_value=True) or (alternatively) create an
actual final_model directory under tmp_path and point the patched
_load_quickstart_train_paths to that tmp_path-based directory; update references
in the test (the patch call and the mocked return_value for
_load_quickstart_train_paths used by main) so the Path.is_dir override is scoped
to Path and/or the created tmp_path/final_model instance rather than globally
patching pathlib.Path.is_dir.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: efae9881-392d-4e08-9828-7f6c07d08d7e
📒 Files selected for processing (7)
forgelm/cli.pyforgelm/templates/grpo-math/config.yamlforgelm/trainer.pyforgelm/wizard.pytests/test_cli_helpers.pytests/test_grpo_math_reward.pytests/test_quickstart_subprocess.py
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/test_cli_helpers.py
- forgelm/templates/grpo-math/config.yaml
…ex, float-eq, type hints Thirteen findings from a SonarCloud scan, all verified against current code before fixing. Constants for repeated literals: - forgelm/cli.py: "forgelm.cli" appeared 3x (logger name + two `python -m` argv calls). Hoisted to a module-level _CLI_MODULE constant. - forgelm/quickstart.py: "Qwen/Qwen2.5-7B-Instruct" appeared 3x in the TEMPLATES registry (customer-support / domain-expert / medical-qa-tr). "HuggingFaceTB/SmolLM2-1.7B-Instruct" appeared 2x. Both hoisted to _QWEN_25_7B_INSTRUCT and _SMOLLM2_17B_INSTRUCT so a future model bump is one edit instead of N. Cognitive complexity reductions: - forgelm/cli.py:_run_quickstart_cmd was 19 / allowed 15. Extracted four helpers: _emit_quickstart_list, _emit_quickstart_result, _run_quickstart_train_subprocess, _run_quickstart_chat_subprocess. The body now reads as a linear dispatch. - forgelm/trainer.py:_build_trainer was 16 / allowed 15. Extracted _build_simple_trl_trainer (covers SFT/ORPO/DPO/SimPO/KTO), _build_grpo_trainer, _resolve_grpo_reward_funcs, and a static _build_classifier_reward. _build_trainer is now a 2-line dispatch. - forgelm/wizard.py:_resolve_byod_dataset_path was 18 / allowed 15. Extracted _validate_local_jsonl helper for the path-existence / first-line-JSONL parse block; the loop body is now flat. - forgelm/wizard.py:run_wizard was 18 / allowed 15. Split into _finalize_quickstart_path (the "start now / save for later" prompt), _detect_hardware_and_backend (Step 1), and _run_full_wizard (Steps 2-8). run_wizard itself is a 5-line dispatch. Regex correctness (forgelm/trainer.py:_ANSWER_PATTERN): - The previous pattern used a reluctant ``(.+?)`` with a triple-alt lookahead including a bare ``$``. SonarCloud flagged the reluctant quantifier as degenerating to "match 1 repetition" because the ``$`` branch fires at every position the engine reaches. Switched to a greedy character-class form ``([^\s.!?\n][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)``: a non-space start, then any non-newline that isn't sentence punctuation, with bare "." between digits preserved via a negative lookahead. Round-tripped against all 15 known cases (decimals, units, time formats, fractions, prose-after-answer, etc.) before commit. Float-equality assertions (Sonar: "Do not perform equality checks with floating point values."): - tests/test_grpo_format_reward.py L39, L40, L80, L81: switched to pytest.approx() — these were comparing exact 0.0 / 1.0 from arithmetic results that could in principle drift on different floating-point paths, even though the current code uses pure Python integer ratios. - tests/test_quickstart_compat.py L156: same fix for an event field comparison. Type-hint correctness: - forgelm/trainer.py:_normalize_answer was annotated ``s: str`` but the body started with ``if s is None: return ""`` and tests pass None. Annotated as ``Optional[str]`` so the type signature matches the documented defensive behavior. No behavior changes; all 638 tests still pass; quickstart smoke for customer-support → dry-run round-trips through the trainer cleanly.
…path, surgical patches
Four findings, all verified against current code before fixing.
CLI exit-code contract (forgelm/cli.py):
- _run_quickstart_train_subprocess used to propagate the child's raw
return code via sys.exit(train_rc). On SIGKILL the OS hands back 137,
on SIGSEGV 139 — neither is part of ForgeLM's documented exit-code
contract (0/1/2/3/4). Added a _PUBLIC_EXIT_CODES frozenset and a clamp:
any rc outside the contract is mapped to EXIT_TRAINING_ERROR (2). The
raw code still gets logged for debuggability.
- _run_quickstart_cmd's tail (training subprocess + no-chat short-circuit
+ chat subprocess + final exit) was extracted into a new
_run_quickstart_train_then_chat helper. The dispatcher is now a flat
sequence of seven steps with cognitive complexity well below the
Sonar threshold.
Gold-answer presence detection (forgelm/trainer.py):
- _dataset_has_gold_answers used `bool(first["gold_answer"])` to gate
presence, which silently treats `0` / `0.0` / `False` as "no gold
answer". A math problem with the literal answer 0 ("What is 5 - 5?")
would have its correctness reward dropped on the floor.
- Switched to presence-based detection with a narrow exception for
empty-string-as-placeholder: `"gold_answer" in first` and `val is not
None and val != ""`. Matches the docstring's "schema placeholder vs.
real label" semantics.
Wizard absolute-path contract (forgelm/wizard.py):
- _validate_local_jsonl's docstring promised absolute paths but only
called Path(...).expanduser() — relative inputs like "./data/foo.jsonl"
came back relative. Added Path.resolve() so callers downstream can
cd freely without losing the dataset.
Test surgical patching (tests/test_quickstart_subprocess.py):
- test_chat_subprocess_uses_absolute_model_path was patching
pathlib.Path.is_dir globally. That overrode every Path.is_dir() call
in the importlib / yaml / setuptools chain that ran during the test —
a foot-gun any time a parallel agent added a new is_dir() probe.
- Replaced with a real tmp_path/checkpoints/final_model directory
pointed at by the patched _load_quickstart_train_paths. Same
assertions; no global mutations.
Tests:
- tests/test_grpo_math_reward.py — three new presence tests
(test_zero_int_, test_zero_float_, test_false_gold_answer_treated_as_present);
test_empty_gold_answer_treated_as_missing extended docstring; new
test_none_gold_answer_treated_as_missing for the None-as-placeholder
case.
- tests/test_cli_quickstart_wiring.py — TestTrainExitCodeClamping with
test_signal_derived_rc_clamped_to_training_error (rc=139 → 2) and
test_documented_rc_propagates_unchanged (rc=3 stays 3).
644 passed (+6 new), smoke clean, no behavior regression for the
existing happy paths.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
forgelm/quickstart.py (1)
401-433:⚠️ Potential issue | 🟡 MinorReminder:
dry_run=Truestill materializes the dataset and writes config.
run_quickstart(..., dry_run=True)still callsconfig_target.parent.mkdir(...)and_resolve_dataset(...)(which copies the seed JSONL). The flag effectively only flips a field onQuickstartResultfor the CLI to consult later — the docstring at line 384 ("Generate the YAML without invoking training") suggests no on-disk side effects beyond the YAML.This was raised in a prior review and not yet addressed; flagging again so it isn't lost. Either tighten the docstring or skip dataset materialization on
dry_run=True.
🧹 Nitpick comments (2)
forgelm/trainer.py (1)
47-50: Redundant\ninside the negated\sclass.SonarCloud is right here: in
[^\s.!?\n],\salready covers\n(along with\t,\r, etc.), so the trailing\nis dead. The second class[^.!?\n]does need the explicit\nbecause there's no\sthere — keep that one.♻️ Drop the duplicate
_ANSWER_PATTERN = re.compile( - r"answer\s*:\s*([^\s.!?\n][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)", + r"answer\s*:\s*([^\s.!?][^.!?\n]*(?:[.!?](?!\s|$)[^.!?\n]*)*)", re.IGNORECASE, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/trainer.py` around lines 47 - 50, The regex in _ANSWER_PATTERN contains a redundant "\n" inside the first negated character class [^\s.!?\n]; remove the "\n" from that class so it becomes [^\s.!?] while leaving the second class [^.!?\n] unchanged; update the _ANSWER_PATTERN definition accordingly to keep semantics identical but eliminate the duplicate newline exclusion.tests/test_grpo_math_reward.py (1)
116-229: Consider adding a non-stringgold_answercase toTestMathRewardFn.
TestDatasetHasGoldAnswerscovers0,0.0, andFalseas present labels, but every case inTestMathRewardFnuses stringgold_answer(e.g.["7"],["2/5"]). That gap is what hides the_normalize_answerAttributeError flagged inforgelm/trainer.py— adding a single parametrized test like_math_reward_fn(["Answer: 0"], gold_answer=[0])would catch it and lock in the contract that the gating function advertises.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_grpo_math_reward.py` around lines 116 - 229, Add a test to TestMathRewardFn that supplies a non-string gold_answer to exercise type handling (e.g., call _math_reward_fn(["Answer: 0"], gold_answer=[0]) and assert reward 1.0), so the reward pipeline (_math_reward_fn and underlying _normalize_answer in forgelm/trainer.py) handles numeric/boolean labels without raising AttributeError; add this as a new unit (or parametrize existing tests) alongside existing string cases to lock in the contract that gold_answer may be non-string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@forgelm/cli.py`:
- Around line 940-953: The except block around run_quickstart currently catches
(FileNotFoundError, ValueError) but not FileExistsError raised by
forgelm.quickstart._resolve_dataset; update the error handling in the try/except
that invokes run_quickstart to also catch FileExistsError (either add it to the
tuple or add a separate except FileExistsError as e), and ensure the handler
uses the same JSON/text reporting logic (printing {"success": False, "error":
str(e)} for output_format == "json" and logger.error("Quickstart failed: %s", e)
otherwise) before calling sys.exit(EXIT_CONFIG_ERROR).
In `@forgelm/trainer.py`:
- Around line 101-123: _normalize_answer currently calls s.strip() and will
crash for non-string gold values (ints/bools) which _dataset_has_gold_answers
allows; to fix, coerce non-None inputs to str at the top of _normalize_answer
(e.g., treat None as "" and otherwise replace s with str(s)) so subsequent
.strip/.rstrip calls are safe, and add a regression test in TestMathRewardFn
that calls _math_reward_fn(["Answer: 0"], gold_answer=[0]) and asserts rewards
== [1.0] (also include a boolean case like gold_answer=[False]) to ensure
numeric/bool gold labels don't crash.
In `@forgelm/wizard.py`:
- Around line 369-389: The HF Hub regex branch misclassifies relative local
paths (like "data/train.jsonl"); change the control flow in the input loop in
wizard.py so you validate the path as a local JSONL first (call
_validate_local_jsonl(dataset_path) immediately after basic checks) and only if
that returns None then test _HF_HUB_ID_RE.match(dataset_path) and treat it as a
Hub ID; keep the existing messages but ensure _validate_local_jsonl is attempted
before the _HF_HUB_ID_RE check so local files are accepted and Hub IDs are a
fallback.
---
Nitpick comments:
In `@forgelm/trainer.py`:
- Around line 47-50: The regex in _ANSWER_PATTERN contains a redundant "\n"
inside the first negated character class [^\s.!?\n]; remove the "\n" from that
class so it becomes [^\s.!?] while leaving the second class [^.!?\n] unchanged;
update the _ANSWER_PATTERN definition accordingly to keep semantics identical
but eliminate the duplicate newline exclusion.
In `@tests/test_grpo_math_reward.py`:
- Around line 116-229: Add a test to TestMathRewardFn that supplies a non-string
gold_answer to exercise type handling (e.g., call _math_reward_fn(["Answer: 0"],
gold_answer=[0]) and assert reward 1.0), so the reward pipeline (_math_reward_fn
and underlying _normalize_answer in forgelm/trainer.py) handles numeric/boolean
labels without raising AttributeError; add this as a new unit (or parametrize
existing tests) alongside existing string cases to lock in the contract that
gold_answer may be non-string.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d63ae65a-270a-42c1-bab9-b9e05ffe1acb
📒 Files selected for processing (9)
forgelm/cli.pyforgelm/quickstart.pyforgelm/trainer.pyforgelm/wizard.pytests/test_cli_quickstart_wiring.pytests/test_grpo_format_reward.pytests/test_grpo_math_reward.pytests/test_quickstart_compat.pytests/test_quickstart_subprocess.py
✅ Files skipped from review due to trivial changes (1)
- tests/test_cli_quickstart_wiring.py
…ative-path misclassify, regex cleanup Five findings, all verified against current code before fixing. Distinct from the round-2 commit dfeb6e5 (exit-code clamp + cog refactor); these touch different code paths. cli.py — FileExistsError missing from quickstart catch tuple: run_quickstart's try/except caught (FileNotFoundError, ValueError) but not FileExistsError, which _resolve_dataset raises when the operator passes --output pointing at a directory that already has a seed dataset (the per-run dir collision guard). Without this, a re-run with an explicit --output emits a Python traceback instead of the actionable three-bullet message. Added FileExistsError to the tuple so the same JSON-or-text reporting path runs. trainer.py — gold-answer presence vs truthiness: _dataset_has_gold_answers used `bool(first["gold_answer"])` which treats `0`, `0.0`, `False`, and `""` all as "no gold". A math problem with the correct answer `"0"` is a real and common case, so dropping it silently is wrong. Switched to presence-based detection: empty string and None still count as missing (those are schema-placeholder shapes), but `0`, `0.0`, `False` now count as real gold answers. trainer.py — _normalize_answer crashed on non-string gold: Once the dataset probe accepts numeric/bool gold values, the reward function passes them straight to `_normalize_answer`, which called `.strip()` on the input and crashed on int/bool with AttributeError. Annotated the parameter as `Any` and added an `isinstance(s, str)` coercion at the top so int/float/bool stringify cleanly. trainer.py — redundant `\n` in _ANSWER_PATTERN char class: The first negated class `[^\s.!?\n]` had `\n` inside it but `\s` already matches newlines. Dropped to `[^\s.!?]`. Pure cleanup, semantics unchanged. wizard.py — relative local paths misclassified as HF Hub IDs: The HF Hub regex `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` matches paths like `data/train.jsonl` (single slash, safe-name char class). The previous loop tested Hub regex first, so the path silently bypassed JSONL parse validation and was sent to the trainer as a Hub ID. Reordered the loop: try local file first; only when the path doesn't exist on disk fall back to Hub semantics. Implementation uses a sentinel `_BYOD_LOCAL_NOT_FOUND` returned by `_validate_local_jsonl` to distinguish "exists-but-malformed" (re-prompt) from "not on disk" (Hub fallback). Tests: - tests/test_grpo_math_reward.py: * `test_non_string_gold_answer_does_not_crash` — parametrized over int 0, float 0.0, bool False, int 42, float 3.14. - tests/test_wizard_byod.py: * `test_byod_relative_local_path_not_misclassified_as_hub_id` — regression for the local-vs-Hub ordering. * Updated `test_byod_expands_user_home` to compare via Path equality instead of string equality (macOS /tmp → /private/tmp symlink canonicalisation now applies after the .resolve() call inside _validate_local_jsonl). 650 passed / 8 skipped. Smoke confirmed: re-running quickstart with the same --output now prints the FileExistsError message instead of a traceback.
…ception reraise, notebook DRY Six findings; verified line numbers had drifted across earlier rounds but all six were genuine in current code. forgelm/cli.py — _output_result cog complexity: Sonar pointed at line 527 (a stale snapshot from before the Phase 10.5 refactor). The actual offending function in current code was _output_result: 6 conditional branches in the JSON envelope plus 5 in the text-mode path drove the metric to 39. Split into four helpers: _build_result_json_envelope, _log_result_status, _log_cost_summary, _log_benchmark_summary. Each helper handles one concern; _output_result is now a 5-line dispatch. forgelm/fit_check.py — estimate_vram cog complexity: 20 → ~10 by extracting three helpers: _resolve_quant_scheme (the 4-bit / 8-bit / dtype-map ladder), _component_breakdown (the LoRA-vs-GaLore branch and the per-component GB calculation), _detect_available_vram_gb (the torch.cuda.mem_get_info() probe with its exception fallback), and _verdict_for (the threshold ladder). _DTYPE_TO_QUANT moved to module scope so it isn't rebuilt on every call. estimate_vram body is now a linear sequence of named steps. forgelm/judge.py — run_judge_evaluation cog complexity: 19 → ~10 by extracting _score_eval_prompts (the per-prompt generation + judge-call + score-clip loop) and _summarize_judge_scores (the no-valid-scores vs avg-vs-min decision tree). The public function reads as input-gate → score → summarize → optional-save → return. forgelm/merging.py — torch.quantile missing dim: Sonar wants an explicit dim argument. flat is already 1-D from .flatten(), so dim=0 produces the same scalar as the default; added it explicitly to satisfy the rule and document intent. tests/runtime_smoke.py — exception wrapping: The smoke test caught SystemExit from main() and re-raised as RuntimeError, masking the CLI's exit-code contract. Sonar flagged this as "reraise this exception". Switched to bare ``raise`` so SystemExit propagates unchanged with its original exit code; the smoke runner sees the same non-zero code the CLI emitted. notebooks/kto_binary_feedback.ipynb — duplicated literal: "What is Python?" appeared 3 times across two cells. Hoisted to PROMPT_PYTHON in Step 3 (where the dataset is built) and reused in Step 8's test_prompts list. Notebook cell ordering means Step 3 always runs before Step 8 in a normal Run-All flow. 650 passed / 8 skipped. fit-check smoke verified estimate_vram still produces an identical breakdown; quickstart smoke for customer-support still round-trips through the trainer cleanly.
…aths
Two SonarCloud security findings.
forgelm/grpo_rewards.py — regex polynomial backtracking risk:
The previous _ANSWER_PATTERN was ``answer\s*:\s*\S[^\n]*?\s*\Z`` —
a reluctant ``[^\n]*?`` followed by ``\s*\Z``. The two quantifiers
overlap (``\s`` is a subset of ``[^\n]``) which static analysis flags
as a polynomial-runtime / catastrophic-backtracking surface; with a
long input that almost-but-doesn't match the trailing ``Answer:``
shape the engine could spend O(n²) walking the reluctant quantifier.
Fix is two-part:
1) The pattern is now a greedy ``answer\s*:\s*\S[^\n]*\Z`` — no
reluctant quantifier, no overlap, end pinned by ``\Z``.
2) The caller in format_match_reward() now strips trailing
whitespace before passing the completion to .search(). That
collapses what used to be the ``\s*\Z`` tail (the source of the
overlap) to a plain ``\Z``.
Linear-time guaranteed; existing semantic still preserved
(case-insensitive "Answer:" anywhere, value at least one non-space
char, optional trailing units up to end-of-string).
tests/test_quickstart_hardening.py — hardcoded /tmp paths:
Two test_null_model_section_is_replaced_not_crashed assertions and
one test_non_mapping_model_value_raises_clear_error call passed
literal "/tmp/data.jsonl" as the dataset path. The path is never
actually opened (it's only rendered into a YAML structure for
comparison), but Sonar flags any string referring to a globally
writable directory because the same pattern in production code
would create a TOCTOU race against an attacker pre-creating the
file. Switched all three call sites to ``str(tmp_path / "data.jsonl")``
using the existing pytest tmp_path fixture — same shape, no
publicly-writable path on the surface.
650 passed / 8 skipped. ReDoS micro-benchmark: 20 000-char worst-case
input matches in 0.07 ms with both old and new patterns (the old
form was theoretically O(n²) but Python's ``re`` engine memoises
many cases; the static analyser is what flags the shape, and the
shape is now linear).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
forgelm/judge.py (1)
308-312:⚠️ Potential issue | 🟡 MinorNon-numeric judge
scorevalues can crash the entire eval run.
float(raw_score)will raiseValueError/TypeErrorif the judge returns a non-numeric score (e.g.,"score": "high","score": "8/10","score": [8]). Because this happens inside the per-prompt loop with notry/except, a single malformed verdict aborts the wholerun_judge_evaluationand bypasses thefailure_count/score=Nonesentinel path that the rest of the refactor carefully preserves.🛡️ Suggested guard
- raw_score = result.get("score") - score = _clip_judge_score(float(raw_score) if raw_score is not None else None) + raw_score = result.get("score") + try: + numeric_score = float(raw_score) if raw_score is not None else None + except (TypeError, ValueError): + logger.warning("Judge returned non-numeric score %r; treating as failure", raw_score) + numeric_score = None + score = _clip_judge_score(numeric_score)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/judge.py` around lines 308 - 312, When converting per-judge raw_score to float inside run_judge_evaluation, guard the conversion to avoid crashing on non-numeric values: wrap float(raw_score) in a try/except (catch ValueError and TypeError), and on exception set score = None, increment failure_count, and proceed to append score to scores (so the existing _clip_judge_score(None) / sentinel path is preserved); use the same symbols raw_score, score, _clip_judge_score, failure_count, scores and result to locate and update the logic.
🧹 Nitpick comments (5)
forgelm/judge.py (1)
326-332: Minor: passlen(eval_prompts)instead of the full list.
_summarize_judge_scoresonly useseval_promptsfor its length in log/error messages. Passing anint(e.g.,num_prompts) tightens the contract and avoids implying the function inspects prompt content.♻️ Optional refactor
def _summarize_judge_scores( *, scores: List[Optional[float]], failure_count: int, - eval_prompts: List[str], + num_prompts: int, min_score: float, ) -> tuple[float, bool, Optional[str]]:…and update the two
len(eval_prompts)references plus the call site at line 265.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/judge.py` around lines 326 - 332, Change _summarize_judge_scores so it accepts num_prompts: int instead of eval_prompts: List[str] (update the signature and type annotation) and replace its internal uses of len(eval_prompts) with num_prompts in log/error messages; then update every call that currently passes eval_prompts to pass len(eval_prompts)) instead (ensure the call site(s) use the same symbol names and types). Ensure the return tuple and behavior remain unchanged and update any docstring/comments referring to eval_prompts.tests/test_grpo_format_reward.py (1)
205-248: Classifier-reward wiring test is correct, but the assertion is weaker than the docstring claims.The docstring on line 244 says "calling it should invoke our mock_rw_model exactly once," but line 248 only asserts
mock_rw_model.called(boolean). Consider tightening toassert mock_rw_model.call_count == 1to actually enforce the "exactly once" contract — otherwise a future regression that double-invokes the reward model per call would slip through.♻️ Tighten the call-count assertion
- assert mock_rw_model.called, "classifier-backed reward closure must call the loaded reward model" + assert mock_rw_model.call_count == 1, ( + "classifier-backed reward closure must call the loaded reward model exactly once" + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_grpo_format_reward.py` around lines 205 - 248, The test test_reward_func_uses_classifier_when_configured currently only asserts mock_rw_model.called; change this to assert the exact call count to match the docstring by replacing that boolean check with a precise check such as assert mock_rw_model.call_count == 1 so the closure invocation is enforced to call the loaded reward model exactly once (refer to mock_rw_model and reward_funcs in the test for locating the assertion to update).tests/test_quickstart_subprocess.py (1)
49-168: LGTM — covers the high-value subprocess argv invariants without over-coupling.The actionable-recovery message check (line 64-65) and the
os.path.isabsportability fix on lines 112/167 hit the right level of specificity. Materializing a realtmp_path/checkpoints/final_modelinstead of monkey-patchingPath.is_dir()globally is the right call — the past foot-gun is gone.Minor optional polish: the two
main()-driven tests share ~80% of their setup (recorder, argv, patches, final_model_dir creation). A small fixture (e.g._invoke_quickstart) would let each test focus on the one assertion that distinguishes them, though it isn't blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_quickstart_subprocess.py` around lines 49 - 168, Tests test_train_subprocess_uses_absolute_config_path and test_chat_subprocess_uses_absolute_model_path duplicate ~80% setup; extract a small helper (e.g. _invoke_quickstart or fixture) that builds argv, creates final_model_dir, constructs the _RunRecorder, applies the same patches (forgelm.quickstart._detect_available_vram_gb, forgelm.cli._load_quickstart_train_paths, subprocess.run, sys.argv), runs main() and returns the recorder and exit code so each test only asserts its specific argv details (use symbols _RunRecorder, main, and the two test names to locate callers to refactor).tests/test_cli_quickstart_wiring.py (1)
32-52: Optional:_RunRecorderis now duplicated verbatim withtests/test_quickstart_subprocess.py.Both files explicitly note the duplication is intentional, but a tiny shared
tests/conftest.py(ortests/_quickstart_helpers.py) holding_RunRecorderand thefake_train_paths/_invoke_mainhelpers would let future quickstart-wiring tests start with one import instead of copy-pasting the harness. Non-blocking — flag to revisit when a third file in the same shape lands.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_cli_quickstart_wiring.py` around lines 32 - 52, Extract the duplicated test helper class _RunRecorder and the helpers fake_train_paths and _invoke_main into a single shared test helper module (e.g., a tests conftest or helpers module) and update both test files to import them instead of duplicating; specifically, move the _RunRecorder implementation (preserving its constructor signature, __call__ behavior and internal _Completed class) and the fake_train_paths/_invoke_main helper functions into the shared module, then replace the verbatim copies in tests/test_cli_quickstart_wiring.py and tests/test_quickstart_subprocess.py with imports from that shared module.forgelm/fit_check.py (1)
253-256: Optional: them.config.torch_dtypefallback is unreachable against the currentModelConfigschema.
ForgeConfig.modelis a PydanticModelConfigwithextra="forbid"and notorch_dtype(orconfig) field — so bothgetattr(m, "torch_dtype", None)andgetattr(getattr(m, "config", None), "torch_dtype", None)always evaluate toNone, and the function always falls through to the bf16 default. If that's intentional defensive code for a future field, fine; if you want to actually honor a user-declared dtype, the field needs to exist onModelConfigfirst.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@forgelm/fit_check.py` around lines 253 - 256, The fallback to m.config.torch_dtype in fit_check.py is unreachable because ForgeConfig.model uses a Pydantic ModelConfig that lacks a torch_dtype (and config) field; add an optional torch_dtype field to the ModelConfig schema (e.g., Optional[str] or Optional[torch.dtype]) with appropriate validation/default so getattr(m, "torch_dtype", None) returns the user-declared dtype, and keep the existing lookup in fit_check.py unchanged; update the ModelConfig class definition to include the new torch_dtype attribute and tests/docs where ModelConfig is constructed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@forgelm/trainer.py`:
- Around line 633-651: The _build_classifier_reward function needs two fixes:
add a pad_token guard before tokenization by setting _rw_tok.pad_token =
_rw_tok.pad_token or _rw_tok.eos_token to avoid "asking to pad" errors for
tokenizers without an explicit pad token (e.g., GPT-2), and update the function
docstring to state the expected reward-head shape (e.g., "expects a
sequence-classification head with output_size=1; logits[:, 0] returns the scalar
reward; 2-class heads will yield class logits and are incompatible without
post-processing"). Make these changes inside the _build_classifier_reward
staticmethod (affecting symbols _rw_tok and _reward_fn and clarifying use of
logits[:, 0]).
---
Outside diff comments:
In `@forgelm/judge.py`:
- Around line 308-312: When converting per-judge raw_score to float inside
run_judge_evaluation, guard the conversion to avoid crashing on non-numeric
values: wrap float(raw_score) in a try/except (catch ValueError and TypeError),
and on exception set score = None, increment failure_count, and proceed to
append score to scores (so the existing _clip_judge_score(None) / sentinel path
is preserved); use the same symbols raw_score, score, _clip_judge_score,
failure_count, scores and result to locate and update the logic.
---
Nitpick comments:
In `@forgelm/fit_check.py`:
- Around line 253-256: The fallback to m.config.torch_dtype in fit_check.py is
unreachable because ForgeConfig.model uses a Pydantic ModelConfig that lacks a
torch_dtype (and config) field; add an optional torch_dtype field to the
ModelConfig schema (e.g., Optional[str] or Optional[torch.dtype]) with
appropriate validation/default so getattr(m, "torch_dtype", None) returns the
user-declared dtype, and keep the existing lookup in fit_check.py unchanged;
update the ModelConfig class definition to include the new torch_dtype attribute
and tests/docs where ModelConfig is constructed.
In `@forgelm/judge.py`:
- Around line 326-332: Change _summarize_judge_scores so it accepts num_prompts:
int instead of eval_prompts: List[str] (update the signature and type
annotation) and replace its internal uses of len(eval_prompts) with num_prompts
in log/error messages; then update every call that currently passes eval_prompts
to pass len(eval_prompts)) instead (ensure the call site(s) use the same symbol
names and types). Ensure the return tuple and behavior remain unchanged and
update any docstring/comments referring to eval_prompts.
In `@tests/test_cli_quickstart_wiring.py`:
- Around line 32-52: Extract the duplicated test helper class _RunRecorder and
the helpers fake_train_paths and _invoke_main into a single shared test helper
module (e.g., a tests conftest or helpers module) and update both test files to
import them instead of duplicating; specifically, move the _RunRecorder
implementation (preserving its constructor signature, __call__ behavior and
internal _Completed class) and the fake_train_paths/_invoke_main helper
functions into the shared module, then replace the verbatim copies in
tests/test_cli_quickstart_wiring.py and tests/test_quickstart_subprocess.py with
imports from that shared module.
In `@tests/test_grpo_format_reward.py`:
- Around line 205-248: The test test_reward_func_uses_classifier_when_configured
currently only asserts mock_rw_model.called; change this to assert the exact
call count to match the docstring by replacing that boolean check with a precise
check such as assert mock_rw_model.call_count == 1 so the closure invocation is
enforced to call the loaded reward model exactly once (refer to mock_rw_model
and reward_funcs in the test for locating the assertion to update).
In `@tests/test_quickstart_subprocess.py`:
- Around line 49-168: Tests test_train_subprocess_uses_absolute_config_path and
test_chat_subprocess_uses_absolute_model_path duplicate ~80% setup; extract a
small helper (e.g. _invoke_quickstart or fixture) that builds argv, creates
final_model_dir, constructs the _RunRecorder, applies the same patches
(forgelm.quickstart._detect_available_vram_gb,
forgelm.cli._load_quickstart_train_paths, subprocess.run, sys.argv), runs main()
and returns the recorder and exit code so each test only asserts its specific
argv details (use symbols _RunRecorder, main, and the two test names to locate
callers to refactor).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4cbe8d1a-2832-49ce-a694-d73acb3787a4
📒 Files selected for processing (15)
forgelm/cli.pyforgelm/fit_check.pyforgelm/judge.pyforgelm/merging.pyforgelm/quickstart.pyforgelm/trainer.pyforgelm/wizard.pynotebooks/kto_binary_feedback.ipynbtests/runtime_smoke.pytests/test_cli_quickstart_wiring.pytests/test_grpo_format_reward.pytests/test_grpo_math_reward.pytests/test_quickstart_compat.pytests/test_quickstart_subprocess.pytests/test_wizard_byod.py
✅ Files skipped from review due to trivial changes (1)
- forgelm/merging.py
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/test_grpo_math_reward.py
- tests/test_quickstart_compat.py
- forgelm/wizard.py
| @staticmethod | ||
| def _build_classifier_reward(reward_model_path: str): | ||
| """Wrap an HF sequence-classification model as a TRL reward callable.""" | ||
| from transformers import AutoModelForSequenceClassification | ||
| from transformers import AutoTokenizer as _AutoTok | ||
|
|
||
| self.trainer = GRPOTrainer(**trainer_kwargs) | ||
| else: | ||
| raise ValueError(f"Unknown trainer_type: {tt}") | ||
| _rw_tok = _AutoTok.from_pretrained(reward_model_path) | ||
| _rw_model = AutoModelForSequenceClassification.from_pretrained(reward_model_path, device_map="auto") | ||
|
|
||
| def _reward_fn(completions, **kwargs): | ||
| import torch as _t | ||
|
|
||
| inputs = _rw_tok(completions, return_tensors="pt", truncation=True, padding=True, max_length=512) | ||
| inputs = {k: v.to(_rw_model.device) for k, v in inputs.items()} | ||
| with _t.no_grad(): | ||
| logits = _rw_model(**inputs).logits | ||
| return logits[:, 0].tolist() | ||
|
|
||
| return _reward_fn |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if accelerate is in install_requires / dependencies
fd -t f 'pyproject.toml|setup.py|setup.cfg|requirements*.txt' --max-depth 3 \
--exec rg -n -i 'accelerate' {} \;Repository: cemililik/ForgeLM
Length of output: 116
🏁 Script executed:
#!/bin/bash
# Find and examine the dependency declaration files
fd -t f 'pyproject.toml|setup.py' --max-depth 3 -x cat -n {} \; | head -100Repository: cemililik/ForgeLM
Length of output: 3589
🏁 Script executed:
#!/bin/bash
# Check for pad_token handling in the trainer.py file
rg -n 'pad_token' forgelm/trainer.py -A 2 -B 2Repository: cemililik/ForgeLM
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Look for any documentation or docstring mentioning reward model shape expectations
rg -n 'reward.*model|reward.*shape|scalar.*reward' forgelm/trainer.py -A 3 -B 1Repository: cemililik/ForgeLM
Length of output: 2458
Add pad_token guard and document reward-model shape assumption in docstring.
The _build_classifier_reward method has two issues that should be addressed:
-
_rw_tok(completions, ..., padding=True, ...)will raiseValueError: Asking to pad but the tokenizer does not have a padding tokenfor tokenizers without an explicit padding token (e.g., some GPT-2-based reward models). Add_rw_tok.pad_token = _rw_tok.pad_token or _rw_tok.eos_tokenbefore tokenizing to handle this edge case. -
The docstring does not document that
logits[:, 0]assumes a scalar (1-output) reward head. For 2-class classifiers, this returns the negative class logit. Document the expected reward-model shape (e.g., "expects a sequence-classification head with output_size=1") so users configure compatible models.
Note: accelerate>=0.27.0 is already a declared runtime dependency, so device_map="auto" support is guaranteed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@forgelm/trainer.py` around lines 633 - 651, The _build_classifier_reward
function needs two fixes: add a pad_token guard before tokenization by setting
_rw_tok.pad_token = _rw_tok.pad_token or _rw_tok.eos_token to avoid "asking to
pad" errors for tokenizers without an explicit pad token (e.g., GPT-2), and
update the function docstring to state the expected reward-head shape (e.g.,
"expects a sequence-classification head with output_size=1; logits[:, 0] returns
the scalar reward; 2-class heads will yield class logits and are incompatible
without post-processing"). Make these changes inside the
_build_classifier_reward staticmethod (affecting symbols _rw_tok and _reward_fn
and clarifying use of logits[:, 0]).
…plicit rationale
Three Codacy annotations against current code; verified each and added
inline rationale comments rather than functional changes — the analyser
warnings are about shape, not behaviour, and the call sites are already
safe by construction.
forgelm/cli.py L884, L907 — subprocess.run without static string:
Both call sites already carried `# noqa: S603 # nosec B603`, but
Codacy's runner doesn't read those Bandit/ruff suppressions inline.
Added a 6-line block comment above each subprocess.run call spelling
out the security argument:
- argv is a fixed list (not a shell string) → shell=False implicit.
- argv[0] is sys.executable, not a user-controlled command name.
- argv[1..3] are the literal "-m"/_CLI_MODULE/[--config|chat]; the
only user-influenced segment is the abspath of the config or
model dir, passed as a single argv element with no shell
expansion or string concatenation.
No injection surface — Codacy can flag this if the rule is purely
syntactic, but reviewers reading the code now have a written argument
for why the suppression is correct.
forgelm/quickstart.py L211 — "Possibly found usage of AI tooling: PyTorch":
Codacy's ML-detection rule fires on `import torch`. Torch is a
documented core dependency for the trainer (declared in pyproject.toml
and the README), and the import is local *only* to keep
`forgelm --help` startup torch-free. Added a multi-line rationale
comment explaining why this is expected and an inline
`# codacy-disable: pytorch-detector — core dep, lazy for cold-start cost`
marker.
Tests still 650 passed / 8 skipped — pure comment / annotation changes,
no logic touched.
|
PR #9 landed Phase 10.5 on main (merge commit 03999d0), but the v0.4.5 git tag and PyPI upload have not happened yet — `git tag -l v0.4.5` returns empty. The roadmap docs claimed 'Tagged but not yet on PyPI' which over-reported the release state. - docs/roadmap.md: latest-release block + doc map line clarified. - docs/roadmap-tr.md: parallel Turkish wording. - docs/roadmap/phase-12-quickstart.md: DONE banner now says 'merged to main as v0.4.5 via PR #9 (...); tag + PyPI publish are the remaining release-engineering steps' instead of 'shipped in v0.4.5'. No content/code changes — wording-only, gets the docs back in sync with what git actually contains.
…reward, CI) CHANGELOG.md, docs/roadmap/releases.md, docs/guides/quickstart.md were written before two later changes landed: - code-assistant fallback model bumped to Qwen2.5-Coder-1.5B-Instruct (was generic SmolLM2-1.7B-Instruct). - medical-qa-tr fallback model bumped to Qwen2.5-1.5B-Instruct (was English-only SmolLM2; new fallback is multilingual). - bundled GRPO baseline reward (forgelm/grpo_rewards.py + _math_reward_fn) replaced the old 'no reward → ValueError' wiring. - Test/CI surface grew with test_quickstart_hardening, test_grpo_math_ reward, test_grpo_format_reward, test_wizard_byod, test_cli_quickstart_ wiring, test_packaging, plus a wheel-install-smoke nightly job. Also corrected example counts in the release notes and CHANGELOG to match the actual JSONL line counts (58/59/49/40 — same numbers used in LICENSES.md), and added a per-template fallback table to the quickstart guide so the auto-downsize behavior is no longer described with a stale single-example sentence. releases.md status banner softened from 'Released' to 'Merged to main via PR #9; tag + PyPI publish pending' so it matches roadmap.md and phase-12-quickstart.md, since 'git tag -l v0.4.5' is empty.
…gent review findings - Wave 5 PR #34 round 3 absorption: full sweep of CodeRabbit's 6 OOD + 20 Minor inline review comments PLUS 24 multi-agent review findings (code-correctness / privacy-security / standards / test-rigor docs under docs/analysis/code_reviews/wave5-*.md). HIGH/Major absorptions: - 4 broken `forgelm.cli.subcommands.*` symbol citations across purge/approvals reference docs (en+tr): _run_row_erasure → _run_purge_row_id, _handle_pending → _run_approvals_list_pending, _handle_show → _run_approvals_show (F-W5-PRIV-02). - configuration.md (en+tr): drop `kserve | triton` ghost choices and the entire `## deployment:` ghost YAML block (no `DeploymentConfig` in ForgeConfig; extra="forbid" rejects it at load) — replace with a v0.6.0+ Phase 14 callout (F-W5-S1, F-W5-S2). - ingestion.md (en+tr): drop conflicting "instructions format default" claim; document raw-chunk JSONL with summary-only --output-format {text,json} (CR-OOD ingestion). - closure-plan PR #33 status: line 131 said "PR #33 pending" but line 119 said merged with `01e40ba` (CR-OOD closure-plan). - leakage.md (en+tr): remove `audit.leakage_check.remove_cross_split_overlap` knob reference — knob doesn't exist in code; replace with the documented jq-based removal flow (CR-OOD leakage). - TR + EN experiment-tracking: drop `$ ` prompt prefix in `text`-tagged preview blocks for `forgelm compare-runs` (CR-OOD experiment-tracking). - roadmap mermaid (en+tr): add Phase 12.6 (Closure Cycle) → v0.5.5 node + edge so visual flow matches table (CR-OOD roadmap). Minor absorptions: - phase-12-6-closure-cycle.md L123: stale "(currently 0.5.1rc1)" parenthetical → already-bumped during Wave 5 (F-W5-S4 / CR Minor). - gdpr-erasure.md (en+tr): rename duplicate `### What gets recorded` / `### Neyi kaydeder` H3 to disambiguate Article 17 vs 15 sections (CR Minor #2 + #20). - verify-gguf.md (en+tr) L147: revise misleading "still match SHA-256" hash explanation; clarify magic-header layer doesn't catch truncation independent of sidecar (CR Minor #3). - verify_audit link mismatch in 4 reference docs (en+tr × verify_gguf + verify_annex_iv): label text now matches href (CR Minor #4 + #8). - air_gap_deployment-tr.md L19: add `text` language tag to ASCII diagram fence (CR Minor #6). - site/index.html L374: drop internal "Phase 19" milestone leak; tag reads `embed` (user-facing label) (CR Minor #7). - air_gap_deployment.md L193: fix dangling sentence fragment in HF_TOKEN gated-models tip (CR Minor #9). - performance.md (en+tr) L184: drop ghost `forgelm audit --output-format parquet`; replace with pandas/pyarrow conversion step (CR Minor #10). - phase-11-5-backlog.md L10-12: link text → package paths matching href (forgelm/data_audit.py → forgelm/data_audit/, etc.) (CR Minor #11). - testing.md L140: stale `23/23` → `39/39` bilingual parity count (CR Minor #12). - testing.md L198: `python3` prefix consistency in checklist (CR Minor #13). - supply-chain.md (en+tr): bandit cadence aligned to standards doc (Nightly 03:00 UTC + release tag, not "every PR + main push") (CR Minor #14 + #15). - translations.js L1927: "Three values" → "Two values" (only forgelm-lang + forgelm-theme listed) (CR Minor #16). - approvals_subcommand.md (en+tr) L26: staging path → match L84 JSON sample (`final_model.staging.<run_id>` canonical, run-id-less form honoured as fallback) (CR Minor #17 + #19). - translations.js L1445/L1462/L1504/L1521 (en+tr): `<code>reject</code>` → `<code>forgelm reject</code>` for fully-qualified copy/paste parity (CR Minor #18). - _meta.yaml header: add formal DE/FR/ES/ZH deferred-policy banner per closure-plan Task L (F-W5-S6). - release.md L20 + L359: stale "0.3.1rc1 current" + "no __version__" claims; corrected to 0.5.5 + reflect that __version__ + __api_version__ ARE exported per Library API standard (F-W5-S3). - purge_subcommand.md (en+tr): add WARNING-on-fallback language for --output-dir asymmetry vs reverse-pii (F-W5-PRIV-05). - verify-audit.md (en+tr): replace overstated "escrow" wording with KMS/HSM-based separation (F-W5-PRIV-06). Tool tightening + new tests: - check_cli_help_consistency.py: tighten _FORWARD_REF_MARKERS to whole-phrase forms (drop bare "planned"/"future"/"roadmap" which matched "planned attacks" / "future work" / "roadmap-driven" in unrelated prose); add Turkish equivalents for bilingual docs (F-W5-01). - test_check_cli_help_consistency.py: 3 new regression tests: bare-substring negative (F-W5-01), >±3-line forward-ref window negative (F-W5-TR-01a), and Wrong:-block scope-to-one-block resumption (F-W5-TR-01b). Suite 17 → 20 tests, all green. User-manual orphan fix: - _meta.yaml: register `reference/json-output` page (existed in filesystem since Wave 2 but never declared in nav config — was invisible on the live site). Now declared = on-disk = 65 each. Skipped (with reason): - CR Minor #5 (library_api.md `forgelm.__version__` example): INVALID — verified `python3 -c "import forgelm; print(forgelm.__version__)"` prints 0.5.5 cleanly; both __version__ and __api_version__ ARE exported per Library API standard. CodeRabbit's web-search must have hit pre-Wave-5 PyPI. Doc is correct; code matches. - F-W5-S5 (parity tool autodiscover usermanuals): registry shape refactor (~50 new pairs) is scope creep for round 3; defer to v0.6. - F-W5-S8 (compliance_summary-tr.md): explicitly deferred to v0.6 by the finding itself. - F-W5-S9, F-W5-TR-11/12 (NIT cite/style): non-load-bearing. - F-W5-02/03/04 (cognitive complexity, unterminated fence, resume-after-Wrong unit test): refactor scope; covered by F-W5-TR-01 block-scope tests for the resume case. - F-W5-TR-02 (cleanup commit baseline regression test): the live CI --strict gate IS the guard; doubling up the integration-test layer is defensible but not blocking. - F-W5-TR-03/05/06/07/08/09/10 (extras): non-blocking coverage gaps. Self-review gauntlet (all green): ruff format/check ✅ bilingual_parity --strict 39/39 ✅ anchor_resolution --strict 264/264 ✅ cli_help_consistency --strict 425/425 ✅ (reduced from 449 due to intentional invocation removals: ghost YAML block, parquet ghost, 4 broken symbol-cite paths, etc.) site_claims --strict (artefacts/templates/GPU/version) ✅ field_descriptions --strict ✅ pytest 1433 passed / 14 skipped (+5 new tests vs round 2) ✅ forgelm --config config_template.yaml --dry-run ✅ build_usermanuals.py: en/tr 65/65 OK, JS bundles regenerate ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>



Summary
forgelm quickstart customer-supportrenders a config the existing trainer accepts unchanged, copies a small seed dataset, runs training, and drops into chat with the result.forgelm/templates/(all QLoRA 4-bit, rank=8, batch=1 — designed to fit on a 12 GB consumer GPU):customer-support,code-assistant,domain-expert(BYOD),medical-qa-tr(Turkish medical Q&A with safety disclaimers),grpo-math.no-gpu-detectedreason without CUDA.forgelm --wizardnow opens with "Start from a template?" Yes routes to quickstart; No falls through to the existing 8-step interactive flow. Same code paths, same YAML schema — no bifurcation.quickstart --dry-run+--config <out> --dry-runfor every bundled template; catches template drift the moment a config schema changes.pip install forgelmusers get templates too via[tool.setuptools.package-data].Closes Phase 10.5 of the roadmap. Bumps version to 0.4.5.
What changed
forgelm/quickstart.pyauto_select_model(),run_quickstart()orchestratorforgelm/templates/(5 dirs +LICENSES.md)forgelm/cli.pyquickstartsubcommand:--list/--model/--dataset/--output/--dry-run/--no-chatforgelm/wizard.pytests/test_quickstart.py(40 tests)load_config()— strongest guard against template drift.github/workflows/nightly.ymlpyproject.toml0.4.5;package-databundles templates into the wheelREADME.md,CHANGELOG.md,docs/guides/quickstart.md,docs/roadmap.md/-tr.md,docs/roadmap/phase-12-quickstart.md,docs/roadmap/releases.mdTest plan
ruff format . && ruff check .— cleanpytest tests/test_quickstart.py— 40/40 passtest_grpo_reward.pyunrelated to this PR — verified viagit stash)forgelm quickstart --listworksforgelm quickstart customer-support --dry-runwrites YAML and exits 0forgelm.config.load_configpip install -e .thenforgelm quickstart --listworks in a fresh venv (templates bundle correctly)Notes for reviewer
mainvia PR feat: Phase 10 post-training completion (v0.4.0) #7. The actual code diff (above) is clean and Phase 10.5-only — recommend squash merge to keepmainhistory tidy.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Packaging