Spaces:
Running
Running
sync: 159 file da Baida98/AI@ac70b534 (2026-08-16 12:27 UTC) [deploy-all]
#46
by Baida07 - opened
- .env.example +4 -0
- api/providers.py +46 -4
- api/state.py +1 -1
- main.py +1 -1
- 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 |
-
|
| 241 |
-
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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,
|