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