sync: 159 file da Baida98/AI@ac70b534 (2026-08-16 12:27 UTC) [deploy-all]

#46
by Baida07 - opened
Files changed (5) hide show
  1. .env.example +4 -0
  2. api/providers.py +46 -4
  3. api/state.py +1 -1
  4. main.py +1 -1
  5. models/ai_client.py +2 -2
.env.example CHANGED
@@ -18,7 +18,11 @@ RAILWAY_PROJECT_ID=YOUR_RAILWAY_PROJECT_ID_A
18
  SUPABASE_URL=
19
  SUPABASE_SERVICE_ROLE_KEY=
20
  GITHUB_TOKEN=
 
21
  HF_TOKEN=
 
 
 
22
 
23
  # ── 3. Quadrante B (HANDS - Collab/Failover) ─────────────────
24
  RAILWAY_TOKEN_B=
 
18
  SUPABASE_URL=
19
  SUPABASE_SERVICE_ROLE_KEY=
20
  GITHUB_TOKEN=
21
+ # Hugging Face Router: endpoint OpenAI-compatible per inferenza.
22
  HF_TOKEN=
23
+ HF_MODEL=Qwen/Qwen2.5-Coder-32B-Instruct
24
+ # Pool opzionale: [{"profile":"primary","api_key":"...","model":"openai/gpt-oss-120b:fastest"}]
25
+ HF_ROUTER_PROFILES_JSON=
26
 
27
  # ── 3. Quadrante B (HANDS - Collab/Failover) ─────────────────
28
  RAILWAY_TOKEN_B=
api/providers.py CHANGED
@@ -1,5 +1,6 @@
1
  """backend/api/providers.py — Health, tools, status, AI health, heartbeat (S354)."""
2
  import os, asyncio, time, logging
 
3
  from fastapi import APIRouter, Request
4
  from fastapi import Depends
5
  from .auth_guard import require_role, AuthRole
@@ -222,6 +223,44 @@ async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACH
222
  from models.ai_client import AIClient
223
  client = AIClient()
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  async def _probe(provider) -> dict:
226
  t0 = time.monotonic()
227
  try:
@@ -237,12 +276,15 @@ async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACH
237
  timeout=8.0,
238
  )
239
  ms = round((time.monotonic() - t0) * 1000)
240
- return {"name": provider.name, "ok": True, "status": "ok", "latency_ms": ms,
241
- "model": provider.default_model.split("/")[-1][:28]}
 
 
242
  except Exception as exc:
243
  ms = round((time.monotonic() - t0) * 1000)
244
- return {"name": provider.name, "ok": False, "status": "error", "latency_ms": ms,
245
- "error": str(exc)[:300], "model": provider.default_model.split("/")[-1][:28]} # S606: 200→300
 
246
 
247
  results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
248
  payload = {"providers": results, "tested_at": int(time.time() * 1000)}
 
1
  """backend/api/providers.py — Health, tools, status, AI health, heartbeat (S354)."""
2
  import os, asyncio, time, logging
3
+ import requests
4
  from fastapi import APIRouter, Request
5
  from fastapi import Depends
6
  from .auth_guard import require_role, AuthRole
 
223
  from models.ai_client import AIClient
224
  client = AIClient()
225
 
226
+ def _classify_probe_error(exc: Exception) -> str:
227
+ message = str(exc).lower()
228
+ if "429" in message or "rate limit" in message or "quota" in message:
229
+ return "rate_limit_or_quota"
230
+ if "402" in message or "payment" in message or "credit" in message:
231
+ return "credits_exhausted"
232
+ if "401" in message or "403" in message or "unauthorized" in message or "forbidden" in message:
233
+ return "authentication_or_permission"
234
+ if "timeout" in message or "timed out" in message:
235
+ return "timeout"
236
+ if "404" in message or "not found" in message:
237
+ return "model_or_endpoint_not_found"
238
+ return "upstream_error"
239
+
240
+ async def _openrouter_key_limits(provider) -> dict:
241
+ if provider.name != "openrouter":
242
+ return {}
243
+ try:
244
+ response = await asyncio.to_thread(
245
+ requests.get,
246
+ "https://openrouter.ai/api/v1/key",
247
+ headers={"Authorization": f"Bearer {provider.api_key}"},
248
+ timeout=8,
249
+ )
250
+ body = response.json() if response.content else {}
251
+ data = body.get("data") if isinstance(body, dict) else {}
252
+ if response.status_code >= 400:
253
+ return {"key_status": response.status_code, "key_error_class": _classify_probe_error(RuntimeError(f"HTTP {response.status_code}"))}
254
+ return {
255
+ "key_status": response.status_code,
256
+ "limit_remaining": data.get("limit_remaining"),
257
+ "limit_reset": data.get("limit_reset"),
258
+ "is_free_tier": data.get("is_free_tier"),
259
+ "usage_daily": data.get("usage_daily"),
260
+ }
261
+ except Exception as exc:
262
+ return {"key_error_class": _classify_probe_error(exc)}
263
+
264
  async def _probe(provider) -> dict:
265
  t0 = time.monotonic()
266
  try:
 
276
  timeout=8.0,
277
  )
278
  ms = round((time.monotonic() - t0) * 1000)
279
+ result = {"name": provider.name, "profile": provider.profile, "ok": True, "status": "ok", "latency_ms": ms,
280
+ "model": provider.default_model.split("/")[-1][:40]}
281
+ result.update(await _openrouter_key_limits(provider))
282
+ return result
283
  except Exception as exc:
284
  ms = round((time.monotonic() - t0) * 1000)
285
+ return {"name": provider.name, "profile": provider.profile, "ok": False, "status": "error", "latency_ms": ms,
286
+ "error_class": _classify_probe_error(exc),
287
+ "error": str(exc)[:300], "model": provider.default_model.split("/")[-1][:40], **(await _openrouter_key_limits(provider))}
288
 
289
  results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
290
  payload = {"providers": results, "tested_at": int(time.time() * 1000)}
api/state.py CHANGED
@@ -121,7 +121,7 @@ SENSITIVE = {
121
  'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY',
122
  'OPENROUTER_PROFILES_JSON', 'GROQ_PROFILES_JSON', 'CEREBRAS_PROFILES_JSON',
123
  'SAMBANOVA_PROFILES_JSON', 'GEMINI_PROFILES_JSON', 'NVIDIA_PROFILES_JSON',
124
- 'HF_ROUTER_PROFILES_JSON',
125
  'GH_PAGES_TOKEN', 'VERCEL_TOKEN',
126
  }
127
 
 
121
  'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY',
122
  'OPENROUTER_PROFILES_JSON', 'GROQ_PROFILES_JSON', 'CEREBRAS_PROFILES_JSON',
123
  'SAMBANOVA_PROFILES_JSON', 'GEMINI_PROFILES_JSON', 'NVIDIA_PROFILES_JSON',
124
+ 'HF_ROUTER_PROFILES_JSON', 'HF_MODEL',
125
  'GH_PAGES_TOKEN', 'VERCEL_TOKEN',
126
  }
127
 
main.py CHANGED
@@ -59,7 +59,7 @@ async def _run_auto_migration():
59
  'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY',
60
  'OPENROUTER_PROFILES_JSON', 'GROQ_PROFILES_JSON', 'CEREBRAS_PROFILES_JSON',
61
  'SAMBANOVA_PROFILES_JSON', 'GEMINI_PROFILES_JSON', 'NVIDIA_PROFILES_JSON',
62
- 'HF_ROUTER_PROFILES_JSON',
63
  'GH_PAGES_TOKEN', 'VERCEL_TOKEN'
64
  ]
65
 
 
59
  'VITE_OPENROUTER_API_KEY', 'VITE_HF_TOKEN', 'VITE_GROQ_API_KEY',
60
  'OPENROUTER_PROFILES_JSON', 'GROQ_PROFILES_JSON', 'CEREBRAS_PROFILES_JSON',
61
  'SAMBANOVA_PROFILES_JSON', 'GEMINI_PROFILES_JSON', 'NVIDIA_PROFILES_JSON',
62
+ 'HF_ROUTER_PROFILES_JSON', 'HF_MODEL',
63
  'GH_PAGES_TOKEN', 'VERCEL_TOKEN'
64
  ]
65
 
models/ai_client.py CHANGED
@@ -307,9 +307,9 @@ class AIClient:
307
  return self._execution_pool(candidates, f"fallback:{purpose}")
308
 
309
  async def _fetch_one(self, provider: ProviderConfig, messages: list, temperature: float, max_tokens: int) -> Tuple[ProviderConfig, str, float]:
310
- client = self._client_for(provider)
311
  start = _time_mod.monotonic()
312
  try:
 
313
  response = await asyncio.wait_for(
314
  asyncio.to_thread(
315
  client.chat.completions.create,
@@ -458,9 +458,9 @@ class AIClient:
458
  attempted: list[str] = []
459
  for provider in providers:
460
  attempted.append(provider.name)
461
- client = self._client_for(provider)
462
  emitted = False
463
  try:
 
464
  stream = await asyncio.to_thread(
465
  client.chat.completions.create,
466
  model=provider.default_model,
 
307
  return self._execution_pool(candidates, f"fallback:{purpose}")
308
 
309
  async def _fetch_one(self, provider: ProviderConfig, messages: list, temperature: float, max_tokens: int) -> Tuple[ProviderConfig, str, float]:
 
310
  start = _time_mod.monotonic()
311
  try:
312
+ client = self._client_for(provider)
313
  response = await asyncio.wait_for(
314
  asyncio.to_thread(
315
  client.chat.completions.create,
 
458
  attempted: list[str] = []
459
  for provider in providers:
460
  attempted.append(provider.name)
 
461
  emitted = False
462
  try:
463
+ client = self._client_for(provider)
464
  stream = await asyncio.to_thread(
465
  client.chat.completions.create,
466
  model=provider.default_model,