use std::collections::BTreeMap;
#[cfg(feature = "structured-output")]
use std::marker::PhantomData;
use std::time::Duration;
use http::Method;
#[cfg(feature = "structured-output")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use crate::Client;
use crate::config::RequestOptions;
use crate::error::{Error, Result};
use crate::generated::endpoints;
#[cfg(feature = "structured-output")]
use crate::helpers::{ParsedResponse, parse_json_payload};
use crate::json_payload::JsonPayload;
use crate::response_meta::ApiResponse;
use crate::stream::{ResponseEventStream, ResponseStream};
use crate::transport::{RequestSpec, merge_json_body};
#[cfg(feature = "realtime")]
use crate::websocket::RealtimeSocket;
#[cfg(feature = "responses-ws")]
use crate::websocket::ResponsesSocket;
use super::{
ChatToolDefinition, ConversationItem, DeleteResponse, InputTokenCount, JsonRequestBuilder,
ListRequestBuilder, NoContentRequestBuilder, RealtimeCallsResource,
RealtimeClientSecretsResource, RealtimeResource, RealtimeSessionPayload, Response,
ResponseCreateParams, ResponseInputItemPayload, ResponseInputItemsResource,
ResponseInputPayload, ResponseInputTokensResource, ResponsesResource, encode_path_segment,
value_from,
};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RealtimeSessionClientSecret {
pub expires_at: u64,
#[serde(default)]
pub value: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RealtimeClientSecretCreateResponse {
pub value: Option<String>,
pub expires_at: Option<u64>,
pub client_secret: Option<RealtimeSessionClientSecret>,
pub secret: Option<String>,
#[serde(rename = "type")]
pub session_type: Option<String>,
pub session: Option<RealtimeSessionPayload>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl RealtimeClientSecretCreateResponse {
pub fn secret_value(&self) -> Option<&str> {
self.client_secret
.as_ref()
.map(|secret| secret.value.as_str())
.or(self.value.as_deref())
.or(self.secret.as_deref())
}
}
impl ResponsesResource {
pub fn create(&self) -> ResponseCreateRequestBuilder {
ResponseCreateRequestBuilder::new(self.client.clone())
}
#[cfg(feature = "structured-output")]
#[cfg_attr(docsrs, doc(cfg(feature = "structured-output")))]
pub fn parse<T>(&self) -> ResponseParseRequestBuilder<T> {
ResponseParseRequestBuilder::new(self.client.clone())
}
pub fn stream(&self) -> ResponseStreamRequestBuilder {
ResponseStreamRequestBuilder::new(self.client.clone())
}
pub fn stream_response(&self, response_id: impl Into<String>) -> ResponseStreamRequestBuilder {
ResponseStreamRequestBuilder::new(self.client.clone()).response_id(response_id)
}
#[cfg(feature = "responses-ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "responses-ws")))]
pub fn ws(&self) -> ResponsesSocketRequestBuilder {
ResponsesSocketRequestBuilder::new(self.client.clone())
}
pub fn retrieve(&self, response_id: impl Into<String>) -> JsonRequestBuilder<Response> {
JsonRequestBuilder::new(
self.client.clone(),
"responses.retrieve",
Method::GET,
format!("/responses/{}", encode_path_segment(response_id.into())),
)
}
pub fn delete(&self, response_id: impl Into<String>) -> JsonRequestBuilder<DeleteResponse> {
JsonRequestBuilder::new(
self.client.clone(),
"responses.delete",
Method::DELETE,
format!("/responses/{}", encode_path_segment(response_id.into())),
)
}
pub fn cancel(&self, response_id: impl Into<String>) -> JsonRequestBuilder<Response> {
JsonRequestBuilder::new(
self.client.clone(),
"responses.cancel",
Method::POST,
format!(
"/responses/{}/cancel",
encode_path_segment(response_id.into())
),
)
}
pub fn compact(&self) -> JsonRequestBuilder<Response> {
JsonRequestBuilder::new(
self.client.clone(),
"responses.compact",
Method::POST,
"/responses/compact",
)
}
pub fn input_items(&self) -> ResponseInputItemsResource {
ResponseInputItemsResource::new(self.client.clone())
}
pub fn input_tokens(&self) -> ResponseInputTokensResource {
ResponseInputTokensResource::new(self.client.clone())
}
}
impl ResponseInputItemsResource {
pub fn list(&self, response_id: impl Into<String>) -> ListRequestBuilder<ConversationItem> {
let endpoint = endpoints::responses::RESPONSES_INPUT_ITEMS_LIST;
ListRequestBuilder::new(
self.client.clone(),
endpoint.id,
endpoint.render(&[("response_id", &encode_path_segment(response_id.into()))]),
)
}
}
impl ResponseInputTokensResource {
pub fn count(&self) -> JsonRequestBuilder<InputTokenCount> {
let endpoint = endpoints::responses::RESPONSES_INPUT_TOKENS_COUNT;
JsonRequestBuilder::new(
self.client.clone(),
endpoint.id,
Method::POST,
endpoint.template,
)
}
}
impl RealtimeResource {
#[cfg(feature = "realtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "realtime")))]
pub fn ws(&self) -> RealtimeSocketRequestBuilder {
RealtimeSocketRequestBuilder::new(self.client.clone())
}
pub fn client_secrets(&self) -> RealtimeClientSecretsResource {
RealtimeClientSecretsResource::new(self.client.clone())
}
pub fn calls(&self) -> RealtimeCallsResource {
RealtimeCallsResource::new(self.client.clone())
}
}
impl RealtimeClientSecretsResource {
pub fn create(&self) -> JsonRequestBuilder<RealtimeClientSecretCreateResponse> {
let endpoint = endpoints::responses::REALTIME_CLIENT_SECRETS_CREATE;
JsonRequestBuilder::new(
self.client.clone(),
endpoint.id,
Method::POST,
endpoint.template,
)
}
}
impl RealtimeCallsResource {
pub fn accept(&self, call_id: impl Into<String>) -> NoContentRequestBuilder {
realtime_call_action(
self.client.clone(),
endpoints::responses::REALTIME_CALLS_ACCEPT,
call_id,
)
}
pub fn hangup(&self, call_id: impl Into<String>) -> NoContentRequestBuilder {
realtime_call_action(
self.client.clone(),
endpoints::responses::REALTIME_CALLS_HANGUP,
call_id,
)
}
pub fn refer(&self, call_id: impl Into<String>) -> NoContentRequestBuilder {
realtime_call_action(
self.client.clone(),
endpoints::responses::REALTIME_CALLS_REFER,
call_id,
)
}
pub fn reject(&self, call_id: impl Into<String>) -> NoContentRequestBuilder {
realtime_call_action(
self.client.clone(),
endpoints::responses::REALTIME_CALLS_REJECT,
call_id,
)
}
}
fn realtime_call_action(
client: Client,
endpoint: endpoints::PathTemplateEndpoint,
call_id: impl Into<String>,
) -> NoContentRequestBuilder {
NoContentRequestBuilder::new(
client,
endpoint.id,
Method::POST,
endpoint.render(&[("call_id", &encode_path_segment(call_id.into()))]),
)
.extra_header("accept", "*/*")
}
#[derive(Debug, Clone, Default)]
pub struct ResponseCreateRequestBuilder {
client: Option<Client>,
pub(crate) params: ResponseCreateParams,
options: RequestOptions,
extra_body: BTreeMap<String, Value>,
provider_options: BTreeMap<String, Value>,
}
#[derive(Debug, Clone)]
pub struct ResponseStreamRequestBuilder {
inner: ResponseCreateRequestBuilder,
response_id: Option<String>,
starting_after: Option<u64>,
}
#[cfg(feature = "realtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "realtime")))]
#[derive(Debug, Clone)]
pub struct RealtimeSocketRequestBuilder {
client: Client,
model: Option<String>,
options: RequestOptions,
}
#[cfg(feature = "responses-ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "responses-ws")))]
#[derive(Debug, Clone)]
pub struct ResponsesSocketRequestBuilder {
client: Client,
options: RequestOptions,
}
impl ResponseStreamRequestBuilder {
pub(crate) fn new(client: Client) -> Self {
Self {
inner: ResponseCreateRequestBuilder::new(client),
response_id: None,
starting_after: None,
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.inner = self.inner.model(model);
self
}
pub fn input_text(mut self, input: impl Into<String>) -> Self {
self.inner = self.inner.input_text(input);
self
}
pub fn input_items(mut self, items: Vec<ResponseInputItemPayload>) -> Self {
self.inner = self.inner.input_items(items);
self
}
pub fn input(mut self, input: impl Into<ResponseInputPayload>) -> Self {
self.inner = self.inner.input(input);
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.inner = self.inner.temperature(temperature);
self
}
pub fn tool(mut self, tool: ChatToolDefinition) -> Self {
self.inner = self.inner.tool(tool);
self
}
pub fn extra_body(mut self, key: impl Into<String>, value: impl Into<JsonPayload>) -> Self {
self.inner = self.inner.extra_body(key, value);
self
}
pub fn provider_option(
mut self,
key: impl Into<String>,
value: impl Into<JsonPayload>,
) -> Self {
self.inner = self.inner.provider_option(key, value);
self
}
pub fn extra_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.inner.options.insert_header(key, value);
self
}
pub fn extra_query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.inner.options.insert_query(key, value);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner.options.timeout = Some(timeout);
self
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.inner.options.max_retries = Some(max_retries);
self
}
pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
self.inner.options.cancellation_token = Some(token);
self
}
pub fn response_id(mut self, response_id: impl Into<String>) -> Self {
self.response_id = Some(response_id.into());
self
}
pub fn starting_after(mut self, sequence_number: u64) -> Self {
self.starting_after = Some(sequence_number);
self
}
pub async fn send(mut self) -> Result<ResponseStream> {
let (client, spec) = if let Some(response_id) = self.response_id.take() {
if self.inner.params.model.is_some()
|| self.inner.params.input.is_some()
|| self.inner.params.temperature.is_some()
|| !self.inner.params.tools.is_empty()
|| !self.inner.extra_body.is_empty()
|| !self.inner.provider_options.is_empty()
{
return Err(Error::InvalidConfig(
"按 response_id 继续流时,不应再设置创建期参数或请求体扩展字段".into(),
));
}
let client = self
.inner
.client
.take()
.ok_or_else(|| Error::InvalidConfig("Responses 构建器缺少客户端".into()))?;
let mut spec = RequestSpec::new(
"responses.stream.retrieve",
Method::GET,
format!("/responses/{}", encode_path_segment(response_id)),
);
spec.options = self.inner.options;
spec.options.insert_query("stream", "true");
if let Some(sequence_number) = self.starting_after {
spec.options
.insert_query("starting_after", sequence_number.to_string());
}
(client, spec)
} else {
if self.starting_after.is_some() {
return Err(Error::InvalidConfig(
"`starting_after` 只能与 `response_id` 一起使用".into(),
));
}
self.inner.build_spec(true)?
};
Ok(ResponseStream::new(client.execute_sse(spec).await?))
}
pub async fn send_events(self) -> Result<ResponseEventStream> {
Ok(self.send().await?.events())
}
}
#[cfg(feature = "realtime")]
impl RealtimeSocketRequestBuilder {
pub(crate) fn new(client: Client) -> Self {
Self {
client,
model: None,
options: RequestOptions::default(),
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn extra_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_header(key, value);
self
}
pub fn extra_query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_query(key, value);
self
}
pub async fn connect(self) -> Result<RealtimeSocket> {
RealtimeSocket::connect(&self.client, self.model, self.options).await
}
}
#[cfg(feature = "responses-ws")]
impl ResponsesSocketRequestBuilder {
pub(crate) fn new(client: Client) -> Self {
Self {
client,
options: RequestOptions::default(),
}
}
pub fn extra_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_header(key, value);
self
}
pub fn extra_query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_query(key, value);
self
}
pub async fn connect(self) -> Result<ResponsesSocket> {
ResponsesSocket::connect(&self.client, self.options).await
}
}
impl ResponseCreateRequestBuilder {
pub(crate) fn new(client: Client) -> Self {
Self {
client: Some(client),
..Self::default()
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.params.model = Some(model.into());
self
}
pub fn input_text(mut self, input: impl Into<String>) -> Self {
self.params.input = Some(ResponseInputPayload::from(input.into()));
self
}
pub fn input_items(mut self, items: Vec<ResponseInputItemPayload>) -> Self {
self.params.input = Some(ResponseInputPayload::from(items));
self
}
pub fn input(mut self, input: impl Into<ResponseInputPayload>) -> Self {
self.params.input = Some(input.into());
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.params.temperature = Some(temperature);
self
}
pub fn tool(mut self, tool: ChatToolDefinition) -> Self {
self.params.tools.push(tool);
self
}
pub fn extra_body(mut self, key: impl Into<String>, value: impl Into<JsonPayload>) -> Self {
self.extra_body.insert(key.into(), value.into().into_raw());
self
}
pub fn provider_option(
mut self,
key: impl Into<String>,
value: impl Into<JsonPayload>,
) -> Self {
self.provider_options
.insert(key.into(), value.into().into_raw());
self
}
pub fn extra_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_header(key, value);
self
}
pub fn extra_query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.insert_query(key, value);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.options.timeout = Some(timeout);
self
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.options.max_retries = Some(max_retries);
self
}
pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
self.options.cancellation_token = Some(token);
self
}
pub(crate) fn build_spec(mut self, stream: bool) -> Result<(Client, RequestSpec)> {
let client = self
.client
.take()
.ok_or_else(|| Error::InvalidConfig("Responses 构建器缺少客户端".into()))?;
if self.params.model.as_deref().unwrap_or_default().is_empty() {
return Err(Error::MissingRequiredField { field: "model" });
}
if self.params.input.is_none() {
return Err(Error::MissingRequiredField { field: "input" });
}
self.params.stream = Some(stream);
let provider_key = client.provider().kind().as_key();
let mut body = merge_json_body(
Some(value_from(&self.params)?),
&self.extra_body,
provider_key,
&self.provider_options,
);
if !self.params.tools.is_empty()
&& let Some(object) = body.as_object_mut()
{
object.insert(
"tools".into(),
Value::Array(
self.params
.tools
.iter()
.map(ChatToolDefinition::as_response_tool_value)
.collect(),
),
);
}
let mut spec = RequestSpec::new(
if stream {
"responses.stream"
} else {
"responses.create"
},
Method::POST,
"/responses",
);
spec.body = Some(body);
spec.options = self.options;
Ok((client, spec))
}
pub async fn send(self) -> Result<Response> {
Ok(self.send_with_meta().await?.data)
}
pub async fn send_with_meta(self) -> Result<ApiResponse<Response>> {
let (client, spec) = self.build_spec(false)?;
client.execute_json(spec).await
}
}
#[cfg(feature = "structured-output")]
#[derive(Debug, Clone)]
pub struct ResponseParseRequestBuilder<T> {
inner: ResponseCreateRequestBuilder,
_marker: PhantomData<T>,
}
#[cfg(feature = "structured-output")]
impl<T> ResponseParseRequestBuilder<T> {
pub(crate) fn new(client: Client) -> Self {
Self {
inner: ResponseCreateRequestBuilder::new(client),
_marker: PhantomData,
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.inner = self.inner.model(model);
self
}
pub fn input_text(mut self, input: impl Into<String>) -> Self {
self.inner = self.inner.input_text(input);
self
}
pub fn input_items(mut self, items: Vec<ResponseInputItemPayload>) -> Self {
self.inner = self.inner.input_items(items);
self
}
}
#[cfg(feature = "structured-output")]
impl<T> ResponseParseRequestBuilder<T>
where
T: JsonSchema + serde::de::DeserializeOwned,
{
pub async fn send(self) -> Result<ParsedResponse<T>> {
let response = self.inner.send().await?;
let output_text = response
.output_text()
.ok_or_else(|| Error::InvalidConfig("Responses 返回中缺少可解析文本".into()))?;
let parsed = parse_json_payload(&output_text)?;
Ok(ParsedResponse { response, parsed })
}
}