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