Skip to content

feat: rlm renderer for the RLM chat format#107

Draft
mikasenghaas wants to merge 2 commits into
mainfrom
feat/rlm-renderer
Draft

feat: rlm renderer for the RLM chat format#107
mikasenghaas wants to merge 2 commits into
mainfrom
feat/rlm-renderer

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jul 18, 2026

Copy link
Copy Markdown
Member

Adds the rlm renderer for the minimal RLM chat format (PrimeIntellect/RLM-Chat-Template, private), designed for the rlm harness.

Format

<system>...</system><user>...</user><assistant>{content}<ipython>{code}</ipython></assistant><output>{result}</output>
  • One message, one tag block. Ten single-token delimiters (reserved Nemotron-3 <SPECIAL_18><SPECIAL_27> slots renamed — no vocab resize for Nemotron Super base). </assistant> (id 23) is eos/stop.
  • Thinking is never dropped: retention is structurally "all" (config validator rejects anything else); assistant content renders verbatim on every turn and the bridge extends without re-rendering. A reasoning_content field is restored as <think>{reasoning}</think> before content rather than dropped (the only deliberate divergence from the jinja template, which cannot know the field exists).
  • ipython is the only tool, rendered inline as <ipython>{code}</ipython>; results are <output> turns (role: "tool"). Exactly one call per assistant turn (mirrors the harness's parallel_tool_calls=False + one-call error in rlm/engine.py).

tools= semantics, end to end

The HF chat template raises on any tools= (deliberate: no schema text ever enters the prompt). This is safe because apply_chat_template is not in the serving path:

  • Serving: rlm-harness sends tools=[IPYTHON_SCHEMA] on every chat-completion (rlm/tools/registry.py — "always just ipython") → verifiers interception dialect.parse_request extracts tools → renderers client.generate(..., tools=tools)this renderer's render(messages, tools=...). The renderer therefore accepts exactly [ipython] as validation-only (renders nothing); any other name or extra tool raises.
  • Template raise only fires on direct apply_chat_template(tools=...) use (e.g. naive SFT preprocessing), where failing loudly is the correct signal that this format renders no schemas.

Wiring

  • renderers/rlm.pyRlmRenderer (render/render_ids/parse_response/get_stop_token_ids/bridge_to_next_turn, per-token attribution matching the nemotron-3 conventions: assistant body+close sampled/content, scaffold tags neither).
  • RlmRendererConfig (name="rlm") in configs + discriminated union; lazy registry, _populate_registry, MODEL_RENDERER_MAP["PrimeIntellect/RLM-Chat-Template"].
  • Real-data validation: 200 samples of PrimeIntellect/SyntheticData-GLM5.2-scaleswe-reward0 / scaleswe_rlm_glm51 render through the renderer (197 clean; 3 raise on genuine data noise — a 'ipy' tool-name typo). Tool calls in these traces arrive as flat JSON strings (verifiers trace shape) and compaction produces mid-list system messages; both are accepted, matching the jinja template's behavior.
  • tests/test_rlm.py — 16 tests: byte parity with the jinja template at every prefix (with/without generation prompt), thinking retention, reasoning_content restoration, tools validation, tool-call rejection paths, parse round-trip (OK span + unclosed block), stop ids, bridge exact-extension + assistant rejection, config validation, content-mask invariants. The tokenizer fixture reconstructs the template repo from the public Nemotron-3 Super tokenizer, so CI needs no private-hub token. All pass locally (15 passed).

🤖 Generated with Claude Code

Note

Add RlmRenderer for the RLM chat format with inline ipython tool support

  • Adds renderers/rlm.py implementing RlmRenderer, a deterministic renderer for the RLM chat format using single-token role tags (<system>, <user>, <assistant>, <ipython>, etc.).
  • Validates at startup that the tokenizer encodes all required role tags as single tokens; fails fast if any are missing.
  • Supports at most one ipython tool call per assistant turn encoded inline as <ipython>code</ipython>; rejects other tool types or multiple calls.
  • Assistant reasoning content is always retained inline as <think>...</think> (thinking retention is forced to "all").
  • parse_response extracts content and ipython code blocks into ParsedToolCall entries, handling both closed and unclosed blocks; bridge_to_next_turn trims to a closed assistant turn before appending new messages.
  • Registers the renderer under the key "rlm" and routes the PrimeIntellect/RLM-Chat-Template model to it automatically.

Macroscope summarized b62d804. (Automatic summaries will resume when PR exits draft mode or review begins).

mikasenghaas and others added 2 commits July 18, 2026 00:01
…emplate)

Minimal role-tag-only format for the rlm harness: one message, one tag
block, ten single-token delimiters (renamed reserved Nemotron-3 slots
18-27), </assistant> as eos. Thinking is structurally never dropped
(retention "all"; bridge extends without re-rendering). The single tool
ipython renders inline as <ipython>{code}</ipython>; tool results are
<output>...</output> turns. tools= is validation-only: exactly
[ipython] is accepted and renders nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real scaleswe rlm traces store tool_calls as flat JSON strings
({"id","name","arguments"}) and carry mid-conversation system messages
from compaction; the chat template renders both, so the renderer must
too. Validated over 200 real dataset samples (197 render, 3 data-noise
tool-name typos raise as intended).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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