Skip to content

Add v2 trainings + train-recipe support (describe, non-blocking start_training, CLI)#510

Draft
leeclemnet wants to merge 3 commits into
mainfrom
feat/train-recipe-v2-trainings
Draft

Add v2 trainings + train-recipe support (describe, non-blocking start_training, CLI)#510
leeclemnet wants to merge 3 commits into
mainfrom
feat/train-recipe-v2-trainings

Conversation

@leeclemnet

@leeclemnet leeclemnet commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds SDK + CLI support for training with a custom trainRecipe (hyperparameter/augmentation sweeps) against the v2 trainings API.

  • Adapters (roboflow/adapters/rfapi.py): get_train_recipe (GET .../v2/trainings/recipe?modelType=), create_training (POST .../v2/trainings, returns {"trainingId", ...}), list_version_trainings, get_version_training — same URL/auth/error conventions as the existing train adapters.
  • SDK (roboflow/core/version.py): Version.describe_train_recipe(model_type) (schema + ready-to-submit template), non-blocking Version.start_training(...) (builds a full recipe from the server template when hyperparameters/online_augmentation are given; mirrors train()'s generation-wait + export-ensuring; docstring includes a sweep-loop example), Version.list_trainings(), Version.get_training(). The blocking Version.train() is untouched.
  • Merge helper (roboflow/util/train_recipe.py): build_train_recipe — shared by SDK and CLI so both submit identical recipes; folds a top-level epochs into the recipe's hyperparameters unless explicitly set there (the server prefers the dense-filled recipe epochs over body epochs, so without the fold a caller's epochs= would be silently ignored).
  • CLI: roboflow train recipe -p <project> -v <N> -m <model_type> (prints schema+template JSON) and --hyperparameters '<json>' / --train-recipe '<json>' on roboflow train start (v2 path, prints the new trainingId; clean errors on invalid JSON / conflicting flags).

Related Issue(s): Companion to roboflow/roboflow#13699 (the backend describe endpoint — this PR's describe_train_recipe/--hyperparameters paths require it; deploy order: platform first, then release this). MCP companion: roboflow/roboflow-mcp#120.

Type of Change

  • New feature (non-breaking change that adds functionality)

Testing

  • I have tested this change locally
  • I have added/updated tests for this change

Test details:
+46 tests: responses-based adapter tests (URLs, query params, conditional camelCase body keys, error raise), Version tests with patched adapters (template merge, epochs fold incl. no-clobber and aug-only cases, export-ensuring), CLI tests via CliRunner + patched adapters (recipe command happy/error paths, --hyperparameters merge, --epochs fold, conflicting-flags error). Full suite: python -m unittest → 909 tests OK. ruff format --check, ruff check, mypy roboflow all clean. CLI smoke-tested via the installed entry point.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code where necessary, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors
  • I have updated the documentation accordingly (if applicable) — CLI-COMMANDS.md updated; docs.roboflow.com CLI reference is a tracked follow-up

Additional Context

Sweep usage the PR enables:

v = rf.workspace().project("safety-vests").version(5)
ids = [v.start_training(model_type="rfdetr-nano", hyperparameters={"lr": lr})["trainingId"]
       for lr in (1e-5, 1e-4, 1e-3)]

Recipe-based training requires the workspace's online-augmentation training entitlement (403 otherwise). Against a backend without the describe endpoint, describe_train_recipe/knob kwargs fail with the upstream 404; passing a full train_recipe works today.

🤖 Generated with Claude Code

leeclemnet and others added 3 commits July 17, 2026 18:23
Wraps the new v2 trainings API surface:

  GET  /{ws}/{proj}/{ver}/v2/trainings/recipe?modelType=...
  POST /{ws}/{proj}/{ver}/v2/trainings
  GET  /{ws}/{proj}/{ver}/v2/trainings
  GET  /{ws}/{proj}/{ver}/v2/trainings/get?trainingId=...

Adapters (roboflow/adapters/rfapi.py): get_train_recipe,
create_training (camelCase modelType/trainRecipe, only non-None body
keys), list_version_trainings, get_version_training — same URL/error
conventions as start_version_training.

SDK (roboflow/core/version.py): Version.describe_train_recipe,
Version.start_training (non-blocking; builds a full trainRecipe from
the server template when hyperparameters/online_augmentation are
given — the backend dense-fills defaults server-side; mirrors
train()'s generation-wait + export-ensuring when model_type is set),
Version.list_trainings, Version.get_training. train() unchanged.

Shared merge logic lives in roboflow/util/train_recipe.py
(build_train_recipe) so the CLI and SDK submit identical recipes;
online_augmentation defaults splits to ["train"].

CLI (roboflow/cli/handlers/train.py):
  roboflow train recipe -p <project> -v <N> -m <model_type>
  roboflow train start ... --hyperparameters '<json>'
  roboflow train start ... --train-recipe '<json>'
Recipe flags route through the v2 create_training path and print the
new trainingId; invalid JSON and recipe-vs-hyperparameters conflicts
fail with structured errors, not tracebacks.

Tests: +34 (9 rfapi via responses, 13 Version via patched rfapi,
12 CLI via CliRunner + patched adapters). Full suite 897 passing;
ruff format/check and mypy clean. CLI-COMMANDS.md documents the new
command and flags (roboflow-product-docs follow-up out of scope).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server behavior: normalizeRecipeHyperparameters dense-fills a submitted
recipe's hyperparameters (adding a manifest-default epochs, e.g. 100 for
rf-detr) and trainJobConfig resolves
trainRecipe.hyperparameters.epochs ?? body.epochs, so any recipe built
from the template silently swallowed the top-level epochs argument:
start_training(model_type=..., epochs=50, hyperparameters={"lr": 2e-4})
trained 100 epochs, not 50 — likewise CLI --epochs + --hyperparameters,
and even epochs + online_augmentation with no hyperparameters at all.

Client-side fix (POST path unchanged): build_train_recipe gains an
epochs kwarg that folds a non-None value into the recipe's
hyperparameters unless the caller already set "epochs" there; threaded
from both template-building call sites (Version.start_training override
path, CLI --hyperparameters path). Override dicts are copied so caller
input is never mutated.

Explicit --train-recipe / train_recipe= stays authoritative; the
start_training docstring and the --train-recipe flag help now state
that a recipe's (dense-filled) epochs takes precedence over the
top-level epochs argument.

Tests: +12 (new tests/util/test_train_recipe.py covering fold /
no-clobber / no-mutation; 3 Version cases incl. augmentation-only +
epochs and body epochs still sent; 1 CLI case asserting the merged
create_training body). Full suite 909 passing; ruff + mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…explicit recipes

Addresses two Jarbas review warnings on #510 plus trivia:

Non-object JSON flag values (e.g. --hyperparameters '5',
--train-recipe '[1]') previously passed the syntax-only parse and
either crashed with a raw TypeError in build_train_recipe (after a
wasted recipe fetch) or shipped a malformed trainRecipe to the server.
_parse_json_flag now requires a JSON object and exits with a
structured error before any network call.

epochs was still silently ignored when combined with an explicit
train_recipe: the server dense-fills recipe hyperparameters, so recipe
epochs always beats body epochs, and a template-derived recipe +
--epochs 5 smoke run would train the full default epochs at GPU cost.
Explicit recipes are now run through the same build_train_recipe fold
(setdefault semantics: an epochs set inside the recipe still wins;
the fold creates the hyperparameters key when a hand-written recipe
omits it). Docstrings and --train-recipe help updated from
"recipe epochs takes precedence" to the fold semantics.

Trivia: sweep docstring example now imports time (it calls
time.sleep); CHANGELOG.md gains the Unreleased entry for the v2
trainings SDK methods, CLI command, and flags (#510).

Tests: +7 (util fold-creates-missing-key; Version explicit-recipe
fold / recipe-epochs-wins / missing-hyperparameters-key; CLI
non-object --hyperparameters and --train-recipe assert structured
error with no adapter call, --train-recipe + --epochs asserts merged
body). Full suite 916 passing; ruff + mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant