1#![cfg(feature = "vllm")]
84
85use serde::{Deserialize, Serialize};
86
87#[derive(Serialize, Deserialize, Debug, Clone, Default)]
94pub struct SamplingParams {
95 #[serde(skip_serializing_if = "Option::is_none")]
98 pub min_p: Option<f32>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
104 pub repetition_penalty: Option<f32>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
109 pub stop_token_ids: Option<Vec<u32>>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
114 pub ignore_eos: Option<bool>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
119 pub min_tokens: Option<u32>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub skip_special_tokens: Option<bool>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
128 pub spaces_between_special_tokens: Option<bool>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
133 pub include_stop_str_in_output: Option<bool>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub truncate_prompt_tokens: Option<i64>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub truncation_side: Option<TruncationSide>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
148 pub prompt_logprobs: Option<u32>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
153 pub logprob_token_ids: Option<Vec<u32>>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub allowed_token_ids: Option<Vec<u32>>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
162 pub bad_words: Option<Vec<String>>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
167 pub length_penalty: Option<f32>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
172 pub use_beam_search: Option<bool>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
177 pub watermarking: Option<bool>,
178}
179
180#[derive(Serialize, Deserialize, Debug, Clone, Default)]
186pub struct ChatParams {
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub echo: Option<bool>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
194 pub add_generation_prompt: Option<bool>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
200 pub continue_final_message: Option<bool>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
205 pub add_special_tokens: Option<bool>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
210 pub chat_template: Option<String>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
217 pub chat_template_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
222 pub documents: Option<Vec<std::collections::HashMap<String, String>>>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
227 pub mm_processor_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub media_io_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
237 pub structured_outputs: Option<StructuredOutputsParams>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
243 pub priority: Option<i64>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
248 pub session_id: Option<String>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
253 pub cache_salt: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
258 pub stream_interval: Option<u32>,
259
260 #[serde(skip_serializing_if = "Option::is_none")]
263 pub kv_transfer_params: Option<serde_json::Map<String, serde_json::Value>>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
268 pub ec_transfer_params: Option<serde_json::Map<String, serde_json::Value>>,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
273 pub return_tokens_as_token_ids: Option<bool>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub return_token_ids: Option<bool>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
282 pub return_token_offsets: Option<bool>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub return_prompt_text: Option<bool>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub repetition_detection: Option<RepetitionDetectionParams>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
295 pub vllm_xargs: Option<serde_json::Map<String, serde_json::Value>>,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
300 pub routed_experts_prompt_start: Option<u32>,
301}
302
303#[derive(Serialize, Deserialize, Debug, Clone, Default)]
315pub struct StructuredOutputsParams {
316 #[serde(skip_serializing_if = "Option::is_none")]
319 pub json: Option<serde_json::Value>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub regex: Option<String>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub choice: Option<Vec<String>>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub grammar: Option<String>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
336 pub json_object: Option<bool>,
337
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub structural_tag: Option<String>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub disable_any_whitespace: Option<bool>,
345
346 #[serde(skip_serializing_if = "Option::is_none")]
349 pub disable_additional_properties: Option<bool>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub whitespace_pattern: Option<String>,
354}
355
356#[derive(Serialize, Deserialize, Debug, Clone, Default)]
362pub struct RepetitionDetectionParams {
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub max_pattern_size: Option<u32>,
366
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub min_pattern_size: Option<u32>,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub min_count: Option<u32>,
374}
375
376crate::wire_string_enum! {
377 pub enum TruncationSide {
379 Left => "left",
381 Right => "right",
383 }
384}
385
386#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
393pub struct Logprob {
394 pub logprob: f64,
396 pub rank: Option<u32>,
399 pub decoded_token: Option<String>,
401}
402
403#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
409#[serde(untagged)]
410pub enum StopReason {
411 Text(String),
413 TokenId(u32),
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::chat::create::request::{Message, RequestBody};
422
423 #[test]
426 fn sampling_params_flatten_to_top_level() {
427 let request = RequestBody {
428 messages: vec![Message::user("Hello")],
429 model: "Qwen/Qwen3-8B".to_string(),
430 vllm_sampling: Some(SamplingParams {
431 min_p: Some(0.1),
432 repetition_penalty: Some(1.05),
433 stop_token_ids: Some(vec![151645, 151643]),
434 ignore_eos: Some(false),
435 min_tokens: Some(16),
436 truncate_prompt_tokens: Some(-1),
437 truncation_side: Some(TruncationSide::Left),
438 prompt_logprobs: Some(3),
439 allowed_token_ids: Some(vec![1, 2, 3]),
440 bad_words: Some(vec!["<|im_start|>".to_string()]),
441 ..Default::default()
442 }),
443 ..Default::default()
444 };
445
446 let json = serde_json::to_value(&request).unwrap();
447 assert_eq!(json["min_p"], serde_json::json!(0.1f32));
448 assert_eq!(json["repetition_penalty"], serde_json::json!(1.05f32));
449 assert_eq!(json["stop_token_ids"], serde_json::json!([151645, 151643]));
450 assert_eq!(json["ignore_eos"], false);
451 assert_eq!(json["min_tokens"], 16);
452 assert_eq!(json["truncate_prompt_tokens"], -1);
453 assert_eq!(json["truncation_side"], "left");
454 assert_eq!(json["prompt_logprobs"], 3);
455 assert_eq!(json["bad_words"], serde_json::json!(["<|im_start|>"]));
456 assert!(json.get("skip_special_tokens").is_none());
458 assert!(json.get("watermarking").is_none());
459 }
460
461 #[test]
464 fn chat_params_flatten_to_top_level() {
465 let request = RequestBody {
466 messages: vec![Message::user("Hello")],
467 model: "Qwen/Qwen3-8B".to_string(),
468 vllm_chat: Some(ChatParams {
469 chat_template_kwargs: Some(
470 serde_json::from_value(serde_json::json!({"enable_thinking": false})).unwrap(),
471 ),
472 structured_outputs: Some(StructuredOutputsParams {
473 choice: Some(vec!["positive".to_string(), "negative".to_string()]),
474 disable_any_whitespace: Some(true),
475 ..Default::default()
476 }),
477 priority: Some(10),
478 kv_transfer_params: Some(
479 serde_json::from_value(serde_json::json!({"do_remote_decode": true})).unwrap(),
480 ),
481 ..Default::default()
482 }),
483 ..Default::default()
484 };
485
486 let json = serde_json::to_value(&request).unwrap();
487 assert_eq!(json["chat_template_kwargs"]["enable_thinking"], false);
488 assert_eq!(
489 json["structured_outputs"],
490 serde_json::json!({"choice": ["positive", "negative"], "disable_any_whitespace": true})
491 );
492 assert_eq!(json["priority"], 10);
493 assert_eq!(json["kv_transfer_params"]["do_remote_decode"], true);
494 assert!(json.get("echo").is_none());
495 }
496
497 #[test]
499 fn structured_outputs_json_accepts_object_and_string() {
500 let inline = StructuredOutputsParams {
501 json: Some(serde_json::json!({"type": "object"})),
502 ..Default::default()
503 };
504 assert_eq!(
505 serde_json::to_value(&inline).unwrap()["json"],
506 serde_json::json!({"type": "object"})
507 );
508
509 let as_string = StructuredOutputsParams {
510 json: Some(serde_json::json!(r#"{"type":"object"}"#)),
511 ..Default::default()
512 };
513 assert_eq!(
514 serde_json::to_value(&as_string).unwrap()["json"],
515 serde_json::json!(r#"{"type":"object"}"#)
516 );
517 }
518
519 #[test]
524 fn vllm_body_round_trips_without_duplicating_keys() {
525 let json = r#"{
526 "model": "Qwen/Qwen3-8B",
527 "messages": [{"role": "user", "content": "Hello"}],
528 "min_p": 0.1,
529 "prompt_logprobs": 2,
530 "chat_template_kwargs": {"enable_thinking": true},
531 "structured_outputs": {"regex": "[a-z]+"},
532 "some_future_vllm_field": 42
533 }"#;
534
535 let parsed: RequestBody = serde_json::from_str(json).unwrap();
536
537 let sampling = parsed.vllm_sampling.as_ref().expect("vllm_sampling");
538 assert_eq!(sampling.min_p, Some(0.1));
539 assert_eq!(sampling.prompt_logprobs, Some(2));
540
541 let chat = parsed.vllm_chat.as_ref().expect("vllm_chat");
542 assert_eq!(
543 chat.chat_template_kwargs
544 .as_ref()
545 .expect("chat_template_kwargs")["enable_thinking"],
546 serde_json::json!(true)
547 );
548 assert_eq!(
549 chat.structured_outputs
550 .as_ref()
551 .expect("structured_outputs")
552 .regex
553 .as_deref(),
554 Some("[a-z]+")
555 );
556
557 let extra = parsed.extra_body_map.as_ref().expect("extra_body_map");
561 assert_eq!(extra.len(), 1, "extra_body_map: {extra:?}");
562 assert_eq!(extra["some_future_vllm_field"], 42);
563
564 let reserialized = serde_json::to_string(&parsed).unwrap();
565 let reparsed: serde_json::Value = serde_json::from_str(&reserialized).unwrap();
566 assert_eq!(reparsed["min_p"], 0.1);
567 assert_eq!(reparsed["prompt_logprobs"], 2);
568 assert_eq!(reparsed["structured_outputs"]["regex"], "[a-z]+");
569 assert_eq!(reparsed["some_future_vllm_field"], 42);
570 assert_eq!(
573 reserialized.matches(r#""min_p""#).count(),
574 1,
575 "duplicated key in {reserialized}"
576 );
577 assert_eq!(
578 reserialized.matches(r#""structured_outputs""#).count(),
579 1,
580 "duplicated key in {reserialized}"
581 );
582 }
583
584 #[test]
587 fn unset_vllm_params_add_no_keys() {
588 let request = RequestBody {
589 messages: vec![Message::user("Hello")],
590 model: "gpt-4.1".to_string(),
591 vllm_sampling: Some(SamplingParams::default()),
592 vllm_chat: Some(ChatParams::default()),
593 ..Default::default()
594 };
595
596 let json = serde_json::to_string(&request).unwrap();
597 assert_eq!(
598 json,
599 r#"{"messages":[{"role":"user","content":"Hello"}],"model":"gpt-4.1"}"#
600 );
601 }
602
603 #[test]
606 fn completions_body_takes_sampling_params_only() {
607 let request = crate::completions::request::CompletionRequest {
608 model: "Qwen/Qwen3-8B".to_string(),
609 prompt: crate::completions::request::Prompt::PromptString("Hello".to_string()),
610 vllm_sampling: Some(SamplingParams {
611 min_p: Some(0.1),
612 skip_special_tokens: Some(false),
613 ..Default::default()
614 }),
615 ..Default::default()
616 };
617
618 let json = serde_json::to_value(&request).unwrap();
619 assert_eq!(json["min_p"], serde_json::json!(0.1f32));
620 assert_eq!(json["skip_special_tokens"], false);
621 }
622
623 #[test]
627 fn non_streaming_response_extras_parse() {
628 use crate::chat::ChatCompletion;
629
630 let json = r#"{
631 "id": "chatcmpl-abc123",
632 "object": "chat.completion",
633 "created": 1735113344,
634 "model": "Qwen/Qwen3-8B",
635 "choices": [{
636 "index": 0,
637 "message": {"role": "assistant", "content": "Hi"},
638 "logprobs": null,
639 "finish_reason": "stop",
640 "stop_reason": "<|im_end|>",
641 "token_ids": [9707, 151645]
642 }],
643 "usage": {"prompt_tokens": 4, "completion_tokens": 2, "total_tokens": 6},
644 "prompt_logprobs": [
645 null,
646 {"9707": {"logprob": -0.001, "rank": 1, "decoded_token": "Hello"}}
647 ],
648 "prompt_token_ids": [151644, 8948, 198, 9707],
649 "prompt_text": "<|im_start|>user\nHello<|im_end|>\n",
650 "kv_transfer_params": {"do_remote_decode": true}
651 }"#;
652
653 let parsed: ChatCompletion = json.parse().expect("vLLM response must parse");
654
655 assert_eq!(
656 parsed.prompt_token_ids.unwrap(),
657 vec![151644, 8948, 198, 9707]
658 );
659 assert!(parsed.prompt_text.unwrap().starts_with("<|im_start|>"));
660 assert_eq!(
661 parsed.kv_transfer_params.unwrap()["do_remote_decode"],
662 serde_json::json!(true)
663 );
664
665 let prompt_logprobs = parsed.prompt_logprobs.expect("prompt_logprobs");
667 assert_eq!(prompt_logprobs.len(), 2);
668 assert!(prompt_logprobs[0].is_none());
669 let entry = &prompt_logprobs[1].as_ref().expect("position 1")[&9707];
670 assert_eq!(entry.rank, Some(1));
671 assert_eq!(entry.decoded_token.as_deref(), Some("Hello"));
672 assert!((entry.logprob - -0.001).abs() < 1e-9);
673
674 let choice = &parsed.choices[0];
675 assert_eq!(choice.token_ids.clone().unwrap(), vec![9707, 151645]);
676 assert_eq!(
677 choice.stop_reason,
678 Some(StopReason::Text("<|im_end|>".to_string()))
679 );
680 assert_eq!(choice.finish_reason.as_str(), "stop");
681 }
682
683 #[test]
686 fn stop_reason_accepts_token_id_and_string() {
687 use crate::chat::create::response::streaming::ChatCompletionChunk;
688
689 let by_id: ChatCompletionChunk = r#"{
690 "id": "chatcmpl-abc123", "object": "chat.completion.chunk",
691 "created": 1735113344, "model": "Qwen/Qwen3-8B",
692 "prompt_token_ids": [151644, 9707],
693 "choices": [{
694 "index": 0, "delta": {"role": "assistant", "content": "Hi"},
695 "logprobs": null, "finish_reason": "stop",
696 "stop_reason": 151645, "token_ids": [9707]
697 }]
698 }"#
699 .parse()
700 .expect("chunk with an integer stop_reason must parse");
701
702 assert_eq!(
703 by_id.choices[0].stop_reason,
704 Some(StopReason::TokenId(151645))
705 );
706 assert_eq!(by_id.choices[0].token_ids.clone().unwrap(), vec![9707]);
707 assert_eq!(by_id.prompt_token_ids.clone().unwrap(), vec![151644, 9707]);
708 assert!(by_id.prompt_text.is_none());
709
710 let by_text: ChatCompletionChunk = r#"{
711 "id": "chatcmpl-abc123", "object": "chat.completion.chunk",
712 "created": 1735113344, "model": "Qwen/Qwen3-8B",
713 "choices": [{
714 "index": 0, "delta": {"content": "Hi"},
715 "finish_reason": "stop", "stop_reason": "<|im_end|>"
716 }]
717 }"#
718 .parse()
719 .expect("chunk with a string stop_reason must parse");
720
721 assert_eq!(
722 by_text.choices[0].stop_reason,
723 Some(StopReason::Text("<|im_end|>".to_string()))
724 );
725 assert!(by_text.prompt_token_ids.is_none());
726 }
727
728 #[test]
731 fn plain_openai_response_leaves_vllm_fields_none() {
732 use crate::chat::ChatCompletion;
733
734 let parsed: ChatCompletion = r#"{
735 "id": "chatcmpl-1", "object": "chat.completion", "created": 1,
736 "model": "gpt-4.1",
737 "choices": [{
738 "index": 0,
739 "message": {"role": "assistant", "content": "Hi"},
740 "finish_reason": "stop"
741 }],
742 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
743 }"#
744 .parse()
745 .expect("plain OpenAI response must parse with the vllm feature on");
746
747 assert!(parsed.prompt_logprobs.is_none());
748 assert!(parsed.prompt_token_ids.is_none());
749 assert!(parsed.prompt_text.is_none());
750 assert!(parsed.kv_transfer_params.is_none());
751 assert!(parsed.ec_transfer_params.is_none());
752 assert!(parsed.choices[0].stop_reason.is_none());
753 assert!(parsed.choices[0].token_ids.is_none());
754 assert!(parsed.choices[0].routed_experts.is_none());
755 }
756
757 #[test]
761 fn reasoning_streams_under_vllm_key_and_accumulates() {
762 use crate::chat::create::accumulator::ChatCompletionAccumulator;
763 use crate::chat::create::response::streaming::ChatCompletionChunk;
764
765 let chunks = [
766 r#"{"id":"c","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","reasoning":"Let me "},"finish_reason":null}]}"#,
767 r#"{"id":"c","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"reasoning":"think."},"finish_reason":null}]}"#,
768 r#"{"id":"c","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":"stop"}]}"#,
769 ];
770
771 let mut accumulator = ChatCompletionAccumulator::new();
772 for chunk in chunks {
773 let parsed: ChatCompletionChunk = chunk.parse().expect("chunk must parse");
774 accumulator.push(&parsed);
775 }
776
777 assert_eq!(accumulator.reasoning(), "Let me think.");
778 assert_eq!(accumulator.reasoning_content(), "");
780 assert_eq!(accumulator.content(), "Hi");
781
782 let message = accumulator.into_message();
783 assert_eq!(message.reasoning.as_deref(), Some("Let me think."));
784 assert_eq!(message.reasoning_content, None);
785 assert_eq!(message.content.as_deref(), Some("Hi"));
786 }
787
788 #[test]
792 fn usage_extras_parse() {
793 use crate::chat::ChatCompletion;
794
795 let parsed: ChatCompletion = r#"{
796 "id": "chatcmpl-1", "object": "chat.completion", "created": 1,
797 "model": "Qwen/Qwen3-8B",
798 "choices": [{
799 "index": 0,
800 "message": {"role": "assistant", "content": "Hi"},
801 "finish_reason": "stop"
802 }],
803 "usage": {
804 "prompt_tokens": 100, "completion_tokens": 20, "total_tokens": 120,
805 "prompt_tokens_details": {"cached_tokens": 64},
806 "completion_tokens_details": {
807 "reasoning_tokens": null,
808 "num_speculative_tokens": 8
809 }
810 }
811 }"#
812 .parse()
813 .expect("vLLM usage must parse");
814
815 let usage = parsed.usage.expect("usage");
816 assert_eq!(
817 usage
818 .completion_tokens_details
819 .as_ref()
820 .expect("completion_tokens_details")
821 .num_speculative_tokens,
822 Some(8)
823 );
824 assert_eq!(
825 usage
826 .prompt_tokens_details
827 .as_ref()
828 .expect("prompt_tokens_details")
829 .cached_tokens,
830 Some(64)
831 );
832 }
833
834 #[test]
837 fn model_card_extras_parse() {
838 let models: crate::models::list::response::ListModelsResponse = r#"{
839 "object": "list",
840 "data": [{
841 "id": "Qwen/Qwen3-8B",
842 "object": "model",
843 "created": 1735113344,
844 "owned_by": "vllm",
845 "root": "Qwen/Qwen3-8B",
846 "parent": null,
847 "max_model_len": 131072
848 }]
849 }"#
850 .parse()
851 .expect("vLLM model list must parse");
852
853 let model = &models.data[0];
854 assert_eq!(model.root.as_deref(), Some("Qwen/Qwen3-8B"));
855 assert_eq!(model.parent, None);
856 assert_eq!(model.max_model_len, Some(131072));
857 assert_eq!(model.owned_by, "vllm");
858 }
859}