1use std::borrow::Cow;
63use std::collections::{HashMap, HashSet};
64use std::sync::Arc;
65
66use base64::Engine as _;
67use base64::prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD};
68
69use crate::conversation::pairing::is_result;
70use crate::message::{
71 Api, AssistantContent, AssistantMessage, CallId, DocumentData, DocumentMediaType,
72 DocumentSourceKind, ImageMediaType, Message, Origin, Text, ToolResult, ToolResultContent,
73 UserContent,
74};
75use crate::wasm_compat::WasmCompatSync;
76
77pub const USER_IMAGE_OMITTED: &str = "(image omitted: model does not support images)";
79
80pub const ASSISTANT_IMAGE_OMITTED: &str = "(image omitted: model does not support images)";
83
84pub const TOOL_IMAGE_OMITTED: &str = "(tool image omitted: model does not support images)";
86
87pub const IMAGE_UNSENDABLE: &str = "(image omitted: the provider cannot receive it in this form)";
89
90pub const AUDIO_UNSENDABLE: &str = "(audio omitted: the provider cannot receive it in this form)";
92
93pub const VIDEO_UNSENDABLE: &str = "(video omitted: the provider cannot receive it in this form)";
95
96pub const DOCUMENT_UNSENDABLE: &str =
99 "(document omitted: the provider cannot receive it in this form)";
100
101pub const TOOL_IMAGE_ATTACHED: &str = "(see attached image)";
104
105pub const TOOL_IMAGES_HEADING: &str = "Attached image(s) from tool result:";
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct Accepts {
111 pub user_images: bool,
113 pub assistant_images: bool,
115 pub tool_result_images: bool,
117 pub tools: bool,
119 pub foreign_reasoning: bool,
126}
127
128impl Accepts {
129 pub const ALL: Self = Self {
132 user_images: true,
133 assistant_images: true,
134 tool_result_images: true,
135 tools: true,
136 foreign_reasoning: false,
137 };
138
139 pub const TEXT: Self = Self {
142 user_images: false,
143 assistant_images: false,
144 tool_result_images: false,
145 tools: true,
146 foreign_reasoning: false,
147 };
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Place {
153 User,
155 ToolResult,
157 Assistant,
159}
160
161#[derive(Debug, Clone, Copy)]
165pub enum Media<'a> {
166 Image(&'a crate::message::Image, Place),
168 Audio(&'a crate::message::Audio),
170 Video(&'a crate::message::Video),
172 Document(&'a crate::message::Document),
174}
175
176pub trait ReplayTarget: std::fmt::Debug + WasmCompatSync {
179 fn api(&self) -> Api;
181
182 fn provider(&self) -> &str;
184
185 fn model(&self) -> &str;
187
188 fn accepts(&self, model: &str) -> Accepts;
191
192 fn map_options(
202 &self,
203 request: &crate::completion::CompletionRequest,
204 fields: crate::completion::options::OptionFields<'_>,
205 ) -> crate::completion::options::OptionMap;
206
207 fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
211 None
212 }
213
214 fn encodes(&self, model: &str, media: Media<'_>) -> bool {
221 let _ = (model, media);
222 true
223 }
224
225 fn normalize_tool_call_id(&self, id: &str, model: &str, source: Option<&Origin>) -> String {
230 let _ = (model, source);
231 id.to_owned()
232 }
233
234 fn continues_stored(&self, request: &crate::completion::CompletionRequest) -> bool {
239 let _ = request;
240 false
241 }
242
243 fn declares_tools(&self, request: &crate::completion::CompletionRequest) -> bool {
249 declares_tools(request)
250 }
251
252 fn identity(&self, item: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
256 let _ = item;
257 serde_json::Map::new()
258 }
259
260 fn call_id_slot(&self) -> Option<&'static str> {
265 None
266 }
267
268 fn states_finish_reason(&self) -> bool {
272 true
273 }
274
275 fn binds_context(&self, model: &str) -> bool {
279 let _ = model;
280 false
281 }
282
283 fn drops_unbound_items(&self, request: &crate::completion::CompletionRequest) -> bool {
287 let _ = request;
288 false
289 }
290
291 fn route(&self, request: &crate::completion::CompletionRequest) -> Option<&dyn ReplayTarget> {
296 let _ = request;
297 None
298 }
299
300 fn takes_documents(&self) -> bool {
303 false
304 }
305
306 fn needs_next(&self, item: &serde_json::Value) -> bool {
310 let _ = item;
311 false
312 }
313
314 fn hosted_pair(&self, item: &serde_json::Value) -> Option<(Pairing, String)> {
320 let _ = item;
321 None
322 }
323
324 fn starts_with_user(&self) -> bool {
328 false
329 }
330
331 fn later_system(&self, model: &str) -> LaterSystem {
335 let _ = model;
336 LaterSystem::InPlace
337 }
338
339 fn alternates_roles(&self) -> bool {
343 false
344 }
345
346 fn hosted_needs_tools(&self) -> bool {
349 false
350 }
351
352 fn result_parts(&self, model: &str) -> bool {
356 let _ = model;
357 false
358 }
359
360 fn sends_alone(&self, block: &AssistantContent) -> bool {
365 let _ = block;
366 true
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum LaterSystem {
374 InPlace,
376 Leading,
378 UserText,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
385pub enum Pairing {
386 Use,
388 Result,
390}
391
392#[derive(Debug, Clone, PartialEq)]
394pub enum Replay<'a> {
395 Item(std::borrow::Cow<'a, serde_json::Value>),
397 Identity(serde_json::Map<String, serde_json::Value>),
400 Rebuild,
402}
403
404impl AssistantContent {
405 pub fn replay(
408 &self,
409 target: &dyn ReplayTarget,
410 ids: &crate::providers::internal::wire_ids::WireIds,
411 ) -> Replay<'_> {
412 if let Some(item) = self.native_item() {
413 return match (self, target.call_id_slot()) {
414 (AssistantContent::ToolCall(call), Some(slot)) => {
415 let mut item = item.clone();
416 if let Some(id) = ids.of(&call.id) {
417 set_pointer(&mut item, slot, serde_json::Value::String(id.to_owned()));
418 }
419 Replay::Item(std::borrow::Cow::Owned(item))
420 }
421 _ => Replay::Item(std::borrow::Cow::Borrowed(item)),
422 };
423 }
424 let identity = self
425 .stale_item()
426 .map(|item| target.identity(item))
427 .unwrap_or_default();
428 if identity.is_empty() {
429 Replay::Rebuild
430 } else {
431 Replay::Identity(identity)
432 }
433 }
434}
435
436fn set_pointer(item: &mut serde_json::Value, pointer: &str, value: serde_json::Value) {
438 let mut at = item;
439 let mut keys = pointer.split('/').skip(1).peekable();
440 while let Some(key) = keys.next() {
441 if !at.is_object() {
442 *at = serde_json::Value::Object(serde_json::Map::new());
443 }
444 let serde_json::Value::Object(fields) = at else {
445 return;
446 };
447 if keys.peek().is_none() {
448 fields.insert(key.to_owned(), value);
449 return;
450 }
451 at = fields
452 .entry(key.to_owned())
453 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
454 }
455}
456
457pub fn adapt(history: &[Message], target: &dyn ReplayTarget) -> Vec<Message> {
484 let history = history.iter().cloned().map(Arc::new).collect();
485 let adapted = adapt_for(history, target, &Request::default(), &mut Memo::default());
486 adapted.into_iter().map(Arc::unwrap_or_clone).collect()
487}
488
489#[derive(Debug, Default)]
496pub(crate) struct Memo {
497 shape: Option<Shape>,
498 given: Vec<Arc<Message>>,
499 shaped: Vec<Option<Arc<Message>>>,
500 taken: HashSet<String>,
501}
502
503#[derive(Debug, PartialEq)]
505struct Shape {
506 api: Api,
507 provider: String,
508 model: String,
509 context: Option<crate::message::Fingerprint>,
510 accepts: Accepts,
511 from_user: bool,
512}
513
514#[derive(Debug, Clone, Copy)]
516pub(crate) struct Request<'a> {
517 pub(crate) model: Option<&'a str>,
519 pub(crate) stored: bool,
521 pub(crate) tools: bool,
524 pub(crate) context: Option<crate::message::Fingerprint>,
526 pub(crate) foreign_reasoning: bool,
531}
532
533impl Default for Request<'_> {
534 fn default() -> Self {
535 Self {
536 model: None,
537 stored: false,
538 tools: true,
539 context: None,
540 foreign_reasoning: false,
541 }
542 }
543}
544
545pub(crate) fn context_of(
551 request: &crate::completion::CompletionRequest,
552 target: &dyn ReplayTarget,
553 model: &str,
554) -> crate::message::Fingerprint {
555 let mut tools: Vec<_> = request.tools.iter().collect();
556 tools.sort_by(|left, right| left.name.cmp(&right.name));
557 let raw = raw_tools(request);
558 let folds = target.later_system(model) == LaterSystem::Leading;
559 let mut system: Vec<&str> = request
560 .chat_history
561 .iter()
562 .map_while(|message| match &**message {
563 Message::System { content } => Some(Some(content.as_str())),
564 Message::User { .. } | Message::Assistant(_) => folds.then_some(None),
565 })
566 .flatten()
567 .filter(|content| !content.trim().is_empty())
568 .collect();
569 let joined;
570 if folds && system.len() > 1 {
571 joined = system.join("\n\n");
572 system = vec![joined.as_str()];
573 }
574 let mut fields = vec![serde_json::json!("context"), serde_json::json!(tools)];
575 fields.push(serde_json::json!(system));
576 if !raw.is_empty() {
577 fields.push(serde_json::json!(raw));
578 }
579 let fields = crate::message::Canonical(&serde_json::Value::Array(fields));
581 crate::message::Fingerprint::of_canonical(&fields)
582}
583
584pub(crate) fn declares_tools(request: &crate::completion::CompletionRequest) -> bool {
587 !request.tools.is_empty() || !raw_tools(request).is_empty()
588}
589
590pub(crate) fn raw_tools(request: &crate::completion::CompletionRequest) -> &[serde_json::Value] {
593 use crate::json_utils::Lenient;
594 request
595 .additional_params
596 .as_ref()
597 .map_or(&[][..], |params| params.arr("tools"))
598}
599
600pub(crate) fn adapt_for(
602 history: Vec<Arc<Message>>,
603 target: &dyn ReplayTarget,
604 request: &Request<'_>,
605 memo: &mut Memo,
606) -> Vec<Arc<Message>> {
607 let model = request.model.unwrap_or(target.model());
608 let stored = request.stored;
609 let same = Same {
610 api: target.api(),
611 provider: target.provider(),
612 model,
613 context: request.context.filter(|_| target.binds_context(model)),
614 };
615 let mut accepts = target.accepts(model);
616 accepts.tools &= request.tools;
617 accepts.foreign_reasoning &= request.foreign_reasoning;
618 let starts_with_user = target.starts_with_user() && !stored;
619 let shape = Shape {
620 api: same.api.clone(),
621 provider: same.provider.to_owned(),
622 model: model.to_owned(),
623 context: same.context,
624 accepts,
625 from_user: starts_with_user,
626 };
627 let resumed = memo.shape.as_ref() == Some(&shape)
628 && history.get(..memo.given.len()).is_some_and(|given| {
629 given
630 .iter()
631 .zip(&memo.given)
632 .all(|(message, was)| Arc::ptr_eq(message, was) || message == was)
633 });
634 if !resumed {
635 *memo = Memo::default();
636 }
637 let from = memo.given.len().saturating_sub(1);
639 let last_turn = history
640 .iter()
641 .rposition(|message| matches!(&**message, Message::Assistant(_)));
642 let (hosted, open) = hosted_pairs(&history, from, last_turn, target, &same);
643 let mut ids = Renamed {
644 to: Vec::new(),
645 before: &memo.taken,
646 taken: HashSet::new(),
647 };
648 let mut claimed = Vec::new();
649 let mut kept = 0;
650 let form = Form {
651 target,
652 model,
653 accepts,
654 };
655 let mut shaped: Vec<Option<Arc<Message>>> = Vec::with_capacity(history.len());
657 shaped.extend(memo.shaped.iter().cloned());
658 for (at, message) in history.iter().enumerate().skip(from) {
659 if Some(at) == last_turn {
660 claimed = ids.taken.iter().map(|id| id.to_string()).collect();
661 kept = shaped.len();
662 }
663 match &**message {
664 Message::System { content } => {
665 if !content.trim().is_empty() {
666 shaped.push(Some(message.clone()));
667 }
668 }
669 Message::User { content } => {
670 shaped.extend(
671 user(message, content, &mut ids, &form)
672 .into_iter()
673 .map(Some),
674 );
675 }
676 Message::Assistant(turn)
678 if turn.stop.as_ref().is_some_and(|stop| stop.is_failure()) =>
679 {
680 shaped.push(None);
681 }
682 Message::Assistant(turn) => {
683 let last = Some(at) == last_turn
686 && (turn
687 .content
688 .iter()
689 .any(|block| matches!(block, AssistantContent::ToolCall(_)))
690 || history
691 .get(at + 1..)
692 .into_iter()
693 .flatten()
694 .all(|message| matches!(&**message, Message::System { .. })));
695 let here: HashSet<usize> = hosted
696 .iter()
697 .filter(|(message, _)| *message == at)
698 .map(|(_, block)| *block)
699 .collect();
700 shaped.push(assistant(
701 message, turn, &form, &same, &mut ids, &here, last,
702 ));
703 }
704 }
705 }
706 let (before, tail) = shaped.split_at(memo.shaped.len().min(shaped.len()));
707 let last = before.last().and_then(Option::as_deref);
708 if settled(tail, last, starts_with_user, accepts.tools) {
709 drop(ids);
710 match last_turn.filter(|_| !open) {
713 Some(upto) => {
714 let given = history.get(memo.given.len()..=upto).unwrap_or_default();
715 memo.given.extend(given.iter().cloned());
716 let shaped = shaped.get(memo.shaped.len()..kept).unwrap_or_default();
717 memo.shaped.extend(shaped.iter().cloned());
718 memo.taken.extend(claimed);
719 memo.shape = Some(shape);
720 }
721 None => *memo = Memo::default(),
722 }
723 let settled: Vec<Arc<Message>> = shaped.into_iter().flatten().collect();
724 #[cfg(test)]
725 {
726 let shaped = settled.iter().map(|message| Some(Message::clone(message)));
727 let rebuilt = reshaped(shaped.collect(), target, model, stored, accepts.tools);
728 let settled: Vec<&Message> = settled.iter().map(Arc::as_ref).collect();
729 assert_eq!(
730 settled,
731 rebuilt.iter().collect::<Vec<_>>(),
732 "a settled history changed"
733 );
734 }
735 return settled;
736 }
737 drop(ids);
738 *memo = Memo::default();
739 let shaped = shaped
740 .into_iter()
741 .map(|message| message.map(Arc::unwrap_or_clone));
742 let shaped = reshaped(shaped.collect(), target, model, stored, accepts.tools);
743 shaped.into_iter().map(Arc::new).collect()
744}
745
746fn settled<'a>(
753 history: &'a [Option<Arc<Message>>],
754 mut last: Option<&'a Message>,
755 starts_with_user: bool,
756 tools: bool,
757) -> bool {
758 let mut waiting: Vec<&CallId> = Vec::new();
759 for message in history {
760 let Some(message) = message.as_deref() else {
761 return false;
762 };
763 match (message, last) {
764 (Message::System { .. }, None) => continue,
765 (Message::System { .. }, Some(_))
766 | (Message::User { .. }, Some(Message::User { .. }))
767 | (Message::Assistant(_), Some(Message::Assistant(_))) => return false,
768 (Message::Assistant(_), None) if starts_with_user => return false,
769 (Message::Assistant(turn), _) => {
772 waiting.extend(turn.tool_calls().map(|call| &call.id));
773 if !tools && !waiting.is_empty() {
774 return false;
775 }
776 }
777 (Message::User { content }, _) => {
778 let results = content.iter().take_while(|part| is_result(part)).count();
779 let answered = content.iter().take(results).all(|part| match part {
780 UserContent::ToolResult(result) => {
781 let at = waiting.iter().position(|id| **id == result.call);
782 at.map(|at| waiting.remove(at)).is_some()
783 }
784 _ => false,
785 });
786 if content.is_empty()
787 || (!tools && results > 0)
788 || !answered
789 || !waiting.is_empty()
790 || content.iter().skip(results).any(is_result)
791 {
792 return false;
793 }
794 }
795 }
796 last = Some(message);
797 }
798 waiting.is_empty()
799}
800
801fn reshaped(
804 shaped: Vec<Option<Message>>,
805 target: &dyn ReplayTarget,
806 model: &str,
807 stored: bool,
808 tools: bool,
809) -> Vec<Message> {
810 let pairing = crate::conversation::pairing::Pairing {
814 stored,
815 answers: tools,
816 };
817 let shaped = merge_users(crate::conversation::pairing::pair(shaped, pairing).messages);
818 let shaped = if tools { shaped } else { tools_as_text(shaped) };
819 let shaped = match target.later_system(model) {
820 LaterSystem::InPlace => shaped,
821 LaterSystem::Leading => leading_system(shaped),
822 LaterSystem::UserText => system_as_user_text(shaped),
823 };
824 let shaped = if target.starts_with_user() && !stored {
825 from_first_user(shaped)
826 } else {
827 shaped
828 };
829 if target.alternates_roles() {
830 alternated(shaped)
831 } else {
832 shaped
833 }
834}
835
836pub(crate) fn cache_points(
844 points: &[crate::completion::CachePoint],
845 written: &[Arc<Message>],
846 documents: Option<&Message>,
847 adapted: &[Arc<Message>],
848 target: &dyn ReplayTarget,
849 request: &Request<'_>,
850) -> Vec<crate::completion::CachePoint> {
851 use crate::completion::CachePoint;
852 let prefix_of = |turns: usize, items: Option<usize>| -> Option<Vec<Arc<Message>>> {
853 let mut prefix = Vec::new();
854 let mut counted = 0;
855 for message in written {
856 if counted == turns {
857 break;
858 }
859 let mut message = Arc::clone(message);
860 if !matches!(*message, Message::System { .. }) {
861 counted += 1;
862 }
863 if let Some(items) = items.filter(|_| counted == turns) {
865 match Arc::make_mut(&mut message) {
866 Message::User { content } => content.truncate(items),
867 Message::Assistant(turn) => turn.content.truncate(items),
868 Message::System { .. } => {}
869 }
870 }
871 prefix.push(message);
872 }
873 (counted == turns).then_some(prefix)
874 };
875 points
876 .iter()
877 .filter_map(|point| {
878 let prefix = match *point {
879 CachePoint::Tools | CachePoint::System | CachePoint::Messages(0) => {
880 return Some(*point);
881 }
882 CachePoint::Messages(turns) => prefix_of(turns, None),
883 CachePoint::Within { message, items } => message
884 .checked_add(1)
885 .and_then(|turns| prefix_of(turns, Some(items))),
886 };
887 let mut prefix = prefix?;
888 if let Some(documents) = documents {
889 crate::completion::request::insert_after_leading_system(
890 &mut prefix,
891 documents.clone(),
892 );
893 }
894 let prefix = adapt_for(prefix, target, request, &mut Memo::default());
895 Some(shared_prefix(adapted, &prefix))
896 })
897 .collect()
898}
899
900fn shared_prefix(
903 history: &[Arc<Message>],
904 prefix: &[Arc<Message>],
905) -> crate::completion::CachePoint {
906 use crate::completion::CachePoint;
907 fn turns(history: &[Arc<Message>]) -> impl Iterator<Item = &Message> {
908 history
909 .iter()
910 .map(Arc::as_ref)
911 .filter(|message| !matches!(message, Message::System { .. }))
912 }
913 fn lead<T: PartialEq>(whole: &[T], part: &[T]) -> (usize, usize, usize) {
914 let shared = whole
915 .iter()
916 .zip(part)
917 .take_while(|(whole, part)| whole == part)
918 .count();
919 (shared, whole.len(), part.len())
920 }
921 let mut message = 0;
922 for (whole, part) in turns(history).zip(turns(prefix)) {
923 let (shared, whole, part) = match (whole, part) {
924 (Message::User { content: whole }, Message::User { content: part }) => {
925 lead(whole, part)
926 }
927 (Message::Assistant(whole), Message::Assistant(part)) => {
928 lead(&whole.content, &part.content)
929 }
930 _ => (0, 1, 1),
931 };
932 if shared == whole && shared == part {
933 message += 1;
934 continue;
935 }
936 return if shared == 0 {
937 CachePoint::Messages(message)
938 } else {
939 CachePoint::Within {
940 message,
941 items: shared,
942 }
943 };
944 }
945 CachePoint::Messages(message)
946}
947
948fn tools_as_text(history: Vec<Message>) -> Vec<Message> {
952 let history = history
953 .into_iter()
954 .map(|message| match message {
955 Message::Assistant(mut turn) => {
956 for block in &mut turn.content {
957 if let AssistantContent::ToolCall(call) = block {
958 *block = AssistantContent::Text(Text::new(format!(
959 "[called tool {} with {}]",
960 call.function.name,
961 crate::json_utils::to_canonical_string(
962 &call.function.arguments_value()
963 )
964 )));
965 }
966 }
967 Message::Assistant(turn)
968 }
969 Message::User { content } => Message::User {
970 content: content
971 .into_iter()
972 .map(|part| match part {
973 UserContent::ToolResult(result) => UserContent::text(result_text(&result)),
974 part => part,
975 })
976 .collect(),
977 },
978 message => message,
979 })
980 .collect();
981 merge_users(history)
982}
983
984fn from_first_user(mut history: Vec<Message>) -> Vec<Message> {
988 let lead = history
989 .iter()
990 .take_while(|message| matches!(message, Message::System { .. }))
991 .count();
992 while let Some(Message::Assistant(turn)) = history.get(lead) {
993 let calls: HashSet<CallId> = turn.tool_calls().map(|call| call.id.clone()).collect();
994 history.remove(lead);
995 if let Some(Message::User { content }) = history.get_mut(lead) {
996 content.retain(|part| {
997 !matches!(part, UserContent::ToolResult(result) if calls.contains(&result.call))
998 });
999 if content.is_empty() {
1000 history.remove(lead);
1001 }
1002 }
1003 }
1004 history
1005}
1006
1007fn has_later_system(history: &[Message]) -> bool {
1010 history
1011 .iter()
1012 .skip_while(|message| matches!(message, Message::System { .. }))
1013 .any(|message| matches!(message, Message::System { .. }))
1014}
1015
1016fn system_as_user_text(history: Vec<Message>) -> Vec<Message> {
1018 let mut leading = true;
1019 let history = history
1020 .into_iter()
1021 .map(|message| match message {
1022 Message::System { content } if !leading => Message::User {
1023 content: vec![UserContent::text(content)],
1024 },
1025 message => {
1026 leading &= matches!(message, Message::System { .. });
1027 message
1028 }
1029 })
1030 .collect();
1031 merge_users(history)
1032}
1033
1034fn alternated(history: Vec<Message>) -> Vec<Message> {
1038 let mut alternated: Vec<Message> = Vec::with_capacity(history.len());
1039 let mut held: Vec<Message> = Vec::new();
1040 for message in history {
1041 let started = alternated
1042 .iter()
1043 .any(|message| !matches!(message, Message::System { .. }));
1044 match (message, alternated.last_mut()) {
1045 (message @ Message::System { .. }, _) if started => held.push(message),
1046 (Message::User { content }, Some(Message::User { content: previous })) => {
1047 previous.extend(content);
1048 }
1049 (Message::Assistant(turn), Some(Message::Assistant(previous))) => {
1050 if previous.origin != turn.origin {
1051 previous.origin = None;
1052 }
1053 previous.stop = turn.stop;
1054 previous.content.extend(turn.content);
1055 }
1056 (message, _) => {
1057 alternated.append(&mut held);
1058 alternated.push(message);
1059 }
1060 }
1061 }
1062 alternated.append(&mut held);
1063 alternated
1064}
1065
1066fn leading_system(history: Vec<Message>) -> Vec<Message> {
1069 if !has_later_system(&history) {
1070 return history;
1071 }
1072 let (system, rest): (Vec<Message>, Vec<Message>) = history
1073 .into_iter()
1074 .partition(|message| matches!(message, Message::System { .. }));
1075 let prompt: Vec<String> = system
1076 .into_iter()
1077 .filter_map(|message| match message {
1078 Message::System { content } => Some(content),
1079 Message::User { .. } | Message::Assistant(_) => None,
1080 })
1081 .collect();
1082 let mut history = Vec::with_capacity(rest.len() + 1);
1083 if !prompt.is_empty() {
1084 history.push(Message::system(prompt.join("\n\n")));
1085 }
1086 history.extend(merge_users(rest));
1088 history
1089}
1090
1091struct Renamed<'a> {
1097 to: Vec<(Cow<'a, CallId>, Cow<'a, CallId>)>,
1100 before: &'a HashSet<String>,
1102 taken: HashSet<Cow<'a, str>>,
1103}
1104
1105impl<'a> Renamed<'a> {
1106 fn turn(&mut self) {
1108 self.to.clear();
1109 }
1110
1111 fn claim(&mut self, source: Cow<'a, CallId>, wanted: Cow<'a, str>) -> Cow<'a, CallId> {
1119 let id = match &*source {
1120 CallId::Local(_) => source.clone(),
1121 CallId::Provider(_) => {
1122 let mut id = wanted.clone();
1123 let mut attempt: u64 = 1;
1124 while self.before.contains(id.as_ref()) || self.taken.contains(&id) {
1125 id = Cow::Owned(with_counter(&wanted, attempt));
1126 attempt += 1;
1127 }
1128 self.taken.insert(id.clone());
1129 if id == source.wire() {
1130 source.clone()
1131 } else {
1132 Cow::Owned(CallId::from_wire(id))
1133 }
1134 }
1135 };
1136 self.to.push((source, id.clone()));
1137 id
1138 }
1139
1140 fn answer(&mut self, source: &CallId) -> Option<Cow<'a, CallId>> {
1143 let mut calls = (self.to.iter().enumerate())
1144 .filter(|(_, (from, _))| from.as_ref() == source)
1145 .map(|(at, _)| at);
1146 let first = calls.next()?;
1147 if calls.next().is_some() {
1148 Some(self.to.remove(first).1)
1149 } else {
1150 self.to.get(first).map(|(_, id)| id.clone())
1151 }
1152 }
1153}
1154
1155fn with_counter(id: &str, attempt: u64) -> String {
1157 let mut digits = Vec::new();
1158 let mut value = attempt;
1159 while value > 0 {
1160 digits.push(char::from_digit((value % 36) as u32, 36).unwrap_or('0'));
1161 value /= 36;
1162 }
1163 digits.reverse();
1164 let keep = id.chars().count().saturating_sub(digits.len());
1165 id.chars().take(keep).chain(digits).collect()
1166}
1167
1168struct Same<'a> {
1170 api: Api,
1171 provider: &'a str,
1172 model: &'a str,
1173 context: Option<crate::message::Fingerprint>,
1175}
1176
1177impl Same<'_> {
1178 fn is(&self, origin: &Origin) -> bool {
1181 origin.same_model(&self.api, self.provider, self.model)
1182 && self
1183 .context
1184 .is_none_or(|context| origin.context == Some(context))
1185 }
1186}
1187
1188fn assistant<'a>(
1192 message: &Arc<Message>,
1193 turn: &'a AssistantMessage,
1194 form: &Form<'_>,
1195 same_model: &Same<'_>,
1196 ids: &mut Renamed<'a>,
1197 hosted: &HashSet<usize>,
1198 last: bool,
1199) -> Option<Arc<Message>> {
1200 let Form {
1201 target,
1202 model,
1203 accepts,
1204 } = *form;
1205 let same = turn
1206 .origin
1207 .as_ref()
1208 .is_some_and(|origin| same_model.is(origin));
1209 ids.turn();
1210 let content: Vec<Option<Cow<'a, AssistantContent>>> = turn
1211 .content
1212 .iter()
1213 .map(|block| {
1214 let block = if same {
1215 match block {
1216 AssistantContent::ToolCall(call) => {
1217 let id = ids.claim(Cow::Borrowed(&call.id), call.id.wire());
1218 if *id == call.id {
1219 Cow::Borrowed(block)
1220 } else {
1221 let item = block
1225 .native_item()
1226 .filter(|_| target.call_id_slot().is_some());
1227 let id = id.into_owned();
1228 let renamed = crate::message::ToolCall { id, ..call.clone() };
1229 let renamed = AssistantContent::ToolCall(renamed);
1230 Cow::Owned(match item {
1231 Some(item) if renamed.native_item().is_none() => {
1232 renamed.into_canonical().with_native(item.clone())
1233 }
1234 _ => renamed,
1235 })
1236 }
1237 }
1238 AssistantContent::Image(_) if !accepts.assistant_images => return None,
1241 block => Cow::Borrowed(block),
1242 }
1243 } else {
1244 Cow::Owned(match block.canonical() {
1245 AssistantContent::Reasoning(reasoning) => {
1246 if reasoning.redacted || reasoning.text.trim().is_empty() {
1247 return None;
1248 }
1249 if accepts.foreign_reasoning {
1250 AssistantContent::Reasoning(reasoning)
1251 } else {
1252 AssistantContent::Text(Text::new(reasoning.text))
1253 }
1254 }
1255 AssistantContent::Opaque(_) => return None,
1256 AssistantContent::Image(_) if !accepts.assistant_images => {
1257 AssistantContent::Text(Text::new(ASSISTANT_IMAGE_OMITTED))
1258 }
1259 AssistantContent::ToolCall(mut call) => {
1260 let normalized = target.normalize_tool_call_id(
1261 &call.id.wire(),
1262 model,
1263 turn.origin.as_ref(),
1264 );
1265 let source = Cow::Owned(call.id);
1266 call.id = ids.claim(source, Cow::Owned(normalized)).into_owned();
1267 AssistantContent::ToolCall(call)
1268 }
1269 block => block,
1270 })
1271 };
1272 let block = match &*block {
1275 AssistantContent::Opaque(opaque)
1276 if !accepts.tools
1277 && target.hosted_needs_tools()
1278 && target.hosted_pair(&opaque.item).is_some() =>
1279 {
1280 return None;
1281 }
1282 _ => block,
1283 };
1284 (!block.is_blank()).then_some(block)
1285 })
1286 .collect();
1287 let content: Vec<Cow<'a, AssistantContent>> = if same {
1288 paired(content, target, accepts.tools, hosted, last)
1289 } else {
1290 content.into_iter().flatten().collect()
1291 };
1292 let content: Vec<Cow<'a, AssistantContent>> = content
1293 .into_iter()
1294 .map(|block| match &*block {
1295 AssistantContent::Image(image) if image.native.is_none() => {
1296 if sendable_as_is(image) && form.sends(image, Place::Assistant) {
1297 return block;
1298 }
1299 let image = sendable_image(image.clone());
1300 Cow::Owned(
1301 if matches!(image.data, DocumentSourceKind::Unknown)
1302 || !target.encodes(model, Media::Image(&image, Place::Assistant))
1303 {
1304 AssistantContent::Text(Text::new(IMAGE_UNSENDABLE))
1305 } else {
1306 AssistantContent::Image(image)
1307 },
1308 )
1309 }
1310 _ => block,
1311 })
1312 .collect();
1313 if !turn.content.is_empty() && !content.iter().any(|block| target.sends_alone(block)) {
1314 return None;
1315 }
1316 if content.len() == turn.content.len()
1317 && content
1318 .iter()
1319 .all(|block| matches!(block, Cow::Borrowed(_)))
1320 {
1321 return Some(message.clone());
1322 }
1323 Some(Arc::new(Message::Assistant(AssistantMessage {
1324 content: content.into_iter().map(Cow::into_owned).collect(),
1325 origin: turn.origin.clone(),
1326 stop: turn.stop.clone(),
1327 })))
1328}
1329
1330fn hosted_pairs(
1338 history: &[Arc<Message>],
1339 from: usize,
1340 boundary: Option<usize>,
1341 target: &dyn ReplayTarget,
1342 same: &Same<'_>,
1343) -> (HashSet<(usize, usize)>, bool) {
1344 let mut open: HashMap<String, (usize, usize)> = HashMap::new();
1345 let mut paired = HashSet::new();
1346 let mut open_at_boundary = false;
1347 for (at, message) in history.iter().enumerate().skip(from) {
1348 if Some(at) == boundary {
1349 open_at_boundary = !open.is_empty();
1350 }
1351 let Message::Assistant(turn) = &**message else {
1352 continue;
1353 };
1354 if turn.stop.as_ref().is_some_and(|stop| stop.is_failure())
1355 || !turn.origin.as_ref().is_some_and(|origin| same.is(origin))
1356 {
1357 continue;
1358 }
1359 for (index, block) in turn.content.iter().enumerate() {
1360 let AssistantContent::Opaque(opaque) = block else {
1361 continue;
1362 };
1363 if !opaque.replay {
1364 continue;
1365 }
1366 match target.hosted_pair(&opaque.item) {
1367 Some((Pairing::Use, id)) => {
1368 open.insert(id, (at, index));
1369 }
1370 Some((Pairing::Result, id)) => {
1371 if let Some(used) = open.remove(&id) {
1372 paired.insert(used);
1373 paired.insert((at, index));
1374 }
1375 }
1376 None => {}
1377 }
1378 }
1379 }
1380 (paired, open_at_boundary)
1381}
1382
1383fn paired<'a>(
1391 mut content: Vec<Option<Cow<'a, AssistantContent>>>,
1392 target: &dyn ReplayTarget,
1393 tools: bool,
1394 hosted: &HashSet<usize>,
1395 last: bool,
1396) -> Vec<Cow<'a, AssistantContent>> {
1397 let pair = |block: &AssistantContent| match block {
1398 AssistantContent::Opaque(opaque) if opaque.replay => target.hosted_pair(&opaque.item),
1399 _ => None,
1400 };
1401 for at in 0..content.len() {
1402 let Some((side, _)) = content.get(at).and_then(Option::as_deref).and_then(pair) else {
1403 continue;
1404 };
1405 let running = last
1406 && side == Pairing::Use
1407 && content
1408 .get(at + 1..)
1409 .into_iter()
1410 .flatten()
1411 .flatten()
1412 .all(|block| {
1413 matches!(
1414 **block,
1415 AssistantContent::ToolCall(_) | AssistantContent::Opaque(_)
1416 )
1417 });
1418 if !hosted.contains(&at)
1419 && !running
1420 && let Some(slot) = content.get_mut(at)
1421 {
1422 *slot = None;
1423 }
1424 }
1425 let (kept, needs): (Vec<bool>, Vec<bool>) = content
1430 .iter()
1431 .map(|block| {
1432 let item = block.as_deref().and_then(|block| match block {
1433 AssistantContent::Opaque(opaque) => opaque.replay.then_some(&opaque.item),
1434 block => block
1435 .native()
1436 .filter(|(item, current)| *current || !target.identity(item).is_empty())
1437 .map(|(item, _)| item),
1438 });
1439 (
1440 item.is_some(),
1441 item.is_some_and(|item| target.needs_next(item)),
1442 )
1443 })
1444 .unzip();
1445 for at in (0..content.len()).rev() {
1446 let next_gone = content
1450 .get(at + 1)
1451 .and_then(Option::as_deref)
1452 .is_none_or(|next| {
1453 !kept.get(at + 1).copied().unwrap_or_default()
1454 || (!tools && matches!(next, AssistantContent::ToolCall(_)))
1455 });
1456 if needs.get(at).copied().unwrap_or_default()
1457 && next_gone
1458 && let Some(slot) = content.get_mut(at)
1459 {
1460 *slot = None;
1461 }
1462 }
1463 content.into_iter().flatten().collect()
1464}
1465
1466struct Form<'a> {
1468 target: &'a dyn ReplayTarget,
1469 model: &'a str,
1470 accepts: Accepts,
1471}
1472
1473impl Form<'_> {
1474 fn sends(&self, image: &crate::message::Image, place: Place) -> bool {
1476 let reads = match place {
1477 Place::User => self.accepts.user_images,
1478 Place::ToolResult => self.accepts.tool_result_images,
1479 Place::Assistant => self.accepts.assistant_images,
1480 };
1481 reads
1482 && !matches!(image.data, DocumentSourceKind::Unknown)
1483 && self.target.encodes(self.model, Media::Image(image, place))
1484 }
1485}
1486
1487fn user<'a>(
1491 message: &Arc<Message>,
1492 content: &'a [UserContent],
1493 ids: &mut Renamed<'a>,
1494 form: &Form<'_>,
1495) -> Vec<Arc<Message>> {
1496 let mut shaped: Vec<Cow<'_, UserContent>> = Vec::with_capacity(content.len());
1497 let mut attached = Vec::new();
1498 for part in content {
1499 let placeholder = match part {
1500 UserContent::Image(image)
1501 if sendable_as_is(image) && form.sends(image, Place::User) =>
1502 {
1503 shaped.push(Cow::Borrowed(part));
1504 continue;
1505 }
1506 UserContent::Image(image) => {
1507 let image = sendable_image(image.clone());
1508 if form.sends(&image, Place::User) {
1509 shaped.push(Cow::Owned(UserContent::Image(image)));
1510 continue;
1511 }
1512 if form.accepts.user_images {
1513 IMAGE_UNSENDABLE
1514 } else {
1515 USER_IMAGE_OMITTED
1516 }
1517 }
1518 UserContent::Audio(audio) => {
1519 let mut audio = audio.clone();
1520 audio.data = sendable(audio.data);
1521 if !matches!(audio.data, DocumentSourceKind::Unknown)
1522 && form.target.encodes(form.model, Media::Audio(&audio))
1523 {
1524 shaped.push(Cow::Owned(UserContent::Audio(audio)));
1525 continue;
1526 }
1527 AUDIO_UNSENDABLE
1528 }
1529 UserContent::Video(video) => {
1530 let mut video = video.clone();
1531 video.data = sendable(video.data);
1532 if !matches!(video.data, DocumentSourceKind::Unknown)
1533 && form.target.encodes(form.model, Media::Video(&video))
1534 {
1535 shaped.push(Cow::Owned(UserContent::Video(video)));
1536 continue;
1537 }
1538 VIDEO_UNSENDABLE
1539 }
1540 UserContent::Document(document) => {
1541 let mut document = document.clone();
1542 if let DocumentData::File(source) = document.data {
1543 document.data = DocumentData::File(sendable(source));
1544 }
1545 if document.data != DocumentData::File(DocumentSourceKind::Unknown)
1546 && form.target.encodes(form.model, Media::Document(&document))
1547 {
1548 shaped.push(Cow::Owned(UserContent::Document(document)));
1549 continue;
1550 }
1551 let text = match document.data {
1554 DocumentData::Text(text) => Some(text),
1555 DocumentData::File(DocumentSourceKind::Base64(data))
1556 if !matches!(document.media_type, None | Some(DocumentMediaType::PDF)) =>
1557 {
1558 BASE64_STANDARD
1559 .decode(data.as_bytes())
1560 .ok()
1561 .and_then(|bytes| String::from_utf8(bytes).ok())
1562 }
1563 DocumentData::File(_) => None,
1564 };
1565 if let Some(text) = text {
1566 shaped.push(Cow::Owned(UserContent::text(text)));
1567 continue;
1568 }
1569 DOCUMENT_UNSENDABLE
1570 }
1571 UserContent::ToolResult(result) => {
1572 let answer = ids.answer(&result.call);
1573 let parts = form.accepts.tool_result_images || form.target.result_parts(form.model);
1574 if answer.as_ref().is_none_or(|id| **id == result.call)
1575 && result_as_is(result, form, parts)
1576 {
1577 shaped.push(Cow::Borrowed(part));
1578 continue;
1579 }
1580 let mut result = result.clone();
1581 if let Some(id) = answer {
1582 result.call = id.into_owned();
1583 }
1584 result.content = result_images(result.content, form, &mut attached);
1585 result.content = result_text_parts(result.content, parts, result.is_error);
1586 shaped.push(Cow::Owned(UserContent::ToolResult(result)));
1587 continue;
1588 }
1589 UserContent::Text(text) if text.text.trim().is_empty() => continue,
1591 UserContent::Text(_) => {
1592 shaped.push(Cow::Borrowed(part));
1593 continue;
1594 }
1595 };
1596 let repeated = matches!(
1597 shaped.last().map(|part| &**part),
1598 Some(UserContent::Text(text)) if text.text == placeholder
1599 );
1600 if !repeated {
1601 shaped.push(Cow::Owned(UserContent::text(placeholder)));
1602 }
1603 }
1604 let unchanged =
1605 shaped.len() == content.len() && shaped.iter().all(|part| matches!(part, Cow::Borrowed(_)));
1606 if !shaped.is_empty() && unchanged && attached.is_empty() {
1607 return vec![message.clone()];
1608 }
1609 let mut messages = Vec::new();
1610 if !shaped.is_empty() {
1611 let content = shaped.into_iter().map(Cow::into_owned).collect();
1612 messages.push(Arc::new(Message::User { content }));
1613 }
1614 if !attached.is_empty() {
1615 let mut content = vec![UserContent::text(TOOL_IMAGES_HEADING)];
1616 content.extend(attached.into_iter().map(UserContent::Image));
1617 messages.push(Arc::new(Message::User { content }));
1618 }
1619 messages
1620}
1621
1622fn result_as_is(result: &ToolResult, form: &Form<'_>, parts: bool) -> bool {
1626 let images = result.content.iter().all(|part| match part {
1627 ToolResultContent::Image(image) => {
1628 sendable_as_is(image) && form.sends(image, Place::ToolResult)
1629 }
1630 ToolResultContent::Text(_) | ToolResultContent::Json { .. } => true,
1631 });
1632 images && !says_nothing(&result.content) && (parts || texts(&result.content) < 2)
1633}
1634
1635pub const NO_TOOL_OUTPUT: &str = "(no tool output)";
1638
1639fn result_text_parts(
1643 content: Vec<ToolResultContent>,
1644 parts: bool,
1645 is_error: bool,
1646) -> Vec<ToolResultContent> {
1647 if says_nothing(&content) {
1648 let text = if is_error {
1649 format!("[tool error] {NO_TOOL_OUTPUT}")
1650 } else {
1651 NO_TOOL_OUTPUT.to_owned()
1652 };
1653 return vec![ToolResultContent::text(text)];
1654 }
1655 if parts || texts(&content) < 2 {
1656 return content;
1657 }
1658 let mut joined: Vec<String> = Vec::new();
1659 let mut images = Vec::new();
1660 for part in content {
1661 match part {
1662 ToolResultContent::Text(text) => joined.push(text.text),
1663 ToolResultContent::Json { value } => joined.push(value.to_string()),
1664 image @ ToolResultContent::Image(_) => images.push(image),
1665 }
1666 }
1667 std::iter::once(ToolResultContent::text(joined.join("\n")))
1668 .chain(images)
1669 .collect()
1670}
1671
1672fn says_nothing(content: &[ToolResultContent]) -> bool {
1674 content.iter().all(|part| match part {
1675 ToolResultContent::Text(text) => text.text.trim().is_empty(),
1676 ToolResultContent::Json { .. } | ToolResultContent::Image(_) => false,
1677 })
1678}
1679
1680fn texts(content: &[ToolResultContent]) -> usize {
1682 content
1683 .iter()
1684 .filter(|part| !matches!(part, ToolResultContent::Image(_)))
1685 .count()
1686}
1687
1688fn result_text(result: &ToolResult) -> String {
1690 let text: Vec<String> = result
1691 .content
1692 .iter()
1693 .map(|part| match part {
1694 ToolResultContent::Text(text) => text.text.clone(),
1695 ToolResultContent::Json { value } => value.to_string(),
1696 ToolResultContent::Image(_) => TOOL_IMAGE_OMITTED.to_owned(),
1697 })
1698 .collect();
1699 let kind = if result.is_error { "error" } else { "result" };
1700 format!("[tool {} {kind}] {}", result.name, text.join("\n"))
1701}
1702
1703fn result_images(
1707 content: Vec<ToolResultContent>,
1708 form: &Form<'_>,
1709 attached: &mut Vec<crate::message::Image>,
1710) -> Vec<ToolResultContent> {
1711 let mut shaped: Vec<ToolResultContent> = Vec::with_capacity(content.len());
1712 for part in content {
1713 match part {
1714 ToolResultContent::Image(image) => {
1715 let image = sendable_image(image);
1716 if form.sends(&image, Place::ToolResult) {
1717 shaped.push(ToolResultContent::Image(image));
1718 continue;
1719 }
1720 let placeholder = if form.sends(&image, Place::User) {
1721 attached.push(image);
1722 TOOL_IMAGE_ATTACHED
1723 } else {
1724 TOOL_IMAGE_OMITTED
1725 };
1726 if shaped.last().and_then(ToolResultContent::as_text) != Some(placeholder) {
1727 shaped.push(ToolResultContent::text(placeholder));
1728 }
1729 }
1730 part => shaped.push(part),
1731 }
1732 }
1733 shaped
1734}
1735
1736fn sendable(source: DocumentSourceKind) -> DocumentSourceKind {
1739 match source {
1740 DocumentSourceKind::Raw(bytes) => DocumentSourceKind::Base64(BASE64_STANDARD.encode(bytes)),
1741 source => source,
1742 }
1743}
1744
1745fn sendable_as_is(image: &crate::message::Image) -> bool {
1748 match &image.data {
1749 DocumentSourceKind::Raw(_) => false,
1750 DocumentSourceKind::Base64(data) => image.media_type.is_some() || sniffed(data).is_none(),
1751 _ => true,
1752 }
1753}
1754
1755fn sendable_image(mut image: crate::message::Image) -> crate::message::Image {
1758 image.data = sendable(image.data);
1759 if image.media_type.is_none()
1760 && let DocumentSourceKind::Base64(data) = &image.data
1761 {
1762 image.media_type = sniffed(data);
1763 }
1764 image
1765}
1766
1767fn sniffed(data: &str) -> Option<ImageMediaType> {
1769 let head: String = data.chars().take(24).collect();
1770 let bytes = BASE64_STANDARD
1771 .decode(head.as_bytes())
1772 .or_else(|_| BASE64_STANDARD_NO_PAD.decode(head.as_bytes()))
1773 .ok()?;
1774 ImageMediaType::sniff(&bytes)
1775}
1776
1777fn merge_users(history: Vec<Message>) -> Vec<Message> {
1781 let mut merged: Vec<Message> = Vec::with_capacity(history.len());
1782 for message in history {
1783 match (merged.last_mut(), message) {
1784 (Some(Message::User { content: previous }), Message::User { content })
1785 if !previous.is_empty() && !content.is_empty() =>
1786 {
1787 previous.extend(content);
1788 }
1789 (_, message) => merged.push(message),
1790 }
1791 }
1792 merged
1793}
1794
1795#[cfg(test)]
1796mod tests;