varsshancoder commited on
Commit
276b74c
·
verified ·
1 Parent(s): 1a1f0f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +371 -514
app.py CHANGED
@@ -1,8 +1,9 @@
1
  import os
2
  import time
3
- import uuid
4
  import json
5
- import threading
 
 
6
 
7
  from fastapi import FastAPI, Request, HTTPException, Depends
8
  from fastapi.middleware.cors import CORSMiddleware
@@ -18,40 +19,19 @@ from llama_cpp import Llama
18
  # ============================================================
19
 
20
  APP_NAME = "Varsh LLM"
 
21
 
22
  MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
23
  MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
24
 
25
- # This MUST exactly match the model ID expected by OmniRoute.
26
- MODEL_ID = "qwen2.5-coder-7b-instruct-q4_k_m"
27
-
28
  HOST = "0.0.0.0"
29
  PORT = int(os.getenv("PORT", "7860"))
30
 
31
- # Your current HF Space = 2 vCPU.
32
- # Keep this at 2 unless you upgrade the Space.
33
- N_THREADS = int(os.getenv("N_THREADS", "2"))
34
-
35
- # llama.cpp prompt processing batch size.
36
- N_BATCH = int(os.getenv("N_BATCH", "512"))
37
-
38
- # Context window.
39
- N_CTX = int(os.getenv("N_CTX", "4096"))
40
-
41
-
42
- # ============================================================
43
- # API KEY
44
- # ============================================================
45
-
46
- # Set this in:
47
  #
48
- # Hugging Face Space
49
- # -> Settings
50
- # -> Secrets
51
- # -> CUSTOM_API_KEY
52
- #
53
- # DO NOT hard-code the secret in this file.
54
-
55
  CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY")
56
 
57
  if not CUSTOM_API_KEY:
@@ -62,33 +42,23 @@ if not CUSTOM_API_KEY:
62
 
63
 
64
  # ============================================================
65
- # FASTAPI APPLICATION
66
  # ============================================================
67
 
68
  app = FastAPI(
69
- title="Varsh LLM API",
70
- version="1.0.0",
71
- description="OpenAI-compatible Qwen 2.5 Coder API",
72
  )
73
 
74
-
75
- # ============================================================
76
- # CORS
77
- # ============================================================
78
-
79
  app.add_middleware(
80
  CORSMiddleware,
81
  allow_origins=["*"],
82
- allow_credentials=False,
83
  allow_methods=["*"],
84
  allow_headers=["*"],
85
  )
86
 
87
 
88
- # ============================================================
89
- # AUTHENTICATION
90
- # ============================================================
91
-
92
  security = HTTPBearer(auto_error=False)
93
 
94
 
@@ -98,16 +68,13 @@ def verify_api_key(
98
  if credentials is None:
99
  raise HTTPException(
100
  status_code=401,
101
- detail="Missing API key.",
102
- headers={
103
- "WWW-Authenticate": "Bearer"
104
- },
105
  )
106
 
107
  if credentials.scheme.lower() != "bearer":
108
  raise HTTPException(
109
  status_code=401,
110
- detail="Authentication must use Bearer token.",
111
  )
112
 
113
  if credentials.credentials != CUSTOM_API_KEY:
@@ -116,25 +83,19 @@ def verify_api_key(
116
  detail="Invalid API key.",
117
  )
118
 
119
- return credentials.credentials
120
 
121
 
122
  # ============================================================
123
- # MODEL LOADING
124
  # ============================================================
125
 
126
- print("=" * 70)
127
  print("Starting Varsh LLM")
128
- print("=" * 70)
129
-
130
- print(f"Model repository : {MODEL_REPO}")
131
- print(f"Model file : {MODEL_FILE}")
132
- print(f"Model ID : {MODEL_ID}")
133
- print(f"Threads : {N_THREADS}")
134
- print(f"Batch size : {N_BATCH}")
135
- print(f"Context size : {N_CTX}")
136
-
137
- print("Checking/downloading model...")
138
 
139
  model_path = hf_hub_download(
140
  repo_id=MODEL_REPO,
@@ -142,58 +103,234 @@ model_path = hf_hub_download(
142
  )
143
 
144
  print(f"Model path: {model_path}")
145
-
146
  print("Loading Qwen 2.5 Coder 7B...")
147
 
148
  llm = Llama(
149
  model_path=model_path,
150
 
151
- # Context window
152
- n_ctx=N_CTX,
153
-
154
- # CPU threads
155
- n_threads=N_THREADS,
156
 
157
- # Prompt processing batch
158
- n_batch=N_BATCH,
159
 
160
- # Let llama.cpp use its Qwen chat template
161
- chat_format=None,
 
162
 
163
  verbose=False,
164
  )
165
 
166
  print("Model loaded successfully.")
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
 
169
  # ============================================================
170
- # GENERATION LOCK
171
  # ============================================================
172
 
173
- # The HF Space has one model instance on CPU.
174
- #
175
- # This prevents multiple requests from trying to generate
176
- # through the same llama.cpp instance simultaneously.
177
 
178
- generation_lock = threading.Lock()
179
 
 
180
 
181
- # ============================================================
182
- # STARTUP INFORMATION
183
- # ============================================================
184
 
185
- print("=" * 70)
186
- print("Varsh LLM API READY")
187
- print("=" * 70)
 
 
 
188
 
189
- print(f"Listening on: http://{HOST}:{PORT}")
190
- print("Root endpoint: /")
191
- print("Health endpoint: /health")
192
- print("Models endpoint: /v1/models")
193
- print("Chat endpoint: /v1/chat/completions")
194
- print("Authentication: Bearer API key")
195
- print("Streaming: ENABLED")
196
- print("=" * 70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
 
199
  # ============================================================
@@ -201,10 +338,10 @@ print("=" * 70)
201
  # ============================================================
202
 
203
  @app.get("/")
204
- def root():
205
  return {
206
  "status": "healthy",
207
- "service": APP_NAME,
208
  "security": "enabled",
209
  "model": MODEL_ID,
210
  "endpoint": "/v1/chat/completions",
@@ -214,117 +351,30 @@ def root():
214
 
215
 
216
  # ============================================================
217
- # HEALTH
218
- # ============================================================
219
-
220
- @app.get("/health")
221
- def health():
222
- return {
223
- "status": "healthy",
224
- "service": APP_NAME,
225
- "model": MODEL_ID,
226
- }
227
-
228
-
229
- # ============================================================
230
- # OPENAI /v1/models
231
  # ============================================================
232
 
233
  @app.get(
234
  "/v1/models",
235
  dependencies=[Depends(verify_api_key)],
236
  )
237
- def list_models():
 
 
 
238
  return {
239
  "object": "list",
240
  "data": [
241
  {
242
  "id": MODEL_ID,
243
  "object": "model",
244
- "created": int(time.time()),
245
  "owned_by": "varsshancoder",
246
  }
247
  ],
248
  }
249
 
250
 
251
- # ============================================================
252
- # OPENAI /v1/models/{model_id}
253
- # ============================================================
254
-
255
- @app.get(
256
- "/v1/models/{model_id}",
257
- dependencies=[Depends(verify_api_key)],
258
- )
259
- def get_model(model_id: str):
260
-
261
- if model_id != MODEL_ID:
262
- raise HTTPException(
263
- status_code=404,
264
- detail=f"Model '{model_id}' not found.",
265
- )
266
-
267
- return {
268
- "id": MODEL_ID,
269
- "object": "model",
270
- "created": int(time.time()),
271
- "owned_by": "varsshancoder",
272
- }
273
-
274
-
275
- # ============================================================
276
- # MESSAGE VALIDATION
277
- # ============================================================
278
-
279
- def clean_messages(messages):
280
-
281
- if not isinstance(messages, list):
282
- raise HTTPException(
283
- status_code=400,
284
- detail="'messages' must be a list.",
285
- )
286
-
287
- cleaned = []
288
-
289
- for message in messages:
290
-
291
- if not isinstance(message, dict):
292
- raise HTTPException(
293
- status_code=400,
294
- detail="Each message must be an object.",
295
- )
296
-
297
- role = message.get("role", "user")
298
- content = message.get("content", "")
299
-
300
- if content is None:
301
- content = ""
302
-
303
- if not isinstance(content, str):
304
- raise HTTPException(
305
- status_code=400,
306
- detail=(
307
- "Message content must be a string. "
308
- "Multimodal content is not supported."
309
- ),
310
- )
311
-
312
- cleaned.append(
313
- {
314
- "role": role,
315
- "content": content,
316
- }
317
- )
318
-
319
- if not cleaned:
320
- raise HTTPException(
321
- status_code=400,
322
- detail="No messages were provided.",
323
- )
324
-
325
- return cleaned
326
-
327
-
328
  # ============================================================
329
  # CHAT COMPLETIONS
330
  # ============================================================
@@ -335,46 +385,36 @@ def clean_messages(messages):
335
  )
336
  async def chat_completions(request: Request):
337
 
338
- # --------------------------------------------------------
339
- # Parse JSON
340
- # --------------------------------------------------------
341
-
342
  try:
343
  body = await request.json()
344
-
345
  except Exception:
346
  raise HTTPException(
347
  status_code=400,
348
  detail="Invalid JSON request body.",
349
  )
350
 
351
- # --------------------------------------------------------
352
- # Model
353
- # --------------------------------------------------------
354
-
355
- requested_model = body.get(
356
- "model",
357
- MODEL_ID,
358
- )
359
-
360
- if requested_model != MODEL_ID:
361
-
362
  raise HTTPException(
363
- status_code=404,
364
- detail=f"Model '{requested_model}' not found.",
365
  )
366
 
367
- # --------------------------------------------------------
368
- # Messages
369
- # --------------------------------------------------------
370
 
371
- messages = clean_messages(
372
- body.get("messages")
 
373
  )
374
 
375
- # --------------------------------------------------------
376
- # Parameters
377
- # --------------------------------------------------------
 
 
 
 
378
 
379
  temperature = body.get(
380
  "temperature",
@@ -383,87 +423,76 @@ async def chat_completions(request: Request):
383
 
384
  max_tokens = body.get(
385
  "max_tokens",
386
- 1024,
387
- )
388
-
389
- top_p = body.get(
390
- "top_p",
391
- 0.95,
392
  )
393
 
394
- stop = body.get(
395
- "stop",
396
- None,
397
- )
398
-
399
- stream = body.get(
400
- "stream",
401
- False,
402
  )
403
 
404
- # --------------------------------------------------------
405
- # Optional seed
406
- # --------------------------------------------------------
407
-
408
- seed = body.get(
409
- "seed",
410
- None,
411
- )
412
 
413
- # --------------------------------------------------------
414
- # Request metadata
415
- # --------------------------------------------------------
 
416
 
417
- request_id = (
418
- "chatcmpl-"
419
- + uuid.uuid4().hex
420
- )
421
 
422
- created = int(
423
- time.time()
424
- )
425
 
426
- # ========================================================
427
- # STREAMING
428
- # ========================================================
429
 
430
- if stream:
 
431
 
432
- def generate_stream():
 
433
 
434
- start_time = time.perf_counter()
 
 
 
435
 
436
- try:
 
437
 
438
- # ------------------------------------------------
439
- # Lock model generation
440
- # ------------------------------------------------
441
 
442
- with generation_lock:
 
 
443
 
444
- stream_kwargs = {
445
- "messages": messages,
446
- "temperature": float(temperature),
447
- "max_tokens": int(max_tokens),
448
- "top_p": float(top_p),
449
- "stream": True,
450
- }
451
 
452
- if stop is not None:
453
- stream_kwargs["stop"] = stop
454
 
455
- if seed is not None:
456
- stream_kwargs["seed"] = int(seed)
457
 
458
- response_stream = (
459
- llm.create_chat_completion(
460
- **stream_kwargs
461
- )
 
 
 
 
 
462
  )
463
 
464
- first_chunk = True
465
-
466
- for chunk in response_stream:
467
 
468
  choices = chunk.get(
469
  "choices",
@@ -480,136 +509,51 @@ async def chat_completions(request: Request):
480
  {},
481
  )
482
 
483
- finish_reason = choice.get(
484
- "finish_reason",
485
- None,
486
- )
487
-
488
- # ----------------------------------------
489
- # First chunk
490
- # ----------------------------------------
491
-
492
- if first_chunk:
493
-
494
- first_chunk = False
495
-
496
- first_data = {
497
- "id": request_id,
498
- "object": "chat.completion.chunk",
499
- "created": created,
500
- "model": MODEL_ID,
501
- "choices": [
502
- {
503
- "index": 0,
504
- "delta": {
505
- "role": "assistant"
506
- },
507
- "finish_reason": None,
508
- }
509
- ],
510
- }
511
-
512
- yield (
513
- "data: "
514
- + json.dumps(
515
- first_data,
516
- ensure_ascii=False,
517
- )
518
- + "\n\n"
519
- )
520
-
521
- # ----------------------------------------
522
- # Content chunk
523
- # ----------------------------------------
524
-
525
  content = delta.get(
526
  "content",
527
  "",
528
  )
529
 
530
- if content:
531
-
532
- data = {
533
- "id": request_id,
534
- "object": "chat.completion.chunk",
535
- "created": created,
536
- "model": MODEL_ID,
537
- "choices": [
538
- {
539
- "index": 0,
540
- "delta": {
541
- "content": content
542
- },
543
- "finish_reason": None,
544
- }
545
- ],
546
- }
547
-
548
- yield (
549
- "data: "
550
- + json.dumps(
551
- data,
552
- ensure_ascii=False,
553
- )
554
- + "\n\n"
555
- )
556
 
557
- # ----------------------------------------
558
- # Finish reason
559
- # ----------------------------------------
560
-
561
- if finish_reason is not None:
562
-
563
- final_data = {
564
- "id": request_id,
565
- "object": "chat.completion.chunk",
566
- "created": created,
567
- "model": MODEL_ID,
568
- "choices": [
569
- {
570
- "index": 0,
571
- "delta": {},
572
- "finish_reason": finish_reason,
573
- }
574
- ],
575
- }
576
-
577
- yield (
578
- "data: "
579
- + json.dumps(
580
- final_data,
581
- ensure_ascii=False,
582
- )
583
- + "\n\n"
584
- )
585
 
586
- # ------------------------------------------------
587
- # DONE
588
- # ------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
  yield "data: [DONE]\n\n"
591
 
592
- elapsed = (
593
- time.perf_counter()
594
- - start_time
595
- )
596
-
597
- print(
598
- f"Stream {request_id} "
599
- f"completed in "
600
- f"{elapsed:.2f}s"
601
- )
602
-
603
- except Exception as e:
604
-
605
- print(
606
- f"Streaming error "
607
- f"{request_id}: {e}"
608
- )
609
 
610
- error_data = {
611
  "error": {
612
- "message": str(e),
613
  "type": "server_error",
614
  }
615
  }
@@ -617,16 +561,14 @@ async def chat_completions(request: Request):
617
  yield (
618
  "data: "
619
  + json.dumps(
620
- error_data,
621
  ensure_ascii=False,
622
  )
623
  + "\n\n"
624
  )
625
 
626
- yield "data: [DONE]\n\n"
627
-
628
  return StreamingResponse(
629
- generate_stream(),
630
  media_type="text/event-stream",
631
  headers={
632
  "Cache-Control": "no-cache",
@@ -635,149 +577,64 @@ async def chat_completions(request: Request):
635
  },
636
  )
637
 
638
- # ========================================================
639
- # NON-STREAMING
640
- # ========================================================
641
-
642
- start_time = time.perf_counter()
643
-
644
- try:
645
-
646
- with generation_lock:
647
-
648
- completion_kwargs = {
649
- "messages": messages,
650
- "temperature": float(temperature),
651
- "max_tokens": int(max_tokens),
652
- "top_p": float(top_p),
653
- "stream": False,
654
- }
655
-
656
- if stop is not None:
657
- completion_kwargs["stop"] = stop
658
-
659
- if seed is not None:
660
- completion_kwargs["seed"] = int(seed)
661
-
662
- response = (
663
- llm.create_chat_completion(
664
- **completion_kwargs
665
- )
666
- )
667
-
668
- except Exception as e:
669
-
670
- print(
671
- f"Inference error "
672
- f"{request_id}: {e}"
673
- )
674
-
675
- raise HTTPException(
676
- status_code=500,
677
- detail=f"Model inference failed: {str(e)}",
678
- )
679
-
680
- elapsed = (
681
- time.perf_counter()
682
- - start_time
683
- )
684
-
685
  # --------------------------------------------------------
686
- # Extract response
687
  # --------------------------------------------------------
688
 
689
  try:
690
 
691
- choice = response["choices"][0]
692
 
693
- message = choice.get(
694
- "message",
695
- {},
696
- )
 
 
 
697
 
698
- text_output = message.get(
699
- "content",
700
- "",
701
  )
702
 
703
- finish_reason = choice.get(
704
- "finish_reason",
705
- "stop",
706
  )
707
 
708
- except Exception as e:
709
 
710
  print(
711
- f"Response parsing error: {e}"
712
  )
713
 
714
  raise HTTPException(
715
  status_code=500,
716
- detail=(
717
- "Invalid response returned "
718
- "by the model."
719
- ),
720
  )
721
 
722
- # --------------------------------------------------------
723
- # Usage
724
- # --------------------------------------------------------
725
-
726
- usage = response.get(
727
- "usage",
728
- {},
729
- )
730
 
731
- prompt_tokens = usage.get(
732
- "prompt_tokens",
733
- 0,
734
- )
735
 
736
- completion_tokens = usage.get(
737
- "completion_tokens",
738
- 0,
739
- )
 
 
740
 
741
- total_tokens = usage.get(
742
- "total_tokens",
743
- prompt_tokens + completion_tokens,
744
- )
745
 
746
- print(
747
- f"Request {request_id} | "
748
- f"prompt={prompt_tokens} | "
749
- f"completion={completion_tokens} | "
750
- f"time={elapsed:.2f}s"
751
- )
752
 
753
- # --------------------------------------------------------
754
- # OpenAI-compatible response
755
- # --------------------------------------------------------
756
 
757
- return JSONResponse(
758
- content={
759
- "id": request_id,
760
- "object": "chat.completion",
761
- "created": created,
762
- "model": MODEL_ID,
763
- "choices": [
764
- {
765
- "index": 0,
766
- "message": {
767
- "role": "assistant",
768
- "content": text_output,
769
- },
770
- "finish_reason": finish_reason,
771
- }
772
- ],
773
- "usage": {
774
- "prompt_tokens": prompt_tokens,
775
- "completion_tokens": completion_tokens,
776
- "total_tokens": total_tokens,
777
- },
778
- "x_generation_time_seconds": round(
779
- elapsed,
780
- 3,
781
- ),
782
- }
783
  )
 
1
  import os
2
  import time
 
3
  import json
4
+ import uuid
5
+ import asyncio
6
+ from typing import Any
7
 
8
  from fastapi import FastAPI, Request, HTTPException, Depends
9
  from fastapi.middleware.cors import CORSMiddleware
 
19
  # ============================================================
20
 
21
  APP_NAME = "Varsh LLM"
22
+ MODEL_ID = "qwen2.5-coder-7b-instruct-q4_k_m"
23
 
24
  MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
25
  MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
26
 
 
 
 
27
  HOST = "0.0.0.0"
28
  PORT = int(os.getenv("PORT", "7860"))
29
 
30
+ # IMPORTANT:
31
+ # Add CUSTOM_API_KEY in:
32
+ # Hugging Face Space -> Settings -> Secrets
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  #
34
+ # Do NOT hard-code the key in this file.
 
 
 
 
 
 
35
  CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY")
36
 
37
  if not CUSTOM_API_KEY:
 
42
 
43
 
44
  # ============================================================
45
+ # FASTAPI
46
  # ============================================================
47
 
48
  app = FastAPI(
49
+ title=APP_NAME,
50
+ version="2.0.0",
 
51
  )
52
 
 
 
 
 
 
53
  app.add_middleware(
54
  CORSMiddleware,
55
  allow_origins=["*"],
56
+ allow_credentials=True,
57
  allow_methods=["*"],
58
  allow_headers=["*"],
59
  )
60
 
61
 
 
 
 
 
62
  security = HTTPBearer(auto_error=False)
63
 
64
 
 
68
  if credentials is None:
69
  raise HTTPException(
70
  status_code=401,
71
+ detail="Missing Authorization header.",
 
 
 
72
  )
73
 
74
  if credentials.scheme.lower() != "bearer":
75
  raise HTTPException(
76
  status_code=401,
77
+ detail="Authorization must use Bearer authentication.",
78
  )
79
 
80
  if credentials.credentials != CUSTOM_API_KEY:
 
83
  detail="Invalid API key.",
84
  )
85
 
86
+ return True
87
 
88
 
89
  # ============================================================
90
+ # MODEL
91
  # ============================================================
92
 
93
+ print("=" * 60)
94
  print("Starting Varsh LLM")
95
+ print("=" * 60)
96
+ print(f"Model repository: {MODEL_REPO}")
97
+ print(f"Model file: {MODEL_FILE}")
98
+ print("Downloading/checking model...")
 
 
 
 
 
 
99
 
100
  model_path = hf_hub_download(
101
  repo_id=MODEL_REPO,
 
103
  )
104
 
105
  print(f"Model path: {model_path}")
 
106
  print("Loading Qwen 2.5 Coder 7B...")
107
 
108
  llm = Llama(
109
  model_path=model_path,
110
 
111
+ # Your Space has 2 vCPUs / 16 GB RAM.
112
+ n_ctx=4096,
113
+ n_threads=2,
114
+ n_threads_batch=2,
 
115
 
116
+ # Moderate batch size for CPU deployment.
117
+ n_batch=256,
118
 
119
+ # Keep memory usage reasonable.
120
+ use_mmap=True,
121
+ use_mlock=False,
122
 
123
  verbose=False,
124
  )
125
 
126
  print("Model loaded successfully.")
127
+ print("=" * 60)
128
+ print("Varsh LLM API READY")
129
+ print("=" * 60)
130
+ print(f"Listening on: {HOST}:{PORT}")
131
+ print("Root endpoint: /")
132
+ print("Models endpoint: /v1/models")
133
+ print("Chat endpoint: /v1/chat/completions")
134
+ print("Authentication: Bearer API key")
135
+ print("=" * 60)
136
+
137
+
138
+ # llama.cpp generation should be serialized on a small CPU instance.
139
+ generation_lock = asyncio.Lock()
140
 
141
 
142
  # ============================================================
143
+ # HELPERS
144
  # ============================================================
145
 
146
+ def normalize_content(content: Any) -> str:
147
+ """
148
+ Converts OpenAI-style message content into plain text.
 
149
 
150
+ Supports:
151
 
152
+ "Hello"
153
 
154
+ and:
 
 
155
 
156
+ [
157
+ {
158
+ "type": "text",
159
+ "text": "Hello"
160
+ }
161
+ ]
162
 
163
+ Qwen2.5-Coder here is text-only, so image/audio parts
164
+ cannot be processed.
165
+ """
166
+
167
+ if content is None:
168
+ return ""
169
+
170
+ # Normal OpenAI text content
171
+ if isinstance(content, str):
172
+ return content
173
+
174
+ # OpenAI multimodal/content-part format
175
+ if isinstance(content, list):
176
+ text_parts = []
177
+
178
+ for part in content:
179
+ if isinstance(part, str):
180
+ text_parts.append(part)
181
+ continue
182
+
183
+ if not isinstance(part, dict):
184
+ continue
185
+
186
+ part_type = part.get("type")
187
+
188
+ # Standard text part
189
+ if part_type == "text":
190
+ text = part.get("text", "")
191
+ if isinstance(text, str):
192
+ text_parts.append(text)
193
+
194
+ # Some clients may use slightly different structures.
195
+ elif "text" in part and isinstance(part["text"], str):
196
+ text_parts.append(part["text"])
197
+
198
+ # Images/audio are intentionally not processed.
199
+ elif part_type in {
200
+ "image",
201
+ "image_url",
202
+ "input_image",
203
+ "audio",
204
+ "input_audio",
205
+ }:
206
+ continue
207
+
208
+ return "\n".join(text_parts)
209
+
210
+ # Fallback for unusual clients
211
+ return str(content)
212
+
213
+
214
+ def normalize_messages(messages: Any) -> list:
215
+ """
216
+ Normalize incoming OpenAI-compatible messages so that
217
+ llama.cpp receives string content.
218
+ """
219
+
220
+ if not isinstance(messages, list):
221
+ raise HTTPException(
222
+ status_code=400,
223
+ detail="'messages' must be an array.",
224
+ )
225
+
226
+ normalized = []
227
+
228
+ for message in messages:
229
+ if not isinstance(message, dict):
230
+ continue
231
+
232
+ role = message.get("role", "user")
233
+
234
+ if not isinstance(role, str):
235
+ role = "user"
236
+
237
+ content = normalize_content(
238
+ message.get("content", "")
239
+ )
240
+
241
+ new_message = {
242
+ "role": role,
243
+ "content": content,
244
+ }
245
+
246
+ # Preserve name if supplied.
247
+ if "name" in message:
248
+ new_message["name"] = message["name"]
249
+
250
+ # Preserve tool_call_id if supplied.
251
+ if "tool_call_id" in message:
252
+ new_message["tool_call_id"] = message["tool_call_id"]
253
+
254
+ # Preserve tool calls when clients send them.
255
+ if "tool_calls" in message:
256
+ new_message["tool_calls"] = message["tool_calls"]
257
+
258
+ normalized.append(new_message)
259
+
260
+ if not normalized:
261
+ raise HTTPException(
262
+ status_code=400,
263
+ detail="'messages' cannot be empty.",
264
+ )
265
+
266
+ return normalized
267
+
268
+
269
+ def create_chat_id() -> str:
270
+ return f"chatcmpl-{uuid.uuid4().hex}"
271
+
272
+
273
+ def create_timestamp() -> int:
274
+ return int(time.time())
275
+
276
+
277
+ def build_non_stream_response(
278
+ result: dict,
279
+ model_id: str,
280
+ ) -> dict:
281
+
282
+ choices = result.get("choices", [])
283
+
284
+ if not choices:
285
+ text = ""
286
+ finish_reason = "stop"
287
+ else:
288
+ choice = choices[0]
289
+
290
+ message = choice.get("message", {})
291
+
292
+ text = message.get("content", "")
293
+
294
+ if text is None:
295
+ text = ""
296
+
297
+ finish_reason = choice.get(
298
+ "finish_reason",
299
+ "stop",
300
+ )
301
+
302
+ usage = result.get("usage", {})
303
+
304
+ return {
305
+ "id": create_chat_id(),
306
+ "object": "chat.completion",
307
+ "created": create_timestamp(),
308
+ "model": model_id,
309
+ "choices": [
310
+ {
311
+ "index": 0,
312
+ "message": {
313
+ "role": "assistant",
314
+ "content": text,
315
+ },
316
+ "finish_reason": finish_reason,
317
+ }
318
+ ],
319
+ "usage": {
320
+ "prompt_tokens": usage.get(
321
+ "prompt_tokens",
322
+ 0,
323
+ ),
324
+ "completion_tokens": usage.get(
325
+ "completion_tokens",
326
+ 0,
327
+ ),
328
+ "total_tokens": usage.get(
329
+ "total_tokens",
330
+ 0,
331
+ ),
332
+ },
333
+ }
334
 
335
 
336
  # ============================================================
 
338
  # ============================================================
339
 
340
  @app.get("/")
341
+ async def root():
342
  return {
343
  "status": "healthy",
344
+ "service": "Varsh LLM",
345
  "security": "enabled",
346
  "model": MODEL_ID,
347
  "endpoint": "/v1/chat/completions",
 
351
 
352
 
353
  # ============================================================
354
+ # MODELS
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  # ============================================================
356
 
357
  @app.get(
358
  "/v1/models",
359
  dependencies=[Depends(verify_api_key)],
360
  )
361
+ async def models():
362
+
363
+ created = create_timestamp()
364
+
365
  return {
366
  "object": "list",
367
  "data": [
368
  {
369
  "id": MODEL_ID,
370
  "object": "model",
371
+ "created": created,
372
  "owned_by": "varsshancoder",
373
  }
374
  ],
375
  }
376
 
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  # ============================================================
379
  # CHAT COMPLETIONS
380
  # ============================================================
 
385
  )
386
  async def chat_completions(request: Request):
387
 
 
 
 
 
388
  try:
389
  body = await request.json()
 
390
  except Exception:
391
  raise HTTPException(
392
  status_code=400,
393
  detail="Invalid JSON request body.",
394
  )
395
 
396
+ if not isinstance(body, dict):
 
 
 
 
 
 
 
 
 
 
397
  raise HTTPException(
398
+ status_code=400,
399
+ detail="Request body must be a JSON object.",
400
  )
401
 
402
+ messages = normalize_messages(
403
+ body.get("messages", [])
404
+ )
405
 
406
+ requested_model = body.get(
407
+ "model",
408
+ MODEL_ID,
409
  )
410
 
411
+ # Accept the model exposed by /v1/models.
412
+ # Also tolerate a provider-prefixed model ID.
413
+ if requested_model not in {
414
+ MODEL_ID,
415
+ f"varsh/{MODEL_ID}",
416
+ }:
417
+ requested_model = MODEL_ID
418
 
419
  temperature = body.get(
420
  "temperature",
 
423
 
424
  max_tokens = body.get(
425
  "max_tokens",
426
+ 512,
 
 
 
 
 
427
  )
428
 
429
+ stream = bool(
430
+ body.get(
431
+ "stream",
432
+ False,
433
+ )
 
 
 
434
  )
435
 
436
+ # Keep values safe for the 2-vCPU deployment.
437
+ try:
438
+ temperature = float(temperature)
439
+ except Exception:
440
+ temperature = 0.2
 
 
 
441
 
442
+ try:
443
+ max_tokens = int(max_tokens)
444
+ except Exception:
445
+ max_tokens = 512
446
 
447
+ if max_tokens < 1:
448
+ max_tokens = 1
 
 
449
 
450
+ if max_tokens > 4096:
451
+ max_tokens = 4096
 
452
 
453
+ if temperature < 0:
454
+ temperature = 0
 
455
 
456
+ if temperature > 2:
457
+ temperature = 2
458
 
459
+ # Optional parameters supported by many OpenAI clients.
460
+ top_p = body.get("top_p", 0.95)
461
 
462
+ try:
463
+ top_p = float(top_p)
464
+ except Exception:
465
+ top_p = 0.95
466
 
467
+ if top_p <= 0:
468
+ top_p = 0.01
469
 
470
+ if top_p > 1:
471
+ top_p = 1.0
 
472
 
473
+ # --------------------------------------------------------
474
+ # Streaming
475
+ # --------------------------------------------------------
476
 
477
+ if stream:
 
 
 
 
 
 
478
 
479
+ chat_id = create_chat_id()
480
+ created = create_timestamp()
481
 
482
+ async def event_generator():
 
483
 
484
+ try:
485
+ async with generation_lock:
486
+
487
+ result_stream = llm.create_chat_completion(
488
+ messages=messages,
489
+ temperature=temperature,
490
+ top_p=top_p,
491
+ max_tokens=max_tokens,
492
+ stream=True,
493
  )
494
 
495
+ for chunk in result_stream:
 
 
496
 
497
  choices = chunk.get(
498
  "choices",
 
509
  {},
510
  )
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  content = delta.get(
513
  "content",
514
  "",
515
  )
516
 
517
+ if content is None:
518
+ content = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
+ finish_reason = choice.get(
521
+ "finish_reason"
522
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
+ payload = {
525
+ "id": chat_id,
526
+ "object": "chat.completion.chunk",
527
+ "created": created,
528
+ "model": requested_model,
529
+ "choices": [
530
+ {
531
+ "index": 0,
532
+ "delta": {
533
+ "role": "assistant",
534
+ "content": content,
535
+ },
536
+ "finish_reason": finish_reason,
537
+ }
538
+ ],
539
+ }
540
+
541
+ yield (
542
+ "data: "
543
+ + json.dumps(
544
+ payload,
545
+ ensure_ascii=False,
546
+ )
547
+ + "\n\n"
548
+ )
549
 
550
  yield "data: [DONE]\n\n"
551
 
552
+ except Exception as exc:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
 
554
+ error_payload = {
555
  "error": {
556
+ "message": str(exc),
557
  "type": "server_error",
558
  }
559
  }
 
561
  yield (
562
  "data: "
563
  + json.dumps(
564
+ error_payload,
565
  ensure_ascii=False,
566
  )
567
  + "\n\n"
568
  )
569
 
 
 
570
  return StreamingResponse(
571
+ event_generator(),
572
  media_type="text/event-stream",
573
  headers={
574
  "Cache-Control": "no-cache",
 
577
  },
578
  )
579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  # --------------------------------------------------------
581
+ # Non-streaming
582
  # --------------------------------------------------------
583
 
584
  try:
585
 
586
+ async with generation_lock:
587
 
588
+ result = llm.create_chat_completion(
589
+ messages=messages,
590
+ temperature=temperature,
591
+ top_p=top_p,
592
+ max_tokens=max_tokens,
593
+ stream=False,
594
+ )
595
 
596
+ response = build_non_stream_response(
597
+ result,
598
+ requested_model,
599
  )
600
 
601
+ return JSONResponse(
602
+ content=response
 
603
  )
604
 
605
+ except Exception as exc:
606
 
607
  print(
608
+ f"Generation error: {type(exc).__name__}: {exc}"
609
  )
610
 
611
  raise HTTPException(
612
  status_code=500,
613
+ detail=f"Generation failed: {str(exc)}",
 
 
 
614
  )
615
 
 
 
 
 
 
 
 
 
616
 
617
+ # ============================================================
618
+ # HEALTH CHECK
619
+ # ============================================================
 
620
 
621
+ @app.get("/health")
622
+ async def health():
623
+ return {
624
+ "status": "ok",
625
+ "model": MODEL_ID,
626
+ }
627
 
 
 
 
 
628
 
629
+ # ============================================================
630
+ # STARTUP
631
+ # ============================================================
 
 
 
632
 
633
+ if __name__ == "__main__":
634
+ import uvicorn
 
635
 
636
+ uvicorn.run(
637
+ app,
638
+ host=HOST,
639
+ port=PORT,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  )