diff --git a/README.md b/README.md index f27d7a957..aed156fac 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Each agent has a `soul.md` (personality), `memory.md` (long-term memory), and a - Network access to LLM API endpoints > **Note:** Clawith does not run any AI models locally — all LLM inference is handled by external API providers (OpenAI, Anthropic, etc.). The local deployment is a standard web application with Docker orchestration. +> +> **OrcaRouter** is available as a first-class provider: in the enterprise LLM settings, add a model with provider **OrcaRouter** (base URL `https://api.orcarouter.ai/v1`) and any `sk-orca-...` key. See [orcarouter.ai](https://www.orcarouter.ai). It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. #### Recommended Configurations diff --git a/backend/app/services/llm/client.py b/backend/app/services/llm/client.py index d30105509..4d75ba2de 100644 --- a/backend/app/services/llm/client.py +++ b/backend/app/services/llm/client.py @@ -2376,6 +2376,13 @@ class ProviderSpec: default_base_url="https://openrouter.ai/api/v1", default_max_tokens=4096, ), + "orcarouter": ProviderSpec( + provider="orcarouter", + display_name="OrcaRouter", + protocol="openai_compatible", + default_base_url="https://api.orcarouter.ai/v1", + default_max_tokens=16384, + ), "zhipu": ProviderSpec( provider="zhipu", display_name="Zhipu", diff --git a/backend/tests/test_orcarouter_provider.py b/backend/tests/test_orcarouter_provider.py new file mode 100644 index 000000000..7d6d1932e --- /dev/null +++ b/backend/tests/test_orcarouter_provider.py @@ -0,0 +1,55 @@ +"""Provider registry tests for the OrcaRouter integration.""" + +from app.services.llm.client import ( + PROVIDER_REGISTRY, + create_llm_client, + get_provider_base_url, + get_provider_manifest, + normalize_provider, +) + + +def test_orcarouter_registered_in_provider_registry() -> None: + spec = PROVIDER_REGISTRY.get("orcarouter") + assert spec is not None + assert spec.provider == "orcarouter" + assert spec.display_name == "OrcaRouter" + assert spec.protocol == "openai_compatible" + assert spec.default_base_url == "https://api.orcarouter.ai/v1" + assert spec.supports_tool_choice is True + assert spec.default_max_tokens > 0 + + +def test_orcarouter_manifest_round_trip() -> None: + manifest = {entry["provider"]: entry for entry in get_provider_manifest()} + entry = manifest.get("orcarouter") + assert entry is not None + assert entry["display_name"] == "OrcaRouter" + assert entry["default_base_url"] == "https://api.orcarouter.ai/v1" + + +def test_orcarouter_base_url_resolution() -> None: + assert get_provider_base_url("orcarouter") == "https://api.orcarouter.ai/v1" + # explicit custom base_url takes precedence + assert get_provider_base_url("orcarouter", "https://example.com/v1") == "https://example.com/v1" + # aliases normalize to the canonical provider id + assert normalize_provider("OrcaRouter") == "orcarouter" + + +def test_create_orcarouter_client_uses_openai_compatible_protocol() -> None: + client = create_llm_client( + provider="orcarouter", + api_key="sk-orca-test", + model="orcarouter/auto", + ) + try: + assert client.base_url == "https://api.orcarouter.ai/v1" + assert client.model == "orcarouter/auto" + assert client.supports_tool_choice is True + # Bearer auth headers for the OpenAI-compatible gateway + headers = client._get_headers() + assert headers["Authorization"] == "Bearer sk-orca-test" + finally: + import asyncio + + asyncio.run(client.close()) diff --git a/frontend/src/pages/enterprise-settings/tabs/LlmTab.tsx b/frontend/src/pages/enterprise-settings/tabs/LlmTab.tsx index e23b407cc..62ed666ab 100644 --- a/frontend/src/pages/enterprise-settings/tabs/LlmTab.tsx +++ b/frontend/src/pages/enterprise-settings/tabs/LlmTab.tsx @@ -57,6 +57,7 @@ const FALLBACK_LLM_PROVIDERS: LLMProviderSpec[] = [ { provider: 'baidu', display_name: 'Baidu (Qianfan)', protocol: 'openai_compatible', default_base_url: 'https://qianfan.baidubce.com/v2', supports_tool_choice: false, default_max_tokens: 4096 }, { provider: 'gemini', display_name: 'Gemini', protocol: 'gemini', default_base_url: 'https://generativelanguage.googleapis.com/v1beta', supports_tool_choice: true, default_max_tokens: 8192 }, { provider: 'openrouter', display_name: 'OpenRouter', protocol: 'openai_compatible', default_base_url: 'https://openrouter.ai/api/v1', supports_tool_choice: true, default_max_tokens: 4096 }, + { provider: 'orcarouter', display_name: 'OrcaRouter', protocol: 'openai_compatible', default_base_url: 'https://api.orcarouter.ai/v1', supports_tool_choice: true, default_max_tokens: 16384 }, { provider: 'kimi', display_name: 'Kimi (Moonshot)', protocol: 'openai_compatible', default_base_url: 'https://api.moonshot.cn/v1', supports_tool_choice: true, default_max_tokens: 8192 }, { provider: 'vllm', display_name: 'vLLM', protocol: 'openai_compatible', default_base_url: 'http://localhost:8000/v1', supports_tool_choice: true, default_max_tokens: 4096 }, { provider: 'ollama', display_name: 'Ollama', protocol: 'openai_compatible', default_base_url: 'http://localhost:11434/v1', supports_tool_choice: true, default_max_tokens: 4096 },