Ephraimmm commited on
Commit
4bf38c0
·
verified ·
1 Parent(s): 4208f8c

Update handler.py

Browse files
Files changed (1) hide show
  1. handler.py +77 -868
handler.py CHANGED
@@ -1,194 +1,18 @@
1
  #tope_version
2
  # handler.py
3
  # handler.py
4
- # from typing import Any, Dict, List, Union, Optional
5
- # import torch
6
- # from huggingface_inference_toolkit.logging import logger
7
- # from unsloth import FastLanguageModel
8
- # from peft import PeftModel
9
-
10
- # # OpenAI-style prompt:
11
- # # - str
12
- # # - [{"role": "...", "content": "..." | [{"type":"text","text":"..."}]}]
13
- # Prompt = Union[str, List[Dict[str, Any]], Dict[str, Any]]
14
-
15
-
16
- # def _to_block_list(content: Any) -> List[Dict[str, str]]:
17
- # """Ensure content is a list[{'type':'text','text':...}]."""
18
- # if content is None:
19
- # return [{"type": "text", "text": ""}]
20
- # if isinstance(content, list):
21
- # # Assume already blocks
22
- # return content
23
- # # Fallback: stringify
24
- # return [{"type": "text", "text": str(content)}]
25
-
26
-
27
- # class EndpointHandler:
28
- # """
29
- # HF Inference Endpoint handler that mirrors your Gradio setup:
30
- # - Loads Unsloth 4-bit Gemma-3-4B IT
31
- # - Attaches LoRA adapter
32
- # - Uses tokenizer.apply_chat_template
33
- # - Always replies in Nigerian Pidgin
34
- # """
35
-
36
- # def __init__(self, model_dir: str, **_: Any):
37
- # logger.info(f"Initializing with model_dir={model_dir}")
38
-
39
- # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
40
- # model, tokenizer = FastLanguageModel.from_pretrained(
41
- # model_name=base_model_name,
42
- # max_seq_length=2048,
43
- # dtype=torch.float16,
44
- # load_in_4bit=True,
45
- # )
46
-
47
- # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
48
- # model = PeftModel.from_pretrained(model, lora_repo)
49
- # FastLanguageModel.for_inference(model)
50
-
51
- # self.model = model.eval()
52
- # self.tokenizer = tokenizer
53
- # self.device = getattr(
54
- # self.model,
55
- # "device",
56
- # torch.device("cuda" if torch.cuda.is_available() else "cpu"),
57
- # )
58
- # # Safety: some tokenizers lack pad_token_id
59
- # if getattr(self.tokenizer, "pad_token_id", None) is None:
60
- # self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
61
-
62
- # logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
63
-
64
- # self._system_text = (
65
- # "You are a Nigerian assistant that speaks PIDGIN ENGLISH. "
66
- # "When asked 'how far', reply 'I dey o, how you dey?'. "
67
- # "Always answer in Pidgin English."
68
- # )
69
-
70
- # def _normalize_messages(self, prompt: Prompt) -> List[Dict[str, Any]]:
71
- # """Return a list of messages with block-style contents only."""
72
- # # Accept single dict
73
- # if isinstance(prompt, dict):
74
- # prompt = [prompt]
75
-
76
- # # Accept raw string
77
- # if isinstance(prompt, str):
78
- # msgs = [
79
- # {"role": "system", "content": _to_block_list(self._system_text)},
80
- # {"role": "user", "content": _to_block_list(prompt)},
81
- # ]
82
- # return msgs
83
-
84
- # if not isinstance(prompt, list):
85
- # raise ValueError("`inputs` must be a string, a message dict, or a list of messages.")
86
-
87
- # # Normalize all contents to blocks
88
- # norm = []
89
- # for m in prompt:
90
- # role = m.get("role", "user")
91
- # content = _to_block_list(m.get("content", ""))
92
- # norm.append({"role": role, "content": content})
93
-
94
- # # Ensure a system message exists (prepend if missing)
95
- # if not any(m.get("role") == "system" for m in norm):
96
- # norm = [{"role": "system", "content": _to_block_list(self._system_text)}] + norm
97
-
98
- # return norm
99
-
100
- # def _build_model_inputs(self, prompt: Prompt):
101
- # messages = self._normalize_messages(prompt)
102
- # # Use the same path as your Gradio script
103
- # model_inputs = self.tokenizer.apply_chat_template(
104
- # messages,
105
- # add_generation_prompt=True,
106
- # return_tensors="pt",
107
- # tokenize=True,
108
- # return_dict=True,
109
- # ).to(self.device)
110
- # return model_inputs
111
-
112
- # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
113
- # logger.info(f"Incoming keys: {list(data.keys())}")
114
- # if "inputs" not in data:
115
- # raise ValueError("Missing `inputs` in request body.")
116
-
117
- # prompt: Prompt = data["inputs"]
118
- # params: Dict[str, Any] = (data.get("parameters") or {})
119
-
120
- # # Defaults aligned with your Gradio code
121
- # max_new_tokens = int(params.get("max_new_tokens", 256)) # raise per request if needed
122
- # temperature = float(params.get("temperature", 0.1))
123
- # top_p = float(params.get("top_p", 1.0))
124
- # top_k: Optional[int] = params.get("top_k", None)
125
- # use_cache = bool(params.get("use_cache", False))
126
- # repetition_penalty = float(params.get("repetition_penalty", 1.0))
127
- # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
128
- # pad_token_id = params.get("pad_token_id", self.tokenizer.pad_token_id)
129
-
130
- # inputs = self._build_model_inputs(prompt)
131
-
132
- # gen_kwargs = dict(
133
- # **inputs,
134
- # max_new_tokens=max_new_tokens,
135
- # temperature=temperature,
136
- # top_p=top_p,
137
- # do_sample=True, # streaming-like sampling on
138
- # use_cache=use_cache,
139
- # repetition_penalty=repetition_penalty,
140
- # eos_token_id=eos_token_id,
141
- # pad_token_id=pad_token_id,
142
- # )
143
- # if top_k is not None:
144
- # gen_kwargs["top_k"] = int(top_k)
145
-
146
- # with torch.no_grad():
147
- # output_ids = self.model.generate(**gen_kwargs)
148
-
149
- # # Return ONLY the continuation (like your streamer)
150
- # input_len = inputs["input_ids"].shape[-1]
151
- # generated_ids = output_ids[0, input_len:]
152
- # reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
153
-
154
- # return {
155
- # "reply": [reply_text],
156
- # "usage": {
157
- # "prompt_tokens": int(input_len),
158
- # "generated_tokens": int(generated_ids.shape[-1]),
159
- # },
160
- # }
161
-
162
-
163
- # if __name__ == "__main__":
164
- # h = EndpointHandler(model_dir=".")
165
- # # Raw string
166
- # print(h({"inputs": "How far?", "parameters": {"max_new_tokens": 64}}))
167
- # # Block-style content (your Gradio shape)
168
- # print(h({
169
- # "inputs": [
170
- # {
171
- # "role": "user",
172
- # "content": [{"type": "text", "text": "Explain why sky dey blue."}]
173
- # }
174
- # ],
175
- # "parameters": {"max_new_tokens": 64}
176
- # }))
177
-
178
- #openai api
179
  from typing import Any, Dict, List, Union, Optional
180
  import torch
181
- import time
182
  from huggingface_inference_toolkit.logging import logger
183
  from unsloth import FastLanguageModel
184
  from peft import PeftModel
185
-
186
  # OpenAI-style prompt:
187
  # - str
188
  # - [{"role": "...", "content": "..." | [{"type":"text","text":"..."}]}]
189
  Prompt = Union[str, List[Dict[str, Any]], Dict[str, Any]]
190
-
191
-
192
  def _to_block_list(content: Any) -> List[Dict[str, str]]:
193
  """Ensure content is a list[{'type':'text','text':...}]."""
194
  if content is None:
@@ -198,22 +22,20 @@ def _to_block_list(content: Any) -> List[Dict[str, str]]:
198
  return content
199
  # Fallback: stringify
200
  return [{"type": "text", "text": str(content)}]
201
-
202
-
203
  class EndpointHandler:
204
  """
205
- HF Inference Endpoint handler that mirrors OpenAI API format:
206
- - Accepts OpenAI-style requests with 'messages', 'model', etc.
207
- - Returns OpenAI-style responses with 'choices', 'usage', etc.
208
  - Loads Unsloth 4-bit Gemma-3-4B IT
209
  - Attaches LoRA adapter
210
  - Uses tokenizer.apply_chat_template
211
  - Always replies in Nigerian Pidgin
212
  """
213
-
214
  def __init__(self, model_dir: str, **_: Any):
215
  logger.info(f"Initializing with model_dir={model_dir}")
216
-
217
  base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
218
  model, tokenizer = FastLanguageModel.from_pretrained(
219
  model_name=base_model_name,
@@ -221,11 +43,11 @@ class EndpointHandler:
221
  dtype=torch.float16,
222
  load_in_4bit=True,
223
  )
224
-
225
  lora_repo = "Ephraimmm/PIDGIN_gemma-3"
226
  model = PeftModel.from_pretrained(model, lora_repo)
227
  FastLanguageModel.for_inference(model)
228
-
229
  self.model = model.eval()
230
  self.tokenizer = tokenizer
231
  self.device = getattr(
@@ -236,143 +58,83 @@ class EndpointHandler:
236
  # Safety: some tokenizers lack pad_token_id
237
  if getattr(self.tokenizer, "pad_token_id", None) is None:
238
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
239
-
240
  logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
241
-
242
  self._system_text = (
243
  "You are a Nigerian assistant that speaks PIDGIN ENGLISH. "
244
  "When asked 'how far', reply 'I dey o, how you dey?'. "
245
  "Always answer in Pidgin English."
246
  )
247
-
248
- def _normalize_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
249
- """Normalize OpenAI-style messages to block-style contents."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  norm = []
251
- for m in messages:
252
  role = m.get("role", "user")
253
- content = m.get("content", "")
254
-
255
- # Convert content to block list format
256
- if isinstance(content, str):
257
- content = _to_block_list(content)
258
- elif isinstance(content, list):
259
- # Already in block format or needs conversion
260
- content = _to_block_list(content)
261
- else:
262
- content = _to_block_list(str(content))
263
-
264
  norm.append({"role": role, "content": content})
265
-
266
  # Ensure a system message exists (prepend if missing)
267
  if not any(m.get("role") == "system" for m in norm):
268
  norm = [{"role": "system", "content": _to_block_list(self._system_text)}] + norm
269
-
270
  return norm
271
-
272
- def _build_model_inputs(self, messages: List[Dict[str, Any]]):
273
- normalized_messages = self._normalize_messages(messages)
 
274
  model_inputs = self.tokenizer.apply_chat_template(
275
- normalized_messages,
276
  add_generation_prompt=True,
277
  return_tensors="pt",
278
  tokenize=True,
279
  return_dict=True,
280
  ).to(self.device)
281
  return model_inputs
282
-
283
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
284
- """
285
- Process OpenAI-style requests and return OpenAI-style responses.
286
-
287
- Input format (OpenAI API):
288
- {
289
- "messages": [{"role": "user", "content": "Hello"}],
290
- "model": "gpt-3.5-turbo", # optional
291
- "temperature": 0.7, # optional
292
- "max_tokens": 256, # optional
293
- "top_p": 1.0, # optional
294
- ...
295
- }
296
-
297
- Output format (OpenAI API):
298
- {
299
- "id": "chatcmpl-...",
300
- "object": "chat.completion",
301
- "created": 1234567890,
302
- "model": "gemma-3-4b-pidgin",
303
- "choices": [{
304
- "index": 0,
305
- "message": {
306
- "role": "assistant",
307
- "content": "Response text"
308
- },
309
- "finish_reason": "stop"
310
- }],
311
- "usage": {
312
- "prompt_tokens": 10,
313
- "completion_tokens": 20,
314
- "total_tokens": 30
315
- }
316
- }
317
- """
318
  logger.info(f"Incoming keys: {list(data.keys())}")
319
-
320
- # Support both OpenAI format ('messages') and legacy format ('inputs')
321
- if "messages" in data:
322
- messages = data["messages"]
323
- if not isinstance(messages, list):
324
- raise ValueError("`messages` must be a list of message objects.")
325
- elif "inputs" in data:
326
- # Legacy support: convert inputs to messages format
327
- prompt = data["inputs"]
328
- if isinstance(prompt, str):
329
- messages = [{"role": "user", "content": prompt}]
330
- elif isinstance(prompt, dict):
331
- messages = [prompt]
332
- elif isinstance(prompt, list):
333
- messages = prompt
334
- else:
335
- raise ValueError("Invalid `inputs` format.")
336
- else:
337
- raise ValueError("Missing `messages` or `inputs` in request body.")
338
-
339
- # Extract parameters (support both OpenAI and custom parameter names)
340
- params: Dict[str, Any] = data.get("parameters", {})
341
-
342
- # Map OpenAI parameters to model parameters
343
- max_new_tokens = int(
344
- params.get("max_new_tokens") or
345
- data.get("max_tokens") or
346
- params.get("max_tokens") or
347
- 256
348
- )
349
- temperature = float(
350
- params.get("temperature") or
351
- data.get("temperature") or
352
- 0.1
353
- )
354
- top_p = float(
355
- params.get("top_p") or
356
- data.get("top_p") or
357
- 1.0
358
- )
359
- top_k: Optional[int] = (
360
- params.get("top_k") or
361
- data.get("top_k")
362
- )
363
  use_cache = bool(params.get("use_cache", False))
364
  repetition_penalty = float(params.get("repetition_penalty", 1.0))
365
  eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
366
  pad_token_id = params.get("pad_token_id", self.tokenizer.pad_token_id)
367
-
368
- inputs = self._build_model_inputs(messages)
369
-
370
  gen_kwargs = dict(
371
  **inputs,
372
  max_new_tokens=max_new_tokens,
373
  temperature=temperature,
374
  top_p=top_p,
375
- do_sample=True,
376
  use_cache=use_cache,
377
  repetition_penalty=repetition_penalty,
378
  eos_token_id=eos_token_id,
@@ -380,588 +142,35 @@ class EndpointHandler:
380
  )
381
  if top_k is not None:
382
  gen_kwargs["top_k"] = int(top_k)
383
-
384
  with torch.no_grad():
385
  output_ids = self.model.generate(**gen_kwargs)
386
-
387
- # Extract only the generated tokens (exclude prompt)
388
  input_len = inputs["input_ids"].shape[-1]
389
  generated_ids = output_ids[0, input_len:]
390
  reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
391
-
392
- # Calculate tokens
393
- prompt_tokens = int(input_len)
394
- completion_tokens = int(generated_ids.shape[-1])
395
- total_tokens = prompt_tokens + completion_tokens
396
-
397
- # Return OpenAI-style response
398
- response = {
399
- "id": f"chatcmpl-{int(time.time())}",
400
- "object": "chat.completion",
401
- "created": int(time.time()),
402
- "model": data.get("model", "gemma-3-4b-pidgin"),
403
- "choices": [
404
- {
405
- "index": 0,
406
- "message": {
407
- "role": "assistant",
408
- "content": reply_text
409
- },
410
- "finish_reason": "stop"
411
- }
412
- ],
413
  "usage": {
414
- "prompt_tokens": prompt_tokens,
415
- "completion_tokens": completion_tokens,
416
- "total_tokens": total_tokens
417
- }
418
  }
419
-
420
- return response
421
-
422
-
423
  if __name__ == "__main__":
424
  h = EndpointHandler(model_dir=".")
425
-
426
- # Test with OpenAI-style format
427
- print("=== OpenAI Format Test ===")
428
- response = h({
429
- "messages": [
430
- {"role": "user", "content": "How far?"}
431
- ],
432
- "temperature": 0.7,
433
- "max_tokens": 64
434
- })
435
- print(response)
436
-
437
- print("\n=== OpenAI Format with System Message ===")
438
- response = h({
439
- "messages": [
440
- {"role": "system", "content": "You are a helpful assistant."},
441
- {"role": "user", "content": "Explain why sky dey blue."}
442
  ],
443
- "max_tokens": 64
444
- })
445
- print(response)
446
-
447
- # Legacy format still works
448
- print("\n=== Legacy Format Test ===")
449
- response = h({
450
- "inputs": "How far?",
451
  "parameters": {"max_new_tokens": 64}
452
- })
453
- print(response)
454
-
455
-
456
-
457
- # from typing import Any, Dict, List, Union, Optional
458
-
459
- # import torch
460
- # from huggingface_inference_toolkit.logging import logger
461
- # from unsloth import FastLanguageModel
462
- # from peft import PeftModel
463
-
464
- # Prompt = Union[str, List[Dict[str, str]]]
465
-
466
-
467
- # class EndpointHandler:
468
- # """
469
- # Hugging Face Inference Endpoint handler.
470
- # - Loads Unsloth 4-bit base
471
- # - Attaches LoRA adapter
472
- # - Uses tokenizer.apply_chat_template like your Gradio app
473
- # - Always injects the Pidgin-English system instruction
474
- # """
475
-
476
- # def __init__(self, model_dir: str, **_: Any):
477
- # logger.info(f"Initializing with model_dir={model_dir}")
478
-
479
- # # --- Base model (4-bit) ---
480
- # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
481
- # model, tokenizer = FastLanguageModel.from_pretrained(
482
- # model_name=base_model_name,
483
- # max_seq_length=2048,
484
- # dtype=torch.float16,
485
- # load_in_4bit=True,
486
- # )
487
-
488
- # # --- LoRA adapter ---
489
- # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
490
- # model = PeftModel.from_pretrained(model, lora_repo) # default adapter
491
- # FastLanguageModel.for_inference(model)
492
-
493
- # self.model = model.eval()
494
- # self.tokenizer = tokenizer
495
- # self.device = getattr(self.model, "device",
496
- # torch.device("cuda" if torch.cuda.is_available() else "cpu"))
497
- # logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
498
-
499
- # # System prompt copied from your Gradio app
500
- # self._system_text = (
501
- # "You are a Nigerian assistant that speaks PIDGIN ENGLISH."
502
- # " when asked how far reply I de o, how you de"
503
- # )
504
-
505
- # # -----------------------------
506
- # # Input preparation
507
- # # -----------------------------
508
- # def _ensure_messages(self, prompt: Prompt) -> List[Dict[str, str]]:
509
- # """Normalize to a list of messages and inject the system instruction."""
510
- # sys_msg = {"role": "system", "content": self._system_text}
511
- # if isinstance(prompt, str):
512
- # return [sys_msg, {"role": "user", "content": prompt}]
513
- # if isinstance(prompt, list):
514
- # has_system = any(m.get("role") == "system" for m in prompt)
515
- # return ([sys_msg] + prompt) if not has_system else prompt
516
- # raise ValueError("`inputs` must be a string or a list of {role, content} messages.")
517
-
518
- # def _build_model_inputs(self, prompt: Prompt):
519
- # """
520
- # Use tokenizer.apply_chat_template exactly like your Gradio code:
521
- # add_generation_prompt=True, return_tensors='pt', tokenize=True.
522
- # """
523
- # messages = self._ensure_messages(prompt)
524
-
525
- # # Some tokenizers support content as either str or structured content.
526
- # # We pass OpenAI-style messages; Unsloth/Gemma-3 chat templates handle it.
527
- # model_inputs = self.tokenizer.apply_chat_template(
528
- # messages,
529
- # add_generation_prompt=True,
530
- # return_tensors="pt",
531
- # tokenize=True,
532
- # return_dict=True,
533
- # ).to(self.device)
534
-
535
- # return model_inputs
536
-
537
- # # -----------------------------
538
- # # Generation
539
- # # -----------------------------
540
- # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
541
- # """
542
- # Expects: {"inputs": <str|messages>, "parameters": {...}}
543
- # Mirrors your Gradio defaults (temperature=0.1, top_p=1.0, top_k=None, use_cache=False).
544
- # """
545
- # logger.info(f"Incoming keys: {list(data.keys())}")
546
- # if "inputs" not in data:
547
- # raise ValueError("Missing `inputs` in request body.")
548
-
549
- # prompt: Prompt = data["inputs"]
550
- # params: Dict[str, Any] = (data.get("parameters") or {})
551
-
552
- # # Map your defaults
553
- # max_new_tokens = int(params.get("max_new_tokens", 256)) # safer default than 9000
554
- # temperature = float(params.get("temperature", 0.1))
555
- # top_p = float(params.get("top_p", 1.0))
556
- # top_k: Optional[int] = params.get("top_k", None)
557
- # use_cache = bool(params.get("use_cache", False))
558
- # repetition_penalty = float(params.get("repetition_penalty", 1.0))
559
- # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
560
- # pad_token_id = params.get("pad_token_id", getattr(self.tokenizer, "pad_token_id", None))
561
-
562
- # # Prepare inputs via chat template (like your Gradio code)
563
- # inputs = self._build_model_inputs(prompt)
564
-
565
- # gen_kwargs = dict(
566
- # **inputs,
567
- # max_new_tokens=max_new_tokens,
568
- # temperature=temperature,
569
- # top_p=top_p,
570
- # do_sample=True, # sampling on (like your streamer setup)
571
- # use_cache=use_cache,
572
- # repetition_penalty=repetition_penalty,
573
- # eos_token_id=eos_token_id,
574
- # )
575
- # if pad_token_id is not None:
576
- # gen_kwargs["pad_token_id"] = pad_token_id
577
- # if top_k is not None:
578
- # gen_kwargs["top_k"] = int(top_k)
579
-
580
- # with torch.no_grad():
581
- # output_ids = self.model.generate(**gen_kwargs)
582
-
583
- # # Return only the continuation (like what streaming would show)
584
- # input_len = inputs["input_ids"].shape[-1]
585
- # generated_ids = output_ids[0, input_len:]
586
- # reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
587
-
588
- # return {
589
- # "reply": [reply_text],
590
- # "usage": {
591
- # "prompt_tokens": int(input_len),
592
- # "generated_tokens": int(generated_ids.shape[-1]),
593
- # },
594
- # }
595
-
596
-
597
- # # Local quick test
598
- # if __name__ == "__main__":
599
- # h = EndpointHandler(model_dir=".")
600
-
601
- # # Raw string
602
- # out1 = h({
603
- # "inputs": "How far?",
604
- # "parameters": {"max_new_tokens": 64}
605
- # })
606
- # print(">> STRING:", out1["reply"][0])
607
-
608
- # # Chat-style messages
609
- # out2 = h({
610
- # "inputs": [
611
- # {"role": "user", "content": "Explain why sky dey blue."}
612
- # ],
613
- # "parameters": {"max_new_tokens": 64}
614
- # })
615
- # print(">> CHAT:", out2["reply"][0])
616
-
617
-
618
-
619
- # # handler.py
620
-
621
- # from typing import Any, Dict, List, Union
622
-
623
- # import torch
624
- # from huggingface_inference_toolkit.logging import logger
625
- # from unsloth import FastLanguageModel
626
- # from peft import PeftModel
627
-
628
- # Prompt = Union[str, List[Dict[str, str]]]
629
-
630
-
631
- # class EndpointHandler:
632
- # """
633
- # Custom handler for Hugging Face Inference Endpoints.
634
- # Loads an Unsloth 4-bit base and attaches a LoRA adapter.
635
- # """
636
-
637
- # def __init__(self, model_dir: str, **_: Any):
638
- # """
639
- # Called once at startup. `model_dir` is the downloaded repo path.
640
- # """
641
- # logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
642
-
643
- # # 1) Load Unsloth 4-bit base
644
- # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
645
- # model, tokenizer = FastLanguageModel.from_pretrained(
646
- # model_name=base_model_name,
647
- # max_seq_length=2048,
648
- # dtype=torch.float16,
649
- # load_in_4bit=True,
650
- # )
651
-
652
- # # 2) Attach LoRA adapter
653
- # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
654
- # model = PeftModel.from_pretrained(model, lora_repo) # default adapter
655
- # FastLanguageModel.for_inference(model) # optimize for generation
656
-
657
- # # 3) Finalize
658
- # self.model = model.eval()
659
- # self.tokenizer = tokenizer
660
- # self.device = getattr(
661
- # self.model,
662
- # "device",
663
- # torch.device("cuda" if torch.cuda.is_available() else "cpu"),
664
- # )
665
- # logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
666
-
667
- # def _build_input_ids(self, prompt: Prompt):
668
- # """
669
- # Accepts either a single string, or OpenAI-style chat messages:
670
- # [{"role":"system|user|assistant","content":"..."}]
671
- # Always injects system prompt for Pidgin English.
672
- # """
673
- # system_instruction = {
674
- # "role": "system",
675
- # "content": "Always reply in Pidgin English, answer in context of question."
676
- # }
677
-
678
- # if isinstance(prompt, list):
679
- # # Ensure system prompt is first
680
- # if not any(m.get("role") == "system" for m in prompt):
681
- # prompt = [system_instruction] + prompt
682
-
683
- # if hasattr(self.tokenizer, "apply_chat_template"):
684
- # text = self.tokenizer.apply_chat_template(
685
- # prompt, tokenize=False, add_generation_prompt=True
686
- # )
687
- # else:
688
- # # Fallback concat
689
- # text = "\n".join(
690
- # f"{m.get('role','user')}: {m.get('content','')}"
691
- # for m in prompt
692
- # ) + "\nassistant:"
693
- # else:
694
- # # If it's just a raw string, prepend system message
695
- # text = f"System: {system_instruction['content']}\nUser: {prompt}\nAssistant:"
696
-
697
- # return self.tokenizer(text, return_tensors="pt").to(self.device), text
698
-
699
- # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
700
- # """
701
- # Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
702
- # """
703
- # logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
704
-
705
- # if "inputs" not in data:
706
- # raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
707
-
708
- # prompt: Prompt = data["inputs"]
709
- # params = data.get("parameters", {}) or {}
710
-
711
- # max_new_tokens = int(params.get("max_new_tokens", 64))
712
- # temperature = float(params.get("temperature", 1))
713
- # top_k = int(params.get("top_k", 64))
714
- # top_p = float(params.get("top_p", 0.95))
715
- # do_sample = bool(params.get("do_sample", True))
716
- # repetition_penalty = float(params.get("repetition_penalty", 1.1))
717
- # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
718
-
719
- # inputs, rendered_prompt = self._build_input_ids(prompt)
720
-
721
- # with torch.no_grad():
722
- # output_ids = self.model.generate(
723
- # **inputs,
724
- # max_new_tokens=max_new_tokens,
725
- # temperature=temperature,
726
- # top_k=top_k,
727
- # top_p=top_p,
728
- # do_sample=do_sample,
729
- # repetition_penalty=repetition_penalty,
730
- # eos_token_id=eos_token_id,
731
- # )
732
-
733
- # full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
734
-
735
- # # Best-effort continuation extraction
736
- # continuation = (
737
- # full_text[len(rendered_prompt):]
738
- # if isinstance(prompt, str) and full_text.startswith(rendered_prompt)
739
- # else full_text
740
- # )
741
-
742
- # return {
743
- # "reply": [full_text],
744
- # "usage": {
745
- # "prompt_tokens": int(inputs["input_ids"].shape[-1]),
746
- # "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
747
- # },
748
- # }
749
-
750
-
751
- # # Local quick test
752
- # if __name__ == "__main__":
753
- # h = EndpointHandler(model_dir=".")
754
-
755
- # # Test with plain string
756
- # resp1 = h({
757
- # "inputs": "Explain why sky dey blue.",
758
- # "parameters": {"max_new_tokens": 64}
759
- # })
760
- # print("Response (string):", resp1)
761
-
762
- # # Test with chat-style messages
763
- # resp2 = h({
764
- # "inputs": [
765
- # {"role": "user", "content": "How I go cook jollof rice?"}
766
- # ],
767
- # "parameters": {"max_new_tokens": 64}
768
- # })
769
- # print("Response (chat):", resp2)
770
-
771
-
772
-
773
- # from typing import Any, Dict, List, Union
774
-
775
- # import torch
776
-
777
- # from huggingface_inference_toolkit.logging import logger
778
-
779
- # from unsloth import FastLanguageModel
780
-
781
- # from peft import PeftModel
782
-
783
- # Prompt = Union[str, List[Dict[str, str]]]
784
-
785
-
786
- # class EndpointHandler:
787
-
788
- # """
789
-
790
- # Custom handler for Hugging Face Inference Endpoints.
791
-
792
- # Loads an Unsloth 4-bit base and attaches a LoRA adapter.
793
-
794
- # """
795
-
796
- # def __init__(self, model_dir: str, **_: Any):
797
-
798
- # """
799
-
800
- # Called once at startup. `model_dir` is the downloaded repo path.
801
-
802
- # """
803
-
804
- # logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
805
-
806
- # # 1) Load Unsloth 4-bit base
807
-
808
- # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
809
-
810
- # model, tokenizer = FastLanguageModel.from_pretrained(
811
-
812
- # model_name = base_model_name,
813
-
814
- # max_seq_length = 2048,
815
-
816
- # dtype = torch.float16,
817
-
818
- # load_in_4bit = True,
819
-
820
- # )
821
-
822
- # # 2) Attach LoRA adapter
823
-
824
- # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
825
-
826
- # model = PeftModel.from_pretrained(model, lora_repo) # use default adapter name
827
-
828
- # FastLanguageModel.for_inference(model) # optimize for generation
829
-
830
- # # 3) Finalize
831
-
832
- # self.model = model.eval()
833
-
834
- # self.tokenizer = tokenizer
835
-
836
- # self.device = getattr(self.model, "device", torch.device("cuda" if torch.cuda.is_available() else "cpu"))
837
-
838
- # logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
839
-
840
- # def _build_input_ids(self, prompt: Prompt):
841
-
842
- # """
843
-
844
- # Accepts either a single string, or OpenAI-style chat messages:
845
-
846
- # [{"role":"system|user|assistant","content":"..."}]
847
-
848
- # """
849
-
850
- # if isinstance(prompt, list):
851
-
852
- # # If tokenizer has a chat template, use it.
853
-
854
- # if hasattr(self.tokenizer, "apply_chat_template"):
855
-
856
- # text = self.tokenizer.apply_chat_template(
857
-
858
- # prompt, tokenize=False, add_generation_prompt=True
859
-
860
- # )
861
-
862
- # else:
863
-
864
- # # Fallback: naive concat
865
-
866
- # text = "\n".join(f"{m.get('role','user')}: {m.get('content','')}" for m in prompt) + "\nassistant:"
867
-
868
- # else:
869
-
870
- # text = prompt
871
-
872
- # return self.tokenizer(text, return_tensors="pt").to(self.device), text
873
-
874
- # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
875
-
876
- # """
877
-
878
- # Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
879
-
880
- # """
881
-
882
- # logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
883
-
884
- # if "inputs" not in data:
885
-
886
- # raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
887
-
888
- # prompt: Prompt = data["inputs"]
889
-
890
- # params = data.get("parameters", {}) or {}
891
-
892
- # max_new_tokens = int(params.get("max_new_tokens", 64))
893
-
894
- # temperature = float(params.get("temperature", 1))
895
-
896
- # top_k = int(params.get("top_k", 64))
897
-
898
- # top_p = float(params.get("top_p", 0.95))
899
-
900
- # do_sample = bool(params.get("do_sample", True))
901
-
902
- # repetition_penalty = float(params.get("repetition_penalty", 1.1))
903
-
904
- # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
905
-
906
- # inputs, rendered_prompt = self._build_input_ids(prompt)
907
-
908
- # with torch.no_grad():
909
-
910
- # output_ids = self.model.generate(
911
-
912
- # **inputs,
913
-
914
- # max_new_tokens=max_new_tokens,
915
-
916
- # temperature=temperature,
917
-
918
- # top_k=top_k,
919
-
920
- # top_p=top_p,
921
-
922
- # do_sample=do_sample,
923
-
924
- # repetition_penalty=repetition_penalty,
925
-
926
- # eos_token_id=eos_token_id,
927
-
928
- # )
929
-
930
- # full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
931
-
932
- # # Best-effort continuation extraction
933
-
934
- # continuation = full_text[len(rendered_prompt):] if isinstance(prompt, str) and full_text.startswith(rendered_prompt) else full_text
935
-
936
- # return {
937
-
938
- # "reply": [full_text],
939
-
940
- # # "continuations": [continuation],
941
-
942
- # "usage": {
943
-
944
- # "prompt_tokens": int(inputs["input_ids"].shape[-1]),
945
-
946
- # "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
947
-
948
- # },}
949
-
950
-
951
- # # Local quick test
952
-
953
- # if __name__ == "__main__":
954
-
955
- # h = EndpointHandler(model_dir=".")
956
-
957
- # resp = h({
958
-
959
- # "inputs": "Write in Nigerian Pidgin: How you dey?",
960
-
961
- # "parameters": {"max_new_tokens": 64}
962
-
963
- # })
964
-
965
- # print("Response:", resp)
966
-
967
-
 
1
  #tope_version
2
  # handler.py
3
  # handler.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from typing import Any, Dict, List, Union, Optional
5
  import torch
 
6
  from huggingface_inference_toolkit.logging import logger
7
  from unsloth import FastLanguageModel
8
  from peft import PeftModel
9
+
10
  # OpenAI-style prompt:
11
  # - str
12
  # - [{"role": "...", "content": "..." | [{"type":"text","text":"..."}]}]
13
  Prompt = Union[str, List[Dict[str, Any]], Dict[str, Any]]
14
+
15
+
16
  def _to_block_list(content: Any) -> List[Dict[str, str]]:
17
  """Ensure content is a list[{'type':'text','text':...}]."""
18
  if content is None:
 
22
  return content
23
  # Fallback: stringify
24
  return [{"type": "text", "text": str(content)}]
25
+
26
+
27
  class EndpointHandler:
28
  """
29
+ HF Inference Endpoint handler that mirrors your Gradio setup:
 
 
30
  - Loads Unsloth 4-bit Gemma-3-4B IT
31
  - Attaches LoRA adapter
32
  - Uses tokenizer.apply_chat_template
33
  - Always replies in Nigerian Pidgin
34
  """
35
+
36
  def __init__(self, model_dir: str, **_: Any):
37
  logger.info(f"Initializing with model_dir={model_dir}")
38
+
39
  base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
40
  model, tokenizer = FastLanguageModel.from_pretrained(
41
  model_name=base_model_name,
 
43
  dtype=torch.float16,
44
  load_in_4bit=True,
45
  )
46
+
47
  lora_repo = "Ephraimmm/PIDGIN_gemma-3"
48
  model = PeftModel.from_pretrained(model, lora_repo)
49
  FastLanguageModel.for_inference(model)
50
+
51
  self.model = model.eval()
52
  self.tokenizer = tokenizer
53
  self.device = getattr(
 
58
  # Safety: some tokenizers lack pad_token_id
59
  if getattr(self.tokenizer, "pad_token_id", None) is None:
60
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
61
+
62
  logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
63
+
64
  self._system_text = (
65
  "You are a Nigerian assistant that speaks PIDGIN ENGLISH. "
66
  "When asked 'how far', reply 'I dey o, how you dey?'. "
67
  "Always answer in Pidgin English."
68
  )
69
+
70
+ def _normalize_messages(self, prompt: Prompt) -> List[Dict[str, Any]]:
71
+ """Return a list of messages with block-style contents only."""
72
+ # Accept single dict
73
+ if isinstance(prompt, dict):
74
+ prompt = [prompt]
75
+
76
+ # Accept raw string
77
+ if isinstance(prompt, str):
78
+ msgs = [
79
+ {"role": "system", "content": _to_block_list(self._system_text)},
80
+ {"role": "user", "content": _to_block_list(prompt)},
81
+ ]
82
+ return msgs
83
+
84
+ if not isinstance(prompt, list):
85
+ raise ValueError("`inputs` must be a string, a message dict, or a list of messages.")
86
+
87
+ # Normalize all contents to blocks
88
  norm = []
89
+ for m in prompt:
90
  role = m.get("role", "user")
91
+ content = _to_block_list(m.get("content", ""))
 
 
 
 
 
 
 
 
 
 
92
  norm.append({"role": role, "content": content})
93
+
94
  # Ensure a system message exists (prepend if missing)
95
  if not any(m.get("role") == "system" for m in norm):
96
  norm = [{"role": "system", "content": _to_block_list(self._system_text)}] + norm
97
+
98
  return norm
99
+
100
+ def _build_model_inputs(self, prompt: Prompt):
101
+ messages = self._normalize_messages(prompt)
102
+ # Use the same path as your Gradio script
103
  model_inputs = self.tokenizer.apply_chat_template(
104
+ messages,
105
  add_generation_prompt=True,
106
  return_tensors="pt",
107
  tokenize=True,
108
  return_dict=True,
109
  ).to(self.device)
110
  return model_inputs
111
+
112
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  logger.info(f"Incoming keys: {list(data.keys())}")
114
+ if "inputs" not in data:
115
+ raise ValueError("Missing `inputs` in request body.")
116
+
117
+ prompt: Prompt = data["inputs"]
118
+ params: Dict[str, Any] = (data.get("parameters") or {})
119
+
120
+ # Defaults aligned with your Gradio code
121
+ max_new_tokens = int(params.get("max_new_tokens", 256)) # raise per request if needed
122
+ temperature = float(params.get("temperature", 0.1))
123
+ top_p = float(params.get("top_p", 1.0))
124
+ top_k: Optional[int] = params.get("top_k", None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  use_cache = bool(params.get("use_cache", False))
126
  repetition_penalty = float(params.get("repetition_penalty", 1.0))
127
  eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
128
  pad_token_id = params.get("pad_token_id", self.tokenizer.pad_token_id)
129
+
130
+ inputs = self._build_model_inputs(prompt)
131
+
132
  gen_kwargs = dict(
133
  **inputs,
134
  max_new_tokens=max_new_tokens,
135
  temperature=temperature,
136
  top_p=top_p,
137
+ do_sample=True, # streaming-like sampling on
138
  use_cache=use_cache,
139
  repetition_penalty=repetition_penalty,
140
  eos_token_id=eos_token_id,
 
142
  )
143
  if top_k is not None:
144
  gen_kwargs["top_k"] = int(top_k)
145
+
146
  with torch.no_grad():
147
  output_ids = self.model.generate(**gen_kwargs)
148
+
149
+ # Return ONLY the continuation (like your streamer)
150
  input_len = inputs["input_ids"].shape[-1]
151
  generated_ids = output_ids[0, input_len:]
152
  reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
153
+
154
+ return {
155
+ "reply": [reply_text],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  "usage": {
157
+ "prompt_tokens": int(input_len),
158
+ "generated_tokens": int(generated_ids.shape[-1]),
159
+ },
 
160
  }
161
+
162
+
 
 
163
  if __name__ == "__main__":
164
  h = EndpointHandler(model_dir=".")
165
+ # Raw string
166
+ print(h({"inputs": "How far?", "parameters": {"max_new_tokens": 64}}))
167
+ # Block-style content (your Gradio shape)
168
+ print(h({
169
+ "inputs": [
170
+ {
171
+ "role": "user",
172
+ "content": [{"type": "text", "text": "Explain why sky dey blue."}]
173
+ }
 
 
 
 
 
 
 
 
174
  ],
 
 
 
 
 
 
 
 
175
  "parameters": {"max_new_tokens": 64}
176
+ }))