use crate::error::LingerError;
use crate::RequestId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateThreadRequest {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub messages: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_resources: Option<Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl CreateThreadRequest {
pub fn builder() -> CreateThreadRequestBuilder {
CreateThreadRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateThreadRequestBuilder {
messages: Vec<Value>,
tool_resources: Option<Value>,
metadata: BTreeMap<String, String>,
}
impl CreateThreadRequestBuilder {
pub fn message(mut self, message: Value) -> Self {
self.messages.push(message);
self
}
pub fn messages(mut self, messages: impl IntoIterator<Item = Value>) -> Self {
self.messages = messages.into_iter().collect();
self
}
pub fn tool_resources(mut self, tool_resources: Value) -> Self {
self.tool_resources = Some(tool_resources);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<CreateThreadRequest, LingerError> {
validate_messages(&self.messages)?;
validate_metadata(&self.metadata)?;
if self.tool_resources.as_ref().is_some_and(Value::is_null) {
return Err(LingerError::invalid_config(
"tool_resources must not be null",
));
}
Ok(CreateThreadRequest {
messages: self.messages,
tool_resources: self.tool_resources,
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ModifyThreadRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_resources: Option<Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl ModifyThreadRequest {
pub fn builder() -> ModifyThreadRequestBuilder {
ModifyThreadRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ModifyThreadRequestBuilder {
tool_resources: Option<Value>,
metadata: BTreeMap<String, String>,
}
impl ModifyThreadRequestBuilder {
pub fn tool_resources(mut self, tool_resources: Value) -> Self {
self.tool_resources = Some(tool_resources);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<ModifyThreadRequest, LingerError> {
validate_metadata(&self.metadata)?;
if self.tool_resources.as_ref().is_some_and(Value::is_null) {
return Err(LingerError::invalid_config(
"tool_resources must not be null",
));
}
Ok(ModifyThreadRequest {
tool_resources: self.tool_resources,
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateThreadMessageRequest {
pub role: String,
pub content: Value,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl CreateThreadMessageRequest {
pub fn builder() -> CreateThreadMessageRequestBuilder {
CreateThreadMessageRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateThreadMessageRequestBuilder {
role: Option<String>,
content: Option<Value>,
attachments: Vec<Value>,
metadata: BTreeMap<String, String>,
}
impl CreateThreadMessageRequestBuilder {
pub fn role(mut self, role: impl Into<String>) -> Self {
self.role = Some(role.into());
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(Value::String(content.into()));
self
}
pub fn content_json(mut self, content: Value) -> Self {
self.content = Some(content);
self
}
pub fn attachment(mut self, attachment: Value) -> Self {
self.attachments.push(attachment);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<CreateThreadMessageRequest, LingerError> {
let role = required_string("role", self.role)?;
let content = self
.content
.filter(|value| !value.is_null())
.ok_or_else(|| LingerError::invalid_config("content is required"))?;
if self.attachments.iter().any(Value::is_null) {
return Err(LingerError::invalid_config(
"attachments must not contain null",
));
}
validate_metadata(&self.metadata)?;
Ok(CreateThreadMessageRequest {
role,
content,
attachments: self.attachments,
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ModifyThreadMessageRequest {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl ModifyThreadMessageRequest {
pub fn builder() -> ModifyThreadMessageRequestBuilder {
ModifyThreadMessageRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ModifyThreadMessageRequestBuilder {
metadata: BTreeMap<String, String>,
}
impl ModifyThreadMessageRequestBuilder {
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<ModifyThreadMessageRequest, LingerError> {
validate_metadata(&self.metadata)?;
Ok(ModifyThreadMessageRequest {
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateThreadAndRunRequest {
pub assistant_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateThreadAndRunRequest {
pub fn builder() -> CreateThreadAndRunRequestBuilder {
CreateThreadAndRunRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateThreadAndRunRequestBuilder {
assistant_id: Option<String>,
thread: Option<Value>,
model: Option<String>,
instructions: Option<String>,
tools: Vec<Value>,
metadata: BTreeMap<String, String>,
extra: BTreeMap<String, Value>,
}
impl CreateThreadAndRunRequestBuilder {
pub fn assistant_id(mut self, assistant_id: impl Into<String>) -> Self {
self.assistant_id = Some(assistant_id.into());
self
}
pub fn thread(mut self, thread: Value) -> Self {
self.thread = Some(thread);
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn tool(mut self, tool: Value) -> Self {
self.tools.push(tool);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateThreadAndRunRequest, LingerError> {
let assistant_id = required_string("assistant_id", self.assistant_id)?;
if self.thread.as_ref().is_some_and(Value::is_null) {
return Err(LingerError::invalid_config("thread must not be null"));
}
validate_optional_string("model", &self.model)?;
validate_optional_string("instructions", &self.instructions)?;
validate_json_items("tools", &self.tools)?;
validate_metadata(&self.metadata)?;
validate_extra_fields(&self.extra)?;
Ok(CreateThreadAndRunRequest {
assistant_id,
thread: self.thread,
model: self.model,
instructions: self.instructions,
tools: self.tools,
metadata: self.metadata,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateThreadRunRequest {
pub assistant_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_instructions: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub additional_messages: Vec<Value>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
include_file_search_result_content: bool,
}
impl CreateThreadRunRequest {
pub fn builder() -> CreateThreadRunRequestBuilder {
CreateThreadRunRequestBuilder::default()
}
pub(crate) fn path(&self, thread_id: &str) -> String {
path_with_query(
&format!("/v1/threads/{thread_id}/runs"),
ThreadListQuery {
limit: None,
order: None,
after: None,
before: None,
run_id: None,
include_file_search_result_content: self.include_file_search_result_content,
},
)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateThreadRunRequestBuilder {
assistant_id: Option<String>,
model: Option<String>,
instructions: Option<String>,
additional_instructions: Option<String>,
additional_messages: Vec<Value>,
tools: Vec<Value>,
metadata: BTreeMap<String, String>,
extra: BTreeMap<String, Value>,
include_file_search_result_content: bool,
}
impl CreateThreadRunRequestBuilder {
pub fn assistant_id(mut self, assistant_id: impl Into<String>) -> Self {
self.assistant_id = Some(assistant_id.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn additional_instructions(mut self, additional_instructions: impl Into<String>) -> Self {
self.additional_instructions = Some(additional_instructions.into());
self
}
pub fn additional_message(mut self, message: Value) -> Self {
self.additional_messages.push(message);
self
}
pub fn additional_messages(mut self, messages: impl IntoIterator<Item = Value>) -> Self {
self.additional_messages = messages.into_iter().collect();
self
}
pub fn tool(mut self, tool: Value) -> Self {
self.tools.push(tool);
self
}
pub fn tools(mut self, tools: impl IntoIterator<Item = Value>) -> Self {
self.tools = tools.into_iter().collect();
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn include_file_search_result_content(mut self) -> Self {
self.include_file_search_result_content = true;
self
}
pub fn build(self) -> Result<CreateThreadRunRequest, LingerError> {
let assistant_id = required_string("assistant_id", self.assistant_id)?;
validate_optional_string("model", &self.model)?;
validate_optional_string("instructions", &self.instructions)?;
validate_optional_string("additional_instructions", &self.additional_instructions)?;
validate_json_items("additional_messages", &self.additional_messages)?;
validate_json_items("tools", &self.tools)?;
validate_metadata(&self.metadata)?;
validate_extra_fields(&self.extra)?;
Ok(CreateThreadRunRequest {
assistant_id,
model: self.model,
instructions: self.instructions,
additional_instructions: self.additional_instructions,
additional_messages: self.additional_messages,
tools: self.tools,
metadata: self.metadata,
extra: self.extra,
include_file_search_result_content: self.include_file_search_result_content,
})
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ModifyThreadRunRequest {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
}
impl ModifyThreadRunRequest {
pub fn builder() -> ModifyThreadRunRequestBuilder {
ModifyThreadRunRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ModifyThreadRunRequestBuilder {
metadata: BTreeMap<String, String>,
}
impl ModifyThreadRunRequestBuilder {
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<ModifyThreadRunRequest, LingerError> {
validate_metadata(&self.metadata)?;
Ok(ModifyThreadRunRequest {
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubmitToolOutput {
pub tool_call_id: String,
pub output: String,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubmitToolOutputsRequest {
pub tool_outputs: Vec<SubmitToolOutput>,
}
impl SubmitToolOutputsRequest {
pub fn builder() -> SubmitToolOutputsRequestBuilder {
SubmitToolOutputsRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct SubmitToolOutputsRequestBuilder {
tool_outputs: Vec<SubmitToolOutput>,
}
impl SubmitToolOutputsRequestBuilder {
pub fn tool_output(
mut self,
tool_call_id: impl Into<String>,
output: impl Into<String>,
) -> Self {
self.tool_outputs.push(SubmitToolOutput {
tool_call_id: tool_call_id.into(),
output: output.into(),
});
self
}
pub fn build(self) -> Result<SubmitToolOutputsRequest, LingerError> {
if self.tool_outputs.is_empty() {
return Err(LingerError::invalid_config(
"tool_outputs must not be empty",
));
}
for output in &self.tool_outputs {
if output.tool_call_id.trim().is_empty() {
return Err(LingerError::invalid_config("tool_call_id is required"));
}
}
Ok(SubmitToolOutputsRequest {
tool_outputs: self.tool_outputs,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct Thread {
pub id: String,
pub object: String,
pub created_at: u64,
#[serde(default)]
pub tool_resources: Option<Value>,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ThreadMessage {
pub id: String,
pub object: String,
pub created_at: u64,
pub thread_id: String,
pub role: String,
#[serde(default)]
pub content: Vec<Value>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub incomplete_details: Option<Value>,
#[serde(default)]
pub completed_at: Option<u64>,
#[serde(default)]
pub incomplete_at: Option<u64>,
#[serde(default)]
pub assistant_id: Option<String>,
#[serde(default)]
pub run_id: Option<String>,
#[serde(default)]
pub attachments: Vec<Value>,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ThreadRun {
pub id: String,
pub object: String,
pub created_at: u64,
pub thread_id: String,
pub assistant_id: String,
pub status: String,
#[serde(default)]
pub required_action: Option<Value>,
#[serde(default)]
pub last_error: Option<Value>,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub started_at: Option<u64>,
#[serde(default)]
pub cancelled_at: Option<u64>,
#[serde(default)]
pub failed_at: Option<u64>,
#[serde(default)]
pub completed_at: Option<u64>,
#[serde(default)]
pub incomplete_details: Option<Value>,
pub model: String,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub tools: Vec<Value>,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub usage: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ThreadRunPage {
pub object: String,
#[serde(default)]
pub data: Vec<ThreadRun>,
#[serde(default)]
pub first_id: Option<String>,
#[serde(default)]
pub last_id: Option<String>,
pub has_more: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RunStep {
pub id: String,
pub object: String,
pub created_at: u64,
pub assistant_id: String,
pub thread_id: String,
pub run_id: String,
#[serde(rename = "type")]
pub kind: String,
pub status: String,
pub step_details: Value,
#[serde(default)]
pub last_error: Option<Value>,
#[serde(default)]
pub expired_at: Option<u64>,
#[serde(default)]
pub cancelled_at: Option<u64>,
#[serde(default)]
pub failed_at: Option<u64>,
#[serde(default)]
pub completed_at: Option<u64>,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub usage: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RunStepPage {
pub object: String,
#[serde(default)]
pub data: Vec<RunStep>,
#[serde(default)]
pub first_id: Option<String>,
#[serde(default)]
pub last_id: Option<String>,
pub has_more: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ThreadMessage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
impl ThreadRun {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
impl ThreadRunPage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ThreadRunListOrder {
Asc,
Desc,
}
impl ThreadRunListOrder {
pub(crate) fn as_query_value(self) -> &'static str {
match self {
Self::Asc => "asc",
Self::Desc => "desc",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ThreadRunListRequest {
pub limit: Option<u8>,
pub order: Option<ThreadRunListOrder>,
pub after: Option<String>,
pub before: Option<String>,
}
impl ThreadRunListRequest {
pub fn builder() -> ThreadRunListRequestBuilder {
ThreadRunListRequestBuilder::default()
}
pub(crate) fn path(&self, thread_id: &str) -> String {
path_with_query(
&format!("/v1/threads/{thread_id}/runs"),
ThreadListQuery {
limit: self.limit,
order: self.order.map(ThreadRunListOrder::as_query_value),
after: self.after.as_deref(),
before: self.before.as_deref(),
run_id: None,
include_file_search_result_content: false,
},
)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ThreadRunListRequestBuilder {
limit: Option<u8>,
order: Option<ThreadRunListOrder>,
after: Option<String>,
before: Option<String>,
}
impl ThreadRunListRequestBuilder {
pub fn limit(mut self, limit: u8) -> Self {
self.limit = Some(limit);
self
}
pub fn order(mut self, order: ThreadRunListOrder) -> Self {
self.order = Some(order);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn build(self) -> Result<ThreadRunListRequest, LingerError> {
if let Some(limit) = self.limit {
if limit == 0 || limit > 100 {
return Err(LingerError::invalid_config(
"limit must be between 1 and 100",
));
}
}
validate_optional_cursor("after", self.after.as_deref())?;
validate_optional_cursor("before", self.before.as_deref())?;
Ok(ThreadRunListRequest {
limit: self.limit,
order: self.order,
after: self.after,
before: self.before,
})
}
}
impl RunStep {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
impl RunStepPage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RunStepListOrder {
Asc,
Desc,
}
impl RunStepListOrder {
pub(crate) fn as_query_value(self) -> &'static str {
match self {
Self::Asc => "asc",
Self::Desc => "desc",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunStepListRequest {
pub limit: Option<u8>,
pub order: Option<RunStepListOrder>,
pub after: Option<String>,
pub before: Option<String>,
include_file_search_result_content: bool,
}
impl RunStepListRequest {
pub fn builder() -> RunStepListRequestBuilder {
RunStepListRequestBuilder::default()
}
pub(crate) fn path(&self, thread_id: &str, run_id: &str) -> String {
path_with_query(
&format!("/v1/threads/{thread_id}/runs/{run_id}/steps"),
ThreadListQuery {
limit: self.limit,
order: self.order.map(RunStepListOrder::as_query_value),
after: self.after.as_deref(),
before: self.before.as_deref(),
run_id: None,
include_file_search_result_content: self.include_file_search_result_content,
},
)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RunStepListRequestBuilder {
limit: Option<u8>,
order: Option<RunStepListOrder>,
after: Option<String>,
before: Option<String>,
include_file_search_result_content: bool,
}
impl RunStepListRequestBuilder {
pub fn limit(mut self, limit: u8) -> Self {
self.limit = Some(limit);
self
}
pub fn order(mut self, order: RunStepListOrder) -> Self {
self.order = Some(order);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn include_file_search_result_content(mut self) -> Self {
self.include_file_search_result_content = true;
self
}
pub fn build(self) -> Result<RunStepListRequest, LingerError> {
if let Some(limit) = self.limit {
if limit == 0 || limit > 100 {
return Err(LingerError::invalid_config(
"limit must be between 1 and 100",
));
}
}
validate_optional_cursor("after", self.after.as_deref())?;
validate_optional_cursor("before", self.before.as_deref())?;
Ok(RunStepListRequest {
limit: self.limit,
order: self.order,
after: self.after,
before: self.before,
include_file_search_result_content: self.include_file_search_result_content,
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunStepRetrieveRequest {
include_file_search_result_content: bool,
}
impl RunStepRetrieveRequest {
pub fn builder() -> RunStepRetrieveRequestBuilder {
RunStepRetrieveRequestBuilder::default()
}
pub(crate) fn path(&self, thread_id: &str, run_id: &str, step_id: &str) -> String {
path_with_query(
&format!("/v1/threads/{thread_id}/runs/{run_id}/steps/{step_id}"),
ThreadListQuery {
limit: None,
order: None,
after: None,
before: None,
run_id: None,
include_file_search_result_content: self.include_file_search_result_content,
},
)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RunStepRetrieveRequestBuilder {
include_file_search_result_content: bool,
}
impl RunStepRetrieveRequestBuilder {
pub fn include_file_search_result_content(mut self) -> Self {
self.include_file_search_result_content = true;
self
}
pub fn build(self) -> RunStepRetrieveRequest {
RunStepRetrieveRequest {
include_file_search_result_content: self.include_file_search_result_content,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ThreadMessagePage {
pub object: String,
#[serde(default)]
pub data: Vec<ThreadMessage>,
#[serde(default)]
pub first_id: Option<String>,
#[serde(default)]
pub last_id: Option<String>,
pub has_more: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ThreadMessagePage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ThreadMessageListOrder {
Asc,
Desc,
}
impl ThreadMessageListOrder {
pub(crate) fn as_query_value(self) -> &'static str {
match self {
Self::Asc => "asc",
Self::Desc => "desc",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ThreadMessageListRequest {
pub limit: Option<u8>,
pub order: Option<ThreadMessageListOrder>,
pub after: Option<String>,
pub before: Option<String>,
pub run_id: Option<String>,
}
impl ThreadMessageListRequest {
pub fn builder() -> ThreadMessageListRequestBuilder {
ThreadMessageListRequestBuilder::default()
}
pub(crate) fn path(&self, thread_id: &str) -> String {
path_with_query(
&format!("/v1/threads/{thread_id}/messages"),
ThreadListQuery {
limit: self.limit,
order: self.order.map(ThreadMessageListOrder::as_query_value),
after: self.after.as_deref(),
before: self.before.as_deref(),
run_id: self.run_id.as_deref(),
include_file_search_result_content: false,
},
)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ThreadMessageListRequestBuilder {
limit: Option<u8>,
order: Option<ThreadMessageListOrder>,
after: Option<String>,
before: Option<String>,
run_id: Option<String>,
}
impl ThreadMessageListRequestBuilder {
pub fn limit(mut self, limit: u8) -> Self {
self.limit = Some(limit);
self
}
pub fn order(mut self, order: ThreadMessageListOrder) -> Self {
self.order = Some(order);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn run_id(mut self, run_id: impl Into<String>) -> Self {
self.run_id = Some(run_id.into());
self
}
pub fn build(self) -> Result<ThreadMessageListRequest, LingerError> {
if let Some(limit) = self.limit {
if limit == 0 || limit > 100 {
return Err(LingerError::invalid_config(
"limit must be between 1 and 100",
));
}
}
validate_optional_cursor("after", self.after.as_deref())?;
validate_optional_cursor("before", self.before.as_deref())?;
validate_optional_cursor("run_id", self.run_id.as_deref())?;
Ok(ThreadMessageListRequest {
limit: self.limit,
order: self.order,
after: self.after,
before: self.before,
run_id: self.run_id,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ThreadMessageDeletion {
pub id: String,
pub object: String,
pub deleted: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ThreadMessageDeletion {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
impl Thread {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ThreadDeletion {
pub id: String,
pub object: String,
pub deleted: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ThreadDeletion {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
fn validate_messages(messages: &[Value]) -> Result<(), LingerError> {
if messages.iter().any(Value::is_null) {
return Err(LingerError::invalid_config(
"messages must not contain null",
));
}
Ok(())
}
fn validate_json_items(name: &str, values: &[Value]) -> Result<(), LingerError> {
if values.iter().any(Value::is_null) {
return Err(LingerError::invalid_config(format!(
"{name} must not contain null"
)));
}
Ok(())
}
fn validate_metadata(metadata: &BTreeMap<String, String>) -> Result<(), LingerError> {
for key in metadata.keys() {
if key.trim().is_empty() {
return Err(LingerError::invalid_config(
"metadata keys must not be empty",
));
}
}
Ok(())
}
fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
for (key, value) in extra {
if key.trim().is_empty() {
return Err(LingerError::invalid_config(
"extra field names must not be empty",
));
}
if value.is_null() {
return Err(LingerError::invalid_config(format!(
"extra field {key} must not be null"
)));
}
}
Ok(())
}
fn validate_optional_string(name: &str, value: &Option<String>) -> Result<(), LingerError> {
if value.as_ref().is_some_and(|value| value.trim().is_empty()) {
return Err(LingerError::invalid_config(format!(
"{name} must not be empty"
)));
}
Ok(())
}
fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
value
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
}
fn validate_optional_cursor(name: &str, value: Option<&str>) -> Result<(), LingerError> {
if value.is_some_and(|value| value.trim().is_empty()) {
return Err(LingerError::invalid_config(format!(
"{name} must not be empty"
)));
}
Ok(())
}
struct ThreadListQuery<'a> {
limit: Option<u8>,
order: Option<&'static str>,
after: Option<&'a str>,
before: Option<&'a str>,
run_id: Option<&'a str>,
include_file_search_result_content: bool,
}
fn path_with_query(base: &str, params: ThreadListQuery<'_>) -> String {
let mut query = Vec::new();
if let Some(limit) = params.limit {
query.push(format!("limit={limit}"));
}
if let Some(order) = params.order {
query.push(format!("order={order}"));
}
if let Some(after) = params.after {
query.push(format!("after={}", encode_query_value(after)));
}
if let Some(before) = params.before {
query.push(format!("before={}", encode_query_value(before)));
}
if let Some(run_id) = params.run_id {
query.push(format!("run_id={}", encode_query_value(run_id)));
}
if params.include_file_search_result_content {
query.push(format!(
"include[]={}",
encode_include_query_value("step_details.tool_calls[*].file_search.results[*].content")
));
}
if query.is_empty() {
base.to_string()
} else {
format!("{base}?{}", query.join("&"))
}
}
fn encode_query_value(value: &str) -> String {
encode_query_value_inner(value, false)
}
fn encode_include_query_value(value: &str) -> String {
encode_query_value_inner(value, true)
}
fn encode_query_value_inner(value: &str, preserve_wildcards: bool) -> String {
let mut encoded = String::new();
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
encoded.push(byte as char);
}
b'*' if preserve_wildcards => encoded.push('*'),
_ => {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
encoded.push('%');
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0F) as usize] as char);
}
}
}
encoded
}