Skip to main content

just_llm_client/client/
factory.rs

1use std::{collections::HashMap, sync::Arc};
2
3use crate::{error::BackendConstructError, provider::LlmBackend};
4
5/// Function pointer that builds a shared backend from raw inputs.
6type BackendBuilder = fn(
7    reqwest::ClientBuilder,
8    &str,
9    Option<&str>,
10) -> Result<Arc<dyn LlmBackend>, BackendConstructError>;
11
12/// Dispatch table from backend family to its constructor function.
13///
14/// A composable primitive: `family -> constructor`, with no held configuration and no caching.
15/// Each [`create`](Self::create) call builds a fresh shared backend; downstream that wants
16/// sharing caches the returned [`Arc`] or clones the [`crate::ChatClient`] built from it.
17///
18/// Constructors and family names come from the [`LlmBackend`] trait itself
19/// ([`LlmBackend::new`] / [`LlmBackend::family`]) — every backend type carries them. The common
20/// entry point is [`new`](Self::new), which pre-seeds every compiled-in built-in backend; use
21/// [`empty`](Self::empty) for full control over registration.
22pub struct BackendFactory {
23    builders: HashMap<&'static str, BackendBuilder>,
24}
25
26impl BackendFactory {
27    /// A factory pre-seeded with every compiled-in built-in backend.
28    ///
29    /// The common path: under default features both the DeepSeek and OpenAI-compatible backends
30    /// are registered automatically. With no backend features enabled this yields an empty factory
31    /// (equivalent to [`empty`](Self::empty)).
32    pub fn new() -> Self {
33        #[cfg(any(feature = "deepseek", feature = "openai-compat"))]
34        {
35            let mut factory = Self::empty();
36            #[cfg(feature = "deepseek")]
37            factory.register::<crate::provider::DeepSeekBackend>();
38            #[cfg(feature = "openai-compat")]
39            factory.register::<crate::provider::OpenAiCompatBackend>();
40            factory
41        }
42        #[cfg(not(any(feature = "deepseek", feature = "openai-compat")))]
43        {
44            Self::empty()
45        }
46    }
47
48    /// An empty factory; the caller registers backends explicitly via [`register`](Self::register).
49    pub fn empty() -> Self {
50        Self {
51            builders: HashMap::new(),
52        }
53    }
54
55    /// Register (or replace) a backend, keyed on its [`LlmBackend::family`].
56    ///
57    /// Captures [`LlmBackend::new`] as the constructor. Takes only a type parameter, so the call
58    /// requires turbofish: `factory.register::<DeepSeekBackend>()`. Registering a family that is
59    /// already registered replaces the previous constructor.
60    pub fn register<C: LlmBackend>(&mut self) -> &mut Self {
61        // Fully qualified: `family` exists on both `LlmBackend` (static) and `Identifiable`
62        // (instance); only the static one is callable without a receiver, but Rust still requires
63        // disambiguation here.
64        self.builders.insert(<C as LlmBackend>::family(), C::new);
65        self
66    }
67
68    /// Build a shared backend for `family` from raw inputs.
69    ///
70    /// Returns [`BackendConstructError::unknown_family`](crate::BackendConstructError::unknown_family)
71    /// when no constructor is registered for `family`.
72    pub fn create(
73        &self,
74        family: &str,
75        http: reqwest::ClientBuilder,
76        api_key: &str,
77        base_url: Option<&str>,
78    ) -> Result<Arc<dyn LlmBackend>, BackendConstructError> {
79        let build = self
80            .builders
81            .get(family)
82            .copied()
83            .ok_or_else(|| BackendConstructError::unknown_family(family.to_owned()))?;
84        build(http, api_key, base_url)
85    }
86
87    /// The family names of every registered constructor.
88    pub fn families(&self) -> impl Iterator<Item = &str> {
89        self.builders.keys().copied()
90    }
91}
92
93impl Default for BackendFactory {
94    fn default() -> Self {
95        Self::new()
96    }
97}