R.C.M. commited on
Commit
67b3d14
·
1 Parent(s): 53f3964

Remove from hf space

Browse files
Dockerfile DELETED
@@ -1,15 +0,0 @@
1
- FROM python:3.13-slim
2
-
3
- RUN apt-get update && apt-get install -y --no-install-recommends git \
4
- && rm -rf /var/lib/apt/lists/*
5
-
6
- RUN useradd -m -u 1000 user
7
- USER user
8
- ENV PATH="/home/user/.local/bin:$PATH"
9
-
10
- WORKDIR /app
11
-
12
- COPY --chown=user start.sh /app/start.sh
13
- RUN chmod +x /app/start.sh
14
-
15
- CMD ["/app/start.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,863 +0,0 @@
1
- import os
2
- import time
3
- import hashlib
4
- import hmac
5
- from fastapi import WebSocket, WebSocketDisconnect
6
- from collections import defaultdict, deque
7
- import json
8
- from fastapi import FastAPI, Request, HTTPException, status, Header
9
- from fastapi.middleware.cors import CORSMiddleware
10
- from fastapi.responses import (
11
- Response,
12
- JSONResponse,
13
- StreamingResponse,
14
- RedirectResponse,
15
- )
16
- import httpx
17
- from bs4 import BeautifulSoup
18
- from typing import List, Dict, Any
19
- import asyncio
20
- import re
21
- import random
22
- from urllib.parse import quote
23
- import base64
24
- from pathlib import Path
25
- from helper.subscriptions import (
26
- fetch_subscription,
27
- normalize_plan_key,
28
- TIER_CONFIG,
29
- PLAN_ORDER,
30
- resolve_token_identity,
31
- )
32
- from typing import Optional
33
- from helper.keywords import *
34
- from helper.assets import asset_router
35
-
36
- from helper.ratelimit import (
37
- enforce_rate_limit,
38
- resolve_rate_limit_identity,
39
- check_audio_rate_limit,
40
- check_video_rate_limit,
41
- check_image_rate_limit,
42
- MAX_CHAT_PROMPT_BYTES,
43
- MAX_CHAT_PROMPT_CHARS,
44
- MAX_GROQ_PROMPT_BYTES,
45
- MAX_GROQ_PROMPT_CHARS,
46
- MAX_MEDIA_PROMPT_BYTES,
47
- MAX_MEDIA_PROMPT_CHARS,
48
- extract_user_text,
49
- calculate_messages_size,
50
- normalize_prompt_value,
51
- enforce_prompt_size,
52
- resolve_bound_subject,
53
- get_usage_snapshot_for_subject,
54
- consume_metric_for_subject,
55
- get_metric_snapshot_for_subject,
56
- normalize_forwarded_ip,
57
- resolve_verify_token_rate_limit_identity,
58
- )
59
-
60
- from status import router as status_router
61
- from gen import router as gen_router
62
- from payg import router as payg_router
63
-
64
- app = FastAPI()
65
- def print_env_for_dotenv():
66
- import os
67
- for key, value in os.environ.items():
68
- safe = str(value).replace('"', '\\"')
69
- print(f'{key}="{safe}"')
70
- print_env_for_dotenv()
71
- LIGHTNING_TRANSFER_TOKEN = os.getenv("LIGHTNING_TRANSFER_TOKEN")
72
- VERIFY_TOKEN_USAGE_METRIC = "verifyTokenWithEmailDaily"
73
- VERIFY_TOKEN_FEATURE_NAME = "Token Verification Requests"
74
- VERIFY_TOKEN_CONTACT_NOTICE = (
75
- "Need more usage? Contact us at inferenceportai@gmail.com."
76
- )
77
-
78
- WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY")
79
-
80
- # authentication attempt tracking
81
- AUTH_ATTEMPTS = defaultdict(lambda: deque())
82
-
83
- AUTH_WINDOW_SECONDS = 60
84
- AUTH_MAX_ATTEMPTS = 10
85
-
86
- app.add_middleware(
87
- CORSMiddleware,
88
- allow_origins=["*"],
89
- allow_methods=["GET", "POST", "PUT", "DELETE", "HEAD"],
90
- allow_headers=["*"],
91
- )
92
-
93
- app.include_router(asset_router)
94
- app.include_router(status_router)
95
- app.include_router(gen_router)
96
- app.include_router(payg_router)
97
-
98
- TOOL_REGISTRY_ROOT = Path(os.getenv("TOOL_REGISTRY_ROOT", "/tools"))
99
- TOOL_REGISTRY_INDEX = TOOL_REGISTRY_ROOT / "registry_index.json"
100
- TOOL_REGISTRY_LOCK = asyncio.Lock()
101
-
102
-
103
- def _ensure_tool_registry_root() -> None:
104
- TOOL_REGISTRY_ROOT.mkdir(parents=True, exist_ok=True)
105
- if not TOOL_REGISTRY_INDEX.exists():
106
- TOOL_REGISTRY_INDEX.write_text("{}", encoding="utf-8")
107
-
108
-
109
- def _read_tool_registry_index() -> Dict[str, Dict[str, Any]]:
110
- _ensure_tool_registry_root()
111
- try:
112
- raw = TOOL_REGISTRY_INDEX.read_text(encoding="utf-8")
113
- data = json.loads(raw)
114
- if isinstance(data, dict):
115
- return data
116
- except Exception:
117
- pass
118
- return {}
119
-
120
-
121
- def _write_tool_registry_index(index: Dict[str, Dict[str, Any]]) -> None:
122
- _ensure_tool_registry_root()
123
- TOOL_REGISTRY_INDEX.write_text(json.dumps(index, indent=2), encoding="utf-8")
124
-
125
-
126
- def _sanitize_tool_id(value: str) -> str:
127
- tool_id = re.sub(r"[^a-zA-Z0-9_-]", "-", str(value or "").strip().lower())
128
- tool_id = re.sub(r"-{2,}", "-", tool_id).strip("-")
129
- return tool_id[:80]
130
-
131
-
132
- def _sanitize_tool_file_name(value: str) -> str:
133
- name = Path(value or "tool").name
134
- name = re.sub(r"[^a-zA-Z0-9._-]", "_", name)
135
- return name[:120]
136
-
137
-
138
- def _normalize_tool_version(value: Any) -> str:
139
- version = str(value or "0.1.0").strip()
140
- if re.match(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$", version):
141
- return version[:64]
142
- return "0.1.0"
143
-
144
-
145
- def _normalize_https_url(value: Any) -> str:
146
- raw = str(value or "").strip()
147
- if raw.startswith("https://"):
148
- return raw[:512]
149
- return ""
150
-
151
-
152
- def _to_registry_record(manifest: Dict[str, Any], *, now_iso: str) -> Dict[str, Any]:
153
- tool_id = _sanitize_tool_id(manifest.get("id") or "")
154
- code_file = _sanitize_tool_file_name(manifest.get("codeFile") or "tool")
155
- visibility = str(manifest.get("visibility") or "public").strip().lower()
156
- if visibility not in {"public", "unlisted"}:
157
- visibility = "public"
158
- return {
159
- "id": tool_id,
160
- "name": str(manifest.get("name") or tool_id),
161
- "functionality": str(manifest.get("functionality") or ""),
162
- "version": str(manifest.get("version") or "0.1.0"),
163
- "releaseNotes": str(manifest.get("releaseNotes") or ""),
164
- "websiteUrl": str(manifest.get("websiteUrl") or ""),
165
- "language": str(manifest.get("language") or "javascript"),
166
- "authorEmail": str(manifest.get("authorEmail") or ""),
167
- "authorUserId": manifest.get("authorUserId"),
168
- "codeHash": manifest.get("codeHash"),
169
- "visibility": visibility,
170
- "publishedAt": str(manifest.get("createdAt") or now_iso),
171
- "updatedAt": now_iso,
172
- "requirements": manifest.get("requirements") or {"runtime": [], "build": []},
173
- "userInputs": manifest.get("userInputs") if isinstance(manifest.get("userInputs"), list) else [],
174
- "openai": manifest.get("openai") or {},
175
- "files": {
176
- "manifestPath": f"{tool_id}/manifest.json",
177
- "codePath": f"{tool_id}/{code_file}",
178
- },
179
- }
180
-
181
-
182
- async def _require_registry_identity(authorization: Optional[str]) -> Dict[str, Any]:
183
- if not authorization or not authorization.startswith("Bearer "):
184
- raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
185
- token = authorization.split(" ", 1)[1].strip()
186
- identity = await resolve_token_identity(token)
187
- if not identity or "error" in identity:
188
- raise HTTPException(status_code=401, detail="Invalid authorization token")
189
- return identity
190
-
191
-
192
- @app.get("/tool-registry/tools")
193
- async def list_tool_registry_tools(q: Optional[str] = None, limit: int = 200):
194
- index = _read_tool_registry_index()
195
- rows = []
196
- query = (q or "").strip().lower()
197
- max_items = max(1, min(limit, 500))
198
- for _, record in index.items():
199
- if not isinstance(record, dict):
200
- continue
201
- if record.get("visibility") != "public":
202
- continue
203
- if query:
204
- text = " ".join(
205
- [
206
- str(record.get("id") or ""),
207
- str(record.get("name") or ""),
208
- str(record.get("functionality") or ""),
209
- str(record.get("language") or ""),
210
- ]
211
- ).lower()
212
- if query not in text:
213
- continue
214
- rows.append(record)
215
- rows.sort(key=lambda row: str(row.get("updatedAt") or ""), reverse=True)
216
- return JSONResponse(status_code=200, content={"tools": rows[:max_items]})
217
-
218
-
219
- @app.get("/tool-registry/my-tools")
220
- async def list_my_tool_registry_tools(
221
- authorization: Optional[str] = Header(None),
222
- limit: int = 500,
223
- ):
224
- identity = await _require_registry_identity(authorization)
225
- author_email = str(identity.get("email") or "").strip().lower()
226
- user_id = str(identity.get("user_id") or "").strip()
227
- max_items = max(1, min(limit, 1000))
228
- index = _read_tool_registry_index()
229
- rows = []
230
- for _, record in index.items():
231
- if not isinstance(record, dict):
232
- continue
233
- owner_email = str(record.get("authorEmail") or "").strip().lower()
234
- owner_user_id = str(record.get("authorUserId") or "").strip()
235
- if (user_id and owner_user_id == user_id) or (author_email and owner_email == author_email):
236
- rows.append(record)
237
- rows.sort(key=lambda row: str(row.get("updatedAt") or ""), reverse=True)
238
- return JSONResponse(status_code=200, content={"tools": rows[:max_items]})
239
-
240
-
241
- @app.get("/tool-registry/tools/{tool_id}")
242
- async def get_tool_registry_item(tool_id: str):
243
- normalized_id = _sanitize_tool_id(tool_id)
244
- if not normalized_id:
245
- raise HTTPException(status_code=400, detail="Invalid tool id")
246
- index = _read_tool_registry_index()
247
- record = index.get(normalized_id)
248
- if not record:
249
- raise HTTPException(status_code=404, detail="Tool not found")
250
- return JSONResponse(status_code=200, content={"tool": record})
251
-
252
-
253
- @app.get("/tool-registry/tools/{tool_id}/source")
254
- async def get_tool_registry_source(tool_id: str):
255
- normalized_id = _sanitize_tool_id(tool_id)
256
- if not normalized_id:
257
- raise HTTPException(status_code=400, detail="Invalid tool id")
258
- index = _read_tool_registry_index()
259
- record = index.get(normalized_id)
260
- if not record:
261
- raise HTTPException(status_code=404, detail="Tool not found")
262
- manifest_path = TOOL_REGISTRY_ROOT / str(record.get("files", {}).get("manifestPath", ""))
263
- code_path = TOOL_REGISTRY_ROOT / str(record.get("files", {}).get("codePath", ""))
264
- if not manifest_path.exists() or not code_path.exists():
265
- raise HTTPException(status_code=404, detail="Tool source files not found")
266
- try:
267
- manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
268
- except Exception:
269
- raise HTTPException(status_code=500, detail="Failed to load tool manifest")
270
- try:
271
- code = code_path.read_text(encoding="utf-8")
272
- except Exception:
273
- raise HTTPException(status_code=500, detail="Failed to load tool code")
274
- return JSONResponse(status_code=200, content={"manifest": manifest, "code": code})
275
-
276
-
277
- @app.post("/tool-registry/tools")
278
- async def create_tool_registry_item(
279
- request: Request,
280
- authorization: Optional[str] = Header(None),
281
- ):
282
- identity = await _require_registry_identity(authorization)
283
- body = await request.json()
284
- if not isinstance(body, dict):
285
- raise HTTPException(status_code=400, detail="Invalid JSON body")
286
- manifest = body.get("manifest")
287
- code = body.get("code")
288
- if not isinstance(manifest, dict) or not isinstance(code, str):
289
- raise HTTPException(status_code=400, detail="Manifest and code are required")
290
-
291
- now_iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
292
- author_email = str(identity.get("email") or "").strip().lower()
293
- if not author_email:
294
- raise HTTPException(status_code=401, detail="Unable to resolve account email")
295
-
296
- requested_id = _sanitize_tool_id(manifest.get("id") or manifest.get("name") or "")
297
- if not requested_id:
298
- requested_id = f"tool-{hashlib.sha256(os.urandom(8)).hexdigest()[:10]}"
299
- manifest["id"] = requested_id
300
- manifest["authorEmail"] = author_email
301
- manifest["authorUserId"] = identity.get("user_id")
302
-
303
- code_file = _sanitize_tool_file_name(manifest.get("codeFile") or "tool")
304
- manifest["codeFile"] = code_file
305
- manifest["codeHash"] = hashlib.sha256(code.encode("utf-8")).hexdigest()
306
- manifest["version"] = _normalize_tool_version(manifest.get("version"))
307
- manifest["releaseNotes"] = str(manifest.get("releaseNotes") or "")[:4000]
308
- manifest["websiteUrl"] = _normalize_https_url(manifest.get("websiteUrl"))
309
- visibility = str(manifest.get("visibility") or "public").strip().lower()
310
- if visibility not in {"public", "unlisted"}:
311
- visibility = "public"
312
- manifest["visibility"] = visibility
313
- if not manifest.get("createdAt"):
314
- manifest["createdAt"] = now_iso
315
- manifest["updatedAt"] = now_iso
316
- manifest["published"] = True
317
- manifest["registry"] = {
318
- "source": "lightning",
319
- "uploadedAt": manifest.get("createdAt") or now_iso,
320
- "updatedAt": now_iso,
321
- }
322
-
323
- tool_dir = TOOL_REGISTRY_ROOT / requested_id
324
- manifest_path = tool_dir / "manifest.json"
325
- code_path = tool_dir / code_file
326
- tool_dir.mkdir(parents=True, exist_ok=True)
327
- manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
328
- code_path.write_text(code, encoding="utf-8")
329
-
330
- record = _to_registry_record(manifest, now_iso=now_iso)
331
- record["authorEmail"] = author_email
332
- record["authorUserId"] = identity.get("user_id")
333
-
334
- async with TOOL_REGISTRY_LOCK:
335
- index = _read_tool_registry_index()
336
- existing = index.get(requested_id)
337
- if existing and str(existing.get("authorEmail", "")).strip().lower() != author_email:
338
- raise HTTPException(status_code=403, detail="Tool id already exists")
339
- index[requested_id] = record
340
- _write_tool_registry_index(index)
341
-
342
- return JSONResponse(status_code=200, content={"tool": record})
343
-
344
-
345
- @app.delete("/tool-registry/tools/{tool_id}")
346
- async def delete_tool_registry_item(
347
- tool_id: str,
348
- authorization: Optional[str] = Header(None),
349
- ):
350
- identity = await _require_registry_identity(authorization)
351
- author_email = str(identity.get("email") or "").strip().lower()
352
- normalized_id = _sanitize_tool_id(tool_id)
353
- if not normalized_id:
354
- raise HTTPException(status_code=400, detail="Invalid tool id")
355
-
356
- async with TOOL_REGISTRY_LOCK:
357
- index = _read_tool_registry_index()
358
- record = index.get(normalized_id)
359
- if not record:
360
- raise HTTPException(status_code=404, detail="Tool not found")
361
- owner = str(record.get("authorEmail") or "").strip().lower()
362
- if owner != author_email:
363
- raise HTTPException(status_code=403, detail="Only the publisher can delete this tool")
364
- index.pop(normalized_id, None)
365
- _write_tool_registry_index(index)
366
-
367
- tool_dir = TOOL_REGISTRY_ROOT / normalized_id
368
- if tool_dir.exists():
369
- for child in sorted(tool_dir.glob("**/*"), reverse=True):
370
- try:
371
- if child.is_file():
372
- child.unlink()
373
- elif child.is_dir():
374
- child.rmdir()
375
- except Exception:
376
- continue
377
- try:
378
- tool_dir.rmdir()
379
- except Exception:
380
- pass
381
-
382
- return JSONResponse(status_code=200, content={"ok": True})
383
-
384
-
385
- @app.put("/tool-registry/tools/{tool_id}")
386
- async def update_tool_registry_item(
387
- tool_id: str,
388
- request: Request,
389
- authorization: Optional[str] = Header(None),
390
- ):
391
- identity = await _require_registry_identity(authorization)
392
- author_email = str(identity.get("email") or "").strip().lower()
393
- normalized_id = _sanitize_tool_id(tool_id)
394
- if not normalized_id:
395
- raise HTTPException(status_code=400, detail="Invalid tool id")
396
- body = await request.json()
397
- if not isinstance(body, dict):
398
- raise HTTPException(status_code=400, detail="Invalid JSON body")
399
- manifest = body.get("manifest")
400
- code = body.get("code")
401
- if not isinstance(manifest, dict):
402
- raise HTTPException(status_code=400, detail="Manifest is required")
403
-
404
- async with TOOL_REGISTRY_LOCK:
405
- index = _read_tool_registry_index()
406
- record = index.get(normalized_id)
407
- if not record:
408
- raise HTTPException(status_code=404, detail="Tool not found")
409
- owner = str(record.get("authorEmail") or "").strip().lower()
410
- if owner != author_email:
411
- raise HTTPException(status_code=403, detail="Only the publisher can edit this tool")
412
-
413
- now_iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
414
- manifest["id"] = normalized_id
415
- manifest["authorEmail"] = author_email
416
- manifest["authorUserId"] = identity.get("user_id")
417
- manifest["createdAt"] = manifest.get("createdAt") or record.get("publishedAt") or now_iso
418
- manifest["updatedAt"] = now_iso
419
- manifest["published"] = True
420
- manifest["visibility"] = str(manifest.get("visibility") or record.get("visibility") or "public").lower()
421
- if manifest["visibility"] not in {"public", "unlisted"}:
422
- manifest["visibility"] = "public"
423
- manifest["version"] = _normalize_tool_version(manifest.get("version") or record.get("version"))
424
- manifest["releaseNotes"] = str(manifest.get("releaseNotes") or "")[:4000]
425
- manifest["websiteUrl"] = _normalize_https_url(manifest.get("websiteUrl"))
426
-
427
- code_file = _sanitize_tool_file_name(
428
- manifest.get("codeFile")
429
- or Path(str(record.get("files", {}).get("codePath", "tool"))).name
430
- or "tool"
431
- )
432
- manifest["codeFile"] = code_file
433
-
434
- tool_dir = TOOL_REGISTRY_ROOT / normalized_id
435
- tool_dir.mkdir(parents=True, exist_ok=True)
436
- manifest_path = tool_dir / "manifest.json"
437
- code_path = tool_dir / code_file
438
- if isinstance(code, str):
439
- manifest["codeHash"] = hashlib.sha256(code.encode("utf-8")).hexdigest()
440
- code_path.write_text(code, encoding="utf-8")
441
- manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
442
-
443
- updated_record = _to_registry_record(manifest, now_iso=now_iso)
444
- updated_record["authorEmail"] = author_email
445
- updated_record["authorUserId"] = identity.get("user_id")
446
- index[normalized_id] = updated_record
447
- _write_tool_registry_index(index)
448
-
449
- return JSONResponse(status_code=200, content={"tool": updated_record})
450
-
451
- def check_ws_auth_rate_limit(ip: str):
452
- now = time.time()
453
- q = AUTH_ATTEMPTS[ip]
454
-
455
- # purge old attempts
456
- while q and now - q[0] > AUTH_WINDOW_SECONDS:
457
- q.popleft()
458
-
459
- if len(q) >= AUTH_MAX_ATTEMPTS:
460
- return False
461
-
462
- q.append(now)
463
- return True
464
-
465
-
466
- @app.get("/")
467
- async def reroute_to_home():
468
- return RedirectResponse(
469
- url="https://inference.js.org", status_code=status.HTTP_308_PERMANENT_REDIRECT
470
- )
471
-
472
-
473
- OLLAMA_LIBRARY_URL = "https://ollama.com/library"
474
-
475
-
476
- @app.head("/models")
477
- @app.get("/models")
478
- async def get_models() -> List[Dict]:
479
- async with httpx.AsyncClient() as client:
480
- response = await client.get(OLLAMA_LIBRARY_URL)
481
- html = response.text
482
-
483
- soup = BeautifulSoup(html, "html.parser")
484
- items = soup.select("li[x-test-model]")
485
-
486
- models = []
487
- for item in items:
488
- name = item.select_one("[x-test-model-title] span")
489
- description = item.select_one("p.max-w-lg")
490
- sizes = [el.get_text(strip=True) for el in item.select("[x-test-size]")]
491
- pulls = item.select_one("[x-test-pull-count]")
492
- tags = [
493
- t.get_text(strip=True) for t in item.select('span[class*="text-blue-600"]')
494
- ]
495
- updated = item.select_one("[x-test-updated]")
496
- link = item.select_one("a")
497
-
498
- models.append(
499
- {
500
- "name": name.get_text(strip=True) if name else "",
501
- "description": (
502
- description.get_text(strip=True)
503
- if description
504
- else "No description"
505
- ),
506
- "sizes": sizes,
507
- "pulls": pulls.get_text(strip=True) if pulls else "Unknown",
508
- "tags": tags,
509
- "updated": updated.get_text(strip=True) if updated else "Unknown",
510
- "link": link.get("href") if link else None,
511
- }
512
- )
513
-
514
- return models
515
-
516
- @app.get("/subscription")
517
- async def get_subscription(authorization: Optional[str] = Header(None)):
518
- if not authorization or not authorization.startswith("Bearer "):
519
- raise HTTPException(401, "Missing or invalid Authorization header")
520
-
521
- jwt = authorization.split(" ", 1)[1]
522
-
523
- result = await fetch_subscription(jwt)
524
- if "error" in result:
525
- raise HTTPException(401, result["error"])
526
-
527
- plan_key = normalize_plan_key(result.get("plan_key"))
528
- result["plan_key"] = plan_key
529
- result["plan_name"] = (TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"])["name"]
530
- return result
531
-
532
-
533
- @app.get("/usage")
534
- async def get_usage(
535
- request: Request,
536
- authorization: Optional[str] = Header(None),
537
- x_client_id: Optional[str] = Header(None),
538
- ):
539
- plan_key, subject = await resolve_rate_limit_identity(
540
- request, authorization, x_client_id
541
- )
542
- plan = TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"]
543
- usage = get_usage_snapshot_for_subject(plan_key, subject)
544
- return JSONResponse(
545
- status_code=200,
546
- content={
547
- "plan_key": plan_key,
548
- "plan_name": plan.get("name", "Free Tier"),
549
- "usage": usage,
550
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
551
- },
552
- )
553
-
554
-
555
- def build_verify_token_usage_response(
556
- plan_key: str,
557
- usage: Dict[str, Any],
558
- *,
559
- error: Optional[str] = None,
560
- ):
561
- plan = TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"]
562
- payload: Dict[str, Any] = {
563
- "plan_key": plan_key,
564
- "plan_name": plan.get("name", "Free Tier"),
565
- "feature_name": VERIFY_TOKEN_FEATURE_NAME,
566
- "notice": VERIFY_TOKEN_CONTACT_NOTICE,
567
- "usage": usage,
568
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
569
- }
570
- if error:
571
- payload["error"] = error
572
- return payload
573
-
574
-
575
- @app.get("/usage/verify-token-with-email")
576
- async def get_verify_token_with_email_usage(
577
- request: Request,
578
- authorization: Optional[str] = Header(None),
579
- ):
580
- auth_header = (
581
- authorization if authorization and authorization.startswith("Bearer ") else None
582
- )
583
- request_ip = normalize_forwarded_ip(
584
- request.headers.get("x-forwarded-for")
585
- or request.headers.get("x-real-ip")
586
- or (request.client.host if request.client else None)
587
- )
588
- plan_key, subject = await resolve_verify_token_rate_limit_identity(
589
- auth_header, request_ip
590
- )
591
- usage = get_metric_snapshot_for_subject(plan_key, subject, VERIFY_TOKEN_USAGE_METRIC)
592
- return JSONResponse(
593
- status_code=200,
594
- content=build_verify_token_usage_response(plan_key, usage),
595
- )
596
-
597
-
598
- @app.put("/internal/usage/verify-token-with-email")
599
- async def put_verify_token_with_email_usage(
600
- request: Request,
601
- x_lightning_transfer_token: Optional[str] = Header(None),
602
- ):
603
- if (
604
- not LIGHTNING_TRANSFER_TOKEN
605
- or not x_lightning_transfer_token
606
- or not hmac.compare_digest(x_lightning_transfer_token, LIGHTNING_TRANSFER_TOKEN)
607
- ):
608
- raise HTTPException(401, "Invalid transfer token")
609
-
610
- try:
611
- body = await request.json()
612
- except Exception:
613
- raise HTTPException(400, "Invalid JSON body")
614
-
615
- auth_header = None
616
- raw_jwt = body.get("supabase_jwt") if isinstance(body, dict) else None
617
- raw_ip = body.get("ip") if isinstance(body, dict) else None
618
-
619
- if isinstance(raw_jwt, str) and raw_jwt.strip():
620
- auth_header = f"Bearer {raw_jwt.strip()}"
621
-
622
- plan_key, subject = await resolve_verify_token_rate_limit_identity(
623
- auth_header, normalize_forwarded_ip(raw_ip)
624
- )
625
-
626
- try:
627
- usage = await consume_metric_for_subject(
628
- VERIFY_TOKEN_USAGE_METRIC,
629
- plan_key,
630
- subject,
631
- )
632
- except HTTPException as exc:
633
- if exc.status_code != 429:
634
- raise
635
-
636
- usage = get_metric_snapshot_for_subject(
637
- plan_key, subject, VERIFY_TOKEN_USAGE_METRIC
638
- )
639
- return JSONResponse(
640
- status_code=429,
641
- content=build_verify_token_usage_response(
642
- plan_key,
643
- usage,
644
- error="Token Verification Requests limit reached for today.",
645
- ),
646
- )
647
-
648
- return JSONResponse(
649
- status_code=200,
650
- content=build_verify_token_usage_response(plan_key, usage),
651
- )
652
-
653
-
654
- @app.get("/tier-config")
655
- async def tier_config():
656
- plans = []
657
- for idx, key in enumerate(PLAN_ORDER):
658
- plan = TIER_CONFIG.get(key)
659
- if not plan:
660
- continue
661
- plans.append(
662
- {
663
- "key": key,
664
- "name": plan["name"],
665
- "url": plan["url"],
666
- "price": plan["price"],
667
- "limits": plan["limits"],
668
- "order": idx,
669
- }
670
- )
671
-
672
- return JSONResponse(
673
- status_code=200,
674
- content={
675
- "defaultPlanKey": "free",
676
- "plans": plans,
677
- },
678
- )
679
-
680
-
681
- @app.get("/tiers")
682
- async def tiers():
683
- paid_plans = []
684
- for key in PLAN_ORDER:
685
- if key == "free":
686
- continue
687
- plan = TIER_CONFIG.get(key)
688
- if not plan:
689
- continue
690
- paid_plans.append(
691
- {
692
- "key": key,
693
- "name": plan["name"],
694
- "url": plan["url"],
695
- "price": plan["price"],
696
- "limits": plan["limits"],
697
- }
698
- )
699
-
700
- return JSONResponse(
701
- status_code=200,
702
- content=paid_plans,
703
- )
704
-
705
- @app.websocket("/ws/chat")
706
- async def websocket_chat(ws: WebSocket):
707
- ip = ws.client.host
708
-
709
- # Accept connection
710
- await ws.accept()
711
-
712
- # Basic connection rate limiting
713
- if not check_ws_auth_rate_limit(ip):
714
- await ws.close(code=4408)
715
- return
716
-
717
- try:
718
- # Expect an auth message first
719
- auth_msg = await ws.receive_text()
720
- try:
721
- auth_data = json.loads(auth_msg)
722
- except Exception:
723
- await ws.close(code=4400)
724
- return
725
-
726
- provided_key = auth_data.get("key")
727
-
728
- if not WEBSOCKET_KEY or provided_key != WEBSOCKET_KEY:
729
- await ws.close(code=4403)
730
- return
731
-
732
- await ws.send_json({
733
- "type": "auth",
734
- "status": "ok"
735
- })
736
-
737
- internal_url = "http://127.0.0.1:7860/gen/chat/completions"
738
-
739
- async with httpx.AsyncClient(
740
- timeout=None,
741
- follow_redirects=False
742
- ) as client:
743
- request_counter = 0
744
-
745
- async def handle_incoming_requests():
746
- nonlocal request_counter
747
- while True:
748
- try:
749
- msg = await ws.receive_text()
750
- except WebSocketDisconnect:
751
- break
752
-
753
- try:
754
- data = json.loads(msg)
755
- except Exception:
756
- await ws.send_json({"error": "Invalid JSON"})
757
- continue
758
-
759
- body = data.get("body")
760
- headers = data.get("headers") or {}
761
-
762
- if not body:
763
- await ws.send_json({"error": "Missing body"})
764
- continue
765
-
766
- request_counter += 1
767
- request_id = request_counter
768
-
769
- # Process this request concurrently
770
- asyncio.create_task(process_request(client, request_id, body, headers))
771
-
772
- async def process_request(client, request_id, body, headers):
773
- try:
774
- async with client.stream(
775
- "POST",
776
- internal_url,
777
- json=body,
778
- headers=headers
779
- ) as response:
780
-
781
- # Handle upstream errors
782
- if response.status_code >= 400:
783
- error_text = ""
784
- try:
785
- error_text = (await response.aread()).decode(
786
- "utf-8", errors="replace"
787
- )[:500]
788
- except Exception:
789
- pass
790
-
791
- error_payload = json.dumps({
792
- "error": "Upstream request failed",
793
- "status": response.status_code,
794
- "detail": error_text
795
- })
796
- try:
797
- await ws.send_text(f"{request_id}:{error_payload}")
798
- except RuntimeError:
799
- return
800
- return
801
-
802
- # Stream tokens/lines to the websocket
803
- async for line in response.aiter_lines():
804
- if not line:
805
- continue
806
-
807
- try:
808
- await ws.send_text(f"{request_id}:{line}")
809
- except RuntimeError:
810
- return
811
-
812
- except Exception as stream_error:
813
- try:
814
- error_payload = json.dumps({
815
- "error": "Streaming error",
816
- "detail": str(stream_error)
817
- })
818
- await ws.send_text(f"{request_id}:{error_payload}")
819
- except Exception:
820
- pass
821
-
822
- # Start handling incoming requests
823
- await handle_incoming_requests()
824
-
825
- except WebSocketDisconnect:
826
- return
827
-
828
- except Exception as e:
829
- try:
830
- await ws.send_json({
831
- "error": "Server error",
832
- "detail": str(e)
833
- })
834
- except Exception:
835
- pass
836
-
837
- try:
838
- await ws.close()
839
- except Exception:
840
- pass
841
-
842
- @app.get("/portal")
843
- @app.post("/portal")
844
- async def redirect_to_protal(request: Request):
845
- email = None
846
-
847
- if request.method == "POST":
848
- try:
849
- body = await request.json()
850
- email = body.get("email")
851
- except:
852
- email = None
853
-
854
- base_url = "https://billing.stripe.com/p/login/5kQdR9aIM3ts4steyabbG00"
855
-
856
- if not email:
857
- return RedirectResponse(url=base_url, status_code=status.HTTP_302_FOUND)
858
- if request.method != "POST":
859
- return RedirectResponse(
860
- url=f"{base_url}?prefilled_email={email}", status_code=status.HTTP_302_FOUND
861
- )
862
- else:
863
- return JSONResponse({"redirect_url": (base_url + "?prefilled_email=" + email)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/admins.json DELETED
@@ -1,56 +0,0 @@
1
- {
2
- "sharktidedev@gmail.com": {
3
- "plan_key": "core",
4
- "product_name": "InferencePort AI Core",
5
- "nickname": "Core Plan",
6
- "subscription_id": "admin-override",
7
- "status": "active",
8
- "current_period_end": null,
9
- "price_id": null
10
- },
11
- "hallmax249@gmail.com": {
12
- "plan_key": "creator",
13
- "product_name": "InferencePort AI Creator",
14
- "nickname": "Creator Plan",
15
- "subscription_id": "admin-override",
16
- "status": "active",
17
- "current_period_end": null,
18
- "price_id": null
19
- },
20
- "w81537670@gmail.com": {
21
- "plan_key": "light",
22
- "product_name": "InferencePort AI Light",
23
- "nickname": "Light Plan",
24
- "subscription_id": "admin-override",
25
- "status": "active",
26
- "current_period_end": null,
27
- "price_id": null
28
- },
29
- "jacoblamhome@gmail.com": {
30
- "plan_key": "light",
31
- "product_name": "InferencePort AI Light",
32
- "nickname": "Light Plan",
33
- "subscription_id": "admin-override",
34
- "status": "active",
35
- "current_period_end": null,
36
- "price_id": null
37
- },
38
- "leo.dcam@gmail.com": {
39
- "plan_key": "light",
40
- "product_name": "InferencePort AI Light",
41
- "nickname": "Light Plan",
42
- "subscription_id": "admin-override",
43
- "status": "active",
44
- "current_period_end": null,
45
- "price_id": null
46
- },
47
- "morriszdweck@gmail.com": {
48
- "plan_key": "core",
49
- "product_name": "InferencePort AI Core",
50
- "nickname": "Core Plan",
51
- "subscription_id": "admin-override",
52
- "status": "active",
53
- "current_period_end": null,
54
- "price_id": null
55
- }
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/extra_usage.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "sharktidedev@gmail.com": {
3
- "limits": {
4
- "verifyTokenWithEmailDaily": 500
5
- }
6
- }
7
- }
 
 
 
 
 
 
 
 
assets/payg_config.json DELETED
@@ -1,119 +0,0 @@
1
- {
2
- "dashboard": {
3
- "appName": "InferencePort AI",
4
- "homeUrl": "https://inference.js.org",
5
- "apiBaseUrl": "https://sharktide-lightning.hf.space",
6
- "supportEmail": "inferenceportai@gmail.com",
7
- "hostname": ""
8
- },
9
- "supabase": {
10
- "url": "https://dpixehhdbtzsbckfektd.supabase.co",
11
- "publishableKey": "sb_publishable_hbqJ9rAlgaeiYRzdVAqY4A_5pwWLSR9",
12
- "resetRedirectUrl": "https://inference.js.org/reset.html",
13
- "deletePasswordVerifyEndpoint": "https://dpixehhdbtzsbckfektd.supabase.co/functions/v1/verify_password",
14
- "deleteAccountEndpoint": "https://dpixehhdbtzsbckfektd.supabase.co/functions/v1/delete_account"
15
- },
16
- "billing": {
17
- "currency": "USD",
18
- "packs": [
19
- {
20
- "id": "pack_25",
21
- "label": "$25",
22
- "amountUsd": 25,
23
- "credits": 25,
24
- "stripePaymentLink": "https://buy.stripe.com/14AcN5048bZY9MNdu6bbG0c",
25
- "prod": "prod_UaDtOCJlDvJxvL"
26
- },
27
- {
28
- "id": "pack_50",
29
- "label": "$50",
30
- "amountUsd": 50,
31
- "credits": 50,
32
- "stripePaymentLink": "https://buy.stripe.com/4gM8wPaIMd422klblYbbG0d",
33
- "prod": "prod_UaDudJa2YJ2ZIr"
34
- },
35
- {
36
- "id": "pack_100",
37
- "label": "$100",
38
- "amountUsd": 100,
39
- "credits": 125,
40
- "stripePaymentLink": "https://buy.stripe.com/fZu14ncQUbZYaQRahUbbG0e",
41
- "prod": "prod_UaDvyEon5kRAID"
42
- },
43
- {
44
- "id": "pack_200",
45
- "label": "$200",
46
- "amountUsd": 200,
47
- "credits": 300,
48
- "stripePaymentLink": "https://buy.stripe.com/dRm7sL6swd426ABdu6bbG0f",
49
- "prod": "prod_UaDvUBzDw2lp4F"
50
- },
51
- {
52
- "id": "pack_300",
53
- "label": "$300",
54
- "amountUsd": 300,
55
- "credits": 475,
56
- "stripePaymentLink": "https://buy.stripe.com/fZu4gzg361lk3op9dQbbG0g",
57
- "prod": "prod_UaDxh19Z7kthIf"
58
- },
59
- {
60
- "id": "pack_400",
61
- "label": "$400",
62
- "amountUsd": 400,
63
- "credits": 650,
64
- "stripePaymentLink": "https://buy.stripe.com/6oU9AT3gk7JI7EF89MbbG0h",
65
- "prod": "prod_UaDxXdUqwo0aIw"
66
- },
67
- {
68
- "id": "pack_500",
69
- "label": "$500",
70
- "amountUsd": 500,
71
- "credits": 800,
72
- "stripePaymentLink": "https://buy.stripe.com/14A14nbMQ9RQ5wx4XAbbG0i",
73
- "prod": "prod_UaDyyvIggiDUbQ"
74
- }
75
- ]
76
- },
77
- "pricing": {
78
- "textCreditPerMillionTokens": 0.75,
79
- "imageCreditPerImage": 0.02,
80
- "videoCreditPerSecond": 0.01,
81
- "audioCreditPerSecond": 0.01,
82
- "audioDefaultSeconds": 10,
83
- "textTokenEstimateMultiplier": 1.0
84
- },
85
- "models": [
86
- {
87
- "id": "lightning-text-v2",
88
- "type": "text",
89
- "name": "Lightning Text v2"
90
- },
91
- {
92
- "id": "lightning-image-turbo",
93
- "type": "image",
94
- "name": "Lightning Image Turbo"
95
- },
96
- {
97
- "id": "ltx-2-3",
98
- "type": "video",
99
- "name": "LTX 2.3"
100
- },
101
- {
102
- "id": "acestep-1-5-turbo",
103
- "type": "audio",
104
- "name": "AceStep 1.5 Turbo"
105
- }
106
- ],
107
- "notices": [
108
- {
109
- "id": "welcome",
110
- "level": "info",
111
- "active": true,
112
- "message": "Welcome to the InferencePort AI developer console."
113
- }
114
- ],
115
- "limits": {
116
- "requestsPerMinutePerIdentity": 120,
117
- "minimumTextCreditsBalance": 10
118
- }
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/__init__.py DELETED
@@ -1,21 +0,0 @@
1
- import os
2
- from fastapi import APIRouter
3
-
4
- router = APIRouter(prefix="/gen")
5
-
6
- PKEY = os.getenv("POLLINATIONS_KEY", "")
7
- PKEY2 = os.getenv("POLLINATIONS2_KEY", "")
8
- PKEY3 = os.getenv("POLLINATIONS3_KEY", "")
9
- AIRFORCE_KEY = os.getenv("AIRFORCE")
10
- AIRFORCE_VIDEO_MODEL = "grok-imagine-video"
11
- AIRFORCE_API_URL = "https://api.airforce/v1/images/generations"
12
- WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY", "")
13
-
14
- valid_ratios = {"3:2", "2:3", "1:1", "", None}
15
- ratios = {"3:2", "2:3", "1:1"}
16
- valid_modes = {"normal", "fun", "", None}
17
- modes = {"normal", "fun"}
18
-
19
- MAX_VIDEO_RETRIES = 6
20
-
21
- from . import media, chat, prompt, models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/chat.py DELETED
@@ -1,361 +0,0 @@
1
- import os
2
- import random
3
- import json
4
- import httpx
5
- from typing import Optional
6
- from fastapi import Request, HTTPException, Header
7
- from fastapi.responses import JSONResponse, StreamingResponse
8
- from helper.keywords import *
9
- from helper.ratelimit import (
10
- MAX_CHAT_PROMPT_BYTES,
11
- MAX_CHAT_PROMPT_CHARS,
12
- MAX_GROQ_PROMPT_BYTES,
13
- MAX_GROQ_PROMPT_CHARS,
14
- extract_user_text,
15
- calculate_messages_size,
16
- normalize_prompt_value,
17
- enforce_prompt_size,
18
- )
19
- from . import router, WEBSOCKET_KEY
20
- from .common import check_chat_rate_limit
21
- from .override import get_override, is_allowed_in, resolve_override_routing
22
-
23
- MODEL_MAP = {
24
- "llama-3.1-8b-instant": "Meta Llama 3.1 8B Instant",
25
- "gpt-4o-mini": "OpenAI GPT 4o Mini",
26
- "gpt-4.1": "OpenAI GPT 4.1",
27
- "nemotron-3-super": "NVIDIA Nemotron 3 Super",
28
- "gpt-oss-120b": "OpenAI GPT-OSS 120B",
29
- "zai-glm-4.7": "Z.AI GLM 4.7",
30
- "openai/gpt-oss-120b": "OpenAI GPT-OSS 120B",
31
- "openai/gpt-oss-20b": "OpenAI GPT-OSS 20B",
32
- "qwen-3-235b-a22b-instruct-2507": "Qwen3 Instruct",
33
- "llama-3.3-70b-versatile": "Meta Llama 3.3 70B Versatile",
34
- "meta-llama/llama-4-scout-17b-16e-instruct": "Meta Llama 4 Scout",
35
- }
36
-
37
-
38
- def route_request(messages: list, body: dict, prompt_text: str) -> dict:
39
- """
40
- Analyze messages and determine the best model/provider.
41
- Returns dict with keys: model, provider, url, api_key, model_name, is_override.
42
- If body specifies an override model (from models_override.json), routes there.
43
- """
44
- requested_model = body.get("model")
45
- override = get_override(requested_model) if requested_model else None
46
-
47
- if override:
48
- routing = resolve_override_routing(requested_model)
49
- if routing and is_allowed_in(requested_model, "gen"):
50
- return {
51
- "model": requested_model,
52
- "model_name": MODEL_MAP.get(requested_model, requested_model),
53
- "provider": "override",
54
- "url": routing["api_url"],
55
- "api_key": routing["api_key"],
56
- "is_override": True,
57
- "override_key": requested_model,
58
- }
59
-
60
- total_chars, total_bytes = calculate_messages_size(messages)
61
-
62
- uses_tools = (
63
- "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
64
- ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
65
-
66
- long_context = is_long_context(messages)
67
- code_present = contains_code(prompt_text)
68
- math_heavy = is_math_heavy(prompt_text)
69
- structured_task = is_structured_task(prompt_text)
70
- multi_q = multiple_questions(prompt_text)
71
- code_heavy = is_code_heavy(prompt_text, code_present, long_context)
72
-
73
- score = 0
74
-
75
- if long_context:
76
- score += 3
77
-
78
- if math_heavy:
79
- score += 3
80
-
81
- if structured_task:
82
- score += 2
83
-
84
- if code_present:
85
- score += 2
86
-
87
- if multi_q:
88
- score += 1
89
-
90
- for kw in REASONING_KEYWORDS:
91
- if kw in prompt_text:
92
- score += 1
93
-
94
- chosen_model = "llama-3.3-70b-versatile"
95
- provider = "groq"
96
- has_images = contains_images(messages)
97
-
98
- if has_images:
99
- chosen_model = "gpt-4.1"
100
- provider = "navy vision"
101
- else:
102
- if score > 10:
103
- score = 10
104
- if uses_tools:
105
- if score >= 6:
106
- chosen_model = "nemotron-3-super"
107
- provider = "navy"
108
- elif score >= 4:
109
- chosen_model = "gpt-oss-120b"
110
- provider = "cerebras"
111
- else:
112
- chosen_model = "openai/gpt-oss-20b"
113
- provider = "groq"
114
-
115
- elif code_present:
116
-
117
- if code_heavy and score >= 6:
118
- chosen_model = "o3-mini"
119
- provider = "navy"
120
-
121
- elif score >= 4:
122
- chosen_model = "zai-glm-4.7"
123
- provider = "cerebras"
124
-
125
- elif score >= 4:
126
- chosen_model = "openai/gpt-oss-120b"
127
- provider = "groq"
128
-
129
- elif score >= 6:
130
- chosen_model = "sonar"
131
- provider = "navy"
132
-
133
- if provider == "groq" and (
134
- total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
135
- ):
136
- provider = "cerebras"
137
- chosen_model = "gpt-oss-120b"
138
-
139
- print(
140
- f"""
141
- [ADVANCED ROUTER]
142
- Score: {score}
143
- Uses tools: {uses_tools}
144
- Long context: {long_context}
145
- Code present: {code_present}
146
- Math heavy: {math_heavy}
147
- Structured: {structured_task}
148
- Multi-question: {multi_q}
149
- MULTIMODAL REQUIRED: {has_images}
150
- → Selected: {chosen_model} ({provider})
151
- """
152
- )
153
-
154
- if provider == "groq":
155
- groq_keys = os.getenv("GROQ_KEY", "")
156
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
157
- if not groq_keys_list:
158
- raise HTTPException(500, "Missing GROQ_KEY(s)")
159
- api_key = random.choice(groq_keys_list)
160
- url = "https://api.groq.com/openai/v1/chat/completions"
161
-
162
- elif provider == "cerebras":
163
- cer_keys = os.getenv("CER_KEY", "")
164
- cer_keys_list = [k.strip() for k in cer_keys.split(",") if k.strip()]
165
- if not cer_keys_list:
166
- raise HTTPException(500, "Missing CER_KEY(s)")
167
- api_key = random.choice(cer_keys_list)
168
- url = "https://api.cerebras.ai/v1/chat/completions"
169
-
170
- elif provider == "navy vision":
171
- navy_keys = os.getenv("NAVY_KEY", "")
172
- navy_keys_list = [k.strip() for k in navy_keys.split(",") if k.strip()]
173
- if not navy_keys_list:
174
- raise HTTPException(500, "Missing NAVY Keys(s)")
175
- api_key = random.choice(navy_keys_list)
176
- url = "https://api.navy/v1/chat/completions"
177
-
178
- elif provider == "navy":
179
- navy_keys = os.getenv("NAVY_TEXT_ONLY", "")
180
- navy_keys_list = [k.strip() for k in navy_keys.split(",") if k.strip()]
181
- if not navy_keys_list:
182
- raise HTTPException(500, "Missing NAVY TEXT ONLY Keys(s)")
183
- api_key = random.choice(navy_keys_list)
184
- url = "https://api.navy/v1/chat/completions"
185
-
186
- else:
187
- raise HTTPException(500, "Unknown provider routing error")
188
-
189
- return {
190
- "model": chosen_model,
191
- "model_name": MODEL_MAP.get(chosen_model, chosen_model),
192
- "provider": provider,
193
- "url": url,
194
- "api_key": api_key,
195
- "is_override": False,
196
- "override_key": None,
197
- }
198
-
199
-
200
- @router.post("/chat/completions")
201
- async def generate_text(
202
- request: Request,
203
- authorization: Optional[str] = Header(None),
204
- x_client_id: Optional[str] = Header(None),
205
- ):
206
- body = await request.json()
207
- messages = body.get("messages", [])
208
- if not isinstance(messages, list) or len(messages) == 0:
209
- raise HTTPException(400, "messages[] is required")
210
-
211
- prompt_text = extract_user_text(messages)
212
-
213
- if request.headers.get("x-payg-internal") != WEBSOCKET_KEY:
214
- await check_chat_rate_limit(request, authorization, x_client_id)
215
-
216
- route = route_request(messages, body, prompt_text)
217
- chosen_model = route["model"]
218
- provider = route["provider"]
219
- url = route["url"]
220
- api_key = route["api_key"]
221
-
222
- body["model"] = chosen_model
223
- headers = {"Authorization": f"Bearer {api_key}"}
224
-
225
- stream = body.get("stream", False)
226
- fallback_model = "meta-llama/llama-4-scout-17b-16e-instruct"
227
- fallback_provider = "groq"
228
-
229
- if stream:
230
- body["stream"] = True
231
-
232
- async def stream_primary(client, url, body, headers):
233
- try:
234
- async with client.stream("POST", url, json=body, headers=headers) as r:
235
-
236
- if r.status_code >= 400:
237
- print("[STREAM FALLBACK] Primary provider failed → switching to Groq fallback")
238
- async for chunk in stream_fallback(client, body):
239
- yield chunk
240
- return
241
-
242
- async for line in r.aiter_lines():
243
- if not line:
244
- yield "\n"
245
- continue
246
- if line.startswith("event: error"):
247
- if line.startswith("data:"):
248
- try:
249
- obj = json.loads(line[5:].strip())
250
- if isinstance(obj, dict) and "error" in obj and isinstance(obj["error"], dict):
251
- pass
252
- except:
253
- pass
254
-
255
- yield line + "\n"
256
-
257
- except Exception as e:
258
- print(f"[STREAM ERROR] {e}")
259
- async for chunk in stream_fallback(client, body):
260
- yield chunk
261
-
262
- async def stream_fallback(client, body):
263
- fallback_body = {
264
- "model": fallback_model,
265
- "messages": body["messages"],
266
- "stream": True,
267
- }
268
-
269
- groq_keys = os.getenv("GROQ_KEY", "")
270
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
271
- fallback_headers = {"Authorization": f"Bearer {random.choice(groq_keys_list)}"}
272
-
273
- print("[FALLBACK] Starting Groq fallback stream")
274
-
275
- async with client.stream(
276
- "POST",
277
- "https://api.groq.com/openai/v1/chat/completions",
278
- json=fallback_body,
279
- headers=fallback_headers,
280
- ) as r:
281
-
282
- if r.status_code >= 400:
283
- err = (await r.aread()).decode("utf-8", errors="replace")
284
- yield f'data: {{"error": "Fallback provider failed: {err[:500]}"}}\n\n'
285
- return
286
-
287
- async for line in r.aiter_lines():
288
- if not line:
289
- yield "\n"
290
- continue
291
-
292
- if not line.startswith("data:"):
293
- yield f"data: {line}\n\n"
294
- else:
295
- yield line + "\n"
296
-
297
- async def event_generator():
298
- sent_metadata = False
299
-
300
- async with httpx.AsyncClient(timeout=None) as client:
301
- async for chunk in stream_primary(client, url, body, headers):
302
-
303
- if not sent_metadata:
304
- meta = {
305
- "router_metadata": {
306
- "model_name": route["model_name"]
307
- }
308
- }
309
- yield f"data: {json.dumps(meta)}\n\n"
310
- sent_metadata = True
311
-
312
- yield chunk
313
-
314
- return StreamingResponse(
315
- event_generator(),
316
- media_type="text/event-stream",
317
- headers={
318
- "Cache-Control": "no-cache",
319
- "Connection": "keep-alive",
320
- "X-Accel-Buffering": "no",
321
- },
322
- )
323
-
324
- else:
325
- async with httpx.AsyncClient(timeout=None) as client:
326
- r = await client.post(url, json=body, headers=headers)
327
- if provider == "navy vision" and r.status_code >= 400:
328
- print("[FALLBACK] Navy vision failed — switching to 17B Groq")
329
-
330
- groq_keys = os.getenv("GROQ_KEY", "")
331
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
332
- if not groq_keys_list:
333
- raise HTTPException(500, "Missing GROQ_KEY(s) for fallback")
334
-
335
- api_key = random.choice(groq_keys_list)
336
-
337
- fallback_headers = {"Authorization": f"Bearer {api_key}"}
338
- fallback_body = dict(body)
339
- fallback_body["model"] = fallback_model
340
-
341
- r = await client.post(
342
- "https://api.groq.com/openai/v1/chat/completions",
343
- json=fallback_body,
344
- headers=fallback_headers,
345
- )
346
- content_type = (r.headers.get("content-type") or "").lower()
347
- if "application/json" in content_type:
348
- try:
349
- payload = r.json()
350
- except Exception:
351
- payload = {"error": "Upstream returned invalid JSON"}
352
- else:
353
- payload = {
354
- "error": "Upstream returned non-JSON response",
355
- "status_code": r.status_code,
356
- "message": r.text[:1000],
357
- }
358
-
359
- return JSONResponse(status_code=r.status_code, content=payload)
360
-
361
- raise HTTPException(500, "Unknown provider routing error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/common.py DELETED
@@ -1,35 +0,0 @@
1
- import re
2
- from typing import Optional
3
- from fastapi import Request, Header
4
- from helper.keywords import CREATIVE_KEYWORDS, REASONING_KEYWORDS, LIGHTWEIGHT_KEYWORDS
5
- from helper.ratelimit import enforce_rate_limit
6
-
7
- def is_cinematic_image_prompt(prompt: str) -> bool:
8
- for kw in CREATIVE_KEYWORDS:
9
- if kw in prompt.lower():
10
- return True
11
- return False
12
-
13
- def is_complex_reasoning(prompt: str) -> bool:
14
- if len(prompt) > 800:
15
- return True
16
- for kw in REASONING_KEYWORDS:
17
- if kw in prompt:
18
- return True
19
- if re.search(r"\b(if|therefore|assume|let x|given that)\b", prompt):
20
- return True
21
- return False
22
-
23
- def is_lightweight(prompt: str) -> bool:
24
- if len(prompt) < 100:
25
- for kw in LIGHTWEIGHT_KEYWORDS:
26
- if kw in prompt:
27
- return True
28
- return False
29
-
30
- async def check_chat_rate_limit(
31
- request: Request,
32
- authorization: Optional[str],
33
- client_id: Optional[str] = None,
34
- ):
35
- return await enforce_rate_limit(request, authorization, "cloudChatDaily", client_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/media.py DELETED
@@ -1,408 +0,0 @@
1
- import os
2
- import base64
3
- import httpx
4
- from urllib.parse import quote
5
- from fastapi import Request, HTTPException, Header
6
- from fastapi.responses import Response, JSONResponse
7
- from typing import Optional
8
- from helper.assets import save_base64_image, cleanup_image, is_base64_image
9
- from helper.ratelimit import (
10
- check_audio_rate_limit,
11
- check_video_rate_limit,
12
- check_image_rate_limit,
13
- MAX_MEDIA_PROMPT_BYTES,
14
- MAX_MEDIA_PROMPT_CHARS,
15
- normalize_prompt_value,
16
- enforce_prompt_size,
17
- )
18
- from . import (
19
- router,
20
- PKEY,
21
- PKEY2,
22
- WEBSOCKET_KEY,
23
- AIRFORCE_KEY,
24
- AIRFORCE_VIDEO_MODEL,
25
- AIRFORCE_API_URL,
26
- valid_ratios,
27
- ratios,
28
- valid_modes,
29
- modes,
30
- )
31
- from .common import is_cinematic_image_prompt
32
-
33
-
34
- @router.post("/image")
35
- @router.get("/image/{prompt}")
36
- async def generate_image(
37
- request: Request,
38
- prompt: str = None,
39
- authorization: str = Header(None),
40
- x_client_id: str = Header(None),
41
- ):
42
- timeout = httpx.Timeout(300.0, read=300.0)
43
-
44
- if prompt is None:
45
- payload = await request.json()
46
- prompt = payload.get("prompt")
47
- mode = payload.get("mode")
48
- image_urls = payload.get("image_urls")
49
- else:
50
- mode = request.query_params.get("mode")
51
- image_urls = request.query_params.getlist("image_urls")
52
-
53
- prompt = normalize_prompt_value(prompt, "prompt")
54
- enforce_prompt_size(prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Image prompt")
55
- if request.headers.get("x-payg-internal") != WEBSOCKET_KEY:
56
- await check_image_rate_limit(request, authorization, x_client_id)
57
-
58
- chosen_model = "zimage"
59
- if is_cinematic_image_prompt(prompt):
60
- chosen_model = "flux"
61
-
62
- if isinstance(mode, str):
63
- m = mode.strip().lower()
64
- if m == "fantasy":
65
- chosen_model = "flux"
66
- elif m == "realistic":
67
- chosen_model = "zimage"
68
-
69
- has_input_image = bool(image_urls)
70
- temp_assets = []
71
-
72
- if has_input_image:
73
- chosen_model = "klein"
74
-
75
- params = {"model": chosen_model, "key": PKEY2}
76
-
77
- if has_input_image:
78
- processed = []
79
- for img in image_urls[:2]:
80
- if is_base64_image(img):
81
- image_id = save_base64_image(img)
82
- temp_assets.append(image_id)
83
- served = f"{request.base_url}asset-cdn/assets/{image_id}"
84
- processed.append(served)
85
- else:
86
- processed.append(img)
87
- params["image"] = "|".join(processed)
88
-
89
- encoded_prompt = quote(prompt, safe="")
90
- query = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
91
- url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query}"
92
-
93
- try:
94
- async with httpx.AsyncClient(timeout=timeout) as client:
95
- resp = await client.get(url)
96
- finally:
97
- for aid in temp_assets:
98
- cleanup_image(aid)
99
-
100
- if resp.status_code != 200:
101
- raise HTTPException(500, f"Pollinations error: {resp.status_code}")
102
-
103
- return Response(content=resp.content, media_type="image/jpeg")
104
-
105
-
106
- @router.get("/sfx/{prompt}")
107
- @router.post("/sfx")
108
- async def gensfx(
109
- request: Request,
110
- prompt: str = None,
111
- authorization: str = Header(None),
112
- x_client_id: str = Header(None),
113
- ):
114
- if prompt is None:
115
- payload = await request.json()
116
- prompt = payload.get("prompt")
117
-
118
- prompt = normalize_prompt_value(prompt, "prompt")
119
- enforce_prompt_size(prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Audio prompt")
120
- if request.headers.get("x-payg-internal") != WEBSOCKET_KEY:
121
- await check_audio_rate_limit(request, authorization, x_client_id)
122
-
123
- url = f"https://gen.pollinations.ai/audio/{prompt}?model=acestep&key={PKEY}"
124
-
125
- async with httpx.AsyncClient(timeout=None) as client:
126
- resp = await client.get(url)
127
-
128
- if resp.status_code != 200:
129
- return JSONResponse(
130
- status_code=resp.status_code,
131
- content={"success": False, "error": "Upstream music/sfx generation failed"},
132
- )
133
-
134
- return Response(resp.content, media_type="audio/mpeg")
135
-
136
-
137
- @router.get("/video/{prompt}")
138
- @router.post("/video")
139
- @router.head("/video")
140
- async def genvideo(request: Request, prompt: str = None, authorization: str = Header(None), x_client_id: str = Header(None)):
141
- if request.method == "HEAD":
142
- return Response(
143
- status_code=200,
144
- headers={
145
- "Y-prompt": "string — required. The text prompt used to generate the video.",
146
- "Y-ratio": "string — optional. Aspect ratio of the output video.",
147
- "Y-ratio-values": "3:2,2:3,1:1",
148
- "Y-ratio-default": "3:2",
149
- "Y-mode": "string — optional. Controls generation style.",
150
- "Y-mode-values": "normal,fun",
151
- "Y-mode-default": "normal",
152
- "Y-duration": "integer — optional. Duration in seconds (1–10).",
153
- "Y-duration-default": "5",
154
- "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
155
- "Y-image_urls-max": "2",
156
- "Y-response_format": "video/mp4",
157
- "Y-model": "grok-video",
158
- },
159
- )
160
-
161
- aspectRatio = "3:2"
162
- inputMode = "normal"
163
- duration = 5
164
- image_urls = None
165
- ratio = None
166
- mode = None
167
-
168
- if prompt is None:
169
- user_body = await request.json()
170
- prompt = user_body.get("prompt")
171
- ratio = user_body.get("ratio")
172
- mode = user_body.get("mode")
173
- image_urls = user_body.get("image_urls")
174
- duration = user_body.get("duration", 5)
175
-
176
- if ratio not in valid_ratios:
177
- raise HTTPException(
178
- status_code=400,
179
- detail=f"Invalid aspect ratio '{ratio}'. Must be one of 3:2, 2:3, or 1:1.",
180
- )
181
- if ratio in ratios:
182
- aspectRatio = ratio
183
-
184
- if mode not in valid_modes:
185
- raise HTTPException(
186
- status_code=400,
187
- detail=f"Invalid mode '{mode}'. Must be 'normal' or 'fun'.",
188
- )
189
- if mode in modes:
190
- inputMode = mode
191
-
192
- if image_urls:
193
- if not isinstance(image_urls, list):
194
- raise HTTPException(400, "image_urls must be a list")
195
- if len(image_urls) > 2:
196
- raise HTTPException(400, "You may provide at most two image URLs")
197
-
198
- try:
199
- duration = max(1, min(10, int(duration)))
200
- except (TypeError, ValueError):
201
- duration = 5
202
-
203
- prompt = normalize_prompt_value(prompt, "prompt")
204
- enforce_prompt_size(
205
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
206
- )
207
- if request.headers.get("x-payg-internal") != WEBSOCKET_KEY:
208
- await check_video_rate_limit(request, authorization, x_client_id)
209
-
210
- RATIO_MAP = {
211
- "3:2": "16:9",
212
- "2:3": "9:16",
213
- "1:1": "9:16",
214
- }
215
- pollinations_ratio = RATIO_MAP.get(aspectRatio, "16:9")
216
-
217
- encoded_prompt = quote(prompt, safe="")
218
- params = {
219
- "model": "ltx-2",
220
- "duration": duration,
221
- "aspectRatio": pollinations_ratio,
222
- "seed": -1,
223
- }
224
-
225
- temp_assets = []
226
-
227
- if image_urls:
228
- processed_urls = []
229
-
230
- for img in image_urls[:2]:
231
- if is_base64_image(img):
232
- image_id = save_base64_image(img)
233
- temp_assets.append(image_id)
234
-
235
- served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
236
- processed_urls.append(served_url)
237
- else:
238
- processed_urls.append(img)
239
-
240
- params["image"] = "|".join(processed_urls)
241
-
242
- if inputMode == "fun":
243
- params["enhance"] = "true"
244
-
245
- query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
246
- url = f"https://gen.pollinations.ai/video/{encoded_prompt}?{query_string}"
247
-
248
- print(f"[VIDEO GEN] Pollinations URL: {url}")
249
- url = url + f"&key={PKEY}"
250
- resp = None
251
- try:
252
- async with httpx.AsyncClient(timeout=600) as client:
253
- resp = await client.get(url)
254
- finally:
255
- for aid in temp_assets:
256
- cleanup_image(aid)
257
- if resp is None:
258
- raise HTTPException(502, "Video generation request failed")
259
- if resp.status_code != 200:
260
- body_text = ""
261
- try:
262
- body_text = resp.text
263
- except Exception:
264
- pass
265
- return JSONResponse(
266
- status_code=resp.status_code,
267
- content={
268
- "success": False,
269
- "error": "Upstream video generation failed",
270
- "status_code": resp.status_code,
271
- "message": body_text[:1000],
272
- },
273
- )
274
-
275
- if not resp.content:
276
- raise HTTPException(502, "Pollinations returned empty response")
277
-
278
- return Response(
279
- content=resp.content,
280
- media_type="video/mp4",
281
- headers={
282
- "Content-Length": str(len(resp.content)),
283
- "Accept-Ranges": "bytes",
284
- },
285
- )
286
-
287
-
288
- @router.get("/video/airforce/{prompt}")
289
- @router.post("/video/airforce")
290
- async def genvideo_airforce(
291
- request: Request,
292
- prompt: str = None,
293
- authorization: str = Header(None),
294
- x_client_id: str = Header(None),
295
- ):
296
- if request.method == "HEAD":
297
- return Response(
298
- status_code=200,
299
- headers={
300
- "Y-prompt": "string — required. The text prompt used to generate the video.",
301
- "Y-ratio": "string — optional. Aspect ratio of the output video.",
302
- "Y-ratio-values": "3:2,2:3,1:1",
303
- "Y-ratio-default": "3:2",
304
- "Y-mode": "string — optional. Controls generation style.",
305
- "Y-mode-values": "normal,fun",
306
- "Y-mode-default": "normal",
307
- "Y-duration": "integer — optional. Duration in seconds.",
308
- "Y-duration-default": "5",
309
- "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
310
- "Y-image_urls-max": "2",
311
- "Y-response_format": "video/mp4",
312
- "Y-model": "grok-imagine-video",
313
- },
314
- )
315
-
316
- aspectRatio = "3:2"
317
- inputMode = "normal"
318
- image_urls = None
319
- ratio = None
320
- mode = None
321
-
322
- user_body = {}
323
- if prompt is None:
324
- user_body = await request.json()
325
- prompt = user_body.get("prompt")
326
- ratio = user_body.get("ratio")
327
- mode = user_body.get("mode")
328
- image_urls = user_body.get("image_urls")
329
-
330
- if ratio not in valid_ratios:
331
- raise HTTPException(
332
- status_code=400,
333
- detail=f"Invalid aspect ratio {ratio}. Must be one of 3:2, 2:3, or 1:1. Default is 3:2",
334
- )
335
- if ratio in ratios:
336
- aspectRatio = ratio
337
-
338
- if mode not in valid_modes:
339
- raise HTTPException(
340
- status_code=400,
341
- detail=f"Invalid mode {mode}. Must be 'normal' or 'fun'. Default is normal",
342
- )
343
- if mode in modes:
344
- inputMode = mode
345
-
346
- if image_urls:
347
- if not isinstance(image_urls, list):
348
- raise HTTPException(400, "image_urls must be a list")
349
-
350
- if len(image_urls) > 2:
351
- raise HTTPException(400, "You may provide at most two image URLs")
352
-
353
- prompt = normalize_prompt_value(prompt, "prompt")
354
- enforce_prompt_size(
355
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
356
- )
357
- if request.headers.get("x-payg-internal") != WEBSOCKET_KEY:
358
- await check_video_rate_limit(request, authorization, x_client_id)
359
-
360
- payload = {
361
- "model": AIRFORCE_VIDEO_MODEL,
362
- "prompt": prompt,
363
- "n": 1,
364
- "size": "1024x1024",
365
- "response_format": "b64_json",
366
- "sse": False,
367
- "mode": inputMode,
368
- "aspectRatio": aspectRatio,
369
- }
370
-
371
- if image_urls:
372
- payload["image_urls"] = image_urls
373
-
374
- async with httpx.AsyncClient(timeout=600) as client:
375
- resp = await client.post(
376
- AIRFORCE_API_URL,
377
- headers={
378
- "Authorization": f"Bearer {AIRFORCE_KEY}",
379
- "Content-Type": "application/json",
380
- },
381
- json=payload,
382
- )
383
-
384
- if resp.status_code != 200:
385
- return JSONResponse(status_code=resp.status_code, content=resp.json())
386
-
387
- if not resp.content:
388
- raise HTTPException(502, "api.airforce returned empty response")
389
-
390
- try:
391
- result = resp.json()
392
- b64_video = result["data"][0]["b64_json"]
393
- except Exception:
394
- raise HTTPException(502, f"Invalid api.airforce response: {resp.text[:500]}")
395
-
396
- if not b64_video:
397
- raise HTTPException(502, "Airforce returned empty b64_json")
398
-
399
- video_bytes = base64.b64decode(b64_video)
400
-
401
- return Response(
402
- content=video_bytes,
403
- media_type="video/mp4",
404
- headers={
405
- "Content-Length": str(len(video_bytes)),
406
- "Accept-Ranges": "bytes",
407
- },
408
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/models.py DELETED
@@ -1,10 +0,0 @@
1
- from . import router
2
- from .override import build_all_models
3
-
4
-
5
- @router.get("/models")
6
- def return_models_openai():
7
- return {
8
- "object": "list",
9
- "data": build_all_models(),
10
- }
 
 
 
 
 
 
 
 
 
 
 
gen/override.py DELETED
@@ -1,133 +0,0 @@
1
- import json
2
- import os
3
- from pathlib import Path
4
- from typing import Any, Dict, List, Optional
5
-
6
- CONFIG_PATH = Path(__file__).resolve().parent.parent / "models_override.json"
7
- LIGHTNING_CREATED = 1767225600
8
-
9
- _OVERRIDE_CACHE: Optional[Dict[str, Any]] = None
10
- _OVERRIDE_MTIME: Optional[float] = None
11
-
12
-
13
- def _load_overrides() -> Dict[str, Any]:
14
- global _OVERRIDE_CACHE, _OVERRIDE_MTIME
15
- mtime = None
16
- try:
17
- mtime = CONFIG_PATH.stat().st_mtime
18
- except FileNotFoundError:
19
- pass
20
-
21
- if _OVERRIDE_CACHE is not None and _OVERRIDE_MTIME == mtime:
22
- return _OVERRIDE_CACHE
23
-
24
- data = {"lightning": {}, "overrides": []}
25
- if mtime is not None:
26
- try:
27
- raw = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
28
- if isinstance(raw, dict):
29
- if not isinstance(raw.get("overrides"), list):
30
- raw["overrides"] = []
31
- if not isinstance(raw.get("lightning"), dict):
32
- raw["lightning"] = {}
33
- data = raw
34
- except Exception:
35
- pass
36
-
37
- _OVERRIDE_CACHE = data
38
- _OVERRIDE_MTIME = mtime
39
- return data
40
-
41
-
42
- def get_override(model_id: str) -> Optional[Dict[str, Any]]:
43
- data = _load_overrides()
44
- for entry in data.get("overrides", []):
45
- if isinstance(entry, dict) and entry.get("id") == model_id:
46
- return entry
47
- return None
48
-
49
-
50
- def is_allowed_in(model_id: str, context: str) -> bool:
51
- entry = get_override(model_id)
52
- if not entry:
53
- return False
54
- allowed = entry.get("allowed_in", [])
55
- return context in allowed
56
-
57
-
58
- def resolve_override_routing(model_id: str) -> Optional[Dict[str, Any]]:
59
- entry = get_override(model_id)
60
- if not entry:
61
- return None
62
- api_url = entry.get("api_url")
63
- api_key_env = entry.get("api_key_env")
64
- api_key = os.getenv(api_key_env, "") if api_key_env else ""
65
- if not api_url or not api_key:
66
- return None
67
- return {
68
- "api_url": api_url,
69
- "api_key": api_key,
70
- "pricing_per_million_tokens": float(entry.get("pricing_per_million_tokens", 0)),
71
- }
72
-
73
-
74
- def _ppm_to_per_token(ppm: float) -> str:
75
- return f"{ppm / 1_000_000:.12f}"
76
-
77
-
78
- def _build_lightning_entry(config_data: Dict[str, Any]) -> Dict[str, Any]:
79
- lightning = config_data.get("lightning", {})
80
- ppm = float(lightning.get("pricing_per_million_tokens", 0.75))
81
- pt = _ppm_to_per_token(ppm)
82
- return {
83
- "id": "lightning",
84
- "name": lightning.get("name", "Lightning"),
85
- "created": LIGHTNING_CREATED,
86
- "context_length": lightning.get("context_length", 128000),
87
- "max_output_length": lightning.get("max_output_length", 32000),
88
- "input_modalities": lightning.get("input_modalities", ["text", "image", "file"]),
89
- "output_modalities": lightning.get("output_modalities", ["text"]),
90
- "pricing": {
91
- "prompt": pt,
92
- "completion": pt,
93
- "image": "0",
94
- "request": "0",
95
- "input_cache_read": "0",
96
- },
97
- "supported_features": lightning.get("supported_features", ["tools", "json_mode", "streaming", "structured_outputs"]),
98
- }
99
-
100
-
101
- def _build_override_entry(entry: Dict[str, Any]) -> Dict[str, Any]:
102
- ppm = float(entry.get("pricing_per_million_tokens", 0))
103
- pt = _ppm_to_per_token(ppm)
104
- return {
105
- "id": entry.get("id", ""),
106
- "name": entry.get("name", entry.get("id", "")),
107
- "created": entry.get("created", LIGHTNING_CREATED),
108
- "context_length": entry.get("context_length", 128000),
109
- "max_output_length": entry.get("max_output_length", 32000),
110
- "input_modalities": entry.get("input_modalities", ["text"]),
111
- "output_modalities": entry.get("output_modalities", ["text"]),
112
- "pricing": {
113
- "prompt": pt,
114
- "completion": pt,
115
- "image": "0",
116
- "request": "0",
117
- "input_cache_read": "0",
118
- },
119
- "supported_features": entry.get("supported_features", ["streaming"]),
120
- "quantization": entry.get("quantization"),
121
- "description": entry.get("description"),
122
- "is_ready": True,
123
- "is_free": ppm == 0,
124
- }
125
-
126
-
127
- def build_all_models() -> List[Dict[str, Any]]:
128
- data = _load_overrides()
129
- models = [_build_lightning_entry(data)]
130
- for entry in data.get("overrides", []):
131
- if isinstance(entry, dict):
132
- models.append(_build_override_entry(entry))
133
- return models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gen/prompt.py DELETED
@@ -1,101 +0,0 @@
1
- from fastapi import Request, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from helper.keywords import *
4
- from helper.ratelimit import (
5
- extract_user_text,
6
- calculate_messages_size,
7
- MAX_GROQ_PROMPT_BYTES,
8
- MAX_GROQ_PROMPT_CHARS,
9
- )
10
- from . import router
11
- from .chat import MODEL_MAP
12
-
13
-
14
- @router.post("/prompt_analyze")
15
- async def analyze_prompt(request: Request):
16
- body = await request.json()
17
- messages = body.get("prompt", [])
18
- if not isinstance(messages, list) or len(messages) == 0:
19
- raise HTTPException(400, "messages[] is required")
20
-
21
- total_chars, total_bytes = calculate_messages_size(messages)
22
- prompt_text = extract_user_text(messages)
23
-
24
- uses_tools = (
25
- "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
26
- ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
27
-
28
- long_context = is_long_context(messages)
29
- code_present = contains_code(prompt_text)
30
- math_heavy = is_math_heavy(prompt_text)
31
- structured_task = is_structured_task(prompt_text)
32
- multi_q = multiple_questions(prompt_text)
33
- code_heavy = is_code_heavy(prompt_text, code_present, long_context)
34
-
35
- score = 0
36
-
37
- if long_context:
38
- score += 3
39
-
40
- if math_heavy:
41
- score += 3
42
-
43
- if structured_task:
44
- score += 2
45
-
46
- if code_present:
47
- score += 2
48
-
49
- if multi_q:
50
- score += 1
51
-
52
- for kw in REASONING_KEYWORDS:
53
- if kw in prompt_text:
54
- score += 1
55
-
56
- chosen_model = "llama-3.3-70b-versatile"
57
- provider = "groq"
58
- has_images = contains_images(messages)
59
-
60
- if has_images:
61
- chosen_model = "gpt-4.1"
62
- provider = "navy vision"
63
- else:
64
- if score > 10:
65
- score = 10
66
- if uses_tools:
67
- if score >= 6:
68
- chosen_model = "nemotron-3-super"
69
- provider = "navy"
70
- elif score >= 4:
71
- chosen_model = "gpt-oss-120b"
72
- provider = "cerebras"
73
- else:
74
- chosen_model = "openai/gpt-oss-20b"
75
- provider = "groq"
76
-
77
- elif code_present:
78
-
79
- if code_heavy and score >= 6:
80
- chosen_model = "o3-mini"
81
- provider = "navy"
82
-
83
- elif score >= 4:
84
- chosen_model = "zai-glm-4.7"
85
- provider = "cerebras"
86
-
87
- elif score >= 4:
88
- chosen_model = "openai/gpt-oss-120b"
89
- provider = "groq"
90
-
91
- elif score >= 6:
92
- chosen_model = "sonar"
93
- provider = "navy"
94
-
95
- if provider == "groq" and (
96
- total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
97
- ):
98
- provider = "cerebras"
99
- chosen_model = "gpt-oss-120b"
100
-
101
- return {MODEL_MAP[chosen_model]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helper/assets.py DELETED
@@ -1,80 +0,0 @@
1
- # app.py
2
- import base64
3
- import os
4
- import uuid
5
- from pathlib import Path
6
- from typing import Dict
7
- from PIL import Image
8
- from io import BytesIO
9
-
10
- from fastapi import FastAPI, HTTPException, APIRouter
11
- from fastapi.responses import FileResponse
12
-
13
- BASE_DIR = Path(__file__).parent
14
- ASSETS_DIR = BASE_DIR / "cdn-assets"
15
- ASSETS_DIR.mkdir(exist_ok=True)
16
-
17
- image_registry: Dict[str, Path] = {}
18
- asset_router = APIRouter(prefix="/asset-cdn", tags=["asset-cdn"])
19
-
20
- def save_base64_image(b64_data: str) -> str:
21
- """
22
- Decodes a base64 string, converts the image to PNG, stores it in /cdn-assets and returns the
23
- unique image_id that can be used in the public URL.
24
- """
25
-
26
- if "," in b64_data:
27
- _, b64_data = b64_data.split(",", 1)
28
-
29
- try:
30
- decoded = base64.b64decode(b64_data)
31
- except (base64.binascii.Error, ValueError) as exc:
32
- raise ValueError("Invalid base64 data") from exc
33
-
34
- image = Image.open(BytesIO(decoded))
35
-
36
- image_id = uuid.uuid4().hex
37
- file_path = ASSETS_DIR / f"{image_id}.png"
38
-
39
- image.save(file_path, "PNG")
40
-
41
- image_registry[image_id] = file_path
42
-
43
- return image_id
44
-
45
-
46
- @asset_router.get("/assets/{image_id}")
47
- def get_image(image_id: str):
48
- """
49
- FastAPI route that streams the requested image to the client.
50
- If the image was cleaned up or never existed, a 404 is returned.
51
- """
52
- file_path = image_registry.get(image_id)
53
- if not file_path or not file_path.exists():
54
- raise HTTPException(status_code=404, detail="Image not found")
55
-
56
- return FileResponse(path=file_path, media_type="image/png")
57
-
58
- def cleanup_image(image_id: str) -> bool:
59
- """
60
- Deletes the image file and removes it from the registry.
61
- Returns True if the image existed and was removed, otherwise False.
62
- """
63
- file_path = image_registry.pop(image_id, None)
64
- if file_path and file_path.exists():
65
- try:
66
- file_path.unlink()
67
- except Exception:
68
- image_registry[image_id] = file_path
69
- return False
70
- return True
71
- return False
72
-
73
- def is_base64_image(value: str) -> bool:
74
- if not isinstance(value, str):
75
- return False
76
- if value.startswith("http://") or value.startswith("https://"):
77
- return False
78
- if value.startswith("data:image"):
79
- return True
80
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helper/keywords.py DELETED
@@ -1,274 +0,0 @@
1
- import re
2
- REASONING_KEYWORDS = [
3
- # explicit reasoning requests
4
- "prove", "demonstrate", "derive", "justify", "verify",
5
- "show that", "walk through", "step by step", "reason through",
6
- "chain of reasoning", "rigorous", "formal proof",
7
-
8
- # analysis/comparison
9
- "analyze", "analysis of", "compare and contrast",
10
- "evaluate", "critically assess", "explain why",
11
- "explain how", "what causes", "implications of",
12
-
13
- # problem solving
14
- "solve", "solution to", "how would you approach",
15
- "strategy for", "optimize", "algorithm for",
16
-
17
- # technical domains
18
- "theorem", "lemma", "corollary",
19
- "complexity analysis", "big o", "time complexity",
20
- "mathematical", "statistical", "probabilistic",
21
- "model the", "simulate",
22
- ]
23
-
24
- CODE_KEYWORDS = [
25
- "await", "async", "print(", "console.log(",
26
- "code", "ts", "js", "py", ".repy", ".rb",
27
- "gnu", "gcc", "clang", "clang++", "program",
28
- "coding", "import", "include", "const", "var",
29
- "let", "str", "string", "int", "float", "code",
30
- "bot", "server", "html", "script", "database",
31
- "db", "sql", "postgre", "app store", "tensorflow",
32
- "pytorch", "python", "rust", "api",
33
- ]
34
-
35
- CREATIVE_KEYWORDS = [
36
- # cinematic cues
37
- "cinematic", "film still", "movie scene",
38
- "epic", "dramatic lighting", "moody lighting",
39
- "volumetric lighting", "depth of field",
40
- "anamorphic lens", "8k", "4k",
41
-
42
- # art styles
43
- "concept art", "digital painting",
44
- "fantasy art", "sci-fi", "mythical",
45
- "cyberpunk", "steampunk",
46
- "baroque", "surreal", "abstract",
47
- "oil painting", "watercolor",
48
-
49
- # rendering engines
50
- "octane render", "unreal engine",
51
- "ray tracing", "global illumination",
52
-
53
- # emotional narrative framing
54
- "emotional portrait", "story scene",
55
- "hero shot", "dramatic pose",
56
- ]
57
-
58
- STRUCTURED_KEYWORDS = [
59
- "return as json",
60
- "output json",
61
- "json schema",
62
- "format as json",
63
- "structured output",
64
- "extract entities",
65
- "extract fields",
66
- "parse this",
67
- "convert to table",
68
- "create a table",
69
- "categorize into",
70
- "classify",
71
- "label the following",
72
- "taxonomy",
73
- "generate schema",
74
- ]
75
-
76
- MATH_PATTERNS = [
77
- r"\b∫\b", r"\b∑\b", r"\b∂\b",
78
- r"\bmatrix\b",
79
- r"\blimit\b",
80
- r"\bintegral\b",
81
- r"\bderivative\b",
82
- r"\bdifferential equation\b",
83
- r"\blinear algebra\b",
84
- r"\boptimi[sz]e\b",
85
- r"\bgradient\b",
86
- r"\bbackprop\b",
87
- r"\bproof\b",
88
- r"\btheorem\b",
89
- ]
90
-
91
- LIGHTWEIGHT_KEYWORDS = [
92
- "hello", "hi", "hey",
93
- "thanks", "thank you",
94
- "define", "definition of",
95
- "what is", "who is",
96
- "quick question",
97
- "short answer",
98
- "brief explanation",
99
- "summarize",
100
- "paraphrase",
101
- "rewrite this",
102
- ]
103
-
104
-
105
- def _message_content_to_text(content):
106
- if isinstance(content, str):
107
- return content
108
-
109
- if isinstance(content, list):
110
- parts = []
111
- for item in content:
112
- if isinstance(item, str):
113
- parts.append(item)
114
- elif isinstance(item, dict):
115
- for key in (
116
- "text",
117
- "content",
118
- "arguments",
119
- "function_call",
120
- "function",
121
- "tool_calls",
122
- "prompt",
123
- "input",
124
- "output",
125
- "message",
126
- "description",
127
- "caption",
128
- "query",
129
- "summary",
130
- ):
131
- nested = item.get(key)
132
- if nested is None:
133
- continue
134
- nested_text = _message_content_to_text(nested)
135
- if nested_text:
136
- parts.append(nested_text)
137
- return " ".join(parts)
138
-
139
- if isinstance(content, dict):
140
- parts = []
141
- for key in (
142
- "text",
143
- "content",
144
- "arguments",
145
- "function_call",
146
- "function",
147
- "tool_calls",
148
- "prompt",
149
- "input",
150
- "output",
151
- "message",
152
- "description",
153
- "caption",
154
- "query",
155
- "summary",
156
- ):
157
- nested = content.get(key)
158
- if nested is None:
159
- continue
160
- nested_text = _message_content_to_text(nested)
161
- if nested_text:
162
- parts.append(nested_text)
163
- return " ".join(parts)
164
-
165
- return ""
166
-
167
-
168
- def _message_contains_images(content) -> bool:
169
- if isinstance(content, list):
170
- for item in content:
171
- if isinstance(item, dict):
172
- item_type = str(item.get("type") or "").lower()
173
- if item_type in {"image", "image_url", "input_image", "image_urls"}:
174
- return True
175
- if "image_url" in item:
176
- return True
177
- if _message_contains_images(item.get("content")):
178
- return True
179
- return False
180
-
181
- if isinstance(content, dict):
182
- item_type = str(content.get("type") or "").lower()
183
- if item_type in {"image", "image_url", "input_image", "image_urls"}:
184
- return True
185
- if "image_url" in content:
186
- return True
187
- for key in ("content", "message", "input", "output", "prompt", "function_call", "function", "tool_calls"):
188
- if _message_contains_images(content.get(key)):
189
- return True
190
-
191
- return False
192
-
193
-
194
- def is_long_context(messages: list) -> bool:
195
- total_chars = 0
196
- for message in messages:
197
- if not isinstance(message, dict):
198
- continue
199
- total_chars += len(_message_content_to_text(message.get("content")))
200
- return total_chars > 4000
201
-
202
-
203
- def contains_code(prompt: str) -> bool:
204
- if "```" in prompt:
205
- return True
206
- for kw in CODE_KEYWORDS:
207
- if kw in prompt:
208
- return True
209
- return False
210
-
211
- def is_code_heavy(prompt: str, code_present: bool, long_context: bool) -> bool:
212
- """
213
- Determines whether the coding task is substantial enough
214
- to require a code-optimized or larger model.
215
- """
216
-
217
- if not code_present:
218
- return False
219
-
220
- heavy_patterns = [
221
- r"\brefactor\b",
222
- r"\boptimi[sz]e\b",
223
- r"\bdebug\b",
224
- r"\bfix this\b",
225
- r"\barchitecture\b",
226
- r"\bdesign pattern\b",
227
- r"\bscalable\b",
228
- r"\bmicroservice\b",
229
- r"\bmultiple files\b",
230
- r"\bentire project\b",
231
- r"\bcodebase\b",
232
- r"\bperformance\b",
233
- ]
234
-
235
- for pattern in heavy_patterns:
236
- if re.search(pattern, prompt):
237
- return True
238
-
239
- if prompt.count("```") >= 2:
240
- return True
241
-
242
- if long_context:
243
- return True
244
-
245
- return False
246
-
247
- def is_math_heavy(prompt: str) -> bool:
248
- for pattern in MATH_PATTERNS:
249
- if re.search(pattern, prompt):
250
- return True
251
- return False
252
-
253
- def is_structured_task(prompt: str) -> bool:
254
- for kw in STRUCTURED_KEYWORDS:
255
- if kw in prompt:
256
- return True
257
- return False
258
-
259
- def multiple_questions(prompt: str) -> bool:
260
- return prompt.count("?") >= 3
261
-
262
- def contains_images(messages):
263
- for msg in messages:
264
- if not isinstance(msg, dict):
265
- continue
266
-
267
- content = msg.get("content")
268
- if _message_contains_images(content):
269
- return True
270
-
271
- if "image" in msg or "image_url" in msg:
272
- return True
273
-
274
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helper/payg.py DELETED
@@ -1,670 +0,0 @@
1
- import asyncio
2
- import json
3
- import math
4
- import os
5
- import time
6
- from collections import defaultdict, deque
7
- from decimal import Decimal, ROUND_HALF_UP
8
- from pathlib import Path
9
- from typing import Any, Dict, Optional
10
- from psycopg.errors import UniqueViolation
11
- from fastapi import HTTPException
12
-
13
- from helper.subscriptions import execute_query, resolve_token_identity
14
-
15
- CONFIG_PATH = Path(__file__).resolve().parent.parent / "assets" / "payg_config.json"
16
- CONFIG_LOCK = asyncio.Lock()
17
- _CONFIG_CACHE: Optional[Dict[str, Any]] = None
18
- _CONFIG_MTIME: Optional[float] = None
19
-
20
- SPAM_TRACKER: Dict[str, deque] = defaultdict(deque)
21
- SPAM_LOCK = asyncio.Lock()
22
-
23
- CREDIT_SCALE = Decimal("0.0001")
24
-
25
-
26
- def _default_config() -> Dict[str, Any]:
27
- return {
28
- "dashboard": {
29
- "appName": "InferencePort AI",
30
- "homeUrl": "https://inference.js.org",
31
- "apiBaseUrl": "https://sharktide-lightning.hf.space",
32
- "supportEmail": "inferenceportai@gmail.com",
33
- "hostname": "",
34
- },
35
- "supabase": {
36
- "url": "",
37
- "publishableKey": "",
38
- "resetRedirectUrl": "https://inference.js.org/reset.html",
39
- "deletePasswordVerifyEndpoint": "",
40
- "deleteAccountEndpoint": "",
41
- },
42
- "billing": {
43
- "currency": "USD",
44
- "packs": [],
45
- },
46
- "pricing": {
47
- "textCreditPerMillionTokens": 0.75,
48
- "imageCreditPerImage": 0.02,
49
- "videoCreditPerSecond": 0.01,
50
- "audioCreditPerSecond": 0.01,
51
- "audioDefaultSeconds": 10,
52
- "textTokenEstimateMultiplier": 1.0,
53
- },
54
- "models": [],
55
- "notices": [],
56
- "limits": {
57
- "requestsPerMinutePerIdentity": 120,
58
- "minimumTextCreditsBalance": 10,
59
- },
60
- }
61
-
62
-
63
- def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
64
- out = dict(base)
65
- for key, value in override.items():
66
- if isinstance(value, dict) and isinstance(out.get(key), dict):
67
- out[key] = _deep_merge(out[key], value)
68
- else:
69
- out[key] = value
70
- return out
71
-
72
-
73
- def _to_decimal(value: Any, default: str = "0") -> Decimal:
74
- try:
75
- return Decimal(str(value))
76
- except Exception:
77
- return Decimal(default)
78
-
79
-
80
- def quantize_credits(value: Decimal | float | int) -> Decimal:
81
- if not isinstance(value, Decimal):
82
- value = _to_decimal(value)
83
- return value.quantize(CREDIT_SCALE, rounding=ROUND_HALF_UP)
84
-
85
-
86
- async def get_payg_config() -> Dict[str, Any]:
87
- global _CONFIG_CACHE, _CONFIG_MTIME
88
-
89
- async with CONFIG_LOCK:
90
- mtime = None
91
- try:
92
- mtime = CONFIG_PATH.stat().st_mtime
93
- except FileNotFoundError:
94
- pass
95
-
96
- if _CONFIG_CACHE is not None and _CONFIG_MTIME == mtime:
97
- return _CONFIG_CACHE
98
-
99
- config = _default_config()
100
-
101
- if mtime is not None:
102
- try:
103
- file_data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
104
- if isinstance(file_data, dict):
105
- config = _deep_merge(config, file_data)
106
- except Exception:
107
- pass
108
-
109
- packs = []
110
- for raw_pack in config.get("billing", {}).get("packs", []):
111
- if not isinstance(raw_pack, dict):
112
- continue
113
- credits = _to_decimal(raw_pack.get("credits"), "0")
114
- amount = _to_decimal(raw_pack.get("amountUsd"), "0")
115
- if credits <= 0 or amount <= 0:
116
- continue
117
- packs.append(
118
- {
119
- "id": str(raw_pack.get("id") or "").strip(),
120
- "label": str(raw_pack.get("label") or "").strip() or f"${amount}",
121
- "amountUsd": float(amount),
122
- "credits": float(quantize_credits(credits)),
123
- "stripePaymentLink": str(raw_pack.get("stripePaymentLink") or "").strip(),
124
- "prod": str(raw_pack.get("prod") or "").strip(),
125
- }
126
- )
127
- config["billing"]["packs"] = packs
128
-
129
- _CONFIG_CACHE = config
130
- _CONFIG_MTIME = mtime
131
-
132
- return config
133
-
134
-
135
- async def require_payg_identity(authorization: Optional[str]) -> Dict[str, Any]:
136
- if not authorization or not authorization.startswith("Bearer "):
137
- raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
138
-
139
- token = authorization.split(" ", 1)[1].strip()
140
- identity = await resolve_token_identity(token)
141
- if not identity:
142
- raise HTTPException(status_code=401, detail="Invalid authorization token")
143
-
144
- user_id = identity.get("user_id")
145
- email = identity.get("email")
146
- if not user_id or not email:
147
- raise HTTPException(status_code=401, detail="Unable to resolve account identity")
148
-
149
- return {
150
- "token": token,
151
- "user_id": str(user_id),
152
- "email": str(email).strip().lower(),
153
- "auth_type": identity.get("auth_type") or "unknown",
154
- }
155
-
156
-
157
- async def enforce_payg_spam_limit(subject: str, per_minute_limit: int):
158
- now = time.time()
159
- cutoff = now - 60
160
-
161
- async with SPAM_LOCK:
162
- bucket = SPAM_TRACKER[subject]
163
- while bucket and bucket[0] < cutoff:
164
- bucket.popleft()
165
-
166
- if len(bucket) >= per_minute_limit:
167
- raise HTTPException(status_code=429, detail="Too many requests. Please slow down.")
168
-
169
- bucket.append(now)
170
-
171
-
172
- async def ensure_user_wallet(user_id: str):
173
- await execute_query(
174
- """
175
- insert into public.user_wallets (user_id, balance_credits, lifetime_credits_purchased)
176
- values (%s, 0, 0)
177
- on conflict (user_id) do nothing;
178
- """,
179
- (user_id,),
180
- commit=True,
181
- )
182
-
183
-
184
- async def get_wallet_snapshot(user_id: str) -> Dict[str, Any]:
185
- await ensure_user_wallet(user_id)
186
- row = await execute_query(
187
- """
188
- select
189
- user_id,
190
- balance_credits,
191
- lifetime_credits_purchased,
192
- stripe_customer_id,
193
- updated_at
194
- from public.user_wallets
195
- where user_id = %s
196
- limit 1;
197
- """,
198
- (user_id,),
199
- fetchone=True,
200
- )
201
-
202
- if not row:
203
- raise HTTPException(status_code=500, detail="Unable to load wallet")
204
-
205
- return {
206
- "user_id": str(row.get("user_id")),
207
- "balance_credits": float(quantize_credits(_to_decimal(row.get("balance_credits"), "0"))),
208
- "lifetime_credits_purchased": float(
209
- quantize_credits(_to_decimal(row.get("lifetime_credits_purchased"), "0"))
210
- ),
211
- "stripe_customer_id": row.get("stripe_customer_id"),
212
- "updated_at": row.get("updated_at").isoformat() if hasattr(row.get("updated_at"), "isoformat") else None,
213
- }
214
-
215
-
216
- async def list_credit_ledger(user_id: str, limit: int = 100) -> list[Dict[str, Any]]:
217
- safe_limit = max(1, min(limit, 500))
218
- rows = await execute_query(
219
- """
220
- select
221
- id,
222
- delta_credits,
223
- balance_after,
224
- entry_type,
225
- usage_kind,
226
- units,
227
- unit_label,
228
- source,
229
- source_ref,
230
- metadata,
231
- created_at
232
- from public.credit_ledger
233
- where user_id = %s
234
- order by created_at desc
235
- limit %s;
236
- """,
237
- (user_id, safe_limit),
238
- )
239
-
240
- result = []
241
- for row in rows or []:
242
- result.append(
243
- {
244
- "id": row.get("id"),
245
- "delta_credits": float(quantize_credits(_to_decimal(row.get("delta_credits"), "0"))),
246
- "balance_after": float(quantize_credits(_to_decimal(row.get("balance_after"), "0"))),
247
- "entry_type": row.get("entry_type"),
248
- "usage_kind": row.get("usage_kind"),
249
- "units": float(row.get("units")) if row.get("units") is not None else None,
250
- "unit_label": row.get("unit_label"),
251
- "source": row.get("source"),
252
- "source_ref": row.get("source_ref"),
253
- "metadata": row.get("metadata") if isinstance(row.get("metadata"), dict) else None,
254
- "created_at": row.get("created_at").isoformat() if hasattr(row.get("created_at"), "isoformat") else None,
255
- }
256
- )
257
-
258
- return result
259
-
260
- async def append_credit_ledger_entry(
261
- *,
262
- user_id: str,
263
- delta_credits: Decimal,
264
- entry_type: str,
265
- usage_kind: Optional[str] = None,
266
- units: Optional[float] = None,
267
- unit_label: Optional[str] = None,
268
- source: Optional[str] = None,
269
- source_ref: Optional[str] = None,
270
- idempotency_key: Optional[str] = None,
271
- metadata: Optional[Dict[str, Any]] = None,
272
- ) -> Dict[str, Any]:
273
- await ensure_user_wallet(user_id)
274
-
275
- delta_credits = quantize_credits(delta_credits)
276
- metadata_json = json.dumps(metadata or {})
277
-
278
- row = None
279
-
280
- try:
281
- row = await execute_query(
282
- """
283
- with updated_wallet as (
284
- update public.user_wallets
285
- set
286
- balance_credits = balance_credits + %s,
287
- lifetime_credits_purchased = case
288
- when %s > 0 then lifetime_credits_purchased + %s
289
- else lifetime_credits_purchased
290
- end,
291
- updated_at = timezone('utc', now())
292
- where user_id = %s
293
- and (balance_credits + %s) >= 0
294
- returning balance_credits
295
- ),
296
- inserted as (
297
- insert into public.credit_ledger (
298
- user_id,
299
- delta_credits,
300
- balance_after,
301
- entry_type,
302
- usage_kind,
303
- units,
304
- unit_label,
305
- source,
306
- source_ref,
307
- idempotency_key,
308
- metadata
309
- )
310
- select
311
- %s,
312
- %s,
313
- updated_wallet.balance_credits,
314
- %s,
315
- %s,
316
- %s,
317
- %s,
318
- %s,
319
- %s,
320
- %s,
321
- %s::jsonb
322
- from updated_wallet
323
- returning id, balance_after, created_at
324
- )
325
- select id, balance_after, created_at
326
- from inserted
327
- limit 1;
328
- """,
329
- (
330
- delta_credits,
331
- delta_credits,
332
- delta_credits,
333
- user_id,
334
- delta_credits,
335
- user_id,
336
- delta_credits,
337
- entry_type,
338
- usage_kind,
339
- units,
340
- unit_label,
341
- source,
342
- source_ref,
343
- idempotency_key,
344
- metadata_json,
345
- ),
346
- fetchone=True,
347
- commit=True,
348
- )
349
-
350
- except UniqueViolation:
351
- row = None
352
-
353
- if row:
354
- return {
355
- "ledger_id": row.get("id"),
356
- "balance_after": float(
357
- quantize_credits(
358
- _to_decimal(row.get("balance_after"), "0")
359
- )
360
- ),
361
- "created_at": (
362
- row.get("created_at").isoformat()
363
- if hasattr(row.get("created_at"), "isoformat")
364
- else None
365
- ),
366
- }
367
-
368
- if idempotency_key:
369
- existing = await execute_query(
370
- """
371
- select id, balance_after, created_at
372
- from public.credit_ledger
373
- where user_id = %s
374
- and idempotency_key = %s
375
- limit 1;
376
- """,
377
- (user_id, idempotency_key),
378
- fetchone=True,
379
- )
380
-
381
- if existing:
382
- return {
383
- "ledger_id": existing.get("id"),
384
- "balance_after": float(
385
- quantize_credits(
386
- _to_decimal(existing.get("balance_after"), "0")
387
- )
388
- ),
389
- "created_at": (
390
- existing.get("created_at").isoformat()
391
- if hasattr(existing.get("created_at"), "isoformat")
392
- else None
393
- ),
394
- "idempotent_replay": True,
395
- }
396
-
397
- raise HTTPException(
398
- status_code=402,
399
- detail="Insufficient credits",
400
- )
401
-
402
- def _estimate_tokens_from_text(text: str) -> int:
403
- if not isinstance(text, str):
404
- return 0
405
-
406
- stripped = text.strip()
407
- if not stripped:
408
- return 0
409
-
410
- # Rough multilingual token estimate without relying on model-specific tokenizers.
411
- return max(1, int(math.ceil(len(stripped) / 3.5)))
412
-
413
-
414
- def _collect_chat_text_and_image_counts(value: Any) -> tuple[int, int]:
415
- if value is None:
416
- return 0, 0
417
-
418
- if isinstance(value, str):
419
- return len(value), 0
420
-
421
- if isinstance(value, list):
422
- chars = 0
423
- image_count = 0
424
- for item in value:
425
- item_chars, item_images = _collect_chat_text_and_image_counts(item)
426
- chars += item_chars
427
- image_count += item_images
428
- return chars, image_count
429
-
430
- if isinstance(value, dict):
431
- chars = 0
432
- image_count = 0
433
- item_type = str(value.get("type") or "").lower()
434
- if item_type in {"image", "image_url", "input_image"}:
435
- image_count += 1
436
-
437
- text_value = value.get("text")
438
- if isinstance(text_value, str):
439
- chars += len(text_value)
440
-
441
- if isinstance(value.get("content"), (str, list, dict)):
442
- nested_chars, nested_images = _collect_chat_text_and_image_counts(value.get("content"))
443
- chars += nested_chars
444
- image_count += nested_images
445
-
446
- if isinstance(value.get("arguments"), str):
447
- chars += len(value["arguments"])
448
-
449
- if isinstance(value.get("function_call"), (str, list, dict)):
450
- nested_chars, nested_images = _collect_chat_text_and_image_counts(value.get("function_call"))
451
- chars += nested_chars
452
- image_count += nested_images
453
-
454
- if isinstance(value.get("function"), (str, list, dict)):
455
- nested_chars, nested_images = _collect_chat_text_and_image_counts(value.get("function"))
456
- chars += nested_chars
457
- image_count += nested_images
458
-
459
- if isinstance(value.get("tool_calls"), list):
460
- nested_chars, nested_images = _collect_chat_text_and_image_counts(value.get("tool_calls"))
461
- chars += nested_chars
462
- image_count += nested_images
463
-
464
- if isinstance(value.get("image_url"), dict):
465
- image_count += 1
466
-
467
- for key, nested in value.items():
468
- if key in {
469
- "content",
470
- "text",
471
- "arguments",
472
- "function",
473
- "function_call",
474
- "tool_calls",
475
- "image_url",
476
- "role",
477
- "type",
478
- "index",
479
- "id",
480
- }:
481
- continue
482
-
483
- if isinstance(nested, (list, dict)):
484
- nested_chars, nested_images = _collect_chat_text_and_image_counts(nested)
485
- chars += nested_chars
486
- image_count += nested_images
487
- elif isinstance(nested, str) and key.lower() in {
488
- "prompt",
489
- "input",
490
- "output",
491
- "message",
492
- "description",
493
- "caption",
494
- "query",
495
- "summary",
496
- }:
497
- chars += len(nested)
498
-
499
- return chars, image_count
500
-
501
- return 0, 0
502
-
503
-
504
- def estimate_text_tokens_from_messages(messages: Any) -> int:
505
- if not isinstance(messages, list):
506
- return 0
507
-
508
- chars = 0
509
- image_count = 0
510
-
511
- for message in messages:
512
- message_chars, message_images = _collect_chat_text_and_image_counts(message)
513
- chars += message_chars
514
- image_count += message_images
515
-
516
- estimated = int(math.ceil(chars / 3.5))
517
- estimated += image_count * 300
518
- return max(0, estimated)
519
-
520
-
521
- def estimate_text_tokens_from_text(text: Any) -> int:
522
- if not isinstance(text, str):
523
- return 0
524
- return _estimate_tokens_from_text(text)
525
-
526
-
527
- def extract_usage_tokens(payload: Dict[str, Any], fallback_tokens: int) -> int:
528
- usage = payload.get("usage") if isinstance(payload, dict) else None
529
- if isinstance(usage, dict):
530
- total = usage.get("total_tokens")
531
- try:
532
- if total is not None:
533
- total_int = int(total)
534
- if total_int >= 0:
535
- return total_int
536
- except Exception:
537
- pass
538
-
539
- prompt_tokens = usage.get("prompt_tokens")
540
- completion_tokens = usage.get("completion_tokens")
541
- try:
542
- if prompt_tokens is not None and completion_tokens is not None:
543
- prompt_int = int(prompt_tokens)
544
- completion_int = int(completion_tokens)
545
- if prompt_int >= 0 and completion_int >= 0:
546
- return max(0, prompt_int + completion_int)
547
- except Exception:
548
- pass
549
- return max(0, fallback_tokens)
550
-
551
-
552
- def extract_usage_breakdown(
553
- payload: Dict[str, Any],
554
- fallback_input_tokens: int,
555
- fallback_output_tokens: int = 0,
556
- ) -> Dict[str, int]:
557
- usage = payload.get("usage") if isinstance(payload, dict) else None
558
- input_tokens = max(0, int(fallback_input_tokens))
559
- output_tokens = max(0, int(fallback_output_tokens))
560
- total_tokens = input_tokens + output_tokens
561
-
562
- if isinstance(usage, dict):
563
- try:
564
- prompt_tokens = usage.get("prompt_tokens")
565
- if prompt_tokens is not None:
566
- prompt_int = int(prompt_tokens)
567
- if prompt_int >= 0:
568
- input_tokens = prompt_int
569
- except Exception:
570
- pass
571
-
572
- try:
573
- completion_tokens = usage.get("completion_tokens")
574
- if completion_tokens is not None:
575
- completion_int = int(completion_tokens)
576
- if completion_int >= 0:
577
- output_tokens = completion_int
578
- except Exception:
579
- pass
580
-
581
- try:
582
- total = usage.get("total_tokens")
583
- if total is not None:
584
- total_int = int(total)
585
- if total_int >= 0:
586
- total_tokens = total_int
587
- else:
588
- total_tokens = input_tokens + output_tokens
589
- else:
590
- total_tokens = input_tokens + output_tokens
591
- except Exception:
592
- total_tokens = input_tokens + output_tokens
593
-
594
- if output_tokens <= 0 and total_tokens >= input_tokens:
595
- output_tokens = max(0, total_tokens - input_tokens)
596
-
597
- if total_tokens < input_tokens + output_tokens:
598
- total_tokens = input_tokens + output_tokens
599
-
600
- return {
601
- "input_tokens": max(0, input_tokens),
602
- "output_tokens": max(0, output_tokens),
603
- "total_tokens": max(0, total_tokens),
604
- }
605
-
606
-
607
- def calculate_text_credit_cost(total_tokens: int, config: Dict[str, Any]) -> Decimal:
608
- pricing = config.get("pricing", {})
609
- per_million = _to_decimal(pricing.get("textCreditPerMillionTokens"), "0.75")
610
- multiplier = _to_decimal(pricing.get("textTokenEstimateMultiplier"), "1")
611
- tokens = _to_decimal(total_tokens, "0")
612
- cost = (tokens / Decimal("1000000")) * per_million * multiplier
613
- return quantize_credits(cost)
614
-
615
-
616
- def calculate_image_credit_cost(image_count: int, config: Dict[str, Any]) -> Decimal:
617
- per_image = _to_decimal(config.get("pricing", {}).get("imageCreditPerImage"), "0.02")
618
- cost = _to_decimal(max(1, image_count)) * per_image
619
- return quantize_credits(cost)
620
-
621
-
622
- def calculate_video_credit_cost(seconds: int, config: Dict[str, Any]) -> Decimal:
623
- per_second = _to_decimal(config.get("pricing", {}).get("videoCreditPerSecond"), "0.01")
624
- cost = _to_decimal(max(1, seconds)) * per_second
625
- return quantize_credits(cost)
626
-
627
-
628
- def calculate_audio_credit_cost(seconds: int, config: Dict[str, Any]) -> Decimal:
629
- per_second = _to_decimal(config.get("pricing", {}).get("audioCreditPerSecond"), "0.01")
630
- cost = _to_decimal(max(1, seconds)) * per_second
631
- return quantize_credits(cost)
632
-
633
-
634
- def get_pack_by_session_amount(config: Dict[str, Any], amount_total_cents: int) -> Optional[Dict[str, Any]]:
635
- if amount_total_cents <= 0:
636
- return None
637
- amount_usd = Decimal(amount_total_cents) / Decimal("100")
638
-
639
- for pack in config.get("billing", {}).get("packs", []):
640
- candidate = _to_decimal(pack.get("amountUsd"), "0")
641
- if candidate == amount_usd:
642
- return pack
643
-
644
- return None
645
-
646
-
647
- def build_usage_summary_from_ledger_rows(rows: list[Dict[str, Any]]) -> Dict[str, Any]:
648
- summary = {
649
- "text": {"credits": 0.0, "units": 0.0, "unit_label": "tokens"},
650
- "image": {"credits": 0.0, "units": 0.0, "unit_label": "images"},
651
- "video": {"credits": 0.0, "units": 0.0, "unit_label": "seconds"},
652
- "audio": {"credits": 0.0, "units": 0.0, "unit_label": "seconds"},
653
- "totalCreditsUsed": 0.0,
654
- }
655
-
656
- for row in rows:
657
- if row.get("entry_type") != "usage":
658
- continue
659
-
660
- kind = str(row.get("usage_kind") or "other").lower()
661
- credits = abs(float(row.get("delta_credits") or 0.0))
662
- units = float(row.get("units") or 0.0)
663
-
664
- if kind in summary:
665
- summary[kind]["credits"] += credits
666
- summary[kind]["units"] += units
667
-
668
- summary["totalCreditsUsed"] += credits
669
-
670
- return summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helper/ratelimit.py DELETED
@@ -1,522 +0,0 @@
1
- import asyncio
2
- import hashlib
3
- import os
4
- import re
5
- import time
6
- from typing import Any, Dict, List, Optional
7
-
8
- from fastapi import HTTPException, Request
9
-
10
- from helper.subscriptions import (
11
- TIER_CONFIG,
12
- USAGE_PERIODS,
13
- fetch_subscription,
14
- get_effective_limit_for_email,
15
- normalize_plan_key,
16
- usage_locks,
17
- usage_store,
18
- )
19
-
20
-
21
- # -------------------------------------------------------------------
22
- # Configuration / Constants
23
- # -------------------------------------------------------------------
24
-
25
- IDENTITY_CACHE_TTL_SECONDS = 60
26
-
27
- CLIENT_BIND_TTL_SECONDS = int(
28
- os.getenv("CLIENT_BIND_TTL_SECONDS", str(8 * 24 * 60 * 60))
29
- )
30
-
31
- MAX_CLIENT_ID_LENGTH = 128
32
-
33
- MAX_CHAT_PROMPT_CHARS = int(os.getenv("MAX_CHAT_PROMPT_CHARS", "120000"))
34
- MAX_CHAT_PROMPT_BYTES = int(os.getenv("MAX_CHAT_PROMPT_BYTES", "500000"))
35
-
36
- MAX_GROQ_PROMPT_CHARS = int(os.getenv("MAX_GROQ_PROMPT_CHARS", "90000"))
37
- MAX_GROQ_PROMPT_BYTES = int(os.getenv("MAX_GROQ_PROMPT_BYTES", "350000"))
38
-
39
- MAX_MEDIA_PROMPT_CHARS = int(os.getenv("MAX_MEDIA_PROMPT_CHARS", "4000"))
40
- MAX_MEDIA_PROMPT_BYTES = int(os.getenv("MAX_MEDIA_PROMPT_BYTES", "16000"))
41
-
42
-
43
- # -------------------------------------------------------------------
44
- # In-memory state
45
- # -------------------------------------------------------------------
46
-
47
- identity_cache: Dict[str, Dict[str, Any]] = {}
48
-
49
- client_subject_bindings: Dict[str, Dict[str, Any]] = {}
50
-
51
-
52
- # -------------------------------------------------------------------
53
- # Public Rate Limit APIs
54
- # -------------------------------------------------------------------
55
-
56
- async def resolve_rate_limit_identity(
57
- request: Request,
58
- authorization: Optional[str],
59
- client_id: Optional[str] = None,
60
- ) -> tuple[str, str]:
61
- now = time.time()
62
-
63
- normalized_client_id = sanitize_client_id(client_id)
64
- default_subject = build_default_subject(request, normalized_client_id)
65
-
66
- if not authorization or not authorization.startswith("Bearer "):
67
- return "free", resolve_bound_subject(normalized_client_id, default_subject)
68
-
69
- token = authorization.split(" ", 1)[1].strip()
70
- if not token:
71
- return "free", resolve_bound_subject(normalized_client_id, default_subject)
72
-
73
- cached = identity_cache.get(token)
74
- if cached and cached.get("expires_at", 0) > now:
75
- plan_key = cached.get("plan_key", "free")
76
- subject = cached.get("subject", default_subject)
77
-
78
- bind_client_subject(normalized_client_id, subject, plan_key)
79
- return plan_key, subject
80
-
81
- try:
82
- sub = await fetch_subscription(token)
83
- except HTTPException as authfailure:
84
- raise authfailure
85
- except Exception:
86
- return "free", resolve_bound_subject(normalized_client_id, default_subject)
87
-
88
- if not isinstance(sub, dict) or sub.get("error"):
89
- return "free", resolve_bound_subject(normalized_client_id, default_subject)
90
-
91
- email = sub.get("email")
92
- if isinstance(email, str) and email.strip():
93
- subject = f"user:{email.strip().lower()}"
94
- else:
95
- subject = default_subject
96
-
97
- plan_key = normalize_plan_key(sub.get("plan_key"))
98
-
99
- identity_cache[token] = {
100
- "plan_key": plan_key,
101
- "subject": subject,
102
- "expires_at": now + IDENTITY_CACHE_TTL_SECONDS,
103
- }
104
-
105
- bind_client_subject(normalized_client_id, subject, plan_key)
106
-
107
- return plan_key, subject
108
-
109
-
110
- async def enforce_rate_limit(
111
- request: Request,
112
- authorization: Optional[str],
113
- metric: str,
114
- client_id: Optional[str] = None,
115
- ) -> Dict[str, Optional[int | str]]:
116
- if metric not in usage_store:
117
- raise HTTPException(status_code=500, detail=f"Unknown limit metric: {metric}")
118
-
119
- plan_key, subject = await resolve_rate_limit_identity(
120
- request, authorization, client_id
121
- )
122
- return await consume_metric_for_subject(metric, plan_key, subject)
123
-
124
-
125
- async def resolve_verify_token_rate_limit_identity(
126
- authorization: Optional[str],
127
- ip: Optional[str],
128
- ) -> tuple[str, str]:
129
- if authorization and authorization.startswith("Bearer "):
130
- token = authorization.split(" ", 1)[1].strip()
131
- if token:
132
- try:
133
- sub = await fetch_subscription(token)
134
- except HTTPException:
135
- sub = None
136
- except Exception:
137
- sub = None
138
-
139
- if isinstance(sub, dict) and not sub.get("error"):
140
- email = sub.get("email")
141
- if isinstance(email, str) and email.strip():
142
- return (
143
- normalize_plan_key(sub.get("plan_key")),
144
- f"user:{email.strip().lower()}",
145
- )
146
-
147
- return "free", build_verify_token_ip_subject(ip)
148
-
149
-
150
- async def consume_metric_for_subject(
151
- metric: str,
152
- plan_key: str,
153
- subject: str,
154
- ) -> Dict[str, Optional[int | str]]:
155
- if metric not in usage_store:
156
- raise HTTPException(status_code=500, detail=f"Unknown limit metric: {metric}")
157
-
158
- plan = TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"]
159
- subject_email = subject[5:] if subject.startswith("user:") else None
160
- limit = get_effective_limit_for_email(plan_key, metric, subject_email)
161
- window_key = get_usage_period_key(metric)
162
- lock = get_usage_lock(metric, subject)
163
-
164
- async with lock:
165
- bucket = usage_store[metric]
166
- entry = bucket.get(subject)
167
-
168
- if not entry or entry.get("window") != window_key:
169
- entry = {"window": window_key, "count": 0}
170
- bucket[subject] = entry
171
-
172
- if limit is not None and entry["count"] >= int(limit):
173
- raise HTTPException(
174
- status_code=429,
175
- detail=f"{metric} limit reached for {plan.get('name', 'current plan')}",
176
- )
177
-
178
- entry["count"] += 1
179
- remaining = None if limit is None else max(0, int(limit) - entry["count"])
180
-
181
- return {
182
- "plan_key": plan_key,
183
- "limit": limit,
184
- "remaining": remaining,
185
- "used": entry["count"],
186
- "window": window_key,
187
- "period": USAGE_PERIODS.get(metric, "daily"),
188
- }
189
-
190
-
191
- async def check_audio_rate_limit(
192
- request: Request,
193
- authorization: Optional[str],
194
- client_id: Optional[str] = None,
195
- ):
196
- await enforce_rate_limit(request, authorization, "audioWeekly", client_id)
197
-
198
-
199
- async def check_image_rate_limit(
200
- request: Request,
201
- authorization: Optional[str],
202
- client_id: Optional[str] = None,
203
- ):
204
- await enforce_rate_limit(request, authorization, "imagesDaily", client_id)
205
-
206
-
207
- async def check_video_rate_limit(
208
- request: Request,
209
- authorization: Optional[str],
210
- client_id: Optional[str] = None,
211
- ):
212
- await enforce_rate_limit(request, authorization, "videosDaily", client_id)
213
-
214
-
215
- # -------------------------------------------------------------------
216
- # Prompt Utilities
217
- # -------------------------------------------------------------------
218
-
219
- def normalize_prompt_value(prompt: Optional[str], field_name: str = "prompt") -> str:
220
- if not isinstance(prompt, str):
221
- raise HTTPException(status_code=400, detail=f"{field_name} is required")
222
-
223
- normalized = prompt.strip()
224
-
225
- if not normalized:
226
- raise HTTPException(status_code=400, detail=f"{field_name} is required")
227
-
228
- return normalized
229
-
230
-
231
- def enforce_prompt_size(prompt: str, max_chars: int, max_bytes: int, context: str):
232
- char_len = len(prompt)
233
- byte_len = len(prompt.encode("utf-8"))
234
-
235
- if char_len > max_chars or byte_len > max_bytes:
236
- raise HTTPException(
237
- status_code=413,
238
- detail=(
239
- f"{context} is too large ({char_len} chars, {byte_len} bytes). "
240
- f"Max allowed is {max_chars} chars or {max_bytes} bytes."
241
- ),
242
- )
243
-
244
-
245
- def calculate_messages_size(messages: list) -> tuple[int, int]:
246
- total_chars = 0
247
- total_bytes = 0
248
-
249
- for message in messages:
250
- if not isinstance(message, dict):
251
- continue
252
-
253
- text = message_content_to_text(message.get("content"))
254
-
255
- if not text:
256
- continue
257
-
258
- total_chars += len(text)
259
- total_bytes += len(text.encode("utf-8"))
260
-
261
- return total_chars, total_bytes
262
-
263
-
264
- def extract_user_text(messages: list) -> str:
265
- return " ".join(
266
- message_content_to_text(m.get("content"))
267
- for m in messages
268
- if isinstance(m, dict) and m.get("role") == "user"
269
- ).lower()
270
-
271
-
272
- # -------------------------------------------------------------------
273
- # Usage / Rate Limit Internals
274
- # -------------------------------------------------------------------
275
-
276
- def get_usage_period_key(metric: str) -> str:
277
- now = time.gmtime()
278
-
279
- period = USAGE_PERIODS.get(metric, "daily")
280
-
281
- if period == "weekly":
282
- iso_year, iso_week, _ = time.strftime("%G %V %u", now).split(" ")
283
- return f"{iso_year}-W{iso_week}"
284
-
285
- return time.strftime("%Y-%m-%d", now)
286
-
287
-
288
- def get_usage_lock(metric: str, subject: str) -> asyncio.Lock:
289
- metric_locks = usage_locks.get(metric)
290
-
291
- if metric_locks is None:
292
- metric_locks = {}
293
- usage_locks[metric] = metric_locks
294
-
295
- lock = metric_locks.get(subject)
296
-
297
- if lock is None:
298
- lock = asyncio.Lock()
299
- metric_locks[subject] = lock
300
-
301
- return lock
302
-
303
-
304
- def get_usage_snapshot_for_subject(
305
- plan_key: str, subject: str
306
- ) -> Dict[str, Dict[str, Any]]:
307
- snapshot: Dict[str, Dict[str, Any]] = {}
308
-
309
- for metric in usage_store.keys():
310
- snapshot[metric] = get_metric_snapshot_for_subject(plan_key, subject, metric)
311
-
312
- return snapshot
313
-
314
-
315
- def get_metric_snapshot_for_subject(
316
- plan_key: str,
317
- subject: str,
318
- metric: str,
319
- ) -> Dict[str, Any]:
320
- subject_email = subject[5:] if subject.startswith("user:") else None
321
- limit = get_effective_limit_for_email(plan_key, metric, subject_email)
322
- window_key = get_usage_period_key(metric)
323
- entry = usage_store[metric].get(subject)
324
- used = 0
325
-
326
- if entry and entry.get("window") == window_key:
327
- used = max(0, int(entry.get("count", 0)))
328
-
329
- remaining = None if limit is None else max(0, int(limit) - used)
330
-
331
- return {
332
- "limit": limit,
333
- "used": used,
334
- "remaining": remaining,
335
- "window": window_key,
336
- "period": USAGE_PERIODS.get(metric, "daily"),
337
- }
338
-
339
-
340
- # -------------------------------------------------------------------
341
- # Identity / Client helpers
342
- # -------------------------------------------------------------------
343
-
344
- def sanitize_client_id(raw_client_id: Optional[str]) -> Optional[str]:
345
- if not isinstance(raw_client_id, str):
346
- return None
347
-
348
- trimmed = raw_client_id.strip()
349
-
350
- if not trimmed or len(trimmed) > MAX_CLIENT_ID_LENGTH:
351
- return None
352
-
353
- if not re.match(r"^[A-Za-z0-9._:-]+$", trimmed):
354
- return None
355
-
356
- return trimmed
357
-
358
-
359
- def build_default_subject(request: Request, client_id: Optional[str]) -> str:
360
- if client_id:
361
- client_hash = hashlib.sha256(client_id.encode("utf-8")).hexdigest()[:24]
362
- return f"client:{client_hash}"
363
-
364
- host = request.client.host if request.client else "unknown"
365
-
366
- user_agent = request.headers.get("user-agent", "")
367
-
368
- ua_hash = (
369
- hashlib.sha256(user_agent.encode("utf-8")).hexdigest()[:12]
370
- if user_agent
371
- else "noua"
372
- )
373
-
374
- return f"anon:{host}:{ua_hash}"
375
-
376
-
377
- def normalize_forwarded_ip(raw_ip: Optional[str]) -> Optional[str]:
378
- if not isinstance(raw_ip, str):
379
- return None
380
-
381
- candidate = raw_ip.split(",", 1)[0].strip()
382
-
383
- if not candidate:
384
- return None
385
-
386
- if candidate.startswith("[") and "]" in candidate:
387
- candidate = candidate[1 : candidate.index("]")]
388
- elif candidate.count(":") == 1 and "." in candidate:
389
- candidate = candidate.rsplit(":", 1)[0]
390
-
391
- return candidate or None
392
-
393
-
394
- def build_verify_token_ip_subject(ip: Optional[str]) -> str:
395
- normalized_ip = normalize_forwarded_ip(ip)
396
- if not normalized_ip:
397
- return "verify-ip:unknown"
398
-
399
- ip_hash = hashlib.sha256(normalized_ip.encode("utf-8")).hexdigest()[:24]
400
- return f"verify-ip:{ip_hash}"
401
-
402
-
403
- def bind_client_subject(client_id: Optional[str], subject: str, plan_key: str):
404
- if not client_id:
405
- return
406
-
407
- client_subject_bindings[client_id] = {
408
- "subject": subject,
409
- "plan_key": plan_key,
410
- "expires_at": time.time() + CLIENT_BIND_TTL_SECONDS,
411
- }
412
-
413
-
414
- def resolve_bound_subject(client_id: Optional[str], fallback_subject: str) -> str:
415
- if not client_id:
416
- return fallback_subject
417
-
418
- bound = client_subject_bindings.get(client_id)
419
-
420
- if not bound:
421
- return fallback_subject
422
-
423
- if bound.get("expires_at", 0) <= time.time():
424
- client_subject_bindings.pop(client_id, None)
425
- return fallback_subject
426
-
427
- return bound.get("subject", fallback_subject)
428
-
429
-
430
- # -------------------------------------------------------------------
431
- # Message parsing helpers
432
- # -------------------------------------------------------------------
433
-
434
- def message_content_to_text(content: Any) -> str:
435
- if isinstance(content, str):
436
- return content
437
-
438
- if isinstance(content, list):
439
- parts: List[str] = []
440
-
441
- for item in content:
442
- if isinstance(item, str):
443
- parts.append(item)
444
- continue
445
-
446
- if isinstance(item, dict):
447
- text = item.get("text")
448
- if isinstance(text, str):
449
- parts.append(text)
450
-
451
- nested_content = item.get("content")
452
- if nested_content is not None:
453
- nested_text = message_content_to_text(nested_content)
454
- if nested_text:
455
- parts.append(nested_text)
456
-
457
- arguments = item.get("arguments")
458
- if isinstance(arguments, str):
459
- parts.append(arguments)
460
-
461
- for key in (
462
- "function_call",
463
- "function",
464
- "tool_calls",
465
- "prompt",
466
- "input",
467
- "output",
468
- "message",
469
- "description",
470
- "caption",
471
- "query",
472
- "summary",
473
- ):
474
- nested_value = item.get(key)
475
- if nested_value is None:
476
- continue
477
- nested_text = message_content_to_text(nested_value)
478
- if nested_text:
479
- parts.append(nested_text)
480
-
481
- return " ".join(parts)
482
-
483
- if isinstance(content, dict):
484
- parts: List[str] = []
485
-
486
- text = content.get("text")
487
- if isinstance(text, str):
488
- parts.append(text)
489
-
490
- nested_content = content.get("content")
491
- if nested_content is not None:
492
- nested_text = message_content_to_text(nested_content)
493
- if nested_text:
494
- parts.append(nested_text)
495
-
496
- arguments = content.get("arguments")
497
- if isinstance(arguments, str):
498
- parts.append(arguments)
499
-
500
- for key in (
501
- "function_call",
502
- "function",
503
- "tool_calls",
504
- "prompt",
505
- "input",
506
- "output",
507
- "message",
508
- "description",
509
- "caption",
510
- "query",
511
- "summary",
512
- ):
513
- nested_value = content.get(key)
514
- if nested_value is None:
515
- continue
516
- nested_text = message_content_to_text(nested_value)
517
- if nested_text:
518
- parts.append(nested_text)
519
-
520
- return " ".join(parts)
521
-
522
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
helper/subscriptions.py DELETED
@@ -1,436 +0,0 @@
1
- import asyncio
2
- import hashlib
3
- import json
4
- import os
5
- from datetime import datetime, timezone
6
- from pathlib import Path
7
- import base64
8
- import psycopg
9
- from psycopg.rows import dict_row
10
- from supabase import Client, create_client
11
- import hmac
12
- ADMINS_PATH = Path("assets/admins.json")
13
- EXTRA_USAGE_PATH = Path("assets/extra_usage.json")
14
-
15
- try:
16
- ADMIN_EMAIL_MAP = json.loads(ADMINS_PATH.read_text())
17
- except Exception as e:
18
- print(f"Failed to load admins.json: {e}")
19
- ADMIN_EMAIL_MAP = {}
20
-
21
- try:
22
- EXTRA_USAGE_EMAIL_MAP = json.loads(EXTRA_USAGE_PATH.read_text())
23
- except Exception as e:
24
- print(f"Failed to load extra_usage.json: {e}")
25
- EXTRA_USAGE_EMAIL_MAP = {}
26
-
27
- PLAN_ORDER = ["free", "light", "core", "creator", "professional"]
28
- TIER_CONFIG = {
29
- "free": {
30
- "name": "Free Tier",
31
- "url": "",
32
- "price": "0.00",
33
- "limits": {
34
- "cloudChatDaily": 50,
35
- "imagesDaily": 10,
36
- "videosDaily": 3,
37
- "audioWeekly": 1,
38
- "verifyTokenWithEmailDaily": 100,
39
- },
40
- },
41
- "light": {
42
- "name": "InferencePort AI Light",
43
- "url": "https://buy.stripe.com/bJedR93gk4xw7EFeyabbG08",
44
- "price": "9.99",
45
- "limits": {
46
- "cloudChatDaily": None,
47
- "imagesDaily": 50,
48
- "videosDaily": 10,
49
- "audioWeekly": 5,
50
- "verifyTokenWithEmailDaily": 100,
51
- },
52
- },
53
- "core": {
54
- "name": "InferencePort AI Core",
55
- "url": "https://buy.stripe.com/28E4gzg365BA1gh61EbbG09",
56
- "price": "15.99",
57
- "limits": {
58
- "cloudChatDaily": None,
59
- "imagesDaily": 150,
60
- "videosDaily": 30,
61
- "audioWeekly": 25,
62
- "verifyTokenWithEmailDaily": 100,
63
- },
64
- },
65
- "creator": {
66
- "name": "InferencePort AI Creator",
67
- "url": "https://buy.stripe.com/dRmdR98AE5BA8IJ89MbbG0a",
68
- "price": "29.99",
69
- "limits": {
70
- "cloudChatDaily": None,
71
- "imagesDaily": 300,
72
- "videosDaily": 50,
73
- "audioWeekly": 45,
74
- "verifyTokenWithEmailDaily": 100,
75
- },
76
- },
77
- "professional": {
78
- "name": "InferencePort AI Professional",
79
- "url": "https://buy.stripe.com/14AaEX7wA3ts7EF2PsbbG0b",
80
- "price": "99.99",
81
- "limits": {
82
- "cloudChatDaily": None,
83
- "imagesDaily": None,
84
- "videosDaily": None,
85
- "audioWeekly": 75,
86
- "verifyTokenWithEmailDaily": 100,
87
- },
88
- },
89
- }
90
- USAGE_PERIODS = {
91
- "cloudChatDaily": "daily",
92
- "imagesDaily": "daily",
93
- "videosDaily": "daily",
94
- "audioWeekly": "weekly",
95
- "verifyTokenWithEmailDaily": "daily",
96
- }
97
- usage_store = {
98
- "cloudChatDaily": {},
99
- "imagesDaily": {},
100
- "videosDaily": {},
101
- "audioWeekly": {},
102
- "verifyTokenWithEmailDaily": {},
103
- }
104
- usage_locks = {
105
- "cloudChatDaily": {},
106
- "imagesDaily": {},
107
- "videosDaily": {},
108
- "audioWeekly": {},
109
- "verifyTokenWithEmailDaily": {},
110
- }
111
-
112
- conn = None
113
- conn_lock = asyncio.Lock()
114
-
115
- SUPABASE_URL = os.getenv("SUPABASE_URL")
116
- SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
117
- supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
118
- POSTGRE_SECRET = os.getenv("POSTGRE_SECRET")
119
-
120
- async def get_conn():
121
- global conn
122
-
123
- if conn is None or conn.closed:
124
- conn = await psycopg.AsyncConnection.connect(POSTGRE_SECRET, row_factory=dict_row, sslmode="verify-full", sslrootcert="prod-ca-2021.crt")
125
-
126
- return conn
127
-
128
- import psycopg
129
- from psycopg.rows import dict_row
130
-
131
- async def execute_query(query: str, params=(), *, fetchone=False, commit=False):
132
- async with conn_lock:
133
- conn = await get_conn()
134
-
135
- try:
136
- async with conn.cursor() as cur:
137
- await cur.execute(query, params)
138
-
139
- if cur.description is None:
140
- result = None if fetchone else []
141
- else:
142
- result = await cur.fetchone() if fetchone else await cur.fetchall()
143
-
144
- if commit:
145
- await conn.commit()
146
-
147
- return result
148
-
149
- except psycopg.OperationalError:
150
- # reconnect using ASYNC connection
151
- conn = await psycopg.AsyncConnection.connect(
152
- POSTGRE_SECRET,
153
- row_factory=dict_row,
154
- sslmode="verify-full",
155
- sslrootcert="prod-ca-2021.crt",
156
- )
157
- globals()["conn"] = conn
158
-
159
- async with conn.cursor() as cur:
160
- await cur.execute(query, params)
161
-
162
- if cur.description is None:
163
- result = None if fetchone else []
164
- else:
165
- result = await cur.fetchone() if fetchone else await cur.fetchall()
166
-
167
- if commit:
168
- await conn.commit()
169
-
170
- return result
171
-
172
- except psycopg.Error as e:
173
- await conn.rollback()
174
- raise e
175
-
176
- def normalize_plan_key(plan_name: str | None) -> str:
177
- if not plan_name:
178
- return "free"
179
- normalized = "".join(ch for ch in str(plan_name).lower() if ch.isalpha())
180
- if "professional" in normalized:
181
- return "professional"
182
- if "creator" in normalized:
183
- return "creator"
184
- if "core" in normalized:
185
- return "core"
186
- if "light" in normalized:
187
- return "light"
188
- return "free"
189
-
190
-
191
- def get_effective_limit_for_email(
192
- plan_key: str,
193
- metric: str,
194
- email: str | None = None,
195
- ):
196
- plan = TIER_CONFIG.get(plan_key) or TIER_CONFIG["free"]
197
- default_limit = plan.get("limits", {}).get(metric)
198
-
199
- if not isinstance(email, str) or not email.strip():
200
- return default_limit
201
-
202
- entry = EXTRA_USAGE_EMAIL_MAP.get(email.strip().lower())
203
- if not isinstance(entry, dict):
204
- return default_limit
205
-
206
- raw_limits = entry.get("limits") if isinstance(entry.get("limits"), dict) else entry
207
- override = raw_limits.get(metric) if isinstance(raw_limits, dict) else None
208
-
209
- if override is None and metric not in raw_limits:
210
- return default_limit
211
-
212
- if override is None:
213
- return None
214
-
215
- if isinstance(override, (int, float)) and override >= 0:
216
- return int(override)
217
-
218
- return default_limit
219
-
220
-
221
- async def fetch_subscription(jwt: str):
222
- identity = await resolve_token_identity(jwt)
223
- if "error" in identity:
224
- return identity
225
-
226
- email = identity["email"]
227
- signed_up = identity["signed_up"]
228
-
229
- if email in ADMIN_EMAIL_MAP:
230
- admin = ADMIN_EMAIL_MAP[email]
231
- print(f"[ADMIN OVERRIDE] {email} → forcing plan '{admin['plan_key']}'")
232
-
233
- subscription_obj = {
234
- "subscription_id": admin.get("subscription_id", "admin-override"),
235
- "status": admin.get("status", "active"),
236
- "current_period_end": None,
237
- "price_id": None,
238
- "product_name": admin.get("product_name"),
239
- "nickname": admin.get("nickname"),
240
- "plan_key": admin.get("plan_key"),
241
- }
242
-
243
- return {
244
- "email": email,
245
- "signed_up": signed_up,
246
- "subscription": [subscription_obj],
247
- "plan_key": admin["plan_key"],
248
- "auth_type": identity["auth_type"],
249
- }
250
-
251
- rows = await execute_query(
252
- """
253
- with cust as (
254
- select id
255
- from stripe.customers
256
- where email = %s
257
- ),
258
- subs as (
259
- select
260
- s.id as subscription_id,
261
- s.status,
262
- s.current_period_end,
263
- s.items->'data'->0->'price'->>'id' as price_id
264
- from stripe.subscriptions s
265
- join cust on s.customer = cust.id
266
- where s.status in ('active', 'trialing', 'past_due')
267
- )
268
- select
269
- subs.subscription_id,
270
- subs.status,
271
- subs.current_period_end,
272
- subs.price_id,
273
- prices.nickname,
274
- prices.product as product_id,
275
- products.name as product_name
276
- from subs
277
- left join stripe.prices prices
278
- on prices.id = subs.price_id
279
- left join stripe.products products
280
- on prices.product = products.id;
281
- """,
282
- (email,),
283
- )
284
-
285
- if not rows:
286
- return {
287
- "email": email,
288
- "signed_up": signed_up,
289
- "subscription": None,
290
- "plan_key": "free",
291
- "auth_type": identity["auth_type"],
292
- }
293
-
294
- subscriptions = []
295
- preferred_plan_key = "free"
296
-
297
- for row in rows:
298
- plan_key = normalize_plan_key(row["product_name"] or row["nickname"])
299
-
300
- subscriptions.append({
301
- "subscription_id": row["subscription_id"],
302
- "status": row["status"],
303
- "current_period_end": row["current_period_end"],
304
- "price_id": row["price_id"],
305
- "product_name": row["product_name"],
306
- "nickname": row["nickname"],
307
- "plan_key": plan_key,
308
- })
309
-
310
- if row["status"] in ("active", "trialing"):
311
- preferred_plan_key = plan_key
312
-
313
- return {
314
- "email": email,
315
- "signed_up": signed_up,
316
- "subscription": subscriptions,
317
- "plan_key": preferred_plan_key,
318
- "auth_type": identity["auth_type"],
319
- }
320
-
321
-
322
- from fastapi import HTTPException
323
-
324
- async def resolve_token_identity(token: str | None):
325
- if not token or (token == "public"):
326
- return None
327
-
328
- jwt_identity = resolve_jwt_identity(token)
329
- if jwt_identity is not None:
330
- return jwt_identity
331
-
332
- api_identity = await resolve_api_key_identity(token)
333
- if api_identity is not None:
334
- if "error" in api_identity:
335
- raise HTTPException(status_code=401, detail=api_identity["error"])
336
- return api_identity
337
-
338
- raise HTTPException(status_code=401, detail="Invalid API key or token")
339
-
340
-
341
-
342
- def resolve_jwt_identity(token: str):
343
- try:
344
- auth_res = supabase.auth.get_user(token)
345
- except Exception:
346
- return None
347
-
348
- user = getattr(auth_res, "user", None)
349
- email = getattr(user, "email", None)
350
- created_at = getattr(user, "created_at", None)
351
- user_id = getattr(user, "id", None)
352
- if user is None or not isinstance(email, str) or not email.strip():
353
- return None
354
- return {
355
- "auth_type": "jwt",
356
- "user_id": user_id,
357
- "email": email.strip().lower(),
358
- "signed_up": created_at.isoformat() if hasattr(created_at, "isoformat") else None,
359
- }
360
-
361
- def verify_lightning_api_key(secret: str, stored: str) -> bool:
362
- _, salt_b64, key_b64 = stored.split("$")
363
-
364
- salt = base64.urlsafe_b64decode(salt_b64 + "==")
365
- stored_key = base64.urlsafe_b64decode(key_b64 + "==")
366
-
367
- derived_key = hashlib.scrypt(
368
- secret.encode("utf-8"),
369
- salt=salt,
370
- n=16384, r=8, p=1,
371
- dklen=32
372
- )
373
-
374
- return hmac.compare_digest(derived_key, stored_key)
375
-
376
- async def resolve_api_key_identity(token: str | None):
377
- if token is None or token.strip() == "":
378
- return None
379
-
380
- prefix = token[:16]
381
-
382
- row = await execute_query(
383
- """
384
- select
385
- k.id,
386
- k.user_id,
387
- k.name,
388
- k.expires_at,
389
- k.revoked_at,
390
- k.key_hash,
391
- u.email,
392
- u.created_at as user_created_at
393
- from public.lightning_api_keys k
394
- join auth.users u on u.id = k.user_id
395
- where k.key_prefix = %s
396
- limit 1;
397
- """,
398
- (prefix,),
399
- fetchone=True,
400
- )
401
-
402
- if not row:
403
- raise HTTPException(status_code=401, detail="Invalid API key or token")
404
-
405
- if not verify_lightning_api_key(token, row["key_hash"]):
406
- raise HTTPException(status_code=401, detail="Invalid API key or token")
407
-
408
- if row.get("revoked_at") is not None:
409
- raise HTTPException(status_code=401, detail="Invalid API key or token")
410
-
411
- expires_at = row.get("expires_at")
412
- if expires_at is not None and expires_at <= datetime.now(timezone.utc):
413
- raise HTTPException(status_code=401, detail="Invalid API key or token")
414
-
415
- email = row.get("email")
416
- if not isinstance(email, str) or not email.strip():
417
- raise HTTPException(status_code=401, detail="Invalid API key or token")
418
-
419
- await execute_query(
420
- """
421
- update public.lightning_api_keys
422
- set last_used_at = timezone('utc', now())
423
- where id = %s;
424
- """,
425
- (row["id"],),
426
- commit=True,
427
- )
428
-
429
- return {
430
- "auth_type": "api_key",
431
- "user_id": str(row["user_id"]) if row["user_id"] is not None else None,
432
- "email": email.strip().lower(),
433
- "signed_up": row["user_created_at"].isoformat()
434
- if hasattr(row["user_created_at"], "isoformat")
435
- else None,
436
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models_override.json DELETED
@@ -1,39 +0,0 @@
1
- {
2
- "lightning": {
3
- "name": "Lightning",
4
- "context_length": 128000,
5
- "max_output_length": 32000,
6
- "pricing_per_million_tokens": 0.75,
7
- "input_modalities": ["text", "image", "file"],
8
- "output_modalities": ["text"],
9
- "supported_features": ["tools", "json_mode", "streaming", "structured_outputs"]
10
- },
11
- "overrides": [
12
- {
13
- "id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
14
- "name": "NVIDIA: Nemotron 3 Nano Omni 30B A3B Reasoning",
15
- "api_url": "https://api.aimlapi.com/v1/chat/completions",
16
- "api_key_env": "AIML_KEY",
17
- "allowed_in": ["gen", "payg"],
18
- "context_length": 128000,
19
- "max_output_length": 32000,
20
- "input_modalities": ["text"],
21
- "output_modalities": ["text"],
22
- "supported_features": ["tools", "json_mode", "streaming"],
23
- "pricing_per_million_tokens": 0.04
24
- },
25
- {
26
- "id": "baidu/ernie-4-5-0-3b",
27
- "name": "Baidu: ERNIE 4.5 0 3B",
28
- "api_url": "https://api.aimlapi.com/v1/chat/completions",
29
- "api_key_env": "AIML_KEY",
30
- "allowed_in": ["gen", "payg"],
31
- "context_length": 128000,
32
- "max_output_length": 32000,
33
- "input_modalities": ["text"],
34
- "output_modalities": ["text"],
35
- "supported_features": ["tools", "json_mode", "streaming"],
36
- "pricing_per_million_tokens": 0.04
37
- }
38
- ]
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
payg/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- from fastapi import APIRouter
2
-
3
- router = APIRouter(prefix="/v1")
4
-
5
- from . import stripe, account, chat, media
 
 
 
 
 
 
payg/account.py DELETED
@@ -1,208 +0,0 @@
1
- import time
2
- import uuid
3
- from typing import Any, Dict, Optional
4
- from fastapi import Header, HTTPException, Query, Request
5
- from fastapi.responses import JSONResponse
6
- from helper.payg import (
7
- get_payg_config,
8
- get_wallet_snapshot,
9
- list_credit_ledger,
10
- build_usage_summary_from_ledger_rows,
11
- require_payg_identity,
12
- )
13
- from helper.subscriptions import execute_query
14
- from . import router
15
- from .common import (
16
- build_public_config,
17
- normalize_lightning_api_key_row,
18
- build_lightning_api_key_secret,
19
- build_lightning_api_key_prefix,
20
- hash_lightning_api_key,
21
- parse_lightning_api_key_expiry,
22
- trimmed_string,
23
- )
24
-
25
-
26
- @router.get("/config")
27
- async def v1_get_public_config():
28
- config = await get_payg_config()
29
- return JSONResponse(status_code=200, content=build_public_config(config))
30
-
31
-
32
- @router.get("/models")
33
- async def v1_get_models():
34
- config = await get_payg_config()
35
- from gen.override import build_all_models
36
- return JSONResponse(status_code=200, content={
37
- "object": "list",
38
- "data": build_all_models(),
39
- })
40
-
41
-
42
- @router.get("/me")
43
- async def v1_get_me(authorization: Optional[str] = Header(None)):
44
- identity = await require_payg_identity(authorization)
45
- wallet = await get_wallet_snapshot(identity["user_id"])
46
- ledger = await list_credit_ledger(identity["user_id"], limit=200)
47
- usage_summary = build_usage_summary_from_ledger_rows(ledger)
48
-
49
- return JSONResponse(
50
- status_code=200,
51
- content={
52
- "identity": {
53
- "email": identity["email"],
54
- "auth_type": identity["auth_type"],
55
- "user_id": identity["user_id"],
56
- },
57
- "wallet": wallet,
58
- "usage_summary": usage_summary,
59
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
60
- },
61
- )
62
-
63
-
64
- @router.get("/credits/ledger")
65
- async def v1_get_credit_ledger(
66
- authorization: Optional[str] = Header(None),
67
- limit: int = Query(default=50, ge=1, le=500),
68
- ):
69
- identity = await require_payg_identity(authorization)
70
- ledger = await list_credit_ledger(identity["user_id"], limit=limit)
71
- return {"entries": ledger}
72
-
73
-
74
- @router.get("/lightning-api-keys")
75
- async def v1_list_lightning_api_keys(authorization: Optional[str] = Header(None)):
76
- identity = await require_payg_identity(authorization)
77
- rows = await execute_query(
78
- """
79
- select
80
- id,
81
- name,
82
- key_prefix,
83
- created_at,
84
- last_used_at,
85
- expires_at,
86
- revoked_at
87
- from public.lightning_api_keys
88
- where user_id = %s
89
- order by created_at desc;
90
- """,
91
- (identity["user_id"],),
92
- )
93
-
94
- items = []
95
- for row in rows or []:
96
- normalized = normalize_lightning_api_key_row(row or {})
97
- if normalized:
98
- items.append(normalized)
99
-
100
- return JSONResponse(status_code=200, content={"items": items})
101
-
102
-
103
- @router.post("/lightning-api-keys")
104
- async def v1_create_lightning_api_key(
105
- request: Request,
106
- authorization: Optional[str] = Header(None),
107
- ):
108
- identity = await require_payg_identity(authorization)
109
-
110
- try:
111
- body = await request.json()
112
- except Exception:
113
- raise HTTPException(status_code=400, detail="Invalid JSON body")
114
-
115
- if not isinstance(body, dict):
116
- raise HTTPException(status_code=400, detail="Invalid JSON body")
117
-
118
- name = trimmed_string(body.get("name"))
119
- if not name:
120
- raise HTTPException(status_code=400, detail="API key name is required")
121
- if len(name) > 64:
122
- raise HTTPException(status_code=400, detail="API key name must be 64 characters or fewer")
123
-
124
- expires_at = parse_lightning_api_key_expiry(body.get("expiresAt"))
125
- raw_key = build_lightning_api_key_secret()
126
- key_hash = hash_lightning_api_key(raw_key)
127
- key_prefix = build_lightning_api_key_prefix(raw_key)
128
-
129
- key_id = str(uuid.uuid4())
130
-
131
- row = await execute_query(
132
- """
133
- insert into public.lightning_api_keys (
134
- id,
135
- user_id,
136
- name,
137
- key_hash,
138
- key_prefix,
139
- expires_at
140
- )
141
- values (%s, %s, %s, %s, %s, %s)
142
- returning
143
- id,
144
- name,
145
- key_prefix,
146
- created_at,
147
- last_used_at,
148
- expires_at,
149
- revoked_at;
150
- """,
151
- (
152
- key_id,
153
- identity["user_id"],
154
- name,
155
- key_hash,
156
- key_prefix,
157
- expires_at,
158
- ),
159
- fetchone=True,
160
- commit=True,
161
- )
162
-
163
- normalized = normalize_lightning_api_key_row(row or {})
164
- if not normalized:
165
- raise HTTPException(status_code=500, detail="Created API key response was invalid")
166
-
167
- return JSONResponse(status_code=200, content={"apiKey": normalized, "rawKey": raw_key})
168
-
169
-
170
- @router.delete("/lightning-api-keys/{key_id}")
171
- async def v1_revoke_lightning_api_key(
172
- key_id: str,
173
- authorization: Optional[str] = Header(None),
174
- ):
175
- identity = await require_payg_identity(authorization)
176
- key_id = trimmed_string(key_id)
177
- if not key_id:
178
- raise HTTPException(status_code=400, detail="API key id is required")
179
-
180
- row = await execute_query(
181
- """
182
- update public.lightning_api_keys
183
- set revoked_at = timezone('utc', now())
184
- where id = %s
185
- and user_id = %s
186
- and revoked_at is null
187
- returning
188
- id,
189
- name,
190
- key_prefix,
191
- created_at,
192
- last_used_at,
193
- expires_at,
194
- revoked_at;
195
- """,
196
- (key_id, identity["user_id"]),
197
- fetchone=True,
198
- commit=True,
199
- )
200
-
201
- if not row:
202
- raise HTTPException(status_code=404, detail="API key not found or already revoked")
203
-
204
- normalized = normalize_lightning_api_key_row(row or {})
205
- if not normalized:
206
- raise HTTPException(status_code=500, detail="Updated API key response was invalid")
207
-
208
- return JSONResponse(status_code=200, content={"success": True, "apiKey": normalized})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
payg/chat.py DELETED
@@ -1,292 +0,0 @@
1
- import json
2
- import uuid
3
- from decimal import Decimal, ROUND_HALF_UP
4
- from typing import Any, Dict, Optional
5
- import httpx
6
- from fastapi import Header, HTTPException, Request
7
- from fastapi.responses import JSONResponse, StreamingResponse
8
- from helper.payg import (
9
- append_credit_ledger_entry,
10
- calculate_text_credit_cost,
11
- enforce_payg_spam_limit,
12
- estimate_text_tokens_from_messages,
13
- estimate_text_tokens_from_text,
14
- extract_usage_breakdown,
15
- get_payg_config,
16
- get_wallet_snapshot,
17
- require_payg_identity,
18
- )
19
- from gen.override import get_override, is_allowed_in
20
- from . import router
21
- from .common import (
22
- INTERNAL_API_BASE,
23
- count_input_images,
24
- build_upstream_headers,
25
- get_minimum_text_balance,
26
- extract_stream_usage_and_text,
27
- extract_completion_text_from_payload,
28
- stringify_uuids,
29
- )
30
-
31
-
32
- def _get_override_pricing(model_id: str) -> Decimal:
33
- entry = get_override(model_id)
34
- if entry:
35
- ppm = entry.get("pricing_per_million_tokens", 0)
36
- try:
37
- return Decimal(str(ppm))
38
- except Exception:
39
- pass
40
- return Decimal("0")
41
-
42
-
43
- def quantize_override_cost(tokens: int, ppm: Decimal) -> Decimal:
44
- cost = (Decimal(str(tokens)) / Decimal("1000000")) * ppm
45
- return cost.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
46
-
47
-
48
- @router.post("/chat/completions")
49
- async def v1_chat_completions(
50
- request: Request,
51
- authorization: Optional[str] = Header(None),
52
- x_client_id: Optional[str] = Header(None),
53
- ):
54
- identity = await require_payg_identity(authorization)
55
- config = await get_payg_config()
56
-
57
- limit = int(config.get("limits", {}).get("requestsPerMinutePerIdentity", 120))
58
- await enforce_payg_spam_limit(f"{identity['user_id']}:text", max(1, limit))
59
-
60
- wallet = await get_wallet_snapshot(identity["user_id"])
61
- minimum_balance = get_minimum_text_balance(config)
62
- current_balance = Decimal(str(wallet.get("balance_credits") or 0))
63
- if current_balance < minimum_balance:
64
- raise HTTPException(
65
- status_code=402,
66
- detail=f"At least ${minimum_balance} in credits is required for text generation.",
67
- )
68
-
69
- body = await request.json()
70
- if not isinstance(body, dict):
71
- raise HTTPException(status_code=400, detail="Invalid JSON body")
72
-
73
- requested_model = body.get("model")
74
- override_entry = get_override(requested_model) if requested_model else None
75
- is_override = override_entry is not None and is_allowed_in(requested_model, "payg")
76
- override_ppm = _get_override_pricing(requested_model) if is_override else Decimal("0")
77
-
78
- estimated_input_tokens = estimate_text_tokens_from_messages(body.get("messages"))
79
- image_count = count_input_images(body.get("messages"))
80
- estimated_input_tokens += 15000 * image_count
81
-
82
- estimated_output_tokens = 0
83
- max_tokens_value = body.get("max_completion_tokens")
84
- if max_tokens_value is None:
85
- max_tokens_value = body.get("max_tokens")
86
- try:
87
- if max_tokens_value is not None:
88
- estimated_output_tokens = max(0, int(max_tokens_value))
89
- except Exception:
90
- estimated_output_tokens = 0
91
-
92
- estimated_total_tokens = estimated_input_tokens + estimated_output_tokens
93
-
94
- if is_override:
95
- estimated_total_cost = quantize_override_cost(estimated_total_tokens, override_ppm)
96
- else:
97
- estimated_total_cost = calculate_text_credit_cost(estimated_total_tokens, config)
98
- if estimated_output_tokens <= 0:
99
- estimated_total_cost = calculate_text_credit_cost(estimated_input_tokens, config)
100
-
101
- if current_balance < max(minimum_balance, estimated_total_cost):
102
- raise HTTPException(
103
- status_code=402,
104
- detail=(
105
- "Insufficient credits for this text request. "
106
- f"Current balance is {current_balance:.2f} credits."
107
- ),
108
- )
109
-
110
- stream = bool(body.get("stream"))
111
- if stream:
112
- body["stream"] = True
113
- stream_options = body.get("stream_options")
114
- if not isinstance(stream_options, dict):
115
- stream_options = {}
116
- stream_options["include_usage"] = True
117
- body["stream_options"] = stream_options
118
-
119
- async def settle_text_usage(
120
- *,
121
- input_tokens: int,
122
- output_tokens: int,
123
- source: str,
124
- payload: Optional[Dict[str, Any]] = None,
125
- metadata: Optional[Dict[str, Any]] = None,
126
- ) -> Dict[str, Any]:
127
- input_tokens = max(0, int(input_tokens))
128
- output_tokens = max(0, int(output_tokens))
129
- total_tokens = input_tokens + output_tokens
130
-
131
- if is_override:
132
- final_cost = quantize_override_cost(total_tokens, override_ppm)
133
- else:
134
- final_cost = calculate_text_credit_cost(total_tokens, config)
135
-
136
- usage_key = f"text:{uuid.uuid4().hex}"
137
- entry_metadata = {
138
- "input_tokens": input_tokens,
139
- "output_tokens": output_tokens,
140
- "total_tokens": total_tokens,
141
- "stream": stream,
142
- "is_override": is_override,
143
- "override_model": requested_model if is_override else None,
144
- }
145
- if isinstance(metadata, dict):
146
- entry_metadata.update(metadata)
147
-
148
- ledger_entry = await append_credit_ledger_entry(
149
- user_id=identity["user_id"],
150
- delta_credits=-final_cost,
151
- entry_type="usage",
152
- usage_kind="text",
153
- units=float(total_tokens),
154
- unit_label="tokens",
155
- source=source,
156
- source_ref=usage_key,
157
- idempotency_key=usage_key,
158
- metadata=entry_metadata,
159
- )
160
- ledger_entry = stringify_uuids(ledger_entry)
161
-
162
- if isinstance(payload, dict):
163
- payload.setdefault("usage", {})
164
- payload["usage"]["payg_input_tokens"] = input_tokens
165
- payload["usage"]["payg_output_tokens"] = output_tokens
166
- payload["usage"]["payg_total_tokens"] = total_tokens
167
- payload["usage"]["payg_credits_charged"] = float(final_cost)
168
- payload["usage"]["payg_ledger_entry"] = stringify_uuids(ledger_entry)
169
- return payload or {
170
- "usage": {
171
- "payg_input_tokens": input_tokens,
172
- "payg_output_tokens": output_tokens,
173
- "payg_total_tokens": total_tokens,
174
- "payg_credits_charged": float(final_cost),
175
- "payg_ledger_entry": ledger_entry,
176
- }
177
- }
178
-
179
- try:
180
- if stream:
181
- async def event_generator():
182
- completion_text = []
183
- seen_usage: Optional[Dict[str, Any]] = None
184
-
185
- try:
186
- async with httpx.AsyncClient(timeout=None) as client:
187
- async with client.stream(
188
- "POST",
189
- f"{INTERNAL_API_BASE}/gen/chat/completions",
190
- json=body,
191
- headers=build_upstream_headers(authorization, x_client_id),
192
- ) as upstream:
193
- if upstream.status_code >= 400:
194
- error_text = await upstream.aread()
195
- error_body = error_text.decode("utf-8", errors="replace")
196
- yield (
197
- f'data: {json.dumps({"error": "Upstream request failed", "detail": error_body[:1000]})}\n\n'
198
- )
199
- return
200
-
201
- async for line in upstream.aiter_lines():
202
- if not line:
203
- yield "\n"
204
- continue
205
-
206
- collected_text, usage, is_done = extract_stream_usage_and_text(line)
207
- if collected_text:
208
- completion_text.append(collected_text)
209
- if usage:
210
- seen_usage = usage
211
-
212
- yield line + "\n"
213
-
214
- if is_done:
215
- break
216
-
217
- usage_payload = {"usage": seen_usage} if isinstance(seen_usage, dict) else {}
218
- breakdown = extract_usage_breakdown(
219
- usage_payload,
220
- estimated_input_tokens,
221
- estimate_text_tokens_from_text("".join(completion_text)),
222
- )
223
- final_payload = await settle_text_usage(
224
- input_tokens=breakdown["input_tokens"],
225
- output_tokens=breakdown["output_tokens"],
226
- source="v1_chat_stream",
227
- metadata={
228
- "usage_source": "stream",
229
- "estimated_input_tokens": estimated_input_tokens,
230
- "estimated_output_tokens": estimated_output_tokens,
231
- },
232
- )
233
- yield f"event: payg-usage\ndata: {json.dumps(final_payload.get('usage', {}))}\n\n"
234
- except Exception as exc:
235
- yield f'data: {json.dumps({"error": "Text generation failed", "detail": str(exc)[:500]})}\n\n'
236
-
237
- return StreamingResponse(
238
- event_generator(),
239
- media_type="text/event-stream",
240
- headers={
241
- "Cache-Control": "no-cache",
242
- "Connection": "keep-alive",
243
- "X-Accel-Buffering": "no",
244
- },
245
- )
246
-
247
- async with httpx.AsyncClient(timeout=120) as client:
248
- upstream = await client.post(
249
- f"{INTERNAL_API_BASE}/gen/chat/completions",
250
- json=body,
251
- headers=build_upstream_headers(authorization, x_client_id),
252
- )
253
-
254
- if upstream.status_code >= 400:
255
- content_type = (upstream.headers.get("content-type") or "").lower()
256
- if "application/json" in content_type:
257
- return JSONResponse(status_code=upstream.status_code, content=upstream.json())
258
- return JSONResponse(
259
- status_code=upstream.status_code,
260
- content={"error": "Upstream request failed", "detail": upstream.text[:1000]},
261
- )
262
-
263
- payload = upstream.json()
264
- breakdown = extract_usage_breakdown(
265
- payload,
266
- estimated_input_tokens,
267
- estimate_text_tokens_from_text(extract_completion_text_from_payload(payload)),
268
- )
269
-
270
- if breakdown["output_tokens"] <= 0:
271
- breakdown["output_tokens"] = estimate_text_tokens_from_text(
272
- extract_completion_text_from_payload(payload)
273
- )
274
- breakdown["total_tokens"] = breakdown["input_tokens"] + breakdown["output_tokens"]
275
-
276
- final_payload = await settle_text_usage(
277
- input_tokens=breakdown["input_tokens"],
278
- output_tokens=breakdown["output_tokens"],
279
- source="v1_chat",
280
- payload=payload,
281
- metadata={
282
- "usage_source": "non_stream",
283
- "estimated_input_tokens": estimated_input_tokens,
284
- "estimated_output_tokens": estimated_output_tokens,
285
- },
286
- )
287
- return JSONResponse(status_code=upstream.status_code, content=final_payload)
288
-
289
- except HTTPException:
290
- raise
291
- except Exception as exc:
292
- raise HTTPException(status_code=502, detail=f"Text generation failed: {str(exc)[:300]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
payg/common.py DELETED
@@ -1,316 +0,0 @@
1
- import base64
2
- import hashlib
3
- import json
4
- import secrets
5
- import time
6
- import uuid
7
- from datetime import datetime, timezone
8
- from decimal import Decimal
9
- from typing import Any, Dict, Optional
10
- import os
11
- from fastapi import HTTPException
12
-
13
-
14
- INTERNAL_API_BASE = os.getenv("INTERNAL_API_BASE", "http://127.0.0.1:7860")
15
- STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY", "")
16
- WEBHOOK_KEY = os.getenv("WEBHOOK_KEY", "")
17
-
18
-
19
- def stringify_uuids(obj):
20
- if isinstance(obj, uuid.UUID):
21
- return str(obj)
22
- if isinstance(obj, dict):
23
- return {k: stringify_uuids(v) for k, v in obj.items()}
24
- if isinstance(obj, list):
25
- return [stringify_uuids(v) for v in obj]
26
- return obj
27
-
28
-
29
- def count_input_images(body: Any) -> int:
30
- if not isinstance(body, dict):
31
- return 0
32
-
33
- messages = body.get("messages")
34
- count = 0
35
-
36
- if isinstance(messages, list):
37
- for msg in messages:
38
- content = msg.get("content")
39
-
40
- if isinstance(content, list):
41
- for part in content:
42
- if not isinstance(part, dict):
43
- continue
44
-
45
- t = part.get("type")
46
-
47
- if t in ("image_url", "image"):
48
- count += 1
49
-
50
- if t == "image_urls":
51
- urls = part.get("image_urls")
52
- if isinstance(urls, list):
53
- count += len(urls)
54
-
55
- elif isinstance(content, dict):
56
- t = content.get("type")
57
-
58
- if t in ("image_url", "image"):
59
- count += 1
60
-
61
- if t == "image_urls":
62
- urls = content.get("image_urls")
63
- if isinstance(urls, list):
64
- count += len(urls)
65
-
66
- top_level_urls = body.get("image_urls")
67
- if isinstance(top_level_urls, list):
68
- count += len(top_level_urls)
69
-
70
- return count
71
-
72
-
73
- def build_public_config(config: Dict[str, Any]) -> Dict[str, Any]:
74
- return {
75
- "dashboard": config.get("dashboard", {}),
76
- "supabase": config.get("supabase", {}),
77
- "billing": config.get("billing", {}),
78
- "pricing": config.get("pricing", {}),
79
- "models": config.get("models", []),
80
- "notices": [
81
- notice
82
- for notice in config.get("notices", [])
83
- if isinstance(notice, dict) and notice.get("active") is not False
84
- ],
85
- "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
86
- }
87
-
88
-
89
- def trimmed_string(value: Any) -> Optional[str]:
90
- if value is None:
91
- return None
92
-
93
- if isinstance(value, uuid.UUID):
94
- return str(value)
95
-
96
- if not isinstance(value, str):
97
- return None
98
-
99
- trimmed = value.strip()
100
- return trimmed or None
101
-
102
-
103
- def format_iso_datetime(value: Any) -> Optional[str]:
104
- if value is None:
105
- return None
106
-
107
- if isinstance(value, str):
108
- return (
109
- value
110
- .replace("+00:00", "Z")
111
- .replace("+00", "Z")
112
- )
113
-
114
- if hasattr(value, "isoformat"):
115
- try:
116
- return value.isoformat().replace("+00:00", "Z")
117
- except Exception:
118
- return None
119
-
120
- return None
121
-
122
-
123
- def normalize_lightning_api_key_row(row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
124
- key_id = trimmed_string(row.get("id"))
125
- name = trimmed_string(row.get("name"))
126
- key_prefix = trimmed_string(row.get("key_prefix"))
127
- created_at = format_iso_datetime(row.get("created_at"))
128
- if not key_id or not name or not key_prefix or not created_at:
129
- return None
130
-
131
- last_used_at = format_iso_datetime(row.get("last_used_at"))
132
- expires_at = format_iso_datetime(row.get("expires_at"))
133
- revoked_at = format_iso_datetime(row.get("revoked_at"))
134
- is_expired = False
135
-
136
- if revoked_at is None and expires_at:
137
- try:
138
- expires_dt = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
139
- is_expired = expires_dt <= datetime.now(timezone.utc)
140
- except Exception:
141
- is_expired = False
142
-
143
- return {
144
- "id": key_id,
145
- "name": name,
146
- "keyPrefix": key_prefix,
147
- "createdAt": created_at,
148
- "lastUsedAt": last_used_at,
149
- "expiresAt": expires_at,
150
- "revokedAt": revoked_at,
151
- "isRevoked": revoked_at is not None,
152
- "isExpired": is_expired,
153
- }
154
-
155
-
156
- def build_lightning_api_key_secret() -> str:
157
- secret = base64.urlsafe_b64encode(secrets.token_bytes(24)).decode("ascii").rstrip("=")
158
- return f"ipa_live_{secret}"
159
-
160
-
161
- def build_lightning_api_key_prefix(secret: str) -> str:
162
- return secret[:16]
163
-
164
-
165
- def hash_lightning_api_key(secret: str) -> str:
166
- salt = secrets.token_bytes(16)
167
- derived_key = hashlib.scrypt(
168
- secret.encode("utf-8"),
169
- salt=salt,
170
- n=16384,
171
- r=8,
172
- p=1,
173
- dklen=32,
174
- )
175
- salt_b64 = base64.urlsafe_b64encode(salt).decode("ascii").rstrip("=")
176
- key_b64 = base64.urlsafe_b64encode(derived_key).decode("ascii").rstrip("=")
177
- return f"scrypt${salt_b64}${key_b64}"
178
-
179
-
180
- def parse_lightning_api_key_expiry(value: Any) -> Optional[str]:
181
- trimmed = trimmed_string(value)
182
- if not trimmed:
183
- return None
184
-
185
- normalized = trimmed.replace("Z", "+00:00")
186
- try:
187
- parsed = datetime.fromisoformat(normalized)
188
- except Exception as exc:
189
- raise HTTPException(
190
- status_code=400,
191
- detail="Expiration date must be a valid ISO timestamp",
192
- ) from exc
193
-
194
- if parsed.tzinfo is None:
195
- parsed = parsed.replace(tzinfo=timezone.utc)
196
-
197
- return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
198
-
199
-
200
- def build_upstream_headers(authorization: str, x_client_id: Optional[str]) -> Dict[str, str]:
201
- headers = {
202
- "Authorization": authorization,
203
- "x-payg-internal": WEBHOOK_KEY,
204
- }
205
- if x_client_id:
206
- headers["x-client-id"] = x_client_id
207
- return headers
208
-
209
-
210
- def get_minimum_text_balance(config: Dict[str, Any]) -> Decimal:
211
- raw_value = config.get("limits", {}).get("minimumTextCreditsBalance", 10)
212
- try:
213
- return Decimal(str(raw_value))
214
- except Exception:
215
- return Decimal("10")
216
-
217
-
218
- def extract_stream_usage_and_text(line: str) -> tuple[str, Optional[Dict[str, Any]], bool]:
219
- if not isinstance(line, str) or not line.startswith("data:"):
220
- return "", None, False
221
-
222
- data = line[5:].strip()
223
- if not data:
224
- return "", None, False
225
-
226
- if data == "[DONE]":
227
- return "", None, True
228
-
229
- try:
230
- payload = json.loads(data)
231
- except Exception:
232
- return "", None, False
233
-
234
- if not isinstance(payload, dict):
235
- return "", None, False
236
-
237
- usage = payload.get("usage") if isinstance(payload.get("usage"), dict) else None
238
- collected_text = []
239
-
240
- choices = payload.get("choices")
241
- if isinstance(choices, list):
242
- for choice in choices:
243
- if not isinstance(choice, dict):
244
- continue
245
-
246
- delta = choice.get("delta")
247
- if isinstance(delta, dict):
248
- content = delta.get("content")
249
- if isinstance(content, str):
250
- collected_text.append(content)
251
- elif isinstance(content, list):
252
- for item in content:
253
- if isinstance(item, str):
254
- collected_text.append(item)
255
- elif isinstance(item, dict):
256
- text = item.get("text")
257
- if isinstance(text, str):
258
- collected_text.append(text)
259
-
260
- message = choice.get("message")
261
- if isinstance(message, dict):
262
- content = message.get("content")
263
- if isinstance(content, str):
264
- collected_text.append(content)
265
- elif isinstance(content, list):
266
- for item in content:
267
- if isinstance(item, str):
268
- collected_text.append(item)
269
- elif isinstance(item, dict):
270
- text = item.get("text")
271
- if isinstance(text, str):
272
- collected_text.append(text)
273
-
274
- return "".join(collected_text), usage, False
275
-
276
-
277
- def extract_completion_text_from_payload(payload: Dict[str, Any]) -> str:
278
- if not isinstance(payload, dict):
279
- return ""
280
-
281
- collected_text = []
282
- choices = payload.get("choices")
283
- if isinstance(choices, list):
284
- for choice in choices:
285
- if not isinstance(choice, dict):
286
- continue
287
-
288
- message = choice.get("message")
289
- if isinstance(message, dict):
290
- content = message.get("content")
291
- if isinstance(content, str):
292
- collected_text.append(content)
293
- elif isinstance(content, list):
294
- for item in content:
295
- if isinstance(item, str):
296
- collected_text.append(item)
297
- elif isinstance(item, dict):
298
- text = item.get("text")
299
- if isinstance(text, str):
300
- collected_text.append(text)
301
-
302
- delta = choice.get("delta")
303
- if isinstance(delta, dict):
304
- content = delta.get("content")
305
- if isinstance(content, str):
306
- collected_text.append(content)
307
- elif isinstance(content, list):
308
- for item in content:
309
- if isinstance(item, str):
310
- collected_text.append(item)
311
- elif isinstance(item, dict):
312
- text = item.get("text")
313
- if isinstance(text, str):
314
- collected_text.append(text)
315
-
316
- return "".join(collected_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
payg/media.py DELETED
@@ -1,307 +0,0 @@
1
- import base64
2
- import json
3
- import time
4
- import uuid
5
- from typing import Optional
6
- import httpx
7
- from fastapi import Header, HTTPException, Request
8
- from fastapi.responses import JSONResponse, Response
9
- from helper.payg import (
10
- append_credit_ledger_entry,
11
- calculate_audio_credit_cost,
12
- calculate_image_credit_cost,
13
- calculate_video_credit_cost,
14
- enforce_payg_spam_limit,
15
- get_payg_config,
16
- require_payg_identity,
17
- )
18
- from . import router
19
- from .common import INTERNAL_API_BASE, build_upstream_headers
20
-
21
-
22
- @router.post("/images/generations")
23
- async def v1_images_generations(
24
- request: Request,
25
- authorization: Optional[str] = Header(None),
26
- x_client_id: Optional[str] = Header(None),
27
- ):
28
- identity = await require_payg_identity(authorization)
29
- config = await get_payg_config()
30
-
31
- limit = int(config.get("limits", {}).get("requestsPerMinutePerIdentity", 120))
32
- await enforce_payg_spam_limit(f"{identity['user_id']}:image", max(1, limit))
33
-
34
- body = await request.json()
35
- if not isinstance(body, dict):
36
- raise HTTPException(status_code=400, detail="Invalid JSON body")
37
-
38
- n = int(body.get("n") or 1)
39
- n = max(1, min(n, 4))
40
-
41
- cost = calculate_image_credit_cost(n, config)
42
- usage_key = f"usage:image:{uuid.uuid4().hex}"
43
-
44
- await append_credit_ledger_entry(
45
- user_id=identity["user_id"],
46
- delta_credits=-cost,
47
- entry_type="usage",
48
- usage_kind="image",
49
- units=float(n),
50
- unit_label="images",
51
- source="v1_image",
52
- source_ref=usage_key,
53
- idempotency_key=usage_key,
54
- metadata={"count": n},
55
- )
56
-
57
- prompt = body.get("prompt")
58
- if not isinstance(prompt, str) or not prompt.strip():
59
- raise HTTPException(status_code=400, detail="prompt is required")
60
-
61
- payload = {
62
- "prompt": prompt,
63
- "mode": body.get("mode"),
64
- "image_urls": body.get("image_urls"),
65
- }
66
-
67
- try:
68
- async with httpx.AsyncClient(timeout=300) as client:
69
- upstream = await client.post(
70
- f"{INTERNAL_API_BASE}/gen/image",
71
- json=payload,
72
- headers=build_upstream_headers(authorization, x_client_id),
73
- )
74
-
75
- if upstream.status_code >= 400:
76
- await append_credit_ledger_entry(
77
- user_id=identity["user_id"],
78
- delta_credits=cost,
79
- entry_type="adjustment",
80
- usage_kind="image",
81
- units=float(n),
82
- unit_label="images",
83
- source="v1_image_refund",
84
- source_ref=usage_key,
85
- idempotency_key=f"refund:{usage_key}",
86
- metadata={"reason": "upstream_error"},
87
- )
88
- return JSONResponse(
89
- status_code=upstream.status_code,
90
- content={"error": "Image generation failed", "detail": upstream.text[:1000]},
91
- )
92
-
93
- b64_image = base64.b64encode(upstream.content).decode("ascii")
94
- return JSONResponse(
95
- status_code=200,
96
- content={
97
- "created": int(time.time()),
98
- "data": [{"b64_json": b64_image, "mime_type": "image/jpeg"}],
99
- "usage": {"payg_credits_charged": float(cost), "image_count": n},
100
- },
101
- )
102
-
103
- except Exception as exc:
104
- await append_credit_ledger_entry(
105
- user_id=identity["user_id"],
106
- delta_credits=cost,
107
- entry_type="adjustment",
108
- usage_kind="image",
109
- units=float(n),
110
- unit_label="images",
111
- source="v1_image_refund",
112
- source_ref=usage_key,
113
- idempotency_key=f"fatal-refund:{usage_key}",
114
- metadata={"reason": "exception", "detail": str(exc)[:300]},
115
- )
116
- raise HTTPException(status_code=502, detail="Image generation failed")
117
-
118
-
119
- @router.post("/videos/generations")
120
- async def v1_video_generations(
121
- request: Request,
122
- authorization: Optional[str] = Header(None),
123
- x_client_id: Optional[str] = Header(None),
124
- ):
125
- identity = await require_payg_identity(authorization)
126
- config = await get_payg_config()
127
-
128
- limit = int(config.get("limits", {}).get("requestsPerMinutePerIdentity", 120))
129
- await enforce_payg_spam_limit(f"{identity['user_id']}:video", max(1, limit))
130
-
131
- body = await request.json()
132
- if not isinstance(body, dict):
133
- raise HTTPException(status_code=400, detail="Invalid JSON body")
134
-
135
- seconds = int(body.get("duration") or 5)
136
- seconds = max(1, min(seconds, 10))
137
-
138
- cost = calculate_video_credit_cost(seconds, config)
139
- usage_key = f"usage:video:{uuid.uuid4().hex}"
140
-
141
- await append_credit_ledger_entry(
142
- user_id=identity["user_id"],
143
- delta_credits=-cost,
144
- entry_type="usage",
145
- usage_kind="video",
146
- units=float(seconds),
147
- unit_label="seconds",
148
- source="v1_video",
149
- source_ref=usage_key,
150
- idempotency_key=usage_key,
151
- metadata={"duration_seconds": seconds},
152
- )
153
-
154
- payload = {
155
- "prompt": body.get("prompt"),
156
- "ratio": body.get("ratio"),
157
- "mode": body.get("mode"),
158
- "duration": seconds,
159
- "image_urls": body.get("image_urls"),
160
- }
161
-
162
- try:
163
- async with httpx.AsyncClient(timeout=600) as client:
164
- upstream = await client.post(
165
- f"{INTERNAL_API_BASE}/gen/video",
166
- json=payload,
167
- headers=build_upstream_headers(authorization, x_client_id),
168
- )
169
-
170
- if upstream.status_code >= 400:
171
- await append_credit_ledger_entry(
172
- user_id=identity["user_id"],
173
- delta_credits=cost,
174
- entry_type="adjustment",
175
- usage_kind="video",
176
- units=float(seconds),
177
- unit_label="seconds",
178
- source="v1_video_refund",
179
- source_ref=usage_key,
180
- idempotency_key=f"refund:{usage_key}",
181
- metadata={"reason": "upstream_error"},
182
- )
183
- return JSONResponse(
184
- status_code=upstream.status_code,
185
- content={"error": "Video generation failed", "detail": upstream.text[:1000]},
186
- )
187
-
188
- return Response(
189
- content=upstream.content,
190
- media_type="video/mp4",
191
- headers={
192
- "X-Payg-Credits-Charged": str(float(cost)),
193
- "X-Payg-Units": str(seconds),
194
- "X-Payg-Unit-Label": "seconds",
195
- },
196
- )
197
-
198
- except Exception as exc:
199
- await append_credit_ledger_entry(
200
- user_id=identity["user_id"],
201
- delta_credits=cost,
202
- entry_type="adjustment",
203
- usage_kind="video",
204
- units=float(seconds),
205
- unit_label="seconds",
206
- source="v1_video_refund",
207
- source_ref=usage_key,
208
- idempotency_key=f"fatal-refund:{usage_key}",
209
- metadata={"reason": "exception", "detail": str(exc)[:300]},
210
- )
211
- raise HTTPException(status_code=502, detail="Video generation failed")
212
-
213
-
214
- @router.post("/audio/generations")
215
- async def v1_audio_generations(
216
- request: Request,
217
- authorization: Optional[str] = Header(None),
218
- x_client_id: Optional[str] = Header(None),
219
- ):
220
- identity = await require_payg_identity(authorization)
221
- config = await get_payg_config()
222
-
223
- limit = int(config.get("limits", {}).get("requestsPerMinutePerIdentity", 120))
224
- await enforce_payg_spam_limit(f"{identity['user_id']}:audio", max(1, limit))
225
-
226
- body = await request.json()
227
- if not isinstance(body, dict):
228
- raise HTTPException(status_code=400, detail="Invalid JSON body")
229
-
230
- audio_type = "sfx"
231
-
232
- default_seconds = int(config.get("pricing", {}).get("audioDefaultSeconds", 10))
233
- seconds = int(body.get("duration_seconds") or default_seconds)
234
- seconds = max(1, min(seconds, 90))
235
-
236
- cost = calculate_audio_credit_cost(seconds, config)
237
- usage_key = f"usage:audio:{uuid.uuid4().hex}"
238
-
239
- await append_credit_ledger_entry(
240
- user_id=identity["user_id"],
241
- delta_credits=-cost,
242
- entry_type="usage",
243
- usage_kind="audio",
244
- units=float(seconds),
245
- unit_label="seconds",
246
- source="v1_audio",
247
- source_ref=usage_key,
248
- idempotency_key=usage_key,
249
- metadata={"duration_seconds": seconds, "audio_type": audio_type},
250
- )
251
-
252
- prompt = body.get("prompt")
253
- if not isinstance(prompt, str) or not prompt.strip():
254
- raise HTTPException(status_code=400, detail="prompt is required")
255
-
256
- endpoint = "sfx"
257
-
258
- try:
259
- async with httpx.AsyncClient(timeout=300) as client:
260
- upstream = await client.post(
261
- f"{INTERNAL_API_BASE}/gen/{endpoint}",
262
- json={"prompt": prompt},
263
- headers=build_upstream_headers(authorization, x_client_id),
264
- )
265
-
266
- if upstream.status_code >= 400:
267
- await append_credit_ledger_entry(
268
- user_id=identity["user_id"],
269
- delta_credits=cost,
270
- entry_type="adjustment",
271
- usage_kind="audio",
272
- units=float(seconds),
273
- unit_label="seconds",
274
- source="v1_audio_refund",
275
- source_ref=usage_key,
276
- idempotency_key=f"refund:{usage_key}",
277
- metadata={"reason": "upstream_error"},
278
- )
279
- return JSONResponse(
280
- status_code=upstream.status_code,
281
- content={"error": "Audio generation failed", "detail": upstream.text[:1000]},
282
- )
283
-
284
- return Response(
285
- content=upstream.content,
286
- media_type="audio/mpeg",
287
- headers={
288
- "X-Payg-Credits-Charged": str(float(cost)),
289
- "X-Payg-Units": str(seconds),
290
- "X-Payg-Unit-Label": "seconds",
291
- },
292
- )
293
-
294
- except Exception as exc:
295
- await append_credit_ledger_entry(
296
- user_id=identity["user_id"],
297
- delta_credits=cost,
298
- entry_type="adjustment",
299
- usage_kind="audio",
300
- units=float(seconds),
301
- unit_label="seconds",
302
- source="v1_audio_refund",
303
- source_ref=usage_key,
304
- idempotency_key=f"fatal-refund:{usage_key}",
305
- metadata={"reason": "exception", "detail": str(exc)[:300]},
306
- )
307
- raise HTTPException(status_code=502, detail="Audio generation failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
payg/stripe.py DELETED
@@ -1,250 +0,0 @@
1
- import os
2
- import stripe
3
- from decimal import Decimal
4
- from fastapi import Request, HTTPException
5
- from fastapi.responses import JSONResponse
6
- from helper.payg import (
7
- append_credit_ledger_entry,
8
- get_pack_by_session_amount,
9
- get_payg_config,
10
- get_wallet_snapshot,
11
- quantize_credits,
12
- )
13
- from helper.subscriptions import execute_query
14
- from . import router
15
-
16
- STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY", "")
17
- stripe.api_key = STRIPE_SECRET_KEY
18
- STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET")
19
-
20
-
21
- @router.post("/stripe/webhook")
22
- async def stripe_webhook(request: Request):
23
- payload = await request.body()
24
- sig_header = request.headers.get("stripe-signature")
25
-
26
- try:
27
- event = stripe.Webhook.construct_event(
28
- payload=payload,
29
- sig_header=sig_header,
30
- secret=STRIPE_WEBHOOK_SECRET,
31
- )
32
- except stripe.error.SignatureVerificationError:
33
- raise HTTPException(status_code=400, detail="Invalid Stripe signature")
34
-
35
- if event["type"] == "checkout.session.completed":
36
- session = event["data"]["object"]
37
- checkout_session_id = session["id"]
38
- session = session.to_dict()
39
-
40
- payment_status = (session.get("payment_status") or "").lower()
41
- mode = session.get("mode")
42
-
43
- customer_email = (
44
- (session.get("customer_details") or {}).get("email") or ""
45
- ).strip().lower()
46
-
47
- if mode != "payment":
48
- raise HTTPException(
49
- status_code=400,
50
- detail="Not a one-time payment session",
51
- )
52
-
53
- if payment_status != "paid":
54
- return JSONResponse({"ok": True, "ignored": "not paid yet"})
55
-
56
- if not customer_email:
57
- raise HTTPException(
58
- status_code=400,
59
- detail="Missing customer email",
60
- )
61
-
62
- config = await get_payg_config()
63
-
64
- user_row = await execute_query(
65
- """
66
- SELECT id
67
- FROM auth.users
68
- WHERE lower(email) = %s
69
- LIMIT 1;
70
- """,
71
- (customer_email,),
72
- fetchone=True,
73
- )
74
-
75
- if not user_row:
76
- raise HTTPException(
77
- status_code=404,
78
- detail="User not found",
79
- )
80
-
81
- user_id = str(user_row["id"])
82
-
83
- line_items = stripe.checkout.Session.list_line_items(
84
- checkout_session_id,
85
- limit=100,
86
- )
87
-
88
- packs = config.get("billing", {}).get("packs", [])
89
-
90
- granted_packs = []
91
- total_credits = Decimal("0")
92
-
93
- for item in line_items.data:
94
- product_id = str(item.price.product)
95
- quantity = int(item.quantity or 1)
96
-
97
- pack = next(
98
- (
99
- p for p in packs
100
- if str(p.get("prod")) == product_id
101
- ),
102
- None,
103
- )
104
-
105
- if not pack:
106
- continue
107
-
108
- pack_credits = Decimal(str(pack.get("credits") or 0))
109
-
110
- if pack_credits <= 0:
111
- continue
112
-
113
- granted = pack_credits * Decimal(str(quantity))
114
- total_credits += granted
115
-
116
- granted_packs.append({
117
- "pack_id": pack.get("id"),
118
- "product_id": product_id,
119
- "quantity": quantity,
120
- "credits": float(granted),
121
- })
122
-
123
- if total_credits <= 0:
124
- raise HTTPException(
125
- status_code=400,
126
- detail="No valid packs found in checkout session",
127
- )
128
-
129
- idempotency_key = f"stripe_checkout:{checkout_session_id}"
130
-
131
- entry = await append_credit_ledger_entry(
132
- user_id=user_id,
133
- delta_credits=quantize_credits(total_credits),
134
- entry_type="purchase",
135
- usage_kind=None,
136
- units=float(total_credits),
137
- unit_label="credits",
138
- source="stripe_checkout_session",
139
- source_ref=checkout_session_id,
140
- idempotency_key=idempotency_key,
141
- metadata={
142
- "email": customer_email,
143
- "packs": granted_packs,
144
- },
145
- )
146
-
147
- wallet = await get_wallet_snapshot(user_id)
148
-
149
- return {
150
- "ok": True,
151
- "user_id": user_id,
152
- "email": customer_email,
153
- "granted_credits": float(total_credits),
154
- "packs": granted_packs,
155
- "wallet": wallet,
156
- "ledger": entry,
157
- }
158
-
159
- return JSONResponse({"ok": True})
160
-
161
-
162
- @router.get("/stripe/reconcile/{session_id}")
163
- async def reconcile_checkout_session(session_id: str):
164
- try:
165
- session = stripe.checkout.Session.retrieve(
166
- session_id,
167
- expand=[
168
- "payment_intent.latest_charge",
169
- "invoice",
170
- "subscription.latest_invoice"
171
- ]
172
- )
173
-
174
- line_items = stripe.checkout.Session.list_line_items(session_id, limit=1)
175
- item = line_items.data[0] if line_items.data else None
176
-
177
- receipt_url = None
178
- payment_method_info = None
179
-
180
- def parse_payment_method(charge):
181
- if not charge or not getattr(charge, "payment_method_details", None):
182
- return {"type": "unknown", "brand": None, "last4": None}
183
- pm = charge.payment_method_details
184
- return {
185
- "type": pm.type,
186
- "brand": getattr(pm.card, "brand", None) if pm.type == "card" else None,
187
- "last4": getattr(pm.card, "last4", None) if pm.type == "card" else None,
188
- }
189
-
190
- if session.mode == "payment":
191
- invoice = session.invoice
192
- payment_intent = session.payment_intent
193
-
194
- if invoice and not isinstance(invoice, str):
195
- receipt_url = invoice.hosted_invoice_url or invoice.invoice_pdf
196
-
197
- if payment_intent and not isinstance(payment_intent, str):
198
- charge = getattr(payment_intent, "latest_charge", None)
199
- if charge and not isinstance(charge, str):
200
- receipt_url = charge.receipt_url or receipt_url
201
- payment_method_info = parse_payment_method(charge)
202
-
203
- elif session.amount_total > 0:
204
- return {
205
- "ok": False,
206
- "status": session.status,
207
- "payment_status": session.payment_status,
208
- "message": "Payment not completed yet or still processing."
209
- }
210
-
211
- elif session.mode == "subscription":
212
- subscription = session.subscription
213
- if subscription and not isinstance(subscription, str) and subscription.latest_invoice:
214
- invoice = subscription.latest_invoice
215
- if not isinstance(invoice, str):
216
- receipt_url = invoice.hosted_invoice_url or invoice.invoice_pdf
217
-
218
- try:
219
- full_invoice = stripe.Invoice.retrieve(invoice.id, expand=["latest_charge"])
220
- charge = getattr(full_invoice, "latest_charge", None)
221
- if charge and not isinstance(charge, str):
222
- payment_method_info = parse_payment_method(charge)
223
- except Exception:
224
- pass
225
-
226
- if not payment_method_info:
227
- payment_method_info = {"type": "subscription", "brand": None, "last4": None}
228
-
229
- else:
230
- payment_method_info = {"type": session.mode, "brand": None, "last4": None}
231
-
232
- return {
233
- "ok": True,
234
- "session_id": session.id,
235
- "mode": session.mode,
236
- "free_checkout": session.amount_total == 0,
237
- "amount_total": session.amount_total,
238
- "currency": session.currency,
239
- "customer_email": session.customer_details.email if session.customer_details else None,
240
- "product": {
241
- "name": item.description if item else None,
242
- "quantity": item.quantity if item else None,
243
- "amount_each": item.amount_total if item else (0 if session.amount_total == 0 else None),
244
- },
245
- "payment_method": payment_method_info,
246
- "receipt_url": receipt_url,
247
- }
248
-
249
- except Exception as e:
250
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prod-ca-2021.crt DELETED
@@ -1,23 +0,0 @@
1
- -----BEGIN CERTIFICATE-----
2
- MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL
3
- BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l
4
- dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh
5
- c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow
6
- azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD
7
- YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug
8
- Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW
9
- QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q
10
- DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2
11
- GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi
12
- cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4
13
- O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt
14
- NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX
15
- uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt
16
- aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU
17
- tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b
18
- VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6
19
- jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx
20
- Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2
21
- CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P
22
- o/bKiIz+Fq8=
23
- -----END CERTIFICATE-----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,8 +0,0 @@
1
- fastapi
2
- uvicorn[standard]
3
- httpx
4
- beautifulsoup4
5
- supabase
6
- psycopg[binary]
7
- pillow
8
- stripe
 
 
 
 
 
 
 
 
 
status.py DELETED
@@ -1,69 +0,0 @@
1
- # status.py
2
- from fastapi import APIRouter, Response
3
- from fastapi.responses import JSONResponse
4
-
5
- router = APIRouter(prefix="/status")
6
-
7
- @router.head("/sfx")
8
- async def head_sfx():
9
- return Response(
10
- status_code=200,
11
- headers={
12
- "Content-Type": "audio/mpeg",
13
- "Accept-Ranges": "bytes",
14
- },
15
- )
16
-
17
- @router.head("/image")
18
- async def head_image():
19
- return Response(
20
- status_code=200,
21
- headers={
22
- "Content-Type": "image/jpeg",
23
- "Accept-Ranges": "bytes",
24
- },
25
- )
26
-
27
- @router.head("/video")
28
- async def head_video():
29
- return Response(
30
- status_code=200,
31
- headers={
32
- "Content-Type": "video/mp4",
33
- "Accept-Ranges": "bytes",
34
- },
35
- )
36
-
37
- @router.head("/text")
38
- async def head_text():
39
- return Response(
40
- status_code=200,
41
- headers={
42
- "Content-Type": "application/json",
43
- "Accept-Ranges": "bytes",
44
- },
45
- )
46
-
47
- @router.get("/")
48
- async def get_status():
49
- notify = ""
50
- services = {
51
- "Video Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
52
- "Image Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
53
- "Lightning-Text v2": {"code": 200, "state": "ok", "message": "Running normally"},
54
- "Music/SFX Generation": {"code": 200, "state": "ok", "message": "Running normally"},
55
- }
56
-
57
- overall_state = (
58
- "ok" if all(s["state"] == "ok" for s in services.values()) else "degraded"
59
- )
60
-
61
- return JSONResponse(
62
- status_code=200,
63
- content={
64
- "state": overall_state,
65
- "services": services,
66
- "notifications": notify,
67
- "latest": "2.12.0",
68
- },
69
- )