1use std::ffi::{CStr, CString};
2use std::mem::MaybeUninit;
3use std::os::raw::c_char;
4use std::path::{Path, PathBuf};
5use std::ptr::{self, NonNull};
6use std::sync::Arc;
7
8use vllm_cpp_sys as ffi;
9
10use crate::callback::{
11 callback_trampoline, CallbackState, StreamControl, StreamEvent, StreamOutcome,
12};
13use crate::error::{invalid_configuration, status_result, Error};
14use crate::params::{SamplingParams, SchedulerPolicy, Toggle};
15
16#[derive(Clone)]
18pub struct Engine {
19 pub(crate) inner: Arc<EngineInner>,
20}
21
22pub(crate) struct EngineInner {
23 pub(crate) raw: NonNull<ffi::vllm_engine>,
24}
25
26impl std::fmt::Debug for Engine {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 formatter
29 .debug_struct("Engine")
30 .field("raw", &self.inner.raw)
31 .finish_non_exhaustive()
32 }
33}
34
35#[derive(Clone, Debug)]
37pub struct EngineBuilder {
38 model_path: PathBuf,
39 tokenizer_config_path: Option<PathBuf>,
40 block_size: Option<u32>,
41 num_blocks: Option<u32>,
42 max_model_len: Option<u32>,
43 max_num_seqs: Option<u32>,
44 tool_parser: Option<String>,
45 reasoning_parser: Option<String>,
46 speculative_config: Option<String>,
47 prefix_caching: Toggle,
48 max_num_batched_tokens: Option<u32>,
49 scheduler: SchedulerPolicy,
50 kv_transfer_config: Option<String>,
51 jump_forward: Toggle,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
56#[non_exhaustive]
57pub enum FinishReason {
58 Stop,
59 Length,
60 Abort,
61 Error,
62 Repetition,
63 Unknown,
64 Other(String),
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct Completion {
70 pub text: String,
71 pub finish_reason: Option<FinishReason>,
72 pub prompt_tokens: u32,
73 pub completion_tokens: u32,
74}
75
76impl Engine {
77 pub fn builder(model_path: impl Into<PathBuf>) -> EngineBuilder {
79 EngineBuilder::new(model_path)
80 }
81
82 pub fn load(model_path: impl Into<PathBuf>) -> Result<Self, Error> {
84 Self::builder(model_path).load()
85 }
86
87 pub fn complete(&self, prompt: &str, params: &SamplingParams) -> Result<Completion, Error> {
89 let prompt = to_cstring(prompt, "prompt")?;
90 let params = params.marshal()?;
91 let mut raw = MaybeUninit::<ffi::vllm_completion>::uninit();
92 let status = unsafe {
95 ffi::vllm_complete(
96 self.inner.raw.as_ptr(),
97 prompt.as_ptr(),
98 params.raw(),
99 raw.as_mut_ptr(),
100 )
101 };
102 if status != ffi::vllm_status_VLLM_OK {
103 if let Some(error) = params.logits_processor_error() {
104 return Err(error);
105 }
106 status_result(status)?;
107 unreachable!("non-OK native status unexpectedly succeeded");
108 }
109 let raw = unsafe { raw.assume_init() };
111 let guard = CompletionGuard(raw);
112 if let Some(error) = params.logits_processor_error() {
113 return Err(error);
114 }
115 completion_from_raw(&guard.0)
116 }
117
118 pub fn complete_stream<F>(
123 &self,
124 prompt: &str,
125 params: &SamplingParams,
126 mut callback: F,
127 ) -> Result<StreamOutcome, Error>
128 where
129 F: FnMut(StreamEvent) -> StreamControl,
130 {
131 let prompt = to_cstring(prompt, "prompt")?;
132 let params = params.marshal()?;
133 let mut state = CallbackState::new(&mut callback);
134 let status = unsafe {
137 ffi::vllm_complete_stream(
138 self.inner.raw.as_ptr(),
139 prompt.as_ptr(),
140 params.raw(),
141 Some(callback_trampoline::<F>),
142 ptr::from_mut(&mut state).cast(),
143 )
144 };
145 if let Some(payload) = state.take_panic() {
146 std::panic::resume_unwind(payload);
147 }
148 if let Some(error) = state.take_error() {
149 return Err(error);
150 }
151 if let Some(error) = params.logits_processor_error() {
152 return Err(error);
153 }
154 status_result(status)?;
155 Ok(StreamOutcome {
156 stopped_by_callback: state.stopped(),
157 })
158 }
159
160 pub fn chat_json(&self, request_json: &str) -> Result<String, Error> {
162 let request = to_cstring(request_json, "chat request JSON")?;
163 let mut output: *mut c_char = ptr::null_mut();
164 let status =
167 unsafe { ffi::vllm_chat(self.inner.raw.as_ptr(), request.as_ptr(), &mut output) };
168 status_result(status)?;
169 let output = NonNull::new(output).ok_or_else(|| Error::Runtime {
170 message: "vllm_chat succeeded without a response".to_owned(),
171 })?;
172 let guard = NativeStringGuard(output);
173 c_string_to_owned(guard.0.as_ptr(), "chat response")
174 }
175
176 pub fn chat_stream_json<F>(
178 &self,
179 request_json: &str,
180 mut callback: F,
181 ) -> Result<StreamOutcome, Error>
182 where
183 F: FnMut(StreamEvent) -> StreamControl,
184 {
185 let request = to_cstring(request_json, "chat request JSON")?;
186 let mut state = CallbackState::new(&mut callback);
187 let status = unsafe {
190 ffi::vllm_chat_stream(
191 self.inner.raw.as_ptr(),
192 request.as_ptr(),
193 Some(callback_trampoline::<F>),
194 ptr::from_mut(&mut state).cast(),
195 )
196 };
197 if let Some(payload) = state.take_panic() {
198 std::panic::resume_unwind(payload);
199 }
200 if let Some(error) = state.take_error() {
201 return Err(error);
202 }
203 status_result(status)?;
204 Ok(StreamOutcome {
205 stopped_by_callback: state.stopped(),
206 })
207 }
208
209 #[cfg(feature = "serde")]
210 pub fn chat(&self, request: &serde_json::Value) -> Result<serde_json::Value, Error> {
211 let request_json = serde_json::to_string(request).map_err(|error| Error::Json {
212 context: "failed to serialize chat request",
213 message: error.to_string(),
214 })?;
215 let response = self.chat_json(&request_json)?;
216 serde_json::from_str(&response).map_err(|error| Error::Json {
217 context: "failed to parse chat response",
218 message: error.to_string(),
219 })
220 }
221}
222
223impl Drop for EngineInner {
224 fn drop(&mut self) {
225 unsafe { ffi::vllm_engine_free(self.raw.as_ptr()) };
228 }
229}
230
231unsafe impl Send for EngineInner {}
234unsafe impl Sync for EngineInner {}
237
238impl EngineBuilder {
239 #[must_use]
240 pub fn new(model_path: impl Into<PathBuf>) -> Self {
241 Self {
242 model_path: model_path.into(),
243 tokenizer_config_path: None,
244 block_size: None,
245 num_blocks: None,
246 max_model_len: None,
247 max_num_seqs: None,
248 tool_parser: None,
249 reasoning_parser: None,
250 speculative_config: None,
251 prefix_caching: Toggle::Default,
252 max_num_batched_tokens: None,
253 scheduler: SchedulerPolicy::Fcfs,
254 kv_transfer_config: None,
255 jump_forward: Toggle::Default,
256 }
257 }
258
259 #[must_use]
260 pub fn tokenizer_config_path(mut self, value: impl Into<PathBuf>) -> Self {
261 self.tokenizer_config_path = Some(value.into());
262 self
263 }
264
265 #[must_use]
266 pub fn block_size(mut self, value: u32) -> Self {
267 self.block_size = Some(value);
268 self
269 }
270
271 #[must_use]
272 pub fn num_blocks(mut self, value: u32) -> Self {
273 self.num_blocks = Some(value);
274 self
275 }
276
277 #[must_use]
278 pub fn max_model_len(mut self, value: u32) -> Self {
279 self.max_model_len = Some(value);
280 self
281 }
282
283 #[must_use]
284 pub fn max_num_seqs(mut self, value: u32) -> Self {
285 self.max_num_seqs = Some(value);
286 self
287 }
288
289 #[must_use]
290 pub fn tool_parser(mut self, value: impl Into<String>) -> Self {
291 self.tool_parser = Some(value.into());
292 self
293 }
294
295 #[must_use]
296 pub fn reasoning_parser(mut self, value: impl Into<String>) -> Self {
297 self.reasoning_parser = Some(value.into());
298 self
299 }
300
301 #[must_use]
302 pub fn speculative_config(mut self, value: impl Into<String>) -> Self {
303 self.speculative_config = Some(value.into());
304 self
305 }
306
307 #[must_use]
308 pub fn prefix_caching(mut self, value: Toggle) -> Self {
309 self.prefix_caching = value;
310 self
311 }
312
313 #[must_use]
314 pub fn max_num_batched_tokens(mut self, value: u32) -> Self {
315 self.max_num_batched_tokens = Some(value);
316 self
317 }
318
319 #[must_use]
327 pub fn scheduler(mut self, value: SchedulerPolicy) -> Self {
328 self.scheduler = value;
329 self
330 }
331
332 #[must_use]
333 pub fn kv_transfer_config(mut self, value: impl Into<String>) -> Self {
334 self.kv_transfer_config = Some(value.into());
335 self
336 }
337
338 #[must_use]
339 pub fn jump_forward(mut self, value: Toggle) -> Self {
340 self.jump_forward = value;
341 self
342 }
343
344 pub fn load(self) -> Result<Engine, Error> {
345 ensure_abi()?;
346 let model_path = path_to_cstring(&self.model_path, "model path")?;
347 let tokenizer_config_path = self
348 .tokenizer_config_path
349 .as_deref()
350 .map(|path| path_to_cstring(path, "tokenizer config path"))
351 .transpose()?;
352 let tool_parser = optional_cstring(self.tool_parser.as_deref(), "tool parser")?;
353 let reasoning_parser =
354 optional_cstring(self.reasoning_parser.as_deref(), "reasoning parser")?;
355 let speculative_config = optional_cstring(
356 self.speculative_config.as_deref(),
357 "speculative configuration",
358 )?;
359 let scheduling_policy = to_cstring(self.scheduler.as_str(), "scheduler policy")?;
360 let kv_transfer_config = optional_cstring(
361 self.kv_transfer_config.as_deref(),
362 "KV transfer configuration",
363 )?;
364
365 let mut raw = unsafe { ffi::vllm_model_params_default() };
367 raw.model_path = model_path.as_ptr();
368 raw.tokenizer_config_path = optional_pointer(tokenizer_config_path.as_ref());
369 raw.block_size = optional_u32_to_i32(self.block_size, "block_size")?;
370 raw.num_blocks = optional_u32_to_i32(self.num_blocks, "num_blocks")?;
371 raw.max_model_len = optional_u32_to_i32(self.max_model_len, "max_model_len")?;
372 raw.max_num_seqs = optional_u32_to_i32(self.max_num_seqs, "max_num_seqs")?;
373 raw.tool_parser = optional_pointer(tool_parser.as_ref());
374 raw.reasoning_parser = optional_pointer(reasoning_parser.as_ref());
375 raw.speculative_config = optional_pointer(speculative_config.as_ref());
376 raw.enable_prefix_caching = self.prefix_caching.as_native();
377 raw.max_num_batched_tokens =
378 optional_u32_to_i32(self.max_num_batched_tokens, "max_num_batched_tokens")?;
379 raw.scheduling_policy = scheduling_policy.as_ptr();
380 raw.kv_transfer_config = optional_pointer(kv_transfer_config.as_ref());
381 raw.enable_jump_forward = self.jump_forward.as_native();
382
383 let mut output = ptr::null_mut();
384 let status = unsafe { ffi::vllm_engine_load(&raw, &mut output) };
387 status_result(status)?;
388 let raw = NonNull::new(output).ok_or_else(|| Error::ModelLoad {
389 message: "vllm_engine_load succeeded without a handle".to_owned(),
390 })?;
391 Ok(Engine {
392 inner: Arc::new(EngineInner { raw }),
393 })
394 }
395}
396
397fn ensure_abi() -> Result<(), Error> {
398 let actual = unsafe { ffi::vllm_abi_version() };
400 let expected = ffi::VLLM_ABI_VERSION as i32;
401 if actual == expected {
402 Ok(())
403 } else {
404 Err(Error::AbiMismatch { expected, actual })
405 }
406}
407
408fn completion_from_raw(raw: &ffi::vllm_completion) -> Result<Completion, Error> {
409 if raw.text.is_null() {
410 return Err(Error::Runtime {
411 message: "vllm_complete succeeded without text".to_owned(),
412 });
413 }
414 let text = c_string_to_owned(raw.text, "completion text")?;
415 let finish_reason = if raw.finish_reason.is_null() {
416 None
417 } else {
418 Some(parse_finish_reason(c_string_to_owned(
419 raw.finish_reason,
420 "finish reason",
421 )?))
422 };
423 Ok(Completion {
424 text,
425 finish_reason,
426 prompt_tokens: count_to_u32(raw.prompt_tokens, "prompt token count")?,
427 completion_tokens: count_to_u32(raw.completion_tokens, "completion token count")?,
428 })
429}
430
431fn parse_finish_reason(value: String) -> FinishReason {
432 match value.as_str() {
433 "stop" => FinishReason::Stop,
434 "length" => FinishReason::Length,
435 "abort" => FinishReason::Abort,
436 "error" => FinishReason::Error,
437 "repetition" => FinishReason::Repetition,
438 "unknown" => FinishReason::Unknown,
439 _ => FinishReason::Other(value),
440 }
441}
442
443fn count_to_u32(value: i32, field: &'static str) -> Result<u32, Error> {
444 u32::try_from(value).map_err(|_| invalid_configuration(format!("native {field} was negative")))
445}
446
447fn optional_u32_to_i32(value: Option<u32>, field: &'static str) -> Result<i32, Error> {
448 match value {
449 Some(0) | None => Ok(0),
450 Some(value) => i32::try_from(value)
451 .map_err(|_| invalid_configuration(format!("{field} exceeds native i32 range"))),
452 }
453}
454
455fn optional_cstring(value: Option<&str>, field: &'static str) -> Result<Option<CString>, Error> {
456 value.map(|value| to_cstring(value, field)).transpose()
457}
458
459fn optional_pointer(value: Option<&CString>) -> *const c_char {
460 value.map_or(ptr::null(), |value| value.as_ptr())
461}
462
463fn to_cstring(value: &str, field: &'static str) -> Result<CString, Error> {
464 CString::new(value).map_err(|_| Error::InteriorNul { field })
465}
466
467#[cfg(unix)]
468fn path_to_cstring(path: &Path, field: &'static str) -> Result<CString, Error> {
469 use std::os::unix::ffi::OsStrExt;
470
471 CString::new(path.as_os_str().as_bytes()).map_err(|_| Error::InteriorNul { field })
472}
473
474#[cfg(not(unix))]
475fn path_to_cstring(path: &Path, field: &'static str) -> Result<CString, Error> {
476 path.to_str()
477 .ok_or(Error::PathEncoding)
478 .and_then(|value| to_cstring(value, field))
479}
480
481fn c_string_to_owned(pointer: *const c_char, field: &'static str) -> Result<String, Error> {
482 if pointer.is_null() {
483 return Err(Error::InvalidUtf8 { field });
484 }
485 unsafe { CStr::from_ptr(pointer) }
487 .to_str()
488 .map(str::to_owned)
489 .map_err(|_| Error::InvalidUtf8 { field })
490}
491
492struct CompletionGuard(ffi::vllm_completion);
493
494impl Drop for CompletionGuard {
495 fn drop(&mut self) {
496 unsafe { ffi::vllm_completion_free(&mut self.0) };
499 }
500}
501
502struct NativeStringGuard(NonNull<c_char>);
503
504impl Drop for NativeStringGuard {
505 fn drop(&mut self) {
506 unsafe { ffi::vllm_string_free(self.0.as_ptr()) };
508 }
509}