Skip to content
Open
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
4 changes: 2 additions & 2 deletions rust/src/server/src/routes/inference/generate/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ pub(super) fn prepare_generate_request(
sampling_params.vllm_xargs = merge_kv_transfer_params(
sampling_params.vllm_xargs,
request.kv_transfer_params.as_ref(),
);
)?;
sampling_params.vllm_xargs = merge_ec_transfer_params(
sampling_params.vllm_xargs,
request.ec_transfer_params.as_ref(),
);
)?;

let text_request = TextRequest {
request_id: ctx.request_id.clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ pub(super) fn prepare_chat_request(
structured_outputs,
skip_reading_prefix_cache: None,
vllm_xargs: merge_kv_transfer_params(
merge_ec_transfer_params(request.vllm_xargs, request.ec_transfer_params.as_ref()),
merge_ec_transfer_params(request.vllm_xargs, request.ec_transfer_params.as_ref())?,
request.kv_transfer_params.as_ref(),
),
)?,
},
chat_options: ChatOptions {
generation_prompt_mode,
Expand Down
4 changes: 2 additions & 2 deletions rust/src/server/src/routes/openai/completions/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ pub(super) fn prepare_completion_request(
structured_outputs,
skip_reading_prefix_cache: None,
vllm_xargs: merge_kv_transfer_params(
merge_ec_transfer_params(request.vllm_xargs, request.ec_transfer_params.as_ref()),
merge_ec_transfer_params(request.vllm_xargs, request.ec_transfer_params.as_ref())?,
request.kv_transfer_params.as_ref(),
),
)?,
},
decode_options: TextDecodeOptions {
skip_special_tokens: request.skip_special_tokens,
Expand Down
79 changes: 75 additions & 4 deletions rust/src/server/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ pub fn utility_call_error(method: &str, error: impl AsReport) -> ApiError {
/// Merge `kv_transfer_params` into the `vllm_xargs` map, mirroring the Python
/// vLLM behavior where `kv_transfer_params` is injected into `extra_args` for
/// engine-core consumption.
///
/// Rejects a caller-supplied `kv_transfer_params` key inside `vllm_xargs`;
/// that field must be sent as a top-level request field.
pub fn merge_kv_transfer_params(
mut xargs: Option<HashMap<String, Value>>,
kv_transfer_params: Option<&HashMap<String, Value>>,
) -> Option<HashMap<String, Value>> {
) -> Result<Option<HashMap<String, Value>>, ApiError> {
reject_reserved_xarg(xargs.as_ref(), "kv_transfer_params")?;
if let Some(kv_params) = kv_transfer_params {
let map = xargs.get_or_insert_with(HashMap::new);
map.insert(
Expand All @@ -47,16 +51,20 @@ pub fn merge_kv_transfer_params(
serde_json::to_value(kv_params).unwrap(),
);
}
xargs
Ok(xargs)
}

/// Merge `ec_transfer_params` into the `vllm_xargs` map, mirroring the Python
/// vLLM behavior where `ec_transfer_params` is injected into `extra_args` for
/// engine-core consumption.
///
/// Rejects a caller-supplied `ec_transfer_params` key inside `vllm_xargs`;
/// that field must be sent as a top-level request field.
pub fn merge_ec_transfer_params(
mut xargs: Option<HashMap<String, Value>>,
ec_transfer_params: Option<&HashMap<String, Value>>,
) -> Option<HashMap<String, Value>> {
) -> Result<Option<HashMap<String, Value>>, ApiError> {
reject_reserved_xarg(xargs.as_ref(), "ec_transfer_params")?;
if let Some(ec_params) = ec_transfer_params {
let map = xargs.get_or_insert_with(HashMap::new);
map.insert(
Expand All @@ -65,7 +73,20 @@ pub fn merge_ec_transfer_params(
serde_json::to_value(ec_params).unwrap(),
);
}
xargs
Ok(xargs)
}

fn reject_reserved_xarg(
xargs: Option<&HashMap<String, Value>>,
reserved: &'static str,
) -> Result<(), ApiError> {
if xargs.is_some_and(|map| map.contains_key(reserved)) {
return Err(ApiError::invalid_request(
format!("'{reserved}' must be sent as a top-level field, not via vllm_xargs"),
Some("vllm_xargs"),
));
}
Ok(())
}

pub fn resolve_session_id(
Expand Down Expand Up @@ -157,3 +178,53 @@ pub fn resolve_base_request_id(
id
})
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use super::*;
use serde_json::json;

#[test]
fn merge_kv_rejects_reserved_xargs_key() {
let mut xargs = HashMap::new();
xargs.insert("kv_transfer_params".to_string(), json!("x"));
let err = merge_kv_transfer_params(Some(xargs), None).unwrap_err();
assert!(matches!(
err,
ApiError::InvalidRequest {
param: Some("vllm_xargs"),
..
}
));
}

#[test]
fn merge_ec_rejects_reserved_xargs_key() {
let mut xargs = HashMap::new();
xargs.insert("ec_transfer_params".to_string(), json!({"a": 1}));
let err = merge_ec_transfer_params(Some(xargs), None).unwrap_err();
assert!(matches!(
err,
ApiError::InvalidRequest {
param: Some("vllm_xargs"),
..
}
));
}

#[test]
fn merge_kv_overlays_typed_field() {
let mut xargs = HashMap::new();
xargs.insert("custom".to_string(), json!(1));
let mut kv = HashMap::new();
kv.insert("do_remote_prefill".to_string(), json!(true));
let merged = merge_kv_transfer_params(Some(xargs), Some(&kv)).unwrap().unwrap();
assert_eq!(merged["custom"], json!(1));
assert_eq!(
merged["kv_transfer_params"]["do_remote_prefill"],
json!(true)
);
}
}
76 changes: 76 additions & 0 deletions tests/entrypoints/openai/test_xargs_transfer_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Reject reserved transfer keys planted through vllm_xargs."""

import pytest

from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.exceptions import VLLMValidationError


@pytest.mark.parametrize("bad_value", ["x", 1, 1.5, ["x"], True])
@pytest.mark.parametrize("reserved", ["kv_transfer_params", "ec_transfer_params"])
def test_completion_rejects_reserved_xargs_key(reserved, bad_value):
request = CompletionRequest(
model="test-model",
prompt="hi",
max_tokens=1,
vllm_xargs={reserved: bad_value},
)
with pytest.raises(VLLMValidationError, match="top-level field"):
request.to_sampling_params(max_tokens=1, default_sampling_params={})


@pytest.mark.parametrize("bad_value", ["x", 1, ["x"]])
def test_chat_completion_rejects_reserved_xargs_key(bad_value):
request = ChatCompletionRequest(
model="test-model",
messages=[{"role": "user", "content": "hi"}],
max_tokens=1,
vllm_xargs={"kv_transfer_params": bad_value},
)
with pytest.raises(VLLMValidationError, match="top-level field"):
request.to_sampling_params(max_tokens=1, default_sampling_params={})


def test_responses_rejects_reserved_xargs_key():
request = ResponsesRequest(
model="test-model",
input="hi",
vllm_xargs={"kv_transfer_params": "x"},
)
with pytest.raises(VLLMValidationError, match="top-level field"):
request.to_sampling_params(default_max_tokens=16)


def test_completion_overlays_typed_kv_transfer_params():
kv = {"do_remote_prefill": True}
request = CompletionRequest(
model="test-model",
prompt="hi",
max_tokens=1,
vllm_xargs={"custom": 7, "kv_cache_report_mode": "full"},
kv_transfer_params=kv,
)
params = request.to_sampling_params(max_tokens=1, default_sampling_params={})
assert params.extra_args == {
"custom": 7,
"kv_cache_report_mode": "full",
"kv_transfer_params": kv,
}


def test_completion_does_not_mutate_request_xargs():
xargs = {"custom": 7}
request = CompletionRequest(
model="test-model",
prompt="hi",
max_tokens=1,
vllm_xargs=xargs,
kv_transfer_params={"do_remote_decode": True},
)
request.to_sampling_params(max_tokens=1, default_sampling_params={})
assert request.vllm_xargs == {"custom": 7}
assert "kv_transfer_params" not in xargs
28 changes: 28 additions & 0 deletions tests/test_sampling_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from vllm import SamplingParams
from vllm.exceptions import VLLMValidationError
from vllm.sampling_params import merge_request_extra_args


@dataclass
Expand Down Expand Up @@ -64,6 +65,33 @@ def test_extra_args_accepts_messagepack_integer_boundaries(value):
assert SamplingParams(extra_args=extra_args).extra_args == extra_args


@pytest.mark.parametrize("bad_value", ["x", 1, 1.5, ["x"], True])
@pytest.mark.parametrize("key", ["kv_transfer_params", "ec_transfer_params"])
def test_extra_args_rejects_non_dict_transfer_params(key, bad_value):
with pytest.raises(VLLMValidationError, match=f"{key} must be a dict"):
SamplingParams(extra_args={key: bad_value})


def test_merge_request_extra_args_rejects_reserved_xargs_keys():
with pytest.raises(VLLMValidationError, match="top-level field"):
merge_request_extra_args({"kv_transfer_params": "x"})
with pytest.raises(VLLMValidationError, match="top-level field"):
merge_request_extra_args({"ec_transfer_params": {"do_remote_prefill": True}})


def test_merge_request_extra_args_overlays_typed_fields():
kv = {"do_remote_prefill": True}
merged = merge_request_extra_args(
{"custom": 1, "kv_cache_report_mode": "full"},
kv_transfer_params=kv,
)
assert merged == {
"custom": 1,
"kv_cache_report_mode": "full",
"kv_transfer_params": kv,
}


def test_extra_args_preserves_custom_objects_and_shared_containers():
custom = object()
shared = [custom, (None, "value", 1.5)]
Expand Down
20 changes: 20 additions & 0 deletions tests/v1/test_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import pytest

from vllm import SamplingParams
from vllm.v1.engine import EngineCoreRequest
from vllm.v1.request import Request, RequestStatus
Expand Down Expand Up @@ -40,3 +42,21 @@ def test_request_copies_session_id_from_engine_core_request():
request = Request.from_engine_core_request(engine_request, block_hasher=None)

assert request.session_id == "session-1"


@pytest.mark.parametrize("bad_value", ["x", 1, ["x"], True])
@pytest.mark.parametrize("key", ["kv_transfer_params", "ec_transfer_params"])
def test_request_ignores_non_dict_transfer_params(key, bad_value):
"""EngineCore must not crash if extra_args carries a non-dict transfer blob."""
sampling_params = SamplingParams(max_tokens=1)
sampling_params.extra_args = {key: bad_value}

request = Request(
request_id="request-1",
prompt_token_ids=[1, 2, 3],
sampling_params=sampling_params,
pooling_params=None,
)

assert request.kv_transfer_params is None
assert request.ec_transfer_params is None
13 changes: 6 additions & 7 deletions vllm/entrypoints/openai/chat_completion/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
SamplingParams,
StructuredOutputsParams,
ThinkingTokenBudget,
merge_request_extra_args,
)
from vllm.utils import random_uuid

Expand Down Expand Up @@ -712,13 +713,11 @@ def to_sampling_params(
if prompt_logprobs is None and self.echo:
prompt_logprobs = self.top_logprobs

extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
if self.kv_transfer_params:
# Pass in kv_transfer_params via extra_args
extra_args["kv_transfer_params"] = self.kv_transfer_params
if self.ec_transfer_params:
# Pass in ec_transfer_params via extra_args
extra_args["ec_transfer_params"] = self.ec_transfer_params
extra_args = merge_request_extra_args(
self.vllm_xargs,
kv_transfer_params=self.kv_transfer_params,
ec_transfer_params=self.ec_transfer_params,
)
return SamplingParams.from_optional(
n=self.n,
presence_penalty=self.presence_penalty,
Expand Down
13 changes: 6 additions & 7 deletions vllm/entrypoints/openai/completion/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SamplingParams,
StructuredOutputsParams,
ThinkingTokenBudget,
merge_request_extra_args,
)
from vllm.utils import random_uuid
from vllm.utils.collection_utils import is_list_of
Expand Down Expand Up @@ -361,13 +362,11 @@ def to_sampling_params(

echo_without_generation = self.echo and self.max_tokens == 0

extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
if self.kv_transfer_params:
# Pass in kv_transfer_params via extra_args
extra_args["kv_transfer_params"] = self.kv_transfer_params
if self.ec_transfer_params:
# Pass in ec_transfer_params via extra_args
extra_args["ec_transfer_params"] = self.ec_transfer_params
extra_args = merge_request_extra_args(
self.vllm_xargs,
kv_transfer_params=self.kv_transfer_params,
ec_transfer_params=self.ec_transfer_params,
)
return SamplingParams.from_optional(
n=self.n,
presence_penalty=self.presence_penalty,
Expand Down
11 changes: 6 additions & 5 deletions vllm/entrypoints/openai/responses/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
RequestOutputKind,
SamplingParams,
StructuredOutputsParams,
merge_request_extra_args,
)
from vllm.utils import random_uuid

Expand Down Expand Up @@ -429,11 +430,11 @@ def to_sampling_params(
if isinstance(stop, str):
stop = [stop]

extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
if self.kv_transfer_params:
extra_args["kv_transfer_params"] = self.kv_transfer_params
if self.ec_transfer_params:
extra_args["ec_transfer_params"] = self.ec_transfer_params
extra_args = merge_request_extra_args(
self.vllm_xargs,
kv_transfer_params=self.kv_transfer_params,
ec_transfer_params=self.ec_transfer_params,
)

return SamplingParams.from_optional(
temperature=temperature,
Expand Down
3 changes: 2 additions & 1 deletion vllm/entrypoints/speech_to_text/transcription/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
BeamSearchParams,
RequestOutputKind,
SamplingParams,
merge_request_extra_args,
)
from vllm.utils import random_uuid

Expand Down Expand Up @@ -275,7 +276,7 @@ def to_sampling_params(
output_kind=RequestOutputKind.DELTA
if self.stream
else RequestOutputKind.FINAL_ONLY,
extra_args=self.vllm_xargs,
extra_args=merge_request_extra_args(self.vllm_xargs) or None,
skip_clone=True, # Created fresh per request, safe to skip clone
)

Expand Down
Loading
Loading