Skip to main content

fim_engine/
lib.rs

1//! `fim-engine` — embedded fill-in-the-middle code completion.
2//!
3//! A self-contained local code-completion engine: it downloads a small
4//! quantized [qwen2.5-coder] model on first use, caches it, and runs
5//! inference in-process via [candle] — no external daemon, no API key,
6//! no network after the one-time download.
7//!
8//! Shared by mnml + tmnl. Typical use:
9//!
10//! ```no_run
11//! use fim_engine::{FimEngine, ModelChoice};
12//!
13//! // Blocking — do this on a worker thread.
14//! let cache = fim_engine::default_cache_dir();
15//! let mut engine = FimEngine::load(&cache, ModelChoice::Qwen1_5B, &|p| {
16//!     eprintln!("{}: {}/{:?}", p.label, p.received, p.total);
17//! })?;
18//! let completion = engine.complete("fn add(a: i32, b: i32) -> i32 {\n    ", "\n}", 64)?;
19//! # Ok::<(), String>(())
20//! ```
21//!
22//! [qwen2.5-coder]: https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B
23//! [candle]: https://github.com/huggingface/candle
24
25mod download;
26mod infer;
27
28pub use download::{DownloadProgress, ModelChoice, ModelPaths, is_model_cached};
29
30use std::path::{Path, PathBuf};
31
32/// The canonical, host-agnostic model cache directory — every consumer
33/// (mnml, tmnl, …) should pass this to [`FimEngine::load`] so the
34/// ~1 GB download is shared, not duplicated per app.
35///
36/// `$XDG_CACHE_HOME/fim-engine` when set, else `~/.cache/fim-engine`,
37/// else `./.fim-engine-cache` as a last resort.
38pub fn default_cache_dir() -> PathBuf {
39    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME")
40        && !xdg.is_empty()
41    {
42        return PathBuf::from(xdg).join("fim-engine");
43    }
44    if let Ok(home) = std::env::var("HOME")
45        && !home.is_empty()
46    {
47        return PathBuf::from(home).join(".cache").join("fim-engine");
48    }
49    PathBuf::from(".fim-engine-cache")
50}
51
52/// A loaded local FIM completion engine. Holds the model in memory;
53/// keep one alive and call [`FimEngine::complete`] repeatedly.
54pub struct FimEngine {
55    model: infer::Model,
56}
57
58impl FimEngine {
59    /// Download (if needed) + load the `choice` model. `cache_dir` is
60    /// where the GGUF weights + tokenizer are cached (see
61    /// [`default_cache_dir`]). `progress` fires periodically while
62    /// files download.
63    ///
64    /// Blocking and slow on the first call (a ~1 GB download); fast
65    /// afterwards (just the load). Run it on a worker thread.
66    pub fn load(
67        cache_dir: &Path,
68        choice: ModelChoice,
69        progress: &(dyn Fn(DownloadProgress) + Sync),
70    ) -> Result<Self, String> {
71        let paths = download::ensure_model(cache_dir, choice, progress)?;
72        let model = infer::Model::load(&paths.gguf, &paths.tokenizer)?;
73        Ok(FimEngine { model })
74    }
75
76    /// Generate a completion for the cursor sitting between `prefix`
77    /// (code before) and `suffix` (code after). Returns the text to
78    /// insert — never includes the surrounding code. `max_tokens`
79    /// bounds the length (≈ 64 is a good inline default).
80    ///
81    /// Blocking + CPU-bound (~100–400 ms for the 1.5B model). Call on
82    /// a worker thread; never on the UI thread.
83    pub fn complete(
84        &mut self,
85        prefix: &str,
86        suffix: &str,
87        max_tokens: usize,
88    ) -> Result<String, String> {
89        self.model.complete(prefix, suffix, max_tokens)
90    }
91}