Skip to main content

starweaver_model/
settings.rs

1//! Provider-neutral generation settings.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7
8const OPENAI_PROMPT_CACHE_KEY_LIMIT: usize = 64;
9
10/// Format a stable affinity identifier for `OpenAI` prompt-cache routing.
11#[must_use]
12pub fn format_openai_prompt_cache_key(affinity_id: &str) -> Option<String> {
13    let mut key = String::from("sw_");
14    for ch in affinity_id.trim().chars() {
15        if key.len() >= OPENAI_PROMPT_CACHE_KEY_LIMIT {
16            break;
17        }
18        key.push(
19            if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
20                ch
21            } else {
22                '_'
23            },
24        );
25    }
26    (key.len() > "sw_".len()).then_some(key)
27}
28
29fn normalized_openai_model_name(model_name: &str) -> String {
30    let model_name = model_name.trim().to_ascii_lowercase();
31    let after_namespace = model_name
32        .rsplit_once(':')
33        .map_or(model_name.as_str(), |(_, model)| model);
34    after_namespace
35        .rsplit_once('/')
36        .map_or(after_namespace, |(_, model)| model)
37        .to_string()
38}
39
40/// Return whether Starweaver may auto-derive `OpenAI` prompt-cache keys for a model name.
41#[must_use]
42pub fn supports_automatic_openai_prompt_cache_key(model_name: &str) -> bool {
43    let model = normalized_openai_model_name(model_name);
44    model.starts_with("gpt-")
45        || model.starts_with("chatgpt-")
46        || model.starts_with("o1")
47        || model.starts_with("o3")
48        || model.starts_with("o4")
49}
50
51/// Return whether a model supports `OpenAI`'s GPT-5.6 explicit cache-breakpoint protocol.
52#[must_use]
53pub fn supports_openai_prompt_cache_breakpoints(model_name: &str) -> bool {
54    let model_name = normalized_openai_model_name(model_name);
55    model_name.starts_with("gpt-5.6") || model_name.starts_with("gpt-5-6")
56}
57
58/// Per-request generation configuration.
59#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
60pub struct ModelSettings {
61    /// Maximum generated tokens.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub max_tokens: Option<u32>,
64    /// Sampling temperature.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub temperature: Option<f64>,
67    /// Nucleus sampling.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub top_p: Option<f64>,
70    /// Top-k sampling.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub top_k: Option<u32>,
73    /// Request timeout in milliseconds.
74    ///
75    /// This is the Rust transport-friendly representation of a cross-provider
76    /// `timeout` model setting.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub timeout_ms: Option<u64>,
79    /// Allow multiple tool calls in one response.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub parallel_tool_calls: Option<bool>,
82    /// Tool forcing or availability policy.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub tool_choice: Option<ToolChoice>,
85    /// Best-effort deterministic seed.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub seed: Option<u64>,
88    /// Stop strings.
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub stop_sequences: Vec<String>,
91    /// Presence penalty.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub presence_penalty: Option<f64>,
94    /// Frequency penalty.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub frequency_penalty: Option<f64>,
97    /// Token-level logit bias.
98    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
99    pub logit_bias: BTreeMap<String, i32>,
100    /// Reasoning or thinking controls.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub thinking: Option<ThinkingSettings>,
103    /// Latency/cost tier.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub service_tier: Option<ServiceTier>,
106    /// Provider replay and server-side state controls.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub provider_replay: Option<ProviderReplaySettings>,
109    /// Typed provider-specific settings.
110    #[serde(default, skip_serializing_if = "ProviderSettings::is_empty")]
111    pub provider_settings: ProviderSettings,
112    /// Provider-specific raw settings escape hatch.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub provider_options: Option<Value>,
115    /// Request headers merged after adapter defaults and before request-level overrides.
116    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
117    pub extra_headers: BTreeMap<String, String>,
118    /// Extra JSON object merged into the top-level request body.
119    #[serde(default, skip_serializing_if = "Map::is_empty")]
120    pub extra_body: Map<String, Value>,
121}
122
123impl ModelSettings {
124    /// Merge two settings values field by field, taking values from `overlay` when present.
125    #[must_use]
126    pub fn merge(&self, overlay: &Self) -> Self {
127        Self {
128            max_tokens: overlay.max_tokens.or(self.max_tokens),
129            temperature: overlay.temperature.or(self.temperature),
130            top_p: overlay.top_p.or(self.top_p),
131            top_k: overlay.top_k.or(self.top_k),
132            timeout_ms: overlay.timeout_ms.or(self.timeout_ms),
133            parallel_tool_calls: overlay.parallel_tool_calls.or(self.parallel_tool_calls),
134            tool_choice: overlay
135                .tool_choice
136                .clone()
137                .or_else(|| self.tool_choice.clone()),
138            seed: overlay.seed.or(self.seed),
139            stop_sequences: if overlay.stop_sequences.is_empty() {
140                self.stop_sequences.clone()
141            } else {
142                overlay.stop_sequences.clone()
143            },
144            presence_penalty: overlay.presence_penalty.or(self.presence_penalty),
145            frequency_penalty: overlay.frequency_penalty.or(self.frequency_penalty),
146            logit_bias: if overlay.logit_bias.is_empty() {
147                self.logit_bias.clone()
148            } else {
149                let mut logit_bias = self.logit_bias.clone();
150                logit_bias.extend(overlay.logit_bias.clone());
151                logit_bias
152            },
153            thinking: overlay.thinking.clone().or_else(|| self.thinking.clone()),
154            service_tier: overlay
155                .service_tier
156                .clone()
157                .or_else(|| self.service_tier.clone()),
158            provider_settings: self.provider_settings.merge(&overlay.provider_settings),
159            provider_replay: overlay
160                .provider_replay
161                .clone()
162                .or_else(|| self.provider_replay.clone()),
163            provider_options: overlay
164                .provider_options
165                .clone()
166                .or_else(|| self.provider_options.clone()),
167            extra_headers: if overlay.extra_headers.is_empty() {
168                self.extra_headers.clone()
169            } else {
170                overlay_headers_case_insensitive(&self.extra_headers, &overlay.extra_headers)
171            },
172            extra_body: if overlay.extra_body.is_empty() {
173                self.extra_body.clone()
174            } else {
175                let mut body = self.extra_body.clone();
176                body.extend(overlay.extra_body.clone());
177                body
178            },
179        }
180    }
181}
182
183/// Typed provider-specific settings.
184#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
185pub struct ProviderSettings {
186    /// `OpenAI` Chat Completions settings.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub openai_chat: Option<OpenAiChatSettings>,
189    /// `OpenAI` Responses settings.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub openai_responses: Option<OpenAiResponsesSettings>,
192    /// Anthropic Messages settings.
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub anthropic: Option<AnthropicSettings>,
195    /// Gemini generateContent settings.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub google: Option<GoogleSettings>,
198    /// Bedrock Converse settings.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub bedrock: Option<BedrockSettings>,
201    /// Codex OAuth routing settings.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub codex: Option<CodexSettings>,
204    /// Gateway sticky-routing settings.
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub gateway: Option<GatewaySettings>,
207}
208
209impl ProviderSettings {
210    /// Return whether no provider-specific settings are present.
211    #[must_use]
212    pub const fn is_empty(&self) -> bool {
213        self.openai_chat.is_none()
214            && self.openai_responses.is_none()
215            && self.anthropic.is_none()
216            && self.google.is_none()
217            && self.bedrock.is_none()
218            && self.codex.is_none()
219            && self.gateway.is_none()
220    }
221
222    /// Merge provider settings field by field.
223    #[must_use]
224    pub fn merge(&self, overlay: &Self) -> Self {
225        Self {
226            openai_chat: merge_openai_chat(self.openai_chat.as_ref(), overlay.openai_chat.as_ref()),
227            openai_responses: merge_openai_responses(
228                self.openai_responses.as_ref(),
229                overlay.openai_responses.as_ref(),
230            ),
231            anthropic: merge_anthropic(self.anthropic.as_ref(), overlay.anthropic.as_ref()),
232            google: merge_google(self.google.as_ref(), overlay.google.as_ref()),
233            bedrock: merge_bedrock(self.bedrock.as_ref(), overlay.bedrock.as_ref()),
234            codex: merge_codex(self.codex.as_ref(), overlay.codex.as_ref()),
235            gateway: merge_gateway(self.gateway.as_ref(), overlay.gateway.as_ref()),
236        }
237    }
238}
239
240fn merge_option<T, F>(base: Option<&T>, overlay: Option<&T>, merge: F) -> Option<T>
241where
242    T: Clone,
243    F: FnOnce(&T, &T) -> T,
244{
245    match (base, overlay) {
246        (Some(base), Some(overlay)) => Some(merge(base, overlay)),
247        (Some(base), None) => Some(base.clone()),
248        (None, Some(overlay)) => Some(overlay.clone()),
249        (None, None) => None,
250    }
251}
252
253fn overlay_vec<T: Clone>(base: &[T], overlay: &[T]) -> Vec<T> {
254    if overlay.is_empty() {
255        base.to_vec()
256    } else {
257        overlay.to_vec()
258    }
259}
260
261fn overlay_headers_case_insensitive(
262    base: &BTreeMap<String, String>,
263    overlay: &BTreeMap<String, String>,
264) -> BTreeMap<String, String> {
265    let mut merged = base.clone();
266    for (key, value) in overlay {
267        merged.retain(|existing, _| !existing.eq_ignore_ascii_case(key));
268        merged.insert(key.clone(), value.clone());
269    }
270    merged
271}
272
273fn merge_openai_chat(
274    base: Option<&OpenAiChatSettings>,
275    overlay: Option<&OpenAiChatSettings>,
276) -> Option<OpenAiChatSettings> {
277    merge_option(base, overlay, |base, overlay| OpenAiChatSettings {
278        user: overlay.user.clone().or_else(|| base.user.clone()),
279        store: overlay.store.or(base.store),
280        logprobs: overlay.logprobs.or(base.logprobs),
281        top_logprobs: overlay.top_logprobs.or(base.top_logprobs),
282        prediction: overlay
283            .prediction
284            .clone()
285            .or_else(|| base.prediction.clone()),
286        prompt_cache_key: overlay
287            .prompt_cache_key
288            .clone()
289            .or_else(|| base.prompt_cache_key.clone()),
290        prompt_cache_retention: overlay
291            .prompt_cache_retention
292            .clone()
293            .or_else(|| base.prompt_cache_retention.clone()),
294        prompt_cache_options: overlay
295            .prompt_cache_options
296            .clone()
297            .or_else(|| base.prompt_cache_options.clone()),
298    })
299}
300
301fn merge_openai_responses(
302    base: Option<&OpenAiResponsesSettings>,
303    overlay: Option<&OpenAiResponsesSettings>,
304) -> Option<OpenAiResponsesSettings> {
305    merge_option(base, overlay, |base, overlay| OpenAiResponsesSettings {
306        store: overlay.store.or(base.store),
307        user: overlay.user.clone().or_else(|| base.user.clone()),
308        truncation: overlay
309            .truncation
310            .clone()
311            .or_else(|| base.truncation.clone()),
312        text_verbosity: overlay
313            .text_verbosity
314            .clone()
315            .or_else(|| base.text_verbosity.clone()),
316        context_management: overlay
317            .context_management
318            .clone()
319            .or_else(|| base.context_management.clone()),
320        include: overlay_vec(&base.include, &overlay.include),
321        prompt_cache_key: overlay
322            .prompt_cache_key
323            .clone()
324            .or_else(|| base.prompt_cache_key.clone()),
325        prompt_cache_retention: overlay
326            .prompt_cache_retention
327            .clone()
328            .or_else(|| base.prompt_cache_retention.clone()),
329        prompt_cache_options: overlay
330            .prompt_cache_options
331            .clone()
332            .or_else(|| base.prompt_cache_options.clone()),
333        stream_transport: overlay.stream_transport.or(base.stream_transport),
334    })
335}
336
337fn merge_anthropic(
338    base: Option<&AnthropicSettings>,
339    overlay: Option<&AnthropicSettings>,
340) -> Option<AnthropicSettings> {
341    merge_option(base, overlay, |base, overlay| AnthropicSettings {
342        metadata: overlay.metadata.clone().or_else(|| base.metadata.clone()),
343        betas: overlay_vec(&base.betas, &overlay.betas),
344        context_management: overlay
345            .context_management
346            .clone()
347            .or_else(|| base.context_management.clone()),
348        container: overlay.container.clone().or_else(|| base.container.clone()),
349        service_tier: overlay
350            .service_tier
351            .clone()
352            .or_else(|| base.service_tier.clone()),
353    })
354}
355
356fn merge_google(
357    base: Option<&GoogleSettings>,
358    overlay: Option<&GoogleSettings>,
359) -> Option<GoogleSettings> {
360    merge_option(base, overlay, |base, overlay| GoogleSettings {
361        safety_settings: overlay
362            .safety_settings
363            .clone()
364            .or_else(|| base.safety_settings.clone()),
365        cached_content: overlay
366            .cached_content
367            .clone()
368            .or_else(|| base.cached_content.clone()),
369        labels: overlay.labels.clone().or_else(|| base.labels.clone()),
370        response_logprobs: overlay.response_logprobs.or(base.response_logprobs),
371        logprobs: overlay.logprobs.or(base.logprobs),
372        service_tier: overlay
373            .service_tier
374            .clone()
375            .or_else(|| base.service_tier.clone()),
376        cloud_service_tier: overlay
377            .cloud_service_tier
378            .clone()
379            .or_else(|| base.cloud_service_tier.clone()),
380    })
381}
382
383fn merge_bedrock(
384    base: Option<&BedrockSettings>,
385    overlay: Option<&BedrockSettings>,
386) -> Option<BedrockSettings> {
387    merge_option(base, overlay, |base, overlay| BedrockSettings {
388        guardrail_config: overlay
389            .guardrail_config
390            .clone()
391            .or_else(|| base.guardrail_config.clone()),
392        performance_config: overlay
393            .performance_config
394            .clone()
395            .or_else(|| base.performance_config.clone()),
396        request_metadata: overlay
397            .request_metadata
398            .clone()
399            .or_else(|| base.request_metadata.clone()),
400        additional_model_response_field_paths: overlay_vec(
401            &base.additional_model_response_field_paths,
402            &overlay.additional_model_response_field_paths,
403        ),
404        prompt_variables: overlay
405            .prompt_variables
406            .clone()
407            .or_else(|| base.prompt_variables.clone()),
408        additional_model_request_fields: overlay
409            .additional_model_request_fields
410            .clone()
411            .or_else(|| base.additional_model_request_fields.clone()),
412        inference_profile: overlay
413            .inference_profile
414            .clone()
415            .or_else(|| base.inference_profile.clone()),
416    })
417}
418
419fn merge_codex(
420    base: Option<&CodexSettings>,
421    overlay: Option<&CodexSettings>,
422) -> Option<CodexSettings> {
423    merge_option(base, overlay, |base, overlay| CodexSettings {
424        session_id: overlay
425            .session_id
426            .clone()
427            .or_else(|| base.session_id.clone()),
428        thread_id: overlay.thread_id.clone().or_else(|| base.thread_id.clone()),
429    })
430}
431
432fn merge_gateway(
433    base: Option<&GatewaySettings>,
434    overlay: Option<&GatewaySettings>,
435) -> Option<GatewaySettings> {
436    merge_option(base, overlay, |base, overlay| GatewaySettings {
437        x_session_id: overlay
438            .x_session_id
439            .clone()
440            .or_else(|| base.x_session_id.clone()),
441        extra_headers: overlay_headers_case_insensitive(
442            &base.extra_headers,
443            &overlay.extra_headers,
444        ),
445    })
446}
447
448/// GPT-5.6 prompt-cache breakpoint placement mode.
449#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
450#[serde(rename_all = "snake_case")]
451pub enum OpenAiPromptCacheMode {
452    /// Keep the implicit breakpoint on the latest message and honor explicit points.
453    Implicit,
454    /// Use only explicit cache points.
455    Explicit,
456}
457
458/// GPT-5.6 request-wide prompt-cache lifetime.
459#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
460pub enum OpenAiPromptCacheTtl {
461    /// Keep cache entries for at least thirty minutes.
462    #[serde(rename = "30m")]
463    ThirtyMinutes,
464}
465
466/// Request-wide GPT-5.6 prompt-cache policy.
467#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
468pub struct OpenAiPromptCacheOptions {
469    /// Breakpoint placement mode.
470    pub mode: OpenAiPromptCacheMode,
471    /// Minimum cache lifetime. `OpenAI` currently accepts only `30m`.
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub ttl: Option<OpenAiPromptCacheTtl>,
474}
475
476/// `OpenAI` Chat Completions typed settings.
477#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
478pub struct OpenAiChatSettings {
479    /// End-user identifier forwarded to `OpenAI`.
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub user: Option<String>,
482    /// Store the completion for provider-side distillation/evals where supported.
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub store: Option<bool>,
485    /// Return log probabilities for output tokens.
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub logprobs: Option<bool>,
488    /// Number of top log probabilities to return.
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub top_logprobs: Option<u32>,
491    /// Prediction hint payload.
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub prediction: Option<Value>,
494    /// Prompt cache key.
495    #[serde(default, skip_serializing_if = "Option::is_none")]
496    pub prompt_cache_key: Option<String>,
497    /// Legacy prompt cache retention setting for models before GPT-5.6.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub prompt_cache_retention: Option<String>,
500    /// GPT-5.6 request-wide prompt cache options.
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub prompt_cache_options: Option<OpenAiPromptCacheOptions>,
503}
504
505/// Streaming transport policy for Responses-compatible providers.
506#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
507#[serde(rename_all = "snake_case")]
508pub enum ResponseStreamTransport {
509    /// Use HTTP streaming over server-sent events.
510    Http,
511    /// Use Responses WebSocket mode.
512    WebSocket,
513    /// Prefer WebSocket and fall back to HTTP streaming for retryable pre-event WebSocket errors.
514    Auto,
515}
516
517/// `OpenAI` Responses typed settings.
518#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
519pub struct OpenAiResponsesSettings {
520    /// Store the response for provider-side distillation/evals where supported.
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub store: Option<bool>,
523    /// End-user identifier forwarded to `OpenAI`.
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub user: Option<String>,
526    /// Response truncation strategy.
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub truncation: Option<String>,
529    /// Text verbosity for supported models.
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub text_verbosity: Option<String>,
532    /// Context management payload.
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub context_management: Option<Value>,
535    /// Additional include values.
536    #[serde(default, skip_serializing_if = "Vec::is_empty")]
537    pub include: Vec<String>,
538    /// Prompt cache key.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub prompt_cache_key: Option<String>,
541    /// Legacy prompt cache retention setting for models before GPT-5.6.
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub prompt_cache_retention: Option<String>,
544    /// GPT-5.6 request-wide prompt cache options.
545    #[serde(default, skip_serializing_if = "Option::is_none")]
546    pub prompt_cache_options: Option<OpenAiPromptCacheOptions>,
547    /// Streaming transport policy for Responses-compatible providers.
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub stream_transport: Option<ResponseStreamTransport>,
550}
551
552/// Anthropic Messages typed settings.
553#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
554pub struct AnthropicSettings {
555    /// Request metadata.
556    #[serde(default, skip_serializing_if = "Option::is_none")]
557    pub metadata: Option<Value>,
558    /// Beta feature names merged into the `anthropic-beta` request header.
559    #[serde(default, skip_serializing_if = "Vec::is_empty")]
560    pub betas: Vec<String>,
561    /// Context management payload.
562    #[serde(default, skip_serializing_if = "Option::is_none")]
563    pub context_management: Option<Value>,
564    /// Container identifier.
565    #[serde(default, skip_serializing_if = "Option::is_none")]
566    pub container: Option<String>,
567    /// Service tier.
568    #[serde(default, skip_serializing_if = "Option::is_none")]
569    pub service_tier: Option<String>,
570}
571
572/// Gemini generateContent typed settings.
573#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
574pub struct GoogleSettings {
575    /// Safety settings array.
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub safety_settings: Option<Value>,
578    /// Cached content resource name.
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub cached_content: Option<String>,
581    /// Backend-specific labels map.
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub labels: Option<Value>,
584    /// Return chosen-token log probabilities.
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub response_logprobs: Option<bool>,
587    /// Number of top candidate log probabilities to return.
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub logprobs: Option<u32>,
590    /// Cloud service tier.
591    #[serde(default, skip_serializing_if = "Option::is_none")]
592    pub service_tier: Option<String>,
593    /// Google Cloud routing tier.
594    #[serde(default, skip_serializing_if = "Option::is_none")]
595    pub cloud_service_tier: Option<GoogleCloudServiceTier>,
596}
597
598/// Google Cloud Gemini routing tier.
599#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
600#[serde(rename_all = "snake_case")]
601pub enum GoogleCloudServiceTier {
602    /// Provisioned throughput when available, then standard on-demand spillover.
603    PtThenOnDemand,
604    /// Provisioned throughput only.
605    PtOnly,
606    /// Provisioned throughput when available, then Flex `PayGo` spillover.
607    PtThenFlex,
608    /// Provisioned throughput when available, then Priority `PayGo` spillover.
609    PtThenPriority,
610    /// Standard on-demand only.
611    OnDemand,
612    /// Flex `PayGo` only.
613    FlexOnly,
614    /// Priority `PayGo` only.
615    PriorityOnly,
616}
617
618/// Bedrock Converse typed settings.
619#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
620pub struct BedrockSettings {
621    /// Guardrail config payload.
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub guardrail_config: Option<Value>,
624    /// Performance config payload.
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    pub performance_config: Option<Value>,
627    /// Request metadata payload.
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub request_metadata: Option<Value>,
630    /// Additional response field paths.
631    #[serde(default, skip_serializing_if = "Vec::is_empty")]
632    pub additional_model_response_field_paths: Vec<String>,
633    /// Prompt variables payload.
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub prompt_variables: Option<Value>,
636    /// Additional model request fields merged after typed passthrough fields.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub additional_model_request_fields: Option<Value>,
639    /// Inference profile identifier used as the Bedrock `modelId` routing value.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub inference_profile: Option<String>,
642}
643
644/// Codex OAuth typed routing settings.
645#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
646pub struct CodexSettings {
647    /// Provider session ID for Codex headers.
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub session_id: Option<String>,
650    /// Provider thread ID for Codex headers.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub thread_id: Option<String>,
653}
654
655/// Gateway typed sticky-routing settings.
656#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
657pub struct GatewaySettings {
658    /// Gateway sticky session header value.
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub x_session_id: Option<String>,
661    /// Gateway-specific extra headers.
662    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
663    pub extra_headers: BTreeMap<String, String>,
664}
665
666/// Provider replay and server-side state controls.
667#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
668pub struct ProviderReplaySettings {
669    /// Same-provider response ID chaining policy, such as `auto` or a concrete provider response ID.
670    #[serde(default, skip_serializing_if = "Option::is_none")]
671    pub previous_response_id: Option<String>,
672    /// Same-provider server-side conversation policy, such as `auto` or a concrete conversation ID.
673    #[serde(default, skip_serializing_if = "Option::is_none")]
674    pub conversation_id: Option<String>,
675    /// Whether to replay provider item IDs when the same provider can consume them safely.
676    #[serde(default, skip_serializing_if = "Option::is_none")]
677    pub send_item_ids: Option<bool>,
678    /// Whether to request and replay encrypted reasoning payloads when supported.
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub include_encrypted_reasoning: Option<bool>,
681}
682
683/// Tool selection policy.
684#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
685#[serde(rename_all = "snake_case", tag = "kind")]
686pub enum ToolChoice {
687    /// Provider decides whether to call a tool.
688    Auto,
689    /// Disable tools.
690    None,
691    /// Require any tool call.
692    Required,
693    /// Restrict the model to one or more named function tools.
694    Tools {
695        /// Function tool names.
696        names: Vec<String>,
697    },
698    /// Restrict function tools while keeping structured output, text, and image output available.
699    ToolOrOutput {
700        /// Function tool names.
701        function_tools: Vec<String>,
702    },
703    /// Force a named tool.
704    Tool {
705        /// Tool name.
706        name: String,
707    },
708}
709
710/// Reasoning or thinking controls.
711#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
712pub struct ThinkingSettings {
713    /// Effort level such as low, medium, high, xhigh, or max.
714    pub effort: String,
715    /// Optional token budget.
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub budget_tokens: Option<u32>,
718    /// Provider-specific thinking mode, such as enabled, adaptive, disabled, standard, or pro.
719    #[serde(default, skip_serializing_if = "Option::is_none")]
720    pub mode: Option<String>,
721    /// Whether provider should include thought summaries or thinking traces when supported.
722    #[serde(default, skip_serializing_if = "Option::is_none")]
723    pub include_thoughts: Option<bool>,
724    /// Optional provider-specific reasoning summary mode.
725    #[serde(default, skip_serializing_if = "Option::is_none")]
726    pub summary: Option<String>,
727}
728
729/// Provider service tier.
730#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
731#[serde(rename_all = "snake_case")]
732pub enum ServiceTier {
733    /// Let the provider decide, when it has an explicit auto tier.
734    Auto,
735    /// Explicit standard/default provider tier.
736    Default,
737    /// Low-latency tier.
738    Flex,
739    /// Priority tier.
740    Priority,
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746
747    #[test]
748    fn merges_overlay_fields() {
749        let base = ModelSettings {
750            max_tokens: Some(128),
751            temperature: Some(0.2),
752            stop_sequences: vec!["base".to_string()],
753            ..ModelSettings::default()
754        };
755        let overlay = ModelSettings {
756            temperature: Some(0.7),
757            stop_sequences: vec!["overlay".to_string()],
758            ..ModelSettings::default()
759        };
760
761        let merged = base.merge(&overlay);
762
763        assert_eq!(merged.max_tokens, Some(128));
764        assert_eq!(merged.temperature, Some(0.7));
765        assert_eq!(merged.stop_sequences, vec!["overlay"]);
766    }
767}