use super::per_token::process_decoded_token;
use crate::grammar::tests::{test_tool_defs, test_vocab};
use crate::grammar::{GrammarEngine, GrammarState};
use crate::scheduler::emit_step::emit_token;
use crate::scheduler::io::SchedIo;
use crate::scheduler::lifecycle::finish_sequence;
use crate::scheduler::sched_ctx::SchedCtx;
use crate::scheduler::test_support::{EOS, PreemptStubModel, RespRx, test_seq};
use crate::scheduler::types::ActiveSeq;
use crate::scheduler::verify_k4_verdict::{K4Hidden, k4_apply_verdict};
use std::sync::Arc;
use std::time::Instant;
const ANSWER: u32 = 1004;
const THINK_START: u32 = 151_667;
const THINK_END: u32 = 151_668;
const TOOL_CALL_START: u32 = 151_657;
const MAX_SEQ_LEN: usize = 4096;
const TOY_EOS: u32 = 130;
#[derive(Clone, Copy, Debug)]
enum Path {
Decode,
Emit,
}
const PATHS: [Path; 2] = [Path::Decode, Path::Emit];
fn seq(output: Vec<u32>, inside_thinking: bool) -> (ActiveSeq, RespRx) {
let seq_len = 10 + output.len();
let (mut a, rx) = test_seq(output, 5000, None, seq_len);
a.finished = false;
a.min_tokens = 0;
a.enable_thinking = true;
a.inside_thinking = inside_thinking;
a.think_ended = !inside_thinking;
a.think_start_token = Some(THINK_START);
a.think_end_token = Some(THINK_END);
a.tool_call_start_token = Some(TOOL_CALL_START);
(a, rx)
}
fn sched() -> SchedCtx {
SchedCtx::for_test_with(Arc::new(PreemptStubModel::default()))
}
fn feed(path: Path, a: &mut ActiveSeq, tok: u32, sched: &SchedCtx) {
match path {
Path::Decode => {
let think_end = a.think_end_token;
let think_start = a.think_start_token;
let tool_start = a.tool_call_start_token;
let tool_end = a.tool_call_end_token;
process_decoded_token(
a,
tok,
None,
Instant::now(),
think_end,
think_start,
None,
tool_start,
tool_end,
&PreemptStubModel::default(),
sched,
);
}
Path::Emit => emit_token(a, tok, None, sched),
}
}
fn required_tool_grammar() -> GrammarState {
let vocab = test_vocab();
let mut engine = GrammarEngine::new(&vocab, &[TOY_EOS as i32]).unwrap();
let compiled = engine
.compile_hermes_tool_grammar(&test_tool_defs(), false)
.unwrap();
GrammarState::new(&compiled, engine.vocab_size())
.unwrap()
.with_stop_tokens(&[TOY_EOS])
}
#[test]
fn eos_inside_an_unclosed_think_ends_the_turn_as_reasoning() {
for path in PATHS {
let sched = sched();
let (mut a, rx) = seq(vec![], true);
feed(path, &mut a, ANSWER, &sched);
assert!(!a.finished, "{path:?}: the answer token ended the turn");
feed(path, &mut a, EOS[0], &sched);
assert!(
a.finished,
"{path:?}: EOS inside <think> did not end the turn"
);
assert_eq!(a.output_tokens, vec![ANSWER, EOS[0]], "{path:?}");
assert_eq!(a.thinking_tokens, 2, "{path:?}");
finish_sequence(
&SchedIo::for_test_with(Arc::new(PreemptStubModel::default())),
&mut a,
MAX_SEQ_LEN,
);
let mut rx = rx;
let response = rx
.try_recv()
.expect("finish_sequence sends the blocking response")
.expect("response is Ok");
assert_eq!(response.finish_reason, "stop", "{path:?}");
assert_eq!(response.reasoning_tokens, 2, "{path:?}");
assert_eq!(response.output_tokens, vec![ANSWER, EOS[0]], "{path:?}");
}
}
#[test]
fn eos_outside_thinking_ends_the_turn() {
for path in PATHS {
let sched = sched();
let (mut a, _rx) = seq(vec![ANSWER], false);
feed(path, &mut a, EOS[0], &sched);
assert!(a.finished, "{path:?}");
assert_eq!(a.output_tokens, vec![ANSWER, EOS[0]], "{path:?}");
assert_eq!(a.thinking_tokens, 0, "{path:?}");
}
}
#[test]
fn verify_verdict_with_eos_inside_thinking_ends_the_turn_at_the_eos() {
let sched = sched();
let model = PreemptStubModel::default();
let (mut a, _rx) = seq(vec![], true);
k4_apply_verdict(
&model,
&mut a,
&sched,
&[ANSWER, EOS[0]],
&[ANSWER, EOS[0], 2000],
vec![],
2,
2,
K4Hidden::DeferPropose,
0,
);
assert!(
a.finished,
"EOS accepted inside <think> did not end the turn"
);
assert_eq!(a.output_tokens, vec![ANSWER, EOS[0]]);
assert_eq!(a.thinking_tokens, 2);
}
#[test]
fn grammar_does_not_hold_eos_inside_thinking() {
for path in PATHS {
let sched = sched();
let (mut a, _rx) = seq(vec![], true);
a.eos_tokens = vec![TOY_EOS];
a.grammar_state = Some(required_tool_grammar());
feed(path, &mut a, u32::from(b'4'), &sched);
feed(path, &mut a, TOY_EOS, &sched);
assert!(a.finished, "{path:?}: a grammar held EOS inside <think>");
assert_eq!(a.output_tokens, vec![u32::from(b'4'), TOY_EOS], "{path:?}");
}
}
#[test]
fn grammar_holds_eos_outside_thinking_on_the_decode_path() {
let sched = sched();
let (mut a, _rx) = seq(vec![], false);
a.eos_tokens = vec![TOY_EOS];
a.grammar_state = Some(required_tool_grammar());
feed(Path::Decode, &mut a, TOY_EOS, &sched);
assert!(
!a.finished,
"a grammar that cannot stop let EOS end the turn"
);
assert!(a.output_tokens.is_empty(), "a held EOS was recorded");
}
#[test]
fn required_tool_call_holds_eos_only_outside_thinking() {
for path in PATHS {
let sched = sched();
let (mut a, _rx) = seq(vec![], true);
a.require_tool_call = true;
feed(path, &mut a, ANSWER, &sched);
feed(path, &mut a, EOS[0], &sched);
assert!(
a.finished,
"{path:?}: require_tool_call held EOS inside <think>"
);
let (mut a, _rx) = seq(vec![ANSWER], false);
a.require_tool_call = true;
feed(path, &mut a, EOS[0], &sched);
assert!(
!a.finished,
"{path:?}: EOS ended a turn that owes a tool call"
);
}
}
#[test]
fn tool_armed_turn_ends_on_eos_right_after_think_close() {
for path in PATHS {
let sched = sched();
let (mut a, _rx) = seq(vec![ANSWER], true);
a.tool_request = true;
a.tools_present = true;
feed(path, &mut a, THINK_END, &sched);
assert!(
!a.inside_thinking,
"{path:?}: </think> did not close thinking"
);
assert!(!a.finished, "{path:?}");
feed(path, &mut a, EOS[0], &sched);
assert!(a.finished, "{path:?}: EOS after </think> was held back");
assert_eq!(a.output_tokens.last(), Some(&EOS[0]), "{path:?}");
}
}
#[test]
fn min_tokens_holds_eos_until_that_many_tokens_came_before_it() {
for path in PATHS {
let sched = sched();
let (mut a, _rx) = seq(vec![1000, 1001], true);
a.min_tokens = 3;
feed(path, &mut a, EOS[0], &sched);
assert!(
!a.finished,
"{path:?}: EOS after 2 of 3 min_tokens ended the turn"
);
assert_eq!(a.output_tokens, vec![1000, 1001], "{path:?}");
assert_eq!(a.remaining, 5000, "{path:?}: a held EOS was charged");
let (mut a, _rx) = seq(vec![1000, 1001, 1002], true);
a.min_tokens = 3;
feed(path, &mut a, EOS[0], &sched);
assert!(
a.finished,
"{path:?}: EOS after 3 of 3 min_tokens was held back"
);
}
}
#[test]
fn stray_think_close_is_content_without_thinking_steering() {
for path in PATHS {
let mut sched = sched();
sched.watchdog.thinking_steering = false;
let (mut a, _rx) = seq(vec![ANSWER], false);
feed(path, &mut a, THINK_END, &sched);
assert_eq!(
a.output_tokens,
vec![ANSWER, THINK_END],
"{path:?}: a stray </think> was dropped with steering off"
);
let sched = sched_with_steering();
let (mut a, _rx) = seq(vec![ANSWER], false);
feed(path, &mut a, THINK_END, &sched);
assert_eq!(a.output_tokens, vec![ANSWER], "{path:?}: control");
}
}
fn sched_with_steering() -> SchedCtx {
let mut s = sched();
s.watchdog.thinking_steering = true;
s
}
#[test]
fn a_token_the_grammar_refuses_is_kept_and_the_grammar_stays() {
let sched = sched();
let (mut a, _rx) = seq(vec![], false);
a.eos_tokens = vec![TOY_EOS];
a.grammar_state = Some(required_tool_grammar());
feed(Path::Emit, &mut a, u32::from(b'4'), &sched);
assert_eq!(a.output_tokens, vec![u32::from(b'4')]);
assert!(
a.grammar_state.is_some(),
"the refusing grammar was dropped"
);
feed(Path::Decode, &mut a, TOY_EOS, &sched);
assert!(!a.finished, "the kept grammar no longer holds EOS");
}