diff --git a/mempalace/closet_llm.py b/mempalace/closet_llm.py index 50000c8..a85d517 100644 --- a/mempalace/closet_llm.py +++ b/mempalace/closet_llm.py @@ -169,6 +169,9 @@ def _call_llm(cfg: LLMConfig, source_file: str, wing: str, room: str, content: s parsed = json.loads(text) return parsed, payload.get("usage") except json.JSONDecodeError: + if attempt < 2: + time.sleep(2**attempt) + continue return None, None except urllib.error.HTTPError as e: # 429 / 503 = retry with backoff diff --git a/tests/test_closet_llm.py b/tests/test_closet_llm.py index 3a0e84e..905d28b 100644 --- a/tests/test_closet_llm.py +++ b/tests/test_closet_llm.py @@ -196,10 +196,34 @@ class TestCallLLM: } ) - with patch("urllib.request.urlopen", side_effect=fake_urlopen): - parsed, usage = _call_llm(cfg, "/tmp/x", "w", "r", "c") + with ( + patch("urllib.request.urlopen", side_effect=fake_urlopen), + patch("mempalace.closet_llm.time.sleep"), + ): + parsed, _ = _call_llm(cfg, "/tmp/x", "w", "r", "c") assert parsed is None + def test_retries_on_json_decode_error(self): + cfg = self._make_cfg() + call_count = {"n": 0} + + def fake_urlopen(req, timeout=None): + call_count["n"] += 1 + return _FakeResp( + { + "choices": [{"message": {"content": "not json at all"}}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1}, + } + ) + + with ( + patch("urllib.request.urlopen", side_effect=fake_urlopen), + patch("mempalace.closet_llm.time.sleep"), + ): + parsed, _ = _call_llm(cfg, "/tmp/x", "w", "r", "c") + assert parsed is None + assert call_count["n"] == 3 + # ── regenerate_closets error paths ───────────────────────────────────────