1use crate::error::LingerError;
2use crate::RequestId;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6
7#[derive(Clone, Debug, Default, Serialize, PartialEq)]
10#[non_exhaustive]
11pub struct CreateThreadRequest {
12 #[serde(skip_serializing_if = "Vec::is_empty")]
15 pub messages: Vec<Value>,
16 #[serde(skip_serializing_if = "Option::is_none")]
19 pub tool_resources: Option<Value>,
20 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
23 pub metadata: BTreeMap<String, String>,
24}
25
26impl CreateThreadRequest {
27 pub fn builder() -> CreateThreadRequestBuilder {
30 CreateThreadRequestBuilder::default()
31 }
32}
33
34#[derive(Clone, Debug, Default)]
37#[non_exhaustive]
38pub struct CreateThreadRequestBuilder {
39 messages: Vec<Value>,
40 tool_resources: Option<Value>,
41 metadata: BTreeMap<String, String>,
42}
43
44impl CreateThreadRequestBuilder {
45 pub fn message(mut self, message: Value) -> Self {
48 self.messages.push(message);
49 self
50 }
51
52 pub fn messages(mut self, messages: impl IntoIterator<Item = Value>) -> Self {
55 self.messages = messages.into_iter().collect();
56 self
57 }
58
59 pub fn tool_resources(mut self, tool_resources: Value) -> Self {
62 self.tool_resources = Some(tool_resources);
63 self
64 }
65
66 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
69 self.metadata.insert(key.into(), value.into());
70 self
71 }
72
73 pub fn build(self) -> Result<CreateThreadRequest, LingerError> {
76 validate_messages(&self.messages)?;
77 validate_metadata(&self.metadata)?;
78 if self.tool_resources.as_ref().is_some_and(Value::is_null) {
79 return Err(LingerError::invalid_config(
80 "tool_resources must not be null",
81 ));
82 }
83 Ok(CreateThreadRequest {
84 messages: self.messages,
85 tool_resources: self.tool_resources,
86 metadata: self.metadata,
87 })
88 }
89}
90
91#[derive(Clone, Debug, Default, Serialize, PartialEq)]
94#[non_exhaustive]
95pub struct ModifyThreadRequest {
96 #[serde(skip_serializing_if = "Option::is_none")]
99 pub tool_resources: Option<Value>,
100 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
103 pub metadata: BTreeMap<String, String>,
104}
105
106impl ModifyThreadRequest {
107 pub fn builder() -> ModifyThreadRequestBuilder {
110 ModifyThreadRequestBuilder::default()
111 }
112}
113
114#[derive(Clone, Debug, Default)]
117#[non_exhaustive]
118pub struct ModifyThreadRequestBuilder {
119 tool_resources: Option<Value>,
120 metadata: BTreeMap<String, String>,
121}
122
123impl ModifyThreadRequestBuilder {
124 pub fn tool_resources(mut self, tool_resources: Value) -> Self {
127 self.tool_resources = Some(tool_resources);
128 self
129 }
130
131 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
134 self.metadata.insert(key.into(), value.into());
135 self
136 }
137
138 pub fn build(self) -> Result<ModifyThreadRequest, LingerError> {
141 validate_metadata(&self.metadata)?;
142 if self.tool_resources.as_ref().is_some_and(Value::is_null) {
143 return Err(LingerError::invalid_config(
144 "tool_resources must not be null",
145 ));
146 }
147 Ok(ModifyThreadRequest {
148 tool_resources: self.tool_resources,
149 metadata: self.metadata,
150 })
151 }
152}
153
154#[derive(Clone, Debug, Serialize, PartialEq)]
157#[non_exhaustive]
158pub struct CreateThreadMessageRequest {
159 pub role: String,
162 pub content: Value,
165 #[serde(skip_serializing_if = "Vec::is_empty")]
168 pub attachments: Vec<Value>,
169 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
172 pub metadata: BTreeMap<String, String>,
173}
174
175impl CreateThreadMessageRequest {
176 pub fn builder() -> CreateThreadMessageRequestBuilder {
179 CreateThreadMessageRequestBuilder::default()
180 }
181}
182
183#[derive(Clone, Debug, Default)]
186#[non_exhaustive]
187pub struct CreateThreadMessageRequestBuilder {
188 role: Option<String>,
189 content: Option<Value>,
190 attachments: Vec<Value>,
191 metadata: BTreeMap<String, String>,
192}
193
194impl CreateThreadMessageRequestBuilder {
195 pub fn role(mut self, role: impl Into<String>) -> Self {
198 self.role = Some(role.into());
199 self
200 }
201
202 pub fn content(mut self, content: impl Into<String>) -> Self {
205 self.content = Some(Value::String(content.into()));
206 self
207 }
208
209 pub fn content_json(mut self, content: Value) -> Self {
212 self.content = Some(content);
213 self
214 }
215
216 pub fn attachment(mut self, attachment: Value) -> Self {
219 self.attachments.push(attachment);
220 self
221 }
222
223 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
226 self.metadata.insert(key.into(), value.into());
227 self
228 }
229
230 pub fn build(self) -> Result<CreateThreadMessageRequest, LingerError> {
233 let role = required_string("role", self.role)?;
234 let content = self
235 .content
236 .filter(|value| !value.is_null())
237 .ok_or_else(|| LingerError::invalid_config("content is required"))?;
238 if self.attachments.iter().any(Value::is_null) {
239 return Err(LingerError::invalid_config(
240 "attachments must not contain null",
241 ));
242 }
243 validate_metadata(&self.metadata)?;
244 Ok(CreateThreadMessageRequest {
245 role,
246 content,
247 attachments: self.attachments,
248 metadata: self.metadata,
249 })
250 }
251}
252
253#[derive(Clone, Debug, Default, Serialize, PartialEq)]
256#[non_exhaustive]
257pub struct ModifyThreadMessageRequest {
258 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
261 pub metadata: BTreeMap<String, String>,
262}
263
264impl ModifyThreadMessageRequest {
265 pub fn builder() -> ModifyThreadMessageRequestBuilder {
268 ModifyThreadMessageRequestBuilder::default()
269 }
270}
271
272#[derive(Clone, Debug, Default)]
275#[non_exhaustive]
276pub struct ModifyThreadMessageRequestBuilder {
277 metadata: BTreeMap<String, String>,
278}
279
280impl ModifyThreadMessageRequestBuilder {
281 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
284 self.metadata.insert(key.into(), value.into());
285 self
286 }
287
288 pub fn build(self) -> Result<ModifyThreadMessageRequest, LingerError> {
291 validate_metadata(&self.metadata)?;
292 Ok(ModifyThreadMessageRequest {
293 metadata: self.metadata,
294 })
295 }
296}
297
298#[derive(Clone, Debug, Serialize, PartialEq)]
301#[non_exhaustive]
302pub struct CreateThreadAndRunRequest {
303 pub assistant_id: String,
306 #[serde(skip_serializing_if = "Option::is_none")]
309 pub thread: Option<Value>,
310 #[serde(skip_serializing_if = "Option::is_none")]
313 pub model: Option<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
317 pub instructions: Option<String>,
318 #[serde(skip_serializing_if = "Vec::is_empty")]
321 pub tools: Vec<Value>,
322 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
325 pub metadata: BTreeMap<String, String>,
326 #[serde(flatten)]
329 pub extra: BTreeMap<String, Value>,
330}
331
332impl CreateThreadAndRunRequest {
333 pub fn builder() -> CreateThreadAndRunRequestBuilder {
336 CreateThreadAndRunRequestBuilder::default()
337 }
338}
339
340#[derive(Clone, Debug, Default)]
343#[non_exhaustive]
344pub struct CreateThreadAndRunRequestBuilder {
345 assistant_id: Option<String>,
346 thread: Option<Value>,
347 model: Option<String>,
348 instructions: Option<String>,
349 tools: Vec<Value>,
350 metadata: BTreeMap<String, String>,
351 extra: BTreeMap<String, Value>,
352}
353
354impl CreateThreadAndRunRequestBuilder {
355 pub fn assistant_id(mut self, assistant_id: impl Into<String>) -> Self {
358 self.assistant_id = Some(assistant_id.into());
359 self
360 }
361
362 pub fn thread(mut self, thread: Value) -> Self {
365 self.thread = Some(thread);
366 self
367 }
368
369 pub fn model(mut self, model: impl Into<String>) -> Self {
372 self.model = Some(model.into());
373 self
374 }
375
376 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
379 self.instructions = Some(instructions.into());
380 self
381 }
382
383 pub fn tool(mut self, tool: Value) -> Self {
386 self.tools.push(tool);
387 self
388 }
389
390 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
393 self.metadata.insert(key.into(), value.into());
394 self
395 }
396
397 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
400 self.extra.insert(name.into(), value);
401 self
402 }
403
404 pub fn build(self) -> Result<CreateThreadAndRunRequest, LingerError> {
407 let assistant_id = required_string("assistant_id", self.assistant_id)?;
408 if self.thread.as_ref().is_some_and(Value::is_null) {
409 return Err(LingerError::invalid_config("thread must not be null"));
410 }
411 validate_optional_string("model", &self.model)?;
412 validate_optional_string("instructions", &self.instructions)?;
413 validate_json_items("tools", &self.tools)?;
414 validate_metadata(&self.metadata)?;
415 validate_extra_fields(&self.extra)?;
416 Ok(CreateThreadAndRunRequest {
417 assistant_id,
418 thread: self.thread,
419 model: self.model,
420 instructions: self.instructions,
421 tools: self.tools,
422 metadata: self.metadata,
423 extra: self.extra,
424 })
425 }
426}
427
428#[derive(Clone, Debug, Serialize, PartialEq)]
431#[non_exhaustive]
432pub struct CreateThreadRunRequest {
433 pub assistant_id: String,
436 #[serde(skip_serializing_if = "Option::is_none")]
439 pub model: Option<String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
443 pub instructions: Option<String>,
444 #[serde(skip_serializing_if = "Option::is_none")]
447 pub additional_instructions: Option<String>,
448 #[serde(skip_serializing_if = "Vec::is_empty")]
451 pub additional_messages: Vec<Value>,
452 #[serde(skip_serializing_if = "Vec::is_empty")]
455 pub tools: Vec<Value>,
456 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
459 pub metadata: BTreeMap<String, String>,
460 #[serde(flatten)]
463 pub extra: BTreeMap<String, Value>,
464 #[serde(skip)]
465 include_file_search_result_content: bool,
466}
467
468impl CreateThreadRunRequest {
469 pub fn builder() -> CreateThreadRunRequestBuilder {
472 CreateThreadRunRequestBuilder::default()
473 }
474
475 pub(crate) fn path(&self, thread_id: &str) -> String {
476 path_with_query(
477 &format!("/v1/threads/{thread_id}/runs"),
478 ThreadListQuery {
479 limit: None,
480 order: None,
481 after: None,
482 before: None,
483 run_id: None,
484 include_file_search_result_content: self.include_file_search_result_content,
485 },
486 )
487 }
488}
489
490#[derive(Clone, Debug, Default)]
493#[non_exhaustive]
494pub struct CreateThreadRunRequestBuilder {
495 assistant_id: Option<String>,
496 model: Option<String>,
497 instructions: Option<String>,
498 additional_instructions: Option<String>,
499 additional_messages: Vec<Value>,
500 tools: Vec<Value>,
501 metadata: BTreeMap<String, String>,
502 extra: BTreeMap<String, Value>,
503 include_file_search_result_content: bool,
504}
505
506impl CreateThreadRunRequestBuilder {
507 pub fn assistant_id(mut self, assistant_id: impl Into<String>) -> Self {
510 self.assistant_id = Some(assistant_id.into());
511 self
512 }
513
514 pub fn model(mut self, model: impl Into<String>) -> Self {
517 self.model = Some(model.into());
518 self
519 }
520
521 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
524 self.instructions = Some(instructions.into());
525 self
526 }
527
528 pub fn additional_instructions(mut self, additional_instructions: impl Into<String>) -> Self {
531 self.additional_instructions = Some(additional_instructions.into());
532 self
533 }
534
535 pub fn additional_message(mut self, message: Value) -> Self {
538 self.additional_messages.push(message);
539 self
540 }
541
542 pub fn additional_messages(mut self, messages: impl IntoIterator<Item = Value>) -> Self {
545 self.additional_messages = messages.into_iter().collect();
546 self
547 }
548
549 pub fn tool(mut self, tool: Value) -> Self {
552 self.tools.push(tool);
553 self
554 }
555
556 pub fn tools(mut self, tools: impl IntoIterator<Item = Value>) -> Self {
559 self.tools = tools.into_iter().collect();
560 self
561 }
562
563 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
566 self.metadata.insert(key.into(), value.into());
567 self
568 }
569
570 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
573 self.extra.insert(name.into(), value);
574 self
575 }
576
577 pub fn include_file_search_result_content(mut self) -> Self {
580 self.include_file_search_result_content = true;
581 self
582 }
583
584 pub fn build(self) -> Result<CreateThreadRunRequest, LingerError> {
587 let assistant_id = required_string("assistant_id", self.assistant_id)?;
588 validate_optional_string("model", &self.model)?;
589 validate_optional_string("instructions", &self.instructions)?;
590 validate_optional_string("additional_instructions", &self.additional_instructions)?;
591 validate_json_items("additional_messages", &self.additional_messages)?;
592 validate_json_items("tools", &self.tools)?;
593 validate_metadata(&self.metadata)?;
594 validate_extra_fields(&self.extra)?;
595 Ok(CreateThreadRunRequest {
596 assistant_id,
597 model: self.model,
598 instructions: self.instructions,
599 additional_instructions: self.additional_instructions,
600 additional_messages: self.additional_messages,
601 tools: self.tools,
602 metadata: self.metadata,
603 extra: self.extra,
604 include_file_search_result_content: self.include_file_search_result_content,
605 })
606 }
607}
608
609#[derive(Clone, Debug, Default, Serialize, PartialEq)]
612#[non_exhaustive]
613pub struct ModifyThreadRunRequest {
614 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
617 pub metadata: BTreeMap<String, String>,
618}
619
620impl ModifyThreadRunRequest {
621 pub fn builder() -> ModifyThreadRunRequestBuilder {
624 ModifyThreadRunRequestBuilder::default()
625 }
626}
627
628#[derive(Clone, Debug, Default)]
631#[non_exhaustive]
632pub struct ModifyThreadRunRequestBuilder {
633 metadata: BTreeMap<String, String>,
634}
635
636impl ModifyThreadRunRequestBuilder {
637 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
640 self.metadata.insert(key.into(), value.into());
641 self
642 }
643
644 pub fn build(self) -> Result<ModifyThreadRunRequest, LingerError> {
647 validate_metadata(&self.metadata)?;
648 Ok(ModifyThreadRunRequest {
649 metadata: self.metadata,
650 })
651 }
652}
653
654#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
657#[non_exhaustive]
658pub struct SubmitToolOutput {
659 pub tool_call_id: String,
662 pub output: String,
665}
666
667#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
670#[non_exhaustive]
671pub struct SubmitToolOutputsRequest {
672 pub tool_outputs: Vec<SubmitToolOutput>,
675}
676
677impl SubmitToolOutputsRequest {
678 pub fn builder() -> SubmitToolOutputsRequestBuilder {
681 SubmitToolOutputsRequestBuilder::default()
682 }
683}
684
685#[derive(Clone, Debug, Default)]
688#[non_exhaustive]
689pub struct SubmitToolOutputsRequestBuilder {
690 tool_outputs: Vec<SubmitToolOutput>,
691}
692
693impl SubmitToolOutputsRequestBuilder {
694 pub fn tool_output(
697 mut self,
698 tool_call_id: impl Into<String>,
699 output: impl Into<String>,
700 ) -> Self {
701 self.tool_outputs.push(SubmitToolOutput {
702 tool_call_id: tool_call_id.into(),
703 output: output.into(),
704 });
705 self
706 }
707
708 pub fn build(self) -> Result<SubmitToolOutputsRequest, LingerError> {
711 if self.tool_outputs.is_empty() {
712 return Err(LingerError::invalid_config(
713 "tool_outputs must not be empty",
714 ));
715 }
716 for output in &self.tool_outputs {
717 if output.tool_call_id.trim().is_empty() {
718 return Err(LingerError::invalid_config("tool_call_id is required"));
719 }
720 }
721 Ok(SubmitToolOutputsRequest {
722 tool_outputs: self.tool_outputs,
723 })
724 }
725}
726
727#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
730#[non_exhaustive]
731pub struct Thread {
732 pub id: String,
735 pub object: String,
738 pub created_at: u64,
741 #[serde(default)]
744 pub tool_resources: Option<Value>,
745 #[serde(default)]
748 pub metadata: BTreeMap<String, String>,
749 #[serde(flatten)]
752 pub extra: BTreeMap<String, Value>,
753 #[serde(skip)]
756 request_id: Option<RequestId>,
757}
758
759#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
762#[non_exhaustive]
763pub struct ThreadMessage {
764 pub id: String,
767 pub object: String,
770 pub created_at: u64,
773 pub thread_id: String,
776 pub role: String,
779 #[serde(default)]
782 pub content: Vec<Value>,
783 #[serde(default)]
786 pub status: Option<String>,
787 #[serde(default)]
790 pub incomplete_details: Option<Value>,
791 #[serde(default)]
794 pub completed_at: Option<u64>,
795 #[serde(default)]
798 pub incomplete_at: Option<u64>,
799 #[serde(default)]
802 pub assistant_id: Option<String>,
803 #[serde(default)]
806 pub run_id: Option<String>,
807 #[serde(default)]
810 pub attachments: Vec<Value>,
811 #[serde(default)]
814 pub metadata: BTreeMap<String, String>,
815 #[serde(flatten)]
818 pub extra: BTreeMap<String, Value>,
819 #[serde(skip)]
822 request_id: Option<RequestId>,
823}
824
825#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
828#[non_exhaustive]
829pub struct ThreadRun {
830 pub id: String,
833 pub object: String,
836 pub created_at: u64,
839 pub thread_id: String,
842 pub assistant_id: String,
845 pub status: String,
848 #[serde(default)]
851 pub required_action: Option<Value>,
852 #[serde(default)]
855 pub last_error: Option<Value>,
856 #[serde(default)]
859 pub expires_at: Option<u64>,
860 #[serde(default)]
863 pub started_at: Option<u64>,
864 #[serde(default)]
867 pub cancelled_at: Option<u64>,
868 #[serde(default)]
871 pub failed_at: Option<u64>,
872 #[serde(default)]
875 pub completed_at: Option<u64>,
876 #[serde(default)]
879 pub incomplete_details: Option<Value>,
880 pub model: String,
883 #[serde(default)]
886 pub instructions: Option<String>,
887 #[serde(default)]
890 pub tools: Vec<Value>,
891 #[serde(default)]
894 pub metadata: BTreeMap<String, String>,
895 #[serde(default)]
898 pub usage: Option<Value>,
899 #[serde(flatten)]
902 pub extra: BTreeMap<String, Value>,
903 #[serde(skip)]
906 request_id: Option<RequestId>,
907}
908
909#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
912#[non_exhaustive]
913pub struct ThreadRunPage {
914 pub object: String,
917 #[serde(default)]
920 pub data: Vec<ThreadRun>,
921 #[serde(default)]
924 pub first_id: Option<String>,
925 #[serde(default)]
928 pub last_id: Option<String>,
929 pub has_more: bool,
932 #[serde(skip)]
935 request_id: Option<RequestId>,
936}
937
938#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
941#[non_exhaustive]
942pub struct RunStep {
943 pub id: String,
946 pub object: String,
949 pub created_at: u64,
952 pub assistant_id: String,
955 pub thread_id: String,
958 pub run_id: String,
961 #[serde(rename = "type")]
964 pub kind: String,
965 pub status: String,
968 pub step_details: Value,
971 #[serde(default)]
974 pub last_error: Option<Value>,
975 #[serde(default)]
978 pub expired_at: Option<u64>,
979 #[serde(default)]
982 pub cancelled_at: Option<u64>,
983 #[serde(default)]
986 pub failed_at: Option<u64>,
987 #[serde(default)]
990 pub completed_at: Option<u64>,
991 #[serde(default)]
994 pub metadata: BTreeMap<String, String>,
995 #[serde(default)]
998 pub usage: Option<Value>,
999 #[serde(flatten)]
1002 pub extra: BTreeMap<String, Value>,
1003 #[serde(skip)]
1006 request_id: Option<RequestId>,
1007}
1008
1009#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1012#[non_exhaustive]
1013pub struct RunStepPage {
1014 pub object: String,
1017 #[serde(default)]
1020 pub data: Vec<RunStep>,
1021 #[serde(default)]
1024 pub first_id: Option<String>,
1025 #[serde(default)]
1028 pub last_id: Option<String>,
1029 pub has_more: bool,
1032 #[serde(skip)]
1035 request_id: Option<RequestId>,
1036}
1037
1038impl ThreadMessage {
1039 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1040 self.request_id = request_id;
1041 self
1042 }
1043
1044 pub fn request_id(&self) -> Option<&RequestId> {
1047 self.request_id.as_ref()
1048 }
1049}
1050
1051impl ThreadRun {
1052 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1053 self.request_id = request_id;
1054 self
1055 }
1056
1057 pub fn request_id(&self) -> Option<&RequestId> {
1060 self.request_id.as_ref()
1061 }
1062}
1063
1064impl ThreadRunPage {
1065 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1066 self.request_id = request_id;
1067 self
1068 }
1069
1070 pub fn request_id(&self) -> Option<&RequestId> {
1073 self.request_id.as_ref()
1074 }
1075}
1076
1077#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1080#[non_exhaustive]
1081pub enum ThreadRunListOrder {
1082 Asc,
1085 Desc,
1088}
1089
1090impl ThreadRunListOrder {
1091 pub(crate) fn as_query_value(self) -> &'static str {
1092 match self {
1093 Self::Asc => "asc",
1094 Self::Desc => "desc",
1095 }
1096 }
1097}
1098
1099#[derive(Clone, Debug, Default, PartialEq, Eq)]
1102#[non_exhaustive]
1103pub struct ThreadRunListRequest {
1104 pub limit: Option<u8>,
1107 pub order: Option<ThreadRunListOrder>,
1110 pub after: Option<String>,
1113 pub before: Option<String>,
1116}
1117
1118impl ThreadRunListRequest {
1119 pub fn builder() -> ThreadRunListRequestBuilder {
1122 ThreadRunListRequestBuilder::default()
1123 }
1124
1125 pub(crate) fn path(&self, thread_id: &str) -> String {
1126 path_with_query(
1127 &format!("/v1/threads/{thread_id}/runs"),
1128 ThreadListQuery {
1129 limit: self.limit,
1130 order: self.order.map(ThreadRunListOrder::as_query_value),
1131 after: self.after.as_deref(),
1132 before: self.before.as_deref(),
1133 run_id: None,
1134 include_file_search_result_content: false,
1135 },
1136 )
1137 }
1138}
1139
1140#[derive(Clone, Debug, Default)]
1143#[non_exhaustive]
1144pub struct ThreadRunListRequestBuilder {
1145 limit: Option<u8>,
1146 order: Option<ThreadRunListOrder>,
1147 after: Option<String>,
1148 before: Option<String>,
1149}
1150
1151impl ThreadRunListRequestBuilder {
1152 pub fn limit(mut self, limit: u8) -> Self {
1155 self.limit = Some(limit);
1156 self
1157 }
1158
1159 pub fn order(mut self, order: ThreadRunListOrder) -> Self {
1162 self.order = Some(order);
1163 self
1164 }
1165
1166 pub fn after(mut self, after: impl Into<String>) -> Self {
1169 self.after = Some(after.into());
1170 self
1171 }
1172
1173 pub fn before(mut self, before: impl Into<String>) -> Self {
1176 self.before = Some(before.into());
1177 self
1178 }
1179
1180 pub fn build(self) -> Result<ThreadRunListRequest, LingerError> {
1183 if let Some(limit) = self.limit {
1184 if limit == 0 || limit > 100 {
1185 return Err(LingerError::invalid_config(
1186 "limit must be between 1 and 100",
1187 ));
1188 }
1189 }
1190 validate_optional_cursor("after", self.after.as_deref())?;
1191 validate_optional_cursor("before", self.before.as_deref())?;
1192 Ok(ThreadRunListRequest {
1193 limit: self.limit,
1194 order: self.order,
1195 after: self.after,
1196 before: self.before,
1197 })
1198 }
1199}
1200
1201impl RunStep {
1202 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1203 self.request_id = request_id;
1204 self
1205 }
1206
1207 pub fn request_id(&self) -> Option<&RequestId> {
1210 self.request_id.as_ref()
1211 }
1212}
1213
1214impl RunStepPage {
1215 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1216 self.request_id = request_id;
1217 self
1218 }
1219
1220 pub fn request_id(&self) -> Option<&RequestId> {
1223 self.request_id.as_ref()
1224 }
1225}
1226
1227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1230#[non_exhaustive]
1231pub enum RunStepListOrder {
1232 Asc,
1235 Desc,
1238}
1239
1240impl RunStepListOrder {
1241 pub(crate) fn as_query_value(self) -> &'static str {
1242 match self {
1243 Self::Asc => "asc",
1244 Self::Desc => "desc",
1245 }
1246 }
1247}
1248
1249#[derive(Clone, Debug, Default, PartialEq, Eq)]
1252#[non_exhaustive]
1253pub struct RunStepListRequest {
1254 pub limit: Option<u8>,
1257 pub order: Option<RunStepListOrder>,
1260 pub after: Option<String>,
1263 pub before: Option<String>,
1266 include_file_search_result_content: bool,
1267}
1268
1269impl RunStepListRequest {
1270 pub fn builder() -> RunStepListRequestBuilder {
1273 RunStepListRequestBuilder::default()
1274 }
1275
1276 pub(crate) fn path(&self, thread_id: &str, run_id: &str) -> String {
1277 path_with_query(
1278 &format!("/v1/threads/{thread_id}/runs/{run_id}/steps"),
1279 ThreadListQuery {
1280 limit: self.limit,
1281 order: self.order.map(RunStepListOrder::as_query_value),
1282 after: self.after.as_deref(),
1283 before: self.before.as_deref(),
1284 run_id: None,
1285 include_file_search_result_content: self.include_file_search_result_content,
1286 },
1287 )
1288 }
1289}
1290
1291#[derive(Clone, Debug, Default)]
1294#[non_exhaustive]
1295pub struct RunStepListRequestBuilder {
1296 limit: Option<u8>,
1297 order: Option<RunStepListOrder>,
1298 after: Option<String>,
1299 before: Option<String>,
1300 include_file_search_result_content: bool,
1301}
1302
1303impl RunStepListRequestBuilder {
1304 pub fn limit(mut self, limit: u8) -> Self {
1307 self.limit = Some(limit);
1308 self
1309 }
1310
1311 pub fn order(mut self, order: RunStepListOrder) -> Self {
1314 self.order = Some(order);
1315 self
1316 }
1317
1318 pub fn after(mut self, after: impl Into<String>) -> Self {
1321 self.after = Some(after.into());
1322 self
1323 }
1324
1325 pub fn before(mut self, before: impl Into<String>) -> Self {
1328 self.before = Some(before.into());
1329 self
1330 }
1331
1332 pub fn include_file_search_result_content(mut self) -> Self {
1335 self.include_file_search_result_content = true;
1336 self
1337 }
1338
1339 pub fn build(self) -> Result<RunStepListRequest, LingerError> {
1342 if let Some(limit) = self.limit {
1343 if limit == 0 || limit > 100 {
1344 return Err(LingerError::invalid_config(
1345 "limit must be between 1 and 100",
1346 ));
1347 }
1348 }
1349 validate_optional_cursor("after", self.after.as_deref())?;
1350 validate_optional_cursor("before", self.before.as_deref())?;
1351 Ok(RunStepListRequest {
1352 limit: self.limit,
1353 order: self.order,
1354 after: self.after,
1355 before: self.before,
1356 include_file_search_result_content: self.include_file_search_result_content,
1357 })
1358 }
1359}
1360
1361#[derive(Clone, Debug, Default, PartialEq, Eq)]
1364#[non_exhaustive]
1365pub struct RunStepRetrieveRequest {
1366 include_file_search_result_content: bool,
1367}
1368
1369impl RunStepRetrieveRequest {
1370 pub fn builder() -> RunStepRetrieveRequestBuilder {
1373 RunStepRetrieveRequestBuilder::default()
1374 }
1375
1376 pub(crate) fn path(&self, thread_id: &str, run_id: &str, step_id: &str) -> String {
1377 path_with_query(
1378 &format!("/v1/threads/{thread_id}/runs/{run_id}/steps/{step_id}"),
1379 ThreadListQuery {
1380 limit: None,
1381 order: None,
1382 after: None,
1383 before: None,
1384 run_id: None,
1385 include_file_search_result_content: self.include_file_search_result_content,
1386 },
1387 )
1388 }
1389}
1390
1391#[derive(Clone, Debug, Default)]
1394#[non_exhaustive]
1395pub struct RunStepRetrieveRequestBuilder {
1396 include_file_search_result_content: bool,
1397}
1398
1399impl RunStepRetrieveRequestBuilder {
1400 pub fn include_file_search_result_content(mut self) -> Self {
1403 self.include_file_search_result_content = true;
1404 self
1405 }
1406
1407 pub fn build(self) -> RunStepRetrieveRequest {
1410 RunStepRetrieveRequest {
1411 include_file_search_result_content: self.include_file_search_result_content,
1412 }
1413 }
1414}
1415
1416#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1419#[non_exhaustive]
1420pub struct ThreadMessagePage {
1421 pub object: String,
1424 #[serde(default)]
1427 pub data: Vec<ThreadMessage>,
1428 #[serde(default)]
1431 pub first_id: Option<String>,
1432 #[serde(default)]
1435 pub last_id: Option<String>,
1436 pub has_more: bool,
1439 #[serde(skip)]
1442 request_id: Option<RequestId>,
1443}
1444
1445impl ThreadMessagePage {
1446 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1447 self.request_id = request_id;
1448 self
1449 }
1450
1451 pub fn request_id(&self) -> Option<&RequestId> {
1454 self.request_id.as_ref()
1455 }
1456}
1457
1458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1461#[non_exhaustive]
1462pub enum ThreadMessageListOrder {
1463 Asc,
1466 Desc,
1469}
1470
1471impl ThreadMessageListOrder {
1472 pub(crate) fn as_query_value(self) -> &'static str {
1473 match self {
1474 Self::Asc => "asc",
1475 Self::Desc => "desc",
1476 }
1477 }
1478}
1479
1480#[derive(Clone, Debug, Default, PartialEq, Eq)]
1483#[non_exhaustive]
1484pub struct ThreadMessageListRequest {
1485 pub limit: Option<u8>,
1488 pub order: Option<ThreadMessageListOrder>,
1491 pub after: Option<String>,
1494 pub before: Option<String>,
1497 pub run_id: Option<String>,
1500}
1501
1502impl ThreadMessageListRequest {
1503 pub fn builder() -> ThreadMessageListRequestBuilder {
1506 ThreadMessageListRequestBuilder::default()
1507 }
1508
1509 pub(crate) fn path(&self, thread_id: &str) -> String {
1510 path_with_query(
1511 &format!("/v1/threads/{thread_id}/messages"),
1512 ThreadListQuery {
1513 limit: self.limit,
1514 order: self.order.map(ThreadMessageListOrder::as_query_value),
1515 after: self.after.as_deref(),
1516 before: self.before.as_deref(),
1517 run_id: self.run_id.as_deref(),
1518 include_file_search_result_content: false,
1519 },
1520 )
1521 }
1522}
1523
1524#[derive(Clone, Debug, Default)]
1527#[non_exhaustive]
1528pub struct ThreadMessageListRequestBuilder {
1529 limit: Option<u8>,
1530 order: Option<ThreadMessageListOrder>,
1531 after: Option<String>,
1532 before: Option<String>,
1533 run_id: Option<String>,
1534}
1535
1536impl ThreadMessageListRequestBuilder {
1537 pub fn limit(mut self, limit: u8) -> Self {
1540 self.limit = Some(limit);
1541 self
1542 }
1543
1544 pub fn order(mut self, order: ThreadMessageListOrder) -> Self {
1547 self.order = Some(order);
1548 self
1549 }
1550
1551 pub fn after(mut self, after: impl Into<String>) -> Self {
1554 self.after = Some(after.into());
1555 self
1556 }
1557
1558 pub fn before(mut self, before: impl Into<String>) -> Self {
1561 self.before = Some(before.into());
1562 self
1563 }
1564
1565 pub fn run_id(mut self, run_id: impl Into<String>) -> Self {
1568 self.run_id = Some(run_id.into());
1569 self
1570 }
1571
1572 pub fn build(self) -> Result<ThreadMessageListRequest, LingerError> {
1575 if let Some(limit) = self.limit {
1576 if limit == 0 || limit > 100 {
1577 return Err(LingerError::invalid_config(
1578 "limit must be between 1 and 100",
1579 ));
1580 }
1581 }
1582 validate_optional_cursor("after", self.after.as_deref())?;
1583 validate_optional_cursor("before", self.before.as_deref())?;
1584 validate_optional_cursor("run_id", self.run_id.as_deref())?;
1585 Ok(ThreadMessageListRequest {
1586 limit: self.limit,
1587 order: self.order,
1588 after: self.after,
1589 before: self.before,
1590 run_id: self.run_id,
1591 })
1592 }
1593}
1594
1595#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1598#[non_exhaustive]
1599pub struct ThreadMessageDeletion {
1600 pub id: String,
1603 pub object: String,
1606 pub deleted: bool,
1609 #[serde(skip)]
1612 request_id: Option<RequestId>,
1613}
1614
1615impl ThreadMessageDeletion {
1616 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1617 self.request_id = request_id;
1618 self
1619 }
1620
1621 pub fn request_id(&self) -> Option<&RequestId> {
1624 self.request_id.as_ref()
1625 }
1626}
1627
1628impl Thread {
1629 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1630 self.request_id = request_id;
1631 self
1632 }
1633
1634 pub fn request_id(&self) -> Option<&RequestId> {
1637 self.request_id.as_ref()
1638 }
1639}
1640
1641#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1644#[non_exhaustive]
1645pub struct ThreadDeletion {
1646 pub id: String,
1649 pub object: String,
1652 pub deleted: bool,
1655 #[serde(skip)]
1658 request_id: Option<RequestId>,
1659}
1660
1661impl ThreadDeletion {
1662 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1663 self.request_id = request_id;
1664 self
1665 }
1666
1667 pub fn request_id(&self) -> Option<&RequestId> {
1670 self.request_id.as_ref()
1671 }
1672}
1673
1674fn validate_messages(messages: &[Value]) -> Result<(), LingerError> {
1675 if messages.iter().any(Value::is_null) {
1676 return Err(LingerError::invalid_config(
1677 "messages must not contain null",
1678 ));
1679 }
1680 Ok(())
1681}
1682
1683fn validate_json_items(name: &str, values: &[Value]) -> Result<(), LingerError> {
1684 if values.iter().any(Value::is_null) {
1685 return Err(LingerError::invalid_config(format!(
1686 "{name} must not contain null"
1687 )));
1688 }
1689 Ok(())
1690}
1691
1692fn validate_metadata(metadata: &BTreeMap<String, String>) -> Result<(), LingerError> {
1693 for key in metadata.keys() {
1694 if key.trim().is_empty() {
1695 return Err(LingerError::invalid_config(
1696 "metadata keys must not be empty",
1697 ));
1698 }
1699 }
1700 Ok(())
1701}
1702
1703fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
1704 for (key, value) in extra {
1705 if key.trim().is_empty() {
1706 return Err(LingerError::invalid_config(
1707 "extra field names must not be empty",
1708 ));
1709 }
1710 if value.is_null() {
1711 return Err(LingerError::invalid_config(format!(
1712 "extra field {key} must not be null"
1713 )));
1714 }
1715 }
1716 Ok(())
1717}
1718
1719fn validate_optional_string(name: &str, value: &Option<String>) -> Result<(), LingerError> {
1720 if value.as_ref().is_some_and(|value| value.trim().is_empty()) {
1721 return Err(LingerError::invalid_config(format!(
1722 "{name} must not be empty"
1723 )));
1724 }
1725 Ok(())
1726}
1727
1728fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
1729 value
1730 .filter(|value| !value.trim().is_empty())
1731 .ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
1732}
1733
1734fn validate_optional_cursor(name: &str, value: Option<&str>) -> Result<(), LingerError> {
1735 if value.is_some_and(|value| value.trim().is_empty()) {
1736 return Err(LingerError::invalid_config(format!(
1737 "{name} must not be empty"
1738 )));
1739 }
1740 Ok(())
1741}
1742
1743struct ThreadListQuery<'a> {
1744 limit: Option<u8>,
1745 order: Option<&'static str>,
1746 after: Option<&'a str>,
1747 before: Option<&'a str>,
1748 run_id: Option<&'a str>,
1749 include_file_search_result_content: bool,
1750}
1751
1752fn path_with_query(base: &str, params: ThreadListQuery<'_>) -> String {
1753 let mut query = Vec::new();
1754 if let Some(limit) = params.limit {
1755 query.push(format!("limit={limit}"));
1756 }
1757 if let Some(order) = params.order {
1758 query.push(format!("order={order}"));
1759 }
1760 if let Some(after) = params.after {
1761 query.push(format!("after={}", encode_query_value(after)));
1762 }
1763 if let Some(before) = params.before {
1764 query.push(format!("before={}", encode_query_value(before)));
1765 }
1766 if let Some(run_id) = params.run_id {
1767 query.push(format!("run_id={}", encode_query_value(run_id)));
1768 }
1769 if params.include_file_search_result_content {
1770 query.push(format!(
1771 "include[]={}",
1772 encode_include_query_value("step_details.tool_calls[*].file_search.results[*].content")
1773 ));
1774 }
1775 if query.is_empty() {
1776 base.to_string()
1777 } else {
1778 format!("{base}?{}", query.join("&"))
1779 }
1780}
1781
1782fn encode_query_value(value: &str) -> String {
1783 encode_query_value_inner(value, false)
1784}
1785
1786fn encode_include_query_value(value: &str) -> String {
1787 encode_query_value_inner(value, true)
1788}
1789
1790fn encode_query_value_inner(value: &str, preserve_wildcards: bool) -> String {
1791 let mut encoded = String::new();
1792 for byte in value.bytes() {
1793 match byte {
1794 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
1795 encoded.push(byte as char);
1796 }
1797 b'*' if preserve_wildcards => encoded.push('*'),
1798 _ => {
1799 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1800 encoded.push('%');
1801 encoded.push(HEX[(byte >> 4) as usize] as char);
1802 encoded.push(HEX[(byte & 0x0F) as usize] as char);
1803 }
1804 }
1805 }
1806 encoded
1807}