fix: a model call with no timeout, and tests that measured Redis
The generation run stalled at topic 28 with the process alive and the log frozen. `_call_model` had no timeout — every other call in ai_service.py has one — so a stalled connection to the proxy hung the caller indefinitely. An interactive request survives that because the person gives up; an unattended run of five hundred topics does not, it just stops quietly and looks busy. It now takes a timeout, generous by default and 150s from the article writer: long enough for a full article, short enough that a stall is noticed in minutes rather than found hours later with nothing written since. Separately, the AI Mode tests passed this morning and failed this evening with no code between them. Not flakiness: they call the real Redis rate limiter, and sixty-eight runs of the suite had exhausted a daily limit of sixty. A test that depends on shared external state stops testing the code and starts reporting how often it has been run, so the limiter is now patched out for those tests. 208 backend tests green, and the run is moving again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
18afb138dc
commit
1d5962b40e
3 changed files with 22 additions and 3 deletions
|
|
@ -216,14 +216,22 @@ def extract_questions(
|
|||
raise RuntimeError(f"Failed to extract questions after 3 attempts: {last_error}")
|
||||
|
||||
|
||||
def _call_model(prompt: str, model_id: str | None, api_key: str | None) -> str:
|
||||
"""Call the configured LLM and return raw text response."""
|
||||
def _call_model(prompt: str, model_id: str | None, api_key: str | None,
|
||||
timeout: int = 180) -> str:
|
||||
"""Call the configured LLM and return raw text response.
|
||||
|
||||
The timeout is not optional in practice: every other call in this module has
|
||||
one, and this one did not. A stalled connection to the proxy hung the caller
|
||||
for good — which an interactive request survives by the user giving up, and
|
||||
an unattended run of several hundred topics does not.
|
||||
"""
|
||||
use_model = _proxy_model(model_id or settings.LITELLM_MODEL)
|
||||
use_key = api_key or settings.LITELLM_API_KEY
|
||||
kwargs = {
|
||||
"model": use_model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.1,
|
||||
"timeout": timeout,
|
||||
}
|
||||
if use_key:
|
||||
kwargs["api_key"] = use_key
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ PASSAGES = 14
|
|||
# Enough retrieved prose to write from. Below this the model is being asked to
|
||||
# write a medical article out of fragments, and it will either refuse or invent.
|
||||
MIN_SOURCE_CHARS = 3000
|
||||
# Generous for a long article, short enough that a stalled call is noticed in
|
||||
# minutes rather than discovered hours later with nothing written since.
|
||||
WRITE_TIMEOUT = 150
|
||||
# Long enough to be worth reading, short enough that nobody skims past the point.
|
||||
LONG_WORDS = 550
|
||||
SHELF = "Pediatrics"
|
||||
|
|
@ -148,7 +151,8 @@ def write_article(db: Session, topic: str, category_id: int | None = None,
|
|||
"passages": len(passages)}
|
||||
|
||||
model_id, api_key = get_model_for_task(db, "extraction")
|
||||
raw = _call_model(_prompt(topic, passages), model_id, api_key)
|
||||
# One topic must not be able to stall a run of five hundred.
|
||||
raw = _call_model(_prompt(topic, passages), model_id, api_key, timeout=WRITE_TIMEOUT)
|
||||
text = raw.strip()
|
||||
if text.startswith("```"):
|
||||
text = re.sub(r"^```[a-z]*\n?|```$", "", text).strip()
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ class _AiModeBase(unittest.TestCase):
|
|||
"""Fixtures shared by the route and retrieval cases; holds no tests itself."""
|
||||
|
||||
def setUp(self):
|
||||
# The daily limit is enforced through the real Redis, so without this a
|
||||
# suite that passes today fails once it has been run sixty times — the
|
||||
# tests would be measuring shared state rather than this code.
|
||||
self._no_limit = patch("app.routers.ai_mode.check_rate_limit", lambda **kwargs: None)
|
||||
self._no_limit.start()
|
||||
self.addCleanup(self._no_limit.stop)
|
||||
|
||||
self.bank = fixtures.BuilderTests()
|
||||
self.bank.setUp()
|
||||
self.client = self.bank.client
|
||||
|
|
|
|||
Loading…
Reference in a new issue