258 lines
8.9 KiB
Python
258 lines
8.9 KiB
Python
import json
|
|
import os
|
|
from typing import Dict, List, Optional, Tuple # noqa: UP035
|
|
|
|
import pytest
|
|
import regex
|
|
import requests
|
|
|
|
OPENAI_V1_CHAT_COMPLETION_URL = "http://127.0.0.1:8001/v1/chat/completions"
|
|
|
|
JSON_TOKEN_PATTERN = (
|
|
r"((-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?)|null|true|false|"
|
|
r'("((\\["\\\/bfnrt])|(\\u[0-9a-fA-F]{4})|[^"\\\x00-\x1f])*")'
|
|
)
|
|
JSON_TOKEN_RE = regex.compile(JSON_TOKEN_PATTERN)
|
|
|
|
|
|
def is_json_or_json_prefix(s: str) -> bool:
|
|
try:
|
|
json.loads(s)
|
|
return True
|
|
except json.JSONDecodeError as e:
|
|
# If the JSON decoder reaches the end of s, it is a prefix of a JSON string.
|
|
if e.pos == len(s):
|
|
return True
|
|
# Since json.loads is token-based instead of char-based, there may remain half a token after
|
|
# the matching position.
|
|
# If the left part is a prefix of a valid JSON token, the output is also valid
|
|
regex_match = JSON_TOKEN_RE.fullmatch(s[e.pos :], partial=True)
|
|
return regex_match is not None
|
|
|
|
|
|
def check_openai_nonstream_response(
|
|
response: Dict, # noqa: UP006
|
|
*,
|
|
is_chat_completion: bool,
|
|
model: str,
|
|
object_str: str,
|
|
num_choices: int,
|
|
finish_reasons: List[str], # noqa: UP006
|
|
completion_tokens: Optional[int] = None,
|
|
echo_prompt: Optional[str] = None,
|
|
suffix: Optional[str] = None,
|
|
stop: Optional[List[str]] = None, # noqa: UP006
|
|
require_substr: Optional[List[str]] = None, # noqa: UP006
|
|
json_mode: bool = False,
|
|
):
|
|
assert response["model"] == model
|
|
assert response["object"] == object_str
|
|
|
|
choices = response["choices"]
|
|
assert isinstance(choices, list)
|
|
assert len(choices) <= num_choices
|
|
texts: List[str] = ["" for _ in range(num_choices)] # noqa: UP006
|
|
for choice in choices:
|
|
idx = choice["index"]
|
|
assert choice["finish_reason"] in finish_reasons
|
|
|
|
if not is_chat_completion:
|
|
assert isinstance(choice["text"], str)
|
|
texts[idx] = choice["text"]
|
|
if echo_prompt is not None:
|
|
assert texts[idx]
|
|
if suffix is not None:
|
|
assert texts[idx]
|
|
else:
|
|
message = choice["message"]
|
|
assert message["role"] == "assistant"
|
|
assert isinstance(message["content"], str)
|
|
texts[idx] = message["content"]
|
|
|
|
if stop is not None:
|
|
for stop_str in stop:
|
|
assert stop_str not in texts[idx]
|
|
if require_substr is not None:
|
|
for substr in require_substr:
|
|
assert substr in texts[idx]
|
|
if json_mode:
|
|
assert is_json_or_json_prefix(texts[idx])
|
|
|
|
usage = response["usage"]
|
|
assert isinstance(usage, dict)
|
|
assert usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"]
|
|
assert usage["prompt_tokens"] > 0
|
|
if completion_tokens is not None:
|
|
assert usage["completion_tokens"] == completion_tokens
|
|
|
|
|
|
def check_openai_stream_response(
|
|
responses: List[Dict], # noqa: UP006
|
|
*,
|
|
is_chat_completion: bool,
|
|
model: str,
|
|
object_str: str,
|
|
num_choices: int,
|
|
finish_reasons: List[str], # noqa: UP006
|
|
completion_tokens: Optional[int] = None,
|
|
echo_prompt: Optional[str] = None,
|
|
suffix: Optional[str] = None,
|
|
stop: Optional[List[str]] = None, # noqa: UP006
|
|
require_substr: Optional[List[str]] = None, # noqa: UP006
|
|
json_mode: bool = False,
|
|
):
|
|
assert len(responses) > 0
|
|
|
|
finished = [False for _ in range(num_choices)]
|
|
outputs = ["" for _ in range(num_choices)]
|
|
for response in responses:
|
|
assert response["model"] == model
|
|
assert response["object"] == object_str
|
|
|
|
choices = response["choices"]
|
|
assert isinstance(choices, list)
|
|
assert len(choices) <= num_choices
|
|
for choice in choices:
|
|
idx = choice["index"]
|
|
|
|
if not is_chat_completion:
|
|
assert isinstance(choice["text"], str)
|
|
outputs[idx] += choice["text"]
|
|
else:
|
|
delta = choice["delta"]
|
|
assert delta["role"] == "assistant"
|
|
assert isinstance(delta["content"], str)
|
|
outputs[idx] += delta["content"]
|
|
|
|
if finished[idx]:
|
|
assert choice["finish_reason"] in finish_reasons
|
|
elif choice["finish_reason"] is not None:
|
|
assert choice["finish_reason"] in finish_reasons
|
|
finished[idx] = True
|
|
|
|
if not is_chat_completion:
|
|
usage = response["usage"]
|
|
assert isinstance(usage, dict)
|
|
assert usage["total_tokens"] == usage["prompt_tokens"] + usage["completion_tokens"]
|
|
assert usage["prompt_tokens"] > 0
|
|
if completion_tokens is not None:
|
|
assert usage["completion_tokens"] <= completion_tokens
|
|
|
|
if not is_chat_completion:
|
|
if completion_tokens is not None:
|
|
assert responses[-1]["usage"]["completion_tokens"] == completion_tokens
|
|
|
|
for i, output in enumerate(outputs):
|
|
if echo_prompt is not None:
|
|
assert output.startswith(echo_prompt)
|
|
if suffix is not None:
|
|
assert output.endswith(suffix)
|
|
if stop is not None:
|
|
for stop_str in stop:
|
|
assert stop_str not in output
|
|
if require_substr is not None:
|
|
for substr in require_substr:
|
|
assert substr in output
|
|
if json_mode:
|
|
assert is_json_or_json_prefix(output)
|
|
|
|
|
|
CHAT_COMPLETION_MESSAGES = [
|
|
# messages #0
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "image_url",
|
|
"image_url": "https://llava-vl.github.io/static/images/view.jpg",
|
|
},
|
|
{"type": "text", "text": "What does this image represent?"},
|
|
],
|
|
},
|
|
],
|
|
# messages #1
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "image_url",
|
|
"image_url": "https://llava-vl.github.io/static/images/view.jpg",
|
|
},
|
|
{"type": "text", "text": "What does this image represent?"},
|
|
],
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "The image represents a serene and peaceful scene of a pier extending over a body of water, such as a lake or a river.er. The pier is made of wood and has a bench on it, providing a place for people to sit and enjoy the view. The pier is situated in a natural environment, surrounded by trees and mountains in the background. This setting creates a tranquil atmosphere, inviting visitors to relax and appreciate the beauty of the landscape.", # noqa: E501
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "What country is the image set in? Give me 10 ranked guesses and reasons why.", # noqa: E501
|
|
},
|
|
],
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("stream", [False, True])
|
|
@pytest.mark.parametrize("messages", CHAT_COMPLETION_MESSAGES)
|
|
def test_openai_v1_chat_completions(
|
|
served_model: Tuple[str, str], # noqa: UP006
|
|
launch_server,
|
|
stream: bool,
|
|
messages: List[Dict[str, str]], # noqa: UP006
|
|
):
|
|
# `served_model` and `launch_server` are pytest fixtures
|
|
# defined in conftest.py.
|
|
|
|
payload = {
|
|
"model": served_model[0],
|
|
"messages": messages,
|
|
"stream": stream,
|
|
}
|
|
response = requests.post(OPENAI_V1_CHAT_COMPLETION_URL, json=payload, timeout=180)
|
|
if not stream:
|
|
check_openai_nonstream_response(
|
|
response.json(),
|
|
is_chat_completion=True,
|
|
model=served_model[0],
|
|
object_str="chat.completion",
|
|
num_choices=1,
|
|
finish_reasons=["stop"],
|
|
)
|
|
else:
|
|
responses = []
|
|
for chunk in response.iter_lines(chunk_size=512):
|
|
if not chunk or chunk == b"data: [DONE]":
|
|
continue
|
|
responses.append(json.loads(chunk.decode("utf-8")[6:]))
|
|
check_openai_stream_response(
|
|
responses,
|
|
is_chat_completion=True,
|
|
model=served_model[0],
|
|
object_str="chat.completion.chunk",
|
|
num_choices=1,
|
|
finish_reasons=["stop"],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
model_lib = os.environ.get("MLC_SERVE_MODEL_LIB")
|
|
if model_lib is None:
|
|
raise ValueError(
|
|
'Environment variable "MLC_SERVE_MODEL_LIB" not found. '
|
|
"Please set it to model lib compiled by MLC LLM "
|
|
"(e.g., `dist/Llama-2-7b-chat-hf-q0f16-MLC/Llama-2-7b-chat-hf-q0f16-MLC-cuda.so`)."
|
|
)
|
|
|
|
model = os.environ.get("MLC_SERVE_MODEL")
|
|
if model is None:
|
|
MODEL = (os.path.dirname(model_lib), model_lib)
|
|
else:
|
|
MODEL = (model, model_lib)
|
|
|
|
for msg in CHAT_COMPLETION_MESSAGES:
|
|
test_openai_v1_chat_completions(MODEL, None, stream=False, messages=msg)
|
|
test_openai_v1_chat_completions(MODEL, None, stream=True, messages=msg)
|