mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
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
This commit is contained in:
parent
ad83e3d3c8
commit
3d79ba461b
12 changed files with 185 additions and 0 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]] = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
0
litellm/llms/webwright/__init__.py
Normal file
0
litellm/llms/webwright/__init__.py
Normal file
0
litellm/llms/webwright/chat/__init__.py
Normal file
0
litellm/llms/webwright/chat/__init__.py
Normal file
61
litellm/llms/webwright/chat/transformation.py
Normal file
61
litellm/llms/webwright/chat/transformation.py
Normal file
|
|
@ -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
|
||||
]
|
||||
|
|
@ -4342,6 +4342,7 @@ class LlmProviders(str, Enum):
|
|||
MONGODB = "mongodb"
|
||||
HELICONE = "helicone"
|
||||
HYPERBOLIC = "hyperbolic"
|
||||
WEBWRIGHT = "webwright"
|
||||
RECRAFT = "recraft"
|
||||
FAL_AI = "fal_ai"
|
||||
STABILITY = "stability"
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
|
|
|
|||
0
tests/unit/llms/webwright/__init__.py
Normal file
0
tests/unit/llms/webwright/__init__.py
Normal file
0
tests/unit/llms/webwright/chat/__init__.py
Normal file
0
tests/unit/llms/webwright/chat/__init__.py
Normal file
|
|
@ -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!"
|
||||
Loading…
Add table
Reference in a new issue