From e7745879edf6ef6959d9fae9f66805e362d44853 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 2 Sep 2026 15:14:54 -0700 Subject: [PATCH 01/10] fix(onboard): use Nemotron endpoint probe parameters Apply the sampling and chat-template parameters required by the bundled Nemotron 3 Super endpoint during validation. Fixes #10880 Signed-off-by: Apurv Kumaria --- src/lib/inference/onboard-probes.test.ts | 5 ++++- src/lib/inference/openai-probe-models.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index c7efcc811f7..cc4de535d2e 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -309,11 +309,14 @@ describe("OpenAI-compatible inference probes", () => { }); }); - it("keeps the default chat-completions probe bounded for other models", () => { + it("uses the required NVIDIA Endpoints request shape for Nemotron 3 Super (#10880)", () => { expect(getChatCompletionsProbePayload("nvidia/nemotron-3-super-120b-a12b")).toEqual({ model: "nvidia/nemotron-3-super-120b-a12b", messages: [{ role: "user", content: "Reply with exactly: OK" }], max_tokens: 16, + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, }); }); diff --git a/src/lib/inference/openai-probe-models.ts b/src/lib/inference/openai-probe-models.ts index 85d92c4f3bb..c35aac79a28 100644 --- a/src/lib/inference/openai-probe-models.ts +++ b/src/lib/inference/openai-probe-models.ts @@ -47,6 +47,10 @@ export function isKimiK26Model(model: unknown): boolean { return String(model || "").toLowerCase() === "moonshotai/kimi-k2.6"; } +function isNemotron3Super120bModel(model: unknown): boolean { + return String(model || "").toLowerCase() === "nvidia/nemotron-3-super-120b-a12b"; +} + export function getChatCompletionsProbePayload(model: string): Record { const maxTokensField = resolveMaxTokensField(model); const payload = { @@ -74,6 +78,15 @@ export function getChatCompletionsProbePayload(model: string): Record Date: Wed, 2 Sep 2026 16:33:15 -0700 Subject: [PATCH 02/10] refactor(inference): inline Nemotron probe match Signed-off-by: Apurv Kumaria --- src/lib/inference/openai-probe-models.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib/inference/openai-probe-models.ts b/src/lib/inference/openai-probe-models.ts index c35aac79a28..b5a4d3a749a 100644 --- a/src/lib/inference/openai-probe-models.ts +++ b/src/lib/inference/openai-probe-models.ts @@ -47,10 +47,6 @@ export function isKimiK26Model(model: unknown): boolean { return String(model || "").toLowerCase() === "moonshotai/kimi-k2.6"; } -function isNemotron3Super120bModel(model: unknown): boolean { - return String(model || "").toLowerCase() === "nvidia/nemotron-3-super-120b-a12b"; -} - export function getChatCompletionsProbePayload(model: string): Record { const maxTokensField = resolveMaxTokensField(model); const payload = { @@ -78,7 +74,7 @@ export function getChatCompletionsProbePayload(model: string): Record Date: Wed, 2 Sep 2026 17:05:00 -0700 Subject: [PATCH 03/10] test(inference): describe Nemotron probe behavior Signed-off-by: Apurv Kumaria --- src/lib/inference/onboard-probes.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index cc4de535d2e..4640c26d929 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -309,7 +309,7 @@ describe("OpenAI-compatible inference probes", () => { }); }); - it("uses the required NVIDIA Endpoints request shape for Nemotron 3 Super (#10880)", () => { + it("sends the Nemotron 3 Super validation request parameters (#10880)", () => { expect(getChatCompletionsProbePayload("nvidia/nemotron-3-super-120b-a12b")).toEqual({ model: "nvidia/nemotron-3-super-120b-a12b", messages: [{ role: "user", content: "Reply with exactly: OK" }], From 907848afabb76a7ee10903ae1cd7f9084d7abc5d Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 2 Sep 2026 17:21:40 -0700 Subject: [PATCH 04/10] test(inference): verify serialized Nemotron probe Signed-off-by: Apurv Kumaria --- src/lib/inference/onboard-probes.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index 4640c26d929..f34dfc578f4 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -310,7 +310,15 @@ describe("OpenAI-compatible inference probes", () => { }); it("sends the Nemotron 3 Super validation request parameters (#10880)", () => { - expect(getChatCompletionsProbePayload("nvidia/nemotron-3-super-120b-a12b")).toEqual({ + const args = getChatCompletionsProbeCurlArgs({ + credentialArgs: FAKE_CREDENTIAL_ARGS, + model: "nvidia/nemotron-3-super-120b-a12b", + url: "https://integrate.api.nvidia.com/v1/chat/completions", + isWsl: false, + }); + + expect(args).toContain("-d"); + expect(JSON.parse(args[args.indexOf("-d") + 1])).toEqual({ model: "nvidia/nemotron-3-super-120b-a12b", messages: [{ role: "user", content: "Reply with exactly: OK" }], max_tokens: 16, From 4a6693cde7cb5e160a8574999e89fe36a2e6e0ba Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 2 Sep 2026 17:36:49 -0700 Subject: [PATCH 05/10] test(inference): name serialized Nemotron probe Signed-off-by: Apurv Kumaria --- src/lib/inference/onboard-probes.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index f34dfc578f4..2763f749e7c 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -309,7 +309,7 @@ describe("OpenAI-compatible inference probes", () => { }); }); - it("sends the Nemotron 3 Super validation request parameters (#10880)", () => { + it("serializes the Nemotron 3 Super validation request parameters (#10880)", () => { const args = getChatCompletionsProbeCurlArgs({ credentialArgs: FAKE_CREDENTIAL_ARGS, model: "nvidia/nemotron-3-super-120b-a12b", From 04893823ccf2dd4ef4949d5f5a878959a074734c Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 3 Sep 2026 11:58:42 -0700 Subject: [PATCH 06/10] fix(onboard): scope Nemotron probe parameters Signed-off-by: Apurv Kumaria --- src/lib/inference/health.test.ts | 19 +++++++ src/lib/inference/health.ts | 12 ++++- src/lib/inference/onboard-probes.test.ts | 16 ++++++ src/lib/inference/onboard-probes.ts | 14 ++++- src/lib/inference/openai-probe-models.ts | 10 +++- .../openai-validation-session.test.ts | 39 ++++++++++++++ .../inference/openai-validation-session.ts | 5 +- .../onboard/inference-selection-validation.ts | 1 + src/lib/onboard/setup-nim-selection.test.ts | 51 +++++++++++++++++++ src/lib/onboard/setup-nim-selection.ts | 2 + 10 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/lib/inference/health.test.ts b/src/lib/inference/health.test.ts index 17aa2165cd6..cd04964c468 100644 --- a/src/lib/inference/health.test.ts +++ b/src/lib/inference/health.test.ts @@ -186,6 +186,25 @@ describe("inference health", () => { expect(payload.model).toBe("meta/llama-3.3-70b-instruct"); }); + it("uses the NVIDIA Endpoints request shape for Nemotron 3 Super health (#10880)", () => { + let capturedArgv: string[] = []; + const result = probeRemoteProviderHealth("nvidia-prod", { + model: "nvidia/nemotron-3-super-120b-a12b", + getCredentialImpl: () => "nvapi-test", + runCurlProbeImpl: (argv) => { + capturedArgv = argv; + return httpOk(); + }, + }); + + expect(result?.ok).toBe(true); + expect(JSON.parse(capturedArgv[capturedArgv.indexOf("-d") + 1])).toMatchObject({ + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }); + }); + it("always resolves NVIDIA credentials from NVIDIA_INFERENCE_API_KEY, not the route's default credential env", () => { let resolvedEnvNames: string[] = []; const result = probeRemoteProviderHealth("nvidia-nim", { diff --git a/src/lib/inference/health.ts b/src/lib/inference/health.ts index ccf1b3b17a2..60ada7b444e 100644 --- a/src/lib/inference/health.ts +++ b/src/lib/inference/health.ts @@ -125,6 +125,7 @@ function buildChatCompletionsStatusProbeCurlArgs( endpoint: string, authArgs: readonly string[], isWsl?: boolean, + useNvidiaEndpointProbePayload = false, ): string[] { const args = capStatusProbeOutput( useStatusProbeTiming( @@ -133,6 +134,7 @@ function buildChatCompletionsStatusProbeCurlArgs( model, url: endpoint, isWsl, + useNvidiaEndpointProbePayload, }), ), ); @@ -500,6 +502,7 @@ function probeChatCompletionsProviderHealth( credentialEnv: string, endpoint: string, options: ProviderHealthProbeOptions, + useNvidiaEndpointProbePayload = false, ): ProviderHealthStatus { let apiKey = ""; try { @@ -523,7 +526,13 @@ function probeChatCompletionsProviderHealth( const rawResult = (() => { try { return runCurlProbeImpl( - buildChatCompletionsStatusProbeCurlArgs(model, endpoint, authConfig.args, options.isWsl), + buildChatCompletionsStatusProbeCurlArgs( + model, + endpoint, + authConfig.args, + options.isWsl, + useNvidiaEndpointProbePayload, + ), { trustedConfigFiles: authConfig.trustedConfigFiles }, ); } finally { @@ -659,6 +668,7 @@ export function probeRemoteProviderHealth( NVIDIA_HEALTH_CREDENTIAL_ENV, `${BUILD_ENDPOINT_URL}/chat/completions`, options, + true, ); } diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index 2763f749e7c..b32c223b7c2 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -315,6 +315,7 @@ describe("OpenAI-compatible inference probes", () => { model: "nvidia/nemotron-3-super-120b-a12b", url: "https://integrate.api.nvidia.com/v1/chat/completions", isWsl: false, + useNvidiaEndpointProbePayload: true, }); expect(args).toContain("-d"); @@ -328,6 +329,21 @@ describe("OpenAI-compatible inference probes", () => { }); }); + it("keeps compatible endpoints on the generic request shape for the same Nemotron model (#10880)", () => { + const args = getChatCompletionsProbeCurlArgs({ + credentialArgs: FAKE_CREDENTIAL_ARGS, + model: "nvidia/nemotron-3-super-120b-a12b", + url: "https://compatible.example.test/v1/chat/completions", + isWsl: false, + }); + + expect(JSON.parse(args[args.indexOf("-d") + 1])).toEqual({ + model: "nvidia/nemotron-3-super-120b-a12b", + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: 16, + }); + }); + it("bounds the hosted compatible inference probe for the served Nemotron model", () => { expect(getChatCompletionsProbePayload("nvidia/nvidia/nemotron-3-ultra")).toEqual({ model: "nvidia/nvidia/nemotron-3-ultra", diff --git a/src/lib/inference/onboard-probes.ts b/src/lib/inference/onboard-probes.ts index cc1a798df67..368f93dd442 100644 --- a/src/lib/inference/onboard-probes.ts +++ b/src/lib/inference/onboard-probes.ts @@ -535,6 +535,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { isWsl?: boolean; pinnedAddresses?: readonly string[]; validationTiming?: unknown; + useNvidiaEndpointProbePayload?: boolean; }) { const { credentialArgs, @@ -544,6 +545,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { isWsl: isWslOverride, pinnedAddresses, validationTiming, + useNvidiaEndpointProbePayload, } = opts; const platformOptions = getProbeTimingOptions({ ...(typeof isWslOverride === "boolean" ? { isWsl: isWslOverride } : {}), @@ -559,7 +561,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { "Content-Type: application/json", ...credSlice, "-d", - JSON.stringify(getChatCompletionsProbePayload(model)), + JSON.stringify(getChatCompletionsProbePayload(model, { useNvidiaEndpointProbePayload })), url, ]; } @@ -573,6 +575,7 @@ function runChatCompletionsProbe({ pinnedAddresses, trustedPrivateCapability, validationTiming, + useNvidiaEndpointProbePayload, spawnSyncImpl, }) { const args = getChatCompletionsProbeCurlArgs({ @@ -582,6 +585,7 @@ function runChatCompletionsProbe({ isWsl: isWslOverride, pinnedAddresses, validationTiming, + useNvidiaEndpointProbePayload, }); const probeOpts = { timeoutMs: getProbeProcessTimeoutMs(args), @@ -621,7 +625,11 @@ function runDoubledTimeoutChatCompletionsRetry({ "Content-Type: application/json", ...authConfig.args, "-d", - JSON.stringify(getChatCompletionsProbePayload(model)), + JSON.stringify( + getChatCompletionsProbePayload(model, { + useNvidiaEndpointProbePayload: options.useNvidiaEndpointProbePayload, + }), + ), `${baseUrl}/chat/completions`, ]; const runRetryProbe = () => @@ -879,6 +887,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) { pinnedAddresses, trustedPrivateCapability: options.trustedPrivateCapability, validationTiming, + useNvidiaEndpointProbePayload: options.useNvidiaEndpointProbePayload, spawnSyncImpl: options.spawnSyncImpl, }), }; @@ -1197,6 +1206,7 @@ export async function verifyOnboardInferenceSmoke(options: any, dependencies: an authMode: getProbeAuthMode(options.provider), extraHeaders: getProbeExtraHeaders(options.provider), skipResponsesProbe: true, + useNvidiaEndpointProbePayload: options.provider === "nvidia-prod", pinnedAddresses: options.pinnedAddresses, trustedPrivateCapability: options.trustedPrivateCapability, }); diff --git a/src/lib/inference/openai-probe-models.ts b/src/lib/inference/openai-probe-models.ts index b5a4d3a749a..60fd9d02a7a 100644 --- a/src/lib/inference/openai-probe-models.ts +++ b/src/lib/inference/openai-probe-models.ts @@ -47,7 +47,10 @@ export function isKimiK26Model(model: unknown): boolean { return String(model || "").toLowerCase() === "moonshotai/kimi-k2.6"; } -export function getChatCompletionsProbePayload(model: string): Record { +export function getChatCompletionsProbePayload( + model: string, + options: { useNvidiaEndpointProbePayload?: boolean } = {}, +): Record { const maxTokensField = resolveMaxTokensField(model); const payload = { model, @@ -74,7 +77,10 @@ export function getChatCompletionsProbePayload(model: string): Record { + it("sends the NVIDIA Endpoints Nemotron request shape through native validation (#10880)", async () => { + let observedBody = ""; + const server = http.createServer((request, response) => { + request.setEncoding("utf8"); + request.on("data", (chunk) => { + observedBody += chunk; + }); + request.on("end", () => { + response.end('{"choices":[{"message":{"content":"OK"}}]}'); + }); + }); + const port = await listen(server); + const harness = { + ...createOpenAiValidationTestDeps(), + getChatPayload: getChatCompletionsProbePayload, + }; + + const result = await probeOpenAiLikeEndpointWithValidationSession( + `http://provider.example.test:${port}/v1`, + "nvidia/nemotron-3-super-120b-a12b", + "test-key", + { skipResponsesProbe: true, useNvidiaEndpointProbePayload: true }, + harness, + ); + + expect(result).toMatchObject({ ok: true, api: "openai-completions" }); + expect(JSON.parse(observedBody)).toEqual({ + model: "nvidia/nemotron-3-super-120b-a12b", + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: 16, + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }); + expect(JSON.parse(observedBody)).not.toHaveProperty("thinking"); + expect(harness.legacyProbe).not.toHaveBeenCalled(); + }); + it("uses the GPT-5 reply-budget field for native tool-call validation (#6642)", async () => { let observedBody = ""; const server = http.createServer((request, response) => { diff --git a/src/lib/inference/openai-validation-session.ts b/src/lib/inference/openai-validation-session.ts index 00ac699e4aa..5edacdbdf94 100644 --- a/src/lib/inference/openai-validation-session.ts +++ b/src/lib/inference/openai-validation-session.ts @@ -29,6 +29,7 @@ export interface OpenAiValidationOptions { requireResponsesToolCalling?: boolean; requireChatCompletionsToolCalling?: boolean; retryChatCompletionsToolReadiness?: boolean; + useNvidiaEndpointProbePayload?: boolean; skipResponsesProbe?: boolean; probeStreaming?: boolean; @@ -56,7 +57,7 @@ export interface OpenAiValidationSessionDeps { hasResponsesToolCall(body: string): boolean; hasChatCompletionsToolCall(body: string): boolean; hasChatCompletionsToolCallLeak(body: string): boolean; - getChatPayload(model: string): Record; + getChatPayload(model: string, options: OpenAiValidationOptions): Record; getResponsesTimeoutMs(options: OpenAiValidationOptions): number; getChatTimeoutMs(model: string, options: OpenAiValidationOptions): number; sessionOptions?: ValidationSessionOptions; @@ -344,7 +345,7 @@ export async function probeOpenAiLikeEndpointWithValidationSession( ...auth, body: requireToolCall ? chatToolPayload(model, maxTokens) - : JSON.stringify(deps.getChatPayload(model)), + : JSON.stringify(deps.getChatPayload(model, options)), timeoutMs: deps.getChatTimeoutMs(model, options) * timeoutMultiplier, }), retryTransientHttp, diff --git a/src/lib/onboard/inference-selection-validation.ts b/src/lib/onboard/inference-selection-validation.ts index 792784fa609..88eb5e40ea8 100644 --- a/src/lib/onboard/inference-selection-validation.ts +++ b/src/lib/onboard/inference-selection-validation.ts @@ -68,6 +68,7 @@ export interface OpenAiSelectionValidationOptions { requireResponsesToolCalling?: boolean; requireChatCompletionsToolCalling?: boolean; retryChatCompletionsToolReadiness?: boolean; + useNvidiaEndpointProbePayload?: boolean; /** Provider identity used only for safe, provider-specific diagnostics. */ provider?: string; revalidateSandboxIdentity?: (operation: string) => void; diff --git a/src/lib/onboard/setup-nim-selection.test.ts b/src/lib/onboard/setup-nim-selection.test.ts index 3d691a77e4c..b31c32033a3 100644 --- a/src/lib/onboard/setup-nim-selection.test.ts +++ b/src/lib/onboard/setup-nim-selection.test.ts @@ -271,6 +271,7 @@ describe("createRemoteModelValidator", () => { ); assert.deepEqual(receivedOptions, { provider: "gemini-api", + useNvidiaEndpointProbePayload: false, requireResponsesToolCalling: true, skipResponsesProbe: true, authMode: undefined, @@ -278,4 +279,54 @@ describe("createRemoteModelValidator", () => { capabilityCache: undefined, }); }); + + it("selects the Nemotron probe payload only for NVIDIA Endpoints (#10880)", async () => { + const state = makeState(); + state.provider = "nvidia-prod"; + state.endpointUrl = "https://integrate.api.nvidia.com/v1"; + state.model = "nvidia/nemotron-3-super-120b-a12b"; + let receivedOptions: { useNvidiaEndpointProbePayload?: boolean } | undefined; + const { validateSelectedRemoteModel } = createRemoteModelValidator({ + OPENAI_ENDPOINT_URL: "https://default-openai.example/v1", + ANTHROPIC_ENDPOINT_URL: "https://default-anthropic.example/v1", + requireValue, + isBackToSelection: (_value): _value is never => false, + validateCustomOpenAiLikeSelection: async () => ({ ok: false, retry: "selection" }), + validateCustomAnthropicSelection: async () => ({ ok: false, retry: "selection" }), + validateAnthropicSelectionWithRetryMessage: async () => ({ + ok: false, + retry: "selection", + }), + validateOpenAiLikeSelection: async ( + _label, + _endpointUrl, + _model, + _credentialEnv, + _retryMessage, + _helpUrl, + options, + ) => { + receivedOptions = options; + return { ok: true, api: "openai-completions" }; + }, + shouldRequireResponsesToolCalling: () => false, + shouldSkipResponsesProbe: () => true, + getProbeAuthMode: () => undefined, + }); + + assert.equal( + await validateSelectedRemoteModel({ + selected: { key: "build" }, + remoteConfig: { + label: "NVIDIA Endpoints", + endpointUrl: "https://integrate.api.nvidia.com/v1", + helpUrl: null, + }, + state, + selectedCredentialEnv: "NVIDIA_INFERENCE_API_KEY", + }), + "selected", + ); + assert.equal(receivedOptions?.useNvidiaEndpointProbePayload, true); + }); }); diff --git a/src/lib/onboard/setup-nim-selection.ts b/src/lib/onboard/setup-nim-selection.ts index 95e09debd48..16002a1b11c 100644 --- a/src/lib/onboard/setup-nim-selection.ts +++ b/src/lib/onboard/setup-nim-selection.ts @@ -237,6 +237,7 @@ type ProbeAuthMode = "bearer" | "query-param" | undefined; type ProbeOptions = { requireResponsesToolCalling?: boolean; skipResponsesProbe?: boolean; + useNvidiaEndpointProbePayload?: boolean; authMode?: ProbeAuthMode; extraHeaders?: readonly string[]; capabilityCache?: OnboardInferenceCapabilityCache; @@ -473,6 +474,7 @@ export function createRemoteModelValidator(deps: RemoteModelValidatorDeps): { remoteConfig.helpUrl, withCredentialMutationGuard(state, { provider: state.provider, + useNvidiaEndpointProbePayload: state.provider === "nvidia-prod", requireResponsesToolCalling: deps.shouldRequireResponsesToolCalling(state.provider), skipResponsesProbe: deps.shouldSkipResponsesProbe(state.provider), authMode: deps.getProbeAuthMode(state.provider), From 8e2acae7f7a1ba8b4f21f17121e9228d5bd4156c Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 3 Sep 2026 12:49:33 -0700 Subject: [PATCH 07/10] fix(inference): handle legacy NVIDIA endpoint provider Signed-off-by: Apurv Kumaria --- src/lib/inference/health.test.ts | 39 ++++---- src/lib/inference/health.ts | 4 +- src/lib/inference/onboard-probes.test.ts | 19 ++-- src/lib/inference/onboard-probes.ts | 3 +- src/lib/inference/openai-probe-models.ts | 6 ++ src/lib/onboard/setup-nim-selection.test.ts | 99 +++++++++++---------- src/lib/onboard/setup-nim-selection.ts | 3 +- 7 files changed, 94 insertions(+), 79 deletions(-) diff --git a/src/lib/inference/health.test.ts b/src/lib/inference/health.test.ts index cd04964c468..7d05d016fb9 100644 --- a/src/lib/inference/health.test.ts +++ b/src/lib/inference/health.test.ts @@ -186,24 +186,27 @@ describe("inference health", () => { expect(payload.model).toBe("meta/llama-3.3-70b-instruct"); }); - it("uses the NVIDIA Endpoints request shape for Nemotron 3 Super health (#10880)", () => { - let capturedArgv: string[] = []; - const result = probeRemoteProviderHealth("nvidia-prod", { - model: "nvidia/nemotron-3-super-120b-a12b", - getCredentialImpl: () => "nvapi-test", - runCurlProbeImpl: (argv) => { - capturedArgv = argv; - return httpOk(); - }, - }); - - expect(result?.ok).toBe(true); - expect(JSON.parse(capturedArgv[capturedArgv.indexOf("-d") + 1])).toMatchObject({ - temperature: 1, - top_p: 0.95, - chat_template_kwargs: { enable_thinking: false }, - }); - }); + it.each(["nvidia-prod", "nvidia-nim"])( + "uses the NVIDIA Endpoints request shape for Nemotron 3 Super health through %s (#10880)", + (provider) => { + let capturedArgv: string[] = []; + const result = probeRemoteProviderHealth(provider, { + model: "nvidia/nemotron-3-super-120b-a12b", + getCredentialImpl: () => "nvapi-test", + runCurlProbeImpl: (argv) => { + capturedArgv = argv; + return httpOk(); + }, + }); + + expect(result?.ok).toBe(true); + expect(JSON.parse(capturedArgv[capturedArgv.indexOf("-d") + 1])).toMatchObject({ + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }); + }, + ); it("always resolves NVIDIA credentials from NVIDIA_INFERENCE_API_KEY, not the route's default credential env", () => { let resolvedEnvNames: string[] = []; diff --git a/src/lib/inference/health.ts b/src/lib/inference/health.ts index 60ada7b444e..da41d47c5f0 100644 --- a/src/lib/inference/health.ts +++ b/src/lib/inference/health.ts @@ -18,6 +18,7 @@ import type { LocalProviderHealthProbeOptions } from "./local"; import { probeLocalProviderHealth } from "./local"; import { MIN_PROBE_REPLY_TOKENS } from "./max-tokens-field"; import { getChatCompletionsProbeCurlArgs } from "./onboard-probes"; +import { usesNvidiaEndpointProbePayload } from "./openai-probe-models"; import { BUILD_ENDPOINT_URL } from "./provider-models"; export interface ProviderHealthStatus { @@ -55,7 +56,6 @@ export interface ProviderHealthProbeOptions { } const COMPATIBLE_PROVIDERS = new Set(["compatible-endpoint", "compatible-anthropic-endpoint"]); -const NVIDIA_MANAGED_PROVIDERS = new Set(["nvidia-prod", "nvidia-nim"]); const NVIDIA_HEALTH_CREDENTIAL_ENV = "NVIDIA_INFERENCE_API_KEY"; const HEALTH_PROBE_CONNECT_TIMEOUT_SECONDS = "3"; const HEALTH_PROBE_MAX_TIME_SECONDS = "5"; @@ -661,7 +661,7 @@ export function probeRemoteProviderHealth( if (!config?.model) return null; - if (NVIDIA_MANAGED_PROVIDERS.has(provider)) { + if (usesNvidiaEndpointProbePayload(provider)) { return probeChatCompletionsProviderHealth( providerLabel, config.model, diff --git a/src/lib/inference/onboard-probes.test.ts b/src/lib/inference/onboard-probes.test.ts index b32c223b7c2..b71012a35a4 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -932,7 +932,7 @@ exit 0 }); it("retries chat-completions when /responses errors then chat-completions times out", () => { - const script = `#!/usr/bin/env bash + const script = `#!/usr/bin/env bash outfile="" url="" while [ "$#" -gt 0 ]; do @@ -981,7 +981,7 @@ exit 0 }); it("preserves query-param auth on doubled-timeout chat-completions retry", () => { - const script = `#!/usr/bin/env bash + const script = `#!/usr/bin/env bash outfile="" n=$(cat "${HARNESS_COUNTER}") n=$((n + 1)) @@ -1444,7 +1444,8 @@ exit 0 }); describe("onboard inference smoke abort cleanup", () => { - it("tears down the orphan managed gateway before exiting after a failed smoke", async () => { + it("uses the legacy NVIDIA Endpoints payload before cleaning up a failed smoke (#10880)", async () => { + const optimizedProbe = vi.fn().mockResolvedValue({ ok: false, message: "smoke failed" }); const teardownOrphanManagedGatewayOnAbort = vi.fn(); const exit = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never); const error = vi.spyOn(console, "error").mockImplementation(() => undefined); @@ -1455,18 +1456,18 @@ describe("onboard inference smoke abort cleanup", () => { { endpointUrl: "https://inference.example.com/v1", forceOpenAiLike: true, - model: "example/model", - provider: "example-provider", + model: "nvidia/nemotron-3-super-120b-a12b", + provider: "nvidia-nim", }, { - probeOpenAiLikeEndpointOptimized: vi.fn().mockResolvedValue({ - ok: false, - message: "smoke failed", - }), + probeOpenAiLikeEndpointOptimized: optimizedProbe, teardownOrphanManagedGatewayOnAbort, }, ); + expect(optimizedProbe.mock.calls[0]?.[3]).toMatchObject({ + useNvidiaEndpointProbePayload: true, + }); expect(teardownOrphanManagedGatewayOnAbort).toHaveBeenCalledOnce(); expect(exit).toHaveBeenCalledWith(1); expect(teardownOrphanManagedGatewayOnAbort.mock.invocationCallOrder[0]).toBeLessThan( diff --git a/src/lib/inference/onboard-probes.ts b/src/lib/inference/onboard-probes.ts index 368f93dd442..768f052f66d 100644 --- a/src/lib/inference/onboard-probes.ts +++ b/src/lib/inference/onboard-probes.ts @@ -57,6 +57,7 @@ const { STRICT_TOOL_PROBE_INITIAL_TOKENS, STRICT_TOOL_PROBE_RETRY_TOKEN_LADDER, strictToolProbeReasoningRetryMessage, + usesNvidiaEndpointProbePayload, vllmProbePolicyForModel, } = require("./openai-probe-models"); const { @@ -1206,7 +1207,7 @@ export async function verifyOnboardInferenceSmoke(options: any, dependencies: an authMode: getProbeAuthMode(options.provider), extraHeaders: getProbeExtraHeaders(options.provider), skipResponsesProbe: true, - useNvidiaEndpointProbePayload: options.provider === "nvidia-prod", + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(options.provider), pinnedAddresses: options.pinnedAddresses, trustedPrivateCapability: options.trustedPrivateCapability, }); diff --git a/src/lib/inference/openai-probe-models.ts b/src/lib/inference/openai-probe-models.ts index 60fd9d02a7a..f9b3ea6cc52 100644 --- a/src/lib/inference/openai-probe-models.ts +++ b/src/lib/inference/openai-probe-models.ts @@ -9,6 +9,12 @@ export const STANDARD_NVIDIA_ENDPOINT_PROBE_POLICY = export const EXTENDED_NVIDIA_ENDPOINT_PROBE_POLICY = "nvidia.endpoint-validation.extended/v1"; +const NVIDIA_ENDPOINT_PROVIDERS = new Set(["nvidia-prod", "nvidia-nim"]); + +export function usesNvidiaEndpointProbePayload(provider: unknown): boolean { + return typeof provider === "string" && NVIDIA_ENDPOINT_PROVIDERS.has(provider); +} + export function vllmProbePolicyForModel(model: string): string { const normalized = model.trim().toLowerCase(); const matches = loadManagedInferenceCatalog().models.filter(({ spec }) => diff --git a/src/lib/onboard/setup-nim-selection.test.ts b/src/lib/onboard/setup-nim-selection.test.ts index b31c32033a3..e8fd1b954c2 100644 --- a/src/lib/onboard/setup-nim-selection.test.ts +++ b/src/lib/onboard/setup-nim-selection.test.ts @@ -280,53 +280,56 @@ describe("createRemoteModelValidator", () => { }); }); - it("selects the Nemotron probe payload only for NVIDIA Endpoints (#10880)", async () => { - const state = makeState(); - state.provider = "nvidia-prod"; - state.endpointUrl = "https://integrate.api.nvidia.com/v1"; - state.model = "nvidia/nemotron-3-super-120b-a12b"; - let receivedOptions: { useNvidiaEndpointProbePayload?: boolean } | undefined; - const { validateSelectedRemoteModel } = createRemoteModelValidator({ - OPENAI_ENDPOINT_URL: "https://default-openai.example/v1", - ANTHROPIC_ENDPOINT_URL: "https://default-anthropic.example/v1", - requireValue, - isBackToSelection: (_value): _value is never => false, - validateCustomOpenAiLikeSelection: async () => ({ ok: false, retry: "selection" }), - validateCustomAnthropicSelection: async () => ({ ok: false, retry: "selection" }), - validateAnthropicSelectionWithRetryMessage: async () => ({ - ok: false, - retry: "selection", - }), - validateOpenAiLikeSelection: async ( - _label, - _endpointUrl, - _model, - _credentialEnv, - _retryMessage, - _helpUrl, - options, - ) => { - receivedOptions = options; - return { ok: true, api: "openai-completions" }; - }, - shouldRequireResponsesToolCalling: () => false, - shouldSkipResponsesProbe: () => true, - getProbeAuthMode: () => undefined, - }); - - assert.equal( - await validateSelectedRemoteModel({ - selected: { key: "build" }, - remoteConfig: { - label: "NVIDIA Endpoints", - endpointUrl: "https://integrate.api.nvidia.com/v1", - helpUrl: null, + it.each(["nvidia-prod", "nvidia-nim"])( + "selects the Nemotron probe payload for NVIDIA Endpoints provider %s (#10880)", + async (provider) => { + const state = makeState(); + state.provider = provider; + state.endpointUrl = "https://integrate.api.nvidia.com/v1"; + state.model = "nvidia/nemotron-3-super-120b-a12b"; + let receivedOptions: { useNvidiaEndpointProbePayload?: boolean } | undefined; + const { validateSelectedRemoteModel } = createRemoteModelValidator({ + OPENAI_ENDPOINT_URL: "https://default-openai.example/v1", + ANTHROPIC_ENDPOINT_URL: "https://default-anthropic.example/v1", + requireValue, + isBackToSelection: (_value): _value is never => false, + validateCustomOpenAiLikeSelection: async () => ({ ok: false, retry: "selection" }), + validateCustomAnthropicSelection: async () => ({ ok: false, retry: "selection" }), + validateAnthropicSelectionWithRetryMessage: async () => ({ + ok: false, + retry: "selection", + }), + validateOpenAiLikeSelection: async ( + _label, + _endpointUrl, + _model, + _credentialEnv, + _retryMessage, + _helpUrl, + options, + ) => { + receivedOptions = options; + return { ok: true, api: "openai-completions" }; }, - state, - selectedCredentialEnv: "NVIDIA_INFERENCE_API_KEY", - }), - "selected", - ); - assert.equal(receivedOptions?.useNvidiaEndpointProbePayload, true); - }); + shouldRequireResponsesToolCalling: () => false, + shouldSkipResponsesProbe: () => true, + getProbeAuthMode: () => undefined, + }); + + assert.equal( + await validateSelectedRemoteModel({ + selected: { key: "build" }, + remoteConfig: { + label: "NVIDIA Endpoints", + endpointUrl: "https://integrate.api.nvidia.com/v1", + helpUrl: null, + }, + state, + selectedCredentialEnv: "NVIDIA_INFERENCE_API_KEY", + }), + "selected", + ); + assert.equal(receivedOptions?.useNvidiaEndpointProbePayload, true); + }, + ); }); diff --git a/src/lib/onboard/setup-nim-selection.ts b/src/lib/onboard/setup-nim-selection.ts index 16002a1b11c..3de464f2b7f 100644 --- a/src/lib/onboard/setup-nim-selection.ts +++ b/src/lib/onboard/setup-nim-selection.ts @@ -10,6 +10,7 @@ import { applyCompatibleEndpointContextWindow } from "../inference/compatible-en import type { TrustedPrivateEndpointCapability } from "../inference/endpoint-ssrf-preflight"; import type { GatewayRouteDiscoveryConstraints } from "../inference/gateway-route-compatibility"; import { getProbeExtraHeaders } from "../inference/onboard-probes"; +import { usesNvidiaEndpointProbePayload } from "../inference/openai-probe-models"; import type { OnboardInferenceCapabilityCache } from "./inference-capability-cache"; import type { NvidiaFeaturedModelSession } from "./nvidia-featured-model-selection"; import { exitOnboardFromPrompt, getNavigationChoice } from "./prompt-helpers"; @@ -474,7 +475,7 @@ export function createRemoteModelValidator(deps: RemoteModelValidatorDeps): { remoteConfig.helpUrl, withCredentialMutationGuard(state, { provider: state.provider, - useNvidiaEndpointProbePayload: state.provider === "nvidia-prod", + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(state.provider), requireResponsesToolCalling: deps.shouldRequireResponsesToolCalling(state.provider), skipResponsesProbe: deps.shouldSkipResponsesProbe(state.provider), authMode: deps.getProbeAuthMode(state.provider), From 80db29908779e7772a763f39735063c3c02bedff Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 3 Sep 2026 14:07:22 -0700 Subject: [PATCH 08/10] fix(inference): preserve NVIDIA payload on build validation Signed-off-by: Prekshi Vyas --- src/lib/onboard.ts | 5 + src/lib/onboard/setup-nim-selection.test.ts | 101 ++++++++++++++++++++ test/onboarding/onboard-selection.test.ts | 74 ++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 44fb3caf150..d4868fd7bbb 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -965,6 +965,9 @@ const { validateSelectedRemoteModel } = createRemoteModelValidator({ const { promptRemoteModel, promptInputModel } = modelPrompts; const { validateAnthropicModel, validateOpenAiLikeModel } = providerModels; const nousModels: typeof import("./inference/nous-models") = require("./inference/nous-models"); +const { + usesNvidiaEndpointProbePayload, +}: typeof import("./inference/openai-probe-models") = require("./inference/openai-probe-models"); // Build context helpers — delegated to src/lib/build-context.ts const { shouldIncludeBuildContextPath, copyBuildContextDir, printSandboxCreateRecoveryHints } = @@ -2280,6 +2283,8 @@ async function handleRemoteProviderSelection( "Please choose a provider/model again.", remoteConfig.helpUrl, withCredentialMutationGuard(state, { + provider: state.provider, + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(state.provider), requireResponsesToolCalling: shouldRequireResponsesToolCalling(state.provider), skipResponsesProbe: shouldSkipResponsesProbe(state.provider), authMode: getProbeAuthMode(state.provider), diff --git a/src/lib/onboard/setup-nim-selection.test.ts b/src/lib/onboard/setup-nim-selection.test.ts index e8fd1b954c2..37291dcdbf8 100644 --- a/src/lib/onboard/setup-nim-selection.test.ts +++ b/src/lib/onboard/setup-nim-selection.test.ts @@ -2,11 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import assert from "node:assert/strict"; +import http from "node:http"; import { describe, it } from "vitest"; import { requireValue } from "../core/require-value"; +import { MIN_PROBE_REPLY_TOKENS } from "../inference/max-tokens-field"; +import { probeOpenAiLikeEndpointOptimized } from "../inference/onboard-probes"; import { OnboardInferenceCapabilityCache } from "./inference-capability-cache"; +import { createInferenceSelectionValidationHelpers } from "./inference-selection-validation"; import { applyCloudFallbackSelection, clearNimContainerBeforeRetry, @@ -332,4 +336,101 @@ describe("createRemoteModelValidator", () => { assert.equal(receivedOptions?.useNvidiaEndpointProbePayload, true); }, ); + + it("sends the NVIDIA request payload only for NVIDIA Endpoints providers (#10880)", async () => { + const observedBodies: Array> = []; + const server = http.createServer((request, response) => { + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk) => { + body += chunk; + }); + request.on("end", () => { + observedBodies.push(JSON.parse(body)); + response.setHeader("content-type", "application/json"); + response.end('{"choices":[{"message":{"content":"OK"}}]}'); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + assert.ok(address && typeof address === "object"); + const endpointUrl = `http://provider.example.test:${address.port}/v1`; + const selectionValidation = createInferenceSelectionValidationHelpers({ + isNonInteractive: () => true, + agentProductName: () => "OpenClaw", + getCredential: () => "test-key", + probeOpenAiLikeEndpoint: (endpoint, model, apiKey, options = {}) => + probeOpenAiLikeEndpointOptimized(endpoint, model, apiKey, { + ...options, + validationSessionOptions: { + allowPrivateAddressesForTesting: true, + lookup: async () => [{ address: "127.0.0.1", family: 4 }], + }, + }), + promptValidationRecovery: async () => { + throw new Error("validation recovery must not run"); + }, + }); + const { validateSelectedRemoteModel } = createRemoteModelValidator({ + OPENAI_ENDPOINT_URL: endpointUrl, + ANTHROPIC_ENDPOINT_URL: "https://default-anthropic.example/v1", + requireValue, + isBackToSelection: (_value): _value is never => false, + validateCustomOpenAiLikeSelection: async () => ({ ok: false, retry: "selection" }), + validateCustomAnthropicSelection: async () => ({ ok: false, retry: "selection" }), + validateAnthropicSelectionWithRetryMessage: async () => ({ + ok: false, + retry: "selection", + }), + validateOpenAiLikeSelection: selectionValidation.validateOpenAiLikeSelection, + shouldRequireResponsesToolCalling: () => false, + shouldSkipResponsesProbe: () => true, + getProbeAuthMode: () => undefined, + }); + const model = "nvidia/nemotron-3-super-120b-a12b"; + + try { + for (const [selectedKey, provider] of [ + ["build", "nvidia-prod"], + ["openai", "openai-api"], + ] as const) { + const state = makeState(); + state.provider = provider; + state.endpointUrl = endpointUrl; + state.model = model; + assert.equal( + await validateSelectedRemoteModel({ + selected: { key: selectedKey }, + remoteConfig: { + label: provider === "nvidia-prod" ? "NVIDIA Endpoints" : "OpenAI", + endpointUrl, + helpUrl: null, + }, + state, + selectedCredentialEnv: + provider === "nvidia-prod" ? "NVIDIA_INFERENCE_API_KEY" : "OPENAI_API_KEY", + }), + "selected", + ); + } + } finally { + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + + assert.equal(observedBodies.length, 2); + assert.deepEqual(observedBodies[0], { + model, + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: MIN_PROBE_REPLY_TOKENS, + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }); + assert.ok(!("temperature" in observedBodies[1])); + assert.ok(!("top_p" in observedBodies[1])); + assert.ok(!("chat_template_kwargs" in observedBodies[1])); + }); }); diff --git a/test/onboarding/onboard-selection.test.ts b/test/onboarding/onboard-selection.test.ts index 1c319393f6e..dbad2c10430 100644 --- a/test/onboarding/onboard-selection.test.ts +++ b/test/onboarding/onboard-selection.test.ts @@ -3161,6 +3161,80 @@ reportChildScenario(async () => { ); }); + it("keeps the NVIDIA request payload for final build-provider revalidation (#10880)", () => { + const workspace = onboardProcessWorkspace("nemoclaw-onboard-build-payload-"); + const { root: tmpDir } = workspace; + const fakeBin = workspace.binDir; + const payloadLogPath = path.join(tmpDir, "chat-payloads.jsonl"); + + fs.writeFileSync( + path.join(fakeBin, "curl"), + `#!/usr/bin/env bash +body='{"choices":[{"message":{"content":"OK"}}]}' +status="200" +outfile="" +payload="" +url="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) outfile="$2"; shift 2 ;; + -d) payload="$2"; shift 2 ;; + --config) shift 2 ;; + *) url="$1"; shift ;; + esac +done +if echo "$url" | grep -q '/chat/completions$'; then + printf '%s\n' "$payload" >> "$NEMOCLAW_CHAT_PAYLOAD_LOG" +fi +printf '%s' "$body" > "$outfile" +printf '%s' "$status" +`, + { mode: 0o755 }, + ); + + const script = String.raw` +${onboardChildRuntimeSource} +const runner = require(${runnerPath}); +runner.runCapture = () => ""; +const { setupNim } = require(${onboardPath}); + +reportChildScenario(async () => { + process.env.NEMOCLAW_NON_INTERACTIVE = "1"; + process.env.NEMOCLAW_PROVIDER = "build"; + process.env.NEMOCLAW_MODEL = "nvidia/nemotron-3-super-120b-a12b"; + process.env.NVIDIA_INFERENCE_API_KEY = "nvapi-test"; + return setupNim(null); +}); +`; + const result = workspace.runNodeSource(script, { + name: "build-payload-check.js", + cwd: repoRoot, + env: { + ...process.env, + HOME: tmpDir, + PATH: `${fakeBin}:${process.env.PATH || ""}`, + HTTPS_PROXY: "http://proxy.invalid:8080", + https_proxy: "http://proxy.invalid:8080", + NO_PROXY: "", + no_proxy: "", + NEMOCLAW_CHAT_PAYLOAD_LOG: payloadLogPath, + }, + }); + + assert.equal(result.status, 0, result.stderr); + const payloads = fs + .readFileSync(payloadLogPath, "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line) as Record); + assert.equal(payloads.length, 1); + for (const payload of payloads) { + assert.equal(payload.temperature, 1); + assert.equal(payload.top_p, 0.95); + assert.deepEqual(payload.chat_template_kwargs, { enable_thinking: false }); + } + }); + it("treats a pasted NVIDIA API key at the retry prompt as retry and re-prompts securely", async () => { const state = makeRemoteSelectionState({ model: "nim/meta/llama-3.1-70b-instruct", From 76a0b181213347159ea4770d96504704b9cd794a Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 3 Sep 2026 14:34:55 -0700 Subject: [PATCH 09/10] fix(onboard): preserve Nemotron build validation payload Signed-off-by: Prekshi Vyas --- src/lib/inference/onboard-probes.ts | 17 +++- src/lib/onboard.ts | 7 +- .../inference-selection-validation.test.ts | 78 +++++++++++++++++++ test/onboarding/onboard-selection.test.ts | 8 +- 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/lib/inference/onboard-probes.ts b/src/lib/inference/onboard-probes.ts index 768f052f66d..161ef2ca198 100644 --- a/src/lib/inference/onboard-probes.ts +++ b/src/lib/inference/onboard-probes.ts @@ -35,7 +35,11 @@ const { getHostDockerInternalProbeFailure, isHijackedDockerInternalUrl, } = require("./onboard-host-docker-internal"); -const { isNvcfFunctionNotFoundForAccount, nvcfFunctionNotFoundMessage } = require("../validation"); +const { + isNvcfFunctionNotFoundForAccount, + nvcfFunctionNotFoundMessage, + shouldSkipResponsesProbe, +} = require("../validation"); const { isPrivateHostname, isPrivateIp, isLoopbackHostname } = require("../private-networks"); const { buildResolvePinArgs, isOperatorTrustablePrivateIp } = require("./endpoint-ssrf-preflight"); const { @@ -262,6 +266,16 @@ function getProbeAuthMode(_provider) { return undefined; } +function getOpenAiSelectionProbeOptions(provider) { + return { + provider, + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(provider), + requireResponsesToolCalling: shouldRequireResponsesToolCalling(provider), + skipResponsesProbe: shouldSkipResponsesProbe(provider), + authMode: getProbeAuthMode(provider), + }; +} + export function getProbeExtraHeaders(provider) { if (provider === openrouter.OPENROUTER_PROVIDER_NAME) { return openrouter.getOpenRouterCurlHeaders(); @@ -1131,6 +1145,7 @@ module.exports = { hasChatCompletionsToolCallLeak, shouldRequireResponsesToolCalling, getProbeAuthMode, + getOpenAiSelectionProbeOptions, getProbeExtraHeaders, getValidationProbeCurlArgs, getDeepSeekV4ProValidationProbeCurlArgs, diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 8097f382aaf..33723b21aaa 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -938,6 +938,7 @@ const { verifyOnboardInferenceSmoke, getProbeAuthMode, getValidationProbeCurlArgs, + getOpenAiSelectionProbeOptions, } = require("./inference/onboard-probes"); const { @@ -2315,11 +2316,7 @@ async function handleRemoteProviderSelection( state.credentialEnv, "Please choose a provider/model again.", remoteConfig.helpUrl, - withCredentialMutationGuard(state, { - requireResponsesToolCalling: shouldRequireResponsesToolCalling(state.provider), - skipResponsesProbe: shouldSkipResponsesProbe(state.provider), - authMode: getProbeAuthMode(state.provider), - }), + withCredentialMutationGuard(state, getOpenAiSelectionProbeOptions(state.provider)), ), }); if (buildValidation.retrySelection) return "retry-selection"; diff --git a/src/lib/onboard/inference-selection-validation.test.ts b/src/lib/onboard/inference-selection-validation.test.ts index 4473448aa4b..6d223b78211 100644 --- a/src/lib/onboard/inference-selection-validation.test.ts +++ b/src/lib/onboard/inference-selection-validation.test.ts @@ -93,6 +93,84 @@ describe("inference selection validation", () => { } }); + it.each([ + { + variant: "NVIDIA", + useNvidiaEndpointProbePayload: true, + expectedBody: { + model: "nvidia/nemotron-3-super-120b-a12b", + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: 16, + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }, + }, + { + variant: "generic", + useNvidiaEndpointProbePayload: false, + expectedBody: { + model: "nvidia/nemotron-3-super-120b-a12b", + messages: [{ role: "user", content: "Reply with exactly: OK" }], + max_tokens: 16, + }, + }, + ])( + "emits the $variant Nemotron request through selection validation (#10880)", + async ({ useNvidiaEndpointProbePayload, expectedBody }) => { + let observedBody = ""; + const server = http.createServer((request, response) => { + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk) => { + body += chunk; + }); + request.on("end", () => { + observedBody = body; + response.end('{"choices":[{"message":{"content":"OK"}}]}'); + }); + }); + const port = await listen(server); + const helpers = createInferenceSelectionValidationHelpers({ + isNonInteractive: () => false, + agentProductName: () => "OpenClaw", + promptValidationRecovery: vi.fn(async () => "selection" as const), + }); + const probeOptions = { + apiKey: "test-key", + skipResponsesProbe: true, + validationTiming: { + connectTimeoutSeconds: 1, + maxTimeSeconds: 1, + source: "standard" as const, + }, + validationSessionOptions: { + env: {}, + lookup: async () => [{ address: "127.0.0.1", family: 4 as const }], + allowPrivateAddressesForTesting: true, + }, + }; + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + + try { + await expect( + helpers.validateOpenAiLikeSelection( + "NVIDIA Endpoints", + `http://provider.example.com:${port}/v1`, + "nvidia/nemotron-3-super-120b-a12b", + null, + undefined, + undefined, + { ...probeOptions, useNvidiaEndpointProbePayload }, + ), + ).resolves.toEqual({ ok: true, api: "openai-completions" }); + expect(JSON.parse(observedBody)).toEqual(expectedBody); + } finally { + log.mockRestore(); + } + }, + ); + it("uses an explicit managed key without forwarding it as a probe option", async () => { const apiKey = "f".repeat(64); const getCredential = vi.fn(() => "ambient-key"); diff --git a/test/onboarding/onboard-selection.test.ts b/test/onboarding/onboard-selection.test.ts index 1c319393f6e..b13faaa7aa0 100644 --- a/test/onboarding/onboard-selection.test.ts +++ b/test/onboarding/onboard-selection.test.ts @@ -3071,7 +3071,7 @@ reportChildScenario(async () => { } }); - it("lets users re-enter an NVIDIA API key after authorization failure without restarting selection", () => { + it("lets users re-enter an NVIDIA API key and preserves the build revalidation payload (#10880)", () => { const workspace = onboardProcessWorkspace("nemoclaw-onboard-build-auth-retry-"); const { root: tmpDir } = workspace; const fakeBin = workspace.binDir; @@ -3083,10 +3083,10 @@ body='{"error":{"message":"forbidden"}}' status="403" outfile="" auth="" -url="" +data="" url="" while [ "$#" -gt 0 ]; do case "$1" in - -o) outfile="$2"; shift 2 ;; + -o) outfile="$2"; shift 2 ;; -d) data="$2"; shift 2 ;; -H) if echo "$2" | grep -q '^Authorization: Bearer '; then auth="$2" @@ -3099,7 +3099,7 @@ done if echo "$auth" | grep -q 'nvapi-good' && echo "$url" | grep -q '/responses$'; then body='{"id":"resp_123"}' status="200" -elif echo "$auth" | grep -q 'nvapi-good' && echo "$url" | grep -q '/chat/completions$'; then +elif echo "$auth" | grep -q 'nvapi-good' && echo "$url" | grep -q '/chat/completions$' && echo "$data" | grep -q '"temperature":1' && echo "$data" | grep -q '"top_p":0.95' && echo "$data" | grep -q '"enable_thinking":false'; then body='{"id":"chatcmpl-123"}' status="200" fi From d250e68b567cc938b3469b6219e98a6ed21d2cc9 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 3 Sep 2026 15:15:49 -0700 Subject: [PATCH 10/10] test(inference): align NVIDIA validation evidence Signed-off-by: Prekshi Vyas --- src/lib/inference/onboard-probes.ts | 17 +++- src/lib/onboard.ts | 10 +-- src/lib/onboard/setup-nim-selection.test.ts | 50 ++++++----- .../onboard-nvidia-endpoint-payload.test.ts | 85 +++++++++++++++++++ test/onboarding/onboard-selection.test.ts | 74 ---------------- 5 files changed, 130 insertions(+), 106 deletions(-) create mode 100644 test/onboarding/onboard-nvidia-endpoint-payload.test.ts diff --git a/src/lib/inference/onboard-probes.ts b/src/lib/inference/onboard-probes.ts index 768f052f66d..5f0894151cf 100644 --- a/src/lib/inference/onboard-probes.ts +++ b/src/lib/inference/onboard-probes.ts @@ -35,7 +35,11 @@ const { getHostDockerInternalProbeFailure, isHijackedDockerInternalUrl, } = require("./onboard-host-docker-internal"); -const { isNvcfFunctionNotFoundForAccount, nvcfFunctionNotFoundMessage } = require("../validation"); +const { + isNvcfFunctionNotFoundForAccount, + nvcfFunctionNotFoundMessage, + shouldSkipResponsesProbe, +} = require("../validation"); const { isPrivateHostname, isPrivateIp, isLoopbackHostname } = require("../private-networks"); const { buildResolvePinArgs, isOperatorTrustablePrivateIp } = require("./endpoint-ssrf-preflight"); const { @@ -262,6 +266,16 @@ function getProbeAuthMode(_provider) { return undefined; } +function getRemoteValidationProbeOptions(provider) { + return { + provider, + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(provider), + requireResponsesToolCalling: shouldRequireResponsesToolCalling(provider), + skipResponsesProbe: shouldSkipResponsesProbe(provider), + authMode: getProbeAuthMode(provider), + }; +} + export function getProbeExtraHeaders(provider) { if (provider === openrouter.OPENROUTER_PROVIDER_NAME) { return openrouter.getOpenRouterCurlHeaders(); @@ -1131,6 +1145,7 @@ module.exports = { hasChatCompletionsToolCallLeak, shouldRequireResponsesToolCalling, getProbeAuthMode, + getRemoteValidationProbeOptions, getProbeExtraHeaders, getValidationProbeCurlArgs, getDeepSeekV4ProValidationProbeCurlArgs, diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index d4868fd7bbb..a4a963212ef 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -934,6 +934,7 @@ const { shouldRequireResponsesToolCalling, verifyOnboardInferenceSmoke, getProbeAuthMode, + getRemoteValidationProbeOptions, getValidationProbeCurlArgs, } = require("./inference/onboard-probes"); @@ -965,9 +966,6 @@ const { validateSelectedRemoteModel } = createRemoteModelValidator({ const { promptRemoteModel, promptInputModel } = modelPrompts; const { validateAnthropicModel, validateOpenAiLikeModel } = providerModels; const nousModels: typeof import("./inference/nous-models") = require("./inference/nous-models"); -const { - usesNvidiaEndpointProbePayload, -}: typeof import("./inference/openai-probe-models") = require("./inference/openai-probe-models"); // Build context helpers — delegated to src/lib/build-context.ts const { shouldIncludeBuildContextPath, copyBuildContextDir, printSandboxCreateRecoveryHints } = @@ -2283,11 +2281,7 @@ async function handleRemoteProviderSelection( "Please choose a provider/model again.", remoteConfig.helpUrl, withCredentialMutationGuard(state, { - provider: state.provider, - useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(state.provider), - requireResponsesToolCalling: shouldRequireResponsesToolCalling(state.provider), - skipResponsesProbe: shouldSkipResponsesProbe(state.provider), - authMode: getProbeAuthMode(state.provider), + ...getRemoteValidationProbeOptions(state.provider), }), ), }); diff --git a/src/lib/onboard/setup-nim-selection.test.ts b/src/lib/onboard/setup-nim-selection.test.ts index 37291dcdbf8..c35e162105a 100644 --- a/src/lib/onboard/setup-nim-selection.test.ts +++ b/src/lib/onboard/setup-nim-selection.test.ts @@ -390,29 +390,33 @@ describe("createRemoteModelValidator", () => { const model = "nvidia/nemotron-3-super-120b-a12b"; try { - for (const [selectedKey, provider] of [ - ["build", "nvidia-prod"], - ["openai", "openai-api"], - ] as const) { - const state = makeState(); - state.provider = provider; - state.endpointUrl = endpointUrl; - state.model = model; - assert.equal( - await validateSelectedRemoteModel({ - selected: { key: selectedKey }, - remoteConfig: { - label: provider === "nvidia-prod" ? "NVIDIA Endpoints" : "OpenAI", - endpointUrl, - helpUrl: null, - }, - state, - selectedCredentialEnv: - provider === "nvidia-prod" ? "NVIDIA_INFERENCE_API_KEY" : "OPENAI_API_KEY", - }), - "selected", - ); - } + const nvidiaState = makeState(); + nvidiaState.provider = "nvidia-prod"; + nvidiaState.endpointUrl = endpointUrl; + nvidiaState.model = model; + assert.equal( + await validateSelectedRemoteModel({ + selected: { key: "build" }, + remoteConfig: { label: "NVIDIA Endpoints", endpointUrl, helpUrl: null }, + state: nvidiaState, + selectedCredentialEnv: "NVIDIA_INFERENCE_API_KEY", + }), + "selected", + ); + + const openAiState = makeState(); + openAiState.provider = "openai-api"; + openAiState.endpointUrl = endpointUrl; + openAiState.model = model; + assert.equal( + await validateSelectedRemoteModel({ + selected: { key: "openai" }, + remoteConfig: { label: "OpenAI", endpointUrl, helpUrl: null }, + state: openAiState, + selectedCredentialEnv: "OPENAI_API_KEY", + }), + "selected", + ); } finally { server.closeAllConnections(); await new Promise((resolve, reject) => diff --git a/test/onboarding/onboard-nvidia-endpoint-payload.test.ts b/test/onboarding/onboard-nvidia-endpoint-payload.test.ts new file mode 100644 index 00000000000..10a63035da0 --- /dev/null +++ b/test/onboarding/onboard-nvidia-endpoint-payload.test.ts @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { it, onTestFinished } from "vitest"; + +import { createOnboardProcessWorkspace } from "../helpers/onboard-child-process-harness.js"; +import { onboardChildRuntimeSource } from "../helpers/onboard-child-runtime.js"; + +const repoRoot = path.join(import.meta.dirname, "../.."); +const onboardPath = JSON.stringify(path.join(repoRoot, "src", "lib", "onboard.ts")); +const runnerPath = JSON.stringify(path.join(repoRoot, "src", "lib", "runner.ts")); + +it("keeps the NVIDIA request payload for final build-provider revalidation (#10880)", () => { + const workspace = createOnboardProcessWorkspace("nemoclaw-onboard-build-payload-"); + onTestFinished(() => workspace.remove()); + const { root: tmpDir } = workspace; + const fakeBin = workspace.binDir; + const payloadLogPath = path.join(tmpDir, "chat-payloads.jsonl"); + + fs.writeFileSync( + path.join(fakeBin, "curl"), + `#!/usr/bin/env bash +body='{"choices":[{"message":{"content":"OK"}}]}' +status="200" +outfile="" +payload="" +url="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) outfile="$2"; shift 2 ;; + -d) payload="$2"; shift 2 ;; + --config) shift 2 ;; + *) url="$1"; shift ;; + esac +done +if echo "$url" | grep -q '/chat/completions$'; then + printf '%s\n' "$payload" >> "$NEMOCLAW_CHAT_PAYLOAD_LOG" +fi +printf '%s' "$body" > "$outfile" +printf '%s' "$status" +`, + { mode: 0o755 }, + ); + + const script = String.raw` +${onboardChildRuntimeSource} +const runner = require(${runnerPath}); +runner.runCapture = () => ""; +const { setupNim } = require(${onboardPath}); + +reportChildScenario(async () => { + process.env.NEMOCLAW_NON_INTERACTIVE = "1"; + process.env.NEMOCLAW_PROVIDER = "build"; + process.env.NEMOCLAW_MODEL = "nvidia/nemotron-3-super-120b-a12b"; + process.env.NVIDIA_INFERENCE_API_KEY = "nvapi-test"; + return setupNim(null); +}); +`; + const result = workspace.runNodeSource(script, { + name: "build-payload-check.js", + cwd: repoRoot, + env: { + ...process.env, + HOME: tmpDir, + PATH: `${fakeBin}:${process.env.PATH || ""}`, + HTTPS_PROXY: "http://proxy.invalid:8080", + https_proxy: "http://proxy.invalid:8080", + NO_PROXY: "", + no_proxy: "", + NEMOCLAW_CHAT_PAYLOAD_LOG: payloadLogPath, + }, + }); + + assert.equal(result.status, 0, result.stderr); + const payloadText = fs.readFileSync(payloadLogPath, "utf8").trim(); + assert.equal(payloadText.split("\n").length, 1); + const payload = JSON.parse(payloadText) as Record; + assert.equal(payload.temperature, 1); + assert.equal(payload.top_p, 0.95); + assert.deepEqual(payload.chat_template_kwargs, { enable_thinking: false }); +}); diff --git a/test/onboarding/onboard-selection.test.ts b/test/onboarding/onboard-selection.test.ts index dbad2c10430..1c319393f6e 100644 --- a/test/onboarding/onboard-selection.test.ts +++ b/test/onboarding/onboard-selection.test.ts @@ -3161,80 +3161,6 @@ reportChildScenario(async () => { ); }); - it("keeps the NVIDIA request payload for final build-provider revalidation (#10880)", () => { - const workspace = onboardProcessWorkspace("nemoclaw-onboard-build-payload-"); - const { root: tmpDir } = workspace; - const fakeBin = workspace.binDir; - const payloadLogPath = path.join(tmpDir, "chat-payloads.jsonl"); - - fs.writeFileSync( - path.join(fakeBin, "curl"), - `#!/usr/bin/env bash -body='{"choices":[{"message":{"content":"OK"}}]}' -status="200" -outfile="" -payload="" -url="" -while [ "$#" -gt 0 ]; do - case "$1" in - -o) outfile="$2"; shift 2 ;; - -d) payload="$2"; shift 2 ;; - --config) shift 2 ;; - *) url="$1"; shift ;; - esac -done -if echo "$url" | grep -q '/chat/completions$'; then - printf '%s\n' "$payload" >> "$NEMOCLAW_CHAT_PAYLOAD_LOG" -fi -printf '%s' "$body" > "$outfile" -printf '%s' "$status" -`, - { mode: 0o755 }, - ); - - const script = String.raw` -${onboardChildRuntimeSource} -const runner = require(${runnerPath}); -runner.runCapture = () => ""; -const { setupNim } = require(${onboardPath}); - -reportChildScenario(async () => { - process.env.NEMOCLAW_NON_INTERACTIVE = "1"; - process.env.NEMOCLAW_PROVIDER = "build"; - process.env.NEMOCLAW_MODEL = "nvidia/nemotron-3-super-120b-a12b"; - process.env.NVIDIA_INFERENCE_API_KEY = "nvapi-test"; - return setupNim(null); -}); -`; - const result = workspace.runNodeSource(script, { - name: "build-payload-check.js", - cwd: repoRoot, - env: { - ...process.env, - HOME: tmpDir, - PATH: `${fakeBin}:${process.env.PATH || ""}`, - HTTPS_PROXY: "http://proxy.invalid:8080", - https_proxy: "http://proxy.invalid:8080", - NO_PROXY: "", - no_proxy: "", - NEMOCLAW_CHAT_PAYLOAD_LOG: payloadLogPath, - }, - }); - - assert.equal(result.status, 0, result.stderr); - const payloads = fs - .readFileSync(payloadLogPath, "utf8") - .trim() - .split("\n") - .map((line) => JSON.parse(line) as Record); - assert.equal(payloads.length, 1); - for (const payload of payloads) { - assert.equal(payload.temperature, 1); - assert.equal(payload.top_p, 0.95); - assert.deepEqual(payload.chat_template_kwargs, { enable_thinking: false }); - } - }); - it("treats a pasted NVIDIA API key at the retry prompt as retry and re-prompts securely", async () => { const state = makeRemoteSelectionState({ model: "nim/meta/llama-3.1-70b-instruct",