Bc-AI commited on
Commit
90a71f6
·
verified ·
1 Parent(s): 7e08468

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +342 -289
app.py CHANGED
@@ -1,43 +1,50 @@
1
  import os
2
  import gc
3
  import asyncio
4
- import json
5
  from typing import Dict, List, Optional
6
- from contextlib import asynccontextmanager
7
 
8
  import torch
9
- from fastapi import FastAPI, HTTPException, Header, Depends, Request
10
- from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
 
 
11
  from fastapi.middleware.cors import CORSMiddleware
 
12
  from pydantic import BaseModel, Field
13
- from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
14
- from threading import Thread
15
 
16
 
17
- # ═══════════════════════════════════════════════════
18
- # MODEL REGISTRY — EDIT THIS
19
- # ═══════════════════════════════════════════════════
 
 
20
  MODELS: Dict[str, str] = {
21
  "Qwen2.5-0.5B-Instruct": "Qwen/Qwen2.5-0.5B-Instruct",
22
  "TinyLlama-1.1B-Chat": "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
 
 
23
  }
24
 
25
  if not MODELS:
26
  raise RuntimeError("bro, put at least one model in MODELS.")
27
 
28
  DEFAULT_MODEL = next(iter(MODELS))
 
29
  PRIORITY_API_KEY = os.getenv("PRIORITY_API_KEY", "").strip()
 
30
  MAX_NEW_TOKENS_LIMIT = int(os.getenv("MAX_NEW_TOKENS_LIMIT", "1024"))
 
 
 
31
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
32
 
33
 
34
- # ═══════════════════════════════════════════════════
35
- # PYDANTIC SCHEMAS
36
- # ═══════════════════════════════════════════════════
37
  class ChatMessage(BaseModel):
38
  role: str
39
  content: str
40
 
 
41
  class ChatRequest(BaseModel):
42
  model: str = Field(default=DEFAULT_MODEL)
43
  messages: List[ChatMessage] = Field(min_length=1)
@@ -46,9 +53,6 @@ class ChatRequest(BaseModel):
46
  top_p: float = Field(default=0.95, ge=0.0, le=1.0)
47
 
48
 
49
- # ═══════════════════════════════════════════════════
50
- # MODEL MANAGER
51
- # ═══════════════════════════════════════════════════
52
  class ModelManager:
53
  def __init__(self):
54
  self.current_model = None
@@ -66,64 +70,152 @@ class ModelManager:
66
  def load(self, name: str):
67
  if name == self.current_model and self.model is not None:
68
  return
 
69
  if name not in MODELS:
70
- raise ValueError(f"Model '{name}' not in MODELS.")
71
 
72
  model_id = MODELS[name]
73
  self._cleanup()
 
74
  token = os.getenv("HF_TOKEN") or None
 
75
  use_4bit = os.getenv("USE_4BIT", "1") == "1" and torch.cuda.is_available()
76
 
77
- kwargs = {"token": token, "low_cpu_mem_usage": True}
 
 
 
 
 
78
  if use_4bit:
79
- dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
80
- kwargs.update(load_in_4bit=True, bnb_4bit_compute_dtype=dtype, bnb_4bit_quant_type="nf4")
 
 
 
 
 
 
 
 
81
  else:
82
- kwargs["torch_dtype"] = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
 
 
 
 
 
 
 
 
 
 
83
 
84
- self.tokenizer = AutoTokenizer.from_pretrained(model_id, token=token)
85
  if self.tokenizer.pad_token is None:
86
  self.tokenizer.pad_token = self.tokenizer.eos_token
 
87
  if getattr(self.tokenizer, "pad_token_id", None) is None:
88
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
 
 
 
 
89
  self.tokenizer.padding_side = "left"
90
 
91
- self.model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto", **kwargs)
 
 
 
 
92
  self.model.eval()
93
  self.current_model = name
94
 
95
- def build_prompt(self, messages: List[ChatMessage]) -> str:
96
- raw = [m.model_dump() for m in messages]
 
97
  try:
98
- return self.tokenizer.apply_chat_template(raw, tokenize=False, add_generation_prompt=True)
 
 
 
 
99
  except Exception:
100
  lines = []
101
  for m in messages:
102
- r = m.role.strip().lower()
103
- if r == "system": lines.append(f"System: {m.content}")
104
- elif r == "user": lines.append(f"User: {m.content}")
105
- elif r == "assistant": lines.append(f"Assistant: {m.content}")
106
- else: lines.append(f"{m.role}: {m.content}")
 
 
 
 
107
  lines.append("Assistant:")
108
  return "\n".join(lines)
109
 
110
- def get_max_input_length(self) -> int:
111
- ml = getattr(self.tokenizer, "model_max_length", None)
112
  try:
113
- ml = int(ml) if ml is not None else 4096
114
- except Exception:
115
- ml = 4096
116
- if ml <= 0 or ml > 1_000_000:
117
- ml = 4096
118
- return min(4096, ml)
119
 
 
120
 
121
- model_manager = ModelManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
 
124
- # ═══════════════════════════════════════════════════
125
- # PRIORITY QUEUE BROKER
126
- # ═══════════════════════════════════════════════════
127
  class PriorityBroker:
128
  def __init__(self):
129
  self.queue = asyncio.PriorityQueue()
@@ -135,326 +227,287 @@ class PriorityBroker:
135
 
136
  async def stop(self):
137
  self.counter += 1
138
- await self.queue.put((999999, self.counter, None, None, None))
139
  if self.task:
140
  await self.task
141
 
142
- async def enqueue(self, priority: int, request: ChatRequest, stream: bool = False):
143
  loop = asyncio.get_running_loop()
144
  future = loop.create_future()
145
  self.counter += 1
146
- await self.queue.put((priority, self.counter, future, request, stream))
147
  return await future
148
 
149
  async def _worker(self):
150
  while True:
151
  item = await self.queue.get()
152
- priority, counter, future, request, stream = item
153
 
154
  if future is None:
155
  self.queue.task_done()
156
  break
 
157
  if future.cancelled():
158
  self.queue.task_done()
159
  continue
160
 
161
  try:
162
- if stream:
163
- # For streaming, we set result to a generator function
164
- gen_fn = lambda: self._generate_stream(request)
165
- future.set_result(gen_fn)
166
- else:
167
- result = await asyncio.to_thread(self._generate_full, request)
168
- future.set_result(result)
169
  except Exception as exc:
170
  future.set_exception(exc)
171
  finally:
172
  self.queue.task_done()
173
 
174
- def _generate_full(self, request: ChatRequest) -> str:
175
- try:
176
- model_manager.load(request.model)
177
- prompt = model_manager.build_prompt(request.messages)
178
- max_input = model_manager.get_max_input_length()
179
- inputs = model_manager.tokenizer(prompt, return_tensors="pt", truncation=True, max_length=max_input)
180
- inputs = {k: v.to(model_manager.model.device) for k, v in inputs.items()}
181
-
182
- input_len = inputs["input_ids"].shape[1]
183
- max_new = max(1, min(request.max_new_tokens, MAX_NEW_TOKENS_LIMIT, max_input - input_len))
184
-
185
- gen_kwargs = {"max_new_tokens": max_new, "pad_token_id": model_manager.tokenizer.pad_token_id, "eos_token_id": model_manager.tokenizer.eos_token_id}
186
- if request.temperature > 0:
187
- gen_kwargs.update(do_sample=True, temperature=request.temperature, top_p=request.top_p if request.top_p > 0 else 1.0)
188
- else:
189
- gen_kwargs.update(do_sample=False)
190
-
191
- with torch.inference_mode():
192
- out = model_manager.model.generate(**inputs, **gen_kwargs)
193
- new = out[0, inputs["input_ids"].shape[1]:]
194
- return model_manager.tokenizer.decode(new, skip_special_tokens=True).strip()
195
- except Exception:
196
- model_manager._cleanup()
197
- raise
198
-
199
- def _generate_stream(self, request: ChatRequest):
200
- """Returns a generator that yields tokens. Called in a thread."""
201
- try:
202
- model_manager.load(request.model)
203
- prompt = model_manager.build_prompt(request.messages)
204
- max_input = model_manager.get_max_input_length()
205
- inputs = model_manager.tokenizer(prompt, return_tensors="pt", truncation=True, max_length=max_input)
206
- inputs = {k: v.to(model_manager.model.device) for k, v in inputs.items()}
207
-
208
- input_len = inputs["input_ids"].shape[1]
209
- max_new = max(1, min(request.max_new_tokens, MAX_NEW_TOKENS_LIMIT, max_input - input_len))
210
-
211
- streamer = TextIteratorStreamer(model_manager.tokenizer, skip_prompt=True, skip_special_tokens=True)
212
-
213
- gen_kwargs = {"max_new_tokens": max_new, "pad_token_id": model_manager.tokenizer.pad_token_id, "eos_token_id": model_manager.tokenizer.eos_token_id, "streamer": streamer}
214
- if request.temperature > 0:
215
- gen_kwargs.update(do_sample=True, temperature=request.temperature, top_p=request.top_p if request.top_p > 0 else 1.0)
216
- else:
217
- gen_kwargs.update(do_sample=False)
218
-
219
- def run_gen():
220
- with torch.inference_mode():
221
- model_manager.model.generate(**inputs, **gen_kwargs)
222
-
223
- t = Thread(target=run_gen)
224
- t.start()
225
-
226
- for token_text in streamer:
227
- yield token_text
228
-
229
- t.join(timeout=5)
230
- except Exception:
231
- model_manager._cleanup()
232
- raise
233
-
234
 
 
235
  broker = PriorityBroker()
236
 
237
 
238
- # ═══════════════════════════════════════════════════
239
- # FASTAPI APP
240
- # ══════════════════���════════════════════════════════
241
  @asynccontextmanager
242
  async def lifespan(app: FastAPI):
243
  await broker.start()
244
  yield
245
  await broker.stop()
246
 
 
247
  app = FastAPI(title="Priority Model Chat", lifespan=lifespan)
248
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
249
 
250
 
251
  def require_priority_key(
252
  x_api_key: Optional[str] = Header(default=None, alias="x-api-key"),
253
  authorization: Optional[str] = Header(default=None),
254
  ):
 
 
 
255
  provided = None
 
256
  if x_api_key:
257
  provided = x_api_key.strip()
258
  elif authorization and authorization.lower().startswith("bearer "):
259
  provided = authorization[7:].strip()
 
260
  if not PRIORITY_API_KEY:
261
- raise HTTPException(503, "Set PRIORITY_API_KEY in Space secrets first.")
 
 
 
 
 
 
 
262
  if provided != PRIORITY_API_KEY:
263
- raise HTTPException(401, "Invalid or missing priority API key.")
 
 
 
264
 
265
 
266
  @app.get("/health")
267
  async def health():
268
- return {"ok": True, "device": DEVICE, "models": list(MODELS.keys()), "queue_size": broker.queue.qsize()}
 
 
 
 
 
 
 
269
 
270
  @app.get("/api/models")
271
  async def models():
272
  return {"models": MODELS}
273
 
 
274
  @app.post("/api/chat")
275
  async def chat(req: ChatRequest):
276
- if not req.model: req.model = DEFAULT_MODEL
 
 
277
  if req.model not in MODELS:
278
- raise HTTPException(404, f"Model '{req.model}' not found. Available: {list(MODELS.keys())}")
 
 
 
 
279
  try:
280
- answer = await broker.enqueue(priority=1, request=req, stream=False)
281
- return {"model": req.model, "response": answer}
 
 
 
282
  except Exception as exc:
283
- raise HTTPException(500, f"{type(exc).__name__}: {exc}")
 
284
 
285
  @app.post("/api/priority")
286
- async def priority_chat(req: ChatRequest, _: None = Depends(require_priority_key)):
287
- if not req.model: req.model = DEFAULT_MODEL
 
 
 
 
 
288
  if req.model not in MODELS:
289
- raise HTTPException(404, f"Model '{req.model}' not found. Available: {list(MODELS.keys())}")
 
 
 
 
290
  try:
291
- answer = await broker.enqueue(priority=0, request=req, stream=False)
292
- return {"model": req.model, "response": answer, "priority": True}
 
 
 
 
293
  except Exception as exc:
294
- raise HTTPException(500, f"{type(exc).__name__}: {exc}")
295
 
296
 
297
- # ═══════════════════════════════════════════════════
298
- # STREAMING ENDPOINTS (SSE)
299
- # ═══════════════════════════════════════════════════
300
- async def sse_generator(gen_fn):
301
- """Wraps a sync token generator into SSE format."""
302
- try:
303
- # gen_fn is a lambda that returns a generator when called
304
- token_iter = await asyncio.to_thread(gen_fn)
305
- for token_text in token_iter:
306
- data = json.dumps({"token": token_text})
307
- yield f"data: {data}\n\n"
308
- yield "data: [DONE]\n\n"
309
- except Exception as exc:
310
- err = json.dumps({"error": f"{type(exc).__name__}: {exc}"})
311
- yield f"data: {err}\n\n"
312
- yield "data: [DONE]\n\n"
313
 
314
 
315
- @app.post("/api/chat/stream")
316
- async def chat_stream(req: ChatRequest):
317
- if not req.model: req.model = DEFAULT_MODEL
318
- if req.model not in MODELS:
319
- raise HTTPException(404, f"Model '{req.model}' not found.")
320
- try:
321
- gen_fn = await broker.enqueue(priority=1, request=req, stream=True)
322
- return StreamingResponse(sse_generator(gen_fn), media_type="text/event-stream",
323
- headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
324
- except Exception as exc:
325
- raise HTTPException(500, f"{type(exc).__name__}: {exc}")
 
326
 
 
 
327
 
328
- @app.post("/api/priority/stream")
329
- async def priority_stream(req: ChatRequest, _: None = Depends(require_priority_key)):
330
- if not req.model: req.model = DEFAULT_MODEL
331
- if req.model not in MODELS:
332
- raise HTTPException(404, f"Model '{req.model}' not found.")
333
- try:
334
- gen_fn = await broker.enqueue(priority=0, request=req, stream=True)
335
- return StreamingResponse(sse_generator(gen_fn), media_type="text/event-stream",
336
- headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
337
- except Exception as exc:
338
- raise HTTPException(500, f"{type(exc).__name__}: {exc}")
339
-
340
-
341
- # ═══════════════════════════════════════════════════
342
- # BUILT-IN CHAT UI (served at /)
343
- # ═══════════════════════════════════════════════════
344
- CHAT_UI_HTML = """<!DOCTYPE html>
345
- <html lang="en">
346
- <head>
347
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
348
- <title>⚡ Priority Model Chat</title>
349
- <style>
350
- *{margin:0;padding:0;box-sizing:border-box}
351
- body{background:#0a0a1e;color:#e0e0e0;font-family:'Segoe UI',system-ui,sans-serif;height:100vh;display:flex;flex-direction:column}
352
- .header{text-align:center;padding:1rem;border-bottom:1px solid rgba(0,229,255,0.15)}
353
- .header h1{font-size:1.5rem;background:linear-gradient(90deg,#00e5ff,#7c4dff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
354
- .header p{color:#888;font-size:0.8rem;margin-top:0.2rem}
355
- .chat-area{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:0.6rem;scroll-behavior:smooth}
356
- .msg{max-width:80%;padding:0.7rem 1rem;border-radius:12px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap;font-size:0.95rem}
357
- .msg.user{align-self:flex-end;background:rgba(0,229,255,0.12);border:1px solid rgba(0,229,255,0.25);color:#d0f0ff}
358
- .msg.assistant{align-self:flex-start;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);color:#ddd}
359
- .msg.sys{align-self:center;font-size:0.75rem;color:#666;font-style:italic}
360
- .msg.err{align-self:center;background:rgba(255,50,50,0.12);border:1px solid rgba(255,50,50,0.25);color:#ff6b6b}
361
- .cursor{display:inline-block;width:2px;height:1em;background:#00e5ff;animation:blink 0.8s infinite;vertical-align:text-bottom;margin-left:2px}
362
- @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
363
- .controls{padding:0.5rem 1rem;display:flex;gap:0.8rem;flex-wrap:wrap;align-items:center;border-top:1px solid rgba(255,255,255,0.06);background:rgba(10,10,30,0.5)}
364
- .controls label{color:#aaa;font-size:0.78rem;display:flex;align-items:center;gap:0.3rem}
365
- .controls select,.controls input[type=number]{background:#12122a;border:1px solid rgba(255,255,255,0.12);border-radius:6px;padding:0.25rem 0.4rem;color:#fff;font-size:0.85rem}
366
- .input-row{display:flex;gap:0.5rem;padding:0.75rem 1rem;border-top:1px solid rgba(0,229,255,0.1);background:#0d0d24}
367
- .input-row textarea{flex:1;background:#12122a;border:1px solid rgba(0,229,255,0.2);border-radius:8px;padding:0.6rem;color:#fff;font-size:0.95rem;resize:none;outline:none;min-height:44px;max-height:120px;font-family:inherit}
368
- .input-row textarea:focus{border-color:#00e5ff;box-shadow:0 0 8px rgba(0,229,255,0.15)}
369
- .input-row button{background:linear-gradient(135deg,#00e5ff,#7c4dff);border:none;border-radius:8px;padding:0 1.5rem;color:#000;font-weight:700;cursor:pointer;font-size:0.9rem;transition:all 0.2s}
370
- .input-row button:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,229,255,0.25)}
371
- .input-row button:disabled{opacity:0.4;cursor:not-allowed;transform:none}
372
- .clear-btn{background:transparent;border:1px solid rgba(255,100,100,0.25);color:#ff6b6b;border-radius:6px;padding:0.25rem 0.7rem;cursor:pointer;font-size:0.78rem;margin-left:auto;transition:all 0.2s}
373
- .clear-btn:hover{background:rgba(255,100,100,0.08)}
374
- </style>
375
- </head>
376
- <body>
377
- <div class="header"><h1>⚡ Priority Model Chat</h1><p>streaming • priority queue • no gradio</p></div>
378
- <div class="chat-area" id="chat"></div>
379
- <div class="controls">
380
- <label>Model:<select id="mdl"></select></label>
381
- <label>Temp:<input type="number" id="tmp" value="0.7" min="0" max="2" step="0.05" style="width:55px"></label>
382
- <label>Tokens:<input type="number" id="tok" value="512" min="16" max="2048" step="16" style="width:65px"></label>
383
- <button class="clear-btn" onclick="clearChat()">🧹 Clear</button>
384
- </div>
385
- <div class="input-row">
386
- <textarea id="inp" placeholder="Type a message… (Enter to send, Shift+Enter for newline)" rows="1"></textarea>
387
- <button id="btn" onclick="send()">Send ⚡</button>
388
- </div>
389
- <script>
390
- const chat=document.getElementById('chat'),inp=document.getElementById('inp'),btn=document.getElementById('btn');
391
- let hist=[];
392
- fetch('/api/models').then(r=>r.json()).then(d=>{
393
- const s=document.getElementById('mdl');
394
- Object.keys(d.models).forEach(k=>{const o=document.createElement('option');o.value=k;o.textContent=k;s.appendChild(o)});
395
- }).catch(()=>{});
396
-
397
- function addMsg(role,text,cls){const d=document.createElement('div');d.className='msg '+role+(cls?' '+cls:'');d.textContent=text;chat.appendChild(d);chat.scrollTop=chat.scrollHeight;return d}
398
- function clearChat(){hist=[];chat.innerHTML='<div class="msg sys">chat cleared.</div>'}
399
-
400
- inp.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
401
- inp.addEventListener('input',()=>{inp.style.height='auto';inp.style.height=Math.min(inp.scrollHeight,120)+'px'});
402
-
403
- async function send(){
404
- const text=inp.value.trim();if(!text)return;
405
- addMsg('user',text);
406
- hist.push({role:'user',content:text});
407
- inp.value='';inp.style.height='auto';btn.disabled=true;
408
-
409
- const assistantDiv=addMsg('assistant','');
410
- const cursor=document.createElement('span');cursor.className='cursor';assistantDiv.appendChild(cursor);
411
-
412
- try{
413
- const resp=await fetch('/api/priority/stream',{
414
- method:'POST',
415
- headers:{'Content-Type':'application/json','x-api-key':document.getElementById('apiKey')?.value||''},
416
- body:JSON.stringify({messages:hist,model:document.getElementById('mdl').value,
417
- temperature:parseFloat(document.getElementById('tmp').value)||0.7,
418
- max_new_tokens:parseInt(document.getElementById('tok').value)||512})
419
- });
420
-
421
- if(!resp.ok){
422
- const err=await resp.text();
423
- cursor.remove();assistantDiv.textContent='💀 '+err;assistantDiv.classList.add('err');
424
- btn.disabled=false;return;
425
- }
426
 
427
- const reader=resp.body.getReader();
428
- const decoder=new TextDecoder();
429
- let fullText='';let buffer='';
430
-
431
- while(true){
432
- const{done,value}=await reader.read();
433
- if(done)break;
434
- buffer+=decoder.decode(value,{stream:true});
435
- const lines=buffer.split('\\n');
436
- buffer=lines.pop()||'';
437
- for(const line of lines){
438
- if(!line.startsWith('data: '))continue;
439
- const payload=line.slice(6).trim();
440
- if(payload==='[DONE]'){cursor.remove();hist.push({role:'assistant',content:fullText});btn.disabled=false;return}
441
- try{
442
- const parsed=JSON.parse(payload);
443
- if(parsed.error){cursor.remove();assistantDiv.textContent='💀 '+parsed.error;assistantDiv.classList.add('err');btn.disabled=false;return}
444
- if(parsed.token!==undefined){fullText+=parsed.token;assistantDiv.textContent=fullText;assistantDiv.appendChild(cursor);chat.scrollTop=chat.scrollHeight}
445
- }catch(e){}
446
- }
447
- }
448
- cursor.remove();
449
- if(fullText)hist.push({role:'assistant',content:fullText});
450
- }catch(e){
451
- cursor.remove();assistantDiv.textContent='💀 Network error: '+e.message;assistantDiv.classList.add('err');
452
- }finally{btn.disabled=false;inp.focus()}
453
- }
454
- </script>
455
- </body>
456
- </html>"""
457
 
458
- @app.get("/", response_class=HTMLResponse)
459
- async def serve_ui():
460
- return CHAT_UI_HTML
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import gc
3
  import asyncio
 
4
  from typing import Dict, List, Optional
 
5
 
6
  import torch
7
+ import gradio as gr
8
+ from contextlib import asynccontextmanager
9
+
10
+ from fastapi import FastAPI, HTTPException, Header, Depends
11
  from fastapi.middleware.cors import CORSMiddleware
12
+ from starlette.responses import Response # noqa: F401
13
  from pydantic import BaseModel, Field
14
+ from transformers import AutoTokenizer, AutoModelForCausalLM
 
15
 
16
 
17
+ # --------------------------------------------------------------------
18
+ # EDIT THIS DICTIONARY
19
+ # key = name shown in UI/API
20
+ # value = Hugging Face repo ID or local path
21
+ # --------------------------------------------------------------------
22
  MODELS: Dict[str, str] = {
23
  "Qwen2.5-0.5B-Instruct": "Qwen/Qwen2.5-0.5B-Instruct",
24
  "TinyLlama-1.1B-Chat": "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
25
+ # "My Cool Model": "YourUsername/your-model-repo",
26
+ # "Local Model": "./models/my-local-model",
27
  }
28
 
29
  if not MODELS:
30
  raise RuntimeError("bro, put at least one model in MODELS.")
31
 
32
  DEFAULT_MODEL = next(iter(MODELS))
33
+
34
  PRIORITY_API_KEY = os.getenv("PRIORITY_API_KEY", "").strip()
35
+ ALLOW_OPEN_PRIORITY = os.getenv("ALLOW_OPEN_PRIORITY", "0") == "1"
36
  MAX_NEW_TOKENS_LIMIT = int(os.getenv("MAX_NEW_TOKENS_LIMIT", "1024"))
37
+ MAX_SLIDER = min(8192, max(256, MAX_NEW_TOKENS_LIMIT))
38
+ DEFAULT_MAX_TOKENS = min(512, MAX_SLIDER)
39
+
40
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
41
 
42
 
 
 
 
43
  class ChatMessage(BaseModel):
44
  role: str
45
  content: str
46
 
47
+
48
  class ChatRequest(BaseModel):
49
  model: str = Field(default=DEFAULT_MODEL)
50
  messages: List[ChatMessage] = Field(min_length=1)
 
53
  top_p: float = Field(default=0.95, ge=0.0, le=1.0)
54
 
55
 
 
 
 
56
  class ModelManager:
57
  def __init__(self):
58
  self.current_model = None
 
70
  def load(self, name: str):
71
  if name == self.current_model and self.model is not None:
72
  return
73
+
74
  if name not in MODELS:
75
+ raise ValueError(f"Model '{name}' is not in MODELS.")
76
 
77
  model_id = MODELS[name]
78
  self._cleanup()
79
+
80
  token = os.getenv("HF_TOKEN") or None
81
+ trust_remote_code = os.getenv("TRUST_REMOTE_CODE", "0") == "1"
82
  use_4bit = os.getenv("USE_4BIT", "1") == "1" and torch.cuda.is_available()
83
 
84
+ model_kwargs = {
85
+ "token": token,
86
+ "low_cpu_mem_usage": True,
87
+ "trust_remote_code": trust_remote_code,
88
+ }
89
+
90
  if use_4bit:
91
+ compute_dtype = (
92
+ torch.bfloat16
93
+ if torch.cuda.is_available() and torch.cuda.is_bf16_supported()
94
+ else torch.float16
95
+ )
96
+ model_kwargs.update(
97
+ load_in_4bit=True,
98
+ bnb_4bit_compute_dtype=compute_dtype,
99
+ bnb_4bit_quant_type="nf4",
100
+ )
101
  else:
102
+ model_kwargs["torch_dtype"] = (
103
+ torch.bfloat16
104
+ if torch.cuda.is_available() and torch.cuda.is_bf16_supported()
105
+ else torch.float16
106
+ )
107
+
108
+ self.tokenizer = AutoTokenizer.from_pretrained(
109
+ model_id,
110
+ token=token,
111
+ trust_remote_code=trust_remote_code,
112
+ )
113
 
 
114
  if self.tokenizer.pad_token is None:
115
  self.tokenizer.pad_token = self.tokenizer.eos_token
116
+
117
  if getattr(self.tokenizer, "pad_token_id", None) is None:
118
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
119
+
120
+ if getattr(self.tokenizer, "eos_token_id", None) is None:
121
+ self.tokenizer.eos_token_id = self.tokenizer.pad_token_id or 0
122
+
123
  self.tokenizer.padding_side = "left"
124
 
125
+ self.model = AutoModelForCausalLM.from_pretrained(
126
+ model_id,
127
+ device_map="auto",
128
+ **model_kwargs,
129
+ )
130
  self.model.eval()
131
  self.current_model = name
132
 
133
+ def _build_prompt(self, messages: List[ChatMessage]) -> str:
134
+ raw_messages = [m.model_dump() for m in messages]
135
+
136
  try:
137
+ return self.tokenizer.apply_chat_template(
138
+ raw_messages,
139
+ tokenize=False,
140
+ add_generation_prompt=True,
141
+ )
142
  except Exception:
143
  lines = []
144
  for m in messages:
145
+ role = m.role.strip().lower()
146
+ if role == "system":
147
+ lines.append(f"System: {m.content}")
148
+ elif role == "user":
149
+ lines.append(f"User: {m.content}")
150
+ elif role == "assistant":
151
+ lines.append(f"Assistant: {m.content}")
152
+ else:
153
+ lines.append(f"{m.role}: {m.content}")
154
  lines.append("Assistant:")
155
  return "\n".join(lines)
156
 
157
+ def generate(self, request: ChatRequest) -> str:
 
158
  try:
159
+ self.load(request.model)
 
 
 
 
 
160
 
161
+ prompt = self._build_prompt(request.messages)
162
 
163
+ model_max = getattr(self.tokenizer, "model_max_length", None)
164
+ try:
165
+ model_max = int(model_max) if model_max is not None else 4096
166
+ except Exception:
167
+ model_max = 4096
168
+
169
+ if model_max <= 0 or model_max > 1_000_000:
170
+ model_max = 4096
171
+
172
+ max_input_length = min(4096, model_max)
173
+
174
+ inputs = self.tokenizer(
175
+ prompt,
176
+ return_tensors="pt",
177
+ truncation=True,
178
+ max_length=max_input_length,
179
+ )
180
+ inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
181
+
182
+ input_len = inputs["input_ids"].shape[1]
183
+
184
+ max_new_tokens = max(
185
+ 1,
186
+ min(
187
+ request.max_new_tokens,
188
+ MAX_NEW_TOKENS_LIMIT,
189
+ max_input_length - input_len,
190
+ ),
191
+ )
192
+
193
+ gen_kwargs = {
194
+ "max_new_tokens": max_new_tokens,
195
+ "pad_token_id": self.tokenizer.pad_token_id,
196
+ "eos_token_id": self.tokenizer.eos_token_id,
197
+ }
198
+
199
+ if request.temperature > 0:
200
+ gen_kwargs.update(
201
+ do_sample=True,
202
+ temperature=request.temperature,
203
+ top_p=request.top_p if request.top_p > 0 else 1.0,
204
+ )
205
+ else:
206
+ gen_kwargs.update(do_sample=False)
207
+
208
+ with torch.inference_mode():
209
+ output_ids = self.model.generate(**inputs, **gen_kwargs)
210
+
211
+ new_tokens = output_ids[0, inputs["input_ids"].shape[1]:]
212
+ return self.tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
213
+
214
+ except Exception:
215
+ self._cleanup()
216
+ raise
217
 
218
 
 
 
 
219
  class PriorityBroker:
220
  def __init__(self):
221
  self.queue = asyncio.PriorityQueue()
 
227
 
228
  async def stop(self):
229
  self.counter += 1
230
+ await self.queue.put((999999, self.counter, None, None))
231
  if self.task:
232
  await self.task
233
 
234
+ async def enqueue(self, priority: int, request: ChatRequest) -> str:
235
  loop = asyncio.get_running_loop()
236
  future = loop.create_future()
237
  self.counter += 1
238
+ await self.queue.put((priority, self.counter, future, request))
239
  return await future
240
 
241
  async def _worker(self):
242
  while True:
243
  item = await self.queue.get()
244
+ priority, counter, future, request = item
245
 
246
  if future is None:
247
  self.queue.task_done()
248
  break
249
+
250
  if future.cancelled():
251
  self.queue.task_done()
252
  continue
253
 
254
  try:
255
+ result = await asyncio.to_thread(model_manager.generate, request)
256
+ future.set_result(result)
 
 
 
 
 
257
  except Exception as exc:
258
  future.set_exception(exc)
259
  finally:
260
  self.queue.task_done()
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
+ model_manager = ModelManager()
264
  broker = PriorityBroker()
265
 
266
 
 
 
 
267
  @asynccontextmanager
268
  async def lifespan(app: FastAPI):
269
  await broker.start()
270
  yield
271
  await broker.stop()
272
 
273
+
274
  app = FastAPI(title="Priority Model Chat", lifespan=lifespan)
275
+
276
+ app.add_middleware(
277
+ CORSMiddleware,
278
+ allow_origins=["*"],
279
+ allow_methods=["*"],
280
+ allow_headers=["*"],
281
+ )
282
 
283
 
284
  def require_priority_key(
285
  x_api_key: Optional[str] = Header(default=None, alias="x-api-key"),
286
  authorization: Optional[str] = Header(default=None),
287
  ):
288
+ if ALLOW_OPEN_PRIORITY:
289
+ return
290
+
291
  provided = None
292
+
293
  if x_api_key:
294
  provided = x_api_key.strip()
295
  elif authorization and authorization.lower().startswith("bearer "):
296
  provided = authorization[7:].strip()
297
+
298
  if not PRIORITY_API_KEY:
299
+ raise HTTPException(
300
+ status_code=503,
301
+ detail=(
302
+ "Set PRIORITY_API_KEY in Space secrets, "
303
+ "or set ALLOW_OPEN_PRIORITY=1 for testing."
304
+ ),
305
+ )
306
+
307
  if provided != PRIORITY_API_KEY:
308
+ raise HTTPException(
309
+ status_code=401,
310
+ detail="Invalid or missing priority API key.",
311
+ )
312
 
313
 
314
  @app.get("/health")
315
  async def health():
316
+ return {
317
+ "ok": True,
318
+ "device": DEVICE,
319
+ "models": list(MODELS.keys()),
320
+ "queue_size": broker.queue.qsize(),
321
+ "priority_open": ALLOW_OPEN_PRIORITY,
322
+ }
323
+
324
 
325
  @app.get("/api/models")
326
  async def models():
327
  return {"models": MODELS}
328
 
329
+
330
  @app.post("/api/chat")
331
  async def chat(req: ChatRequest):
332
+ if not req.model:
333
+ req.model = DEFAULT_MODEL
334
+
335
  if req.model not in MODELS:
336
+ raise HTTPException(
337
+ status_code=404,
338
+ detail=f"Model '{req.model}' not found. Available: {list(MODELS.keys())}",
339
+ )
340
+
341
  try:
342
+ answer = await broker.enqueue(priority=1, request=req)
343
+ return {
344
+ "model": req.model,
345
+ "response": answer,
346
+ }
347
  except Exception as exc:
348
+ raise HTTPException(status_code=500, detail=f"{type(exc).__name__}: {exc}")
349
+
350
 
351
  @app.post("/api/priority")
352
+ async def priority_chat(
353
+ req: ChatRequest,
354
+ _: None = Depends(require_priority_key),
355
+ ):
356
+ if not req.model:
357
+ req.model = DEFAULT_MODEL
358
+
359
  if req.model not in MODELS:
360
+ raise HTTPException(
361
+ status_code=404,
362
+ detail=f"Model '{req.model}' not found. Available: {list(MODELS.keys())}",
363
+ )
364
+
365
  try:
366
+ answer = await broker.enqueue(priority=0, request=req)
367
+ return {
368
+ "model": req.model,
369
+ "response": answer,
370
+ "priority": True,
371
+ }
372
  except Exception as exc:
373
+ raise HTTPException(status_code=500, detail=f"{type(exc).__name__}: {exc}")
374
 
375
 
376
+ CSS = """
377
+ .gradio-container {
378
+ max-width: 950px;
379
+ margin: auto;
380
+ }
381
+ footer {
382
+ display: none !important;
383
+ }
384
+ """
 
 
 
 
 
 
 
385
 
386
 
387
+ async def ui_respond(
388
+ model_name,
389
+ user_message,
390
+ history,
391
+ system,
392
+ temperature,
393
+ top_p,
394
+ max_new_tokens,
395
+ ):
396
+ if not user_message or not str(user_message).strip():
397
+ yield history, ""
398
+ return
399
 
400
+ if model_name not in MODELS:
401
+ model_name = DEFAULT_MODEL
402
 
403
+ clean_history = []
404
+ for item in history or []:
405
+ if not isinstance(item, dict):
406
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
+ role = item.get("role")
409
+ content = str(item.get("content", ""))
410
+
411
+ if role not in {"user", "assistant", "system"}:
412
+ continue
413
+
414
+ if content == "⏳ cooking...":
415
+ continue
416
+
417
+ clean_history.append({"role": role, "content": content})
418
+
419
+ api_messages = []
420
+
421
+ if system and str(system).strip():
422
+ api_messages.append({"role": "system", "content": str(system).strip()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
+ api_messages.extend(clean_history)
425
+ api_messages.append({"role": "user", "content": str(user_message).strip()})
426
+
427
+ request = ChatRequest(
428
+ model=model_name,
429
+ messages=[ChatMessage(**m) for m in api_messages],
430
+ max_new_tokens=int(max_new_tokens or DEFAULT_MAX_TOKENS),
431
+ temperature=float(temperature or 0.7),
432
+ top_p=float(top_p or 0.95),
433
+ )
434
+
435
+ display_history = clean_history + [
436
+ {"role": "user", "content": str(user_message).strip()},
437
+ {"role": "assistant", "content": "⏳ cooking..."},
438
+ ]
439
+
440
+ yield display_history, ""
441
+
442
+ try:
443
+ answer = await broker.enqueue(priority=1, request=request)
444
+ display_history[-1]["content"] = answer
445
+ except Exception as exc:
446
+ display_history[-1]["content"] = f"💀 {type(exc).__name__}: {exc}"
447
+
448
+ yield display_history, ""
449
+
450
+
451
+ with gr.Blocks(css=CSS) as demo:
452
+ gr.Markdown("## ⚡ Priority Model Chat")
453
+ gr.Markdown("chat UI uses the normal queue. priority API is separate.")
454
+
455
+ chatbot = gr.Chatbot(
456
+ type="messages",
457
+ height=520,
458
+ )
459
+
460
+ with gr.Row():
461
+ msg = gr.Textbox(
462
+ label="Message",
463
+ placeholder="type something...",
464
+ lines=2,
465
+ scale=5,
466
+ )
467
+ send = gr.Button("Send", variant="primary", scale=1)
468
+
469
+ with gr.Accordion("⚙️ model + generation settings", open=False):
470
+ model = gr.Dropdown(
471
+ choices=list(MODELS.keys()),
472
+ value=DEFAULT_MODEL,
473
+ label="Model",
474
+ )
475
+ system = gr.Textbox(
476
+ label="System prompt",
477
+ value="You are a helpful assistant.",
478
+ lines=2,
479
+ )
480
+ temperature = gr.Slider(
481
+ 0.0,
482
+ 2.0,
483
+ value=0.7,
484
+ step=0.05,
485
+ label="Temperature",
486
+ )
487
+ top_p = gr.Slider(
488
+ 0.0,
489
+ 1.0,
490
+ value=0.95,
491
+ step=0.01,
492
+ label="Top-p",
493
+ )
494
+ max_new_tokens = gr.Slider(
495
+ 16,
496
+ MAX_SLIDER,
497
+ value=DEFAULT_MAX_TOKENS,
498
+ step=16,
499
+ label="Max new tokens",
500
+ )
501
+
502
+ clear = gr.Button("🧹 Clear chat")
503
+ clear.click(lambda: ([], ""), inputs=[], outputs=[chatbot, msg])
504
+
505
+ inputs = [model, msg, chatbot, system, temperature, top_p, max_new_tokens]
506
+ outputs = [chatbot, msg]
507
+
508
+ msg.submit(ui_respond, inputs, outputs)
509
+ send.click(ui_respond, inputs, outputs)
510
+
511
+
512
+ demo.queue()
513
+ app = gr.mount_gradio_app(app, demo, path="/")