From e067bbe99544475dea6e86528f8bff18f8436792 Mon Sep 17 00:00:00 2001 From: "Gerard@msft" <99283778+gjwoods@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:45:09 -0700 Subject: [PATCH] New PF Tool: Open_Source_llm - Dev, Test, & Documentation (#531) # Description The open_source_llm PromptFlow tool. This tool allows users to access their Online Endpoint deployment of the Falcon, LlaMa-2, Dolly, and GTP-2 models from the Azure Machine Learning Model Catalog. More models to This PR includes the full functionality of the open_source_llm tool, along with quality assurance tests, documentation, and supporting common code. Screenshots for the tools appearance in VsCode and local test pass results follow. # All Promptflow Contribution checklist: - [x] **The pull request does not introduce [breaking changes]** - [x] **CHANGELOG is updated for new features, bug fixes or other significant changes.** - [x] **I have read the [contribution guidelines](../CONTRIBUTING.md).** ## General Guidelines and Best Practices - [x] Title of the pull request is clear and informative. - [x] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md). ### Testing Guidelines - [x] Pull request includes test coverage for the included changes. ### Screen Shot in VsCode image ### Tests image --------- Co-authored-by: chjinche <49483542+chjinche@users.noreply.github.com> --- .cspell.json | 3 +- .../open_source_llm_on_vscode_promptflow.png | Bin 0 -> 71130 bytes docs/reference/index.md | 1 + .../tools-reference/open_source_llm_tool.md | 65 +++ src/promptflow-tools/connections.json.example | 12 + .../promptflow/tools/common.py | 9 +- .../promptflow/tools/exception.py | 21 + .../promptflow/tools/open_source_llm.py | 415 ++++++++++++++++++ .../tools/yamls/open_source_llm.yaml | 25 ++ src/promptflow-tools/tests/conftest.py | 14 +- .../tests/test_open_source_llm.py | 180 ++++++++ 11 files changed, 739 insertions(+), 6 deletions(-) create mode 100644 docs/media/reference/tools-reference/open_source_llm_on_vscode_promptflow.png create mode 100644 docs/reference/tools-reference/open_source_llm_tool.md create mode 100644 src/promptflow-tools/promptflow/tools/open_source_llm.py create mode 100644 src/promptflow-tools/promptflow/tools/yamls/open_source_llm.yaml create mode 100644 src/promptflow-tools/tests/test_open_source_llm.py diff --git a/.cspell.json b/.cspell.json index dace721b1c5..54b272ce998 100644 --- a/.cspell.json +++ b/.cspell.json @@ -127,7 +127,8 @@ "pysqlite", "AADSTS700082", "levelno", - "LANCZOS" + "LANCZOS", + "Mobius" ], "allowCompoundWords": true } diff --git a/docs/media/reference/tools-reference/open_source_llm_on_vscode_promptflow.png b/docs/media/reference/tools-reference/open_source_llm_on_vscode_promptflow.png new file mode 100644 index 0000000000000000000000000000000000000000..8d6afabb69d2ecee09aa85a38211b332d70a39c1 GIT binary patch literal 71130 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 str: + response_json = json.loads(output) + try: + if response_key is None: + return response_json[0] + else: + return response_json[0][response_key] + except KeyError as e: + if response_key is None: + message = f"""Expected the response to fit the following schema: +`[ + +]` +Instead, received {response_json} and access failed at key `{e}`. +""" + else: + message = f"""Expected the response to fit the following schema: +`[ + {{ + "{response_key}": + }} +]` +Instead, received {response_json} and access failed at key `{e}`. +""" + raise OpenSourceLLMUserError(message=message) + + +class ModelFamily(str, Enum): + LLAMA = "LLaMa" + DOLLY = "Dolly" + GPT2 = "GPT-2" + FALCON = "Falcon" + + +class API(str, Enum): + CHAT = "chat" + COMPLETION = "completion" + + +class ContentFormatterBase: + """Transform request and response of AzureML endpoint to match with + required schema. + """ + + content_type: Optional[str] = "application/json" + """The MIME type of the input data passed to the endpoint""" + + accepts: Optional[str] = "application/json" + """The MIME type of the response data returned from the endpoint""" + + @staticmethod + def escape_special_characters(prompt: str) -> str: + """Escapes any special characters in `prompt`""" + return re.sub( + r'\\([\\\"a-zA-Z])', + r'\\\1', + prompt) + + @abstractmethod + def format_request_payload(self, prompt: str, model_kwargs: Dict) -> str: + """Formats the request body according to the input schema of + the model. Returns bytes or seekable file like object in the + format specified in the content_type request header. + """ + + @abstractmethod + def format_response_payload(self, output: bytes) -> str: + """Formats the response body according to the output + schema of the model. Returns the data type that is + received from the response. + """ + + +class GPT2ContentFormatter(ContentFormatterBase): + """Content handler for LLMs from the OSS catalog.""" + + def format_request_payload(self, prompt: str, model_kwargs: Dict) -> str: + input_str = json.dumps( + { + "inputs": {"input_string": [ContentFormatterBase.escape_special_characters(prompt)]}, + "parameters": model_kwargs, + } + ) + return input_str + + def format_response_payload(self, output: bytes) -> str: + return format_generic_response_payload(output, response_key="0") + + +class HFContentFormatter(ContentFormatterBase): + """Content handler for LLMs from the HuggingFace catalog.""" + + def format_request_payload(self, prompt: str, model_kwargs: Dict) -> str: + input_str = json.dumps( + { + "inputs": [ContentFormatterBase.escape_special_characters(prompt)], + "parameters": model_kwargs, + } + ) + return input_str + + def format_response_payload(self, output: bytes) -> str: + return format_generic_response_payload(output, response_key="generated_text") + + +class DollyContentFormatter(ContentFormatterBase): + """Content handler for the Dolly-v2-12b model""" + + def format_request_payload(self, prompt: str, model_kwargs: Dict) -> str: + input_str = json.dumps( + { + "input_data": {"input_string": [ContentFormatterBase.escape_special_characters(prompt)]}, + "parameters": model_kwargs, + } + ) + return input_str + + def format_response_payload(self, output: bytes) -> str: + return format_generic_response_payload(output, response_key=None) + + +class LlamaContentFormatter(ContentFormatterBase): + """Content formatter for LLaMa""" + + def __init__(self, api: API, chat_history: Optional[str] = ""): + super().__init__() + self.api = api + self.chat_history = chat_history + + @staticmethod + def parse_chat(chat_str: str) -> List[Dict[str, str]]: + # LLaMa only supports below roles. + separator = r"(?i)\n*(system|user|assistant)\s*:\s*\n" + chunks = re.split(separator, chat_str) + + # remove any empty chunks + chunks = [c.strip() for c in chunks if c.strip()] + + chat_list = [] + for index in range(0, len(chunks), 2): + role = chunks[index].lower() + + # Check if prompt follows chat api message format and has valid role. + try: + validate_role(role, valid_llama_roles) + except ChatAPIInvalidRole as e: + raise OpenSourceLLMUserError(message=e.message) + + if len(chunks) <= index + 1: + message = "Unexpected chat format. Please ensure the query matches the chat format of the model used." + raise OpenSourceLLMUserError(message=message) + + chat_list.append({ + "role": role, + "content": chunks[index+1] + }) + + return chat_list + + def format_request_payload(self, prompt: str, model_kwargs: Dict) -> str: + """Formats the request according the the chosen api""" + if "do_sample" not in model_kwargs: + model_kwargs["do_sample"] = True + + if self.api == API.CHAT: + prompt_value = LlamaContentFormatter.parse_chat(self.chat_history) + else: + prompt_value = [ContentFormatterBase.escape_special_characters(prompt)] + + return json.dumps( + { + "input_data": + { + "input_string": prompt_value, + "parameters": model_kwargs + } + } + ) + + def format_response_payload(self, output: bytes) -> str: + """Formats response""" + response_json = json.loads(output) + + if self.api == API.CHAT and "output" in response_json: + return response_json["output"] + elif self.api == API.COMPLETION and len(response_json) > 0 and "0" in response_json[0]: + return response_json[0]["0"] + else: + error_message = f"Unexpected response format. Response: {response_json}" + print(error_message, file=sys.stderr) + raise OpenSourceLLMOnlineEndpointError(message=error_message) + + +class ContentFormatterFactory: + """Factory class for supported models""" + + def get_content_formatter( + model_family: ModelFamily, api: API, chat_history: Optional[List[Dict]] = [] + ) -> ContentFormatterBase: + if model_family == ModelFamily.LLAMA: + return LlamaContentFormatter(chat_history=chat_history, api=api) + elif model_family == ModelFamily.DOLLY: + return DollyContentFormatter() + elif model_family == ModelFamily.GPT2: + return GPT2ContentFormatter() + elif model_family == ModelFamily.FALCON: + return HFContentFormatter() + + +class AzureMLOnlineEndpoint: + """Azure ML Online Endpoint models. + + Example: + .. code-block:: python + + azure_llm = AzureMLModel( + endpoint_url="https://..inference.ml.azure.com/score", + endpoint_api_key="my-api-key", + content_formatter=content_formatter, + ) + """ # noqa: E501 + + endpoint_url: str = "" + """URL of pre-existing Endpoint. Should be passed to constructor or specified as + env var `AZUREML_ENDPOINT_URL`.""" + + endpoint_api_key: str = "" + """Authentication Key for Endpoint. Should be passed to constructor or specified as + env var `AZUREML_ENDPOINT_API_KEY`.""" + + content_formatter: Any = None + """The content formatter that provides an input and output + transform function to handle formats between the LLM and + the endpoint""" + + model_kwargs: Optional[Dict] = None + """Key word arguments to pass to the model.""" + + def __init__( + self, + endpoint_url: str, + endpoint_api_key: str, + content_formatter: ContentFormatterBase, + deployment_name: Optional[str] = None, + model_kwargs: Optional[Dict] = None, + ): + self.endpoint_url = endpoint_url + self.endpoint_api_key = endpoint_api_key + self.deployment_name = deployment_name + self.content_formatter = content_formatter + self.model_kwargs = model_kwargs + + @property + def _identifying_params(self) -> Mapping[str, Any]: + """Get the identifying parameters.""" + _model_kwargs = self.model_kwargs or {} + return { + **{"model_kwargs": _model_kwargs}, + } + + @property + def _llm_type(self) -> str: + """Return type of llm.""" + return "azureml_endpoint" + + def _call_endpoint(self, body: bytes) -> bytes: + """call.""" + + headers = {"Content-Type": "application/json", "Authorization": ("Bearer " + self.endpoint_api_key)} + + # If this is not set it'll use the default deployment on the endpoint. + if self.deployment_name is not None: + headers["azureml-model-deployment"] = self.deployment_name + + req = urllib.request.Request(self.endpoint_url, body, headers) + response = urllib.request.urlopen(req, timeout=50) + result = response.read() + return result + + def __call__( + self, + prompt: str + ) -> str: + """Call out to an AzureML Managed Online endpoint. + Args: + prompt: The prompt to pass into the model. + stop: Optional list of stop words to use when generating. + Returns: + The string generated by the model. + Example: + .. code-block:: python + response = azureml_model("Tell me a joke.") + """ + _model_kwargs = self.model_kwargs or {} + + body = self.content_formatter.format_request_payload(prompt, _model_kwargs) + endpoint_request = str.encode(body) + endpoint_response = self._call_endpoint(endpoint_request) + response = self.content_formatter.format_response_payload(endpoint_response) + return response + + +class OpenSourceLLM(ToolProvider): + REQUIRED_CONFIG_KEYS = ["endpoint_url", "model_family"] + REQUIRED_SECRET_KEYS = ["endpoint_api_key"] + + def __init__(self, connection: CustomConnection): + super().__init__() + + conn_dict = dict(connection) + for key in self.REQUIRED_CONFIG_KEYS: + if key not in conn_dict: + accepted_keys = ",".join([key for key in self.REQUIRED_CONFIG_KEYS]) + raise OpenSourceLLMKeyValidationError( + message=f"""Required key `{key}` not found in given custom connection. +Required keys are: {accepted_keys}.""" + ) + for key in self.REQUIRED_SECRET_KEYS: + if key not in conn_dict: + accepted_keys = ",".join([key for key in self.REQUIRED_SECRET_KEYS]) + raise OpenSourceLLMKeyValidationError( + message=f"""Required secret key `{key}` not found in given custom connection. +Required keys are: {accepted_keys}.""" + ) + try: + self.model_family = ModelFamily[connection.configs['model_family']] + except KeyError: + accepted_models = ",".join([model.name for model in ModelFamily]) + raise OpenSourceLLMKeyValidationError( + message=f"""Given model_family '{connection.configs['model_family']}' not recognized. +Supported models are: {accepted_models}.""" + ) + self.connection = connection + + @tool + @handle_oneline_endpoint_error() + def call( + self, + prompt: PromptTemplate, + api: API, + deployment_name: Optional[str] = None, + model_kwargs: Optional[Dict] = {}, + **kwargs + ) -> str: + prompt = render_jinja_template(prompt, trim_blocks=True, keep_trailing_newline=True, **kwargs) + + content_formatter = ContentFormatterFactory.get_content_formatter( + model_family=self.model_family, + api=api, + chat_history=prompt + ) + + llm = AzureMLOnlineEndpoint( + endpoint_url=self.connection.configs['endpoint_url'], + endpoint_api_key=self.connection.secrets['endpoint_api_key'], + content_formatter=content_formatter, + deployment_name=deployment_name, + model_kwargs=model_kwargs + ) + + def _do_llm(llm: AzureMLOnlineEndpoint, prompt: str) -> str: + return llm(prompt) + + return _do_llm(llm, prompt) diff --git a/src/promptflow-tools/promptflow/tools/yamls/open_source_llm.yaml b/src/promptflow-tools/promptflow/tools/yamls/open_source_llm.yaml new file mode 100644 index 00000000000..f5bbd809164 --- /dev/null +++ b/src/promptflow-tools/promptflow/tools/yamls/open_source_llm.yaml @@ -0,0 +1,25 @@ +promptflow.tools.open_source_llm.OpenSourceLLM.call: + name: Open Source LLM + description: Use an Open Source model from the Azure Model catalog, deployed to an AzureML Online Endpoint for LLM Chat or Completion API calls. + type: custom_llm + module: promptflow.tools.open_source_llm + class_name: OpenSourceLLM + function: call + inputs: + connection: + type: + - CustomConnection + api: + enum: + - chat + - completion + type: + - string + deployment_name: + default: null + type: + - string + model_kwargs: + default: "{}" + type: + - object diff --git a/src/promptflow-tools/tests/conftest.py b/src/promptflow-tools/tests/conftest.py index 044f4c7bd0d..013ce5a25c3 100644 --- a/src/promptflow-tools/tests/conftest.py +++ b/src/promptflow-tools/tests/conftest.py @@ -8,6 +8,7 @@ # Avoid circular dependencies: Use import 'from promptflow._internal' instead of 'from promptflow' # since the code here is in promptflow namespace as well from promptflow._internal import ConnectionManager +from promptflow.connections import CustomConnection, OpenAIConnection, SerpConnection from promptflow.tools.aoai import AzureOpenAI PROMOTFLOW_ROOT = Path(__file__).absolute().parents[1] @@ -44,6 +45,11 @@ def serp_connection(): return ConnectionManager().get("serp_connection") +@pytest.fixture +def gpt2_custom_connection(): + return ConnectionManager().get("gpt2_connection") + + @pytest.fixture(autouse=True) def skip_if_no_key(request, mocker): mocker.patch.dict(os.environ, {"PROMPTFLOW_CONNECTIONS": CONNECTION_FILE}) @@ -51,8 +57,12 @@ def skip_if_no_key(request, mocker): conn_name = request.node.get_closest_marker('skip_if_no_key').args[0] connection = request.getfixturevalue(conn_name) # if dummy placeholder key, skip. - if "-api-key" in connection.api_key: - pytest.skip('skipped because no key') + if isinstance(connection, OpenAIConnection) or isinstance(connection, SerpConnection): + if "-api-key" in connection.api_key: + pytest.skip('skipped because no key') + elif isinstance(connection, CustomConnection): + if "endpoint_api_key" not in connection.secrets or "-api-key" in connection.secrets["endpoint_api_key"]: + pytest.skip('skipped because no key') # example prompts diff --git a/src/promptflow-tools/tests/test_open_source_llm.py b/src/promptflow-tools/tests/test_open_source_llm.py new file mode 100644 index 00000000000..6b75e54dac2 --- /dev/null +++ b/src/promptflow-tools/tests/test_open_source_llm.py @@ -0,0 +1,180 @@ +import pytest +from promptflow.tools.exception import ( + OpenSourceLLMOnlineEndpointError, + OpenSourceLLMUserError, + OpenSourceLLMKeyValidationError +) +from promptflow.tools.open_source_llm import OpenSourceLLM, API, ContentFormatterBase, LlamaContentFormatter + + +@pytest.fixture +def gpt2_provider(gpt2_custom_connection) -> OpenSourceLLM: + return OpenSourceLLM(gpt2_custom_connection) + + +@pytest.mark.usefixtures("use_secrets_config_file") +class TestOpenSourceLLM: + completion_prompt = "In the context of Azure ML, what does the ML stand for?" + chat_prompt = """user: +You are a AI which helps Customers answer questions. + +user: +""" + completion_prompt + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_completion(self, gpt2_provider): + response = gpt2_provider.call( + self.completion_prompt, + API.COMPLETION) + assert len(response) > 25 + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_completion_with_deploy(self, gpt2_provider): + response = gpt2_provider.call( + self.completion_prompt, + API.COMPLETION, + deployment_name="gpt2-8") + assert len(response) > 25 + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_chat(self, gpt2_provider): + response = gpt2_provider.call( + self.chat_prompt, + API.CHAT) + assert len(response) > 25 + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_chat_with_deploy(self, gpt2_provider): + response = gpt2_provider.call( + self.chat_prompt, + API.CHAT, + deployment_name="gpt2-8") + assert len(response) > 25 + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_con_url_chat(self, gpt2_custom_connection): + del gpt2_custom_connection.configs['endpoint_url'] + with pytest.raises(OpenSourceLLMKeyValidationError) as exc_info: + os = OpenSourceLLM(gpt2_custom_connection) + os.call(self.chat_prompt, API.CHAT) + assert exc_info.value.message == """Required key `endpoint_url` not found in given custom connection. +Required keys are: endpoint_url,model_family.""" + assert exc_info.value.error_codes == "UserError/ToolValidationError/OpenSourceLLMKeyValidationError".split("/") + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_con_key_chat(self, gpt2_custom_connection): + del gpt2_custom_connection.secrets['endpoint_api_key'] + with pytest.raises(OpenSourceLLMKeyValidationError) as exc_info: + os = OpenSourceLLM(gpt2_custom_connection) + os.call(self.chat_prompt, API.CHAT) + assert exc_info.value.message == ( + "Required secret key `endpoint_api_key` " + + """not found in given custom connection. +Required keys are: endpoint_api_key.""") + assert exc_info.value.error_codes == "UserError/ToolValidationError/OpenSourceLLMKeyValidationError".split("/") + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_con_model_chat(self, gpt2_custom_connection): + del gpt2_custom_connection.configs['model_family'] + with pytest.raises(OpenSourceLLMKeyValidationError) as exc_info: + os = OpenSourceLLM(gpt2_custom_connection) + os.call(self.completion_prompt, API.COMPLETION) + assert exc_info.value.message == """Required key `model_family` not found in given custom connection. +Required keys are: endpoint_url,model_family.""" + assert exc_info.value.error_codes == "UserError/ToolValidationError/OpenSourceLLMKeyValidationError".split("/") + + def test_open_source_llm_escape_chat(self): + danger = r"The quick \brown fox\tjumped\\over \the \\boy\r\n" + out_of_danger = ContentFormatterBase.escape_special_characters(danger) + assert out_of_danger == "The quick \\brown fox\\tjumped\\\\over \\the \\\\boy\\r\\n" + + def test_open_source_llm_llama_parse_chat_with_chat(self): + LlamaContentFormatter.parse_chat(self.chat_prompt) + + def test_open_source_llm_llama_parse_multi_turn(self): + multi_turn_chat = """user: +You are a AI which helps Customers answer questions. + +What is the best movie of all time? + +assistant: +Mobius, which starred Jared Leto + +user: +Why was that the greatest movie of all time? +""" + LlamaContentFormatter.parse_chat(multi_turn_chat) + + def test_open_source_llm_llama_parse_system_not_accepted(self): + bad_chat_prompt = """system: +You are a AI which helps Customers answer questions. + +user: +""" + self.completion_prompt + with pytest.raises(OpenSourceLLMUserError) as exc_info: + LlamaContentFormatter.parse_chat(bad_chat_prompt) + assert exc_info.value.message == ( + "The Chat API requires a specific format for prompt definition," + + " and the prompt should include separate lines as role delimiters: 'assistant:\\n','user:\\n'." + + " Current parsed role 'system' does not meet the requirement. If you intend to use the Completion " + + "API, please select the appropriate API type and deployment name. If you do intend to use the Chat " + + "API, please refer to the guideline at https://aka.ms/pfdoc/chat-prompt or view the samples in our " + + "gallery that contain 'Chat' in the name.") + assert exc_info.value.error_codes == "UserError/OpenSourceLLMUserError".split("/") + + def test_open_source_llm_llama_parse_ignore_whitespace(self): + bad_chat_prompt = f"""system: +You are a AI which helps Customers answer questions. + +user: + +user: +{self.completion_prompt}""" + with pytest.raises(OpenSourceLLMUserError) as exc_info: + LlamaContentFormatter.parse_chat(bad_chat_prompt) + assert exc_info.value.message == ( + "The Chat API requires a specific format for prompt definition, and " + + "the prompt should include separate lines as role delimiters: 'assistant:\\n','user:\\n'. Current parsed " + + "role 'system' does not meet the requirement. If you intend to use the Completion API, please select the " + + "appropriate API type and deployment name. If you do intend to use the Chat API, please refer to the " + + "guideline at https://aka.ms/pfdoc/chat-prompt or view the samples in our gallery that contain 'Chat' " + + "in the name.") + assert exc_info.value.error_codes == "UserError/OpenSourceLLMUserError".split("/") + + def test_open_source_llm_llama_parse_chat_with_comp(self): + with pytest.raises(OpenSourceLLMUserError) as exc_info: + LlamaContentFormatter.parse_chat(self.completion_prompt) + assert exc_info.value.message == ( + "The Chat API requires a specific format for prompt definition, and " + + "the prompt should include separate lines as role delimiters: 'assistant:\\n','user:\\n'. Current parsed " + + "role 'in the context of azure ml, what does the ml stand for?' does not meet the requirement. If you " + + "intend to use the Completion API, please select the appropriate API type and deployment name. If you do " + + "intend to use the Chat API, please refer to the guideline at https://aka.ms/pfdoc/chat-prompt or view " + + "the samples in our gallery that contain 'Chat' in the name.") + assert exc_info.value.error_codes == "UserError/OpenSourceLLMUserError".split("/") + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_llama_endpoint_miss(self, gpt2_custom_connection): + gpt2_custom_connection.configs['endpoint_url'] += 'completely/real/endpoint' + os = OpenSourceLLM(gpt2_custom_connection) + with pytest.raises(OpenSourceLLMOnlineEndpointError) as exc_info: + os.call( + self.completion_prompt, + API.COMPLETION) + assert exc_info.value.message == ( + "Exception hit calling Oneline Endpoint: " + + "HTTPError: HTTP Error 424: Failed Dependency") + assert exc_info.value.error_codes == "UserError/OpenSourceLLMOnlineEndpointError".split("/") + + @pytest.mark.skip_if_no_key("gpt2_custom_connection") + def test_open_source_llm_llama_deployment_miss(self, gpt2_custom_connection): + os = OpenSourceLLM(gpt2_custom_connection) + with pytest.raises(OpenSourceLLMOnlineEndpointError) as exc_info: + os.call( + self.completion_prompt, + API.COMPLETION, + deployment_name="completely/real/deployment-007") + assert exc_info.value.message == ( + "Exception hit calling Oneline Endpoint: " + + "HTTPError: HTTP Error 404: Not Found") + assert exc_info.value.error_codes == "UserError/OpenSourceLLMOnlineEndpointError".split("/")