Skip to main content

rig_core/completion/
options.rs

1//! Portable generation options: provider-neutral knobs a request sets once
2//! and every completion wire maps to its own JSON, or refuses. They hold only
3//! what [`CompletionRequest`](super::CompletionRequest) has no field for, so
4//! `temperature`, `max_tokens`, `tool_choice` and `output_schema` stay on the
5//! request.
6//!
7//! ```
8//! use rig_core::completion::{CacheRetention, CompletionRequest, Effort, GenerationOptions};
9//!
10//! let options = GenerationOptions::default()
11//!     .reasoning(Effort::High)
12//!     .cache(CacheRetention::Long);
13//! let request = CompletionRequest::new("Plan the refactor.").options(options);
14//! assert!(!request.options.is_default());
15//! ```
16
17use std::borrow::Cow;
18
19use serde::{Deserialize, Serialize};
20
21mod mapping;
22mod merge;
23
24pub(crate) use mapping::OptionName;
25pub use mapping::{Mapping, OptionFields, OptionMap};
26#[cfg(feature = "image")]
27pub(crate) use merge::deep_merge;
28pub use merge::{BaseInput, FinalBody, RawAt, Rewrite, check, param, request_params};
29pub(crate) use merge::{BodyRefusal, CatalogRefusal, catalog_refusals, collect_refusals};
30
31/// Provider-neutral generation knobs for one request. An unset field leaves
32/// the provider's default. A field the wire or model cannot honour is
33/// reported through [`Self::unsupported_policy`], never silently dropped.
34///
35/// This is the reusable value: build it once and hand it to an agent
36/// (`AgentBuilder::options`), overlay a run's on an agent's
37/// ([`Self::overlay`]), or store it serialized with a request. For a single
38/// request, `CompletionRequest`, `AgentBuilder` and `AgentRunner` also have
39/// a shortcut per field (`.seed(7)`) that writes that one field into their
40/// options.
41#[non_exhaustive]
42#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
43pub struct GenerationOptions {
44    /// How much the model reasons before answering.
45    #[serde(default)]
46    pub reasoning: Option<Reasoning>,
47    /// How long the provider keeps the prompt prefix cached.
48    #[serde(default)]
49    pub cache: Option<CacheRetention>,
50    /// The processing tier the request asks for.
51    #[serde(default)]
52    pub service_tier: Option<ServiceTier>,
53    /// How long the answer should be.
54    #[serde(default)]
55    pub verbosity: Option<Verbosity>,
56    /// Whether the model may call several tools in one turn.
57    #[serde(default)]
58    pub parallel_tool_calls: Option<bool>,
59    /// Nucleus sampling probability mass.
60    #[serde(default)]
61    pub top_p: Option<f64>,
62    /// Sampling seed, for providers that offer best-effort determinism.
63    #[serde(default)]
64    pub seed: Option<u64>,
65    /// Sequences that end generation. Empty means none.
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub stop: Vec<String>,
68    /// A key the provider routes prompt-cache lookups by, such as a
69    /// conversation's id: requests with the same key reach the server that
70    /// holds their prefix. A hint: providers that route without one send
71    /// nothing for it.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub cache_key: Option<String>,
74    /// What happens to an option the wire or model cannot honour. `None`
75    /// leaves the policy unset, which acts as [`OnUnsupported::Error`]
76    /// ([`Self::unsupported_policy`]); a set policy counts as setting an
77    /// option.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub on_unsupported: Option<OnUnsupported>,
80}
81
82impl GenerationOptions {
83    /// No option set and no policy set; the same as [`Self::default`].
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Whether every field holds its default: no option set, and no
89    /// policy set. A request whose options are default is not checked
90    /// against its model's catalog rules; setting any field, the policy
91    /// included, turns the checks on.
92    pub fn is_default(&self) -> bool {
93        *self == Self::default()
94    }
95
96    /// The policy in effect: the one set, or [`OnUnsupported::Error`].
97    pub fn unsupported_policy(&self) -> OnUnsupported {
98        self.on_unsupported.unwrap_or_default()
99    }
100
101    /// Set the reasoning level or budget.
102    pub fn reasoning(mut self, reasoning: impl Into<Reasoning>) -> Self {
103        self.reasoning = Some(reasoning.into());
104        self
105    }
106
107    /// Set the cache retention.
108    pub fn cache(mut self, cache: CacheRetention) -> Self {
109        self.cache = Some(cache);
110        self
111    }
112
113    /// Set the service tier.
114    pub fn service_tier(mut self, tier: ServiceTier) -> Self {
115        self.service_tier = Some(tier);
116        self
117    }
118
119    /// Set the answer verbosity.
120    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
121        self.verbosity = Some(verbosity);
122        self
123    }
124
125    /// Allow or forbid several tool calls in one turn.
126    pub fn parallel_tool_calls(mut self, parallel: bool) -> Self {
127        self.parallel_tool_calls = Some(parallel);
128        self
129    }
130
131    /// Set the nucleus sampling probability mass.
132    pub fn top_p(mut self, top_p: f64) -> Self {
133        self.top_p = Some(top_p);
134        self
135    }
136
137    /// Set the sampling seed.
138    pub fn seed(mut self, seed: u64) -> Self {
139        self.seed = Some(seed);
140        self
141    }
142
143    /// Replace the stop sequences.
144    pub fn stop<S: Into<String>>(mut self, stop: impl IntoIterator<Item = S>) -> Self {
145        self.stop = stop.into_iter().map(Into::into).collect();
146        self
147    }
148
149    /// Set the prompt-cache routing key.
150    pub fn cache_key(mut self, key: impl Into<String>) -> Self {
151        self.cache_key = Some(key.into());
152        self
153    }
154
155    /// Set what happens to an option the wire or model cannot honour.
156    pub fn on_unsupported(mut self, policy: OnUnsupported) -> Self {
157        self.on_unsupported = Some(policy);
158        self
159    }
160
161    /// Every option but the policy, borrowed, as a wire maps it.
162    /// Destructures `self` with no `..`, so a new field fails to compile
163    /// here first.
164    pub fn fields(&self) -> OptionFields<'_> {
165        let Self {
166            reasoning,
167            cache,
168            service_tier,
169            verbosity,
170            parallel_tool_calls,
171            top_p,
172            seed,
173            stop,
174            cache_key,
175            on_unsupported: _,
176        } = self;
177        OptionFields {
178            reasoning: reasoning.as_ref(),
179            cache: cache.as_ref(),
180            service_tier: service_tier.as_ref(),
181            verbosity: verbosity.as_ref(),
182            parallel_tool_calls: *parallel_tool_calls,
183            top_p: *top_p,
184            seed: *seed,
185            stop,
186            cache_key: cache_key.as_deref(),
187        }
188    }
189
190    /// `self` with every field `over` sets put on top: a `Some` option, a
191    /// non-empty `stop` list, a set `on_unsupported` (so a run can restore
192    /// `Error` over an agent's `Ignore`). Every other
193    /// field keeps `self`'s value. An agent's options overlaid with a run's
194    /// give the run's where it sets one.
195    pub fn overlay(self, over: &GenerationOptions) -> GenerationOptions {
196        let GenerationOptions {
197            reasoning,
198            cache,
199            service_tier,
200            verbosity,
201            parallel_tool_calls,
202            top_p,
203            seed,
204            stop,
205            cache_key,
206            on_unsupported,
207        } = over;
208        GenerationOptions {
209            reasoning: reasoning.or(self.reasoning),
210            cache: cache.or(self.cache),
211            service_tier: service_tier.or(self.service_tier),
212            verbosity: verbosity.or(self.verbosity),
213            parallel_tool_calls: parallel_tool_calls.or(self.parallel_tool_calls),
214            top_p: top_p.or(self.top_p),
215            seed: seed.or(self.seed),
216            stop: if stop.is_empty() {
217                self.stop
218            } else {
219                stop.clone()
220            },
221            cache_key: cache_key.clone().or(self.cache_key),
222            on_unsupported: on_unsupported.or(self.on_unsupported),
223        }
224    }
225}
226
227/// How much the model reasons before answering.
228#[non_exhaustive]
229#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
230#[serde(rename_all = "lowercase")]
231pub enum Reasoning {
232    /// No reasoning.
233    Off,
234    /// A portable effort level.
235    Effort(Effort),
236    /// An explicit reasoning-token budget.
237    Budget {
238        /// The most tokens the model may spend reasoning.
239        tokens: u32,
240    },
241}
242
243impl From<Effort> for Reasoning {
244    fn from(effort: Effort) -> Self {
245        Self::Effort(effort)
246    }
247}
248
249/// A portable reasoning effort level, lowest first.
250#[non_exhaustive]
251#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
252#[serde(rename_all = "lowercase")]
253pub enum Effort {
254    /// The least reasoning the model offers.
255    Minimal,
256    /// Low effort.
257    Low,
258    /// Medium effort.
259    Medium,
260    /// High effort.
261    High,
262    /// Extra-high effort.
263    XHigh,
264    /// The most reasoning the model offers.
265    Max,
266}
267
268impl Effort {
269    /// The level's lower-case wire word, as its serde name spells it:
270    /// `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` or `"max"`.
271    pub fn as_str(&self) -> &'static str {
272        match self {
273            Self::Minimal => "minimal",
274            Self::Low => "low",
275            Self::Medium => "medium",
276            Self::High => "high",
277            Self::XHigh => "xhigh",
278            Self::Max => "max",
279        }
280    }
281}
282
283/// How long the provider keeps the prompt prefix cached.
284#[non_exhaustive]
285#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
286#[serde(rename_all = "lowercase")]
287pub enum CacheRetention {
288    /// Do not cache.
289    None,
290    /// The provider's short retention, typically minutes.
291    Short,
292    /// The provider's long retention, typically an hour or more.
293    Long,
294}
295
296/// The processing tier a request asks for.
297#[non_exhaustive]
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[serde(rename_all = "lowercase")]
300pub enum ServiceTier {
301    /// The provider chooses.
302    Auto,
303    /// The standard tier.
304    Default,
305    /// Cheaper, slower processing.
306    Flex,
307    /// Faster, dearer processing.
308    Priority,
309}
310
311/// How long the answer should be.
312#[non_exhaustive]
313#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
314#[serde(rename_all = "lowercase")]
315pub enum Verbosity {
316    /// Terse.
317    Low,
318    /// Balanced.
319    Medium,
320    /// Detailed.
321    High,
322}
323
324impl Verbosity {
325    /// The level's lower-case wire word: `"low"`, `"medium"` or `"high"`.
326    pub fn as_str(&self) -> &'static str {
327        match self {
328            Self::Low => "low",
329            Self::Medium => "medium",
330            Self::High => "high",
331        }
332    }
333}
334
335/// What happens to an option the wire or model cannot honour.
336#[non_exhaustive]
337#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
338#[serde(rename_all = "lowercase")]
339pub enum OnUnsupported {
340    /// The request fails with [`UnsupportedOption`].
341    #[default]
342    Error,
343    /// The option is skipped with a warning.
344    Ignore,
345}
346
347/// An option the wire or model cannot honour. Carried by
348/// [`ProviderError::UnsupportedOption`](crate::error::ProviderError::UnsupportedOption).
349#[non_exhaustive]
350#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
351#[error("`{option}` is not supported by {provider} model `{model}`: {reason}")]
352pub struct UnsupportedOption {
353    /// The [`GenerationOptions`] field name, such as `"reasoning"`, a
354    /// provider option's `"<provider>.<section>.<field>"`, such as
355    /// `"openrouter.*.provider"`, or the body key a model's catalog entry
356    /// refuses, such as `"temperature"` or `"tools"`.
357    pub option: Cow<'static, str>,
358    /// The provider that refused it.
359    pub provider: String,
360    /// The model the request resolved to.
361    pub model: String,
362    /// Why it cannot be honoured.
363    pub reason: String,
364}
365
366impl UnsupportedOption {
367    /// The refusal of `option` by `provider` for `model`, because of `reason`.
368    pub fn new(
369        option: impl Into<Cow<'static, str>>,
370        provider: impl Into<String>,
371        model: impl Into<String>,
372        reason: impl Into<String>,
373    ) -> Self {
374        Self {
375            option: option.into(),
376            provider: provider.into(),
377            model: model.into(),
378            reason: reason.into(),
379        }
380    }
381}
382
383/// Why a dry run of a completion request failed: what
384/// [`DynModel::check`](crate::DynModel::check) returns.
385#[non_exhaustive]
386#[derive(Debug, thiserror::Error)]
387pub enum CheckError {
388    /// Every option the wire or model cannot honour, in the order the wire
389    /// meets them. The request builds once they are removed.
390    #[error("{}", unsupported_list(.0))]
391    Unsupported(Vec<UnsupportedOption>),
392    /// The request cannot be built for another reason, such as an empty
393    /// history. Refusals met before it are not listed, since the build
394    /// stopped there.
395    #[error(transparent)]
396    Invalid(crate::error::ProviderError),
397}
398
399/// The refusals, one per clause.
400fn unsupported_list(refusals: &[UnsupportedOption]) -> String {
401    refusals
402        .iter()
403        .map(ToString::to_string)
404        .collect::<Vec<_>>()
405        .join("; ")
406}
407
408#[cfg(test)]
409mod tests;