Spaces:
Running
Running
sync: 188 file da Baida98/AI@143f6f8b (2026-08-26 07:02 UTC) [deploy-all]
Browse files- api/providers.py +36 -0
- tests/test_provider_heartbeat_state.py +18 -0
api/providers.py
CHANGED
|
@@ -213,6 +213,42 @@ async def health_manager_status(role: AuthRole = Depends(require_role(AuthRole.M
|
|
| 213 |
from .health_manager import health_manager
|
| 214 |
return await health_manager.get_status()
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
@router.get('/api/ai/health')
|
| 217 |
async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACHINE))): # GAP-1-fix
|
| 218 |
"""Testa tutti i provider AI in parallelo — risultati cachati 60s."""
|
|
|
|
| 213 |
from .health_manager import health_manager
|
| 214 |
return await health_manager.get_status()
|
| 215 |
|
| 216 |
+
def build_server_provider_readiness_payload(
|
| 217 |
+
cached_payload: dict | None,
|
| 218 |
+
configured_count: int,
|
| 219 |
+
) -> dict:
|
| 220 |
+
"""Crea una risposta pubblica minima per il gate UI senza effettuare probe.
|
| 221 |
+
|
| 222 |
+
Se la cache della health ha provider positivi, usa quel dato recente. Altrimenti
|
| 223 |
+
espone solo l’esistenza di provider configurati nel runtime: nessuna chiave,
|
| 224 |
+
quota, modello, errore upstream o identificativo profilo lascia il server.
|
| 225 |
+
"""
|
| 226 |
+
providers = cached_payload.get("providers", []) if isinstance(cached_payload, dict) else []
|
| 227 |
+
healthy_count = sum(
|
| 228 |
+
1 for provider in providers
|
| 229 |
+
if isinstance(provider, dict) and provider.get("ok") is True
|
| 230 |
+
)
|
| 231 |
+
if healthy_count > 0:
|
| 232 |
+
return {"configured": True, "available": healthy_count, "source": "cache"}
|
| 233 |
+
return {
|
| 234 |
+
"configured": configured_count > 0,
|
| 235 |
+
"available": max(configured_count, 0),
|
| 236 |
+
"source": "configured",
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@router.get('/api/ai/readiness')
|
| 241 |
+
async def ai_provider_readiness(role: AuthRole = Depends(require_role(AuthRole.MACHINE))):
|
| 242 |
+
"""Readiness economica per il gate pubblico, senza chiamare provider esterni."""
|
| 243 |
+
configured_count = 0
|
| 244 |
+
try:
|
| 245 |
+
from models.ai_client import AIClient
|
| 246 |
+
configured_count = len(AIClient().providers)
|
| 247 |
+
except Exception as exc:
|
| 248 |
+
_logger.warning("[providers] readiness configuration unavailable: %s", type(exc).__name__)
|
| 249 |
+
return build_server_provider_readiness_payload(_ai_health_cache.get("data"), configured_count)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
@router.get('/api/ai/health')
|
| 253 |
async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACHINE))): # GAP-1-fix
|
| 254 |
"""Testa tutti i provider AI in parallelo — risultati cachati 60s."""
|
tests/test_provider_heartbeat_state.py
CHANGED
|
@@ -24,6 +24,24 @@ class ProviderHeartbeatStateTests(unittest.IsolatedAsyncioTestCase):
|
|
| 24 |
state._heartbeat_state.clear()
|
| 25 |
state._heartbeat_state.update(original)
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
async def test_start_heartbeat_creates_a_single_background_task(self):
|
| 28 |
original_task = providers._heartbeat_task
|
| 29 |
providers._heartbeat_task = None
|
|
|
|
| 24 |
state._heartbeat_state.clear()
|
| 25 |
state._heartbeat_state.update(original)
|
| 26 |
|
| 27 |
+
def test_readiness_prefers_positive_cached_health_without_exposing_provider_details(self):
|
| 28 |
+
payload = providers.build_server_provider_readiness_payload(
|
| 29 |
+
{"providers": [{"ok": True, "name": "hidden", "profile": "secret-profile"}, {"ok": False}]},
|
| 30 |
+
configured_count=0,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
self.assertEqual(payload, {"configured": True, "available": 1, "source": "cache"})
|
| 34 |
+
self.assertNotIn("name", payload)
|
| 35 |
+
self.assertNotIn("profile", payload)
|
| 36 |
+
|
| 37 |
+
def test_readiness_uses_configuration_when_health_cache_is_empty(self):
|
| 38 |
+
payload = providers.build_server_provider_readiness_payload(None, configured_count=3)
|
| 39 |
+
self.assertEqual(payload, {"configured": True, "available": 3, "source": "configured"})
|
| 40 |
+
|
| 41 |
+
def test_readiness_reports_unconfigured_without_triggering_a_probe(self):
|
| 42 |
+
payload = providers.build_server_provider_readiness_payload({"providers": [{"ok": False}]}, configured_count=0)
|
| 43 |
+
self.assertEqual(payload, {"configured": False, "available": 0, "source": "configured"})
|
| 44 |
+
|
| 45 |
async def test_start_heartbeat_creates_a_single_background_task(self):
|
| 46 |
original_task = providers._heartbeat_task
|
| 47 |
providers._heartbeat_task = None
|