Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/cli/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Environment variables override configuration file settings. This is useful for C
| `NMP_OUTPUT_FORMAT` | Output format (table, json, yaml, csv, markdown) |
| `NMP_TIMESTAMP_FORMAT` | Timestamp format (relative, iso8601) |
| `NMP_COLOR_OUTPUT` | Enable/disable colored output (true/false) |
| `NEMO_TELEMETRY_ENABLED` | Enable CLI telemetry only when set to `true`; any other explicit value disables it |

Example:

Expand All @@ -115,6 +116,27 @@ Settings are resolved in this order (highest priority first):

This means you can set defaults in your config file and override them as needed with environment variables or flags.

## CLI Telemetry

The NeMo CLI sends anonymous usage telemetry by default to help improve setup, command reliability, and product workflows. The first CLI invocation with telemetry enabled prints a notice to stderr and creates a `telemetry-notice-shown` marker next to the CLI config file. Stdout is not changed, so scripts that parse command output remain stable.

Telemetry events include the command or workflow category, task status, duration, client version, a random session ID that rotates every 30 days, deployment type, and whether the command appears to be running in CI. Telemetry does not include prompts, model inputs or outputs, datasets, secrets, access tokens, configuration file contents, file contents, local paths, usernames, email addresses, or hostnames.

To disable telemetry for a single command or shell session:

```bash
NEMO_TELEMETRY_ENABLED=false nemo models list
export NEMO_TELEMETRY_ENABLED=false
```

To disable telemetry persistently, add this top-level field to your CLI config file:

```yaml
telemetry_enabled: false
```

If `NEMO_TELEMETRY_ENABLED` is set, only `true` keeps the environment layer enabled. Any other explicit value disables telemetry for that process. A persisted `telemetry_enabled: false` value disables telemetry even when the environment variable is set to `true`.
Comment thread
mckornfield marked this conversation as resolved.

## Shell Completion

The NeMo CLI supports tab completion for Bash, Zsh, and Fish shells. Enable it with:
Expand Down
1 change: 1 addition & 0 deletions packages/nemo_platform_ext/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"ngcsdk>=4.8.2",
"nvidia-ml-py>=13.0.0",
"psutil>=5.9.0",
"httpx>=0.23.0,<1",
]
requires-python = ">=3.11,<3.15"
readme = "README.md"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from nemo_platform_ext.cli.telemetry.handler import (
QueuedEvent,
TelemetryHandler,
build_payload,
)

__all__ = [
"QueuedEvent",
"TelemetryHandler",
"build_payload",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Fire-and-flush emission with three opt-out layers and the first-run notice."""

from __future__ import annotations

import logging
import sys
from pathlib import Path

from nemo_platform_ext.cli.telemetry.events import PlatformTelemetryEvent
from nemo_platform_ext.cli.telemetry.handler import TelemetryHandler, _telemetry_enabled
from nemo_platform_ext.cli.telemetry.session import get_session_id

logger = logging.getLogger(__name__)

_invocation_opt_out = False

_NOTICE_TEXT = (
"NeMo Platform CLI telemetry is on by default and sends anonymous usage data to improve the product. "
"It does not send prompts, model inputs or outputs, datasets, secrets, file contents, or personal identifiers. "
"Turn it off with NEMO_TELEMETRY_ENABLED=false or telemetry_enabled: false in the CLI config. "
"Run nemo docs cli/configuration for details.\n"
Comment thread
mckornfield marked this conversation as resolved.
)


def set_invocation_opt_out(value: bool) -> None:
"""Per-invocation opt-out (e.g. a --no-telemetry flag on the current command)."""
global _invocation_opt_out
_invocation_opt_out = value


def _config_opted_out() -> bool:
"""True when the persisted config file sets ``telemetry_enabled: false``."""
try:
from nemo_platform_ext.config.config import Config

cfg = Config.load()
return cfg.get_config_file().telemetry_enabled is False
except Exception:
# A privacy control must fail closed: if we cannot read the config to confirm
# the user is opted in, treat them as opted out and do not send.
logger.debug("Could not read telemetry opt-out config; failing closed (opted out)", exc_info=True)
return True
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def telemetry_opted_in() -> bool:
"""Opted in only when all three layers agree: per-invocation, env, and config."""
if _invocation_opt_out:
return False
if not _telemetry_enabled():
return False
return not _config_opted_out()


def _client_version() -> str:
try:
import nemo_platform

return nemo_platform.__version__
except Exception:
logger.debug("Could not resolve client version for telemetry", exc_info=True)
return "undefined"


def emit_event(event: PlatformTelemetryEvent) -> None:
"""Best effort. Telemetry must never break a user command."""
try:
if not telemetry_opted_in():
return
# No retries on the CLI exit path: a synchronous send blocks the user's command,
# so cap the worst case at one bounded send (SEND_TIMEOUT_SECONDS) rather than
# retrying against a slow or unreachable endpoint while the user waits.
handler = TelemetryHandler(source_client_version=_client_version(), session_id=get_session_id(), max_retries=0)
handler.enqueue(event)
handler.stop()
except Exception:
logger.debug("Failed to emit telemetry event", exc_info=True)


def _notice_marker_path() -> Path:
from nemo_platform_ext.config.config import Config

return Config.get_default_config_path().parent / "telemetry-notice-shown"


def maybe_print_first_run_notice() -> None:
"""Print the first-run notice to stderr once. Stdout stays machine-clean."""
try:
if not telemetry_opted_in():
return
marker = _notice_marker_path()
if marker.exists():
return
marker.parent.mkdir(parents=True, exist_ok=True)
marker.touch()
sys.stderr.write(_NOTICE_TEXT)
Comment thread
mckornfield marked this conversation as resolved.
except Exception:
logger.debug("Failed to print telemetry notice", exc_info=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Platform usage-telemetry event models.

Field names and aliases follow the shared NeMo telemetry schema
(aire/microservices/nemo-telemetry, schemas/anonymous_events.json, v1.9).
"""

from __future__ import annotations

import os
from enum import Enum
from typing import ClassVar

from pydantic import BaseModel, ConfigDict, Field

_CI_ENV_VARS = (
"CI",
"GITLAB_CI",
"GITHUB_ACTIONS",
"BUILDKITE",
"CIRCLECI",
"JENKINS_URL",
"TEAMCITY_VERSION",
"TF_BUILD",
"TRAVIS",
)
_FALSEY = ("", "0", "false", "no", "off")


def is_ci_environment() -> bool:
return any(os.getenv(v, "").lower() not in _FALSEY for v in _CI_ENV_VARS)


class TaskStatusEnum(str, Enum):
COMPLETED = "completed"
ERROR = "error"
CANCELED = "canceled"
UNDEFINED = "undefined"


class DeploymentTypeEnum(str, Enum):
CLI = "cli"
SDK = "sdk"
NVIDIA_INTERNAL = "nvidia-internal"
UNDEFINED = "undefined"


def _deployment_type() -> DeploymentTypeEnum:
raw = os.getenv("NEMO_DEPLOYMENT_TYPE", "cli").lower()
try:
return DeploymentTypeEnum(raw)
except ValueError:
return DeploymentTypeEnum.UNDEFINED


class PlatformTelemetryEvent(BaseModel):
"""Base for all platform events. extra="forbid" is a privacy guard."""
Comment thread
mckornfield marked this conversation as resolved.

model_config = ConfigDict(extra="forbid", populate_by_name=True)

_event_name: ClassVar[str] = "undefined"
_schema_version: ClassVar[str] = "1.9"

nemo_source: str = Field(default="platform", serialization_alias="nemoSource")
task_status: TaskStatusEnum = Field(serialization_alias="taskStatus")
deployment_type: DeploymentTypeEnum = Field(default_factory=_deployment_type, serialization_alias="deploymentType")
is_ci: bool = Field(default_factory=is_ci_environment, serialization_alias="isCi")


class OnboardingStepEvent(PlatformTelemetryEvent):
_event_name: ClassVar[str] = "onboarding_step"

step: str
provider_type: str = Field(default="undefined", serialization_alias="providerType")
models_discovered_bucket: str = Field(default="undefined", serialization_alias="modelsDiscoveredBucket")
skills_target: str = Field(default="undefined", serialization_alias="skillsTarget")
agent_deployed: bool = Field(default=False, serialization_alias="agentDeployed")


class CommandInvokedEvent(PlatformTelemetryEvent):
_event_name: ClassVar[str] = "command_invoked"

command: str
duration_sec: float = Field(serialization_alias="durationSec")
agent_mode: bool = Field(default=False, serialization_alias="agentMode")


class JobRunEvent(PlatformTelemetryEvent):
_event_name: ClassVar[str] = "job_run"

job_type: str = Field(serialization_alias="jobType")
duration_sec: float = Field(default=-1.0, serialization_alias="durationSec")
plugins: list[str] = Field(default_factory=list)
model: str = "undefined"
input_tokens: int = Field(default=-1, serialization_alias="inputTokens")
output_tokens: int = Field(default=-1, serialization_alias="outputTokens")
Loading
Loading