sync: 188 file da Baida98/AI@143f6f8b (2026-08-26 07:02 UTC) [deploy-all]

#104
by Baida07 - opened
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