Spaces:
Running
Running
sync: 158 file da Baida98/AI@c4f6430d (2026-08-15 16:49 UTC) [deploy-all]
#41
by Baida07 - opened
- .env.example +1 -1
- api/exec.py +1 -1
- api/providers.py +17 -14
- models/role_router.py +11 -11
- tests/test_provider_model_migrations.py +116 -0
.env.example
CHANGED
|
@@ -61,5 +61,5 @@ UPSTASH_REDIS_REST_TOKEN=
|
|
| 61 |
# ── 9. Feature Flags ─────────────────────────────────────────
|
| 62 |
VITE_ENABLE_BROWSER_SANDBOX=false
|
| 63 |
UNIFIED_LOOP_MAX_STEPS=8
|
| 64 |
-
LLM_MODEL=
|
| 65 |
|
|
|
|
| 61 |
# ── 9. Feature Flags ─────────────────────────────────────────
|
| 62 |
VITE_ENABLE_BROWSER_SANDBOX=false
|
| 63 |
UNIFIED_LOOP_MAX_STEPS=8
|
| 64 |
+
LLM_MODEL=openai/gpt-oss-20b:free
|
| 65 |
|
api/exec.py
CHANGED
|
@@ -574,8 +574,8 @@ async def llm_fix_code(
|
|
| 574 |
or_key = os.getenv('OPENROUTER_API_KEY', '')
|
| 575 |
if or_key:
|
| 576 |
for m in [
|
|
|
|
| 577 |
'meta-llama/llama-3.1-8b-instruct:free',
|
| 578 |
-
'google/gemini-2.0-flash-exp:free',
|
| 579 |
'qwen/qwen-2.5-coder-7b-instruct:free',
|
| 580 |
]:
|
| 581 |
_FIX_CHAIN.append(('https://openrouter.ai/api/v1', or_key, m))
|
|
|
|
| 574 |
or_key = os.getenv('OPENROUTER_API_KEY', '')
|
| 575 |
if or_key:
|
| 576 |
for m in [
|
| 577 |
+
'openai/gpt-oss-20b:free',
|
| 578 |
'meta-llama/llama-3.1-8b-instruct:free',
|
|
|
|
| 579 |
'qwen/qwen-2.5-coder-7b-instruct:free',
|
| 580 |
]:
|
| 581 |
_FIX_CHAIN.append(('https://openrouter.ai/api/v1', or_key, m))
|
api/providers.py
CHANGED
|
@@ -883,37 +883,40 @@ async def update_provider_models(role: AuthRole = Depends(require_role(AuthRole.
|
|
| 883 |
if _sb is None:
|
| 884 |
return {"ok": False, "error": "Supabase non configurato", "updated": 0}
|
| 885 |
|
| 886 |
-
# Mappa: modello_vecchio
|
|
|
|
|
|
|
|
|
|
| 887 |
MODEL_FIXES = [
|
| 888 |
-
("llama-3.1-70b-versatile",
|
| 889 |
-
("llama3.1-70b",
|
| 890 |
-
("llama-3.1-405b-instruct",
|
| 891 |
-
("llama-3.1-405b",
|
| 892 |
-
("llama3-70b",
|
| 893 |
-
("gemini-1.5-flash",
|
| 894 |
-
("gemini-1.5-pro",
|
| 895 |
-
("
|
| 896 |
-
("claude-3.5-sonnet", "meta-llama/llama-4-scout:free"),
|
| 897 |
]
|
| 898 |
|
| 899 |
import asyncio as _aio
|
| 900 |
total_updated = 0
|
| 901 |
results = []
|
| 902 |
|
| 903 |
-
for old_model, new_model in MODEL_FIXES:
|
| 904 |
try:
|
| 905 |
r = await _aio.to_thread(
|
| 906 |
-
lambda om=old_model, nm=new_model: _sb.table("ai_providers")
|
| 907 |
.update({"default_model": nm})
|
|
|
|
| 908 |
.eq("default_model", om)
|
| 909 |
.execute()
|
| 910 |
)
|
| 911 |
n = len(r.data) if r.data else 0
|
| 912 |
total_updated += n
|
| 913 |
if n > 0:
|
| 914 |
-
results.append({"old": old_model, "new": new_model, "rows": n})
|
| 915 |
except Exception as exc:
|
| 916 |
-
results.append({"old": old_model, "new": new_model, "error": str(exc)[:100]})
|
| 917 |
|
| 918 |
# Disattiva provider E2B (non sono LLM provider)
|
| 919 |
try:
|
|
|
|
| 883 |
if _sb is None:
|
| 884 |
return {"ok": False, "error": "Supabase non configurato", "updated": 0}
|
| 885 |
|
| 886 |
+
# Mappa provider-specifica: (provider, modello_vecchio, modello_nuovo).
|
| 887 |
+
# Lo stesso ID modello può essere valido su un provider e non su un altro:
|
| 888 |
+
# filtrare per `name` evita di applicare un formato incompatibile alla riga sbagliata.
|
| 889 |
+
# GPT-OSS 120B non compare come vecchio valore perché è già un modello supportato.
|
| 890 |
MODEL_FIXES = [
|
| 891 |
+
("groq", "llama-3.1-70b-versatile", "openai/gpt-oss-120b"),
|
| 892 |
+
("cerebras", "llama3.1-70b", "gpt-oss-120b"),
|
| 893 |
+
("nvidia", "llama-3.1-405b-instruct", "meta/llama-3.3-70b-instruct"),
|
| 894 |
+
("openrouter", "llama-3.1-405b", "openai/gpt-oss-20b:free"),
|
| 895 |
+
("sambanova", "llama3-70b", "DeepSeek-V3.2"),
|
| 896 |
+
("gemini", "gemini-1.5-flash", "gemini-3.5-flash-lite"),
|
| 897 |
+
("gemini", "gemini-1.5-pro", "gemini-3.6-flash"),
|
| 898 |
+
("openrouter", "claude-3.5-sonnet", "openai/gpt-oss-20b:free"),
|
|
|
|
| 899 |
]
|
| 900 |
|
| 901 |
import asyncio as _aio
|
| 902 |
total_updated = 0
|
| 903 |
results = []
|
| 904 |
|
| 905 |
+
for provider_name, old_model, new_model in MODEL_FIXES:
|
| 906 |
try:
|
| 907 |
r = await _aio.to_thread(
|
| 908 |
+
lambda pn=provider_name, om=old_model, nm=new_model: _sb.table("ai_providers")
|
| 909 |
.update({"default_model": nm})
|
| 910 |
+
.eq("name", pn)
|
| 911 |
.eq("default_model", om)
|
| 912 |
.execute()
|
| 913 |
)
|
| 914 |
n = len(r.data) if r.data else 0
|
| 915 |
total_updated += n
|
| 916 |
if n > 0:
|
| 917 |
+
results.append({"provider": provider_name, "old": old_model, "new": new_model, "rows": n})
|
| 918 |
except Exception as exc:
|
| 919 |
+
results.append({"provider": provider_name, "old": old_model, "new": new_model, "error": str(exc)[:100]})
|
| 920 |
|
| 921 |
# Disattiva provider E2B (non sono LLM provider)
|
| 922 |
try:
|
models/role_router.py
CHANGED
|
@@ -37,13 +37,13 @@ _logger = logging.getLogger("models.role_router")
|
|
| 37 |
|
| 38 |
class Role(str, Enum):
|
| 39 |
FAST = "fast" # greetings, math semplice, identity — openai/gpt-oss-20b
|
| 40 |
-
ARCHITECT = "architect" # planning, ragionamento complesso —
|
| 41 |
CODER = "coder" # coding, debug — openai/gpt-oss-120b
|
| 42 |
TESTER = "tester" # test gen, debug hints — openai/gpt-oss-120b
|
| 43 |
CONTEXT = "context" # summarization, context compression — openai/gpt-oss-120b
|
| 44 |
DEFAULT = "default" # AIClient() primary
|
| 45 |
-
RESEARCHER = "researcher" # web research + document synthesis —
|
| 46 |
-
REASONER = "reasoner" # throughput massimo — Cerebras
|
| 47 |
SAMBANOVA = "sambanova"
|
| 48 |
NVIDIA = "nvidia" # NVIDIA NIM — nemotron-3-ultra-550b (1M ctx) # DeepSeek-V3.2 via SambaNova (404ms, 100% qualità benchmark)
|
| 49 |
|
|
@@ -107,8 +107,8 @@ class RoleRouter:
|
|
| 107 |
|
| 108 |
@staticmethod
|
| 109 |
def _architect_client() -> Any:
|
| 110 |
-
"""NVIDIA NIM
|
| 111 |
-
Fallback 1: Groq
|
| 112 |
from models.ai_client import AIClient, ProviderConfig
|
| 113 |
nvidia_key = os.getenv("NVIDIA_API_KEY")
|
| 114 |
if nvidia_key:
|
|
@@ -125,7 +125,7 @@ class RoleRouter:
|
|
| 125 |
client.default_model = nvidia.default_model
|
| 126 |
client.client = client._client_for(nvidia)
|
| 127 |
return client
|
| 128 |
-
# Fallback 1: Groq
|
| 129 |
groq_key = os.getenv("GROQ_API_KEY")
|
| 130 |
if groq_key:
|
| 131 |
client = AIClient()
|
|
@@ -133,7 +133,7 @@ class RoleRouter:
|
|
| 133 |
name="groq-architect",
|
| 134 |
api_key=groq_key,
|
| 135 |
base_url="https://api.groq.com/openai/v1",
|
| 136 |
-
default_model=os.getenv("ARCHITECT_MODEL", "
|
| 137 |
)
|
| 138 |
rest = [p for p in client.providers if p.name not in ("groq", "groq-architect")]
|
| 139 |
client.providers = [architect, *rest]
|
|
@@ -141,7 +141,7 @@ class RoleRouter:
|
|
| 141 |
client.default_model = architect.default_model
|
| 142 |
client.client = client._client_for(architect)
|
| 143 |
return client
|
| 144 |
-
# Fallback: OpenRouter
|
| 145 |
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
| 146 |
if openrouter_key:
|
| 147 |
client = AIClient()
|
|
@@ -149,7 +149,7 @@ class RoleRouter:
|
|
| 149 |
name="openrouter-architect",
|
| 150 |
api_key=openrouter_key,
|
| 151 |
base_url="https://openrouter.ai/api/v1",
|
| 152 |
-
default_model="
|
| 153 |
)
|
| 154 |
rest = [p for p in client.providers if not p.name.startswith("openrouter")]
|
| 155 |
client.providers = [fallback, *rest]
|
|
@@ -205,7 +205,7 @@ class RoleRouter:
|
|
| 205 |
name="openrouter-coder",
|
| 206 |
api_key=openrouter_key,
|
| 207 |
base_url="https://openrouter.ai/api/v1",
|
| 208 |
-
default_model="
|
| 209 |
purpose="coding",
|
| 210 |
)
|
| 211 |
rest = [p for p in client.providers if not p.name.startswith("openrouter")]
|
|
@@ -286,7 +286,7 @@ class RoleRouter:
|
|
| 286 |
name="cerebras-reasoner",
|
| 287 |
api_key=cerebras_key,
|
| 288 |
base_url="https://api.cerebras.ai/v1",
|
| 289 |
-
default_model=os.getenv("CEREBRAS_MODEL", "
|
| 290 |
)
|
| 291 |
rest = [provider for provider in client.providers if not provider.name.startswith("cerebras")]
|
| 292 |
client.providers = [reasoner, *rest]
|
|
|
|
| 37 |
|
| 38 |
class Role(str, Enum):
|
| 39 |
FAST = "fast" # greetings, math semplice, identity — openai/gpt-oss-20b
|
| 40 |
+
ARCHITECT = "architect" # planning, ragionamento complesso — GPT-OSS 120B
|
| 41 |
CODER = "coder" # coding, debug — openai/gpt-oss-120b
|
| 42 |
TESTER = "tester" # test gen, debug hints — openai/gpt-oss-120b
|
| 43 |
CONTEXT = "context" # summarization, context compression — openai/gpt-oss-120b
|
| 44 |
DEFAULT = "default" # AIClient() primary
|
| 45 |
+
RESEARCHER = "researcher" # web research + document synthesis — GPT-OSS 120B
|
| 46 |
+
REASONER = "reasoner" # throughput massimo — Cerebras GPT-OSS 120B
|
| 47 |
SAMBANOVA = "sambanova"
|
| 48 |
NVIDIA = "nvidia" # NVIDIA NIM — nemotron-3-ultra-550b (1M ctx) # DeepSeek-V3.2 via SambaNova (404ms, 100% qualità benchmark)
|
| 49 |
|
|
|
|
| 107 |
|
| 108 |
@staticmethod
|
| 109 |
def _architect_client() -> Any:
|
| 110 |
+
"""NVIDIA NIM come primario per architettura.
|
| 111 |
+
Fallback 1: Groq GPT-OSS 120B. Fallback 2: OpenRouter GPT-OSS 20B gratuito."""
|
| 112 |
from models.ai_client import AIClient, ProviderConfig
|
| 113 |
nvidia_key = os.getenv("NVIDIA_API_KEY")
|
| 114 |
if nvidia_key:
|
|
|
|
| 125 |
client.default_model = nvidia.default_model
|
| 126 |
client.client = client._client_for(nvidia)
|
| 127 |
return client
|
| 128 |
+
# Fallback 1: Groq GPT-OSS 120B, modello production supportato.
|
| 129 |
groq_key = os.getenv("GROQ_API_KEY")
|
| 130 |
if groq_key:
|
| 131 |
client = AIClient()
|
|
|
|
| 133 |
name="groq-architect",
|
| 134 |
api_key=groq_key,
|
| 135 |
base_url="https://api.groq.com/openai/v1",
|
| 136 |
+
default_model=os.getenv("ARCHITECT_MODEL", "openai/gpt-oss-120b"),
|
| 137 |
)
|
| 138 |
rest = [p for p in client.providers if p.name not in ("groq", "groq-architect")]
|
| 139 |
client.providers = [architect, *rest]
|
|
|
|
| 141 |
client.default_model = architect.default_model
|
| 142 |
client.client = client._client_for(architect)
|
| 143 |
return client
|
| 144 |
+
# Fallback: OpenRouter GPT-OSS 20B gratuito, con ID verificato nel catalogo.
|
| 145 |
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
| 146 |
if openrouter_key:
|
| 147 |
client = AIClient()
|
|
|
|
| 149 |
name="openrouter-architect",
|
| 150 |
api_key=openrouter_key,
|
| 151 |
base_url="https://openrouter.ai/api/v1",
|
| 152 |
+
default_model=os.getenv("OPENROUTER_MODEL", "openai/gpt-oss-20b:free"),
|
| 153 |
)
|
| 154 |
rest = [p for p in client.providers if not p.name.startswith("openrouter")]
|
| 155 |
client.providers = [fallback, *rest]
|
|
|
|
| 205 |
name="openrouter-coder",
|
| 206 |
api_key=openrouter_key,
|
| 207 |
base_url="https://openrouter.ai/api/v1",
|
| 208 |
+
default_model=os.getenv("OPENROUTER_MODEL", "openai/gpt-oss-20b:free"),
|
| 209 |
purpose="coding",
|
| 210 |
)
|
| 211 |
rest = [p for p in client.providers if not p.name.startswith("openrouter")]
|
|
|
|
| 286 |
name="cerebras-reasoner",
|
| 287 |
api_key=cerebras_key,
|
| 288 |
base_url="https://api.cerebras.ai/v1",
|
| 289 |
+
default_model=os.getenv("CEREBRAS_MODEL", "gpt-oss-120b"),
|
| 290 |
)
|
| 291 |
rest = [provider for provider in client.providers if not provider.name.startswith("cerebras")]
|
| 292 |
client.providers = [reasoner, *rest]
|
tests/test_provider_model_migrations.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import unittest
|
| 3 |
+
from types import SimpleNamespace
|
| 4 |
+
from unittest.mock import patch
|
| 5 |
+
|
| 6 |
+
from api import providers
|
| 7 |
+
from models.ai_client import AIClient
|
| 8 |
+
from models.role_router import Role, RoleRouter
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class _RecordingTable:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.calls: list[dict] = []
|
| 14 |
+
self._pending: dict = {}
|
| 15 |
+
|
| 16 |
+
def update(self, values: dict):
|
| 17 |
+
self._pending = {"values": values, "filters": []}
|
| 18 |
+
return self
|
| 19 |
+
|
| 20 |
+
def eq(self, field: str, value: str):
|
| 21 |
+
self._pending["filters"].append((field, value))
|
| 22 |
+
return self
|
| 23 |
+
|
| 24 |
+
def execute(self):
|
| 25 |
+
self.calls.append(self._pending)
|
| 26 |
+
return SimpleNamespace(data=[{"id": len(self.calls)}])
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class _RecordingSupabase:
|
| 30 |
+
def __init__(self):
|
| 31 |
+
self.table_ref = _RecordingTable()
|
| 32 |
+
|
| 33 |
+
def table(self, name: str):
|
| 34 |
+
if name != "ai_providers":
|
| 35 |
+
raise AssertionError(f"Tabella inattesa: {name}")
|
| 36 |
+
return self.table_ref
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class RoleRouterModelMigrationTests(unittest.TestCase):
|
| 40 |
+
@patch.object(AIClient, "_load_providers", return_value=[])
|
| 41 |
+
@patch.dict(
|
| 42 |
+
os.environ,
|
| 43 |
+
{"GROQ_API_KEY": "test-groq-key"},
|
| 44 |
+
clear=True,
|
| 45 |
+
)
|
| 46 |
+
def test_architect_uses_supported_groq_default(self, _load_providers):
|
| 47 |
+
client = RoleRouter.get_client(Role.ARCHITECT)
|
| 48 |
+
|
| 49 |
+
self.assertEqual(client.provider_name, "groq-architect")
|
| 50 |
+
self.assertEqual(client.default_model, "openai/gpt-oss-120b")
|
| 51 |
+
|
| 52 |
+
@patch.object(AIClient, "_load_providers", return_value=[])
|
| 53 |
+
@patch.dict(
|
| 54 |
+
os.environ,
|
| 55 |
+
{"OPENROUTER_API_KEY": "test-openrouter-key"},
|
| 56 |
+
clear=True,
|
| 57 |
+
)
|
| 58 |
+
def test_openrouter_role_fallbacks_use_available_free_model(self, _load_providers):
|
| 59 |
+
architect = RoleRouter.get_client(Role.ARCHITECT)
|
| 60 |
+
coder = RoleRouter.get_client(Role.CODER)
|
| 61 |
+
|
| 62 |
+
self.assertEqual(architect.default_model, "openai/gpt-oss-20b:free")
|
| 63 |
+
self.assertEqual(coder.default_model, "openai/gpt-oss-20b:free")
|
| 64 |
+
|
| 65 |
+
@patch.object(AIClient, "_load_providers", return_value=[])
|
| 66 |
+
@patch.dict(
|
| 67 |
+
os.environ,
|
| 68 |
+
{"CEREBRAS_API_KEY": "test-cerebras-key"},
|
| 69 |
+
clear=True,
|
| 70 |
+
)
|
| 71 |
+
def test_reasoner_uses_cerebras_gpt_oss_default(self, _load_providers):
|
| 72 |
+
client = RoleRouter.get_client(Role.REASONER)
|
| 73 |
+
|
| 74 |
+
self.assertEqual(client.provider_name, "cerebras-reasoner")
|
| 75 |
+
self.assertEqual(client.default_model, "gpt-oss-120b")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class ProviderTableMigrationTests(unittest.IsolatedAsyncioTestCase):
|
| 79 |
+
async def test_update_models_filters_by_provider_and_never_downgrades_gpt_oss(self):
|
| 80 |
+
original_supabase = providers._sb
|
| 81 |
+
fake_supabase = _RecordingSupabase()
|
| 82 |
+
providers._sb = fake_supabase
|
| 83 |
+
try:
|
| 84 |
+
payload = await providers.update_provider_models(role=None)
|
| 85 |
+
finally:
|
| 86 |
+
providers._sb = original_supabase
|
| 87 |
+
|
| 88 |
+
self.assertTrue(payload["ok"])
|
| 89 |
+
self.assertEqual(payload["total_updated"], 8)
|
| 90 |
+
self.assertEqual(len(fake_supabase.table_ref.calls), 8)
|
| 91 |
+
|
| 92 |
+
expected = {
|
| 93 |
+
("groq", "llama-3.1-70b-versatile", "openai/gpt-oss-120b"),
|
| 94 |
+
("cerebras", "llama3.1-70b", "gpt-oss-120b"),
|
| 95 |
+
("nvidia", "llama-3.1-405b-instruct", "meta/llama-3.3-70b-instruct"),
|
| 96 |
+
("openrouter", "llama-3.1-405b", "openai/gpt-oss-20b:free"),
|
| 97 |
+
("sambanova", "llama3-70b", "DeepSeek-V3.2"),
|
| 98 |
+
("gemini", "gemini-1.5-flash", "gemini-3.5-flash-lite"),
|
| 99 |
+
("gemini", "gemini-1.5-pro", "gemini-3.6-flash"),
|
| 100 |
+
("openrouter", "claude-3.5-sonnet", "openai/gpt-oss-20b:free"),
|
| 101 |
+
}
|
| 102 |
+
actual = {
|
| 103 |
+
(
|
| 104 |
+
dict(call["filters"])["name"],
|
| 105 |
+
dict(call["filters"])["default_model"],
|
| 106 |
+
call["values"]["default_model"],
|
| 107 |
+
)
|
| 108 |
+
for call in fake_supabase.table_ref.calls
|
| 109 |
+
}
|
| 110 |
+
self.assertEqual(actual, expected)
|
| 111 |
+
self.assertNotIn("llama-4-scout", {new for _, _, new in actual})
|
| 112 |
+
self.assertNotIn(("cerebras", "gpt-oss-120b", "llama-4-scout"), actual)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
unittest.main()
|