diff --git a/Cargo.toml b/Cargo.toml
index 502df438..1e976e99 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
-version = "0.3.10"
+version = "0.3.11"
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/RightNow-AI/openfang"
diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html
index 76d3e2d1..079856c3 100644
--- a/crates/openfang-api/static/index_body.html
+++ b/crates/openfang-api/static/index_body.html
@@ -3067,7 +3067,7 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
- | Model | Provider | Tier | Context | Input Cost | Output Cost | Status |
+ | Model | Provider | Tier | Context | Input Cost | Output Cost | Status | |
@@ -3078,6 +3078,7 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
|
|
|
+ |
diff --git a/crates/openfang-api/static/js/pages/settings.js b/crates/openfang-api/static/js/pages/settings.js
index 4a5d2dff..8b14788e 100644
--- a/crates/openfang-api/static/js/pages/settings.js
+++ b/crates/openfang-api/static/js/pages/settings.js
@@ -258,6 +258,17 @@ function settingsPage() {
}
},
+ async deleteCustomModel(modelId) {
+ if (!confirm('Delete custom model "' + modelId + '"?')) return;
+ try {
+ await OpenFangAPI.del('/api/models/custom/' + encodeURIComponent(modelId));
+ OpenFangToast.success('Model deleted');
+ await this.loadModels();
+ } catch(e) {
+ OpenFangToast.error('Failed to delete: ' + (e.message || 'Unknown error'));
+ }
+ },
+
async loadConfigSchema() {
try {
var results = await Promise.all([
diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs
index 21a858d3..90599daf 100644
--- a/crates/openfang-cli/src/main.rs
+++ b/crates/openfang-cli/src/main.rs
@@ -1306,8 +1306,14 @@ fn detect_best_provider() -> (&'static str, &'static str, &'static str) {
ui::success("Detected Gemini (GOOGLE_API_KEY)");
return ("gemini", "GOOGLE_API_KEY", "gemini-2.5-flash");
}
+ // Check if Ollama is running locally (no API key needed)
+ if check_ollama_available() {
+ ui::success("Detected Ollama running locally (no API key needed)");
+ return ("ollama", "OLLAMA_API_KEY", "llama3.2");
+ }
ui::hint("No LLM provider API keys found");
ui::hint("Groq offers a free tier: https://console.groq.com");
+ ui::hint("Or install Ollama for local models: https://ollama.com");
("groq", "GROQ_API_KEY", "llama-3.3-70b-versatile")
}
@@ -1333,6 +1339,15 @@ fn provider_list() -> Vec<(&'static str, &'static str, &'static str, &'static st
]
}
+/// Quick probe to check if Ollama is running on localhost.
+fn check_ollama_available() -> bool {
+ std::net::TcpStream::connect_timeout(
+ &std::net::SocketAddr::from(([127, 0, 0, 1], 11434)),
+ std::time::Duration::from_millis(500),
+ )
+ .is_ok()
+}
+
/// Write config.toml if it doesn't already exist.
fn write_config_if_missing(
openfang_dir: &std::path::Path,
diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs
index 4d3c282b..405ef7dc 100644
--- a/crates/openfang-kernel/src/kernel.rs
+++ b/crates/openfang-kernel/src/kernel.rs
@@ -50,7 +50,8 @@ impl LlmDriver for StubDriver {
async fn complete(&self, _request: CompletionRequest) -> Result {
Err(LlmError::MissingApiKey(
"No LLM provider configured. Set an API key (e.g. GROQ_API_KEY) and restart, \
- or configure a provider via the dashboard."
+ configure a provider via the dashboard, \
+ or use Ollama for local models (no API key needed)."
.to_string(),
))
}