gglib_core/domain/sampling_provenance.rs
1//! Which layer supplied each resolved sampling parameter.
2//!
3//! ## Why this exists
4//!
5//! [`InferenceConfig::resolve_layers`](crate::domain::InferenceConfig::resolve_layers)
6//! folds an ordered ladder of sampling layers into one config under two rules
7//! that are individually defensible and jointly opaque: the coupled trio
8//! (`presence_penalty`, `repeat_penalty`, `min_p`) travels with the
9//! `temperature` it was tuned against, and a model's stored defaults rank
10//! above or below global settings depending on whether a person set them.
11//!
12//! The resolved numbers alone cannot distinguish a value someone chose from
13//! one that fell out of a floor. `0.0` is a number; "`0.0`, from the floor,
14//! because the profile claimed the temperature" is an explanation, and only
15//! the second makes the behaviour auditable.
16//!
17//! ## One computation, not two
18//!
19//! [`FieldSources`] is produced by
20//! [`resolve_layers_with_sources`](crate::domain::InferenceConfig::resolve_layers_with_sources),
21//! the same pass that decides the values — never by a second function that
22//! re-derives the rules. That is deliberate: this provenance previously lived
23//! in a separate `describe_provenance` helper in the request pipeline, and the
24//! two implementations had already drifted. A ladder where `cli` supplied a
25//! `presence_penalty` and a lower layer claimed the `temperature` resolved the
26//! penalty from the claiming layer while the log named `cli`.
27//!
28//! The same `(value, source)` shape
29//! [`resolve_context_size_with_source`](crate::server_config::resolve_context_size_with_source)
30//! uses, and for the same reason.
31
32use serde::{Deserialize, Serialize};
33
34/// Which rung of a sampling ladder supplied one resolved parameter.
35///
36/// [`Layer`](Self::Layer) carries an index into the ladder that was resolved,
37/// rather than a name, because the ladders differ: the request pipeline adds
38/// `cli` and `client` rungs above the five that
39/// [`SamplingLayer`] describes. Callers map the index back to whatever names
40/// their own ladder used.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum ParamSource {
43 /// The layer at this index in the resolved ladder named the value.
44 Layer(usize),
45 /// The class floor, because no layer named a value at all.
46 Floor,
47 /// The class floor, because a layer claimed `temperature` and this
48 /// parameter is tuned against it, so no layer beneath was eligible to
49 /// supply one. Distinct from [`Floor`](Self::Floor): here a lower layer
50 /// may well have named a value and was deliberately passed over.
51 FloorCoupled,
52 /// Nothing named it and the floor carries none either — `max_tokens` is
53 /// the only parameter with no floor value, deliberately. See
54 /// [`InferenceConfig::with_hardcoded_defaults`](crate::domain::InferenceConfig::with_hardcoded_defaults).
55 Unset,
56}
57
58impl ParamSource {
59 /// Whether the value came from the floor rather than from any layer.
60 #[must_use]
61 pub const fn is_floor(self) -> bool {
62 matches!(self, Self::Floor | Self::FloorCoupled)
63 }
64}
65
66/// The five rungs of the ladder
67/// [`resolve_with_profile`](crate::domain::InferenceConfig::resolve_with_profile)
68/// builds, in priority order.
69///
70/// Only one of [`ModelUserSet`](Self::ModelUserSet) and
71/// [`ModelAutoDetected`](Self::ModelAutoDetected) is ever populated for a
72/// given model — both name `Model.inference_defaults`, and
73/// [`DefaultsOrigin`](crate::domain::DefaultsOrigin) decides which rung it
74/// occupies.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum SamplingLayer {
77 /// Caller-supplied overrides — request parameters or CLI flags.
78 Request,
79 /// The named profile the caller selected.
80 Profile,
81 /// Per-model defaults a person tuned deliberately.
82 ModelUserSet,
83 /// Global settings defaults.
84 Global,
85 /// Per-model defaults written automatically from the model's `reasoning`
86 /// tag and never reviewed, so they rank below global settings.
87 ModelAutoDetected,
88}
89
90impl SamplingLayer {
91 /// The rung at `index` in the ladder `resolve_with_profile` builds.
92 ///
93 /// The mapping lives here rather than at each call site so a change to the
94 /// ladder's order cannot silently mislabel a `ParamSource::Layer`.
95 #[must_use]
96 pub const fn from_index(index: usize) -> Option<Self> {
97 match index {
98 0 => Some(Self::Request),
99 1 => Some(Self::Profile),
100 2 => Some(Self::ModelUserSet),
101 3 => Some(Self::Global),
102 4 => Some(Self::ModelAutoDetected),
103 _ => None,
104 }
105 }
106
107 /// Short human-readable label, e.g. `per-model defaults (user-set)`.
108 #[must_use]
109 pub const fn label(self) -> &'static str {
110 match self {
111 Self::Request => "request parameters",
112 Self::Profile => "profile",
113 Self::ModelUserSet => "per-model defaults (user-set)",
114 Self::Global => "global settings",
115 Self::ModelAutoDetected => "per-model defaults (auto-detected)",
116 }
117 }
118}
119
120/// Per-field provenance for one resolved [`InferenceConfig`].
121///
122/// [`InferenceConfig`]: crate::domain::InferenceConfig
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub struct FieldSources {
125 /// Where the resolved `temperature` came from.
126 pub temperature: ParamSource,
127 /// Where the resolved `top_p` came from.
128 pub top_p: ParamSource,
129 /// Where the resolved `top_k` came from.
130 pub top_k: ParamSource,
131 /// Where the resolved `presence_penalty` came from.
132 pub presence_penalty: ParamSource,
133 /// Where the resolved `repeat_penalty` came from.
134 pub repeat_penalty: ParamSource,
135 /// Where the resolved `min_p` came from.
136 pub min_p: ParamSource,
137 /// Where the resolved `max_tokens` came from.
138 pub max_tokens: ParamSource,
139}
140
141impl FieldSources {
142 /// `(field_name, source)` pairs in display order.
143 ///
144 /// The single iteration order every consumer renders, so the CLI's table
145 /// and the pipeline's debug line cannot disagree about which parameter is
146 /// which. The coupled trio is kept adjacent because it is only
147 /// interpretable as a group.
148 pub fn iter(&self) -> impl Iterator<Item = (&'static str, ParamSource)> {
149 [
150 ("temperature", self.temperature),
151 ("top_p", self.top_p),
152 ("top_k", self.top_k),
153 ("presence_penalty", self.presence_penalty),
154 ("repeat_penalty", self.repeat_penalty),
155 ("min_p", self.min_p),
156 ("max_tokens", self.max_tokens),
157 ]
158 .into_iter()
159 }
160
161 /// Render as `field=layer` pairs against the ladder's own layer names.
162 ///
163 /// `names` is indexed by [`ParamSource::Layer`]; an index past its end
164 /// renders as `?`, which can only happen if a caller passes names for a
165 /// different ladder than it resolved.
166 #[must_use]
167 pub fn describe(&self, names: &[&str]) -> String {
168 self.iter()
169 .map(|(field, source)| {
170 let label = match source {
171 ParamSource::Layer(i) => names.get(i).copied().unwrap_or("?"),
172 ParamSource::Floor | ParamSource::FloorCoupled => "floor",
173 ParamSource::Unset => "unset",
174 };
175 format!("{field}={label}")
176 })
177 .collect::<Vec<_>>()
178 .join(" ")
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn iter_yields_every_field_once_in_display_order() {
188 let sources = FieldSources {
189 temperature: ParamSource::Layer(0),
190 top_p: ParamSource::Layer(0),
191 top_k: ParamSource::Floor,
192 presence_penalty: ParamSource::FloorCoupled,
193 repeat_penalty: ParamSource::FloorCoupled,
194 min_p: ParamSource::FloorCoupled,
195 max_tokens: ParamSource::Unset,
196 };
197 let fields: Vec<&str> = sources.iter().map(|(name, _)| name).collect();
198 assert_eq!(
199 fields,
200 [
201 "temperature",
202 "top_p",
203 "top_k",
204 "presence_penalty",
205 "repeat_penalty",
206 "min_p",
207 "max_tokens",
208 ]
209 );
210 }
211
212 /// Both floor variants render as `floor` in the terse log form — the
213 /// distinction exists for the `explain` command, which has room for it.
214 #[test]
215 fn describe_names_layers_and_collapses_the_floor_variants() {
216 let sources = FieldSources {
217 temperature: ParamSource::Layer(1),
218 top_p: ParamSource::Layer(0),
219 top_k: ParamSource::Floor,
220 presence_penalty: ParamSource::FloorCoupled,
221 repeat_penalty: ParamSource::Layer(2),
222 min_p: ParamSource::Floor,
223 max_tokens: ParamSource::Unset,
224 };
225 let got = sources.describe(&["cli", "profile", "model"]);
226 assert!(got.contains("temperature=profile"), "{got}");
227 assert!(got.contains("top_p=cli"), "{got}");
228 assert!(got.contains("top_k=floor"), "{got}");
229 assert!(got.contains("presence_penalty=floor"), "{got}");
230 assert!(got.contains("repeat_penalty=model"), "{got}");
231 assert!(got.contains("max_tokens=unset"), "{got}");
232 }
233
234 /// A names array that does not cover the ladder is a caller bug; render it
235 /// visibly rather than panicking inside a log line.
236 #[test]
237 fn describe_marks_an_index_the_names_do_not_cover() {
238 let sources = FieldSources {
239 temperature: ParamSource::Layer(9),
240 top_p: ParamSource::Floor,
241 top_k: ParamSource::Floor,
242 presence_penalty: ParamSource::Floor,
243 repeat_penalty: ParamSource::Floor,
244 min_p: ParamSource::Floor,
245 max_tokens: ParamSource::Unset,
246 };
247 assert!(sources.describe(&["cli"]).contains("temperature=?"));
248 }
249
250 #[test]
251 fn layer_indices_match_the_resolve_with_profile_ladder() {
252 assert_eq!(SamplingLayer::from_index(0), Some(SamplingLayer::Request));
253 assert_eq!(SamplingLayer::from_index(1), Some(SamplingLayer::Profile));
254 assert_eq!(
255 SamplingLayer::from_index(2),
256 Some(SamplingLayer::ModelUserSet)
257 );
258 assert_eq!(SamplingLayer::from_index(3), Some(SamplingLayer::Global));
259 assert_eq!(
260 SamplingLayer::from_index(4),
261 Some(SamplingLayer::ModelAutoDetected)
262 );
263 assert_eq!(SamplingLayer::from_index(5), None);
264 }
265
266 #[test]
267 fn only_the_floor_variants_report_as_floor() {
268 assert!(ParamSource::Floor.is_floor());
269 assert!(ParamSource::FloorCoupled.is_floor());
270 assert!(!ParamSource::Layer(0).is_floor());
271 assert!(!ParamSource::Unset.is_floor());
272 }
273}