litserve 0.2.5.dev0__tar.gz → 0.2.6.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {litserve-0.2.5.dev0/src/litserve.egg-info → litserve-0.2.6.dev0}/PKG-INFO +4 -3
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/README.md +1 -1
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/requirements.txt +0 -1
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__about__.py +1 -1
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__init__.py +12 -2
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/connector.py +15 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/docker_builder.py +1 -1
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/loops.py +210 -15
- litserve-0.2.6.dev0/src/litserve/schema/__init__.py +0 -0
- litserve-0.2.6.dev0/src/litserve/schema/image.py +75 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/server.py +79 -4
- litserve-0.2.6.dev0/src/litserve/specs/__init__.py +4 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/specs/openai.py +7 -6
- litserve-0.2.6.dev0/src/litserve/specs/openai_embedding.py +175 -0
- litserve-0.2.6.dev0/src/litserve/test_examples/openai_embedding_spec_example.py +49 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0/src/litserve.egg-info}/PKG-INFO +4 -3
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/SOURCES.txt +4 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/requires.txt +2 -1
- litserve-0.2.5.dev0/src/litserve/specs/__init__.py +0 -3
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/LICENSE +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/MANIFEST.in +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/setup.cfg +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/setup.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__main__.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/api.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/__init__.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/base.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/defaults/__init__.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/defaults/metric_callback.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/loggers.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/middlewares.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/python_client.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/specs/base.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/__init__.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/openai_spec_example.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/simple_example.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/utils.py +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/dependency_links.txt +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/entry_points.txt +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/not-zip-safe +0 -0
- {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: litserve
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6.dev0
|
|
4
4
|
Summary: Lightweight AI server.
|
|
5
5
|
Home-page: https://github.com/Lightning-AI/litserve
|
|
6
6
|
Download-URL: https://github.com/Lightning-AI/litserve
|
|
@@ -28,11 +28,11 @@ Requires-Python: >=3.8
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: fastapi>=0.100
|
|
31
|
-
Requires-Dist: httpx
|
|
32
31
|
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
33
32
|
Provides-Extra: test
|
|
34
33
|
Requires-Dist: asgi-lifespan; extra == "test"
|
|
35
34
|
Requires-Dist: coverage[toml]>=7.5.3; extra == "test"
|
|
35
|
+
Requires-Dist: httpx>=0.27.0; extra == "test"
|
|
36
36
|
Requires-Dist: lightning>2.0.0; extra == "test"
|
|
37
37
|
Requires-Dist: mypy==1.11.2; extra == "test"
|
|
38
38
|
Requires-Dist: numpy<2.0; extra == "test"
|
|
@@ -41,6 +41,7 @@ Requires-Dist: pillow; extra == "test"
|
|
|
41
41
|
Requires-Dist: psutil; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
43
43
|
Requires-Dist: pytest-cov; extra == "test"
|
|
44
|
+
Requires-Dist: pytest-retry>=1.6.3; extra == "test"
|
|
44
45
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
45
46
|
Requires-Dist: python-multipart; extra == "test"
|
|
46
47
|
Requires-Dist: requests; extra == "test"
|
|
@@ -208,7 +209,7 @@ Use LitServe to deploy any model or AI service: (Compound AI, Gen AI, classic ML
|
|
|
208
209
|
<a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-background-removal-api-with-litserve">Background Removal</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-controlled-image-generation-api-controlnet">Control Stable Diffusion (ControlNet)</a>
|
|
209
210
|
<strong>Speech:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-voice-clone-api-coqui-xtts-v2-model">Text-speech (XTTS V2)</a>, <a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-a-speech-generation-api-using-parler-tts-powered-by-litserve">Parler-TTS</a>
|
|
210
211
|
<strong>Classical ML:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-random-forest-with-litserve">Random forest</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-xgboost-with-litserve">XGBoost</a>
|
|
211
|
-
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>
|
|
212
|
+
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/openai-fault-tolerant-proxy-server">LLM proxy server</a>
|
|
212
213
|
</pre>
|
|
213
214
|
</pre>
|
|
214
215
|
|
|
@@ -155,7 +155,7 @@ Use LitServe to deploy any model or AI service: (Compound AI, Gen AI, classic ML
|
|
|
155
155
|
<a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-background-removal-api-with-litserve">Background Removal</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-controlled-image-generation-api-controlnet">Control Stable Diffusion (ControlNet)</a>
|
|
156
156
|
<strong>Speech:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-voice-clone-api-coqui-xtts-v2-model">Text-speech (XTTS V2)</a>, <a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-a-speech-generation-api-using-parler-tts-powered-by-litserve">Parler-TTS</a>
|
|
157
157
|
<strong>Classical ML:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-random-forest-with-litserve">Random forest</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-xgboost-with-litserve">XGBoost</a>
|
|
158
|
-
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>
|
|
158
|
+
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/openai-fault-tolerant-proxy-server">LLM proxy server</a>
|
|
159
159
|
</pre>
|
|
160
160
|
</pre>
|
|
161
161
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
__version__ = "0.2.
|
|
14
|
+
__version__ = "0.2.6.dev0"
|
|
15
15
|
__author__ = "Lightning-AI et al."
|
|
16
16
|
__author_email__ = "community@lightning.ai"
|
|
17
17
|
__license__ = "Apache-2.0"
|
|
@@ -17,6 +17,16 @@ from litserve.api import LitAPI
|
|
|
17
17
|
from litserve.callbacks import Callback
|
|
18
18
|
from litserve.loggers import Logger
|
|
19
19
|
from litserve.server import LitServer, Request, Response
|
|
20
|
-
from litserve.specs
|
|
20
|
+
from litserve.specs import OpenAIEmbeddingSpec, OpenAISpec
|
|
21
21
|
|
|
22
|
-
__all__ = [
|
|
22
|
+
__all__ = [
|
|
23
|
+
"LitAPI",
|
|
24
|
+
"LitServer",
|
|
25
|
+
"Request",
|
|
26
|
+
"Response",
|
|
27
|
+
"OpenAISpec",
|
|
28
|
+
"OpenAIEmbeddingSpec",
|
|
29
|
+
"test_examples",
|
|
30
|
+
"Callback",
|
|
31
|
+
"Logger",
|
|
32
|
+
]
|
|
@@ -39,6 +39,21 @@ class _Connector:
|
|
|
39
39
|
else:
|
|
40
40
|
self._devices = devices
|
|
41
41
|
|
|
42
|
+
self.check_devices_and_accelerators()
|
|
43
|
+
|
|
44
|
+
def check_devices_and_accelerators(self):
|
|
45
|
+
"""Check if the devices are in a valid fomra and raise an error if they are not."""
|
|
46
|
+
if self._accelerator in ["cuda", "mps"]:
|
|
47
|
+
if not isinstance(self._devices, int) and not (
|
|
48
|
+
isinstance(self._devices, list) and all(isinstance(device, int) for device in self._devices)
|
|
49
|
+
):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"devices must be an integer or a list of integers when using 'cuda' or 'mps', "
|
|
52
|
+
f"instead got {self._devices}"
|
|
53
|
+
)
|
|
54
|
+
elif self._accelerator != "cpu":
|
|
55
|
+
raise ValueError(f"accelerator must be one of (cuda, mps, cpu), instead got {self._accelerator}")
|
|
56
|
+
|
|
42
57
|
@property
|
|
43
58
|
def accelerator(self):
|
|
44
59
|
return self._accelerator
|
|
@@ -67,7 +67,7 @@ CMD ["python", "/app/{server_filename}"]
|
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
CUDA_DOCKER_TEMPLATE = """# Change CUDA and cuDNN version here
|
|
70
|
-
FROM nvidia/cuda:12.4.1-
|
|
70
|
+
FROM nvidia/cuda:12.4.1-base-ubuntu22.04
|
|
71
71
|
ARG PYTHON_VERSION=3.12
|
|
72
72
|
|
|
73
73
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
@@ -17,6 +17,7 @@ import logging
|
|
|
17
17
|
import multiprocessing as mp
|
|
18
18
|
import sys
|
|
19
19
|
import time
|
|
20
|
+
from abc import ABC
|
|
20
21
|
from queue import Empty, Queue
|
|
21
22
|
from typing import Dict, List, Optional, Tuple, Union
|
|
22
23
|
|
|
@@ -30,7 +31,6 @@ from litserve.utils import LitAPIStatus, PickleableHTTPException, WorkerSetupSta
|
|
|
30
31
|
|
|
31
32
|
mp.allow_connection_pickling()
|
|
32
33
|
|
|
33
|
-
|
|
34
34
|
try:
|
|
35
35
|
import uvloop
|
|
36
36
|
|
|
@@ -389,6 +389,190 @@ def run_batched_streaming_loop(
|
|
|
389
389
|
response_queues[response_queue_id].put((uid, (e, LitAPIStatus.ERROR)))
|
|
390
390
|
|
|
391
391
|
|
|
392
|
+
class _BaseLoop(ABC):
|
|
393
|
+
"""Loop runs an inference engine that executes a specific set of hooks, implemented in the LitAPI, in a predefined
|
|
394
|
+
order.
|
|
395
|
+
|
|
396
|
+
For a default loop, LitAPI must implement the following hooks:
|
|
397
|
+
- decode_request
|
|
398
|
+
- batch
|
|
399
|
+
- predict
|
|
400
|
+
- unbatch
|
|
401
|
+
- encode_response
|
|
402
|
+
|
|
403
|
+
To implement a custom loop, subclass this class and implement the `run` method. The `run` method should execute the
|
|
404
|
+
hooks in the desired order.
|
|
405
|
+
|
|
406
|
+
`__call__` method is the entry point for the worker process. It calls the `run` method in a loop until the worker is
|
|
407
|
+
terminated.
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
class TestLoop(_BaseLoop):
|
|
413
|
+
def run(
|
|
414
|
+
self,
|
|
415
|
+
lit_api: LitAPI,
|
|
416
|
+
lit_spec: Optional[LitSpec],
|
|
417
|
+
device: str,
|
|
418
|
+
worker_id: int,
|
|
419
|
+
request_queue: Queue,
|
|
420
|
+
response_queues: List[Queue],
|
|
421
|
+
max_batch_size: int,
|
|
422
|
+
batch_timeout: float,
|
|
423
|
+
stream: bool,
|
|
424
|
+
workers_setup_status: Dict[int, str],
|
|
425
|
+
callback_runner: CallbackRunner,
|
|
426
|
+
):
|
|
427
|
+
item = request_queue.get()
|
|
428
|
+
if item is None:
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
response_queue_id, uid, timestamp, x_enc = item
|
|
432
|
+
# Expects LitAPI to implement the load_cache method
|
|
433
|
+
lit_api.load_cache(x_enc)
|
|
434
|
+
x = lit_api.decode_request(x_enc)
|
|
435
|
+
response = lit_api.predict(x)
|
|
436
|
+
response_enc = lit_api.encode_response(response)
|
|
437
|
+
response_queues[response_queue_id].put((uid, (response_enc, LitAPIStatus.OK)))
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
def __call__(
|
|
443
|
+
self,
|
|
444
|
+
lit_api: LitAPI,
|
|
445
|
+
lit_spec: Optional[LitSpec],
|
|
446
|
+
device: str,
|
|
447
|
+
worker_id: int,
|
|
448
|
+
request_queue: Queue,
|
|
449
|
+
response_queues: List[Queue],
|
|
450
|
+
max_batch_size: int,
|
|
451
|
+
batch_timeout: float,
|
|
452
|
+
stream: bool,
|
|
453
|
+
workers_setup_status: Dict[int, str],
|
|
454
|
+
callback_runner: CallbackRunner,
|
|
455
|
+
):
|
|
456
|
+
while True:
|
|
457
|
+
self.run(
|
|
458
|
+
lit_api,
|
|
459
|
+
lit_spec,
|
|
460
|
+
device,
|
|
461
|
+
worker_id,
|
|
462
|
+
request_queue,
|
|
463
|
+
response_queues,
|
|
464
|
+
max_batch_size,
|
|
465
|
+
batch_timeout,
|
|
466
|
+
stream,
|
|
467
|
+
workers_setup_status,
|
|
468
|
+
callback_runner,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def run(
|
|
472
|
+
self,
|
|
473
|
+
lit_api: LitAPI,
|
|
474
|
+
lit_spec: Optional[LitSpec],
|
|
475
|
+
device: str,
|
|
476
|
+
worker_id: int,
|
|
477
|
+
request_queue: Queue,
|
|
478
|
+
response_queues: List[Queue],
|
|
479
|
+
max_batch_size: int,
|
|
480
|
+
batch_timeout: float,
|
|
481
|
+
stream: bool,
|
|
482
|
+
workers_setup_status: Dict[int, str],
|
|
483
|
+
callback_runner: CallbackRunner,
|
|
484
|
+
):
|
|
485
|
+
raise NotImplementedError
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class SingleLoop(_BaseLoop):
|
|
489
|
+
def __call__(
|
|
490
|
+
self,
|
|
491
|
+
lit_api: LitAPI,
|
|
492
|
+
lit_spec: Optional[LitSpec],
|
|
493
|
+
device: str,
|
|
494
|
+
worker_id: int,
|
|
495
|
+
request_queue: Queue,
|
|
496
|
+
response_queues: List[Queue],
|
|
497
|
+
max_batch_size: int,
|
|
498
|
+
batch_timeout: float,
|
|
499
|
+
stream: bool,
|
|
500
|
+
workers_setup_status: Dict[int, str],
|
|
501
|
+
callback_runner: CallbackRunner,
|
|
502
|
+
):
|
|
503
|
+
run_single_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class BatchedLoop(_BaseLoop):
|
|
507
|
+
def __call__(
|
|
508
|
+
self,
|
|
509
|
+
lit_api: LitAPI,
|
|
510
|
+
lit_spec: Optional[LitSpec],
|
|
511
|
+
device: str,
|
|
512
|
+
worker_id: int,
|
|
513
|
+
request_queue: Queue,
|
|
514
|
+
response_queues: List[Queue],
|
|
515
|
+
max_batch_size: int,
|
|
516
|
+
batch_timeout: float,
|
|
517
|
+
stream: bool,
|
|
518
|
+
workers_setup_status: Dict[int, str],
|
|
519
|
+
callback_runner: CallbackRunner,
|
|
520
|
+
):
|
|
521
|
+
run_batched_loop(
|
|
522
|
+
lit_api,
|
|
523
|
+
lit_spec,
|
|
524
|
+
request_queue,
|
|
525
|
+
response_queues,
|
|
526
|
+
max_batch_size,
|
|
527
|
+
batch_timeout,
|
|
528
|
+
callback_runner,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class StreamingLoop(_BaseLoop):
|
|
533
|
+
def __call__(
|
|
534
|
+
self,
|
|
535
|
+
lit_api: LitAPI,
|
|
536
|
+
lit_spec: Optional[LitSpec],
|
|
537
|
+
device: str,
|
|
538
|
+
worker_id: int,
|
|
539
|
+
request_queue: Queue,
|
|
540
|
+
response_queues: List[Queue],
|
|
541
|
+
max_batch_size: int,
|
|
542
|
+
batch_timeout: float,
|
|
543
|
+
stream: bool,
|
|
544
|
+
workers_setup_status: Dict[int, str],
|
|
545
|
+
callback_runner: CallbackRunner,
|
|
546
|
+
):
|
|
547
|
+
run_streaming_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class BatchedStreamingLoop(_BaseLoop):
|
|
551
|
+
def __call__(
|
|
552
|
+
self,
|
|
553
|
+
lit_api: LitAPI,
|
|
554
|
+
lit_spec: Optional[LitSpec],
|
|
555
|
+
device: str,
|
|
556
|
+
worker_id: int,
|
|
557
|
+
request_queue: Queue,
|
|
558
|
+
response_queues: List[Queue],
|
|
559
|
+
max_batch_size: int,
|
|
560
|
+
batch_timeout: float,
|
|
561
|
+
stream: bool,
|
|
562
|
+
workers_setup_status: Dict[int, str],
|
|
563
|
+
callback_runner: CallbackRunner,
|
|
564
|
+
):
|
|
565
|
+
run_batched_streaming_loop(
|
|
566
|
+
lit_api,
|
|
567
|
+
lit_spec,
|
|
568
|
+
request_queue,
|
|
569
|
+
response_queues,
|
|
570
|
+
max_batch_size,
|
|
571
|
+
batch_timeout,
|
|
572
|
+
callback_runner,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
392
576
|
def inference_worker(
|
|
393
577
|
lit_api: LitAPI,
|
|
394
578
|
lit_spec: Optional[LitSpec],
|
|
@@ -401,6 +585,7 @@ def inference_worker(
|
|
|
401
585
|
stream: bool,
|
|
402
586
|
workers_setup_status: Dict[int, str],
|
|
403
587
|
callback_runner: CallbackRunner,
|
|
588
|
+
loop: Union[str, _BaseLoop],
|
|
404
589
|
):
|
|
405
590
|
callback_runner.trigger_event(EventTypes.BEFORE_SETUP, lit_api=lit_api)
|
|
406
591
|
try:
|
|
@@ -412,25 +597,35 @@ def inference_worker(
|
|
|
412
597
|
lit_api.device = device
|
|
413
598
|
callback_runner.trigger_event(EventTypes.AFTER_SETUP, lit_api=lit_api)
|
|
414
599
|
|
|
415
|
-
|
|
600
|
+
print(f"Setup complete for worker {worker_id}.")
|
|
416
601
|
|
|
417
602
|
if workers_setup_status:
|
|
418
603
|
workers_setup_status[worker_id] = WorkerSetupStatus.READY
|
|
419
604
|
|
|
420
605
|
if lit_spec:
|
|
421
606
|
logging.info(f"LitServe will use {lit_spec.__class__.__name__} spec")
|
|
422
|
-
if stream:
|
|
423
|
-
if max_batch_size > 1:
|
|
424
|
-
run_batched_streaming_loop(
|
|
425
|
-
lit_api, lit_spec, request_queue, response_queues, max_batch_size, batch_timeout, callback_runner
|
|
426
|
-
)
|
|
427
|
-
else:
|
|
428
|
-
run_streaming_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
|
|
429
|
-
return
|
|
430
607
|
|
|
431
|
-
if
|
|
432
|
-
|
|
433
|
-
|
|
608
|
+
if loop == "auto":
|
|
609
|
+
loop = (
|
|
610
|
+
BatchedStreamingLoop()
|
|
611
|
+
if stream and max_batch_size > 1
|
|
612
|
+
else StreamingLoop()
|
|
613
|
+
if stream
|
|
614
|
+
else BatchedLoop()
|
|
615
|
+
if max_batch_size > 1
|
|
616
|
+
else SingleLoop()
|
|
434
617
|
)
|
|
435
|
-
|
|
436
|
-
|
|
618
|
+
|
|
619
|
+
loop(
|
|
620
|
+
lit_api,
|
|
621
|
+
lit_spec,
|
|
622
|
+
device,
|
|
623
|
+
worker_id,
|
|
624
|
+
request_queue,
|
|
625
|
+
response_queues,
|
|
626
|
+
max_batch_size,
|
|
627
|
+
batch_timeout,
|
|
628
|
+
stream,
|
|
629
|
+
workers_setup_status,
|
|
630
|
+
callback_runner,
|
|
631
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, field_serializer, model_validator
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageInput(BaseModel):
|
|
12
|
+
image_data: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
@model_validator(mode="after")
|
|
15
|
+
def validate_base64(self) -> "ImageInput":
|
|
16
|
+
"""Ensure the string is a valid Base64."""
|
|
17
|
+
model_dump = self.model_dump()
|
|
18
|
+
for key, value in model_dump.items():
|
|
19
|
+
if value:
|
|
20
|
+
try:
|
|
21
|
+
base64.b64decode(value)
|
|
22
|
+
except base64.binascii.Error:
|
|
23
|
+
raise ValueError("Invalid Base64 string.")
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def get_image(self, key: Optional[str] = None) -> "Image.Image":
|
|
27
|
+
"""Decode the Base64 string and return a PIL Image object."""
|
|
28
|
+
if key is None:
|
|
29
|
+
key = "image_data"
|
|
30
|
+
image_data = self.model_dump().get(key)
|
|
31
|
+
if not image_data:
|
|
32
|
+
raise ValueError(f"Missing image data for key '{key}'")
|
|
33
|
+
try:
|
|
34
|
+
from PIL import Image, UnidentifiedImageError
|
|
35
|
+
except ImportError:
|
|
36
|
+
raise ImportError("Pillow is required to use the ImageInput schema. Install it with `pip install Pillow`.")
|
|
37
|
+
try:
|
|
38
|
+
decoded_data = base64.b64decode(image_data)
|
|
39
|
+
return Image.open(BytesIO(decoded_data))
|
|
40
|
+
except UnidentifiedImageError as e:
|
|
41
|
+
raise ValueError(f"Error loading image from decoded data: {e}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ImageOutput(BaseModel):
|
|
45
|
+
image: Any
|
|
46
|
+
|
|
47
|
+
@field_serializer("image")
|
|
48
|
+
def serialize_image(self, image: Any, _info):
|
|
49
|
+
"""
|
|
50
|
+
Serialize a PIL Image into a base64 string.
|
|
51
|
+
Args:
|
|
52
|
+
image (Any): The image object to serialize.
|
|
53
|
+
_info: Metadata passed during serialization (not used here).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: Base64-encoded image string.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
from PIL import Image
|
|
60
|
+
except ImportError:
|
|
61
|
+
raise ImportError("Pillow is required to use the ImageOutput schema. Install it with `pip install Pillow`.")
|
|
62
|
+
|
|
63
|
+
if not isinstance(image, Image.Image):
|
|
64
|
+
raise TypeError(f"Expected a PIL Image, got {type(image)}")
|
|
65
|
+
|
|
66
|
+
# Save the image to a BytesIO buffer
|
|
67
|
+
buffer = BytesIO()
|
|
68
|
+
image.save(buffer, format="PNG") # Default format is PNG
|
|
69
|
+
buffer.seek(0)
|
|
70
|
+
|
|
71
|
+
# Encode the buffer content to base64
|
|
72
|
+
base64_bytes = base64.b64encode(buffer.read())
|
|
73
|
+
|
|
74
|
+
# Decode to string for JSON serialization
|
|
75
|
+
return base64_bytes.decode("utf-8")
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import copy
|
|
16
16
|
import inspect
|
|
17
|
+
import json
|
|
17
18
|
import logging
|
|
18
19
|
import multiprocessing as mp
|
|
19
20
|
import os
|
|
@@ -30,7 +31,7 @@ from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union
|
|
|
30
31
|
|
|
31
32
|
import uvicorn
|
|
32
33
|
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
|
33
|
-
from fastapi.responses import StreamingResponse
|
|
34
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
34
35
|
from fastapi.security import APIKeyHeader
|
|
35
36
|
from starlette.formparsers import MultiPartParser
|
|
36
37
|
from starlette.middleware.gzip import GZipMiddleware
|
|
@@ -39,7 +40,7 @@ from litserve import LitAPI
|
|
|
39
40
|
from litserve.callbacks.base import Callback, CallbackRunner, EventTypes
|
|
40
41
|
from litserve.connector import _Connector
|
|
41
42
|
from litserve.loggers import Logger, _LoggerConnector
|
|
42
|
-
from litserve.loops import inference_worker
|
|
43
|
+
from litserve.loops import _BaseLoop, inference_worker
|
|
43
44
|
from litserve.middlewares import MaxSizeMiddleware, RequestCountMiddleware
|
|
44
45
|
from litserve.python_client import client_template
|
|
45
46
|
from litserve.specs import OpenAISpec
|
|
@@ -105,14 +106,42 @@ class LitServer:
|
|
|
105
106
|
max_batch_size: int = 1,
|
|
106
107
|
batch_timeout: float = 0.0,
|
|
107
108
|
api_path: str = "/predict",
|
|
109
|
+
healthcheck_path: str = "/health",
|
|
110
|
+
info_path: str = "/info",
|
|
111
|
+
model_metadata: Optional[dict] = None,
|
|
108
112
|
stream: bool = False,
|
|
109
113
|
spec: Optional[LitSpec] = None,
|
|
110
114
|
max_payload_size=None,
|
|
111
115
|
track_requests: bool = False,
|
|
116
|
+
loop: Optional[Union[str, _BaseLoop]] = "auto",
|
|
112
117
|
callbacks: Optional[Union[List[Callback], Callback]] = None,
|
|
113
118
|
middlewares: Optional[list[Union[Callable, tuple[Callable, dict]]]] = None,
|
|
114
119
|
loggers: Optional[Union[Logger, List[Logger]]] = None,
|
|
115
120
|
):
|
|
121
|
+
"""Initialize a LitServer instance.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
lit_api: The LitAPI instance to use for handling requests.
|
|
125
|
+
accelerator: The type of hardware accelerator to use (e.g., 'auto', 'cpu', 'cuda', 'mps').
|
|
126
|
+
devices: The number of devices to use (e.g., 'auto', 1, 2).
|
|
127
|
+
workers_per_device: The number of workers to use per device.
|
|
128
|
+
timeout: The timeout for requests in seconds.
|
|
129
|
+
max_batch_size: The maximum batch size.
|
|
130
|
+
batch_timeout: The timeout for batching requests in seconds.
|
|
131
|
+
api_path: The path for the prediction API endpoint.
|
|
132
|
+
healthcheck_path: The path for the health check endpoint.
|
|
133
|
+
info_path: The path for the server and model metadata info endpoint.
|
|
134
|
+
model_metadata: Metadata about the model, it will be shown via the `info_path` endpoint.
|
|
135
|
+
stream: Whether to enable streaming responses.
|
|
136
|
+
spec: The specification for the API such as OpenAISpec or OpenAIEmbeddingSpec.
|
|
137
|
+
max_payload_size: The maximum payload size for requests.
|
|
138
|
+
track_requests: Whether to track the number of active requests.
|
|
139
|
+
loop: The inference engine runs with this loop in the worker process.
|
|
140
|
+
callbacks: Callbacks to use for the server.
|
|
141
|
+
middlewares: ASGI middleware for the server.
|
|
142
|
+
loggers: Loggers to use for the server.
|
|
143
|
+
|
|
144
|
+
"""
|
|
116
145
|
if batch_timeout > timeout and timeout not in (False, -1):
|
|
117
146
|
raise ValueError("batch_timeout must be less than timeout")
|
|
118
147
|
if max_batch_size <= 0:
|
|
@@ -120,6 +149,12 @@ class LitServer:
|
|
|
120
149
|
if isinstance(spec, OpenAISpec):
|
|
121
150
|
stream = True
|
|
122
151
|
|
|
152
|
+
if loop is None:
|
|
153
|
+
loop = "auto"
|
|
154
|
+
|
|
155
|
+
if isinstance(loop, str) and loop != "auto":
|
|
156
|
+
raise ValueError("loop must be an instance of _BaseLoop or 'auto'")
|
|
157
|
+
|
|
123
158
|
if middlewares is None:
|
|
124
159
|
middlewares = []
|
|
125
160
|
if not isinstance(middlewares, list):
|
|
@@ -137,6 +172,22 @@ class LitServer:
|
|
|
137
172
|
"Please provide a valid api path like '/predict', '/classify', or '/v1/predict'"
|
|
138
173
|
)
|
|
139
174
|
|
|
175
|
+
if not healthcheck_path.startswith("/"):
|
|
176
|
+
raise ValueError(
|
|
177
|
+
"healthcheck_path must start with '/'. "
|
|
178
|
+
"Please provide a valid api path like '/health', '/healthcheck', or '/v1/health'"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not info_path.startswith("/"):
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"info_path must start with '/'. Please provide a valid api path like '/info', '/details', or '/v1/info'"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
json.dumps(model_metadata)
|
|
188
|
+
except (TypeError, ValueError):
|
|
189
|
+
raise ValueError("model_metadata must be JSON serializable.")
|
|
190
|
+
|
|
140
191
|
# Check if the batch and unbatch methods are overridden in the lit_api instance
|
|
141
192
|
batch_overridden = lit_api.batch.__code__ is not LitAPI.batch.__code__
|
|
142
193
|
unbatch_overridden = lit_api.unbatch.__code__ is not LitAPI.unbatch.__code__
|
|
@@ -147,10 +198,14 @@ class LitServer:
|
|
|
147
198
|
"but the max_batch_size parameter was not set."
|
|
148
199
|
)
|
|
149
200
|
|
|
201
|
+
self._loop = loop
|
|
150
202
|
self.api_path = api_path
|
|
203
|
+
self.healthcheck_path = healthcheck_path
|
|
204
|
+
self.info_path = info_path
|
|
151
205
|
self.track_requests = track_requests
|
|
206
|
+
self.timeout = timeout
|
|
152
207
|
lit_api.stream = stream
|
|
153
|
-
lit_api.request_timeout = timeout
|
|
208
|
+
lit_api.request_timeout = self.timeout
|
|
154
209
|
lit_api._sanitize(max_batch_size, spec=spec)
|
|
155
210
|
self.app = FastAPI(lifespan=self.lifespan)
|
|
156
211
|
self.app.response_queue_id = None
|
|
@@ -172,6 +227,7 @@ class LitServer:
|
|
|
172
227
|
self.batch_timeout = batch_timeout
|
|
173
228
|
self.stream = stream
|
|
174
229
|
self.max_payload_size = max_payload_size
|
|
230
|
+
self.model_metadata = model_metadata
|
|
175
231
|
self._connector = _Connector(accelerator=accelerator, devices=devices)
|
|
176
232
|
self._callback_runner = CallbackRunner(callbacks)
|
|
177
233
|
|
|
@@ -242,6 +298,7 @@ class LitServer:
|
|
|
242
298
|
self.stream,
|
|
243
299
|
self.workers_setup_status,
|
|
244
300
|
self._callback_runner,
|
|
301
|
+
self._loop,
|
|
245
302
|
),
|
|
246
303
|
)
|
|
247
304
|
process.start()
|
|
@@ -316,7 +373,7 @@ class LitServer:
|
|
|
316
373
|
async def index(request: Request) -> Response:
|
|
317
374
|
return Response(content="litserve running")
|
|
318
375
|
|
|
319
|
-
@self.app.get(
|
|
376
|
+
@self.app.get(self.healthcheck_path, dependencies=[Depends(self.setup_auth())])
|
|
320
377
|
async def health(request: Request) -> Response:
|
|
321
378
|
nonlocal workers_ready
|
|
322
379
|
if not workers_ready:
|
|
@@ -327,6 +384,24 @@ class LitServer:
|
|
|
327
384
|
|
|
328
385
|
return Response(content="not ready", status_code=503)
|
|
329
386
|
|
|
387
|
+
@self.app.get(self.info_path, dependencies=[Depends(self.setup_auth())])
|
|
388
|
+
async def info(request: Request) -> Response:
|
|
389
|
+
return JSONResponse(
|
|
390
|
+
content={
|
|
391
|
+
"model": self.model_metadata,
|
|
392
|
+
"server": {
|
|
393
|
+
"devices": self.devices,
|
|
394
|
+
"workers_per_device": self.workers_per_device,
|
|
395
|
+
"timeout": self.timeout,
|
|
396
|
+
"max_batch_size": self.max_batch_size,
|
|
397
|
+
"batch_timeout": self.batch_timeout,
|
|
398
|
+
"stream": self.stream,
|
|
399
|
+
"max_payload_size": self.max_payload_size,
|
|
400
|
+
"track_requests": self.track_requests,
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
|
|
330
405
|
async def predict(request: self.request_type) -> self.response_type:
|
|
331
406
|
self._callback_runner.trigger_event(
|
|
332
407
|
EventTypes.ON_REQUEST,
|
|
@@ -156,7 +156,8 @@ class ChatCompletionRequest(BaseModel):
|
|
|
156
156
|
temperature: Optional[float] = 0.7
|
|
157
157
|
top_p: Optional[float] = 1.0
|
|
158
158
|
n: Optional[int] = 1
|
|
159
|
-
max_tokens: Optional[int] = None
|
|
159
|
+
max_tokens: Optional[int] = None # Kept for backward compatibility
|
|
160
|
+
max_completion_tokens: Optional[int] = None
|
|
160
161
|
stop: Optional[Union[str, List[str]]] = None
|
|
161
162
|
stream: Optional[bool] = False
|
|
162
163
|
presence_penalty: Optional[float] = 0.0
|
|
@@ -365,7 +366,7 @@ class OpenAISpec(LitSpec):
|
|
|
365
366
|
if request.stream:
|
|
366
367
|
return StreamingResponse(
|
|
367
368
|
self.streaming_completion(request, responses),
|
|
368
|
-
media_type="
|
|
369
|
+
media_type="text/event-stream",
|
|
369
370
|
background=background_tasks,
|
|
370
371
|
)
|
|
371
372
|
|
|
@@ -394,9 +395,9 @@ class OpenAISpec(LitSpec):
|
|
|
394
395
|
|
|
395
396
|
# Only use the last item from encode_response
|
|
396
397
|
usage_info = sum(usage_infos)
|
|
397
|
-
chunk = ChatCompletionChunk(model=model, choices=choices, usage=None)
|
|
398
|
+
chunk = ChatCompletionChunk(model=model, choices=choices, usage=None)
|
|
398
399
|
logger.debug(chunk)
|
|
399
|
-
yield f"data: {chunk}\n\n"
|
|
400
|
+
yield f"data: {chunk.model_dump_json()}\n\n"
|
|
400
401
|
|
|
401
402
|
choices = [
|
|
402
403
|
ChatCompletionStreamingChoice(
|
|
@@ -410,8 +411,8 @@ class OpenAISpec(LitSpec):
|
|
|
410
411
|
model=model,
|
|
411
412
|
choices=choices,
|
|
412
413
|
usage=usage_info,
|
|
413
|
-
)
|
|
414
|
-
yield f"data: {last_chunk}\n\n"
|
|
414
|
+
)
|
|
415
|
+
yield f"data: {last_chunk.model_dump_json()}\n\n"
|
|
415
416
|
yield "data: [DONE]\n\n"
|
|
416
417
|
|
|
417
418
|
async def non_streaming_completion(self, request: ChatCompletionRequest, generator_list: List[AsyncGenerator]):
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import asyncio
|
|
15
|
+
import inspect
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from typing import List, Literal, Optional, Union
|
|
20
|
+
|
|
21
|
+
from fastapi import Request, Response, status
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from litserve.specs.base import LitSpec
|
|
25
|
+
from litserve.utils import LitAPIStatus
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmbeddingRequest(BaseModel):
|
|
31
|
+
input: Union[str, List[str]]
|
|
32
|
+
model: str
|
|
33
|
+
dimensions: Optional[int] = None
|
|
34
|
+
encoding_format: Literal["float"] = "float"
|
|
35
|
+
|
|
36
|
+
def ensure_list(self):
|
|
37
|
+
return self.input if isinstance(self.input, list) else [self.input]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Embedding(BaseModel):
|
|
41
|
+
index: int
|
|
42
|
+
embedding: List[float]
|
|
43
|
+
object: Literal["embedding"] = "embedding"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UsageInfo(BaseModel):
|
|
47
|
+
prompt_tokens: int = 0
|
|
48
|
+
total_tokens: int = 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EmbeddingResponse(BaseModel):
|
|
52
|
+
data: List[Embedding]
|
|
53
|
+
model: str
|
|
54
|
+
object: Literal["list"] = "list"
|
|
55
|
+
usage: UsageInfo
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
EMBEDDING_API_EXAMPLE = """
|
|
59
|
+
Please follow the example below for guidance on how to use the OpenAI Embedding spec:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import numpy as np
|
|
63
|
+
from typing import List
|
|
64
|
+
from litserve import LitAPI, OpenAIEmbeddingSpec
|
|
65
|
+
|
|
66
|
+
class TestAPI(LitAPI):
|
|
67
|
+
def setup(self, device):
|
|
68
|
+
self.model = None
|
|
69
|
+
|
|
70
|
+
def decode_request(self, request) -> List[str]:
|
|
71
|
+
return request.ensure_list()
|
|
72
|
+
|
|
73
|
+
def predict(self, x) -> List[List[float]]:
|
|
74
|
+
return np.random.rand(len(x), 768).tolist()
|
|
75
|
+
|
|
76
|
+
def encode_response(self, output) -> dict:
|
|
77
|
+
return {"embeddings": output}
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
import litserve as ls
|
|
81
|
+
server = ls.LitServer(TestAPI(), spec=OpenAIEmbeddingSpec())
|
|
82
|
+
server.run()
|
|
83
|
+
```
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class OpenAIEmbeddingSpec(LitSpec):
|
|
88
|
+
def __init__(self):
|
|
89
|
+
super().__init__()
|
|
90
|
+
# register the endpoint
|
|
91
|
+
self.add_endpoint("/v1/embeddings", self.embeddings, ["POST"])
|
|
92
|
+
self.add_endpoint("/v1/embeddings", self.options_embeddings, ["GET"])
|
|
93
|
+
|
|
94
|
+
def setup(self, server: "LitServer"): # noqa: F821
|
|
95
|
+
from litserve import LitAPI
|
|
96
|
+
|
|
97
|
+
super().setup(server)
|
|
98
|
+
|
|
99
|
+
lit_api = self._server.lit_api
|
|
100
|
+
if inspect.isgeneratorfunction(lit_api.predict):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"You are using yield in your predict method, which is used for streaming.",
|
|
103
|
+
"OpenAIEmbeddingSpec doesn't support streaming because producing embeddings ",
|
|
104
|
+
"is not a sequential operation.",
|
|
105
|
+
"Please consider replacing yield with return in predict.\n",
|
|
106
|
+
EMBEDDING_API_EXAMPLE,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
is_encode_response_original = lit_api.encode_response.__code__ is LitAPI.encode_response.__code__
|
|
110
|
+
if not is_encode_response_original and inspect.isgeneratorfunction(lit_api.encode_response):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"You are using yield in your encode_response method, which is used for streaming.",
|
|
113
|
+
"OpenAIEmbeddingSpec doesn't support streaming because producing embeddings ",
|
|
114
|
+
"is not a sequential operation.",
|
|
115
|
+
"Please consider replacing yield with return in encode_response.\n",
|
|
116
|
+
EMBEDDING_API_EXAMPLE,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
print("OpenAI Embedding Spec is ready.")
|
|
120
|
+
|
|
121
|
+
def decode_request(self, request: EmbeddingRequest, context_kwargs: Optional[dict] = None) -> List[str]:
|
|
122
|
+
return request.ensure_list()
|
|
123
|
+
|
|
124
|
+
def encode_response(self, output: List[List[float]], context_kwargs: Optional[dict] = None) -> dict:
|
|
125
|
+
usage = {
|
|
126
|
+
"prompt_tokens": context_kwargs.get("prompt_tokens", 0) if context_kwargs else 0,
|
|
127
|
+
"total_tokens": context_kwargs.get("total_tokens", 0) if context_kwargs else 0,
|
|
128
|
+
}
|
|
129
|
+
return {"embeddings": output} | usage
|
|
130
|
+
|
|
131
|
+
def _validate_response(self, response: dict) -> None:
|
|
132
|
+
if not isinstance(response, dict):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Expected response to be a dictionary, but got type {type(response)}.",
|
|
135
|
+
"The response should be a dictionary to ensure proper compatibility with the OpenAIEmbeddingSpec.\n\n"
|
|
136
|
+
"Please ensure that your response is a dictionary with the following keys:\n"
|
|
137
|
+
"- 'embeddings' (required)\n"
|
|
138
|
+
"- 'prompt_tokens' (optional)\n"
|
|
139
|
+
"- 'total_tokens' (optional)\n"
|
|
140
|
+
f"{EMBEDDING_API_EXAMPLE}",
|
|
141
|
+
)
|
|
142
|
+
if "embeddings" not in response:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"The response does not contain the key 'embeddings'."
|
|
145
|
+
"The key 'embeddings' is required to ensure proper compatibility with the OpenAIEmbeddingSpec.\n"
|
|
146
|
+
"Please ensure that your response contains the key 'embeddings'.\n"
|
|
147
|
+
f"{EMBEDDING_API_EXAMPLE}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def embeddings(self, request: EmbeddingRequest):
|
|
151
|
+
response_queue_id = self.response_queue_id
|
|
152
|
+
logger.debug("Received embedding request: %s", request)
|
|
153
|
+
uid = uuid.uuid4()
|
|
154
|
+
event = asyncio.Event()
|
|
155
|
+
self._server.response_buffer[uid] = event
|
|
156
|
+
|
|
157
|
+
self._server.request_queue.put_nowait((response_queue_id, uid, time.monotonic(), request.model_copy()))
|
|
158
|
+
await event.wait()
|
|
159
|
+
|
|
160
|
+
response, status = self._server.response_buffer.pop(uid)
|
|
161
|
+
|
|
162
|
+
if status == LitAPIStatus.ERROR:
|
|
163
|
+
raise response
|
|
164
|
+
|
|
165
|
+
logger.debug(response)
|
|
166
|
+
|
|
167
|
+
self._validate_response(response)
|
|
168
|
+
|
|
169
|
+
usage = UsageInfo(**response)
|
|
170
|
+
data = [Embedding(index=i, embedding=embedding) for i, embedding in enumerate(response["embeddings"])]
|
|
171
|
+
|
|
172
|
+
return EmbeddingResponse(data=data, model=request.model, usage=usage)
|
|
173
|
+
|
|
174
|
+
async def options_embeddings(self, request: Request):
|
|
175
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from litserve.api import LitAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestEmbedAPI(LitAPI):
|
|
9
|
+
def setup(self, device):
|
|
10
|
+
self.model = None
|
|
11
|
+
|
|
12
|
+
def decode_request(self, request) -> List[str]:
|
|
13
|
+
return request.ensure_list()
|
|
14
|
+
|
|
15
|
+
def predict(self, x) -> List[List[float]]:
|
|
16
|
+
return np.random.rand(len(x), 768).tolist()
|
|
17
|
+
|
|
18
|
+
def encode_response(self, output) -> dict:
|
|
19
|
+
return {"embeddings": output}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestEmbedBatchedAPI(TestEmbedAPI):
|
|
23
|
+
def predict(self, batch) -> List[List[List[float]]]:
|
|
24
|
+
return [np.random.rand(len(x), 768).tolist() for x in batch]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestEmbedAPIWithUsage(TestEmbedAPI):
|
|
28
|
+
def encode_response(self, output) -> dict:
|
|
29
|
+
return {"embeddings": output, "prompt_tokens": 10, "total_tokens": 10}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestEmbedAPIWithYieldPredict(TestEmbedAPI):
|
|
33
|
+
def predict(self, x):
|
|
34
|
+
yield from np.random.rand(768).tolist()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestEmbedAPIWithYieldEncodeResponse(TestEmbedAPI):
|
|
38
|
+
def encode_response(self, output):
|
|
39
|
+
yield {"embeddings": output}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestEmbedAPIWithNonDictOutput(TestEmbedAPI):
|
|
43
|
+
def encode_response(self, output):
|
|
44
|
+
return output
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestEmbedAPIWithMissingEmbeddings(TestEmbedAPI):
|
|
48
|
+
def encode_response(self, output):
|
|
49
|
+
return {"output": output}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: litserve
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6.dev0
|
|
4
4
|
Summary: Lightweight AI server.
|
|
5
5
|
Home-page: https://github.com/Lightning-AI/litserve
|
|
6
6
|
Download-URL: https://github.com/Lightning-AI/litserve
|
|
@@ -28,11 +28,11 @@ Requires-Python: >=3.8
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: fastapi>=0.100
|
|
31
|
-
Requires-Dist: httpx
|
|
32
31
|
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
33
32
|
Provides-Extra: test
|
|
34
33
|
Requires-Dist: asgi-lifespan; extra == "test"
|
|
35
34
|
Requires-Dist: coverage[toml]>=7.5.3; extra == "test"
|
|
35
|
+
Requires-Dist: httpx>=0.27.0; extra == "test"
|
|
36
36
|
Requires-Dist: lightning>2.0.0; extra == "test"
|
|
37
37
|
Requires-Dist: mypy==1.11.2; extra == "test"
|
|
38
38
|
Requires-Dist: numpy<2.0; extra == "test"
|
|
@@ -41,6 +41,7 @@ Requires-Dist: pillow; extra == "test"
|
|
|
41
41
|
Requires-Dist: psutil; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
43
43
|
Requires-Dist: pytest-cov; extra == "test"
|
|
44
|
+
Requires-Dist: pytest-retry>=1.6.3; extra == "test"
|
|
44
45
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
45
46
|
Requires-Dist: python-multipart; extra == "test"
|
|
46
47
|
Requires-Dist: requests; extra == "test"
|
|
@@ -208,7 +209,7 @@ Use LitServe to deploy any model or AI service: (Compound AI, Gen AI, classic ML
|
|
|
208
209
|
<a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-background-removal-api-with-litserve">Background Removal</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-controlled-image-generation-api-controlnet">Control Stable Diffusion (ControlNet)</a>
|
|
209
210
|
<strong>Speech:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-a-voice-clone-api-coqui-xtts-v2-model">Text-speech (XTTS V2)</a>, <a target="_blank" href="https://lightning.ai/bhimrajyadav/studios/deploy-a-speech-generation-api-using-parler-tts-powered-by-litserve">Parler-TTS</a>
|
|
210
211
|
<strong>Classical ML:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-random-forest-with-litserve">Random forest</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-xgboost-with-litserve">XGBoost</a>
|
|
211
|
-
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>
|
|
212
|
+
<strong>Miscellaneous:</strong> <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-an-media-conversion-api-with-ffmpeg">Media conversion API (ffmpeg)</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/deploy-both-pytorch-and-tensorflow-in-a-single-api">PyTorch + TensorFlow in one API</a>, <a target="_blank" href="https://lightning.ai/lightning-ai/studios/openai-fault-tolerant-proxy-server">LLM proxy server</a>
|
|
212
213
|
</pre>
|
|
213
214
|
</pre>
|
|
214
215
|
|
|
@@ -26,9 +26,13 @@ src/litserve/callbacks/__init__.py
|
|
|
26
26
|
src/litserve/callbacks/base.py
|
|
27
27
|
src/litserve/callbacks/defaults/__init__.py
|
|
28
28
|
src/litserve/callbacks/defaults/metric_callback.py
|
|
29
|
+
src/litserve/schema/__init__.py
|
|
30
|
+
src/litserve/schema/image.py
|
|
29
31
|
src/litserve/specs/__init__.py
|
|
30
32
|
src/litserve/specs/base.py
|
|
31
33
|
src/litserve/specs/openai.py
|
|
34
|
+
src/litserve/specs/openai_embedding.py
|
|
32
35
|
src/litserve/test_examples/__init__.py
|
|
36
|
+
src/litserve/test_examples/openai_embedding_spec_example.py
|
|
33
37
|
src/litserve/test_examples/openai_spec_example.py
|
|
34
38
|
src/litserve/test_examples/simple_example.py
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
fastapi>=0.100
|
|
2
|
-
httpx
|
|
3
2
|
uvicorn[standard]>=0.29.0
|
|
4
3
|
|
|
5
4
|
[perf]
|
|
@@ -10,6 +9,7 @@ uvloop
|
|
|
10
9
|
[test]
|
|
11
10
|
asgi-lifespan
|
|
12
11
|
coverage[toml]>=7.5.3
|
|
12
|
+
httpx>=0.27.0
|
|
13
13
|
lightning>2.0.0
|
|
14
14
|
mypy==1.11.2
|
|
15
15
|
numpy<2.0
|
|
@@ -18,6 +18,7 @@ pillow
|
|
|
18
18
|
psutil
|
|
19
19
|
pytest-asyncio
|
|
20
20
|
pytest-cov
|
|
21
|
+
pytest-retry>=1.6.3
|
|
21
22
|
pytest>=8.0
|
|
22
23
|
python-multipart
|
|
23
24
|
requests
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/defaults/metric_callback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/openai_spec_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|