diff --git a/packages/exchange/src/exchange/providers/anthropic.py b/packages/exchange/src/exchange/providers/anthropic.py index 9f4b72d7..b4da68de 100644 --- a/packages/exchange/src/exchange/providers/anthropic.py +++ b/packages/exchange/src/exchange/providers/anthropic.py @@ -9,8 +9,6 @@ from exchange.providers.utils import retry_if_status, raise_for_status from exchange.langfuse_wrapper import observe_wrapper -ANTHROPIC_HOST = "https://api.anthropic.com/v1/messages" - retry_procedure = retry( wait=wait_fixed(2), stop=stop_after_attempt(2), @@ -23,6 +21,8 @@ class AnthropicProvider(Provider): """Provides chat completions for models hosted directly by Anthropic.""" PROVIDER_NAME = "anthropic" + BASE_URL_ENV_VAR = "ANTHROPIC_HOST" + BASE_URL_DEFAULT = "https://api.anthropic.com/v1/messages" REQUIRED_ENV_VARS = ["ANTHROPIC_API_KEY"] def __init__(self, client: httpx.Client) -> None: @@ -31,7 +31,7 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["AnthropicProvider"]) -> "AnthropicProvider": cls.check_env_vars() - url = os.environ.get("ANTHROPIC_HOST", ANTHROPIC_HOST) + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) key = os.environ.get("ANTHROPIC_API_KEY") client = httpx.Client( base_url=url, @@ -164,5 +164,5 @@ def recommended_models() -> tuple[str, str]: @retry_procedure def _post(self, payload: dict) -> httpx.Response: - response = self.client.post(ANTHROPIC_HOST, json=payload) + response = self.client.post(self.BASE_URL_DEFAULT, json=payload) return raise_for_status(response).json() diff --git a/packages/exchange/src/exchange/providers/azure.py b/packages/exchange/src/exchange/providers/azure.py index fa8814f3..6119f43d 100644 --- a/packages/exchange/src/exchange/providers/azure.py +++ b/packages/exchange/src/exchange/providers/azure.py @@ -8,8 +8,8 @@ class AzureProvider(OpenAiProvider): """Provides chat completions for models hosted by the Azure OpenAI Service.""" PROVIDER_NAME = "azure" + BASE_URL_ENV_VAR = "AZURE_CHAT_COMPLETIONS_HOST_NAME" REQUIRED_ENV_VARS = [ - "AZURE_CHAT_COMPLETIONS_HOST_NAME", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION", "AZURE_CHAT_COMPLETIONS_KEY", @@ -21,13 +21,13 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["AzureProvider"]) -> "AzureProvider": cls.check_env_vars() - url = os.environ.get("AZURE_CHAT_COMPLETIONS_HOST_NAME") + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) deployment_name = os.environ.get("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME") api_version = os.environ.get("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION") key = os.environ.get("AZURE_CHAT_COMPLETIONS_KEY") # format the url host/"openai/deployments/" + deployment_name + "/?api-version=" + api_version - url = f"{url}/openai/deployments/{deployment_name}/" + url = url.join(f"/openai/deployments/{deployment_name}/") client = httpx.Client( base_url=url, headers={"api-key": key, "Content-Type": "application/json"}, diff --git a/packages/exchange/src/exchange/providers/base.py b/packages/exchange/src/exchange/providers/base.py index 3ff17591..1fee0992 100644 --- a/packages/exchange/src/exchange/providers/base.py +++ b/packages/exchange/src/exchange/providers/base.py @@ -1,3 +1,4 @@ +import httpx import os from abc import ABC, abstractmethod from attrs import define, field @@ -22,6 +23,8 @@ def __init__(self, provider_cls: str) -> None: class Provider(ABC): PROVIDER_NAME: str + BASE_URL_ENV_VAR: str = "" + BASE_URL_DEFAULT: str = "" REQUIRED_ENV_VARS: list[str] = [] @classmethod @@ -32,11 +35,23 @@ def from_env(cls: type["Provider"]) -> "Provider": @classmethod def check_env_vars(cls: type["Provider"], instructions_url: Optional[str] = None) -> None: + provider = cls.PROVIDER_NAME missing_vars = [x for x in cls.REQUIRED_ENV_VARS if x not in os.environ] + url_var = cls.BASE_URL_ENV_VAR + if url_var: + val = os.environ.get(url_var, cls.BASE_URL_DEFAULT) + if not val: + raise KeyError(url_var) + else: + url = httpx.URL(val) + + if url.scheme not in ["http", "https"]: + raise ValueError(f"Expected {url_var} to be a 'http' or 'https' url: {val}") + if missing_vars: env_vars = ", ".join(missing_vars) - raise MissingProviderEnvVariableError(env_vars, cls.PROVIDER_NAME, instructions_url) + raise MissingProviderEnvVariableError(env_vars, provider, instructions_url) @abstractmethod def complete( diff --git a/packages/exchange/src/exchange/providers/databricks.py b/packages/exchange/src/exchange/providers/databricks.py index 517ccee6..3dc6f536 100644 --- a/packages/exchange/src/exchange/providers/databricks.py +++ b/packages/exchange/src/exchange/providers/databricks.py @@ -31,10 +31,8 @@ class DatabricksProvider(Provider): """ PROVIDER_NAME = "databricks" - REQUIRED_ENV_VARS = [ - "DATABRICKS_HOST", - "DATABRICKS_TOKEN", - ] + BASE_URL_ENV_VAR = "DATABRICKS_HOST" + REQUIRED_ENV_VARS = ["DATABRICKS_TOKEN"] instructions_url = "https://docs.databricks.com/en/dev-tools/auth/index.html#general-host-token-and-account-id-environment-variables-and-fields" def __init__(self, client: httpx.Client) -> None: @@ -43,7 +41,7 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["DatabricksProvider"]) -> "DatabricksProvider": cls.check_env_vars(cls.instructions_url) - url = os.environ.get("DATABRICKS_HOST") + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR)) key = os.environ.get("DATABRICKS_TOKEN") client = httpx.Client( base_url=url, diff --git a/packages/exchange/src/exchange/providers/google.py b/packages/exchange/src/exchange/providers/google.py index bfb1faf0..cb8c2c80 100644 --- a/packages/exchange/src/exchange/providers/google.py +++ b/packages/exchange/src/exchange/providers/google.py @@ -9,9 +9,6 @@ from exchange.providers.utils import raise_for_status, retry_if_status, encode_image from exchange.langfuse_wrapper import observe_wrapper - -GOOGLE_HOST = "https://generativelanguage.googleapis.com/v1beta" - retry_procedure = retry( wait=wait_fixed(2), stop=stop_after_attempt(2), @@ -24,6 +21,8 @@ class GoogleProvider(Provider): """Provides chat completions for models hosted by Google, including Gemini and other experimental models.""" PROVIDER_NAME = "google" + BASE_URL_ENV_VAR = "GOOGLE_HOST" + BASE_URL_DEFAULT = "https://generativelanguage.googleapis.com/v1beta" REQUIRED_ENV_VARS = ["GOOGLE_API_KEY"] instructions_url = "https://ai.google.dev/gemini-api/docs/api-key" @@ -33,7 +32,7 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["GoogleProvider"]) -> "GoogleProvider": cls.check_env_vars(cls.instructions_url) - url = os.environ.get("GOOGLE_HOST", GOOGLE_HOST) + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) key = os.environ.get("GOOGLE_API_KEY") client = httpx.Client( base_url=url, diff --git a/packages/exchange/src/exchange/providers/groq.py b/packages/exchange/src/exchange/providers/groq.py index 0f6472f8..a4d7ce9c 100644 --- a/packages/exchange/src/exchange/providers/groq.py +++ b/packages/exchange/src/exchange/providers/groq.py @@ -16,8 +16,6 @@ from tenacity import retry, wait_fixed, stop_after_attempt from exchange.providers.utils import retry_if_status -GROQ_HOST = "https://api.groq.com/openai/" - retry_procedure = retry( wait=wait_fixed(5), stop=stop_after_attempt(5), @@ -30,6 +28,8 @@ class GroqProvider(Provider): """Provides chat completions for models hosted directly by OpenAI.""" PROVIDER_NAME = "groq" + BASE_URL_ENV_VAR = "GROQ_HOST" + BASE_URL_DEFAULT = "https://api.groq.com/openai/" REQUIRED_ENV_VARS = ["GROQ_API_KEY"] instructions_url = "https://console.groq.com/docs/quickstart" @@ -39,7 +39,7 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["GroqProvider"]) -> "GroqProvider": cls.check_env_vars(cls.instructions_url) - url = os.environ.get("GROQ_HOST", GROQ_HOST) + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) key = os.environ.get("GROQ_API_KEY") client = httpx.Client( diff --git a/packages/exchange/src/exchange/providers/ollama.py b/packages/exchange/src/exchange/providers/ollama.py index 23079315..01816b53 100644 --- a/packages/exchange/src/exchange/providers/ollama.py +++ b/packages/exchange/src/exchange/providers/ollama.py @@ -1,10 +1,9 @@ import os - import httpx +from typing import Type from exchange.providers.openai import OpenAiProvider -OLLAMA_HOST = "http://localhost:11434/" OLLAMA_MODEL = "qwen2.5" @@ -25,14 +24,18 @@ class OllamaProvider(OpenAiProvider): requires: {{}} """ PROVIDER_NAME = "ollama" + BASE_URL_ENV_VAR = "OLLAMA_HOST" + BASE_URL_DEFAULT = "http://localhost:11434/" + REQUIRED_ENV_VARS = [] def __init__(self, client: httpx.Client) -> None: print("PLEASE NOTE: the ollama provider is experimental, use with care") super().__init__(client) @classmethod - def from_env(cls: type["OllamaProvider"]) -> "OllamaProvider": - ollama_url = os.environ.get("OLLAMA_HOST", OLLAMA_HOST) + def from_env(cls: Type["OllamaProvider"]) -> "OllamaProvider": + cls.check_env_vars(cls.instructions_url) + ollama_url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) timeout = httpx.Timeout(60 * 10) # from_env is expected to fail if required ENV variables are not @@ -41,7 +44,7 @@ def from_env(cls: type["OllamaProvider"]) -> "OllamaProvider": httpx.get(ollama_url, timeout=timeout) # When served by Ollama, the OpenAI API is available at the path "v1/". - client = httpx.Client(base_url=ollama_url + "v1/", timeout=timeout) + client = httpx.Client(base_url=ollama_url.join("v1/"), timeout=timeout) return cls(client) @staticmethod diff --git a/packages/exchange/src/exchange/providers/openai.py b/packages/exchange/src/exchange/providers/openai.py index 8701e542..700453fd 100644 --- a/packages/exchange/src/exchange/providers/openai.py +++ b/packages/exchange/src/exchange/providers/openai.py @@ -16,7 +16,6 @@ from exchange.providers.utils import retry_if_status from exchange.langfuse_wrapper import observe_wrapper -OPENAI_HOST = "https://api.openai.com/" retry_procedure = retry( wait=wait_fixed(2), @@ -30,6 +29,8 @@ class OpenAiProvider(Provider): """Provides chat completions for models hosted directly by OpenAI.""" PROVIDER_NAME = "openai" + BASE_URL_ENV_VAR = "OPENAI_HOST" + BASE_URL_DEFAULT = "https://api.openai.com/" REQUIRED_ENV_VARS = ["OPENAI_API_KEY"] instructions_url = "https://platform.openai.com/docs/api-reference/api-keys" @@ -39,11 +40,11 @@ def __init__(self, client: httpx.Client) -> None: @classmethod def from_env(cls: type["OpenAiProvider"]) -> "OpenAiProvider": cls.check_env_vars(cls.instructions_url) - url = os.environ.get("OPENAI_HOST", OPENAI_HOST) + url = httpx.URL(os.environ.get(cls.BASE_URL_ENV_VAR, cls.BASE_URL_DEFAULT)) key = os.environ.get("OPENAI_API_KEY") client = httpx.Client( - base_url=url + "v1/", + base_url=url.join("v1/"), auth=("Bearer", key), timeout=httpx.Timeout(60 * 10), ) diff --git a/packages/exchange/tests/providers/test_anthropic.py b/packages/exchange/tests/providers/test_anthropic.py index c269d706..1c47ad75 100644 --- a/packages/exchange/tests/providers/test_anthropic.py +++ b/packages/exchange/tests/providers/test_anthropic.py @@ -26,6 +26,14 @@ def anthropic_provider(): return AnthropicProvider.from_env() +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("ANTHROPIC_HOST", "localhost:1234") + monkeypatch.setenv("ANTHROPIC_API_KEY", "test_api_key") + + with pytest.raises(ValueError, match="Expected ANTHROPIC_HOST to be a 'http' or 'https' url: localhost:1234"): + AnthropicProvider.from_env() + + def test_from_env_throw_error_when_missing_api_key(): with patch.dict(os.environ, {}, clear=True): with pytest.raises(MissingProviderEnvVariableError) as context: diff --git a/packages/exchange/tests/providers/test_azure.py b/packages/exchange/tests/providers/test_azure.py index 5a701473..1915fe1b 100644 --- a/packages/exchange/tests/providers/test_azure.py +++ b/packages/exchange/tests/providers/test_azure.py @@ -11,10 +11,21 @@ AZURE_MODEL = os.getenv("AZURE_MODEL", "gpt-4o-mini") +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("AZURE_CHAT_COMPLETIONS_HOST_NAME", "localhost:1234") + monkeypatch.setenv("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME", "test_deployment_name") + monkeypatch.setenv("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION", "test_api_version") + monkeypatch.setenv("AZURE_CHAT_COMPLETIONS_KEY", "test_api_key") + + with pytest.raises( + ValueError, match="Expected AZURE_CHAT_COMPLETIONS_HOST_NAME to be a 'http' or 'https' url: localhost:1234" + ): + AzureProvider.from_env() + + @pytest.mark.parametrize( "env_var_name", [ - "AZURE_CHAT_COMPLETIONS_HOST_NAME", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION", "AZURE_CHAT_COMPLETIONS_KEY", @@ -24,7 +35,6 @@ def test_from_env_throw_error_when_missing_env_var(env_var_name): with patch.dict( os.environ, { - "AZURE_CHAT_COMPLETIONS_HOST_NAME": "test_host_name", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME": "test_deployment_name", "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION": "test_api_version", "AZURE_CHAT_COMPLETIONS_KEY": "test_api_key", diff --git a/packages/exchange/tests/providers/test_base.py b/packages/exchange/tests/providers/test_base.py new file mode 100644 index 00000000..d6661d36 --- /dev/null +++ b/packages/exchange/tests/providers/test_base.py @@ -0,0 +1,101 @@ +import pytest + +from exchange.providers.base import MissingProviderEnvVariableError, Provider + + +def test_missing_provider_env_variable_error_without_instructions_url(): + env_variable = "API_KEY" + provider = "TestProvider" + error = MissingProviderEnvVariableError(env_variable, provider) + + assert error.env_variable == env_variable + assert error.provider == provider + assert error.instructions_url is None + assert error.message == "Missing environment variables: API_KEY for provider TestProvider." + + +def test_missing_provider_env_variable_error_with_instructions_url(): + env_variable = "API_KEY" + provider = "TestProvider" + instructions_url = "http://example.com/instructions" + error = MissingProviderEnvVariableError(env_variable, provider, instructions_url) + + assert error.env_variable == env_variable + assert error.provider == provider + assert error.instructions_url == instructions_url + assert error.message == ( + "Missing environment variables: API_KEY for provider TestProvider.\n" + "Please see http://example.com/instructions for instructions" + ) + + +class TestProvider(Provider): + PROVIDER_NAME = "test_provider" + REQUIRED_ENV_VARS = [] + + def complete(self, model, system, messages, tools, **kwargs): + pass + + +class TestProviderBaseURL(Provider): + PROVIDER_NAME = "test_provider_base_url" + BASE_URL_ENV_VAR = "TEST_PROVIDER_BASE_URL" + REQUIRED_ENV_VARS = [] + + def complete(self, model, system, messages, tools, **kwargs): + pass + + +class TestProviderBaseURLDefault(Provider): + PROVIDER_NAME = "test_provider_base_url_default" + BASE_URL_ENV_VAR = "TEST_PROVIDER_BASE_URL_DEFAULT" + BASE_URL_DEFAULT = "http://localhost:11434/" + REQUIRED_ENV_VARS = [] + + def complete(self, model, system, messages, tools, **kwargs): + pass + + +def test_check_env_vars_no_base_url(): + TestProvider.check_env_vars() + + +def test_check_env_vars_base_url_valid_http(monkeypatch): + monkeypatch.setenv(TestProviderBaseURL.BASE_URL_ENV_VAR, "http://localhost:11434/") + + TestProviderBaseURL.check_env_vars() + + +def test_check_env_vars_base_url_valid_https(monkeypatch): + monkeypatch.setenv(TestProviderBaseURL.BASE_URL_ENV_VAR, "https://localhost:11434/v1") + + TestProviderBaseURL.check_env_vars() + + +def test_check_env_vars_base_url_default(): + TestProviderBaseURLDefault.check_env_vars() + + +def test_check_env_vars_base_url_throw_error_when_empty(monkeypatch): + monkeypatch.setenv(TestProviderBaseURL.BASE_URL_ENV_VAR, "") + + with pytest.raises(KeyError, match="TEST_PROVIDER_BASE_URL"): + TestProviderBaseURL.check_env_vars() + + +def test_check_env_vars_base_url_throw_error_when_missing_schemes(monkeypatch): + monkeypatch.setenv(TestProviderBaseURL.BASE_URL_ENV_VAR, "localhost:11434") + + with pytest.raises( + ValueError, match="Expected TEST_PROVIDER_BASE_URL to be a 'http' or 'https' url: localhost:11434" + ): + TestProviderBaseURL.check_env_vars() + + +def test_check_env_vars_base_url_throw_error_when_invalid_scheme(monkeypatch): + monkeypatch.setenv(TestProviderBaseURL.BASE_URL_ENV_VAR, "ftp://localhost:11434/v1") + + with pytest.raises( + ValueError, match="Expected TEST_PROVIDER_BASE_URL to be a 'http' or 'https' url: ftp://localhost:11434/v1" + ): + TestProviderBaseURL.check_env_vars() diff --git a/packages/exchange/tests/providers/test_databricks.py b/packages/exchange/tests/providers/test_databricks.py index 0b2729dc..e62bc490 100644 --- a/packages/exchange/tests/providers/test_databricks.py +++ b/packages/exchange/tests/providers/test_databricks.py @@ -7,10 +7,17 @@ from exchange.providers.databricks import DatabricksProvider +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("DATABRICKS_HOST", "localhost:1234") + monkeypatch.setenv("DATABRICKS_TOKEN", "test_token") + + with pytest.raises(ValueError, match="Expected DATABRICKS_HOST to be a 'http' or 'https' url: localhost:1234"): + DatabricksProvider.from_env() + + @pytest.mark.parametrize( "env_var_name", [ - "DATABRICKS_HOST", "DATABRICKS_TOKEN", ], ) @@ -18,7 +25,7 @@ def test_from_env_throw_error_when_missing_env_var(env_var_name): with patch.dict( os.environ, { - "DATABRICKS_HOST": "test_host", + "DATABRICKS_HOST": "http://test-host", "DATABRICKS_TOKEN": "test_token", }, clear=True, diff --git a/packages/exchange/tests/providers/test_google.py b/packages/exchange/tests/providers/test_google.py index 3e1028a0..b3f735c4 100644 --- a/packages/exchange/tests/providers/test_google.py +++ b/packages/exchange/tests/providers/test_google.py @@ -12,6 +12,14 @@ GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-1.5-flash") +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("GOOGLE_HOST", "localhost:1234") + monkeypatch.setenv("GOOGLE_API_KEY", "test_api_key") + + with pytest.raises(ValueError, match="Expected GOOGLE_HOST to be a 'http' or 'https' url: localhost:1234"): + GoogleProvider.from_env() + + def example_fn(param: str) -> None: """ Testing function. diff --git a/packages/exchange/tests/providers/test_ollama.py b/packages/exchange/tests/providers/test_ollama.py index b1df70d5..c2eafeb8 100644 --- a/packages/exchange/tests/providers/test_ollama.py +++ b/packages/exchange/tests/providers/test_ollama.py @@ -9,6 +9,13 @@ OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", OLLAMA_MODEL) +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("OLLAMA_HOST", "localhost:1234") + + with pytest.raises(ValueError, match="Expected OLLAMA_HOST to be a 'http' or 'https' url: localhost:1234"): + OllamaProvider.from_env() + + @pytest.mark.vcr() def test_ollama_complete(): reply_message, reply_usage = complete(OllamaProvider, OLLAMA_MODEL) diff --git a/packages/exchange/tests/providers/test_openai.py b/packages/exchange/tests/providers/test_openai.py index e29dadc5..8cd44194 100644 --- a/packages/exchange/tests/providers/test_openai.py +++ b/packages/exchange/tests/providers/test_openai.py @@ -10,6 +10,14 @@ OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") +def test_from_env_throw_error_when_invalid_host(monkeypatch): + monkeypatch.setenv("OPENAI_HOST", "localhost:1234") + monkeypatch.setenv("OPENAI_API_KEY", "test_api_key") + + with pytest.raises(ValueError, match="Expected OPENAI_HOST to be a 'http' or 'https' url: localhost:1234"): + OpenAiProvider.from_env() + + def test_from_env_throw_error_when_missing_api_key(): with patch.dict(os.environ, {}, clear=True): with pytest.raises(MissingProviderEnvVariableError) as context: diff --git a/packages/exchange/tests/test_base.py b/packages/exchange/tests/test_base.py deleted file mode 100644 index 46baaebb..00000000 --- a/packages/exchange/tests/test_base.py +++ /dev/null @@ -1,27 +0,0 @@ -from exchange.providers.base import MissingProviderEnvVariableError - - -def test_missing_provider_env_variable_error_without_instructions_url(): - env_variable = "API_KEY" - provider = "TestProvider" - error = MissingProviderEnvVariableError(env_variable, provider) - - assert error.env_variable == env_variable - assert error.provider == provider - assert error.instructions_url is None - assert error.message == "Missing environment variables: API_KEY for provider TestProvider." - - -def test_missing_provider_env_variable_error_with_instructions_url(): - env_variable = "API_KEY" - provider = "TestProvider" - instructions_url = "http://example.com/instructions" - error = MissingProviderEnvVariableError(env_variable, provider, instructions_url) - - assert error.env_variable == env_variable - assert error.provider == provider - assert error.instructions_url == instructions_url - assert error.message == ( - "Missing environment variables: API_KEY for provider TestProvider.\n" - "Please see http://example.com/instructions for instructions" - )