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:
XiaoHuo888
2026-08-16 00:26:51 +08:00
committed by Xubin Ren
parent 50391b2fb7
commit fd2d419956
7 changed files with 152 additions and 0 deletions
+34
View File
@@ -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"
+1
View File
@@ -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
+12
View File
@@ -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,
+1
View File
@@ -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"),