codewhale_core/
request.rs1use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct ChatRequest {
23 pub model: String,
24 pub model_provider: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub system_prompt: Option<String>,
28 pub messages: Vec<ChatMessage>,
29 pub tools: Vec<Value>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub reasoning_effort: Option<String>,
32 #[serde(default)]
33 pub stream: bool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct ChatMessage {
38 pub role: String,
39 pub content: String,
40 #[serde(default)]
41 pub tool_call_id: Option<String>,
42 #[serde(default)]
43 pub tool_calls: Vec<Value>,
44}
45
46#[must_use]
52pub fn build_chat_request(
53 model: impl Into<String>,
54 model_provider: impl Into<String>,
55 system_prompt: Option<String>,
56 messages: Vec<ChatMessage>,
57 tools: Vec<Value>,
58 reasoning_effort: Option<String>,
59) -> ChatRequest {
60 ChatRequest {
61 model: model.into(),
62 model_provider: model_provider.into(),
63 system_prompt,
64 messages,
65 tools,
66 reasoning_effort,
67 stream: true,
68 }
69}
70
71#[must_use]
76pub fn render_request_bytes(req: &ChatRequest) -> Vec<u8> {
77 serde_json::to_vec(req).expect("ChatRequest is serializable")
78}
79
80#[must_use]
84pub fn byte_parity(a: &ChatRequest, b: &ChatRequest) -> Option<usize> {
85 let ab = render_request_bytes(a);
86 let bb = render_request_bytes(b);
87 if ab == bb {
88 None
89 } else {
90 ab.iter()
91 .zip(bb.iter())
92 .position(|(x, y)| x != y)
93 .or(Some(ab.len().min(bb.len())))
94 }
95}
96
97#[must_use]
102pub fn preview_human(req: &ChatRequest) -> String {
103 let mut out = String::new();
104 out.push_str(&format!("model: {} ({})\n", req.model, req.model_provider));
105 if let Some(sp) = req.system_prompt.as_deref() {
106 out.push_str(&format!("system: {} chars\n", sp.len()));
107 }
108 out.push_str(&format!("messages: {}\n", req.messages.len()));
109 out.push_str(&format!("tools: {}\n", req.tools.len()));
110 if let Some(effort) = req.reasoning_effort.as_deref() {
111 out.push_str(&format!("reasoning_effort: {effort}\n"));
112 }
113 out
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use serde_json::json;
120
121 #[test]
122 fn byte_identical_for_same_inputs() {
123 let msgs = vec![ChatMessage {
124 role: "user".into(),
125 content: "hello".into(),
126 tool_call_id: None,
127 tool_calls: vec![],
128 }];
129 let a = build_chat_request(
130 "deepseek-v4-flash",
131 "deepseek",
132 Some("sys".into()),
133 msgs.clone(),
134 vec![json!({"name":"read"})],
135 Some("low".into()),
136 );
137 let b = build_chat_request(
138 "deepseek-v4-flash",
139 "deepseek",
140 Some("sys".into()),
141 msgs,
142 vec![json!({"name":"read"})],
143 Some("low".into()),
144 );
145 assert_eq!(byte_parity(&a, &b), None);
146 assert_eq!(render_request_bytes(&a), render_request_bytes(&b));
147 }
148
149 #[test]
150 fn dryrun_is_pure_inspection() {
151 let req = build_chat_request("m", "deepseek", None, vec![], vec![], None);
152 let preview = preview_human(&req);
153 assert!(preview.contains("model: m"));
154 let req2 = build_chat_request("m", "deepseek", None, vec![], vec![], None);
156 assert_eq!(render_request_bytes(&req), render_request_bytes(&req2));
157 }
158}