pub mod cancel;
pub mod create;
pub mod delete;
pub mod input_items;
pub mod retrieve;
use std::collections::HashMap;
use serde::Deserialize;
use crate::chat::ServiceTier;
use crate::responses::create::request::Truncation;
#[derive(Debug, Deserialize, Clone)]
pub struct Response {
pub id: String,
pub created_at: u64,
#[serde(default)]
pub error: Option<ResponseError>,
#[serde(default)]
pub incomplete_details: Option<IncompleteDetails>,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub metadata: Option<HashMap<String, String>>,
pub model: String,
pub object: ResponseObject,
#[serde(default)]
pub output: Vec<ResponseOutputItem>,
#[serde(default)]
pub parallel_tool_calls: Option<bool>,
#[serde(default)]
pub previous_response_id: Option<String>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub store: Option<bool>,
#[serde(default)]
pub status: Option<ResponseStatus>,
#[serde(default)]
pub usage: Option<ResponseUsage>,
#[serde(default)]
pub background: Option<bool>,
#[serde(default)]
pub completed_at: Option<u64>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub max_tool_calls: Option<u32>,
#[serde(default)]
pub top_logprobs: Option<u32>,
#[serde(default)]
pub truncation: Option<Truncation>,
#[serde(default)]
pub service_tier: Option<ServiceTier>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub safety_identifier: Option<String>,
#[serde(default)]
pub prompt_cache_key: Option<String>,
}
impl Response {
pub fn is_completed(&self) -> bool {
matches!(self.status, Some(ResponseStatus::Completed))
}
pub fn output_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
if let ResponseOutputItem::Message(message) = item {
for part in &message.content {
if let OutputContent::OutputText { text: t, .. } = part {
text.push_str(t);
}
}
}
}
text
}
pub fn reasoning_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
if let ResponseOutputItem::Reasoning(reasoning) = item {
for part in reasoning.content.as_deref().unwrap_or_default() {
let ReasoningContent::ReasoningText { text: t } = part;
text.push_str(t);
}
}
}
text
}
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
pub enum ResponseObject {
#[serde(rename = "response")]
Response,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
InProgress,
Completed,
Incomplete,
Failed,
Cancelled,
Queued,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseError {
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct IncompleteDetails {
#[serde(default)]
pub reason: Option<IncompleteReason>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum IncompleteReason {
MaxOutputTokens,
MaxMessages,
ContentFilter,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseOutputItem {
Message(ResponseOutputMessage),
Reasoning(ResponseReasoningItem),
FunctionCall(ResponseFunctionCall),
WebSearchCall(ResponseWebSearchCall),
#[cfg(feature = "qwen")]
McpCall(McpCall),
#[cfg(feature = "qwen")]
FileSearchCall(FileSearchCall),
#[cfg(feature = "qwen")]
CodeInterpreterCall(CodeInterpreterCall),
#[cfg(feature = "qwen")]
WebExtractorCall(WebExtractorCall),
#[cfg(feature = "qwen")]
WebSearchImageCall(WebSearchImageCall),
#[cfg(feature = "qwen")]
ImageSearchCall(ImageSearchCall),
#[serde(other)]
Other,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseOutputMessage {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub content: Vec<OutputContent>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ItemStatus {
InProgress,
Completed,
Incomplete,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputContent {
OutputText {
text: String,
#[serde(default)]
annotations: Vec<Annotation>,
},
Refusal {
refusal: String,
},
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Annotation {
UrlCitation {
url: String,
#[serde(default)]
title: Option<String>,
#[serde(default)]
url_citation_index: Option<u32>,
#[serde(default)]
start_index: Option<u32>,
#[serde(default)]
end_index: Option<u32>,
},
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseFunctionCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub call_id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseReasoningItem {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub content: Option<Vec<ReasoningContent>>,
#[serde(default)]
pub summary: Vec<ReasoningSummary>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReasoningContent {
ReasoningText {
text: String,
},
}
#[derive(Debug, Deserialize, Clone)]
pub struct ReasoningSummary {
#[serde(rename = "type")]
pub summary_type: Option<String>,
#[serde(default)]
pub text: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseWebSearchCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
#[serde(default)]
pub action: Option<WebSearchAction>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct WebSearchAction {
#[serde(rename = "type")]
pub action_type: Option<String>,
#[serde(default)]
pub query: Option<String>,
#[serde(default)]
pub sources: Vec<WebSearchSource>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct WebSearchSource {
#[serde(rename = "type")]
pub source_type: Option<String>,
#[serde(default)]
pub url: Option<String>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct McpCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub server_label: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct FileSearchCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub queries: Vec<String>,
#[serde(default)]
pub results: Vec<serde_json::Value>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct CodeInterpreterCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub outputs: Vec<serde_json::Value>,
#[serde(default)]
pub container_id: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct WebExtractorCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub goal: Option<String>,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub urls: Vec<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct WebSearchImageCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[cfg(feature = "qwen")]
#[derive(Debug, Deserialize, Clone)]
pub struct ImageSearchCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: Option<String>,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub status: Option<ItemStatus>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ResponseUsage {
pub input_tokens: usize,
#[serde(default)]
pub input_tokens_details: Option<InputTokensDetails>,
pub output_tokens: usize,
#[serde(default)]
pub output_tokens_details: Option<OutputTokensDetails>,
pub total_tokens: usize,
#[cfg(feature = "qwen")]
#[serde(default)]
pub x_details: Option<Vec<serde_json::Value>>,
#[cfg(feature = "qwen")]
#[serde(default)]
pub x_tools: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct InputTokensDetails {
#[serde(default)]
pub cached_tokens: Option<usize>,
#[serde(default)]
pub cache_write_tokens: Option<usize>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct OutputTokensDetails {
#[serde(default)]
pub reasoning_tokens: Option<usize>,
}
crate::impl_from_str!(Response);
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
const DEEPSEEK_RESPONSE_JSON: &str = r#"
{
"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
}"#;
#[test]
fn parses_documented_deepseek_response() {
let response = Response::from_str(DEEPSEEK_RESPONSE_JSON).unwrap();
assert_eq!(response.object, ResponseObject::Response);
assert_eq!(response.model, "deepseek-v4-flash");
assert!(response.is_completed());
assert_eq!(response.store, Some(false));
assert_eq!(response.parallel_tool_calls, Some(true));
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."
);
let usage = response.usage.unwrap();
assert_eq!(usage.total_tokens, 51);
assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(0));
assert_eq!(
usage.output_tokens_details.unwrap().reasoning_tokens,
Some(27)
);
}
#[test]
fn unknown_output_item_type_is_preserved() {
let json = r#"{
"id": "resp_1",
"object": "response",
"created_at": 0,
"status": "completed",
"model": "m",
"output": [{"type": "something_future"}]
}"#;
let response = Response::from_str(json).unwrap();
assert!(matches!(
response.output.first(),
Some(ResponseOutputItem::Other)
));
}
}
#[cfg(test)]
mod integration {
use futures_util::StreamExt;
use crate::responses::create::request::{Input, RequestBody};
use crate::responses::create::response::ResponseStreamEvent;
use crate::rest::{
default_client,
delete::DeleteNoStream,
get::GetNoStream,
post::{PostNoStream, PostStream},
};
use super::{Response, ResponseObject};
const DEEPSEEK_URL: &str = "https://api.deepseek.com";
const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
const QWEN_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
const QWEN_MODEL: &str = "qwen3-max";
fn deepseek_api_key() -> Option<String> {
std::env::var("DEEPSEEK_API_KEY")
.ok()
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty())
}
fn qwen_api_key() -> Option<String> {
std::env::var("QWEN_API_KEY")
.ok()
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty())
}
async fn run_stream(
request: &RequestBody,
client: &reqwest::Client,
base_url: &str,
api_key: &str,
) -> anyhow::Result<Response> {
let mut stream = request
.get_stream_response(client, base_url, api_key)
.await?;
let mut final_response: Option<Response> = None;
while let Some(event) = stream.next().await {
let event: ResponseStreamEvent = event?;
println!(
"responses stream event: {:?}",
std::mem::discriminant(&event)
);
if let Some(response) = event.final_response() {
final_response = Some(response.clone());
}
}
let response =
final_response.expect("the stream should end with a terminal response event");
Ok(response)
}
#[tokio::test]
async fn test_deepseek_response_no_stream() -> Result<(), anyhow::Error> {
let Some(api_key) = deepseek_api_key() else {
println!("Skipping: set DEEPSEEK_API_KEY to run this test");
return Ok(());
};
let request = RequestBody {
model: DEEPSEEK_MODEL.to_string(),
input: Input::Text("用一句话介绍你自己。".to_string()),
instructions: Some("You are a helpful assistant.".to_string()),
..Default::default()
};
let response = request
.get_response(&default_client(), DEEPSEEK_URL, &api_key)
.await?;
println!("deepseek responses no-stream: {response:#?}");
assert_eq!(response.object, ResponseObject::Response);
assert!(response.is_completed());
assert_eq!(response.store, Some(false));
assert!(!response.output_text().is_empty());
Ok(())
}
#[tokio::test]
async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
let Some(api_key) = deepseek_api_key() else {
println!("Skipping: set DEEPSEEK_API_KEY to run this test");
return Ok(());
};
let request = RequestBody {
model: DEEPSEEK_MODEL.to_string(),
input: Input::Text("用一句话介绍你自己。".to_string()),
stream: Some(true),
..Default::default()
};
let response = run_stream(&request, &default_client(), DEEPSEEK_URL, &api_key).await?;
println!("deepseek responses stream final: {response:#?}");
assert!(response.is_completed());
assert!(!response.output_text().is_empty());
Ok(())
}
#[tokio::test]
async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
let Some(api_key) = qwen_api_key() else {
println!("Skipping: set QWEN_API_KEY to run this test");
return Ok(());
};
let request = RequestBody {
model: QWEN_MODEL.to_string(),
input: Input::Text("用一句话介绍你自己。".to_string()),
instructions: Some("You are a helpful assistant.".to_string()),
store: Some(false),
..Default::default()
};
let response = request
.get_response(&default_client(), QWEN_URL, &api_key)
.await?;
println!("qwen responses no-stream: {response:#?}");
assert_eq!(response.object, ResponseObject::Response);
assert!(response.is_completed());
assert!(!response.output_text().is_empty());
Ok(())
}
#[tokio::test]
async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
let Some(api_key) = qwen_api_key() else {
println!("Skipping: set QWEN_API_KEY to run this test");
return Ok(());
};
let request = RequestBody {
model: QWEN_MODEL.to_string(),
input: Input::Text("用一句话介绍你自己。".to_string()),
stream: Some(true),
store: Some(false),
..Default::default()
};
let response = run_stream(&request, &default_client(), QWEN_URL, &api_key).await?;
println!("qwen responses stream final: {response:#?}");
assert!(response.is_completed());
assert!(!response.output_text().is_empty());
Ok(())
}
#[tokio::test]
async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
let Some(api_key) = qwen_api_key() else {
println!("Skipping: set QWEN_API_KEY to run this test");
return Ok(());
};
let client = default_client();
let first = RequestBody {
model: QWEN_MODEL.to_string(),
input: Input::Text("法国的首都是哪里?".to_string()),
..Default::default()
}
.get_response(&client, QWEN_URL, &api_key)
.await?;
assert!(first.is_completed());
let second = RequestBody {
model: QWEN_MODEL.to_string(),
input: Input::Text("它的人口大约是多少?".to_string()),
previous_response_id: Some(first.id.clone()),
store: Some(false),
..Default::default()
}
.get_response(&client, QWEN_URL, &api_key)
.await?;
println!("qwen responses second turn: {second:#?}");
assert!(second.is_completed());
assert_eq!(
second.previous_response_id.as_deref(),
Some(first.id.as_str())
);
assert!(!second.output_text().is_empty());
Ok(())
}
#[tokio::test]
async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
let Some(api_key) = qwen_api_key() else {
println!("Skipping: set QWEN_API_KEY to run this test");
return Ok(());
};
let client = default_client();
let created = RequestBody {
model: QWEN_MODEL.to_string(),
input: Input::Text("说一个法语单词。".to_string()),
store: Some(true),
..Default::default()
}
.get_response(&client, QWEN_URL, &api_key)
.await?;
assert!(created.is_completed());
let retrieved = crate::responses::retrieve::RetrieveRequest {
response_id: &created.id,
}
.get_response(&client, QWEN_URL, &api_key)
.await?;
assert_eq!(retrieved.id, created.id);
assert_eq!(retrieved.output_text(), created.output_text());
let deleted = crate::responses::delete::DeleteRequest {
response_id: &created.id,
}
.get_response(&client, QWEN_URL, &api_key)
.await?;
assert_eq!(deleted.id, created.id);
assert!(deleted.deleted);
Ok(())
}
}