use serde_json::Value;
const THINK_ON: &str = "<|think_on|>";
const THINK_OFF: &str = "<|think_off|>";
pub(super) fn autoclose_think_before_tool_call(content: &str) -> std::borrow::Cow<'_, str> {
let Some(tool_pos) = content.find("<tool_call>") else {
return std::borrow::Cow::Borrowed(content);
};
let Some(last_think) = content.rfind("<think>") else {
return std::borrow::Cow::Borrowed(content);
};
let last_close = content.rfind("</think>");
let think_unclosed = match last_close {
None => true,
Some(close) => close < last_think,
};
if !think_unclosed {
return std::borrow::Cow::Borrowed(content);
}
if tool_pos > last_think {
let mut out = String::with_capacity(content.len() + "</think>".len());
out.push_str(&content[..tool_pos]);
out.push_str("</think>");
out.push_str(&content[tool_pos..]);
std::borrow::Cow::Owned(out)
} else {
std::borrow::Cow::Owned(format!("{content}</think>"))
}
}
pub(crate) fn resolve_think_control(messages: &[Value]) -> (Vec<Value>, Option<bool>) {
let mut effective: Option<bool> = None;
let out = messages
.iter()
.map(|msg| {
let mut msg = msg.clone();
if let Some(content) = msg.get_mut("content") {
strip_controls_in_content(content, &mut effective);
}
msg
})
.collect();
(out, effective)
}
fn strip_controls_in_content(content: &mut Value, effective: &mut Option<bool>) {
match content {
Value::String(s) => {
if let Some(stripped) = strip_controls_in_str(s, effective) {
*s = stripped;
}
}
Value::Array(items) => {
for item in items.iter_mut() {
if let Some(text) = item.get_mut("text").and_then(|t| match t {
Value::String(s) => Some(s),
_ => None,
}) && let Some(stripped) = strip_controls_in_str(text, effective)
{
*text = stripped;
}
}
}
_ => {}
}
}
fn strip_controls_in_str(s: &str, effective: &mut Option<bool>) -> Option<String> {
if !s.contains(THINK_ON) && !s.contains(THINK_OFF) {
return None;
}
let mut positions: Vec<(usize, bool)> = Vec::new();
for (idx, _) in s.match_indices(THINK_OFF) {
positions.push((idx, false));
}
for (idx, _) in s.match_indices(THINK_ON) {
positions.push((idx, true));
}
positions.sort_by_key(|(idx, _)| *idx);
if let Some((_, last)) = positions.last() {
*effective = Some(*last);
}
let stripped = s.replace(THINK_OFF, "").replace(THINK_ON, "");
Some(stripped)
}
pub(crate) fn autoclose_assistant_think(messages: &mut [Value]) {
for msg in messages.iter_mut() {
let is_assistant = msg.get("role").and_then(|r| r.as_str()) == Some("assistant");
if !is_assistant {
continue;
}
let Some(Value::String(content)) = msg.get_mut("content") else {
continue;
};
if let std::borrow::Cow::Owned(fixed) = autoclose_think_before_tool_call(content) {
*content = fixed;
}
}
}
pub(crate) fn remap_developer_role(messages: Vec<Value>) -> Vec<Value> {
let role_of = |m: &Value| -> Option<String> {
m.get("role").and_then(|r| r.as_str()).map(str::to_string)
};
let is_sys_level =
|m: &Value| matches!(role_of(m).as_deref(), Some("developer") | Some("system"));
let has_dev = messages
.iter()
.any(|m| role_of(m).as_deref() == Some("developer"));
if !has_dev {
return messages;
}
if messages.iter().filter(|m| is_sys_level(m)).count()
== messages
.iter()
.filter(|m| role_of(m).as_deref() == Some("developer"))
.count()
{
let mut messages = messages;
for m in messages.iter_mut() {
if role_of(m).as_deref() == Some("developer")
&& let Some(role) = m.get_mut("role")
{
*role = Value::String("system".to_string());
}
}
return messages;
}
let mut merged = String::new();
let mut out: Vec<Value> = Vec::with_capacity(messages.len());
let mut slot: Option<usize> = None;
for m in messages {
if is_sys_level(&m) {
if let Some(s) = m.get("content").and_then(|c| c.as_str()) {
if !merged.is_empty() && !s.is_empty() {
merged.push_str("\n\n");
}
merged.push_str(s);
if slot.is_none() {
slot = Some(out.len());
out.push(Value::Null);
}
} else {
let mut m = m;
if let Some(role) = m.get_mut("role") {
*role = Value::String("system".to_string());
}
out.push(m);
}
} else {
out.push(m);
}
}
if let Some(i) = slot {
let mut sys = serde_json::Map::new();
sys.insert("role".to_string(), Value::String("system".to_string()));
sys.insert("content".to_string(), Value::String(merged));
out[i] = Value::Object(sys);
}
out
}
#[cfg(test)]
mod tests;