use super::{OpenAIAdapter, ToWebRequestDataOptions};
use crate::adapter::AdapterKind;
use crate::chat::{
CacheControl, ChatMessage, ChatOptions, ChatOptionsSet, ChatRequest, ContentPart, MessageContent, Tool, ToolCall,
ToolChoice,
};
use crate::resolver::{AuthData, Endpoint};
use crate::{ModelIden, ServiceTarget};
use serde_json::{Value, json};
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn test_cache_control_without_eligible_content_does_not_fail_chat_completion() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let assistant_msg = ChatMessage::assistant(MessageContent::from_parts(vec![ContentPart::ToolCall(ToolCall {
call_id: "call_1".to_string(),
fn_name: "get_weather".to_string(),
fn_arguments: json!({}),
thought_signatures: None,
})]))
.with_options(CacheControl::Ephemeral);
let chat_req = ChatRequest::new(vec![ChatMessage::user("hello"), assistant_msg]);
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
chat_req,
ChatOptionsSet::default(),
None,
)?;
assert_eq!(web_req.payload["prompt_cache_options"]["mode"], "explicit");
Ok(())
}
#[test]
fn test_extra_body_merged_into_chat_completion_payload() -> Result<()> {
let chat_options = ChatOptions::default()
.with_temperature(0.2)
.with_extra_body(json!({"temperature": 0.7, "enable_thinking": false}));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&chat_options));
let target = ServiceTarget {
model: test_model(),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
ChatRequest::from_user("hello"),
options_set,
None,
)?;
assert_eq!(web_req.payload["enable_thinking"], false);
assert_eq!(web_req.payload["temperature"], 0.7);
Ok(())
}
#[test]
fn test_tool_choice_specific_tool_serialized_on_chat_completion_payload() -> Result<()> {
let chat_options = ChatOptions::default().with_tool_choice(ToolChoice::tool("get_weather"));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&chat_options));
let target = ServiceTarget {
model: test_model(),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let chat_req = ChatRequest::from_user("weather").with_tools(vec![Tool::new("get_weather")]);
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
chat_req,
options_set,
None,
)?;
assert_eq!(
web_req.payload["tool_choice"],
json!({
"type": "function",
"function": { "name": "get_weather" }
})
);
Ok(())
}
#[test]
fn test_null_usage_is_treated_as_absent_usage() -> Result<()> {
let usage = OpenAIAdapter::into_usage(AdapterKind::OpenAI, Value::Null);
assert!(usage.prompt_tokens.is_none());
assert!(usage.completion_tokens.is_none());
assert!(usage.total_tokens.is_none());
Ok(())
}
#[test]
fn test_reasoning_content_serialized_on_assistant_message() -> Result<()> {
let tool_call = ToolCall {
call_id: "call_1".to_string(),
fn_name: "get_weather".to_string(),
fn_arguments: serde_json::json!({"city": "Paris"}),
thought_signatures: None,
};
let assistant_msg = ChatMessage::assistant(MessageContent::from_parts(vec![
ContentPart::Text("Let me check.".to_string()),
ContentPart::ToolCall(tool_call),
]))
.with_reasoning_content(Some("I should look up the weather.".to_string()));
let chat_req = ChatRequest::new(vec![ChatMessage::user("What's the weather in Paris?"), assistant_msg]);
let parts = OpenAIAdapter::into_openai_request_parts(&test_model(), chat_req, None)?;
let assistant_json = parts
.messages
.get(1)
.ok_or_else(|| std::io::Error::other("assistant message should be present"))?;
assert_eq!(assistant_json["role"], "assistant");
assert_eq!(
assistant_json["reasoning_content"], "I should look up the weather.",
"reasoning_content should be present in serialized assistant message"
);
Ok(())
}
#[test]
fn test_no_reasoning_content_when_absent() -> Result<()> {
let chat_req = ChatRequest::new(vec![ChatMessage::user("Hello"), ChatMessage::assistant("Hi there!")]);
let parts = OpenAIAdapter::into_openai_request_parts(&test_model(), chat_req, None)?;
let assistant_json = parts
.messages
.get(1)
.ok_or_else(|| std::io::Error::other("assistant message should be present"))?;
assert_eq!(assistant_json["role"], "assistant");
assert!(
assistant_json.get("reasoning_content").is_none(),
"reasoning_content should be absent when not set"
);
Ok(())
}
#[test]
fn test_gpt_5_6_chat_completion_defaults_to_explicit_cache_mode() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
ChatRequest::from_user("hello"),
ChatOptionsSet::default(),
None,
)?;
assert_eq!(web_req.payload["prompt_cache_options"]["mode"], "explicit");
assert!(web_req.payload["prompt_cache_options"].get("ttl").is_none());
assert!(web_req.payload["messages"][0]["content"]["prompt_cache_breakpoint"].is_null());
Ok(())
}
#[test]
fn test_gpt_5_6_chat_completion_cache_key_uses_api_default_mode() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.6-mini"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let options = ChatOptions::default().with_prompt_cache_key("stable-key");
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
ChatRequest::from_user("hello"),
options_set,
None,
)?;
assert!(web_req.payload.get("prompt_cache_options").is_none());
assert!(web_req.payload["messages"][0]["content"]["prompt_cache_breakpoint"].is_null());
Ok(())
}
#[test]
fn test_gpt_5_6_chat_completion_places_breakpoint_on_last_eligible_block() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let chat_req = ChatRequest::new(vec![
ChatMessage::user(vec![
ContentPart::from_text("stable text"),
ContentPart::from_binary_url("image/png", "https://example.com/image.png", None),
ContentPart::from_text("last text"),
])
.with_options(CacheControl::Ephemeral),
]);
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
chat_req,
ChatOptionsSet::default(),
None,
)?;
let blocks = web_req.payload["messages"][0]["content"]
.as_array()
.ok_or_else(|| std::io::Error::other("message content should be an array"))?;
let first = blocks.first().ok_or_else(|| std::io::Error::other("missing first block"))?;
assert!(first["prompt_cache_breakpoint"].is_null());
let image = blocks.get(1).ok_or_else(|| std::io::Error::other("missing image block"))?;
assert!(image["prompt_cache_breakpoint"].is_null());
let last = blocks.get(2).ok_or_else(|| std::io::Error::other("missing last block"))?;
assert_eq!(last["prompt_cache_breakpoint"]["mode"], "explicit");
Ok(())
}
#[test]
fn test_gpt_5_5_chat_completion_keeps_legacy_cache_retention() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.5"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let options = ChatOptions::default().with_cache_control(CacheControl::Ephemeral24h);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
ChatRequest::from_user("hello"),
options_set,
None,
)?;
assert_eq!(web_req.payload["prompt_cache_retention"], "24h");
assert!(web_req.payload.get("prompt_cache_options").is_none());
Ok(())
}
#[test]
fn test_gpt_5_6_chat_completion_ignores_tool_cache_control() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
};
let chat_req = ChatRequest::from_user("hello")
.append_tool(Tool::new("get_weather").with_cache_control(CacheControl::Ephemeral));
let web_req = OpenAIAdapter::util_to_web_request_data(
target,
crate::adapter::ServiceType::Chat,
chat_req,
ChatOptionsSet::default(),
None,
)?;
assert_eq!(web_req.payload["prompt_cache_options"]["mode"], "explicit");
assert!(web_req.payload["tools"][0].get("prompt_cache_breakpoint").is_none());
Ok(())
}
#[test]
fn test_managed_body_thinking_disables_zero_effort() -> Result<()> {
let options = ChatOptions::default().with_reasoning_effort(crate::chat::ReasoningEffort::Zero);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let payload = payload("test-model", options_set, Some(managed_options()))?;
assert_eq!(payload["thinking"]["type"], "disabled");
assert!(payload.get("reasoning_effort").is_none());
Ok(())
}
#[test]
fn test_managed_body_thinking_enables_max_effort() -> Result<()> {
let options = ChatOptions::default().with_reasoning_effort(crate::chat::ReasoningEffort::Max);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let payload = payload("test-model", options_set, Some(managed_options()))?;
assert_eq!(payload["thinking"]["type"], "enabled");
assert_eq!(payload["reasoning_effort"], "max");
Ok(())
}
#[test]
fn test_managed_body_thinking_enables_keyword_effort() -> Result<()> {
let options = ChatOptions::default().with_reasoning_effort(crate::chat::ReasoningEffort::Low);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let payload = payload("test-model", options_set, Some(managed_options()))?;
assert_eq!(payload["thinking"]["type"], "enabled");
assert_eq!(payload["reasoning_effort"], "low");
Ok(())
}
#[test]
fn test_managed_body_thinking_preserves_budget_behavior() -> Result<()> {
let options = ChatOptions::default().with_reasoning_effort(crate::chat::ReasoningEffort::Budget(1024));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let payload = payload("test-model", options_set, Some(managed_options()))?;
assert_eq!(payload["thinking"]["type"], "enabled");
assert!(payload.get("reasoning_effort").is_none());
Ok(())
}
#[test]
fn test_managed_body_thinking_omits_fields_without_effort() -> Result<()> {
let options_set = ChatOptionsSet::default();
let payload = payload("test-model", options_set, Some(managed_options()))?;
assert!(payload.get("thinking").is_none());
assert!(payload.get("reasoning_effort").is_none());
Ok(())
}
#[test]
fn test_managed_body_disabled_thinking_preserves_reasoning_effort_payload() -> Result<()> {
let options = ChatOptions::default().with_reasoning_effort(crate::chat::ReasoningEffort::Max);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let payload = payload("test-model", options_set, None)?;
assert!(payload.get("thinking").is_none());
assert_eq!(payload["reasoning_effort"], "max");
Ok(())
}
#[test]
fn test_managed_body_thinking_uses_model_name_derived_effort() -> Result<()> {
let candidates = ["test-model-high", "test-model:high", "test-model@high"];
let (model_name, derived_effort) = candidates
.into_iter()
.find_map(|model_name| {
let (effort, _) = crate::chat::ReasoningEffort::from_model_name(model_name);
effort.map(|effort| (model_name, effort))
})
.ok_or_else(|| std::io::Error::other("a supported model-name reasoning suffix should be available"))?;
let payload = payload(model_name, ChatOptionsSet::default(), Some(managed_options()))?;
assert!(matches!(derived_effort, crate::chat::ReasoningEffort::High));
assert_eq!(payload["thinking"]["type"], "enabled");
assert_eq!(payload["reasoning_effort"], "high");
Ok(())
}
fn test_model() -> ModelIden {
ModelIden::new(AdapterKind::OpenAI, "test-model")
}
fn target(model_name: &str) -> ServiceTarget {
ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAI, model_name),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://api.openai.com/v1/"),
}
}
fn managed_options() -> ToWebRequestDataOptions {
ToWebRequestDataOptions {
managed_body_thinking: true,
..Default::default()
}
}
fn payload(
model_name: &str,
options_set: ChatOptionsSet<'_, '_>,
custom: Option<ToWebRequestDataOptions>,
) -> Result<Value> {
Ok(OpenAIAdapter::util_to_web_request_data(
target(model_name),
crate::adapter::ServiceType::Chat,
ChatRequest::from_user("hello"),
options_set,
custom,
)?
.payload)
}