| import json |
| import os |
| import subprocess |
| import time |
| import uuid |
| from pathlib import Path |
| from threading import Lock, Thread |
| from typing import Any, Dict, Iterable, List |
|
|
| os.environ.setdefault("HF_HOME", "/tmp/hf_home") |
| os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules") |
| os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") |
| os.environ.setdefault("GRADIO_SSR_MODE", "false") |
| for _path in (os.environ["HF_HOME"], os.environ["HF_MODULES_CACHE"], os.environ["MPLCONFIGDIR"]): |
| os.makedirs(_path, exist_ok=True) |
|
|
| import spaces |
|
|
| import httpx |
| import gradio as gr |
| from fastapi import Request |
| from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse |
| from starlette.background import BackgroundTask |
| from huggingface_hub import snapshot_download |
| from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer |
| import torch |
|
|
|
|
| MODEL_ID = "WeiboAI/VibeThinker-3B" |
| MODEL_REVISION = "main" |
| DEFAULT_MAX_NEW_TOKENS = 4096 |
| MAX_NEW_TOKENS = 4096 |
| ROOT_DIR = Path(__file__).resolve().parent |
| CHAT_UI_DIR = ROOT_DIR / "chat-ui" |
| CHAT_UI_PORT = int(os.environ.get("CHAT_UI_PORT", "3000")) |
| CHAT_UI_URL = f"http://127.0.0.1:{CHAT_UI_PORT}" |
| CHAT_UI_BUILD = CHAT_UI_DIR / "build" / "index.js" |
| CHAT_UI_DB = Path(os.environ.get("CHAT_UI_DB", "/tmp/vibethinker-chat-ui-db")) |
|
|
| _model_lock = Lock() |
| _model = None |
| _tokenizer = None |
| _model_device = "cpu" |
| _chat_ui_lock = Lock() |
| _chat_ui_process: subprocess.Popen | None = None |
|
|
|
|
| def _download_model() -> None: |
| print(f"Downloading {MODEL_ID} to the local Hub cache...", flush=True) |
| snapshot_download( |
| repo_id=MODEL_ID, |
| revision=MODEL_REVISION, |
| ignore_patterns=["*.msgpack", "*.h5", "*.ot", "*.onnx"], |
| ) |
| print("Model files are present in the local Hub cache.", flush=True) |
|
|
|
|
| def _load_model_cpu() -> None: |
| global _model, _tokenizer |
| if _model is not None and _tokenizer is not None: |
| return |
|
|
| with _model_lock: |
| if _model is not None and _tokenizer is not None: |
| return |
|
|
| _download_model() |
| print("Loading tokenizer...", flush=True) |
| _tokenizer = AutoTokenizer.from_pretrained( |
| MODEL_ID, |
| revision=MODEL_REVISION, |
| trust_remote_code=True, |
| ) |
| if _tokenizer.pad_token_id is None and _tokenizer.eos_token_id is not None: |
| _tokenizer.pad_token = _tokenizer.eos_token |
|
|
| print("Loading model on CPU...", flush=True) |
| _model = AutoModelForCausalLM.from_pretrained( |
| MODEL_ID, |
| revision=MODEL_REVISION, |
| torch_dtype=torch.bfloat16, |
| low_cpu_mem_usage=True, |
| trust_remote_code=True, |
| attn_implementation="sdpa", |
| ).eval() |
| print("Model loaded on CPU.", flush=True) |
|
|
|
|
| def _ensure_model_on_cuda() -> None: |
| global _model_device |
| _load_model_cpu() |
| if _model_device == "cuda": |
| return |
| if not torch.cuda.is_available(): |
| raise RuntimeError("CUDA is not available. Confirm the Space is running on zero-a10g.") |
| print("Moving model to CUDA...", flush=True) |
| _model.to("cuda") |
| _model_device = "cuda" |
| print("Model is ready on CUDA.", flush=True) |
|
|
|
|
| def _clean_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, str]]: |
| cleaned: List[Dict[str, str]] = [] |
| for item in messages: |
| if not isinstance(item, dict): |
| continue |
| role = str(item.get("role", "")).strip() |
| content = str(item.get("content", "")).strip() |
| if role in {"system", "user", "assistant"} and content: |
| cleaned.append({"role": role, "content": content}) |
| return cleaned |
|
|
|
|
| def _messages_from_json(history_json: str) -> List[Dict[str, str]]: |
| try: |
| raw = json.loads(history_json) |
| except json.JSONDecodeError as exc: |
| raise ValueError("history_json must be valid JSON") from exc |
| if not isinstance(raw, list): |
| raise ValueError("history_json must encode a list of chat messages") |
| messages = _clean_messages(raw) |
| if not messages or messages[-1]["role"] != "user": |
| raise ValueError("The final message must be a user message") |
| return messages |
|
|
|
|
| def _coerce_generation_args( |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| repetition_penalty: float, |
| ) -> Dict[str, Any]: |
| max_new_tokens = max(1, min(int(max_new_tokens or DEFAULT_MAX_NEW_TOKENS), MAX_NEW_TOKENS)) |
| temperature = max(0.0, min(float(temperature), 2.0)) |
| top_p = max(0.05, min(float(top_p), 1.0)) |
| repetition_penalty = max(0.8, min(float(repetition_penalty), 1.5)) |
| return { |
| "max_new_tokens": max_new_tokens, |
| "do_sample": temperature > 0, |
| "temperature": max(temperature, 1e-5), |
| "top_p": top_p, |
| "repetition_penalty": repetition_penalty, |
| "pad_token_id": _tokenizer.pad_token_id, |
| "eos_token_id": _tokenizer.eos_token_id, |
| } |
|
|
|
|
| def _format_prompt(messages: List[Dict[str, str]]) -> str: |
| return _tokenizer.apply_chat_template( |
| messages, |
| tokenize=False, |
| add_generation_prompt=True, |
| ) |
|
|
|
|
| def _estimate_duration( |
| history_json: str, |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| repetition_penalty: float, |
| *args: Any, |
| **kwargs: Any, |
| ) -> int: |
| del history_json, temperature, top_p, repetition_penalty, args, kwargs |
| return min(240, max(60, 40 + int(max_new_tokens or DEFAULT_MAX_NEW_TOKENS) // 12)) |
|
|
|
|
| @spaces.GPU(duration=1) |
| def _zerogpu_probe() -> str: |
| return "ready" |
|
|
|
|
| def _generate_stream( |
| messages: List[Dict[str, str]], |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| repetition_penalty: float, |
| ) -> Iterable[str]: |
| _ensure_model_on_cuda() |
| prompt = _format_prompt(messages) |
| inputs = _tokenizer([prompt], return_tensors="pt").to(_model.device) |
| generation_args = _coerce_generation_args( |
| max_new_tokens=max_new_tokens, |
| temperature=temperature, |
| top_p=top_p, |
| repetition_penalty=repetition_penalty, |
| ) |
| streamer = TextIteratorStreamer( |
| _tokenizer, |
| skip_prompt=True, |
| skip_special_tokens=True, |
| timeout=180, |
| ) |
| generation_kwargs = { |
| **inputs, |
| **generation_args, |
| "streamer": streamer, |
| } |
| worker = Thread(target=_model.generate, kwargs=generation_kwargs, daemon=True) |
| worker.start() |
|
|
| partial = "" |
| for token in streamer: |
| partial += token |
| yield partial |
| worker.join(timeout=1) |
|
|
|
|
| @spaces.GPU(duration=_estimate_duration) |
| def _gpu_chat_stream( |
| history_json: str, |
| max_new_tokens: int, |
| temperature: float, |
| top_p: float, |
| repetition_penalty: float, |
| ) -> Iterable[str]: |
| messages = _messages_from_json(history_json) |
| yield from _generate_stream( |
| messages=messages, |
| max_new_tokens=max_new_tokens, |
| temperature=temperature, |
| top_p=top_p, |
| repetition_penalty=repetition_penalty, |
| ) |
|
|
|
|
| app = gr.Server() |
|
|
|
|
| def _chat_ui_model_config() -> str: |
| return json.dumps( |
| [ |
| { |
| "id": MODEL_ID, |
| "name": MODEL_ID, |
| "displayName": "VibeThinker-3B", |
| "description": "Reasoning-focused 3B model hosted on ZeroGPU.", |
| "modelUrl": f"https://huggingface.co/{MODEL_ID}", |
| "parameters": { |
| "temperature": 1.0, |
| "top_p": 0.95, |
| "max_tokens": DEFAULT_MAX_NEW_TOKENS, |
| }, |
| "supportsReasoning": True, |
| } |
| ], |
| separators=(",", ":"), |
| ) |
|
|
|
|
| def _chat_ui_env() -> Dict[str, str]: |
| env = os.environ.copy() |
| env.update( |
| { |
| "OPENAI_BASE_URL": "http://127.0.0.1:7860/v1", |
| "OPENAI_API_KEY": "sk-local", |
| "USE_USER_TOKEN": "false", |
| "AUTOMATIC_LOGIN": "false", |
| "ALLOW_IFRAME": "true", |
| "PUBLIC_APP_NAME": "VibeThinker", |
| "PUBLIC_APP_ASSETS": "chatui", |
| "PUBLIC_APP_DESCRIPTION": "VibeThinker-3B hosted on ZeroGPU.", |
| "PUBLIC_ORIGIN": os.environ.get( |
| "PUBLIC_ORIGIN", |
| "https://huggingface.co/proxy/mike0021-vibethinker-3b-zerogpu.hf.space", |
| ), |
| "MONGO_STORAGE_PATH": str(CHAT_UI_DB), |
| "MONGODB_DB_NAME": "chat-ui", |
| "MONGODB_DIRECT_CONNECTION": "false", |
| "COOKIE_NAME": "vibethinker-chat-session", |
| "HUSKY": "0", |
| "ENABLE_CONFIG_MANAGER": "false", |
| "LLM_SUMMARIZATION": "false", |
| "TASK_MODEL": MODEL_ID, |
| "MODELS": _chat_ui_model_config(), |
| "PORT": str(CHAT_UI_PORT), |
| "HOST": "127.0.0.1", |
| "BODY_SIZE_LIMIT": os.environ.get("BODY_SIZE_LIMIT", "15728640"), |
| } |
| ) |
| return env |
|
|
|
|
| def _run_chat_ui_command(command: List[str], env: Dict[str, str]) -> None: |
| print(f"[chat-ui] running: {' '.join(command)}", flush=True) |
| subprocess.run(command, cwd=CHAT_UI_DIR, env=env, check=True) |
|
|
|
|
| def _ensure_chat_ui_build(env: Dict[str, str]) -> None: |
| if not CHAT_UI_DIR.exists(): |
| raise RuntimeError("chat-ui source directory is missing from the Space.") |
|
|
| node_modules = CHAT_UI_DIR / "node_modules" |
| if not node_modules.exists(): |
| _run_chat_ui_command(["npm", "ci"], env) |
|
|
| if not CHAT_UI_BUILD.exists(): |
| _run_chat_ui_command(["npm", "run", "build"], env) |
|
|
|
|
| def _chat_ui_is_ready() -> bool: |
| try: |
| with httpx.Client(timeout=2.0) as client: |
| response = client.get(f"{CHAT_UI_URL}/healthcheck") |
| return response.status_code < 500 |
| except Exception: |
| return False |
|
|
|
|
| def _start_chat_ui() -> None: |
| global _chat_ui_process |
| with _chat_ui_lock: |
| if _chat_ui_process is not None and _chat_ui_process.poll() is None: |
| return |
|
|
| env = _chat_ui_env() |
| _ensure_chat_ui_build(env) |
| print(f"[chat-ui] starting on {CHAT_UI_URL}", flush=True) |
| _chat_ui_process = subprocess.Popen( |
| [ |
| "node", |
| "--dns-result-order=ipv4first", |
| str(CHAT_UI_BUILD), |
| "--", |
| "--host", |
| "127.0.0.1", |
| "--port", |
| str(CHAT_UI_PORT), |
| ], |
| cwd=CHAT_UI_DIR, |
| env=env, |
| ) |
|
|
|
|
| def _start_chat_ui_background() -> None: |
| try: |
| time.sleep(3) |
| _start_chat_ui() |
| except Exception as exc: |
| print(f"[chat-ui] failed to start: {exc}", flush=True) |
|
|
|
|
| async def _wait_for_chat_ui(timeout: float = 240.0) -> None: |
| if _chat_ui_process is None or _chat_ui_process.poll() is not None: |
| Thread(target=_start_chat_ui_background, daemon=True).start() |
|
|
| deadline = time.time() + timeout |
| while time.time() < deadline: |
| if _chat_ui_is_ready(): |
| return |
| await anyio_sleep(1.0) |
|
|
| raise RuntimeError("chat-ui did not become ready before the timeout.") |
|
|
|
|
| async def anyio_sleep(seconds: float) -> None: |
| import anyio |
|
|
| await anyio.sleep(seconds) |
|
|
|
|
| @app.on_event("startup") |
| async def start_chat_ui_on_startup() -> None: |
| Thread(target=_start_chat_ui_background, daemon=True).start() |
|
|
|
|
| @app.api(name="zerogpu_probe", concurrency_limit=1, time_limit=30) |
| def zerogpu_probe() -> str: |
| return _zerogpu_probe() |
|
|
|
|
| @app.api(name="chat", concurrency_limit=1, time_limit=300, stream_every=0.25) |
| def chat( |
| history_json: str, |
| max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS, |
| temperature: float = 1.0, |
| top_p: float = 0.95, |
| repetition_penalty: float = 1.0, |
| ) -> Iterable[str]: |
| yield from _gpu_chat_stream( |
| history_json, |
| max_new_tokens, |
| temperature, |
| top_p, |
| repetition_penalty, |
| ) |
|
|
|
|
| @app.get("/health") |
| async def health() -> Dict[str, str]: |
| return {"status": "ok", "model": MODEL_ID} |
|
|
|
|
| @app.get("/v1/models") |
| async def openai_models() -> Dict[str, Any]: |
| created = int(time.time()) |
| return { |
| "object": "list", |
| "data": [ |
| { |
| "id": MODEL_ID, |
| "object": "model", |
| "created": created, |
| "owned_by": "WeiboAI", |
| } |
| ], |
| } |
|
|
|
|
| def _messages_for_openai(payload: Dict[str, Any]) -> List[Dict[str, str]]: |
| raw_messages = payload.get("messages") |
| if not isinstance(raw_messages, list): |
| raise ValueError("messages must be a list") |
| return _clean_messages(raw_messages) |
|
|
|
|
| @app.post("/v1/chat/completions", response_model=None) |
| async def openai_chat_completions(request: Request): |
| try: |
| payload = await request.json() |
| messages = _messages_for_openai(payload) |
| if not messages: |
| raise ValueError("messages cannot be empty") |
| max_new_tokens = int(payload.get("max_tokens") or payload.get("max_new_tokens") or DEFAULT_MAX_NEW_TOKENS) |
| temperature = float(payload.get("temperature", 1.0)) |
| top_p = float(payload.get("top_p", 0.95)) |
| repetition_penalty = float(payload.get("repetition_penalty", 1.0)) |
| stream = bool(payload.get("stream", False)) |
| except Exception as exc: |
| return JSONResponse({"error": {"message": str(exc), "type": "invalid_request_error"}}, status_code=400) |
|
|
| completion_id = f"chatcmpl-{uuid.uuid4().hex}" |
| created = int(time.time()) |
|
|
| if stream: |
| def events() -> Iterable[bytes]: |
| last_text = "" |
| for text in _gpu_chat_stream( |
| json.dumps(messages), |
| max_new_tokens, |
| temperature, |
| top_p, |
| repetition_penalty, |
| ): |
| delta = text[len(last_text) :] |
| last_text = text |
| chunk = { |
| "id": completion_id, |
| "object": "chat.completion.chunk", |
| "created": created, |
| "model": MODEL_ID, |
| "choices": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}], |
| } |
| yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") |
| final = { |
| "id": completion_id, |
| "object": "chat.completion.chunk", |
| "created": created, |
| "model": MODEL_ID, |
| "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], |
| } |
| yield f"data: {json.dumps(final, ensure_ascii=False)}\n\n".encode("utf-8") |
| yield b"data: [DONE]\n\n" |
|
|
| return StreamingResponse(events(), media_type="text/event-stream") |
|
|
| final_text = "" |
| for final_text in _gpu_chat_stream( |
| json.dumps(messages), |
| max_new_tokens, |
| temperature, |
| top_p, |
| repetition_penalty, |
| ): |
| pass |
| return JSONResponse( |
| { |
| "id": completion_id, |
| "object": "chat.completion", |
| "created": created, |
| "model": MODEL_ID, |
| "choices": [ |
| { |
| "index": 0, |
| "message": {"role": "assistant", "content": final_text}, |
| "finish_reason": "stop", |
| } |
| ], |
| } |
| ) |
|
|
|
|
| @app.get("/gradio_api/startup-events") |
| async def gradio_startup_events() -> Dict[str, str]: |
| return {"status": "ok"} |
|
|
|
|
| HOP_BY_HOP_HEADERS = { |
| "connection", |
| "keep-alive", |
| "proxy-authenticate", |
| "proxy-authorization", |
| "te", |
| "trailers", |
| "transfer-encoding", |
| "upgrade", |
| } |
|
|
|
|
| async def _close_upstream(response: httpx.Response, client: httpx.AsyncClient) -> None: |
| await response.aclose() |
| await client.aclose() |
|
|
|
|
| @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) |
| async def proxy_chat_ui(path: str, request: Request): |
| host = request.headers.get("host", "") |
| if path == "" and host.startswith(("localhost:", "127.0.0.1:", "0.0.0.0:")) and not _chat_ui_is_ready(): |
| return PlainTextResponse("chat-ui is starting", status_code=200) |
|
|
| try: |
| await _wait_for_chat_ui() |
| except Exception as exc: |
| return PlainTextResponse(f"chat-ui is starting or failed to start: {exc}", status_code=503) |
|
|
| target_url = f"{CHAT_UI_URL}/{path}" |
| if request.url.query: |
| target_url = f"{target_url}?{request.url.query}" |
|
|
| excluded_request_headers = HOP_BY_HOP_HEADERS | {"host", "content-length"} |
| headers = { |
| key: value |
| for key, value in request.headers.items() |
| if key.lower() not in excluded_request_headers |
| } |
| headers["x-forwarded-host"] = request.headers.get("host", "") |
| headers["x-forwarded-proto"] = request.url.scheme |
|
|
| client = httpx.AsyncClient(timeout=None, follow_redirects=False) |
| try: |
| upstream_request = client.build_request( |
| request.method, |
| target_url, |
| headers=headers, |
| content=await request.body(), |
| ) |
| upstream_response = await client.send(upstream_request, stream=True) |
| except Exception as exc: |
| await client.aclose() |
| return PlainTextResponse(f"chat-ui proxy error: {exc}", status_code=502) |
|
|
| excluded_response_headers = HOP_BY_HOP_HEADERS | {"content-length"} |
| response_headers = { |
| key: value |
| for key, value in upstream_response.headers.items() |
| if key.lower() not in excluded_response_headers |
| } |
| return StreamingResponse( |
| upstream_response.aiter_raw(), |
| status_code=upstream_response.status_code, |
| headers=response_headers, |
| background=BackgroundTask(_close_upstream, upstream_response, client), |
| ) |
|
|
|
|
| demo = app |
|
|
|
|
| if __name__ == "__main__": |
| app.launch() |
|
|