Skip to content

Commit 79e7a34

Browse files
committed
test: add Responses API type conversion tests
Cover message→InputItem, tool_calls→FunctionCall, instructions extraction, tool definition/choice conversion, and output text/tool_call extraction.
1 parent 884f924 commit 79e7a34

File tree

1 file changed

+192
-0
lines changed
  • crates/cratos-llm/src/providers/openai

1 file changed

+192
-0
lines changed

crates/cratos-llm/src/providers/openai/tests.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use super::*;
2+
use crate::message::Message;
3+
use crate::tools::{ToolCall, ToolChoice, ToolDefinition};
24
use crate::util::mask_api_key;
35
use std::time::Duration;
46

@@ -126,3 +128,193 @@ fn test_provider_stores_auth_source() {
126128
let guard = provider.refreshed_api_key.lock().unwrap();
127129
assert!(guard.is_none());
128130
}
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

Comments
 (0)