Ephraimmm commited on
Commit
c68ea64
·
verified ·
1 Parent(s): 1a629ae

Update handler.py

Browse files
Files changed (1) hide show
  1. handler.py +553 -135
handler.py CHANGED
@@ -1,179 +1,597 @@
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
  # from typing import Any, Dict, List, Union, Optional
179
 
 
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
+
179
+
180
+ #OPENAI TRIAL
181
+ # handler.py
182
+ import os
183
+ import json
184
+ import time
185
  import torch
186
+ from typing import Any, Dict, List, Optional, Iterator
187
+
188
+ # Note: these imports match your environment
189
  from huggingface_inference_toolkit.logging import logger
190
  from unsloth import FastLanguageModel
191
  from peft import PeftModel
192
+
193
+ # Try to import transformers' streaming helper (best-effort)
194
+ try:
195
+ from transformers import TextIteratorStreamer
196
+ _HAS_STREAMER = True
197
+ except Exception:
198
+ _HAS_STREAMER = False
199
+
200
+
201
+ # ----- Config / constants -----
202
+ BASE_MODEL = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
203
+ LORA_REPO = "Ephraimmm/PIDGIN_gemma-3"
204
+
205
+ # Fixed system prompt (ALWAYS injected)
206
+ PIDGIN_SYSTEM = (
207
+ "You be AI assistant always answer strictly for Nigerian pidgin. "
208
+ "Keep grammar natural like Naija street pidgin. "
209
+ "No switch go Queen English."
210
+ )
211
+
212
+
213
+ # ----- Helper utilities -----
214
+ def _to_text_blocks(content: Any) -> List[Dict[str, str]]:
215
+ """Ensure content is a list of {'type':'text','text':...} blocks."""
216
  if content is None:
217
  return [{"type": "text", "text": ""}]
218
  if isinstance(content, list):
 
219
  return content
 
220
  return [{"type": "text", "text": str(content)}]
221
+
222
+
223
+ def _normalize_openai_messages(messages: Any) -> List[Dict[str, Any]]:
224
+ """
225
+ Normalize OpenAI-style messages to a list of dicts with block-style content.
226
+ Also ALWAYS prepend PIDGIN_SYSTEM as the first system message.
227
+ """
228
+ if isinstance(messages, str):
229
+ msgs = [
230
+ {"role": "system", "content": _to_text_blocks(PIDGIN_SYSTEM)},
231
+ {"role": "user", "content": _to_text_blocks(messages)},
232
+ ]
233
+ return msgs
234
+
235
+ if isinstance(messages, dict):
236
+ messages = [messages]
237
+
238
+ if not isinstance(messages, list):
239
+ raise ValueError("`messages` must be a string, dict, or list of message dicts.")
240
+
241
+ norm = []
242
+ for m in messages:
243
+ role = m.get("role", "user")
244
+ content = m.get("content", "")
245
+ # Accept both raw string content or block-style content
246
+ if isinstance(content, str):
247
+ content_blocks = _to_text_blocks(content)
248
+ else:
249
+ content_blocks = _to_text_blocks(content)
250
+ norm.append({"role": role, "content": content_blocks})
251
+
252
+ # ALWAYS prepend the pidgin system message
253
+ norm = [{"role": "system", "content": _to_text_blocks(PIDGIN_SYSTEM)}] + norm
254
+ return norm
255
+
256
+
257
+ # ----- Endpoint handler -----
258
  class EndpointHandler:
259
  """
260
+ HuggingFace Inference Endpoint handler.
261
+
262
+ Expects OpenAI-style body:
263
+ {
264
+ "messages": [...],
265
+ "max_tokens": 256,
266
+ "temperature": 0.7,
267
+ "top_p": 0.9,
268
+ "stream": true|false,
269
+ ...
270
+ }
271
+
272
+ When stream=True this returns a generator yielding JSONL string chunks
273
+ in OpenAI-like streaming format (data: ...\n\n).
274
  """
275
+ def __init__(self, model_dir: str = "", **kwargs: Any):
276
+ logger.info(f"Initializing EndpointHandler, model_dir={model_dir}")
277
+
278
+ # Load base model (unsloth 4-bit) and tokenizer via FastLanguageModel
279
+ # returns (model, tokenizer)
280
  model, tokenizer = FastLanguageModel.from_pretrained(
281
+ model_name=BASE_MODEL,
282
  max_seq_length=2048,
283
  dtype=torch.float16,
284
  load_in_4bit=True,
285
  )
286
+
287
+ # Attach LoRA adapter (not merged)
288
+ model = PeftModel.from_pretrained(model, LORA_REPO)
289
+
290
+ # Prepare for inference (unsloth helper)
291
  FastLanguageModel.for_inference(model)
292
+
293
  self.model = model.eval()
294
  self.tokenizer = tokenizer
295
+
296
+ # device (some wrappers set it on the model)
297
+ self.device = getattr(self.model, "device", torch.device("cuda" if torch.cuda.is_available() else "cpu"))
298
+
299
+ # safety: ensure pad token exists
 
300
  if getattr(self.tokenizer, "pad_token_id", None) is None:
301
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
302
+
303
+ logger.info(f"Loaded model. Device={self.device}, eos_id={self.tokenizer.eos_token_id}")
304
+
305
+ # Build tokenized inputs using tokenizer.apply_chat_template if available
306
+ def _build_inputs(self, messages: Any):
307
+ norm_msgs = _normalize_openai_messages(messages)
308
+ if hasattr(self.tokenizer, "apply_chat_template"):
309
+ # Mirror your existing pipeline: add_generation_prompt=True, tokenize=True/return_tensors
310
+ model_inputs = self.tokenizer.apply_chat_template(
311
+ norm_msgs,
312
+ add_generation_prompt=True,
313
+ return_tensors="pt",
314
+ tokenize=True,
315
+ return_dict=True,
316
+ ).to(self.device)
317
+ else:
318
+ # Fallback: naive formatting (Gemma chat format)
319
+ formatted = []
320
+ for m in norm_msgs:
321
+ role = m.get("role", "user")
322
+ text = "".join([b.get("text", "") for b in m.get("content", [])])
323
+ if role in ("system", "user"):
324
+ formatted.append(f"<start_of_turn>user\n{text}<end_of_turn>\n")
325
+ else:
326
+ formatted.append(f"<start_of_turn>model\n{text}<end_of_turn>\n")
327
+ formatted.append("<start_of_turn>model\n")
328
+ prompt = "".join(formatted)
329
+ model_inputs = self.tokenizer(
330
+ prompt,
331
+ return_tensors="pt",
332
+ truncation=True,
333
+ max_length=2048
334
+ ).to(self.device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  return model_inputs
336
+
337
+ # Helper to produce OpenAI-style id
338
+ def _make_id(self) -> str:
339
+ return f"chatcmpl-{os.urandom(12).hex()}"
340
+
341
+ # Streaming generator: best-effort using TextIteratorStreamer if available,
342
+ # otherwise falling back to chunking final text.
343
+ def _stream_generator(
344
+ self,
345
+ model_inputs: Dict[str, Any],
346
+ gen_kwargs: Dict[str, Any],
347
+ model_id: str,
348
+ index: int = 0,
349
+ chunk_size: int = 32,
350
+ ) -> Iterator[bytes]:
351
+ """
352
+ Yields bytes (JSONL lines) compatible with OpenAI streaming:
353
+ - data: <json>\n\n for each chunk
354
+ - data: [DONE]\n\n at end
355
+ Uses TextIteratorStreamer if available; otherwise generates full text and
356
+ yields sliced chunks (best-effort).
357
+ """
358
+ # Prepare some metadata
359
+ prompt_tokens = int(model_inputs["input_ids"].shape[1])
360
+ generated_tokens_so_far = 0
361
+ choices_metadata = {"index": index, "finish_reason": None}
362
+
363
+ # Try TextIteratorStreamer route (token-by-token streaming)
364
+ if _HAS_STREAMER:
365
+ try:
366
+ streamer = TextIteratorStreamer(self.tokenizer, skip_prompt=True, skip_special_tokens=True)
367
+ gen_kwargs_stream = gen_kwargs.copy()
368
+ gen_kwargs_stream["streamer"] = streamer
369
+
370
+ # Kick off generation in a background thread to avoid blocking the generator;
371
+ # but since we are in a single-process environment, we'll call generate in another thread.
372
+ import threading
373
+ def _generate():
374
+ with torch.no_grad():
375
+ # model.generate will populate streamer as tokens arrive
376
+ self.model.generate(**gen_kwargs_stream)
377
+
378
+ gen_thread = threading.Thread(target=_generate, daemon=True)
379
+ gen_thread.start()
380
+
381
+ # Iterate streamer for pieces of text
382
+ buffer = ""
383
+ for piece in streamer:
384
+ if not piece:
385
+ continue
386
+ buffer += piece
387
+ # when buffer reaches chunk_size, yield a chunk
388
+ while len(buffer) >= chunk_size:
389
+ chunk_text = buffer[:chunk_size]
390
+ buffer = buffer[chunk_size:]
391
+ generated_tokens_so_far += len(self.tokenizer.encode(chunk_text, add_special_tokens=False))
392
+ data = {
393
+ "id": model_id,
394
+ "object": "chat.completion.chunk",
395
+ "choices": [
396
+ {
397
+ "delta": {"content": chunk_text},
398
+ "index": index,
399
+ "finish_reason": None,
400
+ }
401
+ ],
402
+ }
403
+ yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
404
+
405
+ # flush remaining buffer
406
+ if buffer:
407
+ generated_tokens_so_far += len(self.tokenizer.encode(buffer, add_special_tokens=False))
408
+ data = {
409
+ "id": model_id,
410
+ "object": "chat.completion.chunk",
411
+ "choices": [
412
+ {
413
+ "delta": {"content": buffer},
414
+ "index": index,
415
+ "finish_reason": None,
416
+ }
417
+ ],
418
+ }
419
+ yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
420
+
421
+ # Wait for generation to finish
422
+ gen_thread.join(timeout=1.0)
423
+
424
+ # final "done" event with finish_reason and usage (best-effort)
425
+ completion_tokens = generated_tokens_so_far
426
+ total_tokens = prompt_tokens + completion_tokens
427
+ final_data = {
428
+ "id": model_id,
429
+ "object": "chat.completion",
430
+ "choices": [
431
+ {
432
+ "index": index,
433
+ "message": {
434
+ "role": "assistant",
435
+ "content": "", # full content is not repeated in chunk streaming
436
+ },
437
+ "finish_reason": "stop",
438
+ }
439
+ ],
440
+ "usage": {
441
+ "prompt_tokens": prompt_tokens,
442
+ "completion_tokens": completion_tokens,
443
+ "total_tokens": total_tokens,
444
+ },
445
+ }
446
+ # final chunk with DONE marker
447
+ yield (f"data: {json.dumps(final_data)}\n\n").encode("utf-8")
448
+ yield b"data: [DONE]\n\n"
449
+ return
450
+
451
+ except Exception as ex:
452
+ logger.warning(f"TextIteratorStreamer failed, falling back to chunking. Reason: {ex}")
453
+
454
+ # Fallback: non-streaming generate then chunk it
455
+ with torch.no_grad():
456
+ output_ids = self.model.generate(**gen_kwargs)
457
+ input_len = model_inputs["input_ids"].shape[-1]
458
+ generated_ids = output_ids[0, input_len:]
459
+ full_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
460
+ # yield chunks
461
+ start = 0
462
+ while start < len(full_text):
463
+ chunk_text = full_text[start:start + chunk_size]
464
+ start += chunk_size
465
+ data = {
466
+ "id": model_id,
467
+ "object": "chat.completion.chunk",
468
+ "choices": [
469
+ {
470
+ "delta": {"content": chunk_text},
471
+ "index": index,
472
+ "finish_reason": None,
473
+ }
474
+ ],
475
+ }
476
+ yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
477
+ time.sleep(0.01) # small pause to give a streaming feel
478
+
479
+ # final metadata
480
+ completion_tokens = int(generated_ids.shape[-1])
481
+ total_tokens = prompt_tokens + completion_tokens
482
+ final_data = {
483
+ "id": model_id,
484
+ "object": "chat.completion",
485
+ "choices": [
486
+ {
487
+ "index": index,
488
+ "message": {
489
+ "role": "assistant",
490
+ "content": full_text,
491
+ },
492
+ "finish_reason": "stop",
493
+ }
494
+ ],
495
+ "usage": {
496
+ "prompt_tokens": prompt_tokens,
497
+ "completion_tokens": completion_tokens,
498
+ "total_tokens": total_tokens,
499
+ },
500
+ }
501
+ yield (f"data: {json.dumps(final_data)}\n\n").encode("utf-8")
502
+ yield b"data: [DONE]\n\n"
503
+
504
+ # The core entrypoint used by HF Inference Endpoint
505
+ def __call__(self, data: Dict[str, Any]):
506
+ """
507
+ Expect OpenAI-style body. Returns either:
508
+ - dict (full OpenAI response) if stream=False
509
+ - generator yielding bytes (JSONL) if stream=True
510
+ """
511
+ # Validate
512
+ messages = data.get("messages")
513
+ if messages is None:
514
+ raise ValueError("Missing required field `messages` (OpenAI chat format).")
515
+
516
+ # read parameters (OpenAI names)
517
+ max_tokens = int(data.get("max_tokens", 256))
518
+ temperature = float(data.get("temperature", 0.7))
519
+ top_p = float(data.get("top_p", 0.9))
520
+ top_k = data.get("top_k", None)
521
+ stream = bool(data.get("stream", False))
522
+ do_sample = temperature > 0.0
523
+
524
+ # Build model inputs (normalizes messages and ALWAYS injects system prompt)
525
+ model_inputs = self._build_inputs(messages)
526
+
527
+ # generation kwargs (map OpenAI names to HF generate)
528
  gen_kwargs = dict(
529
+ **model_inputs,
530
+ max_new_tokens=max_tokens,
531
  temperature=temperature,
532
  top_p=top_p,
533
+ do_sample=do_sample,
534
+ pad_token_id=self.tokenizer.pad_token_id or self.tokenizer.eos_token_id,
535
+ eos_token_id=self.tokenizer.eos_token_id,
 
 
536
  )
537
  if top_k is not None:
538
  gen_kwargs["top_k"] = int(top_k)
539
+
540
+ model_id = self._make_id()
541
+
542
+ # STREAMING behavior: return generator of bytes (JSONL data:)
543
+ if stream:
544
+ return self._stream_generator(model_inputs=model_inputs, gen_kwargs=gen_kwargs, model_id=model_id)
545
+
546
+ # NON-STREAMING: generate and return full OpenAI-style completion dict
547
  with torch.no_grad():
548
  output_ids = self.model.generate(**gen_kwargs)
549
+
550
+ input_len = model_inputs["input_ids"].shape[-1]
 
551
  generated_ids = output_ids[0, input_len:]
552
+ generated_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
553
+
554
+ prompt_tokens = int(input_len)
555
+ completion_tokens = int(generated_ids.shape[-1])
556
+ total_tokens = prompt_tokens + completion_tokens
557
+
558
+ resp = {
559
+ "id": model_id,
560
+ "object": "chat.completion",
561
+ "created": int(time.time()),
562
+ "model": LORA_REPO,
563
+ "choices": [
564
+ {
565
+ "index": 0,
566
+ "message": {
567
+ "role": "assistant",
568
+ "content": generated_text.strip(),
569
+ },
570
+ "finish_reason": "stop",
571
+ }
572
+ ],
573
  "usage": {
574
+ "prompt_tokens": prompt_tokens,
575
+ "completion_tokens": completion_tokens,
576
+ "total_tokens": total_tokens,
577
  },
578
  }
579
+ return resp
580
+
581
+
582
+ # If you want to smoke-test locally
583
  if __name__ == "__main__":
584
  h = EndpointHandler(model_dir=".")
585
+ req = {
586
+ "messages": [
587
+ {"role": "user", "content": "How far?"},
 
 
 
 
 
 
588
  ],
589
+ "max_tokens": 64,
590
+ "temperature": 0.2,
591
+ "stream": False,
592
+ }
593
+ print(json.dumps(h(req), indent=2)[:1000])
594
+
595
 
596
  # from typing import Any, Dict, List, Union, Optional
597