tommytracx commited on
Commit
955b771
·
verified ·
1 Parent(s): c341719

deploy latency-bounded interactive pool

Browse files

Exact source: ttracx/thoxroute@755605a45facd12e12c947d4cab7b886d80bd218

Files changed (4) hide show
  1. Dockerfile +17 -13
  2. README.md +11 -44
  3. app.py +312 -196
  4. requirements.txt +1 -14
Dockerfile CHANGED
@@ -1,10 +1,3 @@
1
- # THOX Rust Coder — llama.cpp via the conda-forge PREBUILT binary.
2
- #
3
- # Do not try to compile llama-cpp-python on an HF Space. It does not fit the
4
- # build timeout -- not in the Spaces pip step, and not in a Docker builder stage
5
- # either, even at -j16 with tests/examples/server/tools disabled. Docker buys a
6
- # glibc-linked extension, not build time. conda-forge ships a prebuilt linux-64
7
- # glibc binary: correct ABI, no compile, ~2 minute build.
8
  FROM mambaorg/micromamba:2.9-debian12
9
 
10
  USER root
@@ -12,22 +5,33 @@ RUN mkdir -p /app && chown 1000:1000 /app
12
  USER $MAMBA_USER
13
 
14
  RUN micromamba install -y -n base -c conda-forge \
15
- python=3.11 \
16
- pip \
17
  llama-cpp-python=0.3.34 \
 
 
 
18
  && micromamba clean --all --yes
19
 
20
  ARG MAMBA_DOCKERFILE_ACTIVATE=1
21
 
22
  COPY --chown=$MAMBA_USER:$MAMBA_USER requirements.txt /tmp/requirements.txt
23
- RUN pip install --no-cache-dir -r /tmp/requirements.txt
24
 
25
  ENV HF_HOME=/home/mambauser/.cache/huggingface \
26
- GRADIO_SERVER_NAME=0.0.0.0 \
27
- GRADIO_SERVER_PORT=7860
 
 
 
 
 
 
 
 
28
 
29
  WORKDIR /app
30
  COPY --chown=$MAMBA_USER:$MAMBA_USER app.py /app/app.py
31
 
32
  EXPOSE 7860
33
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
1
  FROM mambaorg/micromamba:2.9-debian12
2
 
3
  USER root
 
5
  USER $MAMBA_USER
6
 
7
  RUN micromamba install -y -n base -c conda-forge \
8
+ python=3.11.15 \
9
+ pip=26.2.1 \
10
  llama-cpp-python=0.3.34 \
11
+ fastapi=0.141.1 \
12
+ pydantic=2.13.4 \
13
+ uvicorn=0.52.3 \
14
  && micromamba clean --all --yes
15
 
16
  ARG MAMBA_DOCKERFILE_ACTIVATE=1
17
 
18
  COPY --chown=$MAMBA_USER:$MAMBA_USER requirements.txt /tmp/requirements.txt
19
+ RUN pip install --no-cache-dir -r /tmp/requirements.txt && pip check
20
 
21
  ENV HF_HOME=/home/mambauser/.cache/huggingface \
22
+ PORT=7860 \
23
+ THOX_FAST_MODEL_REPO=Qwen/Qwen2.5-0.5B-Instruct-GGUF \
24
+ THOX_FAST_MODEL_REVISION=9217f5db79a29953eb74d5343926648285ec7e67 \
25
+ THOX_FAST_MODEL_FILE=qwen2.5-0.5b-instruct-q4_k_m.gguf \
26
+ THOX_FAST_POOL_SIZE=2 \
27
+ THOX_MAX_OUTPUT_TOKENS=128
28
+
29
+ # Bake the immutable public interactive model into the image. A RUNNING Space
30
+ # therefore cannot report ready while still waiting on a first-request download.
31
+ RUN python -c "from huggingface_hub import hf_hub_download; hf_hub_download('Qwen/Qwen2.5-0.5B-Instruct-GGUF', 'qwen2.5-0.5b-instruct-q4_k_m.gguf', revision='9217f5db79a29953eb74d5343926648285ec7e67')"
32
 
33
  WORKDIR /app
34
  COPY --chown=$MAMBA_USER:$MAMBA_USER app.py /app/app.py
35
 
36
  EXPOSE 7860
37
+ CMD ["python", "/app/app.py"]
README.md CHANGED
@@ -1,53 +1,20 @@
1
  ---
2
- title: THOX Rust Coder
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: docker
7
  app_port: 7860
8
- pinned: false
9
  license: apache-2.0
10
- models:
11
- - Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF
12
  ---
13
 
14
- # THOX Rust Coder Qwen3-Coder-REAP-25B-A3B-Rust
15
 
16
- Hosted Rust coding model, served as GGUF through llama.cpp.
17
 
18
- ```bash
19
- curl -s -X POST https://thox-ai-thoxrustcoder.hf.space/v1/chat/completions \
20
- -H 'Content-Type: application/json' \
21
- -d '{"messages":[{"role":"user","content":"Write a Rust fn to parse semver."}],
22
- "max_tokens":256}'
23
- ```
24
 
25
- `GET /healthz` · `POST /v1/chat/completions` (OpenAI-shaped, ThoxRoute-registerable)
26
-
27
- ## "3B active" is not a memory budget
28
-
29
- This is MoE: **25B total, ~3B active per token.** The two numbers govern
30
- different resources, and conflating them leads to picking a tier that cannot
31
- load the model:
32
-
33
- | | sized by |
34
- |---|---|
35
- | **memory** | **25B total** — every expert stays resident, since the router may pick any of them on any token |
36
- | **compute** | **~3B active** — decode costs about what a 3B dense model costs |
37
-
38
- Q4_K_M is therefore **15.1 GB**, not "3B-worth". What MoE buys is speed per
39
- resident byte — which is precisely what makes a CPU tier viable: 25B-sized
40
- memory, 3B-sized arithmetic.
41
-
42
- Runs on `cpu-upgrade` (8 vCPU / 32 GB). Throughput is measured and reported in
43
- every response under `thox_perf`, and in the UI — no throughput claim appears in
44
- this README that was not taken from this Space.
45
-
46
- ## License — verified, not assumed
47
-
48
- | repo | license | checked |
49
- |---|---|---|
50
- | `Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF` | **Apache-2.0** | ungated, public |
51
-
52
- Apache-2.0 permits hosting, commercial use and redistribution. Verified against
53
- the model repo's own metadata before this Space was published.
 
1
  ---
2
+ title: THOX Interactive and Rust Coder
3
+ emoji: "⚡"
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
  app_port: 7860
 
8
  license: apache-2.0
 
 
9
  ---
10
 
11
+ # THOX interactive and specialist model service
12
 
13
+ This Space exposes one OpenAI-compatible endpoint:
14
 
15
+ - `thox-fast-chat`: eagerly loaded, bounded, interactive Qwen2.5 0.5B target;
16
+ - `thox-rust-coder`: lazily loaded 25B/3B-active Rust-specialist target.
 
 
 
 
17
 
18
+ `GET /healthz` is ready only after the two-instance interactive pool is loaded.
19
+ The interactive model is pinned to immutable model revision
20
+ `9217f5db79a29953eb74d5343926648285ec7e67` and baked into the image.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,196 +1,312 @@
1
- """THOX Rust Coder hosted Qwen3-Coder-REAP-25B-A3B-Rust (GGUF).
2
-
3
- SIZING: "3B active" IS NOT A MEMORY BUDGET
4
- ------------------------------------------
5
- This is a Mixture-of-Experts model: 25B total parameters, ~3B active per token.
6
- Those two numbers govern different resources and it is easy to conflate them:
7
-
8
- active params (~3B) -> COMPUTE per token. Decode is as cheap as a 3B dense.
9
- total params (25B) -> MEMORY. Every expert must be resident, because the
10
- router may select any of them on any token.
11
-
12
- So a Q4_K_M GGUF is **15.1 GB of RAM/VRAM**, not "3B-worth". You cannot fit this
13
- on a tier sized for a 3B model. What MoE buys you here is speed-per-byte, not a
14
- smaller footprint -- which is exactly why a CPU tier is viable at all: we pay
15
- 25B-sized memory but only 3B-sized arithmetic.
16
-
17
- TIER
18
- ----
19
- Starts on `cpu-upgrade` (8 vCPU / 32 GB, ~$0.03/hr). That fits Q4_K_M with room
20
- for the KV cache, and the 3B active path keeps CPU decode tolerable. If measured
21
- throughput is too slow to be a useful coding assistant, escalate to a GPU tier --
22
- but escalate on a MEASUREMENT, not on the assumption that 25B implies a GPU.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import os
28
- import time
29
- import uuid
30
-
31
- import gradio as gr
32
- from fastapi import FastAPI
33
- from huggingface_hub import hf_hub_download
34
- from pydantic import BaseModel
35
-
36
- MODEL_REPO = os.environ.get("THOX_MODEL_REPO", "Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF")
37
- MODEL_FILE = os.environ.get("THOX_MODEL_FILE", "Qwen3-Coder-REAP-25B-A3B-Rust-Q4_K_M.gguf")
38
- N_CTX = int(os.environ.get("THOX_N_CTX", "8192"))
39
-
40
- SYSTEM = (
41
- "You are THOX Rust Coder. You write correct, idiomatic Rust. Prefer showing "
42
- "compiling code over prose. If a request is ambiguous, state the assumption "
43
- "you made in one line, then give the code."
44
- )
45
-
46
- _llm = None
47
-
48
-
49
- def _usable_cpus() -> int:
50
- """Threads from the cgroup quota, not the host.
51
-
52
- `os.cpu_count()` reports the HOST's core count inside a container. On a
53
- sibling Space this oversubscribed a 2-vCPU cgroup ~8x and cost ~290x
54
- throughput -- the model ran slower than the edge device it was meant to
55
- offload. Read the quota.
56
- """
57
- try:
58
- quota, period = open("/sys/fs/cgroup/cpu.max").read().split()
59
- if quota != "max":
60
- return max(1, int(int(quota) / int(period)))
61
- except Exception:
62
- pass
63
- try:
64
- q = int(open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").read())
65
- p = int(open("/sys/fs/cgroup/cpu/cpu.cfs_period_us").read())
66
- if q > 0:
67
- return max(1, q // p)
68
- except Exception:
69
- pass
70
- try:
71
- return max(1, len(os.sched_getaffinity(0)))
72
- except Exception:
73
- return os.cpu_count() or 2
74
-
75
-
76
- def llm():
77
- global _llm
78
- if _llm is None:
79
- from llama_cpp import Llama
80
-
81
- path = hf_hub_download(MODEL_REPO, MODEL_FILE,
82
- token=os.environ.get("HF_TOKEN"))
83
- _llm = Llama(
84
- model_path=path,
85
- n_ctx=N_CTX,
86
- n_threads=_usable_cpus(),
87
- # -1 offloads every layer when a GPU is present, and is simply
88
- # ignored on a CPU build -- so the same image works on both tiers.
89
- n_gpu_layers=int(os.environ.get("THOX_GPU_LAYERS", "-1")),
90
- verbose=False,
91
- )
92
- return _llm
93
-
94
-
95
- def generate(messages, max_tokens=512, temperature=0.2):
96
- t0 = time.time()
97
- # create_chat_completion uses the chat template embedded in the GGUF, rather
98
- # than a hand-rolled one. Qwen3-Coder is ChatML, but reading it from the file
99
- # means a re-quant with a different template does not silently break output.
100
- out = llm().create_chat_completion(
101
- messages=messages, max_tokens=max_tokens, temperature=temperature,
102
- )
103
- dt = time.time() - t0
104
- text = out["choices"][0]["message"]["content"]
105
- n = out.get("usage", {}).get("completion_tokens") or 0
106
- return text, n, dt
107
-
108
-
109
- api = FastAPI()
110
-
111
-
112
- class Msg(BaseModel):
113
- role: str
114
- content: str
115
-
116
-
117
- class ChatRequest(BaseModel):
118
- model: str | None = None
119
- messages: list[Msg]
120
- max_tokens: int | None = 512
121
- temperature: float | None = 0.2
122
-
123
-
124
- @api.get("/healthz")
125
- def healthz():
126
- return {
127
- "status": "ok",
128
- "model": MODEL_REPO,
129
- "file": MODEL_FILE,
130
- "role": "thox-rust-coder",
131
- "n_ctx": N_CTX,
132
- "threads": _usable_cpus(),
133
- "loaded": _llm is not None,
134
- }
135
-
136
-
137
- @api.post("/v1/chat/completions")
138
- def chat_completions(req: ChatRequest):
139
- msgs = [m.model_dump() for m in req.messages]
140
- if not any(m["role"] == "system" for m in msgs):
141
- msgs = [{"role": "system", "content": SYSTEM}] + msgs
142
- text, n, dt = generate(msgs, req.max_tokens or 512, req.temperature or 0.2)
143
- return {
144
- "id": "chatcmpl-" + uuid.uuid4().hex[:12],
145
- "object": "chat.completion",
146
- "created": int(time.time()),
147
- "model": "thox-rust-coder",
148
- "choices": [{"index": 0, "finish_reason": "stop",
149
- "message": {"role": "assistant", "content": text}}],
150
- "usage": {"completion_tokens": n},
151
- "thox_perf": {"tok_per_s": round(n / dt, 1) if dt else None,
152
- "seconds": round(dt, 2)},
153
- }
154
-
155
-
156
- def ui(prompt, max_tokens):
157
- text, n, dt = generate(
158
- [{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}],
159
- int(max_tokens),
160
- )
161
- tps = n / dt if dt else 0
162
- return text, f"{n} tok in {dt:.1f}s = {tps:.1f} tok/s"
163
-
164
-
165
- demo = gr.Interface(
166
- fn=ui,
167
- inputs=[gr.Textbox(label="Prompt", lines=4,
168
- value="Write a Rust function that parses a semver string "
169
- "into (major, minor, patch), returning Result."),
170
- gr.Slider(64, 2048, value=512, step=64, label="max tokens")],
171
- # gradio 5.50's gr.Code has a fixed language allow-list and "rust" is NOT
172
- # on it -- passing it raises ValueError at import and the Space exits 1.
173
- # No highlighting is better than no Space.
174
- outputs=[gr.Code(label="THOX Rust Coder"),
175
- gr.Textbox(label="Measured")],
176
- title="THOX Rust Coder — Qwen3-Coder-REAP-25B-A3B-Rust",
177
- description=(
178
- "Rust-specialised MoE coder, Apache-2.0, served as GGUF.\n\n"
179
- "`POST /v1/chat/completions` (OpenAI-shaped, ThoxRoute-registerable) · "
180
- "`GET /healthz`\n\n"
181
- "**25B total / ~3B active.** Memory is sized by the 25B (all experts stay "
182
- "resident); speed is sized by the 3B. That combination is what makes a "
183
- "CPU tier viable."
184
- ),
185
- )
186
-
187
- app = gr.mount_gradio_app(api, demo, path="/")
188
-
189
-
190
- if __name__ == "__main__":
191
- # Defining `app` does not serve it -- without this the process exits 0 and
192
- # the Space reports RUNTIME_ERROR with no traceback to read.
193
- import uvicorn
194
-
195
- uvicorn.run(app, host="0.0.0.0",
196
- port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenAI-compatible THOX interactive and specialist model service.
2
+
3
+ The interactive model is intentionally small and eagerly loaded so readiness
4
+ means a user request can start generating immediately. The 25B Rust-specialist
5
+ model remains lazy because loading it takes roughly 100 seconds on cpu-upgrade
6
+ and it is not part of the interactive latency contract.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import queue
14
+ import threading
15
+ import time
16
+ import uuid
17
+ from contextlib import asynccontextmanager
18
+ from collections.abc import Iterator
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ from fastapi import FastAPI, HTTPException
23
+ from fastapi.responses import StreamingResponse
24
+ from huggingface_hub import hf_hub_download
25
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
26
+
27
+ FAST_MODEL_ID = "thox-fast-chat"
28
+ FAST_MODEL_REPO = os.environ.get(
29
+ "THOX_FAST_MODEL_REPO", "Qwen/Qwen2.5-0.5B-Instruct-GGUF"
30
+ )
31
+ FAST_MODEL_REVISION = os.environ.get(
32
+ "THOX_FAST_MODEL_REVISION", "9217f5db79a29953eb74d5343926648285ec7e67"
33
+ )
34
+ FAST_MODEL_FILE = os.environ.get(
35
+ "THOX_FAST_MODEL_FILE", "qwen2.5-0.5b-instruct-q4_k_m.gguf"
36
+ )
37
+ CODER_MODEL_ID = "thox-rust-coder"
38
+ CODER_MODEL_REPO = os.environ.get(
39
+ "THOX_CODER_MODEL_REPO", "Em-80/Qwen3-coder-REAP-25B-A3B-Rust-GGUF"
40
+ )
41
+ CODER_MODEL_REVISION = os.environ.get("THOX_CODER_MODEL_REVISION", "main")
42
+ CODER_MODEL_FILE = os.environ.get(
43
+ "THOX_CODER_MODEL_FILE", "Qwen3-Coder-REAP-25B-A3B-Rust-Q4_K_M.gguf"
44
+ )
45
+ N_CTX = int(os.environ.get("THOX_N_CTX", "4096"))
46
+ FAST_POOL_SIZE = max(1, min(int(os.environ.get("THOX_FAST_POOL_SIZE", "2")), 4))
47
+ MAX_OUTPUT_TOKENS = max(
48
+ 1, min(int(os.environ.get("THOX_MAX_OUTPUT_TOKENS", "128")), 512)
49
+ )
50
+ MAX_MESSAGES = 64
51
+ MAX_MESSAGE_CHARS = 65_536
52
+ MAX_REQUEST_CHARS = 131_072
53
+
54
+ SYSTEM = (
55
+ "You are THOX Fast Chat, a concise privacy-first assistant. Follow the "
56
+ "user's requested output format exactly. Never claim that cloud inference "
57
+ "ran locally or on-device."
58
+ )
59
+
60
+
61
+ def _usable_cpus() -> int:
62
+ """Return the container CPU quota instead of the misleading host count."""
63
+
64
+ try:
65
+ quota, period = open("/sys/fs/cgroup/cpu.max", encoding="utf-8").read().split()
66
+ if quota != "max":
67
+ return max(1, int(int(quota) / int(period)))
68
+ except (OSError, ValueError):
69
+ pass
70
+ try:
71
+ quota = int(
72
+ open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8").read()
73
+ )
74
+ period = int(
75
+ open("/sys/fs/cgroup/cpu/cpu.cfs_period_us", encoding="utf-8").read()
76
+ )
77
+ if quota > 0:
78
+ return max(1, quota // period)
79
+ except (OSError, ValueError):
80
+ pass
81
+ try:
82
+ return max(1, len(os.sched_getaffinity(0)))
83
+ except (AttributeError, OSError):
84
+ return os.cpu_count() or 2
85
+
86
+
87
+ def _download(repo: str, filename: str, revision: str) -> str:
88
+ return hf_hub_download(
89
+ repo_id=repo,
90
+ filename=filename,
91
+ revision=revision,
92
+ token=os.environ.get("HF_TOKEN") or None,
93
+ )
94
+
95
+
96
+ def _new_llama(path: str, *, threads: int):
97
+ from llama_cpp import Llama
98
+
99
+ return Llama(
100
+ model_path=path,
101
+ n_ctx=N_CTX,
102
+ n_threads=threads,
103
+ n_gpu_layers=int(os.environ.get("THOX_GPU_LAYERS", "-1")),
104
+ verbose=False,
105
+ )
106
+
107
+
108
+ @dataclass
109
+ class Lease:
110
+ model: Any
111
+ release: Any
112
+
113
+
114
+ class Runtime:
115
+ """Own bounded model capacity without sharing one llama context concurrently."""
116
+
117
+ def __init__(self) -> None:
118
+ fast_path = _download(FAST_MODEL_REPO, FAST_MODEL_FILE, FAST_MODEL_REVISION)
119
+ usable = _usable_cpus()
120
+ per_model_threads = max(1, usable // FAST_POOL_SIZE)
121
+ self._fast: queue.LifoQueue[Any] = queue.LifoQueue(maxsize=FAST_POOL_SIZE)
122
+ for _ in range(FAST_POOL_SIZE):
123
+ self._fast.put(_new_llama(fast_path, threads=per_model_threads))
124
+ self._coder = None
125
+ self._coder_lock = threading.Lock()
126
+
127
+ def acquire(self, model_id: str) -> Lease:
128
+ if model_id == FAST_MODEL_ID:
129
+ try:
130
+ model = self._fast.get_nowait()
131
+ except queue.Empty as exc:
132
+ raise HTTPException(status_code=429, detail="interactive capacity busy") from exc
133
+ return Lease(model=model, release=lambda: self._fast.put(model))
134
+ if model_id == CODER_MODEL_ID:
135
+ if not self._coder_lock.acquire(blocking=False):
136
+ raise HTTPException(status_code=429, detail="specialist capacity busy")
137
+ try:
138
+ if self._coder is None:
139
+ path = _download(
140
+ CODER_MODEL_REPO, CODER_MODEL_FILE, CODER_MODEL_REVISION
141
+ )
142
+ self._coder = _new_llama(path, threads=_usable_cpus())
143
+ except Exception:
144
+ self._coder_lock.release()
145
+ raise
146
+ return Lease(model=self._coder, release=self._coder_lock.release)
147
+ raise HTTPException(status_code=404, detail="model not found")
148
+
149
+ @property
150
+ def interactive_available(self) -> int:
151
+ return self._fast.qsize()
152
+
153
+
154
+ class Msg(BaseModel):
155
+ model_config = ConfigDict(extra="forbid")
156
+
157
+ role: str = Field(pattern=r"^(system|user|assistant|tool)$")
158
+ content: str = Field(min_length=1, max_length=MAX_MESSAGE_CHARS)
159
+
160
+
161
+ class ChatRequest(BaseModel):
162
+ model_config = ConfigDict(extra="forbid")
163
+
164
+ model: str = FAST_MODEL_ID
165
+ messages: list[Msg] = Field(min_length=1, max_length=MAX_MESSAGES)
166
+ max_tokens: int = Field(default=64, ge=1, le=4096)
167
+ temperature: float = Field(default=0.2, ge=0.0, le=2.0)
168
+ stream: bool = False
169
+
170
+ @field_validator("messages")
171
+ @classmethod
172
+ def bound_aggregate_prompt(cls, messages: list[Msg]) -> list[Msg]:
173
+ if sum(len(message.content) for message in messages) > MAX_REQUEST_CHARS:
174
+ raise ValueError("aggregate prompt is too large")
175
+ return messages
176
+
177
+
178
+ runtime: Runtime | None = None
179
+
180
+
181
+ @asynccontextmanager
182
+ async def lifespan(_: FastAPI):
183
+ global runtime
184
+ runtime = Runtime()
185
+ try:
186
+ yield
187
+ finally:
188
+ runtime = None
189
+
190
+
191
+ api = FastAPI(
192
+ title="THOX interactive model service", version="1.0.0", lifespan=lifespan
193
+ )
194
+
195
+
196
+ def _messages(req: ChatRequest) -> list[dict[str, str]]:
197
+ messages = [message.model_dump() for message in req.messages]
198
+ if not any(message["role"] == "system" for message in messages):
199
+ messages.insert(0, {"role": "system", "content": SYSTEM})
200
+ return messages
201
+
202
+
203
+ def _max_tokens(req: ChatRequest) -> int:
204
+ return min(req.max_tokens, MAX_OUTPUT_TOKENS)
205
+
206
+
207
+ def _runtime() -> Runtime:
208
+ if runtime is None:
209
+ raise HTTPException(status_code=503, detail="model runtime is not ready")
210
+ return runtime
211
+
212
+
213
+ @api.get("/")
214
+ @api.get("/healthz")
215
+ def healthz() -> dict[str, Any]:
216
+ active = _runtime()
217
+ return {
218
+ "status": "ready",
219
+ "interactive_model": FAST_MODEL_ID,
220
+ "interactive_model_repo": FAST_MODEL_REPO,
221
+ "interactive_model_revision": FAST_MODEL_REVISION,
222
+ "interactive_pool_size": FAST_POOL_SIZE,
223
+ "interactive_available": active.interactive_available,
224
+ "specialist_model": CODER_MODEL_ID,
225
+ "max_output_tokens": MAX_OUTPUT_TOKENS,
226
+ "n_ctx": N_CTX,
227
+ "threads": _usable_cpus(),
228
+ }
229
+
230
+
231
+ def _completion_id() -> str:
232
+ return "chatcmpl-" + uuid.uuid4().hex[:16]
233
+
234
+
235
+ def _stream_completion(req: ChatRequest, lease: Lease) -> Iterator[bytes]:
236
+ completion_id = _completion_id()
237
+ created = int(time.time())
238
+ try:
239
+ chunks = lease.model.create_chat_completion(
240
+ messages=_messages(req),
241
+ max_tokens=_max_tokens(req),
242
+ temperature=req.temperature,
243
+ stream=True,
244
+ )
245
+ for chunk in chunks:
246
+ choice = chunk.get("choices", [{}])[0]
247
+ payload = {
248
+ "id": completion_id,
249
+ "object": "chat.completion.chunk",
250
+ "created": created,
251
+ "model": req.model,
252
+ "choices": [
253
+ {
254
+ "index": 0,
255
+ "delta": choice.get("delta") or {},
256
+ "finish_reason": choice.get("finish_reason"),
257
+ }
258
+ ],
259
+ }
260
+ yield f"data: {json.dumps(payload, separators=(',', ':'))}\n\n".encode()
261
+ yield b"data: [DONE]\n\n"
262
+ finally:
263
+ lease.release()
264
+
265
+
266
+ @api.post("/v1/chat/completions")
267
+ def chat_completions(req: ChatRequest):
268
+ lease = _runtime().acquire(req.model)
269
+ if req.stream:
270
+ return StreamingResponse(
271
+ _stream_completion(req, lease),
272
+ media_type="text/event-stream",
273
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
274
+ )
275
+
276
+ started = time.monotonic()
277
+ try:
278
+ result = lease.model.create_chat_completion(
279
+ messages=_messages(req),
280
+ max_tokens=_max_tokens(req),
281
+ temperature=req.temperature,
282
+ )
283
+ finally:
284
+ lease.release()
285
+ elapsed = time.monotonic() - started
286
+ choice = result["choices"][0]
287
+ usage = result.get("usage") or {}
288
+ return {
289
+ "id": _completion_id(),
290
+ "object": "chat.completion",
291
+ "created": int(time.time()),
292
+ "model": req.model,
293
+ "choices": [
294
+ {
295
+ "index": 0,
296
+ "finish_reason": choice.get("finish_reason") or "stop",
297
+ "message": choice["message"],
298
+ }
299
+ ],
300
+ "usage": {
301
+ "prompt_tokens": int(usage.get("prompt_tokens") or 0),
302
+ "completion_tokens": int(usage.get("completion_tokens") or 0),
303
+ "total_tokens": int(usage.get("total_tokens") or 0),
304
+ },
305
+ "thox_perf": {"seconds": round(elapsed, 3)},
306
+ }
307
+
308
+
309
+ if __name__ == "__main__":
310
+ import uvicorn
311
+
312
+ uvicorn.run(api, host="0.0.0.0", port=int(os.environ.get("PORT", "7860")))
requirements.txt CHANGED
@@ -1,14 +1 @@
1
- # llama-cpp-python is deliberately ABSENT.
2
- #
3
- # It is installed from conda-forge in the Dockerfile as a prebuilt, glibc-linked
4
- # linux-64 binary. Two pip routes both fail on an HF Space:
5
- # * PyPI is sdist-only -> compiles -> `Job timeout`
6
- # * abetlen's CPU wheel index -> musl-linked -> builds green, RUNNING, /healthz
7
- # ok, then HTTP 500 at the first dlopen
8
- # See thoxllm-factory/docs/SPACE_PINS.md.
9
-
10
- gradio==5.50.0
11
- fastapi
12
- pydantic>=2
13
- uvicorn
14
- huggingface_hub>=0.28
 
1
+ huggingface_hub==1.6.0