Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-dee
&& install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/dcode.real \
&& install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/deepagents-code

ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b
ARG NEMOCLAW_MODEL=nvidia/nemotron-3-ultra-550b-a55b
ARG NEMOCLAW_PROVIDER_KEY=inference
ARG NEMOCLAW_UPSTREAM_PROVIDER=nvidia
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
Expand Down
1 change: 1 addition & 0 deletions agents/langchain-deepagents-code/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ device_pairing: false
# inference.local endpoint using Deep Agents Code's OpenAI-compatible provider.
inference:
provider_type: openai_compatible
default_model: nvidia/nemotron-3-ultra-550b-a55b
base_url_config_key: "models.providers.openai.base_url"
model_config_key: "models.default"
proxy_support: implicit
Expand Down
21 changes: 19 additions & 2 deletions agents/langchain-deepagents-code/patch-managed-deepagents-code.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

# Source-of-truth review for this pinned third-party patch boundary:
# invalidState: upstream entrypoints can independently enable credential stores,
# ambient MCP discovery, update/install flows, or child-process config paths that
# bypass NemoClaw's managed inference, policy, and integrity-bound MCP boundaries.
# ambient MCP discovery, update/install flows, first-run model selection, or
# child-process config paths that bypass NemoClaw's managed inference, policy,
# and integrity-bound MCP boundaries.
# sourceBoundary: deepagents-code owns those Python entrypoints; NemoClaw owns the
# sandbox image posture and therefore validates every patched symbol before build.
# whyNotSourceFix: upstream 0.1.30 has no single managed-runtime hook that can
Expand Down Expand Up @@ -861,6 +862,15 @@ def _nemoclaw_select_with_auth_check(self, model_spec: str, provider: str) -> No
ModelSelectorScreen._select_with_auth_check = _nemoclaw_select_with_auth_check
'''

ONBOARDING_PATCH = r'''

# NemoClaw-managed Deep Agents Code hardening v2.
def should_run_onboarding(state_dir=None) -> bool:
"""Skip upstream first-run setup because NemoClaw owns model configuration."""
del state_dir
return False
'''


def _top_level_functions(tree: ast.Module) -> set[str]:
return {
Expand Down Expand Up @@ -953,6 +963,7 @@ def main() -> None:
"auth_ui": root / "widgets" / "auth.py",
"codex_ui": root / "widgets" / "codex_auth.py",
"model_selector": root / "widgets" / "model_selector.py",
"onboarding": root / "onboarding.py",
"approval": root / "widgets" / "approval.py",
"server": root / "server.py",
"server_config": root / "_server_config.py",
Expand Down Expand Up @@ -1090,6 +1101,9 @@ def main() -> None:
"ModelSelectorScreen",
{"_select_with_auth_check"},
)
_require_functions(
paths["onboarding"], texts["onboarding"], {"should_run_onboarding"}
)
_require_methods(
paths["approval"],
texts["approval"],
Expand Down Expand Up @@ -1164,6 +1178,9 @@ def main() -> None:
transformed["model_selector"] = _append_patch(
paths["model_selector"], texts["model_selector"], MODEL_SELECTOR_PATCH
)
transformed["onboarding"] = _append_patch(
paths["onboarding"], texts["onboarding"], ONBOARDING_PATCH
)
transformed["approval"] = _append_patch(
paths["approval"], texts["approval"], APPROVAL_PATCH
)
Expand Down
4 changes: 4 additions & 0 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ After the terminal smoke checks, onboarding runs `dcode --version` and compares
Fresh and resumed onboarding exit nonzero instead of reporting the runtime ready when the installed version is too old, uses an incompatible version scheme, or cannot be verified.
If the version check fails, review the reported version error and run `nemo-deepagents <sandbox-name> rebuild` before resuming onboarding.
NemoClaw writes `/sandbox/.deepagents/config.toml` with an OpenAI-compatible provider pointed at `https://inference.local/v1`, uses a scoped placeholder API key for that managed route, and sets `use_responses_api = false` for Chat Completions compatibility.
When you use NVIDIA Endpoints without selecting another model, new Deep Agents Code sandboxes default to `nvidia/nemotron-3-ultra-550b-a55b`.
This agent-specific default does not change the shared Nemotron 3 Super default for OpenClaw and Hermes.
NemoClaw/OpenShell keeps real provider credentials in credential handling and does not write them into the Deep Agents config file.
Deep Agents Code reaches `inference.local` through the managed OpenShell L7 proxy rather than direct sandbox DNS.
The image launcher normalizes the runtime proxy environment for interactive, login-shell, and direct-exec paths and removes inherited proxy credentials and bypass entries before `dcode` starts.
Managed interactive sessions skip Deep Agents Code's upstream first-run onboarding and model picker, then open the TUI with the model selected during NemoClaw onboarding.

## Choose the Default Sandbox

Expand Down Expand Up @@ -193,6 +196,7 @@ nemo-deepagents <sandbox-name> snapshot create --name before-change
```

If you upgrade from a release that persisted LangSmith environment values, rebuild each existing Deep Agents Code sandbox so its image includes the corrected `start.sh`.
If an existing sandbox displays `Choose a Recommended Model`, rebuild it so its image includes the managed startup behavior.

`status` reports the selected harness as a terminal runtime and prints the interactive/headless command shape.
If `status` reports `Runtime health: degraded` with an OOM kill count, rebuild the sandbox to restore the terminal runtime.
Expand Down
4 changes: 3 additions & 1 deletion docs/inference/inference-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ The managed install/start vLLM entry appears by default on DGX Spark and DGX Sta
During interactive NVIDIA Endpoints onboarding, NemoClaw loads NVIDIA's public featured model catalog once per onboarding session and reports progress before displaying the model picker.
It excludes retired or unsafe choices and corrects known catalog lag before displaying the result.
If the catalog is unavailable, malformed, or contains no safe model IDs, the wizard warns you and uses the bundled fallback list shown below.
Nemotron 3 Super remains the default when it is present; otherwise, the first live featured model becomes the interactive default.
Nemotron 3 Super remains the shared default for OpenClaw and Hermes when it is present.
LangChain Deep Agents Code uses Nemotron 3 Ultra as its NVIDIA Endpoints default.
If an agent's default is unavailable, the first live featured model becomes the interactive default.
If you set `NEMOCLAW_MODEL` to a safe custom model ID that is absent from the live catalog, it does not replace the live menu default.
Choose **Other** to use that value as the pre-filled manual entry; NemoClaw validates it against NVIDIA Endpoints before continuing.
NemoClaw neither displays nor accepts an unsafe `NEMOCLAW_MODEL` value as the manual-entry prefill.
Expand Down
1 change: 1 addition & 0 deletions src/lib/agent/definition-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface AgentDashboard {
export interface AgentInference {
provider_type?: string;
provider_options?: string[];
default_model?: string;
}

export type AgentMcpSupport = "bridge" | "disabled";
Expand Down
19 changes: 19 additions & 0 deletions src/lib/agent/defs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe("agent definitions", () => {
format: "toml",
});
expect(deepAgentsCode.inference?.provider_type).toBe("openai_compatible");
expect(deepAgentsCode.inference?.default_model).toBe("nvidia/nemotron-3-ultra-550b-a55b");
expect(deepAgentsCode.mcpCapability).toEqual({
support: "bridge",
adapter: "deepagents-config",
Expand Down Expand Up @@ -297,6 +298,24 @@ describe("agent definitions", () => {
expect(() => loadAgent(agentName)).toThrow(/inference\.provider_type/);
});

it.each([
"42",
'"bad model"',
])("rejects invalid inference default models in manifests (%s)", (defaultModel) => {
const agentName = `invalid-inference-default-model-${String(Date.now())}-${defaultModel.length}`;
writeTempAgentManifest(
agentName,
[
`name: ${agentName}`,
"display_name: Broken Inference Default",
"inference:",
` default_model: ${defaultModel}`,
].join("\n"),
);

expect(() => loadAgent(agentName)).toThrow(/inference\.default_model/);
});

it("rejects invalid MCP bridge adapter declarations in manifests", () => {
const agentName = `invalid-mcp-adapter-${String(Date.now())}`;
writeTempAgentManifest(
Expand Down
15 changes: 14 additions & 1 deletion src/lib/agent/manifest-readers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import { isSafeModelId } from "../validation";
import type {
AgentDashboard,
AgentDashboardKind,
Expand Down Expand Up @@ -261,7 +262,19 @@ export function readInference(record: ManifestRecord): AgentInference | undefine
providerOptionList = providerOptions as string[];
}

return { provider_type: providerType, provider_options: providerOptionList };
const defaultModel = inference.default_model;
if (
defaultModel !== undefined &&
(typeof defaultModel !== "string" || !isSafeModelId(defaultModel.trim()))
) {
throw new Error("Agent manifest field 'inference.default_model' must be a safe model ID");
}

return {
provider_type: providerType,
provider_options: providerOptionList,
default_model: typeof defaultModel === "string" ? defaultModel.trim() : undefined,
};
}

export function readMcpCapability(record: ManifestRecord): AgentMcpCapability {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/inference/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,28 @@ import {
OLLAMA_LOCAL_CREDENTIAL_ENV,
parseGatewayInference,
planInferenceRouteReconcile,
resolveAgentDefaultCloudModel,
resolveAgentInferenceApi,
resolveAgentProviderInferenceApi,
sanitizeRouteValueForDisplay,
VLLM_LOCAL_CREDENTIAL_ENV,
} from "./config";

describe("resolveAgentDefaultCloudModel", () => {
it("uses the Deep Agents manifest default without changing shared agent defaults", () => {
expect(
resolveAgentDefaultCloudModel({
name: "langchain-deepagents-code",
inference: { default_model: "nvidia/nemotron-3-ultra-550b-a55b" },
}),
).toBe("nvidia/nemotron-3-ultra-550b-a55b");

for (const agent of [null, { name: "openclaw" }, { name: "hermes" }]) {
expect(resolveAgentDefaultCloudModel(agent)).toBe(DEFAULT_CLOUD_MODEL);
}
});
});

describe("resolveAgentInferenceApi", () => {
it("uses the managed OpenAI frontend for Hermes custom Anthropic routes (#6289)", () => {
expect(
Expand Down
11 changes: 10 additions & 1 deletion src/lib/inference/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* inference output parsing. All functions are pure.
*/

import { shouldSkipResponsesProbe } from "../validation";
import { isSafeModelId, shouldSkipResponsesProbe } from "../validation";
import { DEFAULT_OLLAMA_MODEL } from "./local";

export const INFERENCE_ROUTE_URL = "https://inference.local/v1";
Expand Down Expand Up @@ -68,6 +68,15 @@ export const VLLM_LOCAL_CREDENTIAL_ENV = "NEMOCLAW_VLLM_LOCAL_TOKEN";
export const MANAGED_PROVIDER_ID = "inference";
export { DEFAULT_OLLAMA_MODEL };

/** Resolve an agent-owned NVIDIA Endpoints default without changing shared defaults. */
export function resolveAgentDefaultCloudModel(agent: unknown): string {
const configured = (agent as { inference?: { default_model?: unknown } } | null | undefined)
?.inference?.default_model;
return typeof configured === "string" && isSafeModelId(configured.trim())
? configured.trim()
: DEFAULT_CLOUD_MODEL;
}

export interface ProviderSelectionConfig {
endpointType: string;
endpointUrl: string;
Expand Down
46 changes: 42 additions & 4 deletions src/lib/onboard/nvidia-featured-model-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ vi.mock("../inference/model-prompts", () => ({
}));

vi.mock("../inference/nvidia-featured-models", () => ({
createNvidiaFeaturedModelPromptOptionsLoader: () => () => ({
defaultModelId: "nvidia/nemotron-3-super-120b-a12b",
createNvidiaFeaturedModelPromptOptionsLoader: () => (defaultModelId?: string | null) => ({
defaultModelId:
defaultModelId === "nvidia/nemotron-3-ultra-550b-a55b"
? defaultModelId
: "nvidia/nemotron-3-super-120b-a12b",
cloudModelOptions: [],
}),
}));
Expand All @@ -26,15 +29,19 @@ describe("NVIDIA featured model selection", () => {
it("propagates back navigation from the interactive model prompt (#5827)", async () => {
vi.mocked(promptCloudModel).mockResolvedValueOnce(BACK_TO_SELECTION);

const selected = await createNvidiaFeaturedModelSession(vi.fn()).select(null, null, false);
const selected = await createNvidiaFeaturedModelSession({ writeLine: vi.fn() }).select(
null,
null,
false,
);

expect(selected).toBe(BACK_TO_SELECTION);
});

it("preserves a custom environment model as the manual-entry default (#5827)", async () => {
vi.mocked(promptCloudModel).mockResolvedValueOnce("custom/provider-model");

const selected = await createNvidiaFeaturedModelSession(vi.fn()).select(
const selected = await createNvidiaFeaturedModelSession({ writeLine: vi.fn() }).select(
null,
null,
false,
Expand All @@ -48,4 +55,35 @@ describe("NVIDIA featured model selection", () => {
manualDefaultModelId: "custom/provider-model",
});
});

it("uses the agent default for interactive and non-interactive selection", async () => {
const ultra = "nvidia/nemotron-3-ultra-550b-a55b";
vi.mocked(promptCloudModel).mockResolvedValueOnce(ultra);
const session = createNvidiaFeaturedModelSession({
writeLine: vi.fn(),
defaultModel: ultra,
});

await expect(session.select(null, null, true)).resolves.toBe(ultra);
await expect(session.select(null, null, false)).resolves.toBe(ultra);
expect(promptCloudModel).toHaveBeenCalledWith({
defaultModelId: ultra,
cloudModelOptions: [],
manualDefaultModelId: undefined,
});
});

it("keeps requested, recovered, and environment models ahead of the agent default", async () => {
const session = createNvidiaFeaturedModelSession({
defaultModel: "nvidia/nemotron-3-ultra-550b-a55b",
});

await expect(session.select("requested/model", "recovered/model", true)).resolves.toBe(
"requested/model",
);
await expect(session.select(null, "recovered/model", true)).resolves.toBe("recovered/model");
await expect(session.select(null, null, true, " environment/model ")).resolves.toBe(
"environment/model",
);
});
});
15 changes: 11 additions & 4 deletions src/lib/onboard/nvidia-featured-model-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,31 @@ export type NvidiaFeaturedModelSession = {
) => Promise<ModelPromptResult>;
};

export type NvidiaFeaturedModelSessionOptions = {
writeLine?: (message: string) => void;
defaultModel?: string;
};

/** Create one catalog-backed model selector for an onboarding session. */
export function createNvidiaFeaturedModelSession(
writeLine: (message: string) => void = console.log,
options: NvidiaFeaturedModelSessionOptions = {},
): NvidiaFeaturedModelSession {
const writeLine = options.writeLine ?? console.log;
const defaultModel = options.defaultModel?.trim() || DEFAULT_CLOUD_MODEL;
const loadPromptOptions = createNvidiaFeaturedModelPromptOptionsLoader();
let announcedLoad = false;
return {
async select(requestedModel, recoveredModel, nonInteractive, envModel) {
if (requestedModel) return requestedModel;
if (recoveredModel) return recoveredModel;
if (nonInteractive) return DEFAULT_CLOUD_MODEL;
const configuredModel = envModel?.trim();
if (nonInteractive) return configuredModel || defaultModel;
if (!announcedLoad) {
writeLine(" Loading NVIDIA's featured model catalog...");
announcedLoad = true;
}
const configuredModel = envModel?.trim();
return promptCloudModel({
...loadPromptOptions(configuredModel),
...loadPromptOptions(configuredModel || defaultModel),
manualDefaultModelId: configuredModel,
});
},
Expand Down
31 changes: 31 additions & 0 deletions src/lib/onboard/setup-nim-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,37 @@ afterEach(() => {
});

describe("createSetupNim", () => {
it("passes the Deep Agents manifest default to NVIDIA model selection", async () => {
const ultra = "nvidia/nemotron-3-ultra-550b-a55b";
const log = vi.fn();
const createNvidiaFeaturedModelSession = vi.fn<
SetupNimFlowDeps["createNvidiaFeaturedModelSession"]
>(() => ({ select: async () => unexpected("featured model selection") }));
const handleRemoteProviderSelection = vi.fn<SetupNimFlowDeps["handleRemoteProviderSelection"]>(
async (_args, state) => {
state.model = ultra;
state.provider = "nvidia-prod";
state.endpointUrl = "https://integrate.api.nvidia.com/v1";
state.credentialEnv = "NVIDIA_INFERENCE_API_KEY";
return "selected";
},
);
const setupNim = createSetupNim(
makeDeps({ createNvidiaFeaturedModelSession, handleRemoteProviderSelection, log }),
);
const dcodeAgent = {
name: "langchain-deepagents-code",
inference: { default_model: ultra },
} as AgentDefinition;

await setupNim(null, null, dcodeAgent);

expect(createNvidiaFeaturedModelSession).toHaveBeenCalledWith({
defaultModel: ultra,
writeLine: log,
});
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
it("announces detected Ollama but still prompts and defaults to NVIDIA Endpoints (#6245)", async () => {
vi.stubEnv("NEMOCLAW_PROVIDER", "");
const step = vi.fn();
Expand Down
Loading
Loading