From e00220bdb63feebfb8a0b3d3a904834c91db8917 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Wed, 20 May 2026 00:04:39 +0800 Subject: [PATCH] feat(providers): add Skywork provider support --- docs/configuration.md | 31 +++++++ nanobot/config/schema.py | 1 + nanobot/providers/registry.py | 12 +++ tests/channels/test_websocket_channel.py | 2 + tests/providers/test_skywork_provider.py | 80 +++++++++++++++++++ .../src/components/settings/SettingsView.tsx | 1 + 6 files changed, 127 insertions(+) create mode 100644 tests/providers/test_skywork_provider.py diff --git a/docs/configuration.md b/docs/configuration.md index fed07ff2d..eaa3626b2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -134,6 +134,7 @@ ANTHROPIC_API_KEY="$(bw get password api/anthropic)" nanobot agent | `custom` | Any OpenAI-compatible endpoint | — | | `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) | | `huggingface` | LLM (Hugging Face Inference Providers) | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) | +| `skywork` | LLM (Skywork / APIFree API gateway) | [apifree.ai](https://www.apifree.ai) | | `volcengine` | LLM (VolcEngine, pay-per-use) | [Coding Plan](https://www.volcengine.com/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) · [volcengine.com](https://www.volcengine.com) | | `byteplus` | LLM (VolcEngine international, pay-per-use) | [Coding Plan](https://www.byteplus.com/en/activity/codingplan?utm_campaign=nanobot&utm_content=nanobot&utm_medium=devrel&utm_source=OWO&utm_term=nanobot) · [byteplus.com](https://www.byteplus.com) | | `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) | @@ -164,6 +165,36 @@ ANTHROPIC_API_KEY="$(bw get password api/anthropic)" nanobot agent | `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` | | `qianfan` | LLM (Baidu Qianfan) | [cloud.baidu.com](https://cloud.baidu.com/doc/qianfan/s/Hmh4suq26) | +
+Skywork / APIFree + +Skywork uses the OpenAI-compatible APIFree API endpoint. Configure the provider +once, then use Skywork model IDs such as `skywork-ai/skyclaw-v1`. + +```json +{ + "providers": { + "skywork": { + "apiKey": "${SKYWORK_API_KEY}", + "apiBase": "https://api.apifree.ai/v1" + } + }, + "agents": { + "defaults": { + "provider": "skywork", + "model": "skywork-ai/skyclaw-v1", + "maxTokens": 32768, + "contextWindowTokens": 131072 + } + } +} +``` + +You can also reference `${APIFREE_API_KEY}` in `apiKey` if that is how your +environment names the credential. + +
+
AWS Bedrock (Converse API) diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 6ccabea3f..c0ad7e758 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -190,6 +190,7 @@ class ProvidersConfig(Base): openai: ProviderConfig = Field(default_factory=ProviderConfig) openrouter: ProviderConfig = Field(default_factory=ProviderConfig) huggingface: ProviderConfig = Field(default_factory=ProviderConfig) + skywork: ProviderConfig = Field(default_factory=ProviderConfig) # Skywork / APIFree API gateway deepseek: ProviderConfig = Field(default_factory=ProviderConfig) groq: ProviderConfig = Field(default_factory=ProviderConfig) zhipu: ProviderConfig = Field(default_factory=ProviderConfig) diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index 0f8e45936..58c2ff399 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -155,6 +155,18 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( detect_by_base_keyword="huggingface", default_api_base="https://router.huggingface.co/v1", ), + # Skywork API platform (APIFree): OpenAI-compatible MaaS gateway. + ProviderSpec( + name="skywork", + keywords=("skywork", "skyclaw", "apifree"), + env_key="SKYWORK_API_KEY", + display_name="Skywork", + backend="openai_compat", + env_extras=(("APIFREE_API_KEY", "{api_key}"),), + is_gateway=True, + detect_by_base_keyword="apifree.ai", + default_api_base="https://api.apifree.ai/v1", + ), # AiHubMix: global gateway, OpenAI-compatible interface. # strip_model_prefix=True: doesn't understand "anthropic/claude-3", # strips to bare "claude-3". diff --git a/tests/channels/test_websocket_channel.py b/tests/channels/test_websocket_channel.py index 78953864e..d687cc9e2 100644 --- a/tests/channels/test_websocket_channel.py +++ b/tests/channels/test_websocket_channel.py @@ -1030,6 +1030,8 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist( assert providers["azure_openai"]["api_key_required"] is True assert providers["openrouter"]["configured"] is False assert providers["openrouter"]["api_key_required"] is True + assert providers["skywork"]["label"] == "Skywork" + assert providers["skywork"]["default_api_base"] == "https://api.apifree.ai/v1" assert providers["ant_ling"]["label"] == "Ant Ling" assert providers["ant_ling"]["default_api_base"] == "https://api.ant-ling.com/v1" assert providers["atomic_chat"]["configured"] is False diff --git a/tests/providers/test_skywork_provider.py b/tests/providers/test_skywork_provider.py new file mode 100644 index 000000000..52de462f4 --- /dev/null +++ b/tests/providers/test_skywork_provider.py @@ -0,0 +1,80 @@ +"""Tests for the Skywork provider registration.""" + +from unittest.mock import patch + +from nanobot.config.schema import Config, ProvidersConfig +from nanobot.providers.openai_compat_provider import OpenAICompatProvider +from nanobot.providers.registry import PROVIDERS, find_by_name + + +def test_skywork_config_field_exists() -> None: + config = ProvidersConfig() + + assert hasattr(config, "skywork") + + +def test_skywork_provider_in_registry() -> None: + specs = {spec.name: spec for spec in PROVIDERS} + + assert "skywork" in specs + skywork = specs["skywork"] + assert skywork.backend == "openai_compat" + assert skywork.env_key == "SKYWORK_API_KEY" + assert ("APIFREE_API_KEY", "{api_key}") in skywork.env_extras + assert skywork.display_name == "Skywork" + assert skywork.is_gateway is True + assert skywork.detect_by_base_keyword == "apifree.ai" + assert skywork.default_api_base == "https://api.apifree.ai/v1" + assert skywork.supports_max_completion_tokens is False + + +def test_find_by_name_skywork() -> None: + spec = find_by_name("skywork") + + assert spec is not None + assert spec.name == "skywork" + + +def test_skywork_model_auto_matches_with_default_api_base() -> None: + config = Config.model_validate( + { + "providers": { + "skywork": { + "apiKey": "sky-key", + }, + }, + "agents": { + "defaults": { + "model": "skywork-ai/skyclaw-v1", + }, + }, + } + ) + + assert config.get_provider_name("skywork-ai/skyclaw-v1") == "skywork" + assert config.get_api_key("skywork-ai/skyclaw-v1") == "sky-key" + assert config.get_api_base("skywork-ai/skyclaw-v1") == "https://api.apifree.ai/v1" + + +def test_skywork_preserves_model_id_and_uses_chat_completion_max_tokens() -> None: + spec = find_by_name("skywork") + with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): + provider = OpenAICompatProvider( + api_key="sky-key", + default_model="skywork-ai/skyclaw-v1", + spec=spec, + ) + + kwargs = provider._build_kwargs( + messages=[{"role": "user", "content": "hi"}], + tools=None, + model="skywork-ai/skyclaw-v1", + max_tokens=1024, + temperature=0.7, + reasoning_effort=None, + tool_choice=None, + ) + + assert kwargs["model"] == "skywork-ai/skyclaw-v1" + assert kwargs["max_tokens"] == 1024 + assert "max_completion_tokens" not in kwargs diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index d4a47c1f0..249d400c8 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -2129,6 +2129,7 @@ function providerLabel( const PROVIDER_ICONS: Record = { custom: Hexagon, openrouter: Sparkles, + skywork: Sparkles, aihubmix: Triangle, anthropic: Brain, openai: Bot,