Skip to main content

oxi_ai/providers/
options.rs

1//! Stream options for providers
2
3use crate::{CacheRetention, ThinkingLevel};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt;
7
8/// Per-provider options for fine-grained control.
9///
10/// Each field corresponds to a specific provider's native API option.
11/// Only the relevant provider reads its section; others ignore it.
12/// Mirrors opencode's `providerOptions` pattern where the request carries
13/// a bag of per-provider knobs that the protocol layer reads selectively.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct ProviderOptions {
16    /// Anthropic-specific options.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub anthropic: Option<AnthropicOptions>,
19
20    /// OpenAI-specific options.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub openai: Option<OpenAiOptions>,
23
24    /// Google/Gemini-specific options.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub google: Option<GoogleOptions>,
27
28    /// Generic OpenAI-compatible provider options.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub openai_compatible: Option<OpenAiCompatibleOptions>,
31}
32
33/// Anthropic-specific options.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AnthropicOptions {
36    /// Extended thinking mode.
37    /// - `"enabled"`: Fixed budget thinking
38    /// - `"adaptive"`: Anthropic chooses budget based on effort
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub thinking_type: Option<String>,
41
42    /// Token budget for thinking (when thinking_type is "enabled").
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub thinking_budget: Option<usize>,
45
46    /// Reasoning effort level (when thinking_type is "adaptive").
47    /// Values: "low", "medium", "high", "xhigh", "max".
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub effort: Option<String>,
50}
51
52/// OpenAI-specific options.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct OpenAiOptions {
55    /// Whether to store the response for session continuity.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub store: Option<bool>,
58
59    /// Reasoning effort: "low", "medium", "high", "xhigh".
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub reasoning_effort: Option<String>,
62
63    /// Whether to include reasoning summary in the response.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub reasoning_summary: Option<String>,
66
67    /// Whether to include encrypted reasoning content for session continuity.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub include_encrypted_reasoning: Option<bool>,
70
71    /// Text verbosity: "low", "medium", "high".
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub text_verbosity: Option<String>,
74
75    /// Prompt cache key for server-side caching.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub prompt_cache_key: Option<String>,
78}
79
80/// Google/Gemini-specific options.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct GoogleOptions {
83    /// Whether to include thoughts in the response.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub include_thoughts: Option<bool>,
86
87    /// Thinking level: "low", "medium", "high".
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub thinking_level: Option<String>,
90
91    /// Thinking budget in tokens.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub thinking_budget: Option<usize>,
94}
95
96/// Generic OpenAI-compatible provider options.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct OpenAiCompatibleOptions {
99    /// Reasoning effort level.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub reasoning_effort: Option<String>,
102
103    /// Whether thinking is enabled (for providers like ZAI).
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub enable_thinking: Option<bool>,
106
107    /// Cache control marker.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub cache_control: Option<String>,
110}
111
112/// Options for streaming requests
113#[derive(Clone, Default, Serialize, Deserialize)]
114pub struct StreamOptions {
115    /// Sampling temperature (0.0 to 2.0)
116    #[serde(default)]
117    pub temperature: Option<f64>,
118
119    /// Maximum tokens to generate
120    #[serde(default)]
121    pub max_tokens: Option<usize>,
122
123    /// API key (overrides environment variable)
124    /// This field is excluded from serialization and Debug output to prevent leakage.
125    #[serde(skip)]
126    pub api_key: Option<String>,
127
128    /// Cache retention preference
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub cache_retention: Option<CacheRetention>,
131
132    /// Session ID for providers that support session-based caching
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub session_id: Option<String>,
135
136    /// Custom HTTP headers to include
137    #[serde(default)]
138    pub headers: HashMap<String, String>,
139
140    /// Thinking/reasoning level
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub thinking_level: Option<ThinkingLevel>,
143
144    /// Custom token budgets for thinking levels
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub thinking_budgets: Option<ThinkingBudgets>,
147
148    /// Per-provider options for fine-grained control.
149    ///
150    /// Each provider reads only its own section. For example, the Anthropic
151    /// provider reads `provider_options.anthropic`, OpenAI reads
152    /// `provider_options.openai`. This allows a single request to carry
153    /// options for multiple providers without conflicts.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub provider_options: Option<ProviderOptions>,
156}
157
158impl fmt::Debug for StreamOptions {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("StreamOptions")
161            .field("temperature", &self.temperature)
162            .field("max_tokens", &self.max_tokens)
163            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
164            .field("cache_retention", &self.cache_retention)
165            .field("session_id", &self.session_id)
166            .field("headers", &self.headers)
167            .field("thinking_level", &self.thinking_level)
168            .field("thinking_budgets", &self.thinking_budgets)
169            .field("provider_options", &self.provider_options)
170            .finish()
171    }
172}
173
174impl StreamOptions {
175    /// Create new stream options
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Set temperature
181    pub fn temperature(mut self, temp: f64) -> Self {
182        self.temperature = Some(temp);
183        self
184    }
185
186    /// Set max tokens
187    pub fn max_tokens(mut self, tokens: usize) -> Self {
188        self.max_tokens = Some(tokens);
189        self
190    }
191
192    /// Set API key
193    pub fn api_key(mut self, key: impl Into<String>) -> Self {
194        self.api_key = Some(key.into());
195        self
196    }
197
198    /// Set cache retention
199    pub fn cache_retention(mut self, retention: CacheRetention) -> Self {
200        self.cache_retention = Some(retention);
201        self
202    }
203
204    /// Set session ID
205    pub fn session_id(mut self, id: impl Into<String>) -> Self {
206        self.session_id = Some(id.into());
207        self
208    }
209
210    /// Set thinking level
211    pub fn thinking_level(mut self, level: ThinkingLevel) -> Self {
212        self.thinking_level = Some(level);
213        self
214    }
215}
216
217/// Token budgets for thinking levels
218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct ThinkingBudgets {
220    #[serde(default)]
221    pub minimal: Option<usize>,
222    #[serde(default)]
223    pub low: Option<usize>,
224    #[serde(default)]
225    pub medium: Option<usize>,
226    #[serde(default)]
227    pub high: Option<usize>,
228}
229
230impl ThinkingBudgets {
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    pub fn minimal(mut self, tokens: usize) -> Self {
236        self.minimal = Some(tokens);
237        self
238    }
239
240    pub fn low(mut self, tokens: usize) -> Self {
241        self.low = Some(tokens);
242        self
243    }
244
245    pub fn medium(mut self, tokens: usize) -> Self {
246        self.medium = Some(tokens);
247        self
248    }
249
250    pub fn high(mut self, tokens: usize) -> Self {
251        self.high = Some(tokens);
252        self
253    }
254}