From 0fe4bdeff7b3362a87da6cb672e5a1d1edc91e9a Mon Sep 17 00:00:00 2001 From: Kai Christensen Date: Wed, 28 Oct 2026 11:29:42 -0600 Subject: [PATCH 1/2] Remove legacy YOLO model upload packaging Co-authored-by: OpenAI Codex --- README.md | 4 +- docs/index.md | 4 +- roboflow/cli/handlers/model.py | 2 +- roboflow/util/model_processor.py | 96 +++++++++--------------------- tests/util/test_model_processor.py | 39 ++++++------ 5 files changed, 51 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 1be0577f..55a67355 100644 --- a/README.md +++ b/README.md @@ -164,8 +164,8 @@ workspace.upload_dataset( version = project.version("VERSION_NUMBER") -# upload model weights - yolov10 -version.deploy(model_type="yolov10", model_path=f”{HOME}/runs/detect/train/”, filename="weights.pt") +# upload model weights - YOLOv8 Nano +version.deploy(model_type="yolov8n", model_path=f"{HOME}/runs/detect/train/", filename="weights/best.pt") # run inference (a version may own several trained models; models() returns all of them) model = version.models()[0] diff --git a/docs/index.md b/docs/index.md index decfe174..49d3f7e1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -91,8 +91,8 @@ workspace.upload_dataset( version = project.version("VERSION_NUMBER") -# upload model weights - yolov10 -version.deploy(model_type="yolov10", model_path=f”{HOME}/runs/detect/train/”, filename="weights.pt") +# upload model weights - YOLOv8 Nano +version.deploy(model_type="yolov8n", model_path=f"{HOME}/runs/detect/train/", filename="weights/best.pt") # run inference (a version may own several trained models; models() returns all of them) model = version.models()[0] diff --git a/roboflow/cli/handlers/model.py b/roboflow/cli/handlers/model.py index 2f304fc7..0eb29f77 100644 --- a/roboflow/cli/handlers/model.py +++ b/roboflow/cli/handlers/model.py @@ -91,7 +91,7 @@ def model_infer( @model_app.command("upload") def upload_model( ctx: typer.Context, - model_type: Annotated[str, typer.Option("-t", "--type", help="Model type (e.g. yolov8, yolov5)")], + model_type: Annotated[str, typer.Option("-t", "--type", help="Model type (e.g. yolov8n, yolo26n, rfdetr-nano)")], model_path: Annotated[str, typer.Option("-m", "--model-path", help="Path to the trained model file")], project: Annotated[ Optional[list[str]], typer.Option("-p", "--project", help="Project ID (repeatable for multi-project deploy)") diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 48fc85be..19c19a4b 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -54,12 +54,7 @@ from roboflow.util.versions import get_wrong_dependencies_versions, normalize_yolo_model_type SUPPORTED_MODELS = ( - "yolov5", - "yolov7", - "yolov7-seg", "yolov8", - "yolov9", - "yolov10", "yolov11", "yolov12", "yolo26", @@ -116,12 +111,11 @@ DEFAULT_WEIGHTS_FILENAME = "weights/best.pt" # YOLO families Roboflow rejects without a size suffix (e.g. `yolov8` must be -# `yolov8n`/`yolov8s`/...). Legacy yolov5/7/9 go through the opt.yaml path and are -# intentionally excluded. -ULTRALYTICS_YOLO_FAMILIES = ("yolov8", "yolov10", "yolov11", "yolov12", "yolo26") +# `yolov8n`/`yolov8s`/...). +ULTRALYTICS_YOLO_FAMILIES = ("yolov8", "yolov11", "yolov12", "yolo26") # Canonical (depth_multiple, width_multiple) -> size letter for the classic YOLO -# scaling (v5/v8/v9/v10). Newer families (v11+) instead store an explicit ``scale`` +# scaling in v8. Newer families (v11+) instead store an explicit ``scale`` # letter in the model yaml, which is read first. YOLO_DEPTH_WIDTH_TO_SIZE = { (0.33, 0.25): "n", @@ -552,19 +546,6 @@ def _load_checkpoint(torch_module: Any, checkpoint_path: Path, *, map_location: return torch_module.load(checkpoint_path, **kwargs) -def _legacy_yolo_args(opts: dict[str, Any], opt_path: Path) -> dict[str, Any]: - """Return required legacy YOLO upload args from opt.yaml.""" - if "imgsz" in opts: - image_size = opts["imgsz"] - elif "img_size" in opts: - image_size = opts["img_size"] - else: - raise ModelPackagingError(f"{opt_path} is missing required key 'imgsz' or 'img_size'.") - if "batch_size" not in opts: - raise ModelPackagingError(f"{opt_path} is missing required key 'batch_size'.") - return {"imgsz": image_size, "batch": opts["batch_size"]} - - def _infer_yolo_size(model_instance: Any) -> str | None: """Infer a YOLO size letter (n/s/m/l/x/...) from a loaded checkpoint. @@ -703,8 +684,6 @@ def _process_yolo( allow_mismatch=allow_dependency_mismatch, warnings=warnings, ) - elif "yolov10" in model_type: - ultralytics = _import_required_module("ultralytics", "pip install ultralytics") elif "yolov11" in model_type: ultralytics = _import_required_module("ultralytics", "pip install 'ultralytics>=8.3.0'") _check_dependency_version( @@ -758,50 +737,33 @@ def _process_yolo( ) class_names = _class_names_from_model_instance(model_instance) - if any(name in model_type for name in ULTRALYTICS_YOLO_FAMILIES): - if ultralytics is None: - ultralytics = _import_required_module("ultralytics", "pip install ultralytics") - model_yaml = _require_model_attr(model_instance, "yaml", model_type) - if ( - "-cls" in model_type - or model_type.startswith("yolov10") - or model_type.startswith("yolov11") - or model_type.startswith("yolov12") - or model_type.startswith("yolo26") - ): - if not isinstance(model_yaml, dict) or "nc" not in model_yaml: - raise ModelPackagingError( - f"The {model_type} checkpoint's model config (model.yaml) is missing 'nc'; " - "it does not look like a complete Ultralytics training checkpoint." - ) - nc = model_yaml["nc"] - args = _require_checkpoint_field(checkpoint, "train_args", model_type) - else: - nc = _require_model_attr(model_instance, "nc", model_type) - args = _require_model_attr(model_instance, "args", model_type) - model_artifacts: dict[str, Any] = { - "names": class_names, - "yaml": model_yaml, - "nc": nc, - "args": _filtered_args(args), - "ultralytics_version": ultralytics.__version__, - "model_type": model_type, - } + if ultralytics is None: + ultralytics = _import_required_module("ultralytics", "pip install ultralytics") + model_yaml = _require_model_attr(model_instance, "yaml", model_type) + if ( + "-cls" in model_type + or model_type.startswith("yolov11") + or model_type.startswith("yolov12") + or model_type.startswith("yolo26") + ): + if not isinstance(model_yaml, dict) or "nc" not in model_yaml: + raise ModelPackagingError( + f"The {model_type} checkpoint's model config (model.yaml) is missing 'nc'; " + "it does not look like a complete Ultralytics training checkpoint." + ) + nc = model_yaml["nc"] + args = _require_checkpoint_field(checkpoint, "train_args", model_type) else: - # yolov5 / yolov7 / yolov9 read their upload args from opt.yaml - opt_path = model_path / "opt.yaml" - if not opt_path.exists(): - raise MissingFileError(f"You must provide an opt.yaml file at '{opt_path}' for {model_type} uploads.") - with opt_path.open() as stream: - opts = yaml.safe_load(stream) or {} - model_artifacts = { - "names": class_names, - "nc": _require_model_attr(model_instance, "nc", model_type), - "args": _legacy_yolo_args(opts, opt_path), - "model_type": model_type, - } - if hasattr(model_instance, "yaml"): - model_artifacts["yaml"] = model_instance.yaml + nc = _require_model_attr(model_instance, "nc", model_type) + args = _require_model_attr(model_instance, "args", model_type) + model_artifacts: dict[str, Any] = { + "names": class_names, + "yaml": model_yaml, + "nc": nc, + "args": _filtered_args(args), + "ultralytics_version": ultralytics.__version__, + "model_type": model_type, + } (build_dir / "model_artifacts.json").write_text(json.dumps(model_artifacts)) torch.save(model_instance.state_dict(), build_dir / "state_dict.pt") diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index da39b96e..4c05f0ca 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -25,7 +25,6 @@ _filtered_args, _infer_yolo_size, _is_ptl_checkpoint, - _legacy_yolo_args, _require_rfdetr, _resolve_rfdetr_variant, _resolve_yolo_size, @@ -58,7 +57,7 @@ def test_detect_defaults(self): def test_segment(self): self.assertEqual(task_of_model_type("yolov11-seg"), TASK_SEG) self.assertEqual(task_of_model_type("rfdetr-seg-medium"), TASK_SEG) - self.assertEqual(task_of_model_type("yolov7-seg"), TASK_SEG) + self.assertEqual(task_of_model_type("yolov8-seg"), TASK_SEG) def test_pose(self): self.assertEqual(task_of_model_type("yolov11-pose"), TASK_POSE) @@ -196,22 +195,6 @@ def test_unknown_project_type_is_ignored(self): validate_model_type_for_project("yolov8", "some-new-type", "widgets") -class LegacyYoloArgsTest(unittest.TestCase): - def test_reports_missing_batch_size(self): - with self.assertRaises(ModelPackagingError) as ctx: - _legacy_yolo_args({"imgsz": 640}, Path("opt.yaml")) - self.assertIn("batch_size", str(ctx.exception)) - - def test_reports_missing_image_size(self): - with self.assertRaises(ModelPackagingError) as ctx: - _legacy_yolo_args({"batch_size": 8}, Path("opt.yaml")) - self.assertIn("imgsz", str(ctx.exception)) - - def test_accepts_either_image_size_key(self): - self.assertEqual(_legacy_yolo_args({"imgsz": 640, "batch_size": 8}, Path("x")), {"imgsz": 640, "batch": 8}) - self.assertEqual(_legacy_yolo_args({"img_size": 416, "batch_size": 4}, Path("x")), {"imgsz": 416, "batch": 4}) - - class _FakeYoloModel: def __init__(self, yaml): self.yaml = yaml @@ -275,15 +258,15 @@ def test_keeps_matching_sized_type_without_warning(self): def test_bare_family_uninferable_raises_by_default(self): with self.assertRaises(SizeMismatchError): - _resolve_yolo_size("yolov10", _FakeYoloModel({}), []) + _resolve_yolo_size("yolov8", _FakeYoloModel({}), []) def test_bare_family_uninferable_proceeds_under_allow_mismatch(self): # The interactive retry sets allow_mismatch=True after the user confirms; # this branch must then converge (return) rather than raise forever. warnings: list = [] self.assertEqual( - _resolve_yolo_size("yolov10", _FakeYoloModel({}), warnings, allow_mismatch=True), - "yolov10", + _resolve_yolo_size("yolov8", _FakeYoloModel({}), warnings, allow_mismatch=True), + "yolov8", ) self.assertTrue(warnings and "bare family name" in warnings[0]) @@ -434,6 +417,18 @@ def test_missing_model_path_raises_missing_file(self): with self.assertRaises(MissingFileError): package_custom_weights("yolonas", "/nonexistent/path/for/test") + def test_retired_yolo_families_fail_before_loading_weights(self): + with tempfile.TemporaryDirectory() as tmp: + for family in ("5", "7", "9", "10"): + for model_type in (f"yolov{family}", f"yolov{family}n", f"yolo{family}s-seg"): + with self.subTest(model_type=model_type): + with mock.patch.object(model_processor, "_import_required_module") as import_module: + with mock.patch.object(model_processor, "_load_checkpoint") as load: + with self.assertRaises(UnsupportedModelError): + package_custom_weights(model_type, tmp) + import_module.assert_not_called() + load.assert_not_called() + def test_family_must_be_a_prefix_not_a_substring(self): # 'foo-yolov8n' merely contains a family token; the backend would # reject it after upload, so the gate must reject it up front. @@ -684,7 +679,7 @@ def state_dict(self): self.assertIn("yaml", str(ctx.exception)) def test_yolov11_missing_train_args_raises_packaging_error(self): - # yolov10/11/12/26 read args from checkpoint["train_args"]; a checkpoint + # yolov11/12/26 read args from checkpoint["train_args"]; a checkpoint # without it must fail with the ModelPackagingError contract. class DetectionModel: names = {0: "cat"} From 652e9a7a68e388561e27be01ecdbe676a1e96003 Mon Sep 17 00:00:00 2001 From: Kai Christensen <80430764+mkaic@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:18:21 -0600 Subject: [PATCH 2/2] Document retired YOLO weight uploads Co-authored-by: OpenAI Codex --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5427d878..8eb36bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Removed + +- Custom model weight uploads no longer support YOLOv5, YOLOv7, YOLOv9, or + YOLOv10. Existing models remain available for inference, and YOLO dataset + formats remain supported ([#572](https://github.com/roboflow/roboflow-python/pull/572)). + ## 1.7.3 ### Added