Mohan-Ai / app.py
Senpai315's picture
Update app.py
945e9f4 verified
Raw
History Blame Contribute Delete
62.8 kB
import os
import secrets
import httpx
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, HTMLResponse
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# ── Auth ──────────────────────────────────────────────────────────────────────
MOHAN_API_KEYS_RAW = os.environ.get("MOHAN_API_KEY", "")
VALID_KEYS = set(k.strip() for k in MOHAN_API_KEYS_RAW.split(",") if k.strip())
def verify_key(key):
return any(secrets.compare_digest(key.strip(), v) for v in VALID_KEYS)
# ── Provider Keys (A = primary, B = backup) ───────────────────────────────────
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
GEMINI_API_KEY_B = os.environ.get("GEMINI_API_KEY_B", "")
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
GROQ_API_KEY_B = os.environ.get("GROQ_API_KEY_B", "")
CEREBRAS_API_KEY = os.environ.get("CEREBRAS_API_KEY", "")
CEREBRAS_API_KEY_B = os.environ.get("CEREBRAS_API_KEY_B", "")
MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY", "")
MISTRAL_API_KEY_B = os.environ.get("MISTRAL_API_KEY_B", "")
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
NVIDIA_API_KEY_B = os.environ.get("NVIDIA_API_KEY_B", "")
GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
GROQ_BASE = "https://api.groq.com/openai/v1"
CEREBRAS_BASE = "https://api.cerebras.ai/v1"
MISTRAL_BASE = "https://api.mistral.ai/v1"
NVIDIA_BASE = "https://integrate.api.nvidia.com/v1"
# ── Models List (A = primary, B = backup with _b prefix) ─────────────────────
ALL_MODELS = [
# ── Gemini (A) ──
{"id": "gemini/gemini-2.5-flash", "name": "Gemini 2.5 Flash", "provider": "Gemini"},
{"id": "gemini/gemini-2.5-flash-lite", "name": "Gemini 2.5 Flash Lite", "provider": "Gemini"},
{"id": "gemini/gemini-3-flash-preview", "name": "Gemini 3 Flash Preview", "provider": "Gemini"},
# ── Gemini (B) ──
{"id": "gemini_b/gemini-2.5-flash", "name": "Gemini 2.5 Flash [B]", "provider": "Gemini-B"},
{"id": "gemini_b/gemini-2.5-flash-lite", "name": "Gemini 2.5 Flash Lite [B]", "provider": "Gemini-B"},
{"id": "gemini_b/gemini-3-flash-preview", "name": "Gemini 3 Flash Preview [B]", "provider": "Gemini-B"},
# ── Groq (A) ──
{"id": "groq/llama-3.1-8b-instant", "name": "LLaMA 3.1 8B Instant", "provider": "Groq"},
{"id": "groq/meta-llama/llama-4-scout-17b-16e-instruct", "name": "LLaMA 4 Scout 17B", "provider": "Groq"},
# ── Groq (B) ──
{"id": "groq_b/llama-3.1-8b-instant", "name": "LLaMA 3.1 8B Instant [B]","provider": "Groq-B"},
{"id": "groq_b/meta-llama/llama-4-scout-17b-16e-instruct", "name": "LLaMA 4 Scout 17B [B]", "provider": "Groq-B"},
# ── Cerebras (A) ──
{"id": "cerebras/gemma-4-31b", "name": "Gemma 4 31B (Cerebras ⚑)", "provider": "Cerebras"},
# ── Cerebras (B) ──
{"id": "cerebras_b/gemma-4-31b", "name": "Gemma 4 31B (Cerebras ⚑) [B]", "provider": "Cerebras-B"},
# ── Mistral (A) ──
{"id": "mistral/mistral-small-latest", "name": "Mistral Small 4", "provider": "Mistral"},
{"id": "mistral/open-mistral-nemo", "name": "Mistral Nemo", "provider": "Mistral"},
{"id": "mistral/ministral-8b-latest", "name": "Ministral 8B", "provider": "Mistral"},
{"id": "mistral/codestral-latest", "name": "Codestral (Code)", "provider": "Mistral"},
# ── Mistral (B) ──
{"id": "mistral_b/mistral-small-latest", "name": "Mistral Small 4 [B]", "provider": "Mistral-B"},
{"id": "mistral_b/open-mistral-nemo", "name": "Mistral Nemo [B]", "provider": "Mistral-B"},
{"id": "mistral_b/ministral-8b-latest", "name": "Ministral 8B [B]", "provider": "Mistral-B"},
{"id": "mistral_b/codestral-latest", "name": "Codestral (Code) [B]", "provider": "Mistral-B"},
# ── NVIDIA (A) ──
{"id": "nvidia/meta/llama-3.1-8b-instruct", "name": "LLaMA 3.1 8B (NIM)", "provider": "NVIDIA"},
{"id": "nvidia/meta/llama-3.3-70b-instruct", "name": "LLaMA 3.3 70B (NIM)", "provider": "NVIDIA"},
{"id": "nvidia/mistralai/mistral-small-4-119b-2603", "name": "Mistral Small 4 119B (NIM)", "provider": "NVIDIA"},
{"id": "nvidia/qwen/qwen2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (NIM)", "provider": "NVIDIA"},
{"id": "nvidia/deepseek-ai/deepseek-r1-distill-llama-8b", "name": "DeepSeek R1 8B (NIM)", "provider": "NVIDIA"},
# ── NVIDIA (B) ──
{"id": "nvidia_b/meta/llama-3.1-8b-instruct", "name": "LLaMA 3.1 8B (NIM) [B]", "provider": "NVIDIA-B"},
{"id": "nvidia_b/meta/llama-3.3-70b-instruct", "name": "LLaMA 3.3 70B (NIM) [B]", "provider": "NVIDIA-B"},
{"id": "nvidia_b/mistralai/mistral-small-4-119b-2603", "name": "Mistral Small 4 119B (NIM) [B]","provider": "NVIDIA-B"},
{"id": "nvidia_b/qwen/qwen2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (NIM) [B]","provider": "NVIDIA-B"},
{"id": "nvidia_b/deepseek-ai/deepseek-r1-distill-llama-8b", "name": "DeepSeek R1 8B (NIM) [B]", "provider": "NVIDIA-B"},
]
# ── Provider detection helpers ────────────────────────────────────────────────
def is_groq(m): return m.startswith("groq/")
def is_groq_b(m): return m.startswith("groq_b/")
def is_gemini(m): return m.startswith("gemini/")
def is_gemini_b(m): return m.startswith("gemini_b/")
def is_cerebras(m): return m.startswith("cerebras/")
def is_cerebras_b(m): return m.startswith("cerebras_b/")
def is_mistral(m): return m.startswith("mistral/")
def is_mistral_b(m): return m.startswith("mistral_b/")
def is_nvidia(m): return m.startswith("nvidia/")
def is_nvidia_b(m): return m.startswith("nvidia_b/")
def strip_prefix(m): return m.split("/", 1)[1]
def is_quota_error(status_code: int, body: dict) -> bool:
"""Detect quota/rate-limit errors from any provider."""
if status_code == 429:
return True
# Gemini quota error
if status_code == 200:
return False
error_msg = str(body.get("error", "")).lower()
if any(k in error_msg for k in ["quota", "rate limit", "rate_limit", "exceeded", "too many"]):
return True
return False
# ── Gemini format helpers ─────────────────────────────────────────────────────
def openai_messages_to_gemini(messages):
contents = []
for m in messages:
role = "user" if m["role"] == "user" else "model"
content = m["content"]
if isinstance(content, str):
parts = [{"text": content}]
elif isinstance(content, list):
parts = []
for item in content:
if item.get("type") == "text":
parts.append({"text": item["text"]})
elif item.get("type") == "image_url":
url = item["image_url"]["url"]
if url.startswith("data:"):
header, b64data = url.split(",", 1)
mime = header.split(":")[1].split(";")[0]
parts.append({"inline_data": {"mime_type": mime, "data": b64data}})
else:
parts = [{"text": str(content)}]
contents.append({"role": role, "parts": parts})
return contents
def gemini_response_to_openai(r, model_id):
try: text = r["candidates"][0]["content"]["parts"][0]["text"]
except: text = r.get("error", {}).get("message", "No response")
return {
"id": "gemini-chat", "object": "chat.completion", "model": model_id,
"choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": r.get("usageMetadata", {}).get("promptTokenCount", 0),
"completion_tokens": r.get("usageMetadata", {}).get("candidatesTokenCount", 0),
"total_tokens": r.get("usageMetadata", {}).get("totalTokenCount", 0),
}
}
# ── Generic OpenAI-compatible caller with Aβ†’B fallback ───────────────────────
async def call_openai_compat(base_url: str, key_a: str, key_b: str, model_name: str, body: dict):
"""Try key_a first; if quota error, automatically fallback to key_b."""
payload = {**body, "model": model_name}
async with httpx.AsyncClient(timeout=60) as client:
# ── Try A ──
if key_a:
resp = await client.post(
f"{base_url}/chat/completions",
headers={"Authorization": f"Bearer {key_a}", "Content-Type": "application/json"},
json=payload
)
data = resp.json()
if not is_quota_error(resp.status_code, data):
return JSONResponse(content=data, status_code=resp.status_code)
# ── Fallback to B ──
if key_b:
resp = await client.post(
f"{base_url}/chat/completions",
headers={"Authorization": f"Bearer {key_b}", "Content-Type": "application/json"},
json=payload
)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
return JSONResponse({"error": "Both A and B keys failed or not configured."}, status_code=429)
# ── Gemini caller with Aβ†’B fallback ──────────────────────────────────────────
async def call_gemini(model_name: str, key_a: str, key_b: str, body: dict, model_id: str):
payload = {
"contents": openai_messages_to_gemini(body.get("messages", [])),
"generationConfig": {
"maxOutputTokens": body.get("max_tokens", 1024),
"temperature": body.get("temperature", 1.0)
}
}
async with httpx.AsyncClient(timeout=60) as client:
# ── Try A ──
if key_a:
url = f"{GEMINI_BASE}/{model_name}:generateContent?key={key_a}"
resp = await client.post(url, headers={"Content-Type": "application/json"}, json=payload)
data = resp.json()
if not is_quota_error(resp.status_code, data):
return JSONResponse(content=gemini_response_to_openai(data, model_id), status_code=200)
# ── Fallback to B ──
if key_b:
url = f"{GEMINI_BASE}/{model_name}:generateContent?key={key_b}"
resp = await client.post(url, headers={"Content-Type": "application/json"}, json=payload)
return JSONResponse(content=gemini_response_to_openai(resp.json(), model_id), status_code=200)
return JSONResponse({"error": "Both Gemini A and B keys failed or not configured."}, status_code=429)
# ── HTML UI ───────────────────────────────────────────────────────────────────
HTML_UI = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>Mohan AI</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
:root{
--bg:#060612;--surface:#0b0b1e;--surface2:#10102a;--surface3:#161634;
--border:#1e1e40;--border2:#2a2a55;
--text:#f0f0ff;--text2:#8888bb;--muted:#4a4a7a;
--a:#6c63ff;--a2:#9d97ff;--b:#ff6b9d;--c:#00d4aa;
--a-dim:#6c63ff15;--b-dim:#ff6b9d15;--c-dim:#00d4aa15;
--groq:#f97316;--gemini:#3b82f6;--or:#8b5cf6;
--cerebras:#10b981;--mistral:#ec4899;--nvidia:#22c55e;
--radius:14px;
}
::-webkit-scrollbar{width:3px;height:3px}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;height:100dvh;display:flex;flex-direction:column;overflow:hidden;position:relative}
.ambient-wrap{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden}
.orb{position:absolute;border-radius:50%;filter:blur(80px);opacity:0.18}
.orb-1{width:600px;height:600px;background:radial-gradient(circle,#6c63ff,transparent 70%);top:-200px;left:-150px;animation:floatA 22s ease-in-out infinite alternate}
.orb-2{width:500px;height:500px;background:radial-gradient(circle,#ff6b9d,transparent 70%);bottom:-180px;right:-120px;animation:floatB 28s ease-in-out infinite alternate}
.orb-3{width:400px;height:400px;background:radial-gradient(circle,#00d4aa,transparent 70%);top:35%;left:45%;transform:translate(-50%,-50%);animation:floatC 19s ease-in-out infinite alternate}
@keyframes floatA{0%{transform:translate(0,0) scale(1)}100%{transform:translate(100px,80px) scale(1.2)}}
@keyframes floatB{0%{transform:translate(0,0) scale(1)}100%{transform:translate(-80px,-100px) scale(1.25)}}
@keyframes floatC{0%{transform:translate(-50%,-50%) scale(1)}100%{transform:translate(-50%,-50%) scale(1.4)}}
.particles{position:absolute;inset:0}
.particle{position:absolute;width:2px;height:2px;border-radius:50%;animation:rise linear infinite}
@keyframes rise{0%{transform:translateY(100vh) translateX(0);opacity:0}10%{opacity:1}90%{opacity:.6}100%{transform:translateY(-10vh) translateX(var(--dx,30px));opacity:0}}
.grid-lines{position:absolute;inset:0;background-image:linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black 30%,transparent 100%);opacity:.25;animation:gridPulse 8s ease-in-out infinite}
@keyframes gridPulse{0%,100%{opacity:.15}50%{opacity:.3}}
.scan-line{position:absolute;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,#6c63ff60,#00d4aa60,transparent);animation:scan 12s linear infinite;opacity:.5}
@keyframes scan{0%{top:-2px;opacity:0}5%{opacity:1}95%{opacity:1}100%{top:100%;opacity:0}}
body::after{content:'';position:fixed;inset:0;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
#login-gate{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:999;background:var(--bg)}
.login-card{position:relative;z-index:1;background:linear-gradient(145deg,var(--surface2),var(--surface));border:1px solid var(--border2);border-radius:24px;padding:44px 40px;width:100%;max-width:420px;box-shadow:0 40px 100px #00000090,0 0 0 1px #ffffff05,inset 0 1px 0 #ffffff08;animation:cardIn .6s cubic-bezier(.16,1,.3,1)}
@keyframes cardIn{from{opacity:0;transform:translateY(28px) scale(.97)}to{opacity:1;transform:none}}
.login-logo-wrap{display:flex;flex-direction:column;align-items:center;margin-bottom:28px;gap:14px}
.m-logo-static{position:relative;width:72px;height:72px;display:flex;align-items:center;justify-content:center}
.m-logo-bg{position:absolute;inset:0;border-radius:20px;background:linear-gradient(135deg,#6c63ff,#ff6b9d,#00d4aa);padding:2px}
.m-logo-bg::before{content:'';position:absolute;inset:0;border-radius:20px;background:linear-gradient(135deg,#6c63ff,#ff6b9d,#00d4aa);filter:blur(16px);opacity:.6;animation:logoPulse 3s ease-in-out infinite}
@keyframes logoPulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:.8;transform:scale(1.15)}}
.m-logo-inner{position:relative;z-index:1;width:68px;height:68px;border-radius:19px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-family:'Outfit',sans-serif;font-weight:800;font-size:1.8rem;color:#ffffff}
.ring{position:absolute;border:1px solid;border-radius:50%;animation:ringExpand 2.4s ease-out infinite;pointer-events:none}
.ring-1{width:90px;height:90px;border-color:#6c63ff50;animation-delay:0s}
.ring-2{width:110px;height:110px;border-color:#ff6b9d30;animation-delay:.6s}
.ring-3{width:130px;height:130px;border-color:#00d4aa20;animation-delay:1.2s}
@keyframes ringExpand{0%{transform:scale(.8);opacity:0}30%{opacity:1}100%{transform:scale(1);opacity:0}}
.login-title{font-size:1.8rem;font-weight:800;letter-spacing:-.5px;background:linear-gradient(135deg,#c4b5fd,#f9a8d4,#67e8f9);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.login-sub{color:var(--text2);font-size:.82rem;font-weight:300;margin-top:2px}
.login-divider{height:1px;background:linear-gradient(90deg,transparent,var(--border2),transparent);margin:20px 0}
.login-label{font-size:.65rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;margin-bottom:8px;display:block}
.login-input-wrap{position:relative;margin-bottom:16px}
.login-input{width:100%;padding:14px 48px 14px 18px;background:var(--surface3);border:1px solid var(--border2);border-radius:12px;color:var(--text);font-family:'Outfit',sans-serif;font-size:.9rem;outline:none;transition:all .25s;letter-spacing:2px}
.login-input:focus{border-color:var(--a);box-shadow:0 0 0 3px #6c63ff18,0 0 24px #6c63ff0c}
.login-eye{position:absolute;right:14px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;font-size:.9rem;padding:4px;transition:color .2s}
.login-eye:hover{color:var(--a2)}
.login-error{background:#ff444410;border:1px solid #ff444430;border-radius:10px;padding:10px 14px;font-size:.8rem;color:#ff6b6b;margin-bottom:14px;display:none;align-items:center;gap:8px}
.login-error.show{display:flex}
.login-btn{width:100%;padding:15px;background:linear-gradient(135deg,#6c63ff,#9d97ff);border:none;border-radius:12px;color:#fff;font-family:'Outfit',sans-serif;font-size:.92rem;font-weight:700;cursor:pointer;transition:all .3s;letter-spacing:.3px;position:relative;overflow:hidden;box-shadow:0 8px 32px #6c63ff30}
.login-btn:hover{transform:translateY(-2px);box-shadow:0 0 0 1px #9d97ff40,0 12px 40px #6c63ff50}
.login-btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
.shake{animation:shake .4s ease}
@keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-8px)}40%,80%{transform:translateX(8px)}}
#app{display:none;flex:1;flex-direction:column;overflow:hidden;height:100%;position:relative;z-index:1}
#app.visible{display:flex}
#topbar{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border);background:rgba(6,6,18,.8);backdrop-filter:blur(24px);position:relative;z-index:10;flex-shrink:0}
#logo{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;background:linear-gradient(135deg,#c4b5fd,#f9a8d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;white-space:nowrap}
#model-selector-btn{display:flex;align-items:center;gap:8px;padding:8px 14px;background:var(--surface2);border:1px solid var(--border2);border-radius:10px;color:var(--text);font-family:'Outfit',sans-serif;font-size:.78rem;font-weight:500;cursor:pointer;transition:all .25s;flex:1;min-width:0;max-width:260px}
#model-selector-btn:hover{border-color:var(--a);background:var(--a-dim)}
#model-selector-btn .provider-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
#model-selector-btn .model-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
#model-selector-btn .chevron{color:var(--muted);font-size:.65rem;flex-shrink:0}
.live-indicator{display:flex;align-items:center;gap:6px;margin-left:auto;font-size:.65rem;font-weight:700;color:var(--c);letter-spacing:.5px;text-transform:uppercase}
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--c);box-shadow:0 0 10px var(--c);animation:livePulse 2s ease-in-out infinite}
@keyframes livePulse{0%,100%{box-shadow:0 0 6px var(--c);opacity:1}50%{box-shadow:0 0 16px var(--c),0 0 28px var(--c);opacity:.7}}
#drawer-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(10px);z-index:100}
#drawer-overlay.open{display:block}
#model-drawer{position:fixed;bottom:0;left:0;right:0;max-height:78dvh;background:var(--surface);border-top:1px solid var(--border2);border-radius:24px 24px 0 0;z-index:101;display:flex;flex-direction:column;transform:translateY(100%);transition:transform .35s cubic-bezier(.16,1,.3,1);box-shadow:0 -20px 80px #00000090}
#model-drawer.open{transform:translateY(0)}
.drawer-pill{width:36px;height:3px;background:var(--border2);border-radius:2px;margin:12px auto 0}
.drawer-title{padding:14px 20px 10px;font-family:'Outfit',sans-serif;font-size:.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;flex-shrink:0}
#drawer-search{margin:0 16px 12px;padding:10px 14px;background:var(--surface2);border:1px solid var(--border2);border-radius:10px;color:var(--text);font-family:'Outfit',sans-serif;font-size:.85rem;outline:none;flex-shrink:0}
#drawer-search:focus{border-color:var(--a)}
#drawer-list{flex:1;overflow-y:auto;padding:0 12px 32px}
.provider-group-label{font-size:.6rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;padding:12px 10px 6px}
.model-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:12px;cursor:pointer;transition:all .18s;border:1px solid transparent;margin-bottom:2px}
.model-item:hover{background:var(--surface2);border-color:var(--border2)}
.model-item.active{background:var(--a-dim);border-color:var(--a)}
.model-item .m-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.model-item .m-name{font-size:.83rem;font-weight:500;flex:1}
.model-item .m-badge{font-size:.6rem;font-weight:600;padding:2px 7px;border-radius:5px;background:var(--surface3);color:var(--muted);border:1px solid var(--border)}
.model-item.active .m-name{color:#c4b5fd}
#messages{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:18px;position:relative;z-index:1;scroll-behavior:smooth}
.welcome{margin:auto;text-align:center;padding:40px 24px;animation:fadeUp .7s cubic-bezier(.16,1,.3,1)}
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
.welcome-logo{position:relative;width:80px;height:80px;margin:0 auto 24px;display:flex;align-items:center;justify-content:center}
.welcome-logo-bg{position:absolute;inset:0;border-radius:22px;background:linear-gradient(135deg,#6c63ff,#ff6b9d,#00d4aa);padding:2px}
.welcome-logo-inner{position:relative;z-index:1;width:76px;height:76px;border-radius:21px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-family:'Outfit',sans-serif;font-weight:800;font-size:2rem;color:#ffffff}
.welcome-title{font-family:'Outfit',sans-serif;font-size:1.9rem;font-weight:800;letter-spacing:-.5px;background:linear-gradient(135deg,#c4b5fd,#f9a8d4,#67e8f9);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
.welcome-sub{color:var(--text2);font-size:.85rem;font-weight:300;line-height:1.9;margin-bottom:24px}
.welcome-chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
.chip{padding:8px 16px;border-radius:20px;background:var(--surface2);border:1px solid var(--border2);font-size:.76rem;color:var(--text2);cursor:pointer;transition:all .25s;font-family:'Outfit',sans-serif}
.chip:hover{background:var(--a-dim);border-color:var(--a);color:#c4b5fd;transform:translateY(-2px)}
.msg-row{display:flex;flex-direction:column;gap:4px;animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
@keyframes msgIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.msg-row.user{align-items:flex-end}
.msg-row.ai{align-items:flex-start}
.msg-meta{font-size:.62rem;font-weight:600;color:var(--muted);letter-spacing:.5px;text-transform:uppercase;padding:0 6px}
.msg{max-width:92%;padding:14px 20px;font-size:.88rem;line-height:1.75;word-break:break-word;white-space:pre-wrap}
.msg.user{background:linear-gradient(135deg,#6c63ff1a,#ff6b9d12);border:1px solid #6c63ff25;border-radius:18px 18px 4px 18px;color:var(--text)}
.msg.ai{background:var(--surface2);border:1px solid var(--border2);border-radius:4px 18px 18px 18px;color:var(--text)}
.typing-row{display:flex;align-items:flex-start}
.typing-bubble{display:flex;align-items:center;gap:5px;padding:14px 18px;background:var(--surface2);border:1px solid var(--border2);border-radius:4px 18px 18px 18px}
.typing-bubble span{width:6px;height:6px;border-radius:50%;animation:typeDot .9s ease-in-out infinite}
.typing-bubble span:nth-child(1){background:var(--a);animation-delay:0s}
.typing-bubble span:nth-child(2){background:var(--b);animation-delay:.2s}
.typing-bubble span:nth-child(3){background:var(--c);animation-delay:.4s}
@keyframes typeDot{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-7px);opacity:1}}
#inputbar{padding:12px 14px 18px;border-top:1px solid var(--border);background:rgba(6,6,18,.85);backdrop-filter:blur(24px);position:relative;z-index:10;flex-shrink:0}
.input-wrapper{background:var(--surface2);border:1px solid var(--border2);border-radius:16px;padding:4px 4px 4px 16px;display:flex;align-items:flex-end;gap:8px;transition:border-color .25s}
.input-wrapper:focus-within{border-color:var(--a);box-shadow:0 0 0 3px #6c63ff10}
#prompt{flex:1;padding:9px 0;background:none;border:none;color:var(--text);font-family:'Outfit',sans-serif;font-size:.9rem;resize:none;outline:none;max-height:120px;line-height:1.6}
#prompt::placeholder{color:var(--muted)}
#send-btn{width:42px;height:42px;flex-shrink:0;background:linear-gradient(135deg,var(--a),#9d97ff);border:none;border-radius:12px;color:#fff;font-size:1rem;cursor:pointer;transition:all .25s;display:flex;align-items:center;justify-content:center;margin-bottom:2px}
#send-btn:hover{transform:scale(1.08)}
#send-btn:disabled{opacity:.3;cursor:not-allowed;transform:none}
.action-bar{display:flex;gap:6px;margin-top:10px;align-items:center;flex-wrap:wrap}
.act-btn{display:flex;align-items:center;gap:5px;padding:5px 11px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text2);font-family:'Outfit',sans-serif;font-size:.71rem;font-weight:500;cursor:pointer;transition:all .2s}
.act-btn:hover{color:var(--text);border-color:var(--border2)}
.act-btn.danger{color:#f87171;border-color:#f8717120}
.act-btn.danger:hover{background:#f8717110}
#token-pill{margin-left:auto;padding:4px 10px;background:var(--surface3);border:1px solid var(--border);border-radius:20px;font-size:.67rem;color:var(--muted);font-family:'JetBrains Mono',monospace}
.dot-Gemini{background:var(--gemini)}.dot-Groq{background:var(--groq)}.dot-OpenRouter{background:var(--or)}
.dot-Cerebras{background:var(--cerebras)}.dot-Mistral{background:var(--mistral)}.dot-NVIDIA{background:var(--nvidia)}
.dot-Gemini-B{background:#1d4ed8}.dot-Groq-B{background:#c2410c}.dot-OpenRouter-B{background:#6d28d9}
.dot-Cerebras-B{background:#047857}.dot-Mistral-B{background:#be185d}.dot-NVIDIA-B{background:#15803d}
@media(min-width:768px){
#app.visible{flex-direction:row}
#topbar,#drawer-overlay,#model-drawer{display:none!important}
#sidebar{width:252px;flex-shrink:0;background:linear-gradient(to bottom,var(--surface),var(--bg));border-right:1px solid var(--border);display:flex!important;flex-direction:column;height:100%;position:relative;z-index:2}
.sb-top{padding:22px 18px 16px;border-bottom:1px solid var(--border)}
.sb-brand{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.sb-logo-mark{position:relative;width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.sb-logo-mark-bg{position:absolute;inset:0;border-radius:10px;background:linear-gradient(135deg,#6c63ff,#ff6b9d,#00d4aa);padding:1.5px}
.sb-logo-mark-inner{position:relative;z-index:1;width:33px;height:33px;border-radius:9px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-family:'Outfit',sans-serif;font-weight:800;font-size:.9rem;color:#ffffff}
.sb-logo{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;background:linear-gradient(135deg,#c4b5fd,#f9a8d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
#sidebar-search{width:100%;padding:9px 13px;background:var(--surface3);border:1px solid var(--border2);border-radius:10px;color:var(--text);font-family:'Outfit',sans-serif;font-size:.82rem;outline:none}
#sidebar-list{flex:1;overflow-y:auto;padding:8px 10px 16px}
.sb-provider-label{font-size:.6rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;padding:12px 10px 6px}
.sb-model{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:all .18s;border:1px solid transparent;margin-bottom:1px}
.sb-model:hover{background:var(--surface2);border-color:var(--border)}
.sb-model.active{background:var(--a-dim);border-color:var(--a)}
.sb-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.sb-name{font-size:.79rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.sb-model.active .sb-name{color:#c4b5fd}
#sidebar-footer{padding:14px 16px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:8px}
.sb-status{display:flex;align-items:center;gap:6px;font-size:.68rem;color:var(--muted)}
.sb-logout{padding:5px 12px;background:var(--surface2);border:1px solid var(--border2);border-radius:7px;color:#f87171;font-family:'Outfit',sans-serif;font-size:.7rem;cursor:pointer}
#main-area{flex:1;display:flex;flex-direction:column;overflow:hidden}
#desktop-topbar{display:flex!important;align-items:center;gap:12px;padding:16px 28px;border-bottom:1px solid var(--border);background:rgba(6,6,18,.8);backdrop-filter:blur(24px);flex-shrink:0}
.dt-model{font-family:'Outfit',sans-serif;font-size:.92rem;font-weight:700;color:var(--text)}
.dt-provider-badge{padding:4px 12px;border-radius:20px;font-size:.68rem;font-weight:600;background:var(--a-dim);color:#c4b5fd;border:1px solid #6c63ff30}
.dt-tokens{margin-left:auto;font-size:.7rem;color:var(--muted);font-family:'JetBrains Mono',monospace}
#messages{padding:28px 32px}
.msg{max-width:88%}
#inputbar{padding:14px 28px 20px}
}
@media(max-width:767px){
#sidebar{display:none}
#main-area{flex:1;display:flex;flex-direction:column;overflow:hidden}
#desktop-topbar{display:none!important}
}
</style>
</head>
<body>
<div class="ambient-wrap">
<div class="grid-lines"></div>
<div class="scan-line"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="particles" id="particles"></div>
</div>
<div id="login-gate">
<div class="login-card">
<div class="login-logo-wrap">
<div style="position:relative;display:flex;align-items:center;justify-content:center">
<div class="ring ring-1"></div><div class="ring ring-2"></div><div class="ring ring-3"></div>
<div class="m-logo-static"><div class="m-logo-bg"></div><div class="m-logo-inner">M</div></div>
</div>
<div><div class="login-title">Mohan AI</div><div class="login-sub">Enter your access key to continue</div></div>
</div>
<div class="login-divider"></div>
<div class="login-error" id="login-error"><span>πŸ”’</span><span id="login-error-msg">Invalid key. Access denied.</span></div>
<label class="login-label">API Access Key</label>
<div class="login-input-wrap">
<input class="login-input" type="password" id="api-key-input" placeholder="mk-β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’" autocomplete="off" spellcheck="false"/>
<button class="login-eye" id="eye-btn" onclick="toggleEye()">πŸ‘</button>
</div>
<button class="login-btn" id="login-btn" onclick="doLogin()">Access Mohan AI β†’</button>
</div>
</div>
<div id="app">
<div id="topbar">
<span id="logo">Mohan AI</span>
<button id="model-selector-btn" onclick="openDrawer()">
<span class="provider-dot" id="topbar-dot"></span>
<span class="model-name" id="topbar-model">Select model</span>
<span class="chevron">β–Ύ</span>
</button>
<div class="live-indicator"><span class="live-dot"></span><span>Live</span></div>
</div>
<div id="sidebar">
<div class="sb-top">
<div class="sb-brand">
<div class="sb-logo-mark"><div class="sb-logo-mark-bg"></div><div class="sb-logo-mark-inner">M</div></div>
<span class="sb-logo">Mohan AI</span>
</div>
<input id="sidebar-search" placeholder="Search models…" oninput="filterSidebar()"/>
</div>
<div id="sidebar-list"></div>
<div id="sidebar-footer">
<div class="sb-status"><span class="live-dot"></span><span id="sb-token-count">0 tokens</span></div>
<button class="sb-logout" onclick="logout()">Sign out</button>
</div>
</div>
<div id="main-area">
<div id="desktop-topbar" style="display:none">
<span class="dt-model" id="desktop-model-name">Select a model</span>
<span class="dt-provider-badge" id="desktop-provider"></span>
<span class="dt-tokens" id="dt-token-count">0 tokens</span>
<div class="live-indicator" style="margin-left:0"><span class="live-dot"></span><span style="font-size:.65rem;font-weight:700;color:var(--c);text-transform:uppercase;letter-spacing:.5px">Live</span></div>
</div>
<div id="messages">
<div class="welcome">
<div class="welcome-logo"><div class="welcome-logo-bg"></div><div class="welcome-logo-inner">M</div></div>
<div class="welcome-title">Mohan AI</div>
<div class="welcome-sub">Powered by 20+ frontier models<br>across Gemini, Groq, Mistral, NVIDIA & more</div>
<div class="welcome-chips">
<div class="chip" onclick="useChip(this)">✨ Write a poem</div>
<div class="chip" onclick="useChip(this)">🧠 Explain quantum computing</div>
<div class="chip" onclick="useChip(this)">πŸ’» Debug my code</div>
<div class="chip" onclick="useChip(this)">🌍 Translate to Hindi</div>
</div>
</div>
</div>
<div id="inputbar">
<div class="input-wrapper">
<textarea id="prompt" rows="1" placeholder="Message Mohan AI…"></textarea>
<button id="send-btn" onclick="send()"><span>➀</span></button>
</div>
<div class="action-bar">
<button class="act-btn" onclick="newChat()">β†Ί New chat</button>
<button class="act-btn" onclick="copyLast()">⎘ Copy last</button>
<button class="act-btn" onclick="exportChat()">↓ Export</button>
<button class="act-btn danger" onclick="logout()">⏻</button>
<span id="token-pill">0 tokens</span>
</div>
</div>
</div>
<div id="drawer-overlay" onclick="closeDrawer()"></div>
<div id="model-drawer">
<div class="drawer-pill"></div>
<div class="drawer-title">Choose Model</div>
<input id="drawer-search" placeholder="Search models…" oninput="filterDrawer()"/>
<div id="drawer-list"></div>
</div>
</div>
<script>
(function(){
const c=document.getElementById('particles');
const colors=['#6c63ff','#ff6b9d','#00d4aa','#9d97ff','#67e8f9'];
for(let i=0;i<28;i++){
const p=document.createElement('div');p.className='particle';
const s=Math.random()*2.5+1;
p.style.cssText=`left:${Math.random()*100}%;width:${s}px;height:${s}px;background:${colors[Math.floor(Math.random()*colors.length)]};--dx:${(Math.random()-.5)*80}px;animation-duration:${Math.random()*18+12}s;animation-delay:-${Math.random()*20}s;opacity:0`;
c.appendChild(p);
}
})();
const API=window.location.origin;
let selectedModel=null,selectedProvider=null,history=[],tokenCount=0,allModels=[],SESSION_KEY=null;
const PROV_COLOR={
'Gemini':'dot-Gemini','Groq':'dot-Groq','OpenRouter':'dot-OpenRouter',
'Cerebras':'dot-Cerebras','Mistral':'dot-Mistral','NVIDIA':'dot-NVIDIA',
'Gemini-B':'dot-Gemini-B','Groq-B':'dot-Groq-B','OpenRouter-B':'dot-OpenRouter-B',
'Cerebras-B':'dot-Cerebras-B','Mistral-B':'dot-Mistral-B','NVIDIA-B':'dot-NVIDIA-B'
};
const dotClass=p=>PROV_COLOR[p]||'dot-OpenRouter';
function toggleEye(){const i=document.getElementById('api-key-input'),b=document.getElementById('eye-btn');i.type=i.type==='password'?'text':'password';b.textContent=i.type==='password'?'πŸ‘':'πŸ™ˆ'}
async function doLogin(){
const inp=document.getElementById('api-key-input'),key=inp.value.trim(),btn=document.getElementById('login-btn');
if(!key){showLoginError('Please enter your access key.');return}
btn.disabled=true;btn.textContent='Verifying…';
document.getElementById('login-error').classList.remove('show');
try{
const res=await fetch(API+'/auth/verify',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':key}});
const data=await res.json();
if(res.ok&&data.ok){SESSION_KEY=key;document.getElementById('login-gate').style.display='none';document.getElementById('app').classList.add('visible');inp.value='';loadModels();}
else{showLoginError(data.detail||'Invalid key.');document.querySelector('.login-card').classList.add('shake');setTimeout(()=>document.querySelector('.login-card').classList.remove('shake'),400);}
}catch(e){showLoginError('Connection error.')}
btn.disabled=false;btn.textContent='Access Mohan AI β†’';
}
function showLoginError(msg){document.getElementById('login-error-msg').textContent=msg;document.getElementById('login-error').classList.add('show')}
function logout(){SESSION_KEY=null;history=[];tokenCount=0;allModels=[];document.getElementById('app').classList.remove('visible');document.getElementById('login-gate').style.display='flex';resetMessages()}
document.getElementById('api-key-input').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
async function loadModels(){
const res=await fetch(API+'/models',{headers:{'X-API-Key':SESSION_KEY}});
const data=await res.json();allModels=data.models||[];
renderSidebar(allModels);renderDrawer(allModels);
if(allModels.length>0)selectModel(allModels[0]);
}
function group(models){const g={};models.forEach(m=>{if(!g[m.provider])g[m.provider]=[];g[m.provider].push(m)});return g}
function renderSidebar(models){
const list=document.getElementById('sidebar-list');if(!list)return;list.innerHTML='';
Object.entries(group(models)).sort().forEach(([prov,ms])=>{
const lbl=document.createElement('div');lbl.className='sb-provider-label';lbl.textContent=prov;list.appendChild(lbl);
ms.forEach(m=>{const el=document.createElement('div');el.className='sb-model';el.dataset.id=m.id;el.innerHTML='<span class="sb-dot '+dotClass(m.provider)+'"></span><span class="sb-name">'+m.name+'</span>';el.onclick=()=>selectModel(m);list.appendChild(el);});
});
}
function filterSidebar(){const q=document.getElementById('sidebar-search').value.toLowerCase();document.querySelectorAll('.sb-model').forEach(el=>{el.style.display=el.querySelector('.sb-name').textContent.toLowerCase().includes(q)?'':'none'})}
function renderDrawer(models){
const list=document.getElementById('drawer-list');list.innerHTML='';
Object.entries(group(models)).sort().forEach(([prov,ms])=>{
const grp=document.createElement('div');const hdr=document.createElement('div');hdr.className='provider-group-label';hdr.textContent=prov;grp.appendChild(hdr);
ms.forEach(m=>{const el=document.createElement('div');el.className='model-item';el.dataset.id=m.id;el.innerHTML='<span class="m-dot '+dotClass(m.provider)+'"></span><span class="m-name">'+m.name+'</span><span class="m-badge">'+m.provider+'</span>';el.onclick=()=>{selectModel(m);closeDrawer()};grp.appendChild(el);});
list.appendChild(grp);
});
}
function filterDrawer(){const q=document.getElementById('drawer-search').value.toLowerCase();document.querySelectorAll('.model-item').forEach(el=>{el.style.display=el.querySelector('.m-name').textContent.toLowerCase().includes(q)?'':'none'})}
function openDrawer(){document.getElementById('drawer-overlay').classList.add('open');document.getElementById('model-drawer').classList.add('open')}
function closeDrawer(){document.getElementById('drawer-overlay').classList.remove('open');document.getElementById('model-drawer').classList.remove('open')}
function selectModel(m){
selectedModel=m.id;selectedProvider=m.provider;const dc=dotClass(m.provider);
const td=document.getElementById('topbar-dot');if(td)td.className='provider-dot '+dc;
const tn=document.getElementById('topbar-model');if(tn)tn.textContent=m.name;
const dn=document.getElementById('desktop-model-name');if(dn)dn.textContent=m.name;
const dp=document.getElementById('desktop-provider');if(dp)dp.textContent=m.provider;
document.querySelectorAll('.sb-model').forEach(el=>el.classList.toggle('active',el.dataset.id===m.id));
document.querySelectorAll('.model-item').forEach(el=>el.classList.toggle('active',el.dataset.id===m.id));
}
async function send(){
const prompt=document.getElementById('prompt'),text=prompt.value.trim();
if(!text||!selectedModel||!SESSION_KEY)return;
prompt.value='';prompt.style.height='auto';
document.getElementById('send-btn').disabled=true;
addMsg('user',text);history.push({role:'user',content:text});
const typing=addTyping();
try{
const res=await fetch(API+'/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':SESSION_KEY},body:JSON.stringify({model:selectedModel,messages:history,max_tokens:1024})});
const data=await res.json();typing.remove();
if(res.status===401||res.status===403){addMsg('ai','πŸ”’ Session expired.');setTimeout(logout,1500);return}
const reply=data.choices?.[0]?.message?.content||data.error?.message||JSON.stringify(data);
addMsg('ai',reply);history.push({role:'assistant',content:reply});
tokenCount+=data.usage?.total_tokens||Math.ceil((text+reply).length/4);updateTokenCount();
}catch(e){typing.remove();addMsg('ai','⚠ Error: '+e.message)}
document.getElementById('send-btn').disabled=false;
}
function updateTokenCount(){const t=tokenCount.toLocaleString()+' tokens';['token-pill','sb-token-count','dt-token-count'].forEach(id=>{const el=document.getElementById(id);if(el)el.textContent=t});}
function addMsg(role,text){
const msgs=document.getElementById('messages');
const welcome=msgs.querySelector('.welcome');if(welcome)welcome.remove();
const row=document.createElement('div');row.className='msg-row '+role;
const meta=document.createElement('div');meta.className='msg-meta';meta.textContent=role==='user'?'You':(selectedProvider||'AI');
const bubble=document.createElement('div');bubble.className='msg '+role;bubble.textContent=text;
row.appendChild(meta);row.appendChild(bubble);msgs.appendChild(row);msgs.scrollTop=msgs.scrollHeight;return row;
}
function addTyping(){
const msgs=document.getElementById('messages');
const row=document.createElement('div');row.className='typing-row';
row.innerHTML='<div class="typing-bubble"><span></span><span></span><span></span></div>';
msgs.appendChild(row);msgs.scrollTop=msgs.scrollHeight;return row;
}
function resetMessages(){
document.getElementById('messages').innerHTML='<div class="welcome"><div class="welcome-logo"><div class="welcome-logo-bg"></div><div class="welcome-logo-inner">M</div></div><div class="welcome-title">Mohan AI</div><div class="welcome-sub">Powered by 20+ frontier models<br>across Gemini, Groq, Mistral, NVIDIA & more</div><div class="welcome-chips"><div class="chip" onclick="useChip(this)">✨ Write a poem</div><div class="chip" onclick="useChip(this)">🧠 Explain quantum computing</div><div class="chip" onclick="useChip(this)">πŸ’» Debug my code</div><div class="chip" onclick="useChip(this)">🌍 Translate to Hindi</div></div></div>';
}
function newChat(){history=[];tokenCount=0;resetMessages();['token-pill','sb-token-count','dt-token-count'].forEach(id=>{const el=document.getElementById(id);if(el)el.textContent='0 tokens'});}
function exportChat(){const blob=new Blob([JSON.stringify(history,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='mohan-chat.json';a.click()}
function copyLast(){const msgs=document.querySelectorAll('.msg.ai');if(msgs.length)navigator.clipboard.writeText(msgs[msgs.length-1].textContent)}
function useChip(el){const p=document.getElementById('prompt');p.value=el.textContent.replace(/^[^\s]+ /,'');p.focus()}
document.getElementById('prompt').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
document.getElementById('prompt').addEventListener('input',function(){this.style.height='auto';this.style.height=Math.min(this.scrollHeight,120)+'px'});
</script>
</body>
</html>"""
DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Mohan AI | Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#060612;--surface:#0b0b1e;--surface2:#10102a;--surface3:#161634;--border:#1e1e40;--border2:#2a2a55;--text:#f0f0ff;--text2:#8888bb;--muted:#4a4a7a;--a:#6c63ff;--green:#10b981;--red:#f87171}
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;min-height:100vh;padding:32px 24px}
.header{display:flex;align-items:center;gap:14px;margin-bottom:32px;flex-wrap:wrap}
.h-logo-mark{position:relative;width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.h-logo-mark-bg{position:absolute;inset:0;border-radius:10px;background:linear-gradient(135deg,#6c63ff,#ff6b9d,#00d4aa)}
.h-logo-mark-inner{position:relative;z-index:1;width:33px;height:33px;border-radius:9px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.9rem;color:#fff}
.h-title{font-size:1.4rem;font-weight:800;background:linear-gradient(135deg,#c4b5fd,#f9a8d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.h-right{display:flex;gap:8px;margin-left:auto;align-items:center}
.h-btn{padding:7px 16px;background:var(--surface2);border:1px solid var(--border2);border-radius:9px;color:var(--text2);font-family:'Outfit',sans-serif;font-size:.78rem;cursor:pointer;text-decoration:none;transition:all .2s}
.h-btn:hover{color:var(--text);border-color:var(--a)}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;max-width:960px;margin-bottom:32px}
.stat-card{background:linear-gradient(145deg,var(--surface2),var(--surface));border:1px solid var(--border);padding:22px 20px;border-radius:16px}
.stat-label{font-size:.67rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;margin-bottom:10px}
.stat-val{font-size:2rem;font-weight:800}
.stat-val.violet{color:#c4b5fd}.stat-val.green{color:var(--green)}
.section-title{font-size:.68rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:2px;margin-bottom:14px}
.table-wrap{max-width:960px;background:linear-gradient(145deg,var(--surface2),var(--surface));border:1px solid var(--border);border-radius:16px;overflow:hidden;margin-bottom:32px}
table{width:100%;border-collapse:collapse}
th{padding:13px 18px;text-align:left;font-size:.65rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px;border-bottom:1px solid var(--border);background:var(--surface3)}
td{padding:13px 18px;font-size:.83rem;border-bottom:1px solid var(--border)}
tr:last-child td{border-bottom:none}
tr:hover td{background:var(--surface3)}
.p-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:8px;vertical-align:middle}
.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;font-size:.68rem;font-weight:600}
.badge-ok{background:#10b98120;color:var(--green);border:1px solid #10b98140}
.badge-no{background:#f8717120;color:var(--red);border:1px solid #f8717140}
#auth-overlay{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:999}
.auth-card{background:linear-gradient(145deg,var(--surface2),var(--surface));border:1px solid var(--border2);border-radius:24px;padding:40px 36px;width:100%;max-width:380px;text-align:center}
.auth-title{font-size:1.3rem;font-weight:800;background:linear-gradient(135deg,#c4b5fd,#f9a8d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px}
.auth-sub{color:var(--muted);font-size:.8rem;margin-bottom:24px}
.auth-input{width:100%;padding:12px 14px;background:var(--surface3);border:1px solid var(--border2);border-radius:12px;color:var(--text);font-family:'Outfit',sans-serif;font-size:.9rem;outline:none;margin-bottom:14px;letter-spacing:2px}
.auth-btn{width:100%;padding:13px;background:linear-gradient(135deg,var(--a),#9d97ff);border:none;border-radius:12px;color:#fff;font-family:'Outfit',sans-serif;font-size:.88rem;font-weight:700;cursor:pointer}
.auth-err{font-size:.78rem;color:var(--red);margin-bottom:12px;min-height:20px}
.live-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:#10b98118;border:1px solid #10b98130;border-radius:20px;font-size:.68rem;color:var(--green);font-weight:600}
.live-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
</style>
</head>
<body>
<div id="auth-overlay">
<div class="auth-card">
<div class="auth-title">Admin Dashboard</div>
<div class="auth-sub">Enter your API key to access</div>
<div class="auth-err" id="auth-err"></div>
<input class="auth-input" type="password" id="dash-key" placeholder="mk-β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’" autocomplete="off"/>
<button class="auth-btn" onclick="dashLogin()">Enter Dashboard β†’</button>
</div>
</div>
<div id="dash-content" style="display:none">
<div class="header">
<div class="h-logo-mark"><div class="h-logo-mark-bg"></div><div class="h-logo-mark-inner">M</div></div>
<h1 class="h-title">Admin Dashboard</h1>
<div class="h-right">
<div class="live-badge"><span class="live-badge-dot"></span>Live</div>
<button class="h-btn" onclick="loadDashboard()">↻ Refresh</button>
<a class="h-btn" href="/">← Chat</a>
</div>
</div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">Active Models</div><div class="stat-val violet" id="stat-models">β€”</div></div>
<div class="stat-card"><div class="stat-label">Server Status</div><div class="stat-val green">Online</div></div>
</div>
<div class="section-title">Provider API Configuration</div>
<div class="table-wrap">
<table>
<thead><tr><th>Provider</th><th>Key A</th><th>Key B</th><th>Models</th></tr></thead>
<tbody id="provider-tbody"><tr><td colspan="4" style="color:var(--muted);text-align:center;padding:24px">Loading…</td></tr></tbody>
</table>
</div>
</div>
<script>
const API=window.location.origin;let dashKey=null;
const PROVIDER_COUNTS={OpenRouter:3,Gemini:5,Groq:2,Cerebras:1,Mistral:4,NVIDIA:5};
const PROV_COLORS={OpenRouter:'#8b5cf6',Gemini:'#3b82f6',Groq:'#f97316',Cerebras:'#10b981',Mistral:'#ec4899',NVIDIA:'#22c55e'};
document.getElementById('dash-key').addEventListener('keydown',e=>{if(e.key==='Enter')dashLogin()});
async function dashLogin(){
const key=document.getElementById('dash-key').value.trim(),err=document.getElementById('auth-err');
if(!key){err.textContent='Please enter your key.';return}err.textContent='';
try{
const res=await fetch(API+'/auth/verify',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':key}});
const data=await res.json();
if(res.ok&&data.ok){dashKey=key;document.getElementById('auth-overlay').style.display='none';document.getElementById('dash-content').style.display='block';loadDashboard();setInterval(loadDashboard,30000)}
else{err.textContent=data.detail||'Invalid key.'}
}catch(e){err.textContent='Connection error.'}
}
async function loadDashboard(){
try{const res=await fetch(API+'/models',{headers:{'X-API-Key':dashKey}});if(res.ok){const d=await res.json();document.getElementById('stat-models').textContent=d.total||'β€”'}}catch(e){}
try{
const res=await fetch(API+'/debug',{headers:{'X-API-Key':dashKey}});
if(res.ok){const data=await res.json(),tbody=document.getElementById('provider-tbody');tbody.innerHTML='';
Object.entries(data).forEach(([prov,info])=>{
const name=prov.charAt(0).toUpperCase()+prov.slice(1).replace('_b','');
if(prov.endsWith('_b'))return;
const bKey=data[prov+'_b'];
tbody.innerHTML+=`<tr>
<td><span class="p-dot" style="background:${PROV_COLORS[name]||'#8b5cf6'}"></span>${name}</td>
<td><span class="badge ${info.key_loaded?'badge-ok':'badge-no'}">${info.key_loaded?'βœ“ Set':'βœ— Not Set'}</span></td>
<td><span class="badge ${bKey&&bKey.key_loaded?'badge-ok':'badge-no'}">${bKey&&bKey.key_loaded?'βœ“ Set':'βœ— Not Set'}</span></td>
<td style="color:var(--muted)">${info.key_loaded?(PROVIDER_COUNTS[name]||'?')+' models':'β€”'}</td>
</tr>`;
});
}
}catch(e){}
}
</script>
</body>
</html>"""
# ── Auth Dependency ───────────────────────────────────────────────────────────
async def require_auth(request: Request):
key = request.headers.get("X-API-Key", "").strip()
if not VALID_KEYS:
raise HTTPException(status_code=500, detail="MOHAN_API_KEY not configured on server.")
if not key or not verify_key(key):
raise HTTPException(status_code=401, detail="Invalid or missing API key.")
return key
# ── Routes ────────────────────────────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
def home(): return HTML_UI
@app.get("/dashboard", response_class=HTMLResponse)
def dashboard(): return DASHBOARD_HTML
@app.post("/auth/verify")
async def auth_verify(request: Request):
key = request.headers.get("X-API-Key", "").strip()
if not VALID_KEYS:
raise HTTPException(status_code=500, detail="MOHAN_API_KEY not configured on server.")
if key and verify_key(key):
return {"ok": True}
raise HTTPException(status_code=401, detail="Invalid key. Access denied.")
@app.get("/models")
def get_models(key: str = Depends(require_auth)):
return {"models": ALL_MODELS, "total": len(ALL_MODELS)}
@app.get("/debug")
def debug(key: str = Depends(require_auth)):
return {
"openrouter": {"key_loaded": bool(OPENROUTER_API_KEY)},
"openrouter_b": {"key_loaded": bool(OPENROUTER_API_KEY_B)},
"gemini": {"key_loaded": bool(GEMINI_API_KEY)},
"gemini_b": {"key_loaded": bool(GEMINI_API_KEY_B)},
"groq": {"key_loaded": bool(GROQ_API_KEY)},
"groq_b": {"key_loaded": bool(GROQ_API_KEY_B)},
"cerebras": {"key_loaded": bool(CEREBRAS_API_KEY)},
"cerebras_b": {"key_loaded": bool(CEREBRAS_API_KEY_B)},
"mistral": {"key_loaded": bool(MISTRAL_API_KEY)},
"mistral_b": {"key_loaded": bool(MISTRAL_API_KEY_B)},
"nvidia": {"key_loaded": bool(NVIDIA_API_KEY)},
"nvidia_b": {"key_loaded": bool(NVIDIA_API_KEY_B)},
}
@app.post("/chat/completions")
async def chat(request: Request, key: str = Depends(require_auth)):
body = await request.json()
model_id: str = body.get("model", "")
try:
# ── Gemini A (with auto B fallback) ──
if is_gemini(model_id):
if not GEMINI_API_KEY and not GEMINI_API_KEY_B:
return JSONResponse({"error": "GEMINI_API_KEY not configured."}, status_code=500)
return await call_gemini(strip_prefix(model_id), GEMINI_API_KEY, GEMINI_API_KEY_B, body, model_id)
# ── Gemini B (direct) ──
if is_gemini_b(model_id):
if not GEMINI_API_KEY_B:
return JSONResponse({"error": "GEMINI_API_KEY_B not configured."}, status_code=500)
return await call_gemini(strip_prefix(model_id), GEMINI_API_KEY_B, "", body, model_id)
# ── Groq A (with auto B fallback) ──
if is_groq(model_id):
if not GROQ_API_KEY and not GROQ_API_KEY_B:
return JSONResponse({"error": "GROQ_API_KEY not configured."}, status_code=500)
return await call_openai_compat(GROQ_BASE, GROQ_API_KEY, GROQ_API_KEY_B, strip_prefix(model_id), body)
# ── Groq B (direct) ──
if is_groq_b(model_id):
if not GROQ_API_KEY_B:
return JSONResponse({"error": "GROQ_API_KEY_B not configured."}, status_code=500)
return await call_openai_compat(GROQ_BASE, GROQ_API_KEY_B, "", strip_prefix(model_id), body)
# ── Cerebras A (with auto B fallback) ──
if is_cerebras(model_id):
if not CEREBRAS_API_KEY and not CEREBRAS_API_KEY_B:
return JSONResponse({"error": "CEREBRAS_API_KEY not configured."}, status_code=500)
return await call_openai_compat(CEREBRAS_BASE, CEREBRAS_API_KEY, CEREBRAS_API_KEY_B, strip_prefix(model_id), body)
# ── Cerebras B (direct) ──
if is_cerebras_b(model_id):
if not CEREBRAS_API_KEY_B:
return JSONResponse({"error": "CEREBRAS_API_KEY_B not configured."}, status_code=500)
return await call_openai_compat(CEREBRAS_BASE, CEREBRAS_API_KEY_B, "", strip_prefix(model_id), body)
# ── Mistral A (with auto B fallback) ──
if is_mistral(model_id):
if not MISTRAL_API_KEY and not MISTRAL_API_KEY_B:
return JSONResponse({"error": "MISTRAL_API_KEY not configured."}, status_code=500)
return await call_openai_compat(MISTRAL_BASE, MISTRAL_API_KEY, MISTRAL_API_KEY_B, strip_prefix(model_id), body)
# ── Mistral B (direct) ──
if is_mistral_b(model_id):
if not MISTRAL_API_KEY_B:
return JSONResponse({"error": "MISTRAL_API_KEY_B not configured."}, status_code=500)
return await call_openai_compat(MISTRAL_BASE, MISTRAL_API_KEY_B, "", strip_prefix(model_id), body)
# ── NVIDIA A (with auto B fallback) ──
if is_nvidia(model_id):
if not NVIDIA_API_KEY and not NVIDIA_API_KEY_B:
return JSONResponse({"error": "NVIDIA_API_KEY not configured."}, status_code=500)
return await call_openai_compat(NVIDIA_BASE, NVIDIA_API_KEY, NVIDIA_API_KEY_B, strip_prefix(model_id), body)
# ── NVIDIA B (direct) ──
if is_nvidia_b(model_id):
if not NVIDIA_API_KEY_B:
return JSONResponse({"error": "NVIDIA_API_KEY_B not configured."}, status_code=500)
return await call_openai_compat(NVIDIA_BASE, NVIDIA_API_KEY_B, "", strip_prefix(model_id), body)
# ── OpenRouter B (direct) ──
if is_openrouter_b(model_id):
if not OPENROUTER_API_KEY_B:
return JSONResponse({"error": "OPENROUTER_API_KEY_B not configured."}, status_code=500)
async with httpx.AsyncClient(timeout=60) as client:
real_model = strip_prefix(model_id)
resp = await client.post(
f"{OPENROUTER_BASE}/chat/completions",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY_B}",
"Content-Type": "application/json",
"HTTP-Referer": "https://senpai315-mohan-ai.hf.space",
"X-Title": "Mohan-AI Playground",
},
json={**body, "model": real_model}
)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
# ── OpenRouter A (with auto B fallback) β€” default fallback ──
if not OPENROUTER_API_KEY and not OPENROUTER_API_KEY_B:
return JSONResponse({"error": "OPENROUTER_API_KEY not configured."}, status_code=500)
async with httpx.AsyncClient(timeout=60) as client:
headers_a = {
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://senpai315-mohan-ai.hf.space",
"X-Title": "Mohan-AI Playground",
}
if OPENROUTER_API_KEY:
resp = await client.post(f"{OPENROUTER_BASE}/chat/completions", headers=headers_a, json=body)
data = resp.json()
if not is_quota_error(resp.status_code, data):
return JSONResponse(content=data, status_code=resp.status_code)
# Fallback to B
if OPENROUTER_API_KEY_B:
headers_b = {**headers_a, "Authorization": f"Bearer {OPENROUTER_API_KEY_B}"}
resp = await client.post(f"{OPENROUTER_BASE}/chat/completions", headers=headers_b, json=body)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
return JSONResponse({"error": "All OpenRouter keys exhausted."}, status_code=429)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)