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;