Spaces:
Sleeping
Sleeping
| """ | |
| Flask API — local LLM inference (llama-cpp-python + GGUF only) | |
| Multi-model: up to 2 models loaded simultaneously. | |
| Each model lives in ./model/<ModelName>/chunks/*.gguf | |
| Endpoints: | |
| GET / — API index + examples | |
| GET /health — all models status | |
| GET/POST /chat?message=... — JSON reply | |
| GET/POST /chat/stream?message= — streaming text/plain reply | |
| GET /memory?session=X — list conversation memory | |
| POST /memory/clear — clear session memory | |
| GET /models — all configured models + disk status | |
| GET /models/quants?model=X — HF quantization list | |
| Params: | |
| message — required prompt | |
| model — which model to use (default: first active model) | |
| e.g. ?model=Qwen3.5-4B or ?model=DeepSeek-R1-8B | |
| mode — fast | thinking | balanced | code | auto (default: auto) | |
| max_tokens — int | "auto" (overrides mode token budget) | |
| greedy — 1/true → deterministic, fastest decode | |
| thinking — 1/true → enable <think> CoT | |
| raw — 1/true → NO system prompt (jailbreak / base mode) | |
| session — str (default "default") → isolate memory namespaces | |
| no_memory — 1/true → skip memory lookup | |
| """ | |
| import os, re, json, time, threading, subprocess, sys | |
| from flask import Flask, request, jsonify, Response, stream_with_context | |
| from flask.json.provider import DefaultJSONProvider | |
| _CPU_THREADS = os.cpu_count() or 4 | |
| class _PrettyJSON(DefaultJSONProvider): | |
| def dumps(self, obj, **kw): | |
| kw.setdefault("indent", 2) | |
| kw.setdefault("ensure_ascii", False) | |
| kw.setdefault("sort_keys", False) | |
| return json.dumps(obj, **kw) | |
| app = Flask(__name__) | |
| app.json_provider_class = _PrettyJSON | |
| app.json = _PrettyJSON(app) | |
| # ── Memory (lazy-loaded) ────────────────────────────────────────────────────── | |
| def _mem(): | |
| try: | |
| import memory as _m | |
| return _m | |
| except Exception: | |
| return None | |
| # ── Config ──────────────────────────────────────────────────────────────────── | |
| def _load_json() -> dict: | |
| p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "install.json") | |
| try: | |
| with open(p) as f: | |
| return json.load(f) | |
| except Exception: | |
| return {} | |
| def _cfg(key, section, default): | |
| v = os.environ.get(key) | |
| if v is not None: | |
| return v | |
| c = _load_json() | |
| key_map = { | |
| "MAX_NEW_TOKENS": "max_new_tokens", "TEMPERATURE": "temperature", | |
| "TOP_P": "top_p", "MAX_SEQ_LEN": "max_seq_len", "PORT": "port", | |
| } | |
| sk = key_map.get(key, key.lower()) | |
| if section == "server": | |
| v = c.get("server", {}).get(sk) | |
| return v if v is not None else default | |
| def _get_active_models(cfg: dict) -> list: | |
| v = cfg.get("active_models") | |
| if isinstance(v, list): | |
| return [x for x in v if x] | |
| s = cfg.get("active_model", "") | |
| return [s] if s else [] | |
| # ── Constants (read once at startup) ───────────────────────────────────────── | |
| _CFG_J = _load_json() | |
| MAX_NEW_TOKENS = int(_cfg("MAX_NEW_TOKENS", "server", 512)) | |
| TEMPERATURE = float(_cfg("TEMPERATURE", "server", 0.6)) | |
| TOP_P = float(_cfg("TOP_P", "server", 0.9)) | |
| MAX_SEQ_LEN = int(_cfg("MAX_SEQ_LEN", "server", 4096)) | |
| PORT = int(_cfg("PORT", "server", 5000)) | |
| # ── Runtime state ───────────────────────────────────────────────────────────── | |
| _backends: dict = {} # model_name → GGUFBackend (loaded & ready) | |
| _backend_errors: dict = {} # model_name → error string | |
| _loading_names: set = set()# model names currently being loaded | |
| model_loading = True # True until all models finish (or fail) | |
| load_start = time.time() | |
| _setup_status = "starting" | |
| def _get_backend(model_param: str | None = None): | |
| """ | |
| Resolve model_param to a loaded GGUFBackend. | |
| Returns (backend, error_msg). backend is None on error. | |
| """ | |
| if not model_param: | |
| if _backends: | |
| return next(iter(_backends.values())), None | |
| return None, "No models loaded yet" | |
| # Exact match | |
| if model_param in _backends: | |
| return _backends[model_param], None | |
| # Case-insensitive / partial match | |
| p = model_param.lower() | |
| for name, be in _backends.items(): | |
| if p in name.lower(): | |
| return be, None | |
| loaded = list(_backends.keys()) | |
| if not loaded: | |
| return None, f"No models loaded yet. Requested: '{model_param}'" | |
| return None, ( | |
| f"Model '{model_param}' not loaded. " | |
| f"Loaded: {loaded}. " | |
| f"Use ?model= with one of the loaded model names." | |
| ) | |
| # ── Keyword sets ────────────────────────────────────────────────────────────── | |
| _CODING_KW = { | |
| "code", "html", "css", "javascript", "python", "java", "c++", "bash", "sql", | |
| "express", "flask", "django", "react", "vue", "node", "typescript", "rust", | |
| "write", "create", "implement", "function", "class", "script", "snippet", | |
| "algorithm", "debug", "fix", "api", "loop", "array", "dict", "json", | |
| "program", "build", "make", "compile", "parse", "regex", "database", | |
| } | |
| _DETAIL_KW = { | |
| "explain", "describe", "history", "compare", "difference", | |
| "analyze", "summarize", "how does", "why does", | |
| "tell me about", "in detail", "step by step", | |
| "advantages", "disadvantages", "pros and cons", | |
| "what is", "what are", "ano ang", "anong", "paano", "bakit", | |
| "pilipinas", "philippines", "kultura", "culture", "traditional", | |
| } | |
| _THINKING_KW = { | |
| "solve", "calculate", "prove", "reason", "logic", | |
| "math", "equation", "plan", "strategy", "optimize", | |
| "figure out", "work out", "deduce", | |
| } | |
| _GREET = { | |
| "hi", "hello", "hey", "sup", "yo", "hola", | |
| "kumusta", "kamusta", "greetings", "howdy", | |
| "magandang umaga", "magandang hapon", "magandang gabi", | |
| "good morning", "good afternoon", "good evening", "good day", | |
| } | |
| def _is_greeting(msg: str) -> bool: | |
| m = msg.lower().strip().rstrip("!?.,") | |
| w = m.split() | |
| return m in _GREET or (len(w) <= 3 and any(m.startswith(g) for g in _GREET)) | |
| # ── Token budgets per mode ──────────────────────────────────────────────────── | |
| _MODE_TOKENS = { | |
| "fast": 120, | |
| "thinking": 800, | |
| "thinking_fast": 400, | |
| "balanced": None, | |
| "code": 600, | |
| "auto": None, | |
| } | |
| _MODE_GREEDY = { | |
| "fast": True, | |
| "thinking": False, | |
| "thinking_fast": False, | |
| "balanced": None, | |
| "code": False, | |
| "auto": None, | |
| } | |
| def _auto_max_tokens(message: str, thinking: bool) -> int: | |
| if thinking: | |
| return 700 | |
| msg = message.lower().strip() | |
| n = len(msg.split()) | |
| if _is_greeting(message): | |
| return 100 | |
| if any(k in msg for k in _CODING_KW): | |
| return 350 | |
| if any(k in msg for k in _THINKING_KW): | |
| return 200 | |
| if any(k in msg for k in _DETAIL_KW) or n > 8: | |
| return 200 | |
| return 120 | |
| def _resolve_mode_params(message: str, mode_raw: str, | |
| thinking_flag: bool, greedy_flag: bool, | |
| max_tok_raw) -> tuple: | |
| mode = (mode_raw or "auto").lower().strip() | |
| if mode not in _MODE_TOKENS: | |
| mode = "auto" | |
| thinking = thinking_flag | |
| if mode in ("thinking", "thinking_fast"): | |
| thinking = True | |
| elif mode == "fast": | |
| thinking = False | |
| if max_tok_raw is not None and str(max_tok_raw).lower() != "auto": | |
| try: | |
| max_tok = max(1, min(int(max_tok_raw), 4096)) | |
| except (TypeError, ValueError): | |
| max_tok = _auto_max_tokens(message, thinking) | |
| else: | |
| budget = _MODE_TOKENS[mode] | |
| max_tok = budget if budget is not None else _auto_max_tokens(message, thinking) | |
| if greedy_flag: | |
| greedy = True | |
| elif _MODE_GREEDY[mode] is not None: | |
| greedy = _MODE_GREEDY[mode] | |
| else: | |
| greedy = _is_greeting(message) and not thinking | |
| return thinking, greedy, max_tok, mode | |
| # ── System prompts ──────────────────────────────────────────────────────────── | |
| _SYS = { | |
| "greet": ( | |
| "You are a friendly, helpful AI assistant. " | |
| "Greet the user warmly in 2–3 sentences. " | |
| "Mention you can answer questions, write code, explain topics, and chat." | |
| ), | |
| "brief": ( | |
| "You are a knowledgeable assistant. " | |
| "Give a clear, accurate answer in 2–3 sentences. Be direct and helpful." | |
| ), | |
| "detail": ( | |
| "You are a knowledgeable assistant. " | |
| "Explain clearly and accurately in 3–6 sentences. Cover the key points." | |
| ), | |
| "code": ( | |
| "You are an expert programmer. " | |
| "Write clean, working code with brief inline comments. " | |
| "No lengthy preamble — code first." | |
| ), | |
| "think": ( | |
| "You are an expert problem-solver and programmer. " | |
| "Think step by step inside <think> tags, then give a clear final answer." | |
| ), | |
| "fast": ( | |
| "You are a concise assistant. " | |
| "Answer in 1–2 sentences. Be direct and accurate. No padding." | |
| ), | |
| } | |
| def _auto_sys_mode(message: str, thinking: bool, mode: str) -> str: | |
| if mode == "thinking" or thinking: | |
| return "think" | |
| if mode == "fast": | |
| return "fast" | |
| if mode == "code": | |
| return "code" | |
| if _is_greeting(message): | |
| return "greet" | |
| msg = message.lower() | |
| if any(k in msg for k in _CODING_KW): | |
| return "code" | |
| if any(k in msg for k in _DETAIL_KW) or len(message.split()) > 8: | |
| return "detail" | |
| return "brief" | |
| _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL) | |
| def _clean(text: str) -> tuple: | |
| m = re.search(r"<think>(.*?)</think>", text, re.DOTALL) | |
| if m: | |
| think = m.group(1).strip() | |
| clean = _THINK_RE.sub("", text).strip() | |
| return clean, think if think else None | |
| if "<think>" in text: | |
| idx = text.index("<think>") | |
| think = text[idx + len("<think>"):].strip() | |
| clean = text[:idx].strip() | |
| return clean, think if think else None | |
| return text.strip(), None | |
| def _build_messages(message: str, thinking: bool = False, | |
| raw: bool = False, mode: str = "auto", | |
| context: list | None = None, | |
| backend=None) -> list: | |
| msgs = [] | |
| if not raw: | |
| sys_key = _auto_sys_mode(message, thinking, mode) | |
| msgs.append({"role": "system", "content": _SYS[sys_key]}) | |
| if context: | |
| ctx_text = "\n---\n".join(context[-2:]) | |
| msgs.append({ | |
| "role": "assistant", | |
| "content": f"[Relevant past context]\n{ctx_text}", | |
| }) | |
| user_content = message | |
| if not thinking and backend is not None: | |
| try: | |
| from gguf_backend import GGUFBackend | |
| if isinstance(backend, GGUFBackend): | |
| user_content = message + " /no_think" | |
| except ImportError: | |
| pass | |
| msgs.append({"role": "user", "content": user_content}) | |
| return msgs | |
| # ── Background setup helpers ────────────────────────────────────────────────── | |
| def _pip_install(*packages: str) -> None: | |
| cmd = [sys.executable, "-m", "pip", "install", "-q", "--no-cache-dir"] + list(packages) | |
| subprocess.run(cmd, capture_output=True) | |
| def _pkg_ok(mod: str) -> bool: | |
| try: | |
| __import__(mod) | |
| if mod == "llama_cpp": | |
| import llama_cpp as _lc | |
| _ = _lc.llama_backend_init | |
| return True | |
| except (ImportError, RuntimeError, OSError, AttributeError): | |
| return False | |
| def _ensure_optional_packages() -> None: | |
| global _setup_status | |
| core = [] | |
| for mod, pkg in [ | |
| ("llama_cpp", "llama-cpp-python"), | |
| ("huggingface_hub", "huggingface-hub"), | |
| ("numpy", "numpy"), | |
| ]: | |
| if not _pkg_ok(mod): | |
| core.append(pkg) | |
| if core: | |
| _setup_status = f"installing: {', '.join(core)}" | |
| print(f"[startup] Installing core packages: {core}", flush=True) | |
| if "llama-cpp-python" in core: | |
| subprocess.run( | |
| [sys.executable, "-m", "pip", "install", "-q", "--no-cache-dir", | |
| "llama-cpp-python", | |
| "--extra-index-url", "https://abetlen.github.io/llama-cpp-python/whl/cpu"], | |
| capture_output=True, | |
| ) | |
| core = [p for p in core if p != "llama-cpp-python"] | |
| if core: | |
| _pip_install(*core) | |
| print("[startup] Core packages ready", flush=True) | |
| optional = [] | |
| for mod, pkg in [ | |
| ("chromadb", "chromadb"), | |
| ("sentence_transformers","sentence-transformers"), | |
| ]: | |
| if not _pkg_ok(mod): | |
| optional.append(pkg) | |
| if optional: | |
| _setup_status = f"installing: {', '.join(optional)}" | |
| print(f"[startup] Installing memory packages: {optional}", flush=True) | |
| _pip_install(*optional) | |
| print("[startup] Memory packages ready", flush=True) | |
| def _resolve_model_path(name: str, cfg: dict) -> str: | |
| """Return full path to the model file (may not exist yet — chunks handle it).""" | |
| model_dir = cfg.get("model_dir", "./model") | |
| mc = cfg.get("models", {}).get(name, {}) | |
| fn = mc.get("file", "") | |
| if not fn: | |
| raise RuntimeError(f"Model '{name}' has no file configured in install.json") | |
| return os.path.join(model_dir, name, fn) | |
| def _chunks_exist_for(model_path: str) -> bool: | |
| """True if split chunks exist for the model file.""" | |
| base = os.path.splitext(os.path.basename(model_path))[0] | |
| chunk_dir = os.path.join(os.path.dirname(model_path), "chunks") | |
| if not os.path.isdir(chunk_dir): | |
| return False | |
| return any( | |
| f.startswith(base) and (f.endswith(".gguf") or f.endswith(".part")) | |
| for f in os.listdir(chunk_dir) | |
| ) | |
| # ── Model loading (background thread) ──────────────────────────────────────── | |
| def load_all_models(): | |
| global model_loading, _setup_status | |
| try: | |
| _setup_status = "checking packages" | |
| _ensure_optional_packages() | |
| from gguf_backend import GGUFBackend | |
| cfg = _load_json() | |
| actives = _get_active_models(cfg) | |
| if not actives: | |
| _setup_status = "error: no active_models in install.json" | |
| print(f"[startup] ERROR: active_models is empty in install.json", flush=True) | |
| return | |
| _loading_names.update(actives) | |
| for name in actives: | |
| _setup_status = f"loading {name}" | |
| try: | |
| model_path = _resolve_model_path(name, cfg) | |
| # Check that file or chunks exist | |
| if not os.path.isfile(model_path) and not _chunks_exist_for(model_path): | |
| raise RuntimeError( | |
| f"Model file not found: {model_path}\n" | |
| f"Download with: python model_manager.py download\n" | |
| f"Or switch: python model_manager.py use <ModelName>" | |
| ) | |
| print(f"[startup] Loading {name} ...", flush=True) | |
| backend = GGUFBackend( | |
| model_path, | |
| n_threads=_CPU_THREADS, | |
| n_ctx=MAX_SEQ_LEN, | |
| ) | |
| _backends[name] = backend | |
| elapsed = round(time.time() - load_start, 1) | |
| print(f"[startup] {name} ready — {backend.backend} ({elapsed}s)", flush=True) | |
| except Exception as e: | |
| _backend_errors[name] = str(e) | |
| print(f"[startup] {name} FAILED: {e}", flush=True) | |
| import traceback; traceback.print_exc() | |
| finally: | |
| _loading_names.discard(name) | |
| except Exception as e: | |
| _setup_status = f"error: {e}" | |
| print(f"[startup] FATAL: {e}", flush=True) | |
| import traceback; traceback.print_exc() | |
| finally: | |
| model_loading = False | |
| if _backends: | |
| names = list(_backends.keys()) | |
| _setup_status = f"ready ({len(names)} model{'s' if len(names)>1 else ''})" | |
| else: | |
| _setup_status = "error: all models failed" | |
| total = round(time.time() - load_start, 1) | |
| print(f"[startup] Load complete in {total}s — ready: {list(_backends.keys())}", flush=True) | |
| threading.Thread(target=load_all_models, daemon=True).start() | |
| # ── Request helpers ─────────────────────────────────────────────────────────── | |
| def _get(key, default=None): | |
| if request.method == "GET": | |
| return request.args.get(key, default) | |
| return (request.get_json(silent=True) or {}).get(key, default) | |
| def _ready(): | |
| """Check if at least one model is ready to serve requests.""" | |
| if model_loading and not _backends: | |
| loading_list = list(_loading_names) | |
| return False, jsonify({ | |
| "error": "Models loading — check /health", | |
| "stage": _setup_status, | |
| "loading": loading_list, | |
| }), 503 | |
| if not _backends and _backend_errors: | |
| errs = _backend_errors | |
| return False, jsonify({ | |
| "error": "All models failed to load", | |
| "errors": errs, | |
| }), 500 | |
| return True, None, None | |
| def _parse_bool(val, default: bool = False) -> bool: | |
| if val is None: | |
| return default | |
| return str(val).lower() in ("1", "true", "yes") | |
| # ── Routes ──────────────────────────────────────────────────────────────────── | |
| def index(): | |
| base = request.host_url.rstrip("/") | |
| status = "loading" if (model_loading and not _backends) else ("error" if (not _backends) else "ok") | |
| loaded = list(_backends.keys()) | |
| return jsonify({ | |
| "name": "Local LLM API (multi-model)", | |
| "status": status, | |
| "loaded_models": loaded, | |
| "threads": _CPU_THREADS, | |
| "params": { | |
| "message": "required — the prompt", | |
| "model": f"which model — e.g. {loaded[0] if loaded else 'ModelName'}", | |
| "mode": "fast | thinking | balanced | code | auto (default: auto)", | |
| "max_tokens": "int | auto (overrides mode budget)", | |
| "greedy": "1/true → deterministic, fastest", | |
| "thinking": "1/true → CoT reasoning (same as mode=thinking)", | |
| "raw": "1/true → no system prompt (jailbreak)", | |
| "session": "string → memory namespace", | |
| "no_memory": "1/true → skip memory lookup", | |
| }, | |
| "modes": { | |
| "fast": f"greedy + 120 tok — ~1-2s ({base}/chat?message=hi&mode=fast)", | |
| "thinking": f"CoT + 800 tok — ~20-40s ({base}/chat?message=solve+x&mode=thinking)", | |
| "thinking_fast": f"CoT + 400 tok — ~10-20s ({base}/chat?message=solve+x&mode=thinking_fast)", | |
| "balanced": f"auto detect — 120-350 tok (default)", | |
| "code": f"code prompt + 600 tok ({base}/chat?message=make+a+flask+api&mode=code)", | |
| }, | |
| "examples": { | |
| "fast_hello": f"{base}/chat?message=hi&mode=fast&no_memory=1", | |
| "with_deepseek": f"{base}/chat?message=solve+fibonacci&model=DeepSeek-R1-8B&mode=thinking", | |
| "with_qwen": f"{base}/chat?message=write+hello+world&model=Qwen3.5-4B&mode=code", | |
| "balanced": f"{base}/chat/stream?message=what+is+gravity&no_memory=1", | |
| "memory": f"{base}/memory?session=default", | |
| }, | |
| "model_management": { | |
| "list_models": f"{base}/models", | |
| "list_quants": f"{base}/models/quants?model=Qwen3.5-4B", | |
| "cli_list": "python model_manager.py list", | |
| "cli_add": "python model_manager.py use DeepSeek-R1-8B", | |
| "cli_remove": "python model_manager.py remove DeepSeek-R1-8B", | |
| "cli_quants": "python model_manager.py quants Qwen3.5-4B", | |
| }, | |
| }), 200 | |
| def health(): | |
| elapsed = round(time.time() - load_start, 1) | |
| if model_loading and not _backends: | |
| return jsonify({ | |
| "status": "loading", | |
| "stage": _setup_status, | |
| "loading": list(_loading_names), | |
| "elapsed": elapsed, | |
| }), 503 | |
| mem_info = {} | |
| m = _mem() | |
| if m: | |
| try: | |
| mem_info = m.stats() | |
| except Exception: | |
| mem_info = {"error": "unavailable"} | |
| models_info = {} | |
| for name, be in _backends.items(): | |
| models_info[name] = { | |
| "status": "ok", | |
| "backend": be.backend, | |
| "model_file": getattr(be, "model_fn", "N/A"), | |
| "quantization": getattr(be, "quant", "N/A"), | |
| } | |
| for name, err in _backend_errors.items(): | |
| if name not in models_info: | |
| models_info[name] = {"status": "error", "error": err} | |
| overall = "ok" if _backends else "error" | |
| if _backends and _backend_errors: | |
| overall = "partial" # some loaded, some failed | |
| return jsonify({ | |
| "status": overall, | |
| "loaded": list(_backends.keys()), | |
| "failed": list(_backend_errors.keys()), | |
| "models": models_info, | |
| "threads": _CPU_THREADS, | |
| "n_ctx": MAX_SEQ_LEN, | |
| "startup_sec": elapsed, | |
| "memory": mem_info, | |
| "modes": list(_MODE_TOKENS.keys()), | |
| }), 200 if overall != "error" else 500 | |
| def _chat_common(stream_mode: bool): | |
| ok, *rest = _ready() | |
| if not ok: | |
| return rest[0], rest[1] | |
| # Resolve which model to use | |
| model_param = (_get("model") or "").strip() or None | |
| backend, err_msg = _get_backend(model_param) | |
| if backend is None: | |
| loaded = list(_backends.keys()) | |
| code = 404 if (model_param and loaded) else 503 | |
| return jsonify({ | |
| "error": err_msg, | |
| "loaded_models": loaded, | |
| }), code | |
| message = (_get("message") or "").strip() | |
| if not message: | |
| return jsonify({"error": "Missing 'message'."}), 400 | |
| mode_raw = (_get("mode") or "auto").strip().lower() | |
| think_flag = _parse_bool(_get("thinking"), False) | |
| greedy_flag = _parse_bool(_get("greedy"), False) | |
| no_memory = _parse_bool(_get("no_memory"),False) | |
| raw = _parse_bool(_get("raw"), False) | |
| session = (_get("session") or "default").strip() | |
| thinking, greedy, max_tok, mode = _resolve_mode_params( | |
| message, mode_raw, think_flag, greedy_flag, _get("max_tokens") | |
| ) | |
| if raw: | |
| thinking = think_flag | |
| mode = "raw" | |
| context: list = [] | |
| m = _mem() | |
| if m and not no_memory and not raw and not _is_greeting(message): | |
| try: | |
| context = m.retrieve(message, session=session, top_k=2) | |
| except Exception: | |
| pass | |
| msgs = _build_messages(message, thinking=thinking, raw=raw, | |
| mode=mode, context=context, backend=backend) | |
| be_name = getattr(backend, "backend", "GGUF") | |
| if stream_mode: | |
| def stream_gen(): | |
| try: | |
| full = "" | |
| for chunk in backend.stream(msgs, max_tokens=max_tok, | |
| temperature=TEMPERATURE, top_p=TOP_P, | |
| top_k=40, greedy=greedy): | |
| if chunk.startswith("\x00"): | |
| clean = chunk[1:] | |
| if m and not no_memory and clean: | |
| threading.Thread( | |
| target=m.store, args=(message, clean), | |
| kwargs={"session": session}, daemon=True, | |
| ).start() | |
| else: | |
| full += chunk | |
| yield chunk | |
| except Exception as e: | |
| yield f"\n[ERROR] {e}" | |
| return Response( | |
| stream_with_context(stream_gen()), | |
| content_type="text/plain; charset=utf-8", | |
| ) | |
| try: | |
| t0 = time.time() | |
| response, thk = backend.generate(msgs, max_tokens=max_tok, | |
| temperature=TEMPERATURE, top_p=TOP_P, | |
| top_k=40, greedy=greedy) | |
| elapsed = round(time.time() - t0, 2) | |
| if m and not no_memory and response: | |
| threading.Thread(target=m.store, args=(message, response), | |
| kwargs={"session": session}, daemon=True).start() | |
| result = { | |
| "message": message, | |
| "response": response, | |
| "elapsed_seconds": elapsed, | |
| "max_tokens_used": max_tok, | |
| "mode": mode, | |
| "greedy": greedy, | |
| "thinking": thinking, | |
| "raw": raw, | |
| "session": session, | |
| "memory_context": len(context), | |
| "model": model_param or list(_backends.keys())[0], | |
| "backend": be_name, | |
| } | |
| if thk: | |
| result["think_block"] = thk | |
| return jsonify(result), 200 | |
| except Exception as e: | |
| import traceback; traceback.print_exc() | |
| return jsonify({"error": str(e)}), 500 | |
| def chat(): | |
| return _chat_common(stream_mode=False) | |
| def chat_stream(): | |
| return _chat_common(stream_mode=True) | |
| # ── Memory endpoints ────────────────────────────────────────────────────────── | |
| def memory_list(): | |
| m = _mem() | |
| if not m: | |
| return jsonify({"error": "Memory module not available"}), 503 | |
| session = (_get("session") or "default").strip() | |
| try: | |
| return jsonify({ | |
| "session": session, | |
| "stats": m.stats(), | |
| "recent": m.list_recent(session=session, limit=20), | |
| }), 200 | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| def memory_clear(): | |
| m = _mem() | |
| if not m: | |
| return jsonify({"error": "Memory module not available"}), 503 | |
| session = (_get("session") or "default").strip() | |
| try: | |
| deleted = m.clear(session=session) | |
| return jsonify({"session": session, "deleted": deleted}), 200 | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| # ── Model list / HF quant query endpoints ──────────────────────────────────── | |
| def models_list(): | |
| try: | |
| from model_manager import get_models_status | |
| status = get_models_status(_load_json()) | |
| # Annotate which models are currently loaded in memory | |
| for name in status.get("models", {}): | |
| status["models"][name]["loaded_in_memory"] = name in _backends | |
| status["loaded_in_memory"] = list(_backends.keys()) | |
| return jsonify(status), 200 | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| def models_quants(): | |
| target = (request.args.get("model") or "").strip() | |
| if not target: | |
| return jsonify({ | |
| "error": "Missing ?model= parameter", | |
| "example": "/models/quants?model=Qwen3.5-4B", | |
| "known_models": list(_load_json().get("models", {}).keys()), | |
| }), 400 | |
| try: | |
| from model_manager import get_quants_for_model | |
| files = get_quants_for_model(target, _load_json()) | |
| by_bits: dict = {} | |
| for f in files: | |
| by_bits.setdefault(f["bits"], []).append({ | |
| "file": f["name"], | |
| "quant": f["quant"], | |
| "size_gb": f["size_gb"], | |
| }) | |
| return jsonify({ | |
| "model": target, | |
| "total": len(files), | |
| "by_bits": by_bits, | |
| "all": files, | |
| }), 200 | |
| except RuntimeError as e: | |
| return jsonify({"error": str(e)}), 502 | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| if __name__ == "__main__": | |
| print(f"[server] LLM API port={PORT} threads={_CPU_THREADS}") | |
| print(f"[server] Open: http://localhost:{PORT}/") | |
| app.run(host="0.0.0.0", port=PORT, threaded=True) | |