use crate::models::ToolCaller;
#[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) input_buffer: String,
}
const DEFAULT_STREAM_CHUNK_TIMEOUT_SECS: u64 = 300;
const MIN_STREAM_CHUNK_TIMEOUT_SECS: u64 = 1;
const MAX_STREAM_CHUNK_TIMEOUT_SECS: u64 = 3600;
const STREAM_IDLE_TIMEOUT_ENV: &str = "DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS";
pub(super) fn stream_chunk_timeout_secs() -> u64 {
stream_chunk_timeout_secs_from_env(std::env::var(STREAM_IDLE_TIMEOUT_ENV).ok().as_deref())
}
fn stream_chunk_timeout_secs_from_env(value: Option<&str>) -> u64 {
value
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_STREAM_CHUNK_TIMEOUT_SECS)
.clamp(MIN_STREAM_CHUNK_TIMEOUT_SECS, MAX_STREAM_CHUNK_TIMEOUT_SECS)
}
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(crate) const TOOL_CALL_START_MARKERS: [&str; 5] = [
"[TOOL_CALL]",
"<deepseek:tool_call",
"<tool_call",
"<invoke ",
"<function_calls>",
];
pub(crate) const TOOL_CALL_END_MARKERS: [&str; 5] = [
"[/TOOL_CALL]",
"</deepseek:tool_call>",
"</tool_call>",
"</invoke>",
"</function_calls>",
];
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)
}
pub(crate) fn filter_tool_call_delta(delta: &str, in_tool_call: &mut bool) -> String {
if delta.is_empty() {
return String::new();
}
let mut output = String::new();
let mut rest = delta;
loop {
if *in_tool_call {
let Some((idx, len)) = find_first_marker(rest, &TOOL_CALL_END_MARKERS) else {
break;
};
rest = &rest[idx + len..];
*in_tool_call = false;
} else {
let Some((idx, len)) = find_first_marker(rest, &TOOL_CALL_START_MARKERS) else {
output.push_str(rest);
break;
};
output.push_str(&rest[..idx]);
rest = &rest[idx + len..];
*in_tool_call = true;
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_chunk_timeout_defaults_and_clamps_env_values() {
assert_eq!(stream_chunk_timeout_secs_from_env(None), 300);
assert_eq!(
stream_chunk_timeout_secs_from_env(Some("not-a-number")),
300
);
assert_eq!(stream_chunk_timeout_secs_from_env(Some("0")), 1);
assert_eq!(stream_chunk_timeout_secs_from_env(Some("90")), 90);
assert_eq!(stream_chunk_timeout_secs_from_env(Some("99999")), 3600);
}
}