forked from Karylab-cklius/vllm
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7e11b6f8 | ||
|
|
6784cb3367 | ||
|
|
ac3fb29d95 |
@@ -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
|
||||
|
||||
Generated
+1
@@ -5268,6 +5268,7 @@ dependencies = [
|
||||
"clap",
|
||||
"educe",
|
||||
"expect-test",
|
||||
"form_urlencoded",
|
||||
"futures",
|
||||
"http-body",
|
||||
"hyper",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user