diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json
index 1b418358a4f..6b3971b2134 100644
--- a/ci/platform-matrix.json
+++ b/ci/platform-matrix.json
@@ -93,7 +93,7 @@
"name": "Other OpenAI-compatible endpoint",
"status": "caveated",
"endpoint_type": "Custom OpenAI-compatible",
- "notes": "Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3673`. Behavior on other OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints."
+ "notes": "Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3661`. Behavior on other OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints."
},
{
"name": "Anthropic",
@@ -129,7 +129,7 @@
"name": "Local NVIDIA NIM",
"status": "experimental",
"endpoint_type": "Local OpenAI-compatible",
- "notes": "Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1586`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`."
+ "notes": "Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1632`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`."
},
{
"name": "Local vLLM (already running)",
@@ -218,7 +218,7 @@
{
"name": "Podman / other container runtimes",
"status": "unsupported",
- "notes": "Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard.ts:1611` prints the rejection; `src/lib/onboard/preflight.ts:586` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed)."
+ "notes": "Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard.ts:1599` prints the rejection; `src/lib/onboard/preflight.ts:586` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed)."
},
{
"name": "Intel Mac (macOS x86_64)",
@@ -248,7 +248,7 @@
{
"name": "Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal)",
"status": "unsupported",
- "notes": "Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard.ts:1586`, called at `:1651`). NemoClaw does not install non-NVIDIA accelerator drivers."
+ "notes": "Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard.ts:1632`). NemoClaw does not install non-NVIDIA accelerator drivers."
},
{
"name": "Other LangChain, AutoGen, CrewAI, or non-listed agent harnesses",
diff --git a/docs/inference/inference-options.mdx b/docs/inference/inference-options.mdx
index 7619769f722..77936673aa9 100644
--- a/docs/inference/inference-options.mdx
+++ b/docs/inference/inference-options.mdx
@@ -43,13 +43,13 @@ NemoClaw uses provider-specific local tokens for those routes, and rebuilds of l
|----------|--------|---------------|-------|
| NVIDIA Endpoints | Tested | OpenAI-compatible | Hosted models on integrate.api.nvidia.com |
| OpenAI | Tested | Native OpenAI-compatible | Uses OpenAI model IDs |
-| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3673`. Behavior on other OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints. |
+| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3661`. Behavior on other 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 |
| Other Anthropic-compatible endpoint | Tested with limitations | Custom Anthropic-compatible | Adapter path validated with AWS Bedrock (`src/lib/onboard/bedrock-runtime.ts`). Behavior on other Anthropic-compatible proxies and gateways may vary; this row claims the adapter, not the universe of compatible endpoints. |
| Google Gemini | Tested | OpenAI-compatible | Uses Google's OpenAI-compatible endpoint |
| Hermes Provider | Hermes only | OpenAI-compatible route | Available when onboarding Hermes Agent through `nemohermes` |
| Local Ollama | Tested with limitations | Local Ollama API | Available when Ollama is installed or running on the host. Validated default models: `qwen3.6:35b` (high VRAM), `nemotron-3-nano:30b` (medium VRAM), `qwen3.5:9b` (low VRAM fallback). |
-| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1586`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
+| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1632`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local vLLM (already running) | Tested with limitations | Local OpenAI-compatible | Appears in the onboarding menu when NemoClaw detects a server already on `localhost:8000`. No flag required. Model is whatever the existing server serves. |
| Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Appears by default on DGX Spark and DGX Station. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence). NemoClaw pulls or starts the stable NGC vLLM container for each host profile. See `src/lib/inference/vllm.ts:55,177` for the pins. DGX Spark and DGX Station use `nvcr.io/nvidia/vllm:26.05.post1-py3`; generic Linux NVIDIA GPU hosts use `nvcr.io/nvidia/vllm:26.03.post1-py3`. Validated defaults are listed in `src/lib/inference/vllm-models.ts`: DGX Spark uses `nvidia/Qwen3.6-35B-A3B-NVFP4`, DGX Station uses `deepseek-ai/DeepSeek-V4-Flash`, and Linux NVIDIA GPU uses `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8`. Image pulls require NGC registry login (`docker login nvcr.io`); onboard prompts for the NGC API key when authentication is missing. |
{/* provider-status:end */}
@@ -237,11 +237,18 @@ Route, configuration, and authentication failures still fail immediately.
### Non-Interactive Setup
Set the following environment variables for scripted or CI/CD deployments.
+Set `NEMOCLAW_REASONING=true` when the compatible endpoint serves a reasoning-only model.
+
+
+ Reasoning mode validates only `/v1/chat/completions`; it does not verify tool calling or
+ streaming. Enable it only when the endpoint supports the capabilities your agent needs.
+
```bash
NEMOCLAW_PROVIDER=custom \
NEMOCLAW_ENDPOINT_URL=http://localhost:8000/v1 \
NEMOCLAW_MODEL=meta-llama/Llama-3.1-8B-Instruct \
+ NEMOCLAW_REASONING=true \
COMPATIBLE_API_KEY=dummy \
$$nemoclaw onboard --non-interactive
```
@@ -251,6 +258,7 @@ NEMOCLAW_PROVIDER=custom \
| `NEMOCLAW_PROVIDER` | Set to `custom` for an OpenAI-compatible endpoint. |
| `NEMOCLAW_ENDPOINT_URL` | Base URL of the local server. |
| `NEMOCLAW_MODEL` | Model ID as reported by the server. |
+| `NEMOCLAW_REASONING` | Set to `true` for a reasoning-only compatible endpoint model. Onboarding accepts `true`, `1`, `yes`, and `y` case-insensitively, then stores `true`; the default is `false`. |
| `COMPATIBLE_API_KEY` | API key for the endpoint. Use any non-empty value if authentication is not required. |
### Selecting the API Path
diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx
index f980dc0c6d5..279da8e1967 100644
--- a/docs/reference/platform-support.mdx
+++ b/docs/reference/platform-support.mdx
@@ -95,13 +95,13 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro
|----------|--------|---------------|-------|
| NVIDIA Endpoints | Tested | OpenAI-compatible | Hosted models on integrate.api.nvidia.com |
| OpenAI | Tested | Native OpenAI-compatible | Uses OpenAI model IDs |
-| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3673`. Behavior on other OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints. |
+| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Adapter path validated against OpenRouter as the `compatible-endpoint` provider with `openrouter/auto` (see `src/lib/inference/config.test.ts:119`); the onboarding prompt that surfaces OpenRouter as the worked example is at `src/lib/onboard.ts:3661`. Behavior on other 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 |
| Other Anthropic-compatible endpoint | Tested with limitations | Custom Anthropic-compatible | Adapter path validated with AWS Bedrock (`src/lib/onboard/bedrock-runtime.ts`). Behavior on other Anthropic-compatible proxies and gateways may vary; this row claims the adapter, not the universe of compatible endpoints. |
| Google Gemini | Tested | OpenAI-compatible | Uses Google's OpenAI-compatible endpoint |
| Hermes Provider | Hermes only | OpenAI-compatible route | Available when onboarding Hermes Agent through `nemohermes` |
| Local Ollama | Tested with limitations | Local Ollama API | Available when Ollama is installed or running on the host. Validated default models: `qwen3.6:35b` (high VRAM), `nemotron-3-nano:30b` (medium VRAM), `qwen3.5:9b` (low VRAM fallback). |
-| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1586`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
+| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard.ts:1632`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local vLLM (already running) | Tested with limitations | Local OpenAI-compatible | Appears in the onboarding menu when NemoClaw detects a server already on `localhost:8000`. No flag required. Model is whatever the existing server serves. |
| Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Appears by default on DGX Spark and DGX Station. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence). NemoClaw pulls or starts the stable NGC vLLM container for each host profile. See `src/lib/inference/vllm.ts:55,177` for the pins. DGX Spark and DGX Station use `nvcr.io/nvidia/vllm:26.05.post1-py3`; generic Linux NVIDIA GPU hosts use `nvcr.io/nvidia/vllm:26.03.post1-py3`. Validated defaults are listed in `src/lib/inference/vllm-models.ts`: DGX Spark uses `nvidia/Qwen3.6-35B-A3B-NVFP4`, DGX Station uses `deepseek-ai/DeepSeek-V4-Flash`, and Linux NVIDIA GPU uses `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8`. Image pulls require NGC registry login (`docker login nvcr.io`); onboard prompts for the NGC API key when authentication is missing. |
{/* provider-status-full:end */}
@@ -160,13 +160,13 @@ They are listed here so launch material, sales conversations, and support triage
{/* out-of-scope:begin */}
| Item | Status | Why |
|------|--------|-----|
-| Podman / other container runtimes | Unsupported | Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard.ts:1611` prints the rejection; `src/lib/onboard/preflight.ts:586` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed). |
+| Podman / other container runtimes | Unsupported | Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard.ts:1599` prints the rejection; `src/lib/onboard/preflight.ts:586` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed). |
| Intel Mac (macOS x86_64) | Unsupported | OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:315`). See issue #954 (closed). |
| Non-Ubuntu/Debian Linux distros | Unsupported | Installer assumes `apt-get`. Fedora/Rocky/Alma/Arch/NixOS are not validated and the installer's package-manager probes do not cover them. See open issue #899 (Fedora hang). |
| Native Kubernetes or OpenShift deployments | Unsupported | NemoClaw runs the sandbox as a Docker container, not a Kubernetes pod. The default Docker-driver topology does not embed k3s. Operator-managed K8s/OpenShift deployments are out of scope; see issue #407 (community OpenShift through agent-sandbox CRD). |
| Air-gapped / offline installs | Unsupported | Onboard assumes network reachability for package fetches, container pulls, and provider validation. See open issues #4872 and #2218 (production-deployment epic covering air-gapped support, China network guidance, multi-host topology). |
| Windows-on-ARM GPU passthrough | Unsupported | Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:188`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565. |
-| Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal) | Unsupported | Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard.ts:1586`, called at `:1651`). NemoClaw does not install non-NVIDIA accelerator drivers. |
+| Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal) | Unsupported | Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard.ts:1632`). NemoClaw does not install non-NVIDIA accelerator drivers. |
| Other LangChain, AutoGen, CrewAI, or non-listed agent harnesses | Unsupported | LangChain Deep Agents Code is the only integrated LangChain-family harness (see the Agents section above; status `Experimental`). Other LangChain harnesses, AutoGen, CrewAI, and any agent runtime not listed in the Agents table are not integrated. Bringing more harnesses is tracked as a research epic (see open issue #4861) but is not on the current roadmap. |
| Multi-user host sharing | Unsupported | Sandboxes are scoped to a single host user. NemoClaw treats multi-user hosts as a risk and warns at onboard; see `docs/security/openclaw-controls.mdx` Multi-user detection. |
| Hosted SaaS / managed NemoClaw | Unsupported | There is no managed offering. Supported deployment paths are Local CLI onboard, Remote GPU with Brev CLI, and Brev web UI. |
diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts
index f11ab5d0593..442b83c3eea 100644
--- a/src/lib/onboard.ts
+++ b/src/lib/onboard.ts
@@ -9,6 +9,8 @@ const {
envInt,
LOCAL_INFERENCE_TIMEOUT_SECS,
}: typeof import("./onboard/env") = require("./onboard/env");
+type ProviderSelectionResult =
+ import("./onboard/machine/handlers/provider-inference").ProviderSelectionResult;
const {
agentProductName,
cliDisplayName,
@@ -27,10 +29,9 @@ const {
createRemoteModelValidator,
requireProviderChoice,
}: typeof import("./onboard/setup-nim-selection") = require("./onboard/setup-nim-selection");
-const {
- createSetupNimOllamaHandlers,
-}: typeof import("./onboard/setup-nim-ollama") = require("./onboard/setup-nim-ollama");
+const setupNimOllama: typeof import("./onboard/setup-nim-ollama") = require("./onboard/setup-nim-ollama");
const inferenceInputCapability = require("./onboard/inference-input-capability");
+const reasoningMode: typeof import("./onboard/reasoning-mode") = require("./onboard/reasoning-mode");
const { cleanupTempDir }: typeof import("./onboard/temp-files") = require("./onboard/temp-files");
const {
abortNonInteractive,
@@ -1047,6 +1048,7 @@ const { validateSelectedRemoteModel } = createRemoteModelValidator({
shouldRequireResponsesToolCalling,
shouldSkipResponsesProbe,
getProbeAuthMode,
+ configureCompatibleEndpointReasoning: reasoningMode.configureCompatibleEndpointReasoning,
});
const { promptCloudModel, promptRemoteModel, promptInputModel } = modelPrompts;
@@ -1068,7 +1070,7 @@ const {
handleWindowsHostOllamaSelection,
handleRunningOllamaSelection,
handleInstallOllamaSelection,
-} = createSetupNimOllamaHandlers({
+} = setupNimOllama.createSetupNimOllamaHandlers({
OLLAMA_PORT,
OLLAMA_PROXY_PORT,
process,
@@ -3926,18 +3928,7 @@ async function setupNim(
sandboxName: string | null = null,
agent: AgentDefinition | null = null,
recoverProvider = true,
-): Promise<{
- model: string | null;
- provider: string;
- endpointUrl: string | null;
- credentialEnv: string | null;
- hermesAuthMethod: HermesAuthMethod | null;
- hermesToolGateways: string[];
- preferredInferenceApi: string | null;
- nimContainer: string | null;
- allowToolsIncompatible: boolean;
- skipHostInferenceSmoke: boolean;
-}> {
+): Promise {
step(3, 8, "Configuring inference provider");
let model: string | typeof BACK_TO_SELECTION | null = null;
@@ -3948,6 +3939,7 @@ async function setupNim(
let hermesAuthMethod: HermesAuthMethod | null = null;
let hermesToolGateways: string[] = [];
let preferredInferenceApi: string | null = null;
+ let compatibleEndpointReasoning: string | null = null;
let allowToolsIncompatible = false;
let skipHostInferenceSmoke = false;
@@ -4080,6 +4072,7 @@ async function setupNim(
hermesAuthMethod,
hermesToolGateways,
preferredInferenceApi,
+ compatibleEndpointReasoning,
nimContainer,
allowToolsIncompatible,
};
@@ -4097,6 +4090,7 @@ async function setupNim(
preferredInferenceApi,
allowToolsIncompatible,
} = state);
+ compatibleEndpointReasoning = state.compatibleEndpointReasoning ?? null;
skipHostInferenceSmoke = state.skipHostInferenceSmoke === true;
if (result === "retry-selection") continue selectionLoop;
break;
@@ -4296,6 +4290,8 @@ async function setupNim(
}
}
+ if (provider !== "compatible-endpoint")
+ compatibleEndpointReasoning = reasoningMode.clearCompatibleEndpointReasoning();
const selectedModel = isBackToSelection(model) ? null : model;
await inferenceInputCapability.maybePromptForInferenceInputCapability(selectedModel, {
isNonInteractive,
@@ -4309,6 +4305,7 @@ async function setupNim(
hermesAuthMethod,
hermesToolGateways,
preferredInferenceApi,
+ compatibleEndpointReasoning,
nimContainer,
allowToolsIncompatible,
skipHostInferenceSmoke,
@@ -4888,6 +4885,7 @@ async function onboard(opts: OnboardOptions = {}): Promise {
hermesAuthMethod: normalizeHermesAuthMethod(session?.hermesAuthMethod),
hermesToolGateways: normalizeHermesToolGatewaySelections(session?.hermesToolGateways),
preferredInferenceApi: session?.preferredInferenceApi || null,
+ compatibleEndpointReasoning: session?.compatibleEndpointReasoning || null,
nimContainer: session?.nimContainer || null,
webSearchConfig: session?.webSearchConfig || null,
webSearchSupported: false,
@@ -5028,6 +5026,8 @@ async function onboard(opts: OnboardOptions = {}): Promise {
recordStateSkipped,
recordRepairEvent,
hydrateCredentialEnv,
+ configureCompatibleEndpointReasoning: reasoningMode.configureCompatibleEndpointReasoning,
+ clearCompatibleEndpointReasoning: reasoningMode.clearCompatibleEndpointReasoning,
repairLocalInferenceSystemdOverrideOrExit,
isNonInteractive,
getOpenshellBinary,
diff --git a/src/lib/onboard/compatible-endpoint-smoke.test.ts b/src/lib/onboard/compatible-endpoint-smoke.test.ts
index 7ddfbf4ebaa..ac18b7d45ee 100644
--- a/src/lib/onboard/compatible-endpoint-smoke.test.ts
+++ b/src/lib/onboard/compatible-endpoint-smoke.test.ts
@@ -81,7 +81,7 @@ describe("compatible endpoint sandbox smoke helpers", () => {
expect(script).toContain("INFERENCE_SMOKE_OK");
expect(script).toContain("models.providers.inference");
expect(script).toContain("https://inference.local/v1/chat/completions");
- expect(script).toContain("INITIAL_MAX_TOKENS=256");
+ expect(script).toContain("INITIAL_MAX_TOKENS=512");
expect(script).toContain("RETRY_MAX_TOKENS=1024");
expect(script).toContain("SMOKE_ATTEMPTS=3");
expect(script).toContain("SMOKE_REQUEST_TIMEOUT_SECONDS=60");
diff --git a/src/lib/onboard/compatible-endpoint-smoke.ts b/src/lib/onboard/compatible-endpoint-smoke.ts
index c938242a1a6..a67009ed8e3 100644
--- a/src/lib/onboard/compatible-endpoint-smoke.ts
+++ b/src/lib/onboard/compatible-endpoint-smoke.ts
@@ -197,6 +197,8 @@ export function verifyCompatibleEndpointSandboxSmoke(options: {
* Builds the shell script that runs inside the sandbox to confirm OpenClaw is
* routed through NemoClaw's managed inference provider and can receive assistant
* content from the compatible endpoint.
+ * Reasoning-only endpoints may fill 512 tokens in reasoning_content before final content;
+ * finish_reason=length retries at 1024 until providers offer non-reasoning output.
*/
export function buildCompatibleEndpointSandboxSmokeScript(
model: string,
@@ -204,7 +206,7 @@ export function buildCompatibleEndpointSandboxSmokeScript(
): string {
const configPath = options.configPath || "/sandbox/.openclaw/openclaw.json";
const inferenceUrl = options.inferenceUrl || `${INFERENCE_ROUTE_URL}/chat/completions`;
- const initialMaxTokens = positiveInt(options.initialMaxTokens, 256);
+ const initialMaxTokens = positiveInt(options.initialMaxTokens, 512);
const attempts = positiveInt(options.attempts, COMPATIBLE_ENDPOINT_SMOKE_ATTEMPTS);
const retryDelaySeconds = nonNegativeInt(
options.retryDelaySeconds,
diff --git a/src/lib/onboard/inference-selection-validation.test.ts b/src/lib/onboard/inference-selection-validation.test.ts
index 51ca9b3e694..46fcfe3deab 100644
--- a/src/lib/onboard/inference-selection-validation.test.ts
+++ b/src/lib/onboard/inference-selection-validation.test.ts
@@ -45,4 +45,45 @@ describe("inference selection validation", () => {
exit.mockRestore();
}
});
+
+ it("fails reasoning-mode validation when Chat Completions fails (#3279)", async () => {
+ vi.stubEnv("NEMOCLAW_REASONING", "yes");
+ const probeOpenAiLikeEndpoint = vi.fn(() => ({
+ ok: false,
+ failures: [{ name: "Chat Completions API", httpStatus: 500 }],
+ }));
+ const promptValidationRecovery = vi.fn(async () => "selection" as const);
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
+ const helpers = createInferenceSelectionValidationHelpers({
+ isNonInteractive: () => false,
+ agentProductName: () => "OpenClaw",
+ getCredential: () => "test-key",
+ probeOpenAiLikeEndpoint,
+ promptValidationRecovery,
+ });
+
+ try {
+ await expect(
+ helpers.validateCustomOpenAiLikeSelection(
+ "Custom endpoint",
+ "https://compatible.example/v1",
+ "reasoning-model",
+ "COMPATIBLE_API_KEY",
+ ),
+ ).resolves.toEqual({ ok: false, retry: "selection" });
+ expect(probeOpenAiLikeEndpoint).toHaveBeenCalledWith(
+ "https://compatible.example/v1",
+ "reasoning-model",
+ "test-key",
+ {
+ requireResponsesToolCalling: false,
+ skipResponsesProbe: true,
+ probeStreaming: false,
+ },
+ );
+ } finally {
+ error.mockRestore();
+ vi.unstubAllEnvs();
+ }
+ });
});
diff --git a/src/lib/onboard/inference-selection-validation.ts b/src/lib/onboard/inference-selection-validation.ts
index c8b0e8891d7..b65f4d2a90d 100644
--- a/src/lib/onboard/inference-selection-validation.ts
+++ b/src/lib/onboard/inference-selection-validation.ts
@@ -21,6 +21,7 @@ const { probeAnthropicEndpoint, probeOpenAiLikeEndpoint } =
import { shouldForceCompletionsApi } from "../validation";
import { getProbeRecovery } from "../validation-recovery";
import { summarizeProbeForDisplay } from "./probe-diagnostics";
+import { normalizeReasoningFlag } from "./reasoning-mode";
export type EndpointValidationResult =
| { ok: true; api: string | null; retry?: undefined }
@@ -185,10 +186,13 @@ export function createInferenceSelectionValidationHelpers(
helpUrl: string | null = null,
): Promise {
const apiKey = resolveCredential(credentialEnv);
+ const reasoningEnabled = normalizeReasoningFlag(process.env.NEMOCLAW_REASONING) === "true";
+ // Reasoning-only compatible endpoints often reject Responses, tool-call, and streaming probes.
const probe = runOpenAiLikeProbe(endpointUrl, model, apiKey, {
- requireResponsesToolCalling: true,
- skipResponsesProbe: shouldForceCompletionsApi(process.env.NEMOCLAW_PREFERRED_API),
- probeStreaming: true,
+ requireResponsesToolCalling: !reasoningEnabled,
+ skipResponsesProbe:
+ reasoningEnabled || shouldForceCompletionsApi(process.env.NEMOCLAW_PREFERRED_API),
+ probeStreaming: !reasoningEnabled,
});
if (probe.ok) {
if (probe.note) {
diff --git a/src/lib/onboard/machine/core-flow-phases.test.ts b/src/lib/onboard/machine/core-flow-phases.test.ts
index 9bf53e9b162..20ed7b60d45 100644
--- a/src/lib/onboard/machine/core-flow-phases.test.ts
+++ b/src/lib/onboard/machine/core-flow-phases.test.ts
@@ -40,6 +40,7 @@ function context(
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
@@ -91,6 +92,7 @@ function createPhases(
hermesAuthMethod: null,
hermesToolGateways: ["local"],
preferredInferenceApi: "chat",
+ compatibleEndpointReasoning: null,
nimContainer: "nim-test",
})),
setupInference: vi.fn(async () => ({ ok: true as const })),
@@ -107,6 +109,8 @@ function createPhases(
recordStateSkipped: vi.fn(async () => createSession()),
recordRepairEvent: vi.fn(async () => createSession()),
hydrateCredentialEnv: vi.fn(),
+ configureCompatibleEndpointReasoning: vi.fn(async () => "false" as const),
+ clearCompatibleEndpointReasoning: vi.fn(() => null),
repairLocalInferenceSystemdOverrideOrExit: vi.fn(),
isNonInteractive: () => true,
getOpenshellBinary: () => "openshell",
@@ -231,6 +235,7 @@ describe("core onboard flow phases", () => {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: "chat",
+ compatibleEndpointReasoning: null,
nimContainer: null,
}));
const [providerPhase] = createPhases({ providerDeps: { setupNim } });
diff --git a/src/lib/onboard/machine/core-flow-phases.ts b/src/lib/onboard/machine/core-flow-phases.ts
index de183ad4f7a..dec13cdddf8 100644
--- a/src/lib/onboard/machine/core-flow-phases.ts
+++ b/src/lib/onboard/machine/core-flow-phases.ts
@@ -69,6 +69,7 @@ export function createCoreOnboardFlowPhases<
hermesAuthMethod: context.hermesAuthMethod,
hermesToolGateways: context.hermesToolGateways,
preferredInferenceApi: context.preferredInferenceApi,
+ compatibleEndpointReasoning: context.compatibleEndpointReasoning,
nimContainer: context.nimContainer,
webSearchConfig: context.webSearchConfig,
},
@@ -89,6 +90,7 @@ export function createCoreOnboardFlowPhases<
hermesAuthMethod: providerInferenceResult.hermesAuthMethod,
hermesToolGateways: providerInferenceResult.hermesToolGateways,
preferredInferenceApi: providerInferenceResult.preferredInferenceApi,
+ compatibleEndpointReasoning: providerInferenceResult.compatibleEndpointReasoning,
nimContainer: providerInferenceResult.nimContainer,
webSearchConfig: providerInferenceResult.webSearchConfig,
}),
diff --git a/src/lib/onboard/machine/flow-context.test.ts b/src/lib/onboard/machine/flow-context.test.ts
index c2cf9d51dd1..1e015a20214 100644
--- a/src/lib/onboard/machine/flow-context.test.ts
+++ b/src/lib/onboard/machine/flow-context.test.ts
@@ -33,6 +33,7 @@ function baseContext(): OnboardFlowContext {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: "openai-responses",
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
});
@@ -133,6 +135,7 @@ describe("onboard flow context helpers", () => {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
});
diff --git a/src/lib/onboard/machine/flow-context.ts b/src/lib/onboard/machine/flow-context.ts
index 4fb20a38fdf..319f7c17bb2 100644
--- a/src/lib/onboard/machine/flow-context.ts
+++ b/src/lib/onboard/machine/flow-context.ts
@@ -21,6 +21,7 @@ export interface OnboardFlowContext {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: true,
diff --git a/src/lib/onboard/machine/flow-phases/preflight-gateway.test.ts b/src/lib/onboard/machine/flow-phases/preflight-gateway.test.ts
index ed0451c5d39..82cf445bee0 100644
--- a/src/lib/onboard/machine/flow-phases/preflight-gateway.test.ts
+++ b/src/lib/onboard/machine/flow-phases/preflight-gateway.test.ts
@@ -4,8 +4,8 @@
import { describe, expect, it, vi } from "vitest";
import { createSession } from "../../../state/onboard-session";
-import { advanceTo } from "../result";
import type { OnboardFlowContext } from "../flow-context";
+import { advanceTo } from "../result";
import { createGatewayPhase, createPreflightPhase } from "./preflight-gateway";
function context(): OnboardFlowContext {
@@ -25,6 +25,7 @@ function context(): OnboardFlowContext
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
diff --git a/src/lib/onboard/machine/flow-phases/provider-sandbox.test.ts b/src/lib/onboard/machine/flow-phases/provider-sandbox.test.ts
index ede225a01a3..5b99869b6a0 100644
--- a/src/lib/onboard/machine/flow-phases/provider-sandbox.test.ts
+++ b/src/lib/onboard/machine/flow-phases/provider-sandbox.test.ts
@@ -27,6 +27,7 @@ function context(
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
@@ -52,6 +53,7 @@ describe("provider/sandbox flow phases", () => {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: "openai-responses",
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
},
diff --git a/src/lib/onboard/machine/flow-sequence.test.ts b/src/lib/onboard/machine/flow-sequence.test.ts
index c4f45ba5059..663b0951d2f 100644
--- a/src/lib/onboard/machine/flow-sequence.test.ts
+++ b/src/lib/onboard/machine/flow-sequence.test.ts
@@ -38,6 +38,7 @@ function context(patch: Partial = {}): Context {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
diff --git a/src/lib/onboard/machine/flow-slices.test.ts b/src/lib/onboard/machine/flow-slices.test.ts
index aacae323785..cf95370c2d5 100644
--- a/src/lib/onboard/machine/flow-slices.test.ts
+++ b/src/lib/onboard/machine/flow-slices.test.ts
@@ -12,9 +12,6 @@ import {
type SessionUpdates,
} from "../../state/onboard-session";
import type { OnboardFlowContext } from "./flow-context";
-import { advanceTo, branchTo, completeOnboardMachine } from "./result";
-import { OnboardRuntime, type OnboardRuntimeDeps } from "./runtime";
-import type { OnboardSequencePhase } from "./sequence-runner";
import {
coreOnboardFlowPhases,
finalOnboardFlowPhases,
@@ -23,6 +20,9 @@ import {
runFinalOnboardFlowSequence,
runInitialOnboardFlowSequence,
} from "./flow-slices";
+import { advanceTo, branchTo, completeOnboardMachine } from "./result";
+import { OnboardRuntime, type OnboardRuntimeDeps } from "./runtime";
+import type { OnboardSequencePhase } from "./sequence-runner";
function cloneSession(session: Session): Session {
return normalizeSession(JSON.parse(JSON.stringify(session))) ?? session;
@@ -75,6 +75,7 @@ function context(): OnboardFlowContext {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
diff --git a/src/lib/onboard/machine/handlers/provider-inference.test.ts b/src/lib/onboard/machine/handlers/provider-inference.test.ts
index c31d303d4bb..3b1bc8829f3 100644
--- a/src/lib/onboard/machine/handlers/provider-inference.test.ts
+++ b/src/lib/onboard/machine/handlers/provider-inference.test.ts
@@ -1,9 +1,15 @@
// 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 { createSession, type Session, type SessionUpdates } from "../../../state/onboard-session";
+import { patchStagedDockerfile } from "../../dockerfile-patch";
+import { clearCompatibleEndpointReasoning } from "../../reasoning-mode";
import {
handleProviderInferenceState,
type ProviderInferenceStateOptions,
@@ -22,6 +28,7 @@ const baseSelection: ProviderSelectionResult = {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: "openai-responses",
+ compatibleEndpointReasoning: null,
nimContainer: null,
};
@@ -76,6 +83,9 @@ function createDeps(
recordStateSkipped: calls.recordSkip,
recordRepairEvent: calls.repairEvent,
hydrateCredentialEnv: calls.hydrate,
+ configureCompatibleEndpointReasoning: async (value?: string | null) =>
+ value === "true" ? "true" : "false",
+ clearCompatibleEndpointReasoning: () => null,
repairLocalInferenceSystemdOverrideOrExit: calls.repair,
isNonInteractive: () => true,
getOpenshellBinary: () => "/usr/bin/openshell",
@@ -123,6 +133,7 @@ function baseOptions(
hermesAuthMethod: session?.hermesAuthMethod ?? null,
hermesToolGateways: session?.hermesToolGateways ?? [],
preferredInferenceApi: session?.preferredInferenceApi ?? null,
+ compatibleEndpointReasoning: session?.compatibleEndpointReasoning ?? null,
nimContainer: session?.nimContainer ?? null,
webSearchConfig: session?.webSearchConfig ?? null,
},
@@ -145,10 +156,6 @@ describe("handleProviderInferenceState", () => {
expect(calls.startStep).toHaveBeenNthCalledWith(1, "provider_selection");
expect(calls.setupNim).toHaveBeenCalledWith({ type: "nvidia" }, null, null, true);
- expect(calls.complete).toHaveBeenCalledWith(
- "provider_selection",
- expect.objectContaining({ provider: "nvidia-prod" }),
- );
expect(calls.promptName).toHaveBeenCalledWith(null);
expect(calls.log).toHaveBeenCalledWith("summary:nvidia-prod/nvidia/test/my-assistant");
expect(calls.startStep).toHaveBeenNthCalledWith(2, "inference", {
@@ -171,6 +178,7 @@ describe("handleProviderInferenceState", () => {
model: "nvidia/test",
provider: "nvidia-prod",
preferredInferenceApi: "openai-responses",
+ compatibleEndpointReasoning: null,
});
expect(result.stateResult).toEqual({
type: "transition",
@@ -192,6 +200,72 @@ describe("handleProviderInferenceState", () => {
]);
});
+ describe("compatible endpoint reasoning mode", () => {
+ it("records reasoning state during provider selection", async () => {
+ const setupNim = vi.fn(async () => ({
+ ...baseSelection,
+ compatibleEndpointReasoning: "true",
+ provider: "compatible-endpoint",
+ credentialEnv: "COMPATIBLE_API_KEY",
+ }));
+ const { deps } = createDeps({ setupNim });
+
+ const result = await handleProviderInferenceState({
+ ...baseOptions(deps),
+ env: { NEMOCLAW_REASONING: "true" },
+ });
+
+ expect(result).toMatchObject({
+ compatibleEndpointReasoning: "true",
+ provider: "compatible-endpoint",
+ });
+ });
+
+ it("clears stale resumed state before writing a non-compatible artifact", async () => {
+ vi.stubEnv("NEMOCLAW_REASONING", "true");
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-reasoning-resume-"));
+ const dockerfilePath = path.join(tempDir, "Dockerfile");
+ fs.writeFileSync(dockerfilePath, "ARG NEMOCLAW_REASONING=false\n");
+ const session = createSession({
+ provider: "nvidia-prod",
+ model: "nvidia/test",
+ compatibleEndpointReasoning: "true",
+ });
+ session.steps.provider_selection.status = "complete";
+ const setupInference = vi.fn(async () => {
+ expect(process.env.NEMOCLAW_REASONING).toBeUndefined();
+ patchStagedDockerfile(
+ dockerfilePath,
+ "nvidia/test",
+ "https://chat.example",
+ "build-1",
+ "nvidia-prod",
+ );
+ return { ok: true as const };
+ });
+ const { deps } = createDeps({
+ clearCompatibleEndpointReasoning,
+ setupInference,
+ isInferenceRouteReady: vi.fn(() => false),
+ });
+
+ try {
+ const result = await handleProviderInferenceState({
+ ...baseOptions(deps, session),
+ resume: true,
+ sandboxName: "my-assistant",
+ });
+
+ expect(setupInference).toHaveBeenCalledOnce();
+ expect(result.compatibleEndpointReasoning).toBeNull();
+ expect(fs.readFileSync(dockerfilePath, "utf-8")).toContain("ARG NEMOCLAW_REASONING=false");
+ } finally {
+ vi.unstubAllEnvs();
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+ });
+
it("disables recorded provider recovery during fresh provider selection", async () => {
const { deps, calls } = createDeps();
diff --git a/src/lib/onboard/machine/handlers/provider-inference.ts b/src/lib/onboard/machine/handlers/provider-inference.ts
index c3a7f7d5bf1..a28d01dd1b9 100644
--- a/src/lib/onboard/machine/handlers/provider-inference.ts
+++ b/src/lib/onboard/machine/handlers/provider-inference.ts
@@ -16,6 +16,7 @@ export interface ProviderSelectionResult {
hermesAuthMethod: string | null;
hermesToolGateways: string[];
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning: string | null;
nimContainer: string | null;
allowToolsIncompatible?: boolean;
skipHostInferenceSmoke?: boolean;
@@ -37,6 +38,7 @@ export interface ProviderInferenceStateOptions {
hermesAuthMethod: string | null;
hermesToolGateways: string[];
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning: string | null;
nimContainer: string | null;
webSearchConfig: WebSearchConfig | null;
};
@@ -89,6 +91,8 @@ export interface ProviderInferenceStateOptions {
},
): Promise;
hydrateCredentialEnv(credentialEnv: string | null): string | null | undefined;
+ configureCompatibleEndpointReasoning(storedValue?: string | null): Promise<"true" | "false">;
+ clearCompatibleEndpointReasoning(): null;
repairLocalInferenceSystemdOverrideOrExit(
provider: string | null,
isNonInteractive: () => boolean,
@@ -141,6 +145,7 @@ export interface ProviderInferenceStateResult {
hermesAuthMethod: string | null;
hermesToolGateways: string[];
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning: string | null;
nimContainer: string | null;
webSearchConfig: WebSearchConfig | null;
session: Session | null;
@@ -227,6 +232,7 @@ export async function handleProviderInferenceState({
: null);
let hermesToolGateways = initial.hermesToolGateways;
let preferredInferenceApi = initial.preferredInferenceApi;
+ let compatibleEndpointReasoning = initial.compatibleEndpointReasoning;
let nimContainer = initial.nimContainer;
const webSearchConfig = initial.webSearchConfig;
let forceProviderSelection = initialForceProviderSelection;
@@ -282,6 +288,10 @@ export async function handleProviderInferenceState({
: " [resume] Refreshing compatible-endpoint inference route for messaging.",
);
}
+ compatibleEndpointReasoning =
+ provider === "compatible-endpoint"
+ ? await deps.configureCompatibleEndpointReasoning(compatibleEndpointReasoning)
+ : deps.clearCompatibleEndpointReasoning();
if (provider === "ollama-local") {
const repairMetadata = { repair: "ollama-systemd-loopback" };
await deps.recordRepairEvent("state.repair.started", {
@@ -319,6 +329,7 @@ export async function handleProviderInferenceState({
hermesAuthMethod = selection.hermesAuthMethod;
hermesToolGateways = selection.hermesToolGateways;
preferredInferenceApi = selection.preferredInferenceApi;
+ compatibleEndpointReasoning = selection.compatibleEndpointReasoning;
nimContainer = selection.nimContainer;
allowToolsIncompatible = selection.allowToolsIncompatible === true;
skipHostInferenceSmoke = selection.skipHostInferenceSmoke === true;
@@ -341,6 +352,7 @@ export async function handleProviderInferenceState({
hermesAuthMethod,
hermesToolGateways,
preferredInferenceApi,
+ compatibleEndpointReasoning,
nimContainer,
}),
);
@@ -403,6 +415,7 @@ export async function handleProviderInferenceState({
provider,
model,
hermesAuthMethod,
+ compatibleEndpointReasoning,
nimContainer,
hermesToolGateways,
}),
@@ -443,6 +456,7 @@ export async function handleProviderInferenceState({
provider,
model,
hermesAuthMethod,
+ compatibleEndpointReasoning,
nimContainer,
hermesToolGateways,
}),
@@ -521,6 +535,7 @@ export async function handleProviderInferenceState({
provider,
model,
hermesAuthMethod,
+ compatibleEndpointReasoning,
nimContainer,
hermesToolGateways,
}),
@@ -542,6 +557,7 @@ export async function handleProviderInferenceState({
hermesAuthMethod,
hermesToolGateways,
preferredInferenceApi,
+ compatibleEndpointReasoning,
nimContainer,
webSearchConfig,
session,
diff --git a/src/lib/onboard/machine/initial-flow-phases.test.ts b/src/lib/onboard/machine/initial-flow-phases.test.ts
index 7801c0f2f80..54c6f270987 100644
--- a/src/lib/onboard/machine/initial-flow-phases.test.ts
+++ b/src/lib/onboard/machine/initial-flow-phases.test.ts
@@ -40,6 +40,7 @@ function context(overrides: Partial = {}): Context {
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: false,
diff --git a/src/lib/onboard/reasoning-mode.test.ts b/src/lib/onboard/reasoning-mode.test.ts
new file mode 100644
index 00000000000..2062f8b80a1
--- /dev/null
+++ b/src/lib/onboard/reasoning-mode.test.ts
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { afterEach, describe, expect, it } from "vitest";
+import {
+ clearCompatibleEndpointReasoning,
+ configureCompatibleEndpointReasoning,
+ normalizeReasoningFlag,
+} from "./reasoning-mode";
+
+describe("compatible endpoint reasoning mode", () => {
+ afterEach(() => {
+ delete process.env.NEMOCLAW_REASONING;
+ });
+
+ it("normalizes supported boolean aliases (#3279)", () => {
+ for (const value of ["true", "1", "yes", "y", " YES "]) {
+ expect(normalizeReasoningFlag(value)).toBe("true");
+ }
+ for (const value of ["false", "0", "no", "n", " NO "]) {
+ expect(normalizeReasoningFlag(value)).toBe("false");
+ }
+ expect(normalizeReasoningFlag("maybe")).toBeNull();
+ });
+
+ it("defaults an unset or invalid flag to false (#3279)", async () => {
+ await expect(configureCompatibleEndpointReasoning()).resolves.toBe("false");
+ expect(process.env.NEMOCLAW_REASONING).toBe("false");
+
+ process.env.NEMOCLAW_REASONING = "maybe";
+ await expect(configureCompatibleEndpointReasoning()).resolves.toBe("false");
+ expect(process.env.NEMOCLAW_REASONING).toBe("false");
+ });
+
+ it("restores stored state and clears it when the provider changes (#3279)", async () => {
+ process.env.NEMOCLAW_REASONING = "false";
+ await expect(configureCompatibleEndpointReasoning("yes")).resolves.toBe("true");
+ expect(process.env.NEMOCLAW_REASONING).toBe("true");
+
+ expect(clearCompatibleEndpointReasoning()).toBeNull();
+ expect(process.env.NEMOCLAW_REASONING).toBeUndefined();
+ });
+});
diff --git a/src/lib/onboard/reasoning-mode.ts b/src/lib/onboard/reasoning-mode.ts
new file mode 100644
index 00000000000..52502ef9d1a
--- /dev/null
+++ b/src/lib/onboard/reasoning-mode.ts
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export function normalizeReasoningFlag(value: string | null | undefined): "true" | "false" | null {
+ const normalized = String(value ?? "")
+ .trim()
+ .toLowerCase();
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "y") {
+ return "true";
+ }
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "n") {
+ return "false";
+ }
+ return null;
+}
+
+export async function configureCompatibleEndpointReasoning(
+ storedValue?: string | null,
+): Promise<"true" | "false"> {
+ const configured = normalizeReasoningFlag(storedValue ?? process.env.NEMOCLAW_REASONING);
+ process.env.NEMOCLAW_REASONING = configured ?? "false";
+ return process.env.NEMOCLAW_REASONING as "true" | "false";
+}
+
+export function clearCompatibleEndpointReasoning(): null {
+ delete process.env.NEMOCLAW_REASONING;
+ return null;
+}
diff --git a/src/lib/onboard/session-updates.ts b/src/lib/onboard/session-updates.ts
index cf520c28ca1..d9dd4f315b1 100644
--- a/src/lib/onboard/session-updates.ts
+++ b/src/lib/onboard/session-updates.ts
@@ -13,6 +13,7 @@ export interface OnboardSessionUpdateInput {
credentialEnv?: string | null;
hermesAuthMethod?: HermesAuthMethod | string | null;
preferredInferenceApi?: string | null;
+ compatibleEndpointReasoning?: string | null;
nimContainer?: string | null;
webSearchConfig?: WebSearchConfig | null;
policyPresets?: string[] | null;
@@ -47,6 +48,9 @@ export function toSessionUpdates(updates: OnboardSessionUpdateInput = {}): Sessi
if (updates.preferredInferenceApi !== undefined) {
normalized.preferredInferenceApi = toNullableString(updates.preferredInferenceApi);
}
+ if (updates.compatibleEndpointReasoning !== undefined) {
+ normalized.compatibleEndpointReasoning = toNullableString(updates.compatibleEndpointReasoning);
+ }
if (updates.nimContainer !== undefined)
normalized.nimContainer = toNullableString(updates.nimContainer);
if (updates.webSearchConfig !== undefined) normalized.webSearchConfig = updates.webSearchConfig;
diff --git a/src/lib/onboard/setup-nim-selection.test.ts b/src/lib/onboard/setup-nim-selection.test.ts
index 49b71d85bec..b224935d423 100644
--- a/src/lib/onboard/setup-nim-selection.test.ts
+++ b/src/lib/onboard/setup-nim-selection.test.ts
@@ -71,6 +71,8 @@ describe("createRemoteModelValidator", () => {
state.endpointUrl = "https://compatible.example/v1";
state.model = "model-a";
let calledEndpoint: string | null = null;
+ let configuredReasoning = false;
+ const logLines: string[] = [];
const { validateSelectedRemoteModel } = createRemoteModelValidator({
OPENAI_ENDPOINT_URL: "https://default-openai.example/v1",
ANTHROPIC_ENDPOINT_URL: "https://default-anthropic.example/v1",
@@ -89,6 +91,11 @@ describe("createRemoteModelValidator", () => {
shouldRequireResponsesToolCalling: () => false,
shouldSkipResponsesProbe: () => false,
getProbeAuthMode: () => undefined,
+ configureCompatibleEndpointReasoning: async () => {
+ configuredReasoning = true;
+ return "true";
+ },
+ log: (message) => logLines.push(message),
});
const result = await validateSelectedRemoteModel({
@@ -105,6 +112,11 @@ describe("createRemoteModelValidator", () => {
assert.equal(result, "selected");
assert.equal(calledEndpoint, "https://compatible.example/v1");
assert.equal(state.preferredInferenceApi, "openai-completions");
+ assert.equal(state.compatibleEndpointReasoning, "true");
+ assert.equal(configuredReasoning, true);
+ assert.deepEqual(logLines, [
+ " ⚠ Reasoning mode validates Chat Completions only; tools and streaming are unverified.",
+ ]);
});
it("maps provider validation model retries without mutating selected model state", async () => {
diff --git a/src/lib/onboard/setup-nim-selection.ts b/src/lib/onboard/setup-nim-selection.ts
index 406a3900cf2..5b8e1bc5491 100644
--- a/src/lib/onboard/setup-nim-selection.ts
+++ b/src/lib/onboard/setup-nim-selection.ts
@@ -11,6 +11,7 @@ export type SetupNimSelectionState = {
hermesAuthMethod: THermesAuthMethod | null;
hermesToolGateways: string[];
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning?: string | null;
nimContainer: string | null;
allowToolsIncompatible: boolean;
skipHostInferenceSmoke?: boolean;
@@ -115,6 +116,8 @@ type RemoteModelValidatorDeps = {
shouldRequireResponsesToolCalling: (provider: string) => boolean;
shouldSkipResponsesProbe: (provider: string) => boolean;
getProbeAuthMode: (provider: string) => ProbeAuthMode;
+ configureCompatibleEndpointReasoning?: () => Promise<"true" | "false">;
+ log?: (message: string) => void;
};
type ValidateSelectedRemoteModelArgs = {
@@ -150,6 +153,16 @@ export function createRemoteModelValidator(deps: RemoteModelValidatorDeps): {
`Missing model for ${remoteConfig.label}`,
);
if (selected.key === "custom") {
+ // Reasoning mode is OpenAI-compatible only; Anthropic/native providers use other formats.
+ const reasoning = await deps.configureCompatibleEndpointReasoning?.();
+ if (reasoning) {
+ state.compatibleEndpointReasoning = reasoning;
+ }
+ if (reasoning === "true") {
+ (deps.log ?? console.log)(
+ " ⚠ Reasoning mode validates Chat Completions only; tools and streaming are unverified.",
+ );
+ }
const validation = await deps.validateCustomOpenAiLikeSelection(
remoteConfig.label,
state.endpointUrl || deps.OPENAI_ENDPOINT_URL,
diff --git a/src/lib/state/onboard-session.test.ts b/src/lib/state/onboard-session.test.ts
index ba8c7505e1a..dda5efd6d74 100644
--- a/src/lib/state/onboard-session.test.ts
+++ b/src/lib/state/onboard-session.test.ts
@@ -443,6 +443,7 @@ describe("onboard session", () => {
endpointUrl: "https://example.com/v1",
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
preferredInferenceApi: "openai-completions",
+ compatibleEndpointReasoning: "true",
nimContainer: "nim-123",
policyPresets: ["pypi", "npm"],
apiKey: "nvapi-secret",
@@ -460,8 +461,12 @@ describe("onboard session", () => {
expect(loaded.endpointUrl).toBe("https://example.com/v1");
expect(loaded.credentialEnv).toBe("NVIDIA_INFERENCE_API_KEY");
expect(loaded.preferredInferenceApi).toBe("openai-completions");
+ expect(loaded.compatibleEndpointReasoning).toBe("true");
expect(loaded.nimContainer).toBe("nim-123");
expect(loaded.policyPresets).toEqual(["pypi", "npm"]);
+ expect(requireDebugSummary(session.summarizeForDebug()).compatibleEndpointReasoning).toBe(
+ "true",
+ );
expect("apiKey" in loaded).toBe(false);
expect(loaded.metadata.gatewayName).toBe("nemoclaw");
expect("token" in loaded.metadata).toBe(false);
@@ -591,7 +596,7 @@ describe("onboard session", () => {
});
it("accepts null as an explicit clear for every nullable string field", () => {
- // All six nullable fields that travel through filterSafeUpdates must
+ // All nullable fields that travel through filterSafeUpdates must
// support the null-clear contract. If any regresses to the old
// string-only guard, the test below catches it.
session.saveSession(session.createSession());
@@ -602,6 +607,7 @@ describe("onboard session", () => {
endpointUrl: "https://api.openai.com/v1",
credentialEnv: "OPENAI_API_KEY",
preferredInferenceApi: "openai-completions",
+ compatibleEndpointReasoning: "true",
nimContainer: "nim-abc",
});
@@ -612,6 +618,7 @@ describe("onboard session", () => {
endpointUrl: null,
credentialEnv: null,
preferredInferenceApi: null,
+ compatibleEndpointReasoning: null,
nimContainer: null,
});
@@ -622,6 +629,7 @@ describe("onboard session", () => {
expect(loaded.endpointUrl).toBeNull();
expect(loaded.credentialEnv).toBeNull();
expect(loaded.preferredInferenceApi).toBeNull();
+ expect(loaded.compatibleEndpointReasoning).toBeNull();
expect(loaded.nimContainer).toBeNull();
});
diff --git a/src/lib/state/onboard-session.ts b/src/lib/state/onboard-session.ts
index 7b199772a1d..6940c6fd0eb 100644
--- a/src/lib/state/onboard-session.ts
+++ b/src/lib/state/onboard-session.ts
@@ -100,6 +100,7 @@ export interface Session {
credentialEnv: string | null;
hermesAuthMethod: HermesAuthMethod | null;
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning: string | null;
nimContainer: string | null;
routerPid: number | null;
routerCredentialHash: string | null;
@@ -169,6 +170,7 @@ export interface SessionUpdates {
credentialEnv?: string | null;
hermesAuthMethod?: HermesAuthMethod | null;
preferredInferenceApi?: string | null;
+ compatibleEndpointReasoning?: string | null;
nimContainer?: string | null;
routerPid?: number;
routerCredentialHash?: string;
@@ -198,6 +200,7 @@ export interface DebugSessionSummary {
credentialEnv: string | null;
hermesAuthMethod: HermesAuthMethod | null;
preferredInferenceApi: string | null;
+ compatibleEndpointReasoning: string | null;
nimContainer: string | null;
hermesToolGateways: string[] | null;
policyPresets: string[] | null;
@@ -447,6 +450,7 @@ export function createSession(overrides: Partial = {}): Session {
credentialEnv: overrides.credentialEnv ?? null,
hermesAuthMethod: overrides.hermesAuthMethod ?? null,
preferredInferenceApi: overrides.preferredInferenceApi ?? null,
+ compatibleEndpointReasoning: overrides.compatibleEndpointReasoning ?? null,
nimContainer: overrides.nimContainer ?? null,
routerPid: readPositiveInteger(overrides.routerPid),
routerCredentialHash: overrides.routerCredentialHash ?? null,
@@ -489,6 +493,7 @@ export function normalizeSession(data: Session | SessionJsonValue | undefined):
credentialEnv: readString(data.credentialEnv),
hermesAuthMethod: readHermesAuthMethod(data.hermesAuthMethod),
preferredInferenceApi: readString(data.preferredInferenceApi),
+ compatibleEndpointReasoning: readString(data.compatibleEndpointReasoning),
nimContainer: readString(data.nimContainer),
routerPid: readPositiveInteger(data.routerPid),
routerCredentialHash: readString(data.routerCredentialHash),
@@ -969,6 +974,7 @@ export function filterSafeUpdates(updates: SessionUpdates): Partial {
safe.hermesAuthMethod = null;
}
assignNullableString(safe, "preferredInferenceApi", updates.preferredInferenceApi);
+ assignNullableString(safe, "compatibleEndpointReasoning", updates.compatibleEndpointReasoning);
assignNullableString(safe, "nimContainer", updates.nimContainer);
if (
typeof updates.routerPid === "number" &&
@@ -1276,6 +1282,7 @@ export function summarizeForDebug(
credentialEnv: session.credentialEnv,
hermesAuthMethod: session.hermesAuthMethod,
preferredInferenceApi: session.preferredInferenceApi,
+ compatibleEndpointReasoning: session.compatibleEndpointReasoning,
nimContainer: session.nimContainer,
hermesToolGateways: session.hermesToolGateways,
policyPresets: session.policyPresets,
diff --git a/test/generate-platform-docs.test.ts b/test/generate-platform-docs.test.ts
index 30f477d8099..6c9c228ae5e 100644
--- a/test/generate-platform-docs.test.ts
+++ b/test/generate-platform-docs.test.ts
@@ -428,6 +428,15 @@ print(block)
}
});
+ it("Option 3 docs expose reasoning mode for scripted compatible endpoints (#3279)", () => {
+ const body = readFileSync(
+ path.join(import.meta.dirname, "..", "docs", "inference", "inference-options.mdx"),
+ "utf-8",
+ );
+ expect(body).toContain("| `NEMOCLAW_REASONING` |");
+ expect(body).toContain("Set `NEMOCLAW_REASONING=true` when the compatible endpoint");
+ });
+
// PRA-2 on #5712 follow-up: a canonical launch-claims page that lives in the
// repo but never appears in docs/index.yml is invisible on the published
// site. Pin the registration so removing the nav entry fails CI before
diff --git a/test/helpers/onboard-final-flow-phases.ts b/test/helpers/onboard-final-flow-phases.ts
index 864659b4a41..9a3d8e69c79 100644
--- a/test/helpers/onboard-final-flow-phases.ts
+++ b/test/helpers/onboard-final-flow-phases.ts
@@ -151,6 +151,7 @@ export function context(
hermesAuthMethod: null,
hermesToolGateways: ["local"],
preferredInferenceApi: "chat",
+ compatibleEndpointReasoning: null,
nimContainer: "nim-test",
webSearchConfig: null,
webSearchSupported: true,
diff --git a/test/package-contract/onboard/compatible-endpoint-reasoning.test.ts b/test/package-contract/onboard/compatible-endpoint-reasoning.test.ts
new file mode 100644
index 00000000000..3236e77d2d7
--- /dev/null
+++ b/test/package-contract/onboard/compatible-endpoint-reasoning.test.ts
@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { it } from "vitest";
+
+it("honors NEMOCLAW_REASONING for custom OpenAI-compatible endpoint models (#3279)", () => {
+ const repoRoot = path.join(import.meta.dirname, "..", "..", "..");
+ const tmpDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), "nemoclaw-onboard-custom-openai-reasoning-"),
+ );
+ const fakeBin = path.join(tmpDir, "bin");
+ const scriptPath = path.join(tmpDir, "custom-openai-reasoning-check.js");
+ const curlArgsLog = path.join(tmpDir, "custom-openai-reasoning-curl-args.log");
+ const onboardPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "onboard.js"));
+ const credentialsPath = JSON.stringify(
+ path.join(repoRoot, "dist", "lib", "credentials", "store.js"),
+ );
+ const runnerPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "runner.js"));
+
+ fs.mkdirSync(fakeBin, { recursive: true });
+ fs.writeFileSync(
+ path.join(fakeBin, "curl"),
+ `#!/usr/bin/env bash
+args_log=${JSON.stringify(curlArgsLog)}
+printf '%s\\n' "$*" >> "$args_log"
+body='{"error":{"message":"bad request"}}'
+status="400"
+outfile=""
+url=""
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ -o) outfile="$2"; shift 2 ;;
+ *) url="$1"; shift ;;
+ esac
+done
+if echo "$url" | grep -q '/chat/completions$'; then
+ body='{"id":"chatcmpl-123","choices":[{"message":{"content":"","reasoning_content":"OK"}}]}'
+ status="200"
+fi
+printf '%s' "$body" > "$outfile"
+printf '%s' "$status"
+`,
+ { mode: 0o755 },
+ );
+
+ const script = String.raw`
+const credentials = require(${credentialsPath});
+const runner = require(${runnerPath});
+
+const answers = ["3", "https://proxy.example.com/v1", "reasoning-model"];
+const messages = [];
+
+credentials.prompt = async (message) => {
+ messages.push(message);
+ return answers.shift() || "";
+};
+runner.runCapture = () => "";
+
+const { setupNim } = require(${onboardPath});
+
+(async () => {
+ process.env.COMPATIBLE_API_KEY = "proxy-key";
+ process.env.NEMOCLAW_REASONING = "yes";
+ const originalLog = console.log;
+ const originalError = console.error;
+ const lines = [];
+ console.log = (...args) => lines.push(args.join(" "));
+ console.error = (...args) => lines.push(args.join(" "));
+ try {
+ const result = await setupNim(null);
+ originalLog(JSON.stringify({
+ result,
+ messages,
+ lines,
+ reasoning: process.env.NEMOCLAW_REASONING,
+ }));
+ } finally {
+ console.log = originalLog;
+ console.error = originalError;
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
+`;
+ fs.writeFileSync(scriptPath, script);
+
+ const result = spawnSync(process.execPath, [scriptPath], {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ env: {
+ ...process.env,
+ HOME: tmpDir,
+ PATH: `${fakeBin}:${process.env.PATH || ""}`,
+ },
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+ const stdoutLines = result.stdout.trim().split("\n");
+ const payload = JSON.parse(stdoutLines.at(-1) || "{}");
+ assert.equal(payload.result.provider, "compatible-endpoint");
+ assert.equal(payload.result.model, "reasoning-model");
+ assert.equal(payload.result.preferredInferenceApi, "openai-completions");
+ assert.equal(payload.reasoning, "true");
+ assert.ok(payload.lines.some((line: string) => line.includes("tools and streaming")));
+ const curlInvocations = fs.readFileSync(curlArgsLog, "utf-8");
+ assert.match(curlInvocations, /chat\/completions/);
+ assert.doesNotMatch(curlInvocations, /\/responses/);
+ assert.doesNotMatch(curlInvocations, /(^|\s)-N(\s|$)/);
+ assert.ok(
+ payload.messages.every(
+ (message: string) => !/Enable reasoning mode for this model/.test(message),
+ ),
+ );
+});