sixfingerdev commited on
Commit
f9336ac
·
verified ·
1 Parent(s): f12f121

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -0
app.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SixFinger Code - Pollinations API Proxy Backend
3
+ Hugging Face Spaces üzerinde çalışır
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException, Header
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel
9
+ from typing import List, Optional, Dict, Any
10
+ import requests
11
+ import os
12
+ import secrets
13
+ from datetime import datetime
14
+ import time
15
+
16
+ app = FastAPI(
17
+ title="SixFinger AI Backend",
18
+ description="Pollinations API Proxy for PythonAnywhere",
19
+ version="1.0.0"
20
+ )
21
+
22
+ # CORS - tüm originlere izin ver (production'da domain belirt)
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["*"], # Production'da: ["https://yourdomain.pythonanywhere.com"]
26
+ allow_credentials=True,
27
+ allow_methods=["*"],
28
+ allow_headers=["*"],
29
+ )
30
+
31
+ # ============================================
32
+ # CONFIGURATION
33
+ # ============================================
34
+
35
+ # API Keys - Space Secrets'tan al
36
+ API_KEYS_RAW = os.getenv('AI_API_KEYS', '')
37
+
38
+ def parse_api_keys():
39
+ """AI_API_KEYS'i parse et"""
40
+ if not API_KEYS_RAW:
41
+ # Fallback - public Pollinations (sınırlı)
42
+ return []
43
+
44
+ if API_KEYS_RAW.startswith('['):
45
+ import json
46
+ try:
47
+ return json.loads(API_KEYS_RAW)
48
+ except:
49
+ pass
50
+
51
+ return [k.strip() for k in API_KEYS_RAW.split(',') if k.strip()]
52
+
53
+ API_KEYS = parse_api_keys()
54
+
55
+ # Backend API Key (güvenlik için)
56
+ BACKEND_API_KEY = os.getenv('BACKEND_API_KEY', secrets.token_urlsafe(32))
57
+
58
+ # Pollinations URL
59
+ POLLINATIONS_URL = "https://api.pollinations.ai/v1/chat/completions"
60
+
61
+ # Rate limiting
62
+ REQUEST_COUNTS = {}
63
+ MAX_REQUESTS_PER_MINUTE = 60
64
+
65
+ # ============================================
66
+ # MODELS
67
+ # ============================================
68
+
69
+ class Message(BaseModel):
70
+ role: str
71
+ content: str
72
+
73
+ class ChatRequest(BaseModel):
74
+ model: str
75
+ messages: List[Message]
76
+ stream: Optional[bool] = False
77
+ temperature: Optional[float] = 0.7
78
+ max_tokens: Optional[int] = 2000
79
+
80
+ class ChatResponse(BaseModel):
81
+ id: str
82
+ object: str
83
+ created: int
84
+ model: str
85
+ choices: List[Dict[str, Any]]
86
+ usage: Dict[str, int]
87
+
88
+ class HealthResponse(BaseModel):
89
+ status: str
90
+ timestamp: str
91
+ api_keys_count: int
92
+ version: str
93
+
94
+ # ============================================
95
+ # KEY MANAGER
96
+ # ============================================
97
+
98
+ class APIKeyManager:
99
+ def __init__(self, keys: List[str]):
100
+ self.keys = keys if keys else [None] # None = keyless mode
101
+ self.failed_keys = {}
102
+ self.current_index = 0
103
+ self.cooldown = 60
104
+
105
+ def get_working_key(self) -> Optional[str]:
106
+ if not self.keys or self.keys[0] is None:
107
+ return None # Keyless mode
108
+
109
+ now = time.time()
110
+ attempts = 0
111
+
112
+ while attempts < len(self.keys):
113
+ key = self.keys[self.current_index]
114
+
115
+ if key in self.failed_keys:
116
+ if now - self.failed_keys[key] > self.cooldown:
117
+ del self.failed_keys[key]
118
+ else:
119
+ self.current_index = (self.current_index + 1) % len(self.keys)
120
+ attempts += 1
121
+ continue
122
+
123
+ return key
124
+
125
+ # All keys failed - use oldest
126
+ if self.failed_keys:
127
+ oldest_key = min(self.failed_keys, key=self.failed_keys.get)
128
+ del self.failed_keys[oldest_key]
129
+ return oldest_key
130
+
131
+ return self.keys[0] if self.keys else None
132
+
133
+ def mark_failed(self, key: Optional[str]):
134
+ if key:
135
+ self.failed_keys[key] = time.time()
136
+ self.current_index = (self.current_index + 1) % len(self.keys)
137
+
138
+ def mark_success(self, key: Optional[str]):
139
+ if key and key in self.failed_keys:
140
+ del self.failed_keys[key]
141
+
142
+ key_manager = APIKeyManager(API_KEYS)
143
+
144
+ # ============================================
145
+ # MIDDLEWARE
146
+ # ============================================
147
+
148
+ def verify_api_key(authorization: Optional[str] = Header(None)):
149
+ """Backend API key doğrulama"""
150
+ if not authorization:
151
+ raise HTTPException(status_code=401, detail="Missing authorization header")
152
+
153
+ if not authorization.startswith("Bearer "):
154
+ raise HTTPException(status_code=401, detail="Invalid authorization format")
155
+
156
+ token = authorization.replace("Bearer ", "")
157
+
158
+ if token != BACKEND_API_KEY:
159
+ raise HTTPException(status_code=401, detail="Invalid API key")
160
+
161
+ return token
162
+
163
+ def rate_limit_check(client_id: str):
164
+ """Basit rate limiting"""
165
+ now = time.time()
166
+ minute_ago = now - 60
167
+
168
+ if client_id not in REQUEST_COUNTS:
169
+ REQUEST_COUNTS[client_id] = []
170
+
171
+ # Eski istekleri temizle
172
+ REQUEST_COUNTS[client_id] = [
173
+ req_time for req_time in REQUEST_COUNTS[client_id]
174
+ if req_time > minute_ago
175
+ ]
176
+
177
+ if len(REQUEST_COUNTS[client_id]) >= MAX_REQUESTS_PER_MINUTE:
178
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
179
+
180
+ REQUEST_COUNTS[client_id].append(now)
181
+
182
+ # ============================================
183
+ # ROUTES
184
+ # ============================================
185
+
186
+ @app.get("/", response_model=HealthResponse)
187
+ async def health_check():
188
+ """Health check endpoint"""
189
+ return {
190
+ "status": "healthy",
191
+ "timestamp": datetime.utcnow().isoformat(),
192
+ "api_keys_count": len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0,
193
+ "version": "1.0.0"
194
+ }
195
+
196
+ @app.get("/health", response_model=HealthResponse)
197
+ async def health():
198
+ """Alias for health check"""
199
+ return await health_check()
200
+
201
+ @app.post("/v1/chat/completions")
202
+ async def chat_completion(
203
+ request: ChatRequest,
204
+ authorization: str = Header(None)
205
+ ):
206
+ """
207
+ Pollinations API proxy endpoint
208
+
209
+ Headers:
210
+ Authorization: Bearer YOUR_BACKEND_API_KEY
211
+
212
+ Body:
213
+ {
214
+ "model": "openai",
215
+ "messages": [{"role": "user", "content": "Hello"}],
216
+ "stream": false
217
+ }
218
+ """
219
+
220
+ # Verify backend API key
221
+ verify_api_key(authorization)
222
+
223
+ # Rate limiting (IP bazlı olabilir, şimdilik basit)
224
+ client_id = authorization # veya request.client.host
225
+ rate_limit_check(client_id)
226
+
227
+ # Get working API key
228
+ api_key = key_manager.get_working_key()
229
+
230
+ # Prepare headers
231
+ headers = {
232
+ "Content-Type": "application/json"
233
+ }
234
+
235
+ if api_key:
236
+ headers["Authorization"] = f"Bearer {api_key}"
237
+
238
+ # Prepare payload
239
+ payload = {
240
+ "model": request.model,
241
+ "messages": [{"role": m.role, "content": m.content} for m in request.messages],
242
+ "stream": request.stream,
243
+ "temperature": request.temperature,
244
+ "max_tokens": request.max_tokens
245
+ }
246
+
247
+ # Call Pollinations API
248
+ max_retries = len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 3
249
+ last_error = None
250
+
251
+ for attempt in range(max_retries):
252
+ try:
253
+ response = requests.post(
254
+ POLLINATIONS_URL,
255
+ json=payload,
256
+ headers=headers,
257
+ timeout=120
258
+ )
259
+
260
+ if response.status_code == 200:
261
+ key_manager.mark_success(api_key)
262
+ return response.json()
263
+
264
+ elif response.status_code == 429:
265
+ # Rate limit - try next key
266
+ key_manager.mark_failed(api_key)
267
+ api_key = key_manager.get_working_key()
268
+ if api_key:
269
+ headers["Authorization"] = f"Bearer {api_key}"
270
+ time.sleep(1)
271
+ continue
272
+
273
+ elif response.status_code == 401:
274
+ # Invalid key - try next
275
+ key_manager.mark_failed(api_key)
276
+ api_key = key_manager.get_working_key()
277
+ if api_key:
278
+ headers["Authorization"] = f"Bearer {api_key}"
279
+ continue
280
+
281
+ else:
282
+ # Other error
283
+ last_error = f"API error: {response.status_code} - {response.text[:200]}"
284
+ raise HTTPException(status_code=response.status_code, detail=last_error)
285
+
286
+ except requests.exceptions.Timeout:
287
+ last_error = "Request timeout"
288
+ if attempt < max_retries - 1:
289
+ time.sleep(2)
290
+ continue
291
+ raise HTTPException(status_code=504, detail=last_error)
292
+
293
+ except requests.exceptions.RequestException as e:
294
+ last_error = str(e)
295
+ if attempt < max_retries - 1:
296
+ time.sleep(1)
297
+ continue
298
+ raise HTTPException(status_code=500, detail=f"Request failed: {last_error}")
299
+
300
+ # All retries failed
301
+ raise HTTPException(status_code=503, detail=f"All API keys failed: {last_error}")
302
+
303
+ @app.get("/stats")
304
+ async def get_stats(authorization: str = Header(None)):
305
+ """Backend istatistikleri (admin only)"""
306
+ verify_api_key(authorization)
307
+
308
+ return {
309
+ "total_keys": len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0,
310
+ "failed_keys": len(key_manager.failed_keys),
311
+ "active_clients": len(REQUEST_COUNTS),
312
+ "requests_last_minute": sum(len(v) for v in REQUEST_COUNTS.values())
313
+ }
314
+
315
+ # ============================================
316
+ # ERROR HANDLERS
317
+ # ============================================
318
+
319
+ @app.exception_handler(HTTPException)
320
+ async def http_exception_handler(request, exc):
321
+ return {
322
+ "error": {
323
+ "message": exc.detail,
324
+ "type": "api_error",
325
+ "code": exc.status_code
326
+ }
327
+ }
328
+
329
+ @app.exception_handler(Exception)
330
+ async def general_exception_handler(request, exc):
331
+ return {
332
+ "error": {
333
+ "message": str(exc),
334
+ "type": "internal_error",
335
+ "code": 500
336
+ }
337
+ }
338
+
339
+ # ============================================
340
+ # STARTUP
341
+ # ============================================
342
+
343
+ @app.on_event("startup")
344
+ async def startup_event():
345
+ print("=" * 60)
346
+ print("🚀 SixFinger AI Backend Starting...")
347
+ print("=" * 60)
348
+ print(f"📦 API Keys: {len(API_KEYS) if API_KEYS and API_KEYS[0] is not None else 0}")
349
+ print(f"🔑 Backend API Key: {BACKEND_API_KEY[:10]}...")
350
+ print(f"🌐 Pollinations URL: {POLLINATIONS_URL}")
351
+ print(f"⏱️ Rate Limit: {MAX_REQUESTS_PER_MINUTE} req/min")
352
+ print("=" * 60)
353
+ print("✅ Ready to serve!")
354
+ print("=" * 60)
355
+
356
+ if __name__ == "__main__":
357
+ import uvicorn
358
+ uvicorn.run(app, host="0.0.0.0", port=7860)