use mockito::Server;
use openrouter_rust::{
OpenRouterClient,
responses::ResponsesRequestBuilder,
responses::{
InputItem, InputRole, InputContent, OutputItem, OutputContent,
ReasoningSummary
},
};
use serde_json::json;
#[tokio::test]
async fn test_responses_api_basic() {
let mut server = Server::new_async().await;
let mock_response = json!({
"id": "resp-test-123",
"object": "response",
"created_at": 1704067200.0,
"model": "anthropic/claude-3.5-sonnet",
"status": "completed",
"output": [
{
"type": "message",
"id": "msg-1",
"role": "assistant",
"content": [
{"type": "output_text", "text": "Rust is a systems programming language."}
],
"status": "completed"
}
],
"output_text": "Rust is a systems programming language.",
"usage": {
"input_tokens": 20,
"output_tokens": 10,
"total_tokens": 30,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens_details": {"reasoning_tokens": 0}
}
});
let _m = server.mock("POST", "/responses")
.match_header("authorization", "Bearer test-key")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response.to_string())
.create_async()
.await;
let client = OpenRouterClient::builder()
.api_key("test-key")
.base_url(&server.url())
.build()
.unwrap();
let request = ResponsesRequestBuilder::new("anthropic/claude-3.5-sonnet")
.user_message("What is Rust?")
.build();
let response = client.create_response(request).await.unwrap();
assert_eq!(response.id, "resp-test-123");
assert_eq!(response.model, "anthropic/claude-3.5-sonnet");
assert_eq!(response.status, "completed");
assert_eq!(response.output.len(), 1);
if let OutputItem::Message { content, .. } = &response.output[0] {
if let OutputContent::Text { text, .. } = &content[0] {
assert_eq!(text, "Rust is a systems programming language.");
} else {
panic!("Expected text content");
}
} else {
panic!("Expected message output");
}
let usage = response.usage.unwrap();
assert_eq!(usage.prompt_tokens, 20);
assert_eq!(usage.completion_tokens, 10);
}
#[tokio::test]
async fn test_responses_api_with_reasoning() {
let mut server = Server::new_async().await;
let mock_response = json!({
"id": "resp-reasoning-456",
"object": "response",
"created_at": 1704067200.0,
"model": "anthropic/claude-3.5-sonnet",
"status": "completed",
"output": [
{
"type": "reasoning",
"id": "reasoning-1",
"summary": [
{"type": "summary_text", "text": "Analyzing the problem..."}
],
"status": "completed"
},
{
"type": "message",
"id": "msg-1",
"role": "assistant",
"content": [
{"type": "output_text", "text": "The answer is 42."}
],
"status": "completed"
}
],
"usage": {
"input_tokens": 25,
"output_tokens": 50,
"total_tokens": 75,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens_details": {"reasoning_tokens": 30}
}
});
let _m = server.mock("POST", "/responses")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response.to_string())
.create_async()
.await;
let client = OpenRouterClient::builder()
.api_key("test-key")
.base_url(&server.url())
.build()
.unwrap();
let request = ResponsesRequestBuilder::new("anthropic/claude-3.5-sonnet")
.user_message("Solve this complex problem")
.reasoning("high")
.build();
let response = client.create_response(request).await.unwrap();
assert_eq!(response.output.len(), 2);
if let OutputItem::Reasoning { summary, .. } = &response.output[0] {
if let ReasoningSummary::Text { text } = &summary[0] {
assert_eq!(text, "Analyzing the problem...");
}
} else {
panic!("Expected reasoning output");
}
let usage = response.usage.unwrap();
assert_eq!(usage.completion_tokens_details.as_ref().unwrap().reasoning_tokens, Some(30));
}
#[tokio::test]
async fn test_responses_api_with_function_call() {
let mut server = Server::new_async().await;
let mock_response = json!({
"id": "resp-func-789",
"object": "response",
"created_at": 1704067200.0,
"model": "openai/gpt-4",
"status": "completed",
"output": [
{
"type": "function_call",
"id": "call-1",
"name": "get_weather",
"arguments": "{\"location\":\"San Francisco\"}",
"call_id": "call_abc123",
"status": "completed"
}
],
"usage": {
"input_tokens": 30,
"output_tokens": 20,
"total_tokens": 50
}
});
let _m = server.mock("POST", "/responses")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response.to_string())
.create_async()
.await;
let client = OpenRouterClient::builder()
.api_key("test-key")
.base_url(&server.url())
.build()
.unwrap();
let request = ResponsesRequestBuilder::new("openai/gpt-4")
.system_message("You have access to weather functions")
.user_message("What's the weather in San Francisco?")
.build();
let response = client.create_response(request).await.unwrap();
if let OutputItem::FunctionCall { name, arguments, call_id, .. } = &response.output[0] {
assert_eq!(name, "get_weather");
assert_eq!(arguments, "{\"location\":\"San Francisco\"}");
assert_eq!(call_id, "call_abc123");
} else {
panic!("Expected function call output");
}
}
#[tokio::test]
async fn test_responses_api_error() {
let mut server = Server::new_async().await;
let _m = server.mock("POST", "/responses")
.with_status(400)
.with_header("content-type", "application/json")
.with_body(json!({
"error": {
"code": "invalid_prompt",
"message": "Missing required parameter: 'model'"
}
}).to_string())
.create_async()
.await;
let client = OpenRouterClient::builder()
.api_key("test-key")
.base_url(&server.url())
.build()
.unwrap();
let request = ResponsesRequestBuilder::new("invalid-model")
.user_message("Test")
.build();
let result = client.create_response(request).await;
assert!(result.is_err());
match result.unwrap_err() {
openrouter_rust::OpenRouterError::ApiError { code, .. } => {
assert_eq!(code, 400);
}
_ => panic!("Expected ApiError with 400"),
}
}
#[tokio::test]
async fn test_responses_api_with_system_message() {
let mut server = Server::new_async().await;
let _m = server.mock("POST", "/responses")
.match_body(mockito::Matcher::Json(json!({
"model": "anthropic/claude-3.5-sonnet",
"input": [
{"type": "message", "role": "system", "content": "You are a coding expert"},
{"type": "message", "role": "user", "content": "How do I use closures in Rust?"}
],
"temperature": 0.7
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!({
"id": "resp-system",
"object": "response",
"created_at": 1704067200.0,
"model": "anthropic/claude-3.5-sonnet",
"status": "completed",
"output": [
{
"type": "message",
"id": "msg-1",
"role": "assistant",
"content": [{"type": "output_text", "text": "Closures in Rust are anonymous functions..."}],
"status": "completed"
}
],
"usage": {"input_tokens": 30, "output_tokens": 50, "total_tokens": 80}
}).to_string())
.create_async()
.await;
let client = OpenRouterClient::builder()
.api_key("test-key")
.base_url(&server.url())
.build()
.unwrap();
let request = ResponsesRequestBuilder::new("anthropic/claude-3.5-sonnet")
.system_message("You are a coding expert")
.user_message("How do I use closures in Rust?")
.temperature(0.7)
.build();
let response = client.create_response(request).await.unwrap();
assert_eq!(response.status, "completed");
}
#[test]
fn test_input_item_serialization() {
let item = InputItem::Message {
role: InputRole::User,
content: InputContent::String("Hello".to_string()),
};
let json = serde_json::to_string(&item).unwrap();
assert!(json.contains("\"type\":\"message\""));
assert!(json.contains("\"role\":\"user\""));
}
#[test]
fn test_output_item_deserialization() {
let json = r#"{"type":"message","id":"msg-1","role":"assistant","content":[{"type":"output_text","text":"Hello"}],"status":"completed"}"#;
let item: OutputItem = serde_json::from_str(json).unwrap();
match item {
OutputItem::Message { id, role, status, .. } => {
assert_eq!(id, "msg-1");
assert_eq!(role, "assistant");
assert_eq!(status, "completed");
}
_ => panic!("Expected Message variant"),
}
}