Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 45 additions & 3 deletions nemoclaw-blueprint/scripts/nemotron-inference-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@
{ pattern: /^moonshotai\/kimi-k2\.6$/i, kwargs: { thinking: false } },
];

// #4851: Ultra 550B silently drops intermediate steps from `content` when
// asked to perform multi-step tasks without execution tools — reasoning
// plans all steps but content emits only the final command (or empty).
// chat_template_kwargs.force_nonempty_content doesn't help. When the
// caller hasn't supplied a system message AND has no tools, inject a
// one-paragraph nudge so the model emits the full code/commands the
// user would run manually. Skip when a system message is already
// present (caller's prompt wins) or when tools are present (the model
// should use them).
var TOOL_LESS_SYSTEM_PROMPT_RULES = [
{
pattern: /^nvidia\/nemotron-3-ultra-550b/i,
systemPrompt:
'You do not have tools to write files or execute commands. When the user asks you to perform such actions, include the complete code or command they would need to run manually. Do not skip steps.',
},
];

function chatTemplateKwargsForModel(model) {
var kwargs = null;
CHAT_TEMPLATE_KWARG_RULES.forEach(function (rule) {
Expand Down Expand Up @@ -101,12 +118,37 @@
return true;
}

function toolLessSystemPromptForModel(body) {
if (!body || typeof body.model !== 'string') return null;
var rule = null;
for (var i = 0; i < TOOL_LESS_SYSTEM_PROMPT_RULES.length; i++) {
if (TOOL_LESS_SYSTEM_PROMPT_RULES[i].pattern.test(body.model)) {
rule = TOOL_LESS_SYSTEM_PROMPT_RULES[i];
break;
}
}
if (!rule) return null;
if (!Array.isArray(body.messages) || body.messages.length === 0) return null;
var first = body.messages[0];
if (first && typeof first === 'object' && first.role === 'system') return null;
if (Array.isArray(body.tools) && body.tools.length > 0) return null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return rule.systemPrompt;
}

function applyToolLessSystemPrompt(body) {
var systemPrompt = toolLessSystemPromptForModel(body);
if (!systemPrompt) return false;
body.messages.unshift({ role: 'system', content: systemPrompt });
return true;
}

function patchJsonBody(raw) {
try {
var body = JSON.parse(raw.toString('utf-8'));
if (!applyChatTemplateKwargs(body)) {
return null;
}
var changed = false;
if (applyChatTemplateKwargs(body)) changed = true;
if (applyToolLessSystemPrompt(body)) changed = true;
if (!changed) return null;
return Buffer.from(JSON.stringify(body), 'utf-8');
} catch (_e) {
return null;
Expand Down
90 changes: 90 additions & 0 deletions test/nemotron-inference-fix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,94 @@ main().catch((err) => {
const otherBody = JSON.parse(records[2].body);
expect(otherBody.chat_template_kwargs).toBeUndefined();
});

it("preload injects a tool-less system prompt for Ultra 550B without overriding caller intent (#4851)", () => {
const preload = extractStartScriptHeredoc(src, "NEMOTRON_FIX_EOF");
const harness = `
const http = require('http');
const records = [];
http.request = function (options) {
const record = { options, writes: [], headers: {}, removed: [] };
records.push(record);
return {
write(chunk) {
record.writes.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
return true;
},
end(cb) {
if (typeof cb === 'function') cb();
return true;
},
getHeader(name) { return record.headers[name]; },
setHeader(name, value) { record.headers[name] = value; },
removeHeader(name) { record.removed.push(name); delete record.headers[name]; },
};
};
${preload}
function send(body) {
const req = http.request({ method: 'POST', path: '/v1/chat/completions' });
req.write(body);
req.end();
}
// case 0: Ultra 550B, no system, no tools — expect injected system message
send(JSON.stringify({
model: 'nvidia/nemotron-3-ultra-550b-a55b',
messages: [{ role: 'user', content: 'Create a file and run it.' }],
}));
// case 1: Ultra 550B, existing system message — expect NO injection
send(JSON.stringify({
model: 'nvidia/nemotron-3-ultra-550b-a55b',
messages: [
{ role: 'system', content: 'You are pirate-themed.' },
{ role: 'user', content: 'hi' },
],
}));
// case 2: Ultra 550B, with tools — expect NO injection (model should use tools)
send(JSON.stringify({
model: 'nvidia/nemotron-3-ultra-550b-a55b',
messages: [{ role: 'user', content: 'hi' }],
tools: [{ type: 'function', function: { name: 'exec', parameters: {} } }],
}));
// case 3: non-matching Nemotron, no system, no tools — expect NO injection
send(JSON.stringify({
model: 'nvidia/nemotron-3-super-120b-a12b',
messages: [{ role: 'user', content: 'hi' }],
}));
console.log(JSON.stringify(records));
`;

const result = spawnSync(process.execPath, ["-e", harness], {
encoding: "utf-8",
timeout: 5000,
});
expect(result.status, result.stderr).toBe(0);
const records = JSON.parse(result.stdout.trim());
expect(records).toHaveLength(4);

// case 0: system message injected at position 0
const ultraBare = JSON.parse(records[0].writes.join(""));
expect(ultraBare.messages[0].role).toBe("system");
expect(ultraBare.messages[0].content).toMatch(/do not have tools/i);
expect(ultraBare.messages[1]).toEqual({
role: "user",
content: "Create a file and run it.",
});
// kwargs still applied (Nemotron rule)
expect(ultraBare.chat_template_kwargs).toEqual({ force_nonempty_content: true });

// case 1: caller's system message preserved, no injection prepended
const ultraWithSystem = JSON.parse(records[1].writes.join(""));
expect(ultraWithSystem.messages).toHaveLength(2);
expect(ultraWithSystem.messages[0].content).toBe("You are pirate-themed.");

// case 2: tools present, no injection
const ultraWithTools = JSON.parse(records[2].writes.join(""));
expect(ultraWithTools.messages).toHaveLength(1);
expect(ultraWithTools.messages[0].role).toBe("user");

// case 3: non-matching Nemotron model, no injection
const superModel = JSON.parse(records[3].writes.join(""));
expect(superModel.messages).toHaveLength(1);
expect(superModel.messages[0].role).toBe("user");
});
});
Loading