Skip to content
141 changes: 119 additions & 22 deletions nemoclaw-blueprint/scripts/nemotron-inference-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@
// field (the endpoint returns `reasoning_content` either way), so the field
// carries no behavior to preserve.
//
// Scope: the exact `nvidia/nemotron-3-ultra-550b-a55b` model ID reproduced
// in #6913. Prefix collisions and other Nemotron-3 IDs preserve their
// top-level `thinking` field unless their own accepted scope establishes the
// same endpoint contract.
// Scope: the `nvidia/nemotron-3-` model family when the managed
// `inference.local` route currently selects nvidia-prod. The image-baked
// NEMOCLAW_UPSTREAM_PROVIDER establishes the onboarding route; after an
// in-place provider switch, OpenClaw's private request marker is authoritative
// and this preload removes it before forwarding. Ultra, Super, and Nano all
// reject the top-level field on NVIDIA Build, while unrelated models and
// other upstream providers must remain untouched.
//
// Source boundary: NemoClaw owns the sandbox preload that wraps outgoing
// chat-completions traffic. The `thinking` field originates in OpenClaw's
Expand Down Expand Up @@ -134,6 +137,9 @@
var https = require('https');

var COMPLETIONS_RE = /\/v1\/chat\/completions/;
// Written only by NemoClaw's runtime config synchronization. This is
// non-secret control metadata and must not leave the sandbox.
var UPSTREAM_PROVIDER_HEADER = 'x-nemoclaw-upstream-provider';
var CHAT_TEMPLATE_KWARG_RULES = [
{ pattern: /nemotron/i, kwargs: { force_nonempty_content: true } },
{ pattern: /^deepseek-ai\/deepseek-v4-pro$/i, kwargs: { thinking: false } },
Expand All @@ -145,9 +151,9 @@
// set `chat_template_kwargs.thinking` (a chat-template arg the endpoint
// accepts); this strips the *top-level* `thinking` request field entirely.
//
// Scope is the exact Ultra model ID accepted by #6913. Do not infer support
// for suffix variants or other Nemotron-3 models from this workaround.
var STRIP_TOP_LEVEL_THINKING_RE = /^nvidia\/nemotron-3-ultra-550b-a55b$/i;
// Scope is the Nemotron-3 family verified by #6913. Do not widen this to all
// NVIDIA endpoint models: some models accept and use top-level `thinking`.
var STRIP_TOP_LEVEL_THINKING_RE = /^nvidia\/nemotron-3-/i;

// #4851: Ultra 550B silently drops intermediate steps from `content` when
// asked to perform multi-step tasks without execution-capable tools —
Expand Down Expand Up @@ -310,12 +316,12 @@
return true;
}

function patchJsonBody(raw) {
function patchJsonBody(raw, stripTopLevelThinking) {
try {
var body = JSON.parse(raw.toString('utf-8'));
var changed = false;
if (applyChatTemplateKwargs(body)) changed = true;
if (applyStripTopLevelThinking(body)) changed = true;
if (stripTopLevelThinking && applyStripTopLevelThinking(body)) changed = true;
if (applyToolLessSystemPrompt(body)) changed = true;
if (!changed) return null;
return Buffer.from(JSON.stringify(body), 'utf-8');
Expand Down Expand Up @@ -346,21 +352,81 @@
return isChatCompletionsPost(fetchMethod(input, init), fetchUrl(input));
}

function headersWithoutContentLength(headers) {
function upstreamProviderFromHeaders(headers) {
if (!headers) return undefined;
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
return headers.has(UPSTREAM_PROVIDER_HEADER)
? headers.get(UPSTREAM_PROVIDER_HEADER)
: undefined;
}
if (Array.isArray(headers)) {
for (var i = 0; i < headers.length; i++) {
var entry = headers[i];
if (
entry &&
String(entry[0]).toLowerCase() === UPSTREAM_PROVIDER_HEADER
) {
return String(entry[1]);
}
}
return undefined;
}
if (typeof headers === 'object') {
var keys = Object.keys(headers);
for (var j = 0; j < keys.length; j++) {
if (keys[j].toLowerCase() === UPSTREAM_PROVIDER_HEADER) {
return String(headers[keys[j]]);
}
}
}
return undefined;
}

function isNvidiaBuildUpstream(upstreamProvider) {
var provider =
upstreamProvider === undefined
? process.env.NEMOCLAW_UPSTREAM_PROVIDER
: upstreamProvider;
return provider === 'nvidia-prod';
}

function isManagedBuildHost(host, upstreamProvider) {
return (
isNvidiaBuildUpstream(upstreamProvider) &&
/^inference\.local(?::\d+)?$/i.test(String(host || ''))
);
}

function isManagedBuildFetch(input, upstreamProvider) {
if (!isNvidiaBuildUpstream(upstreamProvider)) return false;
try {
return new URL(fetchUrl(input)).hostname.toLowerCase() === 'inference.local';
} catch (_e) {
return false;
}
}

function isManagedBuildRequest(options, upstreamProvider) {
return isManagedBuildHost(options.hostname || options.host, upstreamProvider);
}

function headersWithoutNames(headers, names) {
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
var copy = new Headers(headers);
copy.delete('content-length');
names.forEach(function (name) {
copy.delete(name);
});
return copy;
}
if (Array.isArray(headers)) {
return headers.filter(function (entry) {
return !entry || String(entry[0]).toLowerCase() !== 'content-length';
return !entry || !names.has(String(entry[0]).toLowerCase());
});
}
if (headers && typeof headers === 'object') {
var next = {};
Object.keys(headers).forEach(function (key) {
if (key.toLowerCase() !== 'content-length') {
if (!names.has(key.toLowerCase())) {
next[key] = headers[key];
}
});
Expand All @@ -369,6 +435,14 @@
return headers;
}

function headersWithoutContentLength(headers) {
return headersWithoutNames(headers, new Set(['content-length']));
}

function headersWithoutUpstreamProvider(headers) {
return headersWithoutNames(headers, new Set([UPSTREAM_PROVIDER_HEADER]));
}

function bytesFromSimpleBody(body) {
if (typeof body === 'string') return Promise.resolve(Buffer.from(body, 'utf-8'));
if (Buffer.isBuffer(body)) return Promise.resolve(body);
Expand Down Expand Up @@ -421,19 +495,34 @@

var origFetch = globalThis.fetch;
var wrappedFetch = async function (input, init) {
var nextInit = init ? Object.assign({}, init) : {};
var effectiveHeaders = nextInit.headers || (input && input.headers);
var upstreamProvider = upstreamProviderFromHeaders(effectiveHeaders);
var hasUpstreamProviderMarker = upstreamProvider !== undefined;
if (hasUpstreamProviderMarker) {
nextInit.headers = headersWithoutUpstreamProvider(effectiveHeaders);
}
if (!isChatCompletionsFetch(input, init)) {
return origFetch.apply(this, arguments);
return hasUpstreamProviderMarker
? origFetch.call(this, input, nextInit)
: origFetch.apply(this, arguments);
}

var nextInit = init ? Object.assign({}, init) : {};
var rawPromise = bytesFromFetch(input, nextInit);
if (!rawPromise) {
return origFetch.apply(this, arguments);
return hasUpstreamProviderMarker
? origFetch.call(this, input, nextInit)
: origFetch.apply(this, arguments);
}

var modified = patchJsonBody(await rawPromise);
var modified = patchJsonBody(
await rawPromise,
isManagedBuildFetch(input, upstreamProvider)
);
if (!modified) {
return origFetch.apply(this, arguments);
return hasUpstreamProviderMarker
? origFetch.call(this, input, nextInit)
: origFetch.apply(this, arguments);
}

nextInit.body = modified.toString('utf-8');
Expand All @@ -450,18 +539,23 @@
var origRequest = mod.request;

mod.request = function (options, callback) {
// Only intercept object-form calls with a recognisable path.
if (typeof options === 'string' || !options) {
return origRequest.apply(mod, arguments);
}

var upstreamProvider = upstreamProviderFromHeaders(options.headers);
var req = origRequest.apply(mod, arguments);
if (upstreamProvider !== undefined && req.removeHeader) {
req.removeHeader(UPSTREAM_PROVIDER_HEADER);
}

// Only intercept object-form calls with a recognisable path.
var path = options.path || '';
if (!isChatCompletionsPost(options.method, path)) {
return origRequest.apply(mod, arguments);
return req;
}

// Create the real request, then intercept write/end to buffer the body.
var req = origRequest.apply(mod, arguments);
var origWrite = req.write;
var origEnd = req.end;
var chunks = [];
Expand All @@ -479,7 +573,10 @@
}

var raw = Buffer.concat(chunks);
var modified = patchJsonBody(raw);
var modified = patchJsonBody(
raw,
isManagedBuildRequest(options, upstreamProvider)
);
var bodyToSend = modified || raw;
if (modified && req.getHeader && req.setHeader) {
req.removeHeader('content-length');
Expand Down
3 changes: 3 additions & 0 deletions src/lib/actions/inference-set-compatible-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ describe("runInferenceSet compatible providers", () => {
providers: {
inference: {
api: "openai-responses",
headers: {
"X-NemoClaw-Upstream-Provider": "compatible-endpoint",
},
models: [{ id: "mock-responses-model", name: "inference/mock-responses-model" }],
},
},
Expand Down
86 changes: 86 additions & 0 deletions src/lib/actions/inference-set-degraded-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,92 @@ describe("runInferenceSet degraded state handling", () => {
expect(deps.calls.restartSandboxGateway).not.toHaveBeenCalled();
});

it("reconciles the provider marker when a config-write retry uses the registered provider", async () => {
const entry = {
name: "alpha",
agent: "openclaw",
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
};
let persistedConfig: ConfigObject = {
agents: {
defaults: { model: { primary: "inference/nvidia/nemotron-3-super-120b-a12b" } },
},
models: {
providers: {
inference: {
baseUrl: "https://inference.local/v1",
api: "openai-completions",
headers: {
"X-NemoClaw-Upstream-Provider": "nvidia-prod",
},
models: [
{
id: "nvidia/nemotron-3-super-120b-a12b",
name: "inference/nvidia/nemotron-3-super-120b-a12b",
},
],
},
},
},
};
const deps = createDeps({
config: structuredClone(persistedConfig),
entry,
session: baseSession({
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
}),
});
deps.calls.readSandboxConfig.mockImplementation(() => structuredClone(persistedConfig));
deps.calls.updateSandbox.mockImplementation((_name, updates) => {
Object.assign(entry, updates);
return true;
});
deps.calls.writeSandboxConfig
.mockImplementationOnce(() => {
throw new Error("sandbox exec crashed");
})
.mockImplementation((_name, _target, config) => {
persistedConfig = structuredClone(config);
});

const options = {
provider: "compatible-endpoint",
model: "openai/gpt-5.4-mini",
endpointUrl: "https://compatible.example/v1",
credentialEnv: "COMPATIBLE_API_KEY",
inferenceApi: "openai-completions",
noVerify: true,
};

await expect(runInferenceSet(options, deps)).resolves.toMatchObject({
inSandboxConfigSynced: false,
});
expect(persistedConfig.models).toMatchObject({
providers: {
inference: {
headers: {
"X-NemoClaw-Upstream-Provider": "nvidia-prod",
},
},
},
});

await expect(runInferenceSet(options, deps)).resolves.toMatchObject({
inSandboxConfigSynced: true,
});
expect(persistedConfig.models).toMatchObject({
providers: {
inference: {
headers: {
"X-NemoClaw-Upstream-Provider": "compatible-endpoint",
},
},
},
});
});

it("reports degraded (not synced) when the in-sandbox hash recompute fails (#3726)", async () => {
const config: ConfigObject = {
agents: { defaults: { model: { primary: "inference/moonshotai/kimi-k2.6" } } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ describe("runInferenceSet OpenClaw gateway restart", () => {
baseUrl: "https://inference.local",
apiKey: "unused",
api: "anthropic-messages",
headers: {
"X-NemoClaw-Upstream-Provider": "compatible-anthropic-endpoint",
},
models: [
{
id: "claude-sonnet-proxy",
Expand Down Expand Up @@ -121,6 +124,9 @@ describe("runInferenceSet OpenClaw gateway restart", () => {
baseUrl: "https://inference.local/v1",
apiKey: "unused",
api: "openai-completions",
headers: {
"X-NemoClaw-Upstream-Provider": "nvidia-prod",
},
models: [{ id: "nvidia/model-a", name: "inference/nvidia/model-a" }],
},
},
Expand Down
Loading
Loading