Skip to main content

cuttlefish_host/
backend.rs

1//! Resolving a spec's model reference into something that can generate.
2//!
3//! Inference will come from several places over this project's life: a local
4//! Ollama, an OpenAI-compatible HTTP endpoint (which covers llama.cpp's own
5//! server, vLLM, LM Studio, and most hosted providers), an embedded llama.cpp,
6//! and others. [`InferBackend`] is the interface they share; this module is how
7//! a spec picks one *without* anything in the call chain knowing the list.
8//!
9//! # Why a registry rather than a match
10//!
11//! The obvious implementation is a `match` on the provider name in the daemon.
12//! It works, and it means every new backend edits the daemon, the parser, and an
13//! enum — three places that have nothing to do with the new backend, touched
14//! only because they enumerate. That is the shape that makes a fourth or fifth
15//! provider progressively less attractive to add.
16//!
17//! Instead a backend supplies a [`BackendFactory`], registers it under a
18//! provider name, and nothing else changes. The spec parser already accepts any
19//! `Provider "target"`; the runner only ever sees [`InferBackend`]. Adding one
20//! is genuinely additive.
21//!
22//! A backend that needs heavy or platform-specific dependencies — embedded
23//! llama.cpp being the obvious case — can live behind a cargo feature and
24//! register itself only when enabled. Nothing here has to change to allow that.
25//!
26//! ```
27//! use cuttlefish_core::spec::ModelRef;
28//! use cuttlefish_host::backend::Registry;
29//!
30//! let registry = Registry::with_builtins();
31//!
32//! let backend = registry.resolve(&ModelRef::new("stub", "anything")).unwrap_or_else(|e| {
33//!     panic!("stub is always registered: {e}")
34//! });
35//! assert_eq!(backend.model_name(), "stub");
36//!
37//! // An unknown provider explains what is available rather than panicking.
38//! let err = registry.resolve(&ModelRef::new("nope", "x")).err().unwrap();
39//! assert!(err.to_string().contains("stub"));
40//! ```
41
42use crate::infer::InferBackend;
43use cuttlefish_core::spec::ModelRef;
44use std::collections::BTreeMap;
45use std::sync::Arc;
46
47/// Builds one kind of [`InferBackend`] from a spec's model target.
48pub trait BackendFactory: Send + Sync {
49    /// The provider name this handles, lowercase — `ollama`, `stub`.
50    fn provider(&self) -> &'static str;
51
52    /// One line describing what this serves, shown when resolution fails.
53    fn describe(&self) -> &'static str;
54
55    /// Build a backend for `target`, whose meaning is this provider's own: a
56    /// model tag, a filesystem path, a URL.
57    ///
58    /// Returning an error here should mean the target is unusable — malformed,
59    /// or naming something that cannot exist. Whether the *service* is reachable
60    /// is deliberately not checked: that would make constructing a backend
61    /// fallible for reasons that change minute to minute, and the failure is
62    /// better reported when a job actually runs, where it lands in that job's
63    /// envelope instead of preventing the daemon from starting.
64    fn build(&self, target: &str) -> anyhow::Result<Arc<dyn InferBackend>>;
65}
66
67/// The providers this host knows how to serve.
68#[derive(Default)]
69pub struct Registry {
70    // BTreeMap rather than HashMap so that the "available providers" list in an
71    // error message comes out in a stable order — an error that reorders itself
72    // between runs is harder to recognise as the same error.
73    factories: BTreeMap<&'static str, Box<dyn BackendFactory>>,
74}
75
76impl Registry {
77    /// An empty registry, which resolves nothing.
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// A registry with every backend compiled into this build.
83    pub fn with_builtins() -> Self {
84        let mut registry = Self::new();
85        registry.register(Box::new(crate::infer::StubFactory));
86        registry.register(Box::new(crate::ollama::OllamaFactory));
87        // Present only when the `llamacpp` feature is on. A spec naming
88        // `llamacpp` in a build without it gets the normal unknown-provider
89        // error listing what *is* available, which is a far better outcome than
90        // a link failure or a silent fallback to something else.
91        #[cfg(feature = "llamacpp")]
92        registry.register(Box::new(crate::llamacpp::LlamaCppFactory));
93        registry
94    }
95
96    /// Add a factory, replacing any previous one for the same provider.
97    ///
98    /// Replacing rather than refusing is deliberate: it lets a test or an
99    /// embedder substitute a provider — pointing `ollama` at a mock, say —
100    /// without needing a separate injection path.
101    pub fn register(&mut self, factory: Box<dyn BackendFactory>) {
102        self.factories.insert(factory.provider(), factory);
103    }
104
105    /// Provider names this registry can resolve, in stable order.
106    pub fn providers(&self) -> Vec<&'static str> {
107        self.factories.keys().copied().collect()
108    }
109
110    /// Resolve a spec's model reference into a backend.
111    pub fn resolve(&self, model: &ModelRef) -> anyhow::Result<Arc<dyn InferBackend>> {
112        let factory = self.factories.get(model.provider.as_str()).ok_or_else(|| {
113            // Listing what *is* available turns "unknown provider" from a dead
114            // end into a correctable mistake — usually a typo or a feature that
115            // was not enabled at build time.
116            let available = self
117                .factories
118                .values()
119                .map(|f| format!("  {} — {}", f.provider(), f.describe()))
120                .collect::<Vec<_>>()
121                .join("\n");
122            anyhow::anyhow!(
123                "unknown model provider `{}`. Available providers:\n{available}",
124                model.provider
125            )
126        })?;
127
128        factory.build(&model.target).map_err(|e| {
129            anyhow::anyhow!(
130                "provider `{}` could not serve `{}`: {e}",
131                model.provider,
132                model.target
133            )
134        })
135    }
136}