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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -225
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
- import time
3
  import json
 
4
  import uuid
5
  import asyncio
6
  from typing import Any
@@ -15,10 +15,11 @@ from llama_cpp import Llama
15
 
16
 
17
  # ============================================================
18
- # CONFIGURATION
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"
@@ -27,27 +28,23 @@ MODEL_FILE = "qwen2.5-coder-7b-instruct-q4_k_m.gguf"
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:
38
  raise RuntimeError(
39
  "CUSTOM_API_KEY is not configured. "
40
- "Add CUSTOM_API_KEY in Hugging Face Space Settings > Secrets."
 
41
  )
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(
@@ -58,12 +55,16 @@ app.add_middleware(
58
  allow_headers=["*"],
59
  )
60
 
61
-
62
  security = HTTPBearer(auto_error=False)
63
 
64
 
 
 
 
 
65
  def verify_api_key(
66
- credentials: HTTPAuthorizationCredentials = Depends(security),
 
67
  ):
68
  if credentials is None:
69
  raise HTTPException(
@@ -90,12 +91,13 @@ def verify_api_key(
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,
@@ -108,15 +110,12 @@ print("Loading Qwen 2.5 Coder 7B...")
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
 
@@ -124,97 +123,131 @@ llm = Llama(
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):
@@ -223,114 +256,98 @@ def normalize_messages(messages: Any) -> list:
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
  # ============================================================
@@ -339,6 +356,7 @@ def build_non_stream_response(
339
 
340
  @app.get("/")
341
  async def root():
 
342
  return {
343
  "status": "healthy",
344
  "service": "Varsh LLM",
@@ -350,6 +368,19 @@ async def root():
350
  }
351
 
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  # ============================================================
354
  # MODELS
355
  # ============================================================
@@ -360,15 +391,13 @@ async def root():
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
  ],
@@ -383,10 +412,17 @@ async def models():
383
  "/v1/chat/completions",
384
  dependencies=[Depends(verify_api_key)],
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,
@@ -399,30 +435,57 @@ async def chat_completions(request: Request):
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",
 
 
 
 
 
 
 
421
  0.2,
422
  )
423
 
424
- max_tokens = body.get(
425
- "max_tokens",
 
 
 
 
 
 
 
 
 
 
 
426
  512,
427
  )
428
 
@@ -433,58 +496,87 @@ async def chat_completions(request: Request):
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,
@@ -492,7 +584,7 @@ async def chat_completions(request: Request):
492
  stream=True,
493
  )
494
 
495
- for chunk in result_stream:
496
 
497
  choices = chunk.get(
498
  "choices",
@@ -517,11 +609,12 @@ async def chat_completions(request: Request):
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,
@@ -530,10 +623,11 @@ async def chat_completions(request: Request):
530
  {
531
  "index": 0,
532
  "delta": {
533
- "role": "assistant",
534
  "content": content,
535
  },
536
- "finish_reason": finish_reason,
 
 
537
  }
538
  ],
539
  }
@@ -541,45 +635,91 @@ async def chat_completions(request: Request):
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
  }
560
 
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",
575
  "Connection": "keep-alive",
576
  "X-Accel-Buffering": "no",
577
  },
578
  )
579
 
580
- # --------------------------------------------------------
581
- # Non-streaming
582
- # --------------------------------------------------------
583
 
584
  try:
585
 
@@ -593,19 +733,78 @@ async def chat_completions(request: Request):
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(
@@ -615,22 +814,11 @@ async def chat_completions(request: Request):
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(
 
1
  import os
 
2
  import json
3
+ import time
4
  import uuid
5
  import asyncio
6
  from typing import Any
 
15
 
16
 
17
  # ============================================================
18
+ # CONFIG
19
  # ============================================================
20
 
21
  APP_NAME = "Varsh LLM"
22
+
23
  MODEL_ID = "qwen2.5-coder-7b-instruct-q4_k_m"
24
 
25
  MODEL_REPO = "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF"
 
28
  HOST = "0.0.0.0"
29
  PORT = int(os.getenv("PORT", "7860"))
30
 
 
 
 
 
 
31
  CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY")
32
 
33
  if not CUSTOM_API_KEY:
34
  raise RuntimeError(
35
  "CUSTOM_API_KEY is not configured. "
36
+ "Add CUSTOM_API_KEY in Hugging Face Space "
37
+ "Settings > Secrets."
38
  )
39
 
40
 
41
  # ============================================================
42
+ # APP
43
  # ============================================================
44
 
45
  app = FastAPI(
46
  title=APP_NAME,
47
+ version="3.0.0",
48
  )
49
 
50
  app.add_middleware(
 
55
  allow_headers=["*"],
56
  )
57
 
 
58
  security = HTTPBearer(auto_error=False)
59
 
60
 
61
+ # ============================================================
62
+ # AUTH
63
+ # ============================================================
64
+
65
  def verify_api_key(
66
+ credentials: HTTPAuthorizationCredentials =
67
+ Depends(security),
68
  ):
69
  if credentials is None:
70
  raise HTTPException(
 
91
  # MODEL
92
  # ============================================================
93
 
94
+ print("=" * 65)
95
  print("Starting Varsh LLM")
96
+ print("=" * 65)
97
+
98
  print(f"Model repository: {MODEL_REPO}")
99
  print(f"Model file: {MODEL_FILE}")
100
+ print("Checking/downloading model...")
101
 
102
  model_path = hf_hub_download(
103
  repo_id=MODEL_REPO,
 
110
  llm = Llama(
111
  model_path=model_path,
112
 
113
+ # 2 vCPU / 16 GB RAM Space
114
  n_ctx=4096,
115
  n_threads=2,
116
  n_threads_batch=2,
 
 
117
  n_batch=256,
118
 
 
119
  use_mmap=True,
120
  use_mlock=False,
121
 
 
123
  )
124
 
125
  print("Model loaded successfully.")
126
+
127
+ print("=" * 65)
128
+ print("VARSH LLM API READY")
129
+ print("=" * 65)
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("Streaming: enabled")
136
+ print("=" * 65)
137
+
138
+
139
+ # llama.cpp should not receive simultaneous generations
140
  generation_lock = asyncio.Lock()
141
 
142
 
143
  # ============================================================
144
+ # CONTENT NORMALIZATION
145
  # ============================================================
146
 
147
+ def content_to_text(content: Any) -> str:
148
  """
149
+ Convert OpenAI/Claude-style content into a plain string.
150
+
151
+ Examples:
152
+
153
+ "hello"
154
 
155
+ becomes:
156
 
157
+ "hello"
158
 
159
+ And:
160
 
161
  [
162
  {
163
  "type": "text",
164
+ "text": "hello"
165
  }
166
  ]
167
 
168
+ becomes:
169
+
170
+ "hello"
171
+
172
+ Image/audio blocks are ignored because this model is
173
+ text-only.
174
  """
175
 
176
  if content is None:
177
  return ""
178
 
179
+ # Normal string
180
  if isinstance(content, str):
181
  return content
182
 
183
+ # OpenAI content array
184
  if isinstance(content, list):
 
185
 
186
+ parts = []
187
+
188
+ for item in content:
189
+
190
+ if isinstance(item, str):
191
+ parts.append(item)
192
  continue
193
 
194
+ if not isinstance(item, dict):
195
  continue
196
 
197
+ item_type = item.get("type")
198
+
199
+ # Standard text block
200
+ if item_type == "text":
201
+ text = item.get("text")
202
 
 
 
 
203
  if isinstance(text, str):
204
+ parts.append(text)
205
+
206
+ continue
207
 
208
+ # Some clients may omit type
209
+ if isinstance(item.get("text"), str):
210
+ parts.append(item["text"])
211
 
212
+ continue
213
+
214
+ # Ignore image/audio content.
215
+ if item_type in (
216
  "image",
217
  "image_url",
218
  "input_image",
219
  "audio",
220
  "input_audio",
221
+ ):
222
  continue
223
 
224
+ return "\n".join(parts)
225
+
226
+ # Dict content
227
+ if isinstance(content, dict):
228
+
229
+ if isinstance(content.get("text"), str):
230
+ return content["text"]
231
+
232
+ if isinstance(content.get("content"), str):
233
+ return content["content"]
234
+
235
+ return json.dumps(
236
+ content,
237
+ ensure_ascii=False,
238
+ )
239
 
 
240
  return str(content)
241
 
242
 
243
+ # ============================================================
244
+ # MESSAGE NORMALIZATION
245
+ # ============================================================
246
+
247
  def normalize_messages(messages: Any) -> list:
248
  """
249
+ Convert all incoming messages into llama.cpp-safe
250
+ text-only messages.
251
  """
252
 
253
  if not isinstance(messages, list):
 
256
  detail="'messages' must be an array.",
257
  )
258
 
259
+ result = []
260
 
261
  for message in messages:
262
+
263
  if not isinstance(message, dict):
264
  continue
265
 
266
+ role = message.get(
267
+ "role",
268
+ "user",
269
+ )
270
 
271
  if not isinstance(role, str):
272
  role = "user"
273
 
274
+ content = content_to_text(
275
+ message.get(
276
+ "content",
277
+ "",
278
+ )
279
  )
280
 
281
+ # IMPORTANT:
282
+ # llama.cpp must receive string content.
283
+ normalized_message = {
284
  "role": role,
285
  "content": content,
286
  }
287
 
288
+ # Preserve name when present.
289
+ if isinstance(
290
+ message.get("name"),
291
+ str,
292
+ ):
293
+ normalized_message["name"] = (
294
+ message["name"]
295
+ )
296
 
297
+ # Preserve tool call ID if present.
298
+ if isinstance(
299
+ message.get("tool_call_id"),
300
+ str,
301
+ ):
302
+ normalized_message["tool_call_id"] = (
303
+ message["tool_call_id"]
304
+ )
305
 
306
+ result.append(
307
+ normalized_message
308
+ )
309
 
310
+ if not result:
311
  raise HTTPException(
312
  status_code=400,
313
  detail="'messages' cannot be empty.",
314
  )
315
 
316
+ return result
 
 
 
 
 
317
 
 
 
318
 
319
+ # ============================================================
320
+ # PARAMETERS
321
+ # ============================================================
322
 
323
+ def safe_float(
324
+ value: Any,
325
+ default: float,
326
+ ) -> float:
 
 
327
 
328
+ try:
329
+ return float(value)
330
+ except Exception:
331
+ return default
 
332
 
 
333
 
334
+ def safe_int(
335
+ value: Any,
336
+ default: int,
337
+ ) -> int:
338
 
339
+ try:
340
+ return int(value)
341
+ except Exception:
342
+ return default
343
 
 
 
 
 
344
 
345
+ # ============================================================
346
+ # ID
347
+ # ============================================================
348
 
349
+ def make_id():
350
+ return f"chatcmpl-{uuid.uuid4().hex}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
 
353
  # ============================================================
 
356
 
357
  @app.get("/")
358
  async def root():
359
+
360
  return {
361
  "status": "healthy",
362
  "service": "Varsh LLM",
 
368
  }
369
 
370
 
371
+ # ============================================================
372
+ # HEALTH
373
+ # ============================================================
374
+
375
+ @app.get("/health")
376
+ async def health():
377
+
378
+ return {
379
+ "status": "ok",
380
+ "model": MODEL_ID,
381
+ }
382
+
383
+
384
  # ============================================================
385
  # MODELS
386
  # ============================================================
 
391
  )
392
  async def models():
393
 
 
 
394
  return {
395
  "object": "list",
396
  "data": [
397
  {
398
  "id": MODEL_ID,
399
  "object": "model",
400
+ "created": int(time.time()),
401
  "owned_by": "varsshancoder",
402
  }
403
  ],
 
412
  "/v1/chat/completions",
413
  dependencies=[Depends(verify_api_key)],
414
  )
415
+ async def chat_completions(
416
+ request: Request,
417
+ ):
418
+
419
+ # --------------------------------------------------------
420
+ # Parse JSON
421
+ # --------------------------------------------------------
422
 
423
  try:
424
  body = await request.json()
425
+
426
  except Exception:
427
  raise HTTPException(
428
  status_code=400,
 
435
  detail="Request body must be a JSON object.",
436
  )
437
 
438
+ # --------------------------------------------------------
439
+ # Messages
440
+ # --------------------------------------------------------
441
+
442
  messages = normalize_messages(
443
+ body.get(
444
+ "messages",
445
+ [],
446
+ )
447
  )
448
 
449
+ # --------------------------------------------------------
450
+ # Model
451
+ # --------------------------------------------------------
452
+
453
  requested_model = body.get(
454
  "model",
455
  MODEL_ID,
456
  )
457
 
458
+ if requested_model in (
 
 
459
  MODEL_ID,
460
  f"varsh/{MODEL_ID}",
461
+ ):
462
  requested_model = MODEL_ID
463
 
464
+ # --------------------------------------------------------
465
+ # Parameters
466
+ # --------------------------------------------------------
467
+
468
+ temperature = safe_float(
469
+ body.get(
470
+ "temperature",
471
+ 0.2,
472
+ ),
473
  0.2,
474
  )
475
 
476
+ top_p = safe_float(
477
+ body.get(
478
+ "top_p",
479
+ 0.95,
480
+ ),
481
+ 0.95,
482
+ )
483
+
484
+ max_tokens = safe_int(
485
+ body.get(
486
+ "max_tokens",
487
+ 512,
488
+ ),
489
  512,
490
  )
491
 
 
496
  )
497
  )
498
 
499
+ # Reasonable limits for your CPU Space.
500
+ temperature = max(
501
+ 0.0,
502
+ min(
503
+ temperature,
504
+ 2.0,
505
+ ),
506
+ )
 
 
 
 
 
507
 
508
+ top_p = max(
509
+ 0.01,
510
+ min(
511
+ top_p,
512
+ 1.0,
513
+ ),
514
+ )
515
 
516
+ max_tokens = max(
517
+ 1,
518
+ min(
519
+ max_tokens,
520
+ 4096,
521
+ ),
522
+ )
523
 
524
+ # --------------------------------------------------------
525
+ # STREAMING
526
+ # --------------------------------------------------------
527
 
528
+ if stream:
 
529
 
530
+ chat_id = make_id()
531
+ created = int(time.time())
 
 
532
 
533
+ async def stream_generator():
 
534
 
535
+ # ------------------------------------------------
536
+ # FIRST EVENT
537
+ #
538
+ # Send this BEFORE model generation.
539
+ #
540
+ # This is important for Claude Code because the
541
+ # proxy/client must receive a real SSE event before
542
+ # waiting for the CPU model.
543
+ # ------------------------------------------------
544
 
545
+ first_chunk = {
546
+ "id": chat_id,
547
+ "object": "chat.completion.chunk",
548
+ "created": created,
549
+ "model": requested_model,
550
+ "choices": [
551
+ {
552
+ "index": 0,
553
+ "delta": {
554
+ "role": "assistant",
555
+ "content": "",
556
+ },
557
+ "finish_reason": None,
558
+ }
559
+ ],
560
+ }
561
 
562
+ yield (
563
+ "data: "
564
+ + json.dumps(
565
+ first_chunk,
566
+ ensure_ascii=False,
567
+ )
568
+ + "\n\n"
569
+ )
570
 
571
+ try:
 
572
 
573
+ # --------------------------------------------
574
+ # Generate
575
+ # --------------------------------------------
576
 
 
577
  async with generation_lock:
578
 
579
+ result = llm.create_chat_completion(
580
  messages=messages,
581
  temperature=temperature,
582
  top_p=top_p,
 
584
  stream=True,
585
  )
586
 
587
+ for chunk in result:
588
 
589
  choices = chunk.get(
590
  "choices",
 
609
  if content is None:
610
  content = ""
611
 
612
+ # ------------------------------------
613
+ # Every streamed event must be valid
614
+ # SSE JSON.
615
+ # ------------------------------------
616
 
617
+ output_chunk = {
618
  "id": chat_id,
619
  "object": "chat.completion.chunk",
620
  "created": created,
 
623
  {
624
  "index": 0,
625
  "delta": {
 
626
  "content": content,
627
  },
628
+ "finish_reason": choice.get(
629
+ "finish_reason"
630
+ ),
631
  }
632
  ],
633
  }
 
635
  yield (
636
  "data: "
637
  + json.dumps(
638
+ output_chunk,
639
  ensure_ascii=False,
640
  )
641
  + "\n\n"
642
  )
643
 
644
+ # --------------------------------------------
645
+ # Final chunk
646
+ # --------------------------------------------
647
+
648
+ final_chunk = {
649
+ "id": chat_id,
650
+ "object": "chat.completion.chunk",
651
+ "created": created,
652
+ "model": requested_model,
653
+ "choices": [
654
+ {
655
+ "index": 0,
656
+ "delta": {},
657
+ "finish_reason": "stop",
658
+ }
659
+ ],
660
+ }
661
+
662
+ yield (
663
+ "data: "
664
+ + json.dumps(
665
+ final_chunk,
666
+ ensure_ascii=False,
667
+ )
668
+ + "\n\n"
669
+ )
670
+
671
  yield "data: [DONE]\n\n"
672
 
673
  except Exception as exc:
674
 
675
+ print(
676
+ "STREAM GENERATION ERROR:",
677
+ repr(exc),
678
+ )
679
+
680
+ error_chunk = {
681
+ "id": chat_id,
682
+ "object": "chat.completion.chunk",
683
+ "created": created,
684
+ "model": requested_model,
685
+ "choices": [
686
+ {
687
+ "index": 0,
688
+ "delta": {
689
+ "content": (
690
+ f"\n[Generation error: "
691
+ f"{str(exc)}]"
692
+ ),
693
+ },
694
+ "finish_reason": "stop",
695
+ }
696
+ ],
697
  }
698
 
699
  yield (
700
  "data: "
701
  + json.dumps(
702
+ error_chunk,
703
  ensure_ascii=False,
704
  )
705
  + "\n\n"
706
  )
707
 
708
+ yield "data: [DONE]\n\n"
709
+
710
  return StreamingResponse(
711
+ stream_generator(),
712
  media_type="text/event-stream",
713
  headers={
714
+ "Cache-Control": "no-cache, no-transform",
715
  "Connection": "keep-alive",
716
  "X-Accel-Buffering": "no",
717
  },
718
  )
719
 
720
+ # ========================================================
721
+ # NON-STREAMING
722
+ # ========================================================
723
 
724
  try:
725
 
 
733
  stream=False,
734
  )
735
 
736
+ choices = result.get(
737
+ "choices",
738
+ [],
739
+ )
740
+
741
+ if choices:
742
+
743
+ choice = choices[0]
744
+
745
+ message = choice.get(
746
+ "message",
747
+ {},
748
+ )
749
+
750
+ output = message.get(
751
+ "content",
752
+ "",
753
+ )
754
+
755
+ finish_reason = choice.get(
756
+ "finish_reason",
757
+ "stop",
758
+ )
759
+
760
+ else:
761
+
762
+ output = ""
763
+ finish_reason = "stop"
764
+
765
+ usage = result.get(
766
+ "usage",
767
+ {},
768
  )
769
 
770
  return JSONResponse(
771
+ content={
772
+ "id": make_id(),
773
+ "object": "chat.completion",
774
+ "created": int(time.time()),
775
+ "model": requested_model,
776
+ "choices": [
777
+ {
778
+ "index": 0,
779
+ "message": {
780
+ "role": "assistant",
781
+ "content": output,
782
+ },
783
+ "finish_reason": finish_reason,
784
+ }
785
+ ],
786
+ "usage": {
787
+ "prompt_tokens": usage.get(
788
+ "prompt_tokens",
789
+ 0,
790
+ ),
791
+ "completion_tokens": usage.get(
792
+ "completion_tokens",
793
+ 0,
794
+ ),
795
+ "total_tokens": usage.get(
796
+ "total_tokens",
797
+ 0,
798
+ ),
799
+ },
800
+ }
801
  )
802
 
803
  except Exception as exc:
804
 
805
  print(
806
+ "GENERATION ERROR:",
807
+ repr(exc),
808
  )
809
 
810
  raise HTTPException(
 
814
 
815
 
816
  # ============================================================
817
+ # MAIN
 
 
 
 
 
 
 
 
 
 
 
 
818
  # ============================================================
819
 
820
  if __name__ == "__main__":
821
+
822
  import uvicorn
823
 
824
  uvicorn.run(