import os import json import time import subprocess import requests from huggingface_hub import hf_hub_download # ----------------------- config (override via Space variables) ----------------------- # Serves the copied 0.8B GGUF from anon334test/qwopus. MODEL_REPO = os.environ.get("MODEL_REPO", "anon334test/qwopus") GGUF_FILE = os.environ.get("GGUF_FILE", "Qwen3.5-0.8B.Q4_K_M.gguf") HF_TOKEN = os.environ.get("HF_TOKEN") # optional; model is public PORT = int(os.environ.get("PORT", "7860")) LLAMA_PORT = int(os.environ.get("LLAMA_PORT", "8080")) # IMPORTANT: cpu-basic = 2 physical vCPU, but os.cpu_count() reports the HOST core # count (e.g. 32/64) inside the cgroup -> launching -t with that number oversubscribes # the 2 vCPU and collapses throughput (~0.1 tok/s). Default to 2; override via NUM_THREADS. NUM_THREADS = os.environ.get("NUM_THREADS", "2") BIN = "/opt/llamabin" # Custom chat template that adds an enable_thinking toggle (the model's own template # ALWAYS opens a block; this lets "Fast (no thinking)" actually skip reasoning). CHAT_TEMPLATE_FILE = os.environ.get("CHAT_TEMPLATE_FILE", "/home/user/app/chat_template.jinja") LLAMA = f"http://127.0.0.1:{LLAMA_PORT}" # Qwen3.5-0.8B native context = 262144. We default to a generous 32768 window (good for # large files / long chats) while keeping startup + memory reasonable on CPU; raise N_CTX up # to 262144 via a Space variable if you really need it (much slower prefill on CPU). N_CTX = os.environ.get("N_CTX", "32768") def _env(): e = os.environ.copy() e["LD_LIBRARY_PATH"] = BIN + ":" + e.get("LD_LIBRARY_PATH", "") return e # ----------------------- step 1: download the GGUF (no conversion) ----------------------- def ensure_gguf(): print(f"[init] downloading {GGUF_FILE} from {MODEL_REPO} ...", flush=True) path = hf_hub_download(MODEL_REPO, GGUF_FILE, repo_type="model", token=HF_TOKEN) print(f"[init] model ready: {path}", flush=True) return path # ----------------------- step 2: launch internal llama-server ----------------------- def start_llama(model_path): cmd = [ os.path.join(BIN, "llama-server"), "-m", model_path, "--host", "127.0.0.1", "--port", str(LLAMA_PORT), "-c", N_CTX, "-t", NUM_THREADS, "-b", "256", "--no-mmap", "--parallel", os.environ.get("PARALLEL", "1"), # Apply our chat template (adds enable_thinking toggle for true fast mode). "--jinja", ] if CHAT_TEMPLATE_FILE and os.path.exists(CHAT_TEMPLATE_FILE): cmd += ["--chat-template-file", CHAT_TEMPLATE_FILE] # Separate the chain-of-thought into reasoning_content so the answer stays clean. cmd += ["--reasoning-format", "auto"] print(f"[init] cpu_count={os.cpu_count()} threads={NUM_THREADS}", flush=True) # Optional extra flags (e.g. "-fa on") via LLAMA_EXTRA_ARGS, space separated. extra = os.environ.get("LLAMA_EXTRA_ARGS", "").split() if extra: cmd += extra print("[init] starting internal llama-server: " + " ".join(cmd), flush=True) subprocess.Popen(cmd, env=_env()) for _ in range(900): try: r = requests.get(LLAMA + "/health", timeout=3) if r.status_code == 200 and r.json().get("status") == "ok": print("[init] internal llama-server is healthy.", flush=True) return except Exception: pass time.sleep(1) raise RuntimeError("internal llama-server did not become healthy in time") # ============================================================================ # Serving: thin PASS-THROUGH proxy to llama-server's native OpenAI endpoints. # llama-server applies the model's embedded chat template (--jinja) itself, so # what the model sees is exactly the messages you send -- nothing injected. # ============================================================================ from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse, Response from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"]) # Sensible defaults (only applied when the caller doesn't set them). Overridable per request. # max_tokens = -1 -> UNLIMITED output (generate until EOS or the context window is full). DEFAULTS = {"temperature": 0.3, "top_p": 0.9, "top_k": 20, "repeat_penalty": 1.05, "max_tokens": int(os.environ.get("MAX_TOKENS", "-1"))} @app.get("/health") def health(): return {"status": "ok"} @app.get("/v1/models") def models(): try: return JSONResponse(requests.get(LLAMA + "/v1/models", timeout=15).json()) except Exception: return {"object": "list", "data": [{"id": GGUF_FILE, "object": "model", "owned_by": "anon334test"}]} # ----- Lightweight branding: short identity prompt that does NOT suppress reasoning ----- # Kept short on purpose: long/defensive prompts make small models reason worse (see report.md). # Empty by default for a 0.8B model so nothing dilutes its limited attention; set SYSTEM_PROMPT # as a Space variable to enable an identity line. SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "").strip() def _prep(body): """Apply defaults and translate convenience fields, then leave everything else untouched so all OpenAI / llama.cpp params pass straight through to the model. Thinking control (Qwen3.5): accept a top-level `enable_thinking` bool or a friendly `thinking: "on"|"off"`. Both map to chat_template_kwargs.enable_thinking, honored by the model's own chat template. If neither is given, the model's default applies. """ for k, v in DEFAULTS.items(): body.setdefault(k, v) think = None if "enable_thinking" in body: think = bool(body.pop("enable_thinking")) if "thinking" in body: t = str(body.pop("thinking")).lower() think = t in ("on", "true", "1", "yes", "smart") if think is not None: ctk = dict(body.get("chat_template_kwargs") or {}) ctk["enable_thinking"] = think body["chat_template_kwargs"] = ctk # Optional identity injection (only if SYSTEM_PROMPT is set). if SYSTEM_PROMPT: msgs = body.get("messages") if isinstance(msgs, list): msgs = [m for m in msgs if not (isinstance(m, dict) and m.get("role") == "system")] msgs.insert(0, {"role": "system", "content": SYSTEM_PROMPT}) body["messages"] = msgs return body @app.post("/v1/chat/completions") async def chat_completions(request: Request): body = _prep(await request.json()) stream = bool(body.get("stream", False)) if stream: def gen(): with requests.post(LLAMA + "/v1/chat/completions", json=body, stream=True, timeout=900) as r: for chunk in r.iter_content(chunk_size=None): if chunk: yield chunk return StreamingResponse(gen(), media_type="text/event-stream") r = requests.post(LLAMA + "/v1/chat/completions", json=body, timeout=900) return Response(content=r.content, media_type="application/json", status_code=r.status_code) @app.post("/v1/completions") async def completions(request: Request): body = _prep(await request.json()) stream = bool(body.get("stream", False)) if stream: def gen(): with requests.post(LLAMA + "/v1/completions", json=body, stream=True, timeout=900) as r: for chunk in r.iter_content(chunk_size=None): if chunk: yield chunk return StreamingResponse(gen(), media_type="text/event-stream") r = requests.post(LLAMA + "/v1/completions", json=body, timeout=900) return Response(content=r.content, media_type="application/json", status_code=r.status_code) @app.get("/", response_class=HTMLResponse) def index(): return INDEX_HTML INDEX_HTML = """ Qwopus3.5-0.8B Chat
Qwopus3.5-0.8B · Qwen3.5 0.8B · fast chat / reasoning · live GGUF
Pure model · OpenAI-compatible API at /v1/chat/completions
""" # ----------------------- main ----------------------- if __name__ == "__main__": model_path = ensure_gguf() start_llama(model_path) import uvicorn print(f"[init] starting public proxy on port {PORT} ...", flush=True) uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")