Skip to main content

ferrin_core/prompt/
call_settings.rs

1//! Sampling settings shared by all text generation entry points.
2
3use ferrin_spec::CallOptions;
4use ferrin_spec::Headers;
5use ferrin_spec::ProviderOptions;
6use ferrin_spec::ReasoningEffort;
7
8use crate::error::Error;
9use crate::middleware::builtin::merge_provider_options;
10
11/// Sampling and request settings applied to every model call.
12///
13/// Rust types absorb most validation (`u32`, `f64`, `Vec<String>`); the
14/// remaining runtime checks are `max_output_tokens >= 1` and finite floats.
15#[derive(Debug, Clone, Default, PartialEq)]
16pub struct CallSettings {
17    /// Maximum number of output tokens.
18    pub max_output_tokens: Option<u32>,
19    /// Sampling temperature.
20    pub temperature: Option<f64>,
21    /// Nucleus sampling.
22    pub top_p: Option<f64>,
23    /// Top-k sampling.
24    pub top_k: Option<u32>,
25    /// Presence penalty.
26    pub presence_penalty: Option<f64>,
27    /// Frequency penalty.
28    pub frequency_penalty: Option<f64>,
29    /// Stop sequences.
30    pub stop_sequences: Option<Vec<String>>,
31    /// Random seed.
32    pub seed: Option<u64>,
33    /// Reasoning effort.
34    pub reasoning: ReasoningEffort,
35    /// Extra request headers.
36    pub headers: Headers,
37    /// Provider-specific options.
38    pub provider_options: ProviderOptions,
39}
40
41impl CallSettings {
42    /// Checks the runtime invariants.
43    ///
44    /// # Errors
45    ///
46    /// Returns [`Error::InvalidArgument`] naming the offending setting.
47    pub fn validate(&self) -> Result<(), Error> {
48        if self.max_output_tokens == Some(0) {
49            return Err(Error::invalid_argument(
50                "max_output_tokens",
51                "must be at least 1",
52            ));
53        }
54        for (name, value) in [
55            ("temperature", self.temperature),
56            ("top_p", self.top_p),
57            ("presence_penalty", self.presence_penalty),
58            ("frequency_penalty", self.frequency_penalty),
59        ] {
60            if let Some(value) = value
61                && !value.is_finite()
62            {
63                return Err(Error::invalid_argument(name, "must be a finite number"));
64            }
65        }
66        Ok(())
67    }
68
69    /// Copies the settings into `options`, merging headers and provider
70    /// options.
71    pub fn apply(&self, options: &mut CallOptions) {
72        options.max_output_tokens = self.max_output_tokens;
73        options.temperature = self.temperature;
74        options.top_p = self.top_p;
75        options.top_k = self.top_k;
76        options.presence_penalty = self.presence_penalty;
77        options.frequency_penalty = self.frequency_penalty;
78        options.stop_sequences = self.stop_sequences.clone();
79        options.seed = self.seed;
80        options.reasoning = self.reasoning;
81        options.headers.merge(&self.headers);
82        options.provider_options =
83            merge_provider_options(&options.provider_options, self.provider_options.clone());
84    }
85
86    /// Overlays the set fields of `other` onto `self` (used by
87    /// `prepare_step` and default-settings middleware).
88    pub fn merge(&mut self, other: &CallSettings) {
89        if other.max_output_tokens.is_some() {
90            self.max_output_tokens = other.max_output_tokens;
91        }
92        if other.temperature.is_some() {
93            self.temperature = other.temperature;
94        }
95        if other.top_p.is_some() {
96            self.top_p = other.top_p;
97        }
98        if other.top_k.is_some() {
99            self.top_k = other.top_k;
100        }
101        if other.presence_penalty.is_some() {
102            self.presence_penalty = other.presence_penalty;
103        }
104        if other.frequency_penalty.is_some() {
105            self.frequency_penalty = other.frequency_penalty;
106        }
107        if other.stop_sequences.is_some() {
108            self.stop_sequences.clone_from(&other.stop_sequences);
109        }
110        if other.seed.is_some() {
111            self.seed = other.seed;
112        }
113        if other.reasoning != ReasoningEffort::default() {
114            self.reasoning = other.reasoning;
115        }
116        self.headers.merge(&other.headers);
117        self.provider_options =
118            merge_provider_options(&self.provider_options, other.provider_options.clone());
119    }
120}