Skip to main content

ferrin_google/
options.rs

1//! Provider options read from `provider_options["google"]` (and the
2//! configured provider name when it differs).
3
4use std::collections::BTreeMap;
5
6use ferrin_provider_util::provider_options::parse_provider_options;
7use ferrin_spec::JsonObject;
8use ferrin_spec::JsonValue;
9use ferrin_spec::ProviderOptions;
10use ferrin_spec::error::InvalidArgumentError;
11use serde::Deserialize;
12use serde::Serialize;
13
14use crate::config::CANONICAL_OPTIONS_KEY;
15use crate::config::GoogleConfig;
16
17/// Thinking configuration (`thinkingConfig`).
18#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct ThinkingConfig {
21    /// Thinking budget in tokens (Gemini 2.5).
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub thinking_budget: Option<i64>,
24    /// Whether thought summaries are returned.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub include_thoughts: Option<bool>,
27    /// Thinking level (`minimal`, `low`, `medium`, `high`; Gemini 3).
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub thinking_level: Option<String>,
30}
31
32/// A safety setting (`{category, threshold}`).
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct SafetySetting {
35    /// Harm category (`HARM_CATEGORY_HATE_SPEECH`, ...).
36    pub category: String,
37    /// Block threshold (`BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_NONE`, `OFF`, ...).
38    pub threshold: String,
39}
40
41/// Image generation configuration (`imageConfig`).
42#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ImageConfig {
45    /// Aspect ratio (`1:1`, `16:9`, ...).
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub aspect_ratio: Option<String>,
48    /// Image size (`1K`, `2K`, `4K`, `512`).
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub image_size: Option<String>,
51    /// Person generation policy (Vertex AI only).
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub person_generation: Option<String>,
54    /// Prominent people policy (Vertex AI only).
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub prominent_people: Option<String>,
57    /// Output options (Vertex AI only).
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub image_output_options: Option<JsonObject>,
60}
61
62/// Language model options (`provider_options["google"]`).
63#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct GoogleLanguageModelOptions {
66    /// Response modalities (`TEXT`, `IMAGE`).
67    #[serde(default)]
68    pub response_modalities: Option<Vec<String>>,
69    /// Thinking configuration.
70    #[serde(default)]
71    pub thinking_config: Option<ThinkingConfig>,
72    /// Name of a cached content resource.
73    #[serde(default)]
74    pub cached_content: Option<String>,
75    /// Whether JSON response formats send `responseSchema` (default `true`).
76    #[serde(default)]
77    pub structured_outputs: Option<bool>,
78    /// Explicit safety settings.
79    #[serde(default)]
80    pub safety_settings: Option<Vec<SafetySetting>>,
81    /// Threshold applied to the four configurable harm categories.
82    #[serde(default)]
83    pub threshold: Option<String>,
84    /// Whether audio timestamps are understood.
85    #[serde(default)]
86    pub audio_timestamp: Option<bool>,
87    /// Request labels.
88    #[serde(default)]
89    pub labels: Option<BTreeMap<String, String>>,
90    /// Media resolution (`MEDIA_RESOLUTION_LOW`, ...).
91    #[serde(default)]
92    pub media_resolution: Option<String>,
93    /// Image generation configuration.
94    #[serde(default)]
95    pub image_config: Option<ImageConfig>,
96    /// Retrieval configuration (`{latLng: {latitude, longitude}}`) added to
97    /// `toolConfig`.
98    #[serde(default)]
99    pub retrieval_config: Option<JsonObject>,
100    /// Vertex AI only: stream function call arguments.
101    #[serde(default)]
102    pub stream_function_call_arguments: Option<bool>,
103    /// Service tier (`standard`, `flex`, `priority`).
104    #[serde(default)]
105    pub service_tier: Option<String>,
106    /// Vertex AI only: shared request type header.
107    #[serde(default)]
108    pub shared_request_type: Option<String>,
109    /// Vertex AI only: request type header.
110    #[serde(default)]
111    pub request_type: Option<String>,
112}
113
114impl GoogleLanguageModelOptions {
115    /// Overlays `other` on `self`: every option set in `other` wins.
116    fn merge(mut self, other: Self) -> Self {
117        macro_rules! take {
118            ($($field:ident),* $(,)?) => {
119                $( if other.$field.is_some() { self.$field = other.$field; } )*
120            };
121        }
122        take!(
123            response_modalities,
124            thinking_config,
125            cached_content,
126            structured_outputs,
127            safety_settings,
128            threshold,
129            audio_timestamp,
130            labels,
131            media_resolution,
132            image_config,
133            retrieval_config,
134            stream_function_call_arguments,
135            service_tier,
136            shared_request_type,
137            request_type,
138        );
139        self
140    }
141}
142
143/// Parses `T` under the canonical `google` key and, when the configured name
144/// differs, under that name as well; `merge` combines the two (the custom
145/// key wins).
146///
147/// # Errors
148///
149/// Returns [`InvalidArgumentError`] when either object does not match the
150/// option schema.
151pub fn parse_merged<T: serde::de::DeserializeOwned + Default>(
152    config: &GoogleConfig,
153    provider_options: &ProviderOptions,
154    merge: impl FnOnce(T, T) -> T,
155) -> Result<T, InvalidArgumentError> {
156    let canonical =
157        parse_provider_options::<T>(CANONICAL_OPTIONS_KEY, provider_options)?.unwrap_or_default();
158    let key = config.options_key();
159    if key == CANONICAL_OPTIONS_KEY {
160        return Ok(canonical);
161    }
162    match parse_provider_options::<T>(key, provider_options)? {
163        Some(custom) => Ok(merge(canonical, custom)),
164        None => Ok(canonical),
165    }
166}
167
168/// Parses the language model options (canonical key, then configured name).
169///
170/// # Errors
171///
172/// Returns [`InvalidArgumentError`] when either object does not match the
173/// option schema.
174pub fn parse_options(
175    config: &GoogleConfig,
176    provider_options: &ProviderOptions,
177) -> Result<GoogleLanguageModelOptions, InvalidArgumentError> {
178    parse_merged(config, provider_options, GoogleLanguageModelOptions::merge)
179}
180
181/// Reads the raw option object of a part or message: the configured name
182/// first, then the canonical key.
183#[must_use]
184pub fn part_options<'a>(
185    config: &GoogleConfig,
186    provider_options: Option<&'a ProviderOptions>,
187) -> Option<&'a JsonObject> {
188    let options = provider_options?;
189    options
190        .get(config.options_key())
191        .or_else(|| options.get(CANONICAL_OPTIONS_KEY))
192}
193
194/// Part-level options and metadata (`thoughtSignature`, `thought`,
195/// `serverToolCallId`, `serverToolType`).
196#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct PartOptions {
199    /// Thought signature returned with a previous response part.
200    #[serde(default)]
201    pub thought_signature: Option<String>,
202    /// Marks an assistant file as a thought (reasoning file).
203    #[serde(default)]
204    pub thought: Option<bool>,
205    /// Id of a server-side tool call.
206    #[serde(default)]
207    pub server_tool_call_id: Option<String>,
208    /// Type of a server-side tool call.
209    #[serde(default)]
210    pub server_tool_type: Option<String>,
211}
212
213/// Reads the part options of a prompt part.
214#[must_use]
215pub fn read_part_options(
216    config: &GoogleConfig,
217    provider_options: Option<&ProviderOptions>,
218) -> PartOptions {
219    read_options(part_options(config, provider_options)).unwrap_or_default()
220}
221
222/// Deserializes `object` into `T`, ignoring unknown keys.
223#[must_use]
224pub fn read_options<T: serde::de::DeserializeOwned>(object: Option<&JsonObject>) -> Option<T> {
225    let object = object?;
226    serde_json::from_value(JsonValue::Object(object.clone())).ok()
227}