mirror of
https://github.com/nubenetes/awesome-kubernetes.git
synced 2026-05-24 01:53:45 +00:00
feat(gemini): implement Dynamic AI Model Discovery and Scoring Engine
This commit is contained in:
@@ -24,14 +24,8 @@ if GEMINI_API_KEY and not os.getenv("GOOGLE_API_KEY"):
|
||||
|
||||
GH_TOKEN = os.getenv("GH_TOKEN")
|
||||
|
||||
# Gemini Configuration (May 2026)
|
||||
GEMINI_API_VERSION = "v1"
|
||||
GEMINI_MODELS = [
|
||||
"gemini-3.1-flash",
|
||||
"gemini-3.1-pro",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash"
|
||||
]
|
||||
# Gemini Configuration (Dynamic Discovery Enabled)
|
||||
GEMINI_API_VERSION = "v1beta"
|
||||
|
||||
TARGET_REPO = "nubenetes/awesome-kubernetes"
|
||||
|
||||
|
||||
@@ -3,12 +3,77 @@ import asyncio
|
||||
import random
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional
|
||||
from src.config import GEMINI_API_KEYS, GEMINI_API_VERSION, GEMINI_MODELS
|
||||
from src.config import GEMINI_API_KEYS, GEMINI_API_VERSION
|
||||
from src.logger import log_event
|
||||
|
||||
# Global para mantener el índice de la API Key actual
|
||||
# Global para mantener el índice de la API Key actual y los modelos descubiertos
|
||||
CURRENT_KEY_INDEX = 0
|
||||
DISCOVERED_MODELS = []
|
||||
|
||||
async def discover_optimal_models():
|
||||
"""
|
||||
Queries the Google Model Service to find available models for the current keys.
|
||||
Ranks them by capability (Pro > Flash) and version (3.1 > 2.5 > 1.5).
|
||||
"""
|
||||
global DISCOVERED_MODELS
|
||||
if DISCOVERED_MODELS: return DISCOVERED_MODELS
|
||||
|
||||
log_event("[*] Starting AI Model Auto-Discovery...", section_break=True)
|
||||
all_supported = []
|
||||
|
||||
# Try all keys to get a union of available models
|
||||
for key in GEMINI_API_KEYS:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Use v1beta for discovery as it lists more models
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models?key={key}"
|
||||
resp = await client.get(url, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
models_data = resp.json().get("models", [])
|
||||
for m in models_data:
|
||||
name = m.get("name", "").replace("models/", "")
|
||||
# Filter only models that support content generation
|
||||
if "generateContent" in m.get("supportedGenerationMethods", []):
|
||||
if name not in all_supported:
|
||||
all_supported.append(name)
|
||||
elif resp.status_code == 429:
|
||||
log_event(f" [!] Discovery Key is rate-limited (429). Skipping this key for discovery.")
|
||||
except: pass
|
||||
|
||||
if not all_supported:
|
||||
log_event(" [!] Discovery failed or keys limited. Falling back to safe defaults.")
|
||||
# Fallback to absolute stables
|
||||
DISCOVERED_MODELS = ["gemini-1.5-flash-latest", "gemini-1.5-flash", "gemini-1.5-pro"]
|
||||
return DISCOVERED_MODELS
|
||||
|
||||
# SCORING ALGORITHM
|
||||
def score_model(name: str) -> float:
|
||||
score = 0.0
|
||||
# 1. Version Score (Prioritize 3.x series in 2026)
|
||||
if "3.1" in name: score += 100
|
||||
elif "3.0" in name: score += 80
|
||||
elif "2.5" in name: score += 60
|
||||
elif "2.0" in name: score += 40
|
||||
elif "1.5" in name: score += 20
|
||||
|
||||
# 2. Tier Score
|
||||
if "-pro" in name: score += 50
|
||||
elif "-flash" in name: score += 25
|
||||
elif "-lite" in name: score += 10
|
||||
|
||||
# 3. Latest/Stable Bonus
|
||||
if "-latest" in name: score += 5
|
||||
if "experimental" in name or "exp" in name: score -= 15
|
||||
|
||||
return score
|
||||
|
||||
# Sort by score descending
|
||||
DISCOVERED_MODELS = sorted(all_supported, key=score_model, reverse=True)
|
||||
log_event(f" [+] Discovered {len(DISCOVERED_MODELS)} suitable models.")
|
||||
log_event(f" [+] Top Tier AI: {', '.join(DISCOVERED_MODELS[:3])}")
|
||||
return DISCOVERED_MODELS
|
||||
|
||||
class GeminiDiagnostics:
|
||||
def __init__(self):
|
||||
@@ -32,87 +97,54 @@ class GeminiDiagnostics:
|
||||
return report
|
||||
|
||||
async def resolve_url(url: str) -> str:
|
||||
"""Sigue las redirecciones para obtener la URL larga final, consolidando repositorios y evitando bucles."""
|
||||
"""Sigue las redirecciones para obtener la URL larga final."""
|
||||
shorteners = ['t.co', 'bit.ly', 'buff.ly', 'goo.gl', 'tinyurl.com', 't.ly', 'rb.gy', 'is.gd', 'drp.li', 't.me', 'lnkd.in']
|
||||
try:
|
||||
domain = url.split("//")[-1].split("/")[0].lower()
|
||||
except:
|
||||
return url
|
||||
|
||||
# 1. Expansión Multi-salto (evita intermediarios de tracking)
|
||||
final_url = url
|
||||
max_hops = 5
|
||||
current_hop = 0
|
||||
try: domain = url.split("//")[-1].split("/")[0].lower()
|
||||
except: return url
|
||||
|
||||
final_url, max_hops, current_hop = url, 5, 0
|
||||
async with httpx.AsyncClient(follow_redirects=True, timeout=8) as client:
|
||||
while current_hop < max_hops:
|
||||
try:
|
||||
# Si no es un acortador conocido y ya tenemos una URL larga, paramos
|
||||
current_domain = final_url.split("//")[-1].split("/")[0].lower()
|
||||
if current_hop > 0 and current_domain not in shorteners:
|
||||
break
|
||||
|
||||
if current_hop > 0 and current_domain not in shorteners: break
|
||||
resp = await client.head(final_url, timeout=5)
|
||||
new_url = str(resp.url)
|
||||
if new_url == final_url: break
|
||||
|
||||
final_url = new_url
|
||||
current_hop += 1
|
||||
except:
|
||||
break
|
||||
|
||||
# 2. Consolidación de Repositorios (GitHub/GitLab) con chequeo de MVQ (vía REST si es necesario)
|
||||
repo_domains = ['github.com', 'gitlab.com']
|
||||
current_domain = final_url.split("//")[-1].split("/")[0].lower()
|
||||
|
||||
if any(d in current_domain for d in repo_domains):
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||
resp = await client.head(final_url, timeout=5)
|
||||
if resp.status_code != 200:
|
||||
parts = final_url.split('/')
|
||||
if len(parts) > 4:
|
||||
root_repo = "/".join(parts[:5])
|
||||
resp_root = await client.head(root_repo, timeout=5)
|
||||
if resp_root.status_code == 200:
|
||||
log_event(f" [📦] Consolidación: {final_url} -> {root_repo}")
|
||||
final_url = root_repo
|
||||
except:
|
||||
pass
|
||||
|
||||
final_url, current_hop = new_url, current_hop + 1
|
||||
except: break
|
||||
return final_url
|
||||
|
||||
def is_fuzzy_duplicate(url_a: str, url_b: str) -> bool:
|
||||
"""Detecta si dos URLs son iguales ignorando parámetros de tracking comunes."""
|
||||
def clean(u):
|
||||
u = u.split('#')[0].rstrip('/').lower()
|
||||
# Eliminar parámetros utm_* y otros comunes
|
||||
u = re.sub(r'(\?|&)(utm_[^&]+|s=[^&]+|t=[^&]+|ref=[^&]+)', '', u)
|
||||
if u.endswith('?'): u = u[:-1]
|
||||
return u
|
||||
return u[:-1] if u.endswith('?') else u
|
||||
return clean(url_a) == clean(url_b)
|
||||
|
||||
async def call_gemini_with_retry(prompt: str, response_format: str = "json", max_retries: int = 3):
|
||||
"""
|
||||
Calls Gemini API optimizing for quota usage (pay-per-use).
|
||||
Rotates keys immediately on 429 and uses smart exponential backoff.
|
||||
Calls Gemini API using dynamic discovery and intelligent rotation.
|
||||
Optimizes for cost (Pay-as-you-go) and quality (Pro vs Flash).
|
||||
"""
|
||||
global CURRENT_KEY_INDEX
|
||||
if not GEMINI_API_KEYS:
|
||||
raise ValueError("No GEMINI_API_KEYS configured.")
|
||||
|
||||
# Ensure models are discovered
|
||||
models = await discover_optimal_models()
|
||||
diagnostics = GeminiDiagnostics()
|
||||
|
||||
# Try rotating through all available keys
|
||||
for _ in range(len(GEMINI_API_KEYS)):
|
||||
for key_attempt in range(len(GEMINI_API_KEYS)):
|
||||
api_key = GEMINI_API_KEYS[CURRENT_KEY_INDEX]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
for model in GEMINI_MODELS:
|
||||
for model in models:
|
||||
full_model_name = f"models/{model}"
|
||||
api_url = f"https://generativelanguage.googleapis.com/{GEMINI_API_VERSION}/{full_model_name}:generateContent?key={api_key}"
|
||||
|
||||
key_blocked = False
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
payload = {"contents": [{"parts": [{"text": prompt}]}]}
|
||||
@@ -134,11 +166,16 @@ async def call_gemini_with_retry(prompt: str, response_format: str = "json", max
|
||||
break # Try next model
|
||||
|
||||
elif response.status_code == 429:
|
||||
log_event(f" [!] API 429 on key {CURRENT_KEY_INDEX+1}. Rotating...")
|
||||
log_event(f" [!] API 429 (Rate Limit) on Key {CURRENT_KEY_INDEX+1}. Rotating Key...")
|
||||
CURRENT_KEY_INDEX = (CURRENT_KEY_INDEX + 1) % len(GEMINI_API_KEYS)
|
||||
key_blocked = True
|
||||
break # Break attempt loop
|
||||
# On 429, we recursive call to start over with next key and top model
|
||||
return await call_gemini_with_retry(prompt, response_format, max_retries)
|
||||
|
||||
elif response.status_code == 404:
|
||||
# Model not found for this key or deprecated
|
||||
diagnostics.add_attempt(model, 404, "Not Found/Unsupported")
|
||||
break # Try next model (Pro -> Flash fallback)
|
||||
|
||||
elif response.status_code in [500, 503, 504]:
|
||||
await asyncio.sleep(2 * (attempt + 1))
|
||||
continue
|
||||
@@ -148,21 +185,10 @@ async def call_gemini_with_retry(prompt: str, response_format: str = "json", max
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_msg = f"{type(e).__name__}: {str(e)}"
|
||||
diagnostics.add_attempt(model, 0, error_msg)
|
||||
log_event(f" [!] Model {model} failed with exception: {error_msg}")
|
||||
diagnostics.add_attempt(model, 0, str(e))
|
||||
break
|
||||
|
||||
if key_blocked:
|
||||
break # Break model loop to try next key
|
||||
|
||||
if key_blocked:
|
||||
continue # Try next key_attempt
|
||||
|
||||
# If we are here and didn't succeed with any model, rotate for the next global call
|
||||
# If no model worked for this key, rotate for the next global call
|
||||
CURRENT_KEY_INDEX = (CURRENT_KEY_INDEX + 1) % len(GEMINI_API_KEYS)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
raise Exception(f"Critical Gemini failure after key rotation.\n{diagnostics.get_report()}")
|
||||
|
||||
raise Exception(f"Critical Gemini failure after key and model rotation.\n{diagnostics.get_report()}")
|
||||
|
||||
Reference in New Issue
Block a user