pub mod llm_config;
pub use llm_config::*;
pub mod llm_gateway_presets;
pub use llm_gateway_presets::*;
use serde::Serialize;
pub const OPENAI_CHAT_COMPLETIONS_REL_PATH: &str = "chat/completions";
pub const OPENAI_MODELS_REL_PATH: &str = "models";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub enum LlmSeedOverride {
#[default]
FromConfig,
Fixed(i64),
OmitFromRequest,
}
#[inline]
pub fn resolved_llm_seed(base: Option<i64>, override_: LlmSeedOverride) -> Option<i64> {
match override_ {
LlmSeedOverride::FromConfig => base,
LlmSeedOverride::Fixed(n) => Some(n),
LlmSeedOverride::OmitFromRequest => None,
}
}
#[cfg(test)]
mod llm_seed_tests {
use super::{LlmSeedOverride, resolved_llm_seed};
#[test]
fn resolved_seed_respects_override() {
assert_eq!(
resolved_llm_seed(Some(1), LlmSeedOverride::FromConfig),
Some(1)
);
assert_eq!(
resolved_llm_seed(Some(1), LlmSeedOverride::Fixed(42)),
Some(42)
);
assert_eq!(
resolved_llm_seed(Some(1), LlmSeedOverride::OmitFromRequest),
None
);
assert_eq!(resolved_llm_seed(None, LlmSeedOverride::FromConfig), None);
}
}
mod chat_api;
mod mcp_types;
mod message;
mod real_user_message;
pub mod server_injected_user;
pub mod session_mode;
mod skill_slash_strip;
mod tiktoken_snapshot;
pub mod path_utils;
pub mod text_utils;
pub use chat_api::*;
pub use mcp_types::*;
pub use message::*;
pub use real_user_message::{
first_real_user_task_content, is_real_user_task_message, last_real_user_message_index,
last_real_user_task_content, messages_slice_since_last_real_user,
};
pub use server_injected_user::{
is_server_injected_user_message, strip_orchestration_injected_users_for_conversation_store,
};
pub use session_mode::{SessionMode, parse_optional_session_mode, parse_session_mode};
pub use skill_slash_strip::{is_reserved_slash_head, strip_explicit_skill_slash_prefix_for_model};
pub use tiktoken_snapshot::TiktokenPromptTokensSnapshot;
#[cfg(test)]
mod server_injected_user_store_tests {
use super::*;
#[test]
fn strip_orchestration_injected_users_keeps_real_user_and_workspace_profile() {
let mut v = vec![
Message::user_only("真实用户"),
Message::user_plan_rewrite_injection("你的最终回答缺少**结构化规划**"),
Message::user_first_turn_workspace_context("工作区画像"),
Message::assistant_only("ok"),
];
strip_orchestration_injected_users_for_conversation_store(&mut v);
assert_eq!(v.len(), 3);
assert!(v.iter().any(|m| {
message_content_as_str(&m.content).is_some_and(|c| c.contains("真实用户"))
}));
assert!(
v.iter()
.any(crate::cm_types::is_first_turn_workspace_context_injection)
);
}
}
#[cfg(test)]
mod api_messages_strip_tests {
use super::*;
#[test]
fn filter_web_client_snapshot_drops_system_prompt_and_injections() {
let inj_mem = Message {
role: "user".to_string(),
content: Some(MessageContent::Text("mem".to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: Some(CRABMATE_LONG_TERM_MEMORY_NAME.to_string()),
tool_call_id: None,
};
let inj_cl = Message {
role: "user".to_string(),
content: Some(MessageContent::Text("cl".to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: Some(CRABMATE_WORKSPACE_CHANGELIST_NAME.to_string()),
tool_call_id: None,
};
let inj_ctx = Message::user_first_turn_workspace_context("profile");
let sys = Message::system_only("do not leak to web list");
let plain = Message::user_only("hi");
let reject = Message::user_planner_tool_call_reject_injection(format!(
"{STAGED_PLANNER_TOOL_CALL_REJECT_CONTENT_PREFIX}\n请重写"
));
let v = vec![sys, inj_mem, inj_cl, inj_ctx, plain.clone(), reject];
let out = filter_messages_for_web_client_snapshot(&v);
assert_eq!(out.len(), 1);
assert_eq!(out[0], plain);
}
#[test]
fn web_snapshot_hides_registered_server_injected_users() {
let plain = Message::user_only("真实用户提问");
let cases: Vec<(Message, bool)> = vec![
(plain.clone(), true),
(
Message::user_plan_rewrite_injection(
"你的最终回答缺少**结构化规划**。请加入 agent_reply_plan JSON",
),
false,
),
];
for (msg, expect_visible) in cases {
let out = filter_messages_for_web_client_snapshot(std::slice::from_ref(&msg));
assert_eq!(
out.len() == 1,
expect_visible,
"visible={expect_visible} name={:?}",
msg.name
);
if expect_visible {
assert_eq!(out[0], msg);
}
}
}
#[test]
fn filter_web_client_snapshot_keeps_timeline_system_markers() {
let tl = Message {
role: "system".to_string(),
content: Some(MessageContent::Text(r#"{"kind":"x"}"#.to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: Some("crabmate_timeline".to_string()),
tool_call_id: None,
};
let u = Message::user_only("hi");
let v = vec![Message::system_only("sys"), tl.clone(), u.clone()];
let out = filter_messages_for_web_client_snapshot(&v);
assert_eq!(out.len(), 2);
assert_eq!(out[0], tl);
assert_eq!(out[1], u);
}
#[test]
fn skip_ui_separator_and_strip_reasoning_one_pass() {
let sep = Message::chat_ui_separator(true);
let assistant = Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text("body".to_string())),
reasoning_content: Some("chain".to_string()),
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
};
let v = vec![Message::user_only("u"), sep, assistant];
let out = messages_for_api_stripping_reasoning_skip_ui_separators(&v, false, false);
assert_eq!(out.len(), 2);
assert_eq!(out[0].role, "user");
assert_eq!(out[1].role, "assistant");
assert_eq!(message_content_as_str(&out[1].content), Some("body"));
assert!(out[1].reasoning_content.is_none());
}
#[test]
fn strip_reasoning_only_matches_composing_without_separators() {
let assistant = Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text("x".to_string())),
reasoning_content: Some("r".to_string()),
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
};
let v = vec![Message::user_only("u"), assistant];
let b = messages_for_api_stripping_reasoning_skip_ui_separators(&v, false, false);
assert_eq!(b.len(), 2);
assert!(b[1].reasoning_content.is_none());
}
#[test]
fn preserve_reasoning_for_assistant_tool_calls_when_requested() {
let tc = ToolCall {
id: "x".to_string(),
typ: "function".to_string(),
function: FunctionCall {
name: "f".to_string(),
arguments: "{}".to_string(),
},
};
let asst = Message {
role: "assistant".to_string(),
content: None,
reasoning_content: Some("think".to_string()),
reasoning_details: None,
tool_calls: Some(vec![tc.clone()]),
name: None,
tool_call_id: None,
};
let kept = message_clone_stripping_reasoning_for_api(&asst, true, false);
assert_eq!(kept.reasoning_content.as_deref(), Some("think"));
assert!(kept.reasoning_details.is_none());
let gone = message_clone_stripping_reasoning_for_api(&asst, false, false);
assert!(gone.reasoning_content.is_none());
}
#[test]
fn preserve_inserts_empty_reasoning_when_tool_calls_but_missing() {
let tc = ToolCall {
id: "x".to_string(),
typ: "function".to_string(),
function: FunctionCall {
name: "f".to_string(),
arguments: "{}".to_string(),
},
};
let asst = Message {
role: "assistant".to_string(),
content: None,
reasoning_content: None,
reasoning_details: None,
tool_calls: Some(vec![tc]),
name: None,
tool_call_id: None,
};
let out = message_clone_stripping_reasoning_for_api(&asst, true, false);
assert_eq!(out.reasoning_content.as_deref(), Some(""));
}
#[test]
fn deepseek_thinking_strips_reasoning_when_no_tool_calls_per_vendor_doc() {
let asst = Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text("hi".to_string())),
reasoning_content: Some("think only".to_string()),
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
};
let out = message_clone_stripping_reasoning_for_api(&asst, false, true);
assert!(out.reasoning_content.is_none());
}
#[test]
fn deepseek_thinking_keeps_reasoning_when_tool_calls_present() {
let tc = ToolCall {
id: "t".to_string(),
typ: "function".to_string(),
function: FunctionCall {
name: "f".to_string(),
arguments: "{}".to_string(),
},
};
let asst = Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text("call".to_string())),
reasoning_content: Some("chain".to_string()),
reasoning_details: None,
tool_calls: Some(vec![tc]),
name: None,
tool_call_id: None,
};
let out = message_clone_stripping_reasoning_for_api(&asst, false, true);
assert_eq!(out.reasoning_content.as_deref(), Some("chain"));
}
#[test]
fn api_strip_removes_explicit_skill_slash_from_user() {
let msgs = vec![
Message::system_only("sys"),
Message::user_only("/rust-style 分析 crate"),
Message::user_only("普通问题"),
];
let out = messages_for_api_stripping_reasoning_skip_ui_separators(&msgs, false, false);
let users: Vec<_> = out
.iter()
.filter(|m| m.role == "user")
.map(|m| message_content_plain_for_chat_display(&m.content))
.collect();
assert_eq!(
users,
vec!["分析 crate".to_string(), "普通问题".to_string()]
);
assert!(
message_content_plain_for_chat_display(&msgs[1].content).starts_with("/rust-style")
);
}
}
#[cfg(test)]
mod normalize_messages_tests {
use super::*;
fn asst(content: &str) -> Message {
Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text(content.to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
}
}
fn asst_with_tc(content: &str) -> Message {
Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text(content.to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: Some(vec![ToolCall {
id: "tc1".to_string(),
typ: "function".to_string(),
function: FunctionCall {
name: "noop".to_string(),
arguments: "{}".to_string(),
},
}]),
name: None,
tool_call_id: None,
}
}
#[test]
fn merges_adjacent_assistant_placeholder_after_prior_assistant() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst("prior"),
asst(""),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(n[2].role, "assistant");
assert_eq!(message_content_as_str(&n[2].content), Some("prior"));
}
#[test]
fn drops_trailing_empty_assistant() {
let v = vec![Message::system_only("s"), Message::user_only("u"), asst("")];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 2);
assert_eq!(n[1].role, "user");
}
#[test]
fn merges_streaming_partial_then_full_assistant() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst("hel"),
asst("hello"),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(message_content_as_str(&n[2].content), Some("hello"));
}
#[test]
fn strips_orphan_tool_calls_when_followed_by_assistant_reply() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst_with_tc("calling tool"),
asst("final answer"),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(n[2].role, "assistant");
assert!(n[2].tool_calls.is_none());
assert!(
message_content_as_str(&n[2].content)
.unwrap()
.contains("calling tool")
);
assert!(
message_content_as_str(&n[2].content)
.unwrap()
.contains("final answer")
);
}
#[test]
fn strips_tool_calls_when_followed_by_empty_assistant_only() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst_with_tc("x"),
asst(""),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(message_content_as_str(&n[2].content), Some("x"));
assert!(n[2].tool_calls.is_none());
}
#[test]
fn preserves_merged_tool_calls_when_tool_follows() {
let tool = Message {
role: "tool".to_string(),
content: Some(MessageContent::Text(r#"{"ok":true}"#.to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: Some("tc1".to_string()),
};
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst("reasoning"),
asst_with_tc(""),
tool,
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 4);
assert_eq!(n[2].role, "assistant");
assert!(n[2].tool_calls.as_ref().is_some_and(|c| !c.is_empty()));
assert_eq!(n[3].role, "tool");
}
#[test]
fn collapses_three_consecutive_assistants() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst("a"),
asst("b"),
asst("c"),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(n[2].role, "assistant");
let c = message_content_as_str(&n[2].content).unwrap();
assert!(c.contains('a') && c.contains('c'));
}
#[test]
fn merges_when_assistant_role_has_whitespace() {
let mut odd = asst("x");
odd.role = " Assistant ".to_string();
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
odd,
asst("y"),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(n[2].role, "assistant");
}
#[test]
fn merges_assistant_then_assistant_with_tool_calls() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst("partial"),
asst_with_tc(""),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert!(n[2].tool_calls.is_none());
assert!(
message_content_as_str(&n[2].content)
.unwrap()
.contains("partial")
);
}
#[test]
fn drops_trailing_assistant_when_orphan_tool_calls_cleared_and_content_empty() {
let v = vec![
Message::system_only("s"),
Message::user_only("u"),
asst_with_tc(""),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 2);
assert_eq!(n[1].role, "user");
}
#[test]
fn drops_infix_empty_assistant_without_tool_calls() {
let v = vec![
Message::system_only("s"),
Message::user_only("u1"),
Message {
role: "assistant".to_string(),
content: None,
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
},
Message::user_only("u2"),
];
let n = normalize_messages_for_openai_compatible_request(v);
assert_eq!(n.len(), 3);
assert_eq!(n[0].role, "system");
assert_eq!(n[1].role, "user");
assert_eq!(n[2].role, "user");
}
}
#[cfg(test)]
mod fold_system_messages_tests {
use super::*;
#[test]
fn merges_system_into_following_user() {
let v = vec![Message::system_only("sys"), Message::user_only("hi")];
let o = fold_system_messages_into_following_user(v);
assert_eq!(o.len(), 1);
assert_eq!(o[0].role, "user");
assert_eq!(message_content_as_str(&o[0].content), Some("sys\n\nhi"));
}
#[test]
fn joins_multiple_system_blocks() {
let v = vec![
Message::system_only("a"),
Message::system_only("b"),
Message::user_only("u"),
];
let o = fold_system_messages_into_following_user(v);
assert_eq!(o.len(), 1);
assert_eq!(message_content_as_str(&o[0].content), Some("a\n\nb\n\nu"));
}
#[test]
fn system_before_assistant_inserts_user_carrier() {
let a = Message {
role: "assistant".to_string(),
content: Some(MessageContent::Text("reply".to_string())),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
};
let v = vec![Message::system_only("instr"), a];
let o = fold_system_messages_into_following_user(v);
assert_eq!(o.len(), 2);
assert_eq!(o[0].role, "user");
assert_eq!(message_content_as_str(&o[0].content), Some("instr"));
assert_eq!(o[1].role, "assistant");
}
#[test]
fn trailing_system_only_becomes_user() {
let v = vec![Message::system_only("orphan")];
let o = fold_system_messages_into_following_user(v);
assert_eq!(o.len(), 1);
assert_eq!(o[0].role, "user");
assert_eq!(message_content_as_str(&o[0].content), Some("orphan"));
}
#[test]
fn trims_system_role_case_and_whitespace() {
let mut s = Message::system_only("x");
s.role = " SYSTEM ".to_string();
let v = vec![s, Message::user_only("y")];
let o = fold_system_messages_into_following_user(v);
assert_eq!(o.len(), 1);
assert!(
message_content_as_str(&o[0].content)
.unwrap()
.starts_with("x")
);
}
}
#[cfg(test)]
mod sanitize_tool_call_arguments_tests {
use super::{
prepare_tool_call_arguments_for_local_execution,
sanitize_tool_call_arguments_for_openai_compat,
};
#[test]
fn empty_and_whitespace_become_empty_object() {
assert_eq!(sanitize_tool_call_arguments_for_openai_compat(""), "{}");
assert_eq!(sanitize_tool_call_arguments_for_openai_compat(" "), "{}");
assert_eq!(prepare_tool_call_arguments_for_local_execution(""), "{}");
}
#[test]
fn valid_json_round_trips_compact() {
assert_eq!(
sanitize_tool_call_arguments_for_openai_compat(r#"{"path":"a"}"#),
r#"{"path":"a"}"#
);
}
#[test]
fn invalid_json_becomes_empty_object_for_vendor_only() {
assert_eq!(sanitize_tool_call_arguments_for_openai_compat("{"), "{}");
assert_eq!(
sanitize_tool_call_arguments_for_openai_compat("not json"),
"{}"
);
assert_eq!(prepare_tool_call_arguments_for_local_execution("{"), "{");
assert_eq!(
prepare_tool_call_arguments_for_local_execution("not json"),
"not json"
);
}
#[test]
fn escapes_literal_newline_inside_json_string() {
let raw = concat!("{\"code\": \"def f():", "\n", " pass\"}");
let out = sanitize_tool_call_arguments_for_openai_compat(raw);
let v: serde_json::Value = serde_json::from_str(&out).expect("sanitized must parse");
assert_eq!(v["code"], "def f():\n pass");
let local = prepare_tool_call_arguments_for_local_execution(raw);
let v2: serde_json::Value = serde_json::from_str(&local).expect("local must parse");
assert_eq!(v2["code"], "def f():\n pass");
}
#[test]
fn repairs_truncated_string_and_object() {
let raw = r#"{"code": "partial"#;
let out = sanitize_tool_call_arguments_for_openai_compat(raw);
let v: serde_json::Value = serde_json::from_str(&out).expect("sanitized must parse");
assert_eq!(v["code"], "partial");
}
}