From a4b2e4d44e5975686b5bc7102702a8ddd0f0d35b Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Fri, 8 May 2026 13:30:21 -0700 Subject: [PATCH 1/4] fix(onboard): support reasoning compatible endpoints Fixes #3279 Signed-off-by: Deepak Jain --- src/lib/onboard.ts | 21 ++++ .../handlers/provider-inference.test.ts | 7 ++ test/onboard-selection.test.ts | 100 ++++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 442b83c3eea..08c342879d4 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -771,6 +771,25 @@ const { summarizeCurlFailure, summarizeProbeFailure } = httpProbe; const selectOnboardAgent = createOnboardAgentSelector({ isNonInteractive, note, prompt }); +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; +} + +async function configureCompatibleEndpointReasoning(): Promise<"true" | "false"> { + const configured = normalizeReasoningFlag(process.env.NEMOCLAW_REASONING); + process.env.NEMOCLAW_REASONING = configured ?? "false"; + return process.env.NEMOCLAW_REASONING as "true" | "false"; +} + const { getTransportRecoveryMessage } = validationRecovery; // Validation functions — delegated to src/lib/validation.ts @@ -5358,6 +5377,8 @@ module.exports = { printSandboxCreateRecoveryHints, promptYesNoOrDefault, providerExistsInGateway, + normalizeReasoningFlag, + configureCompatibleEndpointReasoning, parsePolicyPresetEnv, parseSandboxStatus, pruneStaleSandboxEntry, diff --git a/src/lib/onboard/machine/handlers/provider-inference.test.ts b/src/lib/onboard/machine/handlers/provider-inference.test.ts index 3b1bc8829f3..d763cb94012 100644 --- a/src/lib/onboard/machine/handlers/provider-inference.test.ts +++ b/src/lib/onboard/machine/handlers/provider-inference.test.ts @@ -156,6 +156,13 @@ 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({ + compatibleEndpointReasoning: null, + 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", { diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index d6c493043fc..768b0c451bd 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -3039,6 +3039,106 @@ const { setupNim } = require(${onboardPath}); ); }); + it("honors NEMOCLAW_REASONING for custom OpenAI-compatible endpoint models", () => { + 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 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 +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 payload = JSON.parse(result.stdout.trim()); + 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.messages.every( + (message: string) => !/Enable reasoning mode for this model/.test(message), + ), + ); + }); + it("forces chat completions for custom OpenAI-compatible endpoints even when /responses returns valid tool calls (#1932)", () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync( From e6a5de0a1a0029dc5caf5463b743fa57ba38d2ca Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Fri, 8 May 2026 13:47:19 -0700 Subject: [PATCH 2/4] fix: persist compatible endpoint reasoning state Signed-off-by: Deepak Jain --- src/lib/onboard.ts | 21 +++++++++++++++++++-- test/onboard-selection.test.ts | 7 +++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 08c342879d4..9197d6af303 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -771,6 +771,9 @@ const { summarizeCurlFailure, summarizeProbeFailure } = httpProbe; const selectOnboardAgent = createOnboardAgentSelector({ isNonInteractive, note, prompt }); +/** + * Normalize user-provided truthy/falsy aliases for compatible endpoint reasoning mode. + */ function normalizeReasoningFlag(value: string | null | undefined): "true" | "false" | null { const normalized = String(value ?? "") .trim() @@ -784,12 +787,25 @@ function normalizeReasoningFlag(value: string | null | undefined): "true" | "fal return null; } -async function configureCompatibleEndpointReasoning(): Promise<"true" | "false"> { - const configured = normalizeReasoningFlag(process.env.NEMOCLAW_REASONING); +/** + * Resolve compatible-endpoint reasoning mode and mirror it into process env for probes/builds. + */ +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"; } +/** + * Drop compatible-endpoint reasoning state when the user switches providers. + */ +function clearCompatibleEndpointReasoning(): null { + delete process.env.NEMOCLAW_REASONING; + return null; +} + const { getTransportRecoveryMessage } = validationRecovery; // Validation functions — delegated to src/lib/validation.ts @@ -4107,6 +4123,7 @@ async function setupNim( hermesAuthMethod, hermesToolGateways, preferredInferenceApi, + compatibleEndpointReasoning, allowToolsIncompatible, } = state); compatibleEndpointReasoning = state.compatibleEndpointReasoning ?? null; diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index 768b0c451bd..b609b74d247 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -3046,6 +3046,7 @@ const { setupNim } = require(${onboardPath}); ); 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")); @@ -3054,6 +3055,8 @@ const { setupNim } = require(${onboardPath}); 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="" @@ -3132,6 +3135,10 @@ const { setupNim } = require(${onboardPath}); assert.equal(payload.result.model, "reasoning-model"); assert.equal(payload.result.preferredInferenceApi, "openai-completions"); assert.equal(payload.reasoning, "true"); + 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), From d14b7e379777da0ab02978f098a0ec2979fc847e Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Fri, 29 May 2026 19:06:29 -0700 Subject: [PATCH 3/4] refactor(onboard): keep reasoning helpers out of entrypoint Signed-off-by: Deepak Jain --- src/lib/onboard.ts | 37 -------- test/onboard-selection-reasoning.test.ts | 116 +++++++++++++++++++++++ test/onboard-selection.test.ts | 107 --------------------- 3 files changed, 116 insertions(+), 144 deletions(-) create mode 100644 test/onboard-selection-reasoning.test.ts diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 9197d6af303..66652539ee1 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -771,41 +771,6 @@ const { summarizeCurlFailure, summarizeProbeFailure } = httpProbe; const selectOnboardAgent = createOnboardAgentSelector({ isNonInteractive, note, prompt }); -/** - * Normalize user-provided truthy/falsy aliases for compatible endpoint reasoning mode. - */ -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; -} - -/** - * Resolve compatible-endpoint reasoning mode and mirror it into process env for probes/builds. - */ -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"; -} - -/** - * Drop compatible-endpoint reasoning state when the user switches providers. - */ -function clearCompatibleEndpointReasoning(): null { - delete process.env.NEMOCLAW_REASONING; - return null; -} - const { getTransportRecoveryMessage } = validationRecovery; // Validation functions — delegated to src/lib/validation.ts @@ -5394,8 +5359,6 @@ module.exports = { printSandboxCreateRecoveryHints, promptYesNoOrDefault, providerExistsInGateway, - normalizeReasoningFlag, - configureCompatibleEndpointReasoning, parsePolicyPresetEnv, parseSandboxStatus, pruneStaleSandboxEntry, diff --git a/test/onboard-selection-reasoning.test.ts b/test/onboard-selection-reasoning.test.ts new file mode 100644 index 00000000000..bb86a5e6a99 --- /dev/null +++ b/test/onboard-selection-reasoning.test.ts @@ -0,0 +1,116 @@ +// 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", () => { + 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 payload = JSON.parse(result.stdout.trim()); + 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"); + 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), + ), + ); +}); diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index b609b74d247..d6c493043fc 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -3039,113 +3039,6 @@ const { setupNim } = require(${onboardPath}); ); }); - it("honors NEMOCLAW_REASONING for custom OpenAI-compatible endpoint models", () => { - 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 payload = JSON.parse(result.stdout.trim()); - 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"); - 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), - ), - ); - }); - it("forces chat completions for custom OpenAI-compatible endpoints even when /responses returns valid tool calls (#1932)", () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync( From 0def0a81cb71fbf3e6cf396c0be32adfd5fe5b68 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Fri, 26 Jun 2026 20:04:32 -0700 Subject: [PATCH 4/4] fix(onboard): finish reasoning flow rebase Signed-off-by: Deepak Jain --- src/lib/onboard.ts | 1 - test/onboard-selection-reasoning.test.ts | 116 ----------------------- 2 files changed, 117 deletions(-) delete mode 100644 test/onboard-selection-reasoning.test.ts diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 66652539ee1..442b83c3eea 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4088,7 +4088,6 @@ async function setupNim( hermesAuthMethod, hermesToolGateways, preferredInferenceApi, - compatibleEndpointReasoning, allowToolsIncompatible, } = state); compatibleEndpointReasoning = state.compatibleEndpointReasoning ?? null; diff --git a/test/onboard-selection-reasoning.test.ts b/test/onboard-selection-reasoning.test.ts deleted file mode 100644 index bb86a5e6a99..00000000000 --- a/test/onboard-selection-reasoning.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -// 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", () => { - 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 payload = JSON.parse(result.stdout.trim()); - 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"); - 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), - ), - ); -});