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;