""" role_router.py — Multi-model role routing (S362, aggiornato 2026-06-14 benchmark) BENCHMARK RESULTS 2026-06-14 FINALE (14 modelli × 3 test, max_tokens corretti): 100% qualità (ordinati per TTFT): #1 Groq / openai/gpt-oss-20b — 170ms 100% ← FASTEST #2 Cerebras / gpt-oss-120b — 207ms 100% ← REASONING (max_tokens≥500) #3 Groq / openai/gpt-oss-120b — 235ms 100% #4 Cerebras / zai-glm-4.7 — 254ms 100% #5 Groq / compound-mini — 341ms 100% #6 SambaNova / DeepSeek-V3.1 — 482ms 100% #7 SambaNova / gemma-4-31B — 2132ms 100% #8 OpenRouter / gpt-oss-120b:free — 2160ms 100% Role assignments 2026-06-14 FINALE: FAST → Groq openai/gpt-oss-20b (170ms, 100%) ← #1 assoluto ARCHITECT → Groq llama-4-scout-17b 10M ctx (244ms, 67% — best per contesto lungo) CODER → Groq openai/gpt-oss-120b (235ms, 100%) ← #3 qualità TESTER → Groq openai/gpt-oss-20b CONTEXT → Groq openai/gpt-oss-20b RESEARCHER → Gemini 2.5-flash (599ms, 67% — math prompt-sensitive) REASONER → Cerebras gpt-oss-120b (207ms, 100%, reasoning model → max_tokens≥500) SAMBANOVA → SambaNova DeepSeek-V3.1 (482ms, 100%) DEFAULT → AIClient() primary (openai/gpt-oss-120b o primo disponibile) OpenRouter tenuto come fallback secondario (gpt-oss-120b:free = 1645ms ma 100% qualità). """ from __future__ import annotations import os from enum import Enum from typing import Any import logging _logger = logging.getLogger("models.role_router") class Role(str, Enum): FAST = "fast" # greetings, math semplice, identity — openai/gpt-oss-20b ARCHITECT = "architect" # planning, ragionamento complesso — llama-4-scout (10M ctx) CODER = "coder" # coding, debug — llama-3.3-70b-versatile TESTER = "tester" # test gen, debug hints — llama-3.3-70b-versatile CONTEXT = "context" # summarization, context compression — llama-3.3-70b-versatile DEFAULT = "default" # AIClient() primary RESEARCHER = "researcher" # web research + document synthesis — gemini-2.0-flash-exp REASONER = "reasoner" # throughput massimo — Cerebras llama-4-scout (2000+ tok/s) SAMBANOVA = "sambanova" NVIDIA = "nvidia" # NVIDIA NIM — nemotron-3-ultra-550b (1M ctx) # DeepSeek-V3.2 via SambaNova (404ms, 100% qualità benchmark) class RoleRouter: """ Factory: `RoleRouter.get_client(Role.ARCHITECT)`. Restituisce un AIClient configurato per il provider ottimale del ruolo. Tutti i fallback sono silenziosi — restituisce sempre un AIClient valido. """ @staticmethod def get_client(role: Role) -> Any: """Ritorna un AIClient pre-configurato per il ruolo richiesto.""" try: if role == Role.FAST: return RoleRouter._fast_client() if role == Role.ARCHITECT: return RoleRouter._architect_client() if role == Role.CODER: return RoleRouter._coder_client() if role in (Role.TESTER, Role.CONTEXT): return RoleRouter._tester_client() if role == Role.RESEARCHER: return RoleRouter._researcher_client() if role == Role.REASONER: return RoleRouter._reasoner_client() if role == Role.SAMBANOVA: return RoleRouter._sambanova_client() if role == Role.NVIDIA: return RoleRouter._nvidia_client() except Exception as _exc: _logger.warning("[role_router] GAP-ROUT: fallback to default AIClient — role=%s raised %s: %s", role.value, type(_exc).__name__, _exc) # GAP-ROUT-FIX: debug→warning from models.ai_client import AIClient return AIClient() # ── Role-specific builders ───────────────────────────────────────────────── @staticmethod def _fast_client() -> Any: """Groq llama-3.3-70b-versatile — 344ms TTFT, 100% benchmark qualità. Usato per: greetings, calcoli semplici, identity, domande 1-liner.""" from models.ai_client import AIClient, ProviderConfig groq_key = os.getenv("GROQ_API_KEY") if not groq_key: return RoleRouter._tester_client() client = AIClient() fast = ProviderConfig( name="groq-fast", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv("GROQ_FAST_MODEL", "llama-3.3-70b-versatile"), ) rest = [p for p in client.providers if p.name not in ("groq", "groq-fast", "groq-tester")] client.providers = [fast, *rest] client.provider_name = fast.name client.default_model = fast.default_model client.client = client._client_for(fast) return client @staticmethod def _architect_client() -> Any: """NVIDIA NIM deepseek-v4-flash (1M ctx) come primario — massima potenza per architettura. Fallback 1: Groq llama-4-scout (10M ctx, 480ms). Fallback 2: OpenRouter llama-4-scout:free.""" from models.ai_client import AIClient, ProviderConfig nvidia_key = os.getenv("NVIDIA_API_KEY") if nvidia_key: client = AIClient() nvidia = ProviderConfig( name="nvidia-architect", api_key=nvidia_key, base_url="https://integrate.api.nvidia.com/v1", default_model=os.getenv("NVIDIA_ARCHITECT_MODEL", "nvidia/deepseek-v4-flash"), ) rest = [p for p in client.providers if not p.name.startswith("nvidia")] client.providers = [nvidia, *rest] client.provider_name = nvidia.name client.default_model = nvidia.default_model client.client = client._client_for(nvidia) return client # Fallback 1: Groq llama-4-scout (10M ctx, 480ms) groq_key = os.getenv("GROQ_API_KEY") if groq_key: client = AIClient() architect = ProviderConfig( name="groq-architect", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv("ARCHITECT_MODEL", "llama-4-scout"), ) rest = [p for p in client.providers if p.name not in ("groq", "groq-architect")] client.providers = [architect, *rest] client.provider_name = architect.name client.default_model = architect.default_model client.client = client._client_for(architect) return client # Fallback: OpenRouter meta-llama/llama-4-scout:free (1645ms ma 100% qualità) openrouter_key = os.getenv("OPENROUTER_API_KEY") if openrouter_key: client = AIClient() fallback = ProviderConfig( name="openrouter-architect", api_key=openrouter_key, base_url="https://openrouter.ai/api/v1", default_model="meta-llama/llama-4-scout:free", ) rest = [p for p in client.providers if not p.name.startswith("openrouter")] client.providers = [fallback, *rest] client.provider_name = fallback.name client.default_model = fallback.default_model client.client = client._client_for(fallback) return client return AIClient() @staticmethod def _coder_client() -> Any: """Groq llama-3.3-70b-versatile — 358ms TTFT, 100% benchmark qualità. AGGIORNATO 2026-08-04: era Groq openai/gpt-oss-120b. Fallback: OpenRouter llama-4-scout:free se GROQ_API_KEY mancante.""" from models.ai_client import AIClient, ProviderConfig groq_key = os.getenv("GROQ_API_KEY") model = os.getenv("CODER_MODEL", "llama-3.3-70b-versatile") if groq_key: client = AIClient() coder = ProviderConfig( name="groq-coder", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=model, ) rest = [p for p in client.providers if p.name not in ("groq", "groq-coder")] client.providers = [coder, *rest] client.provider_name = coder.name client.default_model = coder.default_model client.client = client._client_for(coder) return client openrouter_key = os.getenv("OPENROUTER_API_KEY") if openrouter_key: client = AIClient() fallback = ProviderConfig( name="openrouter-coder", api_key=openrouter_key, base_url="https://openrouter.ai/api/v1", default_model="meta-llama/llama-4-scout:free", ) rest = [p for p in client.providers if not p.name.startswith("openrouter")] client.providers = [fallback, *rest] client.provider_name = fallback.name client.default_model = fallback.default_model client.client = client._client_for(fallback) return client return AIClient() @staticmethod def _researcher_client() -> Any: """Gemini 2.0-flash-exp — TTFT 910ms, ottima per research/synthesis/doc analysis.""" from models.ai_client import AIClient, ProviderConfig gemini_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") groq_key = os.getenv("GROQ_API_KEY") if gemini_key: client = AIClient() researcher = ProviderConfig( name="gemini-researcher", api_key=gemini_key, base_url="https://generativelanguage.googleapis.com/v1beta/openai", default_model=os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp"), ) rest = [p for p in client.providers if not p.name.startswith("gemini")] client.providers = [researcher, *rest] client.provider_name = researcher.name client.default_model = researcher.default_model client.client = client._client_for(researcher) return client elif groq_key: client = AIClient() groq_compound = ProviderConfig( name="groq-compound-researcher", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv("GROQ_COMPOUND_MODEL", "groq/compound"), ) rest = [p for p in client.providers if p.name not in ("groq", "groq-compound-researcher")] client.providers = [groq_compound, *rest] client.provider_name = groq_compound.name client.default_model = groq_compound.default_model client.client = client._client_for(groq_compound) return client return AIClient() @staticmethod def _reasoner_client() -> Any: """Cerebras llama-4-scout — 207ms TTFT, 100% qualità (bench 2026-08-04). REASONING MODEL: genera "reasoning" field prima del "content". Richiede max_tokens≥500 per output non-vuoto su task non-triviali. Fallback: _coder_client (Groq 70B) se CEREBRAS_API_KEY mancante.""" from models.ai_client import AIClient, ProviderConfig cerebras_key = os.getenv("CEREBRAS_API_KEY") if not cerebras_key: return RoleRouter._coder_client() client = AIClient() reasoner = ProviderConfig( name="cerebras-reasoner", api_key=cerebras_key, base_url="https://api.cerebras.ai/v1", default_model=os.getenv("CEREBRAS_MODEL", "llama-4-scout"), ) rest = [p for p in client.providers if not p.name.startswith("cerebras")] client.providers = [reasoner, *rest] client.provider_name = reasoner.name client.default_model = reasoner.default_model client.client = client._client_for(reasoner) return client @staticmethod def _sambanova_client() -> Any: """SambaNova DeepSeek-V3.1 — 482ms TTFT, 100% qualità (bench 2026-06-14). gemma-4-31B-it: 100% ma 2132ms. Meta-Llama: rate-limited. gpt-oss-120b: ERR. Fallback: _architect_client (Groq) se SAMBANOVA_API_KEY mancante.""" from models.ai_client import AIClient, ProviderConfig sn_key = os.getenv("SAMBANOVA_API_KEY") if not sn_key: return RoleRouter._architect_client() client = AIClient() sambanova = ProviderConfig( name="sambanova", api_key=sn_key, base_url="https://api.sambanova.ai/v1", default_model=os.getenv("SAMBANOVA_MODEL", "DeepSeek-V3.2"), ) rest = [p for p in client.providers if not p.name.startswith("sambanova")] client.providers = [sambanova, *rest] client.provider_name = sambanova.name client.default_model = sambanova.default_model client.client = client._client_for(sambanova) return client @staticmethod def _nvidia_client() -> Any: """NVIDIA NIM nemotron-3-ultra-550b-a55b — 550B params, 1M ctx, API OpenAI-compat. Fallback: _architect_client (Groq) se NVIDIA_API_KEY mancante.""" from models.ai_client import AIClient, ProviderConfig nvidia_key = os.getenv("NVIDIA_API_KEY") if not nvidia_key: return RoleRouter._architect_client() client = AIClient() nvidia = ProviderConfig( name="nvidia", api_key=nvidia_key, base_url="https://integrate.api.nvidia.com/v1", default_model=os.getenv("NVIDIA_MODEL", "nvidia/nemotron-3-ultra-550b-a55b"), ) rest = [p for p in client.providers if not p.name.startswith("nvidia")] client.providers = [nvidia, *rest] client.provider_name = nvidia.name client.default_model = nvidia.default_model client.client = client._client_for(nvidia) return client @staticmethod def _tester_client() -> Any: """Groq llama-3.3-70b-versatile — fast, sufficiente per test gen e debug hints.""" from models.ai_client import AIClient, ProviderConfig groq_key = os.getenv("GROQ_API_KEY") if not groq_key: return AIClient() client = AIClient() tester = ProviderConfig( name="groq-tester", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv("GROQ_FAST_MODEL", "llama-3.3-70b-versatile"), ) rest = [p for p in client.providers if p.name not in ("groq", "groq-tester")] client.providers = [tester, *rest] client.provider_name = tester.name client.default_model = tester.default_model client.client = client._client_for(tester) return client