rig_core/completion/options/mapping.rs
1//! What a completion wire answers for each option: [`OptionFields`] is the
2//! request's options as a wire reads them, and [`OptionMap`] holds one
3//! [`Mapping`] per option. Neither has `..` or a default, so a new option
4//! fails to compile in every wire until it answers for it.
5
6use serde_json::Value;
7
8use super::{CacheRetention, Reasoning, ServiceTier, Verbosity};
9use crate::completion::CompletionRequest;
10
11/// Every option but the policy, borrowed, as a wire's
12/// [`map_options`](crate::completion::ReplayTarget::map_options) reads it.
13/// Not `#[non_exhaustive]`, so wires in companion crates destructure it
14/// whole, which `#[non_exhaustive]` forbids for
15/// [`GenerationOptions`](super::GenerationOptions).
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct OptionFields<'a> {
18 /// How much the model reasons.
19 pub reasoning: Option<&'a Reasoning>,
20 /// How long the prompt prefix stays cached.
21 pub cache: Option<&'a CacheRetention>,
22 /// The processing tier.
23 pub service_tier: Option<&'a ServiceTier>,
24 /// How long the answer should be.
25 pub verbosity: Option<&'a Verbosity>,
26 /// Whether several tool calls may come in one turn.
27 pub parallel_tool_calls: Option<bool>,
28 /// Nucleus sampling probability mass.
29 pub top_p: Option<f64>,
30 /// Sampling seed.
31 pub seed: Option<u64>,
32 /// Stop sequences; empty means unset.
33 pub stop: &'a [String],
34 /// The key the provider routes prompt-cache lookups by.
35 pub cache_key: Option<&'a str>,
36}
37
38impl OptionFields<'_> {
39 /// Whether `option` is set. The request's `temperature` is not one of
40 /// these fields, so it reads as unset.
41 pub(crate) fn is_set(&self, option: OptionName) -> bool {
42 match option {
43 OptionName::Reasoning => self.reasoning.is_some(),
44 OptionName::Cache => self.cache.is_some(),
45 OptionName::ServiceTier => self.service_tier.is_some(),
46 OptionName::Verbosity => self.verbosity.is_some(),
47 OptionName::ParallelToolCalls => self.parallel_tool_calls.is_some(),
48 OptionName::TopP => self.top_p.is_some(),
49 OptionName::Seed => self.seed.is_some(),
50 OptionName::Stop => !self.stop.is_empty(),
51 OptionName::CacheKey => self.cache_key.is_some(),
52 OptionName::Temperature => false,
53 }
54 }
55}
56
57/// A generation option, or the request's `temperature`, which the catalog
58/// also refuses.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub(crate) enum OptionName {
61 Reasoning,
62 Cache,
63 ServiceTier,
64 Verbosity,
65 ParallelToolCalls,
66 TopP,
67 Seed,
68 Stop,
69 CacheKey,
70 Temperature,
71}
72
73impl OptionName {
74 /// The field name errors and logs give the option.
75 pub(crate) const fn as_str(self) -> &'static str {
76 match self {
77 Self::Reasoning => "reasoning",
78 Self::Cache => "cache",
79 Self::ServiceTier => "service_tier",
80 Self::Verbosity => "verbosity",
81 Self::ParallelToolCalls => "parallel_tool_calls",
82 Self::TopP => "top_p",
83 Self::Seed => "seed",
84 Self::Stop => "stop",
85 Self::CacheKey => "cache_key",
86 Self::Temperature => "temperature",
87 }
88 }
89
90 /// Unset the option on `request`.
91 pub(crate) fn clear(self, request: &mut CompletionRequest) {
92 let options = &mut request.options;
93 match self {
94 Self::Reasoning => options.reasoning = None,
95 Self::Cache => options.cache = None,
96 Self::ServiceTier => options.service_tier = None,
97 Self::Verbosity => options.verbosity = None,
98 Self::ParallelToolCalls => options.parallel_tool_calls = None,
99 Self::TopP => options.top_p = None,
100 Self::Seed => options.seed = None,
101 Self::Stop => options.stop.clear(),
102 Self::CacheKey => options.cache_key = None,
103 Self::Temperature => request.temperature = None,
104 }
105 }
106}
107
108/// What a wire does with one option.
109#[non_exhaustive]
110#[derive(Clone, Debug, PartialEq)]
111pub enum Mapping {
112 /// The option is unset. For a set option this is an encode error.
113 Nothing,
114 /// Deep-merge this JSON object into the body, above the wire's own
115 /// encoding of the request and below `additional_params`.
116 Send(Value),
117 /// Honour the option by sending nothing: the provider default already
118 /// does what was asked. Logged at `debug` with the reason.
119 Omit(&'static str),
120 /// Honour the option through markers the wire's base builder writes
121 /// inside the body's arrays. Valid for `cache` only.
122 Place,
123 /// The wire or model cannot honour it: an error under
124 /// [`OnUnsupported::Error`](super::OnUnsupported::Error), a warning
125 /// under [`OnUnsupported::Ignore`](super::OnUnsupported::Ignore).
126 Unsupported(String),
127}
128
129impl Mapping {
130 /// `Nothing` for an unset option, otherwise what `map` answers for it.
131 pub fn of<T>(value: Option<T>, map: impl FnOnce(T) -> Mapping) -> Mapping {
132 value.map_or(Mapping::Nothing, map)
133 }
134
135 /// `Nothing` for an empty stop list, otherwise what `map` answers.
136 pub fn of_stop(stop: &[String], map: impl FnOnce(&[String]) -> Mapping) -> Mapping {
137 if stop.is_empty() {
138 Mapping::Nothing
139 } else {
140 map(stop)
141 }
142 }
143
144 /// The refusal for `reason`. Answer it only for a set option, through
145 /// [`Mapping::of`] or [`Mapping::of_stop`]: an unset option answers
146 /// [`Mapping::Nothing`].
147 pub fn unsupported(reason: impl Into<String>) -> Mapping {
148 Mapping::Unsupported(reason.into())
149 }
150
151 /// The answer for [`cache_key`](super::GenerationOptions::cache_key) of
152 /// a wire whose provider routes its prompt cache without a key:
153 /// `Nothing` when unset, else honoured by sending nothing, as the key
154 /// is only a routing hint.
155 pub fn unrouted(cache_key: Option<&str>) -> Mapping {
156 Mapping::of(cache_key, |_| {
157 Mapping::Omit("the provider routes its prompt cache without a key")
158 })
159 }
160}
161
162/// One [`Mapping`] per option, by field name. Not `#[non_exhaustive]` and no
163/// `Default`, so a wire writes every field.
164#[derive(Clone, Debug, PartialEq)]
165pub struct OptionMap {
166 /// [`GenerationOptions::reasoning`](super::GenerationOptions::reasoning).
167 pub reasoning: Mapping,
168 /// [`GenerationOptions::cache`](super::GenerationOptions::cache).
169 pub cache: Mapping,
170 /// [`GenerationOptions::service_tier`](super::GenerationOptions::service_tier).
171 pub service_tier: Mapping,
172 /// [`GenerationOptions::verbosity`](super::GenerationOptions::verbosity).
173 pub verbosity: Mapping,
174 /// [`GenerationOptions::parallel_tool_calls`](super::GenerationOptions::parallel_tool_calls).
175 pub parallel_tool_calls: Mapping,
176 /// [`GenerationOptions::top_p`](super::GenerationOptions::top_p).
177 pub top_p: Mapping,
178 /// [`GenerationOptions::seed`](super::GenerationOptions::seed).
179 pub seed: Mapping,
180 /// [`GenerationOptions::stop`](super::GenerationOptions::stop).
181 pub stop: Mapping,
182 /// [`GenerationOptions::cache_key`](super::GenerationOptions::cache_key).
183 pub cache_key: Mapping,
184}
185
186impl OptionMap {
187 /// Every option's name and answer, in field order.
188 pub(super) fn into_slots(self) -> [(OptionName, Mapping); 9] {
189 let OptionMap {
190 reasoning,
191 cache,
192 service_tier,
193 verbosity,
194 parallel_tool_calls,
195 top_p,
196 seed,
197 stop,
198 cache_key,
199 } = self;
200 [
201 (OptionName::Reasoning, reasoning),
202 (OptionName::Cache, cache),
203 (OptionName::ServiceTier, service_tier),
204 (OptionName::Verbosity, verbosity),
205 (OptionName::ParallelToolCalls, parallel_tool_calls),
206 (OptionName::TopP, top_p),
207 (OptionName::Seed, seed),
208 (OptionName::Stop, stop),
209 (OptionName::CacheKey, cache_key),
210 ]
211 }
212}