diff --git a/.github/actions/build-nemo-platform-wheel/action.yaml b/.github/actions/build-nemo-platform-wheel/action.yaml
index 4fb5ef1132..3d18d75975 100644
--- a/.github/actions/build-nemo-platform-wheel/action.yaml
+++ b/.github/actions/build-nemo-platform-wheel/action.yaml
@@ -43,9 +43,9 @@ inputs:
required: false
default: "19700101000000"
python-version:
- description: Python version for setup-uv.
+ description: Python version for setup-uv. Must be within the wheel's supported range.
required: false
- default: "3.11"
+ default: "3.12"
source-root:
description: >
Path to the checked-out repository. release-bundle.yaml uses `source`;
diff --git a/README.md b/README.md
index 710251ebaf..41a0286104 100644
--- a/README.md
+++ b/README.md
@@ -20,13 +20,13 @@ Quick install from PyPI:
```bash
curl -LsSf https://astral.sh/uv/0.9.30/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
-uv venv --python 3.13
-source .venv/bin/activate
-uv pip install nemo-platform
+uv tool install "nemo-platform[all]"
nemo setup
```
+`uv tool install` gives you a global `nemo` command in its own isolated environment, with nothing to activate. The `all` extra adds the platform services, so `nemo services run` works; without it you get the SDK and CLI only. To import the SDK from your own code, `pip install "nemo-platform[all]"` into a virtual environment instead.
+
Source checkout for development:
```bash
diff --git a/SETUP.md b/SETUP.md
index 7610a8ca69..57b2a85d9d 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -86,7 +86,9 @@ is unavailable, start Docker and retry—do not proceed to `rm -rf`.
## Bootstrap and start
-The README documents the streamlined path. Prefer this over the manual steps below whenever the task fits — it covers prerequisites install, service startup, provider registration, default/fast model selection, and demo agent deployment in one shot:
+This section is the **source checkout** path: use it to work on NeMo Platform itself, on a local plugin, or on Studio assets. To only *use* the platform, install the published wheel instead — `uv tool install "nemo-platform[all]"` needs no checkout and no toolchain, then continue at `nemo setup`.
+
+The steps below cover prerequisites install, service startup, provider registration, default/fast model selection, and demo agent deployment in one shot. Prefer them over the manual sections further down whenever the task fits:
Before running `make bootstrap`, install Flox from the [Flox installation guide](https://flox.dev/docs/install-flox/install). Flox is the recommended source-development toolchain and does not need to be activated first. Contributors using a preinstalled host toolchain instead need uv `>=0.9.14`, Node.js `22.23.2`, pnpm `10.34.5`, and a C compiler; they must use `make TOOLCHAIN=system bootstrap`.
diff --git a/docs/cli/index.mdx b/docs/cli/index.mdx
index 6cc08b25df..571077a4fc 100644
--- a/docs/cli/index.mdx
+++ b/docs/cli/index.mdx
@@ -28,25 +28,33 @@ If you previously installed the `nemo-microservices` package, uninstall it first
pip uninstall nemo-microservices
```
-### Install in a Virtual Environment
+### Install
-Install the PyPI `nemo-platform` wrapper package in a Python 3.12-3.13 virtual environment. If you are working from a source checkout, run `make bootstrap` from the repository root instead; the generated SDK package under `sdk/python/nemo-platform/pyproject.toml` does not define the wrapper extras.
+Install the PyPI `nemo-platform` wrapper package. From a source checkout, run `make bootstrap` from the repository root instead; the generated SDK package under `sdk/python/nemo-platform/pyproject.toml` does not define the wrapper extras.
+
+
+
+
+
+One command, `nemo` available everywhere, nothing to activate.
```bash
-pip install "nemo-platform[all]"
+uv tool install "nemo-platform[all]"
```
-Or with uv:
+
+
+
+
+Use when you also import the SDK from your own code. `nemo` works only while the environment is active.
```bash
-uv pip install "nemo-platform[all]"
+pip install "nemo-platform[all]"
```
-
-
-When installed in a virtual environment, the `nemo` command is only available when the environment is activated.
+
-
+
### Verify Installation
diff --git a/docs/cli/troubleshooting.mdx b/docs/cli/troubleshooting.mdx
index 9c5550e029..ef04649ff6 100644
--- a/docs/cli/troubleshooting.mdx
+++ b/docs/cli/troubleshooting.mdx
@@ -23,6 +23,25 @@ Activate the virtual environment where you installed the package:
source /path/to/venv/bin/activate
```
+## Installation Issues
+
+### Install Fails Building a Dependency
+
+**Symptoms:**
+- `uv tool install "nemo-platform[all]"` fails while building a dependency, often on a missing C header such as `omp.h`
+
+**Cause:**
+
+NeMo Platform supports Python 3.12 and 3.13, but uv [picks the interpreter itself](https://docs.astral.sh/uv/concepts/tools/#python-versions), so an install can land on Python 3.14.
+
+**Solution:**
+
+Name a supported interpreter. uv downloads one if you do not have it:
+
+```bash
+uv tool install --python 3.13 "nemo-platform[all]"
+```
+
## Connection Issues
### Unable to Connect to Server
diff --git a/docs/get-started/setup.mdx b/docs/get-started/setup.mdx
index d4d5db0d9d..c379e6a942 100644
--- a/docs/get-started/setup.mdx
+++ b/docs/get-started/setup.mdx
@@ -12,7 +12,7 @@ Install NeMo Platform and start working with AI agents.
- Python 3.12-3.13
- An API key from one of: [NVIDIA Build](https://build.nvidia.com), [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/settings/keys), [Google Gemini](https://aistudio.google.com/apikey), or a local [Ollama](https://ollama.com) instance (no key needed)
- 16 GB available disk space and 8 GB RAM
-- For the PyPI install: `pip` in a Python environment
+- For the PyPI install: [uv](https://docs.astral.sh/uv/) `>=0.9.14`, or `pip` in a Python environment
- For the source checkout install: [Git](https://git-scm.com/), GNU Make, [uv](https://docs.astral.sh/uv/) `>=0.9.14`, [Node.js (≥22.23.2, \<23)](https://nodejs.org/en/download) satisfying `web/package.json`, and [pnpm](https://pnpm.io/installation)
## Install
@@ -21,7 +21,23 @@ Choose the install path that matches what you want to do.
### Install from PyPI
-Use the [PyPI package](https://pypi.org/project/nemo-platform/) for the quickest local install. The `all` extra includes the SDK, CLI, platform services, and default first-party plugins:
+Use the [PyPI package](https://pypi.org/project/nemo-platform/) for the quickest local install. The `all` extra includes the SDK, CLI, platform services, and default first-party plugins; drop it for the SDK and CLI alone.
+
+
+
+
+
+One command, `nemo` available everywhere, nothing to activate.
+
+```bash
+uv tool install "nemo-platform[all]"
+```
+
+
+
+
+
+Use when you also import the SDK from your own code. `nemo` works only while the environment is active.
```bash
python -m venv .venv
@@ -29,13 +45,11 @@ source .venv/bin/activate
pip install "nemo-platform[all]"
```
-For SDK and CLI usage only, install the base package:
+
-```bash
-pip install nemo-platform
-```
+
-After installing `nemo-platform[all]` from PyPI, continue with `nemo setup`.
+Continue with `nemo setup`.
### Install from source
diff --git a/packages/nemo_platform/README.md b/packages/nemo_platform/README.md
index f0be59a175..ee6840acff 100644
--- a/packages/nemo_platform/README.md
+++ b/packages/nemo_platform/README.md
@@ -21,13 +21,17 @@ NeMo Platform brings NVIDIA NeMo libraries together under one CLI, Python SDK, a
**Prerequisites:** Python 3.12–3.13 and an API key for an inference provider (NVIDIA Build, OpenAI, Anthropic, Google Gemini, or a local Ollama instance).
-The `nemo-platform` distribution is a convenience wrapper that bundles the SDK, shared runtime packages, default first-party plugins, and platform services into a single wheel. Install just the SDK and CLI, or install everything needed to run the platform locally:
+The `nemo-platform` distribution is a convenience wrapper that bundles the SDK, shared runtime packages, default first-party plugins, and platform services into a single wheel. The `all` extra adds the platform services; drop it for the SDK and CLI alone.
+
+Recommended — one command, `nemo` available everywhere, nothing to activate:
```bash
-# SDK + CLI only
-pip install nemo-platform
+uv tool install "nemo-platform[all]"
+```
-# SDK + CLI + all platform services and default plugins (recommended)
+Use a virtual environment instead when you also import the SDK from your own code. `nemo` then works only while the environment is active:
+
+```bash
pip install "nemo-platform[all]"
```
diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py
index 5c58af463d..c2b57ae906 100644
--- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py
+++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py
@@ -15,6 +15,7 @@
import typer
from nemo_platform_ext.cli.core.help_formatter import create_typer_app
from nemo_platform_ext.cli.docker_preflight import require_docker_for_default_local
+from nemo_platform_ext.local.install import services_extra_install_command
from nemo_platform_ext.local.process import (
ForegroundInstanceError,
InstanceAlreadyRunningError,
@@ -75,10 +76,7 @@ def _require_services_extra() -> None:
"Running local platform services needs extra components that aren't installed yet.\n"
"\n"
"Install them with:\n"
- " pip install 'nemo-platform[all]'\n"
- "\n"
- "On Python 3.14, use:\n"
- " PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform[all]'\n",
+ f" {services_extra_install_command()}\n",
err=True,
)
raise typer.Exit(1)
diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py
index ecfc915d6d..fe4530ee8d 100644
--- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py
+++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py
@@ -36,6 +36,7 @@
from pydantic import SecretStr
from rich import box
from rich.console import Console
+from rich.markup import escape
from rich.panel import Panel
from nemo_platform_ext.cli.commands.skills import registry as skills_registry
@@ -49,6 +50,7 @@
from nemo_platform_ext.client.tls import client_verify_from_env
from nemo_platform_ext.config.config import Config
from nemo_platform_ext.config.models import DEFAULT_BASE_URL, ConfigFile, ConfigParams, LocalServicesConfig, NoAuthUser
+from nemo_platform_ext.local.install import services_extra_install_command
from nemo_platform_ext.local.process import (
PortConflict,
check_port_available_for_start,
@@ -1154,9 +1156,7 @@ def _maybe_start_services(
if importlib.util.find_spec("pyleak") is None:
console.print(f"{CROSS} Local services require extra dependencies that aren't installed.")
console.print(" Install them with:")
- console.print(" [cyan]pip install 'nemo-platform\\[all]'[/cyan]")
- console.print(" On Python 3.14, use:")
- console.print(" [cyan]PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform\\[all]'[/cyan]")
+ console.print(f" [cyan]{escape(services_extra_install_command())}[/cyan]")
raise typer.Exit(1)
# Fail before stop/spawn when default local needs Docker (NVBug 6537617).
diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py b/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py
new file mode 100644
index 0000000000..52b2c7a383
--- /dev/null
+++ b/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""How this NeMo Platform install was created."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+
+def services_extra_install_command() -> str:
+ """Return the command that adds the packaged service dependencies here.
+
+ ``uv tool install`` environments are managed by uv and are not meant to be
+ edited with pip; re-running the tool install with the extra upgrades them
+ in place. uv marks such an environment with a ``uv-receipt.toml`` at its
+ root, which is what distinguishes it from an ordinary virtual environment.
+ """
+ if (Path(sys.prefix) / "uv-receipt.toml").is_file():
+ return "uv tool install 'nemo-platform[all]'"
+ return "pip install 'nemo-platform[all]'"
diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py b/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py
index 6af3f5f04f..2edc4ae7ea 100644
--- a/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py
+++ b/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py
@@ -21,6 +21,7 @@
from nemo_platform import AsyncNeMoPlatform, NeMoPlatform
from nemo_platform_ext.local import process
+from nemo_platform_ext.local.install import services_extra_install_command
from nemo_platform_ext.local.transport import (
EMBEDDED_BASE_URL,
UDS_BASE_URL,
@@ -407,7 +408,7 @@ async def stop_async(self, *, timeout: float = 30.0, force: bool = False) -> pro
def require_services_extra() -> None:
if importlib.util.find_spec("pyleak") is not None:
return
- raise ServicesExtraRequiredError("Install service dependencies with `pip install 'nemo-platform[all]'`.")
+ raise ServicesExtraRequiredError(f"Install service dependencies with `{services_extra_install_command()}`.")
def _validate_socket_path_length(socket_path: Path) -> None:
diff --git a/packages/nemo_platform_ext/tests/local/test_install.py b/packages/nemo_platform_ext/tests/local/test_install.py
new file mode 100644
index 0000000000..7225fa43ef
--- /dev/null
+++ b/packages/nemo_platform_ext/tests/local/test_install.py
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import pytest
+from nemo_platform_ext.local.install import services_extra_install_command
+
+
+@pytest.mark.parametrize(
+ ("uv_tool_env", "expected"),
+ [
+ (False, "pip install 'nemo-platform[all]'"),
+ (True, "uv tool install 'nemo-platform[all]'"),
+ ],
+)
+def test_services_extra_install_command_matches_install_method(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, uv_tool_env: bool, expected: str
+) -> None:
+ if uv_tool_env:
+ (tmp_path / "uv-receipt.toml").write_text("[tool]\n")
+ monkeypatch.setattr(sys, "prefix", str(tmp_path))
+
+ assert services_extra_install_command() == expected
diff --git a/script/compile-wheel-constraints.sh b/script/compile-wheel-constraints.sh
index 3bd03a57f5..56195f386a 100755
--- a/script/compile-wheel-constraints.sh
+++ b/script/compile-wheel-constraints.sh
@@ -35,7 +35,7 @@ emit_constraints() {
local wheel="$1" spec="$2" label="$3" out="$4" venv meta
venv="$(mktemp -d)"
meta="$(mktemp -d)"
- uv venv "${venv}" --python 3.11 --quiet
+ uv venv "${venv}" --python 3.12 --quiet
# Resolve+install once with the cap so we snapshot consistent, py3.14-safe versions.
printf '%s\n' "${LITELLM_CAP%% *}" >"${meta}/cap.txt"
uv pip install --python "${venv}/bin/python" --constraint "${meta}/cap.txt" "${spec}" >/dev/null
@@ -61,7 +61,7 @@ PY
printf '# Regenerate with: script/compile-wheel-constraints.sh \n#\n'
while read -r name; do
[[ -n "${name}" ]] || continue
- ver="$("${venv}/bin/python" -c "import importlib.metadata as m; print(m.version('${name}'))" 2>/dev/null || true)
+ ver="$("${venv}/bin/python" -c "import importlib.metadata as m; print(m.version('${name}'))" 2>/dev/null || true)"
[[ -n "${ver}" ]] && printf '%s==%s\n' "${name}" "${ver}"
done <"${meta}/names.txt" | sort
printf '%s\n' "${LITELLM_CAP}"
diff --git a/script/test-nemo-cli.sh b/script/test-nemo-cli.sh
index cde8c02b98..558783767b 100755
--- a/script/test-nemo-cli.sh
+++ b/script/test-nemo-cli.sh
@@ -52,7 +52,20 @@ done
echo "----- nemo on PATH -----"
command -v nemo
-nemo --version
+version_output="$(nemo --version)"
+echo "${version_output}"
+# A bare `0.0.0` is the CLI's fallback when its importlib.metadata lookup
+# misses the distribution it was installed from, which is invisible unless the
+# output is asserted. Match it exactly: nightly wheels are stamped with a
+# sentinel `0.0.0.dev` that is a legitimate version string. The
+# pattern is the subset of PEP 440 this project stamps — release, optional
+# pre-release, any number of .post/.dev segments, optional local version.
+version="${version_output#nemo version }"
+if [[ "${version}" == "0.0.0" ]] ||
+ [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.(post|dev)[0-9]+)*(\+[0-9a-z.]+)?$ ]]; then
+ echo "::error::nemo --version did not report an installed version: ${version_output}" >&2
+ exit 1
+fi
# Import-time checks for commands that ship in the bundled wrapper. We don't
# call `nemo services --help` here because `nemo services run` below is a
diff --git a/sdk/python/nemo-platform/pyproject.toml b/sdk/python/nemo-platform/pyproject.toml
index d45b878903..482fed2314 100644
--- a/sdk/python/nemo-platform/pyproject.toml
+++ b/sdk/python/nemo-platform/pyproject.toml
@@ -26,13 +26,12 @@ dependencies = [
"fsspec>=2023.1.0",
]
-requires-python = ">= 3.12"
+requires-python = ">=3.12,<3.14"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
- "Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
diff --git a/sdk/python/nemo-platform/src/nemo_platform/_version.py b/sdk/python/nemo-platform/src/nemo_platform/_version.py
index a5a2651220..e7511e7393 100644
--- a/sdk/python/nemo-platform/src/nemo_platform/_version.py
+++ b/sdk/python/nemo-platform/src/nemo_platform/_version.py
@@ -5,7 +5,7 @@
__title__ = "nemo_platform"
try:
- __version__ = _package_version("nemo-platform-sdk")
+ __version__ = _package_version("nemo-platform")
except PackageNotFoundError:
__version__ = "0.0.0"
# Injected at release time for non-production builds; None for RC and production releases.
diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py
index c63b6bf9db..7026222b9a 100644
--- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py
+++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py
@@ -15,6 +15,7 @@
import typer
from nemo_platform.cli.core.help_formatter import create_typer_app
from nemo_platform.cli.docker_preflight import require_docker_for_default_local
+from nemo_platform.local.install import services_extra_install_command
from nemo_platform.local.process import (
ForegroundInstanceError,
InstanceAlreadyRunningError,
@@ -75,10 +76,7 @@ def _require_services_extra() -> None:
"Running local platform services needs extra components that aren't installed yet.\n"
"\n"
"Install them with:\n"
- " pip install 'nemo-platform[all]'\n"
- "\n"
- "On Python 3.14, use:\n"
- " PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform[all]'\n",
+ f" {services_extra_install_command()}\n",
err=True,
)
raise typer.Exit(1)
diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py
index beda9ede74..09d20f1a3b 100644
--- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py
+++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py
@@ -36,6 +36,7 @@
from pydantic import SecretStr
from rich import box
from rich.console import Console
+from rich.markup import escape
from rich.panel import Panel
from nemo_platform.cli.commands.skills import registry as skills_registry
@@ -49,6 +50,7 @@
from nemo_platform.client.tls import client_verify_from_env
from nemo_platform.config.config import Config
from nemo_platform.config.models import DEFAULT_BASE_URL, ConfigFile, ConfigParams, LocalServicesConfig, NoAuthUser
+from nemo_platform.local.install import services_extra_install_command
from nemo_platform.local.process import (
PortConflict,
check_port_available_for_start,
@@ -1154,9 +1156,7 @@ def _maybe_start_services(
if importlib.util.find_spec("pyleak") is None:
console.print(f"{CROSS} Local services require extra dependencies that aren't installed.")
console.print(" Install them with:")
- console.print(" [cyan]pip install 'nemo-platform\\[all]'[/cyan]")
- console.print(" On Python 3.14, use:")
- console.print(" [cyan]PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform\\[all]'[/cyan]")
+ console.print(f" [cyan]{escape(services_extra_install_command())}[/cyan]")
raise typer.Exit(1)
# Fail before stop/spawn when default local needs Docker (NVBug 6537617).
diff --git a/sdk/python/nemo-platform/src/nemo_platform/local/install.py b/sdk/python/nemo-platform/src/nemo_platform/local/install.py
new file mode 100644
index 0000000000..52b2c7a383
--- /dev/null
+++ b/sdk/python/nemo-platform/src/nemo_platform/local/install.py
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""How this NeMo Platform install was created."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+
+def services_extra_install_command() -> str:
+ """Return the command that adds the packaged service dependencies here.
+
+ ``uv tool install`` environments are managed by uv and are not meant to be
+ edited with pip; re-running the tool install with the extra upgrades them
+ in place. uv marks such an environment with a ``uv-receipt.toml`` at its
+ root, which is what distinguishes it from an ordinary virtual environment.
+ """
+ if (Path(sys.prefix) / "uv-receipt.toml").is_file():
+ return "uv tool install 'nemo-platform[all]'"
+ return "pip install 'nemo-platform[all]'"
diff --git a/sdk/python/nemo-platform/src/nemo_platform/local/services.py b/sdk/python/nemo-platform/src/nemo_platform/local/services.py
index 9930d331d6..bbd1091f46 100644
--- a/sdk/python/nemo-platform/src/nemo_platform/local/services.py
+++ b/sdk/python/nemo-platform/src/nemo_platform/local/services.py
@@ -21,6 +21,7 @@
from nemo_platform import AsyncNeMoPlatform, NeMoPlatform
from nemo_platform.local import process
+from nemo_platform.local.install import services_extra_install_command
from nemo_platform.local.transport import (
EMBEDDED_BASE_URL,
UDS_BASE_URL,
@@ -407,7 +408,7 @@ async def stop_async(self, *, timeout: float = 30.0, force: bool = False) -> pro
def require_services_extra() -> None:
if importlib.util.find_spec("pyleak") is not None:
return
- raise ServicesExtraRequiredError("Install service dependencies with `pip install 'nemo-platform[all]'`.")
+ raise ServicesExtraRequiredError(f"Install service dependencies with `{services_extra_install_command()}`.")
def _validate_socket_path_length(socket_path: Path) -> None:
diff --git a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py
new file mode 100644
index 0000000000..b529a43320
--- /dev/null
+++ b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import pytest
+from nemo_platform.local.install import services_extra_install_command
+
+
+@pytest.mark.parametrize(
+ ("uv_tool_env", "expected"),
+ [
+ (False, "pip install 'nemo-platform[all]'"),
+ (True, "uv tool install 'nemo-platform[all]'"),
+ ],
+)
+def test_services_extra_install_command_matches_install_method(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, uv_tool_env: bool, expected: str
+) -> None:
+ if uv_tool_env:
+ (tmp_path / "uv-receipt.toml").write_text("[tool]\n")
+ monkeypatch.setattr(sys, "prefix", str(tmp_path))
+
+ assert services_extra_install_command() == expected
diff --git a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py
index dfa9501f85..e707f5a1d8 100755
--- a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py
+++ b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py
@@ -16,7 +16,7 @@
import tomlkit
import typer
-from nemo_platform_sdk_tools.sdk.core.common import SdkInfo, get_sdk_info
+from nemo_platform_sdk_tools.sdk.core.common import WRAPPER_DISTRIBUTION_NAME, SdkInfo, get_sdk_info
from nemo_platform_sdk_tools.sdk.post_generation_exist_ok import inject_exist_ok
app = typer.Typer(
@@ -24,7 +24,14 @@
)
-def sdk_version_file_content(package_name: str) -> str:
+def sdk_version_file_content(distribution_name: str) -> str:
+ """Render ``_version.py``, which reads the version from installed metadata.
+
+ ``distribution_name`` is the distribution to query at runtime, which is not
+ the distribution being built: the SDK ships bundled inside the
+ ``nemo-platform`` wheel, so ``nemo-platform-sdk`` metadata only exists in
+ the workspace. Both report the same version there.
+ """
return f'''# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
@@ -32,7 +39,7 @@ def sdk_version_file_content(package_name: str) -> str:
__title__ = "nemo_platform"
try:
- __version__ = _package_version("{package_name}")
+ __version__ = _package_version("{distribution_name}")
except PackageNotFoundError:
__version__ = "0.0.0"
# Injected at release time for non-production builds; None for RC and production releases.
@@ -195,15 +202,15 @@ def update_pyproject_toml(sdk_info: SdkInfo) -> bool:
project["urls"] = {"Homepage": "https://docs.nvidia.com/nemo/microservices/latest/about/index.html"}
- # Drop support for Python versions older than 3.12.
- version_to_remove = ["3.8", "3.9", "3.10", "3.11"]
+ # Match the supported range of the nemo-platform wheel this SDK ships in.
+ version_to_remove = ["3.8", "3.9", "3.10", "3.11", "3.14"]
for ver in version_to_remove:
try:
project["classifiers"].remove(f"Programming Language :: Python :: {ver}")
except ValueError:
pass
- project["requires-python"] = ">= 3.12"
+ project["requires-python"] = ">=3.12,<3.14"
dependencies = project.get("dependencies")
if dependencies is not None:
for dep in list(dependencies):
@@ -624,7 +631,7 @@ def ensure_api_image_field() -> None:
version_file = sdk_info.sdk_dir / "src" / sdk_info.module_name / "_version.py"
content = version_file.read_text(encoding="utf-8")
- expected = sdk_version_file_content(sdk_info.package_name)
+ expected = sdk_version_file_content(WRAPPER_DISTRIBUTION_NAME)
if content == expected:
typer.echo(" - Dynamic version metadata already present, skipping.")
return