use std::hash::{Hash, Hasher};
use crate::cm_types::Message;
fn system_prompt_stability_info(messages: &[Message]) -> (u64, usize) {
let sys_content: String = messages
.iter()
.filter(|m| m.role == "system")
.filter_map(|m| m.content.as_ref())
.filter_map(|c| match c {
crate::cm_types::MessageContent::Text(s) => Some(s.as_str()),
_ => None,
})
.collect();
let len = sys_content.len();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
sys_content.hash(&mut hasher);
(hasher.finish(), len)
}
pub fn log_system_prompt_stability(messages: &[Message]) {
let (hash, len) = system_prompt_stability_info(messages);
log::info!(
target: "crate::cm_llm",
"system_prompt_stability hash={:x} chars={}",
hash,
len
);
}
fn single_line_preview(s: &str, max_chars: usize) -> String {
let folded = s.split_whitespace().collect::<Vec<_>>().join(" ");
if max_chars == 0 {
return String::new();
}
let mut iter = folded.chars();
let prefix: String = iter.by_ref().take(max_chars).collect();
if iter.next().is_some() {
format!("{prefix}…(truncated)")
} else {
prefix
}
}
fn sanitize_assistant_tool_call_arguments_for_vendor_in_place(msgs: &mut [Message]) {
use crate::cm_types::sanitize_tool_call_arguments_for_openai_compat;
for m in msgs.iter_mut() {
if !m.role.trim().eq_ignore_ascii_case("assistant") {
continue;
}
let Some(tcs) = m.tool_calls.as_mut() else {
continue;
};
for tc in tcs.iter_mut() {
let orig = tc.function.arguments.as_str();
let s = sanitize_tool_call_arguments_for_openai_compat(orig);
if s == tc.function.arguments {
continue;
}
let trimmed_empty = orig.trim().is_empty();
if trimmed_empty {
log::debug!(
target: "crabmate",
"tool_calls.function.arguments 空串已规范为 {{}} tool_call_id={}",
tc.id
);
} else if s == "{}" && serde_json::from_str::<serde_json::Value>(orig.trim()).is_err() {
log::warn!(
target: "crabmate",
"tool_calls.function.arguments 无法解析为 JSON(已替换为 {{}} 以满足上游校验;常见原因:流式截断、字符串内未转义换行、模型输出非 JSON)。tool_call_id={} preview={}",
tc.id,
single_line_preview(orig, 120)
);
} else {
log::debug!(
target: "crabmate",
"tool_calls.function.arguments 已规整为合法 JSON 形态 tool_call_id={}",
tc.id
);
}
tc.function.arguments = s;
}
}
}
#[inline]
pub fn conversation_messages_to_vendor_body(
messages: &[Message],
fold_system_into_user: bool,
preserve_reasoning_on_assistant_tool_calls: bool,
preserve_deepseek_thinking_reasoning_roundtrip: bool,
) -> Vec<Message> {
log_system_prompt_stability(messages);
let mut v = crate::cm_types::normalize_messages_for_openai_compatible_request(
crate::cm_types::messages_for_api_stripping_reasoning_skip_ui_separators(
messages,
preserve_reasoning_on_assistant_tool_calls,
preserve_deepseek_thinking_reasoning_roundtrip,
),
);
if fold_system_into_user {
v = crate::cm_types::fold_system_messages_into_following_user(v);
}
sanitize_assistant_tool_call_arguments_for_vendor_in_place(&mut v);
v
}
#[inline]
pub fn normalize_stripped_messages_for_vendor_body(
messages: Vec<Message>,
fold_system_into_user: bool,
) -> Vec<Message> {
let mut v = crate::cm_types::normalize_messages_for_openai_compatible_request(messages);
if fold_system_into_user {
v = crate::cm_types::fold_system_messages_into_following_user(v);
}
sanitize_assistant_tool_call_arguments_for_vendor_in_place(&mut v);
v
}