mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
feat(providers): add OrcaRouter as a named gateway provider
Registers OrcaRouter (https://www.orcarouter.ai) as a built-in OpenAI-compatible gateway provider mirroring the OpenRouter wiring: registry spec (sk-orca- key prefix, default base URL), ProvidersConfig field, WebUI icon/brand + deferred model list, docs, and tests.
This commit is contained in:
@@ -123,6 +123,40 @@ appended to nanobot's generated functions. This keeps unrelated local tools such
|
||||
available in the same request. Responses-only server tools require an API surface that the
|
||||
OpenRouter provider does not currently enable.
|
||||
|
||||
### OrcaRouter Gateway
|
||||
|
||||
[OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible model routing gateway. Configure
|
||||
the built-in `orcarouter` provider and use a model ID from OrcaRouter's catalog:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"orcarouter": {
|
||||
"apiKey": "${ORCAROUTER_API_KEY}"
|
||||
}
|
||||
},
|
||||
"modelPresets": {
|
||||
"primary": {
|
||||
"provider": "orcarouter",
|
||||
"model": "orcarouter/auto",
|
||||
"maxTokens": 8192,
|
||||
"contextWindowTokens": 65536
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"modelPreset": "primary"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the model ID exactly as OrcaRouter lists it. `orcarouter/auto` routes to a
|
||||
suitable upstream automatically; explicit IDs such as
|
||||
`anthropic/claude-sonnet-4.6` or `openai/gpt-5` are also accepted. OrcaRouter API keys start with
|
||||
`sk-orca-`. The WebUI can load the account's model catalog after the API key is saved under
|
||||
**Settings → Models**.
|
||||
|
||||
### Eden AI Gateway
|
||||
|
||||
Eden AI exposes an OpenAI-compatible chat-completions endpoint at
|
||||
|
||||
@@ -3132,6 +3132,10 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
|
||||
assert providers["azure_openai"]["api_key_required"] is False # AAD auth supported; no static key required
|
||||
assert providers["openrouter"]["configured"] is False
|
||||
assert providers["openrouter"]["api_key_required"] is True
|
||||
assert providers["orcarouter"]["label"] == "OrcaRouter"
|
||||
assert providers["orcarouter"]["configured"] is False
|
||||
assert providers["orcarouter"]["api_key_required"] is True
|
||||
assert providers["orcarouter"]["default_api_base"] == "https://api.orcarouter.ai/v1"
|
||||
assert providers["skywork"]["label"] == "Skywork"
|
||||
assert providers["skywork"]["default_api_base"] == "https://api.apifree.ai/agent/v1"
|
||||
assert providers["ant_ling"]["label"] == "Ant Ling"
|
||||
|
||||
@@ -262,6 +262,7 @@ class ProvidersConfig(Base):
|
||||
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||
orcarouter: ProviderConfig = Field(default_factory=ProviderConfig) # OrcaRouter API gateway
|
||||
assemblyai: ProviderConfig = Field(default_factory=ProviderConfig) # AssemblyAI voice transcription
|
||||
huggingface: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||
skywork: ProviderConfig = Field(default_factory=ProviderConfig) # Skywork / APIFree API gateway
|
||||
|
||||
@@ -199,6 +199,18 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
|
||||
supports_prompt_caching=True,
|
||||
gateway_reasoning_style="reasoning_effort",
|
||||
),
|
||||
# OrcaRouter: global gateway, keys start with "sk-orca-"
|
||||
ProviderSpec(
|
||||
name="orcarouter",
|
||||
keywords=("orcarouter",),
|
||||
env_key="ORCAROUTER_API_KEY",
|
||||
display_name="OrcaRouter",
|
||||
backend="openai_compat",
|
||||
is_gateway=True,
|
||||
detect_by_key_prefix="sk-orca-",
|
||||
detect_by_base_keyword="orcarouter",
|
||||
default_api_base="https://api.orcarouter.ai/v1",
|
||||
),
|
||||
# Eden AI: OpenAI-compatible gateway. Models use the "provider/model"
|
||||
# naming scheme (e.g. "anthropic/claude-sonnet-4-5"); the full id is sent upstream.
|
||||
ProviderSpec(
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Tests for the OrcaRouter 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_orcarouter_config_field_exists() -> None:
|
||||
config = ProvidersConfig()
|
||||
|
||||
assert hasattr(config, "orcarouter")
|
||||
|
||||
|
||||
def test_orcarouter_provider_in_registry() -> None:
|
||||
specs = {spec.name: spec for spec in PROVIDERS}
|
||||
|
||||
assert "orcarouter" in specs
|
||||
orcarouter = specs["orcarouter"]
|
||||
assert orcarouter.backend == "openai_compat"
|
||||
assert orcarouter.env_key == "ORCAROUTER_API_KEY"
|
||||
assert orcarouter.display_name == "OrcaRouter"
|
||||
assert orcarouter.is_gateway is True
|
||||
assert orcarouter.detect_by_key_prefix == "sk-orca-"
|
||||
assert orcarouter.detect_by_base_keyword == "orcarouter"
|
||||
assert orcarouter.default_api_base == "https://api.orcarouter.ai/v1"
|
||||
assert orcarouter.strip_model_prefix is False
|
||||
|
||||
|
||||
def test_find_by_name_orcarouter() -> None:
|
||||
spec = find_by_name("orcarouter")
|
||||
|
||||
assert spec is not None
|
||||
assert spec.name == "orcarouter"
|
||||
|
||||
|
||||
def test_orcarouter_forced_provider_uses_default_api_base() -> None:
|
||||
config = Config.model_validate({
|
||||
"providers": {
|
||||
"orcarouter": {
|
||||
"apiKey": "sk-orca-test-key",
|
||||
},
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": "deepseek/deepseek-chat",
|
||||
"provider": "orcarouter",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert config.get_provider_name("deepseek/deepseek-chat") == "orcarouter"
|
||||
assert config.get_api_key("deepseek/deepseek-chat") == "sk-orca-test-key"
|
||||
assert config.get_api_base("deepseek/deepseek-chat") == "https://api.orcarouter.ai/v1"
|
||||
|
||||
|
||||
def test_orcarouter_gateway_routes_unprefixed_models_when_configured() -> None:
|
||||
config = Config.model_validate({
|
||||
"providers": {
|
||||
"orcarouter": {
|
||||
"apiKey": "sk-orca-test-key",
|
||||
},
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": "orcarouter/auto",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert config.get_provider_name("orcarouter/auto") == "orcarouter"
|
||||
assert config.get_api_key("orcarouter/auto") == "sk-orca-test-key"
|
||||
assert config.get_api_base("orcarouter/auto") == "https://api.orcarouter.ai/v1"
|
||||
|
||||
|
||||
def test_orcarouter_preserves_model_api_id() -> None:
|
||||
spec = find_by_name("orcarouter")
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="sk-orca-test-key",
|
||||
default_model="anthropic/claude-sonnet-4.6",
|
||||
spec=spec,
|
||||
)
|
||||
|
||||
kwargs = provider._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
model="anthropic/claude-sonnet-4.6",
|
||||
max_tokens=1024,
|
||||
temperature=0.7,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
assert kwargs["model"] == "anthropic/claude-sonnet-4.6"
|
||||
assert kwargs["max_tokens"] == 1024
|
||||
assert "max_completion_tokens" not in kwargs
|
||||
@@ -52,6 +52,7 @@ const DEFERRED_MODEL_LIST_PROVIDERS = new Set([
|
||||
"novita",
|
||||
"ollama",
|
||||
"openrouter",
|
||||
"orcarouter",
|
||||
"ovms",
|
||||
"siliconflow",
|
||||
"vllm",
|
||||
@@ -577,6 +578,7 @@ export function optionRowsWithCurrent(
|
||||
export const PROVIDER_ICONS: Record<string, LucideIcon> = {
|
||||
custom: Hexagon,
|
||||
openrouter: Sparkles,
|
||||
orcarouter: Sparkles,
|
||||
skywork: Sparkles,
|
||||
aihubmix: Triangle,
|
||||
anthropic: Brain,
|
||||
|
||||
@@ -184,6 +184,7 @@ const PROVIDER_BRANDS: Record<string, ProviderBrand> = {
|
||||
ollama: brand("ollama.com", "#111827", "O"),
|
||||
openai: brand("openai.com", "#111827", "AI"),
|
||||
openrouter: brand("openrouter.ai", "#111827", "OR"),
|
||||
orcarouter: brand("orcarouter.ai", "#111827", "OR"),
|
||||
ovms: brand("openvino.ai", "#0071C5", "OV"),
|
||||
qianfan: brand("cloud.baidu.com", "#2932E1", "QF"),
|
||||
searxng: brand("searxng.org", "#3050FF", "SX"),
|
||||
|
||||
Reference in New Issue
Block a user