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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ Status: nemoclaw my-assistant status
Logs: nemoclaw my-assistant logs --follow
──────────────────────────────────────────────────

To change settings later:
Model: nemoclaw inference get
nemoclaw inference set --model <model> --provider <provider> --sandbox my-assistant

[INFO] === Installation complete ===
```

Expand Down
4 changes: 4 additions & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ Status: nemoclaw my-gpt-claw status
Logs: nemoclaw my-gpt-claw logs --follow
──────────────────────────────────────────────────

To change settings later:
Model: nemoclaw inference get
nemoclaw inference set --model <model> --provider <provider> --sandbox my-gpt-claw

[INFO] === Installation complete ===
```

Expand Down
18 changes: 12 additions & 6 deletions docs/inference/switch-inference-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ No restart is required.
## Prerequisites

- A running NemoClaw sandbox.
- The OpenShell CLI on your `PATH`.
- The OpenShell CLI on your `PATH`, which NemoClaw uses under the hood.

## Switch to a Different Model

Expand Down Expand Up @@ -185,19 +185,25 @@ $ nemoclaw onboard --resume --recreate-sandbox

## Verify the Active Model

Run the status command to confirm the change:
Run the inference command to confirm the live gateway route:

```console
$ nemoclaw <name> status
$ nemoclaw inference get
```

Add `--json` for machine-readable output:

```console
$ nemoclaw inference get --json
```

Add the `--json` flag for machine-readable output:
Run the status command when you also need sandbox, service, and messaging health:

```console
$ nemoclaw <name> status --json
$ nemoclaw <name> status
```

The output includes the active provider, model, and endpoint.
The status output includes the active provider, model, and endpoint with the rest of the sandbox state.

## Notes

Expand Down
9 changes: 2 additions & 7 deletions docs/reference/cli-selection-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,6 @@ Use `openshell` when the docs explicitly call for a live OpenShell gateway opera
$ openshell term
```

- Inspect the live gateway inference route:

```console
$ openshell inference get -g nemoclaw
```

- Manage dashboard or service port forwards:

```console
Expand Down Expand Up @@ -181,9 +175,10 @@ Approved endpoints are session-scoped unless you also add them to the policy thr

### Change Models or Providers

Use the NemoClaw command for model or provider switches so the OpenShell route and the running agent config stay consistent:
Use the NemoClaw commands for model or provider inspection and switches so the OpenShell route and the running agent config stay consistent:

```console
$ nemoclaw inference get
$ nemoclaw inference set --provider nvidia-prod --model nvidia/nemotron-3-super-120b-a12b
```

Expand Down
10 changes: 10 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,16 @@ $ nemoclaw status
$ nemoclaw status --json
```

### `nemoclaw inference get`

Show the active live inference provider and model from the NemoClaw-managed OpenShell gateway.
Use this command when you want the direct runtime route without the rest of the sandbox status output.

```console
$ nemoclaw inference get
$ nemoclaw inference get --json
```

### `nemoclaw inference set`

Switch the active inference provider or model for a NemoClaw-managed OpenClaw or Hermes sandbox.
Expand Down
16 changes: 16 additions & 0 deletions src/commands/inference/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import Command from "../../lib/commands/inference/get";
import { withCommandDisplay } from "../../lib/cli/command-display";

export default withCommandDisplay(Command, [
{
usage: "nemoclaw inference get",
description: "Show the active inference provider and model",
flags: "[--json]",
group: "Services",
scope: "global",
order: 36,
},
]);
65 changes: 65 additions & 0 deletions src/lib/actions/inference-get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

vi.mock("../adapters/openshell/runtime", () => ({
captureOpenshell: vi.fn(),
}));

vi.mock("../inference/local", () => ({
DEFAULT_OLLAMA_MODEL: "llama3.1",
}));

import { runInferenceGet, type InferenceGetDeps } from "./inference-get";

function createDeps(output: string, status = 0): InferenceGetDeps & {
log: ReturnType<typeof vi.fn>;
captureOpenshell: ReturnType<typeof vi.fn>;
} {
const captureOpenshell = vi.fn(() => ({ status, output }));
const log = vi.fn();
return {
captureOpenshell: captureOpenshell as unknown as InferenceGetDeps["captureOpenshell"] &
ReturnType<typeof vi.fn>,
log: log as unknown as InferenceGetDeps["log"] & ReturnType<typeof vi.fn>,
};
}

describe("runInferenceGet", () => {
it("prints the live provider and model", async () => {
const deps = createDeps("Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/model\n");

await expect(runInferenceGet({}, deps)).resolves.toEqual({
provider: "nvidia-prod",
model: "nvidia/model",
});

expect(deps.captureOpenshell).toHaveBeenCalledWith(
["inference", "get", "-g", "nemoclaw"],
expect.objectContaining({ ignoreError: true }),
);
expect(deps.log.mock.calls.map(([line]) => line)).toEqual([
"Provider: nvidia-prod",
"Model: nvidia/model",
]);
});

it("supports JSON output", async () => {
const deps = createDeps("Gateway inference:\n Provider: openai-api\n Model: gpt-5.4\n");

await runInferenceGet({ json: true }, deps);

expect(JSON.parse(deps.log.mock.calls[0][0])).toEqual({
provider: "openai-api",
model: "gpt-5.4",
});
});

it("fails when no route is configured", async () => {
const deps = createDeps("Gateway inference:\n\n Not configured\n");

await expect(runInferenceGet({}, deps)).rejects.toThrow(/not configured/);
expect(deps.log).not.toHaveBeenCalled();
});
});
65 changes: 65 additions & 0 deletions src/lib/actions/inference-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { captureOpenshell } from "../adapters/openshell/runtime";
import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts";
import { getLiveGatewayInference } from "../inference/live";

export interface InferenceGetOptions {
json?: boolean;
}

export interface InferenceGetResult {
provider: string | null;
model: string | null;
}

export interface InferenceGetDeps {
captureOpenshell: typeof captureOpenshell;
log: (message?: string) => void;
}

export class InferenceGetError extends Error {
constructor(
message: string,
readonly exitCode = 1,
) {
super(message);
this.name = "InferenceGetError";
}
}

function defaultDeps(): InferenceGetDeps {
return {
captureOpenshell,
log: console.log,
};
}

export async function runInferenceGet(
options: InferenceGetOptions = {},
deps: InferenceGetDeps = defaultDeps(),
): Promise<InferenceGetResult> {
const result = getLiveGatewayInference(deps.captureOpenshell, {
timeout: OPENSHELL_PROBE_TIMEOUT_MS,
});
if (result.status !== 0) {
throw new InferenceGetError("OpenShell inference route lookup failed.", result.status || 1);
}
if (!result.inference) {
throw new InferenceGetError("OpenShell inference route is not configured.");
}

const payload = {
provider: result.inference.provider,
model: result.inference.model,
};
if (options.json) {
deps.log(JSON.stringify(payload, null, 2));
} else {
deps.log(`Provider: ${payload.provider ?? "unknown"}`);
deps.log(`Model: ${payload.model ?? "unknown"}`);
}

return payload;
}
3 changes: 2 additions & 1 deletion src/lib/actions/root-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export function help(): void {
lines.push("");
lines.push(` ${G}Reconfiguration (after onboard):${R}`);
lines.push(
` ${D}• Change inference model: nemoclaw inference set --model <model> --provider <provider>${R}`,
` ${D}• Check inference route: ${CLI_NAME} inference get${R}`,
` ${D}• Change inference model: ${CLI_NAME} inference set --model <model> --provider <provider>${R}`,
);
lines.push(` ${D}• Add network presets: use the policy-add command on your sandbox${R}`);
lines.push(
Expand Down
16 changes: 8 additions & 8 deletions src/lib/cli/command-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import type { CommandDef } from "./command-registry";

describe("command-registry", () => {
describe("COMMANDS array", () => {
it("should contain exactly 57 commands", () => {
// 25 global (20 visible + 5 hidden help/version aliases)
it("should contain exactly 58 commands", () => {
// 26 global (21 visible + 5 hidden help/version aliases)
// 32 sandbox (26 visible + 6 hidden shields/config)
expect(COMMANDS).toHaveLength(57);
expect(COMMANDS).toHaveLength(58);
});

it("should have no duplicate usage strings", () => {
Expand All @@ -39,9 +39,9 @@ describe("command-registry", () => {
});

describe("globalCommands()", () => {
it("should return exactly 25 entries", () => {
// 20 visible + 5 hidden (help, --help, -h, --version, -v)
expect(globalCommands()).toHaveLength(25);
it("should return exactly 26 entries", () => {
// 21 visible + 5 hidden (help, --help, -h, --version, -v)
expect(globalCommands()).toHaveLength(26);
});

it("every entry has scope global", () => {
Expand All @@ -65,10 +65,10 @@ describe("command-registry", () => {
});

describe("visibleCommands()", () => {
it("should exclude 11 hidden commands (46 visible)", () => {
it("should exclude 11 hidden commands (47 visible)", () => {
// 5 hidden global (help, --help, -h, --version, -v) +
// 6 hidden sandbox (shields×3, config get/set/rotate-token)
expect(visibleCommands()).toHaveLength(46);
expect(visibleCommands()).toHaveLength(47);
});

it("no visible command has hidden=true", () => {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/cli/oclif-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ describe("resolveGlobalOclifDispatch", () => {
commandId: "inference:set",
args: ["--provider", "nvidia-prod"],
});
expect(resolveGlobalOclifDispatch("inference", ["get", "--json"])).toEqual({
kind: "oclif",
commandId: "inference:get",
args: ["--json"],
});
expect(resolveGlobalOclifDispatch("--version", [])).toEqual({
kind: "oclif",
commandId: "root:version",
Expand All @@ -44,9 +49,10 @@ describe("resolveGlobalOclifDispatch", () => {
kind: "usageError",
lines: ["tunnel <start|stop>"],
});
expect(resolveGlobalOclifDispatch("inference", ["get"])).toEqual({
expect(resolveGlobalOclifDispatch("inference", ["bogus"])).toEqual({
kind: "usageError",
lines: [
"inference get [--json]",
"inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify]",
],
});
Expand Down
2 changes: 2 additions & 0 deletions src/lib/cli/oclif-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ export function resolveGlobalOclifDispatch(cmd: string, args: string[]): Dispatc

if (cmd === "inference") {
const sub = args[0];
if (sub === "get") return oclif("inference:get", args.slice(1));
if (sub === "set") return oclif("inference:set", args.slice(1));
return {
kind: "usageError",
lines: [
"inference get [--json]",
"inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify]",
],
};
Expand Down
15 changes: 15 additions & 0 deletions src/lib/commands/global-oclif-command-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
renderSandboxInventoryText: vi.fn(),
runBackupAllAction: vi.fn(),
runGarbageCollectImagesAction: vi.fn(),
runInferenceGet: vi.fn(),
runInferenceSet: vi.fn(),
runOnboardAction: vi.fn(),
runSetupAction: vi.fn(),
Expand Down Expand Up @@ -50,6 +51,14 @@ vi.mock("../actions/inference-set", () => ({
runInferenceSet: mocks.runInferenceSet,
}));

vi.mock("../actions/inference-get", () => ({
InferenceGetError: class InferenceGetError extends Error {
exitCode = 1;
},
runInferenceGet: mocks.runInferenceGet,
}));

import InferenceGetCommand from "./inference/get";
import InferenceSetCommand from "./inference/set";
import ListCommand from "./list";
import BackupAllCommand from "./maintenance/backup-all";
Expand Down Expand Up @@ -152,4 +161,10 @@ describe("global oclif command adapters", () => {
noVerify: true,
});
});

it("maps inference get flags into the inference action", async () => {
await InferenceGetCommand.run(["--json"], rootDir);

expect(mocks.runInferenceGet).toHaveBeenCalledWith({ json: true });
});
});
Loading
Loading