use super::*;
use crate::adapter::AdapterKind;
use crate::chat::{ChatMessage, ChatOptions, JsonSpec, Tool, ToolCall, ToolChoice, ToolResponse};
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn test_openai_resp_cache_control_without_eligible_content_does_not_fail_response_request() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
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 =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, chat_req, ChatOptionsSet::default())
.expect("unsupported breakpoint placement should be ignored");
assert_eq!(web_req.payload["prompt_cache_options"]["mode"], "explicit");
}
#[test]
fn custom_grammar_tool_and_roundtrip_use_responses_native_items() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let patch = "*** Begin Patch\n*** Update File: source.c\n@@\n-old\n+new\n*** End Patch\n";
let assistant = ChatMessage::assistant(vec![ToolCall {
call_id: "call_patch".to_string(),
fn_name: "apply_patch".to_string(),
fn_arguments: Value::String(patch.to_string()),
thought_signatures: None,
}]);
let response = ChatMessage::from(ToolResponse::new("call_patch", "Done!"));
let format = json!({
"type": "grammar",
"syntax": "lark",
"definition": "start: PATCH",
});
let request = ChatRequest::new(vec![ChatMessage::user("patch it"), assistant, response]).with_tools(vec![
Tool::new("apply_patch")
.with_description("Apply a patch")
.with_custom_format(format.clone()),
]);
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, request, ChatOptionsSet::default()).unwrap();
assert_eq!(
web_req.payload["tools"][0],
json!({
"type": "custom",
"name": "apply_patch",
"description": "Apply a patch",
"format": format,
})
);
let input = web_req.payload["input"].as_array().unwrap();
assert!(input.iter().any(|item| {
item["type"] == "custom_tool_call" && item["call_id"] == "call_patch" && item["input"] == patch
}));
assert!(input.iter().any(|item| {
item["type"] == "custom_tool_call_output" && item["call_id"] == "call_patch" && item["output"] == "Done!"
}));
}
#[test]
fn function_tool_strict_is_sent_only_when_set() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let schema = json!({
"type": "object",
"properties": {"city": {"type": "string"}}
});
let request = ChatRequest::from_user("weather").with_tools(vec![
Tool::new("unset").with_schema(schema.clone()),
Tool::new("not_strict").with_schema(schema.clone()).with_strict(false),
Tool::new("strict").with_schema(schema.clone()).with_strict(true),
]);
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, request, ChatOptionsSet::default())?;
let tools = &web_req.payload["tools"];
assert!(tools[0].get("strict").is_none());
assert_eq!(tools[0]["parameters"], schema);
assert_eq!(tools[1]["strict"], false);
assert_eq!(tools[1]["parameters"], schema);
assert_eq!(tools[2]["strict"], true);
assert_eq!(tools[2]["parameters"]["additionalProperties"], false);
Ok(())
}
#[test]
fn test_openai_resp_extra_body_merged_into_response_payload() {
let chat_options = ChatOptions::default()
.with_top_p(0.3)
.with_extra_body(json!({"top_p": 0.9, "metadata": {"source": "test"}}));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&chat_options));
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, ChatRequest::from_user("hello"), options_set)
.expect("to_web_request_data should succeed");
assert_eq!(web_req.payload["top_p"], 0.9);
assert_eq!(web_req.payload["metadata"]["source"], "test");
}
#[test]
fn pydantic_union_schema_is_sanitized_for_responses() {
let schema = json!({
"type": "object",
"properties": {
"animal": {
"discriminator": {"propertyName": "kind"},
"oneOf": [{"$ref": "#/$defs/Cat"}, {"$ref": "#/$defs/Dog"}]
}
},
"$defs": {
"Cat": {
"type": "object",
"properties": {"kind": {"const": "cat"}},
"required": ["kind"]
},
"Dog": {
"type": "object",
"properties": {"kind": {"const": "dog"}},
"required": ["kind"]
}
}
});
let options = ChatOptions::default().with_response_format(JsonSpec::new("union", schema));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let web_req = OpenAIRespAdapter::to_web_request_data(
target,
ServiceType::Chat,
ChatRequest::from_user("return an animal"),
options_set,
)
.unwrap();
let animal = &web_req.payload["text"]["format"]["schema"]["properties"]["animal"];
assert!(animal.get("oneOf").is_none());
assert_eq!(animal["discriminator"], json!({"propertyName": "kind"}));
assert_eq!(
animal["anyOf"],
json!([{"$ref": "#/$defs/Cat"}, {"$ref": "#/$defs/Dog"}])
);
}
#[test]
fn dynamic_map_schema_is_sent_to_backend_for_validation() {
let schema = json!({
"type": "object",
"properties": {
"lookup": {"type": "object", "additionalProperties": {"type": "integer"}}
}
});
let options = ChatOptions::default().with_response_format(JsonSpec::new("mapping", schema));
let options_set = ChatOptionsSet::default().with_chat_options(Some(&options));
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let web_req = OpenAIRespAdapter::to_web_request_data(
target,
ServiceType::Chat,
ChatRequest::from_user("return a mapping"),
options_set,
)
.unwrap();
assert_eq!(
web_req.payload["text"]["format"]["schema"]["properties"]["lookup"]["additionalProperties"],
json!({"type": "integer"})
);
}
#[test]
fn test_openai_resp_tool_choice_specific_tool_serialized_on_response_payload() {
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: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let chat_req = ChatRequest::from_user("weather").with_tools(vec![Tool::new("get_weather")]);
let web_req = OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, chat_req, options_set)
.expect("to_web_request_data should succeed");
assert_eq!(
web_req.payload["tool_choice"],
json!({
"type": "function",
"name": "get_weather"
})
);
}
#[test]
fn test_openai_resp_assistant_message_uses_output_text_content_type() {
let model_iden = ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-codex");
let chat_req = ChatRequest::default()
.with_system("You are a helpful assistant.")
.append_message(ChatMessage::user("What's the weather?"))
.append_message(ChatMessage::assistant("The weather is sunny."));
let parts = OpenAIRespAdapter::into_openai_request_parts(&model_iden, chat_req, None)
.expect("Should serialize successfully");
let assistant_msg = parts
.input_items
.iter()
.find(|item| {
item.get("type").and_then(|t| t.as_str()) == Some("message")
&& item.get("role").and_then(|r| r.as_str()) == Some("assistant")
})
.expect("Should have an assistant message");
let content = assistant_msg
.get("content")
.and_then(|c| c.as_array())
.expect("Assistant message should have content array");
assert!(!content.is_empty(), "Content should not be empty");
let first_content = &content[0];
let content_type = first_content
.get("type")
.and_then(|t| t.as_str())
.expect("Content should have a type");
assert_eq!(
content_type, "output_text",
"Assistant message content should use 'output_text' type, not 'input_text'"
);
}
#[test]
fn test_openai_resp_gpt_5_6_responses_defaults_to_explicit_cache_mode() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let web_req = OpenAIRespAdapter::to_web_request_data(
target,
ServiceType::Chat,
ChatRequest::from_user("hello"),
ChatOptionsSet::default(),
)
.expect("to_web_request_data should succeed");
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["input"][0]["content"][0]
.get("prompt_cache_breakpoint")
.is_none()
);
}
#[test]
fn test_openai_resp_gpt_6_responses_defaults_to_explicit_cache_mode() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let web_req = OpenAIRespAdapter::to_web_request_data(
target,
ServiceType::Chat,
ChatRequest::from_user("hello"),
ChatOptionsSet::default(),
)
.expect("to_web_request_data should succeed");
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["input"][0]["content"][0]
.get("prompt_cache_breakpoint")
.is_none()
);
}
#[test]
fn test_openai_resp_gpt_5_6_codex_responses_endpoint_omits_prompt_cache_options() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: Endpoint::from_static("https://chatgpt.com/backend-api/codex/"),
};
let web_req = OpenAIRespAdapter::to_web_request_data(
target,
ServiceType::Chat,
ChatRequest::from_user("hello"),
ChatOptionsSet::default(),
)?;
assert!(web_req.payload.get("prompt_cache_options").is_none());
Ok(())
}
#[test]
fn test_openai_resp_gpt_5_6_responses_cache_key_uses_api_default_cache_mode() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let chat_options = ChatOptions::default().with_prompt_cache_key("stable-key");
let options_set = ChatOptionsSet::default().with_chat_options(Some(&chat_options));
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, ChatRequest::from_user("hello"), options_set)
.expect("to_web_request_data should succeed");
assert!(web_req.payload.get("prompt_cache_options").is_none());
assert!(
web_req.payload["input"][0]["content"][0]
.get("prompt_cache_breakpoint")
.is_none()
);
}
#[test]
fn test_openai_resp_gpt_5_6_responses_places_breakpoint_on_last_eligible_content_block() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
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 =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, chat_req, ChatOptionsSet::default())
.expect("to_web_request_data should succeed");
let blocks = web_req.payload["input"][0]["content"]
.as_array()
.expect("message content should be an array");
assert!(blocks[0].get("prompt_cache_breakpoint").is_none());
assert!(blocks[1].get("prompt_cache_breakpoint").is_none());
assert_eq!(blocks[2]["prompt_cache_breakpoint"]["mode"], "explicit");
}
#[test]
fn test_openai_resp_gpt_5_6_responses_ignores_tool_cache_control() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.6"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let chat_req = ChatRequest::from_user("hello")
.append_tool(Tool::new("get_weather").with_cache_control(CacheControl::Ephemeral));
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, chat_req, ChatOptionsSet::default())
.expect("tool cache control should be ignored");
assert_eq!(web_req.payload["prompt_cache_options"]["mode"], "explicit");
assert!(web_req.payload["tools"][0].get("prompt_cache_breakpoint").is_none());
}
#[test]
fn test_openai_resp_gpt_5_5_responses_keeps_legacy_cache_retention() {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5.5"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let chat_options = ChatOptions::default().with_cache_control(CacheControl::Ephemeral24h);
let options_set = ChatOptionsSet::default().with_chat_options(Some(&chat_options));
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, ChatRequest::from_user("hello"), options_set)
.expect("to_web_request_data should succeed");
assert_eq!(web_req.payload["prompt_cache_retention"], "24h");
assert!(web_req.payload.get("prompt_cache_options").is_none());
}
#[test]
fn test_openai_resp_adapter_adapters_openai_resp_tool_to_openai_tool_optional_description() -> Result<()> {
let target = ServiceTarget {
model: ModelIden::new(AdapterKind::OpenAIResp, "gpt-5-mini"),
auth: AuthData::from_single("test-key"),
endpoint: OpenAIRespAdapter::default_endpoint(AdapterKind::OpenAIResp),
};
let schema = json!({
"type": "object",
"properties": {"city": {"type": "string"}}
});
let request = ChatRequest::from_user("weather").with_tools(vec![
Tool::new("without_description").with_schema(schema.clone()),
Tool::new("with_description")
.with_schema(schema.clone())
.with_description("Get the weather"),
]);
let web_req =
OpenAIRespAdapter::to_web_request_data(target, ServiceType::Chat, request, ChatOptionsSet::default())?;
let tools = web_req.payload["tools"].as_array().ok_or("Should have a tools array")?;
assert_eq!(tools.len(), 2);
assert_eq!(tools[0]["type"], "function");
assert_eq!(tools[0]["name"], "without_description");
assert_eq!(tools[0]["parameters"], schema);
assert!(tools[0].get("description").is_none());
assert_eq!(tools[1]["type"], "function");
assert_eq!(tools[1]["description"], "Get the weather");
assert_eq!(tools[1]["parameters"], schema);
Ok(())
}