Skip to main content

rig_core/completion/
provider_options.rs

1//! Typed per-provider request options and reply extras. A provider's
2//! [`ProviderExtension`] marker names its key, its serialize-only `Options`
3//! and its `Extras` view of a reply. [`ProviderOptions`] holds entries for
4//! several providers at once, and each wire reads only the entry named by
5//! its own provider.
6//!
7//! An entry is an object of sections: [`SHARED`] (`"*"`) for fields every
8//! route of the provider spells the same, and one section per route, named
9//! by its API (`"openai.chat"`, `"openai.responses"`). A wire merges `"*"`,
10//! then the section of the route it encodes, at the top level of the body,
11//! above the mapped generation options and below `additional_params`. A
12//! section for another route is skipped.
13//!
14//! Each `Options` type names its provider ([`ExtensionOptions::Ext`]), so
15//! [`ProviderOptions::set`] and `CompletionRequest::provider_option` take
16//! the options by value, with no provider type and no `?`. Options that do
17//! not serialize fail the request's encode.
18//!
19//! ```
20//! use rig_core::completion::{CompletionRequest, ProviderOptions};
21//! use rig_core::providers::openrouter::extension::{OpenRouterExt, OpenRouterOptions};
22//!
23//! let request =
24//!     CompletionRequest::new("hi").provider_option(OpenRouterOptions::new().session_id("s-1"));
25//! assert!(request.provider_options.contains::<OpenRouterExt>());
26//! ```
27
28use std::collections::BTreeMap;
29use std::fmt;
30use std::panic::{RefUnwindSafe, UnwindSafe};
31use std::sync::Arc;
32
33use serde::{Deserialize, Deserializer, Serialize, Serializer};
34use serde_json::{Map, Value};
35
36use crate::completion::{CompletionRequest, ReplayTarget};
37use crate::json_utils::remove_preserving_order;
38use crate::message::Api;
39
40/// The section every route of a provider reads.
41pub const SHARED: &str = "*";
42
43/// A provider's typed request options and reply extras.
44pub trait ProviderExtension {
45    /// The provider's key: the name its wires report as
46    /// [`ReplayTarget::provider`] and stamp on a reply's `Origin`.
47    ///
48    /// Stable API from 0.44: requests store provider options under this
49    /// key, and the model catalog files the provider's models under it, so
50    /// rig does not rename a provider's key in a minor release.
51    const PROVIDER: &'static str;
52    /// The request options: an object of sections, [`SHARED`] and route API
53    /// names, each an object of body keys.
54    type Options: ExtensionOptions;
55    /// The typed view of a reply's `raw` document.
56    type Extras: ReplyExtras;
57}
58
59/// A provider's request options. Serialize-only: the body keys they write
60/// are their only output. They are plain data, `Send + Sync` on every
61/// target, since a request carrying them may be held in shared host state,
62/// and unwind safe, so a request holding them stays unwind safe.
63pub trait ExtensionOptions:
64    Serialize + Clone + fmt::Debug + Send + Sync + UnwindSafe + RefUnwindSafe + 'static
65{
66    /// The provider these options are for: the entry
67    /// [`ProviderOptions::set`] stores them under. A type that serves as the
68    /// `Options` of several providers names one of them here, and is stored
69    /// for the others with [`ProviderOptions::with`].
70    type Ext: ProviderExtension<Options = Self>;
71
72    /// The fields `target` cannot send for `request`, each a top-level body
73    /// key with the reason. Each one set is reported through the request's
74    /// [`OnUnsupported`](crate::completion::OnUnsupported) policy under the
75    /// name `"<provider>.<section>.<field>"`. It must not call
76    /// [`options::param`](crate::completion::options::param), which reads
77    /// it. By default every field is sent.
78    fn unsupported(
79        &self,
80        target: &dyn ReplayTarget,
81        request: &CompletionRequest,
82    ) -> Vec<(&'static str, String)> {
83        let _ = (target, request);
84        Vec::new()
85    }
86}
87
88/// A typed view of a reply's provider document.
89pub trait ReplyExtras: Sized {
90    /// Read the view from `raw`, the reply's document on `api`.
91    ///
92    /// # Errors
93    ///
94    /// When `raw` does not hold the view's shape.
95    fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error>;
96}
97
98/// The value at `pointer` in a reply document, read as `T`: `None` when it
99/// is absent, `null` or an empty list, which a unary body may state where
100/// the document a stream rebuilds omits it. The one reader every `Extras`
101/// type's `from_reply` goes through.
102///
103/// # Errors
104///
105/// When the value is present but is not a `T`.
106pub(crate) fn reply_field<T: serde::de::DeserializeOwned>(
107    raw: &Value,
108    pointer: &str,
109) -> Result<Option<T>, serde_json::Error> {
110    match raw.pointer(pointer) {
111        None | Some(Value::Null) => Ok(None),
112        Some(Value::Array(items)) if items.is_empty() => Ok(None),
113        Some(value) => T::deserialize(value).map(Some),
114    }
115}
116
117/// Why a provider's options cannot be stored.
118#[non_exhaustive]
119#[derive(Debug, thiserror::Error)]
120pub enum OptionsError {
121    /// The options failed to serialize.
122    #[error("{provider} options do not serialize: {source}")]
123    Serialize {
124        /// The provider key.
125        provider: &'static str,
126        /// The serializer's error.
127        #[source]
128        source: serde_json::Error,
129    },
130    /// The options are not an object whose every value is an object.
131    #[error("{provider} options must serialize to an object of sections, each an object")]
132    NotSections {
133        /// The provider key.
134        provider: &'static str,
135    },
136}
137
138/// The refusal check of the typed options an entry was made from.
139trait Refusals: fmt::Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
140    fn refusals(
141        &self,
142        target: &dyn ReplayTarget,
143        request: &CompletionRequest,
144    ) -> Vec<(&'static str, String)>;
145}
146
147impl<T: ExtensionOptions> Refusals for T {
148    fn refusals(
149        &self,
150        target: &dyn ReplayTarget,
151        request: &CompletionRequest,
152    ) -> Vec<(&'static str, String)> {
153        self.unsupported(target, request)
154    }
155}
156
157/// One provider's entry: its sections and the typed options they came
158/// from, or the error the typed options failed to serialize with.
159#[derive(Clone)]
160enum Entry {
161    Sections {
162        sections: Map<String, Value>,
163        typed: Option<Arc<dyn Refusals>>,
164    },
165    Failed(Failure),
166}
167
168/// The error of options that did not serialize. It is never mutated once
169/// made, so a request holding it stays unwind safe although
170/// `serde_json::Error` is not.
171#[derive(Clone)]
172struct Failure(Arc<OptionsError>);
173
174impl UnwindSafe for Failure {}
175impl RefUnwindSafe for Failure {}
176
177impl Entry {
178    /// The sections, when the options serialized.
179    fn sections(&self) -> Option<&Map<String, Value>> {
180        match self {
181            Self::Sections { sections, .. } => Some(sections),
182            Self::Failed(_) => None,
183        }
184    }
185}
186
187/// `P`'s `options` as sections, empty when they write no field.
188fn sections_of<P: ProviderExtension>(
189    options: &P::Options,
190) -> Result<Map<String, Value>, OptionsError> {
191    let provider = P::PROVIDER;
192    let value = serde_json::to_value(options)
193        .map_err(|source| OptionsError::Serialize { provider, source })?;
194    let Value::Object(sections) = value else {
195        return Err(OptionsError::NotSections { provider });
196    };
197    non_empty_sections(sections).ok_or(OptionsError::NotSections { provider })
198}
199
200/// Typed options for several providers, one entry per provider key. Each
201/// wire reads only the entry named by its own provider. Equality and the
202/// serialized form are the entries' sections. A deserialized entry has no
203/// typed options behind it, so its fields are sent as written: inserting the
204/// typed options again restores their refusal check.
205///
206/// An entry [`Self::set`] stores from options that do not serialize holds
207/// the [`OptionsError`] instead of sections. It fails the encode of every
208/// request that carries it, whichever provider the request goes to, and it
209/// fails serializing this value, so the error is never dropped. Two failed
210/// entries are equal when their errors read the same.
211#[derive(Clone, Default)]
212pub struct ProviderOptions(BTreeMap<String, Entry>);
213
214impl ProviderOptions {
215    /// No entry.
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Store `options` as `P`'s entry, replacing any entry it had. Options
221    /// that write no field leave `P` with no entry.
222    ///
223    /// # Errors
224    ///
225    /// When `options` does not serialize to an object of object sections.
226    pub fn insert<P: ProviderExtension>(
227        &mut self,
228        options: &P::Options,
229    ) -> Result<&mut Self, OptionsError> {
230        let sections = sections_of::<P>(options)?;
231        self.put(P::PROVIDER, sections, || Arc::new(options.clone()));
232        Ok(self)
233    }
234
235    /// `self` with `options` as the entry of their provider,
236    /// [`ExtensionOptions::Ext`], replacing any entry it had. Options that
237    /// write no field leave that provider with no entry.
238    ///
239    /// It cannot fail: options that do not serialize to an object of object
240    /// sections are stored as a failed entry, which fails the encode of a
241    /// request that carries it with the [`OptionsError`] as the source.
242    /// [`Self::with`] reports the same error at once.
243    ///
244    /// The entry is always stored under `O::Ext`'s key, the built-in
245    /// provider the options type belongs to. A third-party provider whose
246    /// extension reuses a built-in options type (say `OpenAiOptions` for an
247    /// OpenAI-compatible gateway) must store them with
248    /// [`Self::with::<P>`](Self::with) instead, or its wire never reads them.
249    ///
250    /// ```
251    /// use rig_core::completion::ProviderOptions;
252    /// use rig_core::providers::openrouter::extension::{OpenRouterExt, OpenRouterOptions};
253    ///
254    /// let options = ProviderOptions::new().set(OpenRouterOptions::new().session_id("s-1"));
255    /// assert!(options.contains::<OpenRouterExt>());
256    /// ```
257    pub fn set<O: ExtensionOptions>(mut self, options: O) -> Self {
258        let provider = <O::Ext as ProviderExtension>::PROVIDER;
259        match sections_of::<O::Ext>(&options) {
260            Ok(sections) => self.put(provider, sections, || Arc::new(options)),
261            Err(error) => {
262                self.0
263                    .insert(provider.to_owned(), Entry::Failed(Failure(Arc::new(error))));
264            }
265        }
266        self
267    }
268
269    /// Store `sections` as `provider`'s entry, with the typed options
270    /// `typed` makes, or remove the entry when `sections` is empty.
271    fn put(
272        &mut self,
273        provider: &str,
274        sections: Map<String, Value>,
275        typed: impl FnOnce() -> Arc<dyn Refusals>,
276    ) {
277        if sections.is_empty() {
278            self.0.remove(provider);
279        } else {
280            self.0.insert(
281                provider.to_owned(),
282                Entry::Sections {
283                    sections,
284                    typed: Some(typed()),
285                },
286            );
287        }
288    }
289
290    /// `self` with `options` as `P`'s entry. See [`Self::insert`].
291    ///
292    /// # Errors
293    ///
294    /// As [`Self::insert`].
295    pub fn with<P: ProviderExtension>(
296        mut self,
297        options: &P::Options,
298    ) -> Result<Self, OptionsError> {
299        self.insert::<P>(options)?;
300        Ok(self)
301    }
302
303    /// `P`'s sections as the wire receives them, when it has an entry.
304    /// `None` for a failed entry ([`Self::set`]).
305    pub fn get<P: ProviderExtension>(&self) -> Option<&Map<String, Value>> {
306        self.0.get(P::PROVIDER).and_then(Entry::sections)
307    }
308
309    /// Remove `P`'s entry.
310    pub fn remove<P: ProviderExtension>(&mut self) {
311        self.0.remove(P::PROVIDER);
312    }
313
314    /// Whether `P` has an entry.
315    pub fn contains<P: ProviderExtension>(&self) -> bool {
316        self.0.contains_key(P::PROVIDER)
317    }
318
319    /// Whether no provider has an entry.
320    pub fn is_empty(&self) -> bool {
321        self.0.is_empty()
322    }
323
324    /// `self` with each entry of `over` in place of its own for the same
325    /// provider. An agent's options overlaid with a run's give the run's
326    /// entry for every provider the run names.
327    pub fn overlay(mut self, over: &ProviderOptions) -> ProviderOptions {
328        for (provider, entry) in &over.0 {
329            self.0.insert(provider.clone(), entry.clone());
330        }
331        self
332    }
333
334    /// The sections of `provider`'s entry.
335    pub(crate) fn sections(&self, provider: &str) -> Option<&Map<String, Value>> {
336        self.0.get(provider).and_then(Entry::sections)
337    }
338
339    /// The error of the first entry whose options did not serialize.
340    pub(crate) fn failure(&self) -> Option<&Arc<OptionsError>> {
341        self.0.values().find_map(|entry| match entry {
342            Entry::Failed(Failure(error)) => Some(error),
343            Entry::Sections { .. } => None,
344        })
345    }
346
347    /// The refusals of the typed options behind `provider`'s entry.
348    pub(crate) fn refusals(
349        &self,
350        provider: &str,
351        target: &dyn ReplayTarget,
352        request: &CompletionRequest,
353    ) -> Vec<(&'static str, String)> {
354        match self.0.get(provider) {
355            Some(Entry::Sections {
356                typed: Some(typed), ..
357            }) => typed.refusals(target, request),
358            _ => Vec::new(),
359        }
360    }
361
362    /// Remove `field` from the `sections` of `provider`'s entry.
363    pub(crate) fn remove_field(&mut self, provider: &str, sections: &[&str], field: &str) {
364        if let Some(Entry::Sections {
365            sections: entry, ..
366        }) = self.0.get_mut(provider)
367        {
368            for section in sections {
369                if let Some(Value::Object(fields)) = entry.get_mut(*section) {
370                    remove_preserving_order(fields, field);
371                }
372            }
373        }
374    }
375}
376
377/// `sections` without its empty sections, or `None` when one is not an
378/// object.
379fn non_empty_sections(sections: Map<String, Value>) -> Option<Map<String, Value>> {
380    let mut kept = Map::new();
381    for (name, section) in sections {
382        match section {
383            Value::Object(fields) if fields.is_empty() => {}
384            Value::Object(fields) => {
385                kept.insert(name, Value::Object(fields));
386            }
387            _ => return None,
388        }
389    }
390    Some(kept)
391}
392
393impl fmt::Debug for Entry {
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        match self {
396            Self::Sections { sections, .. } => sections.fmt(f),
397            Self::Failed(Failure(error)) => {
398                f.debug_tuple("Failed").field(&error.to_string()).finish()
399            }
400        }
401    }
402}
403
404impl fmt::Debug for ProviderOptions {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        f.debug_map().entries(&self.0).finish()
407    }
408}
409
410impl PartialEq for Entry {
411    fn eq(&self, other: &Self) -> bool {
412        match (self, other) {
413            (Self::Sections { sections: a, .. }, Self::Sections { sections: b, .. }) => a == b,
414            (Self::Failed(Failure(a)), Self::Failed(Failure(b))) => a.to_string() == b.to_string(),
415            _ => false,
416        }
417    }
418}
419
420impl PartialEq for ProviderOptions {
421    fn eq(&self, other: &Self) -> bool {
422        self.0 == other.0
423    }
424}
425
426impl Serialize for ProviderOptions {
427    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
428        if let Some(error) = self.failure() {
429            return Err(serde::ser::Error::custom(error));
430        }
431        serializer.collect_map(
432            self.0
433                .iter()
434                .filter_map(|(provider, entry)| Some((provider, entry.sections()?))),
435        )
436    }
437}
438
439impl<'de> Deserialize<'de> for ProviderOptions {
440    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
441        let entries = BTreeMap::<String, Map<String, Value>>::deserialize(deserializer)?;
442        let mut options = BTreeMap::new();
443        for (provider, sections) in entries {
444            let sections = non_empty_sections(sections).ok_or_else(|| {
445                serde::de::Error::custom(format!(
446                    "{provider} options must be an object of sections, each an object"
447                ))
448            })?;
449            if !sections.is_empty() {
450                options.insert(
451                    provider,
452                    Entry::Sections {
453                        sections,
454                        typed: None,
455                    },
456                );
457            }
458        }
459        Ok(Self(options))
460    }
461}
462
463#[cfg(test)]
464mod tests;