feat(RL): add nvext Tokens-in-Tokens-Out and RL related response protocol and frontend support - #9649
Conversation
WalkthroughThis PR extends the LLM backend and OpenAI protocol to surface prompt log-probabilities and additional sampling controls through request parsing, validation, preprocessing, response building, and delta emission across both chat and completion endpoints. ChangesPrompt Log-Probabilities and Extended Sampling Support
🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/llm/src/protocols/unified.rs (1)
279-287:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDelegate
unsupported_fields()forUnifiedRequest.
OpenAIPreprocessor::nvext_passthrough_argsrelies onNvExtProvider::unsupported_fields()fallback (e.g.,cache_salt). With the current impl,UnifiedRequestalways uses the trait defaultNone, so those fields are silently dropped on this path.Suggested fix
impl NvExtProvider for UnifiedRequest { fn nvext(&self) -> Option<&NvExt> { self.inner.nvext.as_ref() } fn raw_prompt(&self) -> Option<String> { None } + + fn unsupported_fields( + &self, + ) -> Option<&std::collections::HashMap<String, serde_json::Value>> { + NvExtProvider::unsupported_fields(&self.inner) + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/llm/src/protocols/unified.rs` around lines 279 - 287, The UnifiedRequest NvExtProvider impl currently only implements nvext() and raw_prompt(), so unsupported_fields() falls back to the trait default and drops passthrough fields; add an unsupported_fields(&self) implementation that delegates through the inner nvext (e.g., return self.inner.nvext.as_ref().and_then(|nv| nv.unsupported_fields())) so OpenAIPreprocessor::nvext_passthrough_args sees fields like cache_salt; keep the implementation inside the impl NvExtProvider for UnifiedRequest and reference the existing nvext() and raw_prompt() methods for placement.
🧹 Nitpick comments (1)
lib/llm/src/protocols/openai/nvext.rs (1)
45-49: ⚡ Quick winAdd explicit test coverage for the DP-rank header alias path.
Line 48 introduces
x-data-parallel-rankfallback, but there’s no direct test for alias-only input (or both headers present). Add a focused test to prevent regressions in routing behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/llm/src/protocols/openai/nvext.rs` around lines 45 - 49, Add unit tests for the DP-rank header alias path to ensure routing uses HEADER_DP_RANK_ALIAS when HEADER_DP_RANK is absent and that precedence is correct when both are present: create tests that call the header-parsing logic in nvext.rs (exercising the dp_rank extraction that uses HEADER_DP_RANK .or_else(HEADER_DP_RANK_ALIAS) and the dp_rank variable) with (a) only HEADER_DP_RANK_ALIAS set and (b) both HEADER_DP_RANK and HEADER_DP_RANK_ALIAS set, asserting the returned dp_rank matches the expected value and that no panics or unexpected routing occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@lib/llm/src/protocols/unified.rs`:
- Around line 279-287: The UnifiedRequest NvExtProvider impl currently only
implements nvext() and raw_prompt(), so unsupported_fields() falls back to the
trait default and drops passthrough fields; add an unsupported_fields(&self)
implementation that delegates through the inner nvext (e.g., return
self.inner.nvext.as_ref().and_then(|nv| nv.unsupported_fields())) so
OpenAIPreprocessor::nvext_passthrough_args sees fields like cache_salt; keep the
implementation inside the impl NvExtProvider for UnifiedRequest and reference
the existing nvext() and raw_prompt() methods for placement.
---
Nitpick comments:
In `@lib/llm/src/protocols/openai/nvext.rs`:
- Around line 45-49: Add unit tests for the DP-rank header alias path to ensure
routing uses HEADER_DP_RANK_ALIAS when HEADER_DP_RANK is absent and that
precedence is correct when both are present: create tests that call the
header-parsing logic in nvext.rs (exercising the dp_rank extraction that uses
HEADER_DP_RANK .or_else(HEADER_DP_RANK_ALIAS) and the dp_rank variable) with (a)
only HEADER_DP_RANK_ALIAS set and (b) both HEADER_DP_RANK and
HEADER_DP_RANK_ALIAS set, asserting the returned dp_rank matches the expected
value and that no panics or unexpected routing occur.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a6eb0b0e-a231-4196-8b4e-f258b6705f6f
📒 Files selected for processing (17)
lib/llm/src/backend.rslib/llm/src/migration.rslib/llm/src/preprocessor.rslib/llm/src/protocols/common.rslib/llm/src/protocols/common/llm_backend.rslib/llm/src/protocols/openai.rslib/llm/src/protocols/openai/chat_completions.rslib/llm/src/protocols/openai/chat_completions/delta.rslib/llm/src/protocols/openai/common_ext.rslib/llm/src/protocols/openai/completions.rslib/llm/src/protocols/openai/completions/delta.rslib/llm/src/protocols/openai/nvext.rslib/llm/src/protocols/openai/validate.rslib/llm/src/protocols/unified.rslib/llm/tests/test_streaming_usage.rslib/llm/tests/tool_choice.rslib/llm/tests/tool_choice_finish_reasons.rs
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
565fd7c to
2256c2a
Compare
Signed-off-by: Biswa Panda <biswa.panda@gmail.com>
9f52a45 to
f46b9bf
Compare
|
Rebased on top of main. |
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
## Why
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today. A
request that works against a raw vLLM server currently fails against a
Dynamo-fronted vLLM server:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it. This PR closes that gap
without changing the response-side path — the existing nvext machinery
already emits `completion_token_ids` on the response.
## What changes
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods that fold
`Some(true)` into `nvext.extra_fields.push("completion_token_ids")`
(idempotent — no duplicate entry if the caller also set the nvext
form) and then drop the alias so the request doesn't ship two
representations of the same intent past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` so they
can call `normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly, not through the `_generic` helper) gets the same fold
inline, right after `normalize_chat_reasoning_template_args`.
- Callers passed `&request` -> `&mut request` (2 production sites in
`http/service/openai.rs`, and 15 test sites updated in bulk).
## Tests
8 new unit tests, mirroring the existing nvext-path tests:
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias;
validate() then succeeds
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops (must not populate extra_fields)
- `n > 1` with the alias is rejected by validate() with the same error
message as the existing nvext path
Rustfmt: clean.
## Not in scope
- Response shape stays on the existing
`choices[].nvext.completion_token_ids` path (merged in ai-dynamo#9649). Strict
vLLM response parity (`choices[].token_ids` at top level) is a
separate ask.
- No RL admin plane, no weight-transfer, no new endpoints. This is only
the request-side field parity split off from the closed ai-dynamo#9131/ai-dynamo#9382.
Closes / relates to the request-side half of the intent expressed in
ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO passthrough (the other half)
already merged in ai-dynamo#9649.
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
## Why
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today. A
request that works against a raw vLLM server currently fails against a
Dynamo-fronted vLLM server:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it. This PR closes that gap
without changing the response-side path — the existing nvext machinery
already emits `completion_token_ids` on the response.
## What changes
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods that fold
`Some(true)` into `nvext.extra_fields.push("completion_token_ids")`
(idempotent — no duplicate entry if the caller also set the nvext
form) and then drop the alias so the request doesn't ship two
representations of the same intent past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` so they
can call `normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly, not through the `_generic` helper) gets the same fold
inline, right after `normalize_chat_reasoning_template_args`.
- Callers passed `&request` -> `&mut request` (2 production sites in
`http/service/openai.rs`, and 15 test sites updated in bulk).
## Tests
8 new unit tests, mirroring the existing nvext-path tests:
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias;
validate() then succeeds
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops (must not populate extra_fields)
- `n > 1` with the alias is rejected by validate() with the same error
message as the existing nvext path
Rustfmt: clean.
## Not in scope
- Response shape stays on the existing
`choices[].nvext.completion_token_ids` path (merged in ai-dynamo#9649). Strict
vLLM response parity (`choices[].token_ids` at top level) is a
separate ask.
- No RL admin plane, no weight-transfer, no new endpoints. This is only
the request-side field parity split off from the closed ai-dynamo#9131/ai-dynamo#9382.
Closes / relates to the request-side half of the intent expressed in
ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO passthrough (the other half)
already merged in ai-dynamo#9649.
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
## Why
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today. A
request that works against a raw vLLM server currently fails against a
Dynamo-fronted vLLM server:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it. This PR closes that gap
without changing the response-side path — the existing nvext machinery
already emits `completion_token_ids` on the response.
## What changes
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods that fold
`Some(true)` into `nvext.extra_fields.push("completion_token_ids")`
(idempotent — no duplicate entry if the caller also set the nvext
form) and then drop the alias so the request doesn't ship two
representations of the same intent past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` so they
can call `normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly, not through the `_generic` helper) gets the same fold
inline, right after `normalize_chat_reasoning_template_args`.
- Callers passed `&request` -> `&mut request` (2 production sites in
`http/service/openai.rs`, and 15 test sites updated in bulk).
## Tests
8 new unit tests, mirroring the existing nvext-path tests:
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias;
validate() then succeeds
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops (must not populate extra_fields)
- `n > 1` with the alias is rejected by validate() with the same error
message as the existing nvext path
Rustfmt: clean.
## Not in scope
- Response shape stays on the existing
`choices[].nvext.completion_token_ids` path (merged in ai-dynamo#9649). Strict
vLLM response parity (`choices[].token_ids` at top level) is a
separate ask.
- No RL admin plane, no weight-transfer, no new endpoints. This is only
the request-side field parity split off from the closed ai-dynamo#9131/ai-dynamo#9382.
Closes / relates to the request-side half of the intent expressed in
ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO passthrough (the other half)
already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Close the gap that makes a `return_token_ids: true` request work
against a raw vLLM server but fail against a Dynamo-fronted vLLM
server, and surface the two response fields a client using that flag
actually needs.
## Request side
Add `return_token_ids: Option<bool>` to `NvCreateChatCompletionRequest`
and `NvCreateCompletionRequest`, a convenience alias for
`nvext.extra_fields = ["completion_token_ids"]`.
vLLM's OpenAI extension of the same name (`return_token_ids: true` in
`extra_body`) is what clients like the `speculators`
`data_generation_offline.py` capture flow and Prime-RL send today.
Without the alias, requests fail with:
```
Validation: Unsupported parameter(s): `return_token_ids`
```
because the field lands in `unsupported_fields` (catch-all) and
`validate_no_unsupported_fields` rejects it.
- Two new `Option<bool>` fields, one per request struct, with
`#[serde(default, skip_serializing_if = "Option::is_none")]`.
- Two `normalize_return_token_ids(&mut self)` methods fold `Some(true)`
into `nvext.extra_fields.push("completion_token_ids")` (idempotent)
and drop the alias so the request doesn't ship two representations
past this point.
- `validate_{chat_,}completion_fields_generic` now take `&mut` and call
`normalize_return_token_ids` before `validate`. The `n == 1`
constraint enforced by `validate_completion_token_ids_single_choice`
inspects `nvext.extra_fields`, so the fold has to happen first.
- The OpenAI Responses handler (which calls `chat_request.validate()`
directly) gets the same fold inline.
## Response side
The two response fields a `return_token_ids: true` client typically
needs alongside `completion_token_ids` are the prompt token ids echoed
back and any `kv_transfer_params` from a disaggregated backend (e.g.
the `hidden_states_path` from vLLM's KV-transfer connector). Add them
under nvext, next to the existing `completion_token_ids`:
- `NvExtResponse.prompt_token_ids: Option<Vec<TokenIdType>>` — sourced
from `LLMEngineOutput.engine_data["prompt_token_ids"]` (the vLLM
Python handler already populates this; other backends leave it
absent, in which case nothing is emitted).
- `NvExtResponse.kv_transfer_params: Option<serde_json::Value>` —
sourced from `BackendOutput.disaggregated_params` (the canonical
engine-owned location per its doc comment), falling back to
`engine_data["kv_transfer_params"]`. Mirrors the pattern in
`protocols/openai/generate.rs:534-560` for the native protocol.
- Both fields are gated by the same `"completion_token_ids"` entry in
`nvext.extra_fields` — so a request that sets `return_token_ids:
true` (or the equivalent nvext form) gets the trio together.
- Both are only emitted on the final chunk (when `finish_reason` is
present) — there's no meaningful streaming semantic for a per-delta
prompt-id list, and the vLLM handler surfaces both fields once at
completion.
- Wired through the shared `NvExtResponseFieldSelection::build_response_nvext`
helper so chat and completions delta generators share one gating
path.
Note: fields land under `response.nvext`, matching Dynamo's existing
extension convention (same layer as `completion_token_ids`). Clients
that expect them at the response top level (as vLLM's own OpenAI
server emits) need a small extractor to look under `nvext`.
## Tests
Request side (8 tests, from the prior revision):
- deserialization: field lives at request root, doesn't land in
unsupported_fields
- normalize folds `Some(true)` into extra_fields and drops the alias
- idempotent when both alias and nvext are set (single entry, not two)
- `false` / `None` are no-ops
- `n > 1` with the alias is rejected
Response side (4 new tests):
- `from_nvext_completion_token_ids_gates_the_return_token_ids_trio` —
a single `"completion_token_ids"` extra_fields entry turns on all
three response fields (completion + prompt + kv_transfer_params)
and does NOT bleed into unrelated selectors (timing, engine_data).
- `build_response_nvext_prompt_token_ids_final_chunk_only` — absent
mid-stream; present on the final chunk.
- `build_response_nvext_kv_transfer_params_final_chunk_only` — same
final-only shape, with a `hidden_states_path` payload.
- `build_response_nvext_return_token_ids_trio_together` — end-to-end:
request-side alias -> selector -> builder emits all three fields
together on the final chunk.
Existing 45 extensions unit tests continue to pass; existing 329
openai-protocol module tests continue to pass.
## Not in scope
- Response fields at the response top level (as vLLM emits) — kept
under `nvext` for consistency with Dynamo's existing extension
layering; a client-side extractor is trivial.
- No RL admin plane, no weight-transfer, no new endpoints.
Closes / relates to the request-side + response-side halves of the
intent expressed in ai-dynamo#9131 and ai-dynamo#9382. `nvext.token_data` / TITO
passthrough (the third piece) already merged in ai-dynamo#9649.
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Summary
lib/llmprotocol, preprocessor, and response plumbing for nvext Tokens-in-Tokens-Outnvext.token_data,cache_salt,extra_fields, token constraints,prompt_logprobs, detokenize, and stop token IDs through the Rust request/response pathcache_saltandstop_token_ids; rejecttruncate_prompt_tokensuntil it is supportedScope
This PR is the Rust
lib/llmhalf of the nvext Tokens-in-Tokens-Out split. It intentionally excludes the vLLM worker implementation so reviewers can focus on the public/internal request protocol and response mapping first.Where To Review
lib/llm/src/protocols/openai/nvext.rslib/llm/src/preprocessor.rslib/llm/src/protocols/openai/completions.rslib/llm/src/protocols/openai/chat_completions.rslib/llm/src/protocols/openai/validate.rsLocal Tests
cargo test -p dynamo-llm protocols::openai::completions::tests:: --libcargo test -p dynamo-llm protocols::openai::chat_completions::tests:: --librustfmt --edition 2024 --check lib/llm/src/protocols/openai/validate.rs lib/llm/src/protocols/openai/completions.rs lib/llm/src/protocols/openai/chat_completions.rsgit diff --checkFollow-up
The vLLM worker-side implementation should be reviewed separately so its diff remains scoped to
components/src/dynamo/vllm/*.Notes
supersedes #9382
Summary by CodeRabbit