use serde_json::json;
use super::*;
use crate::harness::message::Message;
use crate::harness::model::{ModelRequest, ResponseFormat, ToolChoice};
use crate::harness::tool::ToolSchema;
fn model() -> OpenAiModel {
OpenAiModel::new("test-key").with_model("gpt-4.1-mini")
}
#[test]
fn translates_request_to_openai_json_shape() {
let request = ModelRequest::new(vec![
Message::system("You are a sentiment classifier."),
Message::user("I love this product!"),
])
.with_tools(vec![ToolSchema::new(
"get_weather",
"Look up the weather for a city.",
json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}),
)])
.with_tool_choice(ToolChoice::Required)
.with_response_format(ResponseFormat::json_schema(
"sentiment",
json!({
"type": "object",
"properties": {
"sentiment": { "type": "string" },
"score": { "type": "number" }
},
"required": ["sentiment", "score"]
}),
))
.with_temperature(0.2)
.with_max_tokens(256);
let body = model().translate_request(&request).unwrap();
let value = serde_json::to_value(&body).unwrap();
assert_eq!(value["model"], json!("gpt-4.1-mini"));
assert_eq!(value["messages"][0]["role"], json!("system"));
assert_eq!(
value["messages"][0]["content"],
json!("You are a sentiment classifier.")
);
assert_eq!(value["messages"][1]["role"], json!("user"));
assert_eq!(
value["messages"][1]["content"],
json!("I love this product!")
);
assert_eq!(value["tools"][0]["type"], json!("function"));
assert_eq!(value["tools"][0]["function"]["name"], json!("get_weather"));
assert_eq!(
value["tools"][0]["function"]["description"],
json!("Look up the weather for a city.")
);
assert_eq!(
value["tools"][0]["function"]["parameters"]["properties"]["city"]["type"],
json!("string")
);
assert_eq!(value["tool_choice"], json!("required"));
assert_eq!(value["response_format"]["type"], json!("json_schema"));
assert_eq!(
value["response_format"]["json_schema"]["name"],
json!("sentiment")
);
assert_eq!(
value["response_format"]["json_schema"]["strict"],
json!(true)
);
assert_eq!(
value["response_format"]["json_schema"]["schema"]["properties"]["score"]["type"],
json!("number")
);
assert_eq!(value["temperature"], json!(0.2));
assert_eq!(value["max_tokens"], json!(256));
}
#[test]
fn translates_named_tool_choice_and_omits_when_no_tools() {
let with_tool = ModelRequest::new(vec![Message::user("hi")])
.with_tools(vec![ToolSchema::new("t", "d", json!({}))])
.with_tool_choice(ToolChoice::Tool("t".to_string()));
let value = serde_json::to_value(model().translate_request(&with_tool).unwrap()).unwrap();
assert_eq!(
value["tool_choice"],
json!({ "type": "function", "function": { "name": "t" } })
);
let no_tools = ModelRequest::new(vec![Message::user("hi")]);
let value = serde_json::to_value(model().translate_request(&no_tools).unwrap()).unwrap();
assert!(value.get("tool_choice").is_none());
assert!(value.get("tools").is_none());
assert!(value.get("response_format").is_none());
}
#[test]
fn translates_assistant_tool_calls_to_stringified_arguments() {
let request = ModelRequest::new(vec![
Message::user("What is the weather in Paris?"),
Message::Assistant(AssistantMessage {
id: Some("msg-1".to_string()),
content: Vec::new(),
tool_calls: vec![ToolCall {
id: "call-1".to_string(),
name: "get_weather".to_string(),
arguments: json!({ "city": "Paris" }),
}],
usage: None,
}),
Message::tool("call-1", "sunny, 21C"),
]);
let value = serde_json::to_value(model().translate_request(&request).unwrap()).unwrap();
let assistant = &value["messages"][1];
assert_eq!(assistant["role"], json!("assistant"));
assert!(assistant.get("content").is_none());
assert_eq!(assistant["tool_calls"][0]["id"], json!("call-1"));
assert_eq!(assistant["tool_calls"][0]["type"], json!("function"));
assert_eq!(
assistant["tool_calls"][0]["function"]["name"],
json!("get_weather")
);
assert_eq!(
assistant["tool_calls"][0]["function"]["arguments"],
json!("{\"city\":\"Paris\"}")
);
let tool = &value["messages"][2];
assert_eq!(tool["role"], json!("tool"));
assert_eq!(tool["tool_call_id"], json!("call-1"));
assert_eq!(tool["content"], json!("sunny, 21C"));
}
#[test]
fn parses_openai_response_with_content_tool_call_and_usage() {
let body = json!({
"id": "chatcmpl-abc123",
"object": "chat.completion",
"model": "gpt-4.1-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Let me check the weather for you.",
"tool_calls": [
{
"id": "call-99",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 42,
"completion_tokens": 8,
"total_tokens": 50,
"prompt_tokens_details": { "cached_tokens": 30 }
}
});
let response = parse_response(body.clone()).unwrap();
assert_eq!(response.message.id.as_deref(), Some("chatcmpl-abc123"));
assert_eq!(response.text(), "Let me check the weather for you.");
let calls = response.tool_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].id, "call-99");
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].arguments, json!({ "city": "Paris" }));
assert_eq!(response.finish_reason.as_deref(), Some("tool_calls"));
let usage = response.usage.expect("usage present");
assert_eq!(usage.input_tokens, 42);
assert_eq!(usage.output_tokens, 8);
assert_eq!(usage.total_tokens, 50);
assert_eq!(usage.cache_read_tokens, 30);
assert_eq!(response.raw, Some(body));
}
#[test]
fn parses_text_only_response_without_usage_details() {
let body = json!({
"id": "chatcmpl-xyz",
"choices": [
{
"message": { "role": "assistant", "content": "Hello!" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 2,
"total_tokens": 7
}
});
let response = parse_response(body).unwrap();
assert_eq!(response.text(), "Hello!");
assert!(response.tool_calls().is_empty());
assert_eq!(response.finish_reason.as_deref(), Some("stop"));
let usage = response.usage.unwrap();
assert_eq!(usage.input_tokens, 5);
assert_eq!(usage.cache_read_tokens, 0);
}
#[test]
fn parse_response_errors_on_empty_choices() {
let body = json!({ "id": "x", "choices": [] });
let err = parse_response(body).unwrap_err();
assert!(matches!(err, TinyAgentsError::Model(_)));
}
#[test]
fn compatible_presets_set_base_url_and_default_model() {
let deepseek = OpenAiModel::deepseek("k");
assert_eq!(deepseek.base_url(), "https://api.deepseek.com/v1");
assert_eq!(deepseek.model(), "deepseek-chat");
let anthropic = OpenAiModel::anthropic("k");
assert_eq!(anthropic.base_url(), "https://api.anthropic.com/v1");
assert_eq!(anthropic.model(), "claude-3-5-sonnet-latest");
let ollama = OpenAiModel::ollama();
assert_eq!(ollama.base_url(), "http://localhost:11434/v1");
assert_eq!(ollama.model(), "llama3.2");
let custom = OpenAiModel::groq("k").with_model("mixtral-8x7b");
assert_eq!(custom.base_url(), "https://api.groq.com/openai/v1");
assert_eq!(custom.model(), "mixtral-8x7b");
let generic = OpenAiModel::compatible("k", "https://example.test/v1/", "my-model");
assert_eq!(generic.base_url(), "https://example.test/v1");
assert_eq!(generic.model(), "my-model");
}
#[test]
fn from_env_errors_when_api_key_missing() {
let previous = std::env::var("OPENAI_API_KEY").ok();
unsafe {
std::env::remove_var("OPENAI_API_KEY");
}
let result = OpenAiModel::from_env();
if let Some(value) = previous {
unsafe {
std::env::set_var("OPENAI_API_KEY", value);
}
}
assert!(matches!(result, Err(TinyAgentsError::Validation(_))));
}