rmi-backend/app/llm_config.py

169 lines
6.1 KiB
Python

"""
RMI Unified LLM Configuration
==============================
Single source of truth for all LLM providers and models.
Imported by all RAG/content modules to avoid scattered config.
Provider chain:
PRIMARY: DeepSeek v4 Flash (cheap, fast) — HyDE, query expansion, chunking
CONTENT: NVIDIA Nemotron 3 Super (free) — reports, briefings, writeups
ANALYSIS: DeepSeek v4 Pro (promo pricing) — agentic investigation
FALLBACK: OpenRouter (auto-routes to best available free model)
"""
import base64
import contextlib
import logging
import os
logger = logging.getLogger(__name__)
# ── Key resolution ────────────────────────────────────────────────
# DeepSeek key may be stored as base64 (Hermes tooling redacts plain keys)
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "")
if os.getenv("LLM_API_KEY_B64"):
with contextlib.suppress(Exception):
os.environ["LLM_API_KEY"] = base64.b64decode(os.getenv("LLM_API_KEY_B64")).decode()
LLM_API_KEY = os.getenv("LLM_API_KEY", OPENROUTER_KEY)
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") # optional, for direct NVIDIA access
# ── Base URLs ──────────────────────────────────────────────────────
DEEPSEEK_BASE = os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1")
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
# ── Models ─────────────────────────────────────────────────────────
# Content generation: market briefings, scam reports, investigation writeups
CONTENT_MODEL = os.getenv(
"RMI_CONTENT_MODEL",
"nvidia/nemotron-3-super-120b-a12b:free", # 1M context, 120B MoE, FREE
)
# Fast/cheap tasks: HyDE, query expansion, contextual chunking
FAST_MODEL = os.getenv("RMI_FAST_MODEL", "deepseek-v4-flash")
# Deep analysis: agentic investigation, multi-hop reasoning
ANALYSIS_MODEL = os.getenv("RAG_ANALYSIS_MODEL", "deepseek-v4-pro")
# Legacy compat
AI_MODEL = os.getenv("LLM_MODEL", FAST_MODEL)
AI_BASE = os.getenv("LLM_BASE_URL", f"{DEEPSEEK_BASE}/chat/completions")
if not LLM_API_KEY or not os.getenv("LLM_API_KEY"):
AI_BASE = f"{OPENROUTER_BASE}/chat/completions"
def get_content_config() -> dict:
"""Get config for content generation (Nemotron 3 Super primary)."""
return {
"model": CONTENT_MODEL,
"base_url": f"{OPENROUTER_BASE}/chat/completions",
"api_key": OPENROUTER_KEY,
"max_tokens": 4096,
"temperature": 0.7,
}
def get_fast_config() -> dict:
"""Get config for fast/cheap tasks (DeepSeek Flash primary)."""
if LLM_API_KEY and DEEPSEEK_BASE:
return {
"model": FAST_MODEL,
"base_url": f"{DEEPSEEK_BASE}/chat/completions",
"api_key": LLM_API_KEY,
"max_tokens": 2048,
"temperature": 0.3,
}
return get_content_config() # fallback to OpenRouter
def get_analysis_config() -> dict:
"""Get config for deep analysis (DeepSeek Pro primary)."""
if LLM_API_KEY and DEEPSEEK_BASE:
return {
"model": ANALYSIS_MODEL,
"base_url": f"{DEEPSEEK_BASE}/chat/completions",
"api_key": LLM_API_KEY,
"max_tokens": 8192,
"temperature": 0.5,
}
return get_content_config()
async def generate_content(
prompt: str,
system: str = "You are a crypto intelligence analyst. Be concise, data-driven, and trader-focused.",
max_tokens: int = 2048,
temperature: float = 0.7,
) -> str:
"""
Generate content using Nemotron 3 Super (free, 1M context).
Auto-falls back to DeepSeek Flash if OpenRouter unavailable.
"""
import httpx
config = get_content_config()
headers = {
"Authorization": f"Bearer {config['api_key']}",
"Content-Type": "application/json",
"HTTP-Referer": "https://rugmunch.io",
"X-Title": "RMI Content Generator",
}
payload = {
"model": config["model"],
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
"max_tokens": max_tokens,
"temperature": temperature,
}
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(config["base_url"], headers=headers, json=payload)
if resp.status_code == 429:
# Rate limited — try fast config
logger.warning("Nemotron rate-limited, falling back to DeepSeek Flash")
fast_cfg = get_fast_config()
payload["model"] = fast_cfg["model"]
headers["Authorization"] = f"Bearer {fast_cfg['api_key']}"
resp = await client.post(fast_cfg["base_url"], headers=headers, json=payload)
resp.raise_for_status()
data = resp.json()
return data["choices"][0]["message"]["content"]
async def generate_briefing(topic: str, context: str = "") -> str:
"""Generate a crypto market briefing."""
prompt = f"Write a concise crypto market briefing about: {topic}"
if context:
prompt += f"\n\nContext:\n{context}"
prompt += "\n\nKeep it punchy, data-driven, and trader-focused. 3-5 sentences max. No intro."
return await generate_content(prompt, max_tokens=512, temperature=0.6)
async def generate_scam_report(findings: dict) -> str:
"""Generate a structured scam investigation report."""
import json
prompt = f"""Write a structured scam investigation report based on these findings:
{json.dumps(findings, indent=2)}
Format:
1. EXECUTIVE SUMMARY (1-2 sentences)
2. TOKEN DETAILS (name, chain, deployer, age)
3. SCAM INDICATORS (list specific patterns found)
4. RISK ASSESSMENT (critical/high/medium/low with confidence)
5. RECOMMENDATION (avoid/caution/monitor)
"""
return await generate_content(
prompt,
system="You are a blockchain security investigator. Be precise, cite specific on-chain evidence.",
max_tokens=2048,
temperature=0.4,
)