From 3d79ba461bb03398987d86fbdeba344497994071 Mon Sep 17 00:00:00 2001 From: T145 Date: Thu, 15 Oct 2026 20:38:44 +0000 Subject: [PATCH] feat(llms): add WebWright provider for the wren-r1 model WebWright exposes an OpenAI-compatible chat completions API at https://webwright.ai/v1 and serves the wren-r1 model. Add a WebwrightChatConfig that builds on the shared OpenAI-like transformation, modeled on the existing hyperbolic provider. - litellm/llms/webwright/chat/transformation.py: base URL and API key handling - register the provider in constants, the LlmProviders enum, the provider config manager, get_llm_provider, and the lazy import registry - tests/unit/llms/webwright/chat: unit tests for provider resolution, provider info, provider lists, supported params, and a mocked completion call --- litellm/__init__.py | 7 ++ litellm/_lazy_imports_registry.py | 5 + litellm/constants.py | 3 + .../get_llm_provider_logic.py | 8 ++ litellm/llms/webwright/__init__.py | 0 litellm/llms/webwright/chat/__init__.py | 0 litellm/llms/webwright/chat/transformation.py | 61 ++++++++++++ litellm/types/utils.py | 1 + litellm/utils.py | 1 + tests/unit/llms/webwright/__init__.py | 0 tests/unit/llms/webwright/chat/__init__.py | 0 .../chat/test_webwright_transformation.py | 99 +++++++++++++++++++ 12 files changed, 185 insertions(+) create mode 100644 litellm/llms/webwright/__init__.py create mode 100644 litellm/llms/webwright/chat/__init__.py create mode 100644 litellm/llms/webwright/chat/transformation.py create mode 100644 tests/unit/llms/webwright/__init__.py create mode 100644 tests/unit/llms/webwright/chat/__init__.py create mode 100644 tests/unit/llms/webwright/chat/test_webwright_transformation.py diff --git a/litellm/__init__.py b/litellm/__init__.py index ff901ede512..7132a34e0c0 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -701,6 +701,7 @@ morph_models: Set = set() lambda_ai_models: Set = set() inception_models: Set = set() hyperbolic_models: Set = set() +webwright_models: Set = set() black_forest_labs_models: Set = set() recraft_models: Set = set() cometapi_models: Set = set() @@ -975,6 +976,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje inception_models.add(key) elif value.get("litellm_provider") == "hyperbolic": hyperbolic_models.add(key) + elif value.get("litellm_provider") == "webwright": + webwright_models.add(key) elif value.get("litellm_provider") == "black_forest_labs": black_forest_labs_models.add(key) elif value.get("litellm_provider") == "recraft": @@ -1252,6 +1255,7 @@ def _build_models_by_provider() -> dict: "lambda_ai": lambda_ai_models, "inception": inception_models, "hyperbolic": hyperbolic_models, + "webwright": webwright_models, "black_forest_labs": black_forest_labs_models, "recraft": recraft_models, "cometapi": cometapi_models, @@ -2213,6 +2217,9 @@ if TYPE_CHECKING: from .llms.hyperbolic.chat.transformation import ( HyperbolicChatConfig as HyperbolicChatConfig, ) + from .llms.webwright.chat.transformation import ( + WebwrightChatConfig as WebwrightChatConfig, + ) from .llms.vercel_ai_gateway.chat.transformation import ( VercelAIGatewayConfig as VercelAIGatewayConfig, ) diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index d47d72ef7a8..6d5883d1b06 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -349,6 +349,7 @@ LLM_CONFIG_NAMES: Final = ( "LambdaAIChatConfig", "InceptionChatConfig", "HyperbolicChatConfig", + "WebwrightChatConfig", "VercelAIGatewayConfig", "EdenAIChatConfig", "EdenAIResponsesAPIConfig", @@ -1302,6 +1303,10 @@ _LLM_CONFIGS_IMPORT_MAP: Final = { ".llms.hyperbolic.chat.transformation", "HyperbolicChatConfig", ), + "WebwrightChatConfig": ( + ".llms.webwright.chat.transformation", + "WebwrightChatConfig", + ), "VercelAIGatewayConfig": ( ".llms.vercel_ai_gateway.chat.transformation", "VercelAIGatewayConfig", diff --git a/litellm/constants.py b/litellm/constants.py index 1f9e81443f7..83bb1708dfc 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1007,6 +1007,7 @@ openai_compatible_endpoints: Final[list] = [ "https://api.lambda.ai/v1", "https://api.inceptionlabs.ai/v1", "https://api.hyperbolic.xyz/v1", + "https://webwright.ai/v1", "https://ai-gateway.helicone.ai/", "https://ai-gateway.vercel.sh/v1", "https://api.edenai.run/v3", @@ -1082,6 +1083,7 @@ openai_compatible_providers: Final[list] = [ "lambda_ai", "inception", "hyperbolic", + "webwright", "vercel_ai_gateway", "aiml", "edenai", @@ -1127,6 +1129,7 @@ openai_text_completion_compatible_providers: Final[list] = [ # providers that s "v0", "lambda_ai", "hyperbolic", + "webwright", "wandb", ] _openai_like_providers: Final[list[str]] = [ diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 74d9344a172..d047c71b55e 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -391,6 +391,9 @@ def get_llm_provider( elif endpoint == "https://api.hyperbolic.xyz/v1": custom_llm_provider = "hyperbolic" dynamic_api_key = get_secret_str("HYPERBOLIC_API_KEY") + elif endpoint == "https://webwright.ai/v1": + custom_llm_provider = "webwright" + dynamic_api_key = get_secret_str("WEBWRIGHT_API_KEY") elif endpoint == "https://ai-gateway.vercel.sh/v1": custom_llm_provider = "vercel_ai_gateway" dynamic_api_key = get_secret_str("VERCEL_AI_GATEWAY_API_KEY") @@ -913,6 +916,11 @@ def _get_openai_compatible_provider_info( api_base, dynamic_api_key, ) = litellm.HyperbolicChatConfig().get_openai_compatible_provider_info(api_base, api_key) + elif custom_llm_provider == "webwright": + ( + api_base, + dynamic_api_key, + ) = litellm.WebwrightChatConfig().get_openai_compatible_provider_info(api_base, api_key) elif custom_llm_provider == "vercel_ai_gateway": ( api_base, diff --git a/litellm/llms/webwright/__init__.py b/litellm/llms/webwright/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/webwright/chat/__init__.py b/litellm/llms/webwright/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/webwright/chat/transformation.py b/litellm/llms/webwright/chat/transformation.py new file mode 100644 index 00000000000..7ac3806ed5c --- /dev/null +++ b/litellm/llms/webwright/chat/transformation.py @@ -0,0 +1,61 @@ +""" +Translate from OpenAI's `/v1/chat/completions` to WebWright's `/v1/chat/completions` +""" + +from typing import Final + +from litellm.secret_managers.main import get_secret_str + +from ...openai_like.chat.transformation import OpenAILikeChatConfig + + +class WebwrightChatConfig(OpenAILikeChatConfig): + """ + WebWright is OpenAI-compatible with standard endpoints. + """ + + @property + def custom_llm_provider(self) -> str | None: + return "webwright" + + def _get_openai_compatible_provider_info( + self, api_base: str | None, api_key: str | None + ) -> tuple[str | None, str | None]: + # WebWright is openai compatible, we just need to set the api_base + api_base = ( + api_base + or get_secret_str("WEBWRIGHT_API_BASE") + or "https://webwright.ai/v1" # Default WebWright API base URL + ) + dynamic_api_key: Final = api_key or get_secret_str("WEBWRIGHT_API_KEY") + return api_base, dynamic_api_key + + def get_openai_compatible_provider_info( + self, + api_base: str | None, + api_key: str | None, + ) -> tuple[str | None, str | None]: + return self._get_openai_compatible_provider_info(api_base, api_key) + + def get_supported_openai_params(self, model: str) -> list: + """ + WebWright supports the standard OpenAI chat completion parameters. + Reference: https://webwright.ai/v1 + """ + return [ + "messages", # Required + "model", # Required + "stream", # Optional + "temperature", # Optional + "top_p", # Optional + "max_tokens", # Optional + "frequency_penalty", # Optional + "presence_penalty", # Optional + "stop", # Optional + "n", # Optional + "tools", # Optional + "tool_choice", # Optional + "response_format", # Optional + "seed", # Optional + "user", # Optional + ] diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5f97c25d3d2..daf89c03ea0 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4342,6 +4342,7 @@ class LlmProviders(str, Enum): MONGODB = "mongodb" HELICONE = "helicone" HYPERBOLIC = "hyperbolic" + WEBWRIGHT = "webwright" RECRAFT = "recraft" FAL_AI = "fal_ai" STABILITY = "stability" diff --git a/litellm/utils.py b/litellm/utils.py index 1bc3b55992c..3425e5b586c 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8762,6 +8762,7 @@ class ProviderConfigManager: LlmProviders.HEROKU: (lambda: litellm.HerokuChatConfig(), False), LlmProviders.OCI: (lambda: litellm.OCIChatConfig(), False), LlmProviders.HYPERBOLIC: (lambda: litellm.HyperbolicChatConfig(), False), + LlmProviders.WEBWRIGHT: (lambda: litellm.WebwrightChatConfig(), False), LlmProviders.OVHCLOUD: (lambda: litellm.OVHCloudChatConfig(), False), LlmProviders.AMAZON_NOVA: (lambda: litellm.AmazonNovaChatConfig(), False), LlmProviders.LANGGRAPH: ( diff --git a/tests/unit/llms/webwright/__init__.py b/tests/unit/llms/webwright/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/webwright/chat/__init__.py b/tests/unit/llms/webwright/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/webwright/chat/test_webwright_transformation.py b/tests/unit/llms/webwright/chat/test_webwright_transformation.py new file mode 100644 index 00000000000..4b272c69ecb --- /dev/null +++ b/tests/unit/llms/webwright/chat/test_webwright_transformation.py @@ -0,0 +1,99 @@ +""" +Unit tests for the WebWright provider configuration. + +WebWright exposes an OpenAI-compatible chat completions API at +https://webwright.ai/v1 and serves the `wren-r1` model, so the provider is a +thin configuration over the shared OpenAI-like transformation. +""" + +import os +from unittest import mock + +import litellm + +from litellm.llms.webwright.chat.transformation import WebwrightChatConfig + + +def test_get_llm_provider_webwright() -> None: + """Test that get_llm_provider correctly identifies WebWright""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + # Test with webwright/model-name format + model, provider, api_key, api_base = get_llm_provider("webwright/wren-r1") + assert model == "wren-r1" + assert provider == "webwright" + + # Test with api_base containing the WebWright endpoint + model, provider, api_key, api_base = get_llm_provider("wren-r1", api_base="https://webwright.ai/v1") + assert model == "wren-r1" + assert provider == "webwright" + assert api_base == "https://webwright.ai/v1" + + +def test_webwright_config_initialization() -> None: + """Test WebwrightChatConfig initializes correctly""" + config = WebwrightChatConfig() + assert config.custom_llm_provider == "webwright" + + +def test_webwright_get_openai_compatible_provider_info() -> None: + """Test WebWright API base and key handling""" + config = WebwrightChatConfig() + + with mock.patch.dict(os.environ, {}, clear=True): + api_base, api_key = config.get_openai_compatible_provider_info(None, None) + assert api_base == "https://webwright.ai/v1" + assert api_key is None + + with mock.patch.dict( + os.environ, + { + "WEBWRIGHT_API_KEY": "env-key", + "WEBWRIGHT_API_BASE": "https://env.webwright.ai/v1", + }, + ): + api_base, api_key = config.get_openai_compatible_provider_info(None, None) + assert api_base == "https://env.webwright.ai/v1" + assert api_key == "env-key" + + # Explicit arguments take precedence over the environment + api_base, api_key = config.get_openai_compatible_provider_info("https://custom.webwright.ai/v1", "param-key") + assert api_base == "https://custom.webwright.ai/v1" + assert api_key == "param-key" + + +def test_webwright_in_provider_lists() -> None: + """Test that webwright is registered in all necessary provider lists""" + assert "webwright" in litellm.openai_compatible_providers + assert "webwright" in litellm.openai_text_completion_compatible_providers + assert "webwright" in litellm.provider_list + assert "https://webwright.ai/v1" in litellm.openai_compatible_endpoints + + +def test_webwright_supported_params() -> None: + """Test that supported OpenAI parameters are correctly configured""" + config = WebwrightChatConfig() + supported_params = config.get_supported_openai_params("webwright/wren-r1") + + for param in ( + "messages", + "model", + "stream", + "temperature", + "max_tokens", + "tools", + "tool_choice", + ): + assert param in supported_params + + +def test_webwright_completion_call() -> None: + """Test the completion call structure for WebWright with a mocked response""" + response = litellm.completion( + model="webwright/wren-r1", + messages=[{"role": "user", "content": "Hello!"}], + api_key="test-key", + mock_response="Hi there!", + ) + assert response is not None + assert response.choices[0].message.content == "Hi there!"