use super::*;
pub fn emit_token(
a: &mut ActiveSeq,
tok: u32,
logprobs: Option<crate::api::TokenLogprobs>,
sched: &crate::scheduler::sched_ctx::SchedCtx,
) {
tracing::debug!(
"TOK slot={} out_idx={} tok={}",
a.seq.slot_idx,
a.output_tokens.len(),
tok,
);
if sched.io.req.is_cancelled(a.cancel_flag.as_ref()) {
a.finished = true;
return;
}
if let Some(ims) = sched.limits.im_start_hard_stop
&& tok == ims
{
a.output_tokens.push(tok);
a.finished = true;
tracing::debug!(
"<|im_start|> hard-stop fired (id={ims}); ending turn before grammar/suppress_eos"
);
return;
}
if sched.levers.tool_response_stop
&& let Some(trs) = sched.limits.tool_response_hard_stop
&& tok == trs
{
a.output_tokens.push(tok);
a.finished = true;
a.guard_stop = Some(GUARD_STOP_TOOL_RESPONSE);
tracing::debug!("<tool_response> hard-stop fired (id={trs}); ending turn");
return;
}
if a.eos_tokens.contains(&tok) {
let generated_before = a.output_tokens.len();
let hold =
crate::scheduler::eos_hold::eos_hold(a, generated_before, sched.levers.tool_eos_escape);
if hold.any() {
tracing::debug!(
target: "metrale::eos",
tok,
inside_thinking = a.inside_thinking,
by_grammar = hold.grammar,
by_required_tool_call = hold.required_tool_call,
by_min_tokens = hold.min_tokens,
"EOS suppressed in emit_token; model forced to continue"
);
return;
}
crate::scheduler::eos_hold::end_turn_at_eos(a, tok, logprobs);
return;
}
let reopened_think = !a.inside_thinking && a.think_start_token == Some(tok);
if reopened_think {
a.inside_thinking = true;
a.post_think_emitted = 0;
a.think_ended = false;
a.think_skip_count = 0;
if let Some(spontaneous) = a.spontaneous_think_budget {
a.thinking_budget = Some(spontaneous);
}
tracing::debug!("Spontaneous <think> detected in emit_token, entering thinking mode");
if sched.watchdog.thinking_steering {
return;
}
}
if sched.watchdog.thinking_steering && !a.inside_thinking && a.think_end_token == Some(tok) {
a.think_skip_count += 1;
if a.think_skip_count >= 50 {
a.finished = true;
a.guard_stop = Some(GUARD_STOP_THINK_SKIP);
tracing::debug!(
"</think> think-skip watchdog hard-stop fired (50 consecutive strays); \
ending turn"
);
}
return;
}
if a.think_ended {
a.think_skip_count = 0;
}
crate::scheduler::think_close::close_thinking_at_tool_call(
a,
tok,
sched.limits.thinking_closed_by,
);
if a.require_tool_call && a.tool_call_start_token == Some(tok) && !a.inside_thinking {
a.require_tool_call = false;
a.tool_call_opened = true;
}
update_tool_param_state(a, tok);
if a.tool_call_end_token == Some(tok) && !a.inside_thinking {
a.tool_call_completed = true;
}
if a.tool_call_start_token == Some(tok) && !a.inside_thinking {
a.prose_tokens_since_last_tool = 0;
}
if !a.inside_thinking
&& let Some(ref mut gs) = a.grammar_state
&& !gs.accept_token(tok)
{
tracing::error!(
tok,
output_len = a.output_tokens.len(),
"grammar refused an emitted token; keeping the token and the grammar unadvanced"
);
}
if let Some(lp) = logprobs {
a.logprobs_data.push(lp);
}
a.output_tokens.push(tok);
if a.think_ended && !a.inside_thinking {
a.post_think_emitted += 1;
}
if a.inside_thinking {
a.consume_generation_budget();
if a.think_end_token == Some(tok) {
a.inside_thinking = false;
a.think_force_closed = a.force_end_thinking;
a.force_end_thinking = false;
a.sentence_defer_count = 0;
a.think_ended = true;
a.think_just_ended = true;
tracing::info!(
"Thinking ended after {} tokens (budget={:?})",
a.thinking_tokens,
a.thinking_budget,
);
} else if !reopened_think {
a.thinking_tokens += 1;
if let Some(budget) = a.thinking_budget
&& a.thinking_tokens >= budget
&& !a.force_end_thinking
{
a.force_end_thinking = true;
a.sentence_defer_count = 0;
tracing::info!(
source = if a.enable_thinking {
"request (client budget/effort; scaled by --max-thinking-budget)"
} else {
"spontaneous <think> (--max-thinking-budget / MODEL.toml)"
},
"Thinking budget exhausted ({budget} tokens), arming </think>; \
deferring to next sentence boundary"
);
}
}
} else {
a.consume_generation_budget();
a.think_just_ended = false;
use crate::scheduler::helpers::{
CONTENT_LOOP_CHECK_STRIDE, CONTENT_LOOP_MIN_TOKENS, CONTENT_LOOP_PERIOD_MAX,
CONTENT_LOOP_PERIOD_MIN, detect_content_token_loop_normalized_with,
detect_content_token_loop_with,
};
a.content_tokens = a.content_tokens.saturating_add(1);
if !sched.levers.disable_watchdogs
&& a.grammar_state.is_some()
&& a.content_tokens > sched.watchdog.max_post_think_content_tokens
{
tracing::warn!(
content_tokens = a.content_tokens,
max = sched.watchdog.max_post_think_content_tokens,
"post-think content cap exceeded in MTP/emit path; ending response (tool-active request would otherwise burn to max_tokens)"
);
a.guard_stop = Some(GUARD_STOP_POST_THINK_CAP);
a.finished = true;
}
let loop_params = sched.watchdog.content_loop_params(a.repetition_detection);
if !sched.levers.disable_watchdogs
&& sched.levers.loop_watchdog()
&& !a.inside_tool_body
&& a.content_tokens >= CONTENT_LOOP_MIN_TOKENS
&& a.content_tokens.is_multiple_of(CONTENT_LOOP_CHECK_STRIDE)
&& (detect_content_token_loop_with(&a.output_tokens, loop_params)
|| sched.masks.numeric.as_deref().is_some_and(|m| {
detect_content_token_loop_normalized_with(&a.output_tokens, m, loop_params)
}))
{
tracing::warn!(
content_tokens = a.content_tokens,
output_len = a.output_tokens.len(),
"Content-loop watchdog fired in MTP/emit path (period-{}…{} repeat); ending response. \
Tune via --content-loop-min-repeats / METRALE_CONTENT_LOOP_MIN_REPEATS, per-request \
repetition_detection, or disarm via --content-loop-watchdog off / \
METRALE_CONTENT_LOOP_WATCHDOG=0",
CONTENT_LOOP_PERIOD_MIN,
CONTENT_LOOP_PERIOD_MAX,
);
a.guard_stop = Some(GUARD_STOP_CONTENT_LOOP);
a.finished = true;
}
if !sched.levers.disable_watchdogs && !a.inside_tool_body && a.tool_request {
a.prose_tokens_since_last_tool = a.prose_tokens_since_last_tool.saturating_add(1);
let max_prose = sched.watchdog.max_inter_tool_prose;
if a.prose_tokens_since_last_tool > max_prose {
tracing::warn!(
prose_tokens = a.prose_tokens_since_last_tool,
max = max_prose,
output_len = a.output_tokens.len(),
"Inter-tool prose budget exhausted in MTP/emit path; ending response \
(no tool call after budget — would otherwise burn to max_tokens); \
raise via --max-inter-tool-prose / METRALE_MAX_INTER_TOOL_PROSE / \
MODEL.toml [behavior].max_inter_tool_prose (0 disables)"
);
a.guard_stop = Some(GUARD_STOP_INTER_TOOL_PROSE);
a.finished = true;
}
}
}
let suppress_stream = a.inside_thinking && !a.enable_thinking;
if !suppress_stream {
let event = if let Some(lp) = a.logprobs_data.last().cloned() {
StreamEvent::TokenWithLogprobs(tok, lp)
} else {
StreamEvent::Token(tok)
};
if !sched.io.req.emit(&a.sink, event, "token stream") {
a.finished = true;
return;
}
}
if a.remaining == 0 || seqlen_force_stop(a.seq.seq_len, sched.limits.max_seq_len) {
emit_grammar_close(a, &sched.io, sched.levers.grammar_budget_close);
tracing::info!(
"emit_token: remaining={}, seq_len={}, max_seq_len={}, output_tokens={}, thinking_tokens={}",
a.remaining,
a.seq.seq_len,
sched.limits.max_seq_len,
a.output_tokens.len(),
a.thinking_tokens
);
a.finished = true;
}
}