Spaces:
Sleeping
Sleeping
| """ | |
| SixFinger Code - Pollinations API Proxy Backend | |
| Hugging Face Spaces üzerinde çalışır | |
| """ | |
| from fastapi import FastAPI, HTTPException, Header | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Dict, Any | |
| import requests | |
| import os | |
| import secrets | |
| from datetime import datetime | |
| import time | |
| app = FastAPI( | |
| title="SixFinger AI Backend", | |
| description="Pollinations API Proxy for PythonAnywhere", | |
| version="1.0.0" | |
| ) | |
| # CORS - tüm originlere izin ver (production'da domain belirt) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Production'da: ["https://yourdomain.pythonanywhere.com"] | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ============================================ | |
| # CONFIGURATION | |
| # ============================================ | |
| # API Keys - Space Secrets'tan al | |
| API_KEYS_RAW = os.getenv('AI_API_KEYS', '') | |
| def parse_api_keys(): | |
| """AI_API_KEYS'i parse et""" | |
| if not API_KEYS_RAW: | |
| # Fallback - public Pollinations (sınırlı) | |
| return [] | |
| if API_KEYS_RAW.startswith('['): | |
| import json | |
| try: | |
| return json.loads(API_KEYS_RAW) | |
| except: | |
| pass | |
| return [k.strip() for k in API_KEYS_RAW.split(',') if k.strip()] | |
| API_KEYS = parse_api_keys() | |
| # Backend API Key (güvenlik için) | |
| BACKEND_API_KEY = os.getenv('BACKEND_API_KEY', secrets.token_urlsafe(32)) | |
| # Pollinations URL | |
| POLLINATIONS_URL = "https://api.pollinations.ai/v1/chat/completions" | |
| # Rate limiting | |
| REQUEST_COUNTS = {} | |
| MAX_REQUESTS_PER_MINUTE = 60 | |
| # ============================================ | |
| # MODELS | |
| # ============================================ | |
| class Message(BaseModel): | |
| role: str | |
| content: str | |
| class ChatRequest(BaseModel): | |
| model: str | |
| messages: List[Message] | |
| stream: Optional[bool] = False | |
| temperature: Optional[float] = 0.7 | |
| max_tokens: Optional[int] = 2000 | |
| class ChatResponse(BaseModel): | |
| id: str | |
| object: str | |
| created: int | |
| model: str | |
| choices: List[Dict[str, Any]] | |
| usage: Dict[str, int] | |
| class HealthResponse(BaseModel): | |
| status: str | |
| timestamp: str | |
| api_keys_count: int | |
| version: str | |
| # ============================================ | |
| # KEY MANAGER | |
| # ============================================ | |
| class APIKeyManager: | |
| def __init__(self, keys: List[str]): | |
| self.keys = keys if keys else [None] # None = keyless mode | |
| self.failed_keys = {} | |
| self.current_index = 0 | |
| self.cooldown = 60 | |
| def get_working_key(self) -> Optional[str]: | |
| if not self.keys or self.keys[0] is None: | |
| return None # Keyless mode | |
| now = time.time() | |
| attempts = 0 | |
| while attempts < len(self.keys): | |
| key = self.keys[self.current_index] | |
| if key in self.failed_keys: | |
| if now - self.failed_keys[key] > self.cooldown: | |
| del self.failed_keys[key] | |
| else: | |
| self.current_index = (self.current_index + 1) % len(self.keys) | |
| attempts += 1 | |
| continue | |
| return key | |
| # All keys failed - use oldest | |
| if self.failed_keys: | |
| oldest_key = min(self.failed_keys, key=self.failed_keys.get) | |
| del self.failed_keys[oldest_key] | |
| return oldest_key | |
| return self.keys[0] if self.keys else None | |
| def mark_failed(self, key: Optional[str]): | |
| if key: | |
| self.failed_keys[key] = time.time() | |
| self.current_index = (self.current_index + 1) % len(self.keys) | |
| def mark_success(self, key: Optional[str]): | |
| if key and key in self.failed_keys: | |
| del self.failed_keys[key] | |
| key_manager = APIKeyManager(API_KEYS) | |
| # ============================================ | |
| # MIDDLEWARE | |
| # ============================================ | |
| def verify_api_key(authorization: Optional[str] = Header(None)): | |
| """Backend API key doğrulama""" | |
| if not authorization: | |
| raise HTTPException(status_code=401, detail="Missing authorization header") | |
| if not authorization.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Invalid authorization format") | |
| token = authorization.replace("Bearer ", "") | |
| if token != BACKEND_API_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid API key") | |
| return token | |
| def rate_limit_check(client_id: str): | |
| """Basit rate limiting""" | |
| now = time.time() | |
| minute_ago = now - 60 | |
| if client_id not in REQUEST_COUNTS: | |
| REQUEST_COUNTS[client_id] = [] | |
| # Eski istekleri temizle | |
| REQUEST_COUNTS[client_id] = [ | |
| req_time for req_time in REQUEST_COUNTS[client_id] | |
| if req_time > minute_ago | |
| ] | |
| if len(REQUEST_COUNTS[client_id]) >= MAX_REQUESTS_PER_MINUTE: | |
| raise HTTPException(status_code=429, detail="Rate limit exceeded") | |
| REQUEST_COUNTS[client_id].append(now) | |
| # ============================================ | |
| # ROUTES | |
| # ============================================ | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return { | |
| "status": "healthy", | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "api_keys_count": len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0, | |
| "version": "1.0.0" | |
| } | |
| async def health(): | |
| """Alias for health check""" | |
| return await health_check() | |
| async def chat_completion( | |
| request: ChatRequest, | |
| authorization: str = Header(None) | |
| ): | |
| """ | |
| Pollinations API proxy endpoint | |
| Headers: | |
| Authorization: Bearer YOUR_BACKEND_API_KEY | |
| Body: | |
| { | |
| "model": "openai", | |
| "messages": [{"role": "user", "content": "Hello"}], | |
| "stream": false | |
| } | |
| """ | |
| # Verify backend API key | |
| verify_api_key(authorization) | |
| # Rate limiting (IP bazlı olabilir, şimdilik basit) | |
| client_id = authorization # veya request.client.host | |
| rate_limit_check(client_id) | |
| # Get working API key | |
| api_key = key_manager.get_working_key() | |
| # Prepare headers | |
| headers = { | |
| "Content-Type": "application/json" | |
| } | |
| if api_key: | |
| headers["Authorization"] = f"Bearer {api_key}" | |
| # Prepare payload | |
| payload = { | |
| "model": request.model, | |
| "messages": [{"role": m.role, "content": m.content} for m in request.messages], | |
| "stream": request.stream, | |
| "temperature": request.temperature, | |
| "max_tokens": request.max_tokens | |
| } | |
| # Call Pollinations API | |
| max_retries = len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 3 | |
| last_error = None | |
| for attempt in range(max_retries): | |
| try: | |
| response = requests.post( | |
| POLLINATIONS_URL, | |
| json=payload, | |
| headers=headers, | |
| timeout=120 | |
| ) | |
| if response.status_code == 200: | |
| key_manager.mark_success(api_key) | |
| return response.json() | |
| elif response.status_code == 429: | |
| # Rate limit - try next key | |
| key_manager.mark_failed(api_key) | |
| api_key = key_manager.get_working_key() | |
| if api_key: | |
| headers["Authorization"] = f"Bearer {api_key}" | |
| time.sleep(1) | |
| continue | |
| elif response.status_code == 401: | |
| # Invalid key - try next | |
| key_manager.mark_failed(api_key) | |
| api_key = key_manager.get_working_key() | |
| if api_key: | |
| headers["Authorization"] = f"Bearer {api_key}" | |
| continue | |
| else: | |
| # Other error | |
| last_error = f"API error: {response.status_code} - {response.text[:200]}" | |
| raise HTTPException(status_code=response.status_code, detail=last_error) | |
| except requests.exceptions.Timeout: | |
| last_error = "Request timeout" | |
| if attempt < max_retries - 1: | |
| time.sleep(2) | |
| continue | |
| raise HTTPException(status_code=504, detail=last_error) | |
| except requests.exceptions.RequestException as e: | |
| last_error = str(e) | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| continue | |
| raise HTTPException(status_code=500, detail=f"Request failed: {last_error}") | |
| # All retries failed | |
| raise HTTPException(status_code=503, detail=f"All API keys failed: {last_error}") | |
| async def get_stats(authorization: str = Header(None)): | |
| """Backend istatistikleri (admin only)""" | |
| verify_api_key(authorization) | |
| return { | |
| "total_keys": len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0, | |
| "failed_keys": len(key_manager.failed_keys), | |
| "active_clients": len(REQUEST_COUNTS), | |
| "requests_last_minute": sum(len(v) for v in REQUEST_COUNTS.values()) | |
| } | |
| # ============================================ | |
| # ERROR HANDLERS | |
| # ============================================ | |
| async def http_exception_handler(request, exc): | |
| return { | |
| "error": { | |
| "message": exc.detail, | |
| "type": "api_error", | |
| "code": exc.status_code | |
| } | |
| } | |
| async def general_exception_handler(request, exc): | |
| return { | |
| "error": { | |
| "message": str(exc), | |
| "type": "internal_error", | |
| "code": 500 | |
| } | |
| } | |
| # ============================================ | |
| # STARTUP | |
| # ============================================ | |
| async def startup_event(): | |
| print("=" * 60) | |
| print("🚀 SixFinger AI Backend Starting...") | |
| print("=" * 60) | |
| print(f"📦 API Keys: {len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0}") | |
| print(f"🔑 Backend API Key: {BACKEND_API_KEY[:10]}...") | |
| print(f"🌐 Pollinations URL: {POLLINATIONS_URL}") | |
| print(f"⏱️ Rate Limit: {MAX_REQUESTS_PER_MINUTE} req/min") | |
| print("=" * 60) | |
| print("✅ Ready to serve!") | |
| print("=" * 60) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |