diff --git a/docs/configuration.md b/docs/configuration.md index dbd5e2626..17f73619e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -148,6 +148,7 @@ ANTHROPIC_API_KEY="$(bw get password api/anthropic)" nanobot agent | `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) | | `aihubmix` | LLM (API gateway, access to all models) | [aihubmix.com](https://aihubmix.com) | | `siliconflow` | LLM (SiliconFlow/硅基流动) | [siliconflow.cn](https://siliconflow.cn) | +| `novita` | LLM (Novita AI, OpenAI-compatible gateway) | [novita.ai](https://novita.ai) | | `dashscope` | LLM (Qwen) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) | | `moonshot` | LLM (Moonshot/Kimi) | [platform.moonshot.cn](https://platform.moonshot.cn) | | `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) | diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index c0ad7e758..2e094cc09 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -211,6 +211,7 @@ class ProvidersConfig(Base): ant_ling: ProviderConfig = Field(default_factory=ProviderConfig) # Ant Ling aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动) + novita: ProviderConfig = Field(default_factory=ProviderConfig) # Novita AI volcengine: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine (火山引擎) volcengine_coding_plan: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine Coding Plan byteplus: ProviderConfig = Field(default_factory=ProviderConfig) # BytePlus (VolcEngine international) diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index d942c03bf..ab7e2cf1e 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -199,6 +199,18 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( default_api_base="https://api.siliconflow.cn/v1", ), + # Novita AI: OpenAI-compatible gateway for hosted model APIs. + ProviderSpec( + name="novita", + keywords=("novita",), + env_key="NOVITA_API_KEY", + display_name="Novita AI", + backend="openai_compat", + is_gateway=True, + detect_by_base_keyword="novita", + default_api_base="https://api.novita.ai/openai", + ), + # VolcEngine (火山引擎): OpenAI-compatible gateway, pay-per-use models ProviderSpec( name="volcengine", diff --git a/tests/config/test_model_presets.py b/tests/config/test_model_presets.py index 046c5b04d..4786326a9 100644 --- a/tests/config/test_model_presets.py +++ b/tests/config/test_model_presets.py @@ -192,3 +192,19 @@ def test_match_provider_uses_preset_provider_when_forced() -> None: }) name = config.get_provider_name() assert name == "anthropic" + + +def test_match_provider_routes_novita_prefixed_models() -> None: + config = Config.model_validate({ + "providers": { + "novita": {"apiKey": "sk-test"}, + }, + "agents": { + "defaults": { + "model": "novita/deepseek/deepseek-v4-pro", + } + }, + }) + + assert config.get_provider_name() == "novita" + assert config.get_api_base() == "https://api.novita.ai/openai" diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index 6a32981d9..dddc70054 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -441,6 +441,15 @@ def test_openrouter_spec_is_gateway() -> None: assert spec.default_api_base == "https://openrouter.ai/api/v1" +def test_novita_spec_uses_openai_compatible_gateway() -> None: + spec = find_by_name("novita") + assert spec is not None + assert spec.is_gateway is True + assert spec.backend == "openai_compat" + assert spec.env_key == "NOVITA_API_KEY" + assert spec.default_api_base == "https://api.novita.ai/openai" + + def test_gemma_routes_to_gemini_provider() -> None: """gemma models (e.g. gemma-3-27b-it) must auto-route to Gemini when GEMINI_API_KEY is set. Users running gemma via the Gemini API endpoint expect automatic provider detection."""