fix(llm): surface Ollama error body; refresh pre-gemma3 summaries

Two small hardening fixes from the mistral-nemo testing round:

1. stream_chat / stream_chat_with_tools now read the Ollama response
   body and log it before raising on non-2xx. Previously all we saw
   was 'HTTP 400 Bad Request' — the gemma3-no-tools failure would
   have been diagnosed in one step if we'd been logging the body,
   which says e.g. 'model does not support tools'.

2. backfill_project_summaries() now also targets summaries stamped
   before 2026-04-12 (the gemma3:4b cutover). The remaining projects
   still carrying the broken qwen2.5:3b output (token repetition,
   hallucinated topics) will regenerate on next startup on the
   better model.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:13:21 -04:00
parent 9a851de624
commit 782f36ed51
2 changed files with 39 additions and 4 deletions
+20 -2
View File
@@ -159,6 +159,24 @@ async def wait_for_model_loaded(model: str, timeout: float = 90.0) -> bool:
await asyncio.sleep(min(2.0, remaining))
async def _raise_ollama_error(resp: httpx.Response, model: str) -> None:
"""On a non-2xx Ollama response, log its body before raising.
``resp.raise_for_status()`` alone throws away Ollama's error body, which
carries the actual reason (e.g. ``"model does not support tools"``,
``"context length exceeded"``). Reading the body first means failures
surface a usable message instead of a bare HTTPStatusError.
"""
if resp.status_code < 400:
return
body = (await resp.aread()).decode("utf-8", errors="replace").strip()
logger.error(
"Ollama /api/chat %d for model=%s: %s",
resp.status_code, model, body[:500],
)
resp.raise_for_status()
async def stream_chat(
messages: list[dict],
model: str,
@@ -184,7 +202,7 @@ async def stream_chat(
f"{Config.OLLAMA_URL}/api/chat",
json=payload,
) as resp:
resp.raise_for_status()
await _raise_ollama_error(resp, model)
async for line in resp.aiter_lines():
if not line.strip():
continue
@@ -245,7 +263,7 @@ async def stream_chat_with_tools(
f"{Config.OLLAMA_URL}/api/chat",
json=payload,
) as resp:
resp.raise_for_status()
await _raise_ollama_error(resp, model)
accumulated_tool_calls: list[dict] = []
async for line in resp.aiter_lines():
if not line.strip():
+19 -2
View File
@@ -141,13 +141,30 @@ async def generate_project_summary(user_id: int, project_id: int) -> None:
logger.debug("Failed to generate summary for project %d", project_id, exc_info=True)
# Cutoff: summaries older than this were generated by qwen2.5:3b, which
# produced broken output (token repetition, hallucinated topics, misspellings).
# Anything stamped before the switch to gemma3:4b on 2026-04-12 gets
# regenerated at startup so the whole project list ends up on the better model.
_BACKGROUND_MODEL_CUTOVER = datetime(2026, 4, 12, tzinfo=timezone.utc)
async def backfill_project_summaries() -> None:
"""Generate summaries for all projects missing auto_summary. Fire-and-forget."""
"""Generate summaries for projects missing or predating the gemma3:4b cutover.
Fire-and-forget: each summary runs as its own background task.
"""
import asyncio
from sqlalchemy import or_
try:
async with async_session() as session:
rows = (await session.execute(
select(Project.id, Project.user_id).where(Project.auto_summary.is_(None))
select(Project.id, Project.user_id).where(
or_(
Project.auto_summary.is_(None),
Project.summary_updated_at.is_(None),
Project.summary_updated_at < _BACKGROUND_MODEL_CUTOVER,
)
)
)).all()
for row in rows:
asyncio.create_task(generate_project_summary(row.user_id, row.id))