Skip to main content

rig_core/completion/
history.rs

1//! Shaping a conversation for the model it is sent to. [`adapt`] is the one
2//! place history meets its target: a turn the same model produced keeps its
3//! provider items, any other turn replays from its canonical fields, failed
4//! turns are skipped, and every tool call gets an answer.
5//!
6//! ```
7//! use rig_core::completion::CompletionRequest;
8//! use rig_core::completion::history::{Accepts, ReplayTarget, adapt};
9//! use rig_core::completion::options::{Mapping, OptionFields, OptionMap};
10//! use rig_core::message::{Api, Message};
11//!
12//! #[derive(Debug)]
13//! struct Target;
14//!
15//! impl ReplayTarget for Target {
16//!     fn api(&self) -> Api {
17//!         Api::from_static("example.chat")
18//!     }
19//!     fn provider(&self) -> &str {
20//!         "example"
21//!     }
22//!     fn model(&self) -> &str {
23//!         "example-1"
24//!     }
25//!     fn accepts(&self, _model: &str) -> Accepts {
26//!         Accepts::ALL
27//!     }
28//!     fn map_options(&self, _request: &CompletionRequest, fields: OptionFields<'_>) -> OptionMap {
29//!         let OptionFields {
30//!             reasoning,
31//!             cache,
32//!             service_tier,
33//!             verbosity,
34//!             parallel_tool_calls,
35//!             top_p,
36//!             seed,
37//!             stop,
38//!             cache_key,
39//!         } = fields;
40//!         let none = |set: bool| match set {
41//!             true => Mapping::unsupported("the example takes no options"),
42//!             false => Mapping::Nothing,
43//!         };
44//!         OptionMap {
45//!             reasoning: none(reasoning.is_some()),
46//!             cache: none(cache.is_some()),
47//!             service_tier: none(service_tier.is_some()),
48//!             verbosity: none(verbosity.is_some()),
49//!             parallel_tool_calls: none(parallel_tool_calls.is_some()),
50//!             top_p: none(top_p.is_some()),
51//!             seed: none(seed.is_some()),
52//!             stop: none(!stop.is_empty()),
53//!             cache_key: Mapping::unrouted(cache_key),
54//!         }
55//!     }
56//! }
57//!
58//! let history = vec![Message::user("hi"), Message::assistant("hello")];
59//! assert_eq!(adapt(&history, &Target), history);
60//! ```
61
62use 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
77/// What replaces a user image for a model without image input.
78pub const USER_IMAGE_OMITTED: &str = "(image omitted: model does not support images)";
79
80/// What replaces another model's assistant image for a model that reads no
81/// images in assistant turns.
82pub const ASSISTANT_IMAGE_OMITTED: &str = "(image omitted: model does not support images)";
83
84/// What replaces a tool-result image for a model without image input.
85pub const TOOL_IMAGE_OMITTED: &str = "(tool image omitted: model does not support images)";
86
87/// What replaces an image the provider cannot receive in its form.
88pub const IMAGE_UNSENDABLE: &str = "(image omitted: the provider cannot receive it in this form)";
89
90/// What replaces audio the provider cannot receive.
91pub const AUDIO_UNSENDABLE: &str = "(audio omitted: the provider cannot receive it in this form)";
92
93/// What replaces video the provider cannot receive.
94pub const VIDEO_UNSENDABLE: &str = "(video omitted: the provider cannot receive it in this form)";
95
96/// What replaces a document the provider cannot receive, when it holds no
97/// text to send instead.
98pub const DOCUMENT_UNSENDABLE: &str =
99    "(document omitted: the provider cannot receive it in this form)";
100
101/// What a tool-result image becomes in the result when the image moves to
102/// the user message that follows.
103pub const TOOL_IMAGE_ATTACHED: &str = "(see attached image)";
104
105/// The text heading the user message that carries tool-result images.
106pub const TOOL_IMAGES_HEADING: &str = "Attached image(s) from tool result:";
107
108/// What a model reads, as replay sees it.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct Accepts {
111    /// Images in user messages.
112    pub user_images: bool,
113    /// Images in assistant turns another model produced.
114    pub assistant_images: bool,
115    /// Images inside tool results.
116    pub tool_result_images: bool,
117    /// Tool calls and their results.
118    pub tools: bool,
119    /// Another model's reasoning text as reasoning: the wire has a place for
120    /// reasoning that carries no provider item or signature, such as Chat
121    /// Completions' `reasoning_content` field. Read only for a request that
122    /// asks for it ([`CompletionRequest::foreign_reasoning`]).
123    ///
124    /// [`CompletionRequest::foreign_reasoning`]: field@crate::completion::CompletionRequest::foreign_reasoning
125    pub foreign_reasoning: bool,
126}
127
128impl Accepts {
129    /// Every kind of content. Another model's reasoning is replayed as plain
130    /// text (`foreign_reasoning` is off).
131    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    /// Text and tools, no images. Another model's reasoning is replayed as
140    /// plain text.
141    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/// Where an image sits in a history.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Place {
153    /// In a user message.
154    User,
155    /// Inside a tool result.
156    ToolResult,
157    /// In an assistant turn another model produced.
158    Assistant,
159}
160
161/// One media part, as [`ReplayTarget::encodes`] sees it. Raw bytes are
162/// already base64, and an inline image already has the media type its bytes
163/// name.
164#[derive(Debug, Clone, Copy)]
165pub enum Media<'a> {
166    /// An image, and where it sits.
167    Image(&'a crate::message::Image, Place),
168    /// Audio in a user message.
169    Audio(&'a crate::message::Audio),
170    /// Video in a user message.
171    Video(&'a crate::message::Video),
172    /// A document in a user message.
173    Document(&'a crate::message::Document),
174}
175
176/// The model a request is sent to, as replay sees it. Every completion wire
177/// implements it.
178pub trait ReplayTarget: std::fmt::Debug + WasmCompatSync {
179    /// The wire format.
180    fn api(&self) -> Api;
181
182    /// The provider descriptor name.
183    fn provider(&self) -> &str;
184
185    /// The model id the wire addresses.
186    fn model(&self) -> &str;
187
188    /// What `model`, the model a request addresses on this wire, reads.
189    /// [`adapt`] downgrades everything else, so the encoder never sees it.
190    fn accepts(&self, model: &str) -> Accepts;
191
192    /// How this wire answers each option of `request`, whose resolved model
193    /// and typed fields an answer may depend on. No default: every
194    /// completion wire writes one, destructuring `fields` with no `..`, so a
195    /// new option fails to compile until the wire answers for it. An unset
196    /// option answers `Mapping::Nothing`, so wrap each answer in
197    /// `Mapping::of` (`Mapping::of_stop` for `stop`), as in
198    /// `seed: Mapping::of(seed, |_| Mapping::unsupported("no such field"))`;
199    /// any other answer for an unset option fails every request.
200    /// `Completion::prepare` reports each refusal before the wire encodes.
201    fn map_options(
202        &self,
203        request: &crate::completion::CompletionRequest,
204        fields: crate::completion::options::OptionFields<'_>,
205    ) -> crate::completion::options::OptionMap;
206
207    /// The model facts this wire encodes with: the spec it was connected
208    /// to and the catalog that answers for other models. `None`, the
209    /// default, answers from the built-in catalog.
210    fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
211        None
212    }
213
214    /// Whether the encoder carries `media` to `model`: its source (data,
215    /// URL, file id or string), its media type, and where it sits. [`adapt`]
216    /// replaces every part this refuses with a placeholder, or a text
217    /// document with its text, so the encoder never refuses canonical
218    /// content. Row H9 of the history conformance suite holds every wire to
219    /// it. By default every form is carried.
220    fn encodes(&self, model: &str, media: Media<'_>) -> bool {
221        let _ = (model, media);
222        true
223    }
224
225    /// The id `id` takes when a call another model made is sent to `model`
226    /// on this wire. `source` is the call's origin, `None` for a hand-built
227    /// turn. Results answering the call are rewritten to match, and an id
228    /// another call already took is made distinct.
229    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    /// Whether `request` continues a conversation the provider stores, such
235    /// as one naming a previous interaction. Results before the history's
236    /// first turn then answer calls the provider holds, so [`adapt`] keeps
237    /// them.
238    fn continues_stored(&self, request: &crate::completion::CompletionRequest) -> bool {
239        let _ = request;
240        false
241    }
242
243    /// Whether `request` declares tools to the provider, in `tools`, in the
244    /// `tools` of its `additional_params`, or through tools the wire adds.
245    /// A request that declares none gets its history's calls and results as
246    /// text. `ToolChoice::None` still declares them: the wire sends its own
247    /// `none` choice beside the tool history.
248    fn declares_tools(&self, request: &crate::completion::CompletionRequest) -> bool {
249        declares_tools(request)
250    }
251
252    /// The keys of a provider item that survive an edit of its block: an
253    /// encoder rebuilding the edited block keeps them ([`Replay::Identity`]).
254    /// By default none do.
255    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    /// Where a tool call's id sits in this wire's call item, as a JSON
261    /// pointer, when it has one. Replay writes the call's wire id there, so
262    /// a replayed item and its result always agree, and a call the provider
263    /// sent without an id keeps its item only on a wire with a slot.
264    fn call_id_slot(&self) -> Option<&'static str> {
265        None
266    }
267
268    /// Whether every reply on this wire states why it stopped. A reply that
269    /// then names no reason failed; a wire
270    /// with no finish vocabulary, such as a local runtime, says `false`.
271    fn states_finish_reason(&self) -> bool {
272        true
273    }
274
275    /// Whether `model` binds its provider items to the request's tools and
276    /// system prompt, so a turn made under another context replays as if
277    /// from another model.
278    fn binds_context(&self, model: &str) -> bool {
279        let _ = model;
280        false
281    }
282
283    /// Whether `request` asks the provider to drop an item bound to another
284    /// context itself (Anthropic's `drop_block`), so a turn made under
285    /// another context replays verbatim rather than as another model's.
286    fn drops_unbound_items(&self, request: &crate::completion::CompletionRequest) -> bool {
287        let _ = request;
288        false
289    }
290
291    /// The target `request` is sent to, for a wire that picks its API per
292    /// request. Preparing the request and folding its reply both read the
293    /// target this names, so a turn records the API it was made on. `None`,
294    /// the default, is this target.
295    fn route(&self, request: &crate::completion::CompletionRequest) -> Option<&dyn ReplayTarget> {
296        let _ = request;
297        None
298    }
299
300    /// Whether the encoder sends a request's documents itself. Otherwise
301    /// they join the history as text before it is adapted.
302    fn takes_documents(&self) -> bool {
303        false
304    }
305
306    /// Whether this provider item requires the item after it in its turn
307    /// (Responses reasoning): when that one is not replayed, neither is
308    /// this.
309    fn needs_next(&self, item: &serde_json::Value) -> bool {
310        let _ = item;
311        false
312    }
313
314    /// The hosted-tool pair this opaque item belongs to: whether it is the
315    /// use or the result, and the id they share. A use and its result
316    /// replay only together, in one turn or across the model's turns. A use
317    /// still running when the last turn ended (a paused turn, or one whose
318    /// client call the hosted tool made) replays alone.
319    fn hosted_pair(&self, item: &serde_json::Value) -> Option<(Pairing, String)> {
320        let _ = item;
321        None
322    }
323
324    /// Whether the wire rejects a conversation whose first message after
325    /// the system prompt is not a user message. [`adapt`] then drops the
326    /// assistant turns before the first user message, with their results.
327    fn starts_with_user(&self) -> bool {
328        false
329    }
330
331    /// Where `model` takes system messages that come after the
332    /// conversation begins ([`LaterSystem`]). By default where the history
333    /// has them.
334    fn later_system(&self, model: &str) -> LaterSystem {
335        let _ = model;
336        LaterSystem::InPlace
337    }
338
339    /// Whether the wire requires user and assistant messages to alternate.
340    /// [`adapt`] then joins two messages of one role that only system
341    /// messages separate, which move after them.
342    fn alternates_roles(&self) -> bool {
343        false
344    }
345
346    /// Whether a hosted tool's use and result need the request to declare
347    /// tools. A request that declares none then sends neither.
348    fn hosted_needs_tools(&self) -> bool {
349        false
350    }
351
352    /// Whether `model` reads a tool result as several parts. When it reads
353    /// neither parts nor result images, [`adapt`] joins a result's text
354    /// parts into one.
355    fn result_parts(&self, model: &str) -> bool {
356        let _ = model;
357        false
358    }
359
360    /// Whether `block` on its own makes an assistant message this wire
361    /// sends. A turn with no such block carries nothing, so [`adapt`] drops
362    /// it and the user messages around it become one. By default every
363    /// block does.
364    fn sends_alone(&self, block: &AssistantContent) -> bool {
365        let _ = block;
366        true
367    }
368}
369
370/// Where a target takes the system messages that come after the
371/// conversation begins ([`ReplayTarget::later_system`]).
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum LaterSystem {
374    /// Where the history has them.
375    InPlace,
376    /// Joined with the leading ones into one system message.
377    Leading,
378    /// As user text where the history has them, so adding one never moves
379    /// the prefix before it.
380    UserText,
381}
382
383/// Which side of a hosted-tool pair an opaque item is.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
385pub enum Pairing {
386    /// The hosted tool's use.
387    Use,
388    /// Its result.
389    Result,
390}
391
392/// What an encoder sends for one assistant block.
393#[derive(Debug, Clone, PartialEq)]
394pub enum Replay<'a> {
395    /// The provider item, still current, with a call's id slot spelled.
396    Item(std::borrow::Cow<'a, serde_json::Value>),
397    /// The block was edited: rebuild it from its canonical fields, keeping
398    /// these keys of its stale item.
399    Identity(serde_json::Map<String, serde_json::Value>),
400    /// Rebuild it from its canonical fields.
401    Rebuild,
402}
403
404impl AssistantContent {
405    /// What `target`'s encoder sends for this block, with call ids spelled
406    /// by `ids`. Encoders read provider items only through this.
407    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
436/// Set the value at `pointer` in `item`, creating objects on the way.
437fn 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
457/// `history` shaped for `target`.
458///
459/// - Blank system messages are dropped.
460/// - A turn whose origin names `target`'s API, provider and model keeps its
461///   provider items. Its reasoning and blank text are dropped unless they
462///   still hold a current provider item.
463/// - Any other turn keeps only canonical fields: provider items are cleared,
464///   reasoning becomes plain text, or stays reasoning when the request asks
465///   for it and the model reads it ([`Accepts::foreign_reasoning`]);
466///   redacted or empty reasoning is dropped;
467///   [`Opaque`](crate::message::Opaque) items are dropped and call ids go
468///   through [`ReplayTarget::normalize_tool_call_id`], made distinct when two
469///   calls would share one.
470/// - Content the model does not read ([`ReplayTarget::accepts`]) is
471///   downgraded: images become placeholder text, a tool-result image moves
472///   to a user message after the results when only user images are read,
473///   and without tools calls and results become text.
474/// - Opaque items marked not to replay are always dropped, and so is a turn
475///   that ended in an error or was aborted, with the results answering it.
476/// - Calls and results pair by [`crate::conversation::pairing::pair`]: an
477///   unanswered call gets a
478///   [`NO_RESULT_PROVIDED`](crate::conversation::pairing::NO_RESULT_PROVIDED)
479///   result and an orphan result is dropped.
480/// - Adjacent user messages become one. A turn left empty is dropped, and so
481///   is a user message left empty. A message that was empty to begin with is
482///   kept, for the request boundary to reject.
483pub 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/// What [`adapt_for`] keeps of a settled history for the next request
490/// adapted the same way: the history up to its last turn, what the messages
491/// before that turn became, and the call ids they claimed. A message's
492/// shape depends only on the messages before it, but for the last turn's,
493/// so a history that starts with the same messages (the same `Arc`s, or
494/// equal ones) is adapted from that turn on.
495#[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/// What adapting reads besides the messages.
504#[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/// What `adapt` reads of the request a history is sent with.
515#[derive(Debug, Clone, Copy)]
516pub(crate) struct Request<'a> {
517    /// The model it names in place of the wire's own.
518    pub(crate) model: Option<&'a str>,
519    /// Whether it continues a conversation the provider stores.
520    pub(crate) stored: bool,
521    /// Whether it carries tools ([`ReplayTarget::declares_tools`]): a
522    /// request with none gets calls and results as text.
523    pub(crate) tools: bool,
524    /// The fingerprint of its tools and system prompt ([`context_of`]).
525    pub(crate) context: Option<crate::message::Fingerprint>,
526    /// Whether it asks for other models' reasoning as reasoning
527    /// ([`CompletionRequest::foreign_reasoning`]).
528    ///
529    /// [`CompletionRequest::foreign_reasoning`]: field@crate::completion::CompletionRequest::foreign_reasoning
530    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
545/// The fingerprint of `request`'s tool definitions, by name, and of its
546/// system prompt as [`adapt`] sends it to `model` on `target`: what a
547/// context-bound item was made under. The prompt is the leading non-blank
548/// system messages, or every one when the target folds them into one, so
549/// the request before `adapt` and after it give the same fingerprint.
550pub(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    // Hashed as built: `Fingerprint::of` would copy it first.
580    let fields = crate::message::Canonical(&serde_json::Value::Array(fields));
581    crate::message::Fingerprint::of_canonical(&fields)
582}
583
584/// Whether `request` declares tools in `tools` or in the `tools` of its
585/// `additional_params`.
586pub(crate) fn declares_tools(request: &crate::completion::CompletionRequest) -> bool {
587    !request.tools.is_empty() || !raw_tools(request).is_empty()
588}
589
590/// The tools `request` passes as is in the `tools` of its
591/// `additional_params`, such as a provider's hosted tools.
592pub(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
600/// [`adapt`] for `request`.
601pub(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    // The last message kept was the last turn, so it is adapted again.
638    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    // A message adapting leaves as it is goes on as the same `Arc`.
656    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            // A failed turn is skipped, with the results answering it.
677            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                // A use can still be running only in the last turn, and only
684                // while nothing new follows it or the turn awaits a client call.
685                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        // The next history adapts from the last turn on unless a hosted use
711        // before it may yet pair.
712        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
746/// Whether `history`, shaped message by message, is already what pairing
747/// and the role rules make of it, so they can be skipped: no turn was
748/// dropped, the system messages all lead, user and assistant messages
749/// alternate (from a user message when `starts_with_user`), and each turn's
750/// calls are answered exactly, results first, by the user message after it.
751/// Without `tools`, it holds no call or result at all.
752fn 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            // A turn follows nothing or a user message, which answered
770            // every call before it, so nothing waits here.
771            (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
801/// `shaped`, adapted message by message, paired and put in the roles
802/// `target` takes.
803fn reshaped(
804    shaped: Vec<Option<Message>>,
805    target: &dyn ReplayTarget,
806    model: &str,
807    stored: bool,
808    tools: bool,
809) -> Vec<Message> {
810    // Calls and results pair first, so an orphan result goes whether or not
811    // the request declares tools; only then does a request without tools
812    // get them as text, with no result made up for an unanswered call.
813    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
836/// `points`, which name `written` (a request's history before its
837/// `documents` join it), as they fall in `adapted`, what [`adapt_for`] made
838/// of it with `request`. A message point becomes the longest part of
839/// `adapted` that the adaption of its prefix alone begins with: the point
840/// never covers what follows the prefix, however the adaption dropped,
841/// added or joined messages around it. A point past the end of `written`
842/// goes.
843pub(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            // Only the message the point cuts inside is copied.
864            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
900/// Where `prefix` stops being the start of `history`, both adapted, as a
901/// cache point over `history`. System messages are not counted.
902fn 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
948/// `history` with its calls and results as text, for a request that
949/// declares no tools. Keys are sorted, so the text is the same however the
950/// provider ordered the arguments.
951fn 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
984/// `history` from its first user message, after the leading system
985/// messages: an assistant turn before it goes, with the results answering
986/// it, until a user message leads.
987fn 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
1007/// Whether `history` has a system message after its first user or
1008/// assistant message.
1009fn 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
1016/// `history` with every later system message as user text where it stands.
1017fn 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
1034/// `history` with no two user or assistant messages of one role in a row:
1035/// two that only system messages separate become one, and the system
1036/// messages move after them.
1037fn 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
1066/// `history` with its system messages joined into one leading message, when
1067/// any comes after the conversation begins.
1068fn 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    // Moving a system message out may leave two user messages adjacent.
1087    history.extend(merge_users(rest));
1088    history
1089}
1090
1091/// Call ids as the target sends them. Every provider id in the adapted
1092/// history is distinct: a repeated one, in one turn or across turns, takes a
1093/// counter. The renames of the latest turn map its results, in call order,
1094/// so two calls that shared an id are answered by the results that followed
1095/// them in turn.
1096struct Renamed<'a> {
1097    /// For the latest turn: each call's source id and the id it took, in
1098    /// call order.
1099    to: Vec<(Cow<'a, CallId>, Cow<'a, CallId>)>,
1100    /// The ids an earlier adaption of the same messages claimed ([`Memo`]).
1101    before: &'a HashSet<String>,
1102    taken: HashSet<Cow<'a, str>>,
1103}
1104
1105impl<'a> Renamed<'a> {
1106    /// A new turn begins: its results answer only its own calls.
1107    fn turn(&mut self) {
1108        self.to.clear();
1109    }
1110
1111    /// The id the call `source` takes, wanting `wanted`: `wanted`, or, when
1112    /// an earlier call took it, the same id with its tail replaced by a
1113    /// counter until it is free. A counter of lowercase alphanumerics keeps
1114    /// the id's length and is legal on every wire. An id rig issued stays
1115    /// as it is: [`WireIds`] spells it.
1116    ///
1117    /// [`WireIds`]: crate::providers::internal::wire_ids::WireIds
1118    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    /// The id a result for `source` answers: the next call of the latest
1141    /// turn that had it, the last one once each was answered.
1142    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
1155/// `id` with its last characters replaced by `attempt` in base 36.
1156fn 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
1168/// The model a history is sent to, as a turn's origin is compared with it.
1169struct Same<'a> {
1170    api: Api,
1171    provider: &'a str,
1172    model: &'a str,
1173    /// The request's context, when the target binds items to it.
1174    context: Option<crate::message::Fingerprint>,
1175}
1176
1177impl Same<'_> {
1178    /// Whether `origin` is this model, made under this context where the
1179    /// target binds items to it.
1180    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
1188/// The turn `message`, holding `turn`, shaped for `form` and the target
1189/// named by `same`: `message` itself when nothing in it changes, nothing
1190/// when no block it sends alone is left.
1191fn 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                            // A rename is rig's, not an edit: replay spells
1222                            // the new id into the item's slot, so the item
1223                            // stays.
1224                            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                    // The capability holds for the model's own turns too: an
1239                    // image it made but does not read back is left out.
1240                    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            // Keys sorted, so the text is the same however the provider
1273            // ordered the arguments.
1274            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
1330/// The hosted uses and results that pair, as (message, block) positions in
1331/// the model's own replayed turns. A use pairs with the next result of its id,
1332/// in its turn or a later one: a programmatic tool call's code execution
1333/// returns its result in the turn after the client call it made. A second use
1334/// of an id before its result leaves the first unpaired. Read from message
1335/// `from` on, with whether a use was still open when message `boundary`
1336/// began.
1337fn 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
1383/// A same-model turn's kept blocks (`None` where `adapt` dropped one), with
1384/// every block whose partner is gone dropped too: an item that needs the one
1385/// after it ([`ReplayTarget::needs_next`]), edited or not, when that one is
1386/// dropped or only rebuilt, and a hosted use or result whose block position
1387/// is not in `hosted` ([`ReplayTarget::hosted_pair`]). In the `last` turn,
1388/// one that ends the history or awaits a client call, a use followed only by
1389/// calls and opaque items is still running and stays.
1390fn 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    // The item each block goes back as, read once: its item while current,
1426    // or a rebuild that keeps the item's identity (an edited block rebuilt
1427    // under it needs its partner as much as the item itself); whether it
1428    // goes back as one, and whether that item needs the block after it.
1429    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        // The partner must go back as the item the provider issued: its item,
1447        // or a rebuild that keeps its identity, never a block with neither.
1448        // A request without tools gets a call as text.
1449        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
1466/// The target a user message is shaped for.
1467struct Form<'a> {
1468    target: &'a dyn ReplayTarget,
1469    model: &'a str,
1470    accepts: Accepts,
1471}
1472
1473impl Form<'_> {
1474    /// Whether an image the model reads at `place` can be sent there.
1475    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
1487/// The user message `message`, holding `content`, shaped for `form`: one
1488/// message, or two when tool-result images move to a message of their own,
1489/// or `message` itself when nothing in it changes.
1490fn 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                // Base64 of a media type other than PDF that decodes as
1552                // UTF-8 is text the wire can still read.
1553                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            // Blank text says nothing, and several providers reject it.
1590            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
1622/// Whether `result` is sent as it is: every image in it is sent in the
1623/// result as it is ([`result_images`]), and its text needs no placeholder
1624/// or joining ([`result_text_parts`]).
1625fn 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
1635/// What replaces an empty tool result: models answer a call whose result
1636/// says nothing better than one with no content.
1637pub const NO_TOOL_OUTPUT: &str = "(no tool output)";
1638
1639/// `content` of a result that has nothing to say, said plainly, and joined
1640/// into one text unless the model reads several `parts`: Gemini 2 rejects a
1641/// result of several parts.
1642fn 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
1672/// Whether a result's `content` is only blank text, or nothing.
1673fn 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
1680/// How many parts of a result's `content` are text or JSON.
1681fn texts(content: &[ToolResultContent]) -> usize {
1682    content
1683        .iter()
1684        .filter(|part| !matches!(part, ToolResultContent::Image(_)))
1685        .count()
1686}
1687
1688/// A tool result as text, for a model without tools.
1689fn 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
1703/// `content` with each image the result cannot carry replaced: it becomes
1704/// [`TOOL_IMAGE_ATTACHED`] and moves to `attached` when it can go in a user
1705/// message, else [`TOOL_IMAGE_OMITTED`].
1706fn 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
1736/// `source` with raw bytes given as base64, which every wire that takes
1737/// inline data reads.
1738fn sendable(source: DocumentSourceKind) -> DocumentSourceKind {
1739    match source {
1740        DocumentSourceKind::Raw(bytes) => DocumentSourceKind::Base64(BASE64_STANDARD.encode(bytes)),
1741        source => source,
1742    }
1743}
1744
1745/// Whether [`sendable_image`] leaves `image` as it is: no raw bytes to
1746/// encode, and no media type its bytes would name.
1747fn 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
1755/// `image` with raw bytes as base64, and, when it is inline data without a
1756/// media type, the type its bytes name.
1757fn 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
1767/// The image type the base64 `data` starts with.
1768fn 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
1777/// `history` with each run of adjacent user messages made one: a wire that
1778/// requires alternating roles would otherwise reject it. An empty user
1779/// message stays on its own, for the request boundary to reject.
1780fn 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;