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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -164
app.py CHANGED
@@ -1,80 +1,85 @@
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,
@@ -85,21 +90,16 @@ def verify_api_key(
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
  # ============================================================
@@ -110,8 +110,12 @@ 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(
@@ -124,46 +128,90 @@ 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
  # ============================================================
@@ -190,133 +238,131 @@ async def chat_completions(request: Request):
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", {})
@@ -336,56 +382,41 @@ async def chat_completions(request: Request):
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)
 
1
  import os
2
  import time
3
+ import uuid
4
 
5
+ from fastapi import FastAPI, Request, HTTPException, Depends
 
 
 
 
 
6
  from fastapi.middleware.cors import CORSMiddleware
 
7
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8
+ from fastapi.responses import JSONResponse
9
  from huggingface_hub import hf_hub_download
10
  from llama_cpp import Llama
11
 
12
 
13
  # ============================================================
14
+ # CONFIGURATION
15
  # ============================================================
16
 
17
+ APP_NAME = "Varsh LLM"
18
+ MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
19
+ MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
 
 
20
 
21
+ MODEL_ID = "qwen2.5-coder-7b-instruct-q4_k_m"
22
 
23
+ # IMPORTANT:
24
+ # Set CUSTOM_API_KEY in:
25
+ # Hugging Face Space → Settings → Secrets
26
+ CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY")
27
 
28
+ if not CUSTOM_API_KEY:
29
+ raise RuntimeError(
30
+ "CUSTOM_API_KEY is not configured. "
31
+ "Add CUSTOM_API_KEY in Hugging Face Space Settings > Secrets."
32
+ )
 
 
33
 
34
 
35
  # ============================================================
36
+ # SERVER SETTINGS
37
  # ============================================================
38
 
39
+ HOST = "0.0.0.0"
40
+ PORT = int(os.getenv("PORT", "7860"))
41
 
42
+ # Your current HF Space has 2 vCPU.
43
+ # Keep this at 2 unless you upgrade the Space.
44
+ N_THREADS = int(os.getenv("N_THREADS", "2"))
45
 
46
+ # Prompt-processing batch size.
47
+ N_BATCH = int(os.getenv("N_BATCH", "512"))
48
 
49
+ # Context size.
50
+ N_CTX = int(os.getenv("N_CTX", "4096"))
51
 
52
+
53
+ # ============================================================
54
+ # FASTAPI
55
+ # ============================================================
56
+
57
+ app = FastAPI(
58
+ title="Varsh LLM API",
59
+ version="1.0.0",
60
+ description="OpenAI-compatible Qwen 2.5 Coder API",
61
+ )
62
+
63
+
64
+ app.add_middleware(
65
+ CORSMiddleware,
66
+ allow_origins=["*"],
67
+ allow_credentials=False,
68
+ allow_methods=["*"],
69
+ allow_headers=["*"],
70
+ )
71
 
72
 
73
  # ============================================================
74
+ # AUTHENTICATION
75
  # ============================================================
76
 
77
  security = HTTPBearer(auto_error=False)
78
 
79
 
80
  def verify_api_key(
81
+ credentials: HTTPAuthorizationCredentials = Depends(security),
82
  ):
 
 
 
 
 
 
83
  if credentials is None:
84
  raise HTTPException(
85
  status_code=401,
 
90
  if credentials.scheme.lower() != "bearer":
91
  raise HTTPException(
92
  status_code=401,
93
+ detail="Authentication must use Bearer token.",
 
94
  )
95
 
96
+ if credentials.credentials != CUSTOM_API_KEY:
 
 
 
97
  raise HTTPException(
98
  status_code=401,
99
  detail="Invalid API key.",
 
100
  )
101
 
102
+ return credentials.credentials
103
 
104
 
105
  # ============================================================
 
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(f"Threads : {N_THREADS}")
116
+ print(f"Batch size : {N_BATCH}")
117
+ print(f"Context size : {N_CTX}")
118
+
119
  print("Checking/downloading model...")
120
 
121
  model_path = hf_hub_download(
 
128
 
129
  llm = Llama(
130
  model_path=model_path,
131
+ n_ctx=N_CTX,
132
+ n_threads=N_THREADS,
133
+ n_batch=N_BATCH,
 
 
 
 
 
 
 
 
134
  verbose=False,
135
  )
136
 
137
  print("Model loaded successfully.")
138
+
139
+ print("=" * 70)
140
+ print("Varsh LLM API READY")
141
+ print("=" * 70)
142
+ print(f"Listening on: http://{HOST}:{PORT}")
143
+ print("Root endpoint: /")
144
+ print("Models endpoint: /v1/models")
145
+ print("Chat endpoint: /v1/chat/completions")
146
+ print("Authentication: Bearer API key")
147
  print("=" * 70)
148
 
149
 
150
  # ============================================================
151
+ # ROOT / HEALTH
152
  # ============================================================
153
 
154
  @app.get("/")
155
+ def root():
156
  return {
157
  "status": "healthy",
158
+ "service": APP_NAME,
159
  "security": "enabled",
160
  "model": MODEL_ID,
161
  "endpoint": "/v1/chat/completions",
162
+ "models_endpoint": "/v1/models",
163
  }
164
 
165
 
166
  @app.get("/health")
167
+ def health():
168
  return {
169
  "status": "healthy",
170
  "model": MODEL_ID,
171
  }
172
 
173
 
174
+ # ============================================================
175
+ # OPENAI-COMPATIBLE MODELS ENDPOINT
176
+ # ============================================================
177
+
178
+ @app.get(
179
+ "/v1/models",
180
+ dependencies=[Depends(verify_api_key)],
181
+ )
182
+ def list_models():
183
+ return {
184
+ "object": "list",
185
+ "data": [
186
+ {
187
+ "id": MODEL_ID,
188
+ "object": "model",
189
+ "created": int(time.time()),
190
+ "owned_by": "varsshancoder",
191
+ }
192
+ ],
193
+ }
194
+
195
+
196
+ @app.get(
197
+ "/v1/models/{model_id}",
198
+ dependencies=[Depends(verify_api_key)],
199
+ )
200
+ def get_model(model_id: str):
201
+ if model_id != MODEL_ID:
202
+ raise HTTPException(
203
+ status_code=404,
204
+ detail=f"Model '{model_id}' not found.",
205
+ )
206
+
207
+ return {
208
+ "id": MODEL_ID,
209
+ "object": "model",
210
+ "created": int(time.time()),
211
+ "owned_by": "varsshancoder",
212
+ }
213
+
214
+
215
  # ============================================================
216
  # CHAT COMPLETIONS
217
  # ============================================================
 
238
  # Extract parameters
239
  # --------------------------------------------------------
240
 
241
+ requested_model = body.get("model", MODEL_ID)
242
 
243
+ if requested_model != MODEL_ID:
244
  raise HTTPException(
245
+ status_code=404,
246
+ detail=f"Model '{requested_model}' not found.",
247
  )
248
 
249
+ messages = body.get("messages")
250
 
251
+ if not messages or not isinstance(messages, list):
 
252
  raise HTTPException(
253
  status_code=400,
254
+ detail="The 'messages' field is required and must be a list.",
255
  )
256
 
257
  temperature = body.get("temperature", 0.2)
 
258
  max_tokens = body.get("max_tokens", 1024)
259
 
260
+ # Optional OpenAI-compatible parameters
261
+ top_p = body.get("top_p", 0.95)
262
+ stop = body.get("stop", None)
263
+
264
+ stream = body.get("stream", False)
265
+
266
+ # Current implementation returns normal JSON.
267
+ # Streaming can be added separately once basic routing is stable.
268
+ if stream:
269
  raise HTTPException(
270
  status_code=400,
271
+ detail="Streaming is not enabled on this deployment.",
272
  )
273
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  # --------------------------------------------------------
275
+ # Sanitize messages
276
  # --------------------------------------------------------
277
 
278
+ clean_messages = []
279
 
280
  for message in messages:
281
+ if not isinstance(message, dict):
282
+ continue
283
 
284
  role = message.get("role", "user")
 
285
  content = message.get("content", "")
286
 
287
+ if content is None:
288
+ content = ""
 
 
 
 
 
 
 
 
 
 
 
289
 
290
+ # Handle normal string content.
291
+ # For now, multimodal content arrays are not supported.
292
  if not isinstance(content, str):
293
+ raise HTTPException(
294
+ status_code=400,
295
+ detail="Message content must be a string.",
296
+ )
297
+
298
+ clean_messages.append(
299
+ {
300
+ "role": role,
301
+ "content": content,
302
+ }
303
  )
304
 
305
+ if not clean_messages:
306
+ raise HTTPException(
307
+ status_code=400,
308
+ detail="No valid messages were provided.",
309
+ )
310
 
311
  # --------------------------------------------------------
312
+ # Generate response
313
  # --------------------------------------------------------
314
 
315
+ request_id = f"chatcmpl-{uuid.uuid4().hex}"
316
 
317
+ created = int(time.time())
318
 
319
+ start_time = time.perf_counter()
320
+
321
+ try:
322
+ response = llm.create_chat_completion(
323
+ messages=clean_messages,
324
+ temperature=float(temperature),
325
+ max_tokens=int(max_tokens),
326
+ top_p=float(top_p),
327
+ stop=stop,
328
  )
329
 
330
  except Exception as e:
331
+ print(f"Inference error: {e}")
 
332
 
333
  raise HTTPException(
334
  status_code=500,
335
+ detail=f"Model inference failed: {str(e)}",
336
  )
337
 
338
+ generation_time = time.perf_counter() - start_time
339
 
340
  # --------------------------------------------------------
341
+ # Extract generated text
 
342
  # --------------------------------------------------------
343
 
344
  try:
345
+ choice = response["choices"][0]
346
+
347
+ message = choice["message"]
348
 
349
+ text_output = message.get("content", "")
350
+
351
+ finish_reason = choice.get(
352
+ "finish_reason",
353
+ "stop",
354
+ )
355
+
356
+ except Exception as e:
357
+ print(f"Response parsing error: {e}")
358
 
359
  raise HTTPException(
360
  status_code=500,
361
+ detail="Invalid response returned by the model.",
362
  )
363
 
364
  # --------------------------------------------------------
365
+ # Usage
366
  # --------------------------------------------------------
367
 
368
  usage = response.get("usage", {})
 
382
  prompt_tokens + completion_tokens,
383
  )
384
 
385
+ print(
386
+ f"Request {request_id} | "
387
+ f"prompt={prompt_tokens} | "
388
+ f"completion={completion_tokens} | "
389
+ f"time={generation_time:.2f}s"
390
+ )
391
+
392
  # --------------------------------------------------------
393
  # OpenAI-compatible response
394
  # --------------------------------------------------------
395
 
 
 
396
  return JSONResponse(
397
  content={
398
+ "id": request_id,
 
399
  "object": "chat.completion",
 
400
  "created": created,
 
401
  "model": MODEL_ID,
 
402
  "choices": [
403
  {
404
  "index": 0,
 
405
  "message": {
406
  "role": "assistant",
407
  "content": text_output,
408
  },
409
+ "finish_reason": finish_reason,
 
410
  }
411
  ],
 
412
  "usage": {
413
  "prompt_tokens": prompt_tokens,
414
  "completion_tokens": completion_tokens,
415
  "total_tokens": total_tokens,
416
  },
 
417
  "x_generation_time_seconds": round(
418
  generation_time,
419
  3,
420
  ),
421
  }
422
+ )