feat: gemini_guard — rate limiter + circuit breaker på alle Gemini-kall

This commit is contained in:
Chris Christiansen 2026-07-02 17:14:06 +00:00
parent 6ffe23943c
commit 434f591f9c
2 changed files with 104 additions and 1 deletions

View File

@ -17,6 +17,7 @@ from typing import Literal
from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from google.adk.runners import Runner
from gemini_guard import guarded_run, CircuitOpen, RateLimitExceeded
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.genai import types
@ -282,7 +283,7 @@ async def _run_async(message: str, user_id: str, session_id: str, mode: str, cal
output_tokens = 0
request_id = str(uuid.uuid4())
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=new_message):
async for event in guarded_run(runner, user_id=user_id, session_id=session.id, new_message=new_message):
if event.is_final_response() and event.content and event.content.parts:
final_text = event.content.parts[0].text or ""
if hasattr(event, "usage_metadata") and event.usage_metadata:

View File

@ -0,0 +1,102 @@
"""
gemini_guard.py Rate limiter + circuit breaker for alle Gemini-kall i OSVauco.
Wrapper rundt runner.run_async() i _run_async().
"""
import time
import asyncio
import logging
from collections import deque
from typing import AsyncIterator
logger = logging.getLogger(__name__)
# --- Konfigurasjon (env-override mulig) ---
import os
RATE_LIMIT_RPM = int(os.environ.get("GUARD_RATE_LIMIT_RPM", "30"))
CIRCUIT_FAIL_THRESH = int(os.environ.get("GUARD_CIRCUIT_FAILS", "5"))
CIRCUIT_RESET_SECS = int(os.environ.get("GUARD_CIRCUIT_RESET_SECS", "60"))
class CircuitOpen(Exception):
pass
class RateLimitExceeded(Exception):
pass
class GeminiGuard:
"""
Én singleton per prosess.
- Rate limit: maks RATE_LIMIT_RPM kall per minutt (sliding window)
- Circuit breaker: åpner ved CIRCUIT_FAIL_THRESH feil rad,
reset etter CIRCUIT_RESET_SECS sekunder
"""
def __init__(self):
self._timestamps: deque = deque()
self._failures = 0
self._opened_at = 0.0
self._state = "closed" # closed | open | half-open
def _check_rate(self):
now = time.monotonic()
cutoff = now - 60.0
while self._timestamps and self._timestamps[0] < cutoff:
self._timestamps.popleft()
if len(self._timestamps) >= RATE_LIMIT_RPM:
raise RateLimitExceeded(
f"Rate limit {RATE_LIMIT_RPM} RPM nådd ({len(self._timestamps)} kall siste minutt)"
)
self._timestamps.append(now)
def _check_circuit(self):
if self._state == "open":
elapsed = time.monotonic() - self._opened_at
if elapsed >= CIRCUIT_RESET_SECS:
self._state = "half-open"
logger.warning("[guard] circuit half-open — prøver igjen")
else:
raise CircuitOpen(
f"Circuit breaker åpen — venter {CIRCUIT_RESET_SECS - int(elapsed)}s"
)
def on_success(self):
self._failures = 0
self._state = "closed"
def on_failure(self, exc: Exception):
self._failures += 1
logger.error(f"[guard] Gemini-feil #{self._failures}: {exc}")
if self._failures >= CIRCUIT_FAIL_THRESH:
self._state = "open"
self._opened_at = time.monotonic()
logger.critical(f"[guard] Circuit ÅPNET etter {self._failures} feil")
async def run(self, runner, *, user_id, session_id, new_message) -> AsyncIterator:
self._check_circuit()
self._check_rate()
try:
events = []
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=new_message,
):
events.append(event)
self.on_success()
for e in events:
yield e
except (CircuitOpen, RateLimitExceeded):
raise
except Exception as exc:
self.on_failure(exc)
raise
_guard = GeminiGuard()
async def guarded_run(runner, *, user_id, session_id, new_message) -> AsyncIterator:
"""Drop-in erstatning for runner.run_async(...) """
async for event in _guard.run(
runner,
user_id=user_id,
session_id=session_id,
new_message=new_message,
):
yield event