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:
sankalp committed 2026-07-03 18:07:43 +05:45
1 parent f7001d4243
commit 261d04c56f
2 files changed
+53 -67

No files matched your search

+43 -20
View File
@@ -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
View File
@@ -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 ""