use std::collections::HashMap;
use serde::{Deserialize, Serialize};
pub use async_openai::types::responses::*;
pub use async_openai::types::responses::InputContent as UpstreamInputContent;
pub use crate::types::ImageDetail;
pub use crate::types::ReasoningEffort;
pub use crate::types::ResponseFormatJsonSchema;
pub type Input = InputParam;
pub type PromptConfig = Prompt;
pub type TextConfig = ResponseTextParam;
pub type TextResponseFormat = TextResponseFormatConfiguration;
pub type ResponseStream = std::pin::Pin<
Box<dyn futures::Stream<Item = Result<ResponseStreamEvent, crate::error::OpenAIError>> + Send>,
>;
pub const SPEC_NULLABLE_REQUIRED_RESPONSE_FIELDS: &[&str] = &[
"billing",
"completed_at",
"conversation",
"error",
"incomplete_details",
"instructions",
"max_output_tokens",
"max_tool_calls",
"previous_response_id",
"prompt",
"prompt_cache_key",
"prompt_cache_retention",
"reasoning",
"safety_identifier",
"usage",
];
fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where
T: Deserialize<'de>,
D: serde::Deserializer<'de>,
{
Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
}
fn deserialize_null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + Default,
D: serde::Deserializer<'de>,
{
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
}
fn deserialize_tool_choice<'de, D>(deserializer: D) -> Result<Option<ToolChoiceParam>, D::Error>
where
D: serde::Deserializer<'de>,
{
let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
return Ok(None);
};
if let Some(serde_json::Value::String(t)) = value.get("type") {
let mode = match t.as_str() {
"auto" => Some(ToolChoiceOptions::Auto),
"none" => Some(ToolChoiceOptions::None),
"required" => Some(ToolChoiceOptions::Required),
_ => None,
};
if let Some(mode) = mode {
return Ok(Some(ToolChoiceParam::Mode(mode)));
}
}
ToolChoiceParam::deserialize(value)
.map(Some)
.map_err(serde::de::Error::custom)
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct InputOutputTextContent {
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
pub annotations: Vec<Annotation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Vec<LogProb>>,
pub text: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputOutputMessageContent {
OutputText(InputOutputTextContent),
Refusal(RefusalContent),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct InputOutputMessage {
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
pub content: Vec<InputOutputMessageContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub role: AssistantRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<MessagePhase>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<OutputStatus>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct InputImageContent {
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub detail: ImageDetail,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContent {
InputText(InputTextContent),
InputImage(InputImageContent),
InputFile(InputFileContent),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct InputMessage {
pub content: Vec<InputContent>,
pub role: InputRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<OutputStatus>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum EasyInputContent {
Text(String),
ContentList(Vec<InputContent>),
}
impl Default for EasyInputContent {
fn default() -> Self {
Self::Text(String::new())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct EasyInputMessage {
#[serde(default)]
pub r#type: MessageType,
pub role: Role,
pub content: EasyInputContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<MessagePhase>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum MessageItem {
Output(InputOutputMessage),
Input(InputMessage),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct InputReasoningItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
pub summary: Vec<SummaryPart>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<ReasoningTextContent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<OutputStatus>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Item {
Message(MessageItem),
FileSearchCall(FileSearchToolCall),
ComputerCall(ComputerToolCall),
ComputerCallOutput(ComputerCallOutputItemParam),
WebSearchCall(WebSearchToolCall),
FunctionCall(FunctionToolCall),
FunctionCallOutput(FunctionCallOutputItemParam),
ToolSearchCall(ToolSearchCallItemParam),
ToolSearchOutput(ToolSearchOutputItemParam),
Reasoning(InputReasoningItem),
Compaction(CompactionSummaryItemParam),
ImageGenerationCall(ImageGenToolCall),
CodeInterpreterCall(CodeInterpreterToolCall),
LocalShellCall(LocalShellToolCall),
LocalShellCallOutput(LocalShellToolCallOutput),
ShellCall(FunctionShellCallItemParam),
ShellCallOutput(FunctionShellCallOutputItemParam),
ApplyPatchCall(ApplyPatchToolCallItemParam),
ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
McpListTools(MCPListTools),
McpApprovalRequest(MCPApprovalRequest),
McpApprovalResponse(MCPApprovalResponse),
McpCall(MCPToolCall),
CustomToolCallOutput(CustomToolCallOutput),
CustomToolCall(CustomToolCall),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum InputItem {
ItemReference(ItemReference),
Item(Item),
EasyMessage(EasyInputMessage),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum InputParam {
Text(String),
Items(Vec<InputItem>),
}
impl Default for InputParam {
fn default() -> Self {
Self::Text(String::new())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct CreateResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<ConversationParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<IncludeEnum>>,
pub input: InputParam,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<Prompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_retention: Option<PromptCacheRetention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub safety_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<ResponseStreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextParam>,
#[serde(
default,
deserialize_with = "deserialize_tool_choice",
skip_serializing_if = "Option::is_none"
)]
pub tool_choice: Option<ToolChoiceParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
}
#[cfg(test)]
mod tests {
use super::*;
fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
let req: CreateResponse = serde_json::from_value(serde_json::json!({
"input": "hi",
"tool_choice": json,
}))
.expect("CreateResponse should deserialize");
req.tool_choice
}
#[test]
fn tool_choice_mode_object_coerces_to_mode() {
assert_eq!(
tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
);
assert_eq!(
tool_choice_of(serde_json::json!({"type": "none"})),
Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
);
assert_eq!(
tool_choice_of(serde_json::json!({"type": "required"})),
Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
);
}
#[test]
fn tool_choice_bare_string_still_works() {
assert_eq!(
tool_choice_of(serde_json::json!("auto")),
Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
);
}
#[test]
fn tool_choice_specific_function_object_still_works() {
match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
other => panic!("expected Function tool choice, got {other:?}"),
}
}
#[test]
fn tool_choice_absent_is_none() {
let req: CreateResponse =
serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
assert!(req.tool_choice.is_none());
}
#[test]
fn reasoning_input_without_id_deserializes() {
let json = serde_json::json!({
"type": "reasoning",
"summary": [{"type": "summary_text", "text": "thinking"}],
});
match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
InputItem::Item(Item::Reasoning(r)) => {
assert!(r.id.is_none());
assert_eq!(r.summary.len(), 1);
}
other => panic!("expected Item::Reasoning, got {other:?}"),
}
}
#[test]
fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
let json = serde_json::json!({
"type": "reasoning",
"encrypted_content": "AB==",
});
match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
InputItem::Item(Item::Reasoning(r)) => {
assert!(r.id.is_none());
assert!(r.summary.is_empty());
assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
}
other => panic!("expected Item::Reasoning, got {other:?}"),
}
}
#[test]
fn reasoning_input_with_id_still_works() {
let json = serde_json::json!({
"type": "reasoning",
"id": "rs_1",
"summary": [{"type": "summary_text", "text": "x"}],
"status": "completed",
});
match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
other => panic!("expected Item::Reasoning, got {other:?}"),
}
}
#[test]
fn full_request_with_idless_reasoning_item_deserializes() {
let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
"model": "m",
"input": [
{"role": "user", "content": "hi"},
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
],
}));
assert!(
req.is_ok(),
"idless reasoning input should deserialize: {req:?}"
);
}
#[test]
fn relaxed_assistant_message_without_id_or_status() {
let json = serde_json::json!({
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "hi"}]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Output(out))) => {
assert_eq!(out.role, AssistantRole::Assistant);
assert!(out.id.is_none());
assert!(out.status.is_none());
}
other => panic!("expected Item::Message(Output), got {other:?}"),
}
}
#[test]
fn input_image_without_detail_defaults_to_auto() {
let json = serde_json::json!({
"type": "input_image",
"image_url": "https://example.com/cat.jpg"
});
let content: InputContent = serde_json::from_value(json).unwrap();
match content {
InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
other => panic!("expected InputImage, got {other:?}"),
}
}
#[test]
fn input_image_with_explicit_null_detail_defaults_to_auto() {
let json = serde_json::json!({
"type": "input_image",
"image_url": "https://example.com/cat.jpg",
"detail": null
});
let content: InputContent = serde_json::from_value(json).unwrap();
match content {
InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
other => panic!("expected InputImage, got {other:?}"),
}
}
#[test]
fn assistant_message_without_content_field_deserializes() {
let json = serde_json::json!({
"type": "message",
"role": "assistant"
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Output(out))) => {
assert_eq!(out.role, AssistantRole::Assistant);
assert!(out.content.is_empty());
assert!(out.id.is_none());
assert!(out.status.is_none());
}
other => panic!("expected Item::Message(Output), got {other:?}"),
}
}
#[test]
fn assistant_message_with_explicit_null_content_deserializes() {
let json = serde_json::json!({
"type": "message",
"role": "assistant",
"content": null
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Output(out))) => {
assert!(out.content.is_empty());
}
other => panic!("expected Item::Message(Output), got {other:?}"),
}
}
#[test]
fn mcp_call_item_deserializes() {
let json = serde_json::json!({
"type": "mcp_call",
"id": "mcp_1",
"server_label": "srv",
"name": "t",
"arguments": "{}"
});
let item: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
}
#[test]
fn strict_assistant_message_still_deserializes() {
let json = serde_json::json!({
"type": "message",
"role": "assistant",
"id": "msg_1",
"status": "completed",
"content": [{"type": "output_text", "text": "hi", "annotations": []}]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Output(out))) => {
assert_eq!(out.id.as_deref(), Some("msg_1"));
assert_eq!(out.status, Some(OutputStatus::Completed));
}
other => panic!("expected Item::Message(Output), got {other:?}"),
}
}
#[test]
fn user_message_routes_to_input_variant() {
let json = serde_json::json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hi"}]
});
let item: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(
item,
InputItem::Item(Item::Message(MessageItem::Input(_)))
));
}
#[test]
fn function_call_item_still_deserializes() {
let json = serde_json::json!({
"type": "function_call",
"call_id": "c",
"name": "f",
"arguments": "{}"
});
let item: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
}
#[test]
fn easy_message_string_content_routes_to_easymessage() {
let json = serde_json::json!({"role": "assistant", "content": "x"});
let item: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(item, InputItem::EasyMessage(_)));
}
#[test]
fn output_text_without_annotations_defaults_empty() {
let json = serde_json::json!({"type": "output_text", "text": "hi"});
let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
match part {
InputOutputMessageContent::OutputText(t) => {
assert!(t.annotations.is_empty());
}
_ => panic!("expected OutputText"),
}
}
#[test]
fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
match part {
InputOutputMessageContent::OutputText(t) => {
assert!(t.annotations.is_empty());
}
_ => panic!("expected OutputText"),
}
}
#[test]
fn assistant_message_with_explicit_null_id_and_status_deserializes() {
let json = serde_json::json!({
"type": "message",
"role": "assistant",
"id": null,
"status": null,
"content": [{"type": "output_text", "text": "hi", "annotations": null}]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Output(out))) => {
assert!(out.id.is_none());
assert!(out.status.is_none());
assert_eq!(out.content.len(), 1);
}
other => panic!("expected Item::Message(Output), got {other:?}"),
}
}
#[test]
fn create_response_roundtrip_with_relaxed_input() {
let body = serde_json::json!({
"model": "m",
"input": [
{"type": "message", "role": "user", "content": [
{"type": "input_text", "text": "hi"}
]},
{"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
{"type": "message", "role": "assistant", "content": [
{"type": "output_text", "text": "\n\n"}
]},
{"type": "function_call_output", "call_id": "c", "output": "x"}
]
});
let req: CreateResponse = serde_json::from_value(body).unwrap();
let items = match &req.input {
InputParam::Items(items) => items,
_ => panic!("expected Items"),
};
assert_eq!(items.len(), 4);
assert!(matches!(
items[2],
InputItem::Item(Item::Message(MessageItem::Output(_)))
));
}
#[test]
fn easy_message_multimodal_without_type_routes_to_easymessage() {
let json = serde_json::json!({
"role": "user",
"content": [
{"type": "input_image", "image_url": "data:image/png;base64,abc"}
]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::EasyMessage(easy) => {
assert_eq!(easy.role, Role::User);
assert_eq!(easy.r#type, MessageType::Message);
match easy.content {
EasyInputContent::ContentList(parts) => {
assert_eq!(parts.len(), 1);
match &parts[0] {
InputContent::InputImage(img) => {
assert_eq!(img.detail, ImageDetail::Auto);
assert_eq!(
img.image_url.as_deref(),
Some("data:image/png;base64,abc")
);
}
other => panic!("expected InputImage, got {other:?}"),
}
}
other => panic!("expected ContentList, got {other:?}"),
}
}
other => panic!("expected EasyMessage, got {other:?}"),
}
}
#[test]
fn easy_message_multimodal_with_explicit_null_detail() {
let json = serde_json::json!({
"role": "user",
"content": [
{"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
]
});
let item: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(item, InputItem::EasyMessage(_)));
}
#[test]
fn easy_message_assistant_multimodal_without_type() {
let json = serde_json::json!({
"role": "assistant",
"content": [
{"type": "input_text", "text": "ok"}
]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::EasyMessage(easy) => {
assert_eq!(easy.role, Role::Assistant);
}
other => panic!("expected EasyMessage(assistant), got {other:?}"),
}
}
#[test]
fn easy_message_text_only_without_type_unchanged() {
let json = serde_json::json!({"role": "user", "content": "Hello"});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::EasyMessage(easy) => {
assert_eq!(easy.role, Role::User);
assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
}
other => panic!("expected EasyMessage(Text), got {other:?}"),
}
}
#[test]
fn easy_message_with_explicit_type_still_routes_to_item_message() {
let json = serde_json::json!({
"type": "message",
"role": "user",
"content": [
{"type": "input_image", "image_url": "data:image/png;base64,abc"}
]
});
let item: InputItem = serde_json::from_value(json).unwrap();
match item {
InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
assert_eq!(msg.role, InputRole::User);
assert_eq!(msg.content.len(), 1);
}
other => panic!("expected Item::Message(Input), got {other:?}"),
}
}
#[test]
fn create_response_roundtrip_aiperf_pre_pr931_payload() {
let body = serde_json::json!({
"model": "Qwen/Qwen2-VL-2B-Instruct",
"input": [
{
"role": "user",
"content": [
{"type": "input_text", "text": "Describe"},
{"type": "input_image", "image_url": "data:image/png;base64,abc"}
]
},
{
"role": "assistant",
"content": [{"type": "input_text", "text": "ok"}]
},
{
"role": "user",
"content": [{"type": "input_text", "text": "Now describe a different one."}]
}
]
});
let req: CreateResponse = serde_json::from_value(body).unwrap();
let items = match &req.input {
InputParam::Items(items) => items,
_ => panic!("expected Items"),
};
assert_eq!(items.len(), 3);
for (idx, item) in items.iter().enumerate() {
assert!(
matches!(item, InputItem::EasyMessage(_)),
"turn {idx} did not route to EasyMessage: {item:?}",
);
}
}
}