Skip to main content

rig_core/completion/message/
native.rs

1//! Where an assistant turn came from and the provider items it was decoded
2//! from. An [`Origin`] names the wire, provider and requested model of a
3//! turn; a [`Native`] holds one provider item verbatim beside the canonical
4//! block it decoded to. Its baseline or [`Fingerprint`] detects edits so
5//! an edited block stops replaying its stale item.
6//!
7//! ```
8//! use rig_core::message::{AssistantContent, Text};
9//!
10//! let block = AssistantContent::Text(Text::new("hi"))
11//!     .with_native(serde_json::json!({"type": "text", "text": "hi", "citations": []}));
12//! assert!(block.native_item().is_some());
13//! ```
14
15use std::borrow::Cow;
16
17use serde::{Deserialize, Serialize};
18
19/// The wire format a turn was produced by, for example
20/// `"anthropic.messages"`, `"openai.responses"` or `"openai.chat"`.
21#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct Api(Cow<'static, str>);
24
25impl Api {
26    /// The API named `name`, in a const context.
27    pub const fn from_static(name: &'static str) -> Self {
28        Self(Cow::Borrowed(name))
29    }
30
31    /// The API's name.
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl From<&'static str> for Api {
38    fn from(name: &'static str) -> Self {
39        Self::from_static(name)
40    }
41}
42
43impl From<String> for Api {
44    fn from(name: String) -> Self {
45        Self(Cow::Owned(name))
46    }
47}
48
49impl std::fmt::Display for Api {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.write_str(&self.0)
52    }
53}
54
55/// Which wire, provider and model produced an assistant turn.
56///
57/// `model` is the model the request named. Replay compares it, with `api`
58/// and `provider`, against the target: only an exact match replays the
59/// turn's provider items.
60#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
61pub struct Origin {
62    /// The wire format.
63    pub api: Api,
64    /// The provider descriptor name (`"anthropic"`).
65    pub provider: String,
66    /// The model the request named.
67    pub model: String,
68    /// The model the provider reported, when it reported one.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub response_model: Option<String>,
71    /// The provider's response id, when it sent one.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub response_id: Option<String>,
74    /// The fingerprint of the request's tools and system prompt. A wire
75    /// that binds its items to them ([`ReplayTarget::binds_context`])
76    /// replays a turn made under another context as if from another model.
77    ///
78    /// [`ReplayTarget::binds_context`]: crate::completion::ReplayTarget::binds_context
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub context: Option<Fingerprint>,
81}
82
83impl Origin {
84    /// A turn from `model` on `provider` over `api`, with no response
85    /// metadata.
86    pub fn new(api: impl Into<Api>, provider: impl Into<String>, model: impl Into<String>) -> Self {
87        Self {
88            api: api.into(),
89            provider: provider.into(),
90            model: model.into(),
91            response_model: None,
92            response_id: None,
93            context: None,
94        }
95    }
96
97    /// Whether this turn came from exactly the wire, provider and model
98    /// `target` names. The one sameness rule replay uses.
99    pub fn same_model(&self, api: &Api, provider: &str, model: &str) -> bool {
100        &self.api == api && self.provider == provider && self.model == model
101    }
102}
103
104/// How an assistant turn ended.
105///
106/// A turn that ended in [`Self::Error`] or [`Self::Aborted`] is kept in
107/// history as no answer. An errored turn is never replayed to a model; an
108/// aborted one is replayed as the text it had sent.
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111#[non_exhaustive]
112pub enum StopReason {
113    /// The model finished.
114    Stop,
115    /// The output-token limit cut the turn.
116    Length,
117    /// The model stopped to call tools.
118    ToolUse,
119    /// The provider failed the turn or refused it, with its explanation.
120    Error(String),
121    /// The caller cancelled the turn.
122    Aborted(String),
123}
124
125impl StopReason {
126    /// Whether the turn is incomplete, so it answers nothing.
127    pub fn is_failure(&self) -> bool {
128        matches!(self, Self::Error(_) | Self::Aborted(_))
129    }
130}
131
132/// A 64-bit FNV-1a hash of a value's JSON serialization with every object's
133/// keys in sorted order and every whole number written as an integer, so a
134/// store that reorders keys (Postgres `jsonb`, sorted-key dumps) or writes
135/// `20.0` as `20` never changes it. It is stored as 16 hex digits, since a
136/// store that reads JSON numbers as doubles would round a 64-bit number.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub struct Fingerprint(u64);
139
140impl Serialize for Fingerprint {
141    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142        serializer.serialize_str(&format!("{:016x}", self.0))
143    }
144}
145
146impl<'de> Deserialize<'de> for Fingerprint {
147    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
148        let text = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
149        u64::from_str_radix(&text, 16)
150            .map(Self)
151            .map_err(|_| serde::de::Error::custom("a fingerprint is 16 hex digits"))
152    }
153}
154
155impl Fingerprint {
156    /// The fingerprint of `value`'s JSON bytes, keys sorted and whole
157    /// numbers as integers.
158    pub fn of(value: &impl Serialize) -> Self {
159        let mut hash = Fnv(0xcbf2_9ce4_8422_2325);
160        // Message types always serialize; a failure would only leave a
161        // fingerprint no native item matches, which replays canonically.
162        if let Ok(value) = serde_json::to_value(value) {
163            let _ = serde_json::to_writer(&mut hash, &Canonical(&value));
164        }
165        Self(hash.0)
166    }
167
168    /// The fingerprint of `value` hashed exactly as it serializes: every
169    /// JSON object inside must already be canonical (wrapped in
170    /// [`Canonical`], or with sorted keys and no floats).
171    pub(crate) fn of_canonical(value: &impl Serialize) -> Self {
172        let mut hash = Fnv(0xcbf2_9ce4_8422_2325);
173        let _ = serde_json::to_writer(&mut hash, value);
174        Self(hash.0)
175    }
176}
177
178/// FNV-1a over the bytes written to it.
179struct Fnv(u64);
180
181impl std::io::Write for Fnv {
182    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
183        for byte in bytes {
184            self.0 ^= u64::from(*byte);
185            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
186        }
187        Ok(bytes.len())
188    }
189
190    fn flush(&mut self) -> std::io::Result<()> {
191        Ok(())
192    }
193}
194
195/// A JSON value (or object) that serializes canonically: object keys
196/// sorted, and whole floats as integers (through `format!("{float:.0}")`
197/// parsed back, so `1e20` and `-0.0` come out as that parse writes them).
198pub(crate) struct Canonical<'a, T: ?Sized>(pub(crate) &'a T);
199
200impl Serialize for Canonical<'_, serde_json::Map<String, serde_json::Value>> {
201    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
202        let mut fields: Vec<_> = self.0.iter().collect();
203        fields.sort_by_key(|(left, _)| *left);
204        serializer.collect_map(
205            fields
206                .into_iter()
207                .map(|(key, value)| (key, Canonical(value))),
208        )
209    }
210}
211
212impl Serialize for Canonical<'_, serde_json::Value> {
213    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
214        match self.0 {
215            serde_json::Value::Object(fields) => Canonical(fields).serialize(serializer),
216            serde_json::Value::Array(values) => {
217                serializer.collect_seq(values.iter().map(Canonical))
218            }
219            serde_json::Value::Number(number) => match number
220                .as_f64()
221                .filter(|float| number.is_f64() && float.fract() == 0.0)
222                .and_then(|float| format!("{float:.0}").parse::<serde_json::Number>().ok())
223            {
224                Some(whole) => whole.serialize(serializer),
225                None => number.serialize(serializer),
226            },
227            value => value.serialize(serializer),
228        }
229    }
230}
231
232/// One provider item, verbatim in its API's JSON shape, beside the
233/// canonical block (or message) it was decoded to.
234///
235/// `baseline` compares calls after storage; other blocks and older records
236/// use `fingerprint`. An edited block keeps its item for identity, but
237/// encoders rebuild it from canonical fields.
238#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
239pub struct Native {
240    /// The provider item.
241    pub item: serde_json::Value,
242    /// The fingerprint of the canonical form the item was decoded to.
243    pub fingerprint: Fingerprint,
244    /// The original projection of a call containing floats. Floats stay
245    /// numeric; integers and strings carry text markers to preserve their type
246    /// and exact value. Other blocks and older records use fingerprint-only
247    /// validation. Construct this with [`super::AssistantContent::with_native`].
248    /// Keep it unchanged when editing the call.
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub baseline: Option<serde_json::Value>,
251    /// Whether an edit was detected before serialization. A stored edit
252    /// stays stale even if JSON parsing makes its numbers equal again.
253    #[serde(default, skip_serializing_if = "crate::json_utils::is_false")]
254    pub edited: bool,
255    /// Where the item keeps its reasoning blob, as a JSON pointer such as
256    /// `/signature`, when it holds one. The reply fold sets it from the
257    /// wire's [`ReplayTarget::blob_slots`].
258    ///
259    /// [`ReplayTarget::blob_slots`]: crate::completion::ReplayTarget::blob_slots
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub blob_slot: Option<Cow<'static, str>>,
262}
263
264/// The opaque part of a provider's reasoning item ([`Native::reasoning_blob`]).
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
266pub struct Blob<'a> {
267    /// Where it sits in the item, as a JSON pointer such as `/signature`.
268    pub key: &'a str,
269    /// The blob, as the provider sent it.
270    pub value: &'a str,
271}
272
273impl Native {
274    /// The opaque part of this item, when it holds one: a reasoning
275    /// signature or encrypted reasoning, which only the provider that made
276    /// it can read and which it may refuse once anything around it changed.
277    ///
278    /// ```
279    /// use rig_core::message::{Fingerprint, Native};
280    ///
281    /// let native = Native {
282    ///     item: serde_json::json!({"type": "thinking", "thinking": "plan", "signature": "c2ln"}),
283    ///     fingerprint: Fingerprint::of(&"plan"),
284    ///     baseline: None,
285    ///     edited: false,
286    ///     blob_slot: Some("/signature".into()),
287    /// };
288    /// assert_eq!(native.reasoning_blob().map(|blob| blob.value), Some("c2ln"));
289    /// ```
290    pub fn reasoning_blob(&self) -> Option<Blob<'_>> {
291        let key = self.blob_slot.as_deref()?;
292        let value = self.item.pointer(key)?.as_str()?;
293        (!value.is_empty()).then_some(Blob { key, value })
294    }
295
296    /// Records the first of `slots` that holds a non-empty string in the
297    /// item as its blob, or none.
298    pub(crate) fn find_blob(&mut self, slots: &[&'static str]) {
299        let holds = |slot: &&str| {
300            self.item
301                .pointer(slot)
302                .and_then(serde_json::Value::as_str)
303                .is_some_and(|value| !value.is_empty())
304        };
305        self.blob_slot = slots.iter().copied().find(holds).map(Cow::Borrowed);
306    }
307}
308
309pub(super) fn baseline(value: serde_json::Value) -> serde_json::Value {
310    use serde_json::Value;
311    match value {
312        Value::Number(number) if !number.is_f64() => Value::String(format!("i:{number}")),
313        Value::String(text) => Value::String(format!("s:{text}")),
314        Value::Array(values) => Value::Array(values.into_iter().map(baseline).collect()),
315        Value::Object(fields) => Value::Object(
316            fields
317                .into_iter()
318                .map(|(key, value)| (key, baseline(value)))
319                .collect(),
320        ),
321        value => value,
322    }
323}
324
325pub(super) fn matches_baseline(saved: &serde_json::Value, current: &serde_json::Value) -> bool {
326    use serde_json::Value;
327    match (saved, current) {
328        (Value::String(saved), Value::Number(number)) => {
329            let Some(integer) = saved.strip_prefix("i:") else {
330                return false;
331            };
332            if number.is_f64() {
333                number
334                    .as_f64()
335                    .filter(|value| value.fract() == 0.0)
336                    .is_some_and(|value| format!("{value:.0}") == integer)
337            } else {
338                number.to_string() == integer
339            }
340        }
341        (Value::String(saved), Value::String(current)) => {
342            saved.strip_prefix("s:") == Some(current.as_str())
343        }
344        (Value::Array(saved), Value::Array(current)) => {
345            saved.len() == current.len()
346                && saved
347                    .iter()
348                    .zip(current)
349                    .all(|(saved, current)| matches_baseline(saved, current))
350        }
351        (Value::Object(saved), Value::Object(current)) => {
352            saved.len() == current.len()
353                && saved.iter().all(|(key, saved)| {
354                    current
355                        .get(key)
356                        .is_some_and(|current| matches_baseline(saved, current))
357                })
358        }
359        (Value::Number(_), Value::Number(_))
360        | (Value::Bool(_), Value::Bool(_))
361        | (Value::Null, Value::Null) => saved == current,
362        _ => false,
363    }
364}
365
366/// A block's stored `native`, or `None` when it cannot be read, such as one
367/// fingerprinted by an earlier projection: the block then loads with its
368/// canonical fields and replays from them.
369pub(crate) fn lenient<'de, D: serde::Deserializer<'de>>(
370    deserializer: D,
371) -> Result<Option<Native>, D::Error> {
372    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
373    Ok(value.and_then(|value| Native::deserialize(value).ok()))
374}
375
376/// A provider item with no canonical meaning, such as a hosted-tool step or
377/// a compaction record. Only the API that produced it reads it.
378#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
379pub struct Opaque {
380    /// The provider item.
381    pub item: serde_json::Value,
382    /// Whether the item goes back to the model that produced it. A
383    /// client-executed call nothing answers is kept but never sent.
384    pub replay: bool,
385}
386
387impl Opaque {
388    /// The item's `type` field, when it is an object that has one.
389    pub fn kind(&self) -> Option<&str> {
390        self.item.get("type").and_then(serde_json::Value::as_str)
391    }
392}
393
394#[cfg(test)]
395mod tests;