Add hosted auto-label support to the SDK and CLI - #526
Conversation
Expose the four public auto-label endpoints already used by the Roboflow MCP: list the foundation-model catalog, preview one image for free, start a job over a batch, and poll job progress. - rfapi: list_autolabel_models, preview_autolabel, start_autolabel_job, get_autolabel_job (pass-through, no client-side model whitelist) - Workspace.autolabel_models / autolabel_job - Project.autolabel / autolabel_preview / autolabel_job; Roboflow-trained models are sent as custom_roboflow with modelId in modelOptions - roboflow autolabel models | preview | start | job - util.autolabel_utils shared by SDK and CLI (image payload from URL, local file or base64; model_type resolution) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- JSON options accept @file references via the shared train parser - Auto-label adapters use their own response helper over a generic JSON-or-raise implementation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The public signature is `{"class name": "text prompt"}`, but the API's
object form means the opposite — `normalizeOntology` treats the key as
the prompt and the value as the class, which is the `CaptionOntology`
shape the labeling worker consumes. Passing the dict straight through
therefore inverted every non-identity ontology: `ontology={"cat": "a
cat"}` prompted the model with "cat" and wrote the annotations under the
class name "a cat". The `--class` path built an identity map, which is
symmetric, so the tests never caught it.
Serialize through `ontology_payload` instead. The list form names both
sides, so nothing has to be inferred from key order; the backend already
normalizes it on both the preview and the start path, and it is what the
web app sends. `Project.autolabel` also accepts a plain list of class
names now, matching `autolabel_preview`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mpts The class-keyed dict the previous commit settled on cannot express the case the API's ontology exists for: several prompts collapsing to one output class, e.g. "kitten" and "tabby" both labeled `cat`. A dict has room for one prompt per class because its keys are unique. So accept the explicit list on the way in too, alongside the dict and the plain list of class names. It is the same shape already used on the wire, so this is one shape fewer to think about rather than one more. Reject the mirror case while we are here: two classes claiming one prompt. The API keys its ontology by prompt, so it keeps whichever class came last and drops the other, and the job then never labels that class with nothing in the response to say why. `ontology_payload` names the collision instead, and the CLI reports it before any network call rather than letting the ValueError escape as a traceback. `--ontology` accepts a JSON array as well as an object; `_parse_json_flag` grew an opt-in `allow_list` for that and stays object-only everywhere else.
Settles the direction question the last two commits worked around. The
API's ontology is `{prompt: class name}`, and so is the CaptionOntology
the labeling worker consumes, so the SDK and CLI now take that shape
directly instead of translating a class-keyed one into it.
That direction is the useful one, not an accident of the API: prompts are
the unique side, so several of them can collapse onto one output class,
`{"kitten": "cat", "tabby": "cat"}`. A class-keyed object has room for one
prompt per class. The previous commit reached for a [{class, prompt}] list
to get that expressiveness back, which the prompt-keyed object gives for
free.
Dropping the translation drops what surrounded it: the wire form, the
entry-list shape, the guard against two classes claiming one prompt (a
duplicate prompt is now impossible, it is a dict key), and the `allow_list`
opt-in `_parse_json_flag` grew for the array. Net 87 lines lighter.
A plain list of class names still works and still means "prompt each class
with its own name".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed this end to end against the current backend ( The ontology direction was invertedThis is the one that mattered. The SDK, CLI and docs all specified
So Fixed in the three commits I pushed. Read the final state ( Why prompt-keyed rather than translatingIt reads backwards at first, but it's the useful direction rather than an accident of the API: prompts are the unique side, so several of them can collapse onto one output class. ontology={"kitten": "cat", "tabby": "cat", "puppy": "dog"}A class-keyed object has room for exactly one prompt per class, since its keys have to be unique — so it can't express the case the ontology exists for. Taking the API's shape directly also removes the translation step and with it this whole class of bug. A plain list of class names still works and still means "prompt each class with its own name". The tradeoff, and the reason I'm flagging it rather than just merging: roboflow/roboflow-mcp#166 carries the same fix for the MCP, which has the identical inversion live in production today. Worth fixing before mergeA mistyped An unreadable image file escapes as a raw traceback.
Exit code 1 where the CLI contract says 2. Smaller things (8)
Everything green after the three commits: 1026 tests, ruff clean, mypy no new errors, and CI is passing on all 15 checks. |
Five fixes from Iuri's review, in the order he listed them. A mistyped --image path was silently sent as base64. image_payload now expands ~, and anything that is neither a URL, an existing file nor valid base64 is rejected with "Image file not found" instead of reaching the API and failing there with a generic inference error. An unreadable image file escaped as a raw traceback, since the payload was built inside the operation _run wraps and _run only catches RoboflowError and ValueError. preview now builds the payload before resolving the project, so both the not-found and the OSError case print a structured error, and they fail before any network call. `autolabel job` could not find a job `autolabel start -p other-ws/proj` had just created, because start derived the workspace from the shorthand and job only read --workspace or the default. job now takes the same -p and resolves the workspace the same way. preserveExistingAnnotations was not exposed, and the server default (false) replaces annotations already on the batch images. Added `preserve_existing_annotations` to rfapi.start_autolabel_job and Project.autolabel, and `--preserve-existing` to the CLI. The handler re-implemented the credential resolvers and diverged: a missing default workspace exited 1 where the CLI contract says 2. It now uses resolve_ws_and_key, and the project variant is lifted from annotation.py into _resolver.py as resolve_project_context so there is one copy instead of three. Folding _models into _workspace_command fell out of the same change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Let's keep it prompt-keyed, same as the MCP. Thanks for catching the inversion. The five merge-blocking items are fixed in 3d4a4f3 (CI green). |
|
Re-reviewed
1038 tests (up from 1026), ruff clean, mypy no new errors. Lifting Three small things left over from this commit. None are blocking.
Unpadded base64 is now rejected, and data URIs get a misleading message.
Also, The seven other minor items from my first pass are untouched, which is fine — none of them block. The one I'd still like before this ships is Nothing here blocks a merge from my side. Ship it and take the leftovers in a follow-up if you'd rather. |
iurisilvio
left a comment
There was a problem hiding this comment.
Note on the preview docstring, expanded in my approval below.
| ): | ||
| """Preview one image with a foundation model before starting a job. | ||
|
|
||
| Calls ``POST /:workspace/:project/autolabel/preview``. Free: no job is |
There was a problem hiding this comment.
Not totally true, we charge for third-party APIs.
iurisilvio
left a comment
There was a problem hiding this comment.
Approving. The five merge-blocking items are fixed and verified, CI is green, and nothing outstanding is worth holding the PR for.
For the record, I pushed three commits to this branch myself (the ontology direction fix), so treat my approval as covering Lucas's work rather than my own.
On my inline comment about the "free" wording — it's right, and the claim is in four places, not one: rfapi.py:1367, project.py:1171, the preview command docstring in handlers/autolabel.py:42, and CLI-COMMANDS.md:257. gpt-6-astra-boxes and gemini-boxes are both planGated: true, and a preview of either runs real inference through workflowsAdapter.runPreview on the managed provider key, which is third-party spend Roboflow pays and passes through. What's actually true is narrower: no auto-label job is created and no auto-label credits are deducted. Worth rewording to that in all four spots rather than dropping the sentence, since "no job is created" is the part users need.
Everything else from my last two comments is non-blocking and we'll pick it up in a follow-up:
_is_base64still accepts an extensionless path whose length is a multiple of 4 (data/img,photos/a). A minimum length before the base64 check closes it.- Unpadded base64 is rejected, and a
data:...;base64,URI reports "Image file not found". Project.autolabel_previewneeds aRaises:block for the newValueError/OSError.annotation.py:_resolve_project_contextis now a one-line pass-through and can go.--confidencehas nomin/max, so--confidence 50burns credits for zero detections.- The rest of the minor list:
MODEL_TYPESunused and--model-typenot a typer enum,unavailableReasondropped from the models table,output_api_errorcalled with no CLI-shaped hint, the_autolabel_response/_annotation_administration_responsealiases, and_raise_for_trash_responseas a third copy of_json_response_or_raise.
Two that outlive this PR and shouldn't get lost in it:
roboflow-product-docs still needs the new command group, per the docs policy in CLAUDE.md, including the prompt-keyed ontology direction. CLI-COMMANDS.md is only the quickstart.
The same ontology inversion is live in production in the MCP right now and doesn't depend on this merging. roboflow/roboflow-mcp#166 fixes it and is ready for review.
Nice work on the fixes, especially lifting resolve_project_context into _resolver.py instead of just wiring up the existing helper.
Exposes the hosted Auto Label endpoints already used by the Roboflow MCP in the Python SDK and CLI, so scripts and agents using
roboflow-pythoncan list models, preview, start and track auto-label jobs.Workspace.autolabel_models()lists the foundation-model catalog with availability, guidance and credits per imageProject.autolabel_preview(model, image, ontology=...)runs a free single-image preview;imageaccepts an HTTPS URL, a local file or base64Project.autolabel(batch_id, model, model_type="foundational" | "roboflow", ...)starts a job over a batch and returnsjobId/annotationJobIdProject.autolabel_job(job_id)/Workspace.autolabel_job(job_id)poll per-subjob progressroboflow autolabel models | preview | start | job; ontology via repeated--classor--ontologyJSONmodel_type="roboflow"is sent ascustom_roboflowwith the id inmodelOptions.modelId, matching the MCPTesting
lucas-fochesatto/gemini-autolabel-test): models list, preview withsam3-rleandgpt-6-astra-boxes, jobs started withgpt-6-astra-boxesandgemini-boxesran todone🤖 Generated with Claude Code