From fb3a6d2445f5ced4cd595fe7f15441adbb5961f9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 28 Feb 2026 16:06:02 -0500 Subject: [PATCH] Fix 'default' model selection breaking readiness indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user selected the 'Default' option in Settings, the dropdown sent an empty string "" to the backend. The route saved it as a DB row, which caused get_setting() to return "" instead of falling back to Config defaults. The chat status endpoint then tried to match "" against installed model names — always failing — resulting in model: "not_found" and a permanently failing readiness indicator. services/settings.py: - Add delete_setting() helper: removes a setting row so get_setting() correctly falls back to its hardcoded default argument routes/settings.py: - Import delete_setting - When default_model or intent_model are saved as empty string, delete the DB row instead of storing "" — cleanly restores Config fallback routes/chat.py: - chat_status_route: add explicit `or Config.OLLAMA_MODEL` guard for any existing "" rows written before this fix (migration safety net) - send_message and summarize routes: same guard on model resolution so empty settings never cause silent generation failures Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/routes/chat.py | 7 +++++-- src/fabledassistant/routes/settings.py | 17 +++++++++++++++-- src/fabledassistant/services/settings.py | 11 ++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/fabledassistant/routes/chat.py b/src/fabledassistant/routes/chat.py index 6cbdc19..dfe8660 100644 --- a/src/fabledassistant/routes/chat.py +++ b/src/fabledassistant/routes/chat.py @@ -135,7 +135,7 @@ async def send_message_route(conv_id: int): if msg.role != "system": history.append({"role": msg.role, "content": msg.content}) - model = conv.model or await get_setting(uid, "default_model", Config.OLLAMA_MODEL) + model = conv.model or await get_setting(uid, "default_model", Config.OLLAMA_MODEL) or Config.OLLAMA_MODEL # Launch background generation task (context building happens inside the task) asyncio.create_task(run_generation( @@ -257,7 +257,7 @@ async def summarize_conversation_route(conv_id: int): conv = await get_conversation(uid, conv_id) if conv is None: return jsonify({"error": "Conversation not found"}), 404 - model = conv.model or await get_setting(uid, "default_model", Config.OLLAMA_MODEL) + model = conv.model or await get_setting(uid, "default_model", Config.OLLAMA_MODEL) or Config.OLLAMA_MODEL try: note = await summarize_conversation_as_note(uid, conv_id, model) return jsonify(note), 201 @@ -319,6 +319,9 @@ async def chat_status_route(): """Check Ollama availability, model installation, and model load state.""" uid = get_current_user_id() default_model = await get_setting(uid, "default_model", Config.OLLAMA_MODEL) + # Guard against empty-string rows written by older code when user selected "default" + if not default_model: + default_model = Config.OLLAMA_MODEL result = { "ollama": "unavailable", "model": "not_found", diff --git a/src/fabledassistant/routes/settings.py b/src/fabledassistant/routes/settings.py index e555000..ebe5e22 100644 --- a/src/fabledassistant/routes/settings.py +++ b/src/fabledassistant/routes/settings.py @@ -4,7 +4,7 @@ from fabledassistant.auth import login_required, get_current_user_id from fabledassistant.config import Config from fabledassistant.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection from fabledassistant.services.llm import get_installed_models -from fabledassistant.services.settings import get_all_settings, set_settings_batch +from fabledassistant.services.settings import delete_setting, get_all_settings, set_settings_batch settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings") @@ -33,7 +33,20 @@ async def update_settings_route(): if installed and model not in installed: return jsonify({"error": f"Model '{model}' is not installed"}), 400 - await set_settings_batch(uid, {k: str(v) for k, v in data.items()}) + # Empty string for model keys means "reset to system default". + # Delete the DB row so get_setting() falls back to Config defaults + # rather than returning "" and breaking model resolution everywhere. + _MODEL_KEYS = frozenset({"default_model", "intent_model"}) + to_save = {} + for k, v in data.items(): + str_v = str(v) + if k in _MODEL_KEYS and not str_v: + await delete_setting(uid, k) + else: + to_save[k] = str_v + + if to_save: + await set_settings_batch(uid, to_save) settings = await get_all_settings(uid) return jsonify(settings) diff --git a/src/fabledassistant/services/settings.py b/src/fabledassistant/services/settings.py index 980aa73..aefcc7e 100644 --- a/src/fabledassistant/services/settings.py +++ b/src/fabledassistant/services/settings.py @@ -1,6 +1,6 @@ import logging -from sqlalchemy import select +from sqlalchemy import delete as sa_delete, select from fabledassistant.models import async_session from fabledassistant.models.setting import Setting @@ -46,6 +46,15 @@ async def set_settings_batch(user_id: int, settings: dict[str, str]) -> None: logger.info("Batch-updated %d settings for user %d", len(settings), user_id) +async def delete_setting(user_id: int, key: str) -> None: + """Remove a setting row so get_setting() returns its hardcoded default instead.""" + async with async_session() as session: + await session.execute( + sa_delete(Setting).where(Setting.user_id == user_id, Setting.key == key) + ) + await session.commit() + + async def get_all_settings(user_id: int) -> dict[str, str]: async with async_session() as session: result = await session.execute(