refactor(devgen/providers): shared provider base class

Abstract out generate validation and error handling into BaseProvider, eliminating duplicated logic across Anthropic, OpenRouter, and OpenAI providers.
This commit is contained in:
sankalp committed 2026-06-06 17:10:30 +05:45
1 parent e2ec269f16
commit f70b5208ec
7 files changed
+223 -259

No files matched your search

+38 -51
View File
@@ -1,31 +1,22 @@
import time
from google import genai
from google.genai import types
from devgen.utils import format_token_limit_error, is_token_limit_error
from devgen.providers.base import BaseProvider
from devgen.utils import is_token_limit_error
class GeminiProvider:
class GeminiProvider(BaseProvider):
"""Generates content using Google's Gemini models."""
def generate(
self, prompt: str, api_key: str, model: str = "gemini-2.5-flash", **kwargs
) -> str:
"""Generates a response using the Gemini API.
Default model is updated to gemini-2.5-flash which is the latest
recommended model.
"""
if not api_key:
raise ValueError(
"Gemini API key is missing. "
"Set it via `devgen setup config` or pass --api-key."
)
client = genai.Client(api_key=api_key)
DISPLAY_NAME = "Gemini"
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) # 10 seconds base delay for 429
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:
@@ -39,38 +30,34 @@ class GeminiProvider:
max_output_tokens=kwargs.get("max_output_tokens", 2048),
),
)
return response.text.strip()
return response.text
except Exception as e:
error_msg = str(e).upper()
# Token / context-window overflow is not retryable — no point
# backing off, the prompt is just too big.
if is_token_limit_error(e):
raise RuntimeError(format_token_limit_error("Gemini", e)) from e
# Check for 429 or ResourceExhausted in common error formats
if any(
x in error_msg
for x in [
"429",
"RESOURCE_EXHAUSTED",
"QUOTA_EXCEEDED",
"THROTTLED",
]
):
if attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"\n[Gemini] Quota limit hit (429). This is often a temporary rate limit (RPM). "
f"Waiting {delay}s and retrying... (Attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
else:
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 RuntimeError(f"Gemini generation failed: {e}") from 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")
)