1use crate::api::models::{
4 ChatMessage as OpenAIChatMessage, Content as OpenAIContent, ContentPart as OpenAIContentPart,
5 Role as OpenAIRole, Tool, ToolCall as OpenAIToolCall,
6};
7use crate::models::ContentPart;
8use crate::protocol::{FromProvider, ProtocolResult, ToProvider};
9use bamboo_domain::{FunctionCall, ToolCall};
10use bamboo_domain::{FunctionSchema, ToolSchema};
11use bamboo_domain::{Message, MessagePart, MessagePhase, Role};
12
13pub struct OpenAIProtocol;
15
16impl FromProvider<OpenAIChatMessage> for Message {
21 fn from_provider(msg: OpenAIChatMessage) -> ProtocolResult<Self> {
22 let role = convert_openai_role_to_internal(&msg.role);
23
24 let (content, content_parts) = match msg.content {
25 OpenAIContent::Text(text) => (text, None),
26 OpenAIContent::Parts(parts) => {
27 let text = parts
29 .iter()
30 .filter_map(|part| match part {
31 OpenAIContentPart::Text { text } => Some(text.as_str()),
32 OpenAIContentPart::ImageUrl { .. } => None,
33 })
34 .collect::<Vec<_>>()
35 .join("");
36 let message_parts: Vec<MessagePart> = parts.into_iter().map(Into::into).collect();
37 (text, Some(message_parts))
38 }
39 };
40
41 let tool_calls = msg
42 .tool_calls
43 .map(|calls| calls.into_iter().map(ToolCall::from_provider).collect())
44 .transpose()?;
45 let phase = match msg.phase.as_deref() {
46 Some("commentary") => Some(MessagePhase::Commentary),
47 Some("final_answer") => Some(MessagePhase::FinalAnswer),
48 _ => None,
49 };
50
51 Ok(Message {
52 id: String::new(), role,
54 content,
55 reasoning: None,
56 reasoning_signature: None,
57 content_parts,
58 image_ocr: None,
59 phase,
60 tool_calls,
61 tool_call_id: msg.tool_call_id,
62 tool_success: None,
63 compressed: false,
64 compressed_by_event_id: None,
65 never_compress: false,
66 compression_level: 0,
67 created_at: chrono::Utc::now(),
68 metadata: None,
69 })
70 }
71}
72
73impl FromProvider<OpenAIToolCall> for ToolCall {
74 fn from_provider(tc: OpenAIToolCall) -> ProtocolResult<Self> {
75 Ok(ToolCall {
76 id: tc.id,
77 tool_type: tc.tool_type,
78 function: FunctionCall {
79 name: tc.function.name,
80 arguments: tc.function.arguments,
81 },
82 })
83 }
84}
85
86impl FromProvider<Tool> for ToolSchema {
87 fn from_provider(tool: Tool) -> ProtocolResult<Self> {
88 Ok(ToolSchema {
89 schema_type: tool.tool_type,
90 function: FunctionSchema {
91 name: tool.function.name,
92 description: tool.function.description.unwrap_or_default(),
93 parameters: tool.function.parameters,
94 },
95 })
96 }
97}
98
99impl ToProvider<OpenAIChatMessage> for Message {
104 fn to_provider(&self) -> ProtocolResult<OpenAIChatMessage> {
105 let role = convert_internal_role_to_openai(&self.role);
106
107 let content = match self.content_parts.as_ref() {
108 Some(parts) => {
109 OpenAIContent::Parts(parts.iter().cloned().map(ContentPart::from).collect())
110 }
111 None => OpenAIContent::Text(self.content.clone()),
112 };
113
114 let tool_calls = self
115 .tool_calls
116 .as_ref()
117 .map(|calls| calls.iter().map(|tc| tc.to_provider()).collect())
118 .transpose()?;
119
120 Ok(OpenAIChatMessage {
121 role,
122 content,
123 phase: self.phase.as_ref().map(|phase| phase.as_str().to_string()),
124 tool_calls,
125 tool_call_id: self.tool_call_id.clone(),
126 })
127 }
128}
129
130impl ToProvider<OpenAIToolCall> for ToolCall {
131 fn to_provider(&self) -> ProtocolResult<OpenAIToolCall> {
132 Ok(OpenAIToolCall {
133 id: self.id.clone(),
134 tool_type: self.tool_type.clone(),
135 function: crate::api::models::FunctionCall {
136 name: self.function.name.clone(),
137 arguments: self.function.arguments.clone(),
138 },
139 })
140 }
141}
142
143impl ToProvider<Tool> for ToolSchema {
144 fn to_provider(&self) -> ProtocolResult<Tool> {
145 Ok(Tool {
146 tool_type: self.schema_type.clone(),
147 function: crate::api::models::FunctionDefinition {
148 name: self.function.name.clone(),
149 description: Some(self.function.description.clone()),
150 parameters: self.function.parameters.clone(),
151 },
152 })
153 }
154}
155
156fn convert_openai_role_to_internal(role: &OpenAIRole) -> Role {
161 match role {
162 OpenAIRole::System => Role::System,
163 OpenAIRole::User => Role::User,
164 OpenAIRole::Assistant => Role::Assistant,
165 OpenAIRole::Tool => Role::Tool,
166 }
167}
168
169fn convert_internal_role_to_openai(role: &Role) -> OpenAIRole {
170 match role {
171 Role::System => OpenAIRole::System,
172 Role::User => OpenAIRole::User,
173 Role::Assistant => OpenAIRole::Assistant,
174 Role::Tool => OpenAIRole::Tool,
175 }
176}
177
178#[cfg(test)]
184pub trait OpenAIExt: Sized {
185 fn into_internal(self) -> ProtocolResult<Message>;
186 fn to_openai(&self) -> ProtocolResult<OpenAIChatMessage>;
187}
188
189#[cfg(test)]
190impl OpenAIExt for OpenAIChatMessage {
191 fn into_internal(self) -> ProtocolResult<Message> {
192 Message::from_provider(self)
193 }
194
195 fn to_openai(&self) -> ProtocolResult<OpenAIChatMessage> {
196 Ok(self.clone())
197 }
198}
199
200#[cfg(test)]
201impl OpenAIExt for Message {
202 fn into_internal(self) -> ProtocolResult<Message> {
203 Ok(self)
204 }
205
206 fn to_openai(&self) -> ProtocolResult<OpenAIChatMessage> {
207 self.to_provider()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::api::models::{FunctionCall as OpenAIFunctionCall, Role as OpenAIRole};
215 use bamboo_domain::FunctionCall;
216 use bamboo_domain::Role;
217
218 #[test]
219 fn test_openai_to_internal_simple_message() {
220 let openai_msg = OpenAIChatMessage {
221 role: OpenAIRole::User,
222 content: OpenAIContent::Text("Hello".to_string()),
223 phase: None,
224 tool_calls: None,
225 tool_call_id: None,
226 };
227
228 let internal_msg: Message = openai_msg.into_internal().unwrap();
229
230 assert_eq!(internal_msg.role, Role::User);
231 assert_eq!(internal_msg.content, "Hello");
232 assert!(internal_msg.tool_calls.is_none());
233 }
234
235 #[test]
236 fn test_internal_to_openai_simple_message() {
237 let internal_msg = Message::user("Hello");
238
239 let openai_msg: OpenAIChatMessage = internal_msg.to_openai().unwrap();
240
241 assert_eq!(openai_msg.role, OpenAIRole::User);
242 assert!(matches!(openai_msg.content, OpenAIContent::Text(ref t) if t == "Hello"));
243 assert!(openai_msg.tool_calls.is_none());
244 }
245
246 #[test]
247 fn test_openai_to_internal_with_tool_call() {
248 let openai_msg = OpenAIChatMessage {
249 role: OpenAIRole::Assistant,
250 content: OpenAIContent::Text(String::new()),
251 phase: None,
252 tool_calls: Some(vec![OpenAIToolCall {
253 id: "call_1".to_string(),
254 tool_type: "function".to_string(),
255 function: OpenAIFunctionCall {
256 name: "search".to_string(),
257 arguments: r#"{"q":"test"}"#.to_string(),
258 },
259 }]),
260 tool_call_id: None,
261 };
262
263 let internal_msg: Message = Message::from_provider(openai_msg).unwrap();
264
265 assert_eq!(internal_msg.role, Role::Assistant);
266 assert!(internal_msg.tool_calls.is_some());
267 let tool_calls = internal_msg.tool_calls.unwrap();
268 assert_eq!(tool_calls.len(), 1);
269 assert_eq!(tool_calls[0].id, "call_1");
270 assert_eq!(tool_calls[0].function.name, "search");
271 }
272
273 #[test]
274 fn test_internal_to_openai_with_tool_call() {
275 let tool_call = ToolCall {
276 id: "call_1".to_string(),
277 tool_type: "function".to_string(),
278 function: FunctionCall {
279 name: "search".to_string(),
280 arguments: r#"{"q":"test"}"#.to_string(),
281 },
282 };
283
284 let internal_msg = Message::assistant("", Some(vec![tool_call]));
285
286 let openai_msg: OpenAIChatMessage = internal_msg.to_provider().unwrap();
287
288 assert_eq!(openai_msg.role, OpenAIRole::Assistant);
289 assert!(openai_msg.tool_calls.is_some());
290 let tool_calls = openai_msg.tool_calls.unwrap();
291 assert_eq!(tool_calls.len(), 1);
292 assert_eq!(tool_calls[0].id, "call_1");
293 assert_eq!(tool_calls[0].function.name, "search");
294 assert_eq!(tool_calls[0].function.arguments, r#"{"q":"test"}"#);
295 }
296
297 #[test]
298 fn test_roundtrip_conversion() {
299 let original = Message::user("Hello, world!");
300
301 let openai_msg: OpenAIChatMessage = original.to_provider().unwrap();
303
304 let roundtrip: Message = Message::from_provider(openai_msg).unwrap();
306
307 assert_eq!(roundtrip.role, original.role);
308 assert_eq!(roundtrip.content, original.content);
309 }
310
311 #[test]
312 fn test_tool_schema_conversion() {
313 let openai_tool = Tool {
314 tool_type: "function".to_string(),
315 function: crate::api::models::FunctionDefinition {
316 name: "search".to_string(),
317 description: Some("Search the web".to_string()),
318 parameters: serde_json::json!({
319 "type": "object",
320 "properties": {
321 "q": { "type": "string" }
322 }
323 }),
324 },
325 };
326
327 let internal_schema: ToolSchema = ToolSchema::from_provider(openai_tool.clone()).unwrap();
329 assert_eq!(internal_schema.function.name, "search");
330
331 let roundtrip: Tool = internal_schema.to_provider().unwrap();
333 assert_eq!(roundtrip.function.name, "search");
334 assert_eq!(
335 roundtrip.function.description,
336 Some("Search the web".to_string())
337 );
338 }
339
340 #[test]
341 fn test_extension_trait() {
342 let openai_msg = OpenAIChatMessage {
343 role: OpenAIRole::User,
344 content: OpenAIContent::Text("Test".to_string()),
345 phase: None,
346 tool_calls: None,
347 tool_call_id: None,
348 };
349
350 let internal = openai_msg.into_internal().unwrap();
352 assert_eq!(internal.content, "Test");
353 }
354}