fix: preserve rollout meta in the v0 -> v1 legacy bridge - #1586
Merged
Conversation
The bridge only kept token ids: it dropped the prompt messages, the response message (content / reasoning / tool calls), finish_reason, usage, and the task's system prompt / answer — so a v0-bridged Trace was a near-empty skeleton next to a native v1 Trace. The cause: v0 RolloutOutput nests these as pydantic objects (messages, Response) and records finish_reason on response.message, but the mapping only handled plain dicts and read finish_reason off the response. Coerce v0 objects to dicts before mapping (_as_dict), read finish_reason/usage from their v0 locations, mirror tokens onto the response (as the native client does), and carry the prompt's system_prompt / instruction / answer onto the task. A v0-bridged Trace now matches the native v1 schema (verified by diffing reverse-text rollouts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mikasenghaas
marked this pull request as ready for review
June 9, 2026 18:34
Contributor
ApprovabilityVerdict: Approved Bug fix to preserve more metadata (tokens, finish_reason, system_prompt, answer) during v0->v1 trace conversion in the legacy bridge. Changes are self-contained to the conversion layer and authored by the module's original creator. You can customize Macroscope's approvability policy. Learn more. |
pull Bot
pushed a commit
to Stars1233/verifiers
that referenced
this pull request
Jun 23, 2026
…tellect-ai#1586) The bridge only kept token ids: it dropped the prompt messages, the response message (content / reasoning / tool calls), finish_reason, usage, and the task's system prompt / answer — so a v0-bridged Trace was a near-empty skeleton next to a native v1 Trace. The cause: v0 RolloutOutput nests these as pydantic objects (messages, Response) and records finish_reason on response.message, but the mapping only handled plain dicts and read finish_reason off the response. Coerce v0 objects to dicts before mapping (_as_dict), read finish_reason/usage from their v0 locations, mirror tokens onto the response (as the native client does), and carry the prompt's system_prompt / instruction / answer onto the task. A v0-bridged Trace now matches the native v1 schema (verified by diffing reverse-text rollouts). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The v0 → v1 legacy bridge (
rollout_output_to_trace) was producing a near-emptyTurn.response: it kept only the token ids and dropped almost everything else a native v1 trace carries.Root cause: a v0
RolloutOutputnests its data as pydantic objects in-process (SystemMessage/UserMessage/AssistantMessage,Response), and recordsfinish_reasononresponse.message(not the response). The bridge's helpers only handled plaindicts (if not isinstance(m, dict): continue) and readfinish_reasonoff the response top-level — so the message lists were silently skipped and the response fields came back null.Fix:
_as_dict()coerces v0 pydantic objects to dicts before mapping, so the helpers read them whether they arrive as objects or dicts._to_v1_responsenow readsfinish_reasonfromresponse.message(its v0 location), keepsusage, and mirrors the token ids ontoresponse.tokens(as the native v1 client does)._to_wire_taskcarries the prompt's system message →system_prompt, user message(s) →instruction, and the referenceanswer(as aWireTaskextra) onto the task.A v0-bridged
Tracenow matches the native v1 schema.Discrepancies fixed (v0-bridge vs native v1, reverse-text step 0)
Before, the bridge dropped:
turn.prompt(empty),response.message.content/reasoning_content(null),finish_reason(null),usage(null),response.tokens(null),task.instruction(empty),task.system_prompt(null),task.answer(missing).is_truncatedalso stayed wrong because it's computed fromfinish_reason.After, the only remaining schema differences are benign and not lost information:
rewards.reward+metrics.lcs_reward_func; v1 names itrewards.lcs. The value is preserved andtrace.rewardresolves identically — an env-level naming difference the bridge shouldn't rewrite.message.contentstrvsnull|str: sampling artifact; the field is nullable in both.Verification
Captured real v0
RolloutOutputs through the legacy path against a live vLLM server and diffed the resulting traces against native v1 reverse-text rollouts: prompt messages, response content/reasoning,finish_reason,usage, tokens (turn + response), andtask.system_prompt/instruction/answerall now present and matching.finish_reason="length"correctly drives the computedis_truncated.Note
Medium Risk
Changes only the legacy adapter path but affect training-visible trace shape (tokens, finish_reason, task fields); incorrect mapping could still skew truncation or RL reads if edge cases remain.
Overview
The v0→v1 legacy bridge in
rollout_output_to_tracewas dropping most trace metadata because mapping helpers only accepted plain dicts while in-process v0 rollouts use Pydantic models, andfinish_reasonwas read from the wrong level._as_dict()normalizes v0 objects viamodel_dump()before_to_v1_messages,_tool_calls, and_to_v1_responserun, so prompts, assistant content/reasoning/tool calls, and usage populate correctly instead of being skipped._to_v1_responsenow resolvesfinish_reasonfromresponse.message(v0) with a fallback to the response root, restricts values tostop/length/tool_calls, and attaches the same turn tokens onresponse.tokensto match native v1 clients._to_wire_taskreplaces flattening the whole prompt intoinstruction: system →system_prompt, user text →instruction, and optional referenceansweras aWireTaskextra.Together, bridged traces align with native v1 for training-critical fields;
is_truncatedcan compute correctly oncefinish_reason="length"is preserved.Reviewed by Cursor Bugbot for commit fdf82e8. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix rollout metadata preservation in the v0 to v1 legacy bridge
_as_dicthelper in legacy.py to coerce pydantic-like v0 objects to dicts before mapping, fixing dropped messages and tool calls._to_v1_responsenow populatesResponse.tokensand setsfinish_reasononly when it is one ofstop,length, ortool_calls; otherwiseNone.rollout_output_to_tracenow setssystem_promptfrom the first system message, buildsinstructionfrom user messages only, and carriesanswerin task extras.Response.tokensis now populated per turn by passing computedTurnTokensthrough_to_v1_response.Macroscope summarized fdf82e8.