1use serde::{Deserialize, Serialize};
2use std::{convert::Infallible, str::FromStr};
3use thiserror::Error;
4
5use super::CompletionError;
6
7#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
19#[serde(tag = "role", rename_all = "lowercase")]
20pub enum Message {
21 System { content: String },
23
24 User { content: Vec<UserContent> },
26
27 Assistant {
29 id: Option<String>,
31 content: Vec<AssistantContent>,
32 },
33}
34
35pub const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
43
44pub fn require_non_empty<T, E>(items: Vec<T>, error: impl FnOnce() -> E) -> Result<Vec<T>, E> {
74 if items.is_empty() {
75 return Err(error());
76 }
77 Ok(items)
78}
79
80pub fn require_non_empty_response<T>(items: Vec<T>) -> Result<Vec<T>, CompletionError> {
87 require_non_empty(items, || {
88 CompletionError::ResponseError(EMPTY_RESPONSE_ERROR.to_owned())
89 })
90}
91
92pub fn non_empty<T>(items: Vec<T>) -> Option<Vec<T>> {
100 if items.is_empty() { None } else { Some(items) }
101}
102
103#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
107#[serde(tag = "type", rename_all = "lowercase")]
108pub enum UserContent {
109 Text(Text),
111 ToolResult(ToolResult),
113 Image(Image),
115 Audio(Audio),
117 Video(Video),
119 Document(Document),
121}
122
123#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
130#[serde(tag = "type", rename_all = "lowercase")]
131pub enum AssistantContent {
132 Text(Text),
134 ToolCall(ToolCall),
136 Reasoning(Reasoning),
138 Image(Image),
140}
141
142#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
143#[serde(tag = "type", content = "content", rename_all = "snake_case")]
144pub enum ReasoningContent {
146 Text {
148 text: String,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 signature: Option<String>,
151 },
152 Encrypted(String),
154 Redacted { data: String },
156 Summary(String),
158}
159
160#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
161pub struct Reasoning {
163 pub id: Option<String>,
165 pub content: Vec<ReasoningContent>,
167}
168
169impl Reasoning {
170 pub fn new(input: &str) -> Self {
172 Self::new_with_signature(input, None)
173 }
174
175 pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
177 Self {
178 id: None,
179 content: vec![ReasoningContent::Text {
180 text: input.to_string(),
181 signature,
182 }],
183 }
184 }
185
186 pub fn with_id(mut self, id: String) -> Self {
188 self.id = Some(id);
189 self
190 }
191
192 pub fn multi(input: Vec<String>) -> Self {
194 Self {
195 id: None,
196 content: input
197 .into_iter()
198 .map(|text| ReasoningContent::Text {
199 text,
200 signature: None,
201 })
202 .collect(),
203 }
204 }
205
206 pub fn redacted(data: impl Into<String>) -> Self {
208 Self {
209 id: None,
210 content: vec![ReasoningContent::Redacted { data: data.into() }],
211 }
212 }
213
214 pub fn encrypted(data: impl Into<String>) -> Self {
216 Self {
217 id: None,
218 content: vec![ReasoningContent::Encrypted(data.into())],
219 }
220 }
221
222 pub fn summaries(input: Vec<String>) -> Self {
224 Self {
225 id: None,
226 content: input.into_iter().map(ReasoningContent::Summary).collect(),
227 }
228 }
229
230 pub fn display_text(&self) -> String {
232 self.content
233 .iter()
234 .filter_map(|content| match content {
235 ReasoningContent::Text { text, .. } => Some(text.as_str()),
236 ReasoningContent::Summary(summary) => Some(summary.as_str()),
237 ReasoningContent::Redacted { data } => Some(data.as_str()),
238 ReasoningContent::Encrypted(_) => None,
239 })
240 .collect::<Vec<_>>()
241 .join("\n")
242 }
243
244 pub fn first_text(&self) -> Option<&str> {
246 self.content.iter().find_map(|content| match content {
247 ReasoningContent::Text { text, .. } => Some(text.as_str()),
248 _ => None,
249 })
250 }
251
252 pub fn first_signature(&self) -> Option<&str> {
254 self.content.iter().find_map(|content| match content {
255 ReasoningContent::Text {
256 signature: Some(signature),
257 ..
258 } => Some(signature.as_str()),
259 _ => None,
260 })
261 }
262
263 pub fn encrypted_content(&self) -> Option<&str> {
265 self.content.iter().find_map(|content| match content {
266 ReasoningContent::Encrypted(data) => Some(data.as_str()),
267 _ => None,
268 })
269 }
270}
271
272#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
274pub struct ToolResult {
275 pub call: ToolCallId,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub provider: Option<ProviderCallId>,
283 pub name: String,
292 pub content: Vec<ToolResultContent>,
294}
295
296impl ToolResult {
297 pub fn wire_call_id(&self) -> &str {
305 self.provider
306 .as_ref()
307 .map_or(self.call.as_str(), |provider| provider.call_id.as_str())
308 }
309}
310
311#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
313#[serde(tag = "type", rename_all = "lowercase")]
314pub enum ToolResultContent {
315 Text(Text),
317 Image(Image),
319 Json {
321 value: serde_json::Value,
323 },
324}
325
326impl ToolResultContent {
327 pub fn as_text(&self) -> Option<&str> {
329 match self {
330 Self::Text(text) => Some(&text.text),
331 Self::Image(_) | Self::Json { .. } => None,
332 }
333 }
334
335 pub fn as_json(&self) -> Option<&serde_json::Value> {
337 match self {
338 Self::Json { value } => Some(value),
339 Self::Text(_) | Self::Image(_) => None,
340 }
341 }
342
343 pub fn deserialize_json<T>(&self) -> Result<T, serde_json::Error>
350 where
351 T: serde::de::DeserializeOwned,
352 {
353 match self {
354 Self::Json { value } => serde_json::from_value(value.clone()),
355 Self::Text(text) => serde_json::from_str(&text.text),
356 Self::Image(_) => Err(<serde_json::Error as serde::de::Error>::custom(
357 "cannot decode image tool-result content as JSON",
358 )),
359 }
360 }
361}
362
363#[derive(Debug, thiserror::Error)]
368#[error("a tool-call identifier cannot be the empty string; absence is `None` or a minted id")]
369pub struct EmptyToolCallId;
370
371#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
380#[serde(try_from = "String", into = "String")]
381pub struct ToolCallId(String);
382
383impl ToolCallId {
384 pub fn new(id: impl Into<String>) -> Option<Self> {
387 let id = id.into();
388 if id.is_empty() { None } else { Some(Self(id)) }
389 }
390
391 pub fn mint() -> Self {
393 Self(crate::id::generate())
394 }
395
396 pub fn new_or_mint(id: impl Into<String>) -> Self {
399 Self::new(id).unwrap_or_else(Self::mint)
400 }
401
402 pub fn for_provider(provider: Option<&ProviderCallId>) -> Self {
412 provider
413 .and_then(|provider| Self::new(provider.call_id.clone()))
414 .unwrap_or_else(Self::mint)
415 }
416
417 pub fn as_str(&self) -> &str {
419 &self.0
420 }
421
422 pub fn into_string(self) -> String {
424 self.0
425 }
426}
427
428impl std::fmt::Display for ToolCallId {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 f.write_str(&self.0)
431 }
432}
433
434impl AsRef<str> for ToolCallId {
435 fn as_ref(&self) -> &str {
436 &self.0
437 }
438}
439
440impl std::ops::Deref for ToolCallId {
441 type Target = str;
442
443 fn deref(&self) -> &str {
444 &self.0
445 }
446}
447
448impl std::borrow::Borrow<str> for ToolCallId {
449 fn borrow(&self) -> &str {
450 &self.0
451 }
452}
453
454impl TryFrom<String> for ToolCallId {
455 type Error = EmptyToolCallId;
456
457 fn try_from(id: String) -> Result<Self, Self::Error> {
458 Self::new(id).ok_or(EmptyToolCallId)
459 }
460}
461
462impl From<ToolCallId> for String {
463 fn from(id: ToolCallId) -> Self {
464 id.0
465 }
466}
467
468impl PartialEq<str> for ToolCallId {
469 fn eq(&self, other: &str) -> bool {
470 self.0 == other
471 }
472}
473
474impl PartialEq<&str> for ToolCallId {
475 fn eq(&self, other: &&str) -> bool {
476 self.0 == *other
477 }
478}
479
480#[derive(Deserialize)]
483struct ProviderCallIdWire {
484 call_id: String,
485 #[serde(default)]
486 item_id: Option<String>,
487}
488
489#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
497#[serde(try_from = "ProviderCallIdWire")]
498pub struct ProviderCallId {
499 pub call_id: String,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub item_id: Option<String>,
505}
506
507impl ProviderCallId {
508 pub fn new(call_id: impl Into<String>) -> Option<Self> {
511 let call_id = call_id.into();
512 if call_id.is_empty() {
513 None
514 } else {
515 Some(Self {
516 call_id,
517 item_id: None,
518 })
519 }
520 }
521
522 pub fn with_item_id(mut self, item_id: impl Into<String>) -> Self {
524 let item_id = item_id.into();
525 self.item_id = (!item_id.is_empty()).then_some(item_id);
526 self
527 }
528
529 pub fn from_optional_wire(call_id: Option<String>, tool_id: Option<String>) -> Option<Self> {
543 let call_id = call_id.filter(|call_id| !call_id.is_empty());
544 match (call_id, tool_id) {
545 (Some(call_id), tool_id) => Self::new(call_id).map(|provider| match tool_id {
546 Some(tool_id) => provider.with_item_id(tool_id),
547 None => provider,
548 }),
549 (None, Some(tool_id)) => Self::new(tool_id),
550 (None, None) => None,
551 }
552 }
553}
554
555impl TryFrom<ProviderCallIdWire> for ProviderCallId {
556 type Error = EmptyToolCallId;
557
558 fn try_from(wire: ProviderCallIdWire) -> Result<Self, Self::Error> {
559 let Some(provider) = Self::new(wire.call_id) else {
560 return Err(EmptyToolCallId);
561 };
562 Ok(match wire.item_id {
563 Some(item_id) => provider.with_item_id(item_id),
564 None => provider,
565 })
566 }
567}
568
569#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
571pub struct ToolCall {
572 pub id: ToolCallId,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub provider: Option<ProviderCallId>,
581 pub function: ToolFunction,
583 #[serde(default)]
593 pub signature: Option<String>,
594 #[serde(default)]
596 pub additional_params: Option<serde_json::Value>,
597}
598
599impl ToolCall {
600 fn assemble(provider: Option<ProviderCallId>, function: ToolFunction) -> Self {
601 Self {
602 id: ToolCallId::for_provider(provider.as_ref()),
603 provider,
604 function,
605 signature: None,
606 additional_params: None,
607 }
608 }
609
610 pub fn new(id: ToolCallId, function: ToolFunction) -> Self {
612 Self {
613 id,
614 ..Self::assemble(None, function)
615 }
616 }
617
618 pub fn from_wire(wire_id: impl Into<String>, function: ToolFunction) -> Self {
621 Self::assemble(ProviderCallId::new(wire_id), function)
622 }
623
624 pub fn from_dual_wire(
628 item_id: impl Into<String>,
629 call_id: impl Into<String>,
630 function: ToolFunction,
631 ) -> Self {
632 let provider =
633 ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id.into()));
634 Self::assemble(provider, function)
635 }
636
637 pub fn with_provider(mut self, provider: ProviderCallId) -> Self {
639 self.provider = Some(provider);
640 self
641 }
642
643 pub fn wire_call_id(&self) -> &str {
651 self.provider
652 .as_ref()
653 .map_or(self.id.as_str(), |provider| provider.call_id.as_str())
654 }
655
656 pub fn with_signature(mut self, signature: Option<String>) -> Self {
657 self.signature = signature;
658 self
659 }
660
661 pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
662 self.additional_params = additional_params;
663 self
664 }
665}
666
667#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
669pub struct ToolFunction {
670 pub name: String,
672 pub arguments: serde_json::Value,
674}
675
676impl ToolFunction {
677 pub fn new(name: String, arguments: serde_json::Value) -> Self {
679 Self { name, arguments }
680 }
681}
682
683#[derive(Clone, Debug, PartialEq, Serialize)]
712#[serde(transparent)]
713pub struct AdditionalParams(serde_json::Map<String, serde_json::Value>);
714
715impl AdditionalParams {
716 pub fn new(map: serde_json::Map<String, serde_json::Value>) -> Option<Self> {
718 if map.is_empty() {
719 None
720 } else {
721 Some(Self(map))
722 }
723 }
724
725 pub fn from_entries<K, I>(entries: I) -> Option<Self>
730 where
731 K: Into<String>,
732 I: IntoIterator<Item = (K, serde_json::Value)>,
733 {
734 Self::new(
735 entries
736 .into_iter()
737 .map(|(key, value)| (key.into(), value))
738 .collect(),
739 )
740 }
741
742 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
744 self.0.get(key)
745 }
746
747 pub fn as_map(&self) -> &serde_json::Map<String, serde_json::Value> {
749 &self.0
750 }
751
752 pub fn into_value(self) -> serde_json::Value {
754 serde_json::Value::Object(self.0)
755 }
756
757 pub fn merge(&mut self, incoming: Self) {
761 fn merge_maps(
765 existing: &mut serde_json::Map<String, serde_json::Value>,
766 incoming: serde_json::Map<String, serde_json::Value>,
767 ) {
768 for (key, incoming_value) in incoming {
769 match existing.get_mut(&key) {
770 Some(existing_value) => merge_value(existing_value, incoming_value),
771 None => {
772 existing.insert(key, incoming_value);
773 }
774 }
775 }
776 }
777 fn merge_value(existing: &mut serde_json::Value, incoming: serde_json::Value) {
778 match (existing, incoming) {
779 (
780 serde_json::Value::Object(existing_map),
781 serde_json::Value::Object(incoming_map),
782 ) => merge_maps(existing_map, incoming_map),
783 (
784 serde_json::Value::Array(existing_array),
785 serde_json::Value::Array(mut incoming_array),
786 ) => existing_array.append(&mut incoming_array),
787 (existing, incoming) => *existing = incoming,
788 }
789 }
790 merge_maps(&mut self.0, incoming.0);
791 }
792
793 pub fn wire_extras(
803 &self,
804 wire_key: &str,
805 ) -> Option<&serde_json::Map<String, serde_json::Value>> {
806 self.0.get(wire_key).and_then(serde_json::Value::as_object)
807 }
808
809 pub fn into_wire_extras(
813 mut self,
814 wire_key: &str,
815 ) -> Option<serde_json::Map<String, serde_json::Value>> {
816 match self.0.remove(wire_key) {
817 Some(serde_json::Value::Object(map)) => Some(map),
818 _ => None,
819 }
820 }
821
822 pub fn try_from_value(value: serde_json::Value) -> Result<Option<Self>, serde_json::Value> {
827 match value {
828 serde_json::Value::Null => Ok(None),
829 serde_json::Value::Object(map) => Ok(Self::new(map)),
830 other => Err(other),
831 }
832 }
833}
834
835impl From<AdditionalParams> for serde_json::Value {
836 fn from(params: AdditionalParams) -> Self {
837 params.into_value()
838 }
839}
840
841impl std::ops::Index<&str> for AdditionalParams {
842 type Output = serde_json::Value;
843
844 #[allow(clippy::indexing_slicing)]
847 fn index(&self, key: &str) -> &serde_json::Value {
848 &self.0[key]
849 }
850}
851
852impl<'de> Deserialize<'de> for AdditionalParams {
853 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
854 where
855 D: serde::Deserializer<'de>,
856 {
857 match Self::try_from_value(serde_json::Value::deserialize(deserializer)?) {
858 Ok(Some(params)) => Ok(params),
859 Ok(None) => Err(serde::de::Error::custom(
862 "`additional_params` carries no data — omit the field (an `Option` \
863 field routed through `optional_additional_params` canonicalizes \
864 `{}` and `null` to absent)",
865 )),
866 Err(_) => Err(serde::de::Error::custom(
867 "`additional_params` must be a non-empty JSON object",
868 )),
869 }
870 }
871}
872
873pub fn keys_lost_in_round_trip(
909 original: &serde_json::Value,
910 round_tripped: &serde_json::Value,
911) -> Vec<String> {
912 fn walk(
913 original: &serde_json::Value,
914 round_tripped: &serde_json::Value,
915 path: &mut String,
916 lost: &mut Vec<String>,
917 ) {
918 match (original, round_tripped) {
919 (serde_json::Value::Object(original_map), serde_json::Value::Object(round_map)) => {
920 for (key, original_value) in original_map {
921 if original_value.is_null() {
922 continue;
923 }
924 let checkpoint = path.len();
925 if !path.is_empty() {
926 path.push('.');
927 }
928 path.push_str(key);
929 match round_map.get(key) {
930 Some(round_value) => walk(original_value, round_value, path, lost),
931 None => {
936 if !original_value
937 .as_object()
938 .is_some_and(serde_json::Map::is_empty)
939 {
940 lost.push(path.clone());
941 }
942 }
943 }
944 path.truncate(checkpoint);
945 }
946 }
947 (serde_json::Value::Array(original_items), serde_json::Value::Array(round_items)) => {
948 for (index, original_value) in original_items.iter().enumerate() {
949 let checkpoint = path.len();
950 if !path.is_empty() {
951 path.push('.');
952 }
953 path.push_str(&index.to_string());
954 match round_items.get(index) {
955 Some(round_value) => walk(original_value, round_value, path, lost),
956 None => lost.push(path.clone()),
957 }
958 path.truncate(checkpoint);
959 }
960 }
961 (original, round_tripped) => {
962 if original != round_tripped {
963 lost.push(path.clone());
964 }
965 }
966 }
967 }
968
969 let mut lost = Vec::new();
970 walk(original, round_tripped, &mut String::new(), &mut lost);
971 lost
972}
973
974pub fn optional_additional_params<'de, D>(
982 deserializer: D,
983) -> Result<Option<AdditionalParams>, D::Error>
984where
985 D: serde::Deserializer<'de>,
986{
987 match Option::<serde_json::Value>::deserialize(deserializer)? {
988 None => Ok(None),
989 Some(value) => AdditionalParams::try_from_value(value).map_err(|_| {
990 serde::de::Error::custom("`additional_params` must be a JSON object (or null)")
991 }),
992 }
993}
994
995#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1006pub struct Text {
1007 pub text: String,
1009 #[serde(
1011 default,
1012 deserialize_with = "optional_additional_params",
1013 skip_serializing_if = "Option::is_none"
1014 )]
1015 pub additional_params: Option<AdditionalParams>,
1016}
1017
1018impl Text {
1019 pub fn new(text: impl Into<String>) -> Self {
1021 Self {
1022 text: text.into(),
1023 additional_params: None,
1024 }
1025 }
1026
1027 pub fn text(&self) -> &str {
1029 &self.text
1030 }
1031}
1032
1033impl std::fmt::Display for Text {
1034 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1035 let Self { text, .. } = self;
1036 write!(f, "{text}")
1037 }
1038}
1039
1040#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1042pub struct Image {
1043 pub data: DocumentSourceKind,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub media_type: Option<ImageMediaType>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub detail: Option<ImageDetail>,
1051 #[serde(
1053 default,
1054 deserialize_with = "optional_additional_params",
1055 skip_serializing_if = "Option::is_none"
1056 )]
1057 pub additional_params: Option<AdditionalParams>,
1058}
1059
1060#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1062#[serde(tag = "type", content = "value", rename_all = "camelCase")]
1063pub enum DocumentSourceKind {
1064 Url(String),
1066 Base64(String),
1068 FileId(String),
1070 Raw(Vec<u8>),
1072 String(String),
1074 #[default]
1075 Unknown,
1077}
1078
1079impl DocumentSourceKind {
1080 pub fn url(url: &str) -> Self {
1082 Self::Url(url.to_string())
1083 }
1084
1085 pub fn base64(base64_string: &str) -> Self {
1087 Self::Base64(base64_string.to_string())
1088 }
1089
1090 pub fn file_id(file_id: &str) -> Self {
1092 Self::FileId(file_id.to_string())
1093 }
1094
1095 pub fn string(input: &str) -> Self {
1097 Self::String(input.into())
1098 }
1099
1100 pub fn try_into_inner(self) -> Option<String> {
1102 match self {
1103 Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
1104 _ => None,
1105 }
1106 }
1107}
1108
1109impl std::fmt::Display for DocumentSourceKind {
1110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111 match self {
1112 Self::Url(string) => write!(f, "{string}"),
1113 Self::Base64(string) => write!(f, "{string}"),
1114 Self::FileId(string) => write!(f, "{string}"),
1115 Self::String(string) => write!(f, "{string}"),
1116 Self::Raw(_) => write!(f, "<binary data>"),
1117 Self::Unknown => write!(f, "<unknown>"),
1118 }
1119 }
1120}
1121
1122#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1124pub struct Audio {
1125 pub data: DocumentSourceKind,
1127 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub media_type: Option<AudioMediaType>,
1130 #[serde(
1132 default,
1133 deserialize_with = "optional_additional_params",
1134 skip_serializing_if = "Option::is_none"
1135 )]
1136 pub additional_params: Option<AdditionalParams>,
1137}
1138
1139#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1141pub struct Video {
1142 pub data: DocumentSourceKind,
1144 #[serde(skip_serializing_if = "Option::is_none")]
1146 pub media_type: Option<VideoMediaType>,
1147 #[serde(
1149 default,
1150 deserialize_with = "optional_additional_params",
1151 skip_serializing_if = "Option::is_none"
1152 )]
1153 pub additional_params: Option<AdditionalParams>,
1154}
1155
1156#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1158pub struct Document {
1159 pub data: DocumentSourceKind,
1161 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub media_type: Option<DocumentMediaType>,
1164 #[serde(
1166 default,
1167 deserialize_with = "optional_additional_params",
1168 skip_serializing_if = "Option::is_none"
1169 )]
1170 pub additional_params: Option<AdditionalParams>,
1171}
1172
1173#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1175#[serde(rename_all = "lowercase")]
1176pub enum ContentFormat {
1177 #[default]
1178 Base64,
1179 String,
1180 Url,
1181}
1182
1183#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1185pub enum MediaType {
1186 Image(ImageMediaType),
1187 Audio(AudioMediaType),
1188 Document(DocumentMediaType),
1189 Video(VideoMediaType),
1190}
1191
1192#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1195#[serde(rename_all = "lowercase")]
1196pub enum ImageMediaType {
1197 JPEG,
1198 PNG,
1199 GIF,
1200 WEBP,
1201 HEIC,
1202 HEIF,
1203 SVG,
1204}
1205
1206#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1210#[serde(rename_all = "lowercase")]
1211pub enum DocumentMediaType {
1212 PDF,
1213 TXT,
1214 RTF,
1215 HTML,
1216 CSS,
1217 MARKDOWN,
1218 CSV,
1219 XML,
1220 Javascript,
1221 Python,
1222}
1223
1224impl DocumentMediaType {
1225 pub fn is_code(&self) -> bool {
1226 matches!(self, Self::Javascript | Self::Python)
1227 }
1228}
1229
1230#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1233#[serde(rename_all = "lowercase")]
1234pub enum AudioMediaType {
1235 WAV,
1236 MP3,
1237 AIFF,
1238 AAC,
1239 OGG,
1240 FLAC,
1241 M4A,
1242 PCM16,
1243 PCM24,
1244}
1245
1246#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1249#[serde(rename_all = "lowercase")]
1250pub enum VideoMediaType {
1251 AVI,
1252 MP4,
1253 MPEG,
1254 MOV,
1255 WEBM,
1256}
1257
1258#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1260#[serde(rename_all = "lowercase")]
1261pub enum ImageDetail {
1262 Low,
1263 High,
1264 #[default]
1265 Auto,
1266}
1267
1268impl Message {
1273 pub fn rag_text(&self) -> Option<String> {
1276 match self {
1277 Message::User { content } => {
1278 for item in content.iter() {
1279 if let UserContent::Text(Text { text, .. }) = item {
1280 return Some(text.clone());
1281 }
1282 }
1283 None
1284 }
1285 Message::System { .. } => None,
1286 _ => None,
1287 }
1288 }
1289
1290 pub fn system(text: impl Into<String>) -> Self {
1292 Message::System {
1293 content: text.into(),
1294 }
1295 }
1296
1297 pub fn user(text: impl Into<String>) -> Self {
1299 Message::User {
1300 content: vec![UserContent::text(text)],
1301 }
1302 }
1303
1304 pub fn assistant(text: impl Into<String>) -> Self {
1306 Message::Assistant {
1307 id: None,
1308 content: vec![AssistantContent::text(text)],
1309 }
1310 }
1311
1312 pub fn tool_result(
1318 call: impl Into<String>,
1319 name: impl Into<String>,
1320 content: impl Into<String>,
1321 ) -> Self {
1322 Message::User {
1323 content: vec![UserContent::tool_result(
1324 call,
1325 name,
1326 vec![ToolResultContent::text(content)],
1327 )],
1328 }
1329 }
1330}
1331
1332macro_rules! media_ctors {
1337 () => {};
1338 (
1339 $(#[$meta:meta])* $name:ident => Image($kind:ident: $data:ty);
1340 $($rest:tt)*
1341 ) => {
1342 $(#[$meta])*
1343 pub fn $name(
1344 data: impl Into<$data>,
1345 media_type: Option<ImageMediaType>,
1346 detail: Option<ImageDetail>,
1347 ) -> Self {
1348 Self::Image(Image {
1349 data: DocumentSourceKind::$kind(data.into()),
1350 media_type,
1351 detail,
1352 additional_params: None,
1353 })
1354 }
1355 media_ctors! { $($rest)* }
1356 };
1357 (
1358 $(#[$meta:meta])* $name:ident => $variant:ident($mt:ty, $kind:ident: $data:ty);
1359 $($rest:tt)*
1360 ) => {
1361 $(#[$meta])*
1362 pub fn $name(data: impl Into<$data>, media_type: Option<$mt>) -> Self {
1363 Self::$variant($variant {
1364 data: DocumentSourceKind::$kind(data.into()),
1365 media_type,
1366 additional_params: None,
1367 })
1368 }
1369 media_ctors! { $($rest)* }
1370 };
1371}
1372
1373impl UserContent {
1374 pub fn text(text: impl Into<String>) -> Self {
1376 UserContent::Text(text.into().into())
1377 }
1378
1379 media_ctors! {
1380 image_base64 => Image(Base64: String);
1382 image_raw => Image(Raw: Vec<u8>);
1384 image_url => Image(Url: String);
1386 audio => Audio(AudioMediaType, Base64: String);
1388 audio_raw => Audio(AudioMediaType, Raw: Vec<u8>);
1390 audio_url => Audio(AudioMediaType, Url: String);
1392 video => Video(VideoMediaType, Base64: String);
1394 video_raw => Video(VideoMediaType, Raw: Vec<u8>);
1396 video_url => Video(VideoMediaType, Url: String);
1398 document_raw => Document(DocumentMediaType, Raw: Vec<u8>);
1400 document_url => Document(DocumentMediaType, Url: String);
1402 }
1403
1404 pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
1407 let data: String = data.into();
1408 UserContent::Document(Document {
1409 data: DocumentSourceKind::string(&data),
1410 media_type,
1411 additional_params: None,
1412 })
1413 }
1414
1415 pub fn tool_result(
1428 call: impl Into<String>,
1429 name: impl Into<String>,
1430 content: Vec<ToolResultContent>,
1431 ) -> Self {
1432 UserContent::ToolResult(ToolResult {
1433 call: ToolCallId::new_or_mint(call),
1434 provider: None,
1435 name: name.into(),
1436 content,
1437 })
1438 }
1439
1440 pub fn tool_result_from_wire(
1446 wire_id: impl Into<String>,
1447 name: impl Into<String>,
1448 content: Vec<ToolResultContent>,
1449 ) -> Self {
1450 let provider = ProviderCallId::new(wire_id);
1451 let call = ToolCallId::for_provider(provider.as_ref());
1452 Self::tool_result_for(call, provider, name, content)
1453 }
1454
1455 pub fn tool_result_for(
1460 call: ToolCallId,
1461 provider: Option<ProviderCallId>,
1462 name: impl Into<String>,
1463 content: Vec<ToolResultContent>,
1464 ) -> Self {
1465 UserContent::ToolResult(ToolResult {
1466 call,
1467 provider,
1468 name: name.into(),
1469 content,
1470 })
1471 }
1472
1473 pub fn tool_result_with_call_id(
1477 item_id: impl Into<String>,
1478 call_id: impl Into<String>,
1479 name: impl Into<String>,
1480 content: Vec<ToolResultContent>,
1481 ) -> Self {
1482 let provider = ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id));
1483 let call = ToolCallId::for_provider(provider.as_ref());
1484 Self::tool_result_for(call, provider, name, content)
1485 }
1486}
1487
1488impl AssistantContent {
1489 pub fn text(text: impl Into<String>) -> Self {
1491 AssistantContent::Text(text.into().into())
1492 }
1493
1494 media_ctors! {
1495 image_base64 => Image(Base64: String);
1497 }
1498
1499 pub fn tool_call(
1504 id: impl Into<String>,
1505 name: impl Into<String>,
1506 arguments: serde_json::Value,
1507 ) -> Self {
1508 AssistantContent::ToolCall(ToolCall::from_wire(
1509 id,
1510 ToolFunction {
1511 name: name.into(),
1512 arguments,
1513 },
1514 ))
1515 }
1516
1517 pub fn tool_call_with_call_id(
1520 id: impl Into<String>,
1521 call_id: String,
1522 name: impl Into<String>,
1523 arguments: serde_json::Value,
1524 ) -> Self {
1525 AssistantContent::ToolCall(ToolCall::from_dual_wire(
1526 id,
1527 call_id,
1528 ToolFunction {
1529 name: name.into(),
1530 arguments,
1531 },
1532 ))
1533 }
1534
1535 pub fn reasoning(reasoning: impl AsRef<str>) -> Self {
1536 AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()))
1537 }
1538}
1539
1540impl ToolResultContent {
1541 pub fn text(text: impl Into<String>) -> Self {
1543 ToolResultContent::Text(text.into().into())
1544 }
1545
1546 pub fn json(value: serde_json::Value) -> Self {
1548 ToolResultContent::Json { value }
1549 }
1550
1551 media_ctors! {
1552 image_base64 => Image(Base64: String);
1554 image_raw => Image(Raw: Vec<u8>);
1556 image_url => Image(Url: String);
1558 }
1559}
1560
1561pub trait MimeType {
1563 fn from_mime_type(mime_type: &str) -> Option<Self>
1564 where
1565 Self: Sized;
1566 fn to_mime_type(&self) -> &'static str;
1567}
1568
1569impl MimeType for MediaType {
1570 fn from_mime_type(mime_type: &str) -> Option<Self> {
1571 ImageMediaType::from_mime_type(mime_type)
1572 .map(MediaType::Image)
1573 .or_else(|| DocumentMediaType::from_mime_type(mime_type).map(MediaType::Document))
1574 .or_else(|| AudioMediaType::from_mime_type(mime_type).map(MediaType::Audio))
1575 .or_else(|| VideoMediaType::from_mime_type(mime_type).map(MediaType::Video))
1576 }
1577
1578 fn to_mime_type(&self) -> &'static str {
1579 match self {
1580 MediaType::Image(media_type) => media_type.to_mime_type(),
1581 MediaType::Audio(media_type) => media_type.to_mime_type(),
1582 MediaType::Document(media_type) => media_type.to_mime_type(),
1583 MediaType::Video(media_type) => media_type.to_mime_type(),
1584 }
1585 }
1586}
1587
1588macro_rules! impl_mime_type {
1593 ($ty:ident { $($variant:ident => $canonical:literal $(| $alias:literal)*),+ $(,)? }) => {
1594 impl MimeType for $ty {
1595 fn from_mime_type(mime_type: &str) -> Option<Self> {
1596 match mime_type {
1597 $($canonical $(| $alias)* => Some($ty::$variant),)+
1598 _ => None,
1599 }
1600 }
1601
1602 fn to_mime_type(&self) -> &'static str {
1603 match self {
1604 $($ty::$variant => $canonical,)+
1605 }
1606 }
1607 }
1608 };
1609}
1610
1611impl_mime_type!(ImageMediaType {
1612 JPEG => "image/jpeg",
1613 PNG => "image/png",
1614 GIF => "image/gif",
1615 WEBP => "image/webp",
1616 HEIC => "image/heic",
1617 HEIF => "image/heif",
1618 SVG => "image/svg+xml",
1619});
1620
1621impl_mime_type!(DocumentMediaType {
1622 PDF => "application/pdf",
1623 TXT => "text/plain",
1624 RTF => "text/rtf",
1625 HTML => "text/html",
1626 CSS => "text/css",
1627 MARKDOWN => "text/markdown" | "text/md",
1628 CSV => "text/csv",
1629 XML => "text/xml",
1630 Javascript => "application/x-javascript" | "text/x-javascript",
1631 Python => "application/x-python" | "text/x-python",
1632});
1633
1634impl_mime_type!(AudioMediaType {
1635 WAV => "audio/wav",
1636 MP3 => "audio/mp3",
1637 AIFF => "audio/aiff",
1638 AAC => "audio/aac",
1639 OGG => "audio/ogg",
1640 FLAC => "audio/flac",
1641 M4A => "audio/m4a",
1642 PCM16 => "audio/pcm16",
1643 PCM24 => "audio/pcm24",
1644});
1645
1646impl_mime_type!(VideoMediaType {
1647 AVI => "video/avi",
1648 MP4 => "video/mp4",
1649 MPEG => "video/mpeg",
1650 MOV => "video/mov",
1651 WEBM => "video/webm",
1652});
1653
1654impl std::str::FromStr for ImageDetail {
1655 type Err = ();
1656
1657 fn from_str(s: &str) -> Result<Self, Self::Err> {
1658 match s.to_lowercase().as_str() {
1659 "low" => Ok(ImageDetail::Low),
1660 "high" => Ok(ImageDetail::High),
1661 "auto" => Ok(ImageDetail::Auto),
1662 _ => Err(()),
1663 }
1664 }
1665}
1666
1667macro_rules! text_from {
1673 ($($src:ty),+ $(,)?) => {$(
1674 impl From<$src> for Text {
1675 fn from(text: $src) -> Self {
1676 Text {
1677 text: text.into(),
1678 additional_params: None,
1679 }
1680 }
1681 }
1682 )+};
1683}
1684
1685text_from!(String, &String, &str);
1686
1687macro_rules! text_content_from_string {
1689 ($($ty:ident),+ $(,)?) => {$(
1690 impl From<String> for $ty {
1691 fn from(text: String) -> Self {
1692 $ty::text(text)
1693 }
1694 }
1695 )+};
1696}
1697
1698text_content_from_string!(ToolResultContent, AssistantContent, UserContent);
1699
1700macro_rules! single_content_message_from {
1703 (User { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1704 impl From<$src> for Message {
1705 fn from(value: $src) -> Self {
1706 Message::User {
1707 content: vec![UserContent::$variant(value.into())],
1708 }
1709 }
1710 }
1711 )+};
1712 (Assistant { $($src:ty => $variant:ident),+ $(,)? }) => {$(
1713 impl From<$src> for Message {
1714 fn from(value: $src) -> Self {
1715 Message::Assistant {
1716 id: None,
1717 content: vec![AssistantContent::$variant(value.into())],
1718 }
1719 }
1720 }
1721 )+};
1722}
1723
1724single_content_message_from!(User {
1725 String => Text,
1726 &str => Text,
1727 &String => Text,
1728 Text => Text,
1729 Image => Image,
1730 Audio => Audio,
1731 Document => Document,
1732 ToolResult => ToolResult,
1733});
1734
1735single_content_message_from!(Assistant {
1736 ToolCall => ToolCall,
1737});
1738
1739impl FromStr for Text {
1740 type Err = Infallible;
1741
1742 fn from_str(s: &str) -> Result<Self, Self::Err> {
1743 Ok(s.into())
1744 }
1745}
1746
1747impl From<&Message> for Message {
1748 fn from(msg: &Message) -> Self {
1749 msg.clone()
1750 }
1751}
1752
1753impl From<AssistantContent> for Message {
1754 fn from(content: AssistantContent) -> Self {
1755 Message::Assistant {
1756 id: None,
1757 content: vec![content],
1758 }
1759 }
1760}
1761
1762impl From<UserContent> for Message {
1763 fn from(content: UserContent) -> Self {
1764 Message::User {
1765 content: vec![content],
1766 }
1767 }
1768}
1769
1770impl From<Vec<AssistantContent>> for Message {
1771 fn from(content: Vec<AssistantContent>) -> Self {
1772 Message::Assistant { id: None, content }
1773 }
1774}
1775
1776impl From<Vec<UserContent>> for Message {
1777 fn from(content: Vec<UserContent>) -> Self {
1778 Message::User { content }
1779 }
1780}
1781
1782impl From<ToolResultContent> for Message {
1783 fn from(tool_result_content: ToolResultContent) -> Self {
1784 Message::User {
1785 content: vec![UserContent::ToolResult(ToolResult {
1786 call: ToolCallId::mint(),
1787 provider: None,
1788 name: String::new(),
1789 content: vec![tool_result_content],
1790 })],
1791 }
1792 }
1793}
1794
1795#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1796#[serde(rename_all = "snake_case")]
1797pub enum ToolChoice {
1798 #[default]
1799 Auto,
1800 None,
1801 Required,
1802 Specific {
1803 function_names: Vec<String>,
1804 },
1805}
1806
1807#[derive(Debug, Error)]
1813pub enum MessageError {
1814 #[error("Message conversion error: {0}")]
1815 ConversionError(String),
1816}
1817
1818impl From<MessageError> for CompletionError {
1819 fn from(error: MessageError) -> Self {
1820 CompletionError::RequestError(error.into())
1821 }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826 use serde::{Deserialize, Serialize};
1827
1828 use super::{AdditionalParams, Message, Reasoning, ReasoningContent, Text, ToolResultContent};
1829
1830 mod vec_content_serde {
1831 use super::super::{AssistantContent, Message, UserContent};
1832
1833 #[test]
1834 fn message_content_still_serializes_as_a_plain_sequence() {
1835 let message = Message::User {
1839 content: vec![UserContent::text("hi")],
1840 };
1841 let json = serde_json::to_value(&message).expect("serialize");
1842 assert_eq!(
1843 json,
1844 serde_json::json!({
1845 "role": "user",
1846 "content": [{"type": "text", "text": "hi"}],
1847 })
1848 );
1849 }
1850
1851 #[test]
1852 fn message_content_round_trips_byte_identically() {
1853 let message = Message::Assistant {
1854 id: Some("msg_1".to_owned()),
1855 content: vec![AssistantContent::text("hello")],
1856 };
1857 let encoded = serde_json::to_string(&message).expect("serialize");
1858 let decoded: Message = serde_json::from_str(&encoded).expect("deserialize");
1859 assert_eq!(
1860 serde_json::to_string(&decoded).expect("re-serialize"),
1861 encoded
1862 );
1863 }
1864
1865 #[test]
1866 fn an_empty_content_array_now_deserializes() {
1867 let message: Message =
1871 serde_json::from_value(serde_json::json!({"role": "user", "content": []}))
1872 .expect("an empty content list is representable now");
1873 let Message::User { content } = message else {
1874 panic!("expected a user message");
1875 };
1876 assert!(content.is_empty());
1877 }
1878 }
1879
1880 #[test]
1881 fn reasoning_constructors_and_accessors_work() {
1882 let single = Reasoning::new("think");
1883 assert_eq!(single.first_text(), Some("think"));
1884 assert_eq!(single.first_signature(), None);
1885
1886 let signed = Reasoning::new_with_signature("signed", Some("sig-1".to_string()));
1887 assert_eq!(signed.first_text(), Some("signed"));
1888 assert_eq!(signed.first_signature(), Some("sig-1"));
1889
1890 let multi = Reasoning::multi(vec!["a".to_string(), "b".to_string()]);
1891 assert_eq!(multi.display_text(), "a\nb");
1892 assert_eq!(multi.first_text(), Some("a"));
1893
1894 let redacted = Reasoning::redacted("redacted-value");
1895 assert_eq!(redacted.display_text(), "redacted-value");
1896 assert_eq!(redacted.first_text(), None);
1897
1898 let encrypted = Reasoning::encrypted("enc");
1899 assert_eq!(encrypted.encrypted_content(), Some("enc"));
1900 assert_eq!(encrypted.display_text(), "");
1901
1902 let summaries = Reasoning::summaries(vec!["s1".to_string(), "s2".to_string()]);
1903 assert_eq!(summaries.display_text(), "s1\ns2");
1904 assert_eq!(summaries.encrypted_content(), None);
1905 }
1906
1907 #[test]
1908 fn reasoning_content_serde_roundtrip() {
1909 let variants = vec![
1910 ReasoningContent::Text {
1911 text: "plain".to_string(),
1912 signature: Some("sig".to_string()),
1913 },
1914 ReasoningContent::Encrypted("opaque".to_string()),
1915 ReasoningContent::Redacted {
1916 data: "redacted".to_string(),
1917 },
1918 ReasoningContent::Summary("summary".to_string()),
1919 ];
1920
1921 for variant in variants {
1922 let json = serde_json::to_string(&variant).expect("serialize");
1923 let roundtrip: ReasoningContent = serde_json::from_str(&json).expect("deserialize");
1924 assert_eq!(roundtrip, variant);
1925 }
1926 }
1927
1928 #[test]
1929 fn system_message_constructor_and_serde_roundtrip() {
1930 let message = Message::system("You are concise.");
1931
1932 match &message {
1933 Message::System { content } => assert_eq!(content, "You are concise."),
1934 _ => panic!("Expected system message"),
1935 }
1936
1937 let json = serde_json::to_string(&message).expect("serialize");
1938 let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
1939 assert_eq!(roundtrip, message);
1940 }
1941
1942 #[test]
1943 fn current_schema_tool_call_json_round_trips_without_provider_promotion() {
1944 let call = super::ToolCall::new(
1947 super::ToolCallId::new("minted-handle").expect("non-empty"),
1948 super::ToolFunction {
1949 name: "add".to_string(),
1950 arguments: serde_json::json!({}),
1951 },
1952 );
1953
1954 let json = serde_json::to_value(&call).expect("serialize");
1955 assert!(json.get("call_id").is_none());
1956 let roundtrip: super::ToolCall = serde_json::from_value(json).expect("deserialize");
1957 assert_eq!(roundtrip.provider, None);
1958 assert_eq!(roundtrip, call);
1959 }
1960
1961 #[test]
1962 fn empty_params_canonicalize_to_none_in_both_serde_directions() {
1963 for empty_spelling in [serde_json::json!({}), serde_json::Value::Null] {
1970 let text: Text = serde_json::from_value(
1971 serde_json::json!({"text": "x", "additional_params": empty_spelling}),
1972 )
1973 .expect("deserialize");
1974 assert_eq!(text.additional_params, None);
1975 }
1976
1977 let text: Text = serde_json::from_value(
1982 serde_json::json!({"text": "x", "additional_params": {"citations": [1]}}),
1983 )
1984 .expect("deserialize");
1985 assert_eq!(
1986 text.additional_params,
1987 AdditionalParams::from_entries([("citations", serde_json::json!([1]))])
1988 );
1989 assert_eq!(
1990 text.additional_params
1991 .as_ref()
1992 .and_then(|params| params.get("citations")),
1993 Some(&serde_json::json!([1]))
1994 );
1995 let round: Text = serde_json::from_value(serde_json::to_value(&text).expect("serialize"))
1996 .expect("round trip");
1997 assert_eq!(round, text);
1998
1999 assert_eq!(AdditionalParams::new(serde_json::Map::new()), None);
2002 assert_eq!(
2003 AdditionalParams::try_from_value(serde_json::json!({})).expect("object"),
2004 None
2005 );
2006
2007 let tolerant: Text = serde_json::from_value(
2013 serde_json::json!({"text": "x", "citations": ["stray"], "future_field": 1}),
2014 )
2015 .expect("unknown keys on a block must not fail the decode");
2016 assert_eq!(tolerant.text, "x");
2017 assert_eq!(tolerant.additional_params, None);
2018
2019 for malformed in [serde_json::json!([]), serde_json::json!("title")] {
2024 let err = serde_json::from_value::<Text>(
2025 serde_json::json!({"text": "x", "additional_params": malformed}),
2026 )
2027 .expect_err("non-object params must be a decode error");
2028 assert!(
2029 err.to_string().contains("must be a JSON object"),
2030 "unexpected error: {err}"
2031 );
2032 assert!(
2033 AdditionalParams::try_from_value(serde_json::json!([])).is_err(),
2034 "try_from_value must hand a non-object back, not swallow it"
2035 );
2036 }
2037 }
2038
2039 #[test]
2040 fn round_trip_diff_recipe_detects_every_dropped_key() {
2041 let migrated = serde_json::json!({
2050 "role": "assistant",
2051 "content": [
2052 {"type": "text", "text": "cited", "citations": ["not re-nested"]},
2053 {"type": "text", "text": "clean",
2054 "additional_params": {"citations": ["re-nested"]}},
2055 ],
2056 });
2057 let loaded: Message =
2058 serde_json::from_value(migrated.clone()).expect("tolerant decode must succeed");
2059 let reserialized = serde_json::to_value(&loaded).expect("serialize");
2060 assert_eq!(
2061 super::keys_lost_in_round_trip(&migrated, &reserialized),
2062 vec!["content.0.citations".to_string()],
2063 "every dropped key must be reported by path, and only dropped keys \
2064 — writer-added defaults are not differences"
2065 );
2066
2067 let clean = serde_json::json!({
2072 "role": "assistant",
2073 "content": [
2074 {"type": "text", "text": "clean",
2075 "additional_params": {"citations": ["re-nested"]}},
2076 {"type": "text", "text": "mechanically migrated",
2077 "additional_params": {}},
2078 ],
2079 });
2080 let loaded: Message = serde_json::from_value(clean.clone()).expect("decode");
2081 let reserialized = serde_json::to_value(&loaded).expect("serialize");
2082 assert_eq!(
2083 super::keys_lost_in_round_trip(&clean, &reserialized),
2084 Vec::<String>::new(),
2085 "clean history must survive the round trip whole"
2086 );
2087 }
2088
2089 #[test]
2090 fn legacy_call_id_key_is_ignored_not_lifted() {
2091 let legacy = serde_json::json!({
2097 "id": "fc_123",
2098 "call_id": "call_abc",
2099 "function": {"name": "add", "arguments": {"x": 1}},
2100 });
2101
2102 let call: super::ToolCall = serde_json::from_value(legacy).expect("deserialize");
2103 assert_eq!(call.id, "fc_123");
2104 assert_eq!(call.provider, None);
2105 }
2106
2107 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2108 struct ExecutorLikeResponse {
2109 output: serde_json::Value,
2110 logs: Vec<String>,
2111 execution_time_ms: u64,
2112 }
2113
2114 #[test]
2115 fn tool_result_content_decodes_structured_and_legacy_json() {
2116 let response = ExecutorLikeResponse {
2117 output: serde_json::json!({"answer": 42}),
2118 logs: vec!["computed".to_string()],
2119 execution_time_ms: 7,
2120 };
2121 let value = serde_json::to_value(&response).expect("serialize response");
2122
2123 let structured = ToolResultContent::json(value.clone());
2124 assert_eq!(structured.as_json(), Some(&value));
2125 assert_eq!(structured.as_text(), None);
2126 assert_eq!(
2127 structured
2128 .deserialize_json::<ExecutorLikeResponse>()
2129 .expect("decode structured response"),
2130 response
2131 );
2132
2133 let legacy_json = value.to_string();
2134 let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
2135 assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
2136 assert_eq!(legacy_text.as_json(), None);
2137 assert_eq!(
2138 legacy_text
2139 .deserialize_json::<ExecutorLikeResponse>()
2140 .expect("decode legacy response"),
2141 response
2142 );
2143
2144 let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
2145 let image_error = image.deserialize_json::<ExecutorLikeResponse>();
2146 assert!(image_error.is_err());
2147 if let Err(error) = image_error {
2148 assert_eq!(
2149 error.to_string(),
2150 "cannot decode image tool-result content as JSON"
2151 );
2152 }
2153 }
2154}