Skip to content

Commit d31f75e

Browse files
fix: fail over when gemini quota is exhausted
1 parent 390f04e commit d31f75e

2 files changed

Lines changed: 40 additions & 22 deletions

File tree

‎src/llm_runner.py‎

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from __future__ import annotations
1111

12-
import asyncio
1312
import json
1413
import logging
1514
import os
@@ -115,26 +114,33 @@ async def generate_response(
115114
if dry_run:
116115
return f"[DRY RUN / MOCK MODE] Generated response for input:\n{prompt_text}"
117116

118-
# Dispatch to provider
119-
if self.provider == "anthropic" or (self.anthropic_api_key and not self.gemini_api_key):
120-
try:
121-
return await self._generate_anthropic(system_instruction, prompt_text)
122-
except Exception as e:
123-
logger.warning(f"Anthropic generation failed: {e}")
124-
125-
if self.provider == "openai" or (self.openai_api_key and not self.gemini_api_key):
126-
try:
127-
return await self._generate_openai(system_instruction, prompt_text)
128-
except Exception as e:
129-
logger.warning(f"OpenAI generation failed: {e}")
117+
provider_generators = {
118+
"gemini": lambda: self._generate_gemini(system_instruction, prompt_text, dry_run=dry_run, tier=tier),
119+
"openai": lambda: self._generate_openai(system_instruction, prompt_text),
120+
"anthropic": lambda: self._generate_anthropic(system_instruction, prompt_text),
121+
"ollama": lambda: self._generate_ollama(system_instruction, prompt_text),
122+
}
123+
configured = {
124+
"gemini": bool(self.gemini_api_key),
125+
"openai": bool(self.openai_api_key),
126+
"anthropic": bool(self.anthropic_api_key),
127+
"ollama": bool(self.ollama_host),
128+
}
129+
provider_order = [self.provider] + [name for name, enabled in configured.items() if enabled and name != self.provider]
130+
last_error: Optional[Exception] = None
130131

131-
if self.provider == "ollama":
132+
for provider in provider_order:
133+
generator = provider_generators.get(provider)
134+
if not generator or (provider != "gemini" and not configured[provider]):
135+
continue
132136
try:
133-
return await self._generate_ollama(system_instruction, prompt_text)
134-
except Exception as e:
135-
logger.warning(f"Ollama generation failed: {e}")
137+
return await generator()
138+
except Exception as exc:
139+
last_error = exc
140+
logger.warning("%s generation failed; trying next configured provider: %s", provider, exc)
136141

137-
# Default / Fallback: Google Gemini
142+
if last_error:
143+
raise RuntimeError(f"All configured LLM providers failed. Last error: {last_error}") from last_error
138144
return await self._generate_gemini(system_instruction, prompt_text, dry_run=dry_run, tier=tier)
139145

140146
def _get_gemini_candidate_models(self, tier: str = "fast") -> List[str]:
@@ -216,8 +222,6 @@ async def _generate_gemini(self, system_instruction: str, user_input: str, dry_r
216222
except Exception as e:
217223
last_error = e
218224
print(f"[LLM:Gemini] ⚠️ Model {model_name} (SDK) failed: {e}")
219-
if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e):
220-
await asyncio.sleep(2)
221225

222226
# 2. Try Gemini REST API
223227
if self.gemini_api_key:
@@ -241,8 +245,6 @@ async def _generate_gemini(self, system_instruction: str, user_input: str, dry_r
241245
else:
242246
last_error = f"HTTP {resp.status_code}: {resp.text}"
243247
print(f"[LLM:Gemini-REST] ⚠️ Model {model_name} (REST) failed with HTTP {resp.status_code}: {resp.text[:120]}")
244-
if resp.status_code == 429:
245-
await asyncio.sleep(2)
246248
except Exception as e:
247249
last_error = e
248250
print(f"[LLM:Gemini-REST] ⚠️ REST request for {model_name} failed: {e}")

‎tests/test_llm_runner.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from pathlib import Path
3+
from unittest.mock import AsyncMock
34
from src.llm_runner import LLMRunner
45

56

@@ -31,3 +32,18 @@ async def test_generate_response_dry_run():
3132
)
3233
assert "DRY RUN" in response
3334
assert "Format issue #12" in response
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_gemini_quota_fails_over_to_configured_provider():
39+
runner = LLMRunner(provider="gemini")
40+
runner.gemini_api_key = "gemini-key"
41+
runner.openai_api_key = "openai-key"
42+
runner._generate_gemini = AsyncMock(side_effect=RuntimeError("429 RESOURCE_EXHAUSTED quota exceeded"))
43+
runner._generate_openai = AsyncMock(return_value="OpenAI fallback response")
44+
45+
response = await runner.generate_response("system", "input")
46+
47+
assert response == "OpenAI fallback response"
48+
runner._generate_gemini.assert_awaited_once()
49+
runner._generate_openai.assert_awaited_once()

0 commit comments

Comments
 (0)