use crate::models::ToolCaller;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ContentBlockKind {
Text,
Thinking,
ToolUse,
}
#[derive(Debug, Clone)]
pub(super) struct ToolUseState {
pub(super) id: String,
pub(super) name: String,
pub(super) input: serde_json::Value,
pub(super) caller: Option<ToolCaller>,
pub(super) thought_signature: Option<String>,
pub(super) input_buffer: String,
pub(super) input_parse_error: Option<String>,
}
pub(super) const STREAM_MAX_CONTENT_BYTES: usize = 10 * 1024 * 1024; pub(super) const STREAM_MAX_DURATION_SECS: u64 = 1800; pub(super) const MAX_STREAM_ERRORS_BEFORE_FAIL: u32 = 5;
pub(super) const MAX_TRANSPARENT_STREAM_RETRIES: u32 = 2;
pub(super) fn should_transparently_retry_stream(
any_content_received: bool,
transparent_attempts: u32,
cancelled: bool,
) -> bool {
!any_content_received && transparent_attempts < MAX_TRANSPARENT_STREAM_RETRIES && !cancelled
}
pub(super) const MAX_STREAM_RETRIES: u32 = 3;
pub(super) const SLEEP_GAP_THRESHOLD: Duration = Duration::from_secs(10);
pub(super) fn sleep_gap_detected(monotonic_elapsed: Duration, wallclock_elapsed: Duration) -> bool {
wallclock_elapsed.saturating_sub(monotonic_elapsed) > SLEEP_GAP_THRESHOLD
}
pub(super) fn should_resume_after_sleep(
sleep_detected: bool,
retry_attempts: u32,
cancelled: bool,
) -> bool {
sleep_detected && retry_attempts < MAX_STREAM_RETRIES && !cancelled
}
pub(super) fn should_resume_after_network_drop(
headless_host: bool,
network_class_error: bool,
retry_attempts: u32,
cancelled: bool,
) -> bool {
headless_host && network_class_error && retry_attempts < MAX_STREAM_RETRIES && !cancelled
}
pub(super) fn should_resume_interactive_after_network_drop(
terminal_chrome_enabled: bool,
network_class_error: bool,
any_content_received: bool,
tool_uses_empty: bool,
retry_attempts: u32,
cancelled: bool,
) -> bool {
terminal_chrome_enabled
&& network_class_error
&& any_content_received
&& tool_uses_empty
&& retry_attempts < MAX_STREAM_RETRIES
&& !cancelled
}
pub(super) fn stream_read_error_user_message(message: &str, any_content_received: bool) -> String {
let lower = message.to_ascii_lowercase();
let is_stream_read = lower.contains("stream read error")
|| lower.contains("error decoding response body")
|| lower.contains("chunk decode error")
|| lower.contains("body decode");
if !is_stream_read {
return message.to_string();
}
let retry_note = if any_content_received {
"Some output had already streamed, so Codewhale is surfacing the warning instead of replaying the request and risking duplicated output."
} else {
"No output had streamed yet, so Codewhale will retry automatically while retry budget remains."
};
format!(
"Provider stream connection dropped while reading the response body. {retry_note} Details: {message}"
)
}
pub(crate) const TOOL_CALL_MARKER_PAIRS: [(&str, &str); 28] = [
("[TOOL_CALL]", "[/TOOL_CALL]"),
("<codewhale:tool_call", "</codewhale:tool_call>"),
("<tool_call", "</tool_call>"),
("<invoke ", "</invoke>"),
("<function_calls>", "</function_calls>"),
("<|DSML|tool_calls>", "</|DSML|tool_calls>"),
("<|DSML|invoke ", "</|DSML|invoke>"),
("<|DSML|tool_calls>", "</|DSML|tool_calls>"),
("<|DSML|invoke ", "</|DSML|invoke>"),
("<|dsml|tool_calls>", "</|dsml|tool_calls>"),
("<|dsml|invoke ", "</|dsml|invoke>"),
("<|tool_calls>", "</|tool_calls>"),
("<|tool▁calls▁begin|>", "<|tool▁calls▁end|>"),
("<|tool▁call▁begin|>", "<|tool▁call▁end|>"),
("<|tool▁outputs▁begin|>", "<|tool▁outputs▁end|>"),
("<|tool▁output▁begin|>", "<|tool▁output▁end|>"),
("<|tool▁calls▁begin|>", "<|tool▁calls▁end|>"),
("<|tool▁call▁begin|>", "<|tool▁call▁end|>"),
("<|tool▁outputs▁begin|>", "<|tool▁outputs▁end|>"),
("<|tool▁output▁begin|>", "<|tool▁output▁end|>"),
("<|tool_calls_begin|>", "<|tool_calls_end|>"),
("<|tool_call_begin|>", "<|tool_call_end|>"),
("<|tool_outputs_begin|>", "<|tool_outputs_end|>"),
("<|tool_output_begin|>", "<|tool_output_end|>"),
("<|tool_calls_begin|>", "<|tool_calls_end|>"),
("<|tool_call_begin|>", "<|tool_call_end|>"),
("<|tool_outputs_begin|>", "<|tool_outputs_end|>"),
("<|tool_output_begin|>", "<|tool_output_end|>"),
];
pub(crate) const TOOL_CALL_START_MARKERS: [&str; 28] = [
"[TOOL_CALL]",
"<codewhale:tool_call",
"<tool_call",
"<invoke ",
"<function_calls>",
"<|DSML|tool_calls>",
"<|DSML|invoke ",
"<|DSML|tool_calls>",
"<|DSML|invoke ",
"<|dsml|tool_calls>",
"<|dsml|invoke ",
"<|tool_calls>",
"<|tool▁calls▁begin|>",
"<|tool▁call▁begin|>",
"<|tool▁outputs▁begin|>",
"<|tool▁output▁begin|>",
"<|tool▁calls▁begin|>",
"<|tool▁call▁begin|>",
"<|tool▁outputs▁begin|>",
"<|tool▁output▁begin|>",
"<|tool_calls_begin|>",
"<|tool_call_begin|>",
"<|tool_outputs_begin|>",
"<|tool_output_begin|>",
"<|tool_calls_begin|>",
"<|tool_call_begin|>",
"<|tool_outputs_begin|>",
"<|tool_output_begin|>",
];
pub(crate) const TOOL_CALL_END_MARKERS: [&str; 28] = [
"[/TOOL_CALL]",
"</codewhale:tool_call>",
"</tool_call>",
"</invoke>",
"</function_calls>",
"</|DSML|tool_calls>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
"</|DSML|invoke>",
"</|dsml|tool_calls>",
"</|dsml|invoke>",
"</|tool_calls>",
"<|tool▁calls▁end|>",
"<|tool▁call▁end|>",
"<|tool▁outputs▁end|>",
"<|tool▁output▁end|>",
"<|tool▁calls▁end|>",
"<|tool▁call▁end|>",
"<|tool▁outputs▁end|>",
"<|tool▁output▁end|>",
"<|tool_calls_end|>",
"<|tool_call_end|>",
"<|tool_outputs_end|>",
"<|tool_output_end|>",
"<|tool_calls_end|>",
"<|tool_call_end|>",
"<|tool_outputs_end|>",
"<|tool_output_end|>",
];
#[derive(Debug, Default)]
pub(crate) struct ToolCallDeltaFilterState {
in_tool_call: bool,
marker_carry: String,
active_end_marker: Option<&'static str>,
}
pub(crate) const FAKE_WRAPPER_NOTICE: &str =
"Stripped non-API tool-call wrapper from model output (use the API tool channel)";
pub(crate) fn contains_fake_tool_wrapper(text: &str) -> bool {
TOOL_CALL_START_MARKERS.iter().any(|m| text.contains(m))
}
fn find_first_marker(text: &str, markers: &[&str]) -> Option<(usize, usize)> {
markers
.iter()
.filter_map(|marker| text.find(marker).map(|idx| (idx, marker.len())))
.min_by_key(|(idx, _)| *idx)
}
fn find_first_start_marker(text: &str) -> Option<(usize, usize, &'static str)> {
TOOL_CALL_MARKER_PAIRS
.iter()
.filter_map(|(start, end)| text.find(start).map(|idx| (idx, start.len(), *end)))
.min_by_key(|(idx, _, _)| *idx)
}
fn trailing_marker_prefix_len(text: &str, markers: &[&str]) -> usize {
markers
.iter()
.flat_map(|marker| {
marker
.char_indices()
.map(|(idx, _)| idx)
.filter(|idx| *idx > 0)
.chain(std::iter::once(marker.len()))
.filter(|idx| *idx < marker.len())
.filter(|idx| {
let prefix = &marker[..*idx];
text.ends_with(prefix)
})
})
.max()
.unwrap_or(0)
}
fn trailing_start_marker_prefix_len(text: &str) -> usize {
TOOL_CALL_MARKER_PAIRS
.iter()
.flat_map(|(marker, _)| {
marker
.char_indices()
.map(|(idx, _)| idx)
.filter(|idx| *idx > 0)
.chain(std::iter::once(marker.len()))
.filter(|idx| *idx < marker.len())
.filter(|idx| {
let prefix = &marker[..*idx];
text.ends_with(prefix)
})
})
.max()
.unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn filter_tool_call_delta(delta: &str, in_tool_call: &mut bool) -> String {
let mut state = ToolCallDeltaFilterState {
in_tool_call: *in_tool_call,
..ToolCallDeltaFilterState::default()
};
let output = filter_tool_call_delta_with_state(delta, &mut state);
*in_tool_call = state.in_tool_call;
output
}
pub(crate) fn filter_tool_call_delta_with_state(
delta: &str,
state: &mut ToolCallDeltaFilterState,
) -> String {
if delta.is_empty() {
return String::new();
}
let chunk;
let mut rest = if state.marker_carry.is_empty() {
delta
} else {
chunk = format!("{}{delta}", state.marker_carry);
state.marker_carry.clear();
&chunk
};
let mut output = String::new();
loop {
if state.in_tool_call {
let active_end_marker = state.active_end_marker;
let found = active_end_marker
.and_then(|marker| rest.find(marker).map(|idx| (idx, marker.len())))
.or_else(|| find_first_marker(rest, &TOOL_CALL_END_MARKERS));
let Some((idx, len)) = found else {
let keep = active_end_marker.map_or_else(
|| trailing_marker_prefix_len(rest, &TOOL_CALL_END_MARKERS),
|marker| trailing_marker_prefix_len(rest, &[marker]),
);
if keep > 0 {
state.marker_carry.push_str(&rest[rest.len() - keep..]);
}
break;
};
rest = &rest[idx + len..];
state.in_tool_call = false;
state.active_end_marker = None;
} else {
let Some((idx, len, end_marker)) = find_first_start_marker(rest) else {
let keep = trailing_start_marker_prefix_len(rest);
if keep > 0 {
let split = rest.len() - keep;
output.push_str(&rest[..split]);
state.marker_carry.push_str(&rest[split..]);
} else {
output.push_str(rest);
}
break;
};
output.push_str(&rest[..idx]);
rest = &rest[idx + len..];
state.in_tool_call = true;
state.active_end_marker = Some(end_marker);
}
}
output
}
pub(crate) fn flush_tool_call_delta_state(state: &mut ToolCallDeltaFilterState) -> String {
if state.in_tool_call {
state.marker_carry.clear();
return String::new();
}
std::mem::take(&mut state.marker_carry)
}