Spaces:
Paused
Paused
Download gemini_limits.py from Effina-byte/Agent-test: direct link, hf CLI and curl.
- Browser
- Download file 8.8 kB
-
https://huggingface.co/spaces/Effina-byte/Agent-test/resolve/main/gemini_limits.py
- Command line
-
hf download hf://spaces/Effina-byte/Agent-test/gemini_limits.py
-
curl -L -o gemini_limits.py https://huggingface.co/spaces/Effina-byte/Agent-test/resolve/main/gemini_limits.py
8.8 kB
| """Shared rate-limit bookkeeping for free-tier LLM APIs (all providers). | |
| Every agent step is one LLM request, and free tiers allow only a handful of | |
| requests per minute (and per day). This module tracks usage per *endpoint* | |
| (a "provider|model" key) across the whole process so that: | |
| * we never exceed each endpoint's requests-per-minute budget, | |
| * an endpoint that returns 429 is put on cooldown and another is used, | |
| * an endpoint whose DAILY quota is gone, or whose key is rejected, is dropped, | |
| * if everything is cooling down for too long we raise QuotaExhausted instead | |
| of sleeping for hours (the evaluation then resumes later from progress.json). | |
| (The file keeps its old name, gemini_limits.py, so existing imports still work.) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import threading | |
| import time | |
| from collections import defaultdict, deque | |
| # Load keys from a local .env file if present (never overrides real environment | |
| # variables, so Space secrets / shell exports always win). Needs python-dotenv. | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except ImportError: | |
| pass | |
| # Vertex Gemini models used for the agent brain AND for the multimodal tools. | |
| # Unknown names are harmless: a 404 just drops that model for the session. | |
| MODEL_CHAIN = [ | |
| "gemini-3.8-flash", | |
| "gemini-3.7-flash", | |
| "gemini-3.6-flash", | |
| "gemini-3.5-flash", | |
| "gemini-3.5-flash-lite", | |
| "gemini-3.1-flash-lite", | |
| ] | |
| # Default requests/minute for an endpoint that did not register its own. | |
| RPM = int(os.getenv("VERTEX_GEMINI_RPM") or os.getenv("GEMINI_RPM") or "4") | |
| # Longest we are willing to wait for any endpoint to free up. | |
| MAX_WAIT = float(os.getenv("MAX_WAIT_SECONDS", "180")) | |
| class QuotaExhausted(RuntimeError): | |
| """Every configured endpoint is out of quota or cooling down too long.""" | |
| _lock = threading.Lock() | |
| _calls: dict[str, deque] = defaultdict(deque) | |
| _blocked_until: dict[str, float] = defaultdict(float) | |
| _dead: dict[str, str] = {} # key -> reason | |
| _rpm: dict[str, int] = {} # budget per group | |
| _group: dict[str, str] = {} # key -> budget group (== key unless shared) | |
| def make_key(provider: str, model: str) -> str: | |
| return f"{provider}|{model}" | |
| def model_name(key: str) -> str: | |
| return key.split("|", 1)[1] if "|" in key else key | |
| def register(key: str, rpm: int | None, group: str | None = None) -> None: | |
| """Set the requests/minute budget of `key`. | |
| `group` makes several keys share ONE budget and ONE 429 cooldown (e.g. all | |
| NVIDIA NIM models share a single 40 requests/min allowance). | |
| """ | |
| grp = group or key | |
| _group[key] = grp | |
| if rpm: | |
| _rpm[grp] = int(rpm) | |
| def gemini_models() -> list[str]: | |
| """Endpoint keys for Vertex Gemini (multimodal tools use these too).""" | |
| keys = [make_key("gemini", m) for m in model_list()] | |
| for k in keys: | |
| register(k, RPM) | |
| return keys | |
| def model_list() -> list[str]: | |
| """Return the Vertex Gemini chain, optionally overridden by environment.""" | |
| configured = os.getenv("VERTEX_GEMINI_MODELS", "").strip() | |
| if configured: | |
| return [model.strip() for model in configured.split(",") if model.strip()] | |
| return list(MODEL_CHAIN) | |
| def parse_retry_delay(text: str, default: float = 20.0) -> float: | |
| """Understands '4.07s', '7m12.5s', '1h2m', retryDelay: '4s', retry-after.""" | |
| m = re.search(r"(?:retry|try again) in\s+((?:\d+(?:\.\d+)?\s*[hms]\s*)+)", text, re.I) | |
| if not m: | |
| m2 = re.search(r"retryDelay['\"]?:\s*['\"]?(\d+(?:\.\d+)?)s", text) | |
| return float(m2.group(1)) if m2 else default | |
| total = 0.0 | |
| for num, unit in re.findall(r"(\d+(?:\.\d+)?)\s*([hms])", m.group(1), re.I): | |
| total += float(num) * {"h": 3600, "m": 60, "s": 1}[unit.lower()] | |
| return total or default | |
| def wait_time(key: str) -> float: | |
| """Seconds until `key` may be called again (0 = now).""" | |
| now = time.time() | |
| grp = _group.get(key, key) | |
| q = _calls[grp] | |
| while q and now - q[0] >= 60: | |
| q.popleft() | |
| wait = max(0.0, _blocked_until[key] - now) | |
| limit = _rpm.get(grp, RPM) | |
| if len(q) >= limit: | |
| wait = max(wait, 60 - (now - q[0]) + 0.5) | |
| return wait | |
| def acquire(keys: list[str], max_wait: float | None = None) -> str: | |
| """Block until some endpoint in `keys` is usable; reserve a slot on it. | |
| Preference order = order of `keys` (first one with spare capacity wins). | |
| """ | |
| limit = MAX_WAIT if max_wait is None else max_wait | |
| while True: | |
| with _lock: | |
| live = [k for k in keys if k not in _dead] | |
| if not live: | |
| reasons = "; ".join(f"{k}: {_dead.get(k, '?')}" for k in keys) | |
| raise QuotaExhausted( | |
| "No usable model left (" + reasons + "). Wait for the quota " | |
| "to reset, add another provider key, or enable billing." | |
| ) | |
| waits = {k: wait_time(k) for k in live} | |
| best = min(live, key=lambda k: waits[k]) # ties -> earliest in list | |
| if waits[best] <= 0: | |
| _calls[_group.get(best, best)].append(time.time()) | |
| return best | |
| delay = waits[best] | |
| if delay > limit: | |
| raise QuotaExhausted( | |
| f"Every model is rate-limited; the earliest frees up in " | |
| f"{delay / 60:.1f} min. Re-run later: solved questions are " | |
| "saved and will be skipped." | |
| ) | |
| print(f"[RATE] All models busy, waiting {delay:.0f}s...") | |
| time.sleep(min(delay, 30)) | |
| def _has_code(low: str, *codes: str) -> bool: | |
| """True if an HTTP status code appears as a whole number (not inside 14012).""" | |
| return any(re.search(rf"(?<![\d.]){c}(?![\d])", low) for c in codes) | |
| def report_error(key: str, exc: BaseException) -> bool: | |
| """Record a failure. Returns True if the caller should try another endpoint.""" | |
| text = str(exc) | |
| low = text.lower() | |
| squashed = low.replace("_", "").replace(" ", "") | |
| status = getattr(exc, "status_code", None) | |
| if status: # openai exceptions carry the real status; make sure it's matched | |
| low += f" {status}" | |
| with _lock: | |
| # --- permanent for this session ------------------------------------ | |
| if any(s in squashed for s in ( | |
| "perday", "tokensperday", "requestsperday", "dailylimit", "dailyquota", | |
| )): | |
| print(f"[RATE] {key}: daily quota exhausted.") | |
| _dead[key] = "daily quota exhausted" | |
| return True | |
| if (_has_code(low, "401", "403") or any(s in low for s in ( | |
| "invalid api key", "incorrect api key", "api key not valid", | |
| "permission denied", "unauthorized", "authentication", | |
| ))) and not _has_code(low, "429"): | |
| print(f"[RATE] {key}: key rejected, dropping it.") | |
| _dead[key] = "API key rejected" | |
| return True | |
| if (_has_code(low, "404") or "not found" in low or "not_found" in low | |
| or "does not exist" in low or "no such model" in low): | |
| print(f"[RATE] {key}: model not available on this key, skipping.") | |
| _dead[key] = "model not available" | |
| return True | |
| # --- temporary ------------------------------------------------------- | |
| if _has_code(low, "429") or "resourceexhausted" in squashed or "ratelimit" in squashed \ | |
| or "toomanyrequests" in squashed or "quota" in low: | |
| delay = parse_retry_delay(text) + 1 | |
| grp = _group.get(key, key) | |
| for k, g in list(_group.items()) + [(key, grp)]: | |
| if g == grp: # a shared budget cools down together | |
| _blocked_until[k] = time.time() + delay | |
| print(f"[RATE] {key}: 429, cooling down {delay:.0f}s.") | |
| return True | |
| if any(s in low for s in ( | |
| "tool_use_failed", "failed to call a function", "failed_generation", | |
| "request too large", "context length", "maximum context", | |
| )) or _has_code(low, "413"): | |
| _blocked_until[key] = time.time() + 2 | |
| print(f"[RATE] {key}: request rejected ({text[:80]!r}), trying another.") | |
| return True | |
| if _has_code(low, "500", "502", "503", "504") or any(s in low for s in ( | |
| "unavailable", "overloaded", "timeout", "timed out", "connection")): | |
| _blocked_until[key] = time.time() + 15 | |
| return True | |
| if _has_code(low, "400", "422"): | |
| # Often a provider-specific quirk (tool_choice, schema...). Don't | |
| # kill the endpoint for good: park it briefly and try the others. | |
| _blocked_until[key] = time.time() + 30 | |
| print(f"[RATE] {key}: bad request ({text[:100]!r}), trying another.") | |
| return True | |
| return False | |