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:
@@ -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():
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user