From c17e2f7c84d28dfcf5e8cfcc3c5c10bd3caad8b5 Mon Sep 17 00:00:00 2001 From: Reid <61492567+reidliu41@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:05:10 +0800 Subject: [PATCH] [Bugfix][Rust Frontend] Make metrics respect --served-model-name (#45465) Signed-off-by: reidliu41 --- rust/src/server/src/lib.rs | 48 ++++++++++++++---- rust/src/server/src/routes/tests.rs | 79 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/rust/src/server/src/lib.rs b/rust/src/server/src/lib.rs index e1257e7f636..8cbb3e4d9fb 100644 --- a/rust/src/server/src/lib.rs +++ b/rust/src/server/src/lib.rs @@ -35,9 +35,24 @@ use crate::routes::build_router; use crate::server_info::ServerInfoSnapshot; use crate::state::AppState; +/// Resolve the public model names accepted by the frontend. +fn effective_served_model_names(model: &str, served_model_name: &[String]) -> Vec { + if served_model_name.is_empty() { + vec![model.to_string()] + } else { + served_model_name.to_vec() + } +} + /// Build the shared application state for one configured model and one engine /// client. async fn build_state(config: &Config) -> Result> { + // If no served names are specified, fall back to the backend model path so + // that the API always has at least one valid model ID. Use the same primary + // public name for frontend-side metrics labels. + let served_model_names = effective_served_model_names(&config.model, &config.served_model_name); + let metrics_model_name = served_model_names[0].clone(); + // Load both backends from the same model metadata so they stay in sync. let loaded = load_model_backends( &config.model, @@ -68,7 +83,7 @@ async fn build_state(config: &Config) -> Result> { let client = EngineCoreClient::connect(EngineCoreClientConfig { transport_mode: config.transport_mode.clone(), coordinator_mode, - model_name: config.model.clone(), + model_name: metrics_model_name, client_index: 0, }) .await @@ -81,14 +96,6 @@ async fn build_state(config: &Config) -> Result> { .with_tool_call_parser(config.tool_call_parser.clone()) .with_reasoning_parser(config.reasoning_parser.clone()); - // If no served names are specified, fall back to the backend model path so - // that the API always has at least one valid model ID. - let served_model_names = if config.served_model_name.is_empty() { - vec![config.model.clone()] - } else { - config.served_model_name.clone() - }; - Ok(Arc::new( AppState::new(served_model_names, chat) .with_api_server_options(config.api_server_options) @@ -258,3 +265,26 @@ where .unwrap_or_else(|| Instant::now() + config.shutdown_timeout); state.shutdown(shutdown_deadline).await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn effective_served_model_names_falls_back_to_backend_model() { + assert_eq!( + effective_served_model_names("backend-model", &[]), + vec!["backend-model"] + ); + } + + #[test] + fn effective_served_model_names_preserves_public_names() { + let served_names = vec!["public-model".to_string(), "public-alias".to_string()]; + + assert_eq!( + effective_served_model_names("backend-model", &served_names), + served_names + ); + } +} diff --git a/rust/src/server/src/routes/tests.rs b/rust/src/server/src/routes/tests.rs index c6de4034026..9d351277f1e 100644 --- a/rust/src/server/src/routes/tests.rs +++ b/rust/src/server/src/routes/tests.rs @@ -1677,6 +1677,85 @@ async fn http_metrics_record_list_models_requests() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial] +async fn request_metrics_use_served_model_name_label() { + let ipc = IpcNamespace::new().expect("create ipc namespace"); + let handshake_address = ipc.handshake_endpoint(); + let engine_id = b"engine-openai-served-model-metrics".to_vec(); + + let engine_task = MockEngineTask::new(spawn_mock_engine_task( + handshake_address.clone(), + engine_id.clone(), + |dealer, push| { + boxed_test_future(async move { + let add = recv_engine_message(dealer).await; + let request: EngineCoreRequest = + rmp_serde::from_slice(&add[1]).expect("decode request"); + send_outputs( + push, + engine_outputs_for_request(&request.request_id, default_stream_output_specs()), + ) + .await; + }) + }, + )); + + let client = EngineCoreClient::connect( + EngineCoreClientConfig::new_single(handshake_address) + .with_model_name("served-model-metrics") + .with_local_input_output_addresses( + Some(ipc.input_endpoint()), + Some(ipc.output_endpoint()), + ), + ) + .await + .expect("connect client"); + let chat = ChatLlm::from_shared_backend(test_llm(client), Arc::new(FakeChatBackend::new())); + let mut app = build_router(Arc::new(AppState::new( + vec![ + "served-model-metrics".to_string(), + "served-model-alias".to_string(), + ], + chat, + ))); + let before = METRICS.render().unwrap(); + + let response = app + .call( + Request::builder() + .method("POST") + .uri("/v1/chat/completions") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "model": "served-model-alias", + "stream": false, + "messages": [{"role": "user", "content": "hello"}] + }) + .to_string(), + )) + .expect("build request"), + ) + .await + .expect("call app"); + + assert_eq!(response.status(), StatusCode::OK); + let _ = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + + let after = METRICS.render().unwrap(); + assert_eq!( + metric_delta( + &before, + &after, + "vllm:request_success_total", + Some("model_name=\"served-model-metrics\",engine=\"0\",finished_reason=\"stop\""), + ), + 1.0 + ); + engine_task.await.expect("mock engine task"); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial] async fn wrong_model_returns_not_found() {