diff --git a/src/lib/inference/health.test.ts b/src/lib/inference/health.test.ts index 17aa2165cd6..7d05d016fb9 100644 --- a/src/lib/inference/health.test.ts +++ b/src/lib/inference/health.test.ts @@ -186,6 +186,28 @@ describe("inference health", () => { expect(payload.model).toBe("meta/llama-3.3-70b-instruct"); }); + 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[] = []; const result = probeRemoteProviderHealth("nvidia-nim", { diff --git a/src/lib/inference/health.ts b/src/lib/inference/health.ts index ccf1b3b17a2..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"; @@ -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 { @@ -652,13 +661,14 @@ export function probeRemoteProviderHealth( if (!config?.model) return null; - if (NVIDIA_MANAGED_PROVIDERS.has(provider)) { + if (usesNvidiaEndpointProbePayload(provider)) { return probeChatCompletionsProviderHealth( providerLabel, config.model, 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 c7efcc811f7..b71012a35a4 100644 --- a/src/lib/inference/onboard-probes.test.ts +++ b/src/lib/inference/onboard-probes.test.ts @@ -309,8 +309,35 @@ describe("OpenAI-compatible inference probes", () => { }); }); - it("keeps the default chat-completions probe bounded for other models", () => { - expect(getChatCompletionsProbePayload("nvidia/nemotron-3-super-120b-a12b")).toEqual({ + it("serializes the Nemotron 3 Super validation request parameters (#10880)", () => { + 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, + useNvidiaEndpointProbePayload: true, + }); + + 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, + temperature: 1, + top_p: 0.95, + chat_template_kwargs: { enable_thinking: false }, + }); + }); + + 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, @@ -905,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 @@ -954,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)) @@ -1417,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); @@ -1428,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 cc1a798df67..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 { @@ -57,6 +61,7 @@ const { STRICT_TOOL_PROBE_INITIAL_TOKENS, STRICT_TOOL_PROBE_RETRY_TOKEN_LADDER, strictToolProbeReasoningRetryMessage, + usesNvidiaEndpointProbePayload, vllmProbePolicyForModel, } = require("./openai-probe-models"); const { @@ -261,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(); @@ -535,6 +550,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { isWsl?: boolean; pinnedAddresses?: readonly string[]; validationTiming?: unknown; + useNvidiaEndpointProbePayload?: boolean; }) { const { credentialArgs, @@ -544,6 +560,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { isWsl: isWslOverride, pinnedAddresses, validationTiming, + useNvidiaEndpointProbePayload, } = opts; const platformOptions = getProbeTimingOptions({ ...(typeof isWslOverride === "boolean" ? { isWsl: isWslOverride } : {}), @@ -559,7 +576,7 @@ export function getChatCompletionsProbeCurlArgs(opts: { "Content-Type: application/json", ...credSlice, "-d", - JSON.stringify(getChatCompletionsProbePayload(model)), + JSON.stringify(getChatCompletionsProbePayload(model, { useNvidiaEndpointProbePayload })), url, ]; } @@ -573,6 +590,7 @@ function runChatCompletionsProbe({ pinnedAddresses, trustedPrivateCapability, validationTiming, + useNvidiaEndpointProbePayload, spawnSyncImpl, }) { const args = getChatCompletionsProbeCurlArgs({ @@ -582,6 +600,7 @@ function runChatCompletionsProbe({ isWsl: isWslOverride, pinnedAddresses, validationTiming, + useNvidiaEndpointProbePayload, }); const probeOpts = { timeoutMs: getProbeProcessTimeoutMs(args), @@ -621,7 +640,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 +902,7 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) { pinnedAddresses, trustedPrivateCapability: options.trustedPrivateCapability, validationTiming, + useNvidiaEndpointProbePayload: options.useNvidiaEndpointProbePayload, spawnSyncImpl: options.spawnSyncImpl, }), }; @@ -1121,6 +1145,7 @@ module.exports = { hasChatCompletionsToolCallLeak, shouldRequireResponsesToolCalling, getProbeAuthMode, + getOpenAiSelectionProbeOptions, getProbeExtraHeaders, getValidationProbeCurlArgs, getDeepSeekV4ProValidationProbeCurlArgs, @@ -1197,6 +1222,7 @@ export async function verifyOnboardInferenceSmoke(options: any, dependencies: an authMode: getProbeAuthMode(options.provider), extraHeaders: getProbeExtraHeaders(options.provider), skipResponsesProbe: true, + 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 85d92c4f3bb..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 }) => @@ -47,7 +53,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,6 +83,18 @@ 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 c42dd063533..2000b6f4f3b 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.ts b/src/lib/onboard.ts index 54674c214c9..4ecee3a2c4d 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -936,6 +936,7 @@ const { verifyOnboardInferenceSmoke, getProbeAuthMode, getValidationProbeCurlArgs, + getOpenAiSelectionProbeOptions, } = require("./inference/onboard-probes"); const { @@ -2280,11 +2281,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/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..e8fd1b954c2 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,57 @@ describe("createRemoteModelValidator", () => { capabilityCache: undefined, }); }); + + 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" }; + }, + 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..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"; @@ -237,6 +238,7 @@ type ProbeAuthMode = "bearer" | "query-param" | undefined; type ProbeOptions = { requireResponsesToolCalling?: boolean; skipResponsesProbe?: boolean; + useNvidiaEndpointProbePayload?: boolean; authMode?: ProbeAuthMode; extraHeaders?: readonly string[]; capabilityCache?: OnboardInferenceCapabilityCache; @@ -473,6 +475,7 @@ export function createRemoteModelValidator(deps: RemoteModelValidatorDeps): { remoteConfig.helpUrl, withCredentialMutationGuard(state, { provider: state.provider, + useNvidiaEndpointProbePayload: usesNvidiaEndpointProbePayload(state.provider), requireResponsesToolCalling: deps.shouldRequireResponsesToolCalling(state.provider), skipResponsesProbe: deps.shouldSkipResponsesProbe(state.provider), authMode: deps.getProbeAuthMode(state.provider), 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