Spaces:
Running
Running
sync: 153 file da Baida98/AI@aea39689 (2026-08-15 10:11 UTC) [deploy-all]
#31
by Baida07 - opened
models/ai_client.py
CHANGED
|
@@ -96,6 +96,23 @@ class AIClient:
|
|
| 96 |
return os.getenv(definition["model_env"], database_model)
|
| 97 |
return database_model
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def _try_load_from_supabase(self) -> list[ProviderConfig]:
|
| 100 |
url = os.getenv("SUPABASE_URL", "")
|
| 101 |
key = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
@@ -104,14 +121,22 @@ class AIClient:
|
|
| 104 |
try:
|
| 105 |
from supabase import create_client
|
| 106 |
sb = create_client(url, key)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
rows = res.data or []
|
| 116 |
return [
|
| 117 |
ProviderConfig(
|
|
|
|
| 96 |
return os.getenv(definition["model_env"], database_model)
|
| 97 |
return database_model
|
| 98 |
|
| 99 |
+
@staticmethod
|
| 100 |
+
def _is_legacy_schema_error(exc: Exception) -> bool:
|
| 101 |
+
"""Riconosce il layout `ai_providers` precedente alla flotta canonica.
|
| 102 |
+
|
| 103 |
+
Quel layout espone `model_name`, `priority` e `provider_type`, ma
|
| 104 |
+
contiene record storici e modelli deprecati. Fino alla migrazione non va
|
| 105 |
+
promosso a source of truth: il fallback ambiente aggiornato è più sicuro.
|
| 106 |
+
"""
|
| 107 |
+
message = str(exc).lower()
|
| 108 |
+
return (
|
| 109 |
+
"column ai_providers." in message
|
| 110 |
+
and "does not exist" in message
|
| 111 |
+
and any(column in message for column in (
|
| 112 |
+
"default_model", "tier", "purpose", "success_count",
|
| 113 |
+
))
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
def _try_load_from_supabase(self) -> list[ProviderConfig]:
|
| 117 |
url = os.getenv("SUPABASE_URL", "")
|
| 118 |
key = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
|
|
| 121 |
try:
|
| 122 |
from supabase import create_client
|
| 123 |
sb = create_client(url, key)
|
| 124 |
+
try:
|
| 125 |
+
res = (
|
| 126 |
+
sb.table("ai_providers")
|
| 127 |
+
.select("id,name,api_key,base_url,default_model,tier,purpose")
|
| 128 |
+
.eq("is_active", True)
|
| 129 |
+
.order("tier", desc=False)
|
| 130 |
+
.order("success_count", desc=True)
|
| 131 |
+
.execute()
|
| 132 |
+
)
|
| 133 |
+
except Exception as exc:
|
| 134 |
+
if self._is_legacy_schema_error(exc):
|
| 135 |
+
# Non usare il layout storico: contiene provider fittizi e
|
| 136 |
+
# modelli superati. La migrazione normalizzerà la tabella;
|
| 137 |
+
# nel frattempo il caller seleziona il fallback env corrente.
|
| 138 |
+
return []
|
| 139 |
+
raise
|
| 140 |
rows = res.data or []
|
| 141 |
return [
|
| 142 |
ProviderConfig(
|
tests/test_ai_client_schema_compatibility.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import types
|
| 4 |
+
import unittest
|
| 5 |
+
from unittest.mock import patch
|
| 6 |
+
|
| 7 |
+
from models.ai_client import AIClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class _LegacySchemaError(Exception):
|
| 11 |
+
pass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class _Result:
|
| 15 |
+
def __init__(self, data):
|
| 16 |
+
self.data = data
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class _LegacyQuery:
|
| 20 |
+
def __init__(self, rows):
|
| 21 |
+
self.rows = rows
|
| 22 |
+
self.selects = []
|
| 23 |
+
|
| 24 |
+
def select(self, columns):
|
| 25 |
+
self.selects.append(columns)
|
| 26 |
+
if "default_model" in columns:
|
| 27 |
+
raise _LegacySchemaError("column ai_providers.default_model does not exist")
|
| 28 |
+
return self
|
| 29 |
+
|
| 30 |
+
def eq(self, *_args, **_kwargs):
|
| 31 |
+
return self
|
| 32 |
+
|
| 33 |
+
def order(self, *_args, **_kwargs):
|
| 34 |
+
return self
|
| 35 |
+
|
| 36 |
+
def execute(self):
|
| 37 |
+
return _Result(self.rows)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _LegacySupabase:
|
| 41 |
+
def __init__(self, rows):
|
| 42 |
+
self.query = _LegacyQuery(rows)
|
| 43 |
+
|
| 44 |
+
def table(self, name):
|
| 45 |
+
assert name == "ai_providers"
|
| 46 |
+
return self.query
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class LegacySchemaCompatibilityTests(unittest.TestCase):
|
| 50 |
+
def test_detects_only_known_legacy_missing_columns(self):
|
| 51 |
+
self.assertTrue(
|
| 52 |
+
AIClient._is_legacy_schema_error(
|
| 53 |
+
_LegacySchemaError("column ai_providers.default_model does not exist")
|
| 54 |
+
)
|
| 55 |
+
)
|
| 56 |
+
self.assertFalse(
|
| 57 |
+
AIClient._is_legacy_schema_error(
|
| 58 |
+
_LegacySchemaError("column ai_providers.api_key does not exist")
|
| 59 |
+
)
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def test_legacy_schema_returns_empty_so_current_environment_fallback_stays_authoritative(self):
|
| 63 |
+
legacy_supabase = _LegacySupabase([])
|
| 64 |
+
fake_supabase = types.SimpleNamespace(
|
| 65 |
+
create_client=lambda _url, _key: legacy_supabase,
|
| 66 |
+
)
|
| 67 |
+
client = AIClient.__new__(AIClient)
|
| 68 |
+
|
| 69 |
+
with patch.dict(
|
| 70 |
+
os.environ,
|
| 71 |
+
{"SUPABASE_URL": "https://example.supabase.co", "SUPABASE_SERVICE_ROLE_KEY": "test"},
|
| 72 |
+
clear=True,
|
| 73 |
+
), patch.dict(sys.modules, {"supabase": fake_supabase}):
|
| 74 |
+
providers = client._try_load_from_supabase()
|
| 75 |
+
|
| 76 |
+
self.assertEqual(legacy_supabase.query.selects, [
|
| 77 |
+
"id,name,api_key,base_url,default_model,tier,purpose",
|
| 78 |
+
])
|
| 79 |
+
self.assertEqual(providers, [])
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
unittest.main()
|