1use serde_json::Value;
4
5use crate::error::{LlamaError, Result};
6use crate::logit_bias::LlamaLogitBias;
7use crate::sampling::LlamaSampler;
8use crate::token::LlamaToken;
9use llama_crab_sys as sys;
10
11use super::Llama;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct Completion {
16 pub text: String,
18 pub n_tokens: usize,
20 pub stop_reason: StopReason,
22 pub logprobs: Option<CompletionLogprobs>,
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct CompletionLogprobs {
29 pub tokens: Vec<String>,
31 pub text_offset: Vec<usize>,
33 pub token_logprobs: Vec<Option<f32>>,
35 pub top_logprobs: Vec<Option<Vec<TokenLogprob>>>,
37}
38
39impl CompletionLogprobs {
40 fn new() -> Self {
41 Self {
42 tokens: Vec::new(),
43 text_offset: Vec::new(),
44 token_logprobs: Vec::new(),
45 top_logprobs: Vec::new(),
46 }
47 }
48
49 fn from_record(record: TokenLogprobRecord) -> Self {
50 let mut logprobs = Self::new();
51 logprobs.push(record);
52 logprobs
53 }
54
55 fn push(&mut self, record: TokenLogprobRecord) {
56 self.tokens.push(record.token);
57 self.text_offset.push(record.text_offset);
58 self.token_logprobs.push(Some(record.token_logprob));
59 self.top_logprobs.push(Some(record.top_logprobs));
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct TokenLogprob {
66 pub token: i32,
68 pub text: String,
70 pub logprob: f32,
72}
73
74#[derive(Debug, Clone, PartialEq)]
75struct TokenLogprobRecord {
76 token: String,
77 text_offset: usize,
78 token_logprob: f32,
79 top_logprobs: Vec<TokenLogprob>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct CompletionOptions {
85 pub max_tokens: usize,
87 pub stop_sequences: Vec<String>,
89 pub sampling: SamplingOptions,
91 pub echo_prompt: bool,
93 pub suffix: Option<String>,
95 pub logit_bias: Vec<LlamaLogitBias>,
97 pub min_tokens: usize,
99 pub logprobs: Option<usize>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct SamplingOptions {
106 pub temperature: f32,
109 pub top_k: i32,
111 pub top_p: f32,
113 pub tfs_z: f32,
115 pub min_p: f32,
117 pub typical_p: f32,
119 pub min_keep: usize,
121 pub penalty_last_n: i32,
123 pub repeat_penalty: f32,
125 pub frequency_penalty: f32,
127 pub presence_penalty: f32,
129 pub mirostat_mode: i32,
131 pub mirostat_tau: f32,
133 pub mirostat_eta: f32,
135 pub seed: Option<u32>,
137}
138
139impl Default for SamplingOptions {
140 fn default() -> Self {
141 Self {
142 temperature: 0.8,
143 top_k: 40,
144 top_p: 0.95,
145 tfs_z: 1.0,
146 min_p: 0.05,
147 typical_p: 1.0,
148 min_keep: 1,
149 penalty_last_n: 64,
150 repeat_penalty: 1.0,
151 frequency_penalty: 0.0,
152 presence_penalty: 0.0,
153 mirostat_mode: 0,
154 mirostat_tau: 5.0,
155 mirostat_eta: 0.1,
156 seed: None,
157 }
158 }
159}
160
161impl SamplingOptions {
162 #[must_use]
164 pub fn chat() -> Self {
165 Self {
166 temperature: 0.2,
167 ..Self::default()
168 }
169 }
170
171 #[must_use]
173 pub const fn with_temperature(mut self, temperature: f32) -> Self {
174 self.temperature = temperature;
175 self
176 }
177
178 #[must_use]
180 pub const fn with_seed(mut self, seed: u32) -> Self {
181 self.seed = Some(seed);
182 self
183 }
184
185 pub fn build_sampler(self, llama: &Llama) -> Result<LlamaSampler> {
187 let mut samplers = Vec::new();
188 let seed = self.seed.unwrap_or(u32::MAX);
189
190 if self.repeat_penalty != 1.0
191 || self.frequency_penalty != 0.0
192 || self.presence_penalty != 0.0
193 {
194 samplers.push(
195 LlamaSampler::penalties(
196 self.penalty_last_n,
197 self.repeat_penalty,
198 self.frequency_penalty,
199 self.presence_penalty,
200 )
201 .ok_or_else(|| LlamaError::Batch("sampler_init_penalties returned null".into()))?,
202 );
203 }
204
205 if self.temperature < 0.0 {
206 samplers.push(
207 LlamaSampler::dist(seed)
208 .ok_or_else(|| LlamaError::Batch("sampler_init_dist returned null".into()))?,
209 );
210 } else if self.temperature == 0.0 {
211 samplers
212 .push(LlamaSampler::greedy().ok_or_else(|| {
213 LlamaError::Batch("sampler_init_greedy returned null".into())
214 })?);
215 } else if self.mirostat_mode == 1 {
216 samplers.push(
217 LlamaSampler::mirostat(
218 llama.model().n_vocab(),
219 seed,
220 self.mirostat_tau,
221 self.mirostat_eta,
222 100,
223 )
224 .ok_or_else(|| LlamaError::Batch("sampler_init_mirostat returned null".into()))?,
225 );
226 } else if self.mirostat_mode == 2 {
227 samplers.push(
228 LlamaSampler::mirostat_v2(seed, self.mirostat_tau, self.mirostat_eta).ok_or_else(
229 || LlamaError::Batch("sampler_init_mirostat_v2 returned null".into()),
230 )?,
231 );
232 } else {
233 samplers.push(
234 LlamaSampler::top_k(self.top_k)
235 .ok_or_else(|| LlamaError::Batch("sampler_init_top_k returned null".into()))?,
236 );
237 if self.tfs_z != 1.0 {
238 samplers.push(
239 LlamaSampler::tail_free(self.tfs_z, self.min_keep).ok_or_else(|| {
240 LlamaError::Batch("sampler_init_tail_free returned null".into())
241 })?,
242 );
243 }
244 samplers.push(
245 LlamaSampler::typical(self.typical_p, self.min_keep).ok_or_else(|| {
246 LlamaError::Batch("sampler_init_typical returned null".into())
247 })?,
248 );
249 samplers.push(
250 LlamaSampler::top_p(self.top_p, self.min_keep)
251 .ok_or_else(|| LlamaError::Batch("sampler_init_top_p returned null".into()))?,
252 );
253 samplers.push(
254 LlamaSampler::min_p(self.min_p, self.min_keep)
255 .ok_or_else(|| LlamaError::Batch("sampler_init_min_p returned null".into()))?,
256 );
257 samplers.push(
258 LlamaSampler::temp(self.temperature)
259 .ok_or_else(|| LlamaError::Batch("sampler_init_temp returned null".into()))?,
260 );
261 samplers.push(
262 LlamaSampler::dist(seed)
263 .ok_or_else(|| LlamaError::Batch("sampler_init_dist returned null".into()))?,
264 );
265 }
266
267 LlamaSampler::chain(samplers, false)
268 .ok_or_else(|| LlamaError::Batch("sampler_chain_init returned null".into()))
269 }
270}
271
272impl CompletionOptions {
273 #[must_use]
275 pub const fn new(max_tokens: usize) -> Self {
276 Self {
277 max_tokens,
278 stop_sequences: Vec::new(),
279 sampling: SamplingOptions {
280 temperature: 0.0,
281 top_k: 40,
282 top_p: 0.95,
283 tfs_z: 1.0,
284 min_p: 0.05,
285 typical_p: 1.0,
286 min_keep: 1,
287 penalty_last_n: 64,
288 repeat_penalty: 1.0,
289 frequency_penalty: 0.0,
290 presence_penalty: 0.0,
291 mirostat_mode: 0,
292 mirostat_tau: 5.0,
293 mirostat_eta: 0.1,
294 seed: None,
295 },
296 echo_prompt: false,
297 suffix: None,
298 logit_bias: Vec::new(),
299 min_tokens: 0,
300 logprobs: None,
301 }
302 }
303
304 #[must_use]
306 pub fn sampled(max_tokens: usize) -> Self {
307 Self {
308 max_tokens,
309 stop_sequences: Vec::new(),
310 sampling: SamplingOptions::default(),
311 echo_prompt: false,
312 suffix: None,
313 logit_bias: Vec::new(),
314 min_tokens: 0,
315 logprobs: None,
316 }
317 }
318
319 #[must_use]
321 pub const fn with_sampling(mut self, sampling: SamplingOptions) -> Self {
322 self.sampling = sampling;
323 self
324 }
325
326 #[must_use]
328 pub const fn with_echo_prompt(mut self, echo_prompt: bool) -> Self {
329 self.echo_prompt = echo_prompt;
330 self
331 }
332
333 #[must_use]
335 pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
336 let suffix = suffix.into();
337 self.suffix = (!suffix.is_empty()).then_some(suffix);
338 self
339 }
340
341 #[must_use]
343 pub fn with_logit_biases<I>(mut self, biases: I) -> Self
344 where
345 I: IntoIterator<Item = LlamaLogitBias>,
346 {
347 self.logit_bias = biases.into_iter().collect();
348 self
349 }
350
351 #[must_use]
353 pub const fn with_min_tokens(mut self, min_tokens: usize) -> Self {
354 self.min_tokens = min_tokens;
355 self
356 }
357
358 #[must_use]
360 pub const fn with_logprobs(mut self, logprobs: usize) -> Self {
361 self.logprobs = Some(logprobs);
362 self
363 }
364
365 pub fn build_sampler(&self, llama: &Llama) -> Result<LlamaSampler> {
367 build_completion_sampler(llama, self)
368 }
369
370 #[must_use]
372 pub fn with_stop_sequence(mut self, stop_sequence: impl Into<String>) -> Self {
373 let stop_sequence = stop_sequence.into();
374 if !stop_sequence.is_empty() {
375 self.stop_sequences.push(stop_sequence);
376 }
377 self
378 }
379
380 #[must_use]
382 pub fn with_stop_sequences<I, S>(mut self, stop_sequences: I) -> Self
383 where
384 I: IntoIterator<Item = S>,
385 S: Into<String>,
386 {
387 self.stop_sequences.extend(
388 stop_sequences
389 .into_iter()
390 .map(Into::into)
391 .filter(|s: &String| !s.is_empty()),
392 );
393 self
394 }
395}
396
397#[derive(Debug, Clone, PartialEq)]
399pub struct CompletionChunk {
400 pub text: String,
402 pub token: Option<LlamaToken>,
407 pub n_tokens: usize,
409 pub stop_reason: Option<StopReason>,
411 pub logprobs: Option<CompletionLogprobs>,
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
417pub enum StopReason {
418 Length,
420 Eos,
422 Stop,
424 ToolCalls,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
430pub enum StreamControl {
431 Continue,
433 Stop,
435}
436
437pub fn create_completion(llama: &mut Llama, prompt: &str, max_tokens: usize) -> Result<Completion> {
447 create_completion_with_options(llama, prompt, CompletionOptions::new(max_tokens))
448}
449
450pub fn create_completion_with_options(
452 llama: &mut Llama,
453 prompt: &str,
454 options: CompletionOptions,
455) -> Result<Completion> {
456 create_completion_stream(llama, prompt, options, |_| StreamControl::Continue)
457}
458
459pub fn create_completion_with_sampler(
461 llama: &mut Llama,
462 prompt: &str,
463 options: CompletionOptions,
464 sampler: &mut LlamaSampler,
465) -> Result<Completion> {
466 create_completion_stream_with_sampler(llama, prompt, options, sampler, |_| {
467 StreamControl::Continue
468 })
469}
470
471pub fn create_completion_stream<F>(
474 llama: &mut Llama,
475 prompt: &str,
476 options: CompletionOptions,
477 on_chunk: F,
478) -> Result<Completion>
479where
480 F: FnMut(CompletionChunk) -> StreamControl,
481{
482 let mut sampler = options.build_sampler(llama)?;
483 create_completion_stream_with_sampler(llama, prompt, options, &mut sampler, on_chunk)
484}
485
486fn build_completion_sampler(llama: &Llama, options: &CompletionOptions) -> Result<LlamaSampler> {
487 let base_sampler = options.sampling.build_sampler(llama)?;
488 if options.logit_bias.is_empty() {
489 return Ok(base_sampler);
490 }
491
492 let raw_biases: Vec<sys::llama_logit_bias> = options
493 .logit_bias
494 .iter()
495 .map(|bias| sys::llama_logit_bias {
496 token: bias.token,
497 bias: bias.bias,
498 })
499 .collect();
500 let bias_sampler = unsafe { LlamaSampler::logit_bias(llama.model().n_vocab(), &raw_biases) }
501 .ok_or_else(|| LlamaError::Batch("sampler_init_logit_bias returned null".into()))?;
502 LlamaSampler::chain(vec![bias_sampler, base_sampler], false)
503 .ok_or_else(|| LlamaError::Batch("sampler_chain_init returned null".into()))
504}
505
506pub fn create_completion_stream_with_sampler<F>(
508 llama: &mut Llama,
509 prompt: &str,
510 options: CompletionOptions,
511 sampler: &mut LlamaSampler,
512 mut on_chunk: F,
513) -> Result<Completion>
514where
515 F: FnMut(CompletionChunk) -> StreamControl,
516{
517 let _ = llama.context().seq_rm(0, -1, -1);
520
521 let tokens = llama.model().tokenize(prompt, true, true)?;
522
523 let mut batch = crate::batch::LlamaBatch::new(tokens.len(), 1);
525 for (i, &t) in tokens.iter().enumerate() {
526 batch
527 .add(t, i as i32, &[0], i + 1 == tokens.len())
528 .map_err(LlamaError::from)?;
529 }
530 llama.context().decode(&batch)?;
531
532 let eos = llama.model().token_eos();
533 let eot = llama.model().token_eot();
534 let mut generated = String::new();
535 let mut stop_buffer = StopBuffer::new(options.stop_sequences);
536 let mut last_pos = tokens.len() as i32;
537 let mut n_generated = 0_usize;
538 let mut stop_reason = StopReason::Length;
539
540 if options.echo_prompt
541 && emit_chunk(
542 &mut on_chunk,
543 &mut generated,
544 prompt.to_string(),
545 None,
546 0,
547 None,
548 None,
549 ) == StreamControl::Stop
550 {
551 return Ok(Completion {
552 text: generated,
553 n_tokens: 0,
554 stop_reason: StopReason::Stop,
555 logprobs: None,
556 });
557 }
558
559 let mut logprobs = options.logprobs.map(|_| CompletionLogprobs::new());
560 for _ in 0..options.max_tokens {
561 let idx = if n_generated == 0 {
566 (tokens.len() as i32) - 1
567 } else {
568 0
569 };
570 let suppress_eog = n_generated < options.min_tokens;
571 let next = sample_next_token(llama, sampler, idx, suppress_eog)?;
572 let logits_for_logprobs = options
573 .logprobs
574 .map(|_| logits_for_logprobs(llama, suppress_eog))
575 .transpose()?;
576 if next == eos || next == eot {
577 stop_reason = StopReason::Eos;
578 break;
579 }
580 let piece = llama.model().detokenize(&[next], false)?;
581 let mut chunk_logprobs = None;
582 if let (Some(logprobs), Some(top_n), Some(logits)) =
583 (&mut logprobs, options.logprobs, logits_for_logprobs)
584 {
585 let text_offset = if options.echo_prompt {
586 generated.len()
587 } else {
588 prompt.len() + generated.len()
589 };
590 let mut record = token_logprob_record(&logits, next, piece.clone(), text_offset, top_n);
591 for candidate in &mut record.top_logprobs {
592 if candidate.text == candidate.token.to_string() {
593 candidate.text = llama
594 .model()
595 .detokenize(&[LlamaToken::from(candidate.token)], false)?;
596 }
597 }
598 chunk_logprobs = Some(CompletionLogprobs::from_record(record.clone()));
599 logprobs.push(record);
600 }
601 n_generated += 1;
602 let step = stop_buffer.push(&piece);
603 if step.stopped {
604 stop_reason = StopReason::Stop;
605 if emit_chunk(
606 &mut on_chunk,
607 &mut generated,
608 step.text,
609 Some(next),
610 n_generated,
611 None,
612 chunk_logprobs,
613 ) == StreamControl::Stop
614 {
615 stop_reason = StopReason::Stop;
616 }
617 break;
618 }
619 if emit_chunk(
620 &mut on_chunk,
621 &mut generated,
622 step.text,
623 Some(next),
624 n_generated,
625 None,
626 chunk_logprobs,
627 ) == StreamControl::Stop
628 {
629 stop_reason = StopReason::Stop;
630 break;
631 }
632 let mut single = crate::batch::LlamaBatch::new(1, 1);
634 single
635 .add(next, last_pos, &[0], true)
636 .map_err(LlamaError::from)?;
637 llama.context().decode(&single)?;
638 last_pos += 1;
639 }
640
641 let final_text = format!(
642 "{}{}",
643 stop_buffer.flush(),
644 options.suffix.as_deref().unwrap_or("")
645 );
646 if emit_chunk(
647 &mut on_chunk,
648 &mut generated,
649 final_text,
650 None,
651 n_generated,
652 Some(stop_reason),
653 None,
654 ) == StreamControl::Stop
655 {
656 stop_reason = StopReason::Stop;
657 }
658
659 Ok(Completion {
660 text: generated,
661 n_tokens: n_generated,
662 stop_reason,
663 logprobs,
664 })
665}
666
667fn logits_for_logprobs(llama: &mut Llama, suppress_eog: bool) -> Result<Vec<f32>> {
668 let ctx = llama.context().raw_handle();
669 let logits = unsafe { sys::llama_get_logits(ctx) };
670 if logits.is_null() {
671 return Err(LlamaError::Batch("no logits".into()));
672 }
673 let n_vocab = llama.model().n_vocab() as usize;
674 let mut logits = unsafe { std::slice::from_raw_parts(logits, n_vocab) }.to_vec();
675 if suppress_eog {
676 for token in [llama.model().token_eos(), llama.model().token_eot()] {
677 let raw = token.raw();
678 if raw >= 0 && (raw as usize) < logits.len() {
679 logits[raw as usize] = f32::NEG_INFINITY;
680 }
681 }
682 }
683 Ok(logits)
684}
685
686fn token_logprob_record(
687 logits: &[f32],
688 selected: LlamaToken,
689 selected_text: String,
690 text_offset: usize,
691 top_n: usize,
692) -> TokenLogprobRecord {
693 let logprobs = logits_to_logprobs(logits);
694 let selected_id = selected.raw();
695 let selected_logprob = selected_logprob(&logprobs, selected_id);
696 let mut candidates: Vec<(i32, f32)> = logprobs
697 .iter()
698 .enumerate()
699 .map(|(token, &logprob)| (token as i32, logprob))
700 .collect();
701 candidates.sort_by(|(_, lhs), (_, rhs)| rhs.total_cmp(lhs));
702 candidates.truncate(top_n);
703 if !candidates.iter().any(|(token, _)| *token == selected_id) {
704 candidates.push((selected_id, selected_logprob));
705 }
706
707 let top_logprobs = candidates
708 .into_iter()
709 .map(|(token, logprob)| TokenLogprob {
710 token,
711 text: if token == selected_id {
712 selected_text.clone()
713 } else {
714 token.to_string()
715 },
716 logprob,
717 })
718 .collect();
719
720 TokenLogprobRecord {
721 token: selected_text,
722 text_offset,
723 token_logprob: selected_logprob,
724 top_logprobs,
725 }
726}
727
728fn selected_logprob(logprobs: &[f32], selected: i32) -> f32 {
729 if selected < 0 {
730 return f32::NEG_INFINITY;
731 }
732 logprobs
733 .get(selected as usize)
734 .copied()
735 .unwrap_or(f32::NEG_INFINITY)
736}
737
738fn logits_to_logprobs(logits: &[f32]) -> Vec<f32> {
739 let max = logits
740 .iter()
741 .copied()
742 .filter(|value| value.is_finite())
743 .fold(f32::NEG_INFINITY, f32::max);
744 if !max.is_finite() {
745 return vec![f32::NEG_INFINITY; logits.len()];
746 }
747 let sum_exp: f32 = logits
748 .iter()
749 .copied()
750 .filter(|value| value.is_finite())
751 .map(|value| (value - max).exp())
752 .sum();
753 let log_sum_exp = max + sum_exp.ln();
754 logits
755 .iter()
756 .map(|&value| {
757 if value.is_finite() {
758 value - log_sum_exp
759 } else {
760 f32::NEG_INFINITY
761 }
762 })
763 .collect()
764}
765
766fn sample_next_token(
767 llama: &mut Llama,
768 sampler: &mut LlamaSampler,
769 idx: i32,
770 suppress_eog: bool,
771) -> Result<LlamaToken> {
772 if !suppress_eog {
773 return Ok(unsafe { sampler.sample(llama.context().raw_handle(), idx) });
774 }
775
776 let eos = llama.model().token_eos();
777 let eot = llama.model().token_eot();
778 let ctx = llama.context().raw_handle();
779 let logits = unsafe { sys::llama_get_logits_ith(ctx, idx) };
780 if logits.is_null() {
781 return Err(LlamaError::Batch(format!("no logits at index {idx}")));
782 }
783
784 let mut restore = Vec::new();
785 for token in [eos, eot] {
786 let raw = token.raw();
787 if raw >= 0 && raw < llama.model().n_vocab() {
788 let slot = unsafe { logits.add(raw as usize) };
789 let previous = unsafe { *slot };
790 unsafe {
791 *slot = f32::NEG_INFINITY;
792 }
793 restore.push((slot, previous));
794 }
795 }
796
797 let sampled = unsafe { sampler.sample(ctx, idx) };
798 for (slot, previous) in restore {
799 unsafe {
800 *slot = previous;
801 }
802 }
803 Ok(sampled)
804}
805
806#[cfg(test)]
807fn format_completion_text(prompt: &str, generated: &str, options: &CompletionOptions) -> String {
808 let mut text = String::new();
809 if options.echo_prompt {
810 text.push_str(prompt);
811 }
812 text.push_str(generated);
813 if let Some(suffix) = &options.suffix {
814 text.push_str(suffix);
815 }
816 text
817}
818
819pub fn json_schema_grammar(schema: &Value) -> Result<String> {
836 crate::json_schema::schema_to_grammar(schema, "root")
837 .map_err(|e| LlamaError::JsonSchemaToGrammar(e.to_string()))
838}
839
840fn emit_chunk<F>(
841 on_chunk: &mut F,
842 generated: &mut String,
843 text: String,
844 token: Option<LlamaToken>,
845 n_tokens: usize,
846 stop_reason: Option<StopReason>,
847 logprobs: Option<CompletionLogprobs>,
848) -> StreamControl
849where
850 F: FnMut(CompletionChunk) -> StreamControl,
851{
852 if text.is_empty() && stop_reason.is_none() {
853 return StreamControl::Continue;
854 }
855
856 generated.push_str(&text);
857 on_chunk(CompletionChunk {
858 text,
859 token,
860 n_tokens,
861 stop_reason,
862 logprobs,
863 })
864}
865
866#[derive(Debug, Clone, PartialEq, Eq)]
867struct StopBuffer {
868 pending: String,
869 stop_sequences: Vec<String>,
870 stopped: bool,
871}
872
873#[derive(Debug, Clone, PartialEq, Eq)]
874struct StopBufferStep {
875 text: String,
876 stopped: bool,
877}
878
879impl StopBuffer {
880 fn new(stop_sequences: Vec<String>) -> Self {
881 Self {
882 pending: String::new(),
883 stop_sequences: stop_sequences
884 .into_iter()
885 .filter(|s| !s.is_empty())
886 .collect(),
887 stopped: false,
888 }
889 }
890
891 fn push(&mut self, text: &str) -> StopBufferStep {
892 if self.stopped {
893 return StopBufferStep {
894 text: String::new(),
895 stopped: true,
896 };
897 }
898 if self.stop_sequences.is_empty() {
899 return StopBufferStep {
900 text: text.to_string(),
901 stopped: false,
902 };
903 }
904
905 self.pending.push_str(text);
906 if let Some(stop_start) = self.find_stop_start() {
907 self.stopped = true;
908 let text = self.pending[..stop_start].to_string();
909 self.pending.clear();
910 return StopBufferStep {
911 text,
912 stopped: true,
913 };
914 }
915
916 let hold_start = self.longest_stop_prefix_suffix_start();
917 let text = self.pending[..hold_start].to_string();
918 self.pending = self.pending[hold_start..].to_string();
919 StopBufferStep {
920 text,
921 stopped: false,
922 }
923 }
924
925 fn flush(&mut self) -> String {
926 std::mem::take(&mut self.pending)
927 }
928
929 fn find_stop_start(&self) -> Option<usize> {
930 self.stop_sequences
931 .iter()
932 .filter_map(|stop| self.pending.find(stop))
933 .min()
934 }
935
936 fn longest_stop_prefix_suffix_start(&self) -> usize {
937 let mut hold_start = self.pending.len();
938 for (start, _) in self.pending.char_indices() {
939 let suffix = &self.pending[start..];
940 if self
941 .stop_sequences
942 .iter()
943 .any(|stop| stop.starts_with(suffix))
944 {
945 hold_start = start;
946 break;
947 }
948 }
949 hold_start
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use super::{format_completion_text, token_logprob_record, CompletionOptions, StopBuffer};
956 use crate::LlamaToken;
957
958 #[test]
959 fn stop_buffer_holds_stop_prefix_across_token_boundaries() {
960 let mut buffer = StopBuffer::new(vec!["</stop>".to_string()]);
961
962 let first = buffer.push("hello </");
963 assert_eq!(first.text, "hello ");
964 assert!(!first.stopped);
965
966 let second = buffer.push("stop> ignored");
967 assert_eq!(second.text, "");
968 assert!(second.stopped);
969 }
970
971 #[test]
972 fn stop_buffer_removes_stop_sequence_inside_chunk() {
973 let mut buffer = StopBuffer::new(vec!["END".to_string()]);
974
975 let step = buffer.push("answerEND trailing");
976
977 assert_eq!(step.text, "answer");
978 assert!(step.stopped);
979 }
980
981 #[test]
982 fn completion_options_apply_echo_and_suffix_to_final_text() {
983 let options = CompletionOptions::new(16)
984 .with_echo_prompt(true)
985 .with_suffix(" done");
986
987 let text = format_completion_text("prompt: ", "answer", &options);
988
989 assert_eq!(text, "prompt: answer done");
990 }
991
992 #[test]
993 fn completion_options_apply_min_tokens() {
994 let options = CompletionOptions::new(8).with_min_tokens(3);
995
996 assert_eq!(options.min_tokens, 3);
997 }
998
999 #[test]
1000 fn completion_options_apply_logprobs() {
1001 let options = CompletionOptions::new(8).with_logprobs(3);
1002
1003 assert_eq!(options.logprobs, Some(3));
1004 }
1005
1006 #[test]
1007 fn token_logprobs_include_selected_token_and_top_candidates() {
1008 let record = token_logprob_record(
1009 &[0.0, 2.0, 1.0],
1010 LlamaToken::from(0),
1011 "zero".to_string(),
1012 4,
1013 1,
1014 );
1015
1016 assert_eq!(record.token, "zero");
1017 assert_eq!(record.text_offset, 4);
1018 assert_eq!(record.top_logprobs.len(), 2);
1019 assert!(record
1020 .top_logprobs
1021 .iter()
1022 .any(|candidate| candidate.token == 0 && candidate.text == "zero"));
1023 }
1024
1025 #[test]
1026 fn stop_buffer_flushes_pending_prefix_when_generation_finishes_without_stop() {
1027 let mut buffer = StopBuffer::new(vec!["foobar".to_string()]);
1028
1029 let step = buffer.push("hello foo");
1030 assert_eq!(step.text, "hello ");
1031 assert!(!step.stopped);
1032
1033 assert_eq!(buffer.flush(), "foo");
1034 }
1035}