feat(llm): retry tenacity su errori transient connection/timeout/5xx

Avvolge LLMClient.complete con tenacity (3 attempts, backoff esponenziale
2-10s) che ritenta solo su errori transient di OpenAI/Anthropic SDK
(APIConnectionError, APITimeoutError, InternalServerError). RateLimit,
Authentication e 4xx non vengono ritentati. reraise=True preserva
l'eccezione originale dopo l'esaurimento dei tentativi.

Aggiunti 2 test (marker slow): esaurimento retry su APIConnectionError
e successo al secondo tentativo dopo APITimeoutError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 22:32:34 +02:00
parent 18259325a1
commit ea403ec2d5
2 changed files with 73 additions and 0 deletions
+24
View File
@@ -2,8 +2,16 @@ from __future__ import annotations
from dataclasses import dataclass
import anthropic
import openai
from anthropic import Anthropic
from openai import OpenAI
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
from ..genome.hypothesis import HypothesisAgentGenome, ModelTier
@@ -12,6 +20,16 @@ MODEL_TIER_C = "qwen/qwen-2.5-72b-instruct" # via OpenRouter
MODEL_TIER_B = "claude-sonnet-4-6" # via Anthropic
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
# Errori transient: retry. RateLimit/Auth/InvalidRequest: NO retry.
_RETRYABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (
openai.APIConnectionError,
openai.APITimeoutError,
openai.InternalServerError,
anthropic.APIConnectionError,
anthropic.APITimeoutError,
anthropic.InternalServerError,
)
@dataclass(frozen=True)
class CompletionResult:
@@ -31,6 +49,12 @@ class LLMClient:
self._openrouter = OpenAI(api_key=openrouter_api_key, base_url=OPENROUTER_BASE_URL)
self._anthropic = Anthropic(api_key=anthropic_api_key) if anthropic_api_key else None
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1.0, min=2.0, max=10.0),
retry=retry_if_exception_type(_RETRYABLE_EXCEPTIONS),
reraise=True,
)
def complete(
self,
genome: HypothesisAgentGenome,