Skip to main content

aurum_core/provider_platform/
mod.rs

1//! Shared provider platform: identity, registry, factories (JOE-1933 / JOE-1932 / JOE-1936).
2//!
3//! Direction-specific execution traits remain in [`crate::providers`] (STT) and
4//! [`crate::tts::provider`] (TTS). This module owns **how** those implementations
5//! are identified, registered, and constructed without flattening STT/TTS
6//! semantics. See `docs/development/adr-002-provider-registry.md`.
7//!
8//! Capability discovery and conformance (JOE-1936) route through the registry
9//! via [`capabilities_for`] and [`conformance`].
10
11mod builtin;
12mod conformance;
13mod context;
14mod descriptor;
15mod evidence;
16mod factory;
17mod id;
18mod listing;
19mod lookup;
20mod qualification;
21mod registry;
22
23pub use builtin::{LocalSttFactory, OpenRouterSttFactory};
24#[cfg(feature = "tts")]
25pub use builtin::{LocalTtsFactory, OpenRouterTtsFactory};
26pub use conformance::{
27    check_builtin_conformance, check_descriptor_capabilities, check_network_claim,
28    check_unique_descriptor_identities, ConformanceFailure,
29};
30pub use context::ProviderBuildContext;
31pub use descriptor::{
32    NetworkRequirement, ProviderDescriptor, ProviderOperations, ProviderStability,
33};
34pub use evidence::{
35    detect_catalogue_drift, effective_provider_tier, evaluate_supported_evidence_gate,
36    load_evidence_dir, local_stt_evidence, local_tts_evidence, provider_tier_matrix,
37    tier_from_registry_stability, CatalogueDriftReport, EvidenceFailureCategory,
38    EvidenceGateFinding, EvidenceGateReport, EvidenceOperation, ProviderEvidenceIndex,
39    ProviderEvidenceRecord, SupportTier, SupportedRouteClaim, PROVIDER_EVIDENCE_SCHEMA_VERSION,
40    SUPPORTED_EVIDENCE_MAX_AGE_SECS,
41};
42
43/// Optional overrides when resolving a provider through [`crate::AurumEngine`] (JOE-1938).
44#[derive(Debug, Clone, Default)]
45pub struct ProviderResolveOptions {
46    /// When true, local STT/TTS may emit progress on stderr.
47    pub show_progress: bool,
48    /// Override OpenRouter STT mode; default parses config.
49    pub stt_mode: Option<crate::providers::OpenRouterSttMode>,
50    /// Override `local_only`; default uses config.
51    pub local_only: Option<bool>,
52}
53#[cfg(feature = "tts")]
54pub use factory::SynthesisProviderFactory;
55pub use factory::TranscriptionProviderFactory;
56pub use id::{ProviderId, MAX_PROVIDER_ID_LEN};
57pub use listing::{
58    list_provider_summaries, merge_provider_summaries, provider_list, ProviderList,
59    ProviderSummary, PROVIDER_LIST_SCHEMA_VERSION,
60};
61pub use lookup::{capabilities_for, preflight_stt_with_registry, preflight_tts_with_registry};
62pub use registry::{ProviderRegistry, ProviderRegistryBuilder};
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::capabilities::{CapabilityOperation, ProviderCapabilities};
68    use crate::secret::SecretString;
69    use std::sync::Arc;
70
71    struct FakeStt {
72        desc: ProviderDescriptor,
73    }
74
75    impl TranscriptionProviderFactory for FakeStt {
76        fn descriptor(&self) -> &ProviderDescriptor {
77            &self.desc
78        }
79
80        fn capabilities(&self, model: &str) -> crate::error::Result<ProviderCapabilities> {
81            let mut caps = ProviderCapabilities::with_core(
82                self.desc.id.as_str(),
83                model,
84                CapabilityOperation::Stt,
85            );
86            caps.stt_backend = Some(crate::capabilities::SttBackendClass::Asr);
87            caps.timestamps_reliable = true;
88            caps.languages = vec!["en".into()];
89            caps.max_duration_secs = Some(60.0);
90            caps.supports_cancellation = true;
91            caps.requires_network = false;
92            caps.local_only_ok = true;
93            caps.output_formats = vec!["txt".into()];
94            Ok(caps)
95        }
96
97        fn build(
98            &self,
99            _ctx: &ProviderBuildContext,
100        ) -> crate::error::Result<Arc<dyn crate::providers::TranscriptionProvider>> {
101            Err(crate::error::UserError::Other {
102                message: "fake stt does not implement inference".into(),
103            }
104            .into())
105        }
106    }
107
108    #[test]
109    fn rejects_duplicate_stt_registration() {
110        let f1: Arc<dyn TranscriptionProviderFactory> = Arc::new(FakeStt {
111            desc: ProviderDescriptor::new(
112                ProviderId::must("fake"),
113                "Fake",
114                ProviderOperations::STT_ONLY,
115                NetworkRequirement::LocalOnly,
116                ProviderStability::TestOnly,
117            ),
118        });
119        let f2: Arc<dyn TranscriptionProviderFactory> = Arc::new(FakeStt {
120            desc: ProviderDescriptor::new(
121                ProviderId::must("fake"),
122                "Fake2",
123                ProviderOperations::STT_ONLY,
124                NetworkRequirement::LocalOnly,
125                ProviderStability::TestOnly,
126            ),
127        });
128        let err = match ProviderRegistry::builder()
129            .register_stt(f1)
130            .unwrap()
131            .register_stt(f2)
132        {
133            Ok(_) => panic!("expected duplicate registration error"),
134            Err(e) => e,
135        };
136        let msg = err.to_string();
137        assert!(msg.contains("duplicate"), "{msg}");
138    }
139
140    #[test]
141    fn builtin_registers_local_and_openrouter_stt() {
142        let reg = ProviderRegistry::builtin().unwrap();
143        let local = ProviderId::local();
144        let or = ProviderId::openrouter();
145        assert!(reg.stt_factory(&local).is_ok());
146        assert!(reg.stt_factory(&or).is_ok());
147        let caps = reg
148            .stt_factory(&local)
149            .unwrap()
150            .capabilities("tiny-q5_1")
151            .unwrap();
152        assert_eq!(caps.provider, "local");
153        assert!(!caps.requires_network || caps.local_only_ok);
154
155        assert!(reg.stt_factory(&ProviderId::must("openai")).is_ok());
156        assert!(reg.stt_factory(&ProviderId::must("elevenlabs")).is_err());
157    }
158
159    #[test]
160    #[cfg(feature = "tts")]
161    fn builtin_registers_local_and_openrouter_tts() {
162        let reg = ProviderRegistry::builtin().unwrap();
163        let local = ProviderId::local();
164        assert!(reg.tts_factory(&local).is_ok());
165        // OpenRouter remote TTS (JOE-1939) + first-party OpenAI (JOE-1940).
166        assert!(reg.tts_factory(&ProviderId::openrouter()).is_ok());
167        assert!(reg.tts_factory(&ProviderId::must("openai")).is_ok());
168        assert!(reg.stt_factory(&ProviderId::must("openai")).is_ok());
169    }
170
171    #[test]
172    fn descriptors_are_deterministic() {
173        let a = ProviderRegistry::builtin().unwrap();
174        let b = ProviderRegistry::builtin().unwrap();
175        let da: Vec<_> = a.descriptors().iter().map(|d| d.id.as_str()).collect();
176        let db: Vec<_> = b.descriptors().iter().map(|d| d.id.as_str()).collect();
177        assert_eq!(da, db);
178        assert!(da.contains(&"local"));
179        assert!(da.contains(&"openrouter"));
180    }
181
182    #[test]
183    fn openrouter_factory_rejects_local_only() {
184        let reg = ProviderRegistry::builtin().unwrap();
185        let f = reg.stt_factory(&ProviderId::openrouter()).unwrap();
186        let ctx = ProviderBuildContext::new("/tmp/aurum-reg-test")
187            .with_local_only(true)
188            .with_api_key(Some(SecretString::new("sk-test")));
189        let err = match f.build(&ctx) {
190            Ok(_) => panic!("expected local_only rejection"),
191            Err(e) => e,
192        };
193        let msg = err.to_string();
194        assert!(
195            msg.contains("local_only") || msg.contains("remote"),
196            "{msg}"
197        );
198    }
199
200    #[test]
201    fn local_stt_builds_without_secret() {
202        let reg = ProviderRegistry::builtin().unwrap();
203        let f = reg.stt_factory(&ProviderId::local()).unwrap();
204        let ctx = ProviderBuildContext::new(std::env::temp_dir().join("aurum-reg-local"));
205        let p = f.build(&ctx).unwrap();
206        assert_eq!(p.name(), "local");
207    }
208
209    #[test]
210    fn secret_scoping_does_not_leak_via_debug() {
211        let ctx = ProviderBuildContext::new("/tmp/x")
212            .with_api_key(Some(SecretString::new("sk-must-not-appear-in-debug")));
213        assert!(!format!("{ctx:?}").contains("sk-must-not-appear"));
214    }
215
216    #[test]
217    fn capabilities_for_routes_through_registry() {
218        let reg = ProviderRegistry::builtin().unwrap();
219        let caps =
220            capabilities_for(&reg, &ProviderId::local(), CapabilityOperation::Stt, "base").unwrap();
221        assert_eq!(caps.provider, "local");
222        assert_eq!(
223            caps.schema_version,
224            crate::capabilities::CAPABILITY_SCHEMA_VERSION
225        );
226    }
227}