sixfingerdev commited on
Commit
e2965ee
·
verified ·
1 Parent(s): 6741397

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +472 -0
app.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sixfinger Backend API - FRONTEND UYUMLU VERSİYON
3
+ Ultra-fast AI Chat Backend with Multi-Model Support
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import json
9
+ import logging
10
+ from typing import Optional, Dict, Any
11
+ from datetime import datetime
12
+
13
+ from fastapi import FastAPI, HTTPException, Header, Request
14
+ from fastapi.responses import StreamingResponse, JSONResponse
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from pydantic import BaseModel, Field
17
+ from groq import Groq
18
+
19
+ # ========== CONFIGURATION ==========
20
+ API_VERSION = "1.0.0"
21
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
22
+
23
+ # Model mapping - Plan bazlı erişim kontrolü
24
+ MODELS = {
25
+ # FREE Plan Models
26
+ "llama-8b-instant": {
27
+ "groq_id": "llama-3.1-8b-instant",
28
+ "size": "8B",
29
+ "language": "Multilingual",
30
+ "speed": "⚡⚡⚡",
31
+ "plans": ["free", "starter", "pro", "plus"],
32
+ "daily_limit": 14400
33
+ },
34
+ "allam-2-7b": {
35
+ "groq_id": "llama-3.1-8b-instant", # Fallback
36
+ "size": "7B",
37
+ "language": "Turkish/Arabic",
38
+ "speed": "⚡⚡",
39
+ "plans": ["free", "starter", "pro", "plus"],
40
+ "daily_limit": 300
41
+ },
42
+
43
+ # STARTER Plan Models
44
+ "qwen3-32b": {
45
+ "groq_id": "llama-3.3-70b-versatile",
46
+ "size": "32B",
47
+ "language": "Turkish/Chinese",
48
+ "speed": "⚡⚡",
49
+ "plans": ["starter", "pro", "plus"],
50
+ "daily_limit": 1000
51
+ },
52
+ "llama-70b": {
53
+ "groq_id": "llama-3.3-70b-versatile",
54
+ "size": "70B",
55
+ "language": "Multilingual",
56
+ "speed": "⚡⚡",
57
+ "plans": ["starter", "pro", "plus"],
58
+ "daily_limit": 1000
59
+ },
60
+ "llama-maverick-17b": {
61
+ "groq_id": "llama-3.1-8b-instant",
62
+ "size": "17B",
63
+ "language": "Multilingual",
64
+ "speed": "⚡⚡",
65
+ "plans": ["starter", "pro", "plus"],
66
+ "daily_limit": 1000
67
+ },
68
+ "llama-scout-17b": {
69
+ "groq_id": "llama-3.1-8b-instant",
70
+ "size": "17B",
71
+ "language": "Multilingual",
72
+ "speed": "⚡⚡⚡",
73
+ "plans": ["starter", "pro", "plus"],
74
+ "daily_limit": 1000
75
+ },
76
+ "gpt-oss-20b": {
77
+ "groq_id": "llama-3.1-8b-instant",
78
+ "size": "20B",
79
+ "language": "Multilingual",
80
+ "speed": "⚡⚡",
81
+ "plans": ["starter", "pro", "plus"],
82
+ "daily_limit": 1000
83
+ },
84
+
85
+ # PRO Plan Models
86
+ "gpt-oss-120b": {
87
+ "groq_id": "llama-3.3-70b-versatile",
88
+ "size": "120B",
89
+ "language": "Multilingual",
90
+ "speed": "⚡⚡",
91
+ "plans": ["pro", "plus"],
92
+ "daily_limit": 1000
93
+ },
94
+ "kimi-k2": {
95
+ "groq_id": "llama-3.3-70b-versatile",
96
+ "size": "Unknown",
97
+ "language": "Chinese",
98
+ "speed": "⚡⚡",
99
+ "plans": ["pro", "plus"],
100
+ "daily_limit": 1000
101
+ }
102
+ }
103
+
104
+ # Plan bazlı otomatik model seçimi
105
+ DEFAULT_MODELS = {
106
+ "free": "llama-8b-instant",
107
+ "starter": "qwen3-32b",
108
+ "pro": "llama-70b",
109
+ "plus": "gpt-oss-120b"
110
+ }
111
+
112
+ # ========== LOGGING ==========
113
+ logging.basicConfig(
114
+ level=logging.INFO,
115
+ format='[%(asctime)s] %(levelname)s: %(message)s',
116
+ datefmt='%Y-%m-%d %H:%M:%S'
117
+ )
118
+ logger = logging.getLogger(__name__)
119
+
120
+ # ========== FASTAPI APP ==========
121
+ app = FastAPI(
122
+ title="Sixfinger Backend API",
123
+ version=API_VERSION,
124
+ description="Ultra-fast AI Chat Backend",
125
+ docs_url="/docs",
126
+ redoc_url="/redoc"
127
+ )
128
+
129
+ # CORS
130
+ app.add_middleware(
131
+ CORSMiddleware,
132
+ allow_origins=["*"], # Production'da kısıtlayın
133
+ allow_credentials=True,
134
+ allow_methods=["*"],
135
+ allow_headers=["*"],
136
+ )
137
+
138
+ # Groq Client
139
+ groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
140
+
141
+ # ========== MODELS ==========
142
+ class ChatRequest(BaseModel):
143
+ prompt: str = Field(..., description="User's message")
144
+ max_tokens: int = Field(default=300, ge=1, le=4000)
145
+ temperature: float = Field(default=0.7, ge=0, le=2)
146
+ top_p: float = Field(default=0.9, ge=0, le=1)
147
+ system_prompt: Optional[str] = None
148
+ history: Optional[list] = None
149
+
150
+ class ChatResponse(BaseModel):
151
+ response: str
152
+ model: str
153
+ model_key: str
154
+ model_size: str
155
+ model_language: str
156
+ attempts: int
157
+ usage: Dict[str, int]
158
+ parameters: Dict[str, Any]
159
+
160
+ # ========== HELPER FUNCTIONS ==========
161
+ def get_allowed_models(plan: str) -> list:
162
+ """Plan'a göre izin verilen modelleri döndür"""
163
+ return [k for k, v in MODELS.items() if plan in v["plans"]]
164
+
165
+ def select_model(plan: str, preferred_model: Optional[str] = None) -> str:
166
+ """Model seçimi yap"""
167
+ allowed_models = get_allowed_models(plan)
168
+
169
+ # Eğer kullanıcı model belirtmişse ve erişimi varsa
170
+ if preferred_model and preferred_model in allowed_models:
171
+ return preferred_model
172
+
173
+ # Otomatik seçim
174
+ default = DEFAULT_MODELS.get(plan, "llama-8b-instant")
175
+ return default if default in allowed_models else allowed_models[0]
176
+
177
+ def build_messages(prompt: str, system_prompt: Optional[str], history: Optional[list]) -> list:
178
+ """Chat messages listesi oluştur"""
179
+ messages = []
180
+
181
+ # System prompt
182
+ if system_prompt:
183
+ messages.append({"role": "system", "content": system_prompt})
184
+
185
+ # History
186
+ if history:
187
+ for msg in history:
188
+ if "role" in msg and "content" in msg:
189
+ messages.append(msg)
190
+
191
+ # Current prompt
192
+ messages.append({"role": "user", "content": prompt})
193
+
194
+ return messages
195
+
196
+ def call_groq_api(
197
+ model_id: str,
198
+ messages: list,
199
+ max_tokens: int,
200
+ temperature: float,
201
+ top_p: float,
202
+ stream: bool = False
203
+ ):
204
+ """Groq API'ye istek at (SYNC)"""
205
+ if not groq_client:
206
+ raise HTTPException(status_code=500, detail="Groq API key not configured")
207
+
208
+ try:
209
+ response = groq_client.chat.completions.create(
210
+ model=model_id,
211
+ messages=messages,
212
+ max_tokens=max_tokens,
213
+ temperature=temperature,
214
+ top_p=top_p,
215
+ stream=stream
216
+ )
217
+ return response
218
+ except Exception as e:
219
+ logger.error(f"Groq API error: {e}")
220
+ raise HTTPException(status_code=500, detail=f"Groq API error: {str(e)}")
221
+
222
+ # ========== ENDPOINTS ==========
223
+
224
+ @app.get("/health")
225
+ def health_check():
226
+ """Health check endpoint"""
227
+ return {
228
+ "status": "healthy",
229
+ "version": API_VERSION,
230
+ "timestamp": datetime.now().isoformat(),
231
+ "groq_configured": bool(GROQ_API_KEY)
232
+ }
233
+
234
+ @app.post("/api/chat")
235
+ def chat(
236
+ request: ChatRequest,
237
+ x_user_plan: str = Header(default="free", alias="X-User-Plan"),
238
+ x_model: Optional[str] = Header(default=None, alias="X-Model")
239
+ ):
240
+ """
241
+ Normal chat endpoint (JSON response)
242
+ Frontend'e TAM UYUMLU format
243
+ """
244
+ start_time = time.time()
245
+
246
+ # Model seçimi
247
+ model_key = select_model(x_user_plan, x_model)
248
+ model_config = MODELS[model_key]
249
+ groq_model_id = model_config["groq_id"]
250
+
251
+ logger.info(f"Chat request: plan={x_user_plan}, model={model_key}")
252
+
253
+ # Messages
254
+ messages = build_messages(
255
+ request.prompt,
256
+ request.system_prompt,
257
+ request.history
258
+ )
259
+
260
+ # Groq API call
261
+ try:
262
+ response = call_groq_api(
263
+ model_id=groq_model_id,
264
+ messages=messages,
265
+ max_tokens=request.max_tokens,
266
+ temperature=request.temperature,
267
+ top_p=request.top_p,
268
+ stream=False
269
+ )
270
+
271
+ # Extract response
272
+ content = response.choices[0].message.content
273
+ usage = {
274
+ "prompt_tokens": response.usage.prompt_tokens,
275
+ "completion_tokens": response.usage.completion_tokens,
276
+ "total_tokens": response.usage.total_tokens
277
+ }
278
+
279
+ elapsed = time.time() - start_time
280
+ logger.info(f"Chat completed: tokens={usage['total_tokens']}, time={elapsed:.2f}s")
281
+
282
+ # Frontend'in beklediği EXACT format
283
+ return {
284
+ "response": content,
285
+ "model": groq_model_id,
286
+ "model_key": model_key,
287
+ "model_size": model_config["size"],
288
+ "model_language": model_config["language"],
289
+ "attempts": 1,
290
+ "usage": usage,
291
+ "parameters": {
292
+ "max_tokens": request.max_tokens,
293
+ "temperature": request.temperature,
294
+ "top_p": request.top_p
295
+ }
296
+ }
297
+
298
+ except HTTPException:
299
+ raise
300
+ except Exception as e:
301
+ logger.error(f"Chat error: {e}")
302
+ raise HTTPException(status_code=500, detail=str(e))
303
+
304
+ @app.post("/api/chat/stream")
305
+ def chat_stream(
306
+ request: ChatRequest,
307
+ x_user_plan: str = Header(default="free", alias="X-User-Plan"),
308
+ x_model: Optional[str] = Header(default=None, alias="X-Model")
309
+ ):
310
+ """
311
+ Streaming chat endpoint (SSE)
312
+ Frontend'e TAM UYUMLU SSE format
313
+ ✅ SYNC generator (FastAPI StreamingResponse için doğru)
314
+ """
315
+ # Model seçimi
316
+ model_key = select_model(x_user_plan, x_model)
317
+ model_config = MODELS[model_key]
318
+ groq_model_id = model_config["groq_id"]
319
+
320
+ logger.info(f"Stream request: plan={x_user_plan}, model={model_key}")
321
+
322
+ # Messages
323
+ messages = build_messages(
324
+ request.prompt,
325
+ request.system_prompt,
326
+ request.history
327
+ )
328
+
329
+ def generate():
330
+ """
331
+ SSE generator - SYNC function (FastAPI requirement)
332
+ Frontend iter_content() ile parse edecek
333
+ """
334
+ try:
335
+ # Info mesajı
336
+ info_msg = json.dumps({'info': f'Trying model: {model_key}'})
337
+ yield f"data: {info_msg}\n\n"
338
+
339
+ # Groq streaming (SYNC)
340
+ response = call_groq_api(
341
+ model_id=groq_model_id,
342
+ messages=messages,
343
+ max_tokens=request.max_tokens,
344
+ temperature=request.temperature,
345
+ top_p=request.top_p,
346
+ stream=True
347
+ )
348
+
349
+ total_tokens = 0
350
+ prompt_tokens = 0
351
+ completion_tokens = 0
352
+
353
+ # Stream chunks
354
+ for chunk in response:
355
+ # Text chunk
356
+ if chunk.choices[0].delta.content:
357
+ text = chunk.choices[0].delta.content
358
+ text_msg = json.dumps({'text': text})
359
+ yield f"data: {text_msg}\n\n"
360
+
361
+ # Usage bilgisi (son chunk'ta gelir)
362
+ if hasattr(chunk, 'x_groq') and hasattr(chunk.x_groq, 'usage'):
363
+ usage_data = chunk.x_groq.usage
364
+ if hasattr(usage_data, 'prompt_tokens'):
365
+ prompt_tokens = usage_data.prompt_tokens
366
+ if hasattr(usage_data, 'completion_tokens'):
367
+ completion_tokens = usage_data.completion_tokens
368
+ if hasattr(usage_data, 'total_tokens'):
369
+ total_tokens = usage_data.total_tokens
370
+
371
+ # Son usage hesaplama (eğer gelmediyse)
372
+ if total_tokens == 0 and completion_tokens > 0:
373
+ total_tokens = prompt_tokens + completion_tokens
374
+
375
+ # Done mesajı - Frontend'in beklediği EXACT format
376
+ done_msg = json.dumps({
377
+ 'done': True,
378
+ 'model_key': model_key,
379
+ 'attempts': 1,
380
+ 'usage': {
381
+ 'prompt_tokens': prompt_tokens,
382
+ 'completion_tokens': completion_tokens,
383
+ 'total_tokens': total_tokens
384
+ }
385
+ })
386
+ yield f"data: {done_msg}\n\n"
387
+
388
+ logger.info(f"Stream completed: model={model_key}, tokens={total_tokens}")
389
+
390
+ except Exception as e:
391
+ logger.error(f"Stream error: {e}")
392
+ error_msg = json.dumps({'error': str(e)})
393
+ yield f"data: {error_msg}\n\n"
394
+
395
+ return StreamingResponse(
396
+ generate(),
397
+ media_type="text/event-stream",
398
+ headers={
399
+ "Cache-Control": "no-cache",
400
+ "X-Accel-Buffering": "no",
401
+ "Connection": "keep-alive"
402
+ }
403
+ )
404
+
405
+ @app.get("/api/models")
406
+ def list_models(x_user_plan: str = Header(default="free", alias="X-User-Plan")):
407
+ """
408
+ Kullanıcının erişebileceği modelleri listele
409
+ """
410
+ allowed_models = get_allowed_models(x_user_plan)
411
+
412
+ models_info = []
413
+ for model_key in allowed_models:
414
+ config = MODELS[model_key]
415
+ models_info.append({
416
+ "key": model_key,
417
+ "size": config["size"],
418
+ "language": config["language"],
419
+ "speed": config["speed"],
420
+ "daily_limit": config["daily_limit"]
421
+ })
422
+
423
+ return {
424
+ "plan": x_user_plan,
425
+ "models": models_info,
426
+ "default_model": DEFAULT_MODELS.get(x_user_plan, "llama-8b-instant")
427
+ }
428
+
429
+ @app.exception_handler(HTTPException)
430
+ async def http_exception_handler(request: Request, exc: HTTPException):
431
+ """Custom HTTP exception handler"""
432
+ return JSONResponse(
433
+ status_code=exc.status_code,
434
+ content={
435
+ "error": exc.detail,
436
+ "status_code": exc.status_code
437
+ }
438
+ )
439
+
440
+ @app.exception_handler(Exception)
441
+ async def general_exception_handler(request: Request, exc: Exception):
442
+ """General exception handler"""
443
+ logger.error(f"Unhandled exception: {exc}")
444
+ return JSONResponse(
445
+ status_code=500,
446
+ content={
447
+ "error": "Internal server error",
448
+ "detail": str(exc)
449
+ }
450
+ )
451
+
452
+ # ========== STARTUP/SHUTDOWN ==========
453
+ @app.on_event("startup")
454
+ async def startup_event():
455
+ logger.info("🚀 Sixfinger Backend API started")
456
+ logger.info(f"📦 Version: {API_VERSION}")
457
+ logger.info(f"🔑 Groq API: {'✅ Configured' if GROQ_API_KEY else '❌ Not configured'}")
458
+ logger.info(f"🤖 Models available: {len(MODELS)}")
459
+
460
+ @app.on_event("shutdown")
461
+ async def shutdown_event():
462
+ logger.info("👋 Sixfinger Backend API shutting down")
463
+
464
+ if __name__ == "__main__":
465
+ import uvicorn
466
+ uvicorn.run(
467
+ "main:app",
468
+ host="0.0.0.0",
469
+ port=8000,
470
+ reload=True,
471
+ log_level="info"
472
+ )