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