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}