Skip to main content

synwire_core/language_models/
registry.rs

1//! Model profile registry for tracking model capabilities.
2
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6/// Information about a model's capabilities and metadata.
7///
8/// # Example
9///
10/// ```
11/// use synwire_core::language_models::registry::ModelProfile;
12///
13/// let profile = ModelProfile {
14///     model_id: "gpt-4o".into(),
15///     provider: "openai".into(),
16///     supports_tools: true,
17///     supports_streaming: true,
18///     supports_structured_output: true,
19///     max_context_tokens: Some(128_000),
20///     max_output_tokens: Some(16_384),
21/// };
22///
23/// assert!(profile.supports_tools);
24/// ```
25#[derive(Debug, Clone)]
26pub struct ModelProfile {
27    /// Model identifier (e.g., "gpt-4o", "claude-3-opus").
28    pub model_id: String,
29    /// Provider name (e.g., "openai", "anthropic").
30    pub provider: String,
31    /// Whether the model supports tool calling.
32    pub supports_tools: bool,
33    /// Whether the model supports streaming.
34    pub supports_streaming: bool,
35    /// Whether the model supports structured output.
36    pub supports_structured_output: bool,
37    /// Maximum context window size in tokens.
38    pub max_context_tokens: Option<u64>,
39    /// Maximum output tokens.
40    pub max_output_tokens: Option<u64>,
41}
42
43/// Well-known capability names for [`ModelProfileRegistry::supports`].
44///
45/// These constants match the string values accepted by the `supports` method.
46pub mod capabilities {
47    /// Tool calling capability.
48    pub const TOOLS: &str = "tools";
49    /// Streaming capability.
50    pub const STREAMING: &str = "streaming";
51    /// Structured output capability.
52    pub const STRUCTURED_OUTPUT: &str = "structured_output";
53}
54
55/// Registry for model profiles.
56///
57/// Provides capability look-ups by model identifier.
58pub trait ModelProfileRegistry: Send + Sync {
59    /// Register a model profile.
60    ///
61    /// If a profile with the same `model_id` already exists, it is replaced.
62    fn register(&self, profile: ModelProfile);
63
64    /// Get a model profile by model ID.
65    fn get(&self, model_id: &str) -> Option<ModelProfile>;
66
67    /// Check if a model supports a specific capability.
68    ///
69    /// Known capability strings: `"tools"`, `"streaming"`, `"structured_output"`.
70    /// Unknown capabilities return `false`.
71    fn supports(&self, model_id: &str, capability: &str) -> bool;
72}
73
74/// In-memory implementation of [`ModelProfileRegistry`].
75///
76/// Thread-safe via an internal `RwLock`. Cloning shares the same backing store.
77///
78/// # Example
79///
80/// ```
81/// use synwire_core::language_models::registry::{
82///     InMemoryModelProfileRegistry, ModelProfile, ModelProfileRegistry,
83/// };
84///
85/// let registry = InMemoryModelProfileRegistry::default();
86/// registry.register(ModelProfile {
87///     model_id: "gpt-4o".into(),
88///     provider: "openai".into(),
89///     supports_tools: true,
90///     supports_streaming: true,
91///     supports_structured_output: false,
92///     max_context_tokens: Some(128_000),
93///     max_output_tokens: Some(16_384),
94/// });
95///
96/// assert!(registry.supports("gpt-4o", "tools"));
97/// assert!(!registry.supports("gpt-4o", "structured_output"));
98/// assert!(registry.get("gpt-4o").is_some());
99/// ```
100#[derive(Debug, Clone, Default)]
101pub struct InMemoryModelProfileRegistry {
102    profiles: Arc<RwLock<HashMap<String, ModelProfile>>>,
103}
104
105impl InMemoryModelProfileRegistry {
106    /// Creates a new empty registry.
107    pub fn new() -> Self {
108        Self::default()
109    }
110}
111
112impl ModelProfileRegistry for InMemoryModelProfileRegistry {
113    fn register(&self, profile: ModelProfile) {
114        let Ok(mut map) = self.profiles.write() else {
115            return;
116        };
117        let _ = map.insert(profile.model_id.clone(), profile);
118    }
119
120    fn get(&self, model_id: &str) -> Option<ModelProfile> {
121        let map = self.profiles.read().ok()?;
122        map.get(model_id).cloned()
123    }
124
125    fn supports(&self, model_id: &str, capability: &str) -> bool {
126        let Some(profile) = self.get(model_id) else {
127            return false;
128        };
129        match capability {
130            capabilities::TOOLS => profile.supports_tools,
131            capabilities::STREAMING => profile.supports_streaming,
132            capabilities::STRUCTURED_OUTPUT => profile.supports_structured_output,
133            _ => false,
134        }
135    }
136}
137
138#[cfg(test)]
139#[allow(clippy::unwrap_used)]
140mod tests {
141    use super::*;
142
143    fn sample_profile() -> ModelProfile {
144        ModelProfile {
145            model_id: "gpt-4o".into(),
146            provider: "openai".into(),
147            supports_tools: true,
148            supports_streaming: true,
149            supports_structured_output: false,
150            max_context_tokens: Some(128_000),
151            max_output_tokens: Some(16_384),
152        }
153    }
154
155    #[test]
156    fn register_and_retrieve() {
157        let registry = InMemoryModelProfileRegistry::new();
158        registry.register(sample_profile());
159
160        let profile = registry.get("gpt-4o").unwrap();
161        assert_eq!(profile.model_id, "gpt-4o");
162        assert_eq!(profile.provider, "openai");
163        assert!(profile.supports_tools);
164        assert_eq!(profile.max_context_tokens, Some(128_000));
165    }
166
167    #[test]
168    fn get_returns_none_for_unknown_model() {
169        let registry = InMemoryModelProfileRegistry::new();
170        assert!(registry.get("nonexistent-model").is_none());
171    }
172
173    #[test]
174    fn supports_tools() {
175        let registry = InMemoryModelProfileRegistry::new();
176        registry.register(sample_profile());
177
178        assert!(registry.supports("gpt-4o", "tools"));
179    }
180
181    #[test]
182    fn supports_streaming() {
183        let registry = InMemoryModelProfileRegistry::new();
184        registry.register(sample_profile());
185
186        assert!(registry.supports("gpt-4o", "streaming"));
187    }
188
189    #[test]
190    fn supports_structured_output_false() {
191        let registry = InMemoryModelProfileRegistry::new();
192        registry.register(sample_profile());
193
194        assert!(!registry.supports("gpt-4o", "structured_output"));
195    }
196
197    #[test]
198    fn supports_unknown_capability() {
199        let registry = InMemoryModelProfileRegistry::new();
200        registry.register(sample_profile());
201
202        assert!(!registry.supports("gpt-4o", "vision"));
203    }
204
205    #[test]
206    fn supports_unknown_model() {
207        let registry = InMemoryModelProfileRegistry::new();
208        assert!(!registry.supports("nonexistent", "tools"));
209    }
210
211    #[test]
212    fn register_replaces_existing() {
213        let registry = InMemoryModelProfileRegistry::new();
214        registry.register(sample_profile());
215
216        let mut updated = sample_profile();
217        updated.supports_structured_output = true;
218        registry.register(updated);
219
220        assert!(registry.supports("gpt-4o", "structured_output"));
221    }
222
223    #[test]
224    fn clone_shares_state() {
225        let registry = InMemoryModelProfileRegistry::new();
226        let cloned = registry.clone();
227
228        registry.register(sample_profile());
229        assert!(cloned.get("gpt-4o").is_some());
230    }
231}