mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +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
|
available in the same request. Responses-only server tools require an API surface that the
|
||||||
OpenRouter provider does not currently enable.
|
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 Gateway
|
||||||
|
|
||||||
Eden AI exposes an OpenAI-compatible chat-completions endpoint at
|
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["azure_openai"]["api_key_required"] is False # AAD auth supported; no static key required
|
||||||
assert providers["openrouter"]["configured"] is False
|
assert providers["openrouter"]["configured"] is False
|
||||||
assert providers["openrouter"]["api_key_required"] is True
|
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"]["label"] == "Skywork"
|
||||||
assert providers["skywork"]["default_api_base"] == "https://api.apifree.ai/agent/v1"
|
assert providers["skywork"]["default_api_base"] == "https://api.apifree.ai/agent/v1"
|
||||||
assert providers["ant_ling"]["label"] == "Ant Ling"
|
assert providers["ant_ling"]["label"] == "Ant Ling"
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ class ProvidersConfig(Base):
|
|||||||
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
openrouter: 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
|
assemblyai: ProviderConfig = Field(default_factory=ProviderConfig) # AssemblyAI voice transcription
|
||||||
huggingface: ProviderConfig = Field(default_factory=ProviderConfig)
|
huggingface: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
skywork: ProviderConfig = Field(default_factory=ProviderConfig) # Skywork / APIFree API gateway
|
skywork: ProviderConfig = Field(default_factory=ProviderConfig) # Skywork / APIFree API gateway
|
||||||
|
|||||||
@@ -199,6 +199,18 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
|
|||||||
supports_prompt_caching=True,
|
supports_prompt_caching=True,
|
||||||
gateway_reasoning_style="reasoning_effort",
|
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"
|
# 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.
|
# naming scheme (e.g. "anthropic/claude-sonnet-4-5"); the full id is sent upstream.
|
||||||
ProviderSpec(
|
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",
|
"novita",
|
||||||
"ollama",
|
"ollama",
|
||||||
"openrouter",
|
"openrouter",
|
||||||
|
"orcarouter",
|
||||||
"ovms",
|
"ovms",
|
||||||
"siliconflow",
|
"siliconflow",
|
||||||
"vllm",
|
"vllm",
|
||||||
@@ -577,6 +578,7 @@ export function optionRowsWithCurrent(
|
|||||||
export const PROVIDER_ICONS: Record<string, LucideIcon> = {
|
export const PROVIDER_ICONS: Record<string, LucideIcon> = {
|
||||||
custom: Hexagon,
|
custom: Hexagon,
|
||||||
openrouter: Sparkles,
|
openrouter: Sparkles,
|
||||||
|
orcarouter: Sparkles,
|
||||||
skywork: Sparkles,
|
skywork: Sparkles,
|
||||||
aihubmix: Triangle,
|
aihubmix: Triangle,
|
||||||
anthropic: Brain,
|
anthropic: Brain,
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ const PROVIDER_BRANDS: Record<string, ProviderBrand> = {
|
|||||||
ollama: brand("ollama.com", "#111827", "O"),
|
ollama: brand("ollama.com", "#111827", "O"),
|
||||||
openai: brand("openai.com", "#111827", "AI"),
|
openai: brand("openai.com", "#111827", "AI"),
|
||||||
openrouter: brand("openrouter.ai", "#111827", "OR"),
|
openrouter: brand("openrouter.ai", "#111827", "OR"),
|
||||||
|
orcarouter: brand("orcarouter.ai", "#111827", "OR"),
|
||||||
ovms: brand("openvino.ai", "#0071C5", "OV"),
|
ovms: brand("openvino.ai", "#0071C5", "OV"),
|
||||||
qianfan: brand("cloud.baidu.com", "#2932E1", "QF"),
|
qianfan: brand("cloud.baidu.com", "#2932E1", "QF"),
|
||||||
searxng: brand("searxng.org", "#3050FF", "SX"),
|
searxng: brand("searxng.org", "#3050FF", "SX"),
|
||||||
|
|||||||
Reference in New Issue
Block a user