Electro0023 commited on
Commit
300bbfa
Β·
verified Β·
1 Parent(s): 75442f0

Replace Gradio SmolVLM app with DeepSeek-7B-base GGUF OpenAI-compatible API (Docker)

Browse files
Files changed (8) hide show
  1. .dockerignore +12 -0
  2. .gitignore +16 -0
  3. Dockerfile +52 -0
  4. README.md +60 -8
  5. app.py +0 -84
  6. main.py +250 -0
  7. packages.txt +0 -1
  8. requirements.txt +12 -8
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Keep the build context (and the image) small and model-free.
2
+ *.gguf
3
+ *.bin
4
+ *.safetensors
5
+ venv/
6
+ .venv/
7
+ __pycache__/
8
+ *.pyc
9
+ .git/
10
+ .github/
11
+ .env
12
+ *.md
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Never commit models or caches β€” the model is served from a HF model repo.
2
+ *.gguf
3
+ *.bin
4
+ *.safetensors
5
+ .cache/
6
+ huggingface/
7
+ hf_home/
8
+
9
+ # Python
10
+ venv/
11
+ .venv/
12
+ __pycache__/
13
+ *.pyc
14
+
15
+ # Local env
16
+ .env
Dockerfile ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── GGUF LLM API β€” Hugging Face Spaces (Docker SDK), CPU tier by default ───────
2
+ # Builds llama-cpp-python from source, so the C/C++ toolchain is installed first.
3
+ FROM python:3.11-slim
4
+
5
+ # 1) Build toolchain required to compile llama-cpp-python.
6
+ # build-essential = gcc + g++ + make; cmake is what its build backend invokes.
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ build-essential \
9
+ gcc \
10
+ g++ \
11
+ make \
12
+ cmake \
13
+ git \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # 2) Hugging Face Spaces runs the container as a non-root user (uid 1000).
17
+ # Create it and put ALL writable paths under its home so model downloads work.
18
+ RUN useradd -m -u 1000 user
19
+ USER user
20
+ ENV HOME=/home/user \
21
+ PATH=/home/user/.local/bin:$PATH \
22
+ # HF_HOME is where huggingface_hub caches the downloaded .gguf at startup.
23
+ # It MUST be writable by uid 1000 β€” /home/user is, /root and /app often are not.
24
+ HF_HOME=/home/user/.cache/huggingface \
25
+ # HF CPU-basic = 2 vCPUs. Pin llama.cpp + BLAS to 2 threads so they don't
26
+ # over-subscribe the host's core count and thrash. Raise these (or set the
27
+ # N_THREADS Space variable) if you move to bigger CPU/GPU hardware.
28
+ N_THREADS=2 \
29
+ OMP_NUM_THREADS=2
30
+
31
+ WORKDIR /home/user/app
32
+
33
+ # 3) Install Python deps first (better layer caching than copying everything).
34
+ # The --extra-index-url hosts PREBUILT CPU wheels for llama-cpp-python, so pip
35
+ # installs a wheel instead of compiling from source. This avoids the #1 cause
36
+ # of failed HF CPU builds: the source compile running out of memory. The gcc/
37
+ # cmake toolchain above stays only as a fallback if no wheel matches.
38
+ COPY --chown=user requirements.txt ./
39
+ RUN pip install --no-cache-dir --upgrade pip \
40
+ && pip install --no-cache-dir -r requirements.txt \
41
+ --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
42
+
43
+ # 4) Copy the (lightweight) application code. No model file is baked in β€” it is
44
+ # pulled from the HF model repo on startup.
45
+ COPY --chown=user . ./
46
+
47
+ # 5) Hugging Face Spaces routes public traffic to port 7860.
48
+ EXPOSE 7860
49
+
50
+ # 6) Single worker: llama.cpp holds one non-thread-safe context; the app serialises
51
+ # calls internally. Do NOT add --workers > 1 (each would reload the whole model).
52
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,14 +1,66 @@
1
  ---
2
- title: Model
3
- emoji: πŸ‘
4
  colorFrom: blue
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: DeepSeek 7B GGUF API
3
+ emoji: 🧠
4
  colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
 
 
8
  pinned: false
9
  license: apache-2.0
10
  ---
11
 
12
+ # DeepSeek-LLM-7B-base GGUF API (Docker Space)
13
+
14
+ An OpenAI-compatible FastAPI server that runs
15
+ **`TheBloke/deepseek-llm-7B-base-GGUF` β†’ `deepseek-llm-7b-base.Q4_K_M.gguf`**
16
+ with `llama-cpp-python`. The model file is **not** stored in this repo β€” it is
17
+ downloaded from the Hugging Face model hub on container startup, so this code
18
+ repo stays tiny.
19
+
20
+ > ⚠️ **The YAML block at the very top of this file is required.** Hugging Face
21
+ > reads `sdk: docker` and `app_port: 7860` from it to know how to build and
22
+ > route the Space. Do not delete it.
23
+
24
+ ## Endpoints
25
+
26
+ Base URL: `https://electro0023-model.hf.space`
27
+
28
+ | Method | Path | Purpose |
29
+ |--------|------------------------|-----------------------------------------------|
30
+ | GET | `/health` | Liveness + whether the model finished loading |
31
+ | GET | `/v1/models` | OpenAI-style model list |
32
+ | POST | `/v1/chat/completions` | OpenAI-compatible chat completion |
33
+ | POST | `/generate` | Simple `{prompt, max_tokens}` β†’ text |
34
+
35
+ ## Quick test
36
+
37
+ ```bash
38
+ curl https://electro0023-model.hf.space/health
39
+
40
+ curl -X POST https://electro0023-model.hf.space/v1/chat/completions \
41
+ -H "Content-Type: application/json" \
42
+ -d '{"model":"deepseek-7b-base","messages":[{"role":"user","content":"Newton'\''s second law is"}],"max_tokens":64}'
43
+ ```
44
+
45
+ ## Swapping models (no code changes)
46
+
47
+ Set Space **Variables** (Settings β†’ Variables and secrets) and restart:
48
+
49
+ - `REPO_ID` β€” HF model repo holding the GGUF
50
+ - `FILENAME` β€” the exact `.gguf` file inside that repo
51
+ - `MODEL_ID` β€” name reported to API clients
52
+ - `CHAT_FORMAT` β€” chat template for instruct models (e.g. `llama-3`, `chatml`);
53
+ leave empty for base models
54
+ - `N_CTX`, `N_THREADS`, `N_GPU_LAYERS`, `DEFAULT_MAX_TOKENS` β€” tuning knobs
55
+
56
+ ## Notes
57
+
58
+ - **This default model is a BASE model** (raw text completion, not
59
+ instruction-tuned). For chat/instruction behaviour, switch to
60
+ `TheBloke/deepseek-llm-7B-chat-GGUF` / `deepseek-llm-7b-chat.Q4_K_M.gguf`
61
+ with `CHAT_FORMAT=deepseek` β€” or any other instruct GGUF.
62
+ - On the free `cpu-basic` tier (2 vCPU, 16 GB RAM) a 7B Q4_K_M generates
63
+ roughly 1–3 tokens/sec. Expect long response times; set generous client
64
+ timeouts.
65
+ - First boot downloads ~4.1 GB, so allow several minutes before `/health`
66
+ reports `status: ok`.
app.py DELETED
@@ -1,84 +0,0 @@
1
- import subprocess
2
- import sys
3
-
4
- print("Installing dependencies...")
5
- subprocess.run([
6
- sys.executable, "-m", "pip", "install",
7
- "--force-reinstall", "--no-deps",
8
- "transformers==4.53.0",
9
- "tokenizers==0.23.1",
10
- ], check=True)
11
- print("Done installing")
12
-
13
- import torch
14
- import gradio as gr
15
- from transformers import AutoProcessor, AutoModelForVision2Seq
16
- from PIL import Image
17
-
18
- print(f"Transformers version: {__import__('transformers').__version__}")
19
-
20
- model_id = "HuggingFaceTB/SmolVLM-256M-Instruct"
21
-
22
- print("Loading processor...")
23
- processor = AutoProcessor.from_pretrained(model_id)
24
-
25
- print("Loading model...")
26
- model = AutoModelForVision2Seq.from_pretrained(
27
- model_id,
28
- torch_dtype=torch.float32,
29
- device_map="auto"
30
- )
31
- model.eval()
32
- print("Model ready!")
33
-
34
-
35
- def extract_text(image: Image.Image) -> str:
36
- if image is None:
37
- return "Please upload an image."
38
-
39
- messages = [
40
- {
41
- "role": "user",
42
- "content": [
43
- {"type": "image"},
44
- {"type": "text", "text": (
45
- "Extract all text from this image exactly as it appears. "
46
- "Preserve question numbers, options A B C D, "
47
- "tables, and any mathematical or chemical expressions. "
48
- "Format clearly."
49
- )}
50
- ]
51
- }
52
- ]
53
-
54
- prompt = processor.apply_chat_template(
55
- messages,
56
- add_generation_prompt=True
57
- )
58
-
59
- inputs = processor(
60
- text=prompt,
61
- images=[image],
62
- return_tensors="pt"
63
- ).to(model.device)
64
-
65
- with torch.no_grad():
66
- outputs = model.generate(
67
- **inputs,
68
- max_new_tokens=1024,
69
- do_sample=False
70
- )
71
-
72
- generated = outputs[0][inputs["input_ids"].shape[1]:]
73
- return processor.decode(generated, skip_special_tokens=True)
74
-
75
-
76
- demo = gr.Interface(
77
- fn=extract_text,
78
- inputs=gr.Image(type="pil", label="Upload NEET Question Image"),
79
- outputs=gr.Textbox(label="Extracted Text", lines=20),
80
- title="NEET Question Extractor",
81
- description="Upload scanned NEET question paper image to extract text"
82
- )
83
-
84
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
main.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GGUF LLM API for Hugging Face Spaces (Docker SDK) β€” "Download on Startup".
3
+
4
+ On boot this app downloads a .gguf model from a Hugging Face **Model** repo with
5
+ huggingface_hub, loads it once with llama-cpp-python, and serves it behind an
6
+ OpenAI-compatible chat endpoint plus a simple /generate endpoint.
7
+
8
+ Nothing here is model-specific: which repo/file to pull is chosen entirely by
9
+ environment variables, so you redeploy a new model by changing a variable in the
10
+ Space settings β€” no code edits.
11
+
12
+ Environment variables
13
+ ----------------------
14
+ REPO_ID (required) HF model repo, e.g. "your-username/my-gguf-models"
15
+ FILENAME (required) the .gguf file inside that repo,
16
+ e.g. "mistral-7b-instruct-v0.2.Q4_K_M.gguf"
17
+ HF_TOKEN (optional) a READ token β€” ONLY needed if the model repo is PRIVATE
18
+ MODEL_ID (optional) name reported to clients (default: FILENAME)
19
+ N_CTX (optional) context window in tokens (default 4096)
20
+ N_THREADS (optional) CPU threads for inference (default: all cores)
21
+ N_GPU_LAYERS (optional) layers to offload to GPU (default 0 = CPU only)
22
+ CHAT_FORMAT (optional) e.g. "llama-3", "chatml", "mistral-instruct"
23
+ DEFAULT_MAX_TOKENS (optional) fallback max_tokens per request (default 512)
24
+ """
25
+ import os
26
+ import time
27
+ import uuid
28
+ import logging
29
+ import threading
30
+ from contextlib import asynccontextmanager
31
+ from typing import List, Optional, Dict, Any
32
+
33
+ from fastapi import FastAPI, HTTPException
34
+ from pydantic import BaseModel, Field
35
+ from huggingface_hub import hf_hub_download
36
+ from llama_cpp import Llama
37
+
38
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
39
+ log = logging.getLogger("gguf-api")
40
+
41
+ # ── Configuration (all via environment) ───────────────────────────────────────
42
+ # Defaults point at DeepSeek-LLM-7B-base (Q4_K_M), a PUBLIC GGUF β€” so the Space
43
+ # runs with zero setup. Override REPO_ID/FILENAME in the Space settings to swap
44
+ # models without touching the code.
45
+ REPO_ID = os.getenv("REPO_ID", "TheBloke/deepseek-llm-7B-base-GGUF").strip()
46
+ FILENAME = os.getenv("FILENAME", "deepseek-llm-7b-base.Q4_K_M.gguf").strip()
47
+ HF_TOKEN = os.getenv("HF_TOKEN") or None # None => anonymous (public repo)
48
+ MODEL_ID = os.getenv("MODEL_ID", "") or "deepseek-7b-base" # name reported to clients
49
+ N_CTX = int(os.getenv("N_CTX", "4096"))
50
+ # Default to the number of *usable* CPUs (respects cgroup cpuset), NOT os.cpu_count()
51
+ # which over-reports the host's cores inside a container and causes thread thrash.
52
+ _USABLE_CPUS = len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 4)
53
+ N_THREADS = int(os.getenv("N_THREADS", str(_USABLE_CPUS)))
54
+ N_GPU_LAYERS = int(os.getenv("N_GPU_LAYERS", "0")) # 0 = CPU only; -1 = all layers on GPU
55
+ # deepseek-llm-7b-BASE has no chat template (it is a raw completion model), so no
56
+ # chat format is forced by default; llama-cpp falls back to its generic template.
57
+ # If you swap to an instruct/chat GGUF, set CHAT_FORMAT accordingly (e.g. "llama-3").
58
+ CHAT_FORMAT = os.getenv("CHAT_FORMAT", "").strip()
59
+ DEFAULT_MAX_TOKENS = int(os.getenv("DEFAULT_MAX_TOKENS", "512"))
60
+
61
+ # The loaded model lives here. It is None until startup finishes (or if it failed).
62
+ # llama-cpp is NOT thread-safe for concurrent generation on one context, so every
63
+ # call into the model is serialised behind this lock.
64
+ _llm: Optional[Llama] = None
65
+ _llm_lock = threading.Lock()
66
+ _load_error: Optional[str] = None
67
+
68
+
69
+ def _download_and_load() -> None:
70
+ """Download the .gguf from the HF model repo, then load it with llama-cpp."""
71
+ global _llm, _load_error
72
+
73
+ if not REPO_ID or not FILENAME:
74
+ _load_error = (
75
+ "REPO_ID and FILENAME must both be set. In your Space go to "
76
+ "Settings -> Variables and secrets and add REPO_ID (e.g. "
77
+ "'your-username/my-gguf-models') and FILENAME (e.g. 'model.Q4_K_M.gguf')."
78
+ )
79
+ log.error(_load_error)
80
+ return
81
+
82
+ try:
83
+ log.info("Downloading model '%s' from repo '%s' ...", FILENAME, REPO_ID)
84
+ t0 = time.time()
85
+ # hf_hub_download caches under HF_HOME and returns the local file path.
86
+ # Re-runs are instant once the file is cached in the container layer.
87
+ model_path = hf_hub_download(
88
+ repo_id=REPO_ID,
89
+ filename=FILENAME,
90
+ token=HF_TOKEN, # ignored for public repos, required for private
91
+ )
92
+ log.info("Model downloaded to %s (%.1fs)", model_path, time.time() - t0)
93
+
94
+ kwargs: Dict[str, Any] = dict(
95
+ model_path=model_path,
96
+ n_ctx=N_CTX,
97
+ n_threads=N_THREADS,
98
+ n_gpu_layers=N_GPU_LAYERS,
99
+ verbose=False,
100
+ )
101
+ if CHAT_FORMAT:
102
+ kwargs["chat_format"] = CHAT_FORMAT
103
+
104
+ log.info("Loading model into memory (n_ctx=%d, n_gpu_layers=%d) ...", N_CTX, N_GPU_LAYERS)
105
+ t0 = time.time()
106
+ _llm = Llama(**kwargs)
107
+ log.info("Model ready (%.1fs).", time.time() - t0)
108
+ except Exception as exc: # keep the server up so /health can report the reason
109
+ _load_error = f"Failed to download/load model: {exc}"
110
+ log.exception(_load_error)
111
+
112
+
113
+ @asynccontextmanager
114
+ async def lifespan(app: FastAPI):
115
+ """Start loading the model in a BACKGROUND thread so the HTTP port opens
116
+ immediately.
117
+
118
+ Hugging Face marks a Space "in error" if port 7860 doesn't open within its
119
+ startup window. Downloading a ~2 GB model and loading it *before* the server
120
+ binds would blow past that window (this is exactly what crashes naive Spaces).
121
+ Loading off-thread lets uvicorn bind 7860 in seconds; /health reports
122
+ "loading_or_error" until the model is ready, and inference endpoints return a
123
+ clear 503 until then instead of failing the whole container.
124
+ """
125
+ threading.Thread(target=_download_and_load, name="model-loader", daemon=True).start()
126
+ yield
127
+ # (nothing to clean up: process exit releases the model)
128
+
129
+
130
+ app = FastAPI(title="GGUF LLM API (HF Spaces)", version="1.0.0", lifespan=lifespan)
131
+
132
+
133
+ # ── Request/response schemas ──────────────────────────────────────────────────
134
+ class ChatMessage(BaseModel):
135
+ role: str
136
+ content: str = ""
137
+
138
+
139
+ class ChatCompletionRequest(BaseModel):
140
+ model: Optional[str] = None
141
+ messages: List[ChatMessage]
142
+ temperature: float = 0.7
143
+ top_p: float = 0.95
144
+ max_tokens: Optional[int] = None
145
+ stop: Optional[List[str]] = None
146
+ # OpenAI JSON mode: {"type":"json_object"} -> llama.cpp grammar-constrains the
147
+ # reply to VALID JSON. Essential for small models, which otherwise emit
148
+ # unbalanced brackets (e.g. a stray "]" after each array element).
149
+ response_format: Optional[Dict[str, Any]] = None
150
+
151
+
152
+ class GenerateRequest(BaseModel):
153
+ prompt: str = Field(..., description="Plain text prompt for the model.",
154
+ examples=["State Newton's second law of motion in one sentence."])
155
+ max_tokens: int = Field(DEFAULT_MAX_TOKENS, description="Max tokens to generate.")
156
+ temperature: float = Field(0.7, description="Sampling temperature (0 = deterministic).")
157
+
158
+
159
+ def _require_model() -> Llama:
160
+ """Return the loaded model or raise 503 with a helpful message."""
161
+ if _llm is None:
162
+ raise HTTPException(
163
+ status_code=503,
164
+ detail=_load_error or "Model is still loading, try again in a few seconds.",
165
+ )
166
+ return _llm
167
+
168
+
169
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
170
+ @app.get("/health")
171
+ def health() -> Dict[str, Any]:
172
+ """Liveness probe. status='ok' only once the model is loaded and ready."""
173
+ # Report the CPU picture so thread over-subscription (the #1 cause of slow
174
+ # CPU generation) is visible: if cpu_count >> usable cores, llama.cpp threads
175
+ # thrash. N_THREADS is what we actually hand to llama.cpp.
176
+ usable = len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else None
177
+ return {
178
+ "status": "ok" if _llm is not None else "loading_or_error",
179
+ "model": MODEL_ID,
180
+ "repo_id": REPO_ID,
181
+ "filename": FILENAME,
182
+ "n_ctx": N_CTX,
183
+ "n_gpu_layers": N_GPU_LAYERS,
184
+ "n_threads": N_THREADS,
185
+ "cpu_count": os.cpu_count(),
186
+ "usable_cpus": usable,
187
+ "error": _load_error,
188
+ }
189
+
190
+
191
+ @app.get("/v1/models")
192
+ def list_models() -> Dict[str, Any]:
193
+ """OpenAI-shaped model list (a single loaded model)."""
194
+ return {"object": "list", "data": [{"id": MODEL_ID, "object": "model", "owned_by": "local"}]}
195
+
196
+
197
+ @app.post("/v1/chat/completions")
198
+ def chat_completions(req: ChatCompletionRequest) -> Dict[str, Any]:
199
+ """OpenAI-compatible chat completion (non-streaming)."""
200
+ llm = _require_model()
201
+ if not req.messages:
202
+ raise HTTPException(status_code=400, detail="`messages` must not be empty.")
203
+
204
+ kwargs: Dict[str, Any] = dict(
205
+ messages=[m.model_dump() for m in req.messages],
206
+ temperature=req.temperature,
207
+ top_p=req.top_p,
208
+ max_tokens=req.max_tokens if req.max_tokens is not None else DEFAULT_MAX_TOKENS,
209
+ stop=req.stop,
210
+ )
211
+ # Honour OpenAI JSON mode -> llama.cpp constrains generation to valid JSON.
212
+ if req.response_format and req.response_format.get("type") == "json_object":
213
+ kwargs["response_format"] = {"type": "json_object"}
214
+
215
+ try:
216
+ with _llm_lock: # serialise access to the single llama.cpp context
217
+ result = llm.create_chat_completion(**kwargs)
218
+ except Exception as exc:
219
+ raise HTTPException(status_code=500, detail=f"generation failed: {exc}") from exc
220
+
221
+ # llama-cpp already returns an OpenAI-shaped dict; normalise id/model/created.
222
+ result["id"] = result.get("id") or f"chatcmpl-{uuid.uuid4().hex}"
223
+ result["model"] = MODEL_ID
224
+ result["created"] = result.get("created") or int(time.time())
225
+ return result
226
+
227
+
228
+ @app.post("/generate")
229
+ def generate(req: GenerateRequest) -> Dict[str, Any]:
230
+ """Simplified endpoint: one prompt in, generated text out (plus timing)."""
231
+ llm = _require_model()
232
+ t0 = time.time()
233
+ try:
234
+ with _llm_lock:
235
+ result = llm.create_chat_completion(
236
+ messages=[{"role": "user", "content": req.prompt}],
237
+ temperature=req.temperature,
238
+ max_tokens=req.max_tokens,
239
+ )
240
+ except Exception as exc:
241
+ raise HTTPException(status_code=500, detail=f"generation failed: {exc}") from exc
242
+
243
+ usage = result.get("usage", {}) or {}
244
+ return {
245
+ "prompt": req.prompt,
246
+ "response": result["choices"][0]["message"]["content"],
247
+ "model": MODEL_ID,
248
+ "time_seconds": round(time.time() - t0, 2),
249
+ "output_tokens": usage.get("completion_tokens"),
250
+ }
packages.txt DELETED
@@ -1 +0,0 @@
1
- python3-pip
 
 
requirements.txt CHANGED
@@ -1,8 +1,12 @@
1
- torch
2
- Pillow
3
- accelerate
4
- python-multipart
5
- einops
6
- regex
7
- safetensors
8
- tokenizers
 
 
 
 
 
1
+ # Pinned for reproducible Hugging Face Space builds.
2
+ # Bump these only deliberately β€” a floating llama-cpp-python version is the most
3
+ # common cause of a Space that built yesterday and fails to build today.
4
+
5
+ fastapi==0.115.6
6
+ uvicorn[standard]==0.34.0
7
+ pydantic==2.10.4
8
+ huggingface_hub==0.27.1
9
+
10
+ # llama-cpp-python is compiled from source in the Dockerfile (needs gcc/g++/make/cmake).
11
+ # 0.3.x ships the current llama.cpp; CPU-only build is the default.
12
+ llama-cpp-python==0.3.7