Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,18 @@ The OpenAI-compatible completion routes provide a cross-backend token-in/token-o
| Feature | Request | Response | Notes |
|---|---|---|---|
| Token input | Set `prompt` to an integer array on `/v1/completions`, or set `nvext.token_data` on a chat or completion request. | Standard completion response | `nvext.token_data` bypasses frontend tokenization. |
| Completion token IDs | Add `"completion_token_ids"` to `nvext.extra_fields`. | `nvext.completion_token_ids` | Requires one prompt and one generated choice. Streaming responses contain token deltas; non-streaming responses contain the concatenated IDs. |
| Completion token IDs | Add `"completion_token_ids"` to `nvext.extra_fields`. | `nvext.completion_token_ids` | Requires one prompt and one generated choice. Streaming responses contain ordered token deltas; non-streaming responses contain the concatenated IDs. A tool parser can buffer and rewrite output, so token IDs on parsed streams are not aligned with each rewritten `delta.content`. |
| Completion log probabilities | Set `logprobs` on `/v1/completions`, or set `logprobs: true` and `top_logprobs` on `/v1/chat/completions`. | Standard `choices[].logprobs` | The selected engine must support the requested log probability mode. |
| Prompt log probabilities | Set top-level `prompt_logprobs` and add `"prompt_logprobs"` to `nvext.extra_fields`. | `nvext.prompt_logprobs` on the final response | The first prompt position is `null` because it has no preceding-token probability. |
| Prefix-cache salt | Set `nvext.cache_salt`. | No response field | vLLM includes the opaque salt in prompt cache keys, separating reuse between requests with different salts. |
| Routed expert data | Add `"routed_experts"` to `nvext.extra_fields`. | `nvext.routed_experts` on the final response | Requires routed-expert capture in the engine build and configuration. |
| Raw engine metadata | Add `"engine_data"` to `nvext.extra_fields`. | `nvext.engine_data` | Backend-specific and not a stable cross-backend schema. Prefer named fields when available. |
| SGLang `meta_info` upload | Set `nvext.metadata_upload.url`. | Out-of-band object per choice | Requires an RL-enabled SGLang worker and fsspec support. |

On the legacy chat tool-parser path, a successful streaming response can include a choice-less `nvext` frame after the last generated choice and before the client usage frame and `[DONE]`. Dynamo-aware streaming clients must inspect top-level `nvext` on every frame. Non-streaming requests still return one JSON response because Dynamo aggregates this internal frame into the final top-level `nvext`.

When Dynamo combines buffered metadata, it appends `completion_token_ids` in stream order. For each other top-level field, the latest supplied value for that field wins; a field that is not supplied again keeps its earlier value. Dynamo replaces `engine_data` as one complete value and does not merge its nested fields. The legacy tool parser requires `n: 1` when `extra_fields` requests `engine_data`, `routed_experts`, or `stop_reason`. This limit does not apply to parser v2 or to request-level metadata such as timing and worker IDs.

See [NVIDIA Request Extensions](../../developer-guide/additional-resources/nvidia-request-extensions-nvext.md) for the complete `nvext` reference.

Backend RL flags also select engine-specific behavior:
Expand Down
38 changes: 37 additions & 1 deletion lib/llm/src/http/service/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3181,6 +3181,41 @@ mod tests {
"internal metrics leaked to client SSE: {wire}"
);

// A choice-less Dynamo metadata frame is client-visible.
let metadata: crate::protocols::openai::chat_completions::NvCreateChatCompletionStreamResponse =
serde_json::from_value(serde_json::json!({
"id": "chatcmpl-x", "object": "chat.completion.chunk", "created": 1,
"model": "test-model", "choices": [],
"nvext": {"engine_data": {"prompt_token_ids": [1, 2]}}
}))
.unwrap();
let metadata = Annotated {
id: None,
data: Some(metadata),
event: None,
comment: None,
error: None,
};
let mut http_queue_guard = None;
let event = process_chat_response_using_event_converter_and_observe_metrics(
EventConverter::from(metadata),
&mut collector,
&mut http_queue_guard,
ReasoningField::default(),
)
.expect("conversion ok")
.expect("nvext chunk should yield a client event");
let sse = Sse::new(futures::stream::once(async move {
Ok::<_, std::convert::Infallible>(event)
}));
let body = sse.into_response().into_body();
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
let wire = String::from_utf8_lossy(&bytes);
assert!(
wire.contains("engine_data") && wire.contains("prompt_token_ids"),
"nvext metadata did not reach client SSE: {wire}"
);

// (2) Payload-only usage chunk (event = payload_usage, carries usage data).
let usage: crate::protocols::openai::chat_completions::NvCreateChatCompletionStreamResponse =
serde_json::from_value(serde_json::json!({
Expand All @@ -3198,10 +3233,11 @@ mod tests {
};

let mut http_queue_guard = None;
let result = process_response_using_event_converter_and_observe_metrics(
let result = process_chat_response_using_event_converter_and_observe_metrics(
EventConverter::from(payload_usage),
&mut collector,
&mut http_queue_guard,
ReasoningField::default(),
)
.expect("conversion ok");
assert!(
Expand Down
Loading
Loading