1use super::message::{AssistantContent, DocumentMediaType};
36use crate::message::ToolChoice;
37use crate::provider_response;
38use crate::streaming::StreamingCompletionResponse;
39use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
40use crate::{OneOrMany, http_client};
41use crate::{
42 json_utils,
43 message::{Message, UserContent},
44};
45
46use serde::de::DeserializeOwned;
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use std::ops::{Add, AddAssign};
50use thiserror::Error;
51
52#[derive(Debug, Error)]
83#[non_exhaustive]
84pub enum CompletionError {
85 #[error("HttpError: {0}")]
87 HttpError(#[from] http_client::Error),
88
89 #[error("JsonError: {0}")]
91 JsonError(#[from] serde_json::Error),
92
93 #[error("UrlError: {0}")]
95 UrlError(#[from] url::ParseError),
96
97 #[cfg(not(target_family = "wasm"))]
98 #[error("RequestError: {0}")]
100 RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
101
102 #[cfg(target_family = "wasm")]
103 #[error("RequestError: {0}")]
105 RequestError(#[from] Box<dyn std::error::Error + 'static>),
106
107 #[error("ResponseError: {0}")]
109 ResponseError(String),
110
111 #[error("ProviderError: {0}")]
113 ProviderError(String),
114
115 #[error("ProviderResponseError: {0}")]
117 ProviderResponse(provider_response::ProviderResponseError),
118}
119
120crate::provider_response::impl_provider_response_helpers!(CompletionError);
121
122impl CompletionError {
123 pub(crate) fn from_stream_transport(error: http_client::Error) -> Self {
129 if error.non_success_status().is_some() {
130 Self::HttpError(error)
131 } else {
132 Self::ProviderError(error.to_string())
133 }
134 }
135}
136
137#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
138pub struct Document {
139 pub id: String,
141 pub text: String,
143 #[serde(flatten)]
145 pub additional_props: HashMap<String, String>,
146}
147
148impl std::fmt::Display for Document {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 write!(
151 f,
152 concat!("<file id: {}>\n", "{}\n", "</file>\n"),
153 self.id,
154 if self.additional_props.is_empty() {
155 self.text.clone()
156 } else {
157 let mut sorted_props = self.additional_props.iter().collect::<Vec<_>>();
158 sorted_props.sort_by(|a, b| a.0.cmp(b.0));
159 let metadata = sorted_props
160 .iter()
161 .map(|(k, v)| format!("{k}: {v:?}"))
162 .collect::<Vec<_>>()
163 .join(" ");
164 format!("<metadata {} />\n{}", metadata, self.text)
165 }
166 )
167 }
168}
169
170#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
171pub struct ToolDefinition {
172 pub name: String,
174 pub description: String,
176 pub parameters: serde_json::Value,
178}
179
180#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
185pub struct ProviderToolDefinition {
186 #[serde(rename = "type")]
188 pub kind: String,
189 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
191 pub config: serde_json::Map<String, serde_json::Value>,
192}
193
194impl ProviderToolDefinition {
195 pub fn new(kind: impl Into<String>) -> Self {
197 Self {
198 kind: kind.into(),
199 config: serde_json::Map::new(),
200 }
201 }
202
203 pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
205 self.config.insert(key.into(), value);
206 self
207 }
208}
209
210#[derive(Debug)]
213pub struct CompletionResponse<T> {
214 pub choice: OneOrMany<AssistantContent>,
217 pub usage: Usage,
219 pub raw_response: T,
221 pub message_id: Option<String>,
224}
225
226pub trait GetTokenUsage {
230 fn token_usage(&self) -> crate::completion::Usage;
234}
235
236impl GetTokenUsage for () {
237 fn token_usage(&self) -> crate::completion::Usage {
238 crate::completion::Usage::new()
239 }
240}
241
242impl<T> GetTokenUsage for Option<T>
243where
244 T: GetTokenUsage,
245{
246 fn token_usage(&self) -> crate::completion::Usage {
247 if let Some(usage) = self {
248 usage.token_usage()
249 } else {
250 crate::completion::Usage::new()
251 }
252 }
253}
254
255#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
258pub struct Usage {
259 pub input_tokens: u64,
261 pub output_tokens: u64,
263 pub total_tokens: u64,
265 pub cached_input_tokens: u64,
267 pub cache_creation_input_tokens: u64,
269 #[serde(default)]
271 pub tool_use_prompt_tokens: u64,
272 pub reasoning_tokens: u64,
275}
276
277impl Usage {
278 pub fn new() -> Self {
280 Self {
281 input_tokens: 0,
282 output_tokens: 0,
283 total_tokens: 0,
284 cached_input_tokens: 0,
285 cache_creation_input_tokens: 0,
286 tool_use_prompt_tokens: 0,
287 reasoning_tokens: 0,
288 }
289 }
290
291 pub fn has_values(&self) -> bool {
296 *self != Self::new()
297 }
298}
299
300impl Default for Usage {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306impl Add for Usage {
307 type Output = Self;
308
309 fn add(self, other: Self) -> Self::Output {
310 Self {
311 input_tokens: self.input_tokens + other.input_tokens,
312 output_tokens: self.output_tokens + other.output_tokens,
313 total_tokens: self.total_tokens + other.total_tokens,
314 cached_input_tokens: self.cached_input_tokens + other.cached_input_tokens,
315 cache_creation_input_tokens: self.cache_creation_input_tokens
316 + other.cache_creation_input_tokens,
317 tool_use_prompt_tokens: self.tool_use_prompt_tokens + other.tool_use_prompt_tokens,
318 reasoning_tokens: self.reasoning_tokens + other.reasoning_tokens,
319 }
320 }
321}
322
323impl AddAssign for Usage {
324 fn add_assign(&mut self, other: Self) {
325 self.input_tokens += other.input_tokens;
326 self.output_tokens += other.output_tokens;
327 self.total_tokens += other.total_tokens;
328 self.cached_input_tokens += other.cached_input_tokens;
329 self.cache_creation_input_tokens += other.cache_creation_input_tokens;
330 self.tool_use_prompt_tokens += other.tool_use_prompt_tokens;
331 self.reasoning_tokens += other.reasoning_tokens;
332 }
333}
334
335pub trait CompletionModel: Clone + WasmCompatSend + WasmCompatSync {
339 type Response: WasmCompatSend + WasmCompatSync + Serialize + DeserializeOwned;
341 type StreamingResponse: Clone
343 + Unpin
344 + WasmCompatSend
345 + WasmCompatSync
346 + Serialize
347 + DeserializeOwned
348 + GetTokenUsage;
349
350 type Client;
352
353 fn make(client: &Self::Client, model: impl Into<String>) -> Self;
355
356 fn completion(
358 &self,
359 request: CompletionRequest,
360 ) -> impl std::future::Future<
361 Output = Result<CompletionResponse<Self::Response>, CompletionError>,
362 > + WasmCompatSend;
363
364 fn stream(
365 &self,
366 request: CompletionRequest,
367 ) -> impl std::future::Future<
368 Output = Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError>,
369 > + WasmCompatSend;
370
371 fn completion_request(&self, prompt: impl Into<Message>) -> CompletionRequestBuilder<Self> {
373 CompletionRequestBuilder::new(self.clone(), prompt)
374 }
375
376 fn composes_native_output_with_tools(&self) -> bool {
386 false
387 }
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct CompletionRequest {
393 pub model: Option<String>,
395 pub preamble: Option<String>,
400 pub chat_history: OneOrMany<Message>,
403 pub documents: Vec<Document>,
405 pub tools: Vec<ToolDefinition>,
407 pub temperature: Option<f64>,
409 pub max_tokens: Option<u64>,
411 pub tool_choice: Option<ToolChoice>,
413 pub additional_params: Option<serde_json::Value>,
415 pub output_schema: Option<schemars::Schema>,
418 #[serde(skip)]
436 pub record_telemetry_content: bool,
437}
438
439impl CompletionRequest {
440 pub fn output_schema_name(&self) -> Option<String> {
443 self.output_schema.as_ref().map(|schema| {
444 schema
445 .as_object()
446 .and_then(|o| o.get("title"))
447 .and_then(|v| v.as_str())
448 .unwrap_or("response_schema")
449 .to_string()
450 })
451 }
452
453 pub fn normalized_documents(&self) -> Option<Message> {
457 Self::normalized_documents_from(&self.documents)
458 }
459
460 fn normalized_documents_from(documents: &[Document]) -> Option<Message> {
461 if documents.is_empty() {
462 return None;
463 }
464
465 let messages = documents
468 .iter()
469 .map(|doc| {
470 UserContent::document(
471 doc.to_string(),
472 Some(DocumentMediaType::TXT),
475 )
476 })
477 .collect::<Vec<_>>();
478
479 OneOrMany::from_iter_optional(messages).map(|content| Message::User { content })
480 }
481
482 pub(crate) fn chat_history_with_documents(&self) -> Vec<Message> {
483 let mut chat_history = self.chat_history.iter().cloned().collect::<Vec<_>>();
484 if let Some(documents) = self.normalized_documents() {
485 let insert_at = chat_history
486 .iter()
487 .position(|message| !matches!(message, Message::System { .. }))
488 .unwrap_or(chat_history.len());
489 chat_history.insert(insert_at, documents);
490 }
491 chat_history
492 }
493
494 pub fn with_provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
496 self.additional_params =
497 merge_provider_tools_into_additional_params(self.additional_params, vec![tool]);
498 self
499 }
500
501 pub fn with_provider_tools(mut self, tools: Vec<ProviderToolDefinition>) -> Self {
503 self.additional_params =
504 merge_provider_tools_into_additional_params(self.additional_params, tools);
505 self
506 }
507}
508
509fn merge_provider_tools_into_additional_params(
510 additional_params: Option<serde_json::Value>,
511 provider_tools: Vec<ProviderToolDefinition>,
512) -> Option<serde_json::Value> {
513 if provider_tools.is_empty() {
514 return additional_params;
515 }
516
517 let mut provider_tools_json = provider_tools
518 .into_iter()
519 .map(|ProviderToolDefinition { kind, mut config }| {
520 config.insert("type".to_string(), serde_json::Value::String(kind));
522 serde_json::Value::Object(config)
523 })
524 .collect::<Vec<_>>();
525
526 let mut params_map = match additional_params {
527 Some(serde_json::Value::Object(map)) => map,
528 Some(serde_json::Value::Bool(stream)) => {
529 let mut map = serde_json::Map::new();
530 map.insert("stream".to_string(), serde_json::Value::Bool(stream));
531 map
532 }
533 _ => serde_json::Map::new(),
534 };
535
536 let mut merged_tools = match params_map.remove("tools") {
537 Some(serde_json::Value::Array(existing)) => existing,
538 _ => Vec::new(),
539 };
540 merged_tools.append(&mut provider_tools_json);
541 params_map.insert("tools".to_string(), serde_json::Value::Array(merged_tools));
542 Some(serde_json::Value::Object(params_map))
543}
544
545pub struct CompletionRequestBuilder<M: CompletionModel> {
595 model: M,
596 prompt: Message,
597 request_model: Option<String>,
598 preamble: Option<String>,
599 chat_history: Vec<Message>,
600 documents: Vec<Document>,
601 tools: Vec<ToolDefinition>,
602 provider_tools: Vec<ProviderToolDefinition>,
603 temperature: Option<f64>,
604 max_tokens: Option<u64>,
605 tool_choice: Option<ToolChoice>,
606 additional_params: Option<serde_json::Value>,
607 output_schema: Option<schemars::Schema>,
608 record_telemetry_content: bool,
609}
610
611impl<M: CompletionModel> CompletionRequestBuilder<M> {
612 pub fn new(model: M, prompt: impl Into<Message>) -> Self {
613 Self {
614 model,
615 prompt: prompt.into(),
616 request_model: None,
617 preamble: None,
618 chat_history: Vec::new(),
619 documents: Vec::new(),
620 tools: Vec::new(),
621 provider_tools: Vec::new(),
622 temperature: None,
623 max_tokens: None,
624 tool_choice: None,
625 additional_params: None,
626 output_schema: None,
627 record_telemetry_content: false,
628 }
629 }
630
631 pub fn preamble(mut self, preamble: String) -> Self {
633 self.preamble = Some(preamble);
635 self
636 }
637
638 pub fn model(mut self, model: impl Into<String>) -> Self {
640 self.request_model = Some(model.into());
641 self
642 }
643
644 pub fn model_opt(mut self, model: Option<String>) -> Self {
646 self.request_model = model;
647 self
648 }
649
650 pub fn without_preamble(mut self) -> Self {
651 self.preamble = None;
652 self
653 }
654
655 pub fn message(mut self, message: Message) -> Self {
657 self.chat_history.push(message);
658
659 self
660 }
661
662 pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
664 self.chat_history.extend(messages);
665
666 self
667 }
668
669 pub fn document(mut self, document: Document) -> Self {
671 self.documents.push(document);
672 self
673 }
674
675 pub fn documents(self, documents: impl IntoIterator<Item = Document>) -> Self {
677 documents
678 .into_iter()
679 .fold(self, |builder, doc| builder.document(doc))
680 }
681
682 pub fn tool(mut self, tool: ToolDefinition) -> Self {
684 self.tools.push(tool);
685 self
686 }
687
688 pub fn tools(self, tools: Vec<ToolDefinition>) -> Self {
690 tools
691 .into_iter()
692 .fold(self, |builder, tool| builder.tool(tool))
693 }
694
695 pub fn provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
697 self.provider_tools.push(tool);
698 self
699 }
700
701 pub fn provider_tools(self, tools: Vec<ProviderToolDefinition>) -> Self {
703 tools
704 .into_iter()
705 .fold(self, |builder, tool| builder.provider_tool(tool))
706 }
707
708 pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
714 match self.additional_params {
715 Some(params) => {
716 self.additional_params = Some(json_utils::merge(params, additional_params));
717 }
718 None => {
719 self.additional_params = Some(additional_params);
720 }
721 }
722 self
723 }
724
725 pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
731 self.additional_params = additional_params;
732 self
733 }
734
735 pub fn temperature(mut self, temperature: f64) -> Self {
737 self.temperature = Some(temperature);
738 self
739 }
740
741 pub fn temperature_opt(mut self, temperature: Option<f64>) -> Self {
743 self.temperature = temperature;
744 self
745 }
746
747 pub fn max_tokens(mut self, max_tokens: u64) -> Self {
750 self.max_tokens = Some(max_tokens);
751 self
752 }
753
754 pub fn max_tokens_opt(mut self, max_tokens: Option<u64>) -> Self {
757 self.max_tokens = max_tokens;
758 self
759 }
760
761 pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
763 self.tool_choice = Some(tool_choice);
764 self
765 }
766
767 pub fn output_schema(mut self, schema: schemars::Schema) -> Self {
774 self.output_schema = Some(schema);
775 self
776 }
777
778 pub fn output_schema_opt(mut self, schema: Option<schemars::Schema>) -> Self {
784 self.output_schema = schema;
785 self
786 }
787
788 pub fn record_content_telemetry(mut self, enabled: bool) -> Self {
803 self.record_telemetry_content = enabled;
804 self
805 }
806
807 pub fn messages_for_telemetry(&self) -> Vec<Message> {
809 let mut chat_history = self.chat_history.clone();
810 if let Some(preamble) = &self.preamble {
811 chat_history.insert(0, Message::system(preamble.clone()));
812 }
813 chat_history.push(self.prompt.clone());
814
815 if let Some(documents) = CompletionRequest::normalized_documents_from(&self.documents) {
816 let insert_at = chat_history
817 .iter()
818 .position(|message| !matches!(message, Message::System { .. }))
819 .unwrap_or(chat_history.len());
820 chat_history.insert(insert_at, documents);
821 }
822
823 chat_history
824 }
825
826 pub fn build(self) -> CompletionRequest {
828 let mut chat_history = self.chat_history;
830 let prompt = self.prompt;
831 if let Some(preamble) = self.preamble {
832 chat_history.insert(0, Message::system(preamble));
833 }
834
835 chat_history.push(prompt.clone());
836
837 let chat_history =
838 OneOrMany::from_iter_optional(chat_history).unwrap_or_else(|| OneOrMany::one(prompt));
839 let additional_params = merge_provider_tools_into_additional_params(
840 self.additional_params,
841 self.provider_tools,
842 );
843
844 CompletionRequest {
845 model: self.request_model,
846 preamble: None,
847 chat_history,
848 documents: self.documents,
849 tools: self.tools,
850 temperature: self.temperature,
851 max_tokens: self.max_tokens,
852 tool_choice: self.tool_choice,
853 additional_params,
854 output_schema: self.output_schema,
855 record_telemetry_content: self.record_telemetry_content,
856 }
857 }
858
859 pub async fn send(self) -> Result<CompletionResponse<M::Response>, CompletionError> {
861 let model = self.model.clone();
862 model.completion(self.build()).await
863 }
864
865 pub async fn stream<'a>(
867 self,
868 ) -> Result<StreamingCompletionResponse<M::StreamingResponse>, CompletionError>
869 where
870 <M as CompletionModel>::StreamingResponse: 'a,
871 Self: 'a,
872 {
873 let model = self.model.clone();
874 model.stream(self.build()).await
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 #[test]
881 fn usage_has_values_reflects_the_zero_sentinel() {
882 use super::Usage;
883
884 assert!(!Usage::new().has_values());
885
886 let mut usage = Usage::new();
887 usage.reasoning_tokens = 1;
888 assert!(usage.has_values());
889 }
890
891 use super::*;
892 use crate::test_utils::MockCompletionModel;
893
894 #[test]
895 fn completion_request_content_telemetry_is_opt_in_and_not_serialized() {
896 let default_request =
897 CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
898 .build();
899 assert!(!default_request.record_telemetry_content);
900
901 let default_json = serde_json::to_value(&default_request).expect("serialize request");
902 assert!(
903 default_json.get("record_telemetry_content").is_none(),
904 "safe default should not serialize the telemetry opt-in field"
905 );
906 let default_roundtrip: CompletionRequest =
907 serde_json::from_value(default_json).expect("deserialize default request");
908 assert!(!default_roundtrip.record_telemetry_content);
909
910 let opt_in_request =
911 CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
912 .record_content_telemetry(true)
913 .build();
914 assert!(opt_in_request.record_telemetry_content);
915
916 let opt_in_json = serde_json::to_value(&opt_in_request).expect("serialize opt-in request");
917 assert!(
918 opt_in_json.get("record_telemetry_content").is_none(),
919 "local telemetry policy must not be serialized into provider requests"
920 );
921 let legacy_roundtrip: CompletionRequest =
922 serde_json::from_value(opt_in_json).expect("deserialize legacy request");
923 assert!(
924 !legacy_roundtrip.record_telemetry_content,
925 "missing field should deserialize to the safe default"
926 );
927 }
928
929 fn test_document(id: &str, text: &str) -> Document {
930 Document {
931 id: id.to_string(),
932 text: text.to_string(),
933 additional_props: HashMap::new(),
934 }
935 }
936
937 #[test]
938 fn message_telemetry_includes_normalized_documents() {
939 let builder = CompletionRequestBuilder::new(MockCompletionModel::default(), "prompt")
940 .preamble("system".to_string())
941 .message(Message::user("history"))
942 .document(test_document("doc1", "static context secret"));
943
944 let messages = builder.messages_for_telemetry();
945 assert_eq!(messages.len(), 4);
946 assert!(matches!(messages[0], Message::System { .. }));
947 assert!(is_document_message(&messages[1], "doc1"));
948 assert!(matches!(
949 &messages[2],
950 Message::User { content }
951 if matches!(content.first(), UserContent::Text(text) if text.text == "history")
952 ));
953 assert!(matches!(
954 &messages[3],
955 Message::User { content }
956 if matches!(content.first(), UserContent::Text(text) if text.text == "prompt")
957 ));
958
959 let request = builder.build();
960 assert_eq!(messages, request.chat_history_with_documents());
961 }
962
963 fn is_document_message(message: &Message, expected_id: &str) -> bool {
964 let Message::User { content } = message else {
965 return false;
966 };
967
968 content.iter().any(|content| {
969 matches!(
970 content,
971 UserContent::Document(document)
972 if document.data.to_string().contains(&format!("<file id: {expected_id}>"))
973 )
974 })
975 }
976
977 #[test]
978 fn test_document_display_without_metadata() {
979 let doc = Document {
980 id: "123".to_string(),
981 text: "This is a test document.".to_string(),
982 additional_props: HashMap::new(),
983 };
984
985 let expected = "<file id: 123>\nThis is a test document.\n</file>\n";
986 assert_eq!(format!("{doc}"), expected);
987 }
988
989 #[test]
990 fn test_document_display_with_metadata() {
991 let mut additional_props = HashMap::new();
992 additional_props.insert("author".to_string(), "John Doe".to_string());
993 additional_props.insert("length".to_string(), "42".to_string());
994
995 let doc = Document {
996 id: "123".to_string(),
997 text: "This is a test document.".to_string(),
998 additional_props,
999 };
1000
1001 let expected = concat!(
1002 "<file id: 123>\n",
1003 "<metadata author: \"John Doe\" length: \"42\" />\n",
1004 "This is a test document.\n",
1005 "</file>\n"
1006 );
1007 assert_eq!(format!("{doc}"), expected);
1008 }
1009
1010 #[test]
1011 fn test_normalize_documents_with_documents() {
1012 let doc1 = Document {
1013 id: "doc1".to_string(),
1014 text: "Document 1 text.".to_string(),
1015 additional_props: HashMap::new(),
1016 };
1017
1018 let doc2 = Document {
1019 id: "doc2".to_string(),
1020 text: "Document 2 text.".to_string(),
1021 additional_props: HashMap::new(),
1022 };
1023
1024 let request = CompletionRequest {
1025 model: None,
1026 preamble: None,
1027 chat_history: OneOrMany::one("What is the capital of France?".into()),
1028 documents: vec![doc1, doc2],
1029 tools: Vec::new(),
1030 temperature: None,
1031 max_tokens: None,
1032 tool_choice: None,
1033 additional_params: None,
1034 output_schema: None,
1035 record_telemetry_content: false,
1036 };
1037
1038 let expected = Message::User {
1039 content: OneOrMany::many(vec![
1040 UserContent::document(
1041 "<file id: doc1>\nDocument 1 text.\n</file>\n".to_string(),
1042 Some(DocumentMediaType::TXT),
1043 ),
1044 UserContent::document(
1045 "<file id: doc2>\nDocument 2 text.\n</file>\n".to_string(),
1046 Some(DocumentMediaType::TXT),
1047 ),
1048 ])
1049 .expect("There will be at least one document"),
1050 };
1051
1052 assert_eq!(request.normalized_documents(), Some(expected));
1053 }
1054
1055 #[test]
1056 fn test_normalize_documents_without_documents() {
1057 let request = CompletionRequest {
1058 model: None,
1059 preamble: None,
1060 chat_history: OneOrMany::one("What is the capital of France?".into()),
1061 documents: Vec::new(),
1062 tools: Vec::new(),
1063 temperature: None,
1064 max_tokens: None,
1065 tool_choice: None,
1066 additional_params: None,
1067 output_schema: None,
1068 record_telemetry_content: false,
1069 };
1070
1071 assert_eq!(request.normalized_documents(), None);
1072 }
1073
1074 #[test]
1075 fn preamble_builder_funnels_to_system_message() {
1076 let request =
1077 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1078 .preamble("System prompt".to_string())
1079 .message(Message::user("History"))
1080 .build();
1081
1082 assert_eq!(request.preamble, None);
1083
1084 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1085 assert_eq!(history.len(), 3);
1086 assert!(matches!(
1087 &history[0],
1088 Message::System { content } if content == "System prompt"
1089 ));
1090 assert!(matches!(&history[1], Message::User { .. }));
1091 assert!(matches!(&history[2], Message::User { .. }));
1092 }
1093
1094 #[test]
1095 fn without_preamble_removes_legacy_preamble_injection() {
1096 let request =
1097 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1098 .preamble("System prompt".to_string())
1099 .without_preamble()
1100 .build();
1101
1102 assert_eq!(request.preamble, None);
1103 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1104 assert_eq!(history.len(), 1);
1105 assert!(matches!(&history[0], Message::User { .. }));
1106 }
1107
1108 #[test]
1109 fn build_places_documents_after_preamble_system_message() {
1110 let request =
1111 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1112 .preamble("System prompt".to_string())
1113 .document(test_document("doc1", "Document text."))
1114 .build();
1115
1116 assert_eq!(request.documents.len(), 1);
1117
1118 let history = request.chat_history_with_documents();
1119 let history = history.iter().collect::<Vec<_>>();
1120 assert_eq!(history.len(), 3);
1121 assert!(matches!(
1122 history[0],
1123 Message::System { content } if content == "System prompt"
1124 ));
1125 assert!(is_document_message(history[1], "doc1"));
1126 assert!(matches!(history[2], Message::User { .. }));
1127 }
1128
1129 #[test]
1130 fn build_places_documents_after_leading_system_messages_before_prior_history() {
1131 let request =
1132 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1133 .message(Message::system("System one"))
1134 .message(Message::system("System two"))
1135 .message(Message::user("Earlier user turn"))
1136 .message(Message::assistant("Earlier assistant turn"))
1137 .document(test_document("doc1", "Document text."))
1138 .build();
1139
1140 let history = request.chat_history_with_documents();
1141 let history = history.iter().collect::<Vec<_>>();
1142 assert_eq!(history.len(), 6);
1143 assert!(matches!(
1144 history[0],
1145 Message::System { content } if content == "System one"
1146 ));
1147 assert!(matches!(
1148 history[1],
1149 Message::System { content } if content == "System two"
1150 ));
1151 assert!(is_document_message(history[2], "doc1"));
1152 assert!(matches!(history[3], Message::User { .. }));
1153 assert!(matches!(history[4], Message::Assistant { .. }));
1154 assert!(matches!(history[5], Message::User { .. }));
1155 }
1156
1157 #[test]
1158 fn build_without_documents_keeps_message_order_unchanged() {
1159 let request =
1160 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1161 .message(Message::system("System prompt"))
1162 .message(Message::user("Earlier user turn"))
1163 .build();
1164
1165 let history = request.chat_history.iter().collect::<Vec<_>>();
1166 assert_eq!(history.len(), 3);
1167 assert!(matches!(
1168 history[0],
1169 Message::System { content } if content == "System prompt"
1170 ));
1171 assert!(matches!(history[1], Message::User { .. }));
1172 assert!(matches!(history[2], Message::User { .. }));
1173 }
1174
1175 #[test]
1176 fn chat_history_with_documents_places_documents_after_leading_system_messages() {
1177 let request = CompletionRequest {
1178 model: None,
1179 preamble: None,
1180 chat_history: OneOrMany::many(vec![
1181 Message::system("System prompt"),
1182 Message::assistant("Earlier assistant turn"),
1183 Message::user("Earlier user turn"),
1184 Message::user("Prompt"),
1185 ])
1186 .unwrap(),
1187 documents: vec![test_document("doc1", "Document text.")],
1188 tools: Vec::new(),
1189 temperature: None,
1190 max_tokens: None,
1191 tool_choice: None,
1192 additional_params: None,
1193 output_schema: None,
1194 record_telemetry_content: false,
1195 };
1196
1197 assert_eq!(request.documents.len(), 1);
1198
1199 let history = request.chat_history_with_documents();
1200 let history = history.iter().collect::<Vec<_>>();
1201 assert_eq!(history.len(), 5);
1202 assert!(matches!(history[0], Message::System { .. }));
1203 assert!(is_document_message(history[1], "doc1"));
1204 assert!(matches!(history[2], Message::Assistant { .. }));
1205 assert!(matches!(history[3], Message::User { .. }));
1206 assert!(matches!(history[4], Message::User { .. }));
1207 }
1208
1209 #[test]
1210 fn chat_history_with_documents_places_documents_before_mid_conversation_system_messages() {
1211 let request = CompletionRequest {
1212 model: None,
1213 preamble: None,
1214 chat_history: OneOrMany::many(vec![
1215 Message::system("Leading system prompt"),
1216 Message::assistant("Earlier assistant turn"),
1217 Message::system("Mid-conversation instruction"),
1218 Message::user("Prompt"),
1219 ])
1220 .unwrap(),
1221 documents: vec![test_document("doc1", "Document text.")],
1222 tools: Vec::new(),
1223 temperature: None,
1224 max_tokens: None,
1225 tool_choice: None,
1226 additional_params: None,
1227 output_schema: None,
1228 record_telemetry_content: false,
1229 };
1230
1231 let history = request.chat_history_with_documents();
1232 let history = history.iter().collect::<Vec<_>>();
1233 assert_eq!(history.len(), 5);
1234 assert!(matches!(
1235 history[0],
1236 Message::System { content } if content == "Leading system prompt"
1237 ));
1238 assert!(is_document_message(history[1], "doc1"));
1239 assert!(matches!(history[2], Message::Assistant { .. }));
1240 assert!(matches!(
1241 history[3],
1242 Message::System { content } if content == "Mid-conversation instruction"
1243 ));
1244 assert!(matches!(history[4], Message::User { .. }));
1245 }
1246
1247 #[test]
1248 fn chat_history_with_documents_does_not_duplicate_documents() {
1249 let request = CompletionRequest {
1250 model: None,
1251 preamble: None,
1252 chat_history: OneOrMany::many(vec![
1253 Message::system("System prompt"),
1254 Message::user("Earlier user turn"),
1255 Message::assistant("Earlier assistant turn"),
1256 Message::user("Prompt"),
1257 ])
1258 .unwrap(),
1259 documents: vec![test_document("doc1", "Document text.")],
1260 tools: Vec::new(),
1261 temperature: None,
1262 max_tokens: None,
1263 tool_choice: None,
1264 additional_params: None,
1265 output_schema: None,
1266 record_telemetry_content: false,
1267 };
1268
1269 let history = request.chat_history_with_documents();
1270 let document_messages = history
1271 .iter()
1272 .filter(|message| is_document_message(message, "doc1"))
1273 .count();
1274 assert_eq!(document_messages, 1);
1275 }
1276
1277 #[test]
1278 fn completion_error_provider_response_helpers_with_preserved_json_body() {
1279 let body = r#"{"error":{"code":"rate_limit","message":"slow down"}}"#;
1280 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1281 status: None,
1282 body: body.to_string(),
1283 });
1284
1285 assert_eq!(error.provider_response_body(), Some(body));
1286 assert_eq!(error.provider_response_status(), None);
1287 assert_eq!(
1288 error
1289 .provider_response_json()
1290 .expect("fixture body should parse as valid JSON"),
1291 Some(serde_json::json!({
1292 "error": {
1293 "code": "rate_limit",
1294 "message": "slow down"
1295 }
1296 }))
1297 );
1298 }
1299
1300 #[test]
1301 fn completion_error_provider_response_helpers_with_preserved_status() {
1302 let body = r#"{"error":{"message":"too many requests"}}"#;
1303 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1304 status: Some(http::StatusCode::TOO_MANY_REQUESTS),
1305 body: body.to_string(),
1306 });
1307
1308 assert_eq!(error.provider_response_body(), Some(body));
1309 assert_eq!(
1310 error.provider_response_status(),
1311 Some(http::StatusCode::TOO_MANY_REQUESTS)
1312 );
1313 }
1314
1315 #[test]
1316 fn completion_error_provider_response_helpers_with_preserved_plain_text_body() {
1317 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1318 status: None,
1319 body: "provider exploded".to_string(),
1320 });
1321
1322 assert_eq!(error.provider_response_body(), Some("provider exploded"));
1323 assert_eq!(error.provider_response_status(), None);
1324 assert!(error.provider_response_json().is_err());
1325 }
1326
1327 #[test]
1328 fn completion_error_provider_error_is_not_a_provider_response() {
1329 let error = CompletionError::ProviderError("stream transport failed".to_string());
1332
1333 assert_eq!(error.provider_response_body(), None);
1334 assert_eq!(error.provider_response_status(), None);
1335 assert_eq!(
1336 error
1337 .provider_response_json()
1338 .expect("no body is not an error"),
1339 None
1340 );
1341 }
1342
1343 #[test]
1344 fn completion_error_provider_response_helpers_with_http_non_success_body_and_status() {
1345 let body = r#"{"error":{"type":"invalid_request","message":"bad request"}}"#;
1346 let error = CompletionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
1347 http::StatusCode::BAD_REQUEST,
1348 body.to_string(),
1349 ));
1350
1351 assert_eq!(error.provider_response_body(), Some(body));
1352 assert_eq!(
1353 error.provider_response_status(),
1354 Some(http::StatusCode::BAD_REQUEST)
1355 );
1356 assert_eq!(
1357 error.provider_response_json().expect("valid JSON body"),
1358 Some(serde_json::json!({
1359 "error": {
1360 "type": "invalid_request",
1361 "message": "bad request"
1362 }
1363 }))
1364 );
1365 }
1366
1367 #[test]
1368 fn completion_error_provider_response_helpers_with_unrelated_variant() {
1369 let error = CompletionError::ResponseError("failed to parse provider response".to_string());
1370
1371 assert_eq!(error.provider_response_body(), None);
1372 assert_eq!(error.provider_response_status(), None);
1373 assert_eq!(
1374 error
1375 .provider_response_json()
1376 .expect("no body is not an error"),
1377 None
1378 );
1379 }
1380
1381 #[test]
1382 fn provider_response_json_returns_none_for_empty_preserved_body() {
1383 let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1384 status: None,
1385 body: String::new(),
1386 });
1387
1388 assert_eq!(error.provider_response_body(), Some(""));
1389 assert_eq!(
1390 error
1391 .provider_response_json()
1392 .expect("empty body is not a JSON parse error"),
1393 None
1394 );
1395 }
1396}