From 0e01de156fc1944d8253dd0eadeacfefcca8719a Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Tue, 12 May 2026 00:44:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(llm):=20RateLimitError=20retryable=20+=20re?= =?UTF-8?q?try=20tenacity=203=E2=86=925=20+=20backoff=20fino=20a=2030s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run phase2-5-qwen25-prompt-mut-003 fallito a gen 5 (76 evals done, $0.061 spesi) per HTTP 429 RateLimit upstream da DeepInfra su qwen-2.5-72b. RateLimitError NON era in _RETRYABLE_EXCEPTIONS quindi tenacity falliva subito, propagando il 429 a propose() e all'orchestrator (run failed). Tre fix: 1) Aggiunto openai.RateLimitError a _RETRYABLE_EXCEPTIONS. 2) Bumpato stop_after_attempt(3) → 5 e wait_exponential max 10s → 30s. Più tempo per il rate limit upstream di sbloccare prima di rinunciare. 3) hypothesis.py: try/except RateLimitError in propose() come per EmptyCompletionError — anche se tenacity esaurisce i 5 retry, il genome viene marcato fitness=0 e il loop esterno continua senza crash totale. Test: aggiornato test_completion_retries_on_connection_error per assert call_count == 5 (era 3). Tutti 182 verdi. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/multi_swarm/agents/hypothesis.py | 8 ++++++++ src/multi_swarm/llm/client.py | 9 ++++++--- tests/unit/test_llm_client.py | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/multi_swarm/agents/hypothesis.py b/src/multi_swarm/agents/hypothesis.py index 49ee4bc..7eec368 100644 --- a/src/multi_swarm/agents/hypothesis.py +++ b/src/multi_swarm/agents/hypothesis.py @@ -3,6 +3,8 @@ from __future__ import annotations import re from dataclasses import dataclass, field +import openai + from ..genome.hypothesis import HypothesisAgentGenome from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient from ..protocol.parser import ParseError, Strategy, parse_strategy @@ -282,6 +284,12 @@ class HypothesisAgent: errors.append(f"empty_completion: {e}") last_raw = "" continue + except openai.RateLimitError as e: + # Provider upstream rate limited oltre i retry tenacity. + # Marca genome come fallito senza propagare l'eccezione al run. + errors.append(f"rate_limit: {e}") + last_raw = "" + continue completions.append(completion) last_raw = completion.text diff --git a/src/multi_swarm/llm/client.py b/src/multi_swarm/llm/client.py index 1ea0183..f7c58cb 100644 --- a/src/multi_swarm/llm/client.py +++ b/src/multi_swarm/llm/client.py @@ -26,11 +26,14 @@ class EmptyCompletionError(RuntimeError): pass -# Errori transient: retry. RateLimit/Auth/InvalidRequest: NO retry. +# Errori transient: retry. Auth/InvalidRequest: NO retry. +# RateLimitError (HTTP 429) ora retryable: provider OpenRouter come DeepInfra +# applicano rate limit upstream temporaneo, recuperabile con backoff. _RETRYABLE_EXCEPTIONS: tuple[type[BaseException], ...] = ( openai.APIConnectionError, openai.APITimeoutError, openai.InternalServerError, + openai.RateLimitError, EmptyCompletionError, ) @@ -79,8 +82,8 @@ class LLMClient: self._client = OpenAI(api_key=openrouter_api_key, base_url=openrouter_base_url) @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1.0, min=2.0, max=10.0), + stop=stop_after_attempt(5), + wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0), retry=retry_if_exception_type(_RETRYABLE_EXCEPTIONS), reraise=True, ) diff --git a/tests/unit/test_llm_client.py b/tests/unit/test_llm_client.py index 32a2677..8514319 100644 --- a/tests/unit/test_llm_client.py +++ b/tests/unit/test_llm_client.py @@ -75,7 +75,7 @@ def test_completion_retries_on_connection_error(mocker): with pytest.raises(openai.APIConnectionError): client.complete(g, system="sys", user="usr") - assert fake_openai.chat.completions.create.call_count == 3 + assert fake_openai.chat.completions.create.call_count == 5 def test_completion_uses_custom_model_tier_c(mocker):