NeMo Platform CLI Development
The CLI provides a command-line interface for NeMo Platform, implemented using Typer in packages/nemo_platform_ext/src/nemo_platform_ext/cli/.
Command Categories
| Category | Location | Description | Editable? |
|---|---|---|---|
| API | commands/api/ |
Auto-generated from SDK | ❌ Never edit |
| Configuration | commands/config.py, commands/configure.py |
Config management | ✅ Yes |
| Use-case | commands/use_cases/ |
High-level commands (e.g., chat) |
✅ Yes |
| Quickstart | commands/quickstart/ |
Local deployment | ✅ Yes |
Directory Structure
packages/nemo_platform_ext/src/nemo_platform_ext/cli/
├── app.py # Entry point, command registration, global options
├── core/ # Shared utilities
└── commands/
├── config.py # kubectl-style config management
├── configure.py # Interactive configuration
├── quickstart/ # Local deployment commands
├── use_cases/ # High-level commands (chat, wait)
└── api/ # ⚠️ AUTO-GENERATED - DO NOT EDIT
Building the CLI
Development Shortcut
During local CLI development, you can run _nemo to execute the CLI directly from packages/nemo_platform_ext.
This lets you test changes immediately without running vendoring first.
uv run _nemo --help
Use vendoring (make vendor-nemo-platform-ext) when you need to validate behavior in sdk/python/nemo-platform.
Build everything (recommended):
make update-cli
Individual steps:
# Step 1: Generate CLI commands from SDK
make generate-cli-commands
# Step 2: Vendor into SDK package
make vendor-nemo-platform-ext
# Step 3: Generate documentation
make generate-cli-reference-docs
Adding a Manual Command
Manual commands go in commands/use_cases/ or directly in commands/.
Pattern
from __future__ import annotations
from typing import Annotated
import typer
from nemo_platform_ext.cli.core.context import CLIContext
from nemo_platform_ext.cli.core.errors import handle_errors
@handle_errors
def my_command(
ctx: typer.Context,
name: Annotated[str, typer.Argument(help="The name argument")],
option: Annotated[str | None,
typer.Option(
"--option",
"-o",
help="An optional flag",
rich_help_panel="Options",
),
] = None,
) -> None:
"""
Short description of what the command does.
Examples:
nemo my-command foo
nemo my-command bar --option value
"""
state: CLIContext = ctx.obj
client = state.get_client()
# Use client to make API calls
result = client.some_resource.some_method(name=name)
# Output result (use state.output() for formatted output)
state.output(result)
Register in app.py
from nemo_platform_ext.cli.commands.my_module import my_command
# Single command
app.command(name="my-command", rich_help_panel="Use cases")(my_command)
# Command group (sub-app)
app.add_typer(my_sub_app, name="my-group", rich_help_panel="Category")
CLI Generator
The generator creates commands in commands/api/ from SDK introspection.
Key Files
tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/cli_generator/
├── cli_config.yaml # Generation configuration
├── generator.py # Main generation logic
├── sdk_introspector.py # SDK introspection
├── operation_classifier.py # Classifies methods (list, get, create, etc.)
├── templates/ # Jinja2 templates
├── context_collectors/ # Collect template context per operation type
└── overrides/ # Custom command implementations
Configuration (cli_config.yaml)
Configure table columns for list commands:
- resource: [customization, jobs]
methods:
list:
columns:
- name
- description
- status
- created_at
Skip a method:
- resource: [filesets]
methods:
upload_file:
skip: true
Use custom implementation:
- resource: [secrets]
methods:
create:
override: secrets/create.py
Command Overrides
For methods requiring custom handling (file uploads, streaming), create overrides:
Add config in
cli_config.yaml:- resource: [files] additional_methods: upload: override: files/upload.pyCreate override file in
overrides/files/upload.py:from typing import Annotated, Any, cast import typer from nemo_platform_ext.cli.core.context import CLIContext from nemo_platform_ext.cli.core.errors import handle_errors app = cast(Any, None) # override-skip: provided by generated file @app.command("upload") @handle_errors def upload_files( ctx: typer.Context, path: Annotated[str, typer.Argument(help="Local path to upload")], ) -> None: """Upload files to a fileset.""" state: CLIContext = ctx.obj client = state.get_client() # Custom implementation...
The # override-skip comment strips that line from output (useful for linter satisfaction).
Templates
Templates use Jinja2. Key variables available:
command_name- CLI command name (e.g., "list")method_name- SDK method name (e.g., "list")resource_path- Resource path (e.g., ["customization", "jobs"])params- List of parameter infodocstring- Method docstring
Core Utilities
CLIContext
Access via ctx.obj:
state: CLIContext = ctx.obj
client = state.get_client() # Get SDK client
state.output(data) # Format and print output
state.verbosity # 0 or 1
Error Handling
Always use @handle_errors decorator:
from nemo_platform_ext.cli.core.errors import handle_errors
@handle_errors
def my_command(ctx: typer.Context) -> None:
...
Building API Request Bodies
from nemo_platform_ext.cli.core.api import build_kwargs
body = build_kwargs(
model="my-model",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
temperature=0.7,
)
Testing
Run CLI tests:
uv run pytest packages/nemo_platform_ext/tests/cli/ -v
Run generator tests:
uv run --package nemo-platform-sdk-tools pytest tools/nemo-platform-sdk-tools/tests/sdk/cli_generator/ -v
Common Tasks
Add table columns for a new resource
Edit cli_config.yaml:
- resource: [my_service, my_resource]
methods:
list:
columns:
- name
- status
- created_at
Then rebuild: make update-cli
Add a new use-case command
- Create file in
commands/use_cases/my_command.py - Implement using pattern above
- Register in
app.py - Vendor:
make vendor-nemo-platform-ext
Modify how a command is generated
- Check which template is used (see
operation_classifier.py) - Edit template in
templates/ - Rebuild:
make update-cli
Override an auto-generated command
- Add override config in
cli_config.yaml - Create override file in
overrides/ - Rebuild:
make update-cli
Troubleshooting
Typer errors after CLI update
If tests fail with Typer errors after running make update-cli, the issue is usually one of:
1. Unsupported type in Typer
Typer doesn't support all Python types (e.g., complex unions, nested TypedDicts). Fix by updating the generator code to handle the type:
- Check
type_formatter.pyfor type formatting - Check
typed_dict_utils.pyfor TypedDict handling - Add type conversion or simplification in the relevant context collector
2. Method miscategorized
The operation classifier may assign the wrong template (e.g., "default" instead of "create"). Fix via cli_config.yaml:
- resource: [my_service]
methods:
my_method:
operation_type: create # Override the auto-detected type
Available operation types: list, get, create, update, delete, download, default
3. Skip the method temporarily
If the fix is non-trivial, skip the method to unblock your work:
- resource: [my_service]
methods:
problematic_method:
skip: true # TODO: Fix...
Create a GitLab issue to track the fix, then rebuild: make update-cli