- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
169 lines
6.1 KiB
Python
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,
|
|
)
|