use crate::ir::StreamDelta;
use crate::tool_parser;
use super::super::sanitizer::sanitize_content_chunk;
use super::super::stream_guards::flush_content_sanitizer;
use super::ctx::StreamCtx;
use super::state::StreamState;
use super::tool_dispatch::dispatch_tool_output;
type DeltaVec = Vec<StreamDelta>;
#[allow(clippy::too_many_arguments)]
pub(super) fn handle_done(
state: &mut StreamState,
ctx: &StreamCtx,
finish_reason: String,
completion_tokens: usize,
time_to_first_token_ms: f64,
decode_time_ms: f64,
reasoning_tokens: u32,
cached_prompt_tokens: u32,
accepted_prediction_tokens: usize,
) -> DeltaVec {
let mut deltas: DeltaVec = Vec::new();
if let Some(raw) = state.raw_toks.take() {
crate::api::chat_blocking_choice::log_raw_generation(
&ctx.state,
&raw,
&format!("stream {}, finish {finish_reason}", ctx.id),
);
}
if !ctx.stop_strings.is_empty()
&& !state.stop_string_triggered
&& state.stop_string_emitted_len < state.accumulated_content.len()
{
let tail = state.accumulated_content[state.stop_string_emitted_len..].to_string();
state.stop_string_emitted_len = state.accumulated_content.len();
if !tail.is_empty() {
if let Some(det) = state.detector.as_mut() {
let outputs = det.process(&tail);
for output in outputs {
if let Some(text) = dispatch_tool_output(state, ctx, output, &mut deltas) {
let sanitized = sanitize_content_chunk(
&text,
&mut state.tag_scan_buf,
&mut state.suppressing_param_leak,
&mut state.inside_envelope,
&ctx.leak_markers,
);
if !sanitized.is_empty() {
deltas.push(StreamDelta::Content {
text: sanitized,
token_ids: Vec::new(),
});
}
}
}
} else {
let sanitized = sanitize_content_chunk(
&tail,
&mut state.tag_scan_buf,
&mut state.suppressing_param_leak,
&mut state.inside_envelope,
&ctx.leak_markers,
);
if !sanitized.is_empty() {
if state.refusal_scan_buf.len() < 16_384 {
state.refusal_scan_buf.push_str(&sanitized);
}
deltas.push(StreamDelta::Content {
text: sanitized,
token_ids: Vec::new(),
});
}
}
}
}
if state.detector.is_some() {
let outputs = {
let det = state.detector.as_mut().expect("detector is Some");
det.flush()
};
for output in outputs {
if let Some(text) = dispatch_tool_output(state, ctx, output, &mut deltas) {
let sanitized = sanitize_content_chunk(
&text,
&mut state.tag_scan_buf,
&mut state.suppressing_param_leak,
&mut state.inside_envelope,
&ctx.leak_markers,
);
if !sanitized.is_empty() {
deltas.push(StreamDelta::Content {
text: sanitized,
token_ids: state.take_ids_if(ctx.req_return_token_ids),
});
}
}
}
}
let tail = flush_content_sanitizer(
&mut state.tag_scan_buf,
&mut state.suppressing_param_leak,
&ctx.leak_markers,
);
if !tail.is_empty() {
if state.refusal_scan_buf.len() < 16_384 {
state.refusal_scan_buf.push_str(&tail);
}
deltas.push(StreamDelta::Content {
text: tail,
token_ids: state.take_ids_if(ctx.req_return_token_ids),
});
}
let usage = crate::ir::Usage {
prompt_tokens: ctx.prompt_len,
completion_tokens,
cached_prompt_tokens: cached_prompt_tokens as usize,
reasoning_tokens: reasoning_tokens as usize,
accepted_prediction_tokens,
time_to_first_token_ms,
decode_time_ms,
response_tokens_per_second: crate::ir::Usage::decode_rate_tok_s(
completion_tokens,
decode_time_ms,
),
};
let fr = resolve_wire_finish_reason(
&finish_reason,
state.tool_loop_capped,
state.detector.as_ref().is_some_and(|d| d.has_tool_calls()) || state.salvaged_tool_call,
state.stop_string_matched,
state.guard_stop,
);
let refusal_signal = if state.detector.as_ref().is_none_or(|d| !d.has_tool_calls()) {
crate::refusal::detect(&state.refusal_scan_buf)
} else {
None
};
if let Some(ref r) = refusal_signal {
deltas.push(StreamDelta::Refusal { text: r.clone() });
}
deltas.push(StreamDelta::Finish {
reason: crate::ir::FinishReason::from(fr),
usage,
token_ids: state.take_ids_if(ctx.req_return_token_ids),
});
crate::metrics::PROMPT_TOKENS_TOTAL.inc_by(ctx.prompt_len as u64);
crate::metrics::GENERATION_TOKENS_TOTAL.inc_by(completion_tokens as u64);
crate::metrics::TTFT_SECONDS
.with_label_values(&[ctx.model.as_str()])
.observe(time_to_first_token_ms / 1000.0);
if let Some(ref rctx) = ctx.req_ctx {
let actual = (ctx.prompt_len + completion_tokens) as u64;
let refund = rctx.reserved_tokens.saturating_sub(actual);
if refund > 0 {
ctx.state.rate_limiter.refund_tokens(&rctx.identity, refund);
}
}
if let (Some(seq), Some(dump)) = (ctx.dump_seq, ctx.state.dump_writer.as_ref()) {
let has_tool_calls = state.detector.as_ref().is_some_and(|d| d.has_tool_calls());
let usage_for_dump = crate::openai::Usage::from(&usage);
let body = serde_json::json!({
"id": ctx.id,
"model": ctx.model,
"object": "chat.completion.synthesized",
"finish_reason": fr,
"content": state.refusal_scan_buf,
"has_tool_calls": has_tool_calls,
"usage": usage_for_dump,
"stop_string_triggered": state.stop_string_triggered,
"loop_watchdog_triggered": state.loop_watchdog_triggered,
"tool_loop_capped": state.tool_loop_capped,
"guard_stop": state.guard_stop,
"_note": "Synthesized from post-sanitizer accumulators; \
per-chunk capture is a follow-up.",
});
dump.dump_response("/v1/chat/completions", seq, &body, true);
}
deltas
}
fn resolve_wire_finish_reason<'a>(
scheduler_reason: &'a str,
tool_loop_capped: bool,
has_tool_calls: bool,
stop_string_matched: bool,
stream_guard_stop: Option<&'static str>,
) -> &'a str {
if scheduler_reason == crate::ir::FINISH_REASON_TIMEOUT {
scheduler_reason
} else if tool_loop_capped {
"length"
} else if has_tool_calls {
"tool_calls"
} else if stop_string_matched {
"stop"
} else if stream_guard_stop.is_some() {
"length"
} else {
scheduler_reason
}
}
#[cfg(test)]
mod wire_finish_reason_tests {
use super::resolve_wire_finish_reason;
use crate::ir::FINISH_REASON_TIMEOUT;
#[test]
fn stop_string_match_is_stop_not_length() {
assert_eq!(
resolve_wire_finish_reason("length", false, false, true, None),
"stop"
);
assert_eq!(
resolve_wire_finish_reason("length", false, false, false, None),
"length"
);
assert_eq!(
resolve_wire_finish_reason("stop", false, false, false, None),
"stop"
);
}
#[test]
fn stream_side_guard_cut_reports_length() {
for guard in ["simhash_semantic_loop", "token_loop_watchdog"] {
assert_eq!(
resolve_wire_finish_reason("stop", false, false, false, Some(guard)),
"length",
"guard={guard}"
);
}
}
#[test]
fn stream_guard_does_not_outrank_what_actually_happened() {
assert_eq!(
resolve_wire_finish_reason("stop", false, true, false, Some("token_loop_watchdog")),
"tool_calls"
);
assert_eq!(
resolve_wire_finish_reason("stop", false, false, true, Some("token_loop_watchdog")),
"stop"
);
assert_eq!(
resolve_wire_finish_reason("stop", false, false, false, None),
"stop"
);
}
#[test]
fn timeout_and_tool_overrides_keep_their_rank() {
assert_eq!(
resolve_wire_finish_reason(FINISH_REASON_TIMEOUT, true, true, true, None),
FINISH_REASON_TIMEOUT
);
assert_eq!(
resolve_wire_finish_reason("stop", true, true, true, None),
"length",
"tool-loop cap outranks tool_calls and the stop-string match"
);
assert_eq!(
resolve_wire_finish_reason("stop", false, true, true, None),
"tool_calls",
"parsed tool calls outrank the stop-string override"
);
}
}