Skip to content

fix(nemotron3): render later system messages instead of raising - #200

Open
mottopanikeiku wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
mottopanikeiku:fix/nemotron3-later-system-messages
Open

mottopanikeiku wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
mottopanikeiku:fix/nemotron3-later-system-messages

Conversation

@mottopanikeiku

@mottopanikeiku mottopanikeiku commented Oct 22, 2026 •

Copy link
Copy Markdown

Nemotron3Renderer.render() raises ValueError: System message must be at the beginning. for any system message that is not the first message. The Nemotron 3 chat templates (Nano, Super, Ultra, 3.5 Lightning) accept that message and render it as its own turn through the user branch:

{%- elif message.role == "user" or message.role == "system" %}
    {{- '<|im_start|>' + message.role + '\n' }}

bridge_to_next_turn in the same renderer already renders an appended system message this way, so such a conversation can be extended through the bridge but not rendered in full. In verifiers' train client a tail that contains a system message is not bridged, so the full render() is the path that runs. verifiers' Responses dialect also turns instructions plus a developer input item into two system messages.

This change renders a non-leading system message through the user branch under its own role header, as the template does. The reasoning-effort hint still goes only on the last user message. The later-system parity scenario now also runs for nemotron-3, nemotron-3-ultra and nemotron-3.5.

Reproducer:

from renderers import create_renderer
from renderers.base import load_tokenizer

tok = load_tokenizer("nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16")
renderer = create_renderer(tok)
messages = [
    {"role": "system", "content": "You are a coding agent."},
    {"role": "user", "content": "Run the tests."},
    {"role": "assistant", "content": "All 12 tests pass."},
    {"role": "system", "content": "You have 5 turns left."},
    {"role": "user", "content": "Now add a test for the empty-input case."},
]
expected = list(
    tok.apply_chat_template(
        messages, add_generation_prompt=True, tokenize=True, return_dict=False
    )
)
print("template tokens:", len(expected))
got = renderer.render_ids(messages, add_generation_prompt=True)
print("renderer tokens:", len(got), "identical:", got == expected)
before after
reproducer above ValueError 68 tokens, identical to the template
verifiers ResponsesDialect.parse_request of an instructions + developer request, rendered with the nemotron-3 config from configs/advanced/nemotron-3-super/swe.toml ValueError 309 tokens, identical to the template (text parts joined first, as vLLM does)
pytest tests/test_parity.py -k "Nemotron and later-system" 32 failed 32 passed

After the change, pytest tests -k "nemotron or Nemotron" gives 1423 passed, 17 skipped (PIL/torch-dependent and capability skips). ruff 0.15.12 check and format pass on the changed files.

I found this by reading and testing the code.

I made this PR and directed its development, with AI assistance from Claude and GPT-6 Astra.

Note

Render later system messages in Nemotron3 renderer instead of raising

  • Only the initial system message goes through the separate system-prompt path; later system messages now use the user-like emission path in nemotron3.py
  • The emitted role header is derived from the actual message role, so later system messages are labeled as system rather than user
  • Adds Nemotron-3, Nemotron-3 Ultra, and Nemotron-3.5 to the later-system scenario's renderer allowlist in parity.py
  • Behavioral Change: conversations with non-initial system messages now render instead of being rejected

Macroscope summarized 83e2027.

The Nemotron 3 chat templates render a system message that is not first as its own system turn, and bridge_to_next_turn already does the same, but render() raised ValueError. I render it through the user branch like the template does and run the later-system parity scenario for the Nemotron renderers.

Assisted-by: Claude
Assisted-by: GPT-6 Astra
Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant