[Rust Frontend] Coerce completion `max_tokens: null` to default (#47166)

Signed-off-by: Blas Rodriguez Irizar <rodrigblas@gmail.com>

Blas Rodriguez Irizar committed Jul 1, 2026 at 07:41 UTC 5b431b905c7aac89c47fde879efedc6d67615faf
2 files changed +34 -1
rust/src/server/src/routes/openai/completions/convert.rs
+23
@@ -199,6 +199,7 @@ mod tests {
199 use super::prepare_completion_request;
200 use crate::lora::LoraModelResolution;
201 use crate::routes::openai::completions::types::CompletionRequest;
202 + use crate::routes::openai::utils::types::Normalizable;
203 use crate::utils::{ResolvedRequestContext, resolve_request_context};
204
205 fn request_context(headers: &HeaderMap, request_id: Option<&str>) -> ResolvedRequestContext {
@@ -249,6 +250,28 @@ mod tests {
250 assert!(request.ignore_eos);
251 }
252
253 + #[test]
254 + fn normalize_coerces_null_max_tokens_to_default() {
255 + // An absent `max_tokens` already gets the serde default.
256 + let absent: CompletionRequest =
257 + serde_json::from_value(base_request_json()).expect("parse request");
258 + assert_eq!(absent.max_tokens, Some(16));
259 +
260 + // An explicit `null` deserializes to `None`, bypassing the default;
261 + // `normalize` must coerce it back to match Python vLLM.
262 + let mut request: CompletionRequest = serde_json::from_value(json!({
263 + "model": "Qwen/Qwen1.5-0.5B-Chat",
264 + "prompt": "hello",
265 + "stream": true,
266 + "max_tokens": null
267 + }))
268 + .expect("parse request");
269 + assert_eq!(request.max_tokens, None);
270 +
271 + request.normalize();
272 + assert_eq!(request.max_tokens, Some(16));
273 + }
274 +
275 #[test]
276 fn prepare_completion_request_maps_sampling_fields() {
277 let request: CompletionRequest = serde_json::from_value(json!({
rust/src/server/src/routes/openai/completions/types.rs
+11 -1
@@ -179,7 +179,17 @@ pub struct CompletionRequest {
179 pub other: Map<String, Value>,
180 }
181
182 -impl Normalizable for CompletionRequest {}
182 +impl Normalizable for CompletionRequest {
183 + /// Normalize the request by applying defaults.
184 + fn normalize(&mut self) {
185 + // An explicit `"max_tokens": null` deserializes to `None`, bypassing the
186 + // serde field default. Coerce it back to the default so it behaves like
187 + // an absent field, matching Python vLLM's `normalize_null_max_tokens`.
188 + if self.max_tokens.is_none() {
189 + self.max_tokens = default_completion_max_tokens();
190 + }
191 + }
192 +}
193
194 /// Mirrors the Python vLLM `CompletionResponse` class.
195 #[serde_with::skip_serializing_none]