use serde_json::{Value, json};
use super::Chat;
use crate::completion::{CompletionRequest, Message, ToolDefinition};
use crate::message::{
AssistantContent, AssistantMessage, CallId, DocumentMediaType, DocumentSourceKind, Image,
ImageMediaType, ToolName, ToolResult, ToolResultContent, UserContent,
};
use crate::providers::openai::wire::{GROQ, OpenAIConfig};
use crate::test_utils::json_body;
use crate::wire::{Mode, Operation, Wire};
fn wire() -> Chat {
OpenAIConfig::new("key").chat("gpt-4o-mini")
}
fn name(name: &str) -> ToolName {
ToolName::new(name).expect("a tool name")
}
fn tool(tool: &str) -> ToolDefinition {
ToolDefinition::new(
name(tool),
"a tool",
json!({"type": "object", "properties": {}}),
)
}
fn body_on(wire: &Chat, request: CompletionRequest) -> Value {
let request = crate::operation::Completion::prepare(request, &wire.describe())
.expect("the request prepares");
json_body(&wire.encode(request, Mode::Unary).expect("encodes").request)
}
fn body(request: CompletionRequest) -> Value {
body_on(&wire(), request)
}
fn result(call: &str, content: Vec<ToolResultContent>) -> UserContent {
UserContent::ToolResult(ToolResult {
call: CallId::from_wire(call),
name: name("tool"),
content,
is_error: false,
})
}
fn calling(calls: &[&str]) -> Message {
Message::Assistant(AssistantMessage::new(
calls
.iter()
.map(|id| AssistantContent::tool_call(*id, name("tool"), json!({})))
.collect(),
))
}
fn with_tool(history: Vec<Message>) -> CompletionRequest {
let mut request = CompletionRequest::from(history);
request.tools = vec![tool("tool")];
request
}
#[test]
fn a_result_is_a_string_or_its_parts() {
let parts = vec![
ToolResultContent::text("first"),
ToolResultContent::json(json!({"status": "ok"})),
];
let history = vec![
Message::user("q"),
calling(&["call-id"]),
Message::User {
content: vec![result("call-id", parts)],
},
];
let string = body(with_tool(history.clone()));
assert_eq!(
string["messages"][2]["content"], "first\n{\"status\":\"ok\"}",
"{string}"
);
let array = body_on(&wire().with_tool_result_array_content(), with_tool(history));
assert_eq!(
array["messages"][2]["content"],
json!([
{"type": "text", "text": "first"},
{"type": "text", "text": "{\"status\":\"ok\"}"}
]),
"array mode keeps each part (#2201): {array}"
);
let image = ToolResultContent::image_base64("iVBORw0KGgo=", Some(ImageMediaType::PNG), None);
let history = vec![
Message::user("q"),
calling(&["call-id"]),
Message::User {
content: vec![result("call-id", vec![image])],
},
];
let llamacpp = OpenAIConfig::with_key(&crate::providers::openai::wire::LLAMACPP, "")
.chat("Qwen3-VL-2B-Instruct-Q8_0");
let sent = body_on(&llamacpp, with_tool(history));
assert_eq!(
sent["messages"][2]["content"][0]["image_url"]["url"], "data:image/png;base64,iVBORw0KGgo=",
"{sent}"
);
}
#[test]
fn user_parts_take_their_wire_shapes() {
let document = |data: DocumentSourceKind, media_type| {
UserContent::Document(crate::message::Document {
data,
media_type,
additional_params: None,
})
};
let content = vec![
UserContent::text("look"),
document(
DocumentSourceKind::Base64("JVBERi0xLjQK".into()),
Some(DocumentMediaType::PDF),
),
document(
DocumentSourceKind::Url("https://example.com/x.pdf".into()),
Some(DocumentMediaType::PDF),
),
document(DocumentSourceKind::FileId("file_abc".into()), None),
UserContent::Image(Image {
data: DocumentSourceKind::Base64("iVBORw0KGgo=".into()),
media_type: Some(ImageMediaType::PNG),
..Image::default()
}),
UserContent::video_url("https://example.com/v.mp4".to_owned(), None),
];
let openrouter = OpenAIConfig::with_key(&crate::providers::openai::wire::OPENROUTER, "key")
.chat("google/gemini-2.5-flash");
let sent = body_on(
&openrouter,
CompletionRequest::from(vec![Message::User { content }]),
);
assert_eq!(
sent["messages"][0]["content"],
json!([
{"type": "text", "text": "look"},
{"type": "file", "file": {"file_data": "data:application/pdf;base64,JVBERi0xLjQK",
"filename": "document.pdf"}},
{"type": "file", "file": {"file_data": "https://example.com/x.pdf",
"filename": "document.pdf"}},
{"type": "text", "text": "(document omitted: the provider cannot receive it in this form)"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo=",
"detail": "auto"}},
{"type": "video_url", "video_url": {"url": "https://example.com/v.mp4"}},
]),
"OpenRouter takes no file ids: {sent}"
);
let file_id = body(CompletionRequest::from(vec![Message::User {
content: vec![
UserContent::text("read"),
document(DocumentSourceKind::FileId("file_abc".into()), None),
],
}]));
assert_eq!(
file_id["messages"][0]["content"][1],
json!({"type": "file", "file": {"file_id": "file_abc"}})
);
}
#[test]
fn the_output_cap_is_spelled_for_the_model() {
let request = |model: &str, max_tokens: Option<u64>, params: Option<Value>| {
let mut request = CompletionRequest::new("Hello")
.model(model.to_owned())
.max_tokens(max_tokens);
request.additional_params = params;
body(request)
};
let legacy = request("gpt-4o-mini", Some(4096), None);
assert_eq!(legacy["max_tokens"], 4096);
assert!(legacy.get("max_completion_tokens").is_none());
let modern = request("gpt-5", Some(4096), Some(json!({"top_p": 0.5})));
assert_eq!(modern["max_completion_tokens"], 4096);
assert!(modern.get("max_tokens").is_none());
assert_eq!(modern["top_p"], 0.5);
let theirs = request(
"gpt-5",
Some(4096),
Some(json!({"max_completion_tokens": 48})),
);
assert_eq!(theirs["max_completion_tokens"], 48);
let upgraded = request("gpt-5", None, Some(json!({"max_tokens": 48})));
assert_eq!(upgraded["max_completion_tokens"], 48);
assert!(upgraded.get("max_tokens").is_none());
let none = request("gpt-5", None, None);
assert!(none.get("max_tokens").is_none() && none.get("max_completion_tokens").is_none());
}
#[test]
fn passthrough_tools_join_the_typed_ones() {
let request = || {
CompletionRequest::new("Hello")
.tools(vec![tool("builder_tool")])
.additional_params(json!({"tools": [
{"type": "function", "function": {"name": "params_tool",
"description": "from additional_params", "parameters": {"type": "object"}}},
{"type": "browser_search"},
]}))
};
let groq = OpenAIConfig::with_key(&GROQ, "key").chat("openai/gpt-oss-20b");
for sent in [body_on(&groq, request()), body(request())] {
let kinds: Vec<&str> = sent["tools"]
.as_array()
.into_iter()
.flatten()
.map(|tool| {
tool["function"]["name"]
.as_str()
.or_else(|| tool["type"].as_str())
.unwrap_or_default()
})
.collect();
assert_eq!(
kinds,
["builder_tool", "params_tool", "browser_search"],
"{sent}"
);
assert!(sent.get("compound_custom").is_none(), "{sent}");
}
}
#[test]
fn additional_params_override_typed_fields() {
let request = CompletionRequest::new("hi")
.temperature(0.1)
.additional_params(json!({"temperature": 0.9, "top_p": 0.5}));
let body = body(request);
assert_eq!(body["temperature"], 0.9, "{body}");
assert_eq!(body["top_p"], 0.5, "{body}");
}
#[test]
fn the_response_format_waits_for_a_tool_result() {
let schema: schemars::Schema = serde_json::from_value(json!({
"title": "WeatherResponse",
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}))
.expect("a schema");
let first = body(
CompletionRequest::new("Weather in London?")
.output_schema(schema.clone())
.tools(vec![tool("tool")]),
);
assert!(first.get("response_format").is_none(), "{first}");
let mut later = with_tool(vec![
Message::user("Weather in London?"),
calling(&["call_1"]),
Message::User {
content: vec![result("call_1", vec![ToolResultContent::text("fire")])],
},
]);
later.output_schema = Some(schema);
let later = body(later);
assert_eq!(
later["response_format"]["json_schema"]["name"],
"WeatherResponse"
);
}
#[test]
fn ollama_compatible_bodies_send_reasoning_and_refuse_native_options() {
let wire = crate::providers::ollama::OllamaConfig::new().completion("qwen3");
let sent = |request: CompletionRequest| {
let request = crate::operation::Completion::prepare(request, &wire.describe())
.expect("the request prepares");
wire.encode(request, Mode::Unary)
.map(|encoded| json_body(&encoded.request))
};
let raw = |params: Value| CompletionRequest::new("hi").additional_params(params);
let body = sent(raw(json!({"keep_alive": "10m"}))).expect("encodes");
assert_eq!(body["keep_alive"], json!("10m"));
assert!(body.get("reasoning_effort").is_none());
for (reasoning, effort) in [
(crate::completion::Reasoning::Off, json!("none")),
(crate::completion::Effort::High.into(), json!("high")),
(crate::completion::Effort::Low.into(), json!("low")),
] {
let body = sent(
CompletionRequest::new("hi")
.options(crate::completion::GenerationOptions::default().reasoning(reasoning)),
)
.expect("encodes");
assert_eq!(body["reasoning_effort"], effort, "{reasoning:?}");
assert!(body.get("think").is_none());
}
let body = sent(raw(json!({"think": true}))).expect("a raw `think` is sent");
assert_eq!(body["think"], json!(true));
assert!(body.get("reasoning_effort").is_none());
for (params, named) in [
(json!({"num_ctx": 8192}), "`num_ctx`"),
(json!({"options": {"num_ctx": 8192}}), "`options`"),
] {
let refused = sent(raw(params.clone())).expect_err("the request is refused");
assert!(refused.to_string().contains(named), "{params}: {refused}");
}
}