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, StopReason, Text, ToolResult,
73 ToolResultContent, UserContent,
74};
75use crate::wasm_compat::WasmCompatSync;
76
77pub const IMAGE_OMITTED: &str = "(image omitted: model does not support images)";
81
82pub const TOOL_IMAGE_OMITTED: &str = "(tool image omitted: model does not support images)";
84
85pub const IMAGE_UNSENDABLE: &str = "(image omitted: the provider cannot receive it in this form)";
87
88pub const AUDIO_UNSENDABLE: &str = "(audio omitted: the provider cannot receive it in this form)";
90
91pub const VIDEO_UNSENDABLE: &str = "(video omitted: the provider cannot receive it in this form)";
93
94pub const DOCUMENT_UNSENDABLE: &str =
97 "(document omitted: the provider cannot receive it in this form)";
98
99pub const TOOL_IMAGE_ATTACHED: &str = "(see attached image)";
102
103pub const TOOL_IMAGES_HEADING: &str = "Attached image(s) from tool result:";
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct Accepts {
109 pub user_images: bool,
111 pub assistant_images: bool,
113 pub tool_result_images: bool,
115 pub tools: bool,
117 pub foreign_reasoning: bool,
124}
125
126impl Accepts {
127 pub const ALL: Self = Self {
130 user_images: true,
131 assistant_images: true,
132 tool_result_images: true,
133 tools: true,
134 foreign_reasoning: false,
135 };
136
137 pub const TEXT: Self = Self {
140 user_images: false,
141 assistant_images: false,
142 tool_result_images: false,
143 tools: true,
144 foreign_reasoning: false,
145 };
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum Place {
151 User,
153 ToolResult,
155 Assistant,
157}
158
159#[derive(Debug, Clone, Copy)]
163pub enum Media<'a> {
164 Image(&'a crate::message::Image, Place),
166 Audio(&'a crate::message::Audio),
168 Video(&'a crate::message::Video),
170 Document(&'a crate::message::Document),
172}
173
174pub trait ReplayTarget: std::fmt::Debug + WasmCompatSync {
177 fn api(&self) -> Api;
179
180 fn provider(&self) -> &str;
182
183 fn model(&self) -> &str;
185
186 fn accepts(&self, model: &str) -> Accepts;
189
190 fn map_options(
200 &self,
201 request: &crate::completion::CompletionRequest,
202 fields: crate::completion::options::OptionFields<'_>,
203 ) -> crate::completion::options::OptionMap;
204
205 fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
209 None
210 }
211
212 fn encodes(&self, model: &str, media: Media<'_>) -> bool {
219 let _ = (model, media);
220 true
221 }
222
223 fn normalize_tool_call_id(&self, id: &str, model: &str, source: Option<&Origin>) -> String {
228 let _ = (model, source);
229 id.to_owned()
230 }
231
232 fn continues_stored(&self, request: &crate::completion::CompletionRequest) -> bool {
237 let _ = request;
238 false
239 }
240
241 fn declares_tools(&self, request: &crate::completion::CompletionRequest) -> bool {
247 declares_tools(request)
248 }
249
250 fn identity(&self, item: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
254 let _ = item;
255 serde_json::Map::new()
256 }
257
258 fn call_id_slot(&self) -> Option<&'static str> {
263 None
264 }
265
266 fn blob_slots(&self) -> &'static [&'static str] {
271 &[]
272 }
273
274 fn states_finish_reason(&self) -> bool {
278 true
279 }
280
281 fn catalog_priced(&self) -> bool {
285 true
286 }
287
288 fn binds_context(&self, model: &str) -> bool {
292 let _ = model;
293 false
294 }
295
296 fn drops_unbound_items(&self, request: &crate::completion::CompletionRequest) -> bool {
300 let _ = request;
301 false
302 }
303
304 fn route(&self, request: &crate::completion::CompletionRequest) -> Option<&dyn ReplayTarget> {
309 let _ = request;
310 None
311 }
312
313 fn takes_documents(&self) -> bool {
316 false
317 }
318
319 fn needs_next(&self, item: &serde_json::Value) -> bool {
323 let _ = item;
324 false
325 }
326
327 fn hosted_pair(&self, item: &serde_json::Value) -> Option<(Pairing, String)> {
333 let _ = item;
334 None
335 }
336
337 fn starts_with_user(&self) -> bool {
341 false
342 }
343
344 fn later_system(&self, model: &str) -> LaterSystem {
348 let _ = model;
349 LaterSystem::InPlace
350 }
351
352 fn alternates_roles(&self) -> bool {
356 false
357 }
358
359 fn hosted_needs_tools(&self) -> bool {
362 false
363 }
364
365 fn result_parts(&self, model: &str) -> bool {
369 let _ = model;
370 false
371 }
372
373 fn sends_alone(&self, block: &AssistantContent) -> bool {
378 let _ = block;
379 true
380 }
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub enum LaterSystem {
387 InPlace,
389 Leading,
391 UserText,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
398pub enum Pairing {
399 Use,
401 Result,
403}
404
405#[derive(Debug, Clone, PartialEq)]
407pub enum Replay<'a> {
408 Item(std::borrow::Cow<'a, serde_json::Value>),
410 Identity(serde_json::Map<String, serde_json::Value>),
413 Rebuild,
415}
416
417impl AssistantContent {
418 pub fn replay(
421 &self,
422 target: &dyn ReplayTarget,
423 ids: &crate::providers::internal::wire_ids::WireIds,
424 ) -> Replay<'_> {
425 if let Some(item) = self.native_item() {
426 return match (self, target.call_id_slot()) {
427 (AssistantContent::ToolCall(call), Some(slot)) => {
428 let mut item = item.clone();
429 if let Some(id) = ids.of(&call.id) {
430 set_pointer(&mut item, slot, serde_json::Value::String(id.to_owned()));
431 }
432 Replay::Item(std::borrow::Cow::Owned(item))
433 }
434 _ => Replay::Item(std::borrow::Cow::Borrowed(item)),
435 };
436 }
437 let identity = self
438 .stale_item()
439 .map(|item| target.identity(item))
440 .unwrap_or_default();
441 if identity.is_empty() {
442 Replay::Rebuild
443 } else {
444 Replay::Identity(identity)
445 }
446 }
447}
448
449fn set_pointer(item: &mut serde_json::Value, pointer: &str, value: serde_json::Value) {
451 let mut at = item;
452 let mut keys = pointer.split('/').skip(1).peekable();
453 while let Some(key) = keys.next() {
454 if !at.is_object() {
455 *at = serde_json::Value::Object(serde_json::Map::new());
456 }
457 let serde_json::Value::Object(fields) = at else {
458 return;
459 };
460 if keys.peek().is_none() {
461 fields.insert(key.to_owned(), value);
462 return;
463 }
464 at = fields
465 .entry(key.to_owned())
466 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
467 }
468}
469
470pub fn adapt(history: &[Message], target: &dyn ReplayTarget) -> Vec<Message> {
499 let history = history.iter().cloned().map(Arc::new).collect();
500 let adapted = adapt_for(history, target, &Request::default(), &mut Memo::default());
501 adapted.into_iter().map(Arc::unwrap_or_clone).collect()
502}
503
504#[derive(Debug, Default)]
511pub(crate) struct Memo {
512 shape: Option<Shape>,
513 given: Vec<Arc<Message>>,
514 shaped: Vec<Option<Arc<Message>>>,
515 taken: HashSet<String>,
516}
517
518#[derive(Debug, PartialEq)]
520struct Shape {
521 api: Api,
522 provider: String,
523 model: String,
524 context: Option<crate::message::Fingerprint>,
525 accepts: Accepts,
526 from_user: bool,
527}
528
529#[derive(Debug, Clone, Copy)]
531pub(crate) struct Request<'a> {
532 pub(crate) model: Option<&'a str>,
534 pub(crate) stored: bool,
536 pub(crate) tools: bool,
539 pub(crate) context: Option<crate::message::Fingerprint>,
541 pub(crate) foreign_reasoning: bool,
546}
547
548impl Default for Request<'_> {
549 fn default() -> Self {
550 Self {
551 model: None,
552 stored: false,
553 tools: true,
554 context: None,
555 foreign_reasoning: false,
556 }
557 }
558}
559
560pub(crate) fn context_of(
566 request: &crate::completion::CompletionRequest,
567 target: &dyn ReplayTarget,
568 model: &str,
569) -> crate::message::Fingerprint {
570 let mut tools: Vec<_> = request.tools.iter().collect();
571 tools.sort_by(|left, right| left.name.cmp(&right.name));
572 let raw = raw_tools(request);
573 let folds = target.later_system(model) == LaterSystem::Leading;
574 let mut system: Vec<&str> = request
575 .chat_history
576 .iter()
577 .map_while(|message| match &**message {
578 Message::System { content } => Some(Some(content.as_str())),
579 Message::User { .. } | Message::Assistant(_) => folds.then_some(None),
580 })
581 .flatten()
582 .filter(|content| !content.trim().is_empty())
583 .collect();
584 let joined;
585 if folds && system.len() > 1 {
586 joined = system.join("\n\n");
587 system = vec![joined.as_str()];
588 }
589 let mut fields = vec![serde_json::json!("context"), serde_json::json!(tools)];
590 fields.push(serde_json::json!(system));
591 if !raw.is_empty() {
592 fields.push(serde_json::json!(raw));
593 }
594 let fields = crate::message::Canonical(&serde_json::Value::Array(fields));
596 crate::message::Fingerprint::of_canonical(&fields)
597}
598
599pub(crate) fn declares_tools(request: &crate::completion::CompletionRequest) -> bool {
602 !request.tools.is_empty() || !raw_tools(request).is_empty()
603}
604
605pub(crate) fn raw_tools(request: &crate::completion::CompletionRequest) -> &[serde_json::Value] {
608 use crate::json_utils::Lenient;
609 request
610 .additional_params
611 .as_ref()
612 .map_or(&[][..], |params| params.arr("tools"))
613}
614
615pub(crate) fn adapt_for(
617 history: Vec<Arc<Message>>,
618 target: &dyn ReplayTarget,
619 request: &Request<'_>,
620 memo: &mut Memo,
621) -> Vec<Arc<Message>> {
622 let model = request.model.unwrap_or(target.model());
623 let stored = request.stored;
624 let same = Same {
625 api: target.api(),
626 provider: target.provider(),
627 model,
628 context: request.context.filter(|_| target.binds_context(model)),
629 };
630 let mut accepts = target.accepts(model);
631 accepts.tools &= request.tools;
632 accepts.foreign_reasoning &= request.foreign_reasoning;
633 let starts_with_user = target.starts_with_user() && !stored;
634 let shape = Shape {
635 api: same.api.clone(),
636 provider: same.provider.to_owned(),
637 model: model.to_owned(),
638 context: same.context,
639 accepts,
640 from_user: starts_with_user,
641 };
642 let resumed = memo.shape.as_ref() == Some(&shape)
643 && history.get(..memo.given.len()).is_some_and(|given| {
644 given
645 .iter()
646 .zip(&memo.given)
647 .all(|(message, was)| Arc::ptr_eq(message, was) || message == was)
648 });
649 if !resumed {
650 *memo = Memo::default();
651 }
652 let from = memo.given.len().saturating_sub(1);
654 let last_turn = history
655 .iter()
656 .rposition(|message| matches!(&**message, Message::Assistant(_)));
657 let (hosted, open) = hosted_pairs(&history, from, last_turn, target, &same);
658 let mut ids = Renamed {
659 to: Vec::new(),
660 before: &memo.taken,
661 taken: HashSet::new(),
662 };
663 let mut claimed = Vec::new();
664 let mut kept = 0;
665 let form = Form {
666 target,
667 model,
668 accepts,
669 };
670 let mut shaped: Vec<Option<Arc<Message>>> = Vec::with_capacity(history.len());
672 shaped.extend(memo.shaped.iter().cloned());
673 for (at, message) in history.iter().enumerate().skip(from) {
674 if Some(at) == last_turn {
675 claimed = ids.taken.iter().map(|id| id.to_string()).collect();
676 kept = shaped.len();
677 }
678 match &**message {
679 Message::System { content } => {
680 if !content.trim().is_empty() {
681 shaped.push(Some(message.clone()));
682 }
683 }
684 Message::User { content } => {
685 shaped.extend(
686 user(message, content, &mut ids, &form)
687 .into_iter()
688 .map(Some),
689 );
690 }
691 Message::Assistant(turn) if matches!(turn.stop, Some(StopReason::Aborted(_))) => {
692 shaped.push(stopped(turn));
693 }
694 Message::Assistant(turn)
696 if turn.stop.as_ref().is_some_and(|stop| stop.is_failure()) =>
697 {
698 shaped.push(None);
699 }
700 Message::Assistant(turn) => {
701 let last = Some(at) == last_turn
704 && (turn
705 .content
706 .iter()
707 .any(|block| matches!(block, AssistantContent::ToolCall(_)))
708 || history
709 .get(at + 1..)
710 .into_iter()
711 .flatten()
712 .all(|message| matches!(&**message, Message::System { .. })));
713 let here: HashSet<usize> = hosted
714 .iter()
715 .filter(|(message, _)| *message == at)
716 .map(|(_, block)| *block)
717 .collect();
718 shaped.push(assistant(
719 message, turn, &form, &same, &mut ids, &here, last,
720 ));
721 }
722 }
723 }
724 let (before, tail) = shaped.split_at(memo.shaped.len().min(shaped.len()));
725 let last = before.last().and_then(Option::as_deref);
726 if settled(tail, last, starts_with_user, accepts.tools) {
727 drop(ids);
728 match last_turn.filter(|_| !open) {
731 Some(upto) => {
732 let given = history.get(memo.given.len()..=upto).unwrap_or_default();
733 memo.given.extend(given.iter().cloned());
734 let shaped = shaped.get(memo.shaped.len()..kept).unwrap_or_default();
735 memo.shaped.extend(shaped.iter().cloned());
736 memo.taken.extend(claimed);
737 memo.shape = Some(shape);
738 }
739 None => *memo = Memo::default(),
740 }
741 let settled: Vec<Arc<Message>> = shaped.into_iter().flatten().collect();
742 #[cfg(test)]
743 {
744 let shaped = settled.iter().map(|message| Some(Message::clone(message)));
745 let rebuilt = reshaped(shaped.collect(), target, model, stored, accepts.tools);
746 let settled: Vec<&Message> = settled.iter().map(Arc::as_ref).collect();
747 assert_eq!(
748 settled,
749 rebuilt.iter().collect::<Vec<_>>(),
750 "a settled history changed"
751 );
752 }
753 return settled;
754 }
755 drop(ids);
756 *memo = Memo::default();
757 let shaped = shaped
758 .into_iter()
759 .map(|message| message.map(Arc::unwrap_or_clone));
760 let shaped = reshaped(shaped.collect(), target, model, stored, accepts.tools);
761 shaped.into_iter().map(Arc::new).collect()
762}
763
764fn settled<'a>(
771 history: &'a [Option<Arc<Message>>],
772 mut last: Option<&'a Message>,
773 starts_with_user: bool,
774 tools: bool,
775) -> bool {
776 let mut waiting: Vec<&CallId> = Vec::new();
777 for message in history {
778 let Some(message) = message.as_deref() else {
779 return false;
780 };
781 match (message, last) {
782 (Message::System { .. }, None) => continue,
783 (Message::System { .. }, Some(_))
784 | (Message::User { .. }, Some(Message::User { .. }))
785 | (Message::Assistant(_), Some(Message::Assistant(_))) => return false,
786 (Message::Assistant(_), None) if starts_with_user => return false,
787 (Message::Assistant(turn), _) => {
790 waiting.extend(turn.tool_calls().map(|call| &call.id));
791 if !tools && !waiting.is_empty() {
792 return false;
793 }
794 }
795 (Message::User { content }, _) => {
796 let results = content.iter().take_while(|part| is_result(part)).count();
797 let answered = content.iter().take(results).all(|part| match part {
798 UserContent::ToolResult(result) => {
799 let at = waiting.iter().position(|id| **id == result.call);
800 at.map(|at| waiting.remove(at)).is_some()
801 }
802 _ => false,
803 });
804 if content.is_empty()
805 || (!tools && results > 0)
806 || !answered
807 || !waiting.is_empty()
808 || content.iter().skip(results).any(is_result)
809 {
810 return false;
811 }
812 }
813 }
814 last = Some(message);
815 }
816 waiting.is_empty()
817}
818
819fn reshaped(
822 shaped: Vec<Option<Message>>,
823 target: &dyn ReplayTarget,
824 model: &str,
825 stored: bool,
826 tools: bool,
827) -> Vec<Message> {
828 let pairing = crate::conversation::pairing::Pairing {
832 stored,
833 answers: tools,
834 };
835 let shaped = merge_users(crate::conversation::pairing::pair(shaped, pairing).messages);
836 let shaped = if tools { shaped } else { tools_as_text(shaped) };
837 let shaped = match target.later_system(model) {
838 LaterSystem::InPlace => shaped,
839 LaterSystem::Leading => leading_system(shaped),
840 LaterSystem::UserText => system_as_user_text(shaped),
841 };
842 let shaped = if target.starts_with_user() && !stored {
843 from_first_user(shaped)
844 } else {
845 shaped
846 };
847 if target.alternates_roles() {
848 alternated(shaped)
849 } else {
850 shaped
851 }
852}
853
854pub(crate) fn cache_points(
862 points: &[crate::completion::CachePoint],
863 written: &[Arc<Message>],
864 documents: Option<&Message>,
865 adapted: &[Arc<Message>],
866 target: &dyn ReplayTarget,
867 request: &Request<'_>,
868) -> Vec<crate::completion::CachePoint> {
869 use crate::completion::CachePoint;
870 let prefix_of = |turns: usize, items: Option<usize>| -> Option<Vec<Arc<Message>>> {
871 let mut prefix = Vec::new();
872 let mut counted = 0;
873 for message in written {
874 if counted == turns {
875 break;
876 }
877 let mut message = Arc::clone(message);
878 if !matches!(*message, Message::System { .. }) {
879 counted += 1;
880 }
881 if let Some(items) = items.filter(|_| counted == turns) {
883 match Arc::make_mut(&mut message) {
884 Message::User { content } => content.truncate(items),
885 Message::Assistant(turn) => turn.content.truncate(items),
886 Message::System { .. } => {}
887 }
888 }
889 prefix.push(message);
890 }
891 (counted == turns).then_some(prefix)
892 };
893 points
894 .iter()
895 .filter_map(|point| {
896 let prefix = match *point {
897 CachePoint::Tools | CachePoint::System | CachePoint::Messages(0) => {
898 return Some(*point);
899 }
900 CachePoint::Messages(turns) => prefix_of(turns, None),
901 CachePoint::Within { message, items } => message
902 .checked_add(1)
903 .and_then(|turns| prefix_of(turns, Some(items))),
904 };
905 let mut prefix = prefix?;
906 if let Some(documents) = documents {
907 crate::completion::request::insert_after_leading_system(
908 &mut prefix,
909 documents.clone(),
910 );
911 }
912 let prefix = adapt_for(prefix, target, request, &mut Memo::default());
913 Some(shared_prefix(adapted, &prefix))
914 })
915 .collect()
916}
917
918fn shared_prefix(
921 history: &[Arc<Message>],
922 prefix: &[Arc<Message>],
923) -> crate::completion::CachePoint {
924 use crate::completion::CachePoint;
925 fn turns(history: &[Arc<Message>]) -> impl Iterator<Item = &Message> {
926 history
927 .iter()
928 .map(Arc::as_ref)
929 .filter(|message| !matches!(message, Message::System { .. }))
930 }
931 fn lead<T: PartialEq>(whole: &[T], part: &[T]) -> (usize, usize, usize) {
932 let shared = whole
933 .iter()
934 .zip(part)
935 .take_while(|(whole, part)| whole == part)
936 .count();
937 (shared, whole.len(), part.len())
938 }
939 let mut message = 0;
940 for (whole, part) in turns(history).zip(turns(prefix)) {
941 let (shared, whole, part) = match (whole, part) {
942 (Message::User { content: whole }, Message::User { content: part }) => {
943 lead(whole, part)
944 }
945 (Message::Assistant(whole), Message::Assistant(part)) => {
946 lead(&whole.content, &part.content)
947 }
948 _ => (0, 1, 1),
949 };
950 if shared == whole && shared == part {
951 message += 1;
952 continue;
953 }
954 return if shared == 0 {
955 CachePoint::Messages(message)
956 } else {
957 CachePoint::Within {
958 message,
959 items: shared,
960 }
961 };
962 }
963 CachePoint::Messages(message)
964}
965
966fn tools_as_text(history: Vec<Message>) -> Vec<Message> {
970 let history = history
971 .into_iter()
972 .map(|message| match message {
973 Message::Assistant(mut turn) => {
974 for block in &mut turn.content {
975 if let AssistantContent::ToolCall(call) = block {
976 *block = AssistantContent::Text(Text::new(format!(
977 "[called tool {} with {}]",
978 call.function.name,
979 crate::json_utils::to_canonical_string(
980 &call.function.arguments_value()
981 )
982 )));
983 }
984 }
985 Message::Assistant(turn)
986 }
987 Message::User { content } => Message::User {
988 content: content
989 .into_iter()
990 .map(|part| match part {
991 UserContent::ToolResult(result) => UserContent::text(result_text(&result)),
992 part => part,
993 })
994 .collect(),
995 },
996 message => message,
997 })
998 .collect();
999 merge_users(history)
1000}
1001
1002fn from_first_user(mut history: Vec<Message>) -> Vec<Message> {
1006 let lead = history
1007 .iter()
1008 .take_while(|message| matches!(message, Message::System { .. }))
1009 .count();
1010 while let Some(Message::Assistant(turn)) = history.get(lead) {
1011 let calls: HashSet<CallId> = turn.tool_calls().map(|call| call.id.clone()).collect();
1012 history.remove(lead);
1013 if let Some(Message::User { content }) = history.get_mut(lead) {
1014 content.retain(|part| {
1015 !matches!(part, UserContent::ToolResult(result) if calls.contains(&result.call))
1016 });
1017 if content.is_empty() {
1018 history.remove(lead);
1019 }
1020 }
1021 }
1022 history
1023}
1024
1025fn has_later_system(history: &[Message]) -> bool {
1028 history
1029 .iter()
1030 .skip_while(|message| matches!(message, Message::System { .. }))
1031 .any(|message| matches!(message, Message::System { .. }))
1032}
1033
1034fn system_as_user_text(history: Vec<Message>) -> Vec<Message> {
1036 let mut leading = true;
1037 let history = history
1038 .into_iter()
1039 .map(|message| match message {
1040 Message::System { content } if !leading => Message::User {
1041 content: vec![UserContent::text(content)],
1042 },
1043 message => {
1044 leading &= matches!(message, Message::System { .. });
1045 message
1046 }
1047 })
1048 .collect();
1049 merge_users(history)
1050}
1051
1052fn alternated(history: Vec<Message>) -> Vec<Message> {
1056 let mut alternated: Vec<Message> = Vec::with_capacity(history.len());
1057 let mut held: Vec<Message> = Vec::new();
1058 for message in history {
1059 let started = alternated
1060 .iter()
1061 .any(|message| !matches!(message, Message::System { .. }));
1062 match (message, alternated.last_mut()) {
1063 (message @ Message::System { .. }, _) if started => held.push(message),
1064 (Message::User { content }, Some(Message::User { content: previous })) => {
1065 previous.extend(content);
1066 }
1067 (Message::Assistant(turn), Some(Message::Assistant(previous))) => {
1068 if previous.origin != turn.origin {
1069 previous.origin = None;
1070 }
1071 previous.stop = turn.stop;
1072 previous.content.extend(turn.content);
1073 }
1074 (message, _) => {
1075 alternated.append(&mut held);
1076 alternated.push(message);
1077 }
1078 }
1079 }
1080 alternated.append(&mut held);
1081 alternated
1082}
1083
1084fn leading_system(history: Vec<Message>) -> Vec<Message> {
1087 if !has_later_system(&history) {
1088 return history;
1089 }
1090 let (system, rest): (Vec<Message>, Vec<Message>) = history
1091 .into_iter()
1092 .partition(|message| matches!(message, Message::System { .. }));
1093 let prompt: Vec<String> = system
1094 .into_iter()
1095 .filter_map(|message| match message {
1096 Message::System { content } => Some(content),
1097 Message::User { .. } | Message::Assistant(_) => None,
1098 })
1099 .collect();
1100 let mut history = Vec::with_capacity(rest.len() + 1);
1101 if !prompt.is_empty() {
1102 history.push(Message::system(prompt.join("\n\n")));
1103 }
1104 history.extend(merge_users(rest));
1106 history
1107}
1108
1109struct Renamed<'a> {
1115 to: Vec<(Cow<'a, CallId>, Cow<'a, CallId>)>,
1118 before: &'a HashSet<String>,
1120 taken: HashSet<Cow<'a, str>>,
1121}
1122
1123impl<'a> Renamed<'a> {
1124 fn turn(&mut self) {
1126 self.to.clear();
1127 }
1128
1129 fn claim(&mut self, source: Cow<'a, CallId>, wanted: Cow<'a, str>) -> Cow<'a, CallId> {
1137 let id = match &*source {
1138 CallId::Local(_) => source.clone(),
1139 CallId::Provider(_) => {
1140 let mut id = wanted.clone();
1141 let mut attempt: u64 = 1;
1142 while self.before.contains(id.as_ref()) || self.taken.contains(&id) {
1143 id = Cow::Owned(with_counter(&wanted, attempt));
1144 attempt += 1;
1145 }
1146 self.taken.insert(id.clone());
1147 if id == source.wire() {
1148 source.clone()
1149 } else {
1150 Cow::Owned(CallId::from_wire(id))
1151 }
1152 }
1153 };
1154 self.to.push((source, id.clone()));
1155 id
1156 }
1157
1158 fn answer(&mut self, source: &CallId) -> Option<Cow<'a, CallId>> {
1161 let mut calls = (self.to.iter().enumerate())
1162 .filter(|(_, (from, _))| from.as_ref() == source)
1163 .map(|(at, _)| at);
1164 let first = calls.next()?;
1165 if calls.next().is_some() {
1166 Some(self.to.remove(first).1)
1167 } else {
1168 self.to.get(first).map(|(_, id)| id.clone())
1169 }
1170 }
1171}
1172
1173fn with_counter(id: &str, attempt: u64) -> String {
1175 let mut digits = Vec::new();
1176 let mut value = attempt;
1177 while value > 0 {
1178 digits.push(char::from_digit((value % 36) as u32, 36).unwrap_or('0'));
1179 value /= 36;
1180 }
1181 digits.reverse();
1182 let keep = id.chars().count().saturating_sub(digits.len());
1183 id.chars().take(keep).chain(digits).collect()
1184}
1185
1186struct Same<'a> {
1188 api: Api,
1189 provider: &'a str,
1190 model: &'a str,
1191 context: Option<crate::message::Fingerprint>,
1193}
1194
1195impl Same<'_> {
1196 fn is(&self, origin: &Origin) -> bool {
1199 origin.same_model(&self.api, self.provider, self.model)
1200 && self
1201 .context
1202 .is_none_or(|context| origin.context == Some(context))
1203 }
1204}
1205
1206fn assistant<'a>(
1210 message: &Arc<Message>,
1211 turn: &'a AssistantMessage,
1212 form: &Form<'_>,
1213 same_model: &Same<'_>,
1214 ids: &mut Renamed<'a>,
1215 hosted: &HashSet<usize>,
1216 last: bool,
1217) -> Option<Arc<Message>> {
1218 let Form {
1219 target,
1220 model,
1221 accepts,
1222 } = *form;
1223 let same = turn
1224 .origin
1225 .as_ref()
1226 .is_some_and(|origin| same_model.is(origin));
1227 ids.turn();
1228 let content: Vec<Option<Cow<'a, AssistantContent>>> = turn
1229 .content
1230 .iter()
1231 .map(|block| {
1232 let block = if same {
1233 match block {
1234 AssistantContent::ToolCall(call) => {
1235 let id = ids.claim(Cow::Borrowed(&call.id), call.id.wire());
1236 if *id == call.id {
1237 Cow::Borrowed(block)
1238 } else {
1239 let keep =
1243 block.native_item().is_some() && target.call_id_slot().is_some();
1244 let id = id.into_owned();
1245 let renamed = crate::message::ToolCall { id, ..call.clone() };
1246 let mut renamed = AssistantContent::ToolCall(renamed);
1247 if keep
1248 && let Some(native) = renamed.native_slot().and_then(Option::take)
1249 {
1250 let blob_slot = native.blob_slot;
1251 renamed = renamed.with_native(native.item);
1252 if let Some(Some(native)) = renamed.native_slot() {
1253 native.blob_slot = blob_slot;
1254 }
1255 } else if let Some(Some(native)) = renamed.native_slot() {
1256 native.edited = true;
1257 }
1258 Cow::Owned(renamed)
1259 }
1260 }
1261 AssistantContent::Image(_) if !accepts.assistant_images => return None,
1264 block => Cow::Borrowed(block),
1265 }
1266 } else {
1267 Cow::Owned(match block.canonical() {
1268 AssistantContent::Reasoning(reasoning) => {
1269 if reasoning.redacted || reasoning.text.trim().is_empty() {
1270 return None;
1271 }
1272 if accepts.foreign_reasoning {
1273 AssistantContent::Reasoning(reasoning)
1274 } else {
1275 AssistantContent::Text(Text::new(reasoning.text))
1276 }
1277 }
1278 AssistantContent::Opaque(_) => return None,
1279 AssistantContent::Image(_) if !accepts.assistant_images => {
1280 AssistantContent::Text(Text::new(IMAGE_OMITTED))
1281 }
1282 AssistantContent::ToolCall(mut call) => {
1283 let normalized = target.normalize_tool_call_id(
1284 &call.id.wire(),
1285 model,
1286 turn.origin.as_ref(),
1287 );
1288 let source = Cow::Owned(call.id);
1289 call.id = ids.claim(source, Cow::Owned(normalized)).into_owned();
1290 AssistantContent::ToolCall(call)
1291 }
1292 block => block,
1293 })
1294 };
1295 let block = match &*block {
1298 AssistantContent::Opaque(opaque)
1299 if !accepts.tools
1300 && target.hosted_needs_tools()
1301 && target.hosted_pair(&opaque.item).is_some() =>
1302 {
1303 return None;
1304 }
1305 _ => block,
1306 };
1307 (!block.is_blank()).then_some(block)
1308 })
1309 .collect();
1310 let content: Vec<Cow<'a, AssistantContent>> = if same {
1311 paired(content, target, accepts.tools, hosted, last)
1312 } else {
1313 content.into_iter().flatten().collect()
1314 };
1315 let content: Vec<Cow<'a, AssistantContent>> = content
1316 .into_iter()
1317 .map(|block| match &*block {
1318 AssistantContent::Image(image) if image.native.is_none() => {
1319 if sendable_as_is(image) && form.sends(image, Place::Assistant) {
1320 return block;
1321 }
1322 let image = sendable_image(image.clone());
1323 Cow::Owned(
1324 if matches!(image.data, DocumentSourceKind::Unknown)
1325 || !target.encodes(model, Media::Image(&image, Place::Assistant))
1326 {
1327 AssistantContent::Text(Text::new(IMAGE_UNSENDABLE))
1328 } else {
1329 AssistantContent::Image(image)
1330 },
1331 )
1332 }
1333 _ => block,
1334 })
1335 .collect();
1336 if !turn.content.is_empty() && !content.iter().any(|block| target.sends_alone(block)) {
1337 return None;
1338 }
1339 if content.len() == turn.content.len()
1340 && content
1341 .iter()
1342 .all(|block| matches!(block, Cow::Borrowed(_)))
1343 {
1344 return Some(message.clone());
1345 }
1346 Some(Arc::new(Message::Assistant(AssistantMessage {
1347 content: content.into_iter().map(Cow::into_owned).collect(),
1348 origin: turn.origin.clone(),
1349 stop: turn.stop.clone(),
1350 })))
1351}
1352
1353fn stopped(turn: &AssistantMessage) -> Option<Arc<Message>> {
1358 let content: Vec<AssistantContent> = turn
1359 .content
1360 .iter()
1361 .filter(|block| matches!(block, AssistantContent::Text(_)))
1362 .map(AssistantContent::canonical)
1363 .filter(|block| !block.is_blank())
1364 .collect();
1365 (!content.is_empty()).then(|| Arc::new(Message::Assistant(AssistantMessage::new(content))))
1366}
1367
1368fn hosted_pairs(
1376 history: &[Arc<Message>],
1377 from: usize,
1378 boundary: Option<usize>,
1379 target: &dyn ReplayTarget,
1380 same: &Same<'_>,
1381) -> (HashSet<(usize, usize)>, bool) {
1382 let mut open: HashMap<String, (usize, usize)> = HashMap::new();
1383 let mut paired = HashSet::new();
1384 let mut open_at_boundary = false;
1385 for (at, message) in history.iter().enumerate().skip(from) {
1386 if Some(at) == boundary {
1387 open_at_boundary = !open.is_empty();
1388 }
1389 let Message::Assistant(turn) = &**message else {
1390 continue;
1391 };
1392 if turn.stop.as_ref().is_some_and(|stop| stop.is_failure())
1393 || !turn.origin.as_ref().is_some_and(|origin| same.is(origin))
1394 {
1395 continue;
1396 }
1397 for (index, block) in turn.content.iter().enumerate() {
1398 let AssistantContent::Opaque(opaque) = block else {
1399 continue;
1400 };
1401 if !opaque.replay {
1402 continue;
1403 }
1404 match target.hosted_pair(&opaque.item) {
1405 Some((Pairing::Use, id)) => {
1406 open.insert(id, (at, index));
1407 }
1408 Some((Pairing::Result, id)) => {
1409 if let Some(used) = open.remove(&id) {
1410 paired.insert(used);
1411 paired.insert((at, index));
1412 }
1413 }
1414 None => {}
1415 }
1416 }
1417 }
1418 (paired, open_at_boundary)
1419}
1420
1421fn paired<'a>(
1429 mut content: Vec<Option<Cow<'a, AssistantContent>>>,
1430 target: &dyn ReplayTarget,
1431 tools: bool,
1432 hosted: &HashSet<usize>,
1433 last: bool,
1434) -> Vec<Cow<'a, AssistantContent>> {
1435 let pair = |block: &AssistantContent| match block {
1436 AssistantContent::Opaque(opaque) if opaque.replay => target.hosted_pair(&opaque.item),
1437 _ => None,
1438 };
1439 for at in 0..content.len() {
1440 let Some((side, _)) = content.get(at).and_then(Option::as_deref).and_then(pair) else {
1441 continue;
1442 };
1443 let running = last
1444 && side == Pairing::Use
1445 && content
1446 .get(at + 1..)
1447 .into_iter()
1448 .flatten()
1449 .flatten()
1450 .all(|block| {
1451 matches!(
1452 **block,
1453 AssistantContent::ToolCall(_) | AssistantContent::Opaque(_)
1454 )
1455 });
1456 if !hosted.contains(&at)
1457 && !running
1458 && let Some(slot) = content.get_mut(at)
1459 {
1460 *slot = None;
1461 }
1462 }
1463 let (kept, needs): (Vec<bool>, Vec<bool>) = content
1468 .iter()
1469 .map(|block| {
1470 let item = block.as_deref().and_then(|block| match block {
1471 AssistantContent::Opaque(opaque) => opaque.replay.then_some(&opaque.item),
1472 block => block
1473 .native()
1474 .filter(|(item, current)| *current || !target.identity(item).is_empty())
1475 .map(|(item, _)| item),
1476 });
1477 (
1478 item.is_some(),
1479 item.is_some_and(|item| target.needs_next(item)),
1480 )
1481 })
1482 .unzip();
1483 for at in (0..content.len()).rev() {
1484 let next_gone = content
1488 .get(at + 1)
1489 .and_then(Option::as_deref)
1490 .is_none_or(|next| {
1491 !kept.get(at + 1).copied().unwrap_or_default()
1492 || (!tools && matches!(next, AssistantContent::ToolCall(_)))
1493 });
1494 if needs.get(at).copied().unwrap_or_default()
1495 && next_gone
1496 && let Some(slot) = content.get_mut(at)
1497 {
1498 *slot = None;
1499 }
1500 }
1501 content.into_iter().flatten().collect()
1502}
1503
1504struct Form<'a> {
1506 target: &'a dyn ReplayTarget,
1507 model: &'a str,
1508 accepts: Accepts,
1509}
1510
1511impl Form<'_> {
1512 fn sends(&self, image: &crate::message::Image, place: Place) -> bool {
1514 let reads = match place {
1515 Place::User => self.accepts.user_images,
1516 Place::ToolResult => self.accepts.tool_result_images,
1517 Place::Assistant => self.accepts.assistant_images,
1518 };
1519 reads
1520 && !matches!(image.data, DocumentSourceKind::Unknown)
1521 && self.target.encodes(self.model, Media::Image(image, place))
1522 }
1523}
1524
1525fn user<'a>(
1529 message: &Arc<Message>,
1530 content: &'a [UserContent],
1531 ids: &mut Renamed<'a>,
1532 form: &Form<'_>,
1533) -> Vec<Arc<Message>> {
1534 let mut shaped: Vec<Cow<'_, UserContent>> = Vec::with_capacity(content.len());
1535 let mut attached = Vec::new();
1536 for part in content {
1537 let placeholder = match part {
1538 UserContent::Image(image)
1539 if sendable_as_is(image) && form.sends(image, Place::User) =>
1540 {
1541 shaped.push(Cow::Borrowed(part));
1542 continue;
1543 }
1544 UserContent::Image(image) => {
1545 let image = sendable_image(image.clone());
1546 if form.sends(&image, Place::User) {
1547 shaped.push(Cow::Owned(UserContent::Image(image)));
1548 continue;
1549 }
1550 if form.accepts.user_images {
1551 IMAGE_UNSENDABLE
1552 } else {
1553 IMAGE_OMITTED
1554 }
1555 }
1556 UserContent::Audio(audio) => {
1557 let mut audio = audio.clone();
1558 audio.data = sendable(audio.data);
1559 if !matches!(audio.data, DocumentSourceKind::Unknown)
1560 && form.target.encodes(form.model, Media::Audio(&audio))
1561 {
1562 shaped.push(Cow::Owned(UserContent::Audio(audio)));
1563 continue;
1564 }
1565 AUDIO_UNSENDABLE
1566 }
1567 UserContent::Video(video) => {
1568 let mut video = video.clone();
1569 video.data = sendable(video.data);
1570 if !matches!(video.data, DocumentSourceKind::Unknown)
1571 && form.target.encodes(form.model, Media::Video(&video))
1572 {
1573 shaped.push(Cow::Owned(UserContent::Video(video)));
1574 continue;
1575 }
1576 VIDEO_UNSENDABLE
1577 }
1578 UserContent::Document(document) => {
1579 let mut document = document.clone();
1580 if let DocumentData::File(source) = document.data {
1581 document.data = DocumentData::File(sendable(source));
1582 }
1583 if document.data != DocumentData::File(DocumentSourceKind::Unknown)
1584 && form.target.encodes(form.model, Media::Document(&document))
1585 {
1586 shaped.push(Cow::Owned(UserContent::Document(document)));
1587 continue;
1588 }
1589 let text = match document.data {
1592 DocumentData::Text(text) => Some(text),
1593 DocumentData::File(DocumentSourceKind::Base64(data))
1594 if !matches!(document.media_type, None | Some(DocumentMediaType::PDF)) =>
1595 {
1596 BASE64_STANDARD
1597 .decode(data.as_bytes())
1598 .ok()
1599 .and_then(|bytes| String::from_utf8(bytes).ok())
1600 }
1601 DocumentData::File(_) => None,
1602 };
1603 if let Some(text) = text {
1604 shaped.push(Cow::Owned(UserContent::text(text)));
1605 continue;
1606 }
1607 DOCUMENT_UNSENDABLE
1608 }
1609 UserContent::ToolResult(result) => {
1610 let answer = ids.answer(&result.call);
1611 let parts = form.accepts.tool_result_images || form.target.result_parts(form.model);
1612 if answer.as_ref().is_none_or(|id| **id == result.call)
1613 && result_as_is(result, form, parts)
1614 {
1615 shaped.push(Cow::Borrowed(part));
1616 continue;
1617 }
1618 let mut result = result.clone();
1619 if let Some(id) = answer {
1620 result.call = id.into_owned();
1621 }
1622 result.content = result_images(result.content, form, &mut attached);
1623 result.content = result_text_parts(result.content, parts, result.is_error);
1624 shaped.push(Cow::Owned(UserContent::ToolResult(result)));
1625 continue;
1626 }
1627 UserContent::Text(text) if text.text.trim().is_empty() => continue,
1629 UserContent::Text(_) => {
1630 shaped.push(Cow::Borrowed(part));
1631 continue;
1632 }
1633 };
1634 let repeated = matches!(
1635 shaped.last().map(|part| &**part),
1636 Some(UserContent::Text(text)) if text.text == placeholder
1637 );
1638 if !repeated {
1639 shaped.push(Cow::Owned(UserContent::text(placeholder)));
1640 }
1641 }
1642 let unchanged =
1643 shaped.len() == content.len() && shaped.iter().all(|part| matches!(part, Cow::Borrowed(_)));
1644 if !shaped.is_empty() && unchanged && attached.is_empty() {
1645 return vec![message.clone()];
1646 }
1647 let mut messages = Vec::new();
1648 if !shaped.is_empty() {
1649 let content = shaped.into_iter().map(Cow::into_owned).collect();
1650 messages.push(Arc::new(Message::User { content }));
1651 }
1652 if !attached.is_empty() {
1653 let mut content = vec![UserContent::text(TOOL_IMAGES_HEADING)];
1654 content.extend(attached.into_iter().map(UserContent::Image));
1655 messages.push(Arc::new(Message::User { content }));
1656 }
1657 messages
1658}
1659
1660fn result_as_is(result: &ToolResult, form: &Form<'_>, parts: bool) -> bool {
1664 let images = result.content.iter().all(|part| match part {
1665 ToolResultContent::Image(image) => {
1666 sendable_as_is(image) && form.sends(image, Place::ToolResult)
1667 }
1668 ToolResultContent::Text(_) | ToolResultContent::Json { .. } => true,
1669 });
1670 images && !says_nothing(&result.content) && (parts || texts(&result.content) < 2)
1671}
1672
1673pub const NO_TOOL_OUTPUT: &str = "(no tool output)";
1676
1677fn result_text_parts(
1681 content: Vec<ToolResultContent>,
1682 parts: bool,
1683 is_error: bool,
1684) -> Vec<ToolResultContent> {
1685 if says_nothing(&content) {
1686 let text = if is_error {
1687 format!("[tool error] {NO_TOOL_OUTPUT}")
1688 } else {
1689 NO_TOOL_OUTPUT.to_owned()
1690 };
1691 return vec![ToolResultContent::text(text)];
1692 }
1693 if parts || texts(&content) < 2 {
1694 return content;
1695 }
1696 let mut joined: Vec<String> = Vec::new();
1697 let mut images = Vec::new();
1698 for part in content {
1699 match part {
1700 ToolResultContent::Text(text) => joined.push(text.text),
1701 ToolResultContent::Json { value } => joined.push(value.to_string()),
1702 image @ ToolResultContent::Image(_) => images.push(image),
1703 }
1704 }
1705 std::iter::once(ToolResultContent::text(joined.join("\n")))
1706 .chain(images)
1707 .collect()
1708}
1709
1710fn says_nothing(content: &[ToolResultContent]) -> bool {
1712 content.iter().all(|part| match part {
1713 ToolResultContent::Text(text) => text.text.trim().is_empty(),
1714 ToolResultContent::Json { .. } | ToolResultContent::Image(_) => false,
1715 })
1716}
1717
1718fn texts(content: &[ToolResultContent]) -> usize {
1720 content
1721 .iter()
1722 .filter(|part| !matches!(part, ToolResultContent::Image(_)))
1723 .count()
1724}
1725
1726fn result_text(result: &ToolResult) -> String {
1728 let text: Vec<String> = result
1729 .content
1730 .iter()
1731 .map(|part| match part {
1732 ToolResultContent::Text(text) => text.text.clone(),
1733 ToolResultContent::Json { value } => value.to_string(),
1734 ToolResultContent::Image(_) => TOOL_IMAGE_OMITTED.to_owned(),
1735 })
1736 .collect();
1737 let kind = if result.is_error { "error" } else { "result" };
1738 format!("[tool {} {kind}] {}", result.name, text.join("\n"))
1739}
1740
1741fn result_images(
1745 content: Vec<ToolResultContent>,
1746 form: &Form<'_>,
1747 attached: &mut Vec<crate::message::Image>,
1748) -> Vec<ToolResultContent> {
1749 let mut shaped: Vec<ToolResultContent> = Vec::with_capacity(content.len());
1750 for part in content {
1751 match part {
1752 ToolResultContent::Image(image) => {
1753 let image = sendable_image(image);
1754 if form.sends(&image, Place::ToolResult) {
1755 shaped.push(ToolResultContent::Image(image));
1756 continue;
1757 }
1758 let placeholder = if form.sends(&image, Place::User) {
1759 attached.push(image);
1760 TOOL_IMAGE_ATTACHED
1761 } else {
1762 TOOL_IMAGE_OMITTED
1763 };
1764 if shaped.last().and_then(ToolResultContent::as_text) != Some(placeholder) {
1765 shaped.push(ToolResultContent::text(placeholder));
1766 }
1767 }
1768 part => shaped.push(part),
1769 }
1770 }
1771 shaped
1772}
1773
1774fn sendable(source: DocumentSourceKind) -> DocumentSourceKind {
1777 match source {
1778 DocumentSourceKind::Raw(bytes) => DocumentSourceKind::Base64(BASE64_STANDARD.encode(bytes)),
1779 source => source,
1780 }
1781}
1782
1783fn sendable_as_is(image: &crate::message::Image) -> bool {
1786 match &image.data {
1787 DocumentSourceKind::Raw(_) => false,
1788 DocumentSourceKind::Base64(data) => image.media_type.is_some() || sniffed(data).is_none(),
1789 _ => true,
1790 }
1791}
1792
1793fn sendable_image(mut image: crate::message::Image) -> crate::message::Image {
1796 image.data = sendable(image.data);
1797 if image.media_type.is_none()
1798 && let DocumentSourceKind::Base64(data) = &image.data
1799 {
1800 image.media_type = sniffed(data);
1801 }
1802 image
1803}
1804
1805fn sniffed(data: &str) -> Option<ImageMediaType> {
1807 let head: String = data.chars().take(24).collect();
1808 let bytes = BASE64_STANDARD
1809 .decode(head.as_bytes())
1810 .or_else(|_| BASE64_STANDARD_NO_PAD.decode(head.as_bytes()))
1811 .ok()?;
1812 ImageMediaType::sniff(&bytes)
1813}
1814
1815fn merge_users(history: Vec<Message>) -> Vec<Message> {
1819 let mut merged: Vec<Message> = Vec::with_capacity(history.len());
1820 for message in history {
1821 match (merged.last_mut(), message) {
1822 (Some(Message::User { content: previous }), Message::User { content })
1823 if !previous.is_empty() && !content.is_empty() =>
1824 {
1825 previous.extend(content);
1826 }
1827 (_, message) => merged.push(message),
1828 }
1829 }
1830 merged
1831}
1832
1833#[cfg(test)]
1834mod tests;