Skip to main content

oxicode_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    /// Forces the next assistant turn's tool choice. `None`/`Auto` = no
149    /// change from today's behavior. See [`crate::tools::ToolChoice`].
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub tool_choice: Option<crate::tools::ToolChoice>,
152
153    /// Per-provider options for fine-grained control.
154    ///
155    /// Each provider reads only its own section. For example, the Anthropic
156    /// provider reads `provider_options.anthropic`, OpenAI reads
157    /// `provider_options.openai`. This allows a single request to carry
158    /// options for multiple providers without conflicts.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub provider_options: Option<ProviderOptions>,
161}
162
163impl fmt::Debug for StreamOptions {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_struct("StreamOptions")
166            .field("temperature", &self.temperature)
167            .field("max_tokens", &self.max_tokens)
168            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
169            .field("cache_retention", &self.cache_retention)
170            .field("session_id", &self.session_id)
171            .field("headers", &self.headers)
172            .field("thinking_level", &self.thinking_level)
173            .field("thinking_budgets", &self.thinking_budgets)
174            .field("provider_options", &self.provider_options)
175            .finish()
176    }
177}
178
179impl StreamOptions {
180    /// Create new stream options
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Set temperature
186    pub fn temperature(mut self, temp: f64) -> Self {
187        self.temperature = Some(temp);
188        self
189    }
190
191    /// Set max tokens
192    pub fn max_tokens(mut self, tokens: usize) -> Self {
193        self.max_tokens = Some(tokens);
194        self
195    }
196
197    /// Set API key
198    pub fn api_key(mut self, key: impl Into<String>) -> Self {
199        self.api_key = Some(key.into());
200        self
201    }
202
203    /// Set cache retention
204    pub fn cache_retention(mut self, retention: CacheRetention) -> Self {
205        self.cache_retention = Some(retention);
206        self
207    }
208
209    /// Set session ID
210    pub fn session_id(mut self, id: impl Into<String>) -> Self {
211        self.session_id = Some(id.into());
212        self
213    }
214
215    /// Set thinking level
216    pub fn thinking_level(mut self, level: ThinkingLevel) -> Self {
217        self.thinking_level = Some(level);
218        self
219    }
220}
221
222/// Token budgets for thinking levels
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224pub struct ThinkingBudgets {
225    #[serde(default)]
226    pub minimal: Option<usize>,
227    #[serde(default)]
228    pub low: Option<usize>,
229    #[serde(default)]
230    pub medium: Option<usize>,
231    #[serde(default)]
232    pub high: Option<usize>,
233}
234
235impl ThinkingBudgets {
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    pub fn minimal(mut self, tokens: usize) -> Self {
241        self.minimal = Some(tokens);
242        self
243    }
244
245    pub fn low(mut self, tokens: usize) -> Self {
246        self.low = Some(tokens);
247        self
248    }
249
250    pub fn medium(mut self, tokens: usize) -> Self {
251        self.medium = Some(tokens);
252        self
253    }
254
255    pub fn high(mut self, tokens: usize) -> Self {
256        self.high = Some(tokens);
257        self
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::tools::ToolChoice;
265
266    #[test]
267    fn stream_options_default_has_no_tool_choice() {
268        let opts = StreamOptions::default();
269        assert!(opts.tool_choice.is_none());
270    }
271
272    #[test]
273    fn tool_choice_named_round_trips_through_serde() {
274        let tc = ToolChoice::Named("todo".to_string());
275        let json = serde_json::to_string(&tc).unwrap();
276        let back: ToolChoice = serde_json::from_str(&json).unwrap();
277        assert_eq!(back, tc);
278    }
279}