1use super::message::{AssistantContent, DocumentMediaType};
36use crate::http_client;
37use crate::message::ToolChoice;
38use crate::provider_response;
39use crate::streaming::StreamingCompletionResponse;
40use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
41use crate::{
42 json_utils,
43 message::{Message, UserContent},
44};
45
46use serde::{Deserialize, Serialize};
47use std::collections::HashMap;
48use std::ops::{Add, AddAssign};
49use thiserror::Error;
50
51#[derive(Debug, Error)]
82pub enum CompletionError {
83 #[error("HttpError: {0}")]
85 HttpError(#[from] http_client::Error),
86
87 #[error("JsonError: {0}")]
89 JsonError(#[from] serde_json::Error),
90
91 #[error("UrlError: {0}")]
93 UrlError(#[from] url::ParseError),
94
95 #[cfg(not(target_family = "wasm"))]
96 #[error("RequestError: {0}")]
98 RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
99
100 #[cfg(target_family = "wasm")]
101 #[error("RequestError: {0}")]
103 RequestError(#[from] Box<dyn std::error::Error + 'static>),
104
105 #[error("ResponseError: {0}")]
107 ResponseError(String),
108
109 #[error("ProviderError: {0}")]
111 ProviderError(String),
112
113 #[error("ProviderResponseError: {0}")]
115 ProviderResponse(provider_response::ProviderResponseError),
116}
117
118crate::provider_response::impl_provider_response_helpers!(CompletionError);
119
120impl CompletionError {
121 pub(crate) fn from_stream_transport(error: http_client::Error) -> Self {
127 if error.non_success_status().is_some() {
128 Self::HttpError(error)
129 } else {
130 Self::ProviderError(error.to_string())
131 }
132 }
133}
134
135#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
136pub struct Document {
137 pub id: String,
139 pub text: String,
141 #[serde(flatten)]
143 pub additional_props: HashMap<String, String>,
144}
145
146impl std::fmt::Display for Document {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(
149 f,
150 concat!("<file id: {}>\n", "{}\n", "</file>\n"),
151 self.id,
152 if self.additional_props.is_empty() {
153 self.text.clone()
154 } else {
155 let mut sorted_props = self.additional_props.iter().collect::<Vec<_>>();
156 sorted_props.sort_by(|a, b| a.0.cmp(b.0));
157 let metadata = sorted_props
158 .iter()
159 .map(|(k, v)| format!("{k}: {v:?}"))
160 .collect::<Vec<_>>()
161 .join(" ");
162 format!("<metadata {} />\n{}", metadata, self.text)
163 }
164 )
165 }
166}
167
168#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
169pub struct ToolDefinition {
170 pub name: String,
172 pub description: String,
174 pub parameters: serde_json::Value,
176}
177
178#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
183pub struct ProviderToolDefinition {
184 #[serde(rename = "type")]
186 pub kind: String,
187 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
189 pub config: serde_json::Map<String, serde_json::Value>,
190}
191
192impl ProviderToolDefinition {
193 pub fn new(kind: impl Into<String>) -> Self {
195 Self {
196 kind: kind.into(),
197 config: serde_json::Map::new(),
198 }
199 }
200
201 pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
203 self.config.insert(key.into(), value);
204 self
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum FinishReason {
225 Stop,
227 Length,
229 ToolCalls,
231 ContentFilter,
233 Other(String),
236}
237
238impl FinishReason {
239 pub fn reconcile_with_output(self, has_tool_call: bool) -> Self {
257 if has_tool_call && matches!(self, Self::Stop) {
258 Self::ToolCalls
259 } else {
260 self
261 }
262 }
263
264 pub fn truncated_output(&self) -> bool {
285 matches!(self, Self::Length | Self::ContentFilter)
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(from = "CompletionResponseRepr")]
302pub struct CompletionResponse {
303 pub choice: Vec<AssistantContent>,
306 pub usage: Usage,
308 #[serde(default)]
319 pub message_id: Option<String>,
320 #[serde(default)]
326 pub response_id: Option<String>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub provider_request_id: Option<String>,
337 #[serde(default)]
346 finish_reason: Option<FinishReason>,
347 pub provider: String,
351 #[serde(default)]
356 pub model: Option<String>,
357 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
376 pub raw: serde_json::Value,
377}
378
379#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
384pub struct ResponseIdentity {
385 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub message_id: Option<String>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub response_id: Option<String>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub provider_request_id: Option<String>,
400}
401
402impl CompletionResponse {
403 pub fn new(choice: Vec<AssistantContent>, usage: Usage, provider: impl Into<String>) -> Self {
406 Self {
407 choice,
408 usage,
409 message_id: None,
410 response_id: None,
411 provider_request_id: None,
412 finish_reason: None,
413 provider: provider.into(),
414 model: None,
415 raw: serde_json::Value::Null,
416 }
417 }
418
419 pub fn finish_reason(&self) -> Option<FinishReason> {
421 self.finish_reason.clone()
422 }
423
424 pub fn identity(&self) -> ResponseIdentity {
426 ResponseIdentity {
427 message_id: self.message_id.clone(),
428 response_id: self.response_id.clone(),
429 provider_request_id: self.provider_request_id.clone(),
430 }
431 }
432
433 pub fn with_finish_reason(self, finish_reason: FinishReason) -> Self {
436 self.with_optional_finish_reason(Some(finish_reason))
437 }
438
439 pub fn with_optional_finish_reason(mut self, finish_reason: Option<FinishReason>) -> Self {
446 let has_tool_call = self
447 .choice
448 .iter()
449 .any(|content| matches!(content, AssistantContent::ToolCall(_)));
450 self.finish_reason =
451 finish_reason.map(|reason| reason.reconcile_with_output(has_tool_call));
452 self
453 }
454}
455
456crate::provider_response::response_metadata_setters!(CompletionResponse);
457
458#[derive(Deserialize)]
469struct CompletionResponseRepr {
470 choice: Vec<AssistantContent>,
471 usage: Usage,
472 #[serde(default)]
473 message_id: Option<String>,
474 #[serde(default)]
475 response_id: Option<String>,
476 #[serde(default)]
477 provider_request_id: Option<String>,
478 #[serde(default)]
479 finish_reason: Option<FinishReason>,
480 provider: String,
481 #[serde(default)]
482 model: Option<String>,
483 #[serde(default)]
487 raw: serde_json::Value,
488}
489
490impl From<CompletionResponseRepr> for CompletionResponse {
491 fn from(repr: CompletionResponseRepr) -> Self {
492 let CompletionResponseRepr {
493 choice,
494 usage,
495 message_id,
496 response_id,
497 provider_request_id,
498 finish_reason,
499 provider,
500 model,
501 raw,
502 } = repr;
503 Self::new(choice, usage, provider)
504 .with_optional_message_id(message_id)
505 .with_optional_response_id(response_id)
506 .with_optional_provider_request_id(provider_request_id)
507 .with_optional_finish_reason(finish_reason)
508 .with_optional_model(model)
509 .with_raw(raw)
510 }
511}
512
513pub trait NormalizeCompletionResponse {
529 fn normalize(self, provider: &str) -> Result<CompletionResponse, CompletionError>;
531}
532
533#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
536pub struct Usage {
537 pub input_tokens: u64,
539 pub output_tokens: u64,
541 pub total_tokens: u64,
543 pub cached_input_tokens: u64,
545 pub cache_creation_input_tokens: u64,
547 #[serde(default)]
549 pub tool_use_prompt_tokens: u64,
550 pub reasoning_tokens: u64,
553}
554
555impl Usage {
556 pub fn new() -> Self {
558 Self {
559 input_tokens: 0,
560 output_tokens: 0,
561 total_tokens: 0,
562 cached_input_tokens: 0,
563 cache_creation_input_tokens: 0,
564 tool_use_prompt_tokens: 0,
565 reasoning_tokens: 0,
566 }
567 }
568
569 pub fn has_values(&self) -> bool {
574 *self != Self::new()
575 }
576}
577
578impl Default for Usage {
579 fn default() -> Self {
580 Self::new()
581 }
582}
583
584impl Add for Usage {
585 type Output = Self;
586
587 fn add(mut self, other: Self) -> Self::Output {
588 self += other;
589 self
590 }
591}
592
593impl AddAssign for Usage {
594 fn add_assign(&mut self, other: Self) {
595 self.input_tokens += other.input_tokens;
596 self.output_tokens += other.output_tokens;
597 self.total_tokens += other.total_tokens;
598 self.cached_input_tokens += other.cached_input_tokens;
599 self.cache_creation_input_tokens += other.cache_creation_input_tokens;
600 self.tool_use_prompt_tokens += other.tool_use_prompt_tokens;
601 self.reasoning_tokens += other.reasoning_tokens;
602 }
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
616pub struct ProviderCapabilities {
617 pub composes_native_output_with_tools: bool,
627}
628
629impl ProviderCapabilities {
630 pub const fn new() -> Self {
632 Self {
633 composes_native_output_with_tools: false,
634 }
635 }
636
637 pub const fn with_native_output_tool_composition(mut self, supported: bool) -> Self {
639 self.composes_native_output_with_tools = supported;
640 self
641 }
642}
643
644pub trait CompletionModel: WasmCompatSend + WasmCompatSync {
660 fn completion(
662 &self,
663 request: CompletionRequest,
664 ) -> impl std::future::Future<Output = Result<CompletionResponse, CompletionError>> + WasmCompatSend;
665
666 fn stream(
668 &self,
669 request: CompletionRequest,
670 ) -> impl std::future::Future<Output = Result<StreamingCompletionResponse, CompletionError>>
671 + WasmCompatSend;
672
673 fn completion_request(&self, prompt: impl Into<Message>) -> CompletionRequestBuilder<Self>
675 where
676 Self: Sized + Clone,
677 {
678 CompletionRequestBuilder::new(self.clone(), prompt)
679 }
680
681 fn capabilities(&self) -> ProviderCapabilities {
686 ProviderCapabilities::default()
687 }
688}
689
690impl<M: CompletionModel + ?Sized> CompletionModel for std::sync::Arc<M> {
696 fn completion(
697 &self,
698 request: CompletionRequest,
699 ) -> impl std::future::Future<Output = Result<CompletionResponse, CompletionError>> + WasmCompatSend
700 {
701 (**self).completion(request)
702 }
703
704 fn stream(
705 &self,
706 request: CompletionRequest,
707 ) -> impl std::future::Future<Output = Result<StreamingCompletionResponse, CompletionError>>
708 + WasmCompatSend {
709 (**self).stream(request)
710 }
711
712 fn capabilities(&self) -> ProviderCapabilities {
713 (**self).capabilities()
714 }
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct CompletionRequest {
720 pub model: Option<String>,
722 pub preamble: Option<String>,
727 pub chat_history: Vec<Message>,
735 pub documents: Vec<Document>,
737 pub tools: Vec<ToolDefinition>,
739 pub temperature: Option<f64>,
741 pub max_tokens: Option<u64>,
743 pub tool_choice: Option<ToolChoice>,
745 pub additional_params: Option<serde_json::Value>,
747 pub output_schema: Option<schemars::Schema>,
750 #[serde(skip)]
768 pub record_telemetry_content: bool,
769}
770
771impl CompletionRequest {
772 pub fn validate_message_content(&self) -> Result<(), CompletionError> {
813 if self.chat_history.is_empty() {
814 return Err(CompletionError::RequestError(
815 "request has an empty chat history; providers require at least one message"
816 .to_owned()
817 .into(),
818 ));
819 }
820
821 let empty_message = |role: &str, index: usize| {
822 CompletionError::RequestError(
823 format!(
824 "{role} message at index {index} has no content; \
825 providers reject empty content blocks"
826 )
827 .into(),
828 )
829 };
830
831 for (index, message) in self.chat_history.iter().enumerate() {
834 match message {
835 Message::System { .. } => {}
836 Message::Assistant { content, .. } => {
837 if content.is_empty() {
838 return Err(empty_message("assistant", index));
839 }
840 }
841 Message::User { content } => {
842 if content.is_empty() {
843 return Err(empty_message("user", index));
844 }
845
846 for (position, item) in content.iter().enumerate() {
847 match item {
852 UserContent::ToolResult(result) if result.content.is_empty() => {
853 let name = &result.name;
854 return Err(CompletionError::RequestError(
855 format!(
856 "tool result for `{name}` at index {position} of the \
857 user message at index {index} has no content; \
858 providers reject empty content blocks"
859 )
860 .into(),
861 ));
862 }
863 UserContent::ToolResult(_)
864 | UserContent::Text(_)
865 | UserContent::Image(_)
866 | UserContent::Audio(_)
867 | UserContent::Video(_)
868 | UserContent::Document(_) => {}
869 }
870 }
871 }
872 }
873 }
874
875 Ok(())
876 }
877
878 pub fn output_schema_name(&self) -> Option<String> {
881 self.output_schema.as_ref().map(|schema| {
882 schema
883 .as_object()
884 .and_then(|o| o.get("title"))
885 .and_then(|v| v.as_str())
886 .unwrap_or("response_schema")
887 .to_string()
888 })
889 }
890
891 pub fn normalized_documents(&self) -> Option<Message> {
895 Self::normalized_documents_from(&self.documents)
896 }
897
898 fn normalized_documents_from(documents: &[Document]) -> Option<Message> {
899 if documents.is_empty() {
900 return None;
901 }
902
903 let messages = documents
906 .iter()
907 .map(|doc| {
908 UserContent::document(
909 doc.to_string(),
910 Some(DocumentMediaType::TXT),
913 )
914 })
915 .collect::<Vec<_>>();
916
917 crate::message::non_empty(messages).map(|content| Message::User { content })
918 }
919
920 pub(crate) fn chat_history_with_documents(&self) -> Vec<Message> {
921 let mut chat_history = self.chat_history.clone();
922 if let Some(documents) = self.normalized_documents() {
923 insert_after_leading_system(&mut chat_history, documents);
924 }
925 chat_history
926 }
927}
928
929fn insert_after_leading_system(chat_history: &mut Vec<Message>, message: Message) {
933 let insert_at = chat_history
934 .iter()
935 .position(|message| !matches!(message, Message::System { .. }))
936 .unwrap_or(chat_history.len());
937 chat_history.insert(insert_at, message);
938}
939
940fn merge_provider_tools_into_additional_params(
941 additional_params: Option<serde_json::Value>,
942 provider_tools: Vec<ProviderToolDefinition>,
943) -> Option<serde_json::Value> {
944 if provider_tools.is_empty() {
945 return additional_params;
946 }
947
948 let mut provider_tools_json = provider_tools
949 .into_iter()
950 .map(|ProviderToolDefinition { kind, mut config }| {
951 config.insert("type".to_string(), serde_json::Value::String(kind));
953 serde_json::Value::Object(config)
954 })
955 .collect::<Vec<_>>();
956
957 let mut params_map = match additional_params {
958 Some(serde_json::Value::Object(map)) => map,
959 Some(serde_json::Value::Bool(stream)) => {
960 let mut map = serde_json::Map::new();
961 map.insert("stream".to_string(), serde_json::Value::Bool(stream));
962 map
963 }
964 _ => serde_json::Map::new(),
965 };
966
967 let mut merged_tools = match params_map.remove("tools") {
968 Some(serde_json::Value::Array(existing)) => existing,
969 _ => Vec::new(),
970 };
971 merged_tools.append(&mut provider_tools_json);
972 params_map.insert("tools".to_string(), serde_json::Value::Array(merged_tools));
973 Some(serde_json::Value::Object(params_map))
974}
975
976pub struct CompletionRequestBuilder<M: CompletionModel> {
1026 model: M,
1027 prompt: Message,
1028 request_model: Option<String>,
1029 preamble: Option<String>,
1030 chat_history: Vec<Message>,
1031 documents: Vec<Document>,
1032 tools: Vec<ToolDefinition>,
1033 provider_tools: Vec<ProviderToolDefinition>,
1034 temperature: Option<f64>,
1035 max_tokens: Option<u64>,
1036 tool_choice: Option<ToolChoice>,
1037 additional_params: Option<serde_json::Value>,
1038 output_schema: Option<schemars::Schema>,
1039 record_telemetry_content: bool,
1040}
1041
1042impl<M: CompletionModel> CompletionRequestBuilder<M> {
1043 pub fn new(model: M, prompt: impl Into<Message>) -> Self {
1044 Self {
1045 model,
1046 prompt: prompt.into(),
1047 request_model: None,
1048 preamble: None,
1049 chat_history: Vec::new(),
1050 documents: Vec::new(),
1051 tools: Vec::new(),
1052 provider_tools: Vec::new(),
1053 temperature: None,
1054 max_tokens: None,
1055 tool_choice: None,
1056 additional_params: None,
1057 output_schema: None,
1058 record_telemetry_content: false,
1059 }
1060 }
1061
1062 pub fn preamble(mut self, preamble: String) -> Self {
1064 self.preamble = Some(preamble);
1066 self
1067 }
1068
1069 pub fn model(mut self, model: impl Into<String>) -> Self {
1071 self.request_model = Some(model.into());
1072 self
1073 }
1074
1075 pub fn model_opt(mut self, model: Option<String>) -> Self {
1077 self.request_model = model;
1078 self
1079 }
1080
1081 pub fn without_preamble(mut self) -> Self {
1082 self.preamble = None;
1083 self
1084 }
1085
1086 pub fn message(mut self, message: Message) -> Self {
1088 self.chat_history.push(message);
1089
1090 self
1091 }
1092
1093 pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
1095 self.chat_history.extend(messages);
1096
1097 self
1098 }
1099
1100 pub fn document(mut self, document: Document) -> Self {
1102 self.documents.push(document);
1103 self
1104 }
1105
1106 pub fn documents(self, documents: impl IntoIterator<Item = Document>) -> Self {
1108 documents
1109 .into_iter()
1110 .fold(self, |builder, doc| builder.document(doc))
1111 }
1112
1113 pub fn tool(mut self, tool: ToolDefinition) -> Self {
1115 self.tools.push(tool);
1116 self
1117 }
1118
1119 pub fn tools(self, tools: Vec<ToolDefinition>) -> Self {
1121 tools
1122 .into_iter()
1123 .fold(self, |builder, tool| builder.tool(tool))
1124 }
1125
1126 pub fn provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
1128 self.provider_tools.push(tool);
1129 self
1130 }
1131
1132 pub fn provider_tools(self, tools: Vec<ProviderToolDefinition>) -> Self {
1134 tools
1135 .into_iter()
1136 .fold(self, |builder, tool| builder.provider_tool(tool))
1137 }
1138
1139 pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
1145 match self.additional_params {
1146 Some(params) => {
1147 self.additional_params = Some(json_utils::merge(params, additional_params));
1148 }
1149 None => {
1150 self.additional_params = Some(additional_params);
1151 }
1152 }
1153 self
1154 }
1155
1156 pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
1162 self.additional_params = additional_params;
1163 self
1164 }
1165
1166 pub fn temperature(mut self, temperature: f64) -> Self {
1168 self.temperature = Some(temperature);
1169 self
1170 }
1171
1172 pub fn temperature_opt(mut self, temperature: Option<f64>) -> Self {
1174 self.temperature = temperature;
1175 self
1176 }
1177
1178 pub fn max_tokens(mut self, max_tokens: u64) -> Self {
1181 self.max_tokens = Some(max_tokens);
1182 self
1183 }
1184
1185 pub fn max_tokens_opt(mut self, max_tokens: Option<u64>) -> Self {
1188 self.max_tokens = max_tokens;
1189 self
1190 }
1191
1192 pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1194 self.tool_choice = Some(tool_choice);
1195 self
1196 }
1197
1198 pub fn output_schema(mut self, schema: schemars::Schema) -> Self {
1205 self.output_schema = Some(schema);
1206 self
1207 }
1208
1209 pub fn output_schema_opt(mut self, schema: Option<schemars::Schema>) -> Self {
1215 self.output_schema = schema;
1216 self
1217 }
1218
1219 pub fn record_content_telemetry(mut self, enabled: bool) -> Self {
1234 self.record_telemetry_content = enabled;
1235 self
1236 }
1237
1238 pub fn messages_for_telemetry(&self) -> Vec<Message> {
1240 let mut chat_history = self.chat_history.clone();
1241 if let Some(preamble) = &self.preamble {
1242 chat_history.insert(0, Message::system(preamble.clone()));
1243 }
1244 chat_history.push(self.prompt.clone());
1245
1246 if let Some(documents) = CompletionRequest::normalized_documents_from(&self.documents) {
1247 insert_after_leading_system(&mut chat_history, documents);
1248 }
1249
1250 chat_history
1251 }
1252
1253 pub fn build(self) -> CompletionRequest {
1255 self.into_model_and_request().1
1256 }
1257
1258 fn into_model_and_request(self) -> (M, CompletionRequest) {
1264 let model = self.model;
1265 let mut chat_history = self.chat_history;
1267 let prompt = self.prompt;
1268 if let Some(preamble) = self.preamble {
1269 chat_history.insert(0, Message::system(preamble));
1270 }
1271
1272 chat_history.push(prompt);
1276 let additional_params = merge_provider_tools_into_additional_params(
1277 self.additional_params,
1278 self.provider_tools,
1279 );
1280
1281 let request = CompletionRequest {
1282 model: self.request_model,
1283 preamble: None,
1284 chat_history,
1285 documents: self.documents,
1286 tools: self.tools,
1287 temperature: self.temperature,
1288 max_tokens: self.max_tokens,
1289 tool_choice: self.tool_choice,
1290 additional_params,
1291 output_schema: self.output_schema,
1292 record_telemetry_content: self.record_telemetry_content,
1293 };
1294 (model, request)
1295 }
1296
1297 pub async fn send(self) -> Result<CompletionResponse, CompletionError> {
1299 let (model, request) = self.into_model_and_request();
1300 request.validate_message_content()?;
1301 model.completion(request).await
1302 }
1303
1304 pub async fn stream(self) -> Result<StreamingCompletionResponse, CompletionError> {
1306 let (model, request) = self.into_model_and_request();
1307 request.validate_message_content()?;
1308 model.stream(request).await
1309 }
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::{CompletionResponse, FinishReason, ProviderCapabilities, Usage};
1315 use crate::message::AssistantContent;
1316
1317 mod message_content_validation {
1318 use super::super::CompletionRequest;
1319 use crate::message::{AssistantContent, Message, UserContent};
1320
1321 fn request(chat_history: Vec<Message>) -> CompletionRequest {
1322 CompletionRequest {
1323 model: None,
1324 preamble: None,
1325 chat_history,
1326 documents: Vec::new(),
1327 tools: Vec::new(),
1328 temperature: None,
1329 max_tokens: None,
1330 tool_choice: None,
1331 additional_params: None,
1332 output_schema: None,
1333 record_telemetry_content: false,
1334 }
1335 }
1336
1337 #[test]
1338 fn a_populated_history_passes() {
1339 let request = request(vec![Message::user("hello")]);
1340 assert!(request.validate_message_content().is_ok());
1341 }
1342
1343 #[test]
1344 fn an_empty_history_is_rejected() {
1345 let error = request(Vec::new())
1348 .validate_message_content()
1349 .expect_err("an empty history must not reach a provider");
1350 assert!(
1351 error.to_string().contains("empty chat history"),
1352 "unexpected error: {error}"
1353 );
1354 }
1355
1356 #[test]
1357 fn an_empty_user_message_is_rejected_by_index_and_role() {
1358 let error = request(vec![
1359 Message::user("hello"),
1360 Message::User {
1361 content: Vec::new(),
1362 },
1363 ])
1364 .validate_message_content()
1365 .expect_err("an empty user message must not reach a provider");
1366 let message = error.to_string();
1367 assert!(message.contains("user message at index 1"), "{message}");
1368 }
1369
1370 #[test]
1371 fn an_empty_assistant_message_is_rejected_by_index_and_role() {
1372 let error = request(vec![Message::Assistant {
1373 id: None,
1374 content: Vec::new(),
1375 }])
1376 .validate_message_content()
1377 .expect_err("an empty assistant message must not reach a provider");
1378 let message = error.to_string();
1379 assert!(
1380 message.contains("assistant message at index 0"),
1381 "{message}"
1382 );
1383 }
1384
1385 #[test]
1386 fn an_empty_system_message_is_not_rejected() {
1387 let request = request(vec![
1392 Message::System {
1393 content: String::new(),
1394 },
1395 Message::user("hello"),
1396 ]);
1397 assert!(request.validate_message_content().is_ok());
1398 }
1399
1400 #[test]
1401 fn a_block_less_tool_result_is_rejected_naming_the_tool() {
1402 use crate::message::{ToolCallId, ToolResult, ToolResultContent};
1403 let error = request(vec![
1408 Message::user("hello"),
1409 Message::User {
1410 content: vec![UserContent::ToolResult(ToolResult {
1411 call: ToolCallId::new_or_mint("call_1"),
1412 provider: None,
1413 name: "lookup".to_owned(),
1414 content: Vec::<ToolResultContent>::new(),
1415 })],
1416 },
1417 ])
1418 .validate_message_content()
1419 .expect_err("a block-less tool result must not reach a provider");
1420 let message = error.to_string();
1421 assert!(message.contains("`lookup`"), "{message}");
1422 assert!(message.contains("index 0"), "{message}");
1423 assert!(message.contains("user message at index 1"), "{message}");
1424 }
1425
1426 #[test]
1427 fn a_tool_result_with_one_empty_string_block_is_accepted() {
1428 use crate::message::{ToolCallId, ToolResult, ToolResultContent};
1429 let request = request(vec![Message::User {
1435 content: vec![UserContent::ToolResult(ToolResult {
1436 call: ToolCallId::new_or_mint("call_1"),
1437 provider: None,
1438 name: "lookup".to_owned(),
1439 content: vec![ToolResultContent::text("")],
1440 })],
1441 }]);
1442 assert!(request.validate_message_content().is_ok());
1443 }
1444
1445 #[test]
1446 fn the_legacy_fabricated_sentinel_still_passes() {
1447 let request = request(vec![
1455 Message::user("hello"),
1456 Message::Assistant {
1457 id: None,
1458 content: vec![AssistantContent::text("")],
1459 },
1460 Message::User {
1461 content: vec![UserContent::text("and again")],
1462 },
1463 ]);
1464 assert!(request.validate_message_content().is_ok());
1465 }
1466 }
1467
1468 fn tool_call_choice() -> Vec<AssistantContent> {
1469 vec![AssistantContent::tool_call(
1470 "call_1",
1471 "lookup",
1472 serde_json::json!({"query": "rig"}),
1473 )]
1474 }
1475
1476 #[test]
1477 fn normalized_response_round_trips_through_serde() {
1478 let response = CompletionResponse::new(
1479 vec![AssistantContent::text("hello")],
1480 Usage {
1481 input_tokens: 3,
1482 output_tokens: 2,
1483 total_tokens: 5,
1484 cached_input_tokens: 1,
1485 cache_creation_input_tokens: 0,
1486 tool_use_prompt_tokens: 0,
1487 reasoning_tokens: 1,
1488 },
1489 "example",
1490 )
1491 .with_message_id("msg_123")
1492 .with_finish_reason(FinishReason::Stop)
1493 .with_model("provider-model-v2");
1494
1495 let encoded = serde_json::to_value(&response).expect("serialize response");
1496 let decoded =
1497 serde_json::from_value::<CompletionResponse>(encoded.clone()).expect("deserialize");
1498
1499 assert_eq!(
1500 serde_json::to_value(decoded).expect("re-serialize"),
1501 encoded
1502 );
1503 }
1504
1505 #[test]
1509 fn deserializing_stop_with_a_tool_call_reconciles_to_tool_calls() {
1510 let mut encoded = serde_json::to_value(CompletionResponse::new(
1511 tool_call_choice(),
1512 Usage::new(),
1513 "example",
1514 ))
1515 .expect("serialize response");
1516 encoded["finish_reason"] = serde_json::json!("stop");
1517
1518 let decoded =
1519 serde_json::from_value::<CompletionResponse>(encoded).expect("deserialize response");
1520
1521 assert_eq!(decoded.finish_reason(), Some(FinishReason::ToolCalls));
1522 }
1523
1524 #[test]
1527 fn deserializing_empty_identifiers_yields_none() {
1528 let mut encoded = serde_json::to_value(CompletionResponse::new(
1529 vec![AssistantContent::text("hello")],
1530 Usage::new(),
1531 "example",
1532 ))
1533 .expect("serialize response");
1534 encoded["message_id"] = serde_json::json!("");
1535 encoded["response_id"] = serde_json::json!("");
1536 encoded["model"] = serde_json::json!("");
1537
1538 let decoded =
1539 serde_json::from_value::<CompletionResponse>(encoded).expect("deserialize response");
1540
1541 assert_eq!(decoded.message_id, None);
1542 assert_eq!(decoded.response_id, None);
1543 assert_eq!(decoded.model, None);
1544 }
1545
1546 #[test]
1547 fn unknown_finish_reason_survives_a_serde_round_trip_verbatim() {
1548 let reason = FinishReason::Other("provider_specific_stop".to_owned());
1549 let encoded = serde_json::to_string(&reason).expect("serialize");
1550 let decoded = serde_json::from_str::<FinishReason>(&encoded).expect("deserialize");
1551
1552 assert_eq!(decoded, reason);
1553 }
1554
1555 #[test]
1556 fn stop_with_a_tool_call_reconciles_to_tool_calls() {
1557 let response = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1558 .with_finish_reason(FinishReason::Stop);
1559
1560 assert_eq!(response.finish_reason, Some(FinishReason::ToolCalls));
1561 }
1562
1563 #[test]
1567 fn optional_setter_reconciles_exactly_like_the_plain_setter() {
1568 let via_option = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1569 .with_optional_finish_reason(Some(FinishReason::Stop));
1570 let via_plain = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1571 .with_finish_reason(FinishReason::Stop);
1572
1573 assert_eq!(via_option.finish_reason, Some(FinishReason::ToolCalls));
1574 assert_eq!(via_option.finish_reason, via_plain.finish_reason);
1575 }
1576
1577 #[test]
1578 fn reconciliation_only_upgrades_a_natural_stop() {
1579 for reason in [
1582 FinishReason::Length,
1583 FinishReason::ContentFilter,
1584 FinishReason::Other("provider_specific".to_owned()),
1585 ] {
1586 let response = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1587 .with_finish_reason(reason.clone());
1588
1589 assert_eq!(response.finish_reason, Some(reason));
1590 }
1591 }
1592
1593 #[test]
1594 fn reconciliation_leaves_a_stop_without_tool_calls_alone() {
1595 let response = CompletionResponse::new(
1596 vec![AssistantContent::text("done")],
1597 Usage::new(),
1598 "example",
1599 )
1600 .with_finish_reason(FinishReason::Stop);
1601
1602 assert_eq!(response.finish_reason, Some(FinishReason::Stop));
1603 }
1604
1605 #[test]
1606 fn provider_capabilities_are_externally_configurable_from_default() {
1607 let capabilities =
1608 ProviderCapabilities::default().with_native_output_tool_composition(true);
1609
1610 assert!(capabilities.composes_native_output_with_tools);
1611 assert!(!ProviderCapabilities::new().composes_native_output_with_tools);
1612 assert_eq!(ProviderCapabilities::new(), ProviderCapabilities::default());
1613 }
1614
1615 #[test]
1616 fn usage_has_values_reflects_the_zero_sentinel() {
1617 use super::Usage;
1618
1619 assert!(!Usage::new().has_values());
1620
1621 let mut usage = Usage::new();
1622 usage.reasoning_tokens = 1;
1623 assert!(usage.has_values());
1624 }
1625
1626 use super::*;
1627 use crate::test_utils::MockCompletionModel;
1628
1629 #[test]
1630 fn completion_request_content_telemetry_is_opt_in_and_not_serialized() {
1631 let default_request =
1632 CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
1633 .build();
1634 assert!(!default_request.record_telemetry_content);
1635
1636 let default_json = serde_json::to_value(&default_request).expect("serialize request");
1637 assert!(
1638 default_json.get("record_telemetry_content").is_none(),
1639 "safe default should not serialize the telemetry opt-in field"
1640 );
1641 let default_roundtrip: CompletionRequest =
1642 serde_json::from_value(default_json).expect("deserialize default request");
1643 assert!(!default_roundtrip.record_telemetry_content);
1644
1645 let opt_in_request =
1646 CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
1647 .record_content_telemetry(true)
1648 .build();
1649 assert!(opt_in_request.record_telemetry_content);
1650
1651 let opt_in_json = serde_json::to_value(&opt_in_request).expect("serialize opt-in request");
1652 assert!(
1653 opt_in_json.get("record_telemetry_content").is_none(),
1654 "local telemetry policy must not be serialized into provider requests"
1655 );
1656 let legacy_roundtrip: CompletionRequest =
1657 serde_json::from_value(opt_in_json).expect("deserialize legacy request");
1658 assert!(
1659 !legacy_roundtrip.record_telemetry_content,
1660 "missing field should deserialize to the safe default"
1661 );
1662 }
1663
1664 #[test]
1669 fn normalized_response_raw_round_trips_through_serde_mirror() {
1670 let payload = serde_json::json!({
1671 "id": "chatcmpl-1",
1672 "system_fingerprint": "fp_abc",
1673 "choices": [{"finish_reason": "stop"}]
1674 });
1675 let response = CompletionResponse::new(
1676 vec![AssistantContent::text("hello")],
1677 Usage::new(),
1678 "example",
1679 )
1680 .with_response_id("chatcmpl-1")
1681 .with_raw(payload.clone());
1682
1683 let encoded = serde_json::to_value(&response).expect("serialize response");
1684 assert_eq!(encoded["raw"], payload);
1685 let decoded: CompletionResponse =
1686 serde_json::from_value(encoded.clone()).expect("deserialize response");
1687 assert_eq!(decoded.raw, payload);
1688 assert_eq!(decoded.response_id.as_deref(), Some("chatcmpl-1"));
1689 assert_eq!(
1690 serde_json::to_value(&decoded).expect("re-serialize"),
1691 encoded
1692 );
1693
1694 let legacy = serde_json::json!({
1695 "choice": [{"type": "text", "text": "hello"}],
1696 "usage": serde_json::to_value(Usage::new()).unwrap(),
1697 "provider": "example"
1698 });
1699 let decoded: CompletionResponse = serde_json::from_value(legacy).expect("legacy loads");
1700 assert!(decoded.raw.is_null());
1701
1702 let bare = serde_json::to_value(CompletionResponse::new(
1703 vec![AssistantContent::text("hello")],
1704 Usage::new(),
1705 "example",
1706 ))
1707 .unwrap();
1708 assert!(bare.get("raw").is_none());
1709 }
1710
1711 fn test_document(id: &str, text: &str) -> Document {
1712 Document {
1713 id: id.to_string(),
1714 text: text.to_string(),
1715 additional_props: HashMap::new(),
1716 }
1717 }
1718
1719 #[test]
1720 fn message_telemetry_includes_normalized_documents() {
1721 let builder = CompletionRequestBuilder::new(MockCompletionModel::default(), "prompt")
1722 .preamble("system".to_string())
1723 .message(Message::user("history"))
1724 .document(test_document("doc1", "static context secret"));
1725
1726 let messages = builder.messages_for_telemetry();
1727 assert_eq!(messages.len(), 4);
1728 assert!(matches!(messages[0], Message::System { .. }));
1729 assert!(is_document_message(&messages[1], "doc1"));
1730 assert!(matches!(
1731 &messages[2],
1732 Message::User { content }
1733 if matches!(content.first(), Some(UserContent::Text(text)) if text.text == "history")
1734 ));
1735 assert!(matches!(
1736 &messages[3],
1737 Message::User { content }
1738 if matches!(content.first(), Some(UserContent::Text(text)) if text.text == "prompt")
1739 ));
1740
1741 let request = builder.build();
1742 assert_eq!(messages, request.chat_history_with_documents());
1743 }
1744
1745 fn is_document_message(message: &Message, expected_id: &str) -> bool {
1746 let Message::User { content } = message else {
1747 return false;
1748 };
1749
1750 content.iter().any(|content| {
1751 matches!(
1752 content,
1753 UserContent::Document(document)
1754 if document.data.to_string().contains(&format!("<file id: {expected_id}>"))
1755 )
1756 })
1757 }
1758
1759 #[test]
1760 fn test_document_display_without_metadata() {
1761 let doc = Document {
1762 id: "123".to_string(),
1763 text: "This is a test document.".to_string(),
1764 additional_props: HashMap::new(),
1765 };
1766
1767 let expected = "<file id: 123>\nThis is a test document.\n</file>\n";
1768 assert_eq!(format!("{doc}"), expected);
1769 }
1770
1771 #[test]
1772 fn test_document_display_with_metadata() {
1773 let mut additional_props = HashMap::new();
1774 additional_props.insert("author".to_string(), "John Doe".to_string());
1775 additional_props.insert("length".to_string(), "42".to_string());
1776
1777 let doc = Document {
1778 id: "123".to_string(),
1779 text: "This is a test document.".to_string(),
1780 additional_props,
1781 };
1782
1783 let expected = concat!(
1784 "<file id: 123>\n",
1785 "<metadata author: \"John Doe\" length: \"42\" />\n",
1786 "This is a test document.\n",
1787 "</file>\n"
1788 );
1789 assert_eq!(format!("{doc}"), expected);
1790 }
1791
1792 #[test]
1793 fn test_normalize_documents_with_documents() {
1794 let doc1 = Document {
1795 id: "doc1".to_string(),
1796 text: "Document 1 text.".to_string(),
1797 additional_props: HashMap::new(),
1798 };
1799
1800 let doc2 = Document {
1801 id: "doc2".to_string(),
1802 text: "Document 2 text.".to_string(),
1803 additional_props: HashMap::new(),
1804 };
1805
1806 let request = CompletionRequest {
1807 model: None,
1808 preamble: None,
1809 chat_history: vec!["What is the capital of France?".into()],
1810 documents: vec![doc1, doc2],
1811 tools: Vec::new(),
1812 temperature: None,
1813 max_tokens: None,
1814 tool_choice: None,
1815 additional_params: None,
1816 output_schema: None,
1817 record_telemetry_content: false,
1818 };
1819
1820 let expected = Message::User {
1821 content: vec![
1822 UserContent::document(
1823 "<file id: doc1>\nDocument 1 text.\n</file>\n".to_string(),
1824 Some(DocumentMediaType::TXT),
1825 ),
1826 UserContent::document(
1827 "<file id: doc2>\nDocument 2 text.\n</file>\n".to_string(),
1828 Some(DocumentMediaType::TXT),
1829 ),
1830 ],
1831 };
1832
1833 assert_eq!(request.normalized_documents(), Some(expected));
1834 }
1835
1836 #[test]
1837 fn test_normalize_documents_without_documents() {
1838 let request = CompletionRequest {
1839 model: None,
1840 preamble: None,
1841 chat_history: vec!["What is the capital of France?".into()],
1842 documents: Vec::new(),
1843 tools: Vec::new(),
1844 temperature: None,
1845 max_tokens: None,
1846 tool_choice: None,
1847 additional_params: None,
1848 output_schema: None,
1849 record_telemetry_content: false,
1850 };
1851
1852 assert_eq!(request.normalized_documents(), None);
1853 }
1854
1855 #[test]
1856 fn preamble_builder_funnels_to_system_message() {
1857 let request =
1858 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1859 .preamble("System prompt".to_string())
1860 .message(Message::user("History"))
1861 .build();
1862
1863 assert_eq!(request.preamble, None);
1864
1865 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1866 assert_eq!(history.len(), 3);
1867 assert!(matches!(
1868 &history[0],
1869 Message::System { content } if content == "System prompt"
1870 ));
1871 assert!(matches!(&history[1], Message::User { .. }));
1872 assert!(matches!(&history[2], Message::User { .. }));
1873 }
1874
1875 #[test]
1876 fn without_preamble_removes_legacy_preamble_injection() {
1877 let request =
1878 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1879 .preamble("System prompt".to_string())
1880 .without_preamble()
1881 .build();
1882
1883 assert_eq!(request.preamble, None);
1884 let history = request.chat_history.into_iter().collect::<Vec<_>>();
1885 assert_eq!(history.len(), 1);
1886 assert!(matches!(&history[0], Message::User { .. }));
1887 }
1888
1889 #[test]
1890 fn build_places_documents_after_preamble_system_message() {
1891 let request =
1892 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1893 .preamble("System prompt".to_string())
1894 .document(test_document("doc1", "Document text."))
1895 .build();
1896
1897 assert_eq!(request.documents.len(), 1);
1898
1899 let history = request.chat_history_with_documents();
1900 let history = history.iter().collect::<Vec<_>>();
1901 assert_eq!(history.len(), 3);
1902 assert!(matches!(
1903 history[0],
1904 Message::System { content } if content == "System prompt"
1905 ));
1906 assert!(is_document_message(history[1], "doc1"));
1907 assert!(matches!(history[2], Message::User { .. }));
1908 }
1909
1910 #[test]
1911 fn build_places_documents_after_leading_system_messages_before_prior_history() {
1912 let request =
1913 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1914 .message(Message::system("System one"))
1915 .message(Message::system("System two"))
1916 .message(Message::user("Earlier user turn"))
1917 .message(Message::assistant("Earlier assistant turn"))
1918 .document(test_document("doc1", "Document text."))
1919 .build();
1920
1921 let history = request.chat_history_with_documents();
1922 let history = history.iter().collect::<Vec<_>>();
1923 assert_eq!(history.len(), 6);
1924 assert!(matches!(
1925 history[0],
1926 Message::System { content } if content == "System one"
1927 ));
1928 assert!(matches!(
1929 history[1],
1930 Message::System { content } if content == "System two"
1931 ));
1932 assert!(is_document_message(history[2], "doc1"));
1933 assert!(matches!(history[3], Message::User { .. }));
1934 assert!(matches!(history[4], Message::Assistant { .. }));
1935 assert!(matches!(history[5], Message::User { .. }));
1936 }
1937
1938 #[test]
1939 fn build_without_documents_keeps_message_order_unchanged() {
1940 let request =
1941 CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1942 .message(Message::system("System prompt"))
1943 .message(Message::user("Earlier user turn"))
1944 .build();
1945
1946 let history = request.chat_history.iter().collect::<Vec<_>>();
1947 assert_eq!(history.len(), 3);
1948 assert!(matches!(
1949 history[0],
1950 Message::System { content } if content == "System prompt"
1951 ));
1952 assert!(matches!(history[1], Message::User { .. }));
1953 assert!(matches!(history[2], Message::User { .. }));
1954 }
1955
1956 #[test]
1957 fn chat_history_with_documents_places_documents_after_leading_system_messages() {
1958 let request = CompletionRequest {
1959 model: None,
1960 preamble: None,
1961 chat_history: vec![
1962 Message::system("System prompt"),
1963 Message::assistant("Earlier assistant turn"),
1964 Message::user("Earlier user turn"),
1965 Message::user("Prompt"),
1966 ],
1967 documents: vec![test_document("doc1", "Document text.")],
1968 tools: Vec::new(),
1969 temperature: None,
1970 max_tokens: None,
1971 tool_choice: None,
1972 additional_params: None,
1973 output_schema: None,
1974 record_telemetry_content: false,
1975 };
1976
1977 assert_eq!(request.documents.len(), 1);
1978
1979 let history = request.chat_history_with_documents();
1980 let history = history.iter().collect::<Vec<_>>();
1981 assert_eq!(history.len(), 5);
1982 assert!(matches!(history[0], Message::System { .. }));
1983 assert!(is_document_message(history[1], "doc1"));
1984 assert!(matches!(history[2], Message::Assistant { .. }));
1985 assert!(matches!(history[3], Message::User { .. }));
1986 assert!(matches!(history[4], Message::User { .. }));
1987 }
1988
1989 #[test]
1990 fn chat_history_with_documents_places_documents_before_mid_conversation_system_messages() {
1991 let request = CompletionRequest {
1992 model: None,
1993 preamble: None,
1994 chat_history: vec![
1995 Message::system("Leading system prompt"),
1996 Message::assistant("Earlier assistant turn"),
1997 Message::system("Mid-conversation instruction"),
1998 Message::user("Prompt"),
1999 ],
2000 documents: vec![test_document("doc1", "Document text.")],
2001 tools: Vec::new(),
2002 temperature: None,
2003 max_tokens: None,
2004 tool_choice: None,
2005 additional_params: None,
2006 output_schema: None,
2007 record_telemetry_content: false,
2008 };
2009
2010 let history = request.chat_history_with_documents();
2011 let history = history.iter().collect::<Vec<_>>();
2012 assert_eq!(history.len(), 5);
2013 assert!(matches!(
2014 history[0],
2015 Message::System { content } if content == "Leading system prompt"
2016 ));
2017 assert!(is_document_message(history[1], "doc1"));
2018 assert!(matches!(history[2], Message::Assistant { .. }));
2019 assert!(matches!(
2020 history[3],
2021 Message::System { content } if content == "Mid-conversation instruction"
2022 ));
2023 assert!(matches!(history[4], Message::User { .. }));
2024 }
2025
2026 #[test]
2027 fn chat_history_with_documents_does_not_duplicate_documents() {
2028 let request = CompletionRequest {
2029 model: None,
2030 preamble: None,
2031 chat_history: vec![
2032 Message::system("System prompt"),
2033 Message::user("Earlier user turn"),
2034 Message::assistant("Earlier assistant turn"),
2035 Message::user("Prompt"),
2036 ],
2037 documents: vec![test_document("doc1", "Document text.")],
2038 tools: Vec::new(),
2039 temperature: None,
2040 max_tokens: None,
2041 tool_choice: None,
2042 additional_params: None,
2043 output_schema: None,
2044 record_telemetry_content: false,
2045 };
2046
2047 let history = request.chat_history_with_documents();
2048 let document_messages = history
2049 .iter()
2050 .filter(|message| is_document_message(message, "doc1"))
2051 .count();
2052 assert_eq!(document_messages, 1);
2053 }
2054
2055 #[test]
2056 fn completion_error_provider_response_helpers_with_preserved_json_body() {
2057 let body = r#"{"error":{"code":"rate_limit","message":"slow down"}}"#;
2058 let error = CompletionError::ProviderResponse(
2059 provider_response::ProviderResponseError::without_status(body.to_string()),
2060 );
2061
2062 assert_eq!(error.provider_response_body(), Some(body));
2063 assert_eq!(error.provider_response_status(), None);
2064 assert_eq!(
2065 error
2066 .provider_response_json()
2067 .expect("fixture body should parse as valid JSON"),
2068 Some(serde_json::json!({
2069 "error": {
2070 "code": "rate_limit",
2071 "message": "slow down"
2072 }
2073 }))
2074 );
2075 }
2076
2077 #[test]
2078 fn completion_error_provider_response_helpers_with_preserved_status() {
2079 let body = r#"{"error":{"message":"too many requests"}}"#;
2080 let error =
2081 CompletionError::ProviderResponse(provider_response::ProviderResponseError::new(
2082 http::StatusCode::TOO_MANY_REQUESTS,
2083 body.to_string(),
2084 ));
2085
2086 assert_eq!(error.provider_response_body(), Some(body));
2087 assert_eq!(
2088 error.provider_response_status(),
2089 Some(http::StatusCode::TOO_MANY_REQUESTS)
2090 );
2091 }
2092
2093 #[test]
2094 fn completion_error_provider_response_helpers_with_preserved_plain_text_body() {
2095 let error = CompletionError::ProviderResponse(
2096 provider_response::ProviderResponseError::without_status(
2097 "provider exploded".to_string(),
2098 ),
2099 );
2100
2101 assert_eq!(error.provider_response_body(), Some("provider exploded"));
2102 assert_eq!(error.provider_response_status(), None);
2103 assert!(error.provider_response_json().is_err());
2104 }
2105
2106 #[test]
2107 fn completion_error_provider_error_is_not_a_provider_response() {
2108 let error = CompletionError::ProviderError("stream transport failed".to_string());
2111
2112 assert_eq!(error.provider_response_body(), None);
2113 assert_eq!(error.provider_response_status(), None);
2114 assert_eq!(
2115 error
2116 .provider_response_json()
2117 .expect("no body is not an error"),
2118 None
2119 );
2120 }
2121
2122 #[test]
2123 fn completion_error_provider_response_helpers_with_http_non_success_body_and_status() {
2124 let body = r#"{"error":{"type":"invalid_request","message":"bad request"}}"#;
2125 let error = CompletionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
2126 http::StatusCode::BAD_REQUEST,
2127 body.to_string(),
2128 ));
2129
2130 assert_eq!(error.provider_response_body(), Some(body));
2131 assert_eq!(
2132 error.provider_response_status(),
2133 Some(http::StatusCode::BAD_REQUEST)
2134 );
2135 assert_eq!(
2136 error.provider_response_json().expect("valid JSON body"),
2137 Some(serde_json::json!({
2138 "error": {
2139 "type": "invalid_request",
2140 "message": "bad request"
2141 }
2142 }))
2143 );
2144 }
2145
2146 #[test]
2147 fn completion_error_provider_response_helpers_with_unrelated_variant() {
2148 let error = CompletionError::ResponseError("failed to parse provider response".to_string());
2149
2150 assert_eq!(error.provider_response_body(), None);
2151 assert_eq!(error.provider_response_status(), None);
2152 assert_eq!(
2153 error
2154 .provider_response_json()
2155 .expect("no body is not an error"),
2156 None
2157 );
2158 }
2159
2160 #[test]
2161 fn provider_response_json_returns_none_for_empty_preserved_body() {
2162 let error = CompletionError::ProviderResponse(
2163 provider_response::ProviderResponseError::without_status(String::new()),
2164 );
2165
2166 assert_eq!(error.provider_response_body(), Some(""));
2167 assert_eq!(
2168 error
2169 .provider_response_json()
2170 .expect("empty body is not a JSON parse error"),
2171 None
2172 );
2173 }
2174}
2175
2176#[cfg(test)]
2177mod response_identity_tests {
2178 use super::*;
2179
2180 #[test]
2183 fn completion_response_without_request_id_still_deserializes() {
2184 let response: CompletionResponse = serde_json::from_str(
2185 r#"{"choice": [{"type": "text", "text": "hi"}],
2186 "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2,
2187 "cached_input_tokens": 0, "cache_creation_input_tokens": 0,
2188 "reasoning_tokens": 0},
2189 "provider": "test"}"#,
2190 )
2191 .expect("pre-identity CompletionResponse JSON should load");
2192 assert_eq!(response.provider_request_id, None);
2193 assert_eq!(response.identity(), ResponseIdentity::default());
2194 }
2195
2196 #[test]
2198 fn identity_accessor_mirrors_flat_fields() {
2199 let response = CompletionResponse::new(
2200 vec![crate::completion::AssistantContent::text("hi")],
2201 Usage::new(),
2202 "test",
2203 )
2204 .with_message_id("msg_1")
2205 .with_response_id("resp_1")
2206 .with_provider_request_id("req_1");
2207 assert_eq!(
2208 response.identity(),
2209 ResponseIdentity {
2210 message_id: Some("msg_1".into()),
2211 response_id: Some("resp_1".into()),
2212 provider_request_id: Some("req_1".into()),
2213 }
2214 );
2215 }
2216}