This commit is contained in:
T145 2026-11-05 19:44:59 +00:00 • committed by GitHub
commit a7c0de8e1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 237 additions and 0 deletions

View file

@ -384,6 +384,7 @@ jobs:
tests/unit/llms/voyage
tests/unit/llms/wandb
tests/unit/llms/watsonx
tests/unit/llms/webwright
tests/unit/llms/xai
tests/unit/llms/xinference
tests/unit/llms/you_com

View file

@ -473,6 +473,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [VoyageAI by MongoDB (`voyage`)](https://docs.litellm.ai/docs/providers/voyage) | | | | ✅ | | | | | | |
| [WandB Inference (`wandb`)](https://docs.litellm.ai/docs/providers/wandb_inference) | ✅ | ✅ | ✅ | | | | | | | |
| [Watsonx Text (`watsonx_text`)](https://docs.litellm.ai/docs/providers/watsonx) | ✅ | ✅ | ✅ | | | | | | | |
| [WebWright (`webwright`)](https://docs.litellm.ai/docs/providers/webwright) | ✅ | ✅ | ✅ | | | | | | | |
| [xAI (`xai`)](https://docs.litellm.ai/docs/providers/xai) | ✅ | ✅ | ✅ | | | | | | | |
| [Xinference (`xinference`)](https://docs.litellm.ai/docs/providers/xinference) | | | | ✅ | | | | | | |
| [ZeroGPU (`zerogpu`)](https://docs.litellm.ai/docs/providers/zerogpu) | ✅ | ✅ | ✅ | | | | | | | |

View file

@ -696,6 +696,7 @@ opencode_go_models: Final[set[str]] = set() # mutable-ok: populated when the sh
lambda_ai_models: Set = set()
inception_models: Set = set()
hyperbolic_models: Set = set()
webwright_models: set[str] = set() # mutable-ok: mirrors the sibling provider model sets
black_forest_labs_models: Set = set()
recraft_models: Set = set()
cometapi_models: Set = set()
@ -1001,6 +1002,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":
@ -1310,6 +1313,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,
@ -2305,6 +2309,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,
)

View file

@ -359,6 +359,7 @@ LLM_CONFIG_NAMES: Final = (
"LambdaAIChatConfig",
"InceptionChatConfig",
"HyperbolicChatConfig",
"WebwrightChatConfig",
"VercelAIGatewayConfig",
"EdenAIChatConfig",
"EdenAIResponsesAPIConfig",
@ -1325,6 +1326,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",

View file

@ -1078,6 +1078,7 @@ openai_compatible_endpoints: Final[list[str]] = [
"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",
@ -1160,6 +1161,7 @@ openai_compatible_providers: Final[list[str]] = [
"lambda_ai",
"inception",
"hyperbolic",
"webwright",
"vercel_ai_gateway",
"aiml",
"edenai",
@ -1209,6 +1211,7 @@ openai_text_completion_compatible_providers: Final[list] = [ # providers that s
"v0",
"lambda_ai",
"hyperbolic",
"webwright",
"wandb",
]
_openai_like_providers: Final[list[str]] = [

View file

@ -412,6 +412,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" # rebind-ok: mirrors the sibling endpoint dispatch
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")
@ -951,6 +954,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, # rebind-ok: mirrors the sibling OpenAI-compatible provider dispatch
dynamic_api_key, # rebind-ok: mirrors the sibling OpenAI-compatible provider dispatch
) = litellm.WebwrightChatConfig().get_openai_compatible_provider_info(api_base, api_key)
elif custom_llm_provider == "vercel_ai_gateway":
(
api_base,

View file

View file

View file

@ -0,0 +1,59 @@
"""
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
resolved_api_base: Final = (
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 resolved_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[str]: # mutable-ok: matches the base provider signature
"""
WebWright supports the OpenAI-compatible chat completion parameters that
its published model abilities reference declares.
Reference: https://webwright.ai/models
Reasoning is declared in the abilities reference, but no caller-facing
reasoning field or allowed values are published, so it is not advertised.
"""
return [
"messages", # Required
"model", # Required
"stream", # Optional
"temperature", # Optional
"top_p", # Optional
"max_tokens", # Optional
"stop", # Optional
"tools", # Optional
"tool_choice", # Optional
"response_format", # Optional
]

View file

@ -2071,6 +2071,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "WEBWRIGHT",
"provider_display_name": "WebWright",
"litellm_provider": "webwright",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": false,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "wren-r1"
},
{
"provider": "Infinity",
"provider_display_name": "Infinity",

View file

@ -4732,6 +4732,7 @@ class LlmProviders(str, Enum):
OPENSEARCH = "opensearch"
HELICONE = "helicone"
HYPERBOLIC = "hyperbolic"
WEBWRIGHT = "webwright"
RECRAFT = "recraft"
FAL_AI = "fal_ai"
STABILITY = "stability"

View file

@ -9936,6 +9936,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: (

View file

@ -4437,6 +4437,24 @@
"generateContent": true
}
},
"webwright": {
"display_name": "WebWright (`webwright`)",
"url": "https://docs.litellm.ai/docs/providers/webwright",
"endpoints": {
"chat_completions": true,
"messages": true,
"responses": true,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false,
"interactions": false
}
},
"watsonx_text": {
"display_name": "Watsonx Text (`watsonx_text`)",
"url": "https://docs.litellm.ai/docs/providers/watsonx",

View file

View file

@ -0,0 +1,105 @@
"""
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",
"top_p",
"max_tokens",
"stop",
"tools",
"tool_choice",
"response_format",
):
assert param in supported_params
for unsupported in ("frequency_penalty", "presence_penalty", "n", "seed", "user", "reasoning_effort"):
assert unsupported not 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!"