fix(llm): preserve nvext through tool-call jail - #13730
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review. WalkthroughThe tool-call jail now preserves and merges ChangesResponse metadata preservation
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change preserves response metadata while buffered tool-call chunks are rewritten. The fix is localized and validated by the reported tests and checks; no actionable merge-blocking risk remains after normal review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
jthomson04
left a comment
There was a problem hiding this comment.
Automated review of the nvext-through-the-jail change. The direction is right, but I think the core mechanism — "stamp the accumulated nvext onto the next emitted data chunk" — has a case where it doesn't deliver the metadata it's meant to preserve, plus a merge-semantics issue and a missing end-of-stream flush. Details inline.
One finding falls outside the diff, so it can't be an inline comment:
lib/llm/src/protocols/openai/chat_completions.rs:300-302 — stream_choice_chunk_from_template is the one synthetic-chunk site this PR didn't consolidate, and it still leaks nvext.
The PR's doc comment now says the scrub helper is "kept as one function so the call sites cannot drift apart", but stream_choice_chunk_from_template clears inner.usage and llm_metrics on the cloned envelope and not nvext — the exact drift scrub_synthetic_chunk_metadata exists to prevent.
Repro: DYN_ENABLE_EXPERIMENTAL_PARSERS_V2=1, Qwen3-Coder family, stream_options.include_usage unset (so no empty-choices chunk ever refreshes template), engine drops the terminal finish_reason. tool_parser_v2::apply_stream's EOF backstop calls finish_unterminated_choices(..., template) where template is a clone of the last content chunk, nvext included. The trailing ToolCalls chunk therefore re-emits completion_token_ids: [16], DeltaAggregator append-merges it, and the aggregated response gets [..., 16, 16] — plus a duplicate engine_data/timing on the wire for streaming clients.
Note tool_parser_v2.rs:1065 and :1118 already assert llm_metrics.is_none() on these chunks; the nvext counterpart is missing.
🤖 Generated with Claude Code
18335e0 to
f3bf4f3
Compare
|
The out-of-diff parser-v2 finding is also fixed in f3bf4f3. I moved the existing synthetic-chunk scrub helper into the shared chat-completion module with crate-only visibility. Both legacy recovery/EOF clones and parser-v2 |
|
/ok to test f3bf4f3 |
f3bf4f3 to
57acc75
Compare
|
/ok to test 57acc75 |
57acc75 to
70b3f5b
Compare
|
Left one small comment, and otherwise needs merge conflicts fixed |
70b3f5b to
2875787
Compare
|
/ok to test 2875787 |
Signed-off-by: jthomson04 <jwillthomson19@gmail.com>
2875787 to
6f8b788
Compare
rmccorm4
left a comment
There was a problem hiding this comment.
one nit, otherwise LGTM: https://github.com/ai-dynamo/dynamo/pull/13730/changes#r3875689803
Signed-off-by: jthomson04 <jwillthomson19@gmail.com>
Overview
Preserve response-level
nvextmetadata when the legacy streaming tool-call jail buffers and rewrites OpenAI chat chunks.Summary
The jail unwraps
NvCreateChatCompletionStreamResponsebefore parsing. It previously rebuilt emitted chunks withnvext: None, which removed requested metadata such asengine_data, completion token IDs, timing, worker IDs, and routed experts even when the backend returned it correctly.The faulty path is present in Dynamo 1.3, Dynamo 1.4.1, and current
main.Details
llm_metrics,nvext, and one clean response envelope only when an input contains metadata. Reuse the legacy jail's terminal-error latch as the single failure state.completion_token_idsby ordered append. For every other top-level field, the latest supplied value for that field wins. Replaceengine_dataas one complete opaque value.llm_metricsonto any emitted data chunk because the HTTP layer observes metrics before it removes payload-only usage chunks.nvextonto a non-payload-usage output with a choice. If valid metadata remains at successful EOF, emit it once in a clean choice-less chunk before client usage and[DONE].DeltaAggregatorabsorbs the internal EOF metadata chunk into the final top-levelnvext.n > 1whennvext.extra_fieldsrequestsengine_data,routed_experts, orstop_reason. Request-level metadata, Muse and Qwen unified routes, parser v2, and pass-through remain supported withn > 1.llm_metrics, andnvext.nvexton every chunk.This is independent of #13588 and #13640. Those PRs produce or expose training metadata; this fix prevents the legacy jail from deleting it.
Downstream context:
After merge, please backport this fix to the maintained
release/1.4.0andrelease/1.3.0branches. NeMo-RL currently usesai-dynamo[vllm]==1.3.0.post1.Where should the reviewer start?
OpenAIPreprocessor::apply_tool_calling_jailinlib/llm/src/preprocessor.rs, thenjail_flushes_terminal_nvext_before_client_usage,jail_discards_pending_metadata_after_transport_error, and the two merge tests inlib/llm/tests/test_streaming_tool_parsers.rs.Related Issues
Validation
Validated at
2875787bd81017d92eafd9beea8209766f0d87baon canonical main9a48f622e0169376a7f8cc0e697677d467e788c7.cargo test -p dynamo-llm --test test_streaming_tool_parsers— 45 passed.cargo test -p dynamo-llm --lib protocols::openai::chat_completions::tool_parser_v2::tests— 20 passed.cargo test -p dynamo-llm --lib protocols::openai::chat_completions::unified_parser::tests— 61 passed.cargo test -p dynamo-llm --test postprocessor_parsing_stream— 92 passed.InvalidArgumentbefore the unreachable backend is called.DeltaAggregatormetadata tests passed.cargo fmt --all -- --check— passed.cargo clippy --no-deps -p dynamo-llm --all-targets -- -D warnings— passed.Summary by CodeRabbit