Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -906,6 +906,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
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 });
});
});
36 changes: 36 additions & 0 deletions src/lib/commands/inference/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Flags } from "@oclif/core";

import {
InferenceGetError,
runInferenceGet,
} from "../../actions/inference-get";
import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command";

export default class InferenceGetCommand extends NemoClawCommand {
static id = "inference:get";
static strict = true;
static summary = "Show the active NemoClaw inference route";
static description = "Read the live OpenShell inference route through the NemoClaw CLI.";
static usage = ["inference get [--json]"];
static examples = ["<%= config.bin %> inference get", "<%= config.bin %> inference get --json"];
static flags = {
json: Flags.boolean({
description: "Print provider and model as JSON",
}),
};

public async run(): Promise<void> {
const { flags } = await this.parse(InferenceGetCommand);
try {
await runInferenceGet({ json: flags.json === true });
} catch (error) {
if (error instanceof InferenceGetError) {
this.error(error.message, { exit: error.exitCode });
}
throw error;
}
}
}
Loading
Loading