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, with a [`Fingerprint`] of that block so an edited
5//! 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 but never replayed to a model.
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110#[non_exhaustive]
111pub enum StopReason {
112 /// The model finished.
113 Stop,
114 /// The output-token limit cut the turn.
115 Length,
116 /// The model stopped to call tools.
117 ToolUse,
118 /// The provider failed the turn or refused it, with its explanation.
119 Error(String),
120 /// The caller cancelled the turn.
121 Aborted(String),
122}
123
124impl StopReason {
125 /// Whether the turn is incomplete and must not be replayed.
126 pub fn is_failure(&self) -> bool {
127 matches!(self, Self::Error(_) | Self::Aborted(_))
128 }
129}
130
131/// A 64-bit FNV-1a hash of a value's JSON serialization with every object's
132/// keys in sorted order and every whole number written as an integer, so a
133/// store that reorders keys (Postgres `jsonb`, sorted-key dumps) or writes
134/// `20.0` as `20` never changes it. It is stored as 16 hex digits, since a
135/// store that reads JSON numbers as doubles would round a 64-bit number.
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
137pub struct Fingerprint(u64);
138
139impl Serialize for Fingerprint {
140 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
141 serializer.serialize_str(&format!("{:016x}", self.0))
142 }
143}
144
145impl<'de> Deserialize<'de> for Fingerprint {
146 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147 let text = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
148 u64::from_str_radix(&text, 16)
149 .map(Self)
150 .map_err(|_| serde::de::Error::custom("a fingerprint is 16 hex digits"))
151 }
152}
153
154impl Fingerprint {
155 /// The fingerprint of `value`'s JSON bytes, keys sorted and whole
156 /// numbers as integers.
157 pub fn of(value: &impl Serialize) -> Self {
158 let mut hash = Fnv(0xcbf2_9ce4_8422_2325);
159 // Message types always serialize; a failure would only leave a
160 // fingerprint no native item matches, which replays canonically.
161 if let Ok(value) = serde_json::to_value(value) {
162 let _ = serde_json::to_writer(&mut hash, &Canonical(&value));
163 }
164 Self(hash.0)
165 }
166
167 /// The fingerprint of `value` hashed exactly as it serializes: every
168 /// JSON object inside must already be canonical (wrapped in
169 /// [`Canonical`], or with sorted keys and no floats).
170 pub(crate) fn of_canonical(value: &impl Serialize) -> Self {
171 let mut hash = Fnv(0xcbf2_9ce4_8422_2325);
172 let _ = serde_json::to_writer(&mut hash, value);
173 Self(hash.0)
174 }
175}
176
177/// FNV-1a over the bytes written to it.
178struct Fnv(u64);
179
180impl std::io::Write for Fnv {
181 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
182 for byte in bytes {
183 self.0 ^= u64::from(*byte);
184 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
185 }
186 Ok(bytes.len())
187 }
188
189 fn flush(&mut self) -> std::io::Result<()> {
190 Ok(())
191 }
192}
193
194/// A JSON value (or object) that serializes canonically: object keys
195/// sorted, and whole floats as integers (through `format!("{float:.0}")`
196/// parsed back, so `1e20` and `-0.0` come out as that parse writes them).
197pub(crate) struct Canonical<'a, T: ?Sized>(pub(crate) &'a T);
198
199impl Serialize for Canonical<'_, serde_json::Map<String, serde_json::Value>> {
200 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201 let mut fields: Vec<_> = self.0.iter().collect();
202 fields.sort_by_key(|(left, _)| *left);
203 serializer.collect_map(
204 fields
205 .into_iter()
206 .map(|(key, value)| (key, Canonical(value))),
207 )
208 }
209}
210
211impl Serialize for Canonical<'_, serde_json::Value> {
212 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
213 match self.0 {
214 serde_json::Value::Object(fields) => Canonical(fields).serialize(serializer),
215 serde_json::Value::Array(values) => {
216 serializer.collect_seq(values.iter().map(Canonical))
217 }
218 serde_json::Value::Number(number) => match number
219 .as_f64()
220 .filter(|float| number.is_f64() && float.fract() == 0.0)
221 .and_then(|float| format!("{float:.0}").parse::<serde_json::Number>().ok())
222 {
223 Some(whole) => whole.serialize(serializer),
224 None => number.serialize(serializer),
225 },
226 value => value.serialize(serializer),
227 }
228 }
229}
230
231/// One provider item, verbatim in its API's JSON shape, beside the
232/// canonical block (or message) it was decoded to.
233///
234/// `fingerprint` is the canonical form's at decode time. When the block is
235/// edited its fingerprint changes, the item is stale, and encoders rebuild
236/// the wire item from the canonical fields instead.
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238pub struct Native {
239 /// The provider item.
240 pub item: serde_json::Value,
241 /// The fingerprint of the canonical form the item was decoded to.
242 pub fingerprint: Fingerprint,
243}
244
245/// Where each wire keeps the opaque part of a reasoning item (a signature
246/// or an encrypted copy of the reasoning), as a JSON pointer into the item,
247/// by the [`Api`] that made it: Anthropic's `thinking.signature` and
248/// `redacted_thinking.data`, Bedrock Converse's `reasoningText.signature`
249/// and `redactedContent`, Responses' `reasoning.encrypted_content`, and
250/// Gemini's `thoughtSignature` (on any part, a call's included).
251const REASONING_BLOBS: [(&str, &str); 7] = [
252 ("anthropic.messages", "/signature"),
253 ("anthropic.messages", "/data"),
254 (
255 "bedrock.converse",
256 "/reasoningContent/reasoningText/signature",
257 ),
258 ("bedrock.converse", "/reasoningContent/redactedContent"),
259 ("openai.responses", "/encrypted_content"),
260 ("gemini.generate_content", "/thoughtSignature"),
261 ("vertexai.generate_content", "/thoughtSignature"),
262];
263
264/// The opaque part of a provider's reasoning item, as the wire that made it
265/// spells it ([`Native::reasoning_blob`]).
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub struct Blob<'a> {
268 /// Where it sits in the item, as a JSON pointer such as `/signature`.
269 pub key: &'static str,
270 /// The blob, as the provider sent it.
271 pub value: &'a str,
272}
273
274impl Native {
275 /// The opaque part of this item when `api`, the wire that made it, keeps
276 /// one there: a reasoning signature or encrypted reasoning, which only
277 /// that provider can read and which it may refuse once anything around
278 /// it changed. `None` for an item with none, or a wire rig does not know
279 /// blobs of.
280 ///
281 /// ```
282 /// use rig_core::message::{Api, Fingerprint, Native};
283 ///
284 /// let native = Native {
285 /// item: serde_json::json!({"type": "thinking", "thinking": "plan", "signature": "c2ln"}),
286 /// fingerprint: Fingerprint::of(&"plan"),
287 /// };
288 /// let api = Api::from_static("anthropic.messages");
289 /// assert_eq!(native.reasoning_blob(&api).map(|blob| blob.value), Some("c2ln"));
290 /// ```
291 pub fn reasoning_blob(&self, api: &Api) -> Option<Blob<'_>> {
292 REASONING_BLOBS
293 .iter()
294 .filter(|(wire, _)| *wire == api.as_str())
295 .find_map(|(_, key)| {
296 let value = self.item.pointer(key)?.as_str()?;
297 (!value.is_empty()).then_some(Blob { key, value })
298 })
299 }
300}
301
302/// A block's stored `native`, or `None` when it cannot be read, such as one
303/// fingerprinted by an earlier projection: the block then loads with its
304/// canonical fields and replays from them.
305pub(crate) fn lenient<'de, D: serde::Deserializer<'de>>(
306 deserializer: D,
307) -> Result<Option<Native>, D::Error> {
308 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
309 Ok(value.and_then(|value| Native::deserialize(value).ok()))
310}
311
312/// A provider item with no canonical meaning, such as a hosted-tool step or
313/// a compaction record. Only the API that produced it reads it.
314#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
315pub struct Opaque {
316 /// The provider item.
317 pub item: serde_json::Value,
318 /// Whether the item goes back to the model that produced it. A
319 /// client-executed call nothing answers is kept but never sent.
320 pub replay: bool,
321}
322
323impl Opaque {
324 /// The item's `type` field, when it is an object that has one.
325 pub fn kind(&self) -> Option<&str> {
326 self.item.get("type").and_then(serde_json::Value::as_str)
327 }
328}
329
330#[cfg(test)]
331mod tests;