use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::client::{DeletedObject, OpenAiClient};
use crate::error::OpenAiError;
use crate::sse::{sse_events, EventStream};
pub struct Responses<'a> {
pub(crate) client: &'a OpenAiClient,
}
impl Responses<'_> {
pub async fn create(&self, request: &ResponseRequest) -> Result<Response, OpenAiError> {
self.client.post_json("/responses", request).await
}
pub async fn stream(
&self,
request: &ResponseRequest,
) -> Result<EventStream<ResponseStreamEvent>, OpenAiError> {
let mut request = request.clone();
request.stream = Some(true);
let response = self.client.post_json_sse("/responses", &request).await?;
Ok(sse_events(response))
}
pub async fn retrieve(&self, response_id: &str) -> Result<Response, OpenAiError> {
self.client
.get_json(&format!("/responses/{response_id}"))
.await
}
pub async fn delete(&self, response_id: &str) -> Result<DeletedObject, OpenAiError> {
self.client
.delete_json(&format!("/responses/{response_id}"))
.await
}
pub async fn cancel(&self, response_id: &str) -> Result<Response, OpenAiError> {
self.client
.post_empty(&format!("/responses/{response_id}/cancel"))
.await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ResponseRequest {
model: String,
input: ResponseInput,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning: Option<ReasoningConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<TextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
truncation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
}
impl ResponseRequest {
pub fn new(model: impl Into<String>, input: impl Into<ResponseInput>) -> Self {
Self {
model: model.into(),
input: input.into(),
instructions: None,
max_output_tokens: None,
temperature: None,
top_p: None,
reasoning: None,
text: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
store: None,
previous_response_id: None,
metadata: None,
truncation: None,
service_tier: None,
background: None,
stream: None,
}
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn max_output_tokens(mut self, max_output_tokens: u32) -> Self {
self.max_output_tokens = Some(max_output_tokens);
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn top_p(mut self, top_p: f32) -> Self {
self.top_p = Some(top_p);
self
}
pub fn reasoning(mut self, reasoning: ReasoningConfig) -> Self {
self.reasoning = Some(reasoning);
self
}
pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
self.reasoning.get_or_insert_with(Default::default).effort = Some(effort.into());
self
}
pub fn text(mut self, text: TextConfig) -> Self {
self.text = Some(text);
self
}
pub fn json_schema(mut self, name: impl Into<String>, schema: Value) -> Self {
self.text.get_or_insert_with(Default::default).format = Some(TextFormat::JsonSchema {
name: name.into(),
schema,
description: None,
strict: Some(true),
});
self
}
pub fn verbosity(mut self, verbosity: impl Into<String>) -> Self {
self.text.get_or_insert_with(Default::default).verbosity = Some(verbosity.into());
self
}
pub fn tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn tool_choice(mut self, tool_choice: Value) -> Self {
self.tool_choice = Some(tool_choice);
self
}
pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
self.parallel_tool_calls = Some(parallel_tool_calls);
self
}
pub fn store(mut self, store: bool) -> Self {
self.store = Some(store);
self
}
pub fn previous_response_id(mut self, previous_response_id: impl Into<String>) -> Self {
self.previous_response_id = Some(previous_response_id.into());
self
}
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = Some(metadata);
self
}
pub fn truncation(mut self, truncation: impl Into<String>) -> Self {
self.truncation = Some(truncation.into());
self
}
pub fn service_tier(mut self, service_tier: impl Into<String>) -> Self {
self.service_tier = Some(service_tier.into());
self
}
pub fn background(mut self, background: bool) -> Self {
self.background = Some(background);
self
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ResponseInput {
Text(String),
Items(Vec<InputItem>),
}
impl From<&str> for ResponseInput {
fn from(text: &str) -> Self {
ResponseInput::Text(text.to_string())
}
}
impl From<String> for ResponseInput {
fn from(text: String) -> Self {
ResponseInput::Text(text)
}
}
impl From<Vec<InputItem>> for ResponseInput {
fn from(items: Vec<InputItem>) -> Self {
ResponseInput::Items(items)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum InputItem {
Message(InputMessage),
FunctionCallOutput(FunctionCallOutput),
Other(Value),
}
impl InputItem {
pub fn message(role: impl Into<String>, content: impl Into<InputContent>) -> Self {
InputItem::Message(InputMessage {
role: role.into(),
content: content.into(),
})
}
pub fn system(content: impl Into<InputContent>) -> Self {
Self::message("system", content)
}
pub fn developer(content: impl Into<InputContent>) -> Self {
Self::message("developer", content)
}
pub fn user(content: impl Into<InputContent>) -> Self {
Self::message("user", content)
}
pub fn assistant(content: impl Into<InputContent>) -> Self {
Self::message("assistant", content)
}
pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
InputItem::FunctionCallOutput(FunctionCallOutput {
item_type: "function_call_output",
call_id: call_id.into(),
output: output.into(),
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct InputMessage {
pub role: String,
pub content: InputContent,
}
#[derive(Debug, Clone, Serialize)]
pub struct FunctionCallOutput {
#[serde(rename = "type")]
item_type: &'static str,
pub call_id: String,
pub output: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum InputContent {
Text(String),
Parts(Vec<InputContentPart>),
}
impl From<&str> for InputContent {
fn from(text: &str) -> Self {
InputContent::Text(text.to_string())
}
}
impl From<String> for InputContent {
fn from(text: String) -> Self {
InputContent::Text(text)
}
}
impl From<Vec<InputContentPart>> for InputContent {
fn from(parts: Vec<InputContentPart>) -> Self {
InputContent::Parts(parts)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum InputContentPart {
#[serde(rename = "input_text")]
Text {
text: String,
},
#[serde(rename = "input_image")]
Image {
image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
#[serde(rename = "input_file")]
File {
#[serde(skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_data: Option<String>,
},
}
impl InputContentPart {
pub fn text(text: impl Into<String>) -> Self {
InputContentPart::Text { text: text.into() }
}
pub fn image_url(image_url: impl Into<String>) -> Self {
InputContentPart::Image {
image_url: image_url.into(),
detail: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReasoningConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TextConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<TextFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TextFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
name: String,
schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Tool {
#[serde(rename = "function")]
Function {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
parameters: Value,
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
#[serde(rename = "web_search")]
WebSearch,
#[serde(rename = "file_search")]
FileSearch {
vector_store_ids: Vec<String>,
},
#[serde(rename = "code_interpreter")]
CodeInterpreter {
container: Value,
},
#[serde(rename = "image_generation")]
ImageGeneration,
#[serde(untagged)]
Other(Value),
}
impl Tool {
pub fn function(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
) -> Self {
Tool::Function {
name: name.into(),
description: Some(description.into()),
parameters,
strict: Some(true),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Response {
pub id: String,
pub object: Option<String>,
pub created_at: Option<f64>,
pub status: Option<String>,
pub model: Option<String>,
#[serde(default)]
pub output: Vec<OutputItem>,
pub error: Option<ResponseErrorDetail>,
pub incomplete_details: Option<Value>,
pub previous_response_id: Option<String>,
pub usage: Option<ResponseUsage>,
pub reasoning: Option<ReasoningConfig>,
#[serde(default)]
pub metadata: Option<HashMap<String, String>>,
}
impl Response {
pub fn output_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
let OutputItem::Message(message) = item else {
continue;
};
for content in &message.content {
if let OutputContent::OutputText { text: part, .. } = content {
text.push_str(part);
}
}
}
text
}
pub fn function_calls(&self) -> Vec<&FunctionCall> {
self.output
.iter()
.filter_map(|item| match item {
OutputItem::FunctionCall(call) => Some(call),
_ => None,
})
.collect()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ResponseErrorDetail {
pub code: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum OutputItem {
#[serde(rename = "message")]
Message(OutputMessage),
#[serde(rename = "function_call")]
FunctionCall(FunctionCall),
#[serde(rename = "reasoning")]
Reasoning(ReasoningItem),
#[serde(untagged)]
Other(Value),
}
#[derive(Debug, Clone, Deserialize)]
pub struct OutputMessage {
pub id: Option<String>,
pub role: String,
pub status: Option<String>,
#[serde(default)]
pub content: Vec<OutputContent>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FunctionCall {
pub id: Option<String>,
pub call_id: String,
pub name: String,
pub arguments: String,
pub status: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ReasoningItem {
pub id: Option<String>,
#[serde(default)]
pub summary: Vec<Value>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum OutputContent {
#[serde(rename = "output_text")]
OutputText {
text: String,
#[serde(default)]
annotations: Vec<Value>,
},
#[serde(rename = "refusal")]
Refusal {
refusal: String,
},
#[serde(untagged)]
Other(Value),
}
#[derive(Debug, Clone, Deserialize)]
pub struct ResponseUsage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub total_tokens: u64,
pub input_tokens_details: Option<InputTokensDetails>,
pub output_tokens_details: Option<OutputTokensDetails>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InputTokensDetails {
#[serde(default)]
pub cached_tokens: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OutputTokensDetails {
#[serde(default)]
pub reasoning_tokens: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseStreamEvent {
#[serde(rename = "response.created")]
Created {
response: Response,
},
#[serde(rename = "response.in_progress")]
InProgress {
response: Response,
},
#[serde(rename = "response.output_item.added")]
OutputItemAdded {
item: OutputItem,
output_index: Option<u32>,
},
#[serde(rename = "response.output_item.done")]
OutputItemDone {
item: OutputItem,
output_index: Option<u32>,
},
#[serde(rename = "response.output_text.delta")]
OutputTextDelta {
delta: String,
item_id: Option<String>,
output_index: Option<u32>,
content_index: Option<u32>,
},
#[serde(rename = "response.output_text.done")]
OutputTextDone {
text: String,
},
#[serde(rename = "response.function_call_arguments.delta")]
FunctionCallArgumentsDelta {
delta: String,
item_id: Option<String>,
},
#[serde(rename = "response.completed")]
Completed {
response: Response,
},
#[serde(rename = "response.failed")]
Failed {
response: Response,
},
#[serde(rename = "response.incomplete")]
Incomplete {
response: Response,
},
#[serde(rename = "error")]
Error {
code: Option<String>,
message: String,
},
#[serde(untagged)]
Other(Value),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serializes_minimal_request() {
let request = ResponseRequest::new("gpt-5.6-terra", "Hello");
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value, json!({"model": "gpt-5.6-terra", "input": "Hello"}));
}
#[test]
fn serializes_full_request() {
let request = ResponseRequest::new(
"gpt-5.6-sol",
vec![
InputItem::user("What is in this image?"),
InputItem::user(vec![InputContentPart::image_url(
"https://example.com/a.png",
)]),
InputItem::function_call_output("call_1", "42"),
],
)
.instructions("Be brief.")
.max_output_tokens(500)
.reasoning_effort("high")
.verbosity("low")
.tools(vec![
Tool::function("get_weather", "Get weather", json!({"type": "object"})),
Tool::WebSearch,
])
.store(false)
.previous_response_id("resp_123");
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["instructions"], "Be brief.");
assert_eq!(value["max_output_tokens"], 500);
assert_eq!(value["reasoning"]["effort"], "high");
assert_eq!(value["text"]["verbosity"], "low");
assert_eq!(value["input"][0]["role"], "user");
assert_eq!(value["input"][0]["content"], "What is in this image?");
assert_eq!(value["input"][1]["content"][0]["type"], "input_image");
assert_eq!(value["input"][2]["type"], "function_call_output");
assert_eq!(value["tools"][0]["type"], "function");
assert_eq!(value["tools"][0]["name"], "get_weather");
assert_eq!(value["tools"][0]["strict"], true);
assert_eq!(value["tools"][1], json!({"type": "web_search"}));
assert_eq!(value["store"], false);
assert_eq!(value["previous_response_id"], "resp_123");
assert!(value.get("stream").is_none());
}
#[test]
fn serializes_json_schema_format() {
let request = ResponseRequest::new("gpt-5.6-luna", "hi")
.json_schema("answer", json!({"type": "object", "properties": {}}));
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["text"]["format"]["type"], "json_schema");
assert_eq!(value["text"]["format"]["name"], "answer");
assert_eq!(value["text"]["format"]["strict"], true);
}
#[test]
fn deserializes_response_and_extracts_text() {
let body = json!({
"id": "resp_1",
"object": "response",
"created_at": 1_750_000_000,
"status": "completed",
"model": "gpt-5.6-terra",
"output": [
{"type": "reasoning", "id": "rs_1", "summary": []},
{
"type": "message",
"id": "msg_1",
"role": "assistant",
"status": "completed",
"content": [
{"type": "output_text", "text": "Hello ", "annotations": []},
{"type": "output_text", "text": "world"}
]
},
{"type": "web_search_call", "id": "ws_1", "status": "completed"}
],
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"output_tokens_details": {"reasoning_tokens": 2}
}
});
let response: Response = serde_json::from_value(body).unwrap();
assert_eq!(response.output_text(), "Hello world");
assert_eq!(response.status.as_deref(), Some("completed"));
let usage = response.usage.unwrap();
assert_eq!(usage.total_tokens, 15);
assert_eq!(usage.output_tokens_details.unwrap().reasoning_tokens, 2);
assert!(matches!(response.output[2], OutputItem::Other(_)));
}
#[test]
fn deserializes_function_call_output_item() {
let body = json!({
"id": "resp_2",
"output": [{
"type": "function_call",
"id": "fc_1",
"call_id": "call_9",
"name": "get_weather",
"arguments": "{\"city\":\"Tokyo\"}",
"status": "completed"
}]
});
let response: Response = serde_json::from_value(body).unwrap();
let calls = response.function_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].call_id, "call_9");
}
#[test]
fn deserializes_stream_events() {
let delta: ResponseStreamEvent = serde_json::from_value(json!({
"type": "response.output_text.delta",
"item_id": "msg_1",
"output_index": 0,
"content_index": 0,
"delta": "Hel"
}))
.unwrap();
assert!(
matches!(delta, ResponseStreamEvent::OutputTextDelta { delta, .. } if delta == "Hel")
);
let unknown: ResponseStreamEvent = serde_json::from_value(json!({
"type": "response.audio.delta",
"delta": "..."
}))
.unwrap();
assert!(matches!(unknown, ResponseStreamEvent::Other(_)));
}
}