159 lines
6.6 KiB
Python
159 lines
6.6 KiB
Python
# Copyright (c) ModelScope Contributors. All rights reserved.
|
|
import aiohttp
|
|
import json
|
|
from copy import deepcopy
|
|
from dacite import from_dict
|
|
from dataclasses import asdict
|
|
from requests.exceptions import HTTPError
|
|
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union
|
|
|
|
from swift.metrics import Metric
|
|
from .infer_engine import InferEngine
|
|
from .protocol import (ChatCompletionRequest, ChatCompletionResponse, ChatCompletionStreamResponse, InferRequest,
|
|
ModelList, RequestConfig)
|
|
|
|
|
|
class InferClient(InferEngine):
|
|
|
|
def __init__(self,
|
|
host: str = '127.0.0.1',
|
|
port: int = 8000,
|
|
api_key: str = 'EMPTY',
|
|
*,
|
|
base_url: Optional[str] = None,
|
|
timeout: Optional[int] = 86400) -> None:
|
|
"""
|
|
Initialize the InferClient.
|
|
|
|
Args:
|
|
host (str): The hostname of the inference server. Defaults to '127.0.0.1'.
|
|
port (str): The port of the inference server. Defaults to '8000'.
|
|
api_key (str): The API key for authentication. Defaults to 'EMPTY'.
|
|
timeout (Optional[int]): The timeout for requests in seconds. Defaults to None.
|
|
"""
|
|
self.api_key = api_key
|
|
self.host = host
|
|
self.port = port
|
|
self.timeout = timeout
|
|
if base_url is None:
|
|
base_url = f'http://{self.host}:{self.port}/v1'
|
|
self.base_url = base_url
|
|
self._models = None
|
|
|
|
@property
|
|
def models(self):
|
|
if self._models is None:
|
|
models = []
|
|
for model in self.get_model_list().data:
|
|
models.append(model.id)
|
|
assert len(models) > 0, f'models: {models}'
|
|
self._models = models
|
|
return self._models
|
|
|
|
def get_model_list(self) -> ModelList:
|
|
"""Get model list from the inference server.
|
|
"""
|
|
coro = self.get_model_list_async()
|
|
return self.safe_asyncio_run(coro)
|
|
|
|
def _get_request_kwargs(self) -> Dict[str, Any]:
|
|
request_kwargs = {}
|
|
if isinstance(self.timeout, (int, float)) and self.timeout > 0:
|
|
request_kwargs['timeout'] = self.timeout
|
|
if self.api_key is not None:
|
|
request_kwargs['headers'] = {'Authorization': f'Bearer {self.api_key}'}
|
|
return request_kwargs
|
|
|
|
async def get_model_list_async(self) -> ModelList:
|
|
url = f"{self.base_url.rstrip('/')}/models"
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url, **self._get_request_kwargs()) as resp:
|
|
resp_obj = await resp.json()
|
|
return from_dict(ModelList, resp_obj)
|
|
|
|
def infer(
|
|
self,
|
|
infer_requests: List[InferRequest],
|
|
request_config: Optional[RequestConfig] = None,
|
|
metrics: Optional[List[Metric]] = None,
|
|
*,
|
|
model: Optional[str] = None,
|
|
use_tqdm: Optional[bool] = None
|
|
) -> List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
|
|
"""
|
|
Perform inference using the specified model.
|
|
|
|
Args:
|
|
infer_requests (List[InferRequest]): A list of inference requests.
|
|
request_config (Optional[RequestConfig]): Configuration for the request. Defaults to None.
|
|
metrics (Optional[List[Metric]]): The usage information to return. Defaults to None.
|
|
model (Optional[str]): The model name to be used for inference. Defaults to None.
|
|
use_tqdm (Optional[bool]): Whether to use tqdm for progress tracking. Defaults to None.
|
|
|
|
Returns:
|
|
List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
|
|
The inference responses or an iterator of streaming responses.
|
|
"""
|
|
return super().infer(infer_requests, request_config, metrics, model=model, use_tqdm=use_tqdm)
|
|
|
|
@staticmethod
|
|
def _prepare_request_data(model: str, infer_request: InferRequest, request_config: RequestConfig) -> Dict[str, Any]:
|
|
if not isinstance(infer_request, dict):
|
|
infer_request = asdict(infer_request)
|
|
res = asdict(ChatCompletionRequest(model, **infer_request, **asdict(request_config)))
|
|
# ignore empty
|
|
empty_request = ChatCompletionRequest('', [])
|
|
for k in list(res.keys()):
|
|
if res[k] == getattr(empty_request, k):
|
|
res.pop(k)
|
|
return res
|
|
|
|
@staticmethod
|
|
def _parse_stream_data(data: bytes) -> Optional[str]:
|
|
data = data.decode(encoding='utf-8')
|
|
data = data.strip()
|
|
if len(data) == 0:
|
|
return
|
|
assert data.startswith('data:'), f'data: {data}'
|
|
return data[5:].strip()
|
|
|
|
async def infer_async(
|
|
self,
|
|
infer_request: InferRequest,
|
|
request_config: Optional[RequestConfig] = None,
|
|
*,
|
|
model: Optional[str] = None,
|
|
) -> Union[ChatCompletionResponse, AsyncIterator[ChatCompletionStreamResponse]]:
|
|
request_config = deepcopy(request_config or RequestConfig())
|
|
if model is None:
|
|
if len(self.models) == 1:
|
|
model = self.models[0]
|
|
else:
|
|
raise ValueError(f'Please explicitly specify the model. Available models: {self.models}.')
|
|
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
|
|
|
request_data = self._prepare_request_data(model, infer_request, request_config)
|
|
if request_config.stream:
|
|
|
|
async def _gen_stream() -> AsyncIterator[ChatCompletionStreamResponse]:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(url, json=request_data, **self._get_request_kwargs()) as resp:
|
|
async for data in resp.content:
|
|
data = self._parse_stream_data(data)
|
|
if data == '[DONE]':
|
|
break
|
|
if data is not None:
|
|
resp_obj = json.loads(data)
|
|
if resp_obj['object'] == 'error':
|
|
raise HTTPError(resp_obj['message'])
|
|
yield from_dict(ChatCompletionStreamResponse, resp_obj)
|
|
|
|
return _gen_stream()
|
|
else:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(url, json=request_data, **self._get_request_kwargs()) as resp:
|
|
resp_obj = await resp.json()
|
|
if resp_obj['object'] == 'error':
|
|
raise HTTPError(resp_obj['message'])
|
|
return from_dict(ChatCompletionResponse, resp_obj)
|