|
1 | 1 | use super::*; |
| 2 | +use crate::message::Message; |
| 3 | +use crate::tools::{ToolCall, ToolChoice, ToolDefinition}; |
2 | 4 | use crate::util::mask_api_key; |
3 | 5 | use std::time::Duration; |
4 | 6 |
|
@@ -126,3 +128,193 @@ fn test_provider_stores_auth_source() { |
126 | 128 | let guard = provider.refreshed_api_key.lock().unwrap(); |
127 | 129 | assert!(guard.is_none()); |
128 | 130 | } |
| 131 | + |
| 132 | +// ── Responses API conversion tests ────────────────────────────────── |
| 133 | + |
| 134 | +#[test] |
| 135 | +fn test_responses_convert_messages_basic() { |
| 136 | + use async_openai::types::responses::{InputItem, Role}; |
| 137 | + |
| 138 | + let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")]; |
| 139 | + |
| 140 | + let items = responses::convert_messages_to_input(messages).unwrap(); |
| 141 | + assert_eq!(items.len(), 2); |
| 142 | + |
| 143 | + // First item: user message |
| 144 | + if let InputItem::EasyMessage(msg) = &items[0] { |
| 145 | + assert!(matches!(msg.role, Role::User)); |
| 146 | + } else { |
| 147 | + panic!("Expected EasyMessage for user"); |
| 148 | + } |
| 149 | + |
| 150 | + // Second item: assistant message |
| 151 | + if let InputItem::EasyMessage(msg) = &items[1] { |
| 152 | + assert!(matches!(msg.role, Role::Assistant)); |
| 153 | + } else { |
| 154 | + panic!("Expected EasyMessage for assistant"); |
| 155 | + } |
| 156 | +} |
| 157 | + |
| 158 | +#[test] |
| 159 | +fn test_responses_convert_messages_with_tool_calls() { |
| 160 | + use async_openai::types::responses::{InputItem, Item}; |
| 161 | + |
| 162 | + let tool_calls = vec![ToolCall { |
| 163 | + id: "call_123".to_string(), |
| 164 | + name: "get_weather".to_string(), |
| 165 | + arguments: r#"{"city":"Seoul"}"#.to_string(), |
| 166 | + thought_signature: None, |
| 167 | + }]; |
| 168 | + |
| 169 | + let messages = vec![ |
| 170 | + Message::assistant_with_tool_calls("Let me check", tool_calls), |
| 171 | + Message::tool_response("call_123", r#"{"temp":20}"#), |
| 172 | + ]; |
| 173 | + |
| 174 | + let items = responses::convert_messages_to_input(messages).unwrap(); |
| 175 | + assert_eq!(items.len(), 3); // text + function_call + function_call_output |
| 176 | + |
| 177 | + // First: assistant text |
| 178 | + assert!(matches!(&items[0], InputItem::EasyMessage(_))); |
| 179 | + |
| 180 | + // Second: function call |
| 181 | + if let InputItem::Item(Item::FunctionCall(fc)) = &items[1] { |
| 182 | + assert_eq!(fc.call_id, "call_123"); |
| 183 | + assert_eq!(fc.name, "get_weather"); |
| 184 | + assert_eq!(fc.arguments, r#"{"city":"Seoul"}"#); |
| 185 | + } else { |
| 186 | + panic!("Expected Item::FunctionCall, got {:?}", &items[1]); |
| 187 | + } |
| 188 | + |
| 189 | + // Third: function call output |
| 190 | + if let InputItem::Item(Item::FunctionCallOutput(fco)) = &items[2] { |
| 191 | + assert_eq!(fco.call_id, "call_123"); |
| 192 | + } else { |
| 193 | + panic!("Expected Item::FunctionCallOutput, got {:?}", &items[2]); |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +#[test] |
| 198 | +fn test_responses_extract_instructions() { |
| 199 | + let messages = vec![ |
| 200 | + Message::system("You are helpful"), |
| 201 | + Message::user("Hello"), |
| 202 | + Message::system("Extra system msg"), |
| 203 | + ]; |
| 204 | + |
| 205 | + let (instructions, rest) = responses::extract_instructions(messages); |
| 206 | + |
| 207 | + assert_eq!(instructions, Some("You are helpful".to_string())); |
| 208 | + assert_eq!(rest.len(), 2); |
| 209 | + // First remaining is user |
| 210 | + assert_eq!(rest[0].role, MessageRole::User); |
| 211 | + // Second remaining is the extra system (handled as developer in convert_messages_to_input) |
| 212 | + assert_eq!(rest[1].role, MessageRole::System); |
| 213 | +} |
| 214 | + |
| 215 | +#[test] |
| 216 | +fn test_responses_convert_tools() { |
| 217 | + use async_openai::types::responses::Tool; |
| 218 | + |
| 219 | + let tools = vec![ |
| 220 | + ToolDefinition::new( |
| 221 | + "get_weather", |
| 222 | + "Get weather for a location", |
| 223 | + serde_json::json!({ |
| 224 | + "type": "object", |
| 225 | + "properties": { |
| 226 | + "location": {"type": "string"} |
| 227 | + }, |
| 228 | + "required": ["location"] |
| 229 | + }), |
| 230 | + ), |
| 231 | + ToolDefinition::new("search", "Search the web", serde_json::json!({})), |
| 232 | + ]; |
| 233 | + |
| 234 | + let converted = responses::convert_tools(tools); |
| 235 | + assert_eq!(converted.len(), 2); |
| 236 | + |
| 237 | + if let Tool::Function(ft) = &converted[0] { |
| 238 | + assert_eq!(ft.name, "get_weather"); |
| 239 | + assert_eq!(ft.description.as_deref(), Some("Get weather for a location")); |
| 240 | + assert!(ft.parameters.is_some()); |
| 241 | + } else { |
| 242 | + panic!("Expected Tool::Function"); |
| 243 | + } |
| 244 | +} |
| 245 | + |
| 246 | +#[test] |
| 247 | +fn test_responses_convert_tool_choice() { |
| 248 | + use async_openai::types::responses::{ToolChoiceOptions, ToolChoiceParam}; |
| 249 | + |
| 250 | + assert!(matches!( |
| 251 | + responses::convert_tool_choice(&ToolChoice::Auto), |
| 252 | + ToolChoiceParam::Mode(ToolChoiceOptions::Auto) |
| 253 | + )); |
| 254 | + assert!(matches!( |
| 255 | + responses::convert_tool_choice(&ToolChoice::None), |
| 256 | + ToolChoiceParam::Mode(ToolChoiceOptions::None) |
| 257 | + )); |
| 258 | + assert!(matches!( |
| 259 | + responses::convert_tool_choice(&ToolChoice::Required), |
| 260 | + ToolChoiceParam::Mode(ToolChoiceOptions::Required) |
| 261 | + )); |
| 262 | + // Tool(name) falls back to Auto |
| 263 | + assert!(matches!( |
| 264 | + responses::convert_tool_choice(&ToolChoice::Tool("foo".to_string())), |
| 265 | + ToolChoiceParam::Mode(ToolChoiceOptions::Auto) |
| 266 | + )); |
| 267 | +} |
| 268 | + |
| 269 | +#[test] |
| 270 | +fn test_responses_extract_text_from_output() { |
| 271 | + use async_openai::types::responses::{ |
| 272 | + AssistantRole, OutputItem, OutputMessage, OutputMessageContent, OutputStatus, |
| 273 | + OutputTextContent, |
| 274 | + }; |
| 275 | + |
| 276 | + let output = vec![OutputItem::Message(OutputMessage { |
| 277 | + id: "msg_1".to_string(), |
| 278 | + role: AssistantRole::Assistant, |
| 279 | + status: OutputStatus::Completed, |
| 280 | + content: vec![OutputMessageContent::OutputText(OutputTextContent { |
| 281 | + text: "Hello world".to_string(), |
| 282 | + annotations: vec![], |
| 283 | + logprobs: None, |
| 284 | + })], |
| 285 | + })]; |
| 286 | + |
| 287 | + let text = responses::extract_text(&output); |
| 288 | + assert_eq!(text, "Hello world"); |
| 289 | +} |
| 290 | + |
| 291 | +#[test] |
| 292 | +fn test_responses_extract_tool_calls_from_output() { |
| 293 | + use async_openai::types::responses::{FunctionToolCall, OutputItem}; |
| 294 | + |
| 295 | + let output = vec![ |
| 296 | + OutputItem::FunctionCall(FunctionToolCall { |
| 297 | + call_id: "call_abc".to_string(), |
| 298 | + name: "search".to_string(), |
| 299 | + arguments: r#"{"q":"rust"}"#.to_string(), |
| 300 | + id: Some("fc_1".to_string()), |
| 301 | + status: None, |
| 302 | + }), |
| 303 | + OutputItem::FunctionCall(FunctionToolCall { |
| 304 | + call_id: "call_def".to_string(), |
| 305 | + name: "read_file".to_string(), |
| 306 | + arguments: r#"{"path":"/tmp"}"#.to_string(), |
| 307 | + id: None, |
| 308 | + status: None, |
| 309 | + }), |
| 310 | + ]; |
| 311 | + |
| 312 | + let calls = responses::extract_tool_calls(&output); |
| 313 | + assert_eq!(calls.len(), 2); |
| 314 | + assert_eq!(calls[0].id, "call_abc"); |
| 315 | + assert_eq!(calls[0].name, "search"); |
| 316 | + assert_eq!(calls[1].id, "call_def"); |
| 317 | + assert_eq!(calls[1].name, "read_file"); |
| 318 | + // thought_signature should be None |
| 319 | + assert!(calls[0].thought_signature.is_none()); |
| 320 | +} |
0 commit comments