Skip to content

Add unified ul CLI for Platform and YOLO - #59

Open
JaviChulvi wants to merge 11 commits into
ultralytics:mainfrom
JaviChulvi:feat/unified-ul-introspection
Open

Add unified ul CLI for Platform and YOLO#59
JaviChulvi wants to merge 11 commits into
ultralytics:mainfrom
JaviChulvi:feat/unified-ul-introspection

Conversation

@JaviChulvi

@JaviChulvi JaviChulvi commented Sep 7, 2026

Copy link
Copy Markdown

Why

Add ul as one entry point for Platform API commands and existing YOLO commands, starting with the resource commands in the unified CLI plan. Cloud commands work with the SDK alone; local commands and login/logout use the installed ultralytics package. Higher-level cloud workflows can follow separately.

Example: login → train → deploy → predict

From this PR's checkout, install into a Python 3.11+ environment. ultralytics supplies login/logout and local YOLO commands; ul cloud can also run with the SDK alone using ULTRALYTICS_API_KEY.

uv pip install -e ./sdk/python ultralytics

# Validate and save your key, inspect the account, and list your datasets.
ul login YOUR_API_KEY
ul cloud account summary
ul cloud datasets

# Create a private project and an untrained model record.
ul cloud projects create project=cli-demo name="CLI demo" visibility=private
ul cloud models create 'body={"project":"cli-demo","model":"detector","name":"Detector","task":"detect"}'

Copy the model creation response's id into MODEL_ID below. model_id identifies the destination model record; train_args.model selects the starting weights. This small example uses coco8.yaml; a Platform dataset can instead be passed as ul://YOUR_USERNAME/datasets/YOUR_DATASET.

Cloud training spends credits. These commands submit or inspect one operation and exit; they do not wait for the whole workflow to finish.

ul cloud training gpu-availability
ul cloud training start model_id=MODEL_ID gpu_type=rtx-4090 \
    'train_args={"model":"yolo26n.pt","data":"coco8.yaml","epochs":1,"imgsz":640,"batch":16}'

# Repeat this check until model.status is "completed" and model.hasWeights is true.
# If training fails or is cancelled, inspect the response before continuing.
ul cloud models retrieve project=cli-demo model=detector

After training completes, create the endpoint:

ul cloud deployments create project=cli-demo model=detector \
    deployment=cli-demo-api name="CLI demo API" region=europe-west1

# Repeat until deployment.status is "ready" before sending predictions.
ul cloud deployments retrieve deployment=cli-demo-api
ul cloud deployments health deployment=cli-demo-api

# Use an existing local image.jpg. The @ prefix opens the file for multipart upload.
ul cloud deployments predict deployment=cli-demo-api \
    'body={"file":"@image.jpg","conf":0.25}'

Path owner defaults to the logged-in username; pass owner=WORKSPACE explicitly when accessing another workspace. Nested request bodies use JSON, and argument names match the SDK's Python names. Use ul cloud <resource> <operation> --help for the full signature.

Example: YOLO compatibility

Local commands delegate to the existing ultralytics.cfg.entrypoint with the same arguments and YOLO executable identity. YOLO continues to own argument validation, defaults, execution, and settings.

Mode Existing YOLO command Equivalent unified command
Train yolo train model=yolo26n.pt data=coco8.yaml epochs=1 ul train model=yolo26n.pt data=coco8.yaml epochs=1
Validate yolo val model=yolo26n.pt data=coco8.yaml ul val model=yolo26n.pt data=coco8.yaml
Predict yolo predict model=yolo26n.pt source=image.jpg ul predict model=yolo26n.pt source=image.jpg
Export yolo export model=yolo26n.pt format=onnx ul export model=yolo26n.pt format=onnx
Track yolo track model=yolo26n.pt source=video.mp4 ul track model=yolo26n.pt source=video.mp4
Benchmark yolo benchmark model=yolo26n.pt data=coco8.yaml format=onnx ul benchmark model=yolo26n.pt data=coco8.yaml format=onnx

Task prefixes (detect, segment, semantic, depth, classify, pose, obb) and utilities (settings, checks, cfg, copy-cfg, solutions, login, logout) follow the same forwarding path. Private ul:// model and dataset inputs continue through YOLO's existing loaders. ul train runs training locally; ul cloud training start launches cloud training. Top-level ul help shows unified help, and ul version reports both SDK and YOLO versions.

Design

The CLI reads argument types and help from SDK signatures and docstrings, then calls the SDK to make requests. This keeps API definitions and transport in their existing owner and avoids maintaining a second command registry. YOLO continues to own local commands and credential writes.

Root cli.py is the maintained source. Its generated copy is committed because package builds and CI consume sdk/python. Only multipart file fields need a small generated mapping.

Validation: Python 3.11/3.14 tests, real loopback HTTP, a Git-subdirectory package install, Ruff, and regeneration checks pass locally.

Before merge: openapi#50 must land first; current CI fails because it regenerates from generator main. Login and launcher documentation still needs updating before release.

@UltralyticsAssistant UltralyticsAssistant added dependencies Dependency-related topics documentation Improvements or additions to documentation enhancement New feature or request labels Sep 7, 2026
@UltralyticsAssistant

Copy link
Copy Markdown
Member

👋 Hello @JaviChulvi, thank you for submitting a ultralytics/sdk 🚀 PR! This automated message confirms your contribution was received, and an Ultralytics engineer will assist with the review. To ensure a seamless integration of your work, please review the following checklist:

  • Define a Purpose: Clearly explain the purpose of your fix or feature in your PR description, and link to any relevant issues. Ensure your commit messages are clear, concise, and adhere to the project's conventions.
  • Synchronize with Source: Confirm your PR is synchronized with the ultralytics/sdk main branch. If it's behind, update it by clicking the 'Update branch' button or by running git pull and git merge main locally.
  • Ensure CI Checks Pass: Verify all Ultralytics Continuous Integration (CI) checks are passing. If any checks fail, please address the issues.
  • Update Documentation: Update the relevant documentation for any new or modified features.
  • Add Tests: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
  • Sign the CLA: Please ensure you have signed our Contributor License Agreement if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
  • Minimize Changes: Limit your changes to the minimum necessary for your bug fix or feature addition. "It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is." — Bruce Lee

For more guidance, please refer to our Contributing Guide. Don't hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review

Made with ❤️ by Ultralytics Actions

Reviewed the unified CLI runtime, generated copy, authentication provider, CI/configuration changes, and focused tests. The main command flow is coherent, but empty-value parsing can turn help into an unintended mutation and credential persistence needs atomic, restrictive file handling.

💬 Posted 2 inline comments
  • ⚠️ HIGH auth.py:51 This truncates the shared settings file in place and creates a new file with the process umask (commonly world-readable under a 022 umask). A fresh ul login can therefore expose the bearer API key, while interruption or concurrent YOLO settings updates can leave the entire settings file truncated or lose unrelated changes. Write a same-directory temporary file with restrictive permissions and atomically replace the target, then regenerate the package copy.
  • 💡 MEDIUM cli.py:78 An empty assignment consumes a following help token: ul cloud projects create project=p name= --help becomes name="--help", so help_requested stays false and the write operation is sent instead of displaying help. Exclude help, --help, and -h from this empty-value join; regenerate the installed copy afterward.

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 2

Made with ❤️ by Ultralytics Actions

Since the previous review, the empty-assignment help regression is fixed, and credential writes now use restrictive temporary files with atomic replacement. One race remains: concurrent settings writers can still lose unrelated or newer settings during the unsynchronized read-modify-replace sequence. No other concrete issues found.

💬 Posted 1 inline comment
  • 💡 MEDIUM auth.py:45 The temporary-file replacement is atomic, but this read-modify-write is still vulnerable to lost updates. If yolo or another ul login/logout updates settings.json after this read and before os.replace, this stale dictionary overwrites the newer credential or unrelated settings such as runs_dir. Serialize the read/merge/replace with a cross-process lock (or the shared settings writer's lock), then regenerate the packaged copy.

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 3

Made with ❤️ by Ultralytics Actions

Since the previous review, the empty-value/help regression remains fixed, and the credential-write race is resolved by removing the SDK settings writer. The current diff has one remaining behavioral issue: project owners are still implicitly inferred even though the CLI contract explicitly forbids that. Otherwise, no additional concrete regressions found.

💬 Posted 1 inline comment
  • 💡 MEDIUM cli.py:234 This infers owner for every positional owner operation, including projects. As a result, ul cloud projects and ul cloud projects project=p perform an account lookup and target the logged-in username, despite the documented contract that project owners are never inferred. Make owner inference policy-aware and do not exempt projects' required owner from the missing-argument check; regenerate the packaged copy.

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 4

Made with ❤️ by Ultralytics Actions

Since review 3, the credential-writer race and help-token regression remain addressed, and the owner-inference clarification is reflected in the current contract. I found one remaining bounded parsing defect: nested non-finite JSON values are accepted by the CLI and fail only later during request serialization. All other reviewed changes appear consistent with the stated design.

💬 Posted 1 inline comment
  • 📝 LOW cli.py:131 json.loads accepts non-standard NaN, Infinity, and -Infinity values inside objects and arrays. For example, body={"conf":NaN} passes the top-level type check and reaches the SDK as a non-finite nested value, causing invalid JSON or a later serialization failure instead of the documented local input error before any request. Reject these constants while parsing (or recursively validate finiteness), then regenerate the packaged copy.

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 5

Made with ❤️ by Ultralytics Actions

Since review 4, the nested non-finite-number parsing fix is present, and the earlier authentication race, help-token, and owner-inference issues remain addressed. The unified CLI and generated copy otherwise look consistent; one low-severity API error-message regression remains because standard detail responses are not displayed.

💬 Posted 1 inline comment
  • 📝 LOW cli.py:280 When the API returns the standard SDK error shape {"detail": "invalid"} (as covered by tests/test_python_sdk.py), body.get("error") is None, so the CLI prints only API request failed (HTTP 422). and drops the server message. Read the detail field as a fallback as well.

Comment thread cli.py
return 0
except APIError as error:
body = error.json
detail = body.get("error") if isinstance(body, dict) else None

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep the current implementation here. The Platform's error contract defines {"error": "..."}, which the CLI already displays.

The detail fixture tests whether the SDK preserves an arbitrary error response body; it doesn't establish the Platform's response format. The shared API validator and prediction routes also return error.

Unless we can identify an actual Platform endpoint returning detail, this would add a fallback without a demonstrated API case.

Comment thread cli.py
return 0
except APIError as error:
body = error.json
detail = body.get("error") if isinstance(body, dict) else None

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep the current implementation here. The Platform's error contract defines {"error": "..."}, which the CLI already displays.

The detail fixture tests whether the SDK preserves an arbitrary error response body; it doesn't establish the Platform's response format. The shared API validator and prediction routes also return error.

Unless we can identify an actual Platform endpoint returning detail, this would add a fallback without a demonstrated API case.

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 6

Made with ❤️ by Ultralytics Actions

Since review 5, the prior parsing and authentication findings remain addressed, and the API error-detail concern was settled by the author. The current diff otherwise looks consistent with the generated SDK design; one new regression remains: the documented no-argument ul login path crashes instead of prompting. പരിഗ

💬 Posted 1 inline comment
  • 💡 MEDIUM cli.py:274 The documented ul login invocation is delegated unchanged as ['login'], so no API key is supplied and this code does not prompt for one. The installed ultralytics handler accesses args[1], causing an uncaught IndexError instead of prompting or returning a controlled CLI error. Read the key here before delegation, or change the documentation to require ul login API_KEY.

@JaviChulvi JaviChulvi changed the title Add unified ul CLI using SDK introspection Add unified ul CLI for Platform and YOLO Sep 8, 2026

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review 7

Made with ❤️ by Ultralytics Actions

Reviewed all 12 changed files, including the unified CLI, generated package copy, packaging/CI configuration, documentation, and tests. The documented ul login API_KEY path is consistent with the current delegation, and no additional concrete security, correctness, compatibility, or performance issues remain. LGTM.

@JaviChulvi
JaviChulvi marked this pull request as ready for review September 8, 2026 00:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency-related topics documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants