Skip to main content

systemprompt_api/services/gateway/
registry.rs

1//! Process-global registry of gateway upstream providers and adapters.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::collections::HashMap;
7use std::sync::{Arc, OnceLock};
8
9use super::protocol::outbound::anthropic::AnthropicOutbound;
10use super::protocol::outbound::gemini::GeminiOutbound;
11use super::protocol::outbound::openai_chat::OpenAiChatOutbound;
12use super::protocol::outbound::openai_responses::OpenAiResponsesOutbound;
13use super::protocol::outbound::{OutboundAdapter, OutboundAdapterRegistration};
14use systemprompt_ai::{NullScanner, SafetyScanner, SafetyScannerRegistration};
15use systemprompt_models::profile::WireProtocol;
16
17pub struct GatewayUpstreamRegistry {
18    entries: HashMap<String, Arc<dyn OutboundAdapter>>,
19}
20
21impl std::fmt::Debug for GatewayUpstreamRegistry {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("GatewayUpstreamRegistry")
24            .field("tags", &self.tags())
25            .finish()
26    }
27}
28
29impl GatewayUpstreamRegistry {
30    pub fn global() -> &'static Self {
31        static REGISTRY: OnceLock<GatewayUpstreamRegistry> = OnceLock::new();
32        REGISTRY.get_or_init(Self::build)
33    }
34
35    pub fn get(&self, tag: &str) -> Option<&Arc<dyn OutboundAdapter>> {
36        self.entries.get(tag)
37    }
38
39    pub fn tags(&self) -> Vec<&str> {
40        self.entries.keys().map(String::as_str).collect()
41    }
42
43    pub(super) fn build() -> Self {
44        let mut entries: HashMap<String, Arc<dyn OutboundAdapter>> = HashMap::new();
45
46        entries.insert(
47            WireProtocol::Anthropic.as_tag().to_owned(),
48            Arc::new(AnthropicOutbound),
49        );
50        entries.insert(
51            WireProtocol::OpenAiChat.as_tag().to_owned(),
52            Arc::new(OpenAiChatOutbound),
53        );
54        entries.insert(
55            WireProtocol::OpenAiResponses.as_tag().to_owned(),
56            Arc::new(OpenAiResponsesOutbound),
57        );
58        entries.insert(
59            WireProtocol::Gemini.as_tag().to_owned(),
60            Arc::new(GeminiOutbound),
61        );
62
63        for registration in inventory::iter::<OutboundAdapterRegistration> {
64            let tag = registration.tag.to_owned();
65            if entries.contains_key(&tag) {
66                tracing::warn!(
67                    tag = %registration.tag,
68                    "Extension-registered gateway upstream shadows a built-in"
69                );
70            }
71            entries.insert(tag, (registration.factory)());
72        }
73
74        Self { entries }
75    }
76}
77
78pub struct SafetyScannerRegistry {
79    entries: HashMap<String, Arc<dyn SafetyScanner>>,
80}
81
82impl std::fmt::Debug for SafetyScannerRegistry {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("SafetyScannerRegistry")
85            .field("names", &self.names())
86            .finish()
87    }
88}
89
90impl SafetyScannerRegistry {
91    pub fn global() -> &'static Self {
92        static REGISTRY: OnceLock<SafetyScannerRegistry> = OnceLock::new();
93        REGISTRY.get_or_init(Self::build)
94    }
95
96    pub fn get(&self, name: &str) -> Option<&Arc<dyn SafetyScanner>> {
97        self.entries.get(name)
98    }
99
100    pub fn names(&self) -> Vec<&str> {
101        self.entries.keys().map(String::as_str).collect()
102    }
103
104    // Why: the builtin `heuristic` scanner is NOT registered here — it is
105    // constructed per policy from `SafetyConfig.heuristic` at scan time, and
106    // only when no extension registration shadows the name.
107    pub(super) fn build() -> Self {
108        let mut entries: HashMap<String, Arc<dyn SafetyScanner>> = HashMap::new();
109        entries.insert("null".to_owned(), Arc::new(NullScanner));
110
111        for registration in inventory::iter::<SafetyScannerRegistration> {
112            let name = registration.name.to_owned();
113            if entries.contains_key(&name) || name == "heuristic" {
114                tracing::warn!(
115                    name = %registration.name,
116                    "Extension-registered safety scanner shadows a built-in"
117                );
118            }
119            entries.insert(name, (registration.factory)());
120        }
121
122        Self { entries }
123    }
124}