Skip to main content

ferrin_spec/language_model/
call_options.rs

1//! Call options passed to language model adapters.
2
3use serde::Deserialize;
4use serde::Serialize;
5use tokio_util::sync::CancellationToken;
6
7use super::prompt::Prompt;
8use super::tool::ToolDefinition;
9use crate::json::JsonValue;
10use crate::shared::Headers;
11use crate::shared::ProviderOptions;
12use crate::shared::ToolName;
13
14/// Options for a single `do_generate` or `do_stream` call.
15///
16/// The core validates and normalizes application settings before building
17/// this struct; adapters translate it into a provider request. Options the
18/// provider does not support must produce a [`Warning`](crate::Warning) and be
19/// ignored rather than cause an error.
20#[derive(Debug, Clone, Default)]
21pub struct CallOptions {
22    /// The prompt in specification form.
23    pub prompt: Prompt,
24    /// Maximum number of output tokens.
25    pub max_output_tokens: Option<u32>,
26    /// Sampling temperature.
27    pub temperature: Option<f64>,
28    /// Nucleus sampling probability mass.
29    pub top_p: Option<f64>,
30    /// Top-k sampling.
31    pub top_k: Option<u32>,
32    /// Presence penalty.
33    pub presence_penalty: Option<f64>,
34    /// Frequency penalty.
35    pub frequency_penalty: Option<f64>,
36    /// Stop sequences.
37    pub stop_sequences: Option<Vec<String>>,
38    /// Random seed for deterministic sampling.
39    pub seed: Option<u64>,
40    /// Output format (text or JSON with an optional schema).
41    pub response_format: Option<ResponseFormat>,
42    /// Tools available to the model.
43    pub tools: Vec<ToolDefinition>,
44    /// Tool choice constraint.
45    pub tool_choice: Option<ToolChoice>,
46    /// Whether the stream should include `Raw` parts with provider chunks.
47    pub include_raw_chunks: bool,
48    /// Requested reasoning effort.
49    pub reasoning: ReasoningEffort,
50    /// Additional request headers.
51    pub headers: Headers,
52    /// Provider-specific options keyed by provider name.
53    pub provider_options: ProviderOptions,
54    /// Cancellation token; when triggered the adapter aborts the request.
55    pub cancellation: CancellationToken,
56}
57
58impl CallOptions {
59    /// Creates call options for `prompt` with every other field at its default.
60    #[must_use]
61    pub fn new(prompt: Prompt) -> Self {
62        Self {
63            prompt,
64            ..Self::default()
65        }
66    }
67
68    /// Returns a serializable copy without the cancellation token.
69    ///
70    /// Used for fixtures, snapshots and telemetry. Sensitive header values
71    /// are masked when the record is serialized.
72    #[must_use]
73    pub fn to_recordable(&self) -> CallOptionsRecord {
74        CallOptionsRecord {
75            prompt: self.prompt.clone(),
76            max_output_tokens: self.max_output_tokens,
77            temperature: self.temperature,
78            top_p: self.top_p,
79            top_k: self.top_k,
80            presence_penalty: self.presence_penalty,
81            frequency_penalty: self.frequency_penalty,
82            stop_sequences: self.stop_sequences.clone(),
83            seed: self.seed,
84            response_format: self.response_format.clone(),
85            tools: self.tools.clone(),
86            tool_choice: self.tool_choice.clone(),
87            include_raw_chunks: self.include_raw_chunks,
88            reasoning: self.reasoning,
89            headers: self.headers.clone(),
90            provider_options: self.provider_options.clone(),
91        }
92    }
93
94    /// Returns the provider options stored under `provider_key`, if any.
95    #[must_use]
96    pub fn provider_options_for(&self, provider_key: &str) -> Option<&crate::json::JsonObject> {
97        self.provider_options.get(provider_key)
98    }
99}
100
101/// Serializable projection of [`CallOptions`] (everything but cancellation).
102#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
103pub struct CallOptionsRecord {
104    /// See [`CallOptions::prompt`].
105    pub prompt: Prompt,
106    /// See [`CallOptions::max_output_tokens`].
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub max_output_tokens: Option<u32>,
109    /// See [`CallOptions::temperature`].
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub temperature: Option<f64>,
112    /// See [`CallOptions::top_p`].
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub top_p: Option<f64>,
115    /// See [`CallOptions::top_k`].
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub top_k: Option<u32>,
118    /// See [`CallOptions::presence_penalty`].
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub presence_penalty: Option<f64>,
121    /// See [`CallOptions::frequency_penalty`].
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub frequency_penalty: Option<f64>,
124    /// See [`CallOptions::stop_sequences`].
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub stop_sequences: Option<Vec<String>>,
127    /// See [`CallOptions::seed`].
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub seed: Option<u64>,
130    /// See [`CallOptions::response_format`].
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub response_format: Option<ResponseFormat>,
133    /// See [`CallOptions::tools`].
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    pub tools: Vec<ToolDefinition>,
136    /// See [`CallOptions::tool_choice`].
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub tool_choice: Option<ToolChoice>,
139    /// See [`CallOptions::include_raw_chunks`].
140    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
141    pub include_raw_chunks: bool,
142    /// See [`CallOptions::reasoning`].
143    #[serde(default)]
144    pub reasoning: ReasoningEffort,
145    /// See [`CallOptions::headers`]; serialized with sensitive values masked.
146    #[serde(default, skip_serializing_if = "Headers::is_empty")]
147    pub headers: Headers,
148    /// See [`CallOptions::provider_options`].
149    #[serde(default, skip_serializing_if = "ProviderOptions::is_empty")]
150    pub provider_options: ProviderOptions,
151}
152
153impl From<CallOptionsRecord> for CallOptions {
154    fn from(record: CallOptionsRecord) -> Self {
155        Self {
156            prompt: record.prompt,
157            max_output_tokens: record.max_output_tokens,
158            temperature: record.temperature,
159            top_p: record.top_p,
160            top_k: record.top_k,
161            presence_penalty: record.presence_penalty,
162            frequency_penalty: record.frequency_penalty,
163            stop_sequences: record.stop_sequences,
164            seed: record.seed,
165            response_format: record.response_format,
166            tools: record.tools,
167            tool_choice: record.tool_choice,
168            include_raw_chunks: record.include_raw_chunks,
169            reasoning: record.reasoning,
170            headers: record.headers,
171            provider_options: record.provider_options,
172            cancellation: CancellationToken::new(),
173        }
174    }
175}
176
177/// Requested output format.
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(tag = "type", rename_all = "lowercase")]
180pub enum ResponseFormat {
181    /// Free-form text.
182    Text,
183    /// JSON output, optionally constrained by a JSON Schema.
184    Json {
185        /// JSON Schema (draft-07) the output must satisfy.
186        #[serde(default, skip_serializing_if = "Option::is_none")]
187        schema: Option<JsonValue>,
188        /// Schema name passed to providers that require one.
189        #[serde(default, skip_serializing_if = "Option::is_none")]
190        name: Option<String>,
191        /// Schema description passed to providers that accept one.
192        #[serde(default, skip_serializing_if = "Option::is_none")]
193        description: Option<String>,
194    },
195}
196
197impl ResponseFormat {
198    /// JSON output constrained by `schema`, without name or description.
199    #[must_use]
200    pub fn json(schema: JsonValue) -> Self {
201        Self::Json {
202            schema: Some(schema),
203            name: None,
204            description: None,
205        }
206    }
207
208    /// JSON output without a schema.
209    #[must_use]
210    pub fn json_unconstrained() -> Self {
211        Self::Json {
212            schema: None,
213            name: None,
214            description: None,
215        }
216    }
217}
218
219/// Constraint on which tool the model may call.
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(tag = "type", rename_all = "lowercase")]
222pub enum ToolChoice {
223    /// The model decides whether to call a tool.
224    Auto,
225    /// The model must not call a tool.
226    None,
227    /// The model must call one of the available tools.
228    Required,
229    /// The model must call the named tool.
230    Tool {
231        /// Name of the required tool.
232        tool_name: ToolName,
233    },
234}
235
236impl ToolChoice {
237    /// Requires the model to call the tool named `name`.
238    #[must_use]
239    pub fn tool(name: impl Into<ToolName>) -> Self {
240        Self::Tool {
241            tool_name: name.into(),
242        }
243    }
244}
245
246/// Requested reasoning effort.
247#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
248#[serde(rename_all = "kebab-case")]
249pub enum ReasoningEffort {
250    /// Let the provider apply its default.
251    #[default]
252    ProviderDefault,
253    /// Disable reasoning where the provider allows it.
254    None,
255    /// Minimal effort.
256    Minimal,
257    /// Low effort.
258    Low,
259    /// Medium effort.
260    Medium,
261    /// High effort.
262    High,
263    /// Extra-high effort.
264    #[serde(rename = "xhigh")]
265    XHigh,
266}
267
268impl ReasoningEffort {
269    /// Returns the wire representation (`provider-default`, `none`, ..., `xhigh`).
270    #[must_use]
271    pub fn as_str(self) -> &'static str {
272        match self {
273            Self::ProviderDefault => "provider-default",
274            Self::None => "none",
275            Self::Minimal => "minimal",
276            Self::Low => "low",
277            Self::Medium => "medium",
278            Self::High => "high",
279            Self::XHigh => "xhigh",
280        }
281    }
282}
283
284impl std::fmt::Display for ReasoningEffort {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        f.write_str(self.as_str())
287    }
288}