diff --git a/interface/web/src/Settings.jsx b/interface/web/src/Settings.jsx
index 935cc34..2ec4d48 100644
--- a/interface/web/src/Settings.jsx
+++ b/interface/web/src/Settings.jsx
@@ -3,6 +3,8 @@ import { API_BASE } from "./config";
const DEFAULTS = {
model: "",
+ auto_chat_model: "", // Auto-mode: model for chat intent ("" = built-in preference)
+ auto_code_model: "", // Auto-mode: model for code intent
think: false, // Qwen3-style reasoning; off = much faster chat/memory
temperature: 0.7,
num_ctx: 0, // context window in tokens; 0 = model default
@@ -30,8 +32,11 @@ export function Settings() {
const [selectedApps, setSelectedApps] = useState([]);
const [brandQueue, setBrandQueue] = useState([]);
const [isBranding, setIsBranding] = useState(false);
+ const [modelList, setModelList] = useState([]);
useEffect(() => {
+ fetch(`${API_BASE}/models`).then(r => r.ok ? r.json() : null)
+ .then(d => setModelList((d && d.models) || [])).catch(() => {});
fetch(`${API_BASE}/settings`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) setForm(f => ({ ...f, ...d })); })
@@ -165,6 +170,29 @@ export function Settings() {
Settings
+ {/* Auto model routing */}
+
+
Auto model routing
+
+ When no model is pinned (chat picker on "auto"), which model fires for each
+ detected intent. "Auto" = the built-in preference for your hardware.
+ {form.model && A model is currently pinned in chat, so routing is bypassed until you set it back to auto. }
+
+
+ {[["auto_chat_model", "Chat / general"], ["auto_code_model", "Coding questions"]].map(([key, label]) => (
+
+ {label}
+ update(key, e.target.value)}
+ style={{ width: "100%", padding: "0.6rem", background: "#222", color: "#eee", border: "1px solid #333", borderRadius: "8px", marginTop: "0.3rem" }}>
+ Auto (best installed)
+ {modelList.map(m => {m} )}
+ {form[key] && !modelList.includes(form[key]) && {form[key]} (not installed) }
+
+
+ ))}
+
+
+
{/* Generation */}
Generation
diff --git a/synapse/main.py b/synapse/main.py
index e761848..4e8577c 100644
--- a/synapse/main.py
+++ b/synapse/main.py
@@ -104,6 +104,11 @@ async def _auto_select_model(message: str = "") -> str:
if s.get("model"):
return s["model"]
intent = _detect_intent(message) if message else "chat"
+ # Auto-mode remap: a configured model for this intent fires first;
+ # otherwise fall back to the built-in preference list.
+ remap = s.get(f"auto_{intent}_model")
+ if remap:
+ return remap
return await get_ollama_manager().select_best_model(intent)
except Exception:
return DEFAULT_CHAT_MODEL
diff --git a/synapse/memory/store.py b/synapse/memory/store.py
index 668da32..0f06693 100644
--- a/synapse/memory/store.py
+++ b/synapse/memory/store.py
@@ -986,6 +986,10 @@ class PersistentMemoryStore:
# -----------------------------
_SETTINGS_DEFAULTS: Dict[str, Any] = {
"model": "",
+ # Auto-mode routing overrides (blank = built-in preference). When no model
+ # is pinned, a message's detected intent picks which of these fires first.
+ "auto_chat_model": "",
+ "auto_code_model": "",
# Qwen3-style reasoning. Off by default: the hidden block is pure
# latency for chat/memory. Turn on for hard multi-step problems.
"think": False,
diff --git a/synapse/ollama_manager.py b/synapse/ollama_manager.py
index 3c897bd..3b2e6a2 100644
--- a/synapse/ollama_manager.py
+++ b/synapse/ollama_manager.py
@@ -162,7 +162,7 @@ def _detect_gpu_backend() -> tuple[str, dict]:
# tail differs by task. Prefix-matched against installed model names.
_MODEL_PREFERENCE = {
"chat": ("qwen2.5:3b", "qwen2.5", "gemma3:1b", "gemma3", "phi3", "phi-3", "gemma2", "gemma"),
- "code": ("qwen2.5:3b", "qwen2.5", "gemma3:1b", "gemma3", "phi3", "phi-3", "codellama", "deepseek-coder", "codegemma"),
+ "code": ("qwen2.5-coder", "qwen3-coder", "deepseek-coder", "codellama", "codegemma", "qwen2.5:3b", "qwen2.5", "gemma3", "phi3", "phi-3"),
}
diff --git a/tests/test_smoke.py b/tests/test_smoke.py
index 3e70e03..dd1426c 100644
--- a/tests/test_smoke.py
+++ b/tests/test_smoke.py
@@ -43,6 +43,19 @@ def test_keep_alive_pins_the_model():
assert "keep_alive" not in mgr._apply_keep_alive({"model": "x"})
+def test_auto_model_remap(monkeypatch):
+ import asyncio
+ from synapse import main
+ cfg = {"model": "", "auto_chat_model": "chatX", "auto_code_model": "coderY"}
+ monkeypatch.setattr(main.store, "get_settings", lambda: cfg)
+ # code intent ("function") routes to the code remap; chat intent to the chat remap
+ assert asyncio.run(main._auto_select_model("write a function to sort a list")) == "coderY"
+ assert asyncio.run(main._auto_select_model("how are you today")) == "chatX"
+ # an explicit pin beats the remap
+ cfg["model"] = "pinnedZ"
+ assert asyncio.run(main._auto_select_model("debug this code")) == "pinnedZ"
+
+
def test_hardware_fit_logic():
from synapse import hardware
assert hardware._fit(2.5, 4.0, 16.0) == "gpu" # 2.5+1 <= 4 -> fits GPU