File size: 17,449 Bytes
42da440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import asyncio
import uuid
from openai import AsyncOpenAI
import cohere
from httpx import Timeout
import traceback
import json


def make_id():
    return str(uuid.uuid4())


# How it'll work:
# Things that we want to attach to the engine wrapper will execute either before or after a generation
# it's like input or output observers
# they take the input or output as their input respectively; they're functions
# if we want to customize their behavior (i.e., log outputs to a specific directory) then we make them return a callback that's passed in instead
# inputs = all the messages
# outputs = the completion
# oh and correction, they also take a flag for completion mode, since the input will vary based on that


class EngineWrapper:
    def __init__(
        self,
        model,
        api_key=None,
        base_url=None,
        mode="api",  # can be one of api, aphrodite, llama.cpp, cohere
        input_observers=[],
        output_observers=[],
        timeout_total=500.0,  # Total operation timeout
        timeout_read=240.0,  # Read timeout between chunks
        timeout_api_call=600,  # Timeout for individual API calls
        **kwargs,
    ):
        self.mode = mode
        self.model = model
        self.input_observers = input_observers
        self.output_observers = output_observers
        self.timeout_api_call = timeout_api_call  # Store for use in API calls
        if mode == "cohere":
            self.client = cohere.AsyncClient(api_key=api_key)
        elif mode == "api":
            self.client = AsyncOpenAI(
                timeout=Timeout(
                    timeout=timeout_total,  # Total operation timeout
                    connect=10.0,  # Connection timeout
                    read=timeout_read,  # Read timeout between chunks (increased for streaming)
                    write=30.0,  # Write timeout
                    pool=10.0,  # Pool timeout
                ),
                api_key=api_key,
                base_url=base_url,
            )

    async def submit_completion(
        self, prompt, sampling_params, return_completion_only=False
    ):  # Submit request and wait for it to stream back fully
        print(prompt)
        if "temperature" not in sampling_params:
            sampling_params["temperature"] = 1
        if "top_p" not in sampling_params:
            sampling_params["top_p"] = 1
        if "max_tokens" not in sampling_params:
            sampling_params["max_tokens"] = 3000
        if "stop" not in sampling_params:
            sampling_params["stop"] = [
                "<|im_end|>"
            ]  # NOTE we hardcode the im end stop token due to the custom dataset generation model always needing it. I really need to get better at configuring my tokenizers...
        else:
            if "<|im_end|>" not in sampling_params["stop"]:
                sampling_params["stop"].append("<|im_end|>")
        if "n_predict" not in sampling_params:
            sampling_params["n_predict"] = sampling_params["max_tokens"]

        use_min_p = False
        if "min_p" in sampling_params:
            use_min_p = True

        for input_observer in self.input_observers:
            input_observer(prompt, completion_mode=True)

        if self.mode == "api":
            timed_out = False
            completion = ""
            if use_min_p:
                stream = await self.client.completions.create(
                    model=self.model,
                    prompt=prompt,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    extra_body={"min_p": sampling_params["min_p"]},
                    stream=True,
                    timeout=self.timeout_api_call,  # Use configurable timeout
                )
            else:
                stream = await self.client.completions.create(
                    model=self.model,
                    prompt=prompt,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    stream=True,
                    timeout=self.timeout_api_call,  # Use configurable timeout
                )
            async for chunk in stream:
                try:
                    completion = completion + chunk.choices[0].text
                except Exception as e:
                    timed_out = True

            for output_observer in self.output_observers:
                output_observer(
                    prompt, completion, True
                )  # input, output, completion_mode (this is the input format for output observers)

            if not return_completion_only:
                return prompt + completion, timed_out
            else:
                return completion, timed_out

        if self.mode == "cohere":
            raise Exception("Cohere not compatible with completion mode!")

    async def submit_chat(
        self, messages, sampling_params
    ):  # Submit request and wait for it to stream back fully
        if "temperature" not in sampling_params:
            sampling_params["temperature"] = 1
        if "top_p" not in sampling_params:
            sampling_params["top_p"] = 1
        if "max_tokens" not in sampling_params:
            sampling_params["max_tokens"] = 3000
        if "stop" not in sampling_params:
            sampling_params["stop"] = ["<|im_end|>"]
        else:
            if "<|im_end|>" not in sampling_params["stop"]:
                sampling_params["stop"].append("<|im_end|>")

        use_min_p = False
        if "min_p" in sampling_params:
            use_min_p = True

        for input_observer in self.input_observers:
            input_observer(messages, False)

        if self.mode == "api":
            completion = ""
            timed_out = False
            if use_min_p:
                stream = await self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    extra_body={"min_p": sampling_params["min_p"]},
                    stream=True,
                    timeout=self.timeout_api_call,  # Use configurable timeout
                )
            else:
                stream = await self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    stream=True,
                    timeout=self.timeout_api_call,  # Use configurable timeout
                )
            async for chunk in stream:
                try:
                    # print(chunk.choices)
                    try:
                        if chunk.choices[0].delta.content:
                            completion = completion + chunk.choices[0].delta.content
                    except Exception as e:
                        # print("Really strange exception!")
                        # print(chunk)
                        # traceback.print_exc()
                        pass
                except Exception as e:
                    print("\n\n------------CAUGHT EXCEPTION DURING GENERATION")
                    print(e)
                    traceback.print_exc()
                    timed_out = True
                    print("\n\n-----/\------")

            for output_observer in self.output_observers:
                output_observer(messages, completion, False)

            return completion, timed_out

        elif self.mode == "cohere":
            timed_out = False
            completion = ""
            messages_cohereified = [
                {
                    "role": "USER" if message["role"] == "user" else "CHATBOT",
                    "message": message["content"],
                }
                for message in messages
            ]
            stream = self.client.chat_stream(
                model=self.model,
                chat_history=messages_cohereified[1:-1],
                message=messages_cohereified[-1]["message"],
                preamble=messages_cohereified[0]["message"],
                temperature=sampling_params["temperature"],
                p=sampling_params["top_p"],
                stop_sequences=sampling_params["stop"],
                max_tokens=sampling_params["max_tokens"],
            )
            async for chunk in stream:
                try:
                    if chunk.event_type == "text-generation":
                        completion = completion + chunk.text
                except Exception as e:
                    print("THIS RESPONSE TIMED OUT PARTWAY THROUGH GENERATION!")
                    print(e)
                    timed_out = True

            for output_observer in self.output_observers:
                output_observer(messages, completion, False)

            return completion, timed_out
        else:
            raise Exception("Aphrodite not compatible with chat mode!")

    async def submit_completion_streaming(
        self, prompt, sampling_params, return_completion_only=False
    ):
        """Submit request and yield chunks as they arrive for streaming"""
        print(prompt)
        if "temperature" not in sampling_params:
            sampling_params["temperature"] = 1
        if "top_p" not in sampling_params:
            sampling_params["top_p"] = 1
        if "max_tokens" not in sampling_params:
            sampling_params["max_tokens"] = 3000
        if "stop" not in sampling_params:
            sampling_params["stop"] = ["<|im_end|>"]
        else:
            if "<|im_end|>" not in sampling_params["stop"]:
                sampling_params["stop"].append("<|im_end|>")
        if "n_predict" not in sampling_params:
            sampling_params["n_predict"] = sampling_params["max_tokens"]

        use_min_p = False
        if "min_p" in sampling_params:
            use_min_p = True

        for input_observer in self.input_observers:
            input_observer(prompt, completion_mode=True)

        if self.mode == "api":
            timed_out = False
            completion = ""

            if use_min_p:
                stream = await self.client.completions.create(
                    model=self.model,
                    prompt=prompt,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    extra_body={"min_p": sampling_params["min_p"]},
                    stream=True,
                    timeout=self.timeout_api_call,
                )
            else:
                stream = await self.client.completions.create(
                    model=self.model,
                    prompt=prompt,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    stream=True,
                    timeout=self.timeout_api_call,
                )

            async for chunk in stream:
                try:
                    text_chunk = chunk.choices[0].text
                    completion += text_chunk
                    # Yield chunk in SSE format
                    yield f"data: {json.dumps({'text': text_chunk, 'done': False})}\n\n"
                except Exception as e:
                    timed_out = True
                    yield f"data: {json.dumps({'error': str(e), 'done': True})}\n\n"
                    break

            # Send completion signal
            if not timed_out:
                yield f"data: {json.dumps({'text': '', 'done': True})}\n\n"

            for output_observer in self.output_observers:
                output_observer(prompt, completion, True)

        if self.mode == "cohere":
            raise Exception("Cohere not compatible with completion mode!")

    async def submit_chat_streaming(self, messages, sampling_params):
        """Submit chat request and yield chunks as they arrive for streaming"""
        if "temperature" not in sampling_params:
            sampling_params["temperature"] = 1
        if "top_p" not in sampling_params:
            sampling_params["top_p"] = 1
        if "max_tokens" not in sampling_params:
            sampling_params["max_tokens"] = 3000
        if "stop" not in sampling_params:
            sampling_params["stop"] = ["<|im_end|>"]
        else:
            if "<|im_end|>" not in sampling_params["stop"]:
                sampling_params["stop"].append("<|im_end|>")

        use_min_p = False
        if "min_p" in sampling_params:
            use_min_p = True

        for input_observer in self.input_observers:
            input_observer(messages, False)

        if self.mode == "api":
            completion = ""
            timed_out = False

            if use_min_p:
                stream = await self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    extra_body={"min_p": sampling_params["min_p"]},
                    stream=True,
                    timeout=self.timeout_api_call,
                )
            else:
                stream = await self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=sampling_params["temperature"],
                    top_p=sampling_params["top_p"],
                    stop=sampling_params["stop"],
                    max_tokens=sampling_params["max_tokens"],
                    stream=True,
                    timeout=self.timeout_api_call,
                )

            async for chunk in stream:
                try:
                    try:
                        if chunk.choices[0].delta.content:
                            text_chunk = chunk.choices[0].delta.content
                            completion += text_chunk
                            # Yield chunk in SSE format
                            yield f"data: {json.dumps({'text': text_chunk, 'done': False})}\n\n"
                    except Exception as e:
                        pass
                except Exception as e:
                    print("\n\n------------CAUGHT EXCEPTION DURING GENERATION")
                    print(e)
                    traceback.print_exc()
                    timed_out = True
                    yield f"data: {json.dumps({'error': str(e), 'done': True})}\n\n"
                    break

            # Send completion signal
            if not timed_out:
                yield f"data: {json.dumps({'text': '', 'done': True})}\n\n"

            for output_observer in self.output_observers:
                output_observer(messages, completion, False)

        elif self.mode == "cohere":
            timed_out = False
            completion = ""
            messages_cohereified = [
                {
                    "role": "USER" if message["role"] == "user" else "CHATBOT",
                    "message": message["content"],
                }
                for message in messages
            ]
            stream = self.client.chat_stream(
                model=self.model,
                chat_history=messages_cohereified[1:-1],
                message=messages_cohereified[-1]["message"],
                preamble=messages_cohereified[0]["message"],
                temperature=sampling_params["temperature"],
                p=sampling_params["top_p"],
                stop_sequences=sampling_params["stop"],
                max_tokens=sampling_params["max_tokens"],
            )
            async for chunk in stream:
                try:
                    if chunk.event_type == "text-generation":
                        text_chunk = chunk.text
                        completion += text_chunk
                        yield f"data: {json.dumps({'text': text_chunk, 'done': False})}\n\n"
                except Exception as e:
                    print("THIS RESPONSE TIMED OUT PARTWAY THROUGH GENERATION!")
                    print(e)
                    timed_out = True
                    yield f"data: {json.dumps({'error': str(e), 'done': True})}\n\n"
                    break

            if not timed_out:
                yield f"data: {json.dumps({'text': '', 'done': True})}\n\n"

            for output_observer in self.output_observers:
                output_observer(messages, completion, False)
        else:
            raise Exception("Aphrodite not compatible with chat mode!")