codenexus / services /ai_router.py
farhan1221's picture
Upload 26 files
91990f9 verified
Raw
History Blame Contribute Delete
11.8 kB
import httpx
import json
import re
import logging
from config.settings import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
def _extract_json(text: str) -> dict:
"""
Bulletproof JSON extraction for Level 4-8 'Heavyweight' models.
Supports markdown fences, meta-commentary, and nested structures.
"""
text = text.strip()
# 1. Trial: Pure JSON
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 2. Trial: Regex for ```json codes blocks
fence_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if fence_match:
try:
return json.loads(fence_match.group(1).strip())
except json.JSONDecodeError:
pass
# 3. Trial: Brute-force first '{' to last '}'
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
json_str = text[start:end+1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
raise ValueError(f"Could not extract valid JSON from AI response. Status: {text[:100]}...")
def _sanitize_result(data: dict) -> dict:
"""
Ensures that 'issues' and 'suggestions' are always flat lists of strings.
Handles 'Mistral Categorization' where the AI returns objects instead of arrays.
"""
for key in ["issues", "suggestions"]:
val = data.get(key)
if isinstance(val, dict):
# Flatten dictionary: {"category": "issue"} -> ["category: issue"]
new_list = []
for k, v in val.items():
if isinstance(v, list):
new_list.extend([f"{k}: {item}" for item in v])
else:
new_list.append(f"{k}: {v}")
data[key] = new_list
elif val and not isinstance(val, list):
data[key] = [str(val)]
elif not val:
data[key] = []
return data
# Smart Model Dictionary: Maps logical names to provider-specific model IDs
# Ordered from Level 1 (Entry) to Level 8 (Flagship)
MODEL_MAP = {
"gemma-4-31b": {
"openrouter": "google/gemma-4-31b-it:free"
},
"llama-3.1": {
"groq": "llama-3.1-8b-instant",
"openrouter": "meta-llama/llama-3.1-8b-instruct:free"
},
"qwen-2.5": {
"openrouter": "qwen/qwen-2.5-7b-instruct:free",
"huggingface": "Qwen/Qwen2.5-7B-Instruct"
},
"nemotron-120b": {
"openrouter": "nvidia/nemotron-3-super-120b-a12b:free"
},
"minimax-2.5": {
"openrouter": "minimax/minimax-m2.5:free"
},
"mistral-large": {
"openrouter": "mistralai/mistral-large-2407"
},
"groq-70b": {
"groq": "llama-3.3-70b-versatile",
"openrouter": "meta-llama/llama-3.3-70b-instruct:free"
},
"gemini-flash": {
"gemini": "gemini-2.0-flash",
"openrouter": "google/gemini-2.0-flash-001"
}
}
async def analyze_with_groq(prompt: str, model_id: str = "llama-3.1-8b-instant") -> dict:
"""Backup AI — Groq with specified model."""
if not settings.groq_api_key:
raise Exception("Groq API key not configured")
# Dynamic timeout for large codebases
timeout = 60.0 if len(prompt) > 5000 else 30.0
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.groq_api_key}", "Content-Type": "application/json"},
json={
"model": model_id,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"max_tokens": 2000 # Increased for large code analysis
}
)
if response.status_code != 200:
raise Exception(f"Groq error {response.status_code}: {response.text[:200]}")
result = response.json()
text = result["choices"][0]["message"]["content"].strip()
return _sanitize_result(_extract_json(text))
async def analyze_with_gemini(prompt: str, model_id: str = "gemini-2.0-flash") -> dict:
"""Primary AI — Gemini with specified model."""
if not settings.gemini_api_key:
raise Exception("Gemini API key not configured")
# Support large context in Gemini
timeout = 60.0 if len(prompt) > 5000 else 30.0
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/{model_id}:generateContent?key={settings.gemini_api_key}",
json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"maxOutputTokens": 2000} # Increased
}
)
if response.status_code != 200:
raise Exception(f"Gemini error {response.status_code}: {response.text[:200]}")
result = response.json()
candidates = result.get("candidates")
if not candidates:
raise Exception("Gemini returned no candidates")
text = candidates[0]["content"]["parts"][0]["text"].strip()
return _sanitize_result(_extract_json(text))
async def analyze_with_openrouter(prompt: str, model_id: str = None) -> dict:
"""Last Resort AI — OpenRouter with customizable model."""
if not settings.openrouter_api_key:
raise Exception("OpenRouter API key not configured")
# Use settings default if no specific model requested
target_model = model_id or settings.openrouter_model
# Support large context in OpenRouter
timeout = 60.0 if len(prompt) > 5000 else 30.0
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.openrouter_api_key}", "Content-Type": "application/json"},
json={
"model": target_model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 2000 # Increased
}
)
if response.status_code != 200:
raise Exception(f"OpenRouter error {response.status_code}: {response.text[:200]}")
result = response.json()
if "choices" not in result:
raise Exception(f"Unexpected OpenRouter response: {result}")
text = result["choices"][0]["message"]["content"].strip()
return _sanitize_result(_extract_json(text))
async def analyze_with_huggingface(prompt: str, model_id: str = None) -> dict:
"""Extra Backup — Hugging Face with specific model OR rotation."""
if not settings.huggingface_api_token:
raise Exception("Hugging Face API token not configured")
# If a specific model is requested, try ONLY that one
# Otherwise, use the rotation logic
models_to_try = [model_id] if model_id else settings.huggingface_free_models
last_error = None
for target_model in models_to_try:
try:
timeout = 60.0 if len(prompt) > 5000 else 30.0
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
f"https://api-inference.huggingface.co/models/{target_model}",
headers={"Authorization": f"Bearer {settings.huggingface_api_token}"},
json={
"inputs": prompt,
"parameters": {"max_new_tokens": 2000, "return_full_text": False}
}
)
if response.status_code != 200:
raise Exception(f"HF Model {target_model} failed ({response.status_code})")
result = response.json()
text = result[0]["generated_text"].strip() if isinstance(result, list) else result.get("generated_text", "").strip()
return _sanitize_result(_extract_json(text))
except Exception as e:
logger.warning(f"Hugging Face model {target_model} failed: {e}")
last_error = e
if model_id: # Don't rotate if specific model was forced
break
continue
raise Exception(f"Hugging Face attempt failed. Last error: {last_error}")
async def route_analysis(prompt: str, model_choice: str = "auto") -> dict:
"""
Smart AI router with Zero-Failure 'Smart Recovery' logic.
Tiered Chain: Gemma 4 -> Llama 3.1 -> Qwen 2.5 -> Nemotron 120B ->
MiniMax 2.5 -> Mistral Large -> Groq 70B -> Gemini Flash
"""
# 1. Prepare Provider List
providers = []
# Handle Specific Model Choice (With Recovery)
if model_choice != "auto" and model_choice in MODEL_MAP:
config = MODEL_MAP[model_choice]
if "groq" in config:
providers.append({"func": lambda p: analyze_with_groq(p, config["groq"]), "name": model_choice})
if "gemini" in config:
providers.append({"func": lambda p: analyze_with_gemini(p, config["gemini"]), "name": model_choice})
if "openrouter" in config:
providers.append({"func": lambda p: analyze_with_openrouter(p, config["openrouter"]), "name": model_choice})
if "huggingface" in config:
providers.append({"func": lambda p: analyze_with_huggingface(p, config["huggingface"]), "name": model_choice})
# SMART RECOVERY: If the specific choice fails, pivot to the full AUTO chain
providers.append({"func": lambda p: route_analysis(p, "auto"), "is_meta": True})
else:
# Full Power Progression Chain (8 Levels)
order = ["gemma-4-31b", "llama-3.1", "qwen-2.5", "nemotron-120b", "minimax-2.5", "mistral-large", "groq-70b", "gemini-flash"]
for m_id in order:
m_cfg = MODEL_MAP[m_id]
# Primary provider logic for auto-chain
if m_id == "llama-3.1": # Prioritize Groq's 8B for speed
providers.append({"func": lambda p: analyze_with_groq(p, "llama-3.1-8b-instant"), "name": m_id})
elif m_id == "groq-70b": # Prioritize Groq's 70B
providers.append({"func": lambda p: analyze_with_groq(p, "llama-3.3-70b-versatile"), "name": m_id})
elif m_id == "gemini-flash":
providers.append({"func": lambda p: analyze_with_gemini(p), "name": m_id})
elif "openrouter" in m_cfg:
providers.append({"func": lambda p: analyze_with_openrouter(p, m_cfg["openrouter"]), "name": m_id})
# Ultimate Last Resort
providers.append({"func": lambda p: analyze_with_openrouter(p, "openrouter/free"), "name": "openrouter-free-fallback"})
# 2. Execute with Failover Logic
last_error = None
for i, p_item in enumerate(providers):
try:
p_name = p_item.get("name", "failover-chain")
logger.info(f"Attempting {p_name} ({i+1}/{len(providers)})")
result = await p_item["func"](prompt)
# If it's a metadata-wrapped result from a nested call, return it
if isinstance(result, dict) and "_actual_model" in result:
return result
# Attach actual model name to the result
if isinstance(result, dict):
result["_actual_model"] = p_name
return result
except Exception as e:
logger.error(f"Provider {i+1} failed: {str(e)}")
last_error = e
continue
raise Exception(f"Absolute failure in AI Hub. Last error: {last_error}")