""" 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 / qwen/qwen3.6-27b — 170ms 100% ← FASTEST #2 Cerebras / gpt-oss-120b — 207ms 100% ← REASONING (max_tokens≥500) #3 Groq / qwen/qwen3.6-27b — 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 / openrouter/free — 2160ms 100% Role assignments 2026-06-14 FINALE: FAST → Groq qwen/qwen3.6-27b (170ms, 100%) ← #1 assoluto ARCHITECT → Groq llama-4-scout-17b 10M ctx (244ms, 67% — best per contesto lungo) CODER → Groq qwen/qwen3.6-27b (235ms, 100%) ← #3 qualità TESTER → Groq qwen/qwen3.6-27b CONTEXT → Groq qwen/qwen3.6-27b 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 (qwen/qwen3.6-27b o primo disponibile) OpenRouter tenuto come fallback secondario (openrouter/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 — qwen/qwen3.6-27b ARCHITECT = "architect" # planning, ragionamento complesso — GPT-OSS 120B CODER = "coder" # coding, debug — qwen/qwen3.6-27b TESTER = "tester" # test gen, debug hints — qwen/qwen3.6-27b CONTEXT = "context" # summarization, context compression — qwen/qwen3.6-27b DEFAULT = "default" # AIClient() primary RESEARCHER = "researcher" # web research + document synthesis — GPT-OSS 120B REASONER = "reasoner" # throughput massimo — Cerebras GPT-OSS 120B 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 _prioritize_profile_pool(client: Any, provider_name: str) -> Any: """Promote all configured profiles for one provider without collapsing them.""" profiles = [p for p in client.providers if p.name == provider_name] if not profiles: return None client.providers = profiles + [p for p in client.providers if p.name != provider_name] client.provider_name = profiles[0].name client.default_model = profiles[0].default_model client.client = client._client_for(profiles[0]) return client @staticmethod def _profiled_client(client: Any, provider_names: tuple[str, ...]) -> Any: for provider_name in provider_names: env_name = f"{provider_name.upper()}_PROFILES_JSON" if os.getenv(env_name): profiled = RoleRouter._prioritize_profile_pool(client, provider_name) if profiled: return profiled return None @staticmethod def _fast_client() -> Any: """Groq GPT-OSS 20B per query brevi e a bassa latenza. Usato per: greetings, calcoli semplici, identity, domande 1-liner.""" from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("groq",)) if profiled: return profiled 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", "qwen/qwen3.6-27b"), ) 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 come primario per architettura. Fallback 1: Groq GPT-OSS 120B. Fallback 2: OpenRouter GPT-OSS 20B gratuito.""" from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("nvidia", "groq", "openrouter")) if profiled: return profiled 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 GPT-OSS 120B, modello production supportato. 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", "qwen/qwen3.6-27b"), ) 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: usa il pool multi-profilo, se configurato. if os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENROUTER_PROFILES_JSON"): client = AIClient() profiles = [p for p in client.providers if p.name == "openrouter"] if not profiles and os.getenv("OPENROUTER_API_KEY"): profiles = [ProviderConfig( name="openrouter", api_key=os.getenv("OPENROUTER_API_KEY", ""), base_url="https://openrouter.ai/api/v1", default_model=os.getenv("OPENROUTER_MODEL", "openrouter/free"), profile="legacy", )] if profiles: client.providers = profiles + [p for p in client.providers if p.name != "openrouter"] client.provider_name = profiles[0].name client.default_model = profiles[0].default_model client.client = client._client_for(profiles[0]) return client return AIClient() @staticmethod def _coder_client() -> Any: """Groq GPT-OSS 120B per coding e debug. Fallback: provider ordinari del router se GROQ_API_KEY manca.""" from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("groq", "nvidia", "openrouter")) if profiled: return profiled groq_key = os.getenv("GROQ_API_KEY") nvidia_key = os.getenv("NVIDIA_API_KEY") model = os.getenv("CODER_MODEL", "qwen/qwen3.6-27b") 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, purpose="coding", ) dedicated_fallbacks: list[ProviderConfig] = [] if nvidia_key: dedicated_fallbacks.append( ProviderConfig( name="nvidia-coder", api_key=nvidia_key, base_url="https://integrate.api.nvidia.com/v1", default_model=os.getenv( "NVIDIA_MODEL", "nvidia/nemotron-3-ultra-550b-a55b" ), purpose="coding", ) ) rest = [ provider for provider in client.providers if provider.name not in ("groq", "groq-coder", "nvidia", "nvidia-coder") ] client.providers = [coder, *dedicated_fallbacks, *rest] client.provider_name = coder.name client.default_model = coder.default_model client.client = client._client_for(coder) return client # Fallback OpenRouter: usa il pool multi-profilo, se configurato. if os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENROUTER_PROFILES_JSON"): client = AIClient() profiles = [p for p in client.providers if p.name == "openrouter"] if not profiles and os.getenv("OPENROUTER_API_KEY"): profiles = [ProviderConfig( name="openrouter", api_key=os.getenv("OPENROUTER_API_KEY", ""), base_url="https://openrouter.ai/api/v1", default_model=os.getenv("OPENROUTER_MODEL", "openrouter/free"), profile="legacy", )] if profiles: client.providers = profiles + [p for p in client.providers if p.name != "openrouter"] client.provider_name = profiles[0].name client.default_model = profiles[0].default_model client.client = client._client_for(profiles[0]) return client return AIClient() @staticmethod def _researcher_client() -> Any: """Groq GPT-OSS 120B per analisi e sintesi; la flotta restante è fallback. Gemini può essere configurato ma ha una quota indipendente e più stretta: non deve quindi bloccare i task della persona analyst/researcher quando Groq è sano. L'ordine conserva tutti i provider ordinari dopo Groq. """ from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("groq", "gemini", "openrouter")) if profiled: return profiled groq_key = os.getenv("GROQ_API_KEY") if groq_key: researcher = ProviderConfig( name="groq-researcher", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv( "GROQ_RESEARCH_MODEL", os.getenv("GROQ_MODEL", "qwen/qwen3.6-27b"), ), ) rest = [ provider for provider in client.providers if provider.name not in ("groq", "groq-researcher") ] client.providers = [researcher, *rest] client.provider_name = researcher.name client.default_model = researcher.default_model client.client = client._client_for(researcher) return client return AIClient() @staticmethod def _reasoner_client() -> Any: """Priorità a Groq GPT-OSS per reasoning/MMLU, con flotta runtime come fallback. Gemini è soggetto a quote RPM e non deve essere il percorso iniziale per risposte deterministiche a scelta multipla. Cerebras resta un fallback compatibile quando Groq non è configurato.""" from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("groq", "cerebras", "gemini", "openrouter")) if profiled: return profiled groq_key = os.getenv("GROQ_API_KEY") if groq_key: reasoner = ProviderConfig( name="groq-reasoner", api_key=groq_key, base_url="https://api.groq.com/openai/v1", default_model=os.getenv( "GROQ_REASONER_MODEL", os.getenv("GROQ_MODEL", "qwen/qwen3.6-27b"), ), ) rest = [ provider for provider in client.providers if provider.name not in ("groq", "groq-reasoner") ] client.providers = [reasoner, *rest] client.provider_name = reasoner.name client.default_model = reasoner.default_model client.client = client._client_for(reasoner) return client 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", "gpt-oss-120b"), ) rest = [provider for provider in client.providers if not provider.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 client = AIClient() profiled = RoleRouter._profiled_client(client, ("sambanova",)) if profiled: return profiled 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 client = AIClient() profiled = RoleRouter._profiled_client(client, ("nvidia",)) if profiled: return profiled 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 GPT-OSS 20B per test rapidi e debug hints.""" from models.ai_client import AIClient, ProviderConfig client = AIClient() profiled = RoleRouter._profiled_client(client, ("groq",)) if profiled: return profiled groq_key = os.getenv("GROQ_API_KEY") if not groq_key: return client 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", "qwen/qwen3.6-27b"), purpose="coding", ) 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