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;