1pub mod codex;
8mod transport;
9pub mod vercel;
10
11pub use codex::{CodexProvider, CodexProviderConfig};
12pub use transport::VercelRoutingPolicy;
13pub use vercel::{VercelProvider, VercelProviderConfig};
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17use std::sync::{Arc, RwLock};
18
19use fx_core::Gateway;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use zeroize::Zeroize;
23
24pub const MODEL_ROUTE_SEPARATOR: char = '/';
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct Model {
28 pub provider_id: String,
29 pub id: String,
30 pub name: String,
31 pub context_window: u32,
32 pub max_output_tokens: u32,
33 pub reasoning: bool,
34 pub capabilities: ModelCapabilities,
35}
36
37impl Model {
38 pub fn route(&self) -> String {
39 format!("{}{MODEL_ROUTE_SEPARATOR}{}", self.provider_id, self.id)
40 }
41}
42
43#[derive(Clone, Debug, Default, Eq, PartialEq)]
44pub struct ModelCapabilities {
45 pub native_web_search: Option<NativeWebSearch>,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct NativeWebSearch {
50 pub provider_tool_id: String,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct AuthMethod {
55 pub id: String,
56 pub name: String,
57 pub description: String,
58}
59
60impl AuthMethod {
61 pub fn new(
62 id: impl Into<String>,
63 name: impl Into<String>,
64 description: impl Into<String>,
65 ) -> Self {
66 Self {
67 id: id.into(),
68 name: name.into(),
69 description: description.into(),
70 }
71 }
72}
73
74#[derive(Clone, Deserialize, Serialize)]
77#[serde(tag = "kind", rename_all = "snake_case")]
78pub enum Credential {
79 ApiKey {
80 secret: String,
81 #[serde(default)]
82 attributes: BTreeMap<String, String>,
83 },
84 OAuth {
85 access_token: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 refresh_token: Option<String>,
88 expires_at_ms: i64,
89 #[serde(default)]
90 attributes: BTreeMap<String, String>,
91 },
92}
93
94impl Drop for Credential {
95 fn drop(&mut self) {
96 match self {
97 Self::ApiKey { secret, attributes } => {
98 secret.zeroize();
99 for value in attributes.values_mut() {
100 value.zeroize();
101 }
102 }
103 Self::OAuth {
104 access_token,
105 refresh_token,
106 attributes,
107 ..
108 } => {
109 access_token.zeroize();
110 refresh_token.zeroize();
111 for value in attributes.values_mut() {
112 value.zeroize();
113 }
114 }
115 }
116 }
117}
118
119impl fmt::Debug for Credential {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::ApiKey { attributes, .. } => formatter
123 .debug_struct("ApiKeyCredential")
124 .field("secret", &"[redacted]")
125 .field("attributes", attributes)
126 .finish(),
127 Self::OAuth {
128 refresh_token,
129 expires_at_ms,
130 attributes,
131 ..
132 } => formatter
133 .debug_struct("OAuthCredential")
134 .field("access_token", &"[redacted]")
135 .field(
136 "refresh_token",
137 &refresh_token.as_ref().map(|_| "[redacted]"),
138 )
139 .field("expires_at_ms", expires_at_ms)
140 .field("attributes", attributes)
141 .finish(),
142 }
143 }
144}
145
146pub trait CredentialLease {
149 fn credential(&self) -> Option<&Credential>;
150 fn replace(&mut self, credential: Credential) -> Result<(), ProviderError>;
151 fn delete(&mut self) -> Result<(), ProviderError>;
152}
153
154pub trait CredentialStore: Send + Sync {
155 fn lock<'a>(
156 &'a self,
157 provider_id: &str,
158 ) -> Result<Box<dyn CredentialLease + 'a>, ProviderError>;
159}
160
161pub trait Provider: Send + Sync {
162 fn id(&self) -> &str;
163 fn name(&self) -> &str;
164 fn models(&self) -> Vec<Model>;
165 fn default_model(&self) -> &str;
166 fn auth_methods(&self) -> Vec<AuthMethod>;
167
168 fn authenticate(
171 &self,
172 method_id: &str,
173 credentials: &dyn CredentialStore,
174 ) -> Result<(), ProviderError>;
175
176 fn refresh_models(
181 &self,
182 _credentials: &dyn CredentialStore,
183 ) -> Result<Option<Vec<Model>>, ProviderError> {
184 Ok(None)
185 }
186
187 fn logout(&self, credentials: &dyn CredentialStore) -> Result<(), ProviderError> {
190 let mut lease = credentials.lock(self.id())?;
191 lease.delete()
192 }
193
194 fn gateway(
197 &self,
198 model_id: &str,
199 session_id: Option<&str>,
200 credentials: &dyn CredentialStore,
201 ) -> Result<Arc<dyn Gateway>, ProviderError>;
202}
203
204#[derive(Debug, Error)]
205pub enum ProviderError {
206 #[error("provider `{0}` is not registered")]
207 UnknownProvider(String),
208 #[error("model `{0}` is not registered")]
209 UnknownModel(String),
210 #[error("authentication method `{0}` is not registered")]
211 UnknownAuthMethod(String),
212 #[error("provider `{0}` is already registered")]
213 DuplicateProvider(String),
214 #[error("model route `{0}` is already registered")]
215 DuplicateModel(String),
216 #[error("authentication method `{0}` is already registered")]
217 DuplicateAuthMethod(String),
218 #[error("authentication is required for {provider}: {message}")]
219 AuthenticationRequired { provider: String, message: String },
220 #[error("provider authentication failed: {0}")]
221 Authentication(String),
222 #[error("provider credential store failed: {0}")]
223 CredentialStore(String),
224 #[error("provider configuration is invalid: {0}")]
225 Configuration(String),
226 #[error("provider transport could not be created: {0}")]
227 Transport(String),
228}
229
230#[derive(Clone, Debug, Default, Eq, PartialEq)]
231pub struct AuthenticationOutcome {
232 pub models_refreshed: bool,
233 pub catalog_warning: Option<String>,
237}
238
239#[derive(Default)]
240pub struct ProviderRegistry {
241 providers: BTreeMap<String, Arc<dyn Provider>>,
242 models: Arc<RwLock<BTreeMap<String, Model>>>,
243 auth_methods: BTreeMap<String, RegisteredAuthMethod>,
244 default_model_route: Option<String>,
245}
246
247impl Clone for ProviderRegistry {
248 fn clone(&self) -> Self {
249 Self {
250 providers: self.providers.clone(),
251 models: Arc::new(RwLock::new(read_models(&self.models).clone())),
252 auth_methods: self.auth_methods.clone(),
253 default_model_route: self.default_model_route.clone(),
254 }
255 }
256}
257
258#[derive(Clone)]
259struct RegisteredAuthMethod {
260 provider_id: String,
261 local_id: String,
262 descriptor: AuthMethod,
263}
264
265impl ProviderRegistry {
266 pub fn new() -> Self {
267 Self::default()
268 }
269
270 pub fn register(&mut self, provider: Arc<dyn Provider>) -> Result<(), ProviderError> {
271 validate_component("provider id", provider.id())?;
272 if self.providers.contains_key(provider.id()) {
273 return Err(ProviderError::DuplicateProvider(provider.id().into()));
274 }
275
276 let mut models = Vec::new();
277 let mut seen_models = BTreeSet::new();
278 for model in provider.models() {
279 validate_model_id(&model.id)?;
280 if model.provider_id != provider.id() {
281 return Err(ProviderError::Configuration(format!(
282 "model `{}` declares provider `{}` instead of `{}`",
283 model.id,
284 model.provider_id,
285 provider.id()
286 )));
287 }
288 let route = model.route();
289 if !seen_models.insert(route.clone()) {
290 return Err(ProviderError::DuplicateModel(route));
291 }
292 models.push((route, model));
293 }
294 if models.is_empty() {
295 return Err(ProviderError::Configuration(format!(
296 "provider `{}` has no models",
297 provider.id()
298 )));
299 }
300 let default_route = format!("{}/{}", provider.id(), provider.default_model());
301 if !models.iter().any(|(route, _)| route == &default_route) {
302 return Err(ProviderError::Configuration(format!(
303 "provider `{}` default model `{}` is not in its catalog",
304 provider.id(),
305 provider.default_model()
306 )));
307 }
308
309 let mut methods = Vec::new();
310 let mut seen_methods = BTreeSet::new();
311 for method in provider.auth_methods() {
312 validate_component("authentication method id", &method.id)?;
313 let global_id = auth_route(provider.id(), &method.id);
314 if self.auth_methods.contains_key(&global_id) || !seen_methods.insert(global_id.clone())
315 {
316 return Err(ProviderError::DuplicateAuthMethod(global_id));
317 }
318 let mut descriptor = method.clone();
319 descriptor.id = global_id.clone();
320 methods.push((
321 global_id,
322 RegisteredAuthMethod {
323 provider_id: provider.id().into(),
324 local_id: method.id,
325 descriptor,
326 },
327 ));
328 }
329
330 {
331 let mut registered = write_models(&self.models);
332 if let Some((route, _)) = models
333 .iter()
334 .find(|(route, _)| registered.contains_key(route))
335 {
336 return Err(ProviderError::DuplicateModel(route.clone()));
337 }
338 registered.extend(models);
339 }
340 self.auth_methods.extend(methods);
341 if self.default_model_route.is_none() {
342 self.default_model_route = Some(default_route);
343 }
344 self.providers.insert(provider.id().into(), provider);
345 Ok(())
346 }
347
348 pub fn models(&self) -> Vec<Model> {
349 read_models(&self.models).values().cloned().collect()
350 }
351
352 pub fn model(&self, route: &str) -> Result<Model, ProviderError> {
353 read_models(&self.models)
354 .get(route)
355 .cloned()
356 .ok_or_else(|| ProviderError::UnknownModel(route.into()))
357 }
358
359 pub fn default_model(&self) -> Result<Model, ProviderError> {
360 let route = self
361 .default_model_route
362 .as_deref()
363 .ok_or_else(|| ProviderError::Configuration("provider registry is empty".into()))?;
364 self.model(route)
365 }
366
367 pub fn auth_methods(&self) -> Vec<AuthMethod> {
368 self.auth_methods
369 .values()
370 .map(|method| method.descriptor.clone())
371 .collect()
372 }
373
374 pub fn authenticate(
375 &self,
376 method_id: &str,
377 credentials: &dyn CredentialStore,
378 ) -> Result<AuthenticationOutcome, ProviderError> {
379 let method = self
380 .auth_methods
381 .get(method_id)
382 .ok_or_else(|| ProviderError::UnknownAuthMethod(method_id.into()))?;
383 let provider = &self.providers[&method.provider_id];
384 provider.authenticate(&method.local_id, credentials)?;
385
386 Ok(self.refresh_provider_catalog(&method.provider_id, credentials))
387 }
388
389 pub fn refresh_models(&self, credentials: &dyn CredentialStore) -> AuthenticationOutcome {
393 let mut outcome = AuthenticationOutcome::default();
394 let mut warnings = Vec::new();
395 for provider_id in self.providers.keys() {
396 let refreshed = self.refresh_provider_catalog(provider_id, credentials);
397 outcome.models_refreshed |= refreshed.models_refreshed;
398 if let Some(warning) = refreshed.catalog_warning {
399 warnings.push(format!("{provider_id}: {warning}"));
400 }
401 }
402 if !warnings.is_empty() {
403 outcome.catalog_warning = Some(warnings.join("; "));
404 }
405 outcome
406 }
407
408 fn refresh_provider_catalog(
409 &self,
410 provider_id: &str,
411 credentials: &dyn CredentialStore,
412 ) -> AuthenticationOutcome {
413 let provider = &self.providers[provider_id];
414 match provider.refresh_models(credentials) {
415 Ok(Some(models)) => match self.replace_provider_models(provider_id, models) {
416 Ok(models_refreshed) => AuthenticationOutcome {
417 models_refreshed,
418 catalog_warning: None,
419 },
420 Err(error) => AuthenticationOutcome {
421 models_refreshed: false,
422 catalog_warning: Some(error.to_string()),
423 },
424 },
425 Ok(None) => AuthenticationOutcome::default(),
426 Err(error) => AuthenticationOutcome {
427 models_refreshed: false,
428 catalog_warning: Some(error.to_string()),
429 },
430 }
431 }
432
433 pub fn replace_provider_models(
436 &self,
437 provider_id: &str,
438 models: Vec<Model>,
439 ) -> Result<bool, ProviderError> {
440 let provider = self
441 .providers
442 .get(provider_id)
443 .ok_or_else(|| ProviderError::UnknownProvider(provider_id.into()))?;
444 let mut replacement = BTreeMap::new();
445 for model in models {
446 validate_model_id(&model.id)?;
447 if model.provider_id != provider_id {
448 return Err(ProviderError::Configuration(format!(
449 "model `{}` declares provider `{}` instead of `{provider_id}`",
450 model.id, model.provider_id
451 )));
452 }
453 let route = model.route();
454 if replacement.insert(route.clone(), model).is_some() {
455 return Err(ProviderError::DuplicateModel(route));
456 }
457 }
458 if replacement.is_empty() {
459 return Err(ProviderError::Configuration(format!(
460 "provider `{provider_id}` has no models"
461 )));
462 }
463 let default_route = format!("{provider_id}/{}", provider.default_model());
464 if !replacement.contains_key(&default_route) {
465 return Err(ProviderError::Configuration(format!(
466 "provider `{provider_id}` default model `{}` is not in its refreshed catalog",
467 provider.default_model()
468 )));
469 }
470
471 let mut registered = write_models(&self.models);
472 for route in replacement.keys() {
473 if registered
474 .get(route)
475 .is_some_and(|model| model.provider_id != provider_id)
476 {
477 return Err(ProviderError::DuplicateModel(route.clone()));
478 }
479 }
480 let mut updated = registered
481 .iter()
482 .filter(|(_, model)| model.provider_id != provider_id)
483 .map(|(route, model)| (route.clone(), model.clone()))
484 .collect::<BTreeMap<_, _>>();
485 updated.extend(replacement);
486 if *registered == updated {
487 return Ok(false);
488 }
489 *registered = updated;
490 Ok(true)
491 }
492
493 pub fn logout_all(&self, credentials: &dyn CredentialStore) -> Result<(), ProviderError> {
494 let mut failures = Vec::new();
495 for provider in self.providers.values() {
496 if let Err(error) = provider.logout(credentials) {
497 failures.push(format!("{}: {error}", provider.id()));
498 }
499 }
500 if failures.is_empty() {
501 Ok(())
502 } else {
503 Err(ProviderError::CredentialStore(failures.join("; ")))
504 }
505 }
506
507 pub fn gateway(
508 &self,
509 route: &str,
510 session_id: Option<&str>,
511 credentials: &dyn CredentialStore,
512 ) -> Result<Arc<dyn Gateway>, ProviderError> {
513 let model = self.model(route)?;
514 self.providers[&model.provider_id].gateway(&model.id, session_id, credentials)
515 }
516}
517
518impl fmt::Debug for ProviderRegistry {
519 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
520 formatter
521 .debug_struct("ProviderRegistry")
522 .field("providers", &self.providers.keys().collect::<Vec<_>>())
523 .field(
524 "models",
525 &read_models(&self.models)
526 .keys()
527 .cloned()
528 .collect::<Vec<_>>(),
529 )
530 .field(
531 "auth_methods",
532 &self.auth_methods.keys().collect::<Vec<_>>(),
533 )
534 .finish()
535 }
536}
537
538fn read_models(
539 models: &RwLock<BTreeMap<String, Model>>,
540) -> std::sync::RwLockReadGuard<'_, BTreeMap<String, Model>> {
541 models
542 .read()
543 .unwrap_or_else(std::sync::PoisonError::into_inner)
544}
545
546fn write_models(
547 models: &RwLock<BTreeMap<String, Model>>,
548) -> std::sync::RwLockWriteGuard<'_, BTreeMap<String, Model>> {
549 models
550 .write()
551 .unwrap_or_else(std::sync::PoisonError::into_inner)
552}
553
554fn auth_route(provider_id: &str, method_id: &str) -> String {
555 format!("{provider_id}:{method_id}")
556}
557
558fn validate_component(label: &str, value: &str) -> Result<(), ProviderError> {
559 let valid = !value.is_empty()
560 && value.len() <= 128
561 && !value.contains(MODEL_ROUTE_SEPARATOR)
562 && value
563 .bytes()
564 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'));
565 if valid {
566 Ok(())
567 } else {
568 Err(ProviderError::Configuration(format!(
569 "{label} `{value}` is not a safe identifier"
570 )))
571 }
572}
573
574fn validate_model_id(value: &str) -> Result<(), ProviderError> {
575 let valid = !value.is_empty()
576 && value.len() <= 256
577 && value
578 .split(MODEL_ROUTE_SEPARATOR)
579 .all(|component| validate_component("model id component", component).is_ok());
580 if valid {
581 Ok(())
582 } else {
583 Err(ProviderError::Configuration(format!(
584 "model id `{value}` is not a safe slash-separated identifier"
585 )))
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use std::sync::Mutex;
592
593 use super::*;
594 use fx_core::{BoxFuture, GatewayError, GatewayEventSink, GatewayRequest, GatewayResponse};
595
596 #[derive(Default)]
597 struct MemoryStore(Mutex<Option<Credential>>);
598
599 struct MemoryLease<'a>(std::sync::MutexGuard<'a, Option<Credential>>);
600
601 impl CredentialLease for MemoryLease<'_> {
602 fn credential(&self) -> Option<&Credential> {
603 self.0.as_ref()
604 }
605
606 fn replace(&mut self, credential: Credential) -> Result<(), ProviderError> {
607 *self.0 = Some(credential);
608 Ok(())
609 }
610
611 fn delete(&mut self) -> Result<(), ProviderError> {
612 *self.0 = None;
613 Ok(())
614 }
615 }
616
617 impl CredentialStore for MemoryStore {
618 fn lock<'a>(
619 &'a self,
620 _provider_id: &str,
621 ) -> Result<Box<dyn CredentialLease + 'a>, ProviderError> {
622 Ok(Box::new(MemoryLease(self.0.lock().unwrap())))
623 }
624 }
625
626 struct EmptyGateway;
627
628 impl Gateway for EmptyGateway {
629 fn complete<'a>(
630 &'a self,
631 _request: GatewayRequest,
632 _events: &'a mut dyn GatewayEventSink,
633 ) -> BoxFuture<'a, Result<GatewayResponse, GatewayError>> {
634 Box::pin(async { Ok(GatewayResponse::default()) })
635 }
636 }
637
638 struct TestProvider(&'static str);
639
640 impl Provider for TestProvider {
641 fn id(&self) -> &str {
642 self.0
643 }
644
645 fn name(&self) -> &str {
646 self.0
647 }
648
649 fn models(&self) -> Vec<Model> {
650 vec![Model {
651 provider_id: self.0.into(),
652 id: "model".into(),
653 name: "Model".into(),
654 context_window: 1,
655 max_output_tokens: 1,
656 reasoning: false,
657 capabilities: ModelCapabilities::default(),
658 }]
659 }
660
661 fn default_model(&self) -> &str {
662 "model"
663 }
664
665 fn auth_methods(&self) -> Vec<AuthMethod> {
666 vec![AuthMethod::new("login", "Login", "Login")]
667 }
668
669 fn authenticate(
670 &self,
671 _method_id: &str,
672 _credentials: &dyn CredentialStore,
673 ) -> Result<(), ProviderError> {
674 Ok(())
675 }
676
677 fn gateway(
678 &self,
679 _model_id: &str,
680 _session_id: Option<&str>,
681 _credentials: &dyn CredentialStore,
682 ) -> Result<Arc<dyn Gateway>, ProviderError> {
683 Ok(Arc::new(EmptyGateway))
684 }
685 }
686
687 struct RefreshingProvider;
688
689 impl Provider for RefreshingProvider {
690 fn id(&self) -> &str {
691 "dynamic"
692 }
693
694 fn name(&self) -> &str {
695 "Dynamic"
696 }
697
698 fn models(&self) -> Vec<Model> {
699 vec![test_model("dynamic", "model")]
700 }
701
702 fn default_model(&self) -> &str {
703 "model"
704 }
705
706 fn auth_methods(&self) -> Vec<AuthMethod> {
707 vec![AuthMethod::new("login", "Login", "Login")]
708 }
709
710 fn authenticate(
711 &self,
712 _method_id: &str,
713 _credentials: &dyn CredentialStore,
714 ) -> Result<(), ProviderError> {
715 Ok(())
716 }
717
718 fn refresh_models(
719 &self,
720 _credentials: &dyn CredentialStore,
721 ) -> Result<Option<Vec<Model>>, ProviderError> {
722 Ok(Some(vec![
723 test_model("dynamic", "model"),
724 test_model("dynamic", "new/model"),
725 ]))
726 }
727
728 fn gateway(
729 &self,
730 _model_id: &str,
731 _session_id: Option<&str>,
732 _credentials: &dyn CredentialStore,
733 ) -> Result<Arc<dyn Gateway>, ProviderError> {
734 Ok(Arc::new(EmptyGateway))
735 }
736 }
737
738 fn test_model(provider_id: &str, id: &str) -> Model {
739 Model {
740 provider_id: provider_id.into(),
741 id: id.into(),
742 name: id.into(),
743 context_window: 1,
744 max_output_tokens: 1,
745 reasoning: false,
746 capabilities: ModelCapabilities::default(),
747 }
748 }
749
750 #[test]
751 fn registry_routes_multiple_providers_without_global_state() {
752 let mut registry = ProviderRegistry::new();
753 registry.register(Arc::new(TestProvider("alpha"))).unwrap();
754 registry.register(Arc::new(TestProvider("beta"))).unwrap();
755 assert_eq!(
756 registry
757 .models()
758 .iter()
759 .map(Model::route)
760 .collect::<Vec<_>>(),
761 ["alpha/model", "beta/model"]
762 );
763 assert_eq!(
764 registry
765 .auth_methods()
766 .iter()
767 .map(|method| method.id.as_str())
768 .collect::<Vec<_>>(),
769 ["alpha:login", "beta:login"]
770 );
771 assert!(
772 registry
773 .gateway("beta/model", None, &MemoryStore::default())
774 .is_ok()
775 );
776 }
777
778 #[test]
779 fn registration_is_transactional() {
780 let mut registry = ProviderRegistry::new();
781 registry.register(Arc::new(TestProvider("alpha"))).unwrap();
782 assert!(registry.register(Arc::new(TestProvider("alpha"))).is_err());
783 assert_eq!(registry.models().len(), 1);
784 }
785
786 #[test]
787 fn authentication_atomically_refreshes_only_its_provider_models() {
788 let mut registry = ProviderRegistry::new();
789 registry.register(Arc::new(TestProvider("stable"))).unwrap();
790 registry.register(Arc::new(RefreshingProvider)).unwrap();
791
792 let outcome = registry
793 .authenticate("dynamic:login", &MemoryStore::default())
794 .unwrap();
795 assert!(outcome.models_refreshed);
796 assert!(outcome.catalog_warning.is_none());
797 assert!(registry.model("dynamic/new/model").is_ok());
798 assert!(registry.model("stable/model").is_ok());
799
800 let before = registry.models();
801 assert!(
802 registry
803 .replace_provider_models("dynamic", vec![test_model("other", "model")])
804 .is_err()
805 );
806 assert_eq!(registry.models(), before);
807 }
808
809 struct NestedModelProvider;
810
811 impl Provider for NestedModelProvider {
812 fn id(&self) -> &str {
813 "vercel"
814 }
815
816 fn name(&self) -> &str {
817 "Vercel AI Gateway"
818 }
819
820 fn models(&self) -> Vec<Model> {
821 vec![Model {
822 provider_id: "vercel".into(),
823 id: "zai/glm-5.2".into(),
824 name: "GLM 5.2".into(),
825 context_window: 1,
826 max_output_tokens: 1,
827 reasoning: true,
828 capabilities: ModelCapabilities::default(),
829 }]
830 }
831
832 fn default_model(&self) -> &str {
833 "zai/glm-5.2"
834 }
835
836 fn auth_methods(&self) -> Vec<AuthMethod> {
837 Vec::new()
838 }
839
840 fn authenticate(
841 &self,
842 method_id: &str,
843 _credentials: &dyn CredentialStore,
844 ) -> Result<(), ProviderError> {
845 Err(ProviderError::UnknownAuthMethod(method_id.into()))
846 }
847
848 fn gateway(
849 &self,
850 _model_id: &str,
851 _session_id: Option<&str>,
852 _credentials: &dyn CredentialStore,
853 ) -> Result<Arc<dyn Gateway>, ProviderError> {
854 Ok(Arc::new(EmptyGateway))
855 }
856 }
857
858 #[test]
859 fn registry_accepts_provider_local_model_paths() {
860 let mut registry = ProviderRegistry::new();
861 registry.register(Arc::new(NestedModelProvider)).unwrap();
862 assert_eq!(
863 registry.default_model().unwrap().route(),
864 "vercel/zai/glm-5.2"
865 );
866 }
867}