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
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ ARG NEMOCLAW_INFERENCE_API=openai-completions
ARG NEMOCLAW_CONTEXT_WINDOW=131072
ARG NEMOCLAW_MAX_TOKENS=4096
ARG NEMOCLAW_REASONING=false
# Comma-separated list of input modalities accepted by the primary model
# (e.g. "text" or "text,image" for vision-capable models). OpenClaw's
# model schema currently accepts "text" and "image". See #2421.
ARG NEMOCLAW_INFERENCE_INPUTS=text
# Per-request inference timeout (seconds) baked into agents.defaults.timeoutSeconds.
# Increase for slow local inference (e.g., CPU Ollama). openclaw.json is
# immutable at runtime (Landlock read-only), so this can only be changed by
Expand Down Expand Up @@ -255,6 +259,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_CONTEXT_WINDOW=${NEMOCLAW_CONTEXT_WINDOW} \
NEMOCLAW_MAX_TOKENS=${NEMOCLAW_MAX_TOKENS} \
NEMOCLAW_REASONING=${NEMOCLAW_REASONING} \
NEMOCLAW_INFERENCE_INPUTS=${NEMOCLAW_INFERENCE_INPUTS} \
NEMOCLAW_AGENT_TIMEOUT=${NEMOCLAW_AGENT_TIMEOUT} \
NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64} \
NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \
Expand Down Expand Up @@ -304,6 +309,7 @@ inference_api = os.environ['NEMOCLAW_INFERENCE_API']; \
context_window = int(os.environ.get('NEMOCLAW_CONTEXT_WINDOW', '131072')); \
max_tokens = int(os.environ.get('NEMOCLAW_MAX_TOKENS', '4096')); \
reasoning = os.environ.get('NEMOCLAW_REASONING', 'false') == 'true'; \
inference_inputs = [v.strip() for v in os.environ.get('NEMOCLAW_INFERENCE_INPUTS', 'text').split(',') if v.strip()] or ['text']; \
_raw_agent_timeout = os.environ.get('NEMOCLAW_AGENT_TIMEOUT', '600'); \
agent_timeout = int(_raw_agent_timeout) if _raw_agent_timeout.isdigit() and int(_raw_agent_timeout) > 0 else (_ for _ in ()).throw(ValueError('NEMOCLAW_AGENT_TIMEOUT must be a positive integer')); \
inference_compat = json.loads(base64.b64decode(os.environ['NEMOCLAW_INFERENCE_COMPAT_B64']).decode('utf-8')); \
Expand All @@ -325,7 +331,7 @@ providers = { \
'baseUrl': inference_base_url, \
'apiKey': 'unused', \
'api': inference_api, \
'models': [{**({'compat': inference_compat} if inference_compat else {}), 'id': model, 'name': primary_model_ref, 'reasoning': reasoning, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': context_window, 'maxTokens': max_tokens}] \
'models': [{**({'compat': inference_compat} if inference_compat else {}), 'id': model, 'name': primary_model_ref, 'reasoning': reasoning, 'input': inference_inputs, 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': context_window, 'maxTokens': max_tokens}] \
} \
}; \
config = { \
Expand Down
11 changes: 11 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,17 @@ function patchStagedDockerfile(
`ARG NEMOCLAW_REASONING=${reasoning}`,
);
}
// Honor NEMOCLAW_INFERENCE_INPUTS for vision-capable models. OpenClaw's
// model schema currently accepts "text" and "image" only, so validate
// strictly against that vocabulary. Adding modalities to OpenClaw later
// only requires widening this regex. See #2421.
const inferenceInputs = process.env.NEMOCLAW_INFERENCE_INPUTS;
if (inferenceInputs && /^(text|image)(,(text|image))*$/.test(inferenceInputs)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_INPUTS=.*$/m,
`ARG NEMOCLAW_INFERENCE_INPUTS=${inferenceInputs}`,
);
}
// NEMOCLAW_AGENT_TIMEOUT — override agents.defaults.timeoutSeconds at build
// time. Lets users increase the per-request inference timeout without
// editing the Dockerfile. Ref: issue #2281
Expand Down
100 changes: 100 additions & 0 deletions test/onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,106 @@ describe("onboard helpers", () => {
}
});

it("regression #2421: bakes NEMOCLAW_INFERENCE_INPUTS into the staged Dockerfile when env is set", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-inputs-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_SEARCH_ENABLED=0",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_INFERENCE_INPUTS=text",
].join("\n"),
);

const prior = process.env.NEMOCLAW_INFERENCE_INPUTS;
process.env.NEMOCLAW_INFERENCE_INPUTS = "text,image";
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-inputs",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
assert.match(patched, /^ARG NEMOCLAW_INFERENCE_INPUTS=text,image$/m);
} finally {
if (prior === undefined) {
delete process.env.NEMOCLAW_INFERENCE_INPUTS;
} else {
process.env.NEMOCLAW_INFERENCE_INPUTS = prior;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("regression #2421: rejects malformed NEMOCLAW_INFERENCE_INPUTS and keeps default", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-inputs-bad-"),
);
const dockerfilePath = path.join(tmpDir, "Dockerfile");
const baseDockerfile = [
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_SEARCH_ENABLED=0",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_INFERENCE_INPUTS=text",
].join("\n");

const prior = process.env.NEMOCLAW_INFERENCE_INPUTS;
try {
// Cases that must all leave the default untouched.
const rejectCases = [
undefined,
"audio",
"text,",
"Text,Image",
"text, image",
'text"\nRUN rm -rf /',
];
for (const [index, value] of rejectCases.entries()) {
fs.writeFileSync(dockerfilePath, baseDockerfile);
if (value === undefined) {
delete process.env.NEMOCLAW_INFERENCE_INPUTS;
} else {
process.env.NEMOCLAW_INFERENCE_INPUTS = value;
}
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
`build-inputs-reject-${index}`,
"openai-api",
);
assert.match(
fs.readFileSync(dockerfilePath, "utf8"),
/^ARG NEMOCLAW_INFERENCE_INPUTS=text$/m,
`value="${String(value)}" should not change the ARG default`,
);
}
} finally {
if (prior === undefined) {
delete process.env.NEMOCLAW_INFERENCE_INPUTS;
} else {
process.env.NEMOCLAW_INFERENCE_INPUTS = prior;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("regression #1409: rejects malformed NEMOCLAW_PROXY_HOST/PORT and keeps defaults", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-proxy-bad-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
Expand Down
Loading