refactor(models): route tasks to chat vs worker per new architecture

Chat and background model roles effectively swapped during the
conversation+curator pivot, but call sites still used OLD routing.
This commit re-routes each call to the model whose new role fits.

Moved to background_model (worker — heavy, deliberate):
- services/journal_prep.py: daily prep generation.
- services/user_profile.py: observation consolidation.

Moved to default_model (chat — small, fast):
- services/chat.py save_response_as_note: note title generation.
- services/tag_suggestions.py: tag suggestions.

Already routed correctly (unchanged): curator, closeout, consolidation,
project summaries, history summarization.

SettingsView.vue: help text rewritten for both model fields to
describe new roles. Background Model UI label renamed to Worker
Model so the heavier role is visible from the picker. Warning copy
updated to recommend OLLAMA_MAX_LOADED_MODELS=2+ so chat and worker
can stay loaded simultaneously.

Schema names default_model and background_model unchanged on purpose
(renaming requires migration + touches ~50 call sites for UX-only gain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 11:00:47 -04:00
parent 48b99b62be
commit 85b212fbf2
5 changed files with 46 additions and 11 deletions
+10 -6
View File
@@ -1540,19 +1540,23 @@ function formatUserDate(iso: string): string {
<option value="">Default ({{ defaultChatModel || "qwen3:latest" }})</option>
<option v-for="m in installedModels" :key="m" :value="m">{{ m }}</option>
</select>
<p class="field-hint">Model used for new conversations.</p>
<p class="field-hint">
Used for journal chat (no tools just conversation) and lightweight one-shot tasks like note-title generation and tag suggestions.
Pick a small fast model (e.g. <code>qwen3:8b</code>, <code>llama3.2:3b</code>) speed matters more than depth here.
Ideally runs on GPU.
</p>
</div>
<div class="field">
<label for="background-model">Background Model</label>
<label for="background-model">Worker Model</label>
<select id="background-model" v-model="backgroundModel" class="input">
<option value="">Default (qwen2.5:0.5b)</option>
<option value="">Default (qwen3:latest)</option>
<option v-for="m in installedModels" :key="m" :value="m">{{ m }}</option>
</select>
<p class="field-hint">
Model used for background tasks: title generation, tag suggestions, and project summaries.
Using a small dedicated model (e.g. qwen2.5:0.5b) keeps the chat model's KV cache warm between messages, significantly reducing response time.
Used for heavy async work: the journal curator (capture / propose updates), daily prep generation, end-of-day closeout, task body consolidation, project summaries, and profile observation processing.
Pick a smart model latency doesn't matter, quality does. Often runs on CPU with system RAM (e.g. <code>qwen3:32b</code>, <code>qwen3:30b-a3b</code>).
<span v-if="backgroundModel && backgroundModel === (defaultModel || defaultChatModel)" class="field-hint-warn">
Setting this to the same model as Chat Model will wipe the KV cache after every background task, increasing response latency.
Using the same model for both means worker tasks compete for the chat model's resources. Pick different models so they can be loaded simultaneously (<code>OLLAMA_MAX_LOADED_MODELS&nbsp;=&nbsp;2</code>+).
</span>
</p>
</div>
+9 -2
View File
@@ -243,8 +243,15 @@ async def save_response_as_note(user_id: int, message_id: int) -> dict:
},
{"role": "user", "content": msg.content[:2000]},
]
bg_model = await get_setting(user_id, "background_model", Config.OLLAMA_BACKGROUND_MODEL)
title = await generate_completion(prompt_messages, bg_model)
# 3-8 word title generation is the kind of trivial small-input
# / small-output task the chat model (default_model) handles in
# ~1s. Routing here so the worker (background_model) isn't
# interrupted from heavier curator / prep / closeout passes.
chat_model = (
await get_setting(user_id, "default_model", "")
or Config.OLLAMA_MODEL
)
title = await generate_completion(prompt_messages, chat_model)
title = title.strip().strip('"\'').strip()[:100]
except Exception:
logger.warning("Failed to generate note title, using fallback", exc_info=True)
+10 -1
View File
@@ -446,7 +446,16 @@ async def _generate_prep_prose(
"""Ask the LLM for a direct conversational journal opener built from the sections."""
from fabledassistant.services.llm import generate_completion
model = (await get_setting(user_id, "default_model", "")) or Config.OLLAMA_MODEL
# Daily prep is a deliberate, multi-section generation — runs once a day,
# latency-tolerant, benefits from a smarter model. Route to the worker
# (background_model) rather than the chat model. The chat model in the
# conversation+curator architecture is small/fast/no-tools; prep needs
# the heavier reasoning the worker provides.
model = (
await get_setting(user_id, "background_model", "")
or Config.OLLAMA_BACKGROUND_MODEL
or Config.OLLAMA_MODEL
)
if not model:
logger.warning("No LLM model configured for daily prep — using fallback text")
return _fallback_prep_text(day_date)
@@ -22,7 +22,14 @@ async def suggest_tags(user_id: int, title: str, body: str, current_tags: list[s
return []
existing_tags = await get_all_tags(user_id)
model = await get_setting(user_id, "background_model", Config.OLLAMA_BACKGROUND_MODEL)
# Tag suggestion is a tiny single-shot task (3-5 tags from a note's
# title + body) — route to the chat model (small/fast) rather than
# tying up the worker on a trivial call. The chat model handles this
# in well under a second.
model = (
await get_setting(user_id, "default_model", "")
or Config.OLLAMA_MODEL
)
existing_list = ", ".join(f"#{t}" for t in existing_tags) if existing_tags else "(none yet)"
+9 -1
View File
@@ -144,7 +144,15 @@ async def _consolidate_observations(user_id: int) -> str:
user_prompt += f"Existing summary:\n{existing_summary}\n\n"
user_prompt += f"New observations:\n{obs_text}"
model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL)
# Profile observation consolidation reasons over multiple documents to
# produce a coherent summary — closer to curator-shaped work than chat.
# Route to worker (background_model). Falls back to OLLAMA_MODEL only if
# neither setting nor BACKGROUND default is available.
model = (
await get_setting(user_id, "background_model", "")
or Config.OLLAMA_BACKGROUND_MODEL
or Config.OLLAMA_MODEL
)
try:
new_summary = (await generate_completion(
[