varsshancoder commited on
Commit
d274bd7
·
verified ·
1 Parent(s): a07d2e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +350 -58
app.py CHANGED
@@ -1,99 +1,391 @@
1
  import os
2
  import time
3
- from fastapi import FastAPI, Request, HTTPException, Depends
 
 
 
 
 
 
 
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.responses import JSONResponse
6
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
  from huggingface_hub import hf_hub_download
8
  from llama_cpp import Llama
9
 
10
- app = FastAPI(title="Qwen 2.5 Coder Public Secure Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  app.add_middleware(
13
  CORSMiddleware,
14
  allow_origins=["*"],
15
- allow_credentials=True,
16
  allow_methods=["*"],
17
  allow_headers=["*"],
18
  )
19
 
20
- # 🔐 YOUR PREFERRED CUSTOM API KEY
21
- CUSTOM_API_KEY = "varsshancoder"
22
 
23
- security = HTTPBearer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- def verify_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
26
- if credentials.credentials != CUSTOM_API_KEY:
 
 
 
 
 
 
 
 
 
 
 
27
  raise HTTPException(
28
  status_code=401,
29
- detail="Invalid or missing custom API Key."
 
30
  )
31
- return credentials.credentials
32
 
33
- MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
34
- MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- print("Checking and downloading Qwen 2.5 Coder 7B GGUF model...")
37
- model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- print(f"Loading model into memory from: {model_path}")
40
  llm = Llama(
41
  model_path=model_path,
42
- n_ctx=4096,
43
- n_threads=2,
 
 
 
 
 
 
44
  n_batch=512,
45
- verbose=False
 
 
46
  )
47
- print("Model loaded successfully and secured!")
48
 
49
- @app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  async def chat_completions(request: Request):
51
- body = await request.json()
52
- messages = body.get("messages", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  temperature = body.get("temperature", 0.2)
 
54
  max_tokens = body.get("max_tokens", 1024)
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  formatted_prompt = ""
57
- for msg in messages:
58
- role = msg.get("role", "user")
59
- content = msg.get("content", "")
60
- formatted_prompt += f"<|im_start|>{role}\n{content}<|im_end|>\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  formatted_prompt += "<|im_start|>assistant\n"
62
 
63
- response = llm(
64
- formatted_prompt,
65
- max_tokens=max_tokens,
66
- stop=["<|im_end|>", "<|im_start|>"],
67
- temperature=temperature
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  )
69
 
70
- text_output = response["choices"]["text"]
71
-
72
- return JSONResponse(content={
73
- "id": f"chatcmpl-{int(time.time())}",
74
- "object": "chat.completion",
75
- "created": int(time.time()),
76
- "model": "qwen2.5-coder-7b",
77
- "choices": [{
78
- "index": 0,
79
- "message": {
80
- "role": "assistant",
81
- "content": text_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  },
83
- "finish_reason": "stop"
84
- }],
85
- "usage": {
86
- "prompt_tokens": len(formatted_prompt) // 4,
87
- "completion_tokens": len(text_output) // 4,
88
- "total_tokens": (len(formatted_prompt) + len(text_output)) // 4
89
  }
90
- })
91
 
92
- @app.get("/")
93
- def index():
94
- return {
95
- "status": "healthy",
96
- "security": "enabled",
97
- "model": "qwen2.5-coder-7b-instruct-q4_k_m",
98
- "endpoint": "/v1/chat/completions"
99
- }
 
 
 
 
1
  import os
2
  import time
3
+ import secrets
4
+
5
+ from fastapi import (
6
+ FastAPI,
7
+ Request,
8
+ HTTPException,
9
+ Depends,
10
+ )
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse
13
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
14
  from huggingface_hub import hf_hub_download
15
  from llama_cpp import Llama
16
 
17
+
18
+ # ============================================================
19
+ # APPLICATION
20
+ # ============================================================
21
+
22
+ app = FastAPI(
23
+ title="Varsh LLM",
24
+ description="Secure Qwen 2.5 Coder 7B OpenAI-compatible API",
25
+ version="1.0.0",
26
+ )
27
+
28
+
29
+ # ============================================================
30
+ # CORS
31
+ # ============================================================
32
 
33
  app.add_middleware(
34
  CORSMiddleware,
35
  allow_origins=["*"],
36
+ allow_credentials=False,
37
  allow_methods=["*"],
38
  allow_headers=["*"],
39
  )
40
 
 
 
41
 
42
+ # ============================================================
43
+ # CONFIGURATION
44
+ # ============================================================
45
+
46
+ MODEL_ID = "qwen2.5-coder-7b-instruct-q4_k_m"
47
+
48
+ MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
49
+
50
+ MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
51
+
52
+ # Read API key from Hugging Face Space Secret
53
+ CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY")
54
+
55
+ if not CUSTOM_API_KEY:
56
+ raise RuntimeError(
57
+ "CUSTOM_API_KEY is not configured. "
58
+ "Add CUSTOM_API_KEY in Hugging Face Space Settings > Secrets."
59
+ )
60
+
61
+
62
+ # ============================================================
63
+ # SECURITY
64
+ # ============================================================
65
 
66
+ security = HTTPBearer(auto_error=False)
67
+
68
+
69
+ def verify_api_key(
70
+ credentials: HTTPAuthorizationCredentials | None = Depends(security),
71
+ ):
72
+ """
73
+ Validate:
74
+
75
+ Authorization: Bearer <CUSTOM_API_KEY>
76
+ """
77
+
78
+ if credentials is None:
79
  raise HTTPException(
80
  status_code=401,
81
+ detail="Missing API key.",
82
+ headers={"WWW-Authenticate": "Bearer"},
83
  )
 
84
 
85
+ if credentials.scheme.lower() != "bearer":
86
+ raise HTTPException(
87
+ status_code=401,
88
+ detail="Authentication scheme must be Bearer.",
89
+ headers={"WWW-Authenticate": "Bearer"},
90
+ )
91
+
92
+ if not secrets.compare_digest(
93
+ credentials.credentials,
94
+ CUSTOM_API_KEY,
95
+ ):
96
+ raise HTTPException(
97
+ status_code=401,
98
+ detail="Invalid API key.",
99
+ headers={"WWW-Authenticate": "Bearer"},
100
+ )
101
+
102
+ return True
103
+
104
+
105
+ # ============================================================
106
+ # MODEL LOADING
107
+ # ============================================================
108
 
109
+ print("=" * 70)
110
+ print("Starting Varsh LLM")
111
+ print("=" * 70)
112
+
113
+ print(f"Model repository: {MODEL_REPO}")
114
+ print(f"Model file: {MODEL_FILE}")
115
+ print("Checking/downloading model...")
116
+
117
+ model_path = hf_hub_download(
118
+ repo_id=MODEL_REPO,
119
+ filename=MODEL_FILE,
120
+ )
121
+
122
+ print(f"Model path: {model_path}")
123
+ print("Loading Qwen 2.5 Coder 7B...")
124
 
 
125
  llm = Llama(
126
  model_path=model_path,
127
+
128
+ # Context window
129
+ n_ctx=4096,
130
+
131
+ # Your HF Space has 2 vCPU
132
+ n_threads=2,
133
+
134
+ # Batch size
135
  n_batch=512,
136
+
137
+ # CPU inference
138
+ verbose=False,
139
  )
 
140
 
141
+ print("Model loaded successfully.")
142
+ print("=" * 70)
143
+
144
+
145
+ # ============================================================
146
+ # HEALTH / ROOT
147
+ # ============================================================
148
+
149
+ @app.get("/")
150
+ async def root():
151
+ return {
152
+ "status": "healthy",
153
+ "security": "enabled",
154
+ "model": MODEL_ID,
155
+ "endpoint": "/v1/chat/completions",
156
+ }
157
+
158
+
159
+ @app.get("/health")
160
+ async def health():
161
+ return {
162
+ "status": "healthy",
163
+ "model": MODEL_ID,
164
+ }
165
+
166
+
167
+ # ============================================================
168
+ # CHAT COMPLETIONS
169
+ # ============================================================
170
+
171
+ @app.post(
172
+ "/v1/chat/completions",
173
+ dependencies=[Depends(verify_api_key)],
174
+ )
175
  async def chat_completions(request: Request):
176
+
177
+ # --------------------------------------------------------
178
+ # Parse JSON
179
+ # --------------------------------------------------------
180
+
181
+ try:
182
+ body = await request.json()
183
+ except Exception:
184
+ raise HTTPException(
185
+ status_code=400,
186
+ detail="Invalid JSON request body.",
187
+ )
188
+
189
+ # --------------------------------------------------------
190
+ # Extract parameters
191
+ # --------------------------------------------------------
192
+
193
+ messages = body.get("messages")
194
+
195
+ if not messages:
196
+ raise HTTPException(
197
+ status_code=400,
198
+ detail="The 'messages' field is required.",
199
+ )
200
+
201
+ requested_model = body.get("model", MODEL_ID)
202
+
203
+ # Accept our model ID
204
+ if requested_model != MODEL_ID:
205
+ raise HTTPException(
206
+ status_code=400,
207
+ detail=f"Unsupported model. Use '{MODEL_ID}'.",
208
+ )
209
+
210
  temperature = body.get("temperature", 0.2)
211
+
212
  max_tokens = body.get("max_tokens", 1024)
213
 
214
+ # Safety limits
215
+ try:
216
+ temperature = float(temperature)
217
+ max_tokens = int(max_tokens)
218
+ except (TypeError, ValueError):
219
+ raise HTTPException(
220
+ status_code=400,
221
+ detail="temperature must be a number and max_tokens must be an integer.",
222
+ )
223
+
224
+ if temperature < 0:
225
+ temperature = 0.0
226
+
227
+ if temperature > 2:
228
+ temperature = 2.0
229
+
230
+ if max_tokens < 1:
231
+ max_tokens = 1
232
+
233
+ if max_tokens > 4096:
234
+ max_tokens = 4096
235
+
236
+ # --------------------------------------------------------
237
+ # Build Qwen ChatML prompt
238
+ # --------------------------------------------------------
239
+
240
  formatted_prompt = ""
241
+
242
+ for message in messages:
243
+
244
+ role = message.get("role", "user")
245
+
246
+ content = message.get("content", "")
247
+
248
+ # Handle simple OpenAI-style text messages
249
+ if isinstance(content, list):
250
+
251
+ text_parts = []
252
+
253
+ for item in content:
254
+ if isinstance(item, dict):
255
+ if item.get("type") == "text":
256
+ text_parts.append(
257
+ item.get("text", "")
258
+ )
259
+
260
+ content = "\n".join(text_parts)
261
+
262
+ if not isinstance(content, str):
263
+ content = str(content)
264
+
265
+ formatted_prompt += (
266
+ f"<|im_start|>{role}\n"
267
+ f"{content}"
268
+ f"<|im_end|>\n"
269
+ )
270
+
271
  formatted_prompt += "<|im_start|>assistant\n"
272
 
273
+ # --------------------------------------------------------
274
+ # Generate
275
+ # --------------------------------------------------------
276
+
277
+ start_time = time.time()
278
+
279
+ try:
280
+
281
+ response = llm(
282
+ formatted_prompt,
283
+ max_tokens=max_tokens,
284
+ temperature=temperature,
285
+ stop=[
286
+ "<|im_end|>",
287
+ "<|im_start|>",
288
+ ],
289
+ )
290
+
291
+ except Exception as e:
292
+
293
+ print(f"Generation error: {e}")
294
+
295
+ raise HTTPException(
296
+ status_code=500,
297
+ detail="Model generation failed.",
298
+ )
299
+
300
+ generation_time = time.time() - start_time
301
+
302
+ # --------------------------------------------------------
303
+ # IMPORTANT:
304
+ # llama.cpp returns choices as a LIST
305
+ # --------------------------------------------------------
306
+
307
+ try:
308
+ text_output = response["choices"][0]["text"]
309
+ except (KeyError, IndexError, TypeError):
310
+
311
+ print(f"Unexpected model response: {response}")
312
+
313
+ raise HTTPException(
314
+ status_code=500,
315
+ detail="Invalid response received from model.",
316
+ )
317
+
318
+ # --------------------------------------------------------
319
+ # Token information
320
+ # --------------------------------------------------------
321
+
322
+ usage = response.get("usage", {})
323
+
324
+ prompt_tokens = usage.get(
325
+ "prompt_tokens",
326
+ 0,
327
+ )
328
+
329
+ completion_tokens = usage.get(
330
+ "completion_tokens",
331
+ 0,
332
+ )
333
+
334
+ total_tokens = usage.get(
335
+ "total_tokens",
336
+ prompt_tokens + completion_tokens,
337
  )
338
 
339
+ # --------------------------------------------------------
340
+ # OpenAI-compatible response
341
+ # --------------------------------------------------------
342
+
343
+ created = int(time.time())
344
+
345
+ return JSONResponse(
346
+ content={
347
+ "id": f"chatcmpl-{created}",
348
+
349
+ "object": "chat.completion",
350
+
351
+ "created": created,
352
+
353
+ "model": MODEL_ID,
354
+
355
+ "choices": [
356
+ {
357
+ "index": 0,
358
+
359
+ "message": {
360
+ "role": "assistant",
361
+ "content": text_output,
362
+ },
363
+
364
+ "finish_reason": "stop",
365
+ }
366
+ ],
367
+
368
+ "usage": {
369
+ "prompt_tokens": prompt_tokens,
370
+ "completion_tokens": completion_tokens,
371
+ "total_tokens": total_tokens,
372
  },
373
+
374
+ "x_generation_time_seconds": round(
375
+ generation_time,
376
+ 3,
377
+ ),
 
378
  }
379
+ )
380
 
381
+
382
+ # ============================================================
383
+ # STARTUP MESSAGE
384
+ # ============================================================
385
+
386
+ print("=" * 70)
387
+ print("Varsh LLM API READY")
388
+ print("Listening on: http://0.0.0.0:7860")
389
+ print("Endpoint: /v1/chat/completions")
390
+ print("Authentication: Bearer API key")
391
+ print("=" * 70)