1use std::collections::{BTreeMap, BTreeSet};
9use std::sync::Arc;
10
11use af_llm::{CompletionRequest, CompletionResponse, LlmClient, LlmError};
12use async_trait::async_trait;
13use tokio::sync::mpsc::UnboundedSender;
14
15#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub struct ModelDescriptor {
18 pub id: String,
20 pub provider: String,
22 #[serde(default)]
24 pub reasoning_efforts: BTreeSet<String>,
25 #[serde(default)]
27 pub input_modalities: BTreeSet<String>,
28}
29
30#[derive(Debug, thiserror::Error, PartialEq, Eq)]
32pub enum ModelRegistryError {
33 #[error("model descriptor requires id and provider")]
35 InvalidDescriptor,
36 #[error("model is already registered: {0}")]
38 Duplicate(String),
39 #[error("model is not registered: {0}")]
41 Unknown(String),
42 #[error("preferred model is not allowed: {0}")]
44 NotAllowed(String),
45}
46
47#[derive(Default)]
49pub struct ModelRegistry {
50 models: BTreeMap<String, (ModelDescriptor, Arc<dyn ChatModel>)>,
51}
52
53impl ModelRegistry {
54 pub fn register(
56 &mut self,
57 descriptor: ModelDescriptor,
58 model: Arc<dyn ChatModel>,
59 ) -> Result<(), ModelRegistryError> {
60 if descriptor.id.trim().is_empty() || descriptor.provider.trim().is_empty() {
61 return Err(ModelRegistryError::InvalidDescriptor);
62 }
63 if self.models.contains_key(&descriptor.id) {
64 return Err(ModelRegistryError::Duplicate(descriptor.id));
65 }
66 self.models
67 .insert(descriptor.id.clone(), (descriptor, model));
68 Ok(())
69 }
70
71 pub fn descriptors(&self) -> impl Iterator<Item = &ModelDescriptor> {
73 self.models.values().map(|(descriptor, _)| descriptor)
74 }
75
76 pub fn resolve(
78 &self,
79 preferred: &str,
80 allowed: &BTreeSet<String>,
81 ) -> Result<Arc<dyn ChatModel>, ModelRegistryError> {
82 if !allowed.contains(preferred) {
83 return Err(ModelRegistryError::NotAllowed(preferred.into()));
84 }
85 self.models
86 .get(preferred)
87 .map(|(_, model)| Arc::clone(model))
88 .ok_or_else(|| ModelRegistryError::Unknown(preferred.into()))
89 }
90}
91
92#[async_trait]
94pub trait ChatModel: Send + Sync {
95 async fn complete_streaming(
98 &self,
99 request: &CompletionRequest,
100 delta_tx: UnboundedSender<(String, bool)>,
101 ) -> Result<CompletionResponse, LlmError>;
102}
103
104#[async_trait]
106impl ChatModel for LlmClient {
107 async fn complete_streaming(
108 &self,
109 request: &CompletionRequest,
110 delta_tx: UnboundedSender<(String, bool)>,
111 ) -> Result<CompletionResponse, LlmError> {
112 self.complete_stream_single_attempt(request, |content, has_tools| {
113 let _ = delta_tx.send((content.to_string(), has_tools));
114 })
115 .await
116 }
117}
118
119#[async_trait]
123impl ChatModel for Arc<dyn ChatModel> {
124 async fn complete_streaming(
125 &self,
126 request: &CompletionRequest,
127 delta_tx: UnboundedSender<(String, bool)>,
128 ) -> Result<CompletionResponse, LlmError> {
129 (**self).complete_streaming(request, delta_tx).await
130 }
131}
132
133#[cfg(test)]
134mod registry_tests {
135 use super::*;
136 use af_llm::{ChatMessage, Choice, CompletionResponse, LlmError};
137
138 struct Fixed;
139
140 #[async_trait]
141 impl ChatModel for Fixed {
142 async fn complete_streaming(
143 &self,
144 _: &CompletionRequest,
145 _: UnboundedSender<(String, bool)>,
146 ) -> Result<CompletionResponse, LlmError> {
147 Ok(CompletionResponse {
148 id: "fixed".into(),
149 choices: vec![Choice {
150 index: 0,
151 message: ChatMessage::assistant("ok"),
152 output_blocks: Vec::new(),
153 finish_reason: None,
154 }],
155 usage: None,
156 })
157 }
158 }
159
160 fn descriptor(id: &str) -> ModelDescriptor {
161 ModelDescriptor {
162 id: id.into(),
163 provider: "test".into(),
164 reasoning_efforts: BTreeSet::from(["low".into()]),
165 input_modalities: BTreeSet::from(["text".into()]),
166 }
167 }
168
169 #[test]
170 fn registry_is_exact_sorted_and_rejects_duplicates() {
171 let mut registry = ModelRegistry::default();
172 registry.register(descriptor("z"), Arc::new(Fixed)).unwrap();
173 registry.register(descriptor("a"), Arc::new(Fixed)).unwrap();
174 assert_eq!(
175 registry
176 .descriptors()
177 .map(|value| value.id.as_str())
178 .collect::<Vec<_>>(),
179 vec!["a", "z"]
180 );
181 assert_eq!(
182 registry
183 .register(descriptor("a"), Arc::new(Fixed))
184 .unwrap_err(),
185 ModelRegistryError::Duplicate("a".into())
186 );
187 assert!(registry.resolve("a", &BTreeSet::from(["a".into()])).is_ok());
188 assert!(matches!(
189 registry.resolve("z", &BTreeSet::from(["a".into()])),
190 Err(ModelRegistryError::NotAllowed(value)) if value == "z"
191 ));
192 }
193}