diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 57fd3144c6..669aac9f66 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -296,6 +296,7 @@ const onboardSession: typeof import("./state/onboard-session") = require("./stat const { OnboardRuntimeBoundary }: typeof import("./onboard/runtime-boundary") = require("./onboard/runtime-boundary"); const { handleGatewayState }: typeof import("./onboard/machine/handlers/gateway") = require("./onboard/machine/handlers/gateway"); const { handlePreflightState }: typeof import("./onboard/machine/handlers/preflight") = require("./onboard/machine/handlers/preflight"); +const { handleProviderInferenceState }: typeof import("./onboard/machine/handlers/provider-inference") = require("./onboard/machine/handlers/provider-inference"); const policies: typeof import("./policy") = require("./policy"); const tiers: typeof import("./policy/tiers") = require("./policy/tiers"); const { ensureUsageNoticeConsent } = require("./onboard/usage-notice"); @@ -9405,181 +9406,77 @@ async function onboard(opts: OnboardOptions = {}): Promise { console.error(" Start a fresh onboard with --name to choose a different name."); process.exit(1); } - let model = session?.model || null; - let provider = session?.provider || null; - let endpointUrl = session?.endpointUrl || null; - let credentialEnv = session?.credentialEnv || null; - let hermesAuthMethod: HermesAuthMethod | null = - normalizeHermesAuthMethod(session?.hermesAuthMethod) || - (provider === hermesProviderAuth.HERMES_PROVIDER_NAME && - session?.credentialEnv === HERMES_NOUS_API_KEY_CREDENTIAL_ENV - ? HERMES_AUTH_METHOD_API_KEY - : null); - let hermesToolGateways = normalizeHermesToolGatewaySelections(session?.hermesToolGateways); - let preferredInferenceApi = session?.preferredInferenceApi || null; - let nimContainer = session?.nimContainer || null; - let webSearchConfig = session?.webSearchConfig || null; - let forceProviderSelection = forceProviderSelectionForAgentChange; - while (true) { - const resumeProviderSelection = - !forceProviderSelection && - resume && - session?.steps?.provider_selection?.status === "complete" && - typeof provider === "string" && - typeof model === "string"; - if (resumeProviderSelection) { - skippedStepMessage("provider_selection", `${provider} / ${model}`); - hydrateCredentialEnv(credentialEnv); - // #3342: resume short-circuits provider selection — repair the - // ollama-local systemd loopback override here so legacy 0.0.0.0 - // drop-ins from older NemoClaw versions get rewritten every resume. - repairLocalInferenceSystemdOverrideOrExit(provider, isNonInteractive); - } else { - // #2753: do not persist sandboxName to onboard-session.json before - // the sandbox actually exists in the gateway (Step 6 markStepComplete - // below). A SIGINT between any earlier step and createSandbox would - // otherwise leave a phantom that `nemoclaw list` resurrects until - // manually destroyed. - await startRecordedStep("provider_selection"); - const selection = await setupNim(gpu, sandboxName, agent); - model = selection.model; - provider = selection.provider; - endpointUrl = selection.endpointUrl; - credentialEnv = selection.credentialEnv; - hermesAuthMethod = selection.hermesAuthMethod; - hermesToolGateways = selection.hermesToolGateways; - preferredInferenceApi = selection.preferredInferenceApi; - nimContainer = selection.nimContainer; - await recordStepComplete( - "provider_selection", - toSessionUpdates({ - provider, - model, - endpointUrl, - credentialEnv, - hermesAuthMethod, - hermesToolGateways, - preferredInferenceApi, - nimContainer, - }), - ); - } - - if (typeof provider !== "string" || typeof model !== "string") { - console.error(" Inference selection did not yield a provider/model."); - process.exit(1); - } - process.env.NEMOCLAW_OPENSHELL_BIN = getOpenshellBinary(); - const needsBedrockRuntimeAdapter = - provider === "compatible-anthropic-endpoint" && - bedrockRuntimeOnboard.needsBedrockRuntimeAdapter(endpointUrl); - const resumeInference = - !needsBedrockRuntimeAdapter && - !forceProviderSelection && - resume && - isInferenceRouteReady(provider, model); - if (resumeInference) { - if (provider === hermesProviderAuth.HERMES_PROVIDER_NAME) { - if (!sandboxName) { - sandboxName = await promptValidatedSandboxName(agent); - } - await startRecordedStep("inference", { provider, model }); - const inferenceResult = await setupInference( - sandboxName, - model, - provider, - endpointUrl, - credentialEnv, - hermesAuthMethod, - hermesToolGateways, - ); - if (inferenceResult?.retry === "selection") { - forceProviderSelection = true; - continue; - } - await recordStepComplete( - "inference", - toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), - ); - break; - } - if (isRoutedInferenceProvider(provider)) { - try { - await reconcileModelRouter(); - } catch (err) { - console.error( - ` ✗ Failed to reconcile model router: ${err instanceof Error ? err.message : String(err)}`, - ); - process.exit(1); - } - } - skippedStepMessage("inference", `${provider} / ${model}`); - if (nimContainer && sandboxName) { - registry.updateSandbox(sandboxName, { nimContainer }); - } - await recordStepComplete( - "inference", - toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), - ); - break; - } - - if (!sandboxName) { - sandboxName = await promptValidatedSandboxName(agent); - } - const buildEstimateNote = - process.env.NEMOCLAW_IGNORE_RUNTIME_RESOURCES === "1" - ? null - : formatSandboxBuildEstimateNote(assessHost()); - console.log( - formatOnboardConfigSummary({ - provider, - model, - credentialEnv, - hermesAuthMethod, - webSearchConfig, - hermesToolGateways, - enabledChannels: selectedMessagingChannels.length > 0 ? selectedMessagingChannels : null, - sandboxName, - notes: buildEstimateNote ? [buildEstimateNote] : [], - }), - ); - console.log(" Web search and messaging channels will be prompted next."); - if (!isNonInteractive()) { - if (!(await promptYesNoOrDefault(" Apply this configuration?", null, true))) { - console.log(` Aborted. Re-run \`${cliName()} onboard\` to start over.`); - console.log(" Credentials entered so far were only staged in memory for this run."); - console.log( - " No new gateway credential was registered because onboarding stopped here.", - ); - process.exit(0); - } - } - - await startRecordedStep("inference", { provider, model }); - const inferenceResult = await setupInference( - sandboxName, - model, - provider, - endpointUrl, - credentialEnv, - hermesAuthMethod, - hermesToolGateways, - ); - delete process.env.NVIDIA_API_KEY; - if (inferenceResult?.retry === "selection") { - forceProviderSelection = true; - continue; - } - if (nimContainer && sandboxName) { - registry.updateSandbox(sandboxName, { nimContainer }); - } - await recordStepComplete( - "inference", - toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), - ); - break; - } + const providerInferenceResult = await handleProviderInferenceState({ + resume, + session, + gpu, + sandboxName, + agent, + forceProviderSelection: forceProviderSelectionForAgentChange, + initial: { + model: session?.model || null, + provider: session?.provider || null, + endpointUrl: session?.endpointUrl || null, + credentialEnv: session?.credentialEnv || null, + hermesAuthMethod: session?.hermesAuthMethod || null, + hermesToolGateways: normalizeHermesToolGatewaySelections(session?.hermesToolGateways), + preferredInferenceApi: session?.preferredInferenceApi || null, + nimContainer: session?.nimContainer || null, + webSearchConfig: session?.webSearchConfig || null, + }, + selectedMessagingChannels, + env: process.env, + constants: { + hermesProviderName: hermesProviderAuth.HERMES_PROVIDER_NAME, + hermesApiKeyAuthMethod: HERMES_AUTH_METHOD_API_KEY, + hermesApiKeyCredentialEnv: HERMES_NOUS_API_KEY_CREDENTIAL_ENV, + }, + deps: { + normalizeHermesAuthMethod, + setupNim, + setupInference, + startRecordedStep, + recordStepComplete, + toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters[0]), + skippedStepMessage, + hydrateCredentialEnv, + repairLocalInferenceSystemdOverrideOrExit, + isNonInteractive, + getOpenshellBinary, + needsBedrockRuntimeAdapter: (providerName, url) => + providerName === "compatible-anthropic-endpoint" && + bedrockRuntimeOnboard.needsBedrockRuntimeAdapter(url), + isInferenceRouteReady, + isRoutedInferenceProvider, + reconcileModelRouter, + registryUpdateSandbox: (name, updates) => registry.updateSandbox(name, updates), + promptValidatedSandboxName, + assessHost, + formatSandboxBuildEstimateNote, + formatOnboardConfigSummary, + promptYesNoOrDefault, + cliName, + log: (message) => console.log(message), + error: (message) => console.error(message), + exitProcess: (code) => process.exit(code), + deleteEnv: (name) => { + delete process.env[name]; + }, + }, + }); + session = providerInferenceResult.session; + sandboxName = providerInferenceResult.sandboxName; + const { + model, + provider, + endpointUrl, + credentialEnv, + hermesAuthMethod, + hermesToolGateways, + preferredInferenceApi, + nimContainer, + } = providerInferenceResult; + let webSearchConfig = providerInferenceResult.webSearchConfig; const webSearchSupportProbePath = fromDockerfile ? path.resolve(fromDockerfile) : null; const webSearchSupported = agentSupportsWebSearch(agent, webSearchSupportProbePath, ROOT); diff --git a/src/lib/onboard/machine/handlers/provider-inference.test.ts b/src/lib/onboard/machine/handlers/provider-inference.test.ts new file mode 100644 index 0000000000..bdc67f1cf6 --- /dev/null +++ b/src/lib/onboard/machine/handlers/provider-inference.test.ts @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +import { createSession, type Session, type SessionUpdates } from "../../../state/onboard-session"; +import { + handleProviderInferenceState, + type ProviderInferenceStateOptions, + type ProviderSelectionResult, +} from "./provider-inference"; + +type Gpu = { type: string } | null; +type Agent = { name: string } | null; +type Host = { cpus?: number }; + +const baseSelection: ProviderSelectionResult = { + model: "nvidia/test", + provider: "nvidia-prod", + endpointUrl: "https://integrate.api.nvidia.com/v1", + credentialEnv: "NVIDIA_API_KEY", + hermesAuthMethod: null, + hermesToolGateways: [], + preferredInferenceApi: "openai-responses", + nimContainer: null, +}; + +function createDeps(overrides: Partial["deps"]> = {}) { + const calls = { + setupNim: vi.fn(async () => ({ ...baseSelection })), + setupInference: vi.fn(async () => ({ ok: true as const })), + startStep: vi.fn(async () => undefined), + complete: vi.fn(async () => createSession()), + skipped: vi.fn(), + hydrate: vi.fn(), + repair: vi.fn(), + routeReady: vi.fn(() => false), + reconcileRouter: vi.fn(async () => undefined), + updateSandbox: vi.fn(), + promptName: vi.fn(async () => "my-assistant"), + promptYesNo: vi.fn(async () => true), + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number): never => { + throw new Error(`exit ${code}`); + }), + deleteEnv: vi.fn(), + }; + return { + calls, + deps: { + normalizeHermesAuthMethod: (value: string | null | undefined) => value ?? null, + setupNim: calls.setupNim, + setupInference: calls.setupInference, + startRecordedStep: calls.startStep, + recordStepComplete: calls.complete, + toSessionUpdates: (updates: Record) => updates as SessionUpdates, + skippedStepMessage: calls.skipped, + hydrateCredentialEnv: calls.hydrate, + repairLocalInferenceSystemdOverrideOrExit: calls.repair, + isNonInteractive: () => true, + getOpenshellBinary: () => "/usr/bin/openshell", + needsBedrockRuntimeAdapter: () => false, + isInferenceRouteReady: calls.routeReady, + isRoutedInferenceProvider: (provider: string) => provider === "nvidia-router", + reconcileModelRouter: calls.reconcileRouter, + registryUpdateSandbox: calls.updateSandbox, + promptValidatedSandboxName: calls.promptName, + assessHost: () => ({ cpus: 8 }), + formatSandboxBuildEstimateNote: () => "estimate", + formatOnboardConfigSummary: (options: { + provider: string; + model: string; + sandboxName: string; + }) => `summary:${options.provider}/${options.model}/${options.sandboxName}`, + promptYesNoOrDefault: calls.promptYesNo, + cliName: () => "nemoclaw", + log: calls.log, + error: calls.error, + exitProcess: calls.exit, + deleteEnv: calls.deleteEnv, + ...overrides, + }, + }; +} + +function baseOptions( + deps: ProviderInferenceStateOptions["deps"], + session: Session | null = createSession(), +): ProviderInferenceStateOptions { + return { + resume: false, + session, + gpu: { type: "nvidia" }, + sandboxName: null, + agent: null, + initial: { + model: session?.model ?? null, + provider: session?.provider ?? null, + endpointUrl: session?.endpointUrl ?? null, + credentialEnv: session?.credentialEnv ?? null, + hermesAuthMethod: session?.hermesAuthMethod ?? null, + hermesToolGateways: session?.hermesToolGateways ?? [], + preferredInferenceApi: session?.preferredInferenceApi ?? null, + nimContainer: session?.nimContainer ?? null, + webSearchConfig: session?.webSearchConfig ?? null, + }, + selectedMessagingChannels: [], + env: {}, + constants: { + hermesProviderName: "hermes-provider", + hermesApiKeyAuthMethod: "api_key", + hermesApiKeyCredentialEnv: "NOUS_API_KEY", + }, + deps, + }; +} + +describe("handleProviderInferenceState", () => { + it("runs provider selection and inference setup on a fresh flow", async () => { + const { deps, calls } = createDeps(); + + const result = await handleProviderInferenceState(baseOptions(deps)); + + expect(calls.startStep).toHaveBeenNthCalledWith(1, "provider_selection"); + expect(calls.setupNim).toHaveBeenCalledWith({ type: "nvidia" }, null, null); + expect(calls.complete).toHaveBeenCalledWith("provider_selection", expect.objectContaining({ provider: "nvidia-prod" })); + expect(calls.promptName).toHaveBeenCalledWith(null); + expect(calls.log).toHaveBeenCalledWith("summary:nvidia-prod/nvidia/test/my-assistant"); + expect(calls.startStep).toHaveBeenNthCalledWith(2, "inference", { + provider: "nvidia-prod", + model: "nvidia/test", + }); + expect(calls.setupInference).toHaveBeenCalledWith( + "my-assistant", + "nvidia/test", + "nvidia-prod", + "https://integrate.api.nvidia.com/v1", + "NVIDIA_API_KEY", + null, + [], + ); + expect(calls.deleteEnv).toHaveBeenCalledWith("NVIDIA_API_KEY"); + expect(result).toMatchObject({ + sandboxName: "my-assistant", + model: "nvidia/test", + provider: "nvidia-prod", + preferredInferenceApi: "openai-responses", + }); + }); + + it("skips provider selection and inference setup when resume state is already ready", async () => { + const session = createSession({ + provider: "ollama-local", + model: "llama3.1", + credentialEnv: null, + }); + session.steps.provider_selection.status = "complete"; + const { deps, calls } = createDeps({ isInferenceRouteReady: vi.fn(() => true) }); + + const result = await handleProviderInferenceState({ + ...baseOptions(deps, session), + resume: true, + sandboxName: "my-assistant", + }); + + expect(calls.setupNim).not.toHaveBeenCalled(); + expect(calls.setupInference).not.toHaveBeenCalled(); + expect(calls.skipped).toHaveBeenCalledWith("provider_selection", "ollama-local / llama3.1"); + expect(calls.hydrate).toHaveBeenCalledWith(null); + expect(calls.repair).toHaveBeenCalledWith("ollama-local", deps.isNonInteractive); + expect(calls.skipped).toHaveBeenCalledWith("inference", "ollama-local / llama3.1"); + expect(result).toMatchObject({ provider: "ollama-local", model: "llama3.1" }); + }); + + it("reconciles model router on resumed routed inference", async () => { + const session = createSession({ provider: "nvidia-router", model: "router/model" }); + session.steps.provider_selection.status = "complete"; + const { deps, calls } = createDeps({ isInferenceRouteReady: vi.fn(() => true) }); + + await handleProviderInferenceState({ + ...baseOptions(deps, session), + resume: true, + sandboxName: "router-sandbox", + }); + + expect(calls.reconcileRouter).toHaveBeenCalledOnce(); + }); + + it("returns to provider selection when inference setup requests a retry", async () => { + const setupNim = vi + .fn() + .mockResolvedValueOnce({ ...baseSelection, model: "bad" }) + .mockResolvedValueOnce({ ...baseSelection, model: "good" }); + const setupInference = vi + .fn() + .mockResolvedValueOnce({ retry: "selection" as const }) + .mockResolvedValueOnce({ ok: true as const }); + const { deps, calls } = createDeps({ setupNim, setupInference }); + + const result = await handleProviderInferenceState(baseOptions(deps)); + + expect(setupNim).toHaveBeenCalledTimes(2); + expect(setupInference).toHaveBeenCalledTimes(2); + expect(result.model).toBe("good"); + expect(calls.startStep).toHaveBeenCalledWith("provider_selection"); + }); + + it("aborts before inference setup when the configuration summary is rejected", async () => { + const { deps, calls } = createDeps({ + isNonInteractive: () => false, + promptYesNoOrDefault: vi.fn(async () => false), + }); + + await expect(handleProviderInferenceState(baseOptions(deps))).rejects.toThrow("exit 0"); + + expect(calls.exit).toHaveBeenCalledWith(0); + expect(calls.setupInference).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/onboard/machine/handlers/provider-inference.ts b/src/lib/onboard/machine/handlers/provider-inference.ts new file mode 100644 index 0000000000..161b0f0b0c --- /dev/null +++ b/src/lib/onboard/machine/handlers/provider-inference.ts @@ -0,0 +1,296 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { WebSearchConfig } from "../../../inference/web-search"; +import type { Session, SessionUpdates } from "../../../state/onboard-session"; + +export type ProviderInferenceRetry = { retry: "selection" } | { ok: true; retry?: undefined }; + +export interface ProviderSelectionResult { + model: string | null; + provider: string; + endpointUrl: string | null; + credentialEnv: string | null; + hermesAuthMethod: string | null; + hermesToolGateways: string[]; + preferredInferenceApi: string | null; + nimContainer: string | null; +} + +export interface ProviderInferenceStateOptions { + resume: boolean; + session: Session | null; + gpu: Gpu; + sandboxName: string | null; + agent: Agent; + forceProviderSelection?: boolean; + initial: { + model: string | null; + provider: string | null; + endpointUrl: string | null; + credentialEnv: string | null; + hermesAuthMethod: string | null; + hermesToolGateways: string[]; + preferredInferenceApi: string | null; + nimContainer: string | null; + webSearchConfig: WebSearchConfig | null; + }; + selectedMessagingChannels: string[]; + env: NodeJS.ProcessEnv; + constants: { + hermesProviderName: string; + hermesApiKeyAuthMethod: string; + hermesApiKeyCredentialEnv: string; + }; + deps: { + normalizeHermesAuthMethod(value: string | null | undefined): string | null; + setupNim(gpu: Gpu, sandboxName: string | null, agent: Agent): Promise; + setupInference( + sandboxName: string | null, + model: string, + provider: string, + endpointUrl: string | null, + credentialEnv: string | null, + hermesAuthMethod: string | null, + hermesToolGateways: string[], + ): Promise; + startRecordedStep( + stepName: string, + updates?: { provider?: string | null; model?: string | null }, + ): Promise; + recordStepComplete(stepName: string, updates: SessionUpdates): Promise; + toSessionUpdates(updates: Record): SessionUpdates; + skippedStepMessage(stepName: string, detail?: string | null): void; + hydrateCredentialEnv(credentialEnv: string | null): void; + repairLocalInferenceSystemdOverrideOrExit(provider: string | null, isNonInteractive: () => boolean): void; + isNonInteractive(): boolean; + getOpenshellBinary(): string; + needsBedrockRuntimeAdapter(provider: string, endpointUrl: string | null): boolean; + isInferenceRouteReady(provider: string, model: string): boolean; + isRoutedInferenceProvider(provider: string): boolean; + reconcileModelRouter(): Promise; + registryUpdateSandbox(sandboxName: string, updates: { nimContainer?: string | null }): void; + promptValidatedSandboxName(agent: Agent): Promise; + assessHost(): Host; + formatSandboxBuildEstimateNote(host: Host): string | null; + formatOnboardConfigSummary(options: { + provider: string; + model: string; + credentialEnv: string | null; + hermesAuthMethod: string | null; + webSearchConfig: WebSearchConfig | null; + hermesToolGateways: string[]; + enabledChannels: string[] | null; + sandboxName: string; + notes: string[]; + }): string; + promptYesNoOrDefault(question: string, envVar: string | null, defaultIsYes: boolean): Promise; + cliName(): string; + log(message?: string): void; + error(message?: string): void; + exitProcess(code: number): never; + deleteEnv(name: string): void; + }; +} + +export interface ProviderInferenceStateResult { + sandboxName: string | null; + model: string; + provider: string; + endpointUrl: string | null; + credentialEnv: string | null; + hermesAuthMethod: string | null; + hermesToolGateways: string[]; + preferredInferenceApi: string | null; + nimContainer: string | null; + webSearchConfig: WebSearchConfig | null; + session: Session | null; +} + +function requireSelection(provider: string | null, model: string | null): { provider: string; model: string } { + if (typeof provider !== "string" || typeof model !== "string") { + throw new Error("Inference selection did not yield a provider/model."); + } + return { provider, model }; +} + +export async function handleProviderInferenceState({ + resume, + session, + gpu, + sandboxName, + agent, + forceProviderSelection: initialForceProviderSelection = false, + initial, + selectedMessagingChannels, + env, + constants, + deps, +}: ProviderInferenceStateOptions): Promise { + let model = initial.model; + let provider = initial.provider; + let endpointUrl = initial.endpointUrl; + let credentialEnv = initial.credentialEnv; + let hermesAuthMethod = + deps.normalizeHermesAuthMethod(initial.hermesAuthMethod) || + (provider === constants.hermesProviderName && credentialEnv === constants.hermesApiKeyCredentialEnv + ? constants.hermesApiKeyAuthMethod + : null); + let hermesToolGateways = initial.hermesToolGateways; + let preferredInferenceApi = initial.preferredInferenceApi; + let nimContainer = initial.nimContainer; + const webSearchConfig = initial.webSearchConfig; + let forceProviderSelection = initialForceProviderSelection; + + while (true) { + const resumeProviderSelection = + !forceProviderSelection && + resume && + session?.steps?.provider_selection?.status === "complete" && + typeof provider === "string" && + typeof model === "string"; + if (resumeProviderSelection) { + deps.skippedStepMessage("provider_selection", `${provider} / ${model}`); + deps.hydrateCredentialEnv(credentialEnv); + deps.repairLocalInferenceSystemdOverrideOrExit(provider, deps.isNonInteractive); + } else { + await deps.startRecordedStep("provider_selection"); + const selection = await deps.setupNim(gpu, sandboxName, agent); + model = selection.model; + provider = selection.provider; + endpointUrl = selection.endpointUrl; + credentialEnv = selection.credentialEnv; + hermesAuthMethod = selection.hermesAuthMethod; + hermesToolGateways = selection.hermesToolGateways; + preferredInferenceApi = selection.preferredInferenceApi; + nimContainer = selection.nimContainer; + session = await deps.recordStepComplete( + "provider_selection", + deps.toSessionUpdates({ + provider, + model, + endpointUrl, + credentialEnv, + hermesAuthMethod, + hermesToolGateways, + preferredInferenceApi, + nimContainer, + }), + ); + } + + const selected = requireSelection(provider, model); + provider = selected.provider; + model = selected.model; + env.NEMOCLAW_OPENSHELL_BIN = deps.getOpenshellBinary(); + const needsBedrockRuntimeAdapter = deps.needsBedrockRuntimeAdapter(provider, endpointUrl); + const resumeInference = + !needsBedrockRuntimeAdapter && + !forceProviderSelection && + resume && + deps.isInferenceRouteReady(provider, model); + if (resumeInference) { + if (provider === constants.hermesProviderName) { + if (!sandboxName) sandboxName = await deps.promptValidatedSandboxName(agent); + await deps.startRecordedStep("inference", { provider, model }); + const inferenceResult = await deps.setupInference( + sandboxName, + model, + provider, + endpointUrl, + credentialEnv, + hermesAuthMethod, + hermesToolGateways, + ); + if (inferenceResult?.retry === "selection") { + forceProviderSelection = true; + continue; + } + session = await deps.recordStepComplete( + "inference", + deps.toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), + ); + break; + } + if (deps.isRoutedInferenceProvider(provider)) { + try { + await deps.reconcileModelRouter(); + } catch (err) { + deps.error(` ✗ Failed to reconcile model router: ${err instanceof Error ? err.message : String(err)}`); + deps.exitProcess(1); + } + } + deps.skippedStepMessage("inference", `${provider} / ${model}`); + if (nimContainer && sandboxName) deps.registryUpdateSandbox(sandboxName, { nimContainer }); + session = await deps.recordStepComplete( + "inference", + deps.toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), + ); + break; + } + + if (!sandboxName) sandboxName = await deps.promptValidatedSandboxName(agent); + const buildEstimateNote = + env.NEMOCLAW_IGNORE_RUNTIME_RESOURCES === "1" + ? null + : deps.formatSandboxBuildEstimateNote(deps.assessHost()); + deps.log( + deps.formatOnboardConfigSummary({ + provider, + model, + credentialEnv, + hermesAuthMethod, + webSearchConfig, + hermesToolGateways, + enabledChannels: selectedMessagingChannels.length > 0 ? selectedMessagingChannels : null, + sandboxName, + notes: buildEstimateNote ? [buildEstimateNote] : [], + }), + ); + deps.log(" Web search and messaging channels will be prompted next."); + if (!deps.isNonInteractive()) { + if (!(await deps.promptYesNoOrDefault(" Apply this configuration?", null, true))) { + deps.log(` Aborted. Re-run \`${deps.cliName()} onboard\` to start over.`); + deps.log(" Credentials entered so far were only staged in memory for this run."); + deps.log(" No new gateway credential was registered because onboarding stopped here."); + deps.exitProcess(0); + } + } + + await deps.startRecordedStep("inference", { provider, model }); + const inferenceResult = await deps.setupInference( + sandboxName, + model, + provider, + endpointUrl, + credentialEnv, + hermesAuthMethod, + hermesToolGateways, + ); + deps.deleteEnv("NVIDIA_API_KEY"); + if (inferenceResult?.retry === "selection") { + forceProviderSelection = true; + continue; + } + if (nimContainer && sandboxName) deps.registryUpdateSandbox(sandboxName, { nimContainer }); + session = await deps.recordStepComplete( + "inference", + deps.toSessionUpdates({ provider, model, hermesAuthMethod, nimContainer, hermesToolGateways }), + ); + break; + } + + return { + sandboxName, + model, + provider, + endpointUrl, + credentialEnv, + hermesAuthMethod, + hermesToolGateways, + preferredInferenceApi, + nimContainer, + webSearchConfig, + session, + }; +}