jnjj commited on
Commit
bcd54bc
·
verified ·
1 Parent(s): 15aeb87

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1389 -0
app.py ADDED
@@ -0,0 +1,1389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import json
4
+ import random
5
+ import torch
6
+ import asyncio
7
+ import logging
8
+ import time
9
+ import queue
10
+ import sys
11
+ from typing import List, Dict, Any, Optional, Union, AsyncGenerator, Tuple
12
+ from fastapi import FastAPI, HTTPException, Query, Request, Depends, status
13
+ from fastapi.responses import StreamingResponse, PlainTextResponse, HTMLResponse, JSONResponse
14
+ from fastapi.security import APIKeyHeader
15
+ from pydantic import BaseModel, Field, ValidationError, validator
16
+ from transformers import (
17
+ AutoConfig, AutoModelForCausalLM, AutoTokenizer,
18
+ GenerationConfig, LogitsProcessorList,
19
+ MinLengthLogitsProcessor, MaxLengthCriteria,
20
+ StoppingCriteriaList, StoppingCriteria,
21
+ TextStreamer,
22
+ )
23
+ try:
24
+ from transformers.generation.utils import GenerationOutput
25
+ except ImportError:
26
+ GenerationOutput = Any
27
+ from concurrent.futures import ThreadPoolExecutor
28
+ import uvicorn
29
+ import math
30
+ import torch.nn.functional as F
31
+ import copy
32
+
33
+ class SyncToAsyncQueue:
34
+ def __init__(self):
35
+ self._queue = queue.Queue()
36
+ try:
37
+ self._loop = asyncio.get_event_loop()
38
+ except RuntimeError:
39
+ self._loop = asyncio.new_event_loop()
40
+ asyncio.set_event_loop(self._loop)
41
+ self._get_waiters = []
42
+ self._closed = False
43
+
44
+ def put_nowait(self, item):
45
+ if self._closed:
46
+ raise asyncio.QueueClosed("Queue is closed")
47
+ self._queue.put_nowait(item)
48
+ while self._get_waiters:
49
+ waiter = self._get_waiters.pop(0)
50
+ if not waiter.done():
51
+ waiter.set_result(None)
52
+
53
+ async def get(self):
54
+ if self._closed and self._queue.empty():
55
+ raise asyncio.QueueClosed("Queue is closed")
56
+ if not self._queue.empty():
57
+ return self._queue.get_nowait()
58
+ waiter = self._loop.create_future()
59
+ self._get_waiters.append(waiter)
60
+ try:
61
+ await waiter
62
+ except asyncio.CancelledError:
63
+ if waiter in self._get_waiters:
64
+ self._get_waiters.remove(waiter)
65
+ raise
66
+ if self._closed and self._queue.empty():
67
+ raise asyncio.QueueClosed("Queue is closed")
68
+ if not self._queue.empty():
69
+ return self._queue.get_nowait()
70
+ raise asyncio.QueueClosed("Queue is closed unexpectedly after waiter finished")
71
+
72
+
73
+ def close(self):
74
+ self._closed = True
75
+ while self._get_waiters:
76
+ waiter = self._get_waiters.pop(0)
77
+ if not waiter.done():
78
+ waiter.set_exception(asyncio.QueueClosed("Queue is closed"))
79
+
80
+ class QueueTextStreamer(TextStreamer):
81
+ def __init__(self, queue: SyncToAsyncQueue, tokenizer, skip_prompt: bool = False, **decode_kwargs):
82
+ class QueueWriter:
83
+ def __init__(self, queue):
84
+ self._queue = queue
85
+
86
+ def write(self, text):
87
+ if text:
88
+ try:
89
+ self._queue.put_nowait({"type": "token", "text": text})
90
+ except asyncio.QueueClosed:
91
+ pass
92
+
93
+ def flush(self):
94
+ pass
95
+
96
+ def isatty(self): return False
97
+ def writable(self): return True
98
+
99
+ self.queue = queue
100
+ self.queue_writer = QueueWriter(queue)
101
+ super().__init__(tokenizer, self.queue_writer, skip_prompt=skip_prompt, **decode_kwargs)
102
+
103
+ def on_finalized_text(self, text: str, stream_end: bool = False):
104
+ if text:
105
+ try:
106
+ self.queue.put_nowait({"type": "token", "text": text})
107
+ except asyncio.QueueClosed:
108
+ pass
109
+ if stream_end:
110
+ try:
111
+ self.queue.put_nowait({"type": "stream_end_signal"})
112
+ except asyncio.QueueClosed:
113
+ pass
114
+
115
+ def __call__(self, token_ids, scores=None, **kwargs):
116
+ super().__call__(token_ids, scores, **kwargs)
117
+
118
+ class StopSequenceCriteria(StoppingCriteria):
119
+ def __init__(self, stop_sequences: List[str], tokenizer: AutoTokenizer):
120
+ self.tokenizer = tokenizer
121
+ self.stop_sequences_text = []
122
+ self.stop_sequence_ids = []
123
+ for seq in stop_sequences:
124
+ if seq:
125
+ encoded_ids = tokenizer.encode(seq, add_special_tokens=False)
126
+ decoded_text = tokenizer.decode(encoded_ids, skip_special_tokens=True)
127
+ if decoded_text:
128
+ self.stop_sequences_text.append(decoded_text)
129
+ self.stop_sequence_ids.append(encoded_ids)
130
+
131
+ def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:
132
+ if not self.stop_sequence_ids and not self.stop_sequences_text:
133
+ return False
134
+ input_ids_list = input_ids[0].tolist()
135
+ for stop_seq_ids in self.stop_sequence_ids:
136
+ stop_len = len(stop_seq_ids)
137
+ if len(input_ids_list) >= stop_len:
138
+ if input_ids_list[-stop_len:] == stop_seq_ids:
139
+ return True
140
+ check_tail_len = max(len(ids) for ids in self.stop_sequence_ids) + 10 if self.stop_sequence_ids else 50
141
+ tail_ids = input_ids_list[-min(check_tail_len, len(input_ids_list)):]
142
+ tail_text = self.tokenizer.decode(tail_ids, skip_special_tokens=True)
143
+ for stop_seq_text in self.stop_sequences_text:
144
+ if stop_seq_text and tail_text.endswith(stop_seq_text):
145
+ return True
146
+ return False
147
+
148
+ logging.getLogger("uvicorn").handlers.clear()
149
+ logging.getLogger("uvicorn.error").handlers.clear()
150
+ logging.getLogger("uvicorn.access").handlers.clear()
151
+ logging.getLogger("uvicorn").propagate = False
152
+ logging.getLogger("uvicorn.error").propagate = False
153
+ logging.getLogger("uvicorn.access").propagate = False
154
+ logging.getLogger("uvicorn").setLevel(logging.CRITICAL)
155
+ logging.getLogger("uvicorn.error").setLevel(logging.CRITICAL)
156
+ logging.getLogger("uvicorn.access").setLevel(logging.CRITICAL)
157
+ logging.getLogger("fastapi").setLevel(logging.CRITICAL)
158
+ logging.getLogger("transformers").setLevel(logging.CRITICAL)
159
+ logging.getLogger().handlers.clear()
160
+ logging.getLogger().addHandler(logging.NullHandler())
161
+
162
+ API_KEY_NAME = "X-API-Key"
163
+ api_key_header = APIKeyHeader(name=API_KEY_NAME)
164
+ API_KEY = os.environ.get("API_KEY", "YOUR_SECRET_API_KEY")
165
+
166
+ MODEL_NAME = os.environ.get("MODEL_NAME", "jnjj/multi-dataset-model")
167
+ SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "Eres un asistente profesional y servicial.")
168
+ MAX_CONTEXT_TOKENS = int(os.environ.get("MAX_CONTEXT_TOKENS", 1024))
169
+ MAX_GENERATION_TOKENS = int(os.environ.get("MAX_GENERATION_TOKENS", 512))
170
+ MAX_CONCURRENT_GENERATIONS = int(os.environ.get("MAX_CONCURRENT_GENERATIONS", 4))
171
+ TRUST_REMOTE_CODE = os.environ.get("TRUST_REMOTE_CODE", "false").lower() == "true"
172
+ ENABLE_FLASH_ATTENTION_2 = os.environ.get("ENABLE_FLASH_ATTENTION_2", "false").lower() == "true"
173
+ TORCH_DTYPE_STR = os.environ.get("TORCH_DTYPE", "float16")
174
+ TORCH_DTYPE = getattr(torch, TORCH_DTYPE_STR.lower(), torch.float16)
175
+ if TORCH_DTYPE_STR.lower() == "bfloat16" and (not torch.cuda.is_available() or (torch.cuda.is_available() and not torch.cuda.get_device_properties(0).has_bfloat16)):
176
+ TORCH_DTYPE = torch.float16
177
+ elif not isinstance(TORCH_DTYPE, torch.dtype):
178
+ TORCH_DTYPE = torch.float16
179
+
180
+ global_model = None
181
+ global_tokenizer = None
182
+ global_tokens: Dict[str, Optional[int]] = {}
183
+ executor = ThreadPoolExecutor(max_workers=MAX_CONCURRENT_GENERATIONS)
184
+ generation_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
185
+
186
+ REQUEST_RATE_LIMIT = int(os.environ.get("REQUEST_RATE_LIMIT", 60))
187
+ RATE_LIMIT_WINDOW_SECONDS = int(os.environ.get("RATE_LIMIT_WINDOW_SECONDS", 60))
188
+ request_counts: Dict[str, int] = {}
189
+
190
+ async def rate_limit_reset_task():
191
+ while True:
192
+ await asyncio.sleep(RATE_LIMIT_WINDOW_SECONDS)
193
+ request_counts.clear()
194
+
195
+ class GenerateRequest(BaseModel):
196
+ input_text: str
197
+ history: Optional[List[Dict[str, str]]] = None
198
+ stream: bool = True
199
+ temperature: float = Field(1.0, ge=0.0, le=2.0)
200
+ top_k: int = Field(50, ge=0)
201
+ top_p: float = Field(1.0, ge=0.0, le=1.0)
202
+ repetition_penalty: float = Field(1.0, ge=0.0)
203
+ frequency_penalty: float = Field(0.0, ge=0.0)
204
+ presence_penalty: float = Field(0.0, ge=0.0)
205
+ num_beams: int = Field(1, ge=1)
206
+ length_penalty: float = Field(1.0, ge=0.0)
207
+ no_repeat_ngram_size: int = Field(0, ge=0)
208
+ early_stopping: bool = False
209
+ do_sample: bool = True
210
+ use_mirostat: bool = False
211
+ mirostat_tau: float = Field(5.0, ge=0.0)
212
+ mirostat_eta: float = Field(0.1, ge=0.0)
213
+ max_new_tokens: int = Field(MAX_GENERATION_TOKENS, ge=1)
214
+ system_prompt: Optional[str] = None
215
+ seed: Optional[int] = None
216
+ stop_sequences: Optional[List[str]] = None
217
+ tokenize_only: bool = False
218
+ strip_trailing_whitespace: bool = False
219
+ remove_incomplete_sentences: bool = False
220
+ num_return_sequences: int = Field(1, ge=1, le=5)
221
+ bad_words_ids: Optional[List[List[int]]] = None
222
+ forced_bos_token_id: Optional[int] = None
223
+ forced_eos_token_id: Optional[int] = None
224
+ renormalize_logits: Optional[bool] = None
225
+ suppress_tokens: Optional[List[int]] = None
226
+ begin_suppress_tokens: Optional[List[int]] = None
227
+ end_suppress_tokens: Optional[List[int]] = None
228
+ encoder_no_repeat_ngram_size: int = Field(0, ge=0)
229
+ min_length: int = Field(0, ge=0)
230
+ max_length: Optional[int] = None
231
+ exponential_decay_length_penalty: Optional[Tuple[float, int, float]] = None
232
+ use_cache: bool = True
233
+ top_k_dynamic: Optional[int] = None
234
+ top_p_dynamic: Optional[float] = None
235
+ typical_p: float = Field(1.0, ge=0.0, le=1.0)
236
+ epsilon_cutoff: float = Field(0.0, ge=0.0)
237
+ eta_cutoff: float = Field(0.0, ge=0.0)
238
+ temperature_cutoff: Optional[float] = Field(None, ge=0.0)
239
+ encoder_repetition_penalty: float = Field(1.0, ge=0.0)
240
+ max_time: Optional[float] = Field(None, ge=0.0)
241
+ output_watermark: bool = False
242
+ remove_input_from_output: bool = False
243
+ eos_token_id_override: Optional[int] = None
244
+ pad_token_id_override: Optional[int] = None
245
+ bos_token_id_override: Optional[int] = None
246
+ repetition_penalty_range: Optional[int] = Field(None, ge=0)
247
+ diversity_penalty: float = Field(0.0, ge=0.0)
248
+ num_beam_groups: int = Field(1, ge=1)
249
+ return_dict_in_generate: bool = False
250
+ output_attentions: bool = False
251
+ output_hidden_states: bool = False
252
+ output_scores: bool = False
253
+ return_token_logprobs: bool = False
254
+ return_text_from_sequence: bool = True
255
+ length_normalization_factor: Optional[float] = None
256
+ min_new_tokens: int = Field(0, ge=0)
257
+ do_normalize_logits: bool = False
258
+ return_generation_inputs: bool = False
259
+ return_unused_generate_parameters: bool = False
260
+ use_fast_tokenizer: Optional[bool] = None
261
+ model_kwargs: Optional[Dict[str, Any]] = None
262
+ tokenizer_kwargs: Optional[Dict[str, Any]] = None
263
+ generation_kwargs: Optional[Dict[str, Any]] = None
264
+ prompt_lookup_num_tokens: Optional[int] = Field(None, ge=0)
265
+ tfs_lambda: Optional[float] = Field(None, ge=0.0, le=1.0)
266
+ top_p_min: Optional[float] = Field(None, ge=0.0, le=1.0)
267
+ typical_p_min: Optional[float] = Field(None, ge=0.0, le=1.0)
268
+ sequence_bias: Optional[Dict[int, float]] = None
269
+ negative_prompt_ids: Optional[List[int]] = None
270
+ return_only_text: bool = False
271
+
272
+ @validator('stop_sequences')
273
+ def validate_stop_sequences(cls, v):
274
+ if v is not None:
275
+ if not all(isinstance(seq, str) for seq in v):
276
+ raise ValueError('Each stop sequence must be a string')
277
+ return v
278
+
279
+ @validator('bad_words_ids')
280
+ def validate_bad_words_ids(cls, v):
281
+ if v is not None:
282
+ if not all(isinstance(word_id_list, list) and all(isinstance(token_id, int) for token_id in word_id_list) for word_id_list in v):
283
+ raise ValueError('bad_words_ids must be a list of lists of integers')
284
+ return v
285
+
286
+ @validator('exponential_decay_length_penalty')
287
+ def validate_exponential_decay_length_penalty(cls, v):
288
+ if v is not None:
289
+ if not (isinstance(v, (list, tuple)) and len(v) == 3 and
290
+ isinstance(v[0], (int, float)) and v[0] > 0 and
291
+ isinstance(v[1], int) and v[1] >= 0 and
292
+ isinstance(v[2], (int, float))):
293
+ raise ValueError('exponential_decay_length_penalty must be a tuple/list of 3 numbers (decay_factor, start_index, threshold)')
294
+ return v
295
+
296
+ @validator('sequence_bias')
297
+ def validate_sequence_bias(cls, v):
298
+ if v is not None:
299
+ if not all(isinstance(k, int) and isinstance(val, float) for k, val in v.items()):
300
+ raise ValueError('sequence_bias must be a dictionary with integer keys and float values')
301
+ return v
302
+
303
+ @validator('negative_prompt_ids')
304
+ def validate_negative_prompt_ids(cls, v):
305
+ if v is not None:
306
+ if not all(isinstance(token_id, int) for token_id in v):
307
+ raise ValueError('negative_prompt_ids must be a list of integers')
308
+ return v
309
+
310
+ class TokenizeRequest(BaseModel):
311
+ text: Union[str, List[str]]
312
+ add_special_tokens: bool = True
313
+ is_split_into_words: bool = False
314
+ return_token_type_ids: bool = False
315
+ padding: Union[bool, str] = False
316
+ truncation: Union[bool, str] = False
317
+ max_length: Optional[int] = None
318
+ return_tensors: Optional[str] = None
319
+ return_attention_mask: Optional[bool] = None
320
+ return_offsets_mapping: Optional[bool] = None
321
+ return_length: Optional[bool] = None
322
+ verbose: bool = False
323
+ tokenizer_kwargs: Optional[Dict[str, Any]] = None
324
+ use_fast_tokenizer: Optional[bool] = None
325
+ chat_format: bool = False
326
+ return_word_ids: bool = False
327
+
328
+ class DecodeRequest(BaseModel):
329
+ token_ids: List[int]
330
+ skip_special_tokens: bool = True
331
+ clean_up_tokenization_spaces: bool = True
332
+ decode_kwargs: Optional[Dict[str, Any]] = None
333
+
334
+ class SystemPromptUpdateRequest(BaseModel):
335
+ system_prompt: str
336
+
337
+ class ModelReloadRequest(BaseModel):
338
+ model_name: Optional[str] = None
339
+ trust_remote_code: Optional[bool] = None
340
+ enable_flash_attention_2: Optional[bool] = None
341
+ torch_dtype: Optional[str] = None
342
+ model_kwargs: Optional[Dict[str, Any]] = None
343
+ tokenizer_kwargs: Optional[Dict[str, Any]] = None
344
+
345
+ def authenticate_api_key(api_key: str = Depends(api_key_header)):
346
+ if api_key != API_KEY:
347
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key")
348
+ return api_key
349
+
350
+ def format_conversation(input_text: str, history: Optional[List[Dict[str, str]]], system_prompt: Optional[str], tokenizer: AutoTokenizer) -> str:
351
+ full_history: List[Dict[str, str]] = []
352
+ used_system_prompt = system_prompt if system_prompt is not None else SYSTEM_PROMPT
353
+ if used_system_prompt:
354
+ full_history.append({"role": "system", "content": used_system_prompt})
355
+ if history:
356
+ full_history.extend(history)
357
+ full_history.append({"role": "user", "content": input_text})
358
+ if tokenizer and hasattr(tokenizer, 'apply_chat_template') and tokenizer.chat_template:
359
+ try:
360
+ return tokenizer.apply_chat_template(full_history, tokenize=False, add_generation_prompt=True)
361
+ except Exception:
362
+ pass
363
+ formatted_text = ""
364
+ for message in full_history:
365
+ if message["role"] == "system":
366
+ formatted_text += f"{message['content'].strip()}\n\n"
367
+ elif message["role"] == "user":
368
+ formatted_text += f"Usuario: {message['content'].strip()}\n"
369
+ elif message["role"] == "assistant":
370
+ formatted_text += f"Bot: {message['content'].strip()}\n"
371
+ if not formatted_text.strip().endswith("Bot:"):
372
+ formatted_text = formatted_text.strip() + "\nBot:"
373
+ return formatted_text.strip()
374
+
375
+ def truncate_encoded_ids(input_ids: torch.Tensor, max_length: int) -> torch.Tensor:
376
+ if input_ids.shape[-1] > max_length:
377
+ return input_ids[:, -max_length:]
378
+ return input_ids
379
+
380
+ def apply_seed(seed: Optional[int]):
381
+ if seed is not None:
382
+ torch.manual_seed(seed)
383
+ if torch.cuda.is_available():
384
+ torch.cuda.manual_seed_all(seed)
385
+ random.seed(seed)
386
+
387
+ def get_generation_params(req: GenerateRequest, initial_ids: torch.Tensor, tokenizer: AutoTokenizer, device: str) -> Tuple[GenerationConfig, StoppingCriteriaList, LogitsProcessorList, Dict[str, Any]]:
388
+ initial_len = initial_ids.shape[-1]
389
+ logits_processor = LogitsProcessorList()
390
+ if req.min_length is not None and req.min_length > 0:
391
+ eos_token_id = req.eos_token_id_override if req.eos_token_id_override is not None else global_tokens.get("eos_token_id", -1)
392
+ logits_processor.append(MinLengthLogitsProcessor(initial_len + req.min_length, eos_token_id))
393
+
394
+ stopping_criteria = StoppingCriteriaList()
395
+ max_gen_len = req.max_new_tokens if req.max_new_tokens is not None else MAX_GENERATION_TOKENS
396
+ effective_max_length = req.max_length if req.max_length is not None and req.max_length > 0 else initial_len + max_gen_len
397
+ if effective_max_length > 0:
398
+ stopping_criteria.append(MaxLengthCriteria(effective_max_length))
399
+
400
+ if req.stop_sequences:
401
+ stop_sequence_criteria_instance = StopSequenceCriteria(req.stop_sequences, tokenizer)
402
+ if stop_sequence_criteria_instance.stop_sequence_ids or stop_sequence_criteria_instance.stop_sequences_text:
403
+ stopping_criteria.append(stop_sequence_criteria_instance)
404
+
405
+ gen_cfg_params = {
406
+ "temperature": req.temperature,
407
+ "top_k": req.top_k,
408
+ "top_p": req.top_p,
409
+ "repetition_penalty": req.repetition_penalty,
410
+ "num_beams": req.num_beams if not req.stream else 1,
411
+ "length_penalty": req.length_penalty,
412
+ "no_repeat_ngram_size": req.no_repeat_ngram_size,
413
+ "early_stopping": req.early_stopping,
414
+ "do_sample": req.do_sample,
415
+ "max_new_tokens": req.max_new_tokens,
416
+ "eos_token_id": req.eos_token_id_override if req.eos_token_id_override is not None else global_tokens.get("eos_token_id"),
417
+ "pad_token_id": req.pad_token_id_override if req.pad_token_id_override is not None else global_tokens.get("pad_token_id"),
418
+ "bos_token_id": req.bos_token_id_override if req.bos_token_id_override is not None else (global_tokenizer.bos_token_id if global_tokenizer else None),
419
+ "num_return_sequences": req.num_return_sequences if not req.stream else 1,
420
+ "forced_bos_token_id": req.forced_bos_token_id,
421
+ "forced_eos_token_id": req.forced_eos_token_id,
422
+ "encoder_no_repeat_ngram_size": req.encoder_no_repeat_ngram_size,
423
+ "min_length": req.min_length,
424
+ "max_length": effective_max_length,
425
+ "exponential_decay_length_penalty": req.exponential_decay_length_penalty,
426
+ "use_cache": req.use_cache,
427
+ "top_k_dynamic": req.top_k_dynamic,
428
+ "top_p_dynamic": req.top_p_dynamic,
429
+ "typical_p": req.typical_p,
430
+ "epsilon_cutoff": req.epsilon_cutoff,
431
+ "eta_cutoff": req.eta_cutoff,
432
+ "temperature_cutoff": req.temperature_cutoff,
433
+ "encoder_repetition_penalty": req.encoder_repetition_penalty,
434
+ "max_time": req.max_time,
435
+ "diversity_penalty": req.diversity_penalty,
436
+ "num_beam_groups": req.num_beam_groups if not req.stream else 1,
437
+ "length_normalization_factor": req.length_normalization_factor,
438
+ "min_new_tokens": req.min_new_tokens,
439
+ }
440
+
441
+ direct_generate_params = {
442
+ "bad_words_ids": req.bad_words_ids,
443
+ "sequence_bias": req.sequence_bias,
444
+ "negative_prompt_ids": req.negative_prompt_ids,
445
+ "return_dict_in_generate": req.return_dict_in_generate,
446
+ "output_scores": req.output_scores and not req.stream,
447
+ "output_attentions": req.output_attentions and not req.stream,
448
+ "output_hidden_states": req.output_hidden_states and not req.stream,
449
+ "return_generation_inputs": req.return_generation_inputs,
450
+ "return_unused_generate_parameters": req.return_unused_generate_parameters,
451
+ "frequency_penalty": req.frequency_penalty,
452
+ "presence_penalty": req.presence_penalty,
453
+ "output_watermark": req.output_watermark,
454
+ "do_normalize_logits": req.do_normalize_logits,
455
+ "prompt_lookup_num_tokens": req.prompt_lookup_num_tokens,
456
+ "tfs_lambda": req.tfs_lambda,
457
+ "top_p_min": req.top_p_min,
458
+ "typical_p_min": req.typical_p_min,
459
+ }
460
+ if req.use_mirostat:
461
+ direct_generate_params['use_mirostat_mode'] = 1
462
+ direct_generate_params['mirostat_tau'] = req.mirostat_tau
463
+ direct_generate_params['mirostat_eta'] = req.mirostat_eta
464
+ else:
465
+ direct_generate_params['use_mirostat_mode'] = 0
466
+
467
+ if req.model_kwargs:
468
+ direct_generate_params['model_kwargs'] = req.model_kwargs
469
+
470
+ if req.generation_kwargs:
471
+ direct_generate_params.update(req.generation_kwargs)
472
+
473
+ gen_cfg_params_filtered = {k: v for k, v in gen_cfg_params.items() if v is not None}
474
+ direct_generate_params_filtered = {k: v for k, v in direct_generate_params.items() if v is not None}
475
+
476
+ try:
477
+ gen_cfg = GenerationConfig(**gen_cfg_params_filtered)
478
+ except Exception as e:
479
+ raise ValueError(f"Error creating GenerationConfig: {e}. Parameters: {gen_cfg_params_filtered}")
480
+
481
+ return gen_cfg, stopping_criteria, logits_processor, direct_generate_params_filtered
482
+
483
+ def post_process_text(text: str, strip_trailing_whitespace: bool, remove_incomplete_sentences: bool) -> str:
484
+ if strip_trailing_whitespace:
485
+ text = text.rstrip()
486
+ if remove_incomplete_sentences:
487
+ for terminator in ['.', '!', '?', '\n']:
488
+ last_terminator = text.rfind(terminator)
489
+ if last_terminator != -1:
490
+ text = text[:last_terminator + 1]
491
+ break
492
+ return text
493
+
494
+ async def stream_generation_logic(req: GenerateRequest, initial_ids: torch.Tensor, gen_cfg: GenerationConfig, stopping_criteria: StoppingCriteriaList, logits_processor: LogitsProcessorList, direct_generate_params: Dict[str, Any], device: str) -> AsyncGenerator[str, None]:
495
+ queue = SyncToAsyncQueue()
496
+ streamer = QueueTextStreamer(queue, global_tokenizer, skip_prompt=True)
497
+ initial_len = initial_ids.shape[-1]
498
+
499
+ generation_future = asyncio.get_event_loop().run_in_executor(
500
+ executor,
501
+ lambda: global_model.generate(
502
+ initial_ids,
503
+ generation_config=gen_cfg,
504
+ stopping_criteria=stopping_criteria,
505
+ logits_processor=logits_processor,
506
+ streamer=streamer,
507
+ return_dict_in_generate=True,
508
+ **direct_generate_params,
509
+ )
510
+ )
511
+
512
+ try:
513
+ accumulated_text = ""
514
+ while True:
515
+ try:
516
+ item = await queue.get()
517
+ if item["type"] == "stream_end_signal":
518
+ break
519
+ if item["type"] == "token":
520
+ yield json.dumps(item) + "\n"
521
+ accumulated_text += item.get("text", "")
522
+ elif item["type"] == "error":
523
+ yield json.dumps(item) + "\n"
524
+ except asyncio.QueueClosed:
525
+ break
526
+
527
+ output = await asyncio.wrap_future(generation_future)
528
+
529
+ except Exception as e:
530
+ yield json.dumps({"type": "error", "message": f"Stream processing or generation error: {e}"}) + "\n"
531
+ if generation_future and not generation_future.done():
532
+ generation_future.cancel()
533
+ output = None
534
+ finally:
535
+ queue.close()
536
+
537
+ final_ids_generated = []
538
+ full_sequence_ids = initial_ids.tolist()[0]
539
+ total_generated_tokens = 0
540
+ finish_reason = "error"
541
+ final_text_processed = ""
542
+
543
+ if output is not None and isinstance(output, GenerationOutput) and output.sequences is not None and output.sequences.shape[0] > 0:
544
+ final_sequence = output.sequences[0]
545
+ final_ids_generated = final_sequence[initial_len:].tolist()
546
+ total_generated_tokens = len(final_ids_generated)
547
+ full_sequence_ids = final_sequence.tolist()
548
+
549
+ final_text_raw = global_tokenizer.decode(final_ids_generated, skip_special_tokens=True)
550
+ if req.stop_sequences:
551
+ for stop_seq in req.stop_sequences:
552
+ if stop_seq and stop_seq in final_text_raw:
553
+ final_text_raw = final_text_raw.split(stop_seq, 1)[0]
554
+ break
555
+ final_text_processed = post_process_text(final_text_raw, req.strip_trailing_whitespace, req.remove_incomplete_sentences)
556
+
557
+ if hasattr(output, 'stopping_criteria') and output.stopping_criteria is not None:
558
+ stopped_by = output.stopping_criteria
559
+ if any(isinstance(c, MaxLengthCriteria) for c in stopped_by):
560
+ finish_reason = "max_length"
561
+ elif any(isinstance(c, StopSequenceCriteria) for c in stopped_by):
562
+ finish_reason = "stop_sequence"
563
+ else:
564
+ finish_reason = "stop"
565
+ if finish_reason == "error":
566
+ eos_token_id = req.eos_token_id_override if req.eos_token_id_override is not None else global_tokens.get("eos_token_id")
567
+ if eos_token_id is not None and final_ids_generated and final_ids_generated[-1] == eos_token_id:
568
+ finish_reason = "eos_token"
569
+ elif total_generated_tokens >= (req.max_new_tokens if req.max_new_tokens is not None else MAX_GENERATION_TOKENS):
570
+ finish_reason = "max_new_tokens"
571
+ elif req.max_length is not None and len(full_sequence_ids) >= req.max_length:
572
+ finish_reason = "max_length"
573
+ elif req.stop_sequences and any(seq in post_process_text(global_tokenizer.decode(final_ids_generated, skip_special_tokens=True), False, False) for seq in req.stop_sequences):
574
+ finish_reason = "stop_sequence"
575
+ elif hasattr(output, 'max_time_exceeded') and output.max_time_exceeded:
576
+ finish_reason = "time"
577
+ else:
578
+ finish_reason = "unknown"
579
+
580
+ else:
581
+ finish_reason = "error_or_no_output"
582
+
583
+ final_payload: Dict[str, Any] = {
584
+ "type": "done",
585
+ "total_prompt_tokens": initial_len,
586
+ "total_generated_tokens": total_generated_tokens,
587
+ "total_sequence_tokens": len(full_sequence_ids),
588
+ "final_text": final_text_processed,
589
+ "finish_reason": finish_reason
590
+ }
591
+ if not req.remove_input_from_output:
592
+ final_payload["full_sequence_token_ids"] = full_sequence_ids
593
+ final_payload["generated_token_ids"] = final_ids_generated
594
+ yield json.dumps(final_payload) + "\n"
595
+
596
+ await cleanup(device)
597
+
598
+ async def non_stream_generation_logic(req: GenerateRequest, initial_ids: torch.Tensor, gen_cfg: GenerationConfig, stopping_criteria: StoppingCriteriaList, logits_processor: LogitsProcessorList, direct_generate_params: Dict[str, Any], device: str) -> Dict[str, Any]:
599
+ try:
600
+ generate_params = gen_cfg.to_dict()
601
+ generate_params.update({
602
+ "input_ids": initial_ids,
603
+ "stopping_criteria": stopping_criteria if stopping_criteria else None,
604
+ "logits_processor": logits_processor if logits_processor else None,
605
+ **direct_generate_params,
606
+ })
607
+
608
+ if req.generation_kwargs:
609
+ generate_params.update(req.generation_kwargs)
610
+
611
+ with torch.no_grad():
612
+ out = await asyncio.to_thread(global_model.generate, **generate_params)
613
+
614
+ generated_data = []
615
+ initial_len = initial_ids.shape[-1]
616
+ eos_token_id = req.eos_token_id_override if req.eos_token_id_override is not None else global_tokens.get("eos_token_id")
617
+ effective_max_length = req.max_length if req.max_length is not None and req.max_length > 0 else initial_len + (req.max_new_tokens if req.max_new_tokens is not None else MAX_GENERATION_TOKENS)
618
+
619
+ for i in range(req.num_return_sequences):
620
+ if i >= out.sequences.shape[0]:
621
+ break
622
+ sequence = out.sequences[i]
623
+ generated_ids_tensor = sequence[initial_len:]
624
+ full_sequence_ids = sequence.tolist()
625
+ text_raw = global_tokenizer.decode(generated_ids_tensor, skip_special_tokens=True)
626
+
627
+ if req.stop_sequences:
628
+ for stop_seq in req.stop_sequences:
629
+ if stop_seq and stop_seq in text_raw:
630
+ text_raw = text_raw.split(stop_seq, 1)[0]
631
+ break
632
+
633
+ text_processed = post_process_text(text_raw, req.strip_trailing_whitespace, req.remove_incomplete_sentences)
634
+ generated_tokens_count = len(generated_ids_tensor)
635
+
636
+ finish_reason = "unknown"
637
+ if hasattr(out, 'stopping_criteria') and out.stopping_criteria is not None:
638
+ stopped_by_criteria = out.stopping_criteria
639
+ if any(isinstance(c, MaxLengthCriteria) for c in stopped_by_criteria):
640
+ finish_reason = "max_length"
641
+ elif any(isinstance(c, StopSequenceCriteria) for c in stopped_by_criteria):
642
+ finish_reason = "stop_sequence"
643
+ else:
644
+ finish_reason = "stop"
645
+ if finish_reason == "unknown":
646
+ if eos_token_id is not None and generated_ids_tensor.numel() > 0 and generated_ids_tensor[-1] == eos_token_id:
647
+ finish_reason = "eos_token"
648
+ elif len(full_sequence_ids) >= effective_max_length:
649
+ if req.max_new_tokens is not None and generated_tokens_count >= req.max_new_tokens:
650
+ finish_reason = "max_new_tokens"
651
+ elif req.max_length is not None and len(full_sequence_ids) >= req.max_length:
652
+ finish_reason = "max_length"
653
+ else:
654
+ finish_reason = "length"
655
+ elif req.stop_sequences and any(seq in text_processed for seq in req.stop_sequences):
656
+ finish_reason = "stop_sequence"
657
+ elif hasattr(out, 'max_time_exceeded') and out.max_time_exceeded:
658
+ finish_reason = "time"
659
+ else:
660
+ finish_reason = "stop"
661
+
662
+ item_data: Dict[str, Any] = {
663
+ "text": text_processed if req.return_text_from_sequence else None,
664
+ "token_ids": generated_ids_tensor.tolist(),
665
+ "generated_tokens_count": generated_tokens_count,
666
+ "finish_reason": finish_reason
667
+ }
668
+ if not req.remove_input_from_output:
669
+ item_data["full_sequence_token_ids"] = full_sequence_ids
670
+
671
+ if req.output_scores and hasattr(out, 'scores') and out.scores is not None:
672
+ item_data["scores"] = [score.tolist() for score in out.scores] if isinstance(out.scores, tuple) else out.scores.tolist()
673
+ if req.return_token_logprobs:
674
+ try:
675
+ log_probs_list = []
676
+ if hasattr(out, 'scores') and out.scores is not None:
677
+ raw_scores = out.scores
678
+ if isinstance(raw_scores, tuple):
679
+ if raw_scores and raw_scores[0].ndim == 2:
680
+ step_logits_list = [s[i] for s in raw_scores]
681
+ elif raw_scores:
682
+ step_logits_list = raw_scores
683
+ else:
684
+ step_logits_list = []
685
+ elif raw_scores.ndim == 3:
686
+ step_logits_list = [raw_scores[step_idx, i] for step_idx in range(raw_scores.shape[0])]
687
+ elif raw_scores.ndim == 2:
688
+ step_logits_list = [raw_scores[step_idx] for step_idx in range(raw_scores.shape[0])]
689
+ else:
690
+ step_logits_list = []
691
+
692
+ step_log_probs = [F.log_softmax(l, dim=-1) for l in step_logits_list if l is not None and l.numel() > 0]
693
+ for step_idx, token_id in enumerate(generated_ids_tensor.tolist()):
694
+ if step_idx < len(step_log_probs) and token_id < step_log_probs[step_idx].shape[-1]:
695
+ log_probs_list.append(float(step_log_probs[step_idx][token_id].item()))
696
+ else:
697
+ log_probs_list.append(None)
698
+ item_data["token_logprobs"] = log_probs_list
699
+ else:
700
+ item_data["token_logprobs"] = "Scores not available."
701
+ except Exception:
702
+ item_data["token_logprobs"] = "Error calculating logprobs."
703
+
704
+ if req.output_attentions and hasattr(out, 'attentions') and out.attentions is not None:
705
+ item_data["attentions"] = "Attentions data is large and complex."
706
+ if req.output_hidden_states and hasattr(out, 'hidden_states') and out.hidden_states is not None:
707
+ item_data["hidden_states"] = "Hidden states data is large and complex."
708
+ if req.output_watermark and hasattr(out, 'watermark') and out.watermark is not None:
709
+ item_data["watermark"] = out.watermark[i] if isinstance(out.watermark, list) and len(out.watermark) > i else out.watermark
710
+
711
+ generated_data.append(item_data)
712
+
713
+ response_payload: Dict[str, Any] = {
714
+ "prompt_tokens": initial_len,
715
+ "generated_sequences": generated_data,
716
+ }
717
+ if req.num_return_sequences == 1 and generated_data:
718
+ response_payload["total_tokens"] = response_payload["prompt_tokens"] + generated_data[0]["generated_tokens_count"]
719
+
720
+ if req.return_dict_in_generate and isinstance(out, GenerationOutput):
721
+ raw_out_dict = {}
722
+ for key in out.keys():
723
+ if key not in ['sequences', 'scores', 'attentions', 'hidden_states', 'past_key_values', 'watermark', 'sequences_scores', 'beam_indices', 'stopping_criteria']:
724
+ value = out[key]
725
+ if isinstance(value, torch.Tensor):
726
+ raw_out_dict[key] = value.tolist()
727
+ else:
728
+ raw_out_dict[key] = value
729
+ response_payload["raw_generate_output"] = raw_out_dict
730
+
731
+ await cleanup(device)
732
+ return response_payload
733
+
734
+ except Exception as e:
735
+ await cleanup(device)
736
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Generation error: {e}")
737
+
738
+ async def cleanup(device: str):
739
+ if device == "cuda":
740
+ torch.cuda.empty_cache()
741
+ gc.collect()
742
+
743
+ app = FastAPI(title="Chatbot Profesional API", version="1.0.0")
744
+
745
+ @app.on_event("startup")
746
+ async def startup_event():
747
+ asyncio.create_task(load_model())
748
+ asyncio.create_task(rate_limit_reset_task())
749
+
750
+ async def load_model():
751
+ global global_model, global_tokenizer, global_tokens, MODEL_NAME, TRUST_REMOTE_CODE, ENABLE_FLASH_ATTENTION_2, TORCH_DTYPE, TORCH_DTYPE_STR
752
+ try:
753
+ TORCH_DTYPE = getattr(torch, TORCH_DTYPE_STR.lower(), torch.float16)
754
+ if TORCH_DTYPE_STR.lower() == "bfloat16" and (not torch.cuda.is_available() or (torch.cuda.is_available() and not torch.cuda.get_device_properties(0).has_bfloat16)):
755
+ TORCH_DTYPE = torch.float16
756
+ elif not isinstance(TORCH_DTYPE, torch.dtype):
757
+ TORCH_DTYPE = torch.float16
758
+ except AttributeError:
759
+ TORCH_DTYPE = torch.float16
760
+ try:
761
+ config = AutoConfig.from_pretrained(MODEL_NAME, trust_remote_code=TRUST_REMOTE_CODE)
762
+ tokenizer_kwargs = {"config": config, "trust_remote_code": TRUST_REMOTE_CODE}
763
+ global_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, **tokenizer_kwargs)
764
+ device = "cuda" if torch.cuda.is_available() else "cpu"
765
+ model_kwargs = {"config": config, "torch_dtype": TORCH_DTYPE, "trust_remote_code": TRUST_REMOTE_CODE}
766
+ if ENABLE_FLASH_ATTENTION_2 and device == "cuda":
767
+ try:
768
+ if hasattr(config, '_supports_flash_attn_2') and config._supports_flash_attn_2:
769
+ model_kwargs['use_flash_attention_2'] = True
770
+ elif hasattr(AutoModelForCausalLM, '_supports_flash_attn_2') and AutoModelForCausalLM._supports_flash_attn_2(config):
771
+ model_kwargs['use_flash_attention_2'] = True
772
+ except Exception:
773
+ pass
774
+ global_model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, **model_kwargs)
775
+ global_model.to(device)
776
+ global_model.eval()
777
+ global_tokens["eos_token_id"] = global_tokenizer.eos_token_id
778
+ global_tokens["pad_token_id"] = global_tokenizer.pad_token_id
779
+ if global_tokens["pad_token_id"] is None and global_tokens["eos_token_id"] is not None:
780
+ global_tokens["pad_token_id"] = global_tokens["eos_token_id"]
781
+ if global_model.config.pad_token_id is None:
782
+ global_model.config.pad_token_id = global_tokens["pad_token_id"]
783
+ except Exception:
784
+ global_model = None
785
+ global_tokenizer = None
786
+ global_tokens = {}
787
+
788
+ html_code = """
789
+ <!DOCTYPE html>
790
+ <html lang="es">
791
+ <head>
792
+ <meta charset="UTF-8" />
793
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
794
+ <title>Chatbot Profesional</title>
795
+ <style>
796
+ body { font-family: Arial, sans-serif; margin: 20px; display: flex; flex-direction: column; height: calc(100vh - 40px);}
797
+ #chatbox { flex-grow: 1; border: 1px solid #ccc; padding: 10px; overflow-y: auto; margin-bottom: 10px; display: flex; flex-direction: column;}
798
+ .message { margin-bottom: 10px; padding: 8px; border-radius: 5px; max-width: 80%; word-wrap: break-word;}
799
+ .user-message { background-color: #e1ffc7; align-self: flex-end; }
800
+ .bot-message { background-color: #f1f0f0; align-self: flex-start; }
801
+ .system-message { background-color: #e0f7fa; align-self: flex-start; font-size: 0.9em; color: #555; }
802
+ .error-message { background-color: #ffebee; align-self: flex-start; font-size: 0.9em; color: #d32f2f; }
803
+ .message strong { display: block; margin-bottom: 4px; }
804
+ #input-area { display: flex; }
805
+ #user-input { flex-grow: 1; padding: 8px; box-sizing: border-box; margin-right: 5px;}
806
+ #send-btn { padding: 8px 15px;}
807
+ </style>
808
+ </head>
809
+ <body>
810
+ <h1>Chatbot Profesional (POST API)</h1>
811
+ <div id="chatbox"></div>
812
+ <div id="input-area">
813
+ <input type="text" id="user-input" placeholder="Escribe tu mensaje aquí..." autocomplete="off"/>
814
+ <button id="send-btn">Enviar</button>
815
+ </div>
816
+ <script>
817
+ const chatbox = document.getElementById('chatbox');
818
+ const userInput = document.getElementById('user-input');
819
+ const sendBtn = document.getElementById('send-btn');
820
+ let conversationHistory = [];
821
+ const DEFAULT_SYSTEM_PROMPT = "Eres un asistente profesional y servicial.";
822
+ let currentSystemPrompt = DEFAULT_SYSTEM_PROMPT;
823
+ let currentBotMessageElement = null;
824
+
825
+ function appendMessage(sender, text, type = 'bot', isStreaming = false) {
826
+ let msgDiv;
827
+ let contentElement;
828
+ if (isStreaming && currentBotMessageElement && type === 'bot') {
829
+ contentElement = currentBotMessageElement.querySelector('.message-content');
830
+ if (contentElement) {
831
+ contentElement.textContent += text;
832
+ } else {
833
+ msgDiv = document.createElement('div');
834
+ msgDiv.classList.add('message', `${type}-message`);
835
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="message-content">${text}</span>`;
836
+ chatbox.appendChild(msgDiv);
837
+ currentBotMessageElement = msgDiv;
838
+ }
839
+ } else {
840
+ msgDiv = document.createElement('div');
841
+ msgDiv.classList.add('message', `${type}-message`);
842
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="message-content">${text}</span>`;
843
+ chatbox.appendChild(msgDiv);
844
+ if (sender === 'Bot' && isStreaming) {
845
+ currentBotMessageElement = msgDiv;
846
+ } else {
847
+ currentBotMessageElement = null;
848
+ }
849
+ }
850
+ chatbox.scrollTop = chatbox.scrollHeight;
851
+ }
852
+
853
+ function updateHistory(role, content) {
854
+ conversationHistory.push({ "role": role, "content": content });
855
+ const maxHistorySize = 20;
856
+ if (conversationHistory.length > maxHistorySize) {
857
+ const systemPrompt = conversationHistory.find(m => m.role === 'system');
858
+ let startIndex = Math.max(1, conversationHistory.length - maxHistorySize);
859
+ let newHistory = conversationHistory.slice(startIndex);
860
+ if (systemPrompt && newHistory[0]?.role !== 'system') {
861
+ newHistory.unshift(systemPrompt);
862
+ }
863
+ conversationHistory = newHistory;
864
+ }
865
+ }
866
+
867
+ async function sendMessage() {
868
+ const text = userInput.value.trim();
869
+ if (!text) {
870
+ return;
871
+ }
872
+ appendMessage('Usuario', text, 'user');
873
+ updateHistory("user", text);
874
+ userInput.value = '';
875
+ sendBtn.disabled = true;
876
+ currentBotMessageElement = null;
877
+
878
+ const isStreamingRequest = true;
879
+ const requestPayload = {
880
+ input_text: text,
881
+ history: conversationHistory,
882
+ system_prompt: currentSystemPrompt,
883
+ stream: isStreamingRequest,
884
+ temperature: 0.7,
885
+ top_k: 0,
886
+ top_p: 0.9,
887
+ max_new_tokens: 512,
888
+ };
889
+
890
+ try {
891
+ const response = await fetch('/generate', {
892
+ method: 'POST',
893
+ headers: {
894
+ 'Content-Type': 'application/json',
895
+ 'X-API-Key': 'YOUR_SECRET_API_KEY'
896
+ },
897
+ body: JSON.stringify(requestPayload),
898
+ });
899
+
900
+ if (!response.ok) {
901
+ const errorData = await response.json();
902
+ throw new Error(`API Error: ${response.status} ${response.statusText} - ${errorData.detail || errorData.error || 'Unknown error'}`);
903
+ }
904
+
905
+ if (isStreamingRequest) {
906
+ const reader = response.body.getReader();
907
+ const decoder = new TextDecoder();
908
+ let buffer = '';
909
+ let finalBotText = "";
910
+ appendMessage('Bot', '', 'bot', true);
911
+
912
+ while (true) {
913
+ const { value, done } = await reader.read();
914
+ if (done) break;
915
+ buffer += decoder.decode(value, { stream: true });
916
+ const lines = buffer.split('\n');
917
+ buffer = lines.pop();
918
+ for (const line of lines) {
919
+ if (line.trim() === '') continue;
920
+ try {
921
+ const data = JSON.parse(line);
922
+ if (data.type === 'token') {
923
+ appendMessage('Bot', data.text, 'bot', true);
924
+ finalBotText += data.text;
925
+ } else if (data.type === 'done') {
926
+ appendMessage('System', `Generated ${data.total_generated_tokens} tokens. Finish reason: ${data.finish_reason}`, 'system');
927
+ updateHistory("assistant", data.final_text !== undefined ? data.final_text : finalBotText);
928
+ } else if (data.type === 'error') {
929
+ appendMessage('Error', `Stream Error: ${data.message}`, 'error');
930
+ updateHistory("assistant", finalBotText || "Error during streaming.");
931
+ finalBotText = "";
932
+ reader.cancel();
933
+ return;
934
+ }
935
+ } catch (e) {
936
+ console.error('Failed to parse stream chunk:', e, line);
937
+ appendMessage('Error', 'Failed to process stream chunk.', 'error');
938
+ updateHistory("assistant", finalBotText || "Error processing stream.");
939
+ finalBotText = "";
940
+ reader.cancel();
941
+ return;
942
+ }
943
+ }
944
+ }
945
+ if (buffer.trim() !== '') {
946
+ try {
947
+ const data = JSON.parse(buffer);
948
+ if (data.type === 'token') {
949
+ appendMessage('Bot', data.text, 'bot', true);
950
+ finalBotText += data.text;
951
+ } else if (data.type === 'done') {
952
+ appendMessage('System', `Generated ${data.total_generated_tokens} tokens. Finish reason: ${data.finish_reason}`, 'system');
953
+ updateHistory("assistant", data.final_text !== undefined ? data.final_text : finalBotText);
954
+ } else if (data.type === 'error') {
955
+ appendMessage('Error', `Stream Error: ${data.message}`, 'error');
956
+ updateHistory("assistant", finalBotText || "Error during streaming.");
957
+ finalBotText = "";
958
+ }
959
+ } catch (e) {
960
+ console.error('Failed to parse remaining buffer:', e, buffer);
961
+ appendMessage('Error', 'Failed to process remaining stream data.', 'error');
962
+ updateHistory("assistant", finalBotText || "Error processing remaining stream data.");
963
+ finalBotText = "";
964
+ }
965
+ }
966
+ } else {
967
+ const data = await response.json();
968
+ if (data.text !== undefined && Object.keys(data).length === 1) {
969
+ appendMessage('Bot', data.text, 'bot');
970
+ updateHistory("assistant", data.text);
971
+ } else if (data.generated_sequences && data.generated_sequences.length > 0) {
972
+ const generatedText = data.generated_sequences[0].text;
973
+ appendMessage('Bot', generatedText, 'bot');
974
+ updateHistory("assistant", generatedText);
975
+ appendMessage('System', `Generated ${data.generated_sequences[0].generated_tokens_count} tokens. Finish reason: ${data.generated_sequences[0].finish_reason}`, 'system');
976
+ } else if (data.error) {
977
+ appendMessage('Error', `API Error: ${data.error}`, 'error');
978
+ } else {
979
+ appendMessage('Error', 'Received unexpected non-streaming response format.', 'error');
980
+ console.error("Unexpected non-streaming response:", data);
981
+ }
982
+ }
983
+ } catch (error) {
984
+ console.error('Send message error:', error);
985
+ appendMessage('Error', error.message || 'An unknown error occurred.', 'error');
986
+ updateHistory("assistant", "Error generating response.");
987
+ } finally {
988
+ sendBtn.disabled = false;
989
+ currentBotMessageElement = null;
990
+ }
991
+ }
992
+ sendBtn.onclick = sendMessage;
993
+ userInput.addEventListener('keypress', function(event) {
994
+ if (event.key === 'Enter' && !sendBtn.disabled) {
995
+ event.preventDefault();
996
+ sendMessage();
997
+ }
998
+ });
999
+ document.addEventListener('DOMContentLoaded', () => {
1000
+ appendMessage('System', 'Chatbot cargado. ¡Hola!', 'system');
1001
+ });
1002
+ </script>
1003
+ </body>
1004
+ </html>
1005
+ """
1006
+
1007
+ @app.get("/", response_class=HTMLResponse, summary="Interactive HTML interface")
1008
+ async def root():
1009
+ return HTMLResponse(content=html_code)
1010
+
1011
+ @app.get("/health", summary="Check API health and model status")
1012
+ async def health():
1013
+ if global_model is None or global_tokenizer is None:
1014
+ status_detail = "loading model"
1015
+ if global_tokenizer is None and global_model is not None:
1016
+ status_detail = "tokenizer not loaded"
1017
+ elif global_model is None and global_tokenizer is not None:
1018
+ status_detail = "model not loaded"
1019
+ elif global_model is None and global_tokenizer is None:
1020
+ status_detail = "model and tokenizer not loaded"
1021
+ return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"status": status_detail, "model_loaded": global_model is not None, "tokenizer_loaded": global_tokenizer is not None})
1022
+ try:
1023
+ dummy_input = "Hello"
1024
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1025
+ encoded_dummy = global_tokenizer(dummy_input, return_tensors="pt").to(device)
1026
+ with torch.no_grad():
1027
+ _ = global_model.generate(encoded_dummy.input_ids[:, -1:], max_new_tokens=1, num_beams=1, do_sample=False)
1028
+ return {"status": "ok", "model_loaded": True, "tokenizer_loaded": True, "generation_test": "successful"}
1029
+ except Exception as e:
1030
+ return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"status": "model_loaded_but_generation_failed", "model_loaded": True, "tokenizer_loaded": True, "generation_test_error": str(e)})
1031
+
1032
+
1033
+ @app.get("/config", summary="Get API configuration", dependencies=[Depends(authenticate_api_key)])
1034
+ async def get_config():
1035
+ torch_dtype_str_out = str(TORCH_DTYPE).split('.')[-1] if isinstance(TORCH_DTYPE, torch.dtype) else str(TORCH_DTYPE)
1036
+ return JSONResponse({
1037
+ "model_name": MODEL_NAME,
1038
+ "system_prompt_default": SYSTEM_PROMPT,
1039
+ "max_context_tokens": MAX_CONTEXT_TOKENS,
1040
+ "max_generation_tokens": MAX_GENERATION_TOKENS,
1041
+ "cuda_available": torch.cuda.is_available(),
1042
+ "model_loaded": global_model is not None,
1043
+ "tokenizer_loaded": global_tokenizer is not None,
1044
+ "max_concurrent_generations": MAX_CONCURRENT_GENERATIONS,
1045
+ "trust_remote_code": TRUST_REMOTE_CODE,
1046
+ "enable_flash_attention_2": ENABLE_FLASH_ATTENTION_2,
1047
+ "torch_dtype": torch_dtype_str_out,
1048
+ "eos_token_id": global_tokens.get("eos_token_id"),
1049
+ "pad_token_id": global_tokens.get("pad_token_id"),
1050
+ "bos_token_id": global_tokenizer.bos_token_id if global_tokenizer else None,
1051
+ "request_rate_limit": REQUEST_RATE_LIMIT,
1052
+ "rate_limit_window_seconds": RATE_LIMIT_WINDOW_SECONDS,
1053
+ })
1054
+
1055
+ @app.get("/model/info", summary="Get model information", dependencies=[Depends(authenticate_api_key)])
1056
+ async def get_model_info():
1057
+ if global_model is None or global_tokenizer is None:
1058
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Model is not loaded.")
1059
+ model_config = global_model.config.to_dict()
1060
+ tokenizer_config = global_tokenizer.init_kwargs
1061
+ special_tokens = {
1062
+ "eos_token": global_tokenizer.eos_token,
1063
+ "eos_token_id": global_tokenizer.eos_token_id,
1064
+ "pad_token": global_tokenizer.pad_token,
1065
+ "pad_token_id": global_tokenizer.pad_token_id,
1066
+ "bos_token": global_tokenizer.bos_token,
1067
+ "bos_token_id": global_tokenizer.bos_token_id,
1068
+ "unk_token": global_tokenizer.unk_token,
1069
+ "unk_token_id": global_tokenizer.unk_token_id,
1070
+ }
1071
+ return JSONResponse({
1072
+ "model_name": MODEL_NAME,
1073
+ "model_config": model_config,
1074
+ "tokenizer_config": tokenizer_config,
1075
+ "special_tokens": special_tokens,
1076
+ "vocab_size": global_tokenizer.vocab_size,
1077
+ "max_model_length": global_tokenizer.model_max_length,
1078
+ "is_loaded": True,
1079
+ })
1080
+
1081
+ @app.get("/tokenizer/chat_template", summary="Get the model's chat template", dependencies=[Depends(authenticate_api_key)])
1082
+ async def get_chat_template():
1083
+ if global_tokenizer is None:
1084
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Tokenizer is not loaded.")
1085
+ if not hasattr(global_tokenizer, 'chat_template') or not global_tokenizer.chat_template:
1086
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tokenizer does not have a chat template.")
1087
+ return JSONResponse({"chat_template": global_tokenizer.chat_template})
1088
+
1089
+
1090
+ @app.post("/tokenize", summary="Tokenize text", dependencies=[Depends(authenticate_api_key)])
1091
+ async def tokenize_endpoint(req: TokenizeRequest):
1092
+ if global_tokenizer is None:
1093
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Tokenizer is not loaded.")
1094
+ try:
1095
+ text_to_tokenize = req.text
1096
+ if req.chat_format and isinstance(req.text, str):
1097
+ text_to_tokenize = format_conversation(req.text, [], None, global_tokenizer)
1098
+ req.add_special_tokens = False
1099
+
1100
+ tokenizer_kwargs_final = req.tokenizer_kwargs or {}
1101
+ if req.use_fast_tokenizer is not None:
1102
+ tokenizer_kwargs_final['use_fast'] = req.use_fast_tokenizer
1103
+
1104
+ return_tensors = req.return_tensors if req.return_tensors is not None else None
1105
+ if return_tensors is None and (req.return_token_type_ids or req.return_attention_mask or req.return_offsets_mapping or req.return_length or req.return_word_ids):
1106
+ return_tensors = "pt"
1107
+
1108
+ encoded = global_tokenizer(
1109
+ text_to_tokenize,
1110
+ add_special_tokens=req.add_special_tokens,
1111
+ return_token_type_ids=req.return_token_type_ids,
1112
+ padding=req.padding,
1113
+ truncation=req.truncation,
1114
+ max_length=req.max_length,
1115
+ is_split_into_words=req.is_split_into_words,
1116
+ return_tensors=return_tensors,
1117
+ return_attention_mask=req.return_attention_mask,
1118
+ return_offsets_mapping=req.return_offsets_mapping,
1119
+ return_length=req.return_length,
1120
+ return_word_ids=req.return_word_ids,
1121
+ verbose=req.verbose,
1122
+ **tokenizer_kwargs_final
1123
+ )
1124
+ if return_tensors:
1125
+ return JSONResponse({k: v.tolist() if isinstance(v, torch.Tensor) else v for k, v in encoded.items()})
1126
+ else:
1127
+ if isinstance(req.text, str):
1128
+ response_data = {
1129
+ "tokens": encoded["input_ids"],
1130
+ "token_count": len(encoded["input_ids"]),
1131
+ "input_text": req.text
1132
+ }
1133
+ if req.chat_format:
1134
+ response_data["formatted_text"] = text_to_tokenize
1135
+ if "token_type_ids" in encoded: response_data["token_type_ids"] = encoded["token_type_ids"]
1136
+ if "attention_mask" in encoded: response_data["attention_mask"] = encoded["attention_mask"]
1137
+ if "offset_mapping" in encoded: response_data["offset_mapping"] = encoded["offset_mapping"]
1138
+ if "length" in encoded: response_data["length"] = encoded["length"]
1139
+ if "word_ids" in encoded: response_data["word_ids"] = encoded["word_ids"]
1140
+ return JSONResponse(response_data)
1141
+ elif isinstance(req.text, list):
1142
+ results = []
1143
+ for i, item_text in enumerate(req.text):
1144
+ item_result: Dict[str, Any] = {"text": item_text}
1145
+ if i < len(encoded.get("input_ids", [])):
1146
+ item_result["tokens"] = encoded["input_ids"][i]
1147
+ item_result["token_count"] = len(encoded["input_ids"][i])
1148
+ if "token_type_ids" in encoded and i < len(encoded["token_type_ids"]):
1149
+ item_result["token_type_ids"] = encoded["token_type_ids"][i]
1150
+ if "attention_mask" in encoded and i < len(encoded["attention_mask"]):
1151
+ item_result["attention_mask"] = encoded["attention_mask"][i]
1152
+ if "offset_mapping" in encoded and i < len(encoded["offset_mapping"]):
1153
+ item_result["offset_mapping"] = encoded["offset_mapping"][i]
1154
+ if "length" in encoded and i < len(encoded["length"]):
1155
+ item_result["length"] = encoded["length"][i]
1156
+ if "word_ids" in encoded and i < len(encoded["word_ids"]):
1157
+ item_result["word_ids"] = encoded["word_ids"][i]
1158
+ else:
1159
+ item_result["tokens"] = []
1160
+ item_result["token_count"] = 0
1161
+ results.append(item_result)
1162
+ return JSONResponse(results)
1163
+ except Exception as e:
1164
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Tokenization error: {e}")
1165
+
1166
+ @app.post("/decode", summary="Decode token IDs to text", dependencies=[Depends(authenticate_api_key)])
1167
+ async def decode_endpoint(req: DecodeRequest):
1168
+ if global_tokenizer is None:
1169
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Tokenizer is not loaded.")
1170
+ try:
1171
+ decode_kwargs = req.decode_kwargs or {}
1172
+ text = global_tokenizer.decode(
1173
+ req.token_ids,
1174
+ skip_special_tokens=req.skip_special_tokens,
1175
+ clean_up_tokenization_spaces=req.clean_up_tokenization_spaces,
1176
+ **decode_kwargs
1177
+ )
1178
+ return JSONResponse({"text": text})
1179
+ except Exception as e:
1180
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Decoding error: {e}")
1181
+
1182
+ @app.post("/system_prompt", summary="Update global system prompt", dependencies=[Depends(authenticate_api_key)])
1183
+ async def update_system_prompt(req: SystemPromptUpdateRequest):
1184
+ global SYSTEM_PROMPT
1185
+ if req.system_prompt is not None:
1186
+ SYSTEM_PROMPT = req.system_prompt.strip()
1187
+ return JSONResponse({"status": "success", "message": "Global system prompt updated"})
1188
+ else:
1189
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="System prompt cannot be null")
1190
+
1191
+ @app.get("/system_prompt", summary="Get global system prompt", dependencies=[Depends(authenticate_api_key)])
1192
+ async def get_system_prompt():
1193
+ return JSONResponse({"system_prompt": SYSTEM_PROMPT})
1194
+
1195
+ @app.post("/model/reload", summary="Reload the model", dependencies=[Depends(authenticate_api_key)])
1196
+ async def reload_model(req: ModelReloadRequest):
1197
+ global global_model, global_tokenizer, global_tokens, MODEL_NAME, TRUST_REMOTE_CODE, ENABLE_FLASH_ATTENTION_2, TORCH_DTYPE, TORCH_DTYPE_STR
1198
+ new_model_name = req.model_name if req.model_name else MODEL_NAME
1199
+ new_trust_remote_code = req.trust_remote_code if req.trust_remote_code is not None else TRUST_REMOTE_CODE
1200
+ new_enable_flash_attention_2 = req.enable_flash_attention_2 if req.enable_flash_attention_2 is not None else ENABLE_FLASH_ATTENTION_2
1201
+ new_torch_dtype_str_req = req.torch_dtype if req.torch_dtype else TORCH_DTYPE_STR
1202
+ try:
1203
+ new_torch_dtype = getattr(torch, new_torch_dtype_str_req.lower())
1204
+ if new_torch_dtype_str_req.lower() == "bfloat16" and (not torch.cuda.is_available() or (torch.cuda.is_available() and not torch.cuda.get_device_properties(0).has_bfloat16)):
1205
+ new_torch_dtype = torch.float16
1206
+ elif not isinstance(new_torch_dtype, torch.dtype):
1207
+ raise AttributeError
1208
+ new_torch_dtype_str = str(new_torch_dtype).split('.')[-1]
1209
+ except AttributeError:
1210
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid or unsupported torch_dtype: {new_torch_dtype_str_req}")
1211
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1212
+ async def _reload_model():
1213
+ global global_model, global_tokenizer, global_tokens, MODEL_NAME, TRUST_REMOTE_CODE, ENABLE_FLASH_ATTENTION_2, TORCH_DTYPE, TORCH_DTYPE_STR
1214
+ try:
1215
+ await cleanup(device)
1216
+ if global_model is not None:
1217
+ del global_model
1218
+ global_model = None
1219
+ if global_tokenizer is not None:
1220
+ del global_tokenizer
1221
+ global_tokenizer = None
1222
+ global_tokens = {}
1223
+ torch.cuda.empty_cache()
1224
+ gc.collect()
1225
+ config = AutoConfig.from_pretrained(new_model_name, trust_remote_code=new_trust_remote_code)
1226
+ tokenizer_kwargs = {"config": config, "trust_remote_code": new_trust_remote_code}
1227
+ if req.tokenizer_kwargs:
1228
+ tokenizer_kwargs.update(req.tokenizer_kwargs)
1229
+ tokenizer = AutoTokenizer.from_pretrained(new_model_name, **tokenizer_kwargs)
1230
+ model_kwargs = {"config": config, "torch_dtype": new_torch_dtype, "trust_remote_code": new_trust_remote_code}
1231
+ if new_enable_flash_attention_2 and device == "cuda":
1232
+ try:
1233
+ if hasattr(config, '_supports_flash_attn_2') and config._supports_flash_attn_2:
1234
+ model_kwargs['use_flash_attention_2'] = True
1235
+ elif hasattr(AutoModelForCausalLM, '_supports_flash_attn_2') and AutoModelForCausalLM._supports_flash_attn_2(config):
1236
+ model_kwargs['use_flash_attention_2'] = True
1237
+ except Exception:
1238
+ pass
1239
+ if req.model_kwargs:
1240
+ model_kwargs.update(req.model_kwargs)
1241
+ model = AutoModelForCausalLM.from_pretrained(new_model_name, **model_kwargs)
1242
+ model.to(device)
1243
+ model.eval()
1244
+ global_model = model
1245
+ global_tokenizer = tokenizer
1246
+ global_tokens["eos_token_id"] = global_tokenizer.eos_token_id
1247
+ global_tokens["pad_token_id"] = global_tokenizer.pad_token_id
1248
+ if global_tokens["pad_token_id"] is None and global_tokens["eos_token_id"] is not None:
1249
+ global_tokens["pad_token_id"] = global_tokens["eos_token_id"]
1250
+ if global_model.config.pad_token_id is None:
1251
+ global_model.config.pad_token_id = global_tokens["pad_token_id"]
1252
+ elif global_tokens["pad_token_id"] is None and global_tokens["eos_token_id"] is None:
1253
+ pass
1254
+ MODEL_NAME = new_model_name
1255
+ TRUST_REMOTE_CODE = new_trust_remote_code
1256
+ ENABLE_FLASH_ATTENTION_2 = new_enable_flash_attention_2
1257
+ TORCH_DTYPE = new_torch_dtype
1258
+ TORCH_DTYPE_STR = new_torch_dtype_str
1259
+ except Exception as e:
1260
+ global_model = None
1261
+ global_tokenizer = None
1262
+ global_tokens = {}
1263
+ asyncio.create_task(_reload_model())
1264
+ return JSONResponse({"status": "info", "message": f"Attempting to load model {new_model_name} in background. Check logs for status."})
1265
+
1266
+ @app.post("/model/unload", summary="Unload the model", dependencies=[Depends(authenticate_api_key)])
1267
+ async def unload_model():
1268
+ global global_model, global_tokenizer, global_tokens
1269
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1270
+ try:
1271
+ await cleanup(device)
1272
+ if global_model is not None:
1273
+ del global_model
1274
+ global_model = None
1275
+ if global_tokenizer is not None:
1276
+ del global_tokenizer
1277
+ global_tokenizer = None
1278
+ global_tokens = {}
1279
+ torch.cuda.empty_cache()
1280
+ gc.collect()
1281
+ return JSONResponse({"status": "success", "message": "Model unloaded successfully."})
1282
+ except Exception as e:
1283
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to unload model: {e}")
1284
+
1285
+ @app.get("/queue/status", summary="Get current generation queue status", dependencies=[Depends(authenticate_api_key)])
1286
+ async def queue_status():
1287
+ try:
1288
+ current_running = MAX_CONCURRENT_GENERATIONS - generation_semaphore._value
1289
+ return JSONResponse({
1290
+ "max_concurrent": MAX_CONCURRENT_GENERATIONS,
1291
+ "currently_running": current_running,
1292
+ "available_slots": generation_semaphore._value
1293
+ })
1294
+ except Exception as e:
1295
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting queue status: {e}")
1296
+
1297
+ @app.get("/tokens/special", summary="Get special token IDs", dependencies=[Depends(authenticate_api_key)])
1298
+ async def get_special_tokens():
1299
+ if global_tokenizer is None:
1300
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Tokenizer is not loaded.")
1301
+ try:
1302
+ return JSONResponse({
1303
+ "eos_token": global_tokenizer.eos_token,
1304
+ "eos_token_id": global_tokenizer.eos_token_id,
1305
+ "pad_token": global_tokenizer.pad_token,
1306
+ "pad_token_id": global_tokenizer.pad_token_id,
1307
+ "bos_token": global_tokenizer.bos_token,
1308
+ "bos_token_id": global_tokenizer.bos_token_id,
1309
+ "unk_token": global_tokenizer.unk_token,
1310
+ "unk_token_id": global_tokenizer.unk_token_id,
1311
+ })
1312
+ except Exception as e:
1313
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting special tokens: {e}")
1314
+
1315
+ @app.post("/generate", summary="Generate text", dependencies=[Depends(authenticate_api_key)])
1316
+ async def generate_endpoint(req: GenerateRequest, request: Request):
1317
+ client_host = request.client.host if request.client else "unknown"
1318
+ if client_host in request_counts and request_counts[client_host] >= REQUEST_RATE_LIMIT:
1319
+ raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded")
1320
+ request_counts[client_host] = request_counts.get(client_host, 0) + 1
1321
+
1322
+ if global_model is None or global_tokenizer is None:
1323
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Model is not loaded. It may still be loading or failed to load.")
1324
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1325
+ apply_seed(req.seed)
1326
+ try:
1327
+ initial_prompt_text = format_conversation(req.input_text, req.history, req.system_prompt, global_tokenizer)
1328
+ except Exception as e:
1329
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error formatting conversation: {e}")
1330
+ try:
1331
+ tokenizer_encoding_kwargs = req.tokenizer_kwargs or {}
1332
+ if req.use_fast_tokenizer is not None:
1333
+ tokenizer_encoding_kwargs['use_fast'] = req.use_fast_tokenizer
1334
+ encoded = global_tokenizer(initial_prompt_text, return_tensors="pt", add_special_tokens=False, **tokenizer_encoding_kwargs).to(device)
1335
+ initial_ids_before_trunc = encoded.input_ids
1336
+ initial_prompt_tokens_count_before_trunc = initial_ids_before_trunc.shape[-1]
1337
+ ids = truncate_encoded_ids(initial_ids_before_trunc, MAX_CONTEXT_TOKENS)
1338
+ except Exception as e:
1339
+ await cleanup(device)
1340
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Tokenizer encoding error: {e}")
1341
+ if req.tokenize_only:
1342
+ await cleanup(device)
1343
+ return JSONResponse({
1344
+ "prompt_tokens_count": initial_prompt_tokens_count_before_trunc,
1345
+ "max_context_tokens": MAX_CONTEXT_TOKENS,
1346
+ "truncated": initial_prompt_tokens_count_before_trunc > MAX_CONTEXT_TOKENS,
1347
+ "input_text_processed": initial_prompt_text,
1348
+ "input_ids_truncated": ids.tolist()[0]
1349
+ })
1350
+ try:
1351
+ gen_cfg, stopping_criteria, logits_processor, direct_generate_params = get_generation_params(req, ids, global_tokenizer, device)
1352
+ except ValueError as e:
1353
+ await cleanup(device)
1354
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating generation parameters: {e}")
1355
+ except Exception as e:
1356
+ await cleanup(device)
1357
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error getting generation parameters: {e}")
1358
+
1359
+
1360
+ async with generation_semaphore:
1361
+ if req.stream:
1362
+ if req.return_only_text:
1363
+ pass
1364
+ try:
1365
+ return StreamingResponse(stream_generation_logic(req, ids, gen_cfg, stopping_criteria, logits_processor, direct_generate_params, device), media_type="application/json")
1366
+ except Exception as e:
1367
+ await cleanup(device)
1368
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error setting up streaming generation: {e}")
1369
+ else:
1370
+ try:
1371
+ response_data = await non_stream_generation_logic(req, ids, gen_cfg, stopping_criteria, logits_processor, direct_generate_params, device)
1372
+ if req.return_only_text and response_data.get("generated_sequences"):
1373
+ if response_data["generated_sequences"]:
1374
+ return JSONResponse({"text": response_data["generated_sequences"][0].get("text", "")})
1375
+ else:
1376
+ return JSONResponse({"text": ""})
1377
+ return JSONResponse(response_data)
1378
+ except HTTPException:
1379
+ raise
1380
+ except Exception as e:
1381
+ await cleanup(device)
1382
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Non-streaming generation error: {e}")
1383
+
1384
+ if __name__ == "__main__":
1385
+ uvicorn.run(
1386
+ app, host="0.0.0.0", port=7860,
1387
+ log_level="critical",
1388
+ access_log=False
1389
+ )