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
2 changes: 1 addition & 1 deletion ci/platform-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"name": "OpenRouter",
"status": "tested",
"endpoint_type": "OpenAI-compatible",
"notes": "First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with `OPENAI_BASE_URL=https://openrouter.ai/api/v1`; host-side validation and catalog probes send the default OpenRouter attribution headers."
"notes": "First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with a host runtime adapter URL; host-side validation and runtime traffic send the default OpenRouter attribution headers."
},
{
"name": "OpenAI",
Expand Down
19 changes: 11 additions & 8 deletions docs/inference/inference-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ This setting is generated when NemoClaw builds or recreates the sandbox image.
| Provider | Status | Endpoint type | Notes |
|----------|--------|---------------|-------|
| NVIDIA Endpoints | Tested | OpenAI-compatible | Hosted models on integrate.api.nvidia.com |
| OpenRouter | Tested | OpenAI-compatible | First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with `OPENAI_BASE_URL=https://openrouter.ai/api/v1`; host-side validation and catalog probes send the default OpenRouter attribution headers. |
| OpenRouter | Tested | OpenAI-compatible | First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with a host runtime adapter URL; host-side validation and runtime traffic send the default OpenRouter attribution headers. |
| OpenAI | Tested | Native OpenAI-compatible | Uses OpenAI model IDs |
| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Custom base-URL adapter for servers that implement OpenAI-compatible `/v1/chat/completions` or `/v1/responses`. Behavior on OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints. |
| Anthropic | Tested | Native Anthropic | Uses anthropic-messages |
Expand All @@ -89,24 +89,27 @@ The managed install/start vLLM entry appears by default on DGX Spark and DGX Sta
### Catalog and Default Model Selection

During interactive NVIDIA Endpoints onboarding, NemoClaw loads NVIDIA's public featured model catalog once per onboarding session and reports progress before displaying the model picker.
OpenRouter uses the same catalog-backed picker flow with its own OpenRouter featured model catalog, provider route, and credential check.

OpenRouter uses the same catalog-backed picker flow and the same featured model list as NVIDIA Endpoints, with its own provider route and credential check.
NVIDIA Endpoints excludes NVIDIA-retired or unsafe choices and corrects known catalog lag before displaying the result.
OpenRouter does not inherit the NVIDIA-only retired-model filter, so a model can appear in OpenRouter onboarding after it leaves NVIDIA Endpoints.
For OpenRouter runtime traffic, NemoClaw starts or reuses a local host adapter on port `11437` by default and registers OpenShell with `OPENAI_BASE_URL=http://host.openshell.internal:11437/v1`.
The adapter accepts only the OpenShell `Authorization: Bearer <OPENROUTER_API_KEY>` value bound during setup, forwards requests to `https://openrouter.ai/api/v1`, and injects the OpenRouter attribution headers `HTTP-Referer` and `X-OpenRouter-Title`.
The adapter does not store the API key.
It exposes a host health endpoint and writes logs to `~/.nemoclaw/openrouter-runtime-adapter.log`.
Set `NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT` before onboarding to use a different host port.
If the catalog is unavailable, malformed, or contains no safe model IDs, the wizard warns you and uses the bundled fallback list shown below.

Nemotron 3 Super remains the shared default for OpenClaw and Hermes when it is present.
LangChain Deep Agents Code uses Nemotron 3 Ultra as its NVIDIA Endpoints default.
If an agent's default is unavailable, the first live featured model becomes the interactive default.

If you set `NEMOCLAW_MODEL` to a safe custom model ID that is absent from the live catalog, it does not replace the live menu default.
Choose **Other** to use that value as the pre-filled manual entry; NemoClaw validates it against NVIDIA Endpoints before continuing.
Choose **Other** to use that value as the pre-filled manual entry; NemoClaw validates it against the selected provider before continuing.
NemoClaw neither displays nor accepts an unsafe `NEMOCLAW_MODEL` value as the manual-entry prefill.

| Option | Description | Curated models |
|--------|-------------|----------------|
| NVIDIA Endpoints | Routes to models hosted on [build.nvidia.com](https://build.nvidia.com). You can also enter any model ID from the catalog. Set `NVIDIA_INFERENCE_API_KEY`. | Nemotron 3 Ultra 550B, Nemotron 3 Super 120B, Minimax M3 |
| OpenRouter | Routes OpenClaw, Hermes, and Deep Agents Code through OpenRouter's OpenAI-compatible Chat Completions endpoint. Set `OPENROUTER_API_KEY`; valid keys begin with `sk-or-`. NemoClaw validates manual model entries with OpenRouter and keeps the sandbox on `https://inference.local/v1`. | OpenRouter featured catalog models, or any OpenRouter model ID you enter manually. |
| OpenRouter | Routes OpenClaw, Hermes, and Deep Agents Code through OpenRouter's OpenAI-compatible Chat Completions endpoint. Set `OPENROUTER_API_KEY`; valid keys begin with `sk-or-`. NemoClaw validates manual model entries with OpenRouter and keeps the sandbox on `https://inference.local/v1`. | NVIDIA Endpoints featured catalog models, or any OpenRouter model ID you enter manually. |
| OpenAI | Routes to the OpenAI API. Set `OPENAI_API_KEY`. | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.4-pro-2026-03-05` |
| Other OpenAI-compatible endpoint | Routes to any server that implements `/v1/chat/completions`. NemoClaw uses `/v1/chat/completions` at runtime by default; set `NEMOCLAW_PREFERRED_API=openai-responses` to allow `/v1/responses` for proxies that implement it, such as some llama.cpp builds. The wizard prompts for a base URL and model name. Behavior on OpenAI-compatible proxies, gateways, and self-hosted implementations such as LocalAI or llama.cpp may vary. When you enable Telegram messaging, onboarding also runs a bounded sandbox-side smoke check through `https://inference.local/v1/chat/completions`. Set `COMPATIBLE_API_KEY`. | You provide the model name. |
| Anthropic | Routes to the Anthropic Messages API. Set `ANTHROPIC_API_KEY`. | `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-6` |
Expand All @@ -130,7 +133,7 @@ Managed provider defaults that do not supply an explicit custom endpoint through
NemoClaw accepts `http://host.openshell.internal:<port>` only with an explicit port from `1024` through `65535`; this narrow exception supports NemoClaw's sandbox-to-host inference routes and is not a general private-endpoint bypass.

<Note>
NVIDIA Endpoints, OpenRouter, and Hermes Provider use independent model catalogs, so a model can remain available through one provider after it leaves another provider's curated list.
NVIDIA Endpoints and OpenRouter share the same curated cloud model picker, while Hermes Provider uses its own catalog.
Curated-list updates affect new onboarding choices and do not rewrite existing sandbox configurations.
Use [Switch Inference Providers](switch-inference-providers) to move an existing sandbox before its configured model becomes unavailable.
</Note>
Expand Down Expand Up @@ -291,7 +294,7 @@ OpenRouter credentials are validated as non-empty `sk-or-` keys.
|---|---|
| OpenAI | Tries `/responses` first, then `/chat/completions`. |
| NVIDIA Endpoints | Validates through `/v1/chat/completions` only; NemoClaw skips the `/v1/responses` probe because NVIDIA Build does not expose `/v1/responses` (returns 404 for every model). |
| OpenRouter | Validates through `/v1/chat/completions` only. NemoClaw skips the `/v1/responses` probe and uses managed Chat Completions for OpenClaw, Hermes, and Deep Agents Code. |
| OpenRouter | Host-side catalog, model, and smoke validation use `/v1/chat/completions` only. NemoClaw skips the `/v1/responses` probe and uses managed Chat Completions for OpenClaw, Hermes, and Deep Agents Code. |
| Google Gemini | Validates through Gemini's OpenAI-compatible chat-completions path only; NemoClaw skips the `/v1/responses` probe because Gemini does not support the Responses API. |
| Other OpenAI-compatible endpoint | Tries `/v1/responses` first with a tool-calling probe; falls back to `/v1/chat/completions`. Selected runtime API defaults to `/v1/chat/completions`; set `NEMOCLAW_PREFERRED_API=openai-responses` to allow `/v1/responses` at runtime when validation succeeds. |
| Other Anthropic-compatible endpoint | For agents that support Anthropic Messages, including OpenClaw, tries `/v1/messages` with a non-streaming request, then repeats the request with `stream: true` and validates the SSE event sequence. Set `NEMOCLAW_REASONING=true` to skip the streaming check for reasoning-only endpoints. For Hermes and OpenAI-compatible-only agents, validates `/v1/chat/completions`, the surface used by the managed OpenAI frontend. |
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3086,6 +3086,7 @@ Passthrough commands do not consume flags intended for the downstream command as
| `NEMOCLAW_VLLM_PORT` | 8000 | vLLM / NIM inference |
| `NEMOCLAW_OLLAMA_PORT` | 11434 | Ollama inference |
| `NEMOCLAW_OLLAMA_PROXY_PORT` | 11435 | Ollama auth proxy |
| `NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT` | 11437 | Host-side OpenRouter runtime adapter |
<AgentOnly variant="openclaw,hermes">
| `NEMOCLAW_DASHBOARD_BIND` | *unset* (loopback) | Dashboard or API forward bind address. Set to `0.0.0.0` to opt in to remote bind for SSH-deployed hosts. |
</AgentOnly>
Expand All @@ -3094,7 +3095,8 @@ Passthrough commands do not consume flags intended for the downstream command as
</AgentOnly>

If a port value is not a valid integer or falls outside the allowed range, the CLI exits with an error.
`NEMOCLAW_GATEWAY_PORT` also cannot overlap configured service, vLLM, Ollama, or Ollama proxy ports, and cannot use reserved auto-allocation ranges or the default inference/proxy ports `8000`, `11434`, and `11435`.
`NEMOCLAW_GATEWAY_PORT` also cannot overlap configured service, vLLM, Ollama, Ollama proxy, or OpenRouter runtime adapter ports, and cannot use reserved auto-allocation ranges or the default inference/proxy ports `8000`, `11434`, `11435`, and `11437`.
When you select OpenRouter, `NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT` must also be distinct from the gateway, vLLM, Ollama, and Ollama proxy ports.
When you run multiple NemoClaw gateways with different `NEMOCLAW_GATEWAY_PORT` values, NemoClaw derives a separate gateway name, state directory, and compatibility container name from the port so one gateway does not tear down another.
On non-WSL hosts, `NEMOCLAW_OLLAMA_PORT` and `NEMOCLAW_OLLAMA_PROXY_PORT` must be different.
If you run Ollama on port 11435, set `NEMOCLAW_OLLAMA_PROXY_PORT` to another free port before onboarding.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/platform-support.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro
| Provider | Status | Endpoint type | Notes |
|----------|--------|---------------|-------|
| NVIDIA Endpoints | Tested | OpenAI-compatible | Hosted models on integrate.api.nvidia.com |
| OpenRouter | Tested | OpenAI-compatible | First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with `OPENAI_BASE_URL=https://openrouter.ai/api/v1`; host-side validation and catalog probes send the default OpenRouter attribution headers. |
| OpenRouter | Tested | OpenAI-compatible | First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with a host runtime adapter URL; host-side validation and runtime traffic send the default OpenRouter attribution headers. |
| OpenAI | Tested | Native OpenAI-compatible | Uses OpenAI model IDs |
| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Custom base-URL adapter for servers that implement OpenAI-compatible `/v1/chat/completions` or `/v1/responses`. Behavior on OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints. |
| Anthropic | Tested | Native Anthropic | Uses anthropic-messages |
Expand Down
189 changes: 189 additions & 0 deletions src/lib/actions/uninstall/openrouter-runtime-adapter-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

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

import { type RunResult, runUninstallPlan } from "./run-plan";

function ok(stdout = ""): RunResult {
return { status: 0, stdout, stderr: "" };
}

function notFound(): RunResult {
return { status: 1, stdout: "", stderr: "" };
}

const OPENROUTER_RUNTIME_ADAPTER_CMDLINE =
"/usr/bin/node /home/test/NemoClaw/dist/lib/inference/openrouter-runtime-adapter-entry.js\n";

type RunStub = (args: readonly string[]) => RunResult | null;

function psStub(pidStr: string, opts: { exited: Set<number>; cmdline?: string; owner?: string }) {
const pid = Number(pidStr);
const responses = new Map<string, () => RunResult>([
[
["-p", pidStr, "-o", "pid="].join("\0"),
() => (opts.exited.has(pid) ? notFound() : ok(`${pidStr}\n`)),
],
[["-p", pidStr, "-o", "user="].join("\0"), () => ok(`${opts.owner ?? "testuser"}\n`)],
[
["-p", pidStr, "-o", "args="].join("\0"),
() => ok(opts.cmdline ?? OPENROUTER_RUNTIME_ADAPTER_CMDLINE),
],
]);

return (args: readonly string[]): RunResult | null => {
return responses.get(args.join("\0"))?.() ?? null;
};
}

function defaultRun(command: string, args: readonly string[]): RunResult {
switch (command) {
case "lsof":
return ok("");
default:
switch (args[0]) {
case "-c":
return ok("/fake/bin/tool\n");
case "-f":
return ok("");
default:
return ok();
}
}
}

function runStub(routes: Record<string, RunStub> = {}) {
return (command: string, args: readonly string[]): RunResult => {
return routes[command]?.(args) ?? defaultRun(command, args);
};
}

function lsofPortStub(ports: string[], portPids: Map<string, RunResult>) {
return (args: readonly string[]): RunResult => {
const port = args[1] ?? "";
ports.push(port);
return portPids.get(port) ?? ok("");
};
}

describe("OpenRouter Runtime adapter uninstall cleanup", () => {
it("kills the adapter via the persisted PID file (#5826)", () => {
const logs: string[] = [];
const killed: number[] = [];
const exited = new Set<number>();
const tmpHome = fs.mkdtempSync(
path.join(os.tmpdir(), "nemoclaw-uninstall-test-openrouter-pidfile-"),
);
const pidFile = path.join(tmpHome, ".nemoclaw", "openrouter-runtime-adapter.pid");
fs.mkdirSync(path.join(tmpHome, ".nemoclaw"), { recursive: true });
fs.writeFileSync(pidFile, "44323\n");

try {
const stub = psStub("44323", { exited });
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: () => true,
env: { HOME: tmpHome, LOGNAME: "testuser" } as NodeJS.ProcessEnv,
existsSync: (target) => target === pidFile,
isTty: false,
kill: (pid, _signal) => {
killed.push(pid);
exited.add(pid);
return true;
},
log: (line) => logs.push(line),
rmSync: vi.fn(),
run: runStub({ ps: stub }),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(killed).toContain(44323);
expect(logs).toContain("Stopped OpenRouter Runtime adapter 44323");
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("scans the custom adapter port for orphan adapters (#5826)", () => {
const logs: string[] = [];
const killed: number[] = [];
const exited = new Set<number>();
const lsofPorts: string[] = [];
const stub = psStub("33334", { exited });
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: () => true,
env: {
HOME: "/tmp/nemoclaw-uninstall-test-openrouter-custom-port",
LOGNAME: "testuser",
NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT: "12037",
} as NodeJS.ProcessEnv,
existsSync: () => false,
isTty: false,
kill: (pid, _signal) => {
killed.push(pid);
exited.add(pid);
return true;
},
log: (line) => logs.push(line),
rmSync: vi.fn(),
run: runStub({
lsof: lsofPortStub(lsofPorts, new Map([[":12037", ok("33334\n")]])),
ps: stub,
}),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(lsofPorts).toContain(":12037");
expect(lsofPorts).not.toContain(":11437");
expect(killed).toContain(33334);
expect(logs).toContain("Stopped OpenRouter Runtime adapter 33334");
});

it("never kills a process on the adapter port whose cmdline does not match (#5826)", () => {
const logs: string[] = [];
const killed: number[] = [];
const stub = psStub("99998", {
exited: new Set(),
cmdline: "/usr/sbin/nginx -g daemon off;\n",
});
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: () => true,
env: {
HOME: "/tmp/nemoclaw-uninstall-test-openrouter-foreign",
LOGNAME: "testuser",
} as NodeJS.ProcessEnv,
existsSync: () => false,
isTty: false,
kill: (pid) => {
killed.push(pid);
return true;
},
log: (line) => logs.push(line),
rmSync: vi.fn(),
run: runStub({
lsof: lsofPortStub([], new Map([[":11437", ok("99998\n")]])),
ps: stub,
}),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(killed).not.toContain(99998);
expect(logs).toContain("No OpenRouter Runtime adapter processes found");
});
});
Loading
Loading