1use super::message::{AssistantContent, DocumentMediaType};
38use crate::message::ToolChoice;
39use crate::provider_response;
40use crate::streaming::StreamingCompletionResponse;
41use crate::tool::server::ToolServerError;
42use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
43use crate::{OneOrMany, http_client};
44use crate::{
45 json_utils,
46 message::{Message, UserContent},
47 tool::ToolSetError,
48};
49
50use serde::de::DeserializeOwned;
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use std::ops::{Add, AddAssign};
54use thiserror::Error;
55
56#[derive(Debug, Error)]
88#[non_exhaustive]
89pub enum CompletionError {
90 #[error("HttpError: {0}")]
92 HttpError(#[from] http_client::Error),
93
94 #[error("JsonError: {0}")]
96 JsonError(#[from] serde_json::Error),
97
98 #[error("UrlError: {0}")]
100 UrlError(#[from] url::ParseError),
101
102 #[cfg(not(target_family = "wasm"))]
103 #[error("RequestError: {0}")]
105 RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
106
107 #[cfg(target_family = "wasm")]
108 #[error("RequestError: {0}")]
110 RequestError(#[from] Box<dyn std::error::Error + 'static>),
111
112 #[error("ResponseError: {0}")]
114 ResponseError(String),
115
116 #[error("ProviderError: {0}")]
118 ProviderError(String),
119
120 #[error("ProviderResponseError: {0}")]
122 ProviderResponse(provider_response::ProviderResponseError),
123}
124
125crate::provider_response::impl_provider_response_helpers!(CompletionError);
126
127impl CompletionError {
128 pub(crate) fn from_stream_transport(error: http_client::Error) -> Self {
134 if error.non_success_status().is_some() {
135 Self::HttpError(error)
136 } else {
137 Self::ProviderError(error.to_string())
138 }
139 }
140}
141
142#[derive(Debug, Error)]
148pub enum PromptError {
149 #[error("CompletionError: {0}")]
151 CompletionError(#[from] CompletionError),
152
153 #[error("ToolCallError: {0}")]
155 ToolError(#[from] ToolSetError),
156
157 #[error("ToolServerError: {0}")]
159 ToolServerError(#[from] Box<ToolServerError>),
160
161 #[error("MaxTurnsError: reached max turns limit: {max_turns}")]
165 MaxTurnsError {
166 max_turns: usize,
167 chat_history: Box<Vec<Message>>,
168 prompt: Box<Message>,
169 },
170
171 #[error("PromptCancelled: {reason}")]
173 PromptCancelled {
174 chat_history: Vec<Message>,
175 reason: String,
176 },
177
178 #[error(
181 "UnknownToolCall: model attempted to call unknown or disallowed tool `{tool_name}`. Available tools: {available_tools:?}. Allowed tools for this turn: {allowed_tools:?}"
182 )]
183 UnknownToolCall {
184 tool_name: String,
185 available_tools: Vec<String>,
186 allowed_tools: Vec<String>,
187 chat_history: Box<Vec<Message>>,
188 },
189}
190
191impl From<crate::memory::MemoryError> for PromptError {
195 fn from(err: crate::memory::MemoryError) -> Self {
196 Self::CompletionError(CompletionError::RequestError(Box::new(err)))
197 }
198}
199
200impl PromptError {
201 pub fn provider_response_body(&self) -> Option<&str> {
203 match self {
204 Self::CompletionError(error) => error.provider_response_body(),
205 _ => None,
206 }
207 }
208
209 pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
216 match self {
217 Self::CompletionError(error) => error.provider_response_json(),
218 _ => Ok(None),
219 }
220 }
221
222 pub fn provider_response_status(&self) -> Option<http::StatusCode> {
225 match self {
226 Self::CompletionError(error) => error.provider_response_status(),
227 _ => None,
228 }
229 }
230
231 pub(crate) fn prompt_cancelled(
232 chat_history: impl IntoIterator<Item = Message>,
233 reason: impl Into<String>,
234 ) -> Self {
235 Self::PromptCancelled {
236 chat_history: chat_history.into_iter().collect(),
237 reason: reason.into(),
238 }
239 }
240}
241
242#[derive(Debug, Error)]
249pub enum StructuredOutputError {
250 #[error("PromptError: {0}")]
252 PromptError(#[from] Box<PromptError>),
253
254 #[error("DeserializationError: {0}")]
256 DeserializationError(#[from] serde_json::Error),
257
258 #[error("EmptyResponse: model returned no content")]
260 EmptyResponse,
261}
262
263impl StructuredOutputError {
264 pub fn provider_response_body(&self) -> Option<&str> {
266 match self {
267 Self::PromptError(error) => error.provider_response_body(),
268 _ => None,
269 }
270 }
271
272 pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
279 match self {
280 Self::PromptError(error) => error.provider_response_json(),
281 _ => Ok(None),
282 }
283 }
284
285 pub fn provider_response_status(&self) -> Option<http::StatusCode> {
287 match self {
288 Self::PromptError(error) => error.provider_response_status(),
289 _ => None,
290 }
291 }
292}
293
294#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
295pub struct Document {
296 pub id: String,
298 pub text: String,
300 #[serde(flatten)]
302 pub additional_props: HashMap<String, String>,
303}
304
305impl std::fmt::Display for Document {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 write!(
308 f,
309 concat!("<file id: {}>\n", "{}\n", "</file>\n"),
310 self.id,
311 if self.additional_props.is_empty() {
312 self.text.clone()
313 } else {
314 let mut sorted_props = self.additional_props.iter().collect::<Vec<_>>();
315 sorted_props.sort_by(|a, b| a.0.cmp(b.0));
316 let metadata = sorted_props
317 .iter()
318 .map(|(k, v)| format!("{k}: {v:?}"))
319 .collect::<Vec<_>>()
320 .join(" ");
321 format!("<metadata {} />\n{}", metadata, self.text)
322 }
323 )
324 }
325}
326
327#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
328pub struct ToolDefinition {
329 pub name: String,
331 pub description: String,
333 pub parameters: serde_json::Value,
335}
336
337#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
342pub struct ProviderToolDefinition {
343 #[serde(rename = "type")]
345 pub kind: String,
346 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
348 pub config: serde_json::Map<String, serde_json::Value>,
349}
350
351impl ProviderToolDefinition {
352 pub fn new(kind: impl Into<String>) -> Self {
354 Self {
355 kind: kind.into(),
356 config: serde_json::Map::new(),
357 }
358 }
359
360 pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
362 self.config.insert(key.into(), value);
363 self
364 }
365}
366
367pub trait Prompt: WasmCompatSend + WasmCompatSync {
372 fn prompt(
381 &self,
382 prompt: impl Into<Message> + WasmCompatSend,
383 ) -> impl std::future::IntoFuture<Output = Result<String, PromptError>, IntoFuture: WasmCompatSend>;
384}
385
386pub trait Chat: WasmCompatSend + WasmCompatSync {
388 fn chat(
402 &self,
403 prompt: impl Into<Message> + WasmCompatSend,
404 chat_history: &mut Vec<Message>,
405 ) -> impl std::future::Future<Output = Result<String, PromptError>> + WasmCompatSend;
406}
407
408pub trait TypedPrompt: WasmCompatSend + WasmCompatSync {
432 type TypedRequest<T>: std::future::IntoFuture<Output = Result<T, StructuredOutputError>>
434 where
435 T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend + 'static;
436
437 fn prompt_typed<T>(&self, prompt: impl Into<Message> + WasmCompatSend) -> Self::TypedRequest<T>
457 where
458 T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend;
459}
460
461pub trait Completion<M: CompletionModel> {
463 fn completion<I, T>(
475 &self,
476 prompt: impl Into<Message> + WasmCompatSend,
477 chat_history: I,
478 ) -> impl std::future::Future<Output = Result<CompletionRequestBuilder<M>, CompletionError>>
479 + WasmCompatSend
480 where
481 I: IntoIterator<Item = T> + WasmCompatSend,
482 T: Into<Message>;
483}
484
485#[derive(Debug)]
488pub struct CompletionResponse<T> {
489 pub choice: OneOrMany<AssistantContent>,
492 pub usage: Usage,
494 pub raw_response: T,
496 pub message_id: Option<String>,
499}
500
501pub trait GetTokenUsage {
505 fn token_usage(&self) -> crate::completion::Usage;
509}
510
511impl GetTokenUsage for () {
512 fn token_usage(&self) -> crate::completion::Usage {
513 crate::completion::Usage::new()
514 }
515}
516
517impl<T> GetTokenUsage for Option<T>
518where
519 T: GetTokenUsage,
520{
521 fn token_usage(&self) -> crate::completion::Usage {
522 if let Some(usage) = self {
523 usage.token_usage()
524 } else {
525 crate::completion::Usage::new()
526 }
527 }
528}
529
530#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
533pub struct Usage {
534 pub input_tokens: u64,
536 pub output_tokens: u64,
538 pub total_tokens: u64,
540 pub cached_input_tokens: u64,
542 pub cache_creation_input_tokens: u64,
544 #[serde(default)]
546 pub tool_use_prompt_tokens: u64,
547 pub reasoning_tokens: u64,
550}
551
552impl Usage {
553 pub fn new() -> Self {
555 Self {
556 input_tokens: 0,
557 output_tokens: 0,
558 total_tokens: 0,
559 cached_input_tokens: 0,
560 cache_creation_input_tokens: 0,
561 tool_use_prompt_tokens: 0,
562 reasoning_tokens: 0,
563 }
564 }
565
566 pub fn has_values(&self) -> bool {
571 *self != Self::new()
572 }
573}
574
575impl Default for Usage {
576 fn default() -> Self {
577 Self::new()
578 }
579}
580
581impl Add for Usage {
582 type Output = Self;
583
584 fn add(self, other: Self) -> Self::Output {
585 Self {
586 input_tokens: self.input_tokens + other.input_tokens,
587 output_tokens: self.output_tokens + other.output_tokens,
588 total_tokens: self.total_tokens + other.total_tokens,
589 cached_input_tokens: self.cached_input_tokens + other.cached_input_tokens,
590 cache_creation_input_tokens: self.cache_creation_input_tokens
591 + other.cache_creation_input_tokens,
592 tool_use_prompt_tokens: self.tool_use_prompt_tokens + other.tool_use_prompt_tokens,
593 reasoning_tokens: self.reasoning_tokens + other.reasoning_tokens,
594 }
595 }
596}
597
598impl AddAssign for Usage {
599 fn add_assign(&mut self, other: Self) {
600 self.input_tokens += other.input_tokens;
601 self.output_tokens += other.output_tokens;
602 self.total_tokens += other.total_tokens;
603 self.cached_input_tokens += other.cached_input_tokens;
604 self.cache_creation_input_tokens += other.cache_creation_input_tokens;
605 self.tool_use_prompt_tokens += other.tool_use_prompt_tokens;
606 self.reasoning_tokens += other.reasoning_tokens;
607 }
608}
609
610pub trait CompletionModel: Clone + WasmCompatSend + WasmCompatSync {
614 type Response: WasmCompatSend + WasmCompatSync + Serialize + DeserializeOwned;
616 type StreamingResponse: Clone
618 + Unpin
619 + WasmCompatSend
620 + WasmCompatSync
621 + Serialize
622 + DeserializeOwned
623 + GetTokenUsage;
624
625 type Client;
627
628 fn make(client: &Self::Client, model: impl Into<String>) -> Self;
630
631 fn completion(
633 &self,
634 request: CompletionRequest,
635 ) -> impl std::future::Future<
636 Output = Result<CompletionResponse<Self::Response>, CompletionError>,
637 > + WasmCompatSend;
638
639 fn stream(
640 &self,
641 request: CompletionRequest,
642 ) -> impl std::future::Future<
643 Output = Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError>,
644 > + WasmCompatSend;
645
646 fn completion_request(&self, prompt: impl Into<Message>) -> CompletionRequestBuilder<Self> {
648 CompletionRequestBuilder::new(self.clone(), prompt)
649 }
650
651 fn composes_native_output_with_tools(&self) -> bool {
662 false
663 }
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct CompletionRequest {
669 pub model: Option<String>,
671 pub preamble: Option<String>,
676 pub chat_history: OneOrMany<Message>,
679 pub documents: Vec<Document>,
681 pub tools: Vec<ToolDefinition>,
683 pub temperature: Option<f64>,
685 pub max_tokens: Option<u64>,
687 pub tool_choice: Option<ToolChoice>,
689 pub additional_params: Option<serde_json::Value>,
691 pub output_schema: Option<schemars::Schema>,
694}
695
696impl CompletionRequest {
697 pub fn output_schema_name(&self) -> Option<String> {
700 self.output_schema.as_ref().map(|schema| {
701 schema
702 .as_object()
703 .and_then(|o| o.get("title"))
704 .and_then(|v| v.as_str())
705 .unwrap_or("response_schema")
706 .to_string()
707 })
708 }
709
710 pub fn normalized_documents(&self) -> Option<Message> {
714 Self::normalized_documents_from(&self.documents)
715 }
716
717 fn normalized_documents_from(documents: &[Document]) -> Option<Message> {
718 if documents.is_empty() {
719 return None;
720 }
721
722 let messages = documents
725 .iter()
726 .map(|doc| {
727 UserContent::document(
728 doc.to_string(),
729 Some(DocumentMediaType::TXT),
732 )
733 })
734 .collect::<Vec<_>>();
735
736 OneOrMany::from_iter_optional(messages).map(|content| Message::User { content })
737 }
738
739 pub(crate) fn chat_history_with_documents(&self) -> Vec<Message> {
740 let mut chat_history = self.chat_history.iter().cloned().collect::<Vec<_>>();
741 if let Some(documents) = self.normalized_documents() {
742 let insert_at = chat_history
743 .iter()
744 .position(|message| !matches!(message, Message::System { .. }))
745 .unwrap_or(chat_history.len());
746 chat_history.insert(insert_at, documents);
747 }
748 chat_history
749 }
750
751 pub fn with_provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
753 self.additional_params =
754 merge_provider_tools_into_additional_params(self.additional_params, vec![tool]);
755 self
756 }
757
758 pub fn with_provider_tools(mut self, tools: Vec<ProviderToolDefinition>) -> Self {
760 self.additional_params =
761 merge_provider_tools_into_additional_params(self.additional_params, tools);
762 self
763 }
764}
765
766fn merge_provider_tools_into_additional_params(
767 additional_params: Option<serde_json::Value>,
768 provider_tools: Vec<ProviderToolDefinition>,
769) -> Option<serde_json::Value> {
770 if provider_tools.is_empty() {
771 return additional_params;
772 }
773
774 let mut provider_tools_json = provider_tools
775 .into_iter()
776 .map(|ProviderToolDefinition { kind, mut config }| {
777 config.insert("type".to_string(), serde_json::Value::String(kind));
779 serde_json::Value::Object(config)
780 })
781 .collect::<Vec<_>>();
782
783 let mut params_map = match additional_params {
784 Some(serde_json::Value::Object(map)) => map,
785 Some(serde_json::Value::Bool(stream)) => {
786 let mut map = serde_json::Map::new();
787 map.insert("stream".to_string(), serde_json::Value::Bool(stream));
788 map
789 }
790 _ => serde_json::Map::new(),
791 };
792
793 let mut merged_tools = match params_map.remove("tools") {
794 Some(serde_json::Value::Array(existing)) => existing,
795 _ => Vec::new(),
796 };
797 merged_tools.append(&mut provider_tools_json);
798 params_map.insert("tools".to_string(), serde_json::Value::Array(merged_tools));
799 Some(serde_json::Value::Object(params_map))
800}
801
802pub struct CompletionRequestBuilder<M: CompletionModel> {
852 model: M,
853 prompt: Message,
854 request_model: Option<String>,
855 preamble: Option<String>,
856 chat_history: Vec<Message>,
857 documents: Vec<Document>,
858 tools: Vec<ToolDefinition>,
859 provider_tools: Vec<ProviderToolDefinition>,
860 temperature: Option<f64>,
861 max_tokens: Option<u64>,
862 tool_choice: Option<ToolChoice>,
863 additional_params: Option<serde_json::Value>,
864 output_schema: Option<schemars::Schema>,
865}
866
867impl<M: CompletionModel> CompletionRequestBuilder<M> {
868 pub fn new(model: M, prompt: impl Into<Message>) -> Self {
869 Self {
870 model,
871 prompt: prompt.into(),
872 request_model: None,
873 preamble: None,
874 chat_history: Vec::new(),
875 documents: Vec::new(),
876 tools: Vec::new(),
877 provider_tools: Vec::new(),
878 temperature: None,
879 max_tokens: None,
880 tool_choice: None,
881 additional_params: None,
882 output_schema: None,
883 }
884 }
885
886 pub fn preamble(mut self, preamble: String) -> Self {
888 self.preamble = Some(preamble);
890 self
891 }
892
893 pub fn model(mut self, model: impl Into<String>) -> Self {
895 self.request_model = Some(model.into());
896 self
897 }
898
899 pub fn model_opt(mut self, model: Option<String>) -> Self {
901 self.request_model = model;
902 self
903 }
904
905 pub fn without_preamble(mut self) -> Self {
906 self.preamble = None;
907 self
908 }
909
910 pub fn message(mut self, message: Message) -> Self {
912 self.chat_history.push(message);
913
914 self
915 }
916
917 pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
919 self.chat_history.extend(messages);
920
921 self
922 }
923
924 pub fn document(mut self, document: Document) -> Self {
926 self.documents.push(document);
927 self
928 }
929
930 pub fn documents(self, documents: impl IntoIterator<Item = Document>) -> Self {
932 documents
933 .into_iter()
934 .fold(self, |builder, doc| builder.document(doc))
935 }
936
937 pub fn tool(mut self, tool: ToolDefinition) -> Self {
939 self.tools.push(tool);
940 self
941 }
942
943 pub fn tools(self, tools: Vec<ToolDefinition>) -> Self {
945 tools
946 .into_iter()
947 .fold(self, |builder, tool| builder.tool(tool))
948 }
949
950 pub fn provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
952 self.provider_tools.push(tool);
953 self
954 }
955
956 pub fn provider_tools(self, tools: Vec<ProviderToolDefinition>) -> Self {
958 tools
959 .into_iter()
960 .fold(self, |builder, tool| builder.provider_tool(tool))
961 }
962
963 pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
969 match self.additional_params {
970 Some(params) => {
971 self.additional_params = Some(json_utils::merge(params, additional_params));
972 }
973 None => {
974 self.additional_params = Some(additional_params);
975 }
976 }
977 self
978 }
979
980 pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
986 self.additional_params = additional_params;
987 self
988 }
989
990 pub fn temperature(mut self, temperature: f64) -> Self {
992 self.temperature = Some(temperature);
993 self
994 }
995
996 pub fn temperature_opt(mut self, temperature: Option<f64>) -> Self {
998 self.temperature = temperature;
999 self
1000 }
1001
1002 pub fn max_tokens(mut self, max_tokens: u64) -> Self {
1005 self.max_tokens = Some(max_tokens);
1006 self
1007 }
1008
1009 pub fn max_tokens_opt(mut self, max_tokens: Option<u64>) -> Self {
1012 self.max_tokens = max_tokens;
1013 self
1014 }
1015
1016 pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1018 self.tool_choice = Some(tool_choice);
1019 self
1020 }
1021
1022 pub fn output_schema(mut self, schema: schemars::Schema) -> Self {
1029 self.output_schema = Some(schema);
1030 self
1031 }
1032
1033 pub fn output_schema_opt(mut self, schema: Option<schemars::Schema>) -> Self {
1039 self.output_schema = schema;
1040 self
1041 }
1042
1043 pub fn build(self) -> CompletionRequest {
1045 let mut chat_history = self.chat_history;
1047 let prompt = self.prompt;
1048 if let Some(preamble) = self.preamble {
1049 chat_history.insert(0, Message::system(preamble));
1050 }
1051
1052 chat_history.push(prompt.clone());
1053
1054 let chat_history =
1055 OneOrMany::from_iter_optional(chat_history).unwrap_or_else(|| OneOrMany::one(prompt));
1056 let additional_params = merge_provider_tools_into_additional_params(
1057 self.additional_params,
1058 self.provider_tools,
1059 );
1060
1061 CompletionRequest {
1062 model: self.request_model,
1063 preamble: None,
1064 chat_history,
1065 documents: self.documents,
1066 tools: self.tools,
1067 temperature: self.temperature,
1068 max_tokens: self.max_tokens,
1069 tool_choice: self.tool_choice,
1070 additional_params,
1071 output_schema: self.output_schema,
1072 }
1073 }
1074
1075 pub async fn send(self) -> Result<CompletionResponse<M::Response>, CompletionError> {
1077 let model = self.model.clone();
1078 model.completion(self.build()).await
1079 }
1080
1081 pub async fn stream<'a>(
1083 self,
1084 ) -> Result<StreamingCompletionResponse<M::StreamingResponse>, CompletionError>
1085 where
1086 <M as CompletionModel>::StreamingResponse: 'a,
1087 Self: 'a,
1088 {
1089 let model = self.model.clone();
1090 model.stream(self.build()).await
1091 }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 #[test]
1097 fn usage_has_values_reflects_the_zero_sentinel() {
1098 use super::Usage;
1099
1100 assert!(!Usage::new().has_values());
1101
1102 let mut usage = Usage::new();
1103 usage.reasoning_tokens = 1;
1104 assert!(usage.has_values());
1105 }
1106
1107 use super::*;
1108 use crate::test_utils::MockCompletionModel;
1109
1110 fn test_document(id: &str, text: &str) -> Document {
1111 Document {
1112 id: id.to_string(),
1113 text: text.to_string(),
1114 additional_props: HashMap::new(),
1115 }
1116 }
1117
1118 fn is_document_message(message: &Message, expected_id: &str) -> bool {
1119 let Message::User { content } = message else {
1120 return false;
1121 };
1122
1123 content.iter().any(|content| {
1124 matches!(
1125 content,
1126 UserContent::Document(document)
1127 if document.data.to_string().contains(&format!("<file id: {expected_id}>"))
1128 )
1129 })
1130 }
1131
1132 #[test]
1133 fn test_document_display_without_metadata() {
1134 let doc = Document {
1135 id: "123".to_string(),
1136 text: "This is a test document.".to_string(),
1137 additional_props: HashMap::new(),
1138 };
1139
1140 let expected = "<file id: 123>\nThis is a test document.\n</file>\n";
1141 assert_eq!(format!("{doc}"), expected);
1142 }
1143
1144 #[test]
1145 fn test_document_display_with_metadata() {
1146 let mut additional_props = HashMap::new();
1147 additional_props.insert("author".to_string(), "John Doe".to_string());
1148 additional_props.insert("length".to_string(), "42".to_string());
1149
1150 let doc = Document {
1151 id: "123".to_string(),
1152 text: "This is a test document.".to_string(),
1153 additional_props,
1154 };
1155
1156 let expected = concat!(
1157 "<file id: 123>\n",
1158 "<metadata author: \"John Doe\" length: \"42\" />\n",
1159 "This is a test document.\n",
1160 "</file>\n"
1161 );
1162 assert_eq!(format!("{doc}"), expected);
1163 }
1164
1165 #[test]
1166 fn test_normalize_documents_with_documents() {
1167 let doc1 = Document {
1168 id: "doc1".to_string(),
1169 text: "Document 1 text.".to_string(),
1170 additional_props: HashMap::new(),
1171 };
1172
1173 let doc2 = Document {
1174 id: "doc2".to_string(),
1175 text: "Document 2 text.".to_string(),
1176 additional_props: HashMap::new(),
1177 };
1178
1179 let request = CompletionRequest {
1180 model: None,
1181 preamble: None,
1182 chat_history: OneOrMany::one("What is the capital of France?".into()),
1183 documents: vec![doc1, doc2],
1184 tools: Vec::new(),
1185 temperature: None,
1186 max_tokens: None,
1187 tool_choice: None,
1188 additional_params: None,
1189 output_schema: None,
1190 };
1191
1192 let expected = Message::User {
1193 content: OneOrMany::many(vec![
1194 UserContent::document(
1195 "<file id: doc1>\nDocument 1 text.\n</file>\n".to_string(),
1196 Some(DocumentMediaType::TXT),
1197 ),
1198 UserContent::document(
1199 "<file id: doc2>\nDocument 2 text.\n</file>\n".to_string(),
1200 Some(DocumentMediaType::TXT),
1201 ),
1202 ])
1203 .expect("There will be at least one document"),
1204 };
1205
1206 assert_eq!(request.normalized_documents(), Some(expected));
1207 }
1208
1209 #[test]
1210 fn test_normalize_documents_without_documents() {
1211 let request = CompletionRequest {
1212 model: None,
1213 preamble: None,
1214 chat_history: OneOrMany::one("What is the capital of France?".into()),
1215 documents: Vec::new(),
1216 tools: Vec::new(),
1217 temperature: None,
1218 max_tokens: None,
1219 tool_choice: None,
1220 additional_params: None,
1221 output_schema: None,
1222 };
1223
1224 assert_eq!(request.normalized_documents(), None);
1225 }
1226
1227 #[test]
1228 fn preamble_builder_funnels_to_system_message() {
1229 let request =
1230 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1231 .preamble("System prompt".to_string())
1232 .message(Message::user("History"))
1233 .build();
1234
1235 assert_eq!(request.preamble, None);
1236
1237 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1238 assert_eq!(history.len(), 3);
1239 assert!(matches!(
1240 &history[0],
1241 Message::System { content } if content == "System prompt"
1242 ));
1243 assert!(matches!(&history[1], Message::User { .. }));
1244 assert!(matches!(&history[2], Message::User { .. }));
1245 }
1246
1247 #[test]
1248 fn without_preamble_removes_legacy_preamble_injection() {
1249 let request =
1250 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1251 .preamble("System prompt".to_string())
1252 .without_preamble()
1253 .build();
1254
1255 assert_eq!(request.preamble, None);
1256 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1257 assert_eq!(history.len(), 1);
1258 assert!(matches!(&history[0], Message::User { .. }));
1259 }
1260
1261 #[test]
1262 fn build_places_documents_after_preamble_system_message() {
1263 let request =
1264 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1265 .preamble("System prompt".to_string())
1266 .document(test_document("doc1", "Document text."))
1267 .build();
1268
1269 assert_eq!(request.documents.len(), 1);
1270
1271 let history = request.chat_history_with_documents();
1272 let history = history.iter().collect::<Vec<_>>();
1273 assert_eq!(history.len(), 3);
1274 assert!(matches!(
1275 history[0],
1276 Message::System { content } if content == "System prompt"
1277 ));
1278 assert!(is_document_message(history[1], "doc1"));
1279 assert!(matches!(history[2], Message::User { .. }));
1280 }
1281
1282 #[test]
1283 fn build_places_documents_after_leading_system_messages_before_prior_history() {
1284 let request =
1285 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1286 .message(Message::system("System one"))
1287 .message(Message::system("System two"))
1288 .message(Message::user("Earlier user turn"))
1289 .message(Message::assistant("Earlier assistant turn"))
1290 .document(test_document("doc1", "Document text."))
1291 .build();
1292
1293 let history = request.chat_history_with_documents();
1294 let history = history.iter().collect::<Vec<_>>();
1295 assert_eq!(history.len(), 6);
1296 assert!(matches!(
1297 history[0],
1298 Message::System { content } if content == "System one"
1299 ));
1300 assert!(matches!(
1301 history[1],
1302 Message::System { content } if content == "System two"
1303 ));
1304 assert!(is_document_message(history[2], "doc1"));
1305 assert!(matches!(history[3], Message::User { .. }));
1306 assert!(matches!(history[4], Message::Assistant { .. }));
1307 assert!(matches!(history[5], Message::User { .. }));
1308 }
1309
1310 #[test]
1311 fn build_without_documents_keeps_message_order_unchanged() {
1312 let request =
1313 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1314 .message(Message::system("System prompt"))
1315 .message(Message::user("Earlier user turn"))
1316 .build();
1317
1318 let history = request.chat_history.iter().collect::<Vec<_>>();
1319 assert_eq!(history.len(), 3);
1320 assert!(matches!(
1321 history[0],
1322 Message::System { content } if content == "System prompt"
1323 ));
1324 assert!(matches!(history[1], Message::User { .. }));
1325 assert!(matches!(history[2], Message::User { .. }));
1326 }
1327
1328 #[test]
1329 fn chat_history_with_documents_places_documents_after_leading_system_messages() {
1330 let request = CompletionRequest {
1331 model: None,
1332 preamble: None,
1333 chat_history: OneOrMany::many(vec![
1334 Message::system("System prompt"),
1335 Message::assistant("Earlier assistant turn"),
1336 Message::user("Earlier user turn"),
1337 Message::user("Prompt"),
1338 ])
1339 .unwrap(),
1340 documents: vec![test_document("doc1", "Document text.")],
1341 tools: Vec::new(),
1342 temperature: None,
1343 max_tokens: None,
1344 tool_choice: None,
1345 additional_params: None,
1346 output_schema: None,
1347 };
1348
1349 assert_eq!(request.documents.len(), 1);
1350
1351 let history = request.chat_history_with_documents();
1352 let history = history.iter().collect::<Vec<_>>();
1353 assert_eq!(history.len(), 5);
1354 assert!(matches!(history[0], Message::System { .. }));
1355 assert!(is_document_message(history[1], "doc1"));
1356 assert!(matches!(history[2], Message::Assistant { .. }));
1357 assert!(matches!(history[3], Message::User { .. }));
1358 assert!(matches!(history[4], Message::User { .. }));
1359 }
1360
1361 #[test]
1362 fn chat_history_with_documents_places_documents_before_mid_conversation_system_messages() {
1363 let request = CompletionRequest {
1364 model: None,
1365 preamble: None,
1366 chat_history: OneOrMany::many(vec![
1367 Message::system("Leading system prompt"),
1368 Message::assistant("Earlier assistant turn"),
1369 Message::system("Mid-conversation instruction"),
1370 Message::user("Prompt"),
1371 ])
1372 .unwrap(),
1373 documents: vec![test_document("doc1", "Document text.")],
1374 tools: Vec::new(),
1375 temperature: None,
1376 max_tokens: None,
1377 tool_choice: None,
1378 additional_params: None,
1379 output_schema: None,
1380 };
1381
1382 let history = request.chat_history_with_documents();
1383 let history = history.iter().collect::<Vec<_>>();
1384 assert_eq!(history.len(), 5);
1385 assert!(matches!(
1386 history[0],
1387 Message::System { content } if content == "Leading system prompt"
1388 ));
1389 assert!(is_document_message(history[1], "doc1"));
1390 assert!(matches!(history[2], Message::Assistant { .. }));
1391 assert!(matches!(
1392 history[3],
1393 Message::System { content } if content == "Mid-conversation instruction"
1394 ));
1395 assert!(matches!(history[4], Message::User { .. }));
1396 }
1397
1398 #[test]
1399 fn chat_history_with_documents_does_not_duplicate_documents() {
1400 let request = CompletionRequest {
1401 model: None,
1402 preamble: None,
1403 chat_history: OneOrMany::many(vec![
1404 Message::system("System prompt"),
1405 Message::user("Earlier user turn"),
1406 Message::assistant("Earlier assistant turn"),
1407 Message::user("Prompt"),
1408 ])
1409 .unwrap(),
1410 documents: vec![test_document("doc1", "Document text.")],
1411 tools: Vec::new(),
1412 temperature: None,
1413 max_tokens: None,
1414 tool_choice: None,
1415 additional_params: None,
1416 output_schema: None,
1417 };
1418
1419 let history = request.chat_history_with_documents();
1420 let document_messages = history
1421 .iter()
1422 .filter(|message| is_document_message(message, "doc1"))
1423 .count();
1424 assert_eq!(document_messages, 1);
1425 }
1426
1427 #[test]
1428 fn completion_error_provider_response_helpers_with_preserved_json_body() {
1429 let body = r#"{"error":{"code":"rate_limit","message":"slow down"}}"#;
1430 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1431 status: None,
1432 body: body.to_string(),
1433 });
1434
1435 assert_eq!(error.provider_response_body(), Some(body));
1436 assert_eq!(error.provider_response_status(), None);
1437 assert_eq!(
1438 error
1439 .provider_response_json()
1440 .expect("fixture body should parse as valid JSON"),
1441 Some(serde_json::json!({
1442 "error": {
1443 "code": "rate_limit",
1444 "message": "slow down"
1445 }
1446 }))
1447 );
1448 }
1449
1450 #[test]
1451 fn completion_error_provider_response_helpers_with_preserved_status() {
1452 let body = r#"{"error":{"message":"too many requests"}}"#;
1453 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1454 status: Some(http::StatusCode::TOO_MANY_REQUESTS),
1455 body: body.to_string(),
1456 });
1457
1458 assert_eq!(error.provider_response_body(), Some(body));
1459 assert_eq!(
1460 error.provider_response_status(),
1461 Some(http::StatusCode::TOO_MANY_REQUESTS)
1462 );
1463 }
1464
1465 #[test]
1466 fn completion_error_provider_response_helpers_with_preserved_plain_text_body() {
1467 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1468 status: None,
1469 body: "provider exploded".to_string(),
1470 });
1471
1472 assert_eq!(error.provider_response_body(), Some("provider exploded"));
1473 assert_eq!(error.provider_response_status(), None);
1474 assert!(error.provider_response_json().is_err());
1475 }
1476
1477 #[test]
1478 fn completion_error_provider_error_is_not_a_provider_response() {
1479 let error = CompletionError::ProviderError("stream transport failed".to_string());
1482
1483 assert_eq!(error.provider_response_body(), None);
1484 assert_eq!(error.provider_response_status(), None);
1485 assert_eq!(
1486 error
1487 .provider_response_json()
1488 .expect("no body is not an error"),
1489 None
1490 );
1491 }
1492
1493 #[test]
1494 fn completion_error_provider_response_helpers_with_http_non_success_body_and_status() {
1495 let body = r#"{"error":{"type":"invalid_request","message":"bad request"}}"#;
1496 let error = CompletionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
1497 http::StatusCode::BAD_REQUEST,
1498 body.to_string(),
1499 ));
1500
1501 assert_eq!(error.provider_response_body(), Some(body));
1502 assert_eq!(
1503 error.provider_response_status(),
1504 Some(http::StatusCode::BAD_REQUEST)
1505 );
1506 assert_eq!(
1507 error.provider_response_json().expect("valid JSON body"),
1508 Some(serde_json::json!({
1509 "error": {
1510 "type": "invalid_request",
1511 "message": "bad request"
1512 }
1513 }))
1514 );
1515 }
1516
1517 #[test]
1518 fn completion_error_provider_response_helpers_with_unrelated_variant() {
1519 let error = CompletionError::ResponseError("failed to parse provider response".to_string());
1520
1521 assert_eq!(error.provider_response_body(), None);
1522 assert_eq!(error.provider_response_status(), None);
1523 assert_eq!(
1524 error
1525 .provider_response_json()
1526 .expect("no body is not an error"),
1527 None
1528 );
1529 }
1530
1531 #[test]
1532 fn prompt_error_provider_response_helpers_forward_wrapped_completion_error() {
1533 let body = r#"{"error":{"code":"invalid_request","message":"bad input"}}"#;
1534 let error = PromptError::CompletionError(CompletionError::ProviderResponse(
1535 provider_response::ProviderResponseError {
1536 status: None,
1537 body: body.to_string(),
1538 },
1539 ));
1540
1541 assert_eq!(error.provider_response_body(), Some(body));
1542 assert_eq!(error.provider_response_status(), None);
1543 assert_eq!(
1544 error.provider_response_json().expect("valid JSON body"),
1545 Some(serde_json::json!({
1546 "error": {
1547 "code": "invalid_request",
1548 "message": "bad input"
1549 }
1550 }))
1551 );
1552 }
1553
1554 #[test]
1555 fn prompt_error_provider_response_helpers_forward_http_status_and_body() {
1556 let body = r#"{"error":{"message":"unauthorized"}}"#;
1557 let error = PromptError::CompletionError(CompletionError::HttpError(
1558 http_client::Error::InvalidStatusCodeWithMessage(
1559 http::StatusCode::UNAUTHORIZED,
1560 body.to_string(),
1561 ),
1562 ));
1563
1564 assert_eq!(error.provider_response_body(), Some(body));
1565 assert_eq!(
1566 error.provider_response_status(),
1567 Some(http::StatusCode::UNAUTHORIZED)
1568 );
1569 assert_eq!(
1570 error.provider_response_json().expect("valid JSON body"),
1571 Some(serde_json::json!({
1572 "error": { "message": "unauthorized" }
1573 }))
1574 );
1575 }
1576
1577 #[test]
1578 fn prompt_error_provider_response_helpers_return_none_for_unrelated_variant() {
1579 let error = PromptError::PromptCancelled {
1580 chat_history: vec![Message::user("hi")],
1581 reason: "cancelled".to_string(),
1582 };
1583
1584 assert_eq!(error.provider_response_body(), None);
1585 assert_eq!(error.provider_response_status(), None);
1586 assert_eq!(
1587 error
1588 .provider_response_json()
1589 .expect("no body is not an error"),
1590 None
1591 );
1592 }
1593
1594 #[test]
1595 fn structured_output_error_provider_response_helpers_forward_prompt_error() {
1596 let body = r#"{"error":{"message":"bad input"}}"#;
1597 let error = StructuredOutputError::PromptError(Box::new(PromptError::CompletionError(
1598 CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1599 status: Some(http::StatusCode::BAD_REQUEST),
1600 body: body.to_string(),
1601 }),
1602 )));
1603
1604 assert_eq!(error.provider_response_body(), Some(body));
1605 assert_eq!(
1606 error.provider_response_status(),
1607 Some(http::StatusCode::BAD_REQUEST)
1608 );
1609 }
1610
1611 #[test]
1612 fn provider_response_json_returns_none_for_empty_preserved_body() {
1613 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1614 status: None,
1615 body: String::new(),
1616 });
1617
1618 assert_eq!(error.provider_response_body(), Some(""));
1619 assert_eq!(
1620 error
1621 .provider_response_json()
1622 .expect("empty body is not a JSON parse error"),
1623 None
1624 );
1625 }
1626}