Skip to main content

aurum_core/provider_platform/
context.rs

1//! Provider construction context with scoped secrets (JOE-1933).
2
3use crate::observability::Metrics;
4use crate::providers::local::SttContextPool;
5use crate::providers::OpenRouterSttMode;
6use crate::runtime::ResourceGovernor;
7use crate::secret::SecretString;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11#[cfg(feature = "tts")]
12use crate::tts::local::TtsSessionPool;
13
14/// Bounded inputs for constructing a single provider instance.
15///
16/// # Secret scoping
17///
18/// At most **one** provider-scoped API key may be present. Factories must not
19/// receive a bag of every vendor credential. Callers (engine / CLI) select the
20/// key for the factory being built before invoking [`TranscriptionProviderFactory::build`]
21/// / [`SynthesisProviderFactory::build`].
22#[derive(Clone)]
23pub struct ProviderBuildContext {
24    cache_dir: PathBuf,
25    local_only: bool,
26    /// Secret for *this* provider only, if any.
27    api_key: Option<SecretString>,
28    base_url: Option<String>,
29    allow_custom_endpoint: bool,
30    use_system_proxy: bool,
31    /// CLI/library progress for local STT/TTS (remote providers ignore).
32    show_progress: bool,
33    /// OpenRouter STT routing mode (ignored by non-OpenRouter factories).
34    stt_mode: OpenRouterSttMode,
35    /// TTS input character budget (local TTS factory).
36    tts_max_chars: Option<usize>,
37    stt_pool: Option<Arc<SttContextPool>>,
38    governor: Option<Arc<ResourceGovernor>>,
39    metrics: Option<Arc<Metrics>>,
40    #[cfg(feature = "tts")]
41    tts_pool: Option<Arc<TtsSessionPool>>,
42}
43
44impl ProviderBuildContext {
45    /// Minimal context: cache root only (process-global pools used by factories if needed).
46    pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
47        Self {
48            cache_dir: cache_dir.into(),
49            local_only: false,
50            api_key: None,
51            base_url: None,
52            allow_custom_endpoint: false,
53            use_system_proxy: false,
54            show_progress: false,
55            stt_mode: OpenRouterSttMode::Auto,
56            tts_max_chars: None,
57            stt_pool: None,
58            governor: None,
59            metrics: None,
60            #[cfg(feature = "tts")]
61            tts_pool: None,
62        }
63    }
64
65    pub fn with_local_only(mut self, local_only: bool) -> Self {
66        self.local_only = local_only;
67        self
68    }
69
70    /// Attach a **single** provider-scoped secret (already selected by the caller).
71    pub fn with_api_key(mut self, key: Option<SecretString>) -> Self {
72        self.api_key = key;
73        self
74    }
75
76    pub fn with_base_url(mut self, base_url: Option<String>) -> Self {
77        self.base_url = base_url;
78        self
79    }
80
81    pub fn with_allow_custom_endpoint(mut self, allow: bool) -> Self {
82        self.allow_custom_endpoint = allow;
83        self
84    }
85
86    pub fn with_use_system_proxy(mut self, use_proxy: bool) -> Self {
87        self.use_system_proxy = use_proxy;
88        self
89    }
90
91    pub fn with_show_progress(mut self, show: bool) -> Self {
92        self.show_progress = show;
93        self
94    }
95
96    pub fn with_stt_mode(mut self, mode: OpenRouterSttMode) -> Self {
97        self.stt_mode = mode;
98        self
99    }
100
101    pub fn with_tts_max_chars(mut self, n: Option<usize>) -> Self {
102        self.tts_max_chars = n;
103        self
104    }
105
106    pub fn with_stt_pool(mut self, pool: Arc<SttContextPool>) -> Self {
107        self.stt_pool = Some(pool);
108        self
109    }
110
111    pub fn with_governor(mut self, gov: Arc<ResourceGovernor>) -> Self {
112        self.governor = Some(gov);
113        self
114    }
115
116    pub fn with_metrics(mut self, metrics: Arc<Metrics>) -> Self {
117        self.metrics = Some(metrics);
118        self
119    }
120
121    #[cfg(feature = "tts")]
122    pub fn with_tts_pool(mut self, pool: Arc<TtsSessionPool>) -> Self {
123        self.tts_pool = Some(pool);
124        self
125    }
126
127    pub fn cache_dir(&self) -> &Path {
128        &self.cache_dir
129    }
130
131    pub fn local_only(&self) -> bool {
132        self.local_only
133    }
134
135    pub fn base_url(&self) -> Option<&str> {
136        self.base_url.as_deref()
137    }
138
139    pub fn allow_custom_endpoint(&self) -> bool {
140        self.allow_custom_endpoint
141    }
142
143    pub fn use_system_proxy(&self) -> bool {
144        self.use_system_proxy
145    }
146
147    pub fn show_progress(&self) -> bool {
148        self.show_progress
149    }
150
151    pub fn stt_mode(&self) -> OpenRouterSttMode {
152        self.stt_mode
153    }
154
155    pub fn tts_max_chars(&self) -> Option<usize> {
156        self.tts_max_chars
157    }
158
159    pub fn stt_pool(&self) -> Option<&Arc<SttContextPool>> {
160        self.stt_pool.as_ref()
161    }
162
163    pub fn governor(&self) -> Option<&Arc<ResourceGovernor>> {
164        self.governor.as_ref()
165    }
166
167    pub fn metrics(&self) -> Option<&Arc<Metrics>> {
168        self.metrics.as_ref()
169    }
170
171    #[cfg(feature = "tts")]
172    pub fn tts_pool(&self) -> Option<&Arc<TtsSessionPool>> {
173        self.tts_pool.as_ref()
174    }
175
176    /// Borrow the scoped API key for provider construction (JOE-1980).
177    ///
178    /// Returns [`SecretString`] — not a plaintext `String`. Providers must retain
179    /// the redacting type until the final Authorization/`xi-api-key` header build.
180    pub fn api_key(&self) -> Option<&SecretString> {
181        self.api_key.as_ref()
182    }
183
184    /// Clone the scoped secret (still redacting) for factory ownership transfer.
185    pub fn api_key_cloned(&self) -> Option<SecretString> {
186        self.api_key.clone()
187    }
188
189    /// Whether a scoped key is present (without revealing it).
190    pub fn has_api_key(&self) -> bool {
191        self.api_key.is_some()
192    }
193
194    /// Deprecated alias: prefer [`Self::api_key_cloned`].
195    #[deprecated(note = "use api_key_cloned() — does not return plaintext")]
196    pub fn api_key_exposed(&self) -> Option<SecretString> {
197        self.api_key_cloned()
198    }
199}
200
201impl std::fmt::Debug for ProviderBuildContext {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.debug_struct("ProviderBuildContext")
204            .field("cache_dir", &self.cache_dir)
205            .field("local_only", &self.local_only)
206            .field("api_key", &self.api_key) // SecretString redacts
207            .field("base_url", &self.base_url)
208            .field("allow_custom_endpoint", &self.allow_custom_endpoint)
209            .field("use_system_proxy", &self.use_system_proxy)
210            .field("show_progress", &self.show_progress)
211            .field("stt_mode", &self.stt_mode)
212            .field("tts_max_chars", &self.tts_max_chars)
213            .field("has_stt_pool", &self.stt_pool.is_some())
214            .field("has_governor", &self.governor.is_some())
215            .field("has_metrics", &self.metrics.is_some())
216            .finish_non_exhaustive()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::secret::SecretString;
224
225    #[test]
226    fn debug_redacts_api_key() {
227        let ctx = ProviderBuildContext::new("/tmp/aurum-test")
228            .with_api_key(Some(SecretString::new("sk-super-secret-key-value")));
229        let dbg = format!("{ctx:?}");
230        assert!(!dbg.contains("sk-super-secret"));
231        assert!(dbg.contains("api_key"));
232    }
233
234    #[test]
235    fn scoped_key_is_optional() {
236        let ctx = ProviderBuildContext::new("/tmp/x");
237        assert!(!ctx.has_api_key());
238        assert!(ctx.api_key().is_none());
239        assert!(ctx.api_key_cloned().is_none());
240    }
241}