1use std::{
8 collections::{HashMap, HashSet},
9 str,
10 sync::Arc,
11};
12
13use ferrum_interfaces::tokenizer::Tokenizer;
14use ferrum_types::{FerrumError, ResponseFormat, Result, StructuredOutputStart, TokenId};
15use llguidance::{
16 api::TopLevelGrammar,
17 toktrie::{InferenceCapabilities, TokEnv, TokRxInfo, TokTrie, TokenizerEnv},
18 JsonCompileOptions, Matcher, ParserFactory,
19};
20use parking_lot::Mutex;
21use serde_json::json;
22
23mod harmony;
24#[cfg(test)]
25mod reasoning_envelope_tests;
26
27const MAX_CACHED_GRAMMARS: usize = 64;
28const AUTO_STRUCTURED_RESERVE_DIVISOR: usize = 2;
31const MIN_AUTO_STRUCTURED_RESERVE_TOKENS: usize = 32;
32const MAX_AUTO_STRUCTURED_RESERVE_TOKENS: usize = 1024;
33const MAX_IDENTICAL_TOKEN_RUN: usize = MAX_AUTO_STRUCTURED_RESERVE_TOKENS / 2;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct StructuredOutputBudgetPlan {
39 pub total_output_tokens: usize,
40 pub reasoning_token_limit: usize,
41 pub boundary_token_count: usize,
42 pub structured_reserve_tokens: usize,
43}
44
45impl StructuredOutputBudgetPlan {
46 fn automatic(total_output_tokens: usize, boundary_token_count: usize) -> Result<Self> {
47 if boundary_token_count == 0 || total_output_tokens <= boundary_token_count {
48 return Err(FerrumError::invalid_request(format!(
49 "structured output requires max_tokens greater than its {boundary_token_count}-token delimiter"
50 )));
51 }
52 let available_after_boundary = total_output_tokens - boundary_token_count;
53 let proportional_reserve = total_output_tokens.div_ceil(AUTO_STRUCTURED_RESERVE_DIVISOR);
54 let structured_reserve_tokens = proportional_reserve
55 .clamp(
56 MIN_AUTO_STRUCTURED_RESERVE_TOKENS,
57 MAX_AUTO_STRUCTURED_RESERVE_TOKENS,
58 )
59 .min(available_after_boundary);
60 Ok(Self {
61 total_output_tokens,
62 reasoning_token_limit: total_output_tokens
63 - boundary_token_count
64 - structured_reserve_tokens,
65 boundary_token_count,
66 structured_reserve_tokens,
67 })
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72struct StructuredOutputLivenessPolicy {
73 max_identical_token_run: usize,
74}
75
76impl StructuredOutputLivenessPolicy {
77 fn for_request(max_output_tokens: usize, budget: Option<StructuredOutputBudgetPlan>) -> Self {
78 let guaranteed_structured_tokens = budget
79 .map(|plan| plan.structured_reserve_tokens)
80 .unwrap_or(max_output_tokens);
81 Self {
82 max_identical_token_run: guaranteed_structured_tokens
83 .div_ceil(2)
84 .clamp(1, MAX_IDENTICAL_TOKEN_RUN),
85 }
86 }
87}
88
89pub struct StructuredOutputFactory {
91 parser_factory: ParserFactory,
92 tokenizer: Arc<dyn Tokenizer + Send + Sync>,
93 vocab_size: usize,
94 defined_token_ids: Arc<[bool]>,
95 json_token_classes: Arc<[StructuredOutputTokenClass]>,
96 grammar_templates: Mutex<HashMap<String, Matcher>>,
97}
98
99impl std::fmt::Debug for StructuredOutputFactory {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("StructuredOutputFactory")
102 .field("vocab_size", &self.vocab_size)
103 .finish_non_exhaustive()
104 }
105}
106
107impl StructuredOutputFactory {
108 pub fn new(tokenizer: Arc<dyn Tokenizer + Send + Sync>) -> Result<Self> {
110 Self::new_with_model_vocab_size(tokenizer, None)
111 }
112
113 pub fn new_with_model_vocab_size(
116 tokenizer: Arc<dyn Tokenizer + Send + Sync>,
117 model_vocab_size: Option<usize>,
118 ) -> Result<Self> {
119 let eos = tokenizer.special_tokens().eos_token.ok_or_else(|| {
120 FerrumError::config("structured output requires a tokenizer EOS token")
121 })?;
122 let vocab_size = model_vocab_size
123 .unwrap_or_else(|| tokenizer.vocab_size())
124 .max(tokenizer.vocab_size());
125 if vocab_size == 0 || eos.get() as usize >= vocab_size {
126 return Err(FerrumError::config(format!(
127 "structured output tokenizer has invalid vocab/EOS: vocab_size={vocab_size}, eos={}",
128 eos.get()
129 )));
130 }
131
132 let special_ids = tokenizer_special_ids(tokenizer.as_ref());
133 let mut defined_token_ids = Vec::with_capacity(vocab_size);
134 let mut json_token_classes = Vec::with_capacity(vocab_size);
135 let token_bytes = (0..vocab_size)
136 .map(|idx| {
137 let token = TokenId::new(idx as u32);
138 if special_ids.contains(&token.get()) {
139 defined_token_ids.push(true);
140 json_token_classes.push(StructuredOutputTokenClass::Control);
141 special_token_marker(token)
142 } else if let Some(bytes) = tokenizer
143 .token_bytes(token)
144 .filter(|bytes| !bytes.is_empty())
145 {
146 defined_token_ids.push(true);
147 json_token_classes.push(classify_json_token_bytes(&bytes));
148 bytes
149 } else {
150 defined_token_ids.push(false);
155 json_token_classes.push(StructuredOutputTokenClass::Undefined);
156 Vec::new()
157 }
158 })
159 .collect::<Vec<_>>();
160
161 let mut eos_tokens = vec![eos.get()];
162 eos_tokens.extend(
163 tokenizer
164 .special_tokens()
165 .extra_eos_tokens
166 .iter()
167 .map(|token| token.get())
168 .filter(|token| *token < vocab_size as u32),
169 );
170 eos_tokens.sort_unstable();
171 eos_tokens.dedup();
172 if let Some(position) = eos_tokens.iter().position(|token| *token == eos.get()) {
173 eos_tokens.swap(0, position);
174 }
175
176 let info = TokRxInfo::new(vocab_size as u32, eos.get());
177 let trie = TokTrie::from(&info, &token_bytes).with_eos_tokens(&eos_tokens);
178 let tok_env: TokEnv = Arc::new(FerrumTokenizerEnv {
179 tokenizer: Arc::clone(&tokenizer),
180 trie,
181 });
182 let mut parser_factory = ParserFactory::new(
183 &tok_env,
184 InferenceCapabilities {
185 ff_tokens: false,
186 conditional_ff_tokens: false,
187 backtrack: false,
188 fork: false,
189 },
190 &llguidance::earley::SlicedBiasComputer::general_slices(),
191 )
192 .map_err(|error| {
193 FerrumError::config(format!("build structured-output parser factory: {error}"))
194 })?;
195 parser_factory.quiet();
196
197 Ok(Self {
198 parser_factory,
199 tokenizer,
200 vocab_size,
201 defined_token_ids: defined_token_ids.into(),
202 json_token_classes: json_token_classes.into(),
203 grammar_templates: Mutex::new(HashMap::new()),
204 })
205 }
206
207 pub fn create_processor(
209 &self,
210 response_format: &ResponseFormat,
211 start: &StructuredOutputStart,
212 max_output_tokens: usize,
213 stop_token_ids: &HashSet<u32>,
214 stop_text_sequences: &[String],
215 ) -> Result<Option<StructuredOutputProcessor>> {
216 let schema = match response_format {
217 ResponseFormat::Text => return Ok(None),
218 ResponseFormat::JsonObject => json!({"type": "object"}),
219 ResponseFormat::JsonSchema(schema) => {
220 serde_json::from_str(schema).map_err(|error| {
221 FerrumError::invalid_request(format!(
222 "response_format.schema is not valid JSON: {error}"
223 ))
224 })?
225 }
226 };
227 let schema = compact_json_schema(schema)?;
228 let grammar_key = serde_json::to_string(&schema).map_err(|error| {
229 FerrumError::invalid_request(format!("serialize structured-output schema: {error}"))
230 })?;
231 let matcher = {
232 let mut templates = self.grammar_templates.lock();
233 if let Some(template) = templates.get(&grammar_key) {
234 template.deep_clone()
235 } else {
236 let grammar = TopLevelGrammar::from_json_schema(schema);
237 let parser = self
238 .parser_factory
239 .create_parser(grammar)
240 .map_err(|error| {
241 FerrumError::invalid_request(format!(
242 "unsupported structured-output grammar: {error}"
243 ))
244 })?;
245 let matcher = Matcher::new(Ok(parser));
246 if templates.len() >= MAX_CACHED_GRAMMARS {
247 templates.clear();
248 }
249 templates.insert(grammar_key, matcher.deep_clone());
250 matcher
251 }
252 };
253 let (activation, budget) = match start {
254 StructuredOutputStart::Immediate => (Activation::Active, None),
255 StructuredOutputStart::HarmonyFinal => {
256 let boundary = harmony::HarmonyBoundary::compile(
257 self.tokenizer.as_ref(),
258 self.vocab_size,
259 max_output_tokens,
260 stop_token_ids,
261 stop_text_sequences,
262 )?;
263 let budget = boundary.budget();
264 (Activation::Harmony(boundary), Some(budget))
265 }
266 StructuredOutputStart::AfterDelimiter(delimiter) => {
267 let delimiter_tokens =
268 self.boundary_token_ids(delimiter, stop_token_ids, stop_text_sequences)?;
269 let budget = StructuredOutputBudgetPlan::automatic(
270 max_output_tokens,
271 delimiter_tokens.len(),
272 )?;
273 (
274 Activation::Boundary {
275 opening_tokens: Vec::new(),
276 delimiter_tokens,
277 allow_reasoning: true,
278 forcing: false,
279 },
280 Some(budget),
281 )
282 }
283 StructuredOutputStart::AfterReasoningEnvelope {
284 opening,
285 closing,
286 allow_reasoning,
287 } => {
288 let opening_tokens =
289 self.boundary_token_ids(opening, stop_token_ids, stop_text_sequences)?;
290 let delimiter_tokens =
291 self.boundary_token_ids(closing, stop_token_ids, stop_text_sequences)?;
292 if let Some(token) = opening_tokens
293 .iter()
294 .chain(&delimiter_tokens)
295 .find(|token| {
296 !self
297 .defined_token_ids
298 .get(**token as usize)
299 .copied()
300 .unwrap_or(false)
301 })
302 {
303 return Err(FerrumError::invalid_request(format!(
304 "structured-output reasoning envelope contains undefined token {token}"
305 )));
306 }
307 let empty_envelope = format!("{opening}{closing}");
310 if let Some(stop) = stop_text_sequences
311 .iter()
312 .find(|stop| !stop.is_empty() && empty_envelope.contains(stop.as_str()))
313 {
314 return Err(FerrumError::invalid_request(format!(
315 "structured-output reasoning envelope conflicts with stop sequence {stop:?}"
316 )));
317 }
318 let mut budget = StructuredOutputBudgetPlan::automatic(
319 max_output_tokens,
320 opening_tokens.len() + delimiter_tokens.len(),
321 )?;
322 if !allow_reasoning {
323 budget.reasoning_token_limit = 0;
324 budget.structured_reserve_tokens =
325 max_output_tokens - budget.boundary_token_count;
326 }
327 (
328 Activation::Boundary {
329 opening_tokens,
330 delimiter_tokens,
331 allow_reasoning: *allow_reasoning,
332 forcing: false,
333 },
334 Some(budget),
335 )
336 }
337 };
338
339 let grammar_start = matches!(activation, Activation::Active).then_some(0);
340 let liveness = StructuredOutputLivenessPolicy::for_request(max_output_tokens, budget);
341 Ok(Some(StructuredOutputProcessor {
342 state: Mutex::new(ProcessorState {
343 matcher,
344 activation: activation.clone(),
345 initial_activation: activation,
346 consumed: 0,
347 boundary_forced: false,
348 boundary_start: None,
349 grammar_start,
350 trailing_grammar_token_id: None,
351 trailing_identical_token_count: 0,
352 liveness_intervention_count: 0,
353 last_liveness_intervention_at: None,
354 }),
355 vocab_size: self.vocab_size,
356 defined_token_ids: Arc::clone(&self.defined_token_ids),
357 json_token_classes: Arc::clone(&self.json_token_classes),
358 budget,
359 liveness,
360 }))
361 }
362
363 fn boundary_token_ids(
364 &self,
365 delimiter: &str,
366 stop_token_ids: &HashSet<u32>,
367 stop_text_sequences: &[String],
368 ) -> Result<Vec<u32>> {
369 if delimiter.is_empty() {
370 return Err(FerrumError::invalid_request(
371 "structured-output delimiter must not be empty",
372 ));
373 }
374 let tokens = if let Some(token) = self.tokenizer.token_id(delimiter) {
375 vec![token.get()]
376 } else {
377 self.tokenizer
378 .encode(delimiter, false)?
379 .into_iter()
380 .map(|token| token.get())
381 .collect::<Vec<_>>()
382 };
383 if tokens.is_empty() {
384 return Err(FerrumError::invalid_request(format!(
385 "structured-output delimiter {delimiter:?} did not tokenize"
386 )));
387 }
388 if let Some(token) = tokens.iter().find(|token| stop_token_ids.contains(token)) {
389 return Err(FerrumError::invalid_request(format!(
390 "structured-output delimiter token {token} conflicts with a stop token"
391 )));
392 }
393 if let Some(stop) = stop_text_sequences
394 .iter()
395 .find(|stop| !stop.is_empty() && delimiter.contains(stop.as_str()))
396 {
397 return Err(FerrumError::invalid_request(format!(
398 "structured-output delimiter {delimiter:?} conflicts with stop sequence {stop:?}"
399 )));
400 }
401 Ok(tokens)
402 }
403}
404
405fn compact_json_schema(schema: serde_json::Value) -> Result<serde_json::Value> {
406 let mut schema = match schema {
407 schema @ serde_json::Value::Object(_) => schema,
408 schema @ serde_json::Value::Bool(_) => json!({"allOf": [schema]}),
409 _ => {
410 return Err(FerrumError::invalid_request(
411 "response_format.schema must be a JSON Schema object or boolean",
412 ));
413 }
414 };
415
416 JsonCompileOptions {
420 whitespace_flexible: false,
421 ..JsonCompileOptions::default()
422 }
423 .apply_to(&mut schema);
424 Ok(schema)
425}
426
427pub struct StructuredOutputProcessor {
429 state: Mutex<ProcessorState>,
430 vocab_size: usize,
431 defined_token_ids: Arc<[bool]>,
432 json_token_classes: Arc<[StructuredOutputTokenClass]>,
433 budget: Option<StructuredOutputBudgetPlan>,
434 liveness: StructuredOutputLivenessPolicy,
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub enum StructuredOutputPhase {
440 WaitingForDelimiter,
441 ForcingDelimiter,
442 EnforcingGrammar,
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451#[repr(u8)]
452pub enum StructuredOutputTokenClass {
453 Whitespace,
454 Number,
455 Structural,
456 StringBoundary,
457 Literal,
458 Other,
459 Control,
460 Undefined,
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465pub struct StructuredOutputMaskOutcome {
466 pub phase: StructuredOutputPhase,
467 pub accepting: bool,
468 pub liveness_intervention: bool,
469 pub grammar_start_token_index: Option<usize>,
474 pub required_delimiter_token_id: Option<u32>,
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub struct StructuredOutputProgress {
485 pub phase: StructuredOutputPhase,
486 pub generated_token_count: usize,
487 pub consumed_token_count: usize,
488 pub delimiter_token_count: Option<usize>,
489 pub delimiter_prefix_token_count: usize,
490 pub reasoning_token_count: Option<usize>,
491 pub boundary_forced: bool,
492 pub budget: Option<StructuredOutputBudgetPlan>,
493 pub grammar_token_count: usize,
494 pub trailing_token_class: Option<StructuredOutputTokenClass>,
495 pub trailing_token_class_count: usize,
496 pub trailing_token_id: Option<u32>,
497 pub trailing_identical_token_count: usize,
498 pub liveness_identical_token_limit: usize,
499 pub liveness_intervention_count: usize,
500 pub accepting: bool,
501}
502
503impl std::fmt::Debug for StructuredOutputProcessor {
504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505 let state = self.state.lock();
506 f.debug_struct("StructuredOutputProcessor")
507 .field("vocab_size", &self.vocab_size)
508 .field("consumed", &state.consumed)
509 .field("active", &state.grammar_start.is_some())
510 .field("budget", &self.budget)
511 .finish()
512 }
513}
514
515struct ProcessorState {
516 matcher: Matcher,
517 activation: Activation,
518 initial_activation: Activation,
519 consumed: usize,
520 boundary_forced: bool,
521 boundary_start: Option<usize>,
522 grammar_start: Option<usize>,
523 trailing_grammar_token_id: Option<u32>,
524 trailing_identical_token_count: usize,
525 liveness_intervention_count: usize,
526 last_liveness_intervention_at: Option<usize>,
527}
528
529#[derive(Clone)]
530enum Activation {
531 Active,
532 Harmony(harmony::HarmonyBoundary),
533 Boundary {
534 opening_tokens: Vec<u32>,
535 delimiter_tokens: Vec<u32>,
536 allow_reasoning: bool,
537 forcing: bool,
538 },
539}
540
541impl StructuredOutputProcessor {
542 pub fn mask_logits(&self, logits: &mut [f32], generated: &[TokenId]) -> Result<()> {
546 self.mask_logits_inner(logits, generated, None, None)
547 .map(|_| ())
548 }
549
550 pub fn mask_logits_with_terminals(
555 &self,
556 logits: &mut [f32],
557 generated: &[TokenId],
558 terminal_token_ids: &HashSet<u32>,
559 hidden_control_token_ids: &HashSet<u32>,
560 ) -> Result<StructuredOutputMaskOutcome> {
561 self.mask_logits_inner(
562 logits,
563 generated,
564 Some(terminal_token_ids),
565 Some(hidden_control_token_ids),
566 )
567 }
568
569 fn mask_logits_inner(
570 &self,
571 logits: &mut [f32],
572 generated: &[TokenId],
573 terminal_token_ids: Option<&HashSet<u32>>,
574 hidden_control_token_ids: Option<&HashSet<u32>>,
575 ) -> Result<StructuredOutputMaskOutcome> {
576 let mut state = self.state.lock();
577 advance_state(&mut state, generated, terminal_token_ids, self.budget)?;
578 activate_forcing_if_due(&mut state, generated, self.budget);
579 if let Activation::Harmony(boundary) = &state.activation {
580 if !boundary.in_payload() {
581 self.mask_undefined_token_ids(logits);
582 let required_delimiter_token_id =
583 boundary.mask_before_payload(logits, hidden_control_token_ids)?;
584 return Ok(StructuredOutputMaskOutcome {
585 phase: boundary.progress().0,
586 accepting: false,
587 liveness_intervention: false,
588 grammar_start_token_index: None,
589 required_delimiter_token_id,
590 });
591 }
592 }
593 if let Activation::Boundary {
594 opening_tokens,
595 delimiter_tokens,
596 forcing,
597 ..
598 } = &state.activation
599 {
600 self.mask_undefined_token_ids(logits);
601 let opening_token = opening_tokens.get(generated.len()).copied();
602 let after_opening = generated.get(opening_tokens.len()..).unwrap_or_default();
603 let delimiter_prefix_token_count =
604 delimiter_prefix_token_count(after_opening, delimiter_tokens);
605 let required_delimiter_token = opening_token
606 .or_else(|| delimiter_tokens.get(delimiter_prefix_token_count).copied())
607 .ok_or_else(|| {
608 FerrumError::internal("structured-output delimiter state has no next token")
609 })?;
610 let force_boundary = opening_token.is_some() || *forcing;
611 if force_boundary {
612 force_exact_token(logits, required_delimiter_token)?;
613 } else if let Some(hidden_control_token_ids) = hidden_control_token_ids {
614 for token_id in hidden_control_token_ids {
615 if required_delimiter_token == *token_id {
616 continue;
617 }
618 if let Some(logit) = logits.get_mut(*token_id as usize) {
619 *logit = f32::NEG_INFINITY;
620 }
621 }
622 }
623 return Ok(StructuredOutputMaskOutcome {
624 phase: if force_boundary {
625 StructuredOutputPhase::ForcingDelimiter
626 } else {
627 StructuredOutputPhase::WaitingForDelimiter
628 },
629 accepting: false,
630 liveness_intervention: false,
631 grammar_start_token_index: None,
632 required_delimiter_token_id: Some(required_delimiter_token),
633 });
634 }
635
636 let grammar_start_token_index = state.grammar_start.ok_or_else(|| {
637 FerrumError::internal(
638 "active structured-output processor has no grammar start token index",
639 )
640 })?;
641
642 let accepting = state.matcher.is_accepting().map_err(|error| {
643 FerrumError::model(format!(
644 "structured-output acceptance check failed: {error}"
645 ))
646 })?;
647 let mask = state.matcher.compute_mask_or_eos().map_err(|error| {
648 FerrumError::model(format!("structured-output mask failed: {error}"))
649 })?;
650 let mut finite_allowed = 0usize;
651 for (idx, logit) in logits.iter_mut().enumerate() {
652 let token = idx as u32;
653 let allowed_terminal = match &state.activation {
654 Activation::Harmony(boundary) => boundary.is_terminal(token),
655 _ => terminal_token_ids.is_some_and(|terminals| terminals.contains(&token)),
656 };
657 let hidden_non_terminal_control = hidden_control_token_ids
658 .is_some_and(|controls| controls.contains(&token))
659 && !allowed_terminal;
660 let protocol_control = matches!(&state.activation,
661 Activation::Harmony(boundary) if boundary.is_control(token) && !allowed_terminal);
662 let allowed = idx < self.vocab_size
663 && self.defined_token_ids.get(idx).copied().unwrap_or(false)
664 && !hidden_non_terminal_control
665 && !protocol_control
666 && (mask.is_allowed(token) || (accepting && allowed_terminal));
667 if !allowed {
668 *logit = f32::NEG_INFINITY;
669 } else if logit.is_finite() {
670 finite_allowed += 1;
671 }
672 }
673 if finite_allowed == 0 {
674 return Err(FerrumError::model(
675 "structured-output grammar has no legal finite token",
676 ));
677 }
678 let liveness_intervention = if !accepting
679 && state.trailing_identical_token_count >= self.liveness.max_identical_token_run
680 {
681 state
682 .trailing_grammar_token_id
683 .and_then(|token| logits.get_mut(token as usize))
684 .is_some_and(|logit| {
685 if logit.is_finite() && finite_allowed > 1 {
686 *logit = f32::NEG_INFINITY;
687 if state.last_liveness_intervention_at != Some(generated.len()) {
688 state.liveness_intervention_count += 1;
689 state.last_liveness_intervention_at = Some(generated.len());
690 }
691 true
692 } else {
693 false
694 }
695 })
696 } else {
697 false
698 };
699 Ok(StructuredOutputMaskOutcome {
700 phase: StructuredOutputPhase::EnforcingGrammar,
701 accepting,
702 liveness_intervention,
703 grammar_start_token_index: Some(grammar_start_token_index),
704 required_delimiter_token_id: None,
705 })
706 }
707
708 fn mask_undefined_token_ids(&self, logits: &mut [f32]) {
709 for (idx, logit) in logits.iter_mut().enumerate() {
710 if !self.defined_token_ids.get(idx).copied().unwrap_or(false) {
711 *logit = f32::NEG_INFINITY;
712 }
713 }
714 }
715
716 pub fn is_accepting(&self, generated: &[TokenId]) -> Result<bool> {
719 self.is_accepting_inner(generated, None)
720 }
721
722 pub fn is_accepting_with_terminals(
725 &self,
726 generated: &[TokenId],
727 terminal_token_ids: &HashSet<u32>,
728 ) -> Result<bool> {
729 self.is_accepting_inner(generated, Some(terminal_token_ids))
730 }
731
732 fn is_accepting_inner(
733 &self,
734 generated: &[TokenId],
735 terminal_token_ids: Option<&HashSet<u32>>,
736 ) -> Result<bool> {
737 Ok(self
738 .progress_inner(generated, terminal_token_ids)?
739 .accepting)
740 }
741
742 pub fn progress_with_terminals(
744 &self,
745 generated: &[TokenId],
746 terminal_token_ids: &HashSet<u32>,
747 ) -> Result<StructuredOutputProgress> {
748 self.progress_inner(generated, Some(terminal_token_ids))
749 }
750
751 fn progress_inner(
752 &self,
753 generated: &[TokenId],
754 terminal_token_ids: Option<&HashSet<u32>>,
755 ) -> Result<StructuredOutputProgress> {
756 let mut state = self.state.lock();
757 advance_state(&mut state, generated, terminal_token_ids, self.budget)?;
758 activate_forcing_if_due(&mut state, generated, self.budget);
759 let (phase, delimiter_token_count, delimiter_prefix_token_count, accepting) =
760 match &state.activation {
761 Activation::Boundary {
762 opening_tokens,
763 delimiter_tokens,
764 forcing,
765 ..
766 } => (
767 if generated.len() < opening_tokens.len() || *forcing {
768 StructuredOutputPhase::ForcingDelimiter
769 } else {
770 StructuredOutputPhase::WaitingForDelimiter
771 },
772 Some(opening_tokens.len() + delimiter_tokens.len()),
773 generated.len().min(opening_tokens.len())
774 + delimiter_prefix_token_count(
775 generated.get(opening_tokens.len()..).unwrap_or_default(),
776 delimiter_tokens,
777 ),
778 false,
779 ),
780 Activation::Active => (
781 StructuredOutputPhase::EnforcingGrammar,
782 None,
783 0,
784 state.matcher.is_accepting().map_err(|error| {
785 FerrumError::model(format!(
786 "structured-output acceptance check failed: {error}"
787 ))
788 })?,
789 ),
790 Activation::Harmony(boundary) => {
791 let (phase, length, prefix) = boundary.progress();
792 (
793 phase,
794 Some(length),
795 prefix,
796 boundary.in_payload()
797 && state.matcher.is_accepting().map_err(|error| {
798 FerrumError::model(format!(
799 "structured-output acceptance check failed: {error}"
800 ))
801 })?,
802 )
803 }
804 };
805 let grammar_tokens = state
806 .grammar_start
807 .and_then(|start| generated.get(start..))
808 .unwrap_or_default();
809 let trailing_token_id = grammar_tokens.last().map(|token| token.get());
810 let trailing_token_class = trailing_token_id.map(|token| {
811 self.json_token_classes
812 .get(token as usize)
813 .copied()
814 .unwrap_or(StructuredOutputTokenClass::Undefined)
815 });
816 let trailing_token_class_count = trailing_token_class.map_or(0, |class| {
817 grammar_tokens
818 .iter()
819 .rev()
820 .take_while(|token| {
821 self.json_token_classes
822 .get(token.get() as usize)
823 .copied()
824 .unwrap_or(StructuredOutputTokenClass::Undefined)
825 == class
826 })
827 .count()
828 });
829 let trailing_identical_token_count = trailing_token_id.map_or(0, |token_id| {
830 grammar_tokens
831 .iter()
832 .rev()
833 .take_while(|token| token.get() == token_id)
834 .count()
835 });
836 let opening_token_count = match &state.initial_activation {
837 Activation::Boundary { opening_tokens, .. } => opening_tokens.len(),
838 _ => 0,
839 };
840 Ok(StructuredOutputProgress {
841 phase,
842 generated_token_count: generated.len(),
843 consumed_token_count: state.consumed,
844 delimiter_token_count: delimiter_token_count
845 .or(self.budget.map(|budget| budget.boundary_token_count)),
846 delimiter_prefix_token_count,
847 reasoning_token_count: self.budget.map(|_| {
848 state
849 .boundary_start
850 .unwrap_or(generated.len())
851 .saturating_sub(opening_token_count)
852 }),
853 boundary_forced: state.boundary_forced,
854 budget: self.budget,
855 grammar_token_count: grammar_tokens.len(),
856 trailing_token_class,
857 trailing_token_class_count,
858 trailing_token_id,
859 trailing_identical_token_count,
860 liveness_identical_token_limit: self.liveness.max_identical_token_run,
861 liveness_intervention_count: state.liveness_intervention_count,
862 accepting,
863 })
864 }
865
866 pub fn reset(&self) -> Result<()> {
867 let mut state = self.state.lock();
868 state
869 .matcher
870 .reset()
871 .map_err(|error| FerrumError::internal(format!("reset structured output: {error}")))?;
872 state.activation = state.initial_activation.clone();
873 state.consumed = 0;
874 state.boundary_forced = false;
875 state.boundary_start = None;
876 state.grammar_start = matches!(state.initial_activation, Activation::Active).then_some(0);
877 state.trailing_grammar_token_id = None;
878 state.trailing_identical_token_count = 0;
879 state.liveness_intervention_count = 0;
880 state.last_liveness_intervention_at = None;
881 Ok(())
882 }
883}
884
885fn activate_forcing_if_due(
886 state: &mut ProcessorState,
887 generated: &[TokenId],
888 budget: Option<StructuredOutputBudgetPlan>,
889) {
890 if let Activation::Harmony(boundary) = &mut state.activation {
891 boundary.activate_forcing_if_due(generated.len());
892 state.boundary_forced = boundary.is_forced();
893 return;
894 }
895 let Some(budget) = budget else {
896 return;
897 };
898 let should_force = match &state.activation {
899 Activation::Boundary {
900 opening_tokens,
901 allow_reasoning,
902 forcing,
903 ..
904 } => {
905 !forcing
906 && generated.len() >= opening_tokens.len()
907 && (!allow_reasoning
908 || generated.len() - opening_tokens.len() >= budget.reasoning_token_limit)
909 }
910 _ => false,
911 };
912 if should_force {
913 let delimiter_prefix_token_count = match &state.activation {
914 Activation::Boundary {
915 opening_tokens,
916 delimiter_tokens,
917 ..
918 } => delimiter_prefix_token_count(&generated[opening_tokens.len()..], delimiter_tokens),
919 Activation::Active | Activation::Harmony(_) => 0,
920 };
921 if let Activation::Boundary { forcing, .. } = &mut state.activation {
922 *forcing = true;
923 }
924 state.boundary_forced = true;
925 state.boundary_start = Some(generated.len() - delimiter_prefix_token_count);
926 }
927}
928
929fn force_exact_token(logits: &mut [f32], required_token: u32) -> Result<()> {
930 let required_index = required_token as usize;
931 if required_index >= logits.len() {
932 return Err(FerrumError::model(format!(
933 "structured-output delimiter token {required_token} is outside logits width {}",
934 logits.len()
935 )));
936 }
937 logits.fill(f32::NEG_INFINITY);
938 logits[required_index] = 0.0;
939 Ok(())
940}
941
942fn delimiter_prefix_token_count(generated: &[TokenId], delimiter_tokens: &[u32]) -> usize {
943 let max_prefix = generated
944 .len()
945 .min(delimiter_tokens.len().saturating_sub(1));
946 (1..=max_prefix)
947 .rev()
948 .find(|prefix_len| {
949 generated[generated.len() - prefix_len..]
950 .iter()
951 .zip(&delimiter_tokens[..*prefix_len])
952 .all(|(token, expected)| token.get() == *expected)
953 })
954 .unwrap_or(0)
955}
956
957fn advance_state(
958 state: &mut ProcessorState,
959 generated: &[TokenId],
960 terminal_token_ids: Option<&HashSet<u32>>,
961 budget: Option<StructuredOutputBudgetPlan>,
962) -> Result<()> {
963 if state.consumed > generated.len() {
964 return Err(FerrumError::internal(
965 "structured-output token history moved backwards without reset",
966 ));
967 }
968
969 if let Activation::Harmony(boundary) = &mut state.activation {
970 while state.consumed < generated.len() && !boundary.in_payload() {
971 let index = state.consumed;
972 if let Some(start) = boundary.observe(generated[index].get(), index)? {
973 state.grammar_start = Some(start);
974 state.boundary_start = Some(boundary.boundary_start());
975 }
976 state.consumed = index + 1;
977 }
978 state.boundary_forced = boundary.is_forced();
979 if !boundary.in_payload() {
980 return Ok(());
981 }
982 }
983
984 if let Activation::Boundary {
985 opening_tokens,
986 delimiter_tokens,
987 allow_reasoning,
988 ..
989 } = &state.activation
990 {
991 if opening_tokens
992 .iter()
993 .zip(generated)
994 .any(|(expected, token)| *expected != token.get())
995 {
996 return Err(FerrumError::model(
997 "structured-output reasoning envelope has an invalid opening header",
998 ));
999 }
1000 if generated.len() < opening_tokens.len() {
1001 state.consumed = generated.len();
1002 return Ok(());
1003 }
1004 if !allow_reasoning
1005 && delimiter_tokens
1006 .iter()
1007 .zip(&generated[opening_tokens.len()..])
1008 .any(|(expected, token)| *expected != token.get())
1009 {
1010 return Err(FerrumError::model(
1011 "structured-output reasoning is disabled; the envelope must close immediately",
1012 ));
1013 }
1014 let search_from = state
1015 .consumed
1016 .saturating_sub(delimiter_tokens.len())
1017 .max(opening_tokens.len());
1018 if let Some(offset) = generated[search_from..]
1019 .windows(delimiter_tokens.len())
1020 .position(|window| {
1021 window
1022 .iter()
1023 .zip(delimiter_tokens)
1024 .all(|(token, expected)| token.get() == *expected)
1025 })
1026 {
1027 let grammar_start = search_from + offset + delimiter_tokens.len();
1028 if !opening_tokens.is_empty() {
1029 let reasoning_count = grammar_start - delimiter_tokens.len() - opening_tokens.len();
1030 state.boundary_forced |= !allow_reasoning
1031 || budget.is_some_and(|plan| {
1032 reasoning_count + delimiter_tokens.len() - 1 >= plan.reasoning_token_limit
1033 });
1034 }
1035 state.boundary_start = Some(grammar_start - delimiter_tokens.len());
1036 state.grammar_start = Some(grammar_start);
1037 state.activation = Activation::Active;
1038 state.consumed = grammar_start;
1039 state.trailing_grammar_token_id = None;
1040 state.trailing_identical_token_count = 0;
1041 } else {
1042 state.consumed = generated.len();
1043 return Ok(());
1044 }
1045 }
1046
1047 for token in &generated[state.consumed..] {
1048 if let Activation::Harmony(boundary) = &mut state.activation {
1049 let accepting = state.matcher.is_accepting().map_err(|error| {
1050 FerrumError::model(format!(
1051 "structured-output acceptance check failed: {error}"
1052 ))
1053 })?;
1054 if boundary.observe_payload_control(token.get(), accepting)? {
1055 continue;
1056 }
1057 } else if terminal_token_ids.is_some_and(|terminals| terminals.contains(&token.get()))
1058 && state.matcher.is_accepting().map_err(|error| {
1059 FerrumError::model(format!(
1060 "structured-output acceptance check failed: {error}"
1061 ))
1062 })?
1063 {
1064 continue;
1065 }
1066 state.matcher.consume_token(token.get()).map_err(|error| {
1067 FerrumError::model(format!(
1068 "structured-output token {} violated the grammar: {error}",
1069 token.get()
1070 ))
1071 })?;
1072 if state.trailing_grammar_token_id == Some(token.get()) {
1073 state.trailing_identical_token_count += 1;
1074 } else {
1075 state.trailing_grammar_token_id = Some(token.get());
1076 state.trailing_identical_token_count = 1;
1077 }
1078 }
1079 state.consumed = generated.len();
1080 Ok(())
1081}
1082
1083struct FerrumTokenizerEnv {
1084 tokenizer: Arc<dyn Tokenizer + Send + Sync>,
1085 trie: TokTrie,
1086}
1087
1088impl TokenizerEnv for FerrumTokenizerEnv {
1089 fn tok_trie(&self) -> &TokTrie {
1090 &self.trie
1091 }
1092
1093 fn tokenize_bytes(&self, bytes: &[u8]) -> Vec<u32> {
1094 str::from_utf8(bytes)
1095 .ok()
1096 .and_then(|text| self.tokenizer.encode(text, false).ok())
1097 .map(|tokens| tokens.into_iter().map(|token| token.get()).collect())
1098 .unwrap_or_else(|| self.trie.greedy_tokenize(bytes))
1099 }
1100
1101 fn tokenize_is_canonical(&self) -> bool {
1102 false
1103 }
1104}
1105
1106fn tokenizer_special_ids(tokenizer: &(dyn Tokenizer + Send + Sync)) -> HashSet<u32> {
1107 let special = tokenizer.special_tokens();
1108 [
1109 special.bos_token,
1110 special.eos_token,
1111 special.unk_token,
1112 special.pad_token,
1113 special.sep_token,
1114 special.cls_token,
1115 special.mask_token,
1116 ]
1117 .into_iter()
1118 .flatten()
1119 .chain(special.extra_eos_tokens.iter().copied())
1120 .map(|token| token.get())
1121 .collect()
1122}
1123
1124fn special_token_marker(token: TokenId) -> Vec<u8> {
1125 let mut marker = vec![TokTrie::SPECIAL_TOKEN_MARKER];
1126 marker.extend_from_slice(format!("[{}]", token.get()).as_bytes());
1127 marker
1128}
1129
1130fn classify_json_token_bytes(bytes: &[u8]) -> StructuredOutputTokenClass {
1131 if bytes.is_empty() {
1132 StructuredOutputTokenClass::Undefined
1133 } else if bytes
1134 .iter()
1135 .all(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
1136 {
1137 StructuredOutputTokenClass::Whitespace
1138 } else if bytes
1139 .iter()
1140 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'-' | b'+' | b'.' | b'e' | b'E'))
1141 {
1142 StructuredOutputTokenClass::Number
1143 } else if bytes
1144 .iter()
1145 .all(|byte| matches!(byte, b'{' | b'}' | b'[' | b']' | b',' | b':'))
1146 {
1147 StructuredOutputTokenClass::Structural
1148 } else if bytes.iter().all(|byte| matches!(byte, b'"' | b'\\')) {
1149 StructuredOutputTokenClass::StringBoundary
1150 } else if bytes.iter().all(u8::is_ascii_alphabetic) {
1151 StructuredOutputTokenClass::Literal
1152 } else {
1153 StructuredOutputTokenClass::Other
1154 }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160 use ferrum_interfaces::tokenizer::{ChatMessage, TokenizerInfo, TokenizerType};
1161 use ferrum_types::SpecialTokens;
1162
1163 const EOS: u32 = 256;
1164 const TEST_MAX_OUTPUT_TOKENS: usize = 128;
1165
1166 pub(super) struct ByteTokenizer {
1167 special: SpecialTokens,
1168 token_text: Vec<String>,
1169 }
1170
1171 impl ByteTokenizer {
1172 pub(super) fn new() -> Self {
1173 let mut token_text = (0u16..=255)
1174 .map(|byte| char::from_u32(byte as u32).unwrap().to_string())
1175 .collect::<Vec<_>>();
1176 token_text.push("<eos>".to_string());
1177 Self {
1178 special: SpecialTokens {
1179 eos_token: Some(TokenId::new(EOS)),
1180 ..SpecialTokens::default()
1181 },
1182 token_text,
1183 }
1184 }
1185 }
1186
1187 impl Tokenizer for ByteTokenizer {
1188 fn encode(&self, text: &str, _add_special: bool) -> Result<Vec<TokenId>> {
1189 Ok(text
1190 .as_bytes()
1191 .iter()
1192 .map(|byte| TokenId::new(*byte as u32))
1193 .collect())
1194 }
1195
1196 fn decode(&self, tokens: &[TokenId], _skip_special: bool) -> Result<String> {
1197 Ok(tokens
1198 .iter()
1199 .filter(|token| token.get() < 256)
1200 .map(|token| token.get() as u8 as char)
1201 .collect())
1202 }
1203
1204 fn decode_incremental(&self, _prev: &[TokenId], next: TokenId) -> Result<String> {
1205 self.decode(&[next], true)
1206 }
1207
1208 fn vocab_size(&self) -> usize {
1209 self.token_text.len()
1210 }
1211
1212 fn special_tokens(&self) -> &SpecialTokens {
1213 &self.special
1214 }
1215
1216 fn token_id(&self, text: &str) -> Option<TokenId> {
1217 (text.len() == 1).then(|| TokenId::new(text.as_bytes()[0] as u32))
1218 }
1219
1220 fn token_text(&self, token_id: TokenId) -> Option<&str> {
1221 self.token_text
1222 .get(token_id.get() as usize)
1223 .map(String::as_str)
1224 }
1225
1226 fn apply_chat_template(&self, messages: &[ChatMessage]) -> Result<String> {
1227 Ok(messages
1228 .iter()
1229 .map(|message| message.content.as_str())
1230 .collect::<Vec<_>>()
1231 .join("\n"))
1232 }
1233
1234 fn info(&self) -> TokenizerInfo {
1235 TokenizerInfo {
1236 tokenizer_type: TokenizerType::BPE,
1237 vocab_size: self.vocab_size(),
1238 special_tokens: self.special.clone(),
1239 supports_incremental: true,
1240 supports_chat_template: false,
1241 max_token_length: Some(1),
1242 model_name: Some("byte-test".to_string()),
1243 }
1244 }
1245 }
1246
1247 struct MergedObjectTokenizer {
1248 inner: ByteTokenizer,
1249 }
1250
1251 impl MergedObjectTokenizer {
1252 const OBJECT: u32 = 256;
1253 const EOS: u32 = 257;
1254
1255 fn new() -> Self {
1256 let mut inner = ByteTokenizer::new();
1257 inner.token_text[EOS as usize] = "{}".to_string();
1258 inner.token_text.push("<eos>".to_string());
1259 inner.special.eos_token = Some(TokenId::new(Self::EOS));
1260 Self { inner }
1261 }
1262 }
1263
1264 impl Tokenizer for MergedObjectTokenizer {
1265 fn encode(&self, text: &str, add_special: bool) -> Result<Vec<TokenId>> {
1266 self.inner.encode(text, add_special)
1267 }
1268
1269 fn decode(&self, tokens: &[TokenId], skip_special: bool) -> Result<String> {
1270 let mut decoded = String::new();
1271 for token in tokens {
1272 match token.get() {
1273 Self::OBJECT => decoded.push_str("{}"),
1274 Self::EOS if skip_special => {}
1275 Self::EOS => decoded.push_str("<eos>"),
1276 _ => decoded.push_str(&self.inner.decode(&[*token], skip_special)?),
1277 }
1278 }
1279 Ok(decoded)
1280 }
1281
1282 fn decode_incremental(&self, _prev: &[TokenId], next: TokenId) -> Result<String> {
1283 self.decode(&[next], true)
1284 }
1285
1286 fn vocab_size(&self) -> usize {
1287 self.inner.token_text.len()
1288 }
1289
1290 fn special_tokens(&self) -> &SpecialTokens {
1291 &self.inner.special
1292 }
1293
1294 fn token_id(&self, text: &str) -> Option<TokenId> {
1295 (text == "{}")
1296 .then(|| TokenId::new(Self::OBJECT))
1297 .or_else(|| self.inner.token_id(text))
1298 }
1299
1300 fn token_text(&self, token_id: TokenId) -> Option<&str> {
1301 self.inner.token_text(token_id)
1302 }
1303
1304 fn info(&self) -> TokenizerInfo {
1305 TokenizerInfo {
1306 vocab_size: self.vocab_size(),
1307 max_token_length: Some(2),
1308 model_name: Some("merged-object-test".to_string()),
1309 ..self.inner.info()
1310 }
1311 }
1312 }
1313
1314 fn factory() -> StructuredOutputFactory {
1315 StructuredOutputFactory::new(Arc::new(ByteTokenizer::new())).unwrap()
1316 }
1317
1318 fn assert_and_append(
1319 processor: &StructuredOutputProcessor,
1320 generated: &mut Vec<TokenId>,
1321 text: &str,
1322 ) {
1323 for byte in text.bytes() {
1324 let mut logits = vec![0.0; EOS as usize + 1];
1325 processor.mask_logits(&mut logits, generated).unwrap();
1326 assert!(
1327 logits[byte as usize].is_finite(),
1328 "byte {byte:?} rejected after {:?}",
1329 generated
1330 );
1331 generated.push(TokenId::new(byte as u32));
1332 }
1333 }
1334
1335 #[test]
1336 fn json_object_hard_masks_non_object_roots() {
1337 let processor = factory()
1338 .create_processor(
1339 &ResponseFormat::JsonObject,
1340 &StructuredOutputStart::Immediate,
1341 TEST_MAX_OUTPUT_TOKENS,
1342 &HashSet::new(),
1343 &[],
1344 )
1345 .unwrap()
1346 .unwrap();
1347 let mut logits = vec![0.0; EOS as usize + 1];
1348 processor.mask_logits(&mut logits, &[]).unwrap();
1349 assert!(logits[b'{' as usize].is_finite());
1350 assert!(!logits[b'[' as usize].is_finite());
1351 assert!(!logits[b'`' as usize].is_finite());
1352 assert!(!logits[EOS as usize].is_finite());
1353 }
1354
1355 #[test]
1356 fn json_object_uses_compact_separators_without_unbounded_whitespace() {
1357 let processor = factory()
1358 .create_processor(
1359 &ResponseFormat::JsonObject,
1360 &StructuredOutputStart::Immediate,
1361 TEST_MAX_OUTPUT_TOKENS,
1362 &HashSet::new(),
1363 &[],
1364 )
1365 .unwrap()
1366 .unwrap();
1367 let generated = vec![TokenId::new(b'{' as u32)];
1368 let mut logits = vec![0.0; EOS as usize + 1];
1369 processor.mask_logits(&mut logits, &generated).unwrap();
1370
1371 assert!(!logits[b' ' as usize].is_finite());
1372 assert!(logits[b'}' as usize].is_finite());
1373 assert!(logits[b'"' as usize].is_finite());
1374 }
1375
1376 #[test]
1377 fn undefined_model_vocab_ids_are_masked_inside_wildcard_strings() {
1378 let tokenizer = Arc::new(ByteTokenizer::new());
1379 let undefined_token = tokenizer.vocab_size() as u32;
1380 assert_eq!(
1381 tokenizer.token_bytes(TokenId::new(undefined_token)),
1382 Some(Vec::new()),
1383 "the test tokenizer must reproduce a decoder that returns empty text for an unknown id"
1384 );
1385 let processor = StructuredOutputFactory::new_with_model_vocab_size(
1386 tokenizer,
1387 Some(undefined_token as usize + 1),
1388 )
1389 .unwrap()
1390 .create_processor(
1391 &ResponseFormat::JsonObject,
1392 &StructuredOutputStart::Immediate,
1393 TEST_MAX_OUTPUT_TOKENS,
1394 &HashSet::new(),
1395 &[],
1396 )
1397 .unwrap()
1398 .unwrap();
1399
1400 let mut generated = Vec::new();
1401 assert_and_append(&processor, &mut generated, r#"{"value":"Ferrum "#);
1402 let mut logits = vec![0.0; undefined_token as usize + 1];
1403 processor.mask_logits(&mut logits, &generated).unwrap();
1404 assert!(logits[b'x' as usize].is_finite());
1405 assert!(!logits[undefined_token as usize].is_finite());
1406 }
1407
1408 #[test]
1409 fn json_object_accepts_nested_unicode_escape_and_eos_only_after_close() {
1410 let processor = factory()
1411 .create_processor(
1412 &ResponseFormat::JsonObject,
1413 &StructuredOutputStart::Immediate,
1414 TEST_MAX_OUTPUT_TOKENS,
1415 &HashSet::new(),
1416 &[],
1417 )
1418 .unwrap()
1419 .unwrap();
1420 let mut generated = Vec::new();
1421 assert_and_append(
1422 &processor,
1423 &mut generated,
1424 r#"{"items":[true,null,{"name":"line\u000A"}],"n":-1.2e+3}"#,
1425 );
1426 assert!(processor.is_accepting(&generated).unwrap());
1427 let mut logits = vec![0.0; EOS as usize + 1];
1428 processor.mask_logits(&mut logits, &generated).unwrap();
1429 assert!(logits[EOS as usize].is_finite());
1430 assert!(!logits[b'x' as usize].is_finite());
1431 }
1432
1433 #[test]
1434 fn json_object_accepts_a_complete_root_from_one_merged_token() {
1435 let processor = StructuredOutputFactory::new(Arc::new(MergedObjectTokenizer::new()))
1436 .unwrap()
1437 .create_processor(
1438 &ResponseFormat::JsonObject,
1439 &StructuredOutputStart::Immediate,
1440 TEST_MAX_OUTPUT_TOKENS,
1441 &HashSet::new(),
1442 &[],
1443 )
1444 .unwrap()
1445 .unwrap();
1446 let generated = vec![TokenId::new(MergedObjectTokenizer::OBJECT)];
1447 let terminals = HashSet::from([MergedObjectTokenizer::EOS]);
1448
1449 let progress = processor
1450 .progress_with_terminals(&generated, &terminals)
1451 .unwrap();
1452 assert!(progress.accepting);
1453
1454 let mut logits = vec![0.0; MergedObjectTokenizer::EOS as usize + 1];
1455 let outcome = processor
1456 .mask_logits_with_terminals(&mut logits, &generated, &terminals, &HashSet::new())
1457 .unwrap();
1458 assert!(outcome.accepting);
1459 assert_eq!(outcome.grammar_start_token_index, Some(0));
1460 assert!(!logits[MergedObjectTokenizer::OBJECT as usize].is_finite());
1461 assert!(logits[MergedObjectTokenizer::EOS as usize].is_finite());
1462 }
1463
1464 #[test]
1465 fn json_object_breaks_an_unbounded_identical_token_run_when_closure_is_legal() {
1466 let processor = StructuredOutputFactory::new(Arc::new(MergedObjectTokenizer::new()))
1467 .unwrap()
1468 .create_processor(
1469 &ResponseFormat::JsonObject,
1470 &StructuredOutputStart::Immediate,
1471 64,
1472 &HashSet::new(),
1473 &[],
1474 )
1475 .unwrap()
1476 .unwrap();
1477 let mut generated =
1478 br#"{"marker":""#.iter().map(|byte| TokenId::new(*byte as u32)).collect::<Vec<_>>();
1479 generated.extend(std::iter::repeat_n(
1480 TokenId::new(MergedObjectTokenizer::OBJECT),
1481 32,
1482 ));
1483
1484 let mut logits = vec![0.0; MergedObjectTokenizer::EOS as usize + 1];
1485 let outcome = processor
1486 .mask_logits_with_terminals(
1487 &mut logits,
1488 &generated,
1489 &HashSet::from([MergedObjectTokenizer::EOS]),
1490 &HashSet::new(),
1491 )
1492 .unwrap();
1493
1494 assert!(!outcome.accepting);
1495 assert!(outcome.liveness_intervention);
1496 assert!(!logits[MergedObjectTokenizer::OBJECT as usize].is_finite());
1497 assert!(logits[b'"' as usize].is_finite());
1498
1499 generated.extend([TokenId::new(b'"' as u32), TokenId::new(b'}' as u32)]);
1500 let progress = processor
1501 .progress_with_terminals(&generated, &HashSet::from([MergedObjectTokenizer::EOS]))
1502 .unwrap();
1503 assert!(progress.accepting);
1504 assert_eq!(progress.liveness_identical_token_limit, 32);
1505 assert_eq!(progress.liveness_intervention_count, 1);
1506 }
1507
1508 #[test]
1509 fn structured_liveness_guard_preserves_the_only_finite_grammar_candidate() {
1510 let processor = StructuredOutputFactory::new(Arc::new(MergedObjectTokenizer::new()))
1511 .unwrap()
1512 .create_processor(
1513 &ResponseFormat::JsonObject,
1514 &StructuredOutputStart::Immediate,
1515 64,
1516 &HashSet::new(),
1517 &[],
1518 )
1519 .unwrap()
1520 .unwrap();
1521 let mut generated =
1522 br#"{"marker":""#.iter().map(|byte| TokenId::new(*byte as u32)).collect::<Vec<_>>();
1523 generated.extend(std::iter::repeat_n(
1524 TokenId::new(MergedObjectTokenizer::OBJECT),
1525 32,
1526 ));
1527 let mut logits = vec![f32::NEG_INFINITY; MergedObjectTokenizer::EOS as usize + 1];
1528 logits[MergedObjectTokenizer::OBJECT as usize] = 0.0;
1529
1530 let outcome = processor
1531 .mask_logits_with_terminals(
1532 &mut logits,
1533 &generated,
1534 &HashSet::from([MergedObjectTokenizer::EOS]),
1535 &HashSet::new(),
1536 )
1537 .unwrap();
1538
1539 assert!(!outcome.liveness_intervention);
1540 assert!(logits[MergedObjectTokenizer::OBJECT as usize].is_finite());
1541 }
1542
1543 #[test]
1544 fn structured_liveness_limit_is_derived_from_the_guaranteed_result_budget() {
1545 let budget = StructuredOutputBudgetPlan::automatic(4096, 1).unwrap();
1546 assert_eq!(budget.structured_reserve_tokens, 1024);
1547 assert_eq!(
1548 StructuredOutputLivenessPolicy::for_request(4096, Some(budget)).max_identical_token_run,
1549 512
1550 );
1551 assert_eq!(
1552 StructuredOutputLivenessPolicy::for_request(64, None).max_identical_token_run,
1553 32
1554 );
1555 }
1556
1557 #[test]
1558 fn strict_schema_rejects_wrong_property_and_accepts_required_value() {
1559 let schema = r#"{
1560 "type":"object",
1561 "properties":{"answer":{"const":42}},
1562 "required":["answer"],
1563 "additionalProperties":false
1564 }"#;
1565 let processor = factory()
1566 .create_processor(
1567 &ResponseFormat::JsonSchema(schema.to_string()),
1568 &StructuredOutputStart::Immediate,
1569 TEST_MAX_OUTPUT_TOKENS,
1570 &HashSet::new(),
1571 &[],
1572 )
1573 .unwrap()
1574 .unwrap();
1575 let mut generated = Vec::new();
1576 assert_and_append(&processor, &mut generated, r#"{"answer":42}"#);
1577 assert!(processor.is_accepting(&generated).unwrap());
1578 }
1579
1580 #[test]
1581 fn request_cannot_override_compact_json_compiler_policy() {
1582 let schema = r#"{
1583 "type":"object",
1584 "properties":{"answer":{"const":42}},
1585 "required":["answer"],
1586 "additionalProperties":false,
1587 "x-guidance":{
1588 "item_separator":", ",
1589 "key_separator":": ",
1590 "whitespace_flexible":true
1591 }
1592 }"#;
1593 let processor = factory()
1594 .create_processor(
1595 &ResponseFormat::JsonSchema(schema.to_string()),
1596 &StructuredOutputStart::Immediate,
1597 TEST_MAX_OUTPUT_TOKENS,
1598 &HashSet::new(),
1599 &[],
1600 )
1601 .unwrap()
1602 .unwrap();
1603 let mut generated = Vec::new();
1604 assert_and_append(&processor, &mut generated, r#"{"answer":"#);
1605 let mut logits = vec![0.0; EOS as usize + 1];
1606 processor.mask_logits(&mut logits, &generated).unwrap();
1607
1608 assert!(!logits[b' ' as usize].is_finite());
1609 assert!(logits[b'4' as usize].is_finite());
1610 assert_and_append(&processor, &mut generated, "42}");
1611 assert!(processor.is_accepting(&generated).unwrap());
1612 }
1613
1614 #[test]
1615 fn boolean_json_schema_keeps_its_semantics_under_compact_policy() {
1616 let processor = factory()
1617 .create_processor(
1618 &ResponseFormat::JsonSchema("true".to_string()),
1619 &StructuredOutputStart::Immediate,
1620 TEST_MAX_OUTPUT_TOKENS,
1621 &HashSet::new(),
1622 &[],
1623 )
1624 .unwrap()
1625 .unwrap();
1626 let mut generated = Vec::new();
1627 assert_and_append(&processor, &mut generated, "true");
1628 assert!(processor.is_accepting(&generated).unwrap());
1629 }
1630
1631 #[test]
1632 fn terminal_progress_classifies_an_unclosed_number_without_retaining_text() {
1633 let processor = factory()
1634 .create_processor(
1635 &ResponseFormat::JsonSchema(
1636 r#"{"type":"object","properties":{"value":{"type":"integer"}},"required":["value"],"additionalProperties":false}"#
1637 .to_string(),
1638 ),
1639 &StructuredOutputStart::Immediate,
1640 TEST_MAX_OUTPUT_TOKENS,
1641 &HashSet::new(),
1642 &[],
1643 )
1644 .unwrap()
1645 .unwrap();
1646 let generated =
1647 r#"{"value":123777"#.bytes().map(|byte| TokenId::new(byte as u32)).collect::<Vec<_>>();
1648 let progress = processor
1649 .progress_with_terminals(&generated, &HashSet::new())
1650 .unwrap();
1651
1652 assert_eq!(progress.phase, StructuredOutputPhase::EnforcingGrammar);
1653 assert_eq!(progress.grammar_token_count, generated.len());
1654 assert_eq!(
1655 progress.trailing_token_class,
1656 Some(StructuredOutputTokenClass::Number)
1657 );
1658 assert_eq!(progress.trailing_token_class_count, 6);
1659 assert_eq!(progress.trailing_token_id, Some(b'7' as u32));
1660 assert_eq!(progress.trailing_identical_token_count, 3);
1661 assert!(!progress.accepting);
1662 }
1663
1664 #[test]
1665 fn lexical_diagnostics_classify_json_bytes_without_decoding_content() {
1666 assert_eq!(
1667 classify_json_token_bytes(b" \n\t"),
1668 StructuredOutputTokenClass::Whitespace
1669 );
1670 assert_eq!(
1671 classify_json_token_bytes(b"-12.5e+3"),
1672 StructuredOutputTokenClass::Number
1673 );
1674 assert_eq!(
1675 classify_json_token_bytes(br#"{}[],:"#),
1676 StructuredOutputTokenClass::Structural
1677 );
1678 assert_eq!(
1679 classify_json_token_bytes(br#"\""#),
1680 StructuredOutputTokenClass::StringBoundary
1681 );
1682 assert_eq!(
1683 classify_json_token_bytes(b"true"),
1684 StructuredOutputTokenClass::Literal
1685 );
1686 assert_eq!(
1687 classify_json_token_bytes(br#""value":"#),
1688 StructuredOutputTokenClass::Other
1689 );
1690 }
1691
1692 struct FragmentedUtf8Tokenizer {
1693 special: SpecialTokens,
1694 token_text: Vec<String>,
1695 }
1696
1697 impl FragmentedUtf8Tokenizer {
1698 const FIRE_HEAD: u32 = 128;
1699 const FIRE_TAIL: u32 = 129;
1700 const EOS: u32 = 130;
1701
1702 fn new() -> Self {
1703 let mut token_text = (0u8..=127)
1704 .map(|byte| (byte as char).to_string())
1705 .collect::<Vec<_>>();
1706 token_text.extend(["\u{fffd}".to_string(), "\u{fffd}".to_string()]);
1707 token_text.push("<eos>".to_string());
1708 Self {
1709 special: SpecialTokens {
1710 eos_token: Some(TokenId::new(Self::EOS)),
1711 ..SpecialTokens::default()
1712 },
1713 token_text,
1714 }
1715 }
1716
1717 fn raw_bytes(token: TokenId) -> Option<Vec<u8>> {
1718 match token.get() {
1719 byte @ 0..=127 => Some(vec![byte as u8]),
1720 Self::FIRE_HEAD => Some(vec![0xf0, 0x9f]),
1721 Self::FIRE_TAIL => Some(vec![0x94, 0xa5]),
1722 _ => None,
1723 }
1724 }
1725 }
1726
1727 impl Tokenizer for FragmentedUtf8Tokenizer {
1728 fn encode(&self, text: &str, _add_special: bool) -> Result<Vec<TokenId>> {
1729 let mut tokens = Vec::new();
1730 let mut bytes = text.as_bytes();
1731 while let Some((&byte, remaining)) = bytes.split_first() {
1732 if bytes.starts_with(&[0xf0, 0x9f, 0x94, 0xa5]) {
1733 tokens.push(TokenId::new(Self::FIRE_HEAD));
1734 tokens.push(TokenId::new(Self::FIRE_TAIL));
1735 bytes = &bytes[4..];
1736 } else if byte <= 127 {
1737 tokens.push(TokenId::new(byte as u32));
1738 bytes = remaining;
1739 } else {
1740 return Err(FerrumError::tokenizer(
1741 "fragmented UTF-8 test tokenizer received unsupported input",
1742 ));
1743 }
1744 }
1745 Ok(tokens)
1746 }
1747
1748 fn decode(&self, tokens: &[TokenId], _skip_special: bool) -> Result<String> {
1749 let bytes = tokens
1750 .iter()
1751 .filter_map(|token| Self::raw_bytes(*token))
1752 .flatten()
1753 .collect::<Vec<_>>();
1754 Ok(String::from_utf8_lossy(&bytes).into_owned())
1755 }
1756
1757 fn decode_incremental(&self, _prev: &[TokenId], next: TokenId) -> Result<String> {
1758 self.decode(&[next], true)
1759 }
1760
1761 fn vocab_size(&self) -> usize {
1762 self.token_text.len()
1763 }
1764
1765 fn special_tokens(&self) -> &SpecialTokens {
1766 &self.special
1767 }
1768
1769 fn token_id(&self, text: &str) -> Option<TokenId> {
1770 (text.len() == 1 && text.is_ascii()).then(|| TokenId::new(text.as_bytes()[0] as u32))
1771 }
1772
1773 fn token_text(&self, token_id: TokenId) -> Option<&str> {
1774 self.token_text
1775 .get(token_id.get() as usize)
1776 .map(String::as_str)
1777 }
1778
1779 fn token_bytes(&self, token_id: TokenId) -> Option<Vec<u8>> {
1780 Self::raw_bytes(token_id)
1781 }
1782
1783 fn info(&self) -> TokenizerInfo {
1784 TokenizerInfo {
1785 tokenizer_type: TokenizerType::BPE,
1786 vocab_size: self.vocab_size(),
1787 special_tokens: self.special.clone(),
1788 supports_incremental: true,
1789 supports_chat_template: false,
1790 max_token_length: Some(2),
1791 model_name: Some("fragmented-utf8-test".to_string()),
1792 }
1793 }
1794 }
1795
1796 #[test]
1797 fn strict_schema_accepts_utf8_split_across_byte_level_tokens() {
1798 let tokenizer = Arc::new(FragmentedUtf8Tokenizer::new());
1799 assert!(tokenizer
1800 .decode(&[TokenId::new(FragmentedUtf8Tokenizer::FIRE_HEAD)], false)
1801 .unwrap()
1802 .contains('\u{fffd}'));
1803 let processor = StructuredOutputFactory::new(tokenizer)
1804 .unwrap()
1805 .create_processor(
1806 &ResponseFormat::JsonSchema(
1807 r#"{"type":"object","properties":{"value":{"const":"\ud83d\udd25"}},"required":["value"],"additionalProperties":false}"#
1808 .to_string(),
1809 ),
1810 &StructuredOutputStart::Immediate,
1811 TEST_MAX_OUTPUT_TOKENS,
1812 &HashSet::new(),
1813 &[],
1814 )
1815 .unwrap()
1816 .unwrap();
1817
1818 let mut generated = Vec::new();
1819 for token in r#"{"value":""#
1820 .bytes()
1821 .map(|byte| TokenId::new(byte as u32))
1822 .chain([
1823 TokenId::new(FragmentedUtf8Tokenizer::FIRE_HEAD),
1824 TokenId::new(FragmentedUtf8Tokenizer::FIRE_TAIL),
1825 ])
1826 .chain(r#""}"#.bytes().map(|byte| TokenId::new(byte as u32)))
1827 {
1828 let mut logits = vec![0.0; FragmentedUtf8Tokenizer::EOS as usize + 1];
1829 processor.mask_logits(&mut logits, &generated).unwrap();
1830 assert!(
1831 logits[token.get() as usize].is_finite(),
1832 "token {} rejected after {:?}",
1833 token.get(),
1834 generated
1835 );
1836 generated.push(token);
1837 }
1838 assert!(processor.is_accepting(&generated).unwrap());
1839 }
1840
1841 #[test]
1842 fn reasoning_delimiter_defers_then_activates_the_grammar() {
1843 let processor = factory()
1844 .create_processor(
1845 &ResponseFormat::JsonObject,
1846 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
1847 TEST_MAX_OUTPUT_TOKENS,
1848 &HashSet::new(),
1849 &[],
1850 )
1851 .unwrap()
1852 .unwrap();
1853 let mut generated = Vec::new();
1854 let controls = HashSet::from([b'<' as u32, b'>' as u32]);
1855 let mut waiting_logits = vec![0.0; EOS as usize + 1];
1856 let waiting = processor
1857 .mask_logits_with_terminals(&mut waiting_logits, &generated, &HashSet::new(), &controls)
1858 .unwrap();
1859 assert_eq!(waiting.phase, StructuredOutputPhase::WaitingForDelimiter);
1860 assert!(!waiting.accepting);
1861 assert_eq!(waiting.grammar_start_token_index, None);
1862 assert_eq!(waiting.required_delimiter_token_id, Some(b'<' as u32));
1863 assert!(waiting_logits[b'<' as usize].is_finite());
1864 assert!(!waiting_logits[b'>' as usize].is_finite());
1865
1866 let delimiter_prefix = "</think"
1867 .bytes()
1868 .map(|byte| TokenId::new(byte as u32))
1869 .collect::<Vec<_>>();
1870 let mut partial_logits = vec![0.0; EOS as usize + 1];
1871 let partial = processor
1872 .mask_logits_with_terminals(
1873 &mut partial_logits,
1874 &delimiter_prefix,
1875 &HashSet::new(),
1876 &controls,
1877 )
1878 .unwrap();
1879 assert_eq!(partial.phase, StructuredOutputPhase::WaitingForDelimiter);
1880 assert_eq!(partial.grammar_start_token_index, None);
1881 assert_eq!(partial.required_delimiter_token_id, Some(b'>' as u32));
1882 assert!(!partial_logits[b'<' as usize].is_finite());
1883 assert!(partial_logits[b'>' as usize].is_finite());
1884 let partial_progress = processor
1885 .progress_with_terminals(&delimiter_prefix, &HashSet::new())
1886 .unwrap();
1887 assert_eq!(partial_progress.delimiter_token_count, Some(8));
1888 assert_eq!(partial_progress.delimiter_prefix_token_count, 7);
1889
1890 processor.reset().unwrap();
1891
1892 assert_and_append(&processor, &mut generated, "reasoning [is free]</think>");
1893 let mut logits = vec![0.0; EOS as usize + 1];
1894 let active = processor
1895 .mask_logits_with_terminals(&mut logits, &generated, &HashSet::new(), &HashSet::new())
1896 .unwrap();
1897 assert_eq!(active.phase, StructuredOutputPhase::EnforcingGrammar);
1898 assert_eq!(active.grammar_start_token_index, Some(27));
1899 assert!(logits[b'{' as usize].is_finite());
1900 assert!(!logits[b'[' as usize].is_finite());
1901 assert!(!logits[EOS as usize].is_finite());
1902
1903 assert_and_append(&processor, &mut generated, r#"{"ok":true}"#);
1904 assert!(processor.is_accepting(&generated).unwrap());
1905 let progress = processor
1906 .progress_with_terminals(&generated, &HashSet::new())
1907 .unwrap();
1908 assert_eq!(progress.phase, StructuredOutputPhase::EnforcingGrammar);
1909 assert!(progress.accepting);
1910 assert_eq!(progress.generated_token_count, generated.len());
1911 assert!(!progress.boundary_forced);
1912 assert_eq!(progress.reasoning_token_count, Some(19));
1913 }
1914
1915 #[test]
1916 fn reasoning_budget_forces_exact_delimiter_and_preserves_structured_reserve() {
1917 let processor = factory()
1918 .create_processor(
1919 &ResponseFormat::JsonObject,
1920 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
1921 48,
1922 &HashSet::new(),
1923 &[],
1924 )
1925 .unwrap()
1926 .unwrap();
1927 let mut generated = "reason!!"
1928 .bytes()
1929 .map(|byte| TokenId::new(byte as u32))
1930 .collect::<Vec<_>>();
1931
1932 for expected in "</think>".bytes() {
1933 let mut logits = vec![f32::NEG_INFINITY; EOS as usize + 1];
1934 let outcome = processor
1935 .mask_logits_with_terminals(
1936 &mut logits,
1937 &generated,
1938 &HashSet::new(),
1939 &HashSet::new(),
1940 )
1941 .unwrap();
1942 assert_eq!(outcome.phase, StructuredOutputPhase::ForcingDelimiter);
1943 assert_eq!(outcome.grammar_start_token_index, None);
1944 assert_eq!(outcome.required_delimiter_token_id, Some(expected as u32));
1945 assert_eq!(
1946 logits
1947 .iter()
1948 .enumerate()
1949 .filter(|(_, logit)| logit.is_finite())
1950 .map(|(token, _)| token)
1951 .collect::<Vec<_>>(),
1952 vec![expected as usize]
1953 );
1954 generated.push(TokenId::new(expected as u32));
1955 }
1956
1957 let mut grammar_logits = vec![0.0; EOS as usize + 1];
1958 let outcome = processor
1959 .mask_logits_with_terminals(
1960 &mut grammar_logits,
1961 &generated,
1962 &HashSet::new(),
1963 &HashSet::new(),
1964 )
1965 .unwrap();
1966 assert_eq!(outcome.phase, StructuredOutputPhase::EnforcingGrammar);
1967 assert!(grammar_logits[b'{' as usize].is_finite());
1968 assert!(!grammar_logits[b'[' as usize].is_finite());
1969
1970 let progress = processor
1971 .progress_with_terminals(&generated, &HashSet::new())
1972 .unwrap();
1973 assert_eq!(progress.reasoning_token_count, Some(8));
1974 assert!(progress.boundary_forced);
1975 assert_eq!(
1976 progress.budget,
1977 Some(StructuredOutputBudgetPlan {
1978 total_output_tokens: 48,
1979 reasoning_token_limit: 8,
1980 boundary_token_count: 8,
1981 structured_reserve_tokens: 32,
1982 })
1983 );
1984
1985 processor.reset().unwrap();
1986 let reset_progress = processor
1987 .progress_with_terminals(&[], &HashSet::new())
1988 .unwrap();
1989 assert_eq!(
1990 reset_progress.phase,
1991 StructuredOutputPhase::WaitingForDelimiter
1992 );
1993 assert!(!reset_progress.boundary_forced);
1994 assert_eq!(reset_progress.reasoning_token_count, Some(0));
1995 }
1996
1997 #[test]
1998 fn reasoning_budget_reserves_half_of_a_normal_completion_for_structure() {
1999 assert_eq!(
2000 StructuredOutputBudgetPlan::automatic(1024, 1).unwrap(),
2001 StructuredOutputBudgetPlan {
2002 total_output_tokens: 1024,
2003 reasoning_token_limit: 511,
2004 boundary_token_count: 1,
2005 structured_reserve_tokens: 512,
2006 }
2007 );
2008 }
2009
2010 #[test]
2011 fn reasoning_delimiter_requires_room_beyond_the_boundary() {
2012 let error = factory()
2013 .create_processor(
2014 &ResponseFormat::JsonObject,
2015 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
2016 8,
2017 &HashSet::new(),
2018 &[],
2019 )
2020 .unwrap_err();
2021 assert!(error
2022 .to_string()
2023 .contains("max_tokens greater than its 8-token delimiter"));
2024 }
2025
2026 #[test]
2027 fn forcing_accounts_for_an_existing_delimiter_prefix() {
2028 let processor = factory()
2029 .create_processor(
2030 &ResponseFormat::JsonObject,
2031 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
2032 48,
2033 &HashSet::new(),
2034 &[],
2035 )
2036 .unwrap()
2037 .unwrap();
2038 let generated = "reason</"
2039 .bytes()
2040 .map(|byte| TokenId::new(byte as u32))
2041 .collect::<Vec<_>>();
2042 let mut logits = vec![0.0; EOS as usize + 1];
2043
2044 let outcome = processor
2045 .mask_logits_with_terminals(&mut logits, &generated, &HashSet::new(), &HashSet::new())
2046 .unwrap();
2047 assert_eq!(outcome.phase, StructuredOutputPhase::ForcingDelimiter);
2048 assert_eq!(outcome.required_delimiter_token_id, Some(b't' as u32));
2049 let progress = processor
2050 .progress_with_terminals(&generated, &HashSet::new())
2051 .unwrap();
2052 assert_eq!(progress.delimiter_prefix_token_count, 2);
2053 assert_eq!(progress.reasoning_token_count, Some(6));
2054 }
2055
2056 #[test]
2057 fn delimiter_rejects_any_conflicting_stop_condition_up_front() {
2058 let token_error = factory()
2059 .create_processor(
2060 &ResponseFormat::JsonObject,
2061 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
2062 TEST_MAX_OUTPUT_TOKENS,
2063 &HashSet::from([b'/' as u32]),
2064 &[],
2065 )
2066 .unwrap_err();
2067 assert!(token_error
2068 .to_string()
2069 .contains("conflicts with a stop token"));
2070
2071 let text_error = factory()
2072 .create_processor(
2073 &ResponseFormat::JsonObject,
2074 &StructuredOutputStart::AfterDelimiter("</think>".to_string()),
2075 TEST_MAX_OUTPUT_TOKENS,
2076 &HashSet::new(),
2077 &["think".to_string()],
2078 )
2079 .unwrap_err();
2080 assert!(text_error
2081 .to_string()
2082 .contains("conflicts with stop sequence"));
2083 }
2084}