Terminal / models /role_router.py
Baida07's picture
sync: 167 file da Baida98/AI@a58fbf7e (2026-08-22 09:03 UTC) (#54)
ab4e3e4
Raw
History Blame Contribute Delete
18.9 kB
"""
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