Skip to content

refactor(cli)!: drop the generated Stainless command tree and the CLI generator - #2004

Draft
maxdubrinsky wants to merge 25 commits into
mainfrom
cli-drop-stainless/mdubrinsky
Draft

refactor(cli)!: drop the generated Stainless command tree and the CLI generator#2004
maxdubrinsky wants to merge 25 commits into
mainfrom
cli-drop-stainless/mdubrinsky

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Final PR of the series: CLIContext.get_client() returns a typed NemoClient, the generated cli/commands/api/* tree and the CLI generator (tools/nemo-platform-sdk-tools/.../cli_generator, make generate-cli-commands, make update-cli) are deleted, and the boundary test asserts that every CLI group, plugin discovery, seed jobs and plugin CLIs import with nemo_platform made un-importable. After this the nemo CLI has no dependency on the Stainless SDK.

Depends on the middle PRs of the stack (core groups 1 and 2, plugin-hosted groups, #2002 plugin runtime typing, #2003 setup); this branch merges them, so its diff shrinks to the flip as they land. Marked ! because nemo_platform_ext.cli.core.context.CLIContext.get_client changes type and the -f code output for every command now emits typed-client Python instead of NeMoPlatform calls.

Changes

  • cli/core/{context,errors,pagination,waiters,code_generator}.py, app.py, version: Stainless branches removed; errors maps plugin ApiError/ValidationError/UnknownInputFieldsError only.
  • cli/commands/api/ and the generator tree deleted; Makefile, tools/lint/lint-fix.sh, tools/mcp-dev-tools/nmp_dev_mcp.py, sdk-tools cli.py lose the generate targets; legacy_code_generator.py removed.
  • nemo_platform_plugin: sdk.py and entity_client.py resolve their Stainless-dependent re-exports lazily via module __getattr__, and the NDJSON wire constants move to functions/frames.py, so plugin discovery no longer imports the generated SDK transitively (refactor(sdk): route platform clients through owned endpoints #1920 added module-level SDK imports to dependencies.py and functions/routes.py, which are left as they are).
  • tests/cli/test_stainless_boundary.py: no allow-list; probes the root app, every group's --help and plugin discovery with a MetaPathFinder that blocks nemo_platform*.
  • Docs: .agents/skills/nmp-cli/SKILL.md, packages/nemo_platform_ext/{AGENTS.md,README.md,docs/overview.md}, nemo_platform_plugin/client/MIGRATION.md rewritten for hand-written groups and nemo.cli entry points.

Stack

Part of the AIRCORE-893 series that moves the nemo CLI off the generated Stainless SDK onto the typed clients in nemo_platform_plugin (blocking AIRCORE-827). Stacked on #1986 (plumbing) but targeted at main so CI runs (the CI workflow only triggers for PRs against main/release/*). Until #1986 merges, the diff includes its commits; this PR's own change is the merge commits of #1999, #2000, #2001, #2002, #2003 plus the two commits refactor(cli)!: drop the generated Stainless command tree and the CLI generator and fix(plugin): keep the generated SDK off the plugin discovery import path. Review that commit, or compare against cli-typed-client-plumbing/mdubrinsky.

  1. feat(cli): typed-client plumbing for the Stainless-free CLI #1986 plumbing (typed-client seam, bootstrap, pagination and request-body helpers)
  2. In parallel on top of it: refactor(cli): rewrite workspaces, projects and iam on typed clients #1999 core groups 1 (secrets, workspaces, projects, iam), refactor(cli): rewrite files, models, adapters and inference on typed clients #2000 core groups 2 (files, models, adapters, inference), refactor(cli): host guardrail, intake and experiments in their owning packages #2001 plugin-hosted groups (guardrail, intake, experiments), refactor(plugins): type platform handles as PlatformClient across the plugin runtime #2002 plugin runtime typing, refactor(cli): run nemo setup on typed clients #2003 setup
  3. refactor(cli)!: drop the generated Stainless command tree and the CLI generator #2004 final flip: drop get_client()'s Stainless path, delete cli/commands/api/* and the CLI generator

Status: draft. Being maintained by an agent on behalf of @mdubrinsky; it re-bases on #1986 as that moves and on @ironcommit's landing plugin migrations (#1971, #1973), and picks up CI and review feedback. It will be re-cut where those PRs overlap (noted per PR below).

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with documentation updates
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification:

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation (on the current head, based on #1986 at 377fb4d0a):

  • make test-unit (whole repo): 18325 passed, 59 skipped, 2 xfailed; 13 failures are the known host port conflicts (test_services.py, nemo-deployments/test_ports.py, nemo-agents/test_port_allocation.py) and pass in CI.
  • bash tools/lint/lint-python-types.sh: passes.
  • make generate-cli-reference-docs: no diff beyond the three plugin-hosted help lines from the plugin-hosted groups PR.
  • NMP_CONFIG_FILE=/tmp/nope.yaml uv run _nemo --help and every group --help with nemo_platform blocked at import: pass (the boundary test).
  • uv run ruff check and uv run ruff format --check: clean.
  • uv run pre-commit run -a: all hooks pass except uv-lock (host uv 0.9.30 vs pinned 0.9.14); uvx --from uv==0.9.14 uv lock --check passes. helm-docs and yq are not installed on the host; both hooks are CI-covered.
  • NMP_CONFIG_FILE=/tmp/nope.yaml uv run _nemo <group> --help diffed against main for every rewritten group: identical, except a trailing period on group help lines.

Adds what hand-written CLI commands need from nemo_platform_plugin:

- InferenceGatewayClient (provider/model/openai proxy routes, provider_ready,
  raw SSE streams, OpenAI model listing) with endpoint and wire tests.
- client_from_platform accepts a NemoClient or AsyncNemoClient and derives
  the typed client with from_client, so callers no longer need to know which
  platform handle they hold; PlatformClient is the runtime-checkable
  structural type for that parameter and platform_default_headers reads
  identity headers off either shape.
- models.refs holds the pure model-reference helpers; packages/models
  re-exports them so the CLI does not import the Stainless-bound package.
- filesets.transfer holds SDK-free upload/download/list/delete; FilesResource
  delegates to it and filesets no longer imports .resources eagerly.
- Bearer tokens are resolved on every HTTP attempt rather than baked into the
  PreparedRequest, so retries and later pages never replay a stale token.
- A 409 on a create sent with exist_ok is no longer retried before send()
  resolves it by fetching the existing entity.
- Query-param TypedDicts and request models for files, iam, virtual models
  and workspaces gain the fields the CLI exposes; GuardrailConfig.data is
  optional to match the server entity.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…ands

Additive plumbing so command groups can be rewritten on the typed clients
one PR at a time while the generated commands keep working:

- client/bootstrap.py resolves config, OIDC discovery and token providers
  without the generated SDK and builds NemoClient/AsyncNemoClient with the
  same retry policy, TLS verification and 5 s connect cap the SDK used;
  factory.py layers the NeMoPlatform constructors on top of it.
- CLIContext.typed_client(XClient) / async_typed_client derive a service
  client that shares the platform client's transport and auth; get_workspace
  exposes the configured default.
- pagination gains collect_offset_pages / collect_cursor_pages for typed
  paginated responses, carrying the server's envelope fields (sort, filter,
  grouped_by) into JSON output; fetch_all_pages stays for generated commands.
- errors maps typed-client and pydantic errors alongside the SDK ones
  (validation and unknown --input-data keys exit 2), and recognises every
  unresolved-workspace message.
- stdin_utils.build_request_body validates --input-data into a request
  model and rejects unknown keys instead of dropping them.
- code_generator renders typed-client snippets (request models as
  constructor calls, SecretStr masked, RootModel positional); generated
  commands are routed to legacy_code_generator until they are replaced.
- waiters accept either platform handle, poll with a monotonic clock and
  normalise str-enum statuses; formatters unwrap NemoResponse; the version
  flag reads distribution metadata; command groups no longer advertise
  shell completion (the root app owns it).

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…p calls it

The new test endpoint stubs raise NotImplementedError like their siblings
instead of an ellipsis body, which ty rejects as an implicit None return.
The auth-idp CLI refresh contract test patches discover_nmp_config on
client.bootstrap, where the CLI now resolves OIDC settings.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
AllPagesResponse and OffsetPageResponse emit the server envelope fields,
but callers that build them without an envelope (fetch_all_pages behind
the generated list commands, and nemo jobs list --all-pages) lost the
sort key that list output has always carried. sort is now always present,
null when the server did not echo one, and a real envelope keeps its own
field order.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
- Narrow the platform handle from object to PlatformClient in
  client_from_platform and platform_default_headers (adapter.py) and the
  nemo.sdk resource-factory owner in NemoPluginSDKResources (sdk.py), so
  the typed-client boundary stops erasing the type where plugins are built.
- build_request_body reports each field's accepted input alias in the
  unknown-input hint (schema, not the internal schema_) so the advertised
  key is one pydantic actually accepts.
- with_options now clears a clone's cached nemo.sdk plugin resources,
  which were built against the original client's transport, so typed
  clients accessed on the clone bind to its headers/retry/timeout.
- Drop the always-false _PLATFORM_JOB_LIFECYCLE watch exclusions from the
  legacy code generator's lifecycle helpers.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Brings in the auth-off-Stainless SDK removal, customization typed-client
migration, and sandbox forward merges. Resolves conflicts in adapter.py and
client.py by combining both sides: keep main's _owns_http lifecycle
(owns_http_client=False) and _platform_default_headers header resolution,
and keep the PlatformClient narrowing plus the with_options cached-resource
clearing added for review.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
The main merge kept the typed resource factories on PlatformClient but took
main's __all__ which no longer re-exports NeMoPlatform/AsyncNeMoPlatform, so
the import became dead and tripped the ruff pre-commit hook (lint-python-style).

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Plugins (nemo-evaluator among others) import the generated NeMoPlatform /
AsyncNeMoPlatform classes from nemo_platform_plugin.sdk. My earlier drop of
that import broke them with an ImportError. Re-export lazily through a module
__getattr__ so plugin discovery still imports this module without requiring
the generated SDK at load time.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Brings in guardrails typed-client (#1966) and other main changes. Resolved
guardrail/types.py to main's landed GuardrailConfig.data shape (RailsConfig,
typed) per the layered-merge rule, and took main's add/add
tests/guardrail/test_endpoints.py. Also fixes the sdk.py re-export regression
in the same push.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky
maxdubrinsky force-pushed the cli-drop-stainless/mdubrinsky branch from 0273a21 to 884b00b Compare September 11, 2026 18:48
@maxdubrinsky
maxdubrinsky changed the base branch from cli-typed-client-plumbing/mdubrinsky to main September 11, 2026 18:50
@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 46877/58577 80.0% 63.9%
Integration Tests 29726/55847 53.2% 24.6%

…bing-merge

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>

# Conflicts:
#	packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/errors.py
…bing-merge

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…bing-merge

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…bing-merge

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>

# Conflicts:
#	packages/nemo_platform_plugin/tests/client/test_adapter.py
@maxdubrinsky
maxdubrinsky force-pushed the cli-drop-stainless/mdubrinsky branch from 884b00b to e44bfc5 Compare September 11, 2026 21:15
…bing-merge

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
… clients

The four groups (including the inference sub-groups: providers, models,
prompts, deployments, deployment-configs, virtual-models, gateway) become
hand-written modules on FilesClient, ModelsClient and
InferenceGatewayClient via state.typed_client(), replacing their generated
Stainless commands and the generator overrides that patched them. Flags,
defaults, choices, table columns and help text are unchanged; the generator
skips these resources.

Fileset upload/download/list/delete go through filesets.transfer. files list
-f code renders the same query the command sends and refuses a glob
--remote-path it cannot express; nemo files otlp logs create sends real
payloads (it previously sent an empty body) labelled application/json.
--wait on deployments create polls through ModelsClient and the gateway
readiness endpoint with a monotonic clock.

Wire-level tests cover every command over a recorded transport; integration
tests run against the in-process Files and Models services.

`files filesets refresh` (added to the generated tree by #1957) is carried
over: FilesClient.refresh_fileset posts to the /refresh route and the command
renders the returned fileset, with the same help text and position.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Hand-written Typer groups for workspaces (with members), projects and iam
on the typed clients from the plumbing PR, replacing their generated
Stainless commands. The CLI surface, output shapes and exit codes are
unchanged: list commands go through collect_offset_pages so --all-pages and
the {data, sort, pagination} envelope match the generated output, and
--input-data bodies go through build_request_body, which rejects unknown
fields with exit code 2 and the accepted field list.

The generated workspaces and projects modules are removed and the generator
config marks them skip: true so make generate-cli-commands cannot bring them
back. The generated iam group was already removed by the core auth port
(#1968); this adds the hand-written hidden group back on IAMClient.

Secrets is not part of this PR: #1971 landed its own port of that group.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
… packages

The three functional groups move out of the generated command tree and
become nemo.cli entry points on the packages that own the services:
guardrail in nemo-guardrails-plugin (GuardrailCLI), intake and experiments
in nmp-intake (IntakeCLI, ExperimentsCLI). They appear only when that
package is installed, the same way every other plugin group does, and are
written on GuardrailClient and IntakeClient via state.typed_client().
Command names, flags, defaults and columns are unchanged; the group help
line becomes the standard "Plugin commands for <name>." row.

The intake typed client gains the read and experiment endpoints the CLI
needs. The nmp-intake bundle inherits nemo.* entry points so the
nemo-platform wheel actually carries the groups (the generated entry-point
table is regenerated with make vendor), and a new test asserts every
bundled package's nemo.* entry points are exposed by the wrapper or SDK
pyproject so a missing inherit fails in CI rather than in the shipped
wheel. The generator skips the three resources.

GuardrailConfig.data is Optional again after #1966 typed it as a bare
RailsConfig: the guardrails entity stores it as Optional and the API returns
"data": null for configs created without a body, which the CLI integration
tests here create. The middleware already handled None.

Rebased over #1965, which added its own experiment endpoints and read
models: main's ExperimentCreateRequest/UpdateRequest/Response (dict-typed
pareto and column_layout) and RetrieveTraceQueryParams are used; the
endpoint stubs keep the create exist_ok / list / delete variants the CLI
needs, and span groups go back through collect_offset_pages now that
list_span_groups returns Paginated[SpanGroup] on main.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
… plugin runtime

Plugin discovery, the scheduler, seed jobs, job result plumbing, the job
API factory and the plugins that build on them (nemo-agents,
nemo-optimization, nemo-agent-hardener, anonymizer, auditor, evaluator,
safe-synthesizer, hello-world) annotated the platform handle they receive
as the generated NeMoPlatform / AsyncNeMoPlatform classes even though they
only forward it to client_from_platform. They now use the structural
PlatformClient type, so the plugin runtime and nemo-agents import without
the generated SDK installed. nemo_platform_plugin.sdk keeps re-exporting
the generated classes for plugins that still import them from there.

jobs.watch classifies transient failures by shape (transport error or
retryable HTTP status) instead of by SDK exception class.

chat_tui decodes SSE itself and consumes typed BinaryContent streams; the
generated SDK's streaming context manager is adapted until nemo chat moves
over. The agent-hardener CLI builds a NemoClient directly.

Rebased over #1973, which reworked nemo-agents' call sites: main's versions
of those files win. This PR keeps the three nemo-agents modules #1973 left
on the generated SDK (optimize_cli, usage/cli, usage/sources/fileset) plus
the other plugins. nemo-optimization resolves platform agents through
AgentsClient instead of the generated sdk.agents surface, since that
attribute does not exist on the typed client; the test serves the agent
over a MockTransport instead of stubbing it.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
setup builds a frozen SetupClients bundle (ModelsClient, SecretsClient,
InferenceGatewayClient) from the CLI context instead of walking the
generated SDK's resource tree, so the wizard no longer depends on
nemo_platform for provider, secret and gateway readiness calls. Behaviour is
unchanged: the same probes, prompts, telemetry events and error paths.

Adds wire-level tests for the setup calls over a recorded transport and
retargets the onboarding telemetry tests at the typed bundle.

The Kind e2e for auto model selection drove _run_auto_mode with a raw
NeMoPlatform handle; it now builds SetupClients from the same handle with
client_from_platform, as the rest of the e2e suite already does.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…dubrinsky

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>

# Conflicts:
#	packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/__init__.py
#	packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/manifest_registry.py
#	packages/nemo_platform_ext/tests/cli/integration/conftest.py
#	tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/cli_generator/cli_config.yaml
…nless/mdubrinsky

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>

# Conflicts:
#	packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/__init__.py
#	packages/nemo_platform_ext/tests/cli/test_app.py
#	tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/cli_generator/cli_config.yaml
…nless/mdubrinsky

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…less/mdubrinsky

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
… generator

With every group hand-written on typed clients, the CLI no longer needs the
generated nemo_platform SDK:

- CLIContext.get_client() returns a NemoClient built by client.bootstrap
  (OAuth refresh, workload identity, API key and direct modes preserved);
  typed_client() derives service clients from it.
- nemo chat, nemo wait, the agent context command and quickstart use typed
  clients; chat_tui consumes typed SSE streams only.
- The generated cli/commands/api tree, its top-level entry table, the
  legacy code-generation and pagination paths that served it, and the
  Jinja CLI generator in nemo-platform-sdk-tools (with the
  generate-cli-commands make target and lint-fix step) are removed.
  A plugin nemo.cli entry point can still replace a same-named built-in
  group in the Core or Functional panels.
- tests/cli/test_stainless_boundary.py enforces the boundary: a static
  scan of the CLI package plus a subprocess that makes nemo_platform
  un-importable and runs every core group, plugin discovery, -f code
  invocations and the plugin-hosted groups.
- The nmp-cli skill and package docs describe the hand-written model.

Based on the jobs port in #1922, which the rewritten jobs group relies on.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
route adapter import the generated SDK at module level. Two client-side
modules reached them transitively: entity_client re-exported
get_entity_client eagerly (pulling in dependencies), and the plugin CLI
command builder imported two wire constants from functions.routes.

get_entity_client is now resolved lazily through a module __getattr__, and
the NDJSON media type and default function path live in functions.frames
(shared by adapter and CLI) with routes re-exporting them. Plugin discovery,
seed jobs and plugin CLIs import cleanly with nemo_platform un-importable,
which the boundary test asserts with no remaining allow-list. The
generated CLI generator tree is removed with the command tree.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky
maxdubrinsky force-pushed the cli-drop-stainless/mdubrinsky branch from e44bfc5 to 193c740 Compare September 11, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking breaking change (!-marked title) refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant