| 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=["*"]) |
|
|
| |
| 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) |
|
|
| |
|
|
| 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" |
|
|
| |
| ALL_MODELS = [ |
|
|
| |
| {"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"}, |
| |
| {"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"}, |
|
|
| |
| {"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"}, |
| |
| {"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"}, |
|
|
| |
| {"id": "cerebras/gemma-4-31b", "name": "Gemma 4 31B (Cerebras β‘)", "provider": "Cerebras"}, |
| |
| {"id": "cerebras_b/gemma-4-31b", "name": "Gemma 4 31B (Cerebras β‘) [B]", "provider": "Cerebras-B"}, |
|
|
| |
| {"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"}, |
| |
| {"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"}, |
|
|
| |
| {"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"}, |
| |
| {"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"}, |
| ] |
|
|
| |
| 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 |
| |
| 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 |
|
|
| |
| 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), |
| } |
| } |
|
|
| |
| 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: |
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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: |
| |
| 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) |
|
|
| |
| 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 = """<!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>""" |
|
|
|
|
| |
| 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 |
|
|
| |
| @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: |
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |