sharktide commited on
Commit
ba68bd5
·
verified ·
1 Parent(s): 5e7d60a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -102
app.py CHANGED
@@ -33,12 +33,78 @@ async def reroute_to_status():
33
 
34
  OLLAMA_LIBRARY_URL = "https://ollama.com/library"
35
 
36
- RATE_LIMIT = 60
37
- WINDOW_SECONDS = 60 * 60 * 24
38
- ip_store = {} # { ip: { "count": int, "reset": timestamp } }
39
- AUDIO_RATE_LIMIT = 10
40
- AUDIO_WINDOW_SECONDS = 60 * 60 * 24
41
- audio_ip_store = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  REASONING_KEYWORDS = [
44
  # explicit reasoning requests
@@ -211,28 +277,106 @@ def extract_user_text(messages: list) -> str:
211
  if m.get("role") == "user"
212
  ).lower()
213
 
214
- def check_audio_rate_limit(ip: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  now = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- if ip not in audio_ip_store:
218
- audio_ip_store[ip] = {
219
- "count": 0,
220
- "reset": now + AUDIO_WINDOW_SECONDS
221
- }
222
 
223
- entry = audio_ip_store[ip]
 
 
 
 
 
 
224
 
225
- if now > entry["reset"]:
226
- entry["count"] = 0
227
- entry["reset"] = now + AUDIO_WINDOW_SECONDS
 
228
 
229
- if entry["count"] >= AUDIO_RATE_LIMIT:
 
 
 
 
 
 
 
 
 
 
230
  raise HTTPException(
231
  status_code=429,
232
- detail="Daily audio limit reached: 10 per IP"
233
  )
234
 
235
  entry["count"] += 1
 
 
 
 
 
 
236
 
237
 
238
  def is_complex_reasoning(prompt: str) -> bool:
@@ -263,35 +407,17 @@ def is_cinematic_image_prompt(prompt: str) -> bool:
263
  return True
264
  return False
265
 
266
- def check_rate_limit(ip: str):
267
- now = time.time()
268
-
269
- if ip not in ip_store:
270
- ip_store[ip] = {"count": 0, "reset": now + WINDOW_SECONDS}
271
-
272
- entry = ip_store[ip]
273
-
274
- if now > entry["reset"]:
275
- entry["count"] = 0
276
- entry["reset"] = now + WINDOW_SECONDS
277
 
278
- if entry["count"] >= RATE_LIMIT:
279
- raise HTTPException(
280
- status_code=429,
281
- detail="Daily limit reached: 25 images per IP"
282
- )
283
 
284
- entry["count"] += 1
 
285
 
286
  PKEY = os.getenv("POLLINATIONS_KEY", "")
287
  PKEY2 = os.getenv("POLLINATIONS2_KEY", "")
288
  PKEY3 = os.getenv("POLLINATIONS3_KEY", "")
289
 
290
- CHAT_RATE_LIMIT = 50
291
- CHAT_WINDOW_SECONDS = 60 * 60
292
-
293
- chat_ip_store = {}
294
-
295
  GROQ_TOOL_MODELS = [
296
  "openai/gpt-oss-120b",
297
  "openai/gpt-oss-20b",
@@ -316,29 +442,8 @@ CEREBRAS_MODELS = [
316
  "zai-glm-4.7",
317
  ]
318
 
319
- def check_chat_rate_limit(ip: str):
320
- now = time.time()
321
-
322
- if ip not in chat_ip_store:
323
- chat_ip_store[ip] = {
324
- "count": 0,
325
- "reset": now + CHAT_WINDOW_SECONDS
326
- }
327
-
328
- entry = chat_ip_store[ip]
329
-
330
- if now > entry["reset"]:
331
- entry["count"] = 0
332
- entry["reset"] = now + CHAT_WINDOW_SECONDS
333
-
334
- if entry["count"] >= CHAT_RATE_LIMIT:
335
- raise HTTPException(
336
- status_code=429,
337
- detail="Chat rate limit exceeded"
338
- )
339
-
340
- entry["count"] += 1
341
- return entry["count"]
342
 
343
  @app.head("/status/sfx")
344
  async def head_sfx():
@@ -423,9 +528,12 @@ async def get_status():
423
 
424
  @app.post("/gen/image")
425
  @app.get("/genimg/{prompt}")
426
- async def generate_image(request: Request, prompt: str = None):
427
- client_ip = request.client.host
428
- check_rate_limit(client_ip)
 
 
 
429
  timeout = httpx.Timeout(300.0, read=300.0)
430
  if prompt is None:
431
  prompt = (await request.json()).get("prompt")
@@ -490,14 +598,13 @@ async def get_models() -> List[Dict]:
490
  return models
491
 
492
  @app.post("/gen/chat/completions")
493
- async def generate_text(request: Request):
494
  body = await request.json()
495
  messages = body.get("messages", [])
496
  if not isinstance(messages, list) or len(messages) == 0:
497
  raise HTTPException(400, "messages[] is required")
498
 
499
- ip = request.client.host
500
- msg_count = check_chat_rate_limit(ip)
501
  prompt_text = extract_user_text(messages)
502
 
503
  uses_tools = (
@@ -645,9 +752,12 @@ async def generate_text(request: Request):
645
 
646
  @app.get("/gen/sfx/{prompt}")
647
  @app.post("/gen/sfx")
648
- async def gensfx(request: Request, prompt: str = None):
649
- client_ip = request.client.host
650
- check_audio_rate_limit(client_ip)
 
 
 
651
  if prompt is None:
652
  prompt = (await request.json()).get("prompt")
653
  url = f"https://gen.pollinations.ai/audio/{prompt}?model=elevenmusic&key={PKEY}"
@@ -675,9 +785,12 @@ async def gensfx(request: Request, prompt: str = None):
675
 
676
  @app.get("/gen/tts/{prompt}")
677
  @app.post("/gen/tts")
678
- async def gensfx(request: Request, prompt: str = None):
679
- client_ip = request.client.host
680
- check_rate_limit(client_ip)
 
 
 
681
  if prompt is None:
682
  prompt = (await request.json()).get("prompt")
683
  url = f"https://gen.pollinations.ai/audio/{prompt}?key={PKEY3}"
@@ -706,9 +819,13 @@ async def gensfx(request: Request, prompt: str = None):
706
  @app.get("/gen/video/{prompt}")
707
  @app.post("/gen/video")
708
  @app.head("/gen/video")
709
- async def genvideo(request: Request, prompt: str = None):
710
- client_ip = request.client.host
711
- check_rate_limit(client_ip)
 
 
 
 
712
  return RedirectResponse(url="/gen/video/airforce", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
713
 
714
  if prompt is None:
@@ -801,7 +918,11 @@ MAX_VIDEO_RETRIES = 6
801
  @app.get("/gen/video/airforce/{prompt}")
802
  @app.post("/gen/video/airforce")
803
  @app.head("/gen/video/airforce")
804
- async def genvideo_airforce(request: Request, prompt: str = None):
 
 
 
 
805
  if request.method == "HEAD":
806
  return Response(
807
  status_code=200,
@@ -833,6 +954,8 @@ async def genvideo_airforce(request: Request, prompt: str = None):
833
  }
834
  )
835
 
 
 
836
  aspectRatio = "3:2"
837
  inputMode = "normal"
838
 
@@ -932,36 +1055,61 @@ async def get_subscription(authorization: Optional[str] = Header(None)):
932
  if "error" in result:
933
  raise HTTPException(401, result["error"])
934
 
 
 
 
935
  return result
936
 
937
- @app.get("/tiers")
938
- async def tiers():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  return JSONResponse(
940
  status_code=200,
941
- content=[
942
- {
943
- "name": "InferencePort AI Light",
944
- "url": "https://buy.stripe.com/test_6oUcN5g665rp7nLgaq8bS00",
945
- "price": "9.99"
946
- },
947
- {
948
- "name": "InferencePort AI Pro",
949
- "url": "https://buy.stripe.com/test_bJe9AT2fg6vt23rgaq8bS01",
950
- "price": "15.99"
951
- },
952
- {
953
- "name": "InferencePort AI Creator",
954
- "url": "https://buy.stripe.com/test_14AaEX9HIdXV8rPf6m8bS02",
955
- "price": "29.99"
956
- },
957
  {
958
- "name": "InferencePort AI Professional",
959
- "url": "https://buy.stripe.com/test_5kQ00jf22cTR0ZncYe8bS03",
960
- "price": "99.99"
 
 
961
  }
962
- ]
 
 
 
 
963
  )
964
 
965
  @app.get("/portal")
966
  def a():
967
- return RedirectResponse(url="https://billing.stripe.com/p/login/test_6oUcN5g665rp7nLgaq8bS00", status_code=status.HTTP_302_FOUND)
 
33
 
34
  OLLAMA_LIBRARY_URL = "https://ollama.com/library"
35
 
36
+ PLAN_ORDER = ["free", "light", "pro", "creator", "professional"]
37
+ TIER_CONFIG = {
38
+ "free": {
39
+ "name": "Free Tier",
40
+ "url": "",
41
+ "price": "0.00",
42
+ "limits": {
43
+ "cloudChatDaily": 50,
44
+ "imagesDaily": 10,
45
+ "videosDaily": 3,
46
+ "audioWeekly": 1,
47
+ },
48
+ },
49
+ "light": {
50
+ "name": "InferencePort AI Light",
51
+ "url": "https://buy.stripe.com/test_6oUcN5g665rp7nLgaq8bS00",
52
+ "price": "9.99",
53
+ "limits": {
54
+ "cloudChatDaily": None,
55
+ "imagesDaily": 50,
56
+ "videosDaily": 10,
57
+ "audioWeekly": 5,
58
+ },
59
+ },
60
+ "pro": {
61
+ "name": "InferencePort AI Pro",
62
+ "url": "https://buy.stripe.com/test_bJe9AT2fg6vt23rgaq8bS01",
63
+ "price": "15.99",
64
+ "limits": {
65
+ "cloudChatDaily": None,
66
+ "imagesDaily": 150,
67
+ "videosDaily": None,
68
+ "audioWeekly": 25,
69
+ },
70
+ },
71
+ "creator": {
72
+ "name": "InferencePort AI Creator",
73
+ "url": "https://buy.stripe.com/test_14AaEX9HIdXV8rPf6m8bS02",
74
+ "price": "29.99",
75
+ "limits": {
76
+ "cloudChatDaily": None,
77
+ "imagesDaily": 300,
78
+ "videosDaily": 50,
79
+ "audioWeekly": 45,
80
+ },
81
+ },
82
+ "professional": {
83
+ "name": "InferencePort AI Professional",
84
+ "url": "https://buy.stripe.com/test_5kQ00jf22cTR0ZncYe8bS03",
85
+ "price": "99.99",
86
+ "limits": {
87
+ "cloudChatDaily": None,
88
+ "imagesDaily": None,
89
+ "videosDaily": None,
90
+ "audioWeekly": 75,
91
+ },
92
+ },
93
+ }
94
+ USAGE_PERIODS = {
95
+ "cloudChatDaily": "daily",
96
+ "imagesDaily": "daily",
97
+ "videosDaily": "daily",
98
+ "audioWeekly": "weekly",
99
+ }
100
+ usage_store = {
101
+ "cloudChatDaily": {},
102
+ "imagesDaily": {},
103
+ "videosDaily": {},
104
+ "audioWeekly": {},
105
+ }
106
+ IDENTITY_CACHE_TTL_SECONDS = 60
107
+ identity_cache = {}
108
 
109
  REASONING_KEYWORDS = [
110
  # explicit reasoning requests
 
277
  if m.get("role") == "user"
278
  ).lower()
279
 
280
+ def normalize_plan_key(plan_name: Optional[str]) -> str:
281
+ if not plan_name:
282
+ return "free"
283
+ normalized = "".join(ch for ch in str(plan_name).lower() if ch.isalpha())
284
+ if "professional" in normalized:
285
+ return "professional"
286
+ if "creator" in normalized:
287
+ return "creator"
288
+ if "pro" in normalized:
289
+ return "pro"
290
+ if "light" in normalized:
291
+ return "light"
292
+ return "free"
293
+
294
+
295
+ def get_usage_period_key(metric: str) -> str:
296
+ now = time.gmtime()
297
+ period = USAGE_PERIODS.get(metric, "daily")
298
+ if period == "weekly":
299
+ iso_year, iso_week, _ = time.strftime("%G %V %u", now).split(" ")
300
+ return f"{iso_year}-W{iso_week}"
301
+ return time.strftime("%Y-%m-%d", now)
302
+
303
+
304
+ async def resolve_rate_limit_identity(
305
+ request: Request,
306
+ authorization: Optional[str],
307
+ ) -> tuple[str, str]:
308
  now = time.time()
309
+ default_subject = f"ip:{request.client.host if request.client else 'unknown'}"
310
+ if not authorization or not authorization.startswith("Bearer "):
311
+ return "free", default_subject
312
+
313
+ token = authorization.split(" ", 1)[1].strip()
314
+ if not token:
315
+ return "free", default_subject
316
+
317
+ cached = identity_cache.get(token)
318
+ if cached and cached.get("expires_at", 0) > now:
319
+ return cached.get("plan_key", "free"), cached.get("subject", default_subject)
320
+
321
+ try:
322
+ sub = await fetch_subscription(token)
323
+ except Exception:
324
+ return "free", default_subject
325
+
326
+ if not isinstance(sub, dict) or sub.get("error"):
327
+ return "free", default_subject
328
+
329
+ email = sub.get("email")
330
+ if isinstance(email, str) and email.strip():
331
+ subject = f"user:{email.strip().lower()}"
332
+ else:
333
+ subject = default_subject
334
+
335
+ plan_key = normalize_plan_key(sub.get("plan_key"))
336
+ identity_cache[token] = {
337
+ "plan_key": plan_key,
338
+ "subject": subject,
339
+ "expires_at": now + IDENTITY_CACHE_TTL_SECONDS,
340
+ }
341
+ return plan_key, subject
342
 
 
 
 
 
 
343
 
344
+ async def enforce_rate_limit(
345
+ request: Request,
346
+ authorization: Optional[str],
347
+ metric: str,
348
+ ) -> Dict[str, Optional[int | str]]:
349
+ if metric not in usage_store:
350
+ raise HTTPException(status_code=500, detail=f"Unknown limit metric: {metric}")
351
 
352
+ plan_key, subject = await resolve_rate_limit_identity(request, authorization)
353
+ plan = TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"]
354
+ plan_limits = plan.get("limits", {})
355
+ limit = plan_limits.get(metric)
356
 
357
+ if limit is None:
358
+ return {"plan_key": plan_key, "remaining": None}
359
+
360
+ window_key = get_usage_period_key(metric)
361
+ bucket = usage_store[metric]
362
+ entry = bucket.get(subject)
363
+ if not entry or entry.get("window") != window_key:
364
+ entry = {"window": window_key, "count": 0}
365
+ bucket[subject] = entry
366
+
367
+ if entry["count"] >= int(limit):
368
  raise HTTPException(
369
  status_code=429,
370
+ detail=f"{metric} limit reached for {plan.get('name', 'current plan')}",
371
  )
372
 
373
  entry["count"] += 1
374
+ remaining = max(0, int(limit) - entry["count"])
375
+ return {"plan_key": plan_key, "remaining": remaining}
376
+
377
+
378
+ async def check_audio_rate_limit(request: Request, authorization: Optional[str]):
379
+ await enforce_rate_limit(request, authorization, "audioWeekly")
380
 
381
 
382
  def is_complex_reasoning(prompt: str) -> bool:
 
407
  return True
408
  return False
409
 
410
+ async def check_image_rate_limit(request: Request, authorization: Optional[str]):
411
+ await enforce_rate_limit(request, authorization, "imagesDaily")
 
 
 
 
 
 
 
 
 
412
 
 
 
 
 
 
413
 
414
+ async def check_video_rate_limit(request: Request, authorization: Optional[str]):
415
+ await enforce_rate_limit(request, authorization, "videosDaily")
416
 
417
  PKEY = os.getenv("POLLINATIONS_KEY", "")
418
  PKEY2 = os.getenv("POLLINATIONS2_KEY", "")
419
  PKEY3 = os.getenv("POLLINATIONS3_KEY", "")
420
 
 
 
 
 
 
421
  GROQ_TOOL_MODELS = [
422
  "openai/gpt-oss-120b",
423
  "openai/gpt-oss-20b",
 
442
  "zai-glm-4.7",
443
  ]
444
 
445
+ async def check_chat_rate_limit(request: Request, authorization: Optional[str]):
446
+ return await enforce_rate_limit(request, authorization, "cloudChatDaily")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
  @app.head("/status/sfx")
449
  async def head_sfx():
 
528
 
529
  @app.post("/gen/image")
530
  @app.get("/genimg/{prompt}")
531
+ async def generate_image(
532
+ request: Request,
533
+ prompt: str = None,
534
+ authorization: Optional[str] = Header(None),
535
+ ):
536
+ await check_image_rate_limit(request, authorization)
537
  timeout = httpx.Timeout(300.0, read=300.0)
538
  if prompt is None:
539
  prompt = (await request.json()).get("prompt")
 
598
  return models
599
 
600
  @app.post("/gen/chat/completions")
601
+ async def generate_text(request: Request, authorization: Optional[str] = Header(None)):
602
  body = await request.json()
603
  messages = body.get("messages", [])
604
  if not isinstance(messages, list) or len(messages) == 0:
605
  raise HTTPException(400, "messages[] is required")
606
 
607
+ await check_chat_rate_limit(request, authorization)
 
608
  prompt_text = extract_user_text(messages)
609
 
610
  uses_tools = (
 
752
 
753
  @app.get("/gen/sfx/{prompt}")
754
  @app.post("/gen/sfx")
755
+ async def gensfx(
756
+ request: Request,
757
+ prompt: str = None,
758
+ authorization: Optional[str] = Header(None),
759
+ ):
760
+ await check_audio_rate_limit(request, authorization)
761
  if prompt is None:
762
  prompt = (await request.json()).get("prompt")
763
  url = f"https://gen.pollinations.ai/audio/{prompt}?model=elevenmusic&key={PKEY}"
 
785
 
786
  @app.get("/gen/tts/{prompt}")
787
  @app.post("/gen/tts")
788
+ async def gensfx(
789
+ request: Request,
790
+ prompt: str = None,
791
+ authorization: Optional[str] = Header(None),
792
+ ):
793
+ await check_audio_rate_limit(request, authorization)
794
  if prompt is None:
795
  prompt = (await request.json()).get("prompt")
796
  url = f"https://gen.pollinations.ai/audio/{prompt}?key={PKEY3}"
 
819
  @app.get("/gen/video/{prompt}")
820
  @app.post("/gen/video")
821
  @app.head("/gen/video")
822
+ async def genvideo(
823
+ request: Request,
824
+ prompt: str = None,
825
+ authorization: Optional[str] = Header(None),
826
+ ):
827
+ if request.method != "HEAD":
828
+ await check_video_rate_limit(request, authorization)
829
  return RedirectResponse(url="/gen/video/airforce", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
830
 
831
  if prompt is None:
 
918
  @app.get("/gen/video/airforce/{prompt}")
919
  @app.post("/gen/video/airforce")
920
  @app.head("/gen/video/airforce")
921
+ async def genvideo_airforce(
922
+ request: Request,
923
+ prompt: str = None,
924
+ authorization: Optional[str] = Header(None),
925
+ ):
926
  if request.method == "HEAD":
927
  return Response(
928
  status_code=200,
 
954
  }
955
  )
956
 
957
+ await check_video_rate_limit(request, authorization)
958
+
959
  aspectRatio = "3:2"
960
  inputMode = "normal"
961
 
 
1055
  if "error" in result:
1056
  raise HTTPException(401, result["error"])
1057
 
1058
+ plan_key = normalize_plan_key(result.get("plan_key"))
1059
+ result["plan_key"] = plan_key
1060
+ result["plan_name"] = (TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"])["name"]
1061
  return result
1062
 
1063
+ @app.get("/tier-config")
1064
+ async def tier_config():
1065
+ plans = []
1066
+ for idx, key in enumerate(PLAN_ORDER):
1067
+ plan = TIER_CONFIG.get(key)
1068
+ if not plan:
1069
+ continue
1070
+ plans.append(
1071
+ {
1072
+ "key": key,
1073
+ "name": plan["name"],
1074
+ "url": plan["url"],
1075
+ "price": plan["price"],
1076
+ "limits": plan["limits"],
1077
+ "order": idx,
1078
+ }
1079
+ )
1080
+
1081
  return JSONResponse(
1082
  status_code=200,
1083
+ content={
1084
+ "defaultPlanKey": "free",
1085
+ "plans": plans,
1086
+ },
1087
+ )
1088
+
1089
+ @app.get("/tiers")
1090
+ async def tiers():
1091
+ paid_plans = []
1092
+ for key in PLAN_ORDER:
1093
+ if key == "free":
1094
+ continue
1095
+ plan = TIER_CONFIG.get(key)
1096
+ if not plan:
1097
+ continue
1098
+ paid_plans.append(
1099
  {
1100
+ "key": key,
1101
+ "name": plan["name"],
1102
+ "url": plan["url"],
1103
+ "price": plan["price"],
1104
+ "limits": plan["limits"],
1105
  }
1106
+ )
1107
+
1108
+ return JSONResponse(
1109
+ status_code=200,
1110
+ content=paid_plans,
1111
  )
1112
 
1113
  @app.get("/portal")
1114
  def a():
1115
+ return RedirectResponse(url="https://billing.stripe.com/p/login/test_6oUcN5g665rp7nLgaq8bS00", status_code=status.HTTP_302_FOUND)