Skip to content

Commit e55cc59

Browse files
[Rust Frontend][CI] Unblock more end-to-end test cases (#47735)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
1 parent ba50b97 commit e55cc59

14 files changed

Lines changed: 105 additions & 40 deletions

File tree

.buildkite/test-amd.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,15 +1977,21 @@ steps:
19771977
- tests/utils.py
19781978
- tests/benchmarks/test_serve_cli.py
19791979
- tests/entrypoints/openai/chat_completion/test_chat_completion.py
1980+
- tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py
19801981
- tests/entrypoints/openai/completion/test_shutdown.py
1982+
- tests/entrypoints/openai/test_return_token_ids.py
1983+
- tests/entrypoints/openai/test_uds.py
19811984
- tests/v1/sample/test_logprobs_e2e.py
19821985
- vllm/platforms/rocm.py
19831986
commands:
19841987
- export VLLM_USE_RUST_FRONTEND=1
19851988
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
19861989
- pytest -v -s benchmarks/test_serve_cli.py -k "not insecure and not (test_bench_serve and not test_bench_serve_chat)"
19871990
- pytest -v -s entrypoints/openai/chat_completion/test_chat_completion.py -k "not test_invalid_json_schema and not test_invalid_regex"
1991+
- pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not multiple"
19881992
- pytest -v -s entrypoints/openai/completion/test_shutdown.py -k "not engine_failure and not test_abort_timeout_exits_quickly"
1993+
- pytest -v -s entrypoints/openai/test_return_token_ids.py -k "not test_comparison"
1994+
- pytest -v -s entrypoints/openai/test_uds.py
19891995
- pytest -v -s v1/sample/test_logprobs_e2e.py -k "test_prompt_logprobs_e2e_server"
19901996

19911997
- label: Rust Frontend Serve Admin Coverage # TBD
@@ -2000,16 +2006,20 @@ steps:
20002006
- vllm/entrypoints/serve/
20012007
- vllm/v1/engine/
20022008
- tests/utils.py
2003-
- tests/entrypoints/serve/disagg/test_serving_tokens.py
2009+
- tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
2010+
- tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
20042011
- tests/entrypoints/serve/instrumentator/test_basic.py
20052012
- tests/entrypoints/serve/instrumentator/test_metrics.py
2013+
- tests/entrypoints/serve/tokenize/test_tokenization.py
20062014
- vllm/platforms/rocm.py
20072015
commands:
20082016
- export VLLM_USE_RUST_FRONTEND=1
20092017
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
2018+
- PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
20102019
- pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not show_version and not server_load"
2011-
- pytest -v -s entrypoints/serve/disagg/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
2020+
- pytest -v -s entrypoints/scale_out/token_in_token_out/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
20122021
- pytest -v -s entrypoints/serve/instrumentator/test_metrics.py -k "text and not show and not run_batch and not test_metrics_counts and not test_metrics_exist"
2022+
- pytest -v -s entrypoints/serve/tokenize/test_tokenization.py -k "not tokenizer_info and not test_tokenize_chat"
20132023

20142024
- label: Rust Frontend Core Correctness # TBD
20152025
timeout_in_minutes: 180

.buildkite/test_areas/rust_frontend.yaml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,26 @@ steps:
1515
- tests/utils.py
1616
- tests/benchmarks/test_serve_cli.py
1717
- tests/entrypoints/openai/chat_completion/test_chat_completion.py
18-
# - tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py
18+
- tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py
1919

2020
# - tests/entrypoints/openai/completion/test_prompt_validation.py
2121
- tests/entrypoints/openai/completion/test_shutdown.py
22-
# - tests/entrypoints/openai/test_return_token_ids.py
23-
# - tests/entrypoints/openai/test_uds.py
22+
- tests/entrypoints/openai/test_return_token_ids.py
23+
- tests/entrypoints/openai/test_uds.py
2424
- tests/v1/sample/test_logprobs_e2e.py
2525
commands:
2626
- export VLLM_USE_RUST_FRONTEND=1
2727
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
2828
- pytest -v -s benchmarks/test_serve_cli.py -k "not insecure and not (test_bench_serve and not test_bench_serve_chat)"
2929
- pytest -v -s entrypoints/openai/chat_completion/test_chat_completion.py -k "not test_invalid_json_schema and not test_invalid_regex"
30-
# - pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not invalid"
30+
- pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not multiple"
3131

3232
# - pytest -v -s entrypoints/openai/completion/test_prompt_validation.py -k "not prompt_embeds"
3333
- pytest -v -s entrypoints/openai/completion/test_shutdown.py -k "not engine_failure and not test_abort_timeout_exits_quickly"
34-
# - pytest -v -s entrypoints/openai/test_return_token_ids.py
35-
# - pytest -v -s entrypoints/openai/test_uds.py
34+
# test_comparison streams differently: Rust emits a separate first (prompt_token_ids) chunk and
35+
# finish chunk without logprobs, while the test reads `logprobs.tokens` on every chunk.
36+
- pytest -v -s entrypoints/openai/test_return_token_ids.py -k "not test_comparison"
37+
- pytest -v -s entrypoints/openai/test_uds.py
3638
- pytest -v -s v1/sample/test_logprobs_e2e.py -k "test_prompt_logprobs_e2e_server"
3739

3840
- label: Rust Frontend Serve/Admin Coverage
@@ -45,19 +47,24 @@ steps:
4547
- vllm/entrypoints/serve/
4648
- vllm/v1/engine/
4749
- tests/utils.py
48-
# - tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
50+
- tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
4951
- tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
5052
- tests/entrypoints/serve/instrumentator/test_basic.py
5153
- tests/entrypoints/serve/instrumentator/test_metrics.py
5254
# - tests/entrypoints/serve/dev/test_sleep.py
55+
- tests/entrypoints/serve/tokenize/test_tokenization.py
5356
commands:
5457
- export VLLM_USE_RUST_FRONTEND=1
5558
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
56-
# - pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
59+
- PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
60+
# server_load can be flaky under the Rust frontend; keep it excluded for now.
5761
- pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not show_version and not server_load"
62+
# test_generate_logprobs expects Python-style top_logprobs truncation (dedup sampled + cap at max(k, 1)).
5863
- pytest -v -s entrypoints/scale_out/token_in_token_out/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
5964
- pytest -v -s entrypoints/serve/instrumentator/test_metrics.py -k "text and not show and not run_batch and not test_metrics_counts and not test_metrics_exist"
6065
# - pytest -v -s entrypoints/serve/dev/test_sleep.py
66+
# /tokenizer_info is not implemented in the Rust frontend (the CLI flag is accepted as a no-op).
67+
- pytest -v -s entrypoints/serve/tokenize/test_tokenization.py -k "not tokenizer_info and not test_tokenize_chat"
6168

6269
- label: Rust Frontend Core Correctness
6370
timeout_in_minutes: 30

rust/src/cmd/src/cli/unsupported.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,17 @@ pub struct ServerUnsupportedArgs {
456456

457457
/// Enable the `/tokenizer_info` endpoint. May expose chat
458458
/// templates and other tokenizer configuration.
459+
///
460+
/// Accepted as a no-op: the Rust frontend serves `/tokenize` and
461+
/// `/detokenize`, but does not implement `/tokenizer_info` yet.
459462
#[arg(
460463
long,
461464
visible_alias = "no-enable-tokenizer-info-endpoint",
462465
default_missing_value = "true",
463-
num_args = 0..=1
466+
num_args = 0..=1,
467+
hide = true
464468
)]
465-
pub enable_tokenizer_info_endpoint: Option<Unsupported>,
469+
pub enable_tokenizer_info_endpoint: Option<Noop>,
466470

467471
/// If set to True, log model outputs (generations).
468472
/// Requires `--enable-log-requests`. As with `--enable-log-requests`,

rust/src/server/src/routes/inference/generate/types.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ pub struct GenerateRequest {
2929
impl Normalizable for GenerateRequest {}
3030

3131
/// Mirrors the Python vLLM `GenerateResponseChoice` class.
32-
#[serde_with::skip_serializing_none]
32+
///
33+
/// Do not skip serializing `None` fields here: non-streaming response types
34+
/// should serialize `None` as explicit `null`.
3335
#[derive(Debug, Clone, Serialize)]
3436
pub(super) struct GenerateResponseChoice {
3537
pub index: u32,
@@ -58,7 +60,6 @@ pub(super) struct GenerateStreamResponse {
5860
}
5961

6062
/// Mirrors the Python vLLM `GenerateResponse` class.
61-
#[serde_with::skip_serializing_none]
6263
#[derive(Debug, Clone, Serialize)]
6364
pub(super) struct GenerateResponse {
6465
pub request_id: String,
@@ -68,7 +69,6 @@ pub(super) struct GenerateResponse {
6869
}
6970

7071
/// Mirrors the Python vLLM `Logprob` class used in prompt-logprobs payloads.
71-
#[serde_with::skip_serializing_none]
7272
#[derive(Debug, Clone, Serialize)]
7373
pub(super) struct GenerateLogprob {
7474
pub logprob: f32,

rust/src/server/src/routes/openai/chat_completions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async fn collect_chat_completion(
211211
Some(prefix) => Some(format!("{prefix}{}", message.text())),
212212
None => Some(message.text()).filter(|t| !t.is_empty()),
213213
},
214-
tool_calls: Some(tool_calls).filter(|calls| !calls.is_empty()),
214+
tool_calls,
215215
reasoning: if include_reasoning { reasoning } else { None },
216216
},
217217
logprobs,

rust/src/server/src/routes/openai/chat_completions/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ mod tests {
830830
let message = ChatCompletionMessage {
831831
role: AssistantRole,
832832
content: Some("answer".to_string()),
833-
tool_calls: None,
833+
tool_calls: Vec::new(),
834834
reasoning: Some("inner".to_string()),
835835
};
836836
let message_json = serde_json::to_value(message).expect("message serializes");

rust/src/server/src/routes/openai/chat_completions/types.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ impl Normalizable for ChatCompletionRequest {
331331
}
332332

333333
/// Mirrors the Python vLLM `ChatCompletionResponse` class.
334-
#[serde_with::skip_serializing_none]
334+
///
335+
/// Do not skip serializing `None` fields here: non-streaming response types
336+
/// should serialize `None` as explicit `null`.
335337
#[derive(Debug, Clone, Serialize)]
336338
pub(super) struct ChatCompletionResponse {
337339
pub id: String,
@@ -347,7 +349,6 @@ pub(super) struct ChatCompletionResponse {
347349
}
348350

349351
/// Mirrors the Python vLLM `ChatCompletionResponseChoice` class.
350-
#[serde_with::skip_serializing_none]
351352
#[derive(Debug, Clone, Serialize)]
352353
pub(super) struct ChatCompletionChoice {
353354
pub index: u32,
@@ -370,12 +371,12 @@ impl fmt::Display for AssistantRole {
370371
}
371372

372373
/// Mirrors the Python vLLM response `ChatMessage` class.
373-
#[serde_with::skip_serializing_none]
374374
#[derive(Debug, Clone, Serialize)]
375375
pub(super) struct ChatCompletionMessage {
376376
pub role: AssistantRole,
377377
pub content: Option<String>,
378-
pub tool_calls: Option<Vec<ToolCall>>,
378+
#[serde(skip_serializing_if = "Vec::is_empty")]
379+
pub tool_calls: Vec<ToolCall>,
379380
pub reasoning: Option<String>,
380381
}
381382

rust/src/server/src/routes/openai/completions/types.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ impl Normalizable for CompletionRequest {
196196
}
197197

198198
/// Mirrors the Python vLLM `CompletionResponse` class.
199-
#[serde_with::skip_serializing_none]
199+
///
200+
/// Do not skip serializing `None` fields here: non-streaming response types
201+
/// should serialize `None` as explicit `null`.
200202
#[derive(Debug, Clone, Serialize)]
201203
pub(super) struct CompletionResponse {
202204
pub id: String,
@@ -210,7 +212,6 @@ pub(super) struct CompletionResponse {
210212
}
211213

212214
/// Mirrors the Python vLLM `CompletionResponseChoice` class.
213-
#[serde_with::skip_serializing_none]
214215
#[derive(Debug, Clone, Serialize)]
215216
pub(super) struct CompletionChoice {
216217
pub index: u32,

rust/src/server/src/routes/openai/utils/types.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,9 @@ pub enum MessageContent {
311311
// ============================================================================
312312

313313
/// Mirrors the Python vLLM `UsageInfo` class.
314-
#[serde_with::skip_serializing_none]
314+
///
315+
/// Do not skip serializing `None` fields here: non-streaming response types
316+
/// should serialize `None` as explicit `null`.
315317
#[derive(Debug, Clone, Serialize)]
316318
pub struct Usage {
317319
pub prompt_tokens: usize,
@@ -402,14 +404,12 @@ pub struct LogProbs {
402404
}
403405

404406
/// Mirrors the Python vLLM `ChatCompletionLogProbs` class.
405-
#[serde_with::skip_serializing_none]
406407
#[derive(Debug, Clone, Serialize)]
407408
pub struct ChatLogProbs {
408409
pub content: Option<Vec<ChatLogProbsContent>>,
409410
}
410411

411412
/// Mirrors the Python vLLM `ChatCompletionLogProbsContent` class.
412-
#[serde_with::skip_serializing_none]
413413
#[derive(Debug, Clone, Serialize)]
414414
pub struct ChatLogProbsContent {
415415
pub token: String,
@@ -419,7 +419,6 @@ pub struct ChatLogProbsContent {
419419
}
420420

421421
/// Mirrors the Python vLLM `ChatCompletionLogProb` class.
422-
#[serde_with::skip_serializing_none]
423422
#[derive(Debug, Clone, Serialize)]
424423
pub struct TopLogProb {
425424
pub token: String,
@@ -436,7 +435,6 @@ pub struct ErrorResponse {
436435
pub error: ErrorDetail,
437436
}
438437

439-
#[serde_with::skip_serializing_none]
440438
#[derive(Debug, Clone, Deserialize, Serialize)]
441439
pub struct ErrorDetail {
442440
pub message: String,

rust/src/server/src/routes/tests.rs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,6 +2191,28 @@ async fn non_stream_chat_returns_json_response() {
21912191
assert_eq!(json["usage"]["prompt_tokens"], 22);
21922192
assert_eq!(json["usage"]["completion_tokens"], 3);
21932193
assert_eq!(json["usage"]["total_tokens"], 25);
2194+
2195+
// Unset optional fields are serialized as explicit `null` on
2196+
// non-streaming responses...
2197+
let response_object = json.as_object().expect("response object");
2198+
let choice = json["choices"][0].as_object().expect("choice object");
2199+
let message = choice["message"].as_object().expect("message object");
2200+
for (object, key) in [
2201+
(response_object, "system_fingerprint"),
2202+
(response_object, "prompt_token_ids"),
2203+
(response_object, "kv_transfer_params"),
2204+
(choice, "logprobs"),
2205+
(choice, "stop_reason"),
2206+
(choice, "token_ids"),
2207+
(message, "reasoning"),
2208+
] {
2209+
assert!(
2210+
object.contains_key(key) && object[key].is_null(),
2211+
"expected explicit null `{key}`: {json}"
2212+
);
2213+
}
2214+
// ...except `tool_calls`, which Python pops from the payload when empty.
2215+
assert!(!message.contains_key("tool_calls"), "{json}");
21942216
}
21952217

21962218
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2956,6 +2978,25 @@ async fn non_stream_completions_return_json_response() {
29562978
assert_eq!(json["choices"][0]["text"], "hi");
29572979
assert_eq!(json["choices"][0]["finish_reason"], "stop");
29582980
assert_eq!(json["usage"]["completion_tokens"], 3);
2981+
2982+
// Unset optional fields are serialized as explicit `null` on
2983+
// non-streaming responses.
2984+
let response_object = json.as_object().expect("response object");
2985+
let choice = json["choices"][0].as_object().expect("choice object");
2986+
for (object, key) in [
2987+
(response_object, "system_fingerprint"),
2988+
(response_object, "kv_transfer_params"),
2989+
(choice, "logprobs"),
2990+
(choice, "stop_reason"),
2991+
(choice, "prompt_logprobs"),
2992+
(choice, "token_ids"),
2993+
(choice, "prompt_token_ids"),
2994+
] {
2995+
assert!(
2996+
object.contains_key(key) && object[key].is_null(),
2997+
"expected explicit null `{key}`: {json}"
2998+
);
2999+
}
29593000
}
29603001

29613002
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4371,10 +4412,10 @@ async fn include_reasoning_false_suppresses_reasoning_in_non_stream_chat() {
43714412
let json: serde_json::Value = serde_json::from_str(&text).expect("decode json");
43724413

43734414
assert_eq!(json["choices"][0]["message"]["content"], "answer");
4415+
// Suppressed fields are serialized as explicit `null` on non-streaming
4416+
// responses.
43744417
assert!(
4375-
json["choices"][0]["message"]
4376-
.as_object()
4377-
.is_some_and(|message| !message.contains_key("reasoning")),
4418+
json["choices"][0]["message"]["reasoning"].is_null(),
43784419
"{text}"
43794420
);
43804421
}
@@ -4476,14 +4517,14 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
44764517
let choice = json["choices"][0].as_object().expect("choice object");
44774518

44784519
assert_eq!(json["choices"][0]["message"]["content"], "answer");
4520+
// Suppressed fields are serialized as explicit `null` on non-streaming
4521+
// responses.
44794522
assert!(
4480-
json["choices"][0]["message"]
4481-
.as_object()
4482-
.is_some_and(|message| !message.contains_key("reasoning")),
4523+
json["choices"][0]["message"]["reasoning"].is_null(),
44834524
"{text}"
44844525
);
4485-
assert!(!choice.contains_key("logprobs"), "{text}");
4486-
assert!(!choice.contains_key("token_ids"), "{text}");
4526+
assert!(choice["logprobs"].is_null(), "{text}");
4527+
assert!(choice["token_ids"].is_null(), "{text}");
44874528
assert!(json["prompt_token_ids"].is_array(), "{text}");
44884529
}
44894530

0 commit comments

Comments
 (0)