Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
11 changes: 11 additions & 0 deletions bin/lib/inference-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ function getProviderSelectionConfig(provider, model) {
provider,
providerLabel: "Local Ollama",
};
case "bedrock":
return {
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: model || "nvidia.nemotron-super-3-120b",
profile: DEFAULT_ROUTE_PROFILE,
credentialEnv: "BEDROCK_API_KEY",
provider,
providerLabel: "Amazon Bedrock (OpenAI-compatible)",
};
default:
return null;
}
Expand Down
54 changes: 50 additions & 4 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const BUILD_ENDPOINT_URL = "https://integrate.api.nvidia.com/v1";
const OPENAI_ENDPOINT_URL = "https://api.openai.com/v1";
const ANTHROPIC_ENDPOINT_URL = "https://api.anthropic.com";
const GEMINI_ENDPOINT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
const BEDROCK_ENDPOINT_URL = `https://bedrock-mantle.${process.env.BEDROCK_REGION || process.env.AWS_REGION || ""}.api.aws/v1`;

const REMOTE_PROVIDER_CONFIG = {
build: {
Expand Down Expand Up @@ -112,6 +113,17 @@ const REMOTE_PROVIDER_CONFIG = {
defaultModel: "",
skipVerify: true,
},
bedrock: {
label: "Amazon Bedrock (OpenAI-compatible endpoint)",
providerName: "bedrock",
providerType: "openai",
credentialEnv: "BEDROCK_API_KEY",
endpointUrl: BEDROCK_ENDPOINT_URL,
helpUrl: "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-generate.html",
modelMode: "curated",
defaultModel: "nvidia.nemotron-super-3-120b",
skipVerify: true,
},
};

const REMOTE_MODEL_OPTIONS = {
Expand All @@ -134,6 +146,15 @@ const REMOTE_MODEL_OPTIONS = {
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
],
bedrock: [
"nvidia.nemotron-nano-3-30b",
"nvidia.nemotron-super-3-120b",
"deepseek.v3.2",
"openai.gpt-oss-120b",
"mistral.devstral-2-123b",
"moonshotai.kimi-k2.5",
"minimax.minimax-m2.5",
],
};

// Non-interactive mode: set by --non-interactive flag or env var.
Expand Down Expand Up @@ -478,6 +499,7 @@ function getSandboxInferenceConfig(model, provider = null, preferredInferenceApi
supportsStore: false,
};
break;
case "bedrock":
case "compatible-endpoint":
providerKey = "inference";
primaryModelRef = `inference/${model}`;
Expand Down Expand Up @@ -1193,10 +1215,10 @@ function getNonInteractiveProvider() {
anthropiccompatible: "anthropicCompatible",
};
const normalized = aliases[providerKey] || providerKey;
const validProviders = new Set(["build", "openai", "anthropic", "anthropicCompatible", "gemini", "ollama", "custom", "nim-local", "vllm"]);
const validProviders = new Set(["build", "openai", "anthropic", "anthropicCompatible", "gemini", "ollama", "custom", "nim-local", "vllm", "bedrock"]);
if (!validProviders.has(normalized)) {
console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`);
console.error(" Valid values: build, openai, anthropic, anthropicCompatible, gemini, ollama, custom, nim-local, vllm");
console.error(" Valid values: build, openai, anthropic, anthropicCompatible, gemini, ollama, custom, nim-local, vllm, bedrock");
process.exit(1);
}

Expand Down Expand Up @@ -1626,6 +1648,7 @@ async function setupNim(gpu) {
(ollamaRunning ? " (suggested)" : ""),
});
}
options.push({ key: "bedrock", label: "Amazon Bedrock (OpenAI-compatible endpoint)" });
if (EXPERIMENTAL && gpu && gpu.nimCapable) {
options.push({ key: "nim-local", label: "Local NVIDIA NIM [experimental]" });
}
Expand Down Expand Up @@ -1723,7 +1746,7 @@ async function setupNim(gpu) {
}
const defaultModel = requestedModel || remoteConfig.defaultModel;
let modelValidator = null;
if (selected.key === "openai" || selected.key === "gemini") {
if (selected.key === "openai" || selected.key === "gemini" || selected.key === "bedrock") {
modelValidator = (candidate) =>
validateOpenAiLikeModel(remoteConfig.label, endpointUrl, candidate, getCredential(credentialEnv));
} else if (selected.key === "anthropic") {
Expand Down Expand Up @@ -1753,6 +1776,28 @@ async function setupNim(gpu) {
if (validation.retry === "selection") {
continue selectionLoop;
}
} else if (selected.key === "bedrock") {
// Bedrock API keys are region-bound — require an explicit region.
if (!process.env.BEDROCK_REGION && !process.env.AWS_REGION) {
console.error(" BEDROCK_REGION or AWS_REGION must be set for Amazon Bedrock.");
if (isNonInteractive()) {
process.exit(1);
}
continue selectionLoop;
}
// Bedrock Mantle exposes an OpenAI-compatible API
const retryMessage = "Please choose a provider/model again.";
preferredInferenceApi = await validateOpenAiLikeSelection(
remoteConfig.label,
endpointUrl,
model,
credentialEnv,
retryMessage
);
if (preferredInferenceApi) {
break;
}
continue selectionLoop;
Comment on lines +1791 to +1804
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fresh non-interactive Bedrock runs will probe without the env-supplied key.

Line 1740 accepts BEDROCK_API_KEY from process.env, but validateOpenAiLikeSelection() resolves the probe secret via getCredential(credentialEnv) only (Line 712). The rest of this file treats getCredential() as store-backed and falls back to process.env when env values are needed, e.g. Line 1522, so a CI run that only exports BEDROCK_API_KEY will hit Bedrock unauthenticated and exit before setupInference() persists the provider.

💡 One way to keep env-only CI runs working
+function resolveCredentialValue(envName) {
+  return envName ? (process.env[envName] || getCredential(envName) || "") : "";
+}
+
 async function validateOpenAiLikeSelection(
   label,
   endpointUrl,
   model,
   credentialEnv = null,
   retryMessage = "Please choose a provider/model again."
 ) {
-  const apiKey = credentialEnv ? getCredential(credentialEnv) : "";
+  const apiKey = resolveCredentialValue(credentialEnv);
   const probe = probeOpenAiLikeEndpoint(endpointUrl, model, apiKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 1778 - 1799, The Bedrock probe can run
unauthenticated because validateOpenAiLikeSelection() only uses
getCredential(credentialEnv) which may not read process.env; update the
credential resolution so the probe uses the env-supplied key when present.
Concretely, modify either getCredential(symbol credentialEnv) or
validateOpenAiLikeSelection(remoteConfig.label, endpointUrl, model,
credentialEnv, ...) to fall back to process.env[credentialEnv] (e.g.,
BEDROCK_API_KEY) when getCredential returns falsy before attempting the probe,
ensuring non-interactive/CI runs that only export BEDROCK_API_KEY succeed and
that setupInference() still persists the provider afterward.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCredential() in credentials.js:28 already checks process.env[key] before the credential store — the probe runs authenticated. So validateOpenAiLikeSelection() at line 802 calling getCredential("BEDROCK_API_KEY") will pick up the env var. The probe runs authenticated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Let me verify by checking the getCredential() implementation directly.


🏁 Script executed:

#!/bin/bash
fd -e js credentials -g '*credentials*' | xargs grep -n "getCredential" | head -20
fd -e js -g '*credentials*' | head -5

Length of output: 148


🏁 Script executed:

#!/bin/bash
cat bin/lib/credentials.js

Length of output: 6749


@mvincentbb, you're absolutely right — I apologize for the noise. getCredential() in credentials.js (Line 30–33) is:

function getCredential(key) {
  if (process.env[key]) return process.env[key];   // ← env-first
  const creds = loadCredentials();
  return creds[key] || null;
}

It checks process.env[key] before the file-backed credential store, so getCredential("BEDROCK_API_KEY") will correctly pick up a CI-exported BEDROCK_API_KEY. The probe runs authenticated and my concern was a false positive. No change needed here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you made another review then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== getSandboxInferenceConfig switch cases ==="
grep -n "case\|providerKey\|primaryModelRef\|inferenceCompat\|providerName" bin/lib/onboard.js | head -60

echo ""
echo "=== printDashboard providerLabel ==="
sed -n '/function printDashboard/,/^}/p' bin/lib/onboard.js | grep -n "providerLabel\|bedrock"

echo ""
echo "=== REMOTE_PROVIDER_CONFIG bedrock credentialEnv ==="
grep -n "credentialEnv\|bedrock" bin/lib/onboard.js | head -30

echo ""
echo "=== inference-config.js bedrock case ==="
grep -n "bedrock\|credentialEnv" bin/lib/inference-config.js | head -20

Length of output: 6160


@mvincentbb, here's a fresh look at the Bedrock additions. I found three issues beyond the already-resolved credential concern:


🔴 1. getSandboxInferenceConfig is missing a "bedrock" case — wrong Dockerfile ARGs baked into every sandbox

The switch statement in getSandboxInferenceConfig() (Lines 484–516) has explicit cases for openai-api, anthropic-prod, gemini-api, compatible-endpoint, etc., but no case for "bedrock". Since REMOTE_PROVIDER_CONFIG.bedrock.providerName is "bedrock", it silently falls into the default branch alongside nvidia-prod, producing:

providerKey = "inference";
primaryModelRef = `inference/${model}`;   // ← wrong; should be openai/${model} or similar
inferenceCompat = null;                   // ← supportsStore flag missing

This gets baked directly into the sandbox Dockerfile as ARG NEMOCLAW_PROVIDER_KEY / ARG NEMOCLAW_PRIMARY_MODEL_REF, so the agent inside the sandbox would be configured as if it were talking to an NVIDIA endpoint rather than an OpenAI-compatible one. The fix should mirror the compatible-endpoint or gemini-api case:

+    case "bedrock":
     case "compatible-endpoint":
       providerKey = "inference";
       primaryModelRef = `inference/${model}`;
       inferenceCompat = {
         supportsStore: false,
       };
       break;

🟠 2. credentialEnv is inconsistent across onboard, sandbox config, and docs

Location Value
REMOTE_PROVIDER_CONFIG.bedrock.credentialEnv (Line 120, onboard.js) "BEDROCK_API_KEY"
inference-config.js case "bedrock": (Line 117) "OPENAI_API_KEY"
PR description usage example export OPENAI_API_KEY=<bedrock-api-key>

During onboarding, the wizard checks/stores BEDROCK_API_KEY. Inside the sandbox, the config references OPENAI_API_KEY. A user following the PR docs (export OPENAI_API_KEY=...) will hit the onboarding credential prompt because BEDROCK_API_KEY is unset — and vice versa in CI. Pick one env var name and use it consistently in all three places.


🟡 3. printDashboard has no label for "bedrock" — shows raw provider name

Lines 6–13 of printDashboard map each known providerName to a human-readable string, but "bedrock" is absent, so the dashboard line reads Model nvidia.nemotron-super-3-120b (bedrock) instead of something like Amazon Bedrock. One-liner fix:

  else if (provider === "ollama-local") providerLabel = "Local Ollama";
+ else if (provider === "bedrock") providerLabel = "Amazon Bedrock";

} else if (selected.key === "anthropicCompatible") {
const validation = await validateCustomAnthropicSelection(
remoteConfig.label,
Expand Down Expand Up @@ -1994,7 +2039,7 @@ async function setupInference(sandboxName, model, provider, endpointUrl = null,
step(4, 7, "Setting up inference provider");
runOpenshell(["gateway", "select", GATEWAY_NAME], { ignoreError: true });

if (provider === "nvidia-prod" || provider === "nvidia-nim" || provider === "openai-api" || provider === "anthropic-prod" || provider === "compatible-anthropic-endpoint" || provider === "gemini-api" || provider === "compatible-endpoint") {
if (provider === "nvidia-prod" || provider === "nvidia-nim" || provider === "openai-api" || provider === "anthropic-prod" || provider === "compatible-anthropic-endpoint" || provider === "gemini-api" || provider === "compatible-endpoint" || provider === "bedrock") {
const config = provider === "nvidia-nim"
? REMOTE_PROVIDER_CONFIG.build
: Object.values(REMOTE_PROVIDER_CONFIG).find((entry) => entry.providerName === provider);
Expand Down Expand Up @@ -2282,6 +2327,7 @@ function printDashboard(sandboxName, model, provider, nimContainer = null) {
else if (provider === "gemini-api") providerLabel = "Google Gemini";
else if (provider === "compatible-endpoint") providerLabel = "Other OpenAI-compatible endpoint";
else if (provider === "vllm-local") providerLabel = "Local vLLM";
else if (provider === "bedrock") providerLabel = "Amazon Bedrock";
else if (provider === "ollama-local") providerLabel = "Local Ollama";

const token = fetchGatewayAuthTokenFromSandbox(sandboxName);
Expand Down