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.
Files changed (41) hide show
  1. {litserve-0.2.5.dev0/src/litserve.egg-info → litserve-0.2.6.dev0}/PKG-INFO +4 -3
  2. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/README.md +1 -1
  3. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/requirements.txt +0 -1
  4. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__about__.py +1 -1
  5. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__init__.py +12 -2
  6. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/connector.py +15 -0
  7. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/docker_builder.py +1 -1
  8. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/loops.py +210 -15
  9. litserve-0.2.6.dev0/src/litserve/schema/__init__.py +0 -0
  10. litserve-0.2.6.dev0/src/litserve/schema/image.py +75 -0
  11. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/server.py +79 -4
  12. litserve-0.2.6.dev0/src/litserve/specs/__init__.py +4 -0
  13. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/specs/openai.py +7 -6
  14. litserve-0.2.6.dev0/src/litserve/specs/openai_embedding.py +175 -0
  15. litserve-0.2.6.dev0/src/litserve/test_examples/openai_embedding_spec_example.py +49 -0
  16. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0/src/litserve.egg-info}/PKG-INFO +4 -3
  17. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/SOURCES.txt +4 -0
  18. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/requires.txt +2 -1
  19. litserve-0.2.5.dev0/src/litserve/specs/__init__.py +0 -3
  20. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/LICENSE +0 -0
  21. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/MANIFEST.in +0 -0
  22. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/setup.cfg +0 -0
  23. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/setup.py +0 -0
  24. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/__main__.py +0 -0
  25. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/api.py +0 -0
  26. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/__init__.py +0 -0
  27. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/base.py +0 -0
  28. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/defaults/__init__.py +0 -0
  29. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/callbacks/defaults/metric_callback.py +0 -0
  30. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/loggers.py +0 -0
  31. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/middlewares.py +0 -0
  32. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/python_client.py +0 -0
  33. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/specs/base.py +0 -0
  34. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/__init__.py +0 -0
  35. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/openai_spec_example.py +0 -0
  36. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/test_examples/simple_example.py +0 -0
  37. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve/utils.py +0 -0
  38. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/dependency_links.txt +0 -0
  39. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/entry_points.txt +0 -0
  40. {litserve-0.2.5.dev0 → litserve-0.2.6.dev0}/src/litserve.egg-info/not-zip-safe +0 -0
  41. {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.5.dev0
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
 
@@ -1,3 +1,2 @@
1
1
  fastapi >=0.100
2
- httpx
3
2
  uvicorn[standard] >=0.29.0
@@ -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.5.dev0"
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.openai import OpenAISpec
20
+ from litserve.specs import OpenAIEmbeddingSpec, OpenAISpec
21
21
 
22
- __all__ = ["LitAPI", "LitServer", "Request", "Response", "OpenAISpec", "test_examples", "Callback", "Logger"]
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-cudnn-runtime-ubuntu20.04
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
- logger.info(f"Setup complete for worker {worker_id}.")
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 max_batch_size > 1:
432
- run_batched_loop(
433
- lit_api, lit_spec, request_queue, response_queues, max_batch_size, batch_timeout, callback_runner
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
- else:
436
- run_single_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
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("/health", dependencies=[Depends(self.setup_auth())])
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,
@@ -0,0 +1,4 @@
1
+ from litserve.specs.openai import OpenAISpec
2
+ from litserve.specs.openai_embedding import OpenAIEmbeddingSpec
3
+
4
+ __all__ = ["OpenAISpec", "OpenAIEmbeddingSpec"]
@@ -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="application/x-ndjson",
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).json()
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
- ).json()
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.5.dev0
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
@@ -1,3 +0,0 @@
1
- from litserve.specs.openai import OpenAISpec
2
-
3
- __all__ = ["OpenAISpec"]
File without changes
File without changes
File without changes
File without changes