refactor(devgen/providers): centralize retry and quota handling in BaseProvider
Move retry logic and quota detection from provider-specific code to BaseProvider to reduce duplication and simplify error handling.
This commit is contained in:
2 files changed
+53
-67
No files matched your search
+43
-20
@@ -30,33 +30,56 @@ class BaseProvider(ABC):
|
||||
model: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Public entry point. Validates input, dispatches, and wraps errors.
|
||||
"""Public entry point. Validates input, dispatches, and wraps errors."""
|
||||
import time
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send to the model.
|
||||
api_key: Provider API key. May be ``None`` for local providers
|
||||
(see :attr:`REQUIRES_API_KEY`).
|
||||
model: Model id. Falls back to :attr:`DEFAULT_MODEL` when ``None``.
|
||||
**kwargs: Forwarded to :meth:`_generate`.
|
||||
|
||||
Returns:
|
||||
The generated text, stripped.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider requires an API key and none was given.
|
||||
RuntimeError: For any underlying failure, with a friendly message.
|
||||
"""
|
||||
if self.REQUIRES_API_KEY and not api_key:
|
||||
raise ValueError(
|
||||
f"{self.DISPLAY_NAME} API key is missing. "
|
||||
"Set it via `devgen setup config` or pass --api-key."
|
||||
)
|
||||
chosen_model = model or self.DEFAULT_MODEL
|
||||
try:
|
||||
text = self._generate(prompt, api_key, chosen_model, **kwargs)
|
||||
return (text or "").strip()
|
||||
except Exception as e:
|
||||
self._handle_error(e)
|
||||
|
||||
max_retries = kwargs.get("max_retries", 5)
|
||||
base_delay = kwargs.get("retry_delay", 10)
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
text = self._generate(prompt, api_key, chosen_model, **kwargs)
|
||||
return (text or "").strip()
|
||||
except Exception as e:
|
||||
if is_token_limit_error(e):
|
||||
self._handle_error(e)
|
||||
if self._is_quota_error(e) and attempt < max_retries:
|
||||
delay = base_delay * (2**attempt)
|
||||
print(
|
||||
f"\n[{self.DISPLAY_NAME}] Quota/Rate limit hit (429). "
|
||||
f"Waiting {delay}s and retrying... "
|
||||
f"(Attempt {attempt + 1}/{max_retries})"
|
||||
)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if self._is_quota_error(e) and attempt >= max_retries:
|
||||
raise RuntimeError(
|
||||
f"{self.DISPLAY_NAME} API quota exhausted (429) after multiple retries."
|
||||
) from e
|
||||
self._handle_error(e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _is_quota_error(error: Exception) -> bool:
|
||||
text = str(error).upper()
|
||||
return any(
|
||||
marker in text
|
||||
for marker in (
|
||||
"429",
|
||||
"RESOURCE_EXHAUSTED",
|
||||
"QUOTA_EXCEEDED",
|
||||
"THROTTLED",
|
||||
"RATE_LIMIT_EXCEEDED",
|
||||
"TOO_MANY_REQUESTS",
|
||||
)
|
||||
)
|
||||
|
||||
def _handle_error(self, error: Exception) -> None:
|
||||
"""Translate an internal exception into a user-facing RuntimeError.
|
||||
|
||||
+10
-47
@@ -1,10 +1,8 @@
|
||||
import time
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
from devgen.providers.base import BaseProvider
|
||||
from devgen.utils import is_token_limit_error
|
||||
|
||||
|
||||
class GeminiProvider(BaseProvider):
|
||||
@@ -14,50 +12,15 @@ class GeminiProvider(BaseProvider):
|
||||
DEFAULT_MODEL = "gemini-2.5-flash"
|
||||
|
||||
def _generate(self, prompt, api_key, model, **kwargs):
|
||||
max_retries = kwargs.get("max_retries", 5)
|
||||
base_delay = kwargs.get("retry_delay", 10) # 10s base for 429 backoff
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=kwargs.get("temperature", 0.7),
|
||||
top_p=kwargs.get("top_p", 0.95),
|
||||
top_k=kwargs.get("top_k", 40),
|
||||
max_output_tokens=kwargs.get("max_output_tokens", 2048),
|
||||
),
|
||||
)
|
||||
return response.text or ""
|
||||
except Exception as e:
|
||||
if is_token_limit_error(e):
|
||||
raise # base class handles with friendly message
|
||||
if self._is_quota_error(e) and attempt < max_retries:
|
||||
delay = base_delay * (2**attempt)
|
||||
print(
|
||||
f"\n[Gemini] Quota limit hit (429). This is often a temporary "
|
||||
f"rate limit (RPM). Waiting {delay}s and retrying... "
|
||||
f"(Attempt {attempt + 1}/{max_retries})"
|
||||
)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if self._is_quota_error(e):
|
||||
raise RuntimeError(
|
||||
"Gemini API quota exhausted (429) after multiple retries.\n\n"
|
||||
"POSSIBLE CAUSES:\n"
|
||||
"1. Rate Limit (RPM/RPD): You are sending requests too fast for the Free Tier.\n"
|
||||
"2. Daily Limit: You have reached the maximum free requests allowed for today (typically 1,500).\n"
|
||||
"3. Credit/Billing: If on a paid plan, your credit may be exhausted.\n\n"
|
||||
"Please check your usage metrics at: https://aistudio.google.com/app/usage"
|
||||
) from e
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _is_quota_error(error: Exception) -> bool:
|
||||
text = str(error).upper()
|
||||
return any(
|
||||
marker in text
|
||||
for marker in ("429", "RESOURCE_EXHAUSTED", "QUOTA_EXCEEDED", "THROTTLED")
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=kwargs.get("temperature", 0.7),
|
||||
top_p=kwargs.get("top_p", 0.95),
|
||||
top_k=kwargs.get("top_k", 40),
|
||||
max_output_tokens=kwargs.get("max_output_tokens", 2048),
|
||||
),
|
||||
)
|
||||
return response.text or ""
|
||||
Reference in new issue
Block a user