use super::*;
pub(super) fn process_decoded_token(
a: &mut ActiveSeq,
tok: u32,
logprobs: Option<crate::api::TokenLogprobs>,
now: std::time::Instant,
think_end_token: Option<u32>,
think_start_token: Option<u32>,
code_fence_token: Option<u32>,
tool_call_start_token: Option<u32>,
tool_call_end_token: Option<u32>,
model: &dyn Model,
sched: &crate::scheduler::sched_ctx::SchedCtx,
) {
a.last_token = tok;
a.last_token_time = now;
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!(target: "met::scheduler::decode_logits_step", "<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; model forced to continue"
);
return;
}
crate::scheduler::eos_hold::end_turn_at_eos(a, tok, logprobs);
return;
}
let reopened_think = !a.inside_thinking && think_start_token == Some(tok);
if reopened_think {
a.inside_thinking = true;
a.think_ended = false;
a.think_skip_count = 0;
if let Some(spontaneous) = a.spontaneous_think_budget {
let decay_shift = a.think_watchdog_fires.min(4);
let decayed = spontaneous >> decay_shift;
a.thinking_budget = Some(decayed.max(8));
if a.think_watchdog_fires > 0 {
tracing::debug!(target: "met::scheduler::decode_logits_step", fires = a.think_watchdog_fires,
decayed_budget = decayed,
"Spontaneous <think> re-entry after watchdog; decayed budget"
);
}
}
tracing::debug!(target: "met::scheduler::decode_logits_step", "Spontaneous <think> detected, entering thinking mode");
if sched.watchdog.thinking_steering {
return;
}
}
if sched.watchdog.thinking_steering && !a.inside_thinking && 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!(target: "met::scheduler::decode_logits_step", "</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.inside_thinking
&& let Some(ref mut gs) = a.grammar_state
{
gs.accept_token(tok);
}
if a.inside_thinking {
a.consume_generation_budget();
if think_end_token == Some(tok) {
a.inside_thinking = false;
a.force_end_thinking = false;
a.sentence_defer_count = 0;
a.consecutive_confident = 0;
a.in_code_fence = false;
a.think_ended = true;
a.think_just_ended = true;
} else if !reopened_think {
a.thinking_tokens += 1;
a.in_code_fence = toggle_code_fence(a.in_code_fence, tok, code_fence_token);
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!(target: "met::scheduler::decode_logits_step", 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 up to {MAX_SENTENCE_DEFER_TOKENS} tokens for sentence boundary"
);
}
if !sched.levers.disable_watchdogs
&& sched.watchdog.enable_think_loop_watchdog
&& !a.force_end_thinking
&& a.thinking_tokens >= THINK_LOOP_MIN_TOKENS
&& a.thinking_tokens.is_multiple_of(THINK_LOOP_CHECK_STRIDE)
&& detect_thinking_token_loop_with(
&a.output_tokens,
a.repetition_detection,
sched.watchdog,
)
{
a.force_end_thinking = true;
a.sentence_defer_count = 0;
a.think_watchdog_fires = a.think_watchdog_fires.saturating_add(1);
tracing::warn!(target: "met::scheduler::decode_logits_step", thinking_tokens = a.thinking_tokens,
watchdog_fires = a.think_watchdog_fires,
"Thinking-loop watchdog fired (period-{}…{} repeat in tail); forcing </think> early",
THINK_LOOP_PERIOD_MIN,
THINK_LOOP_PERIOD_MAX,
);
}
}
} else {
handle_content_token(a, model, sched);
}
if a.require_tool_call && tool_call_start_token == Some(tok) && !a.inside_thinking {
a.require_tool_call = false;
a.tool_call_opened = true;
}
if tool_call_start_token == Some(tok) && !a.inside_thinking {
a.prose_tokens_since_last_tool = 0;
if a.tool_call_completed {
a.post_completion_tool_opens = a.post_completion_tool_opens.saturating_add(1);
const MAX_POST_COMPLETION_TOOL_OPENS: u32 = 8;
if a.post_completion_tool_opens >= MAX_POST_COMPLETION_TOOL_OPENS {
tracing::warn!(target: "met::scheduler::decode_logits_step", opens = a.post_completion_tool_opens,
"tool-call repetition runaway: model re-opened {MAX_POST_COMPLETION_TOOL_OPENS}+ tool-call blocks after a completed call on a tool_choice=auto turn; ending response (was burning to max_tokens). Sanitizer keeps the first valid call(s)."
);
a.output_tokens.push(tok);
a.tool_call_opened = true;
if let Some(ref mut gs) = a.grammar_state {
gs.accept_token(tok);
}
a.finished = true;
return;
}
}
}
if a.require_tool_call && a.output_tokens.len() > 512 {
tracing::warn!(target: "met::scheduler::decode_logits_step", "require_tool_call safety: no <tool_call> after 512 tokens, clearing EOS suppression"
);
a.require_tool_call = false;
}
if let Some(lp) = logprobs {
a.logprobs_data.push(lp);
}
if tool_call_end_token == Some(tok) && !a.inside_thinking {
a.output_tokens.push(tok);
a.tool_call_completed = true;
if a.sink.is_streaming() {
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, "tool_call_end") {
tracing::warn!(target: "met::scheduler::decode_logits_step", "Streaming receiver dropped during tool_call_end, finishing sequence"
);
a.finished = true;
return;
}
}
if a.grammar_state.is_none() && !a.tools_present {
a.finished = true;
}
a.inside_tool_body = false;
if let Some(ref mut gs) = a.grammar_state {
gs.accept_token(tok);
}
a.think_ended = false;
return;
}
super::content_emit::emit_content_token(a, tok, model, sched);
}