sharktide commited on
Commit
7bfd539
·
verified ·
1 Parent(s): c653295

Update gen.py

Browse files
Files changed (1) hide show
  1. gen.py +395 -698
gen.py CHANGED
@@ -6,7 +6,7 @@ from urllib.parse import quote
6
  from fastapi import APIRouter, Request, HTTPException, Header
7
  from fastapi.responses import Response, JSONResponse, StreamingResponse
8
  import re
9
- from typing import Optional
10
  import json
11
  from helper.assets import (
12
  save_base64_image,
@@ -34,6 +34,10 @@ from helper.ratelimit import (
34
  get_usage_snapshot_for_subject,
35
  )
36
  from helper.keywords import *
 
 
 
 
37
  router = APIRouter(prefix="/gen")
38
 
39
  PKEY = os.getenv("POLLINATIONS_KEY", "")
@@ -48,45 +52,207 @@ ratios = {"3:2", "2:3", "1:1"}
48
  valid_modes = {"normal", "fun", "", None}
49
  modes = {"normal", "fun"}
50
 
51
- MAX_VIDEO_RETRIES = 6
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- def is_cinematic_image_prompt(prompt: str) -> bool:
54
- for kw in CREATIVE_KEYWORDS:
55
- if kw in prompt.lower():
56
- return True
57
- return False
58
 
59
- def is_complex_reasoning(prompt: str) -> bool:
60
- if len(prompt) > 800:
61
- return True
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  for kw in REASONING_KEYWORDS:
64
- if kw in prompt:
65
- return True
 
66
 
67
- if re.search(r"\b(if|therefore|assume|let x|given that)\b", prompt):
68
- return True
 
69
 
70
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
- def is_lightweight(prompt: str) -> bool:
74
- if len(prompt) < 100:
75
- for kw in LIGHTWEIGHT_KEYWORDS:
76
- if kw in prompt:
77
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  return False
79
 
80
- async def check_chat_rate_limit(
81
- request: Request,
82
- authorization: Optional[str],
83
- client_id: Optional[str] = None,
84
- ):
85
- return await enforce_rate_limit(request, authorization, "cloudChatDaily", client_id)
86
 
87
- # -----------------------------
88
  # IMAGE GENERATION
89
- # -----------------------------
 
90
  @router.post("/image")
91
  @router.get("/image/{prompt}")
92
  async def generate_image(
@@ -95,21 +261,6 @@ async def generate_image(
95
  authorization: str = Header(None),
96
  x_client_id: str = Header(None),
97
  ):
98
- """
99
- Image generation endpoint.
100
- --------------------------------------------------------------
101
- • Accepts a plain‑text prompt (GET or JSON body).
102
- • Optional JSON fields:
103
- - mode: "fantasy" | "realistic" (keeps current behaviour)
104
- - image_urls: list of up to 2 image URLs or base‑64 strings
105
- • If *any* image is supplied we always use the Pollinations
106
- model **flux-klein** (the “editing” model). Otherwise the
107
- original heuristic (flux / zimage) is retained.
108
- • Base‑64 images are saved temporarily with the helper
109
- `save_base64_image` and served from the asset CDN exactly
110
- like the video endpoint does.
111
- --------------------------------------------------------------f
112
- """
113
  timeout = httpx.Timeout(300.0, read=300.0)
114
 
115
  if prompt is None:
@@ -173,9 +324,10 @@ async def generate_image(
173
  return Response(content=resp.content, media_type="image/jpeg")
174
 
175
 
176
- # -----------------------------
177
  # SFX GENERATION
178
- # -----------------------------
 
179
  @router.get("/sfx/{prompt}")
180
  @router.post("/sfx")
181
  async def gensfx(
@@ -206,9 +358,10 @@ async def gensfx(
206
  return Response(resp.content, media_type="audio/mpeg")
207
 
208
 
209
- # -----------------------------
210
  # TTS GENERATION
211
- # -----------------------------
 
212
  @router.get("/tts/{prompt}")
213
  @router.post("/tts")
214
  async def gentts(
@@ -239,13 +392,19 @@ async def gentts(
239
  return Response(resp.content, media_type="audio/mpeg")
240
 
241
 
242
- # -----------------------------
243
  # VIDEO GENERATION (Pollinations)
244
- # -----------------------------
 
245
  @router.get("/video/{prompt}")
246
  @router.post("/video")
247
  @router.head("/video")
248
- async def genvideo(request: Request, prompt: str = None, authorization: str = Header(None), x_client_id: str = Header(None)):
 
 
 
 
 
249
  if request.method == "HEAD":
250
  return Response(
251
  status_code=200,
@@ -270,8 +429,6 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
270
  inputMode = "normal"
271
  duration = 5
272
  image_urls = None
273
- ratio = None
274
- mode = None
275
 
276
  if prompt is None:
277
  user_body = await request.json()
@@ -282,18 +439,12 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
282
  duration = user_body.get("duration", 5)
283
 
284
  if ratio not in valid_ratios:
285
- raise HTTPException(
286
- status_code=400,
287
- detail=f"Invalid aspect ratio '{ratio}'. Must be one of 3:2, 2:3, or 1:1.",
288
- )
289
  if ratio in ratios:
290
  aspectRatio = ratio
291
 
292
  if mode not in valid_modes:
293
- raise HTTPException(
294
- status_code=400,
295
- detail=f"Invalid mode '{mode}'. Must be 'normal' or 'fun'.",
296
- )
297
  if mode in modes:
298
  inputMode = mode
299
 
@@ -303,23 +454,16 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
303
  if len(image_urls) > 2:
304
  raise HTTPException(400, "You may provide at most two image URLs")
305
 
306
- # Clamp duration
307
  try:
308
  duration = max(1, min(10, int(duration)))
309
  except (TypeError, ValueError):
310
  duration = 5
311
 
312
  prompt = normalize_prompt_value(prompt, "prompt")
313
- enforce_prompt_size(
314
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
315
- )
316
  await check_video_rate_limit(request, authorization, x_client_id)
317
 
318
- RATIO_MAP = {
319
- "3:2": "16:9",
320
- "2:3": "9:16",
321
- "1:1": "9:16",
322
- }
323
  pollinations_ratio = RATIO_MAP.get(aspectRatio, "16:9")
324
 
325
  encoded_prompt = quote(prompt, safe="")
@@ -334,27 +478,23 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
334
 
335
  if image_urls:
336
  processed_urls = []
337
-
338
  for img in image_urls[:2]:
339
  if is_base64_image(img):
340
  image_id = save_base64_image(img)
341
  temp_assets.append(image_id)
342
-
343
  served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
344
  processed_urls.append(served_url)
345
  else:
346
  processed_urls.append(img)
347
-
348
  params["image"] = "|".join(processed_urls)
349
 
350
  if inputMode == "fun":
351
  params["enhance"] = "true"
352
 
353
  query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
354
- url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}"
355
-
356
  print(f"[VIDEO GEN] Pollinations URL: {url}")
357
- url = url + f"&key={PKEY}"
358
  resp = None
359
  try:
360
  async with httpx.AsyncClient(timeout=600) as client:
@@ -362,8 +502,10 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
362
  finally:
363
  for aid in temp_assets:
364
  cleanup_image(aid)
 
365
  if resp is None:
366
  raise HTTPException(502, "Video generation request failed")
 
367
  if resp.status_code != 200:
368
  body_text = ""
369
  try:
@@ -392,6 +534,11 @@ async def genvideo(request: Request, prompt: str = None, authorization: str = He
392
  },
393
  )
394
 
 
 
 
 
 
395
  @router.get("/video/airforce/{prompt}")
396
  @router.post("/video/airforce")
397
  async def genvideo_airforce(
@@ -404,9 +551,7 @@ async def genvideo_airforce(
404
  return Response(
405
  status_code=200,
406
  headers={
407
- # Required field
408
  "Y-prompt": "string — required. The text prompt used to generate the video.",
409
- # Optional fields
410
  "Y-ratio": "string — optional. Aspect ratio of the output video.",
411
  "Y-ratio-values": "3:2,2:3,1:1",
412
  "Y-ratio-default": "3:2",
@@ -417,9 +562,7 @@ async def genvideo_airforce(
417
  "Y-duration-default": "5",
418
  "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
419
  "Y-image_urls-max": "2",
420
- # Response format
421
  "Y-response_format": "video/mp4",
422
- # Model info
423
  "Y-model": "grok-imagine-video",
424
  },
425
  )
@@ -427,10 +570,7 @@ async def genvideo_airforce(
427
  aspectRatio = "3:2"
428
  inputMode = "normal"
429
  image_urls = None
430
- ratio = None
431
- mode = None
432
 
433
- user_body = {}
434
  if prompt is None:
435
  user_body = await request.json()
436
  prompt = user_body.get("prompt")
@@ -439,32 +579,23 @@ async def genvideo_airforce(
439
  image_urls = user_body.get("image_urls")
440
 
441
  if ratio not in valid_ratios:
442
- raise HTTPException(
443
- status_code=400,
444
- detail=f"Invalid aspect ratio {ratio}. Must be one of 3:2, 2:3, or 1:1. Default is 3:2",
445
- )
446
  if ratio in ratios:
447
  aspectRatio = ratio
448
 
449
  if mode not in valid_modes:
450
- raise HTTPException(
451
- status_code=400,
452
- detail=f"Invalid mode {mode}. Must be 'normal' or 'fun'. Default is normal",
453
- )
454
  if mode in modes:
455
  inputMode = mode
456
 
457
  if image_urls:
458
  if not isinstance(image_urls, list):
459
  raise HTTPException(400, "image_urls must be a list")
460
-
461
  if len(image_urls) > 2:
462
  raise HTTPException(400, "You may provide at most two image URLs")
463
 
464
  prompt = normalize_prompt_value(prompt, "prompt")
465
- enforce_prompt_size(
466
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
467
- )
468
  await check_video_rate_limit(request, authorization, x_client_id)
469
 
470
  payload = {
@@ -484,10 +615,7 @@ async def genvideo_airforce(
484
  async with httpx.AsyncClient(timeout=600) as client:
485
  resp = await client.post(
486
  AIRFORCE_API_URL,
487
- headers={
488
- "Authorization": f"Bearer {AIRFORCE_KEY}",
489
- "Content-Type": "application/json",
490
- },
491
  json=payload,
492
  )
493
 
@@ -516,16 +644,20 @@ async def genvideo_airforce(
516
  "Accept-Ranges": "bytes",
517
  },
518
  )
519
- MODEL_MAP = {
520
- "llama-3.1-8b-instant": "Meta Llama 3.1 8B Instant",
521
- "gpt-4o-mini": "OpenAI GPT 4o Mini",
522
- "nemotron-3-super": "NVIDIA Nemotron 3 Super",
523
- "openai/gpt-oss-120b": "OpenAI GPT-OSS 120B",
524
- "openai/gpt-oss-20b": "OpenAI GPT-OSS 20B",
525
- "qwen-3-235b-a22b-instruct-2507": "Qwen3 Instruct",
526
- "llama-3.3-70b-versatile": "Meta Llama 3.3 70B Versatile",
527
- "meta-llama/llama-4-scout-17b-16e-instruct": "Meta Llama 4 Scout"
528
- }
 
 
 
 
529
  @router.post("/chat/completions")
530
  async def generate_text(
531
  request: Request,
@@ -537,422 +669,158 @@ async def generate_text(
537
  if not isinstance(messages, list) or len(messages) == 0:
538
  raise HTTPException(400, "messages[] is required")
539
 
540
- total_chars, total_bytes = calculate_messages_size(messages)
541
- # if total_chars > MAX_CHAT_PROMPT_CHARS or total_bytes > MAX_CHAT_PROMPT_BYTES:
542
- # raise HTTPException(
543
- # status_code=413,
544
- # detail=(
545
- # f"Prompt context too large ({total_chars} chars, {total_bytes} bytes). "
546
- # f"Max allowed is {MAX_CHAT_PROMPT_CHARS} chars or {MAX_CHAT_PROMPT_BYTES} bytes."
547
- # ),
548
- # )
549
-
550
- prompt_text = extract_user_text(messages)
551
-
552
  uses_tools = (
553
  "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
554
  ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
555
 
556
- long_context = is_long_context(messages)
557
- code_present = contains_code(prompt_text)
558
- math_heavy = is_math_heavy(prompt_text)
559
- structured_task = is_structured_task(prompt_text)
560
- multi_q = multiple_questions(prompt_text)
561
- code_heavy = is_code_heavy(prompt_text, code_present, long_context)
562
-
563
- score = 0
564
-
565
- if long_context:
566
- score += 3
567
-
568
- if math_heavy:
569
- score += 3
570
-
571
- if structured_task:
572
- score += 2
573
-
574
- if code_present:
575
- score += 2
576
-
577
- if multi_q:
578
- score += 1
579
-
580
- for kw in REASONING_KEYWORDS:
581
- if kw in prompt_text:
582
- score += 1
583
-
584
- chosen_model = "llama-3.1-8b-instant"
585
- provider = "groq"
586
- has_images = contains_images(messages)
587
 
588
- if has_images:
589
- chosen_model = "gpt-4o-mini"
590
- provider = "navy vision"
591
- else:
592
- if score > 10:
593
- score = 10
594
- if uses_tools:
595
- if score >= 6:
596
- chosen_model = "nemotron-3-super"
597
- provider = "navy"
598
- elif score >= 4:
599
- chosen_model = "openai/gpt-oss-120b"
600
- provider = "groq"
601
- else:
602
- chosen_model = "openai/gpt-oss-20b"
603
- provider = "groq"
604
-
605
- elif code_present:
606
-
607
- if code_heavy and score >= 6:
608
- chosen_model = "o3-mini"
609
- provider = "navy"
610
-
611
- elif score >= 4:
612
- chosen_model = "llama-3.3-70b-versatile"
613
- provider = "groq"
614
-
615
- elif score >= 4:
616
- chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
617
- provider = "groq"
618
-
619
- elif score >= 6:
620
- chosen_model = "sonar"
621
- provider = "navy"
622
-
623
- if provider == "groq" and (
624
- total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
625
- ):
626
- provider = "navy"
627
- chosen_model = "gpt-4o-mini"
628
-
629
- await check_chat_rate_limit(request, authorization, x_client_id)
630
 
631
  body["model"] = chosen_model
632
- print(
633
- f"""
634
- [ADVANCED ROUTER]
635
- Score: {score}
636
- Uses tools: {uses_tools}
637
- Long context: {long_context}
638
- Code present: {code_present}
639
- Math heavy: {math_heavy}
640
- Structured: {structured_task}
641
- Multi-question: {multi_q}
642
- MULTIMODAL REQUIRED: {has_images}
643
- → Selected: {chosen_model} ({provider})
644
- """
645
- )
646
-
647
  stream = body.get("stream", False)
648
- fallback_model = "meta-llama/llama-4-scout-17b-16e-instruct"
649
- fallback_provider = "groq"
650
- if provider == "groq":
651
- groq_keys = os.getenv("GROQ_KEY", "")
652
- print(f"ENV VAR: {groq_keys}")
653
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
654
- print(f"PARSED ENV VAR LIST: {groq_keys_list}")
655
- if not groq_keys_list:
656
- raise HTTPException(500, "Missing GROQ_KEY(s)")
657
- API_KEY = random.choice(groq_keys_list)
658
- print(f"SELECTED API KEY: {API_KEY}")
659
- url = "https://api.groq.com/openai/v1/chat/completions"
660
-
661
- elif provider == "cerebras":
662
- cer_keys = os.getenv("CER_KEY", "")
663
- cer_keys_list = [k.strip() for k in cer_keys.split(",") if k.strip()]
664
- if not cer_keys_list:
665
- raise HTTPException(500, "Missing CER_KEY(s)")
666
- API_KEY = random.choice(cer_keys_list)
667
-
668
- url = "https://api.cerebras.ai/v1/chat/completions"
669
-
670
- elif provider == "navy vision":
671
- navy_keys = os.getenv("NAVY_KEY", "")
672
- navy_keys_list = [k.strip() for k in navy_keys.split(",") if k.strip()]
673
- if not navy_keys_list:
674
- raise HTTPException(500, "Missing NAVY Keys(s)")
675
- API_KEY = random.choice(navy_keys_list)
676
-
677
- url = "https://api.navy/v1/chat/completions"
678
-
679
- elif provider == "navy":
680
- navy_keys = os.getenv("NAVY_TEXT_ONLY", "")
681
- navy_keys_list = [k.strip() for k in navy_keys.split(",") if k.strip()]
682
- if not navy_keys_list:
683
- raise HTTPException(500, "Missing NAVY TEXT ONLY Keys(s)")
684
- API_KEY = random.choice(navy_keys_list)
685
-
686
- url = "https://api.navy/v1/chat/completions"
687
-
688
- else:
689
- raise HTTPException(500, "Unknown provider routing error")
690
 
691
- headers = {"Authorization": f"Bearer {API_KEY}"}
 
692
 
693
  if stream:
694
  body["stream"] = True
695
-
696
- async def stream_primary(client, url, body, headers):
697
- """
698
- Handles the primary provider stream (Navy Vision, Groq, Cerebras, etc.)
699
- Returns either:
700
- - a StreamingResponse generator, OR
701
- - triggers fallback if provider fails
702
- """
703
- try:
704
- async with client.stream("POST", url, json=body, headers=headers) as r:
705
-
706
- if r.status_code >= 400:
707
- print("[STREAM FALLBACK] Primary provider failed → switching to Groq fallback")
708
- async for chunk in stream_fallback(client, body):
709
- yield chunk
710
- return
711
-
712
- async for line in r.aiter_lines():
713
- if not line:
714
- yield "\n"
715
- continue
716
- if line.startswith("event: error"):
717
- fallback()
718
-
719
- if line.startswith("data:"):
720
- try:
721
- obj = json.loads(line[5:].strip())
722
- if isinstance(obj, dict) and "error" in obj and isinstance(obj["error"], dict):
723
- fallback()
724
- except:
725
- pass
726
-
727
- yield line + "\n"
728
-
729
- except Exception as e:
730
- print(f"[STREAM ERROR] {e}")
731
- async for chunk in stream_fallback(client, body):
732
- yield chunk
733
-
734
-
735
- async def stream_fallback(client, body):
736
- """
737
- Clean fallback stream to Groq 17B.
738
- This MUST NOT be nested inside another stream.
739
- """
740
  fallback_body = {
741
- "model": fallback_model,
742
  "messages": body["messages"],
743
  "stream": True,
744
  }
745
-
746
- groq_keys = os.getenv("GROQ_KEY", "")
747
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
748
- fallback_headers = {"Authorization": f"Bearer {random.choice(groq_keys_list)}"}
749
-
750
  print("[FALLBACK] Starting Groq fallback stream")
751
-
752
- async with client.stream(
753
- "POST",
754
- "https://api.groq.com/openai/v1/chat/completions",
755
- json=fallback_body,
756
- headers=fallback_headers,
757
- ) as r:
758
-
759
  if r.status_code >= 400:
760
  err = (await r.aread()).decode("utf-8", errors="replace")
761
  yield f'data: {{"error": "Fallback provider failed: {err[:500]}"}}\n\n'
762
  return
763
-
764
  async for line in r.aiter_lines():
765
  if not line:
766
  yield "\n"
767
  continue
768
-
769
- if not line.startswith("data:"):
770
- yield f"data: {line}\n\n"
771
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  yield line + "\n"
773
-
774
-
 
 
 
775
  async def event_generator():
776
  sent_metadata = False
777
-
778
  async with httpx.AsyncClient(timeout=None) as client:
779
- async for chunk in stream_primary(client, url, body, headers):
780
-
781
  if not sent_metadata:
782
- meta = {
783
- "router_metadata": {
784
- "model_name": MODEL_MAP.get(chosen_model, chosen_model)
785
- }
786
- }
787
  yield f"data: {json.dumps(meta)}\n\n"
788
  sent_metadata = True
789
-
790
  yield chunk
791
 
792
  return StreamingResponse(
793
  event_generator(),
794
  media_type="text/event-stream",
795
- headers={
796
- "Cache-Control": "no-cache",
797
- "Connection": "keep-alive",
798
- "X-Accel-Buffering": "no",
799
- },
800
  )
801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  else:
803
- async with httpx.AsyncClient(timeout=None) as client:
804
- r = await client.post(url, json=body, headers=headers)
805
- if provider == "navy vision" and r.status_code >= 400:
806
- print("[FALLBACK] Navy vision failed — switching to 17B Groq")
807
-
808
- groq_keys = os.getenv("GROQ_KEY", "")
809
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
810
- if not groq_keys_list:
811
- raise HTTPException(500, "Missing GROQ_KEY(s) for fallback")
812
-
813
- API_KEY = random.choice(groq_keys_list)
814
-
815
- fallback_headers = {"Authorization": f"Bearer {API_KEY}"}
816
- fallback_body = dict(body)
817
- fallback_body["model"] = fallback_model
818
-
819
- r = await client.post(
820
- "https://api.groq.com/openai/v1/chat/completions",
821
- json=fallback_body,
822
- headers=fallback_headers,
823
- )
824
- content_type = (r.headers.get("content-type") or "").lower()
825
- if "application/json" in content_type:
826
- try:
827
- payload = r.json()
828
- except Exception:
829
- payload = {"error": "Upstream returned invalid JSON"}
830
- else:
831
- payload = {
832
- "error": "Upstream returned non-JSON response",
833
- "status_code": r.status_code,
834
- "message": r.text[:1000],
835
- }
836
 
837
- return JSONResponse(status_code=r.status_code, content=payload)
838
 
839
- raise HTTPException(500, "Unknown provider routing error")
 
 
 
840
 
841
  @router.post("/prompt_analyze")
842
- async def analyze_prompt(
843
- request: Request
844
- ):
845
  body = await request.json()
846
  messages = body.get("prompt", [])
847
  if not isinstance(messages, list) or len(messages) == 0:
848
  raise HTTPException(400, "messages[] is required")
849
 
850
- total_chars, total_bytes = calculate_messages_size(messages)
851
- prompt_text = extract_user_text(messages)
852
-
853
  uses_tools = (
854
  "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
855
  ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
856
 
857
- long_context = is_long_context(messages)
858
- code_present = contains_code(prompt_text)
859
- math_heavy = is_math_heavy(prompt_text)
860
- structured_task = is_structured_task(prompt_text)
861
- multi_q = multiple_questions(prompt_text)
862
- code_heavy = is_code_heavy(prompt_text, code_present, long_context)
863
-
864
- score = 0
865
 
866
- if long_context:
867
- score += 3
868
 
869
- if math_heavy:
870
- score += 3
871
-
872
- if structured_task:
873
- score += 2
874
-
875
- if code_present:
876
- score += 2
877
-
878
- if multi_q:
879
- score += 1
880
-
881
- for kw in REASONING_KEYWORDS:
882
- if kw in prompt_text:
883
- score += 1
884
-
885
- chosen_model = "llama-3.1-8b-instant"
886
- provider = "groq"
887
- has_images = contains_images(messages)
888
-
889
- if has_images:
890
- chosen_model = "gpt-4o-mini"
891
- provider = "navy vision"
892
- else:
893
- if score > 10:
894
- score = 10
895
- if uses_tools:
896
- if score >= 6:
897
- chosen_model = "nemotron-3-super"
898
- provider = "navy"
899
- elif score >= 4:
900
- chosen_model = "openai/gpt-oss-120b"
901
- provider = "groq"
902
- else:
903
- chosen_model = "openai/gpt-oss-20b"
904
- provider = "groq"
905
-
906
- elif code_present:
907
-
908
- if code_heavy and score >= 6:
909
- chosen_model = "o3-mini"
910
- provider = "navy"
911
-
912
- elif score >= 4:
913
- chosen_model = "llama-3.3-70b-versatile"
914
- provider = "groq"
915
-
916
- elif score >= 4:
917
- chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
918
- provider = "groq"
919
-
920
- elif score >= 6:
921
- chosen_model = "sonar"
922
- provider = "navy"
923
-
924
- if provider == "groq" and (
925
- total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
926
- ):
927
- provider = "navy"
928
- chosen_model = "gpt-4o-mini"
929
-
930
- return { MODEL_MAP[chosen_model] }
931
 
932
  @router.get("/models")
933
  def return_models_openai():
934
  return {
935
- "object": "list",
936
- "data": [
937
- {
938
- "id": "lightning",
939
- "object": "model",
940
- "created": 1767225600,
941
- "owned_by": "inferenceport-ai"
942
- }
943
- ]
944
  }
945
 
946
- from uuid import uuid4
947
- from time import time
948
- from typing import Any, Dict, List, Optional
949
- import json
950
- import os
951
- import random
952
- import httpx
953
 
954
- from fastapi import Request, HTTPException, Header
955
- from fastapi.responses import JSONResponse, StreamingResponse
 
956
 
957
  def _resp_id(prefix: str) -> str:
958
  return f"{prefix}_{uuid4().hex}"
@@ -966,16 +834,17 @@ def _content_to_text(content: Any) -> str:
966
  if isinstance(content, list):
967
  parts = []
968
  for item in content:
969
- if isinstance(item, dict):
970
- t = item.get("type")
971
- if t in ("input_text", "output_text", "text"):
972
- txt = item.get("text")
973
- if isinstance(txt, str):
974
- parts.append(txt)
975
  return "".join(parts)
976
  return ""
977
 
978
- def _responses_input_to_messages(input_data: Any, instructions: Optional[str] = None) -> List[Dict[str, Any]]:
 
 
 
979
  messages: List[Dict[str, Any]] = []
980
  if instructions:
981
  messages.append({"role": "developer", "content": instructions})
@@ -992,16 +861,21 @@ def _responses_input_to_messages(input_data: Any, instructions: Optional[str] =
992
  if not isinstance(item, dict):
993
  continue
994
  role = item.get("role", "user")
995
- content = item.get("content", "")
996
- text = _content_to_text(content)
997
  if text:
998
  messages.append({"role": role, "content": text})
999
 
1000
  return messages
1001
 
1002
- def _openai_responses_payload(model: str, text: str, input_tokens: int = 0, output_tokens: int = 0) -> Dict[str, Any]:
 
 
 
 
 
 
1003
  return {
1004
- "id": _resp_id("resp"),
1005
  "object": "response",
1006
  "created_at": _resp_ts(),
1007
  "status": "completed",
@@ -1017,276 +891,99 @@ def _openai_responses_payload(model: str, text: str, input_tokens: int = 0, outp
1017
  "type": "message",
1018
  "role": "assistant",
1019
  "status": "completed",
1020
- "content": [
1021
- {
1022
- "type": "output_text",
1023
- "text": text,
1024
- "annotations": []
1025
- }
1026
- ]
1027
  }
1028
  ],
1029
  "output_text": text,
1030
  "usage": {
1031
  "input_tokens": input_tokens,
1032
  "output_tokens": output_tokens,
1033
- "total_tokens": input_tokens + output_tokens
1034
- }
1035
  }
1036
 
1037
- async def _generate_text_from_messages(
1038
- request: Request,
1039
- messages: List[Dict[str, Any]],
1040
- authorization: Optional[str],
1041
- xclientid: Optional[str],
1042
- ) -> Dict[str, Any]:
1043
- totalchars, totalbytes = calculate_messages_size(messages)
1044
- prompttext = extract_user_text(messages)
1045
-
1046
- usestools = False
1047
- longcontext = is_long_context(messages)
1048
- codepresent = contains_code(prompttext)
1049
- mathheavy = is_math_heavy(prompttext)
1050
- structuredtask = is_structured_task(prompttext)
1051
- multiq = multiple_questions(prompttext)
1052
- codeheavy = is_code_heavy(prompttext, codepresent, longcontext)
1053
-
1054
- score = 0
1055
- if longcontext:
1056
- score += 3
1057
- if mathheavy:
1058
- score += 3
1059
- if structuredtask:
1060
- score += 2
1061
- if codepresent:
1062
- score += 2
1063
- if multiq:
1064
- score += 1
1065
- for kw in REASONING_KEYWORDS:
1066
- if kw in prompttext:
1067
- score += 1
1068
- if score > 10:
1069
- score = 10
1070
-
1071
- chosenmodel = "llama-3.1-8b-instant"
1072
- provider = "groq"
1073
- hasimages = contains_images(messages)
1074
-
1075
- if hasimages:
1076
- chosenmodel = "gpt-4o-mini"
1077
- provider = "navy vision"
1078
- else:
1079
- if usestools:
1080
- if score >= 6:
1081
- chosenmodel = "nemotron-3-super"
1082
- provider = "navy"
1083
- elif score >= 4:
1084
- chosenmodel = "openai/gpt-oss-120b"
1085
- provider = "groq"
1086
- else:
1087
- chosenmodel = "openai/gpt-oss-20b"
1088
- provider = "groq"
1089
- elif codepresent:
1090
- if codeheavy and score >= 6:
1091
- chosenmodel = "o3-mini"
1092
- provider = "navy"
1093
- elif score >= 4:
1094
- chosenmodel = "llama-3.3-70b-versatile"
1095
- provider = "groq"
1096
- elif score >= 4:
1097
- chosenmodel = "meta-llama/llama-4-scout-17b-16e-instruct"
1098
- provider = "groq"
1099
- elif score >= 6:
1100
- chosenmodel = "sonar"
1101
- provider = "navy"
1102
-
1103
- if provider == "groq" and (totalchars > MAX_GROQ_PROMPT_CHARS or totalbytes > MAX_GROQ_PROMPT_BYTES):
1104
- provider = "navy"
1105
- chosenmodel = "gpt-4o-mini"
1106
-
1107
- await check_chat_rate_limit(request, authorization, xclientid)
1108
-
1109
- if provider == "groq":
1110
- groqkeys = os.getenv("GROQ_KEY")
1111
- groqkeyslist = [k.strip() for k in groqkeys.split(",") if k.strip()] if groqkeys else []
1112
- if not groqkeyslist:
1113
- raise HTTPException(status_code=500, detail="Missing GROQ_KEYs")
1114
- apikey = random.choice(groqkeyslist)
1115
- url = "https://api.groq.com/openai/v1/chat/completions"
1116
- headers = {"Authorization": f"Bearer {apikey}", "Content-Type": "application/json"}
1117
- payload = {"model": chosenmodel, "messages": messages, "stream": False}
1118
- async with httpx.AsyncClient(timeout=None) as client:
1119
- r = await client.post(url, json=payload, headers=headers)
1120
- if r.status_code != 200:
1121
- raise HTTPException(status_code=r.status_code, detail=r.text[:1000])
1122
- data = r.json()
1123
- text = ""
1124
- try:
1125
- text = data["choices"][0]["message"]["content"] or ""
1126
- except Exception:
1127
- text = ""
1128
- return {"text": text, "model": chosenmodel, "provider": provider, "raw": data}
1129
-
1130
- if provider == "navy vision":
1131
- navykeys = os.getenv("NAVY_KEY")
1132
- navykeyslist = [k.strip() for k in navykeys.split(",") if k.strip()] if navykeys else []
1133
- if not navykeyslist:
1134
- raise HTTPException(status_code=500, detail="Missing NAVY_KEYs")
1135
- apikey = random.choice(navykeyslist)
1136
- url = "https://api.navy/v1/chat/completions"
1137
- headers = {"Authorization": f"Bearer {apikey}", "Content-Type": "application/json"}
1138
- payload = {"model": chosenmodel, "messages": messages, "stream": False}
1139
- async with httpx.AsyncClient(timeout=None) as client:
1140
- r = await client.post(url, json=payload, headers=headers)
1141
- if r.status_code != 200:
1142
- raise HTTPException(status_code=r.status_code, detail=r.text[:1000])
1143
- data = r.json()
1144
- text = ""
1145
- try:
1146
- text = data["choices"][0]["message"]["content"] or ""
1147
- except Exception:
1148
- text = ""
1149
- return {"text": text, "model": chosenmodel, "provider": provider, "raw": data}
1150
-
1151
- if provider == "navy":
1152
- navykeys = os.getenv("NAVY_TEXT_ONLY")
1153
- navykeyslist = [k.strip() for k in navykeys.split(",") if k.strip()] if navykeys else []
1154
- if not navykeyslist:
1155
- raise HTTPException(status_code=500, detail="Missing NAVY TEXT ONLY keys")
1156
- apikey = random.choice(navykeyslist)
1157
- url = "https://api.navy/v1/chat/completions"
1158
- headers = {"Authorization": f"Bearer {apikey}", "Content-Type": "application/json"}
1159
- payload = {"model": chosenmodel, "messages": messages, "stream": False}
1160
- async with httpx.AsyncClient(timeout=None) as client:
1161
- r = await client.post(url, json=payload, headers=headers)
1162
- if r.status_code != 200:
1163
- raise HTTPException(status_code=r.status_code, detail=r.text[:1000])
1164
- data = r.json()
1165
- text = ""
1166
- try:
1167
- text = data["choices"][0]["message"]["content"] or ""
1168
- except Exception:
1169
- text = ""
1170
- return {"text": text, "model": chosenmodel, "provider": provider, "raw": data}
1171
-
1172
- raise HTTPException(status_code=500, detail="Unknown provider routing error")
1173
 
1174
  @router.post("/responses")
1175
  async def create_responses(
1176
  request: Request,
1177
  authorization: Optional[str] = Header(None),
1178
- xclientid: Optional[str] = Header(None),
1179
  ):
1180
  body = await request.json()
1181
  model = body.get("model")
1182
  input_data = body.get("input")
1183
  instructions = body.get("instructions")
1184
  stream = body.get("stream", True)
1185
- response_format = body.get("response_format")
1186
 
1187
  if not model:
1188
- raise HTTPException(status_code=400, detail="model is required")
1189
  if input_data is None:
1190
- raise HTTPException(status_code=400, detail="input is required")
1191
 
1192
  messages = _responses_input_to_messages(input_data, instructions=instructions)
1193
  if not messages:
1194
- raise HTTPException(status_code=400, detail="input could not be parsed")
1195
-
 
 
 
 
 
 
 
 
 
 
1196
  if stream is False:
1197
- result = await _generate_text_from_messages(
1198
- request=request,
1199
- messages=messages,
1200
- authorization=authorization,
1201
- xclientid=xclientid,
1202
  )
1203
- if "text" not in result:
1204
- raise HTTPException(status_code=500, detail="upstream generation failed")
1205
- return JSONResponse(content=_openai_responses_payload(model, result["text"]))
1206
 
 
1207
  async def event_stream():
1208
  response_id = _resp_id("resp")
1209
- created = {
 
1210
  "type": "response.created",
1211
  "response": {
1212
  "id": response_id,
1213
  "object": "response",
1214
  "created_at": _resp_ts(),
1215
  "status": "in_progress",
1216
- "model": model
1217
- }
1218
  }
1219
- yield f"data: {json.dumps(created)}\n\n"
1220
 
1221
- result = await _generate_text_from_messages(
1222
- request=request,
1223
- messages=messages,
1224
- authorization=authorization,
1225
- xclientid=xclientid,
1226
- )
1227
-
1228
- if "text" in result:
1229
- text = result["text"]
1230
- if text:
1231
- chunk_size = 64
1232
- for i in range(0, len(text), chunk_size):
1233
- delta = text[i:i + chunk_size]
1234
- evt = {
1235
- "type": "response.output_text.delta",
1236
- "response_id": response_id,
1237
- "delta": delta
1238
- }
1239
- yield f"data: {json.dumps(evt)}\n\n"
1240
-
1241
- completed = {
1242
- "type": "response.completed",
1243
- "response": {
1244
- "id": response_id,
1245
- "object": "response",
1246
- "created_at": _resp_ts(),
1247
- "status": "completed",
1248
- "completed_at": _resp_ts(),
1249
- "model": model,
1250
- "output_text": result["text"],
1251
- "output": [
1252
- {
1253
- "id": _resp_id("msg"),
1254
- "type": "message",
1255
- "role": "assistant",
1256
- "status": "completed",
1257
- "content": [
1258
- {
1259
- "type": "output_text",
1260
- "text": result["text"],
1261
- "annotations": []
1262
- }
1263
- ]
1264
- }
1265
- ],
1266
- "usage": {
1267
- "input_tokens": 0,
1268
- "output_tokens": 0,
1269
- "total_tokens": 0
1270
- }
1271
- }
1272
- }
1273
- yield f"data: {json.dumps(completed)}\n\n"
1274
  yield "data: [DONE]\n\n"
1275
  return
1276
 
1277
- err = {
1278
- "type": "response.error",
1279
- "error": result.get("error", {"message": "upstream error"})
 
 
 
 
 
 
 
 
 
 
1280
  }
1281
- yield f"data: {json.dumps(err)}\n\n"
1282
  yield "data: [DONE]\n\n"
1283
 
1284
  return StreamingResponse(
1285
  event_stream(),
1286
  media_type="text/event-stream",
1287
- headers={
1288
- "Cache-Control": "no-cache",
1289
- "Connection": "keep-alive",
1290
- "X-Accel-Buffering": "no",
1291
- },
1292
  )
 
6
  from fastapi import APIRouter, Request, HTTPException, Header
7
  from fastapi.responses import Response, JSONResponse, StreamingResponse
8
  import re
9
+ from typing import Optional, Any
10
  import json
11
  from helper.assets import (
12
  save_base64_image,
 
34
  get_usage_snapshot_for_subject,
35
  )
36
  from helper.keywords import *
37
+ from uuid import uuid4
38
+ from time import time
39
+ from typing import Dict, List, Optional, Tuple
40
+
41
  router = APIRouter(prefix="/gen")
42
 
43
  PKEY = os.getenv("POLLINATIONS_KEY", "")
 
52
  valid_modes = {"normal", "fun", "", None}
53
  modes = {"normal", "fun"}
54
 
55
+ MODEL_MAP = {
56
+ "llama-3.1-8b-instant": "Meta Llama 3.1 8B Instant",
57
+ "gpt-4o-mini": "OpenAI GPT 4o Mini",
58
+ "nemotron-3-super": "NVIDIA Nemotron 3 Super",
59
+ "openai/gpt-oss-120b": "OpenAI GPT-OSS 120B",
60
+ "openai/gpt-oss-20b": "OpenAI GPT-OSS 20B",
61
+ "qwen-3-235b-a22b-instruct-2507": "Qwen3 Instruct",
62
+ "llama-3.3-70b-versatile": "Meta Llama 3.3 70B Versatile",
63
+ "meta-llama/llama-4-scout-17b-16e-instruct": "Meta Llama 4 Scout",
64
+ }
65
+
66
+ FALLBACK_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
67
+ FALLBACK_PROVIDER = "groq"
68
 
 
 
 
 
 
69
 
70
+ # ──────────────────────────────────────────────
71
+ # CENTRAL ROUTING LOGIC
72
+ # ──────────────────────────────────────────────
73
 
74
+ def route_chat(
75
+ messages: List[Dict[str, Any]],
76
+ uses_tools: bool = False,
77
+ ) -> Tuple[str, str]:
78
+ """
79
+ Inspect messages and return (chosen_model, provider).
80
+
81
+ This is the single source of truth for model selection.
82
+ No API calls, no side-effects — pure routing logic.
83
+ """
84
+ total_chars, total_bytes = calculate_messages_size(messages)
85
+ prompt_text = extract_user_text(messages)
86
+
87
+ long_context = is_long_context(messages)
88
+ code_present = contains_code(prompt_text)
89
+ math_heavy = is_math_heavy(prompt_text)
90
+ structured_task = is_structured_task(prompt_text)
91
+ multi_q = multiple_questions(prompt_text)
92
+ code_heavy = is_code_heavy(prompt_text, code_present, long_context)
93
+ has_images = contains_images(messages)
94
+
95
+ score = 0
96
+ if long_context: score += 3
97
+ if math_heavy: score += 3
98
+ if structured_task: score += 2
99
+ if code_present: score += 2
100
+ if multi_q: score += 1
101
  for kw in REASONING_KEYWORDS:
102
+ if kw in prompt_text:
103
+ score += 1
104
+ score = min(score, 10)
105
 
106
+ # ── multimodal fast-path ──────────────────
107
+ if has_images:
108
+ return "gpt-4o-mini", "navy vision"
109
 
110
+ # ── tool-use branch ──────────────────────
111
+ if uses_tools:
112
+ if score >= 6:
113
+ return "nemotron-3-super", "navy"
114
+ if score >= 4:
115
+ return "openai/gpt-oss-120b", "groq"
116
+ return "openai/gpt-oss-20b", "groq"
117
+
118
+ # ── code branch ──────────────────────────
119
+ if code_present:
120
+ if code_heavy and score >= 6:
121
+ return "o3-mini", "navy"
122
+ if score >= 4:
123
+ return "llama-3.3-70b-versatile", "groq"
124
+
125
+ # ── general reasoning branch ─────────────
126
+ if score >= 6:
127
+ return "sonar", "navy"
128
+ if score >= 4:
129
+ return "meta-llama/llama-4-scout-17b-16e-instruct", "groq"
130
+
131
+ # ── default ──────────────────────────────
132
+ chosen_model, provider = "llama-3.1-8b-instant", "groq"
133
+
134
+ # Groq context-size guard — promote to navy if too large
135
+ if provider == "groq" and (
136
+ total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
137
+ ):
138
+ return "gpt-4o-mini", "navy"
139
+
140
+ return chosen_model, provider
141
+
142
+
143
+ def _log_routing(
144
+ chosen_model: str,
145
+ provider: str,
146
+ messages: List[Dict[str, Any]],
147
+ uses_tools: bool,
148
+ ) -> None:
149
+ prompt_text = extract_user_text(messages)
150
+ long_context = is_long_context(messages)
151
+ code_present = contains_code(prompt_text)
152
+ math_heavy = is_math_heavy(prompt_text)
153
+ structured_task = is_structured_task(prompt_text)
154
+ multi_q = multiple_questions(prompt_text)
155
+ has_images = contains_images(messages)
156
+ print(
157
+ f"\n[ADVANCED ROUTER]\n"
158
+ f" Uses tools: {uses_tools}\n"
159
+ f" Long context: {long_context}\n"
160
+ f" Code present: {code_present}\n"
161
+ f" Math heavy: {math_heavy}\n"
162
+ f" Structured: {structured_task}\n"
163
+ f" Multi-question:{multi_q}\n"
164
+ f" Has images: {has_images}\n"
165
+ f" → Selected: {chosen_model} ({provider})\n"
166
+ )
167
+
168
+
169
+ # ──────────────────────────────────────────────
170
+ # CENTRAL HTTP CALL
171
+ # ──────────────────────────────────────────────
172
+
173
+ def _get_provider_url_and_key(provider: str) -> Tuple[str, str]:
174
+ """Return (url, api_key) for the given provider, raising on misconfiguration."""
175
+ if provider == "groq":
176
+ keys = [k.strip() for k in os.getenv("GROQ_KEY", "").split(",") if k.strip()]
177
+ if not keys:
178
+ raise HTTPException(500, "Missing GROQ_KEY(s)")
179
+ return "https://api.groq.com/openai/v1/chat/completions", random.choice(keys)
180
+
181
+ if provider == "cerebras":
182
+ keys = [k.strip() for k in os.getenv("CER_KEY", "").split(",") if k.strip()]
183
+ if not keys:
184
+ raise HTTPException(500, "Missing CER_KEY(s)")
185
+ return "https://api.cerebras.ai/v1/chat/completions", random.choice(keys)
186
+
187
+ if provider == "navy vision":
188
+ keys = [k.strip() for k in os.getenv("NAVY_KEY", "").split(",") if k.strip()]
189
+ if not keys:
190
+ raise HTTPException(500, "Missing NAVY_KEY(s)")
191
+ return "https://api.navy/v1/chat/completions", random.choice(keys)
192
+
193
+ if provider == "navy":
194
+ keys = [k.strip() for k in os.getenv("NAVY_TEXT_ONLY", "").split(",") if k.strip()]
195
+ if not keys:
196
+ raise HTTPException(500, "Missing NAVY_TEXT_ONLY key(s)")
197
+ return "https://api.navy/v1/chat/completions", random.choice(keys)
198
+
199
+ raise HTTPException(500, f"Unknown provider: {provider!r}")
200
+
201
+
202
+ async def call_chat_completions(
203
+ messages: List[Dict[str, Any]],
204
+ model: str,
205
+ provider: str,
206
+ extra_body: Optional[Dict[str, Any]] = None,
207
+ ) -> Dict[str, Any]:
208
+ """
209
+ Non-streaming chat-completions call.
210
+
211
+ Returns the full upstream JSON payload.
212
+ Raises HTTPException on upstream errors.
213
+ """
214
+ url, api_key = _get_provider_url_and_key(provider)
215
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
216
+ body = {"model": model, "messages": messages, "stream": False}
217
+ if extra_body:
218
+ body.update(extra_body)
219
+
220
+ async with httpx.AsyncClient(timeout=None) as client:
221
+ r = await client.post(url, json=body, headers=headers)
222
+
223
+ if r.status_code != 200:
224
+ raise HTTPException(status_code=r.status_code, detail=r.text[:1000])
225
+
226
+ return r.json()
227
 
228
 
229
+ def _extract_text_from_response(data: Dict[str, Any]) -> str:
230
+ try:
231
+ return data["choices"][0]["message"]["content"] or ""
232
+ except Exception:
233
+ return ""
234
+
235
+
236
+ def _extract_usage(data: Dict[str, Any]) -> Tuple[int, int]:
237
+ usage = data.get("usage", {})
238
+ return usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0)
239
+
240
+
241
+ # ──────────────────────────────────────────────
242
+ # HELPER: image generation
243
+ # ──────────────────────────────────────────────
244
+
245
+ def is_cinematic_image_prompt(prompt: str) -> bool:
246
+ for kw in CREATIVE_KEYWORDS:
247
+ if kw in prompt.lower():
248
+ return True
249
  return False
250
 
 
 
 
 
 
 
251
 
252
+ # ──────────────────────────────────────────────
253
  # IMAGE GENERATION
254
+ # ──────────────────────────────────────────────
255
+
256
  @router.post("/image")
257
  @router.get("/image/{prompt}")
258
  async def generate_image(
 
261
  authorization: str = Header(None),
262
  x_client_id: str = Header(None),
263
  ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  timeout = httpx.Timeout(300.0, read=300.0)
265
 
266
  if prompt is None:
 
324
  return Response(content=resp.content, media_type="image/jpeg")
325
 
326
 
327
+ # ──────────────────────────────────────────────
328
  # SFX GENERATION
329
+ # ──────────────────────────────────────────────
330
+
331
  @router.get("/sfx/{prompt}")
332
  @router.post("/sfx")
333
  async def gensfx(
 
358
  return Response(resp.content, media_type="audio/mpeg")
359
 
360
 
361
+ # ──────────────────────────────────────────────
362
  # TTS GENERATION
363
+ # ──────────────────────────────────────────────
364
+
365
  @router.get("/tts/{prompt}")
366
  @router.post("/tts")
367
  async def gentts(
 
392
  return Response(resp.content, media_type="audio/mpeg")
393
 
394
 
395
+ # ──────────────────────────────────────────────
396
  # VIDEO GENERATION (Pollinations)
397
+ # ──────────────────────────────────────────────
398
+
399
  @router.get("/video/{prompt}")
400
  @router.post("/video")
401
  @router.head("/video")
402
+ async def genvideo(
403
+ request: Request,
404
+ prompt: str = None,
405
+ authorization: str = Header(None),
406
+ x_client_id: str = Header(None),
407
+ ):
408
  if request.method == "HEAD":
409
  return Response(
410
  status_code=200,
 
429
  inputMode = "normal"
430
  duration = 5
431
  image_urls = None
 
 
432
 
433
  if prompt is None:
434
  user_body = await request.json()
 
439
  duration = user_body.get("duration", 5)
440
 
441
  if ratio not in valid_ratios:
442
+ raise HTTPException(400, f"Invalid aspect ratio '{ratio}'. Must be one of 3:2, 2:3, or 1:1.")
 
 
 
443
  if ratio in ratios:
444
  aspectRatio = ratio
445
 
446
  if mode not in valid_modes:
447
+ raise HTTPException(400, f"Invalid mode '{mode}'. Must be 'normal' or 'fun'.")
 
 
 
448
  if mode in modes:
449
  inputMode = mode
450
 
 
454
  if len(image_urls) > 2:
455
  raise HTTPException(400, "You may provide at most two image URLs")
456
 
 
457
  try:
458
  duration = max(1, min(10, int(duration)))
459
  except (TypeError, ValueError):
460
  duration = 5
461
 
462
  prompt = normalize_prompt_value(prompt, "prompt")
463
+ enforce_prompt_size(prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt")
 
 
464
  await check_video_rate_limit(request, authorization, x_client_id)
465
 
466
+ RATIO_MAP = {"3:2": "16:9", "2:3": "9:16", "1:1": "9:16"}
 
 
 
 
467
  pollinations_ratio = RATIO_MAP.get(aspectRatio, "16:9")
468
 
469
  encoded_prompt = quote(prompt, safe="")
 
478
 
479
  if image_urls:
480
  processed_urls = []
 
481
  for img in image_urls[:2]:
482
  if is_base64_image(img):
483
  image_id = save_base64_image(img)
484
  temp_assets.append(image_id)
 
485
  served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
486
  processed_urls.append(served_url)
487
  else:
488
  processed_urls.append(img)
 
489
  params["image"] = "|".join(processed_urls)
490
 
491
  if inputMode == "fun":
492
  params["enhance"] = "true"
493
 
494
  query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
495
+ url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}&key={PKEY}"
 
496
  print(f"[VIDEO GEN] Pollinations URL: {url}")
497
+
498
  resp = None
499
  try:
500
  async with httpx.AsyncClient(timeout=600) as client:
 
502
  finally:
503
  for aid in temp_assets:
504
  cleanup_image(aid)
505
+
506
  if resp is None:
507
  raise HTTPException(502, "Video generation request failed")
508
+
509
  if resp.status_code != 200:
510
  body_text = ""
511
  try:
 
534
  },
535
  )
536
 
537
+
538
+ # ──────────────────────────────────────────────
539
+ # VIDEO GENERATION (Airforce)
540
+ # ──────────────────────────────────────────────
541
+
542
  @router.get("/video/airforce/{prompt}")
543
  @router.post("/video/airforce")
544
  async def genvideo_airforce(
 
551
  return Response(
552
  status_code=200,
553
  headers={
 
554
  "Y-prompt": "string — required. The text prompt used to generate the video.",
 
555
  "Y-ratio": "string — optional. Aspect ratio of the output video.",
556
  "Y-ratio-values": "3:2,2:3,1:1",
557
  "Y-ratio-default": "3:2",
 
562
  "Y-duration-default": "5",
563
  "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
564
  "Y-image_urls-max": "2",
 
565
  "Y-response_format": "video/mp4",
 
566
  "Y-model": "grok-imagine-video",
567
  },
568
  )
 
570
  aspectRatio = "3:2"
571
  inputMode = "normal"
572
  image_urls = None
 
 
573
 
 
574
  if prompt is None:
575
  user_body = await request.json()
576
  prompt = user_body.get("prompt")
 
579
  image_urls = user_body.get("image_urls")
580
 
581
  if ratio not in valid_ratios:
582
+ raise HTTPException(400, f"Invalid aspect ratio {ratio}. Must be one of 3:2, 2:3, or 1:1. Default is 3:2")
 
 
 
583
  if ratio in ratios:
584
  aspectRatio = ratio
585
 
586
  if mode not in valid_modes:
587
+ raise HTTPException(400, f"Invalid mode {mode}. Must be 'normal' or 'fun'. Default is normal")
 
 
 
588
  if mode in modes:
589
  inputMode = mode
590
 
591
  if image_urls:
592
  if not isinstance(image_urls, list):
593
  raise HTTPException(400, "image_urls must be a list")
 
594
  if len(image_urls) > 2:
595
  raise HTTPException(400, "You may provide at most two image URLs")
596
 
597
  prompt = normalize_prompt_value(prompt, "prompt")
598
+ enforce_prompt_size(prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt")
 
 
599
  await check_video_rate_limit(request, authorization, x_client_id)
600
 
601
  payload = {
 
615
  async with httpx.AsyncClient(timeout=600) as client:
616
  resp = await client.post(
617
  AIRFORCE_API_URL,
618
+ headers={"Authorization": f"Bearer {AIRFORCE_KEY}", "Content-Type": "application/json"},
 
 
 
619
  json=payload,
620
  )
621
 
 
644
  "Accept-Ranges": "bytes",
645
  },
646
  )
647
+
648
+
649
+ # ──────────────────────────────────────────────
650
+ # CHAT COMPLETIONS (/gen/chat/completions)
651
+ # ──────────────────────────────────────────────
652
+
653
+ async def _check_chat_rate_limit(
654
+ request: Request,
655
+ authorization: Optional[str],
656
+ client_id: Optional[str] = None,
657
+ ):
658
+ return await enforce_rate_limit(request, authorization, "cloudChatDaily", client_id)
659
+
660
+
661
  @router.post("/chat/completions")
662
  async def generate_text(
663
  request: Request,
 
669
  if not isinstance(messages, list) or len(messages) == 0:
670
  raise HTTPException(400, "messages[] is required")
671
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  uses_tools = (
673
  "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
674
  ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
675
 
676
+ chosen_model, provider = route_chat(messages, uses_tools=uses_tools)
677
+ _log_routing(chosen_model, provider, messages, uses_tools)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
+ await _check_chat_rate_limit(request, authorization, x_client_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
  body["model"] = chosen_model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  stream = body.get("stream", False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
+ url, api_key = _get_provider_url_and_key(provider)
685
+ headers = {"Authorization": f"Bearer {api_key}"}
686
 
687
  if stream:
688
  body["stream"] = True
689
+
690
+ async def stream_fallback(client: httpx.AsyncClient):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  fallback_body = {
692
+ "model": FALLBACK_MODEL,
693
  "messages": body["messages"],
694
  "stream": True,
695
  }
696
+ fb_url, fb_key = _get_provider_url_and_key(FALLBACK_PROVIDER)
697
+ fb_headers = {"Authorization": f"Bearer {fb_key}"}
 
 
 
698
  print("[FALLBACK] Starting Groq fallback stream")
699
+
700
+ async with client.stream("POST", fb_url, json=fallback_body, headers=fb_headers) as r:
 
 
 
 
 
 
701
  if r.status_code >= 400:
702
  err = (await r.aread()).decode("utf-8", errors="replace")
703
  yield f'data: {{"error": "Fallback provider failed: {err[:500]}"}}\n\n'
704
  return
 
705
  async for line in r.aiter_lines():
706
  if not line:
707
  yield "\n"
708
  continue
709
+ yield (line if line.startswith("data:") else f"data: {line}\n\n") + "\n"
710
+
711
+ async def stream_primary(client: httpx.AsyncClient):
712
+ try:
713
+ async with client.stream("POST", url, json=body, headers=headers) as r:
714
+ if r.status_code >= 400:
715
+ print("[STREAM FALLBACK] Primary provider failed → switching to fallback")
716
+ async for chunk in stream_fallback(client):
717
+ yield chunk
718
+ return
719
+
720
+ async for line in r.aiter_lines():
721
+ if not line:
722
+ yield "\n"
723
+ continue
724
+ if line.startswith("data:"):
725
+ try:
726
+ obj = json.loads(line[5:].strip())
727
+ if isinstance(obj, dict) and isinstance(obj.get("error"), dict):
728
+ async for chunk in stream_fallback(client):
729
+ yield chunk
730
+ return
731
+ except Exception:
732
+ pass
733
  yield line + "\n"
734
+ except Exception as e:
735
+ print(f"[STREAM ERROR] {e}")
736
+ async for chunk in stream_fallback(client):
737
+ yield chunk
738
+
739
  async def event_generator():
740
  sent_metadata = False
 
741
  async with httpx.AsyncClient(timeout=None) as client:
742
+ async for chunk in stream_primary(client):
 
743
  if not sent_metadata:
744
+ meta = {"router_metadata": {"model_name": MODEL_MAP.get(chosen_model, chosen_model)}}
 
 
 
 
745
  yield f"data: {json.dumps(meta)}\n\n"
746
  sent_metadata = True
 
747
  yield chunk
748
 
749
  return StreamingResponse(
750
  event_generator(),
751
  media_type="text/event-stream",
752
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
 
 
 
 
753
  )
754
 
755
+ # ── non-streaming ─────────────────────────
756
+ async with httpx.AsyncClient(timeout=None) as client:
757
+ r = await client.post(url, json=body, headers=headers)
758
+
759
+ # navy-vision fallback
760
+ if provider == "navy vision" and r.status_code >= 400:
761
+ print("[FALLBACK] Navy vision failed — switching to fallback")
762
+ fb_url, fb_key = _get_provider_url_and_key(FALLBACK_PROVIDER)
763
+ fallback_body = dict(body)
764
+ fallback_body["model"] = FALLBACK_MODEL
765
+ r = await client.post(fb_url, json=fallback_body, headers={"Authorization": f"Bearer {fb_key}"})
766
+
767
+ content_type = (r.headers.get("content-type") or "").lower()
768
+ if "application/json" in content_type:
769
+ try:
770
+ payload = r.json()
771
+ except Exception:
772
+ payload = {"error": "Upstream returned invalid JSON"}
773
  else:
774
+ payload = {
775
+ "error": "Upstream returned non-JSON response",
776
+ "status_code": r.status_code,
777
+ "message": r.text[:1000],
778
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
 
780
+ return JSONResponse(status_code=r.status_code, content=payload)
781
 
782
+
783
+ # ──────────────────────────────────────────────
784
+ # PROMPT ANALYZE (/gen/prompt_analyze)
785
+ # ──────────────────────────────────────────────
786
 
787
  @router.post("/prompt_analyze")
788
+ async def analyze_prompt(request: Request):
 
 
789
  body = await request.json()
790
  messages = body.get("prompt", [])
791
  if not isinstance(messages, list) or len(messages) == 0:
792
  raise HTTPException(400, "messages[] is required")
793
 
 
 
 
794
  uses_tools = (
795
  "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
796
  ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
797
 
798
+ chosen_model, _ = route_chat(messages, uses_tools=uses_tools)
799
+ return {MODEL_MAP.get(chosen_model, chosen_model)}
 
 
 
 
 
 
800
 
 
 
801
 
802
+ # ──────────────────────────────────────────────
803
+ # MODELS LIST
804
+ # ──────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
  @router.get("/models")
807
  def return_models_openai():
808
  return {
809
+ "object": "list",
810
+ "data": [
811
+ {
812
+ "id": "lightning",
813
+ "object": "model",
814
+ "created": 1767225600,
815
+ "owned_by": "inferenceport-ai",
816
+ }
817
+ ],
818
  }
819
 
 
 
 
 
 
 
 
820
 
821
+ # ──────────────────────────────────────────────
822
+ # RESPONSES API (/gen/responses)
823
+ # ──────────────────────────────────────────────
824
 
825
  def _resp_id(prefix: str) -> str:
826
  return f"{prefix}_{uuid4().hex}"
 
834
  if isinstance(content, list):
835
  parts = []
836
  for item in content:
837
+ if isinstance(item, dict) and item.get("type") in ("input_text", "output_text", "text"):
838
+ txt = item.get("text")
839
+ if isinstance(txt, str):
840
+ parts.append(txt)
 
 
841
  return "".join(parts)
842
  return ""
843
 
844
+ def _responses_input_to_messages(
845
+ input_data: Any,
846
+ instructions: Optional[str] = None,
847
+ ) -> List[Dict[str, Any]]:
848
  messages: List[Dict[str, Any]] = []
849
  if instructions:
850
  messages.append({"role": "developer", "content": instructions})
 
861
  if not isinstance(item, dict):
862
  continue
863
  role = item.get("role", "user")
864
+ text = _content_to_text(item.get("content", ""))
 
865
  if text:
866
  messages.append({"role": role, "content": text})
867
 
868
  return messages
869
 
870
+ def _build_responses_payload(
871
+ model: str,
872
+ text: str,
873
+ response_id: str,
874
+ input_tokens: int = 0,
875
+ output_tokens: int = 0,
876
+ ) -> Dict[str, Any]:
877
  return {
878
+ "id": response_id,
879
  "object": "response",
880
  "created_at": _resp_ts(),
881
  "status": "completed",
 
891
  "type": "message",
892
  "role": "assistant",
893
  "status": "completed",
894
+ "content": [{"type": "output_text", "text": text, "annotations": []}],
 
 
 
 
 
 
895
  }
896
  ],
897
  "output_text": text,
898
  "usage": {
899
  "input_tokens": input_tokens,
900
  "output_tokens": output_tokens,
901
+ "total_tokens": input_tokens + output_tokens,
902
+ },
903
  }
904
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
 
906
  @router.post("/responses")
907
  async def create_responses(
908
  request: Request,
909
  authorization: Optional[str] = Header(None),
910
+ x_client_id: Optional[str] = Header(None),
911
  ):
912
  body = await request.json()
913
  model = body.get("model")
914
  input_data = body.get("input")
915
  instructions = body.get("instructions")
916
  stream = body.get("stream", True)
 
917
 
918
  if not model:
919
+ raise HTTPException(400, "model is required")
920
  if input_data is None:
921
+ raise HTTPException(400, "input is required")
922
 
923
  messages = _responses_input_to_messages(input_data, instructions=instructions)
924
  if not messages:
925
+ raise HTTPException(400, "input could not be parsed")
926
+
927
+ # ── shared helper: route + call + return (text, input_tokens, output_tokens) ──
928
+ async def _generate() -> Tuple[str, int, int]:
929
+ chosen_model, provider = route_chat(messages)
930
+ await _check_chat_rate_limit(request, authorization, x_client_id)
931
+ data = await call_chat_completions(messages, chosen_model, provider)
932
+ text = _extract_text_from_response(data)
933
+ input_tokens, output_tokens = _extract_usage(data)
934
+ return text, input_tokens, output_tokens
935
+
936
+ # ── non-streaming ─────────────────────────
937
  if stream is False:
938
+ text, input_tokens, output_tokens = await _generate()
939
+ response_id = _resp_id("resp")
940
+ return JSONResponse(
941
+ content=_build_responses_payload(model, text, response_id, input_tokens, output_tokens)
 
942
  )
 
 
 
943
 
944
+ # ── streaming ─────────────────────────────
945
  async def event_stream():
946
  response_id = _resp_id("resp")
947
+
948
+ created_evt = {
949
  "type": "response.created",
950
  "response": {
951
  "id": response_id,
952
  "object": "response",
953
  "created_at": _resp_ts(),
954
  "status": "in_progress",
955
+ "model": model,
956
+ },
957
  }
958
+ yield f"data: {json.dumps(created_evt)}\n\n"
959
 
960
+ try:
961
+ text, input_tokens, output_tokens = await _generate()
962
+ except HTTPException as exc:
963
+ err_evt = {"type": "response.error", "error": {"message": exc.detail}}
964
+ yield f"data: {json.dumps(err_evt)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965
  yield "data: [DONE]\n\n"
966
  return
967
 
968
+ # Stream text in chunks
969
+ chunk_size = 64
970
+ for i in range(0, len(text), chunk_size):
971
+ delta_evt = {
972
+ "type": "response.output_text.delta",
973
+ "response_id": response_id,
974
+ "delta": text[i : i + chunk_size],
975
+ }
976
+ yield f"data: {json.dumps(delta_evt)}\n\n"
977
+
978
+ completed_evt = {
979
+ "type": "response.completed",
980
+ "response": _build_responses_payload(model, text, response_id, input_tokens, output_tokens),
981
  }
982
+ yield f"data: {json.dumps(completed_evt)}\n\n"
983
  yield "data: [DONE]\n\n"
984
 
985
  return StreamingResponse(
986
  event_stream(),
987
  media_type="text/event-stream",
988
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
 
 
 
 
989
  )