Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -28,12 +28,7 @@ from helper.subscriptions import (
|
|
| 28 |
)
|
| 29 |
from typing import Optional
|
| 30 |
from helper.keywords import *
|
| 31 |
-
from helper.assets import
|
| 32 |
-
save_base64_image,
|
| 33 |
-
cleanup_image,
|
| 34 |
-
is_base64_image,
|
| 35 |
-
asset_router,
|
| 36 |
-
)
|
| 37 |
|
| 38 |
from helper.ratelimit import (
|
| 39 |
enforce_rate_limit,
|
|
@@ -55,6 +50,9 @@ from helper.ratelimit import (
|
|
| 55 |
get_usage_snapshot_for_subject,
|
| 56 |
)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
| 58 |
app = FastAPI()
|
| 59 |
|
| 60 |
WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY")
|
|
@@ -71,7 +69,10 @@ app.add_middleware(
|
|
| 71 |
allow_methods=["GET", "POST", "HEAD"],
|
| 72 |
allow_headers=["*"],
|
| 73 |
)
|
|
|
|
| 74 |
app.include_router(asset_router)
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def check_ws_auth_rate_limit(ip: str):
|
| 77 |
now = time.time()
|
|
@@ -98,250 +99,6 @@ async def reroute_to_home():
|
|
| 98 |
OLLAMA_LIBRARY_URL = "https://ollama.com/library"
|
| 99 |
|
| 100 |
|
| 101 |
-
def is_complex_reasoning(prompt: str) -> bool:
|
| 102 |
-
if len(prompt) > 800:
|
| 103 |
-
return True
|
| 104 |
-
|
| 105 |
-
for kw in REASONING_KEYWORDS:
|
| 106 |
-
if kw in prompt:
|
| 107 |
-
return True
|
| 108 |
-
|
| 109 |
-
if re.search(r"\b(if|therefore|assume|let x|given that)\b", prompt):
|
| 110 |
-
return True
|
| 111 |
-
|
| 112 |
-
return False
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def is_lightweight(prompt: str) -> bool:
|
| 116 |
-
if len(prompt) < 100:
|
| 117 |
-
for kw in LIGHTWEIGHT_KEYWORDS:
|
| 118 |
-
if kw in prompt:
|
| 119 |
-
return True
|
| 120 |
-
return False
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def is_cinematic_image_prompt(prompt: str) -> bool:
|
| 124 |
-
for kw in CREATIVE_KEYWORDS:
|
| 125 |
-
if kw in prompt.lower():
|
| 126 |
-
return True
|
| 127 |
-
return False
|
| 128 |
-
|
| 129 |
-
PKEY = os.getenv("POLLINATIONS_KEY", "")
|
| 130 |
-
PKEY2 = os.getenv("POLLINATIONS2_KEY", "")
|
| 131 |
-
PKEY3 = os.getenv("POLLINATIONS3_KEY", "")
|
| 132 |
-
|
| 133 |
-
GROQ_TOOL_MODELS = [
|
| 134 |
-
"openai/gpt-oss-120b",
|
| 135 |
-
"openai/gpt-oss-20b",
|
| 136 |
-
"meta-llama/llama-4-scout-17b-16e-instruct",
|
| 137 |
-
"qwen/qwen3-32b",
|
| 138 |
-
"moonshotai/kimi-k2-instruct",
|
| 139 |
-
]
|
| 140 |
-
|
| 141 |
-
GROQ_NORMAL_MODELS = [
|
| 142 |
-
"llama-3.1-8b-instant",
|
| 143 |
-
"llama-3.3-70b-versatile",
|
| 144 |
-
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
| 145 |
-
"meta-llama/llama-guard-4-12b",
|
| 146 |
-
"openai/gpt-oss-safeguard-20b",
|
| 147 |
-
"qwen/qwen3-32b",
|
| 148 |
-
]
|
| 149 |
-
|
| 150 |
-
CEREBRAS_MODELS = [
|
| 151 |
-
"gpt-oss-120b",
|
| 152 |
-
"llama3.1-8b",
|
| 153 |
-
"qwen-3-235b-a22b-instruct-2507",
|
| 154 |
-
"zai-glm-4.7",
|
| 155 |
-
]
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
async def check_chat_rate_limit(
|
| 159 |
-
request: Request,
|
| 160 |
-
authorization: Optional[str],
|
| 161 |
-
client_id: Optional[str] = None,
|
| 162 |
-
):
|
| 163 |
-
return await enforce_rate_limit(request, authorization, "cloudChatDaily", client_id)
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
@app.head("/status/sfx")
|
| 167 |
-
async def head_sfx():
|
| 168 |
-
return Response(
|
| 169 |
-
status_code=200,
|
| 170 |
-
headers={
|
| 171 |
-
"Content-Type": "audio/mpeg",
|
| 172 |
-
"Accept-Ranges": "bytes",
|
| 173 |
-
},
|
| 174 |
-
)
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
@app.head("/status/image")
|
| 178 |
-
async def head_image():
|
| 179 |
-
return Response(
|
| 180 |
-
status_code=200,
|
| 181 |
-
headers={
|
| 182 |
-
"Content-Type": "image/jpeg",
|
| 183 |
-
"Accept-Ranges": "bytes",
|
| 184 |
-
},
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
@app.head("/status/video")
|
| 189 |
-
async def head_video():
|
| 190 |
-
return Response(
|
| 191 |
-
status_code=200,
|
| 192 |
-
headers={
|
| 193 |
-
"Content-Type": "video/mp4",
|
| 194 |
-
"Accept-Ranges": "bytes",
|
| 195 |
-
},
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
@app.head("/status/text")
|
| 200 |
-
async def head_text():
|
| 201 |
-
return Response(
|
| 202 |
-
status_code=200,
|
| 203 |
-
headers={
|
| 204 |
-
"Content-Type": "application/json",
|
| 205 |
-
"Accept-Ranges": "bytes",
|
| 206 |
-
},
|
| 207 |
-
)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
@app.get("/status")
|
| 211 |
-
async def get_status():
|
| 212 |
-
notify = ""
|
| 213 |
-
services = {
|
| 214 |
-
"Video Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
|
| 215 |
-
"Image Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
|
| 216 |
-
"Lightning-Text v2": {
|
| 217 |
-
"code": 200,
|
| 218 |
-
"state": "ok",
|
| 219 |
-
"message": "Running normally",
|
| 220 |
-
},
|
| 221 |
-
"Music/SFX Generation": {
|
| 222 |
-
"code": 200,
|
| 223 |
-
"state": "ok",
|
| 224 |
-
"message": "Running normally",
|
| 225 |
-
},
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
overall_state = (
|
| 229 |
-
"ok" if all(s["state"] == "ok" for s in services.values()) else "degraded"
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
return JSONResponse(
|
| 233 |
-
status_code=200,
|
| 234 |
-
content={
|
| 235 |
-
"state": overall_state,
|
| 236 |
-
"services": services,
|
| 237 |
-
"notifications": notify,
|
| 238 |
-
"latest": "2.9.1",
|
| 239 |
-
},
|
| 240 |
-
)
|
| 241 |
-
|
| 242 |
-
@app.post("/gen/image")
|
| 243 |
-
@app.get("/genimg/{prompt}")
|
| 244 |
-
async def generate_image(
|
| 245 |
-
request: Request,
|
| 246 |
-
prompt: str = None,
|
| 247 |
-
authorization: Optional[str] = Header(None),
|
| 248 |
-
x_client_id: Optional[str] = Header(None),
|
| 249 |
-
):
|
| 250 |
-
"""
|
| 251 |
-
Image generation endpoint.
|
| 252 |
-
--------------------------------------------------------------
|
| 253 |
-
• Accepts a plain‑text prompt (GET or JSON body).
|
| 254 |
-
• Optional JSON fields:
|
| 255 |
-
- mode: "fantasy" | "realistic" (keeps current behaviour)
|
| 256 |
-
- image_urls: list of up to 2 image URLs or base‑64 strings
|
| 257 |
-
• If *any* image is supplied we always use the Pollinations
|
| 258 |
-
model **flux-2-dev** (the “editing” model). Otherwise the
|
| 259 |
-
original heuristic (flux / zimage) is retained.
|
| 260 |
-
• Base‑64 images are saved temporarily with the helper
|
| 261 |
-
`save_base64_image` and served from the asset CDN exactly
|
| 262 |
-
like the video endpoint does.
|
| 263 |
-
--------------------------------------------------------------
|
| 264 |
-
"""
|
| 265 |
-
timeout = httpx.Timeout(300.0, read=300.0)
|
| 266 |
-
payload: Dict[str, Any] = {}
|
| 267 |
-
|
| 268 |
-
if prompt is None:
|
| 269 |
-
payload = await request.json()
|
| 270 |
-
prompt = payload.get("prompt")
|
| 271 |
-
mode = payload.get("mode")
|
| 272 |
-
image_urls = payload.get("image_urls")
|
| 273 |
-
else:
|
| 274 |
-
mode = request.query_params.get("mode")
|
| 275 |
-
image_urls = request.query_params.getlist("image_urls")
|
| 276 |
-
payload = {}
|
| 277 |
-
|
| 278 |
-
prompt = normalize_prompt_value(prompt, "prompt")
|
| 279 |
-
enforce_prompt_size(
|
| 280 |
-
prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Image prompt"
|
| 281 |
-
)
|
| 282 |
-
await check_image_rate_limit(request, authorization, x_client_id)
|
| 283 |
-
|
| 284 |
-
chosen_model = "zimage"
|
| 285 |
-
if is_cinematic_image_prompt(prompt):
|
| 286 |
-
chosen_model = "flux"
|
| 287 |
-
|
| 288 |
-
if isinstance(mode, str):
|
| 289 |
-
normalized_mode = mode.strip().lower()
|
| 290 |
-
if normalized_mode == "fantasy":
|
| 291 |
-
chosen_model = "flux"
|
| 292 |
-
elif normalized_mode == "realistic":
|
| 293 |
-
chosen_model = "zimage"
|
| 294 |
-
|
| 295 |
-
has_input_image = False
|
| 296 |
-
temp_assets: List[str] = []
|
| 297 |
-
if image_urls:
|
| 298 |
-
if not isinstance(image_urls, list):
|
| 299 |
-
raise HTTPException(400, "image_urls must be a list")
|
| 300 |
-
if len(image_urls) > 4:
|
| 301 |
-
raise HTTPException(400, "Maximum of four image URLs allowed")
|
| 302 |
-
has_input_image = True
|
| 303 |
-
|
| 304 |
-
if has_input_image:
|
| 305 |
-
chosen_model = "klein"
|
| 306 |
-
|
| 307 |
-
params = {
|
| 308 |
-
"model": chosen_model,
|
| 309 |
-
"key": PKEY2,
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
if has_input_image:
|
| 313 |
-
processed_urls: List[str] = []
|
| 314 |
-
for img in image_urls[:2]:
|
| 315 |
-
if is_base64_image(img):
|
| 316 |
-
image_id = save_base64_image(img)
|
| 317 |
-
temp_assets.append(image_id)
|
| 318 |
-
served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
|
| 319 |
-
processed_urls.append(served_url)
|
| 320 |
-
else:
|
| 321 |
-
processed_urls.append(img)
|
| 322 |
-
|
| 323 |
-
params["image"] = "|".join(processed_urls)
|
| 324 |
-
|
| 325 |
-
encoded_prompt = quote(prompt, safe="")
|
| 326 |
-
query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
|
| 327 |
-
url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}"
|
| 328 |
-
|
| 329 |
-
try:
|
| 330 |
-
async with httpx.AsyncClient(timeout=timeout) as client:
|
| 331 |
-
response = await client.get(url)
|
| 332 |
-
finally:
|
| 333 |
-
for aid in temp_assets:
|
| 334 |
-
cleanup_image(aid)
|
| 335 |
-
|
| 336 |
-
if response.status_code != 200:
|
| 337 |
-
raise HTTPException(
|
| 338 |
-
status_code=500,
|
| 339 |
-
detail=f"Pollinations error: {response.status_code}",
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
return Response(content=response.content, media_type="image/jpeg")
|
| 343 |
-
|
| 344 |
-
|
| 345 |
@app.head("/models")
|
| 346 |
@app.get("/models")
|
| 347 |
async def get_models() -> List[Dict]:
|
|
@@ -382,591 +139,6 @@ async def get_models() -> List[Dict]:
|
|
| 382 |
|
| 383 |
return models
|
| 384 |
|
| 385 |
-
|
| 386 |
-
@app.post("/gen/chat/completions")
|
| 387 |
-
async def generate_text(
|
| 388 |
-
request: Request,
|
| 389 |
-
authorization: Optional[str] = Header(None),
|
| 390 |
-
x_client_id: Optional[str] = Header(None),
|
| 391 |
-
):
|
| 392 |
-
body = await request.json()
|
| 393 |
-
messages = body.get("messages", [])
|
| 394 |
-
if not isinstance(messages, list) or len(messages) == 0:
|
| 395 |
-
raise HTTPException(400, "messages[] is required")
|
| 396 |
-
|
| 397 |
-
total_chars, total_bytes = calculate_messages_size(messages)
|
| 398 |
-
# if total_chars > MAX_CHAT_PROMPT_CHARS or total_bytes > MAX_CHAT_PROMPT_BYTES:
|
| 399 |
-
# raise HTTPException(
|
| 400 |
-
# status_code=413,
|
| 401 |
-
# detail=(
|
| 402 |
-
# f"Prompt context too large ({total_chars} chars, {total_bytes} bytes). "
|
| 403 |
-
# f"Max allowed is {MAX_CHAT_PROMPT_CHARS} chars or {MAX_CHAT_PROMPT_BYTES} bytes."
|
| 404 |
-
# ),
|
| 405 |
-
# )
|
| 406 |
-
|
| 407 |
-
prompt_text = extract_user_text(messages)
|
| 408 |
-
|
| 409 |
-
uses_tools = (
|
| 410 |
-
"tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
|
| 411 |
-
) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
|
| 412 |
-
|
| 413 |
-
long_context = is_long_context(messages)
|
| 414 |
-
code_present = contains_code(prompt_text)
|
| 415 |
-
math_heavy = is_math_heavy(prompt_text)
|
| 416 |
-
structured_task = is_structured_task(prompt_text)
|
| 417 |
-
multi_q = multiple_questions(prompt_text)
|
| 418 |
-
code_heavy = is_code_heavy(prompt_text, code_present, long_context)
|
| 419 |
-
|
| 420 |
-
score = 0
|
| 421 |
-
|
| 422 |
-
if long_context:
|
| 423 |
-
score += 3
|
| 424 |
-
|
| 425 |
-
if math_heavy:
|
| 426 |
-
score += 3
|
| 427 |
-
|
| 428 |
-
if structured_task:
|
| 429 |
-
score += 2
|
| 430 |
-
|
| 431 |
-
if code_present:
|
| 432 |
-
score += 2
|
| 433 |
-
|
| 434 |
-
if multi_q:
|
| 435 |
-
score += 1
|
| 436 |
-
|
| 437 |
-
for kw in REASONING_KEYWORDS:
|
| 438 |
-
if kw in prompt_text:
|
| 439 |
-
score += 1
|
| 440 |
-
|
| 441 |
-
chosen_model = "llama-3.1-8b-instant"
|
| 442 |
-
provider = "groq"
|
| 443 |
-
has_images = contains_images(messages)
|
| 444 |
-
|
| 445 |
-
if has_images:
|
| 446 |
-
chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
|
| 447 |
-
provider = "groq"
|
| 448 |
-
else:
|
| 449 |
-
if score > 10:
|
| 450 |
-
score = 10
|
| 451 |
-
if uses_tools:
|
| 452 |
-
if score >= 4:
|
| 453 |
-
chosen_model = "openai/gpt-oss-120b"
|
| 454 |
-
else:
|
| 455 |
-
chosen_model = "openai/gpt-oss-20b"
|
| 456 |
-
provider = "groq"
|
| 457 |
-
|
| 458 |
-
elif code_present:
|
| 459 |
-
|
| 460 |
-
if code_heavy and score >= 6:
|
| 461 |
-
chosen_model = "qwen-3-235b-a22b-instruct-2507"
|
| 462 |
-
provider = "cerebras"
|
| 463 |
-
|
| 464 |
-
elif score >= 4:
|
| 465 |
-
chosen_model = "llama-3.3-70b-versatile"
|
| 466 |
-
provider = "groq"
|
| 467 |
-
|
| 468 |
-
elif score >= 4:
|
| 469 |
-
chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
|
| 470 |
-
provider = "groq"
|
| 471 |
-
|
| 472 |
-
if provider == "groq" and (
|
| 473 |
-
total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
|
| 474 |
-
):
|
| 475 |
-
provider = "cerebras"
|
| 476 |
-
chosen_model = "qwen-3-235b-a22b-instruct-2507"
|
| 477 |
-
|
| 478 |
-
await check_chat_rate_limit(request, authorization, x_client_id)
|
| 479 |
-
|
| 480 |
-
body["model"] = chosen_model
|
| 481 |
-
print(
|
| 482 |
-
f"""
|
| 483 |
-
[ADVANCED ROUTER]
|
| 484 |
-
Score: {score}
|
| 485 |
-
Uses tools: {uses_tools}
|
| 486 |
-
Long context: {long_context}
|
| 487 |
-
Code present: {code_present}
|
| 488 |
-
Math heavy: {math_heavy}
|
| 489 |
-
Structured: {structured_task}
|
| 490 |
-
Multi-question: {multi_q}
|
| 491 |
-
MULTIMODAL REQUIRED: {has_images}
|
| 492 |
-
→ Selected: {chosen_model} ({provider})
|
| 493 |
-
"""
|
| 494 |
-
)
|
| 495 |
-
|
| 496 |
-
stream = body.get("stream", False)
|
| 497 |
-
|
| 498 |
-
if provider == "groq":
|
| 499 |
-
groq_keys = os.getenv("GROQ_KEY", "")
|
| 500 |
-
print(f"ENV VAR: {groq_keys}")
|
| 501 |
-
groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
|
| 502 |
-
print(f"PARSED ENV VAR LIST: {groq_keys_list}")
|
| 503 |
-
if not groq_keys_list:
|
| 504 |
-
raise HTTPException(500, "Missing GROQ_KEY(s)")
|
| 505 |
-
API_KEY = random.choice(groq_keys_list)
|
| 506 |
-
print(f"SELECTED API KEY: {API_KEY}")
|
| 507 |
-
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 508 |
-
|
| 509 |
-
elif provider == "cerebras":
|
| 510 |
-
cer_keys = os.getenv("CER_KEY", "")
|
| 511 |
-
cer_keys_list = [k.strip() for k in cer_keys.split(",") if k.strip()]
|
| 512 |
-
if not cer_keys_list:
|
| 513 |
-
raise HTTPException(500, "Missing CER_KEY(s)")
|
| 514 |
-
API_KEY = random.choice(cer_keys_list)
|
| 515 |
-
|
| 516 |
-
url = "https://api.cerebras.ai/v1/chat/completions"
|
| 517 |
-
|
| 518 |
-
else:
|
| 519 |
-
raise HTTPException(500, "Unknown provider routing error")
|
| 520 |
-
|
| 521 |
-
headers = {"Authorization": f"Bearer {API_KEY}"}
|
| 522 |
-
|
| 523 |
-
if stream:
|
| 524 |
-
body["stream"] = True
|
| 525 |
-
|
| 526 |
-
async def event_generator():
|
| 527 |
-
try:
|
| 528 |
-
async with httpx.AsyncClient(timeout=None) as client:
|
| 529 |
-
async with client.stream(
|
| 530 |
-
"POST",
|
| 531 |
-
url,
|
| 532 |
-
json=body,
|
| 533 |
-
headers=headers,
|
| 534 |
-
) as r:
|
| 535 |
-
if r.status_code >= 400:
|
| 536 |
-
error_payload = ""
|
| 537 |
-
try:
|
| 538 |
-
error_payload = (
|
| 539 |
-
(await r.aread()).decode("utf-8", errors="replace")
|
| 540 |
-
)[:800]
|
| 541 |
-
except Exception:
|
| 542 |
-
error_payload = ""
|
| 543 |
-
safe_error_payload = (
|
| 544 |
-
error_payload.replace("\\", "\\\\")
|
| 545 |
-
.replace('"', '\\"')
|
| 546 |
-
.replace("\n", " ")
|
| 547 |
-
.replace("\r", " ")
|
| 548 |
-
)
|
| 549 |
-
yield (
|
| 550 |
-
'data: {"error": '
|
| 551 |
-
f'"Upstream provider error ({r.status_code}): {safe_error_payload}"'
|
| 552 |
-
"}\n\n"
|
| 553 |
-
)
|
| 554 |
-
return
|
| 555 |
-
|
| 556 |
-
async for line in r.aiter_lines():
|
| 557 |
-
if line == "":
|
| 558 |
-
yield "\n"
|
| 559 |
-
continue
|
| 560 |
-
|
| 561 |
-
yield line + "\n"
|
| 562 |
-
|
| 563 |
-
except asyncio.CancelledError:
|
| 564 |
-
return
|
| 565 |
-
except Exception as e:
|
| 566 |
-
yield f'data: {{"error": "{str(e)}"}}\n\n'
|
| 567 |
-
|
| 568 |
-
return StreamingResponse(
|
| 569 |
-
event_generator(),
|
| 570 |
-
media_type="text/event-stream",
|
| 571 |
-
headers={
|
| 572 |
-
"Cache-Control": "no-cache",
|
| 573 |
-
"Connection": "keep-alive",
|
| 574 |
-
"X-Accel-Buffering": "no", # critical for nginx
|
| 575 |
-
},
|
| 576 |
-
)
|
| 577 |
-
else:
|
| 578 |
-
async with httpx.AsyncClient(timeout=None) as client:
|
| 579 |
-
r = await client.post(url, json=body, headers=headers)
|
| 580 |
-
content_type = (r.headers.get("content-type") or "").lower()
|
| 581 |
-
if "application/json" in content_type:
|
| 582 |
-
try:
|
| 583 |
-
payload = r.json()
|
| 584 |
-
except Exception:
|
| 585 |
-
payload = {"error": "Upstream returned invalid JSON"}
|
| 586 |
-
else:
|
| 587 |
-
payload = {
|
| 588 |
-
"error": "Upstream returned non-JSON response",
|
| 589 |
-
"status_code": r.status_code,
|
| 590 |
-
"message": r.text[:1000],
|
| 591 |
-
}
|
| 592 |
-
|
| 593 |
-
return JSONResponse(status_code=r.status_code, content=payload)
|
| 594 |
-
|
| 595 |
-
raise HTTPException(500, "Unknown provider routing error")
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
@app.get("/gen/sfx/{prompt}")
|
| 599 |
-
@app.post("/gen/sfx")
|
| 600 |
-
async def gensfx(
|
| 601 |
-
request: Request,
|
| 602 |
-
prompt: str = None,
|
| 603 |
-
authorization: Optional[str] = Header(None),
|
| 604 |
-
x_client_id: Optional[str] = Header(None),
|
| 605 |
-
):
|
| 606 |
-
payload: Dict[str, Any] = {}
|
| 607 |
-
if prompt is None:
|
| 608 |
-
payload = await request.json()
|
| 609 |
-
prompt = payload.get("prompt")
|
| 610 |
-
prompt = normalize_prompt_value(prompt, "prompt")
|
| 611 |
-
enforce_prompt_size(
|
| 612 |
-
prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Audio prompt"
|
| 613 |
-
)
|
| 614 |
-
await check_audio_rate_limit(request, authorization, x_client_id)
|
| 615 |
-
url = f"https://gen.pollinations.ai/audio/{prompt}?model=acestep&key={PKEY}"
|
| 616 |
-
async with httpx.AsyncClient(timeout=None) as client:
|
| 617 |
-
response = await client.get(url)
|
| 618 |
-
body_text = ""
|
| 619 |
-
try:
|
| 620 |
-
body_text = response.text
|
| 621 |
-
except Exception:
|
| 622 |
-
pass
|
| 623 |
-
if response.status_code != 200:
|
| 624 |
-
return JSONResponse(
|
| 625 |
-
status_code=response.status_code,
|
| 626 |
-
content={
|
| 627 |
-
"success": False,
|
| 628 |
-
"error": "Upstream music/sfx generation failed",
|
| 629 |
-
"status_code": response.status_code,
|
| 630 |
-
"message": body_text[:1000],
|
| 631 |
-
},
|
| 632 |
-
)
|
| 633 |
-
return Response(response.content, media_type="audio/mpeg")
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
@app.get("/gen/tts/{prompt}")
|
| 637 |
-
@app.post("/gen/tts")
|
| 638 |
-
async def gensfx(
|
| 639 |
-
request: Request,
|
| 640 |
-
prompt: str = None,
|
| 641 |
-
authorization: Optional[str] = Header(None),
|
| 642 |
-
x_client_id: Optional[str] = Header(None),
|
| 643 |
-
):
|
| 644 |
-
payload: Dict[str, Any] = {}
|
| 645 |
-
if prompt is None:
|
| 646 |
-
payload = await request.json()
|
| 647 |
-
prompt = payload.get("prompt")
|
| 648 |
-
prompt = normalize_prompt_value(prompt, "prompt")
|
| 649 |
-
enforce_prompt_size(
|
| 650 |
-
prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Audio prompt"
|
| 651 |
-
)
|
| 652 |
-
await check_audio_rate_limit(request, authorization, x_client_id)
|
| 653 |
-
url = f"https://gen.pollinations.ai/audio/{prompt}?key={PKEY3}"
|
| 654 |
-
async with httpx.AsyncClient(timeout=None) as client:
|
| 655 |
-
response = await client.get(url)
|
| 656 |
-
body_text = ""
|
| 657 |
-
try:
|
| 658 |
-
body_text = response.text
|
| 659 |
-
except Exception:
|
| 660 |
-
pass
|
| 661 |
-
if response.status_code != 200:
|
| 662 |
-
return JSONResponse(
|
| 663 |
-
status_code=response.status_code,
|
| 664 |
-
content={
|
| 665 |
-
"success": False,
|
| 666 |
-
"error": "Upstream audio generation failed",
|
| 667 |
-
"status_code": response.status_code,
|
| 668 |
-
"message": body_text[:1000],
|
| 669 |
-
},
|
| 670 |
-
)
|
| 671 |
-
return Response(response.content, media_type="audio/mpeg")
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
@app.get("/gen/video/{prompt}")
|
| 675 |
-
@app.post("/gen/video")
|
| 676 |
-
@app.head("/gen/video")
|
| 677 |
-
async def genvideo_airforce(
|
| 678 |
-
request: Request,
|
| 679 |
-
prompt: str = None,
|
| 680 |
-
authorization: Optional[str] = Header(None),
|
| 681 |
-
x_client_id: Optional[str] = Header(None),
|
| 682 |
-
):
|
| 683 |
-
if request.method == "HEAD":
|
| 684 |
-
return Response(
|
| 685 |
-
status_code=200,
|
| 686 |
-
headers={
|
| 687 |
-
"Y-prompt": "string — required. The text prompt used to generate the video.",
|
| 688 |
-
"Y-ratio": "string — optional. Aspect ratio of the output video.",
|
| 689 |
-
"Y-ratio-values": "3:2,2:3,1:1",
|
| 690 |
-
"Y-ratio-default": "3:2",
|
| 691 |
-
"Y-mode": "string — optional. Controls generation style.",
|
| 692 |
-
"Y-mode-values": "normal,fun",
|
| 693 |
-
"Y-mode-default": "normal",
|
| 694 |
-
"Y-duration": "integer — optional. Duration in seconds (1–10).",
|
| 695 |
-
"Y-duration-default": "5",
|
| 696 |
-
"Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
|
| 697 |
-
"Y-image_urls-max": "2",
|
| 698 |
-
"Y-response_format": "video/mp4",
|
| 699 |
-
"Y-model": "grok-video",
|
| 700 |
-
},
|
| 701 |
-
)
|
| 702 |
-
|
| 703 |
-
aspectRatio = "3:2"
|
| 704 |
-
inputMode = "normal"
|
| 705 |
-
duration = 5
|
| 706 |
-
image_urls = None
|
| 707 |
-
ratio = None
|
| 708 |
-
mode = None
|
| 709 |
-
|
| 710 |
-
if prompt is None:
|
| 711 |
-
user_body = await request.json()
|
| 712 |
-
prompt = user_body.get("prompt")
|
| 713 |
-
ratio = user_body.get("ratio")
|
| 714 |
-
mode = user_body.get("mode")
|
| 715 |
-
image_urls = user_body.get("image_urls")
|
| 716 |
-
duration = user_body.get("duration", 5)
|
| 717 |
-
|
| 718 |
-
if ratio not in valid_ratios:
|
| 719 |
-
raise HTTPException(
|
| 720 |
-
status_code=400,
|
| 721 |
-
detail=f"Invalid aspect ratio '{ratio}'. Must be one of 3:2, 2:3, or 1:1.",
|
| 722 |
-
)
|
| 723 |
-
if ratio in ratios:
|
| 724 |
-
aspectRatio = ratio
|
| 725 |
-
|
| 726 |
-
if mode not in valid_modes:
|
| 727 |
-
raise HTTPException(
|
| 728 |
-
status_code=400,
|
| 729 |
-
detail=f"Invalid mode '{mode}'. Must be 'normal' or 'fun'.",
|
| 730 |
-
)
|
| 731 |
-
if mode in modes:
|
| 732 |
-
inputMode = mode
|
| 733 |
-
|
| 734 |
-
if image_urls:
|
| 735 |
-
if not isinstance(image_urls, list):
|
| 736 |
-
raise HTTPException(400, "image_urls must be a list")
|
| 737 |
-
if len(image_urls) > 2:
|
| 738 |
-
raise HTTPException(400, "You may provide at most two image URLs")
|
| 739 |
-
|
| 740 |
-
# Clamp duration
|
| 741 |
-
try:
|
| 742 |
-
duration = max(1, min(10, int(duration)))
|
| 743 |
-
except (TypeError, ValueError):
|
| 744 |
-
duration = 5
|
| 745 |
-
|
| 746 |
-
prompt = normalize_prompt_value(prompt, "prompt")
|
| 747 |
-
enforce_prompt_size(
|
| 748 |
-
prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
|
| 749 |
-
)
|
| 750 |
-
await check_video_rate_limit(request, authorization, x_client_id)
|
| 751 |
-
|
| 752 |
-
RATIO_MAP = {
|
| 753 |
-
"3:2": "16:9",
|
| 754 |
-
"2:3": "9:16",
|
| 755 |
-
"1:1": "9:16",
|
| 756 |
-
}
|
| 757 |
-
pollinations_ratio = RATIO_MAP.get(aspectRatio, "16:9")
|
| 758 |
-
|
| 759 |
-
encoded_prompt = quote(prompt, safe="")
|
| 760 |
-
params = {
|
| 761 |
-
"model": "ltx-2",
|
| 762 |
-
"duration": duration,
|
| 763 |
-
"aspectRatio": pollinations_ratio,
|
| 764 |
-
"seed": -1,
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
temp_assets = []
|
| 768 |
-
|
| 769 |
-
if image_urls:
|
| 770 |
-
processed_urls = []
|
| 771 |
-
|
| 772 |
-
for img in image_urls[:2]:
|
| 773 |
-
if is_base64_image(img):
|
| 774 |
-
image_id = save_base64_image(img)
|
| 775 |
-
temp_assets.append(image_id)
|
| 776 |
-
|
| 777 |
-
served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
|
| 778 |
-
processed_urls.append(served_url)
|
| 779 |
-
else:
|
| 780 |
-
processed_urls.append(img)
|
| 781 |
-
|
| 782 |
-
params["image"] = "|".join(processed_urls)
|
| 783 |
-
|
| 784 |
-
if inputMode == "fun":
|
| 785 |
-
params["enhance"] = "true"
|
| 786 |
-
|
| 787 |
-
query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
|
| 788 |
-
url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}"
|
| 789 |
-
|
| 790 |
-
print(f"[VIDEO GEN] Pollinations URL: {url}")
|
| 791 |
-
url = url + f"&key={PKEY}"
|
| 792 |
-
resp = None
|
| 793 |
-
try:
|
| 794 |
-
async with httpx.AsyncClient(timeout=600) as client:
|
| 795 |
-
resp = await client.get(url)
|
| 796 |
-
finally:
|
| 797 |
-
for aid in temp_assets:
|
| 798 |
-
cleanup_image(aid)
|
| 799 |
-
if resp is None:
|
| 800 |
-
raise HTTPException(502, "Video generation request failed")
|
| 801 |
-
if resp.status_code != 200:
|
| 802 |
-
body_text = ""
|
| 803 |
-
try:
|
| 804 |
-
body_text = resp.text
|
| 805 |
-
except Exception:
|
| 806 |
-
pass
|
| 807 |
-
return JSONResponse(
|
| 808 |
-
status_code=resp.status_code,
|
| 809 |
-
content={
|
| 810 |
-
"success": False,
|
| 811 |
-
"error": "Upstream video generation failed",
|
| 812 |
-
"status_code": resp.status_code,
|
| 813 |
-
"message": body_text[:1000],
|
| 814 |
-
},
|
| 815 |
-
)
|
| 816 |
-
|
| 817 |
-
if not resp.content:
|
| 818 |
-
raise HTTPException(502, "Pollinations returned empty response")
|
| 819 |
-
|
| 820 |
-
return Response(
|
| 821 |
-
content=resp.content,
|
| 822 |
-
media_type="video/mp4",
|
| 823 |
-
headers={
|
| 824 |
-
"Content-Length": str(len(resp.content)),
|
| 825 |
-
"Accept-Ranges": "bytes",
|
| 826 |
-
},
|
| 827 |
-
)
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
AIRFORCE_KEY = os.getenv("AIRFORCE")
|
| 831 |
-
AIRFORCE_VIDEO_MODEL = "grok-imagine-video"
|
| 832 |
-
AIRFORCE_API_URL = "https://api.airforce/v1/images/generations"
|
| 833 |
-
|
| 834 |
-
valid_ratios = {"3:2", "2:3", "1:1", "", None}
|
| 835 |
-
ratios = {"3:2", "2:3", "1:1"}
|
| 836 |
-
|
| 837 |
-
valid_modes = {"normal", "fun", "", None}
|
| 838 |
-
modes = {"normal", "fun"}
|
| 839 |
-
|
| 840 |
-
MAX_VIDEO_RETRIES = 6
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
@app.get("/gen/video/airforce/{prompt}")
|
| 844 |
-
@app.post("/gen/video/airforce")
|
| 845 |
-
@app.head("/gen/video/airforce")
|
| 846 |
-
async def genvideo_airforce(
|
| 847 |
-
request: Request,
|
| 848 |
-
prompt: str = None,
|
| 849 |
-
authorization: Optional[str] = Header(None),
|
| 850 |
-
x_client_id: Optional[str] = Header(None),
|
| 851 |
-
):
|
| 852 |
-
if request.method == "HEAD":
|
| 853 |
-
return Response(
|
| 854 |
-
status_code=200,
|
| 855 |
-
headers={
|
| 856 |
-
# Required field
|
| 857 |
-
"Y-prompt": "string — required. The text prompt used to generate the video.",
|
| 858 |
-
# Optional fields
|
| 859 |
-
"Y-ratio": "string — optional. Aspect ratio of the output video.",
|
| 860 |
-
"Y-ratio-values": "3:2,2:3,1:1",
|
| 861 |
-
"Y-ratio-default": "3:2",
|
| 862 |
-
"Y-mode": "string — optional. Controls generation style.",
|
| 863 |
-
"Y-mode-values": "normal,fun",
|
| 864 |
-
"Y-mode-default": "normal",
|
| 865 |
-
"Y-duration": "integer — optional. Duration in seconds.",
|
| 866 |
-
"Y-duration-default": "5",
|
| 867 |
-
"Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
|
| 868 |
-
"Y-image_urls-max": "2",
|
| 869 |
-
# Response format
|
| 870 |
-
"Y-response_format": "video/mp4",
|
| 871 |
-
# Model info
|
| 872 |
-
"Y-model": "grok-imagine-video",
|
| 873 |
-
},
|
| 874 |
-
)
|
| 875 |
-
|
| 876 |
-
aspectRatio = "3:2"
|
| 877 |
-
inputMode = "normal"
|
| 878 |
-
image_urls = None
|
| 879 |
-
ratio = None
|
| 880 |
-
mode = None
|
| 881 |
-
|
| 882 |
-
user_body = {}
|
| 883 |
-
if prompt is None:
|
| 884 |
-
user_body = await request.json()
|
| 885 |
-
prompt = user_body.get("prompt")
|
| 886 |
-
ratio = user_body.get("ratio")
|
| 887 |
-
mode = user_body.get("mode")
|
| 888 |
-
image_urls = user_body.get("image_urls")
|
| 889 |
-
|
| 890 |
-
if ratio not in valid_ratios:
|
| 891 |
-
raise HTTPException(
|
| 892 |
-
status_code=400,
|
| 893 |
-
detail=f"Invalid aspect ratio {ratio}. Must be one of 3:2, 2:3, or 1:1. Default is 3:2",
|
| 894 |
-
)
|
| 895 |
-
if ratio in ratios:
|
| 896 |
-
aspectRatio = ratio
|
| 897 |
-
|
| 898 |
-
if mode not in valid_modes:
|
| 899 |
-
raise HTTPException(
|
| 900 |
-
status_code=400,
|
| 901 |
-
detail=f"Invalid mode {mode}. Must be 'normal' or 'fun'. Default is normal",
|
| 902 |
-
)
|
| 903 |
-
if mode in modes:
|
| 904 |
-
inputMode = mode
|
| 905 |
-
|
| 906 |
-
if image_urls:
|
| 907 |
-
if not isinstance(image_urls, list):
|
| 908 |
-
raise HTTPException(400, "image_urls must be a list")
|
| 909 |
-
|
| 910 |
-
if len(image_urls) > 2:
|
| 911 |
-
raise HTTPException(400, "You may provide at most two image URLs")
|
| 912 |
-
|
| 913 |
-
prompt = normalize_prompt_value(prompt, "prompt")
|
| 914 |
-
enforce_prompt_size(
|
| 915 |
-
prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
|
| 916 |
-
)
|
| 917 |
-
await check_video_rate_limit(request, authorization, x_client_id)
|
| 918 |
-
|
| 919 |
-
payload = {
|
| 920 |
-
"model": AIRFORCE_VIDEO_MODEL,
|
| 921 |
-
"prompt": prompt,
|
| 922 |
-
"n": 1,
|
| 923 |
-
"size": "1024x1024",
|
| 924 |
-
"response_format": "b64_json",
|
| 925 |
-
"sse": False,
|
| 926 |
-
"mode": inputMode,
|
| 927 |
-
"aspectRatio": aspectRatio,
|
| 928 |
-
}
|
| 929 |
-
|
| 930 |
-
if image_urls:
|
| 931 |
-
payload["image_urls"] = image_urls
|
| 932 |
-
|
| 933 |
-
async with httpx.AsyncClient(timeout=600) as client:
|
| 934 |
-
resp = await client.post(
|
| 935 |
-
AIRFORCE_API_URL,
|
| 936 |
-
headers={
|
| 937 |
-
"Authorization": f"Bearer {AIRFORCE_KEY}",
|
| 938 |
-
"Content-Type": "application/json",
|
| 939 |
-
},
|
| 940 |
-
json=payload,
|
| 941 |
-
)
|
| 942 |
-
|
| 943 |
-
if resp.status_code != 200:
|
| 944 |
-
return JSONResponse(status_code=resp.status_code, content=resp.json())
|
| 945 |
-
|
| 946 |
-
if not resp.content:
|
| 947 |
-
raise HTTPException(502, "api.airforce returned empty response")
|
| 948 |
-
|
| 949 |
-
try:
|
| 950 |
-
result = resp.json()
|
| 951 |
-
b64_video = result["data"][0]["b64_json"]
|
| 952 |
-
except Exception:
|
| 953 |
-
raise HTTPException(502, f"Invalid api.airforce response: {resp.text[:500]}")
|
| 954 |
-
|
| 955 |
-
if not b64_video:
|
| 956 |
-
raise HTTPException(502, "Airforce returned empty b64_json")
|
| 957 |
-
|
| 958 |
-
video_bytes = base64.b64decode(b64_video)
|
| 959 |
-
|
| 960 |
-
return Response(
|
| 961 |
-
content=video_bytes,
|
| 962 |
-
media_type="video/mp4",
|
| 963 |
-
headers={
|
| 964 |
-
"Content-Length": str(len(video_bytes)),
|
| 965 |
-
"Accept-Ranges": "bytes",
|
| 966 |
-
},
|
| 967 |
-
)
|
| 968 |
-
|
| 969 |
-
|
| 970 |
@app.get("/subscription")
|
| 971 |
async def get_subscription(authorization: Optional[str] = Header(None)):
|
| 972 |
if not authorization or not authorization.startswith("Bearer "):
|
|
@@ -1084,23 +256,19 @@ async def websocket_chat(ws: WebSocket):
|
|
| 1084 |
await ws.close(code=4403)
|
| 1085 |
return
|
| 1086 |
|
| 1087 |
-
# Auth success
|
| 1088 |
await ws.send_json({
|
| 1089 |
"type": "auth",
|
| 1090 |
"status": "ok"
|
| 1091 |
})
|
| 1092 |
|
| 1093 |
-
# Internal endpoint (avoids Hugging Face proxy redirect)
|
| 1094 |
internal_url = "http://127.0.0.1:7860/gen/chat/completions"
|
| 1095 |
|
| 1096 |
-
# Persistent HTTP client for streaming
|
| 1097 |
async with httpx.AsyncClient(
|
| 1098 |
timeout=None,
|
| 1099 |
follow_redirects=False
|
| 1100 |
) as client:
|
| 1101 |
request_counter = 0
|
| 1102 |
|
| 1103 |
-
# Background task to handle incoming requests
|
| 1104 |
async def handle_incoming_requests():
|
| 1105 |
nonlocal request_counter
|
| 1106 |
while True:
|
|
@@ -1220,4 +388,3 @@ async def redirect_to_protal(request: Request):
|
|
| 1220 |
)
|
| 1221 |
else:
|
| 1222 |
return JSONResponse({"redirect_url": (base_url + "?prefilled_email=" + email)})
|
| 1223 |
-
|
|
|
|
| 28 |
)
|
| 29 |
from typing import Optional
|
| 30 |
from helper.keywords import *
|
| 31 |
+
from helper.assets import asset_router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
from helper.ratelimit import (
|
| 34 |
enforce_rate_limit,
|
|
|
|
| 50 |
get_usage_snapshot_for_subject,
|
| 51 |
)
|
| 52 |
|
| 53 |
+
from status import router as status_router
|
| 54 |
+
from gen import router as gen_router
|
| 55 |
+
|
| 56 |
app = FastAPI()
|
| 57 |
|
| 58 |
WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY")
|
|
|
|
| 69 |
allow_methods=["GET", "POST", "HEAD"],
|
| 70 |
allow_headers=["*"],
|
| 71 |
)
|
| 72 |
+
|
| 73 |
app.include_router(asset_router)
|
| 74 |
+
app.include_router(status_router)
|
| 75 |
+
app.include_router(gen_router)
|
| 76 |
|
| 77 |
def check_ws_auth_rate_limit(ip: str):
|
| 78 |
now = time.time()
|
|
|
|
| 99 |
OLLAMA_LIBRARY_URL = "https://ollama.com/library"
|
| 100 |
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
@app.head("/models")
|
| 103 |
@app.get("/models")
|
| 104 |
async def get_models() -> List[Dict]:
|
|
|
|
| 139 |
|
| 140 |
return models
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
@app.get("/subscription")
|
| 143 |
async def get_subscription(authorization: Optional[str] = Header(None)):
|
| 144 |
if not authorization or not authorization.startswith("Bearer "):
|
|
|
|
| 256 |
await ws.close(code=4403)
|
| 257 |
return
|
| 258 |
|
|
|
|
| 259 |
await ws.send_json({
|
| 260 |
"type": "auth",
|
| 261 |
"status": "ok"
|
| 262 |
})
|
| 263 |
|
|
|
|
| 264 |
internal_url = "http://127.0.0.1:7860/gen/chat/completions"
|
| 265 |
|
|
|
|
| 266 |
async with httpx.AsyncClient(
|
| 267 |
timeout=None,
|
| 268 |
follow_redirects=False
|
| 269 |
) as client:
|
| 270 |
request_counter = 0
|
| 271 |
|
|
|
|
| 272 |
async def handle_incoming_requests():
|
| 273 |
nonlocal request_counter
|
| 274 |
while True:
|
|
|
|
| 388 |
)
|
| 389 |
else:
|
| 390 |
return JSONResponse({"redirect_url": (base_url + "?prefilled_email=" + email)})
|
|
|