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]) => ( +
+ + +
+ ))} +
+
+ {/* 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