use super::*;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Response {
pub id: String,
pub object: String,
pub created_at: u64,
pub status: ResponseStatus,
#[serde(default)]
pub error: Option<ResponseError>,
#[serde(default)]
pub incomplete_details: Option<IncompleteDetails>,
pub model: String,
#[serde(default)]
pub output: Vec<OutputItem>,
#[serde(default)]
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
}
impl Response {
pub fn output_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
if let OutputItem::Message { content, .. } = item {
for part in content {
if let ContentPart::OutputText {
text: part_text, ..
} = part
{
text.push_str(part_text);
}
}
}
}
text
}
pub fn reasoning_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
if let OutputItem::Reasoning { content, .. } = item {
for part in content {
if let ContentPart::ReasoningText {
text: part_text, ..
} = part
{
text.push_str(part_text);
}
}
}
}
text
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
InProgress,
Completed,
Incomplete,
Failed,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ResponseError {
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct IncompleteDetails {
pub reason: IncompleteReason,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IncompleteReason {
MaxOutputTokens,
ContentFilter,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputItem {
Message {
id: String,
#[serde(default)]
status: Option<OutputItemStatus>,
#[serde(default)]
role: Option<MessageRole>,
#[serde(default)]
content: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
annotations: Vec<serde_json::Value>,
},
Reasoning {
id: String,
#[serde(default)]
status: Option<OutputItemStatus>,
#[serde(default)]
content: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
summary: Vec<serde_json::Value>,
},
FunctionCall {
id: String,
#[serde(default)]
call_id: Option<String>,
name: String,
arguments: String,
},
WebSearchCall {
id: String,
#[serde(default)]
action: Option<WebSearchAction>,
},
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
OutputText {
text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
annotations: Vec<serde_json::Value>,
},
ReasoningText {
text: String,
},
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputItemStatus {
InProgress,
Completed,
Incomplete,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageRole {
Assistant,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct WebSearchAction {
#[serde(rename = "type")]
pub typ: WebSearchActionType,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WebSearchActionType {
Search,
OpenPage,
FindInPage,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Usage {
pub input_tokens: u64,
#[serde(default)]
pub input_tokens_details: Option<InputTokensDetails>,
pub output_tokens: u64,
#[serde(default)]
pub output_tokens_details: Option<OutputTokensDetails>,
pub total_tokens: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct InputTokensDetails {
#[serde(default)]
pub cached_tokens: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct OutputTokensDetails {
#[serde(default)]
pub reasoning_tokens: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn deserialize_response_example() {
let value = json!({
"id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
"object": "response",
"created_at": 1753000000,
"status": "completed",
"model": "deepseek-v4-flash",
"output": [
{
"type": "reasoning",
"id": "rs_1",
"status": "completed",
"content": [
{"type": "reasoning_text", "text": "The user greets me. I should reply politely."}
],
"summary": []
},
{
"type": "message",
"id": "msg_1",
"status": "completed",
"role": "assistant",
"content": [
{"type": "output_text", "text": "Hello! How can I help you today?", "annotations": []}
]
}
],
"usage": {
"input_tokens": 22,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens": 29,
"output_tokens_details": {"reasoning_tokens": 27},
"total_tokens": 51
},
"store": false,
"parallel_tool_calls": true,
"previous_response_id": null,
"error": null,
"incomplete_details": null
});
let response: Response = serde_json::from_value(value).unwrap();
assert_eq!(response.id, "24778070-1c36-4ae0-a4bd-870afc7fc13e");
assert_eq!(response.status, ResponseStatus::Completed);
assert_eq!(response.output.len(), 2);
assert_eq!(response.output_text(), "Hello! How can I help you today?");
assert_eq!(
response.reasoning_text(),
"The user greets me. I should reply politely."
);
assert_eq!(response.usage.as_ref().unwrap().total_tokens, 51);
assert_eq!(
response
.usage
.as_ref()
.unwrap()
.output_tokens_details
.as_ref()
.unwrap()
.reasoning_tokens,
Some(27)
);
}
#[test]
fn deserialize_function_call_and_unknown_items() {
let value = json!({
"id": "r_1",
"object": "response",
"created_at": 1753000000,
"status": "completed",
"model": "deepseek-v4-flash",
"output": [
{
"type": "function_call",
"id": "fc_1",
"call_id": "call_1",
"name": "get_weather",
"arguments": "{\"location\":\"Hangzhou\"}"
},
{"type": "mystery_item", "id": "x_1"}
],
"usage": {
"input_tokens": 10,
"output_tokens": 20,
"total_tokens": 30
}
});
let response: Response = serde_json::from_value(value).unwrap();
assert_eq!(response.output.len(), 2);
assert!(
matches!(&response.output[0], OutputItem::FunctionCall { name, .. } if name == "get_weather")
);
assert!(matches!(&response.output[1], OutputItem::Unknown));
}
#[test]
fn serde_roundtrip() {
let response = Response {
id: "r_1".to_string(),
object: "response".to_string(),
created_at: 1753000000,
status: ResponseStatus::Completed,
error: None,
incomplete_details: None,
model: "deepseek-v4-flash".to_string(),
output: vec![OutputItem::Message {
id: "msg_1".to_string(),
status: Some(OutputItemStatus::Completed),
role: Some(MessageRole::Assistant),
content: vec![ContentPart::OutputText {
text: "Hi".to_string(),
annotations: vec![],
}],
annotations: vec![],
}],
usage: Some(Usage {
input_tokens: 1,
input_tokens_details: None,
output_tokens: 1,
output_tokens_details: None,
total_tokens: 2,
}),
store: Some(false),
parallel_tool_calls: Some(true),
previous_response_id: None,
};
let value = serde_json::to_value(&response).unwrap();
let back: Response = serde_json::from_value(value).unwrap();
assert_eq!(back, response);
}
}