Compare commits

...
Author SHA1 Message Date
Bugen Zhao 2e7e11b6f8 sleep metrics
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
2026-07-06 13:09:56 +00:00
Bugen Zhao 6784cb3367 encode NULL in non-streaming response
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
2026-07-06 12:45:47 +00:00
Bugen Zhao ac3fb29d95 unblock some new e2e ci
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
2026-07-06 12:20:50 +00:00
20 changed files with 219 additions and 51 deletions
+21 -14
View File
@@ -15,24 +15,26 @@ steps:
- tests/utils.py
- tests/benchmarks/test_serve_cli.py
- tests/entrypoints/openai/chat_completion/test_chat_completion.py
# - tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py
- tests/entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py
# - tests/entrypoints/openai/completion/test_prompt_validation.py
- tests/entrypoints/openai/completion/test_shutdown.py
# - tests/entrypoints/openai/test_return_token_ids.py
# - tests/entrypoints/openai/test_uds.py
- tests/entrypoints/openai/test_return_token_ids.py
- tests/entrypoints/openai/test_uds.py
- tests/v1/sample/test_logprobs_e2e.py
commands:
- export VLLM_USE_RUST_FRONTEND=1
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -v -s benchmarks/test_serve_cli.py -k "not insecure and not (test_bench_serve and not test_bench_serve_chat)"
- pytest -v -s benchmarks/test_serve_cli.py -k "not insecure"
- pytest -v -s entrypoints/openai/chat_completion/test_chat_completion.py -k "not test_invalid_json_schema and not test_invalid_regex"
# - pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not invalid"
- pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not multiple"
# - pytest -v -s entrypoints/openai/completion/test_prompt_validation.py -k "not prompt_embeds"
- pytest -v -s entrypoints/openai/completion/test_shutdown.py -k "not engine_failure and not test_abort_timeout_exits_quickly"
# - pytest -v -s entrypoints/openai/test_return_token_ids.py
# - pytest -v -s entrypoints/openai/test_uds.py
# test_comparison streams differently: Rust emits a separate first (prompt_token_ids) chunk and
# finish chunk without logprobs, while the test reads `logprobs.tokens` on every chunk.
- pytest -v -s entrypoints/openai/test_return_token_ids.py -k "not test_comparison"
- pytest -v -s entrypoints/openai/test_uds.py
- pytest -v -s v1/sample/test_logprobs_e2e.py -k "test_prompt_logprobs_e2e_server"
- label: Rust Frontend Serve/Admin Coverage
@@ -45,19 +47,24 @@ steps:
- vllm/entrypoints/serve/
- vllm/v1/engine/
- tests/utils.py
# - tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
- tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
- tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
- tests/entrypoints/serve/instrumentator/test_basic.py
- tests/entrypoints/serve/instrumentator/test_metrics.py
# - tests/entrypoints/serve/dev/test_sleep.py
- tests/entrypoints/serve/dev/test_sleep.py
- tests/entrypoints/serve/tokenize/test_tokenization.py
commands:
- export VLLM_USE_RUST_FRONTEND=1
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
# - pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
- pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not show_version and not server_load"
- 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"
- pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
# server_load can be flaky under the Rust frontend; keep it excluded for now.
- pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not server_load"
# test_generate_logprobs expects Python-style top_logprobs truncation (dedup sampled + cap at max(k, 1)).
- pytest -v -s entrypoints/scale_out/token_in_token_out/test_serving_tokens.py -k "not lora and not test_generate_logprobs and not stop_string_workflow"
- 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"
# - pytest -v -s entrypoints/serve/dev/test_sleep.py
- pytest -v -s entrypoints/serve/dev/test_sleep.py
# /tokenizer_info is not implemented in the Rust frontend (the CLI flag is accepted as a no-op).
- pytest -v -s entrypoints/serve/tokenize/test_tokenization.py -k "not tokenizer_info"
- label: Rust Frontend Core Correctness
timeout_in_minutes: 30
@@ -85,7 +92,7 @@ steps:
commands:
- export VLLM_USE_RUST_FRONTEND=1
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -v -s tool_use --ignore=tool_use/mistral --models llama3.2 -k "not test_response_format_with_tool_choice_required and not test_parallel_tool_calls_false and not test_tool_call_and_choice"
- pytest -v -s tool_use --ignore=tool_use/mistral --models llama3.2
- label: Rust Frontend Distributed
timeout_in_minutes: 30
+1
View File
@@ -5268,6 +5268,7 @@ dependencies = [
"clap",
"educe",
"expect-test",
"form_urlencoded",
"futures",
"http-body",
"hyper",
+1
View File
@@ -39,6 +39,7 @@ educe = "0.6.0"
enum-as-inner = "0.7.0"
expect-test = "1.5.1"
fastokens = { version = "0.2.1", default-features = false }
form_urlencoded = "1.2.2"
futures = "0.3.31"
half = { version = "2.7.1", features = ["bytemuck"] }
hex = "0.4.3"
+6 -2
View File
@@ -456,13 +456,17 @@ pub struct ServerUnsupportedArgs {
/// Enable the `/tokenizer_info` endpoint. May expose chat
/// templates and other tokenizer configuration.
///
/// Accepted as a no-op: the Rust frontend serves `/tokenize` and
/// `/detokenize`, but does not implement `/tokenizer_info` yet.
#[arg(
long,
visible_alias = "no-enable-tokenizer-info-endpoint",
default_missing_value = "true",
num_args = 0..=1
num_args = 0..=1,
hide = true
)]
pub enable_tokenizer_info_endpoint: Option<Unsupported>,
pub enable_tokenizer_info_endpoint: Option<Noop>,
/// If set to True, log model outputs (generations).
/// Requires `--enable-log-requests`. As with `--enable-log-requests`,
+16
View File
@@ -373,6 +373,12 @@ impl EngineCoreClient {
}
/// Return the engine-side indices connected to this client.
///
/// # Panics
///
/// Panics if any connected engine uses an opaque identity that does not
/// encode an index. Use [`Self::known_engine_indices`] for a lossy,
/// non-panicking variant.
pub fn engine_indices(&self) -> Vec<u32> {
self.engines
.iter()
@@ -380,6 +386,16 @@ impl EngineCoreClient {
.collect()
}
/// Return the engine-side indices connected to this client, skipping
/// engines with opaque identities that do not encode an index (e.g. mock
/// engines in tests).
pub fn known_engine_indices(&self) -> Vec<u32> {
self.engines
.iter()
.filter_map(|engine| engine.engine_id.engine_index())
.collect()
}
/// Return the engine identities of all engines connected to this client.
pub fn engine_identities(&self) -> Vec<&[u8]> {
self.engines.iter().map(|engine| &*engine.engine_id).collect()
+24
View File
@@ -46,6 +46,15 @@ pub struct WaitingReasonLabels {
pub reason: &'static str,
}
/// Labels for `vllm:engine_sleep_state`.
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct SleepStateLabels {
pub model_name: String,
pub engine: u32,
/// One of `awake`, `weights_offloaded`, or `discard_all`.
pub sleep_state: &'static str,
}
/// Adapter names encoded as a deterministic comma-joined Prometheus label value.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct LoraAdapterNames(pub BTreeSet<String>);
@@ -155,6 +164,10 @@ pub struct SchedulerMetrics {
pub scheduler_waiting_by_reason: Family<WaitingReasonLabels, U64Gauge>,
pub kv_cache_usage: Family<EngineLabels, F64Gauge>,
/// `vllm:engine_sleep_state`, driven by the frontend `/sleep` and
/// `/wake_up` routes (mirroring Python `record_sleep_state`).
pub engine_sleep_state: Family<SleepStateLabels, U64Gauge>,
/// `vllm:lora_requests_info`. Value is the emit-time unix timestamp in
/// seconds.
pub lora_info: Family<LoraInfoLabels, F64Gauge>,
@@ -221,6 +234,16 @@ impl SchedulerMetrics {
kv_cache_usage.clone(),
);
let engine_sleep_state = Family::default();
registry.register(
"vllm:engine_sleep_state",
"Engine sleep state; awake = 0 means engine is sleeping; \
awake = 1 means engine is awake; \
weights_offloaded = 1 means sleep level 1; \
discard_all = 1 means sleep level 2.",
engine_sleep_state.clone(),
);
let lora_info = Family::default();
registry.register(
"vllm:lora_requests_info",
@@ -338,6 +361,7 @@ impl SchedulerMetrics {
scheduler_waiting,
scheduler_waiting_by_reason,
kv_cache_usage,
engine_sleep_state,
lora_info,
prefix_cache_queries,
prefix_cache_hits,
+1
View File
@@ -10,6 +10,7 @@ asynk-strim-attr.workspace = true
auto_enums.workspace = true
axum.workspace = true
educe.workspace = true
form_urlencoded.workspace = true
futures.workspace = true
http-body.workspace = true
hyper.workspace = true
+4
View File
@@ -70,6 +70,10 @@ fn build_router_with_options(
dev_mode_enabled: bool,
runtime_lora_updating_enabled: bool,
) -> Router {
// Default `vllm:engine_sleep_state` to awake, matching the init-time
// default recorded by the Python stat logger.
sleep::record_sleep_state(&state, None);
let mut router = Router::new()
// Health & monitoring
.route("/health", get(health::health))
@@ -29,7 +29,9 @@ pub struct GenerateRequest {
impl Normalizable for GenerateRequest {}
/// Mirrors the Python vLLM `GenerateResponseChoice` class.
#[serde_with::skip_serializing_none]
///
/// Do not skip serializing `None` fields here: non-streaming response types
/// should serialize `None` as explicit `null`.
#[derive(Debug, Clone, Serialize)]
pub(super) struct GenerateResponseChoice {
pub index: u32,
@@ -58,7 +60,6 @@ pub(super) struct GenerateStreamResponse {
}
/// Mirrors the Python vLLM `GenerateResponse` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub(super) struct GenerateResponse {
pub request_id: String,
@@ -68,7 +69,6 @@ pub(super) struct GenerateResponse {
}
/// Mirrors the Python vLLM `Logprob` class used in prompt-logprobs payloads.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub(super) struct GenerateLogprob {
pub logprob: f32,
@@ -211,7 +211,7 @@ async fn collect_chat_completion(
Some(prefix) => Some(format!("{prefix}{}", message.text())),
None => Some(message.text()).filter(|t| !t.is_empty()),
},
tool_calls: Some(tool_calls).filter(|calls| !calls.is_empty()),
tool_calls,
reasoning: if include_reasoning { reasoning } else { None },
},
logprobs,
@@ -830,7 +830,7 @@ mod tests {
let message = ChatCompletionMessage {
role: AssistantRole,
content: Some("answer".to_string()),
tool_calls: None,
tool_calls: Vec::new(),
reasoning: Some("inner".to_string()),
};
let message_json = serde_json::to_value(message).expect("message serializes");
@@ -331,7 +331,9 @@ impl Normalizable for ChatCompletionRequest {
}
/// Mirrors the Python vLLM `ChatCompletionResponse` class.
#[serde_with::skip_serializing_none]
///
/// Do not skip serializing `None` fields here: non-streaming response types
/// should serialize `None` as explicit `null`.
#[derive(Debug, Clone, Serialize)]
pub(super) struct ChatCompletionResponse {
pub id: String,
@@ -347,7 +349,6 @@ pub(super) struct ChatCompletionResponse {
}
/// Mirrors the Python vLLM `ChatCompletionResponseChoice` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub(super) struct ChatCompletionChoice {
pub index: u32,
@@ -370,12 +371,12 @@ impl fmt::Display for AssistantRole {
}
/// Mirrors the Python vLLM response `ChatMessage` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub(super) struct ChatCompletionMessage {
pub role: AssistantRole,
pub content: Option<String>,
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
pub reasoning: Option<String>,
}
@@ -196,7 +196,9 @@ impl Normalizable for CompletionRequest {
}
/// Mirrors the Python vLLM `CompletionResponse` class.
#[serde_with::skip_serializing_none]
///
/// Do not skip serializing `None` fields here: non-streaming response types
/// should serialize `None` as explicit `null`.
#[derive(Debug, Clone, Serialize)]
pub(super) struct CompletionResponse {
pub id: String,
@@ -210,7 +212,6 @@ pub(super) struct CompletionResponse {
}
/// Mirrors the Python vLLM `CompletionResponseChoice` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub(super) struct CompletionChoice {
pub index: u32,
@@ -311,7 +311,9 @@ pub enum MessageContent {
// ============================================================================
/// Mirrors the Python vLLM `UsageInfo` class.
#[serde_with::skip_serializing_none]
///
/// Do not skip serializing `None` fields here: non-streaming response types
/// should serialize `None` as explicit `null`.
#[derive(Debug, Clone, Serialize)]
pub struct Usage {
pub prompt_tokens: usize,
@@ -402,14 +404,12 @@ pub struct LogProbs {
}
/// Mirrors the Python vLLM `ChatCompletionLogProbs` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct ChatLogProbs {
pub content: Option<Vec<ChatLogProbsContent>>,
}
/// Mirrors the Python vLLM `ChatCompletionLogProbsContent` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct ChatLogProbsContent {
pub token: String,
@@ -419,7 +419,6 @@ pub struct ChatLogProbsContent {
}
/// Mirrors the Python vLLM `ChatCompletionLogProb` class.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct TopLogProb {
pub token: String,
@@ -436,7 +435,6 @@ pub struct ErrorResponse {
pub error: ErrorDetail,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorDetail {
pub message: String,
+73 -7
View File
@@ -2,10 +2,11 @@ use std::sync::Arc;
use axum::Json;
use axum::extract::rejection::QueryRejection;
use axum::extract::{Query, State};
use axum::extract::{Query, RawQuery, State};
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use vllm_engine_core_client::protocol::utility::PauseMode;
use vllm_metrics::{METRICS, SleepStateLabels};
use crate::error::ApiError;
use crate::state::AppState;
@@ -24,10 +25,18 @@ pub(crate) struct SleepParams {
mode: PauseMode,
}
#[derive(Debug, Default, Deserialize)]
pub(crate) struct WakeUpParams {
#[serde(default)]
tags: Option<Vec<String>>,
/// Parse repeated `tags` query parameters (`?tags=weights&tags=kv_cache`),
/// mirroring FastAPI's `tags: list[str] | None = Query(None)`. Plain
/// `axum::extract::Query` cannot deserialize repeated keys into a `Vec`, so
/// the raw query string is split by hand; unknown parameters are ignored like
/// in the Python frontend.
fn parse_wake_up_tags(query: Option<&str>) -> Option<Vec<String>> {
let query = query?;
let tags: Vec<String> = form_urlencoded::parse(query.as_bytes())
.filter(|(key, _)| key == "tags")
.map(|(_, value)| value.into_owned())
.collect();
(!tags.is_empty()).then_some(tags)
}
const fn default_sleep_level() -> u32 {
@@ -38,6 +47,36 @@ fn invalid_query(error: QueryRejection) -> ApiError {
ApiError::invalid_request(error.body_text(), Some("mode"))
}
/// Update the `vllm:engine_sleep_state` gauges, mirroring the Python
/// `PrometheusStatLogger.record_sleep_state` semantics: any successful sleep
/// records the level flags, and any successful wake-up (even a partial one
/// with tags) records the engine as awake.
pub(crate) fn record_sleep_state(state: &AppState, sleep_level: Option<u32>) {
let (awake, weights_offloaded, discard_all) = match sleep_level {
Some(level) => (0, u64::from(level == 1), u64::from(level == 2)),
None => (1, 0, 0),
};
let model_name = state.primary_model_name();
for engine in state.engine_core_client().known_engine_indices() {
for (sleep_state, value) in [
("awake", awake),
("weights_offloaded", weights_offloaded),
("discard_all", discard_all),
] {
METRICS
.scheduler
.engine_sleep_state
.get_or_create(&SleepStateLabels {
model_name: model_name.to_string(),
engine,
sleep_state,
})
.set(value);
}
}
}
/// Put the engine to sleep.
pub async fn sleep(
State(state): State<Arc<AppState>>,
@@ -51,20 +90,26 @@ pub async fn sleep(
.await
.map_err(|error| utility_call_error("sleep", error))?;
record_sleep_state(&state, Some(params.level));
Ok(StatusCode::OK)
}
/// Wake the engine from sleep mode.
pub async fn wake_up(
State(state): State<Arc<AppState>>,
Query(params): Query<WakeUpParams>,
RawQuery(query): RawQuery,
) -> Result<StatusCode, ApiError> {
let tags = parse_wake_up_tags(query.as_deref());
state
.engine_core_client()
.wake_up(params.tags)
.wake_up(tags)
.await
.map_err(|error| utility_call_error("wake_up", error))?;
record_sleep_state(&state, None);
Ok(StatusCode::OK)
}
@@ -80,3 +125,24 @@ pub async fn is_sleeping(
Ok(Json(IsSleepingResponse { is_sleeping }))
}
#[cfg(test)]
mod tests {
use super::parse_wake_up_tags;
#[test]
fn parse_wake_up_tags_handles_absent_single_and_repeated() {
assert_eq!(parse_wake_up_tags(None), None);
assert_eq!(parse_wake_up_tags(Some("")), None);
assert_eq!(
parse_wake_up_tags(Some("tags=weights")),
Some(vec!["weights".to_string()])
);
assert_eq!(
parse_wake_up_tags(Some("tags=weights&tags=kv_cache")),
Some(vec!["weights".to_string(), "kv_cache".to_string()])
);
// Unknown parameters are ignored, like in the Python frontend.
assert_eq!(parse_wake_up_tags(Some("other=1")), None);
}
}
+49 -8
View File
@@ -2191,6 +2191,28 @@ async fn non_stream_chat_returns_json_response() {
assert_eq!(json["usage"]["prompt_tokens"], 22);
assert_eq!(json["usage"]["completion_tokens"], 3);
assert_eq!(json["usage"]["total_tokens"], 25);
// Unset optional fields are serialized as explicit `null` on
// non-streaming responses...
let response_object = json.as_object().expect("response object");
let choice = json["choices"][0].as_object().expect("choice object");
let message = choice["message"].as_object().expect("message object");
for (object, key) in [
(response_object, "system_fingerprint"),
(response_object, "prompt_token_ids"),
(response_object, "kv_transfer_params"),
(choice, "logprobs"),
(choice, "stop_reason"),
(choice, "token_ids"),
(message, "reasoning"),
] {
assert!(
object.contains_key(key) && object[key].is_null(),
"expected explicit null `{key}`: {json}"
);
}
// ...except `tool_calls`, which Python pops from the payload when empty.
assert!(!message.contains_key("tool_calls"), "{json}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2956,6 +2978,25 @@ async fn non_stream_completions_return_json_response() {
assert_eq!(json["choices"][0]["text"], "hi");
assert_eq!(json["choices"][0]["finish_reason"], "stop");
assert_eq!(json["usage"]["completion_tokens"], 3);
// Unset optional fields are serialized as explicit `null` on
// non-streaming responses.
let response_object = json.as_object().expect("response object");
let choice = json["choices"][0].as_object().expect("choice object");
for (object, key) in [
(response_object, "system_fingerprint"),
(response_object, "kv_transfer_params"),
(choice, "logprobs"),
(choice, "stop_reason"),
(choice, "prompt_logprobs"),
(choice, "token_ids"),
(choice, "prompt_token_ids"),
] {
assert!(
object.contains_key(key) && object[key].is_null(),
"expected explicit null `{key}`: {json}"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4371,10 +4412,10 @@ async fn include_reasoning_false_suppresses_reasoning_in_non_stream_chat() {
let json: serde_json::Value = serde_json::from_str(&text).expect("decode json");
assert_eq!(json["choices"][0]["message"]["content"], "answer");
// Suppressed fields are serialized as explicit `null` on non-streaming
// responses.
assert!(
json["choices"][0]["message"]
.as_object()
.is_some_and(|message| !message.contains_key("reasoning")),
json["choices"][0]["message"]["reasoning"].is_null(),
"{text}"
);
}
@@ -4476,14 +4517,14 @@ async fn include_reasoning_false_suppresses_non_stream_output_metadata() {
let choice = json["choices"][0].as_object().expect("choice object");
assert_eq!(json["choices"][0]["message"]["content"], "answer");
// Suppressed fields are serialized as explicit `null` on non-streaming
// responses.
assert!(
json["choices"][0]["message"]
.as_object()
.is_some_and(|message| !message.contains_key("reasoning")),
json["choices"][0]["message"]["reasoning"].is_null(),
"{text}"
);
assert!(!choice.contains_key("logprobs"), "{text}");
assert!(!choice.contains_key("token_ids"), "{text}");
assert!(choice["logprobs"].is_null(), "{text}");
assert!(choice["token_ids"].is_null(), "{text}");
assert!(json["prompt_token_ids"].is_array(), "{text}");
}
+2 -1
View File
@@ -102,12 +102,13 @@ pub struct DetokenizeRequest {
pub tokens: Vec<u32>,
}
/// Do not skip serializing `None` fields here: non-streaming response types
/// should serialize `None` as explicit `null`.
#[derive(Debug, Clone, Serialize)]
pub struct TokenizeResponse {
pub count: usize,
pub max_model_len: u32,
pub tokens: Vec<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_strs: Option<Vec<String>>,
}
+1 -1
View File
@@ -10,7 +10,7 @@ pub enum TokenIdsError {
#[error("allowed_token_ids should not be empty")]
EmptyAllowedTokenIds,
#[error(
"token_id(s) {token_ids:?} in {parameter} contain out-of-vocab token ids. \
"token_id(s) {token_ids:?} in {parameter} are out of vocabulary. \
Vocabulary size: {vocab_size}"
)]
OutOfVocab {
+2 -1
View File
@@ -40,4 +40,5 @@ async def test_show_version(server: RemoteOpenAIServer):
response = client.get(server.url_for("version"))
response.raise_for_status()
assert response.json() == {"version": VLLM_VERSION}
# Tolerate additive fields (e.g. the Rust frontend reports its own version).
assert response.json()["version"] == VLLM_VERSION
@@ -83,7 +83,8 @@ async def test_show_version(server: RemoteOpenAIServer):
response = requests.get(server.url_for("version"))
response.raise_for_status()
assert response.json() == {"version": VLLM_VERSION}
# Tolerate additive fields (e.g. the Rust frontend reports its own version).
assert response.json()["version"] == VLLM_VERSION
@pytest.mark.asyncio