mirror of
https://github.com/AlexsJones/llmfit.git
synced 2026-08-29 13:11:40 +00:00
chore: fmt
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
This commit is contained in:
parent
4d1542b582
commit
7f35522350
6 changed files with 111 additions and 36 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
llmfit
|
||||
/docs
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -31,6 +31,10 @@ Example of a medium performance home laptop
|
|||
Example of models with Mixture-of-Experts architectures
|
||||
|
||||

|
||||
|
||||
Downloading a model via Ollama integration
|
||||
|
||||

|
||||
---
|
||||
|
||||
## Install
|
||||
|
|
@ -88,6 +92,10 @@ Launches the interactive terminal UI. Your system specs (CPU, RAM, GPU name, VRA
|
|||
| `Esc` or `Enter` | Exit search mode |
|
||||
| `Ctrl-U` | Clear search |
|
||||
| `f` | Cycle fit filter: All, Runnable, Perfect, Good, Marginal |
|
||||
| `p` | Open provider filter popup |
|
||||
| `i` | Toggle installed-first sorting (Ollama only) |
|
||||
| `d` | Pull/download selected model via Ollama |
|
||||
| `r` | Refresh installed models from Ollama |
|
||||
| `1`-`9` | Toggle provider visibility |
|
||||
| `Enter` | Toggle detail view for selected model |
|
||||
| `PgUp` / `PgDn` | Scroll by 10 |
|
||||
|
|
@ -225,6 +233,7 @@ src/
|
|||
hardware.rs -- System RAM/CPU/GPU detection (multi-GPU, backend identification)
|
||||
models.rs -- Model database, quantization hierarchy, dynamic quant selection
|
||||
fit.rs -- Multi-dimensional scoring (Q/S/F/C), speed estimation, MoE offloading
|
||||
providers.rs -- Runtime provider integration (Ollama), model install detection, pull/download
|
||||
display.rs -- Classic CLI table rendering + JSON output
|
||||
tui_app.rs -- TUI application state, filters, navigation
|
||||
tui_ui.rs -- TUI rendering (ratatui)
|
||||
|
|
@ -288,11 +297,36 @@ cargo publish
|
|||
| `serde` / `serde_json` | JSON deserialization for model database |
|
||||
| `tabled` | CLI table formatting |
|
||||
| `colored` | CLI colored output |
|
||||
| `ureq` | HTTP client for Ollama API integration |
|
||||
| `ratatui` | Terminal UI framework |
|
||||
| `crossterm` | Terminal input/output backend for ratatui |
|
||||
|
||||
---
|
||||
|
||||
## Ollama integration
|
||||
|
||||
llmfit integrates with [Ollama](https://ollama.com) to detect which models you already have installed and to download new ones directly from the TUI.
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Ollama must be installed and running** (`ollama serve` or the Ollama desktop app)
|
||||
- llmfit connects to `http://localhost:11434` (Ollama's default API port)
|
||||
- No configuration needed — if Ollama is running, llmfit detects it automatically
|
||||
|
||||
### How it works
|
||||
|
||||
On startup, llmfit queries `GET /api/tags` to list your installed Ollama models. Each installed model gets a green **✓** in the **Inst** column of the TUI. The system bar shows `Ollama: ✓ (N installed)`.
|
||||
|
||||
When you press `d` on a model, llmfit sends `POST /api/pull` to Ollama to download it. The row highlights with an animated progress indicator showing download progress in real-time. Once complete, the model is immediately available for use with Ollama.
|
||||
|
||||
If Ollama is not running, the `d`, `i`, and `r` keybindings are hidden from the status bar and disabled — the TUI works normally without Ollama, you just can't see install status or pull models.
|
||||
|
||||
### Model name mapping
|
||||
|
||||
llmfit's database uses HuggingFace model names (e.g. `Qwen/Qwen2.5-Coder-14B-Instruct`) while Ollama uses its own naming scheme (e.g. `qwen2.5-coder:14b`). llmfit maintains an accurate mapping table between the two so that install detection and pulls resolve to the correct model. Each mapping is exact — `qwen2.5-coder:14b` maps to the Coder model, not the base `qwen2.5:14b`.
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
- **Linux** -- Full support. GPU detection via `nvidia-smi` (NVIDIA), `rocm-smi` (AMD), and sysfs/`lspci` (Intel Arc).
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ pub struct ModelFit {
|
|||
pub estimated_tps: f64, // estimated tokens per second
|
||||
pub best_quant: String, // best quantization for this hardware
|
||||
pub use_case: UseCase, // inferred use case category
|
||||
pub installed: bool, // model found in a local runtime provider
|
||||
pub installed: bool, // model found in a local runtime provider
|
||||
}
|
||||
|
||||
impl ModelFit {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ pub struct PullHandle {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PullEvent {
|
||||
Progress { status: String, percent: Option<f64> },
|
||||
Progress {
|
||||
status: String,
|
||||
percent: Option<f64>,
|
||||
},
|
||||
Done,
|
||||
Error(String),
|
||||
}
|
||||
|
|
@ -193,11 +196,7 @@ pub fn hf_name_to_ollama_candidates(hf_name: &str) -> Vec<String> {
|
|||
let mut candidates = Vec::new();
|
||||
|
||||
// Take the part after the slash (repo name)
|
||||
let repo = hf_name
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(hf_name)
|
||||
.to_lowercase();
|
||||
let repo = hf_name.split('/').last().unwrap_or(hf_name).to_lowercase();
|
||||
|
||||
// Common provider-specific mappings from HF repo names → Ollama tags.
|
||||
// These are checked first since they're authoritative.
|
||||
|
|
@ -282,7 +281,10 @@ pub fn hf_name_to_ollama_candidates(hf_name: &str) -> Vec<String> {
|
|||
("granite-4.0-h-small", "granite4.0-h:small"),
|
||||
("zephyr-7b-beta", "zephyr:7b"),
|
||||
("c4ai-command-r-v01", "command-r"),
|
||||
("nous-hermes-2-mixtral-8x7b-dpo", "nous-hermes2-mixtral:8x7b"),
|
||||
(
|
||||
"nous-hermes-2-mixtral-8x7b-dpo",
|
||||
"nous-hermes2-mixtral:8x7b",
|
||||
),
|
||||
("nomic-embed-text-v1.5", "nomic-embed-text"),
|
||||
("bge-large-en-v1.5", "bge-large"),
|
||||
];
|
||||
|
|
@ -317,13 +319,10 @@ pub fn is_model_installed(hf_name: &str, installed: &HashSet<String>) -> bool {
|
|||
/// Given an HF model name, return the best Ollama tag to use for pulling.
|
||||
pub fn ollama_pull_tag(hf_name: &str) -> String {
|
||||
let candidates = hf_name_to_ollama_candidates(hf_name);
|
||||
candidates.into_iter().next().unwrap_or_else(|| {
|
||||
hf_name
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(hf_name)
|
||||
.to_lowercase()
|
||||
})
|
||||
candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| hf_name.split('/').last().unwrap_or(hf_name).to_lowercase())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -339,7 +338,10 @@ mod tests {
|
|||
installed.insert("qwen2.5-coder:14b".to_string());
|
||||
installed.insert("qwen2.5-coder".to_string());
|
||||
|
||||
assert!(is_model_installed("Qwen/Qwen2.5-Coder-14B-Instruct", &installed));
|
||||
assert!(is_model_installed(
|
||||
"Qwen/Qwen2.5-Coder-14B-Instruct",
|
||||
&installed
|
||||
));
|
||||
// Must NOT match the non-coder model
|
||||
assert!(!is_model_installed("Qwen/Qwen2.5-14B-Instruct", &installed));
|
||||
}
|
||||
|
|
@ -353,7 +355,10 @@ mod tests {
|
|||
installed.insert("qwen2.5".to_string());
|
||||
|
||||
assert!(is_model_installed("Qwen/Qwen2.5-14B-Instruct", &installed));
|
||||
assert!(!is_model_installed("Qwen/Qwen2.5-Coder-14B-Instruct", &installed));
|
||||
assert!(!is_model_installed(
|
||||
"Qwen/Qwen2.5-Coder-14B-Instruct",
|
||||
&installed
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -376,7 +381,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_deepseek_coder_mapping() {
|
||||
let candidates = hf_name_to_ollama_candidates("deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct");
|
||||
let candidates =
|
||||
hf_name_to_ollama_candidates("deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct");
|
||||
assert!(candidates.contains(&"deepseek-coder-v2:16b".to_string()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::fit::{FitLevel, ModelFit};
|
||||
use crate::hardware::SystemSpecs;
|
||||
use crate::models::ModelDatabase;
|
||||
use crate::providers::{self, OllamaProvider, ModelProvider, PullHandle, PullEvent};
|
||||
use crate::providers::{self, ModelProvider, OllamaProvider, PullEvent, PullHandle};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::mpsc;
|
||||
|
|
@ -341,7 +341,9 @@ impl App {
|
|||
if self.pull_active.is_some() {
|
||||
return; // already pulling
|
||||
}
|
||||
let Some(fit) = self.selected_fit() else { return };
|
||||
let Some(fit) = self.selected_fit() else {
|
||||
return;
|
||||
};
|
||||
if fit.installed {
|
||||
self.pull_status = Some("Already installed".to_string());
|
||||
return;
|
||||
|
|
@ -366,7 +368,9 @@ impl App {
|
|||
if self.pull_active.is_some() {
|
||||
self.tick_count = self.tick_count.wrapping_add(1);
|
||||
}
|
||||
let Some(handle) = &self.pull_active else { return };
|
||||
let Some(handle) = &self.pull_active else {
|
||||
return;
|
||||
};
|
||||
// Drain all available events
|
||||
loop {
|
||||
match handle.receiver.try_recv() {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,11 @@ fn draw_system_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
} else {
|
||||
"Ollama: ✗".to_string()
|
||||
};
|
||||
let ollama_color = if app.ollama_available { Color::Green } else { Color::DarkGray };
|
||||
let ollama_color = if app.ollama_available {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::DarkGray
|
||||
};
|
||||
|
||||
let text = Line::from(vec![
|
||||
Span::styled(" CPU: ", Style::default().fg(Color::DarkGray)),
|
||||
|
|
@ -265,8 +269,8 @@ fn pull_indicator(percent: Option<f64>, tick: u64) -> String {
|
|||
|
||||
fn draw_table(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||
let header_cells = [
|
||||
"", "Inst", "Model", "Provider", "Params", "Score", "tok/s", "Quant", "Mode", "Mem %", "Ctx",
|
||||
"Fit", "Use Case",
|
||||
"", "Inst", "Model", "Provider", "Params", "Score", "tok/s", "Quant", "Mode", "Mem %",
|
||||
"Ctx", "Fit", "Use Case",
|
||||
]
|
||||
.iter()
|
||||
.map(|h| {
|
||||
|
|
@ -479,7 +483,10 @@ fn draw_detail(frame: &mut Frame, app: &App, area: Rect) {
|
|||
if fit.installed {
|
||||
Span::styled("✓ Yes (Ollama)", Style::default().fg(Color::Green).bold())
|
||||
} else if app.ollama_available {
|
||||
Span::styled("✗ No (press d to pull)", Style::default().fg(Color::DarkGray))
|
||||
Span::styled(
|
||||
"✗ No (press d to pull)",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)
|
||||
} else {
|
||||
Span::styled("- Ollama not running", Style::default().fg(Color::DarkGray))
|
||||
},
|
||||
|
|
@ -814,9 +821,17 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
|
||||
let (keys, mode_text) = match app.input_mode {
|
||||
InputMode::Normal => {
|
||||
let detail_key = if app.show_detail { "Enter:table" } else { "Enter:detail" };
|
||||
let detail_key = if app.show_detail {
|
||||
"Enter:table"
|
||||
} else {
|
||||
"Enter:detail"
|
||||
};
|
||||
let ollama_keys = if app.ollama_available {
|
||||
let installed_key = if app.installed_first { "i:all" } else { "i:installed↑" };
|
||||
let installed_key = if app.installed_first {
|
||||
"i:all"
|
||||
} else {
|
||||
"i:installed↑"
|
||||
};
|
||||
format!(" {} d:pull r:refresh", installed_key)
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -824,13 +839,15 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
(
|
||||
format!(
|
||||
" ↑↓/jk:nav {} /:search f:fit{} p:providers q:quit",
|
||||
detail_key,
|
||||
ollama_keys,
|
||||
detail_key, ollama_keys,
|
||||
),
|
||||
"NORMAL",
|
||||
)
|
||||
}
|
||||
InputMode::Search => (" Type to search Esc:done Ctrl-U:clear".to_string(), "SEARCH"),
|
||||
InputMode::Search => (
|
||||
" Type to search Esc:done Ctrl-U:clear".to_string(),
|
||||
"SEARCH",
|
||||
),
|
||||
InputMode::ProviderPopup => (
|
||||
" ↑↓/jk:navigate Space:toggle a:all/none Esc:close".to_string(),
|
||||
"PROVIDERS",
|
||||
|
|
@ -840,7 +857,10 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
// Split into two lines: keys + progress
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Min(20), Constraint::Length(progress_text.len() as u16 + 2)])
|
||||
.constraints([
|
||||
Constraint::Min(20),
|
||||
Constraint::Length(progress_text.len() as u16 + 2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let status_line = Line::from(vec![
|
||||
|
|
@ -852,9 +872,16 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
]);
|
||||
frame.render_widget(Paragraph::new(status_line), chunks[0]);
|
||||
|
||||
let pull_color = if app.pull_active.is_some() { Color::Yellow } else { Color::Green };
|
||||
let pull_color = if app.pull_active.is_some() {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Green
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(Span::styled(progress_text, Style::default().fg(pull_color)))),
|
||||
Paragraph::new(Line::from(Span::styled(
|
||||
progress_text,
|
||||
Style::default().fg(pull_color),
|
||||
))),
|
||||
chunks[1],
|
||||
);
|
||||
return;
|
||||
|
|
@ -868,7 +895,11 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
"Enter:detail"
|
||||
};
|
||||
let ollama_keys = if app.ollama_available {
|
||||
let installed_key = if app.installed_first { "i:all" } else { "i:installed↑" };
|
||||
let installed_key = if app.installed_first {
|
||||
"i:all"
|
||||
} else {
|
||||
"i:installed↑"
|
||||
};
|
||||
format!(" {} d:pull r:refresh", installed_key)
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -876,8 +907,7 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
|
|||
(
|
||||
format!(
|
||||
" ↑↓/jk:nav {} /:search f:fit{} p:providers q:quit",
|
||||
detail_key,
|
||||
ollama_keys,
|
||||
detail_key, ollama_keys,
|
||||
),
|
||||
"NORMAL",
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue