- 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>
930 lines
33 KiB
Python
930 lines
33 KiB
Python
"""
|
||
RugCharts Model Registry & Quality Standards
|
||
=============================================
|
||
Smart model routing across free providers. Quality review pipeline.
|
||
All AI tasks go through this module. All output meets human standards.
|
||
|
||
Free Models Available (OpenRouter):
|
||
NVIDIA Nemotron 3 Super 120B - research, analysis, long context (1M)
|
||
Google Gemma 4 26B - writing, prose, natural language
|
||
NVIDIA Nemotron Nano 30B - reasoning, classification
|
||
Qwen3 Coder 480B - code generation, tool use
|
||
Moonshot Kimi K2.6 - fast writing, summaries
|
||
Z.ai GLM 4.5 Air - general purpose, fast
|
||
OpenAI gpt-oss-120b - heavy reasoning, agentic tasks
|
||
OpenAI gpt-oss-20b - lightweight, fast inference
|
||
Liquid LFM 2.5 1.2B - edge, tiny tasks, classification
|
||
|
||
Other Free Providers:
|
||
Groq (Llama 3.1 8B, Llama 3.3 70B) - 14,400 RPD free
|
||
Mistral (via OpenRouter free tier)
|
||
DeepSeek Flash V4 - $0.14/M (near-free with prefix caching)
|
||
|
||
Quality Standards:
|
||
NO: "delve", "tapestry", "landscape", "robust", "moreover", "furthermore",
|
||
"in conclusion", "it is worth noting", "underscores", "showcasing",
|
||
"a testament to", "in the realm of", "paradigm shift"
|
||
YES: direct, specific, human voice, numbers, names, concrete details
|
||
ALWAYS: review step before publishing
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import time
|
||
from collections import defaultdict
|
||
|
||
import httpx
|
||
|
||
logger = logging.getLogger("model_registry")
|
||
|
||
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
||
GROQ_KEY = os.getenv("GROQ_API_KEY", "")
|
||
MISTRAL_KEY = os.getenv("MISTRAL_API_KEY", "")
|
||
OR_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
|
||
MISTRAL_URL = "https://api.mistral.ai/v1/chat/completions"
|
||
|
||
# ── Model Registry ─────────────────────────────────────────────────
|
||
|
||
MODELS = {
|
||
# ── RESEARCH & ANALYSIS ──
|
||
"research": {
|
||
"primary": {
|
||
"id": "nvidia/nemotron-3-super-120b-a12b:free",
|
||
"provider": "openrouter",
|
||
"context": 1000000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["long_context", "analysis", "data_synthesis", "multi_document"],
|
||
},
|
||
"fallback": {
|
||
"id": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["reasoning", "analysis", "structured_output"],
|
||
},
|
||
},
|
||
# ── WRITING & PROSE ──
|
||
"writing": {
|
||
"primary": {
|
||
"id": "nvidia/nemotron-3-super-120b-a12b:free",
|
||
"provider": "openrouter",
|
||
"context": 1000000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["natural_prose", "long_context", "creative"],
|
||
},
|
||
"fallback": {
|
||
"id": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["reasoning", "writing", "structured"],
|
||
},
|
||
},
|
||
# ── CODE & TOOL USE ──
|
||
"coding": {
|
||
"primary": {
|
||
"id": "nvidia/nemotron-3-super-120b-a12b:free",
|
||
"provider": "openrouter",
|
||
"context": 1000000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["code_gen", "agentic", "long_context"],
|
||
},
|
||
"fallback": {
|
||
"id": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["reasoning", "code", "structured_output"],
|
||
},
|
||
},
|
||
# ── REVIEW & QUALITY CHECK ──
|
||
"review": {
|
||
"primary": {
|
||
"id": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["proofreading", "error_detection", "consistency"],
|
||
},
|
||
"fallback": {
|
||
"id": "z-ai/glm-4.5-air:free",
|
||
"provider": "openrouter",
|
||
"context": 131000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "classification", "simple_tasks"],
|
||
},
|
||
},
|
||
# ── FAST / LIGHTWEIGHT ──
|
||
"fast": {
|
||
"primary": {
|
||
"id": "z-ai/glm-4.5-air:free",
|
||
"provider": "openrouter",
|
||
"context": 131000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "classification", "simple_tasks"],
|
||
},
|
||
"fallback": {
|
||
"id": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 20,
|
||
"strengths": ["reasoning", "general", "reliable"],
|
||
},
|
||
"groq": {
|
||
"id": "llama-3.1-8b-instant",
|
||
"provider": "groq",
|
||
"context": 128000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "sub_100ms_ttft", "high_throughput"],
|
||
},
|
||
},
|
||
# ── WRITING (Groq) ──
|
||
"writing_groq": {
|
||
"primary": {
|
||
"id": "llama-3.3-70b-versatile",
|
||
"provider": "groq",
|
||
"context": 128000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["writing", "speed", "quality_prose"],
|
||
},
|
||
"fallback": {
|
||
"id": "llama-3.1-8b-instant",
|
||
"provider": "groq",
|
||
"context": 128000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "throughput", "reliable"],
|
||
},
|
||
},
|
||
# ── MISTRAL FALLBACKS (free tier, 6 models) ──
|
||
"mistral_write": {
|
||
"primary": {
|
||
"id": "mistral-small-latest",
|
||
"provider": "mistral",
|
||
"context": 262144,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["writing", "balanced", "multilingual"],
|
||
},
|
||
"fallback": {
|
||
"id": "ministral-8b-latest",
|
||
"provider": "mistral",
|
||
"context": 262144,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "efficient", "good_prose"],
|
||
},
|
||
},
|
||
"mistral_code": {
|
||
"primary": {
|
||
"id": "codestral-latest",
|
||
"provider": "mistral",
|
||
"context": 256000,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["code_gen", "fill_in_middle", "agentic"],
|
||
},
|
||
},
|
||
"mistral_fast": {
|
||
"primary": {
|
||
"id": "ministral-3b-latest",
|
||
"provider": "mistral",
|
||
"context": 131072,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "tiny", "classification"],
|
||
},
|
||
"fallback": {
|
||
"id": "mistral-tiny-latest",
|
||
"provider": "mistral",
|
||
"context": 131072,
|
||
"cost_per_1k": 0,
|
||
"rpm": 30,
|
||
"strengths": ["speed", "simple_tasks", "high_throughput"],
|
||
},
|
||
},
|
||
}
|
||
|
||
# ── AI ROLE ARCHITECTURE ──────────────────────────────────────────
|
||
# Each role isolated. Each gets its own model + budget. Never interfere.
|
||
|
||
AI_ROLES = {
|
||
"advisor": {
|
||
"name": "Platform Advisor",
|
||
"emoji": "🛡️",
|
||
"description": "Monitors system health, rate limits, anomalies. Proactive alerts.",
|
||
"model": "nvidia/nemotron-3-nano-30b-a3b:free",
|
||
"provider": "openrouter",
|
||
"budget": {"per_hour": 10, "per_day": 50},
|
||
"temperature": 0.2,
|
||
"data_classifier": {
|
||
"name": "Data Classifier",
|
||
"emoji": "🏷️",
|
||
"description": "Categorizes articles, detects sentiment, tags content. High throughput on Groq.",
|
||
"model": "llama-3.1-8b-instant",
|
||
"provider": "groq",
|
||
"fallback": "ministral-3b-latest",
|
||
"fallback_provider": "mistral",
|
||
"budget": {"per_minute": 25, "per_day": 3000},
|
||
"temperature": 0.1,
|
||
},
|
||
"social_writer": {
|
||
"name": "Social Media Writer",
|
||
"emoji": "𝕏", # noqa: RUF001
|
||
"description": "X/Twitter posts, Telegram messages. Runs on Groq, high throughput.",
|
||
"model": "llama-3.1-8b-instant",
|
||
"provider": "groq",
|
||
"fallback": "mistral-small-latest",
|
||
"fallback_provider": "mistral",
|
||
"budget": {"per_task": 2, "per_day": 50},
|
||
"temperature": 0.8,
|
||
},
|
||
"cron_worker": {
|
||
"name": "Cron Worker",
|
||
"emoji": "⏰",
|
||
"description": "Scheduled tasks. Primary on Mistral (unlimited), fallback Groq.",
|
||
"model": "ministral-3b-latest",
|
||
"provider": "mistral",
|
||
"fallback": "llama-3.1-8b-instant",
|
||
"fallback_provider": "groq",
|
||
"budget": {"per_task": 5, "per_day": 200},
|
||
"temperature": 0.5,
|
||
},
|
||
"content_writer": {
|
||
"name": "Content Writer",
|
||
"emoji": "✍️",
|
||
"description": "Quality prose. Mistral primary, Groq for volume.",
|
||
"model": "mistral-small-latest",
|
||
"provider": "mistral",
|
||
"fallback": "llama-3.3-70b-versatile",
|
||
"fallback_provider": "groq",
|
||
"budget": {"per_task": 3, "per_day": 30},
|
||
"temperature": 0.7,
|
||
},
|
||
"advisor": {
|
||
"name": "Platform Advisor",
|
||
"emoji": "🛡️",
|
||
"description": "System health. Uses Groq (never touches OpenRouter research quota).",
|
||
"model": "llama-3.3-70b-versatile",
|
||
"provider": "groq",
|
||
"fallback": "mistral-small-latest",
|
||
"fallback_provider": "mistral",
|
||
"budget": {"per_hour": 10, "per_day": 100},
|
||
"temperature": 0.2,
|
||
},
|
||
},
|
||
"rag_embedder": {
|
||
"name": "RAG Embedder",
|
||
"emoji": "🧠",
|
||
"description": "Vector embeddings. Uses NVIDIA NIM directly (NOT OpenRouter) to avoid quota conflict. Batch + cache.",
|
||
"model": "nvidia/nemo-embed-12b",
|
||
"provider": "nvidia_nim",
|
||
"budget": {"per_day": 50000, "batch_size": 100},
|
||
"temperature": 0.0,
|
||
"strategy": "BATCH: embed 100 docs per call. CACHE: never re-embed. LOCAL: consider sentence-transformers for hot path.",
|
||
},
|
||
"security_auditor": {
|
||
"name": "Security Auditor",
|
||
"emoji": "🔐",
|
||
"description": "Scans code/configs for vulnerabilities, exposed keys, unsafe patterns.",
|
||
"model": "nvidia/nemotron-3-super-120b-a12b:free",
|
||
"provider": "openrouter",
|
||
"budget": {"per_task": 5, "per_day": 10},
|
||
"temperature": 0.1,
|
||
},
|
||
"data_classifier": {
|
||
"name": "Data Classifier",
|
||
"emoji": "🏷️",
|
||
"description": "Categorizes articles, detects sentiment, tags content. High throughput.",
|
||
"model": "ministral-3b-latest",
|
||
"provider": "mistral",
|
||
"fallback": "z-ai/glm-4.5-air:free",
|
||
"fallback_provider": "openrouter",
|
||
"budget": {"per_minute": 20, "per_day": 500},
|
||
"temperature": 0.1,
|
||
},
|
||
"social_writer": {
|
||
"name": "Social Media Writer",
|
||
"emoji": "𝕏", # noqa: RUF001
|
||
"description": "X/Twitter posts, Telegram messages. Punchy, engaging, native to platform.",
|
||
"model": "mistral-small-latest",
|
||
"provider": "mistral",
|
||
"fallback": "llama-3.1-8b-instant",
|
||
"fallback_provider": "groq",
|
||
"budget": {"per_task": 2, "per_day": 20},
|
||
"temperature": 0.8,
|
||
},
|
||
"fact_checker": {
|
||
"name": "Fact Checker",
|
||
"emoji": "✅",
|
||
"description": "Verifies claims against known data. Cross-references sources.",
|
||
"model": "nvidia/nemotron-3-super-120b-a12b:free",
|
||
"provider": "openrouter",
|
||
"budget": {"per_task": 3, "per_day": 15},
|
||
"temperature": 0.1,
|
||
},
|
||
}
|
||
|
||
# ── PROVIDER RATE LIMITS (verified June 2026) ─────────────────────
|
||
# These are HARD LIMITS - going over means 429 errors and downtime.
|
||
|
||
PROVIDER_LIMITS = {
|
||
"openrouter": {
|
||
"name": "OpenRouter",
|
||
"rpm": 20, # requests per minute
|
||
"rpd_free_no_credits": 50, # free users without credits
|
||
"rpd_free_with_credits": 1000, # $10+ credits purchased
|
||
"current_tier": "paid", # user has spent money = higher tier
|
||
"free_model_suffix": ":free",
|
||
"check_endpoint": "https://openrouter.ai/api/v1/key",
|
||
},
|
||
"groq": {
|
||
"name": "Groq",
|
||
"rpm": 30, # requests per minute
|
||
"rpd": 14400, # requests per day (free tier)
|
||
"tpm": 6000, # tokens per minute (approx)
|
||
"current_tier": "free",
|
||
"models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"],
|
||
},
|
||
"mistral": {
|
||
"name": "Mistral",
|
||
"rps": 1, # requests per second (1/sec)
|
||
"tpm": 500000, # tokens per minute (free tier)
|
||
"tpm_budget": 1000000000, # tokens per month (1B free)
|
||
"current_tier": "free",
|
||
},
|
||
"nvidia_nim": {
|
||
"name": "NVIDIA NIM",
|
||
"rpm": 100, # generous free tier
|
||
"rpd": 5000, # daily requests
|
||
"current_tier": "free",
|
||
"base_url": "https://integrate.api.nvidia.com/v1",
|
||
"key_models": [
|
||
"nvidia/nemotron-3-super-120b-a12b", # 1M ctx, best research
|
||
"nvidia/nemotron-3-nano-30b-a3b", # fast reasoning
|
||
"nvidia/nv-embedqa-e5-v5", # embeddings!
|
||
"nvidia/llama-3.3-nemotron-super-49b-v1", # Llama Nemotron
|
||
"nvidia/nemotron-4-340b-instruct", # 340B monster
|
||
"meta/llama-3.3-70b-instruct", # Llama 3.3 70B
|
||
"deepseek-ai/deepseek-v4-flash", # DeepSeek V4 Flash
|
||
"google/gemma-4-31b-it", # Gemma 4 31B
|
||
"mistralai/mistral-large-3-675b-instruct", # Mistral Large 675B
|
||
"qwen/qwen3-coder-480b-a35b-instruct", # Qwen Coder 480B
|
||
"baai/bge-m3", # BGE embedder
|
||
"snowflake/arctic-embed-line", # Arctic embedder
|
||
],
|
||
},
|
||
}
|
||
|
||
# ── INTELLIGENT USAGE TRACKER ─────────────────────────────────────
|
||
# Tracks per-minute, per-hour, per-day usage. Never exceeds limits.
|
||
|
||
|
||
class RateLimitTracker:
|
||
"""Tracks API usage across all providers. Respects hard limits.
|
||
|
||
Budget allocation (of 1,000 OpenRouter + 14,400 Groq + Mistral):
|
||
- Daily Intel report: 3-5 calls/day (research + write + review)
|
||
- CT Rundown: 1-2 calls/day (summarize)
|
||
- Content review: 5-10 calls/day (quality checks)
|
||
- Background tasks: 10-20 calls/day (classification, enrichment)
|
||
- Peak headroom: ~950 calls/day remaining for bursts
|
||
"""
|
||
|
||
def __init__(self):
|
||
self._minute: dict[str, int] = defaultdict(int) # provider → calls this minute
|
||
self._hour: dict[str, int] = defaultdict(int)
|
||
self._day: dict[str, int] = defaultdict(int)
|
||
self._minute_start = time.time()
|
||
self._hour_start = time.time()
|
||
self._day_start = time.time()
|
||
self._total_calls = 0
|
||
self._throttled = 0
|
||
|
||
def _reset_windows(self):
|
||
now = time.time()
|
||
if now - self._minute_start > 60:
|
||
self._minute.clear()
|
||
self._minute_start = now
|
||
if now - self._hour_start > 3600:
|
||
self._hour.clear()
|
||
self._hour_start = now
|
||
if now - self._day_start > 86400:
|
||
self._day.clear()
|
||
self._day_start = now
|
||
|
||
def can_call(self, provider: str) -> tuple[bool, str]:
|
||
"""Check if we can make a call to this provider without exceeding limits."""
|
||
self._reset_windows()
|
||
limits = PROVIDER_LIMITS.get(provider, {})
|
||
if not limits:
|
||
return True, ""
|
||
|
||
# Per-minute check
|
||
rpm = limits.get("rpm", 20)
|
||
if self._minute[provider] >= rpm:
|
||
wait = 60 - (time.time() - self._minute_start)
|
||
return False, f"{provider}: RPM limit ({rpm}/min), retry in {wait:.0f}s"
|
||
|
||
# Per-day check
|
||
if provider == "openrouter":
|
||
rpd = limits.get("rpd_free_with_credits", 1000)
|
||
elif provider == "groq":
|
||
rpd = limits.get("rpd", 14400)
|
||
else:
|
||
rpd = limits.get("rpd", 100000) # Mistral: effectively unlimited for requests
|
||
|
||
if self._day[provider] >= rpd:
|
||
return False, f"{provider}: Daily limit ({rpd}/day) exhausted"
|
||
|
||
# Mistral: 1 req/sec check
|
||
if provider == "mistral" and limits.get("rps", 1):
|
||
if self._minute[provider] >= 58: # Leave 2/sec headroom
|
||
return False, "mistral: nearing RPS limit"
|
||
|
||
return True, ""
|
||
|
||
def record_call(self, provider: str, tokens: int = 0):
|
||
"""Record a successful API call."""
|
||
self._reset_windows()
|
||
self._minute[provider] += 1
|
||
self._hour[provider] += 1
|
||
self._day[provider] += 1
|
||
self._total_calls += 1
|
||
|
||
def record_throttle(self, provider: str):
|
||
"""Record a throttled/blocked call."""
|
||
self._throttled += 1
|
||
|
||
def budget_remaining(self, provider: str) -> dict:
|
||
"""Get remaining budget for a provider."""
|
||
self._reset_windows()
|
||
limits = PROVIDER_LIMITS.get(provider, {})
|
||
rpm = limits.get("rpm", 20)
|
||
|
||
if provider == "openrouter":
|
||
rpd = limits.get("rpd_free_with_credits", 1000)
|
||
elif provider == "groq":
|
||
rpd = limits.get("rpd", 14400)
|
||
else:
|
||
rpd = 100000
|
||
|
||
return {
|
||
"provider": provider,
|
||
"minute_used": self._minute[provider],
|
||
"minute_limit": rpm,
|
||
"minute_remaining": max(0, rpm - self._minute[provider]),
|
||
"day_used": self._day[provider],
|
||
"day_limit": rpd,
|
||
"day_remaining": max(0, rpd - self._day[provider]),
|
||
"day_pct": round(self._day[provider] / max(rpd, 1) * 100, 1),
|
||
}
|
||
|
||
def stats(self) -> dict:
|
||
"""Full usage statistics."""
|
||
return {
|
||
"total_calls": self._total_calls,
|
||
"throttled": self._throttled,
|
||
"providers": {p: self.budget_remaining(p) for p in PROVIDER_LIMITS},
|
||
"budget_allocation": {
|
||
"daily_intel": "3-5 calls/day",
|
||
"ct_rundown": "1-2 calls/day",
|
||
"content_review": "5-10 calls/day",
|
||
"background": "10-20 calls/day",
|
||
"headroom": f"~{1000 - self._day.get('openrouter', 0)} calls remaining today",
|
||
},
|
||
}
|
||
|
||
|
||
# Global tracker instance
|
||
rate_tracker = RateLimitTracker()
|
||
|
||
|
||
def _can_use(model_config: dict) -> bool:
|
||
"""Check if model is under its rate limit using the tracker."""
|
||
provider = model_config.get("provider", "openrouter")
|
||
can, reason = rate_tracker.can_call(provider)
|
||
if not can:
|
||
logger.debug(f"Rate limited: {reason}")
|
||
rate_tracker.record_throttle(provider)
|
||
return False
|
||
return True
|
||
|
||
|
||
def _track_usage(model_id: str, tokens: int = 0):
|
||
"""Track model usage through the rate tracker."""
|
||
for _provider, _limits in PROVIDER_LIMITS.items():
|
||
# Match model to provider
|
||
model_providers = {
|
||
"openrouter": [
|
||
"nvidia/",
|
||
"z-ai/",
|
||
"google/",
|
||
"qwen/",
|
||
"openai/",
|
||
"moonshotai/",
|
||
"liquid/",
|
||
"openrouter/",
|
||
],
|
||
"groq": ["llama-3", "llama-4", "mixtral", "gemma"],
|
||
"mistral": ["mistral", "ministral", "codestral", "open-mistral"],
|
||
}
|
||
for p, prefixes in model_providers.items():
|
||
if any(model_id.startswith(pref) for pref in prefixes):
|
||
rate_tracker.record_call(p, tokens)
|
||
return
|
||
|
||
|
||
async def _call_openrouter(
|
||
model_id: str, system: str, user: str, max_tokens: int = 1000, temperature: float = 0.5
|
||
) -> str:
|
||
"""Call OpenRouter API."""
|
||
if not OPENROUTER_KEY:
|
||
return ""
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=90) as c:
|
||
r = await c.post(
|
||
OR_URL,
|
||
headers={
|
||
"Authorization": f"Bearer {OPENROUTER_KEY}",
|
||
"Content-Type": "application/json",
|
||
"HTTP-Referer": "https://rugmunch.io",
|
||
"X-Title": "RugCharts AI",
|
||
},
|
||
json={
|
||
"model": model_id,
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
"messages": [
|
||
{"role": "system", "content": system},
|
||
{"role": "user", "content": user},
|
||
],
|
||
},
|
||
)
|
||
if r.status_code == 200:
|
||
resp = r.json()
|
||
usage = resp.get("usage", {})
|
||
_track_usage(model_id, usage.get("total_tokens", 0))
|
||
return resp["choices"][0]["message"]["content"]
|
||
else:
|
||
logger.warning(f"OpenRouter {model_id}: {r.status_code}")
|
||
return ""
|
||
except Exception as e:
|
||
logger.warning(f"OpenRouter error {model_id}: {e}")
|
||
return ""
|
||
|
||
|
||
async def _call_groq(model_id: str, system: str, user: str, max_tokens: int = 1000, temperature: float = 0.5) -> str:
|
||
"""Call Groq API (free tier)."""
|
||
if not GROQ_KEY:
|
||
return ""
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=60) as c:
|
||
r = await c.post(
|
||
GROQ_URL,
|
||
headers={"Authorization": f"Bearer {GROQ_KEY}", "Content-Type": "application/json"},
|
||
json={
|
||
"model": model_id,
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
"messages": [
|
||
{"role": "system", "content": system},
|
||
{"role": "user", "content": user},
|
||
],
|
||
},
|
||
)
|
||
if r.status_code == 200:
|
||
return r.json()["choices"][0]["message"]["content"]
|
||
else:
|
||
logger.warning(f"Groq {model_id}: {r.status_code} {r.text[:200]}")
|
||
return ""
|
||
except Exception as e:
|
||
logger.warning(f"Groq error: {e}")
|
||
return ""
|
||
|
||
|
||
async def _call_mistral(model_id: str, system: str, user: str, max_tokens: int = 1000, temperature: float = 0.5) -> str:
|
||
"""Call Mistral API (free tier)."""
|
||
if not MISTRAL_KEY:
|
||
return ""
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=60) as c:
|
||
r = await c.post(
|
||
MISTRAL_URL,
|
||
headers={
|
||
"Authorization": f"Bearer {MISTRAL_KEY}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
json={
|
||
"model": model_id,
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
"messages": [
|
||
{"role": "system", "content": system},
|
||
{"role": "user", "content": user},
|
||
],
|
||
},
|
||
)
|
||
if r.status_code == 200:
|
||
_track_usage(model_id, max_tokens)
|
||
return r.json()["choices"][0]["message"]["content"]
|
||
else:
|
||
logger.warning(f"Mistral {model_id}: {r.status_code}")
|
||
return ""
|
||
except Exception as e:
|
||
logger.warning(f"Mistral error: {e}")
|
||
return ""
|
||
|
||
|
||
async def ai_call(
|
||
task_type: str,
|
||
system_prompt: str,
|
||
user_prompt: str,
|
||
max_tokens: int = 1000,
|
||
temperature: float = 0.5,
|
||
) -> str:
|
||
"""THE method. Call the best free model for a task type.
|
||
|
||
Routes to: research, writing, coding, review, fast.
|
||
Falls back: primary → fallback → groq → mistral → any available.
|
||
Three providers: OpenRouter (3 models), Groq (2 models), Mistral (6 models).
|
||
Zero cost. Always finds a model.
|
||
"""
|
||
if task_type not in MODELS:
|
||
task_type = "fast"
|
||
|
||
config = MODELS[task_type]
|
||
|
||
# Try all tiers in order
|
||
tiers = ["primary", "fallback", "groq"]
|
||
|
||
for tier in tiers:
|
||
if tier not in config:
|
||
continue
|
||
model = config[tier]
|
||
if not _can_use(model):
|
||
continue
|
||
|
||
if model["provider"] == "openrouter":
|
||
result = await _call_openrouter(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
elif model["provider"] == "groq":
|
||
result = await _call_groq(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
elif model["provider"] == "mistral":
|
||
result = await _call_mistral(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
else:
|
||
continue
|
||
|
||
if result:
|
||
return result
|
||
|
||
# ── Extended fallback: try Mistral models ──
|
||
mistral_tasks = ["mistral_fast", "mistral_write", "mistral_code"]
|
||
for mt in mistral_tasks:
|
||
if mt == task_type:
|
||
continue
|
||
mconfig = MODELS.get(mt, {})
|
||
for tier in ["primary", "fallback"]:
|
||
if tier not in mconfig:
|
||
continue
|
||
model = mconfig[tier]
|
||
if _can_use(model):
|
||
result = await _call_mistral(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
if result:
|
||
return result
|
||
|
||
# ── Last resort: try any available free model ──
|
||
for backup_type in ["fast", "writing", "writing_groq"]:
|
||
if backup_type == task_type:
|
||
continue
|
||
backup_config = MODELS[backup_type]
|
||
for tier_name in ["primary", "fallback", "groq"]:
|
||
if tier_name in backup_config:
|
||
model = backup_config[tier_name]
|
||
if _can_use(model):
|
||
if model["provider"] == "openrouter":
|
||
result = await _call_openrouter(
|
||
model["id"], system_prompt, user_prompt, max_tokens, temperature
|
||
)
|
||
elif model["provider"] == "groq":
|
||
result = await _call_groq(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
elif model["provider"] == "mistral":
|
||
result = await _call_mistral(model["id"], system_prompt, user_prompt, max_tokens, temperature)
|
||
if result:
|
||
return result
|
||
|
||
return ""
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# QUALITY STANDARDS & REVIEW
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
FORBIDDEN_WORDS = [
|
||
"delve",
|
||
"tapestry",
|
||
"landscape",
|
||
"robust",
|
||
"moreover",
|
||
"furthermore",
|
||
"in conclusion",
|
||
"it is worth noting",
|
||
"underscores",
|
||
"showcasing",
|
||
"a testament to",
|
||
"in the realm of",
|
||
"paradigm shift",
|
||
"game changer",
|
||
"revolutionize",
|
||
"disrupt",
|
||
"unprecedented",
|
||
"groundbreaking",
|
||
"synergy",
|
||
"ecosystem",
|
||
"holistic",
|
||
"cutting-edge",
|
||
"state-of-the-art",
|
||
"leveraging",
|
||
"utilize",
|
||
"facilitate",
|
||
"spearhead",
|
||
]
|
||
|
||
QUALITY_REVIEW_PROMPT = """You are a ruthless editor at RugCharts. Review this content against STRICT standards:
|
||
|
||
FORBIDDEN (mark as FAIL if found):
|
||
- "delve", "tapestry", "landscape", "robust", "moreover", "furthermore"
|
||
- "in conclusion", "it is worth noting", "underscores", "showcasing"
|
||
- "a testament to", "in the realm of", "paradigm shift"
|
||
- Any vague, corporate, or AI-slop language
|
||
- Overused crypto clichés ("to the moon", "wagmi", "ngmi", "wen")
|
||
|
||
REQUIRED (mark as FAIL if missing):
|
||
- Specific numbers, names, percentages
|
||
- Human, conversational tone (reads like a sharp newsletter)
|
||
- No passive voice where active works better
|
||
- Short paragraphs. Varied sentence length.
|
||
- Hooks the reader in first 2 sentences
|
||
|
||
OUTPUT FORMAT - JSON only:
|
||
{
|
||
"pass": true/false,
|
||
"score": 0-100,
|
||
"issues": ["list of specific problems found"],
|
||
"fixed_version": "rewritten version if score < 80, otherwise original"
|
||
}
|
||
|
||
CONTENT TO REVIEW:
|
||
"""
|
||
|
||
|
||
async def review_content(content: str, content_type: str = "article") -> dict:
|
||
"""Review content against quality standards. Returns pass/fail with fixes."""
|
||
if len(content) < 50:
|
||
return {"pass": True, "score": 100, "issues": [], "fixed_version": content}
|
||
|
||
# ── Automated checks (no AI needed) ──
|
||
issues = []
|
||
content_lower = content.lower()
|
||
|
||
for word in FORBIDDEN_WORDS:
|
||
if word in content_lower:
|
||
issues.append(f"Forbidden word: '{word}'")
|
||
|
||
# Check for AI-slop patterns
|
||
slop_patterns = [
|
||
(r"it is (worth|important|crucial|essential) to", "AI-slop: 'it is X to'"),
|
||
(r"in (conclusion|summary|essence)", "AI-slop: 'in X'"),
|
||
(r"as we (have|can) seen", "AI-slop: 'as we have seen'"),
|
||
(r"plays? a (crucial|vital|key|important) role", "AI-slop: 'plays a X role'"),
|
||
]
|
||
|
||
import re
|
||
|
||
for pattern, label in slop_patterns:
|
||
if re.search(pattern, content_lower):
|
||
issues.append(label)
|
||
|
||
# Automated score
|
||
base_score = 100
|
||
base_score -= len(issues) * 8
|
||
# Penalize very short content
|
||
if len(content) < 300:
|
||
base_score -= 15
|
||
# Penalize very long paragraphs
|
||
paragraphs = [p for p in content.split("\n\n") if len(p) > 50]
|
||
if paragraphs:
|
||
avg_para_len = sum(len(p) for p in paragraphs) / len(paragraphs)
|
||
if avg_para_len > 500:
|
||
base_score -= 10
|
||
issues.append("Paragraphs too long (avg >500 chars)")
|
||
|
||
# ── AI Review (if score is borderline) ──
|
||
if base_score < 85 and len(issues) > 1:
|
||
try:
|
||
ai_review = await ai_call("review", QUALITY_REVIEW_PROMPT, content, max_tokens=800, temperature=0.2)
|
||
if ai_review:
|
||
try:
|
||
review_data = json.loads(ai_review.strip().lstrip("```json").rstrip("```")) # noqa: B005
|
||
issues.extend(review_data.get("issues", []))
|
||
if review_data.get("score", 100) < base_score:
|
||
base_score = review_data["score"]
|
||
if not review_data.get("pass", True):
|
||
return {
|
||
"pass": False,
|
||
"score": base_score,
|
||
"issues": issues,
|
||
"fixed_version": review_data.get("fixed_version", content),
|
||
}
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
|
||
# Fix if needed
|
||
fixed = content
|
||
if base_score < 70:
|
||
try:
|
||
fix_prompt = f"""Rewrite this content to meet quality standards. Remove all AI-slop language, forbidden words, and corporate speak. Make it human, direct, and specific.
|
||
|
||
Current issues: {", ".join(issues[:5])}
|
||
|
||
ORIGINAL:
|
||
{content[:2000]}"""
|
||
fixed = await ai_call(
|
||
"writing",
|
||
"You are a skilled human writer. Rewrite content to be direct, specific, and natural. No AI-slop.",
|
||
fix_prompt,
|
||
max_tokens=len(content) // 2 + 500,
|
||
temperature=0.4,
|
||
)
|
||
if not fixed:
|
||
fixed = content
|
||
except Exception:
|
||
fixed = content
|
||
|
||
return {
|
||
"pass": base_score >= 70,
|
||
"score": max(0, min(100, base_score)),
|
||
"issues": issues[:10],
|
||
"fixed_version": fixed,
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# SMART PROMPT BUILDER
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
def build_research_prompt(topic: str, data: dict | None = None) -> str:
|
||
"""Build a research prompt with all available context."""
|
||
parts = [f"Research task: {topic}\n"]
|
||
|
||
if data:
|
||
for key, value in data.items():
|
||
if isinstance(value, str):
|
||
parts.append(f"## {key.upper()}\n{value[:2000]}")
|
||
elif isinstance(value, list):
|
||
parts.append(f"## {key.upper()}\n" + "\n".join(f"- {str(v)[:200]}" for v in value[:10]))
|
||
elif isinstance(value, dict):
|
||
parts.append(f"## {key.upper()}\n{json.dumps(value, default=str)[:1000]}")
|
||
|
||
return "\n\n".join(parts)
|
||
|
||
|
||
def build_writing_prompt(topic: str, research_notes: str, style: str = "newsletter") -> str:
|
||
"""Build a writing prompt from research notes."""
|
||
return f"""Write a {style} about: {topic}
|
||
|
||
RESEARCH NOTES:
|
||
{research_notes[:3000]}
|
||
|
||
Style guide:
|
||
- Direct, human voice. No corporate speak. No AI-slop.
|
||
- Lead with the most interesting detail.
|
||
- Use specific numbers, names, facts.
|
||
- Vary sentence length. Short paragraphs.
|
||
- End with a clear takeaway.
|
||
|
||
Write the complete piece now:"""
|
||
|
||
|
||
def get_usage_stats() -> dict:
|
||
"""Get current model usage statistics from rate tracker."""
|
||
return rate_tracker.stats()
|