diff --git a/devgen/providers/base.py b/devgen/providers/base.py index 72c881b..99f66bf 100644 --- a/devgen/providers/base.py +++ b/devgen/providers/base.py @@ -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. diff --git a/devgen/providers/gemini.py b/devgen/providers/gemini.py index bc9a42d..1deecb0 100644 --- a/devgen/providers/gemini.py +++ b/devgen/providers/gemini.py @@ -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 ""