From abc8931bfd7a6845230d7f4ce840e9f7d821f7b8 Mon Sep 17 00:00:00 2001 From: dulcestentaciones2920-debug Date: Sun, 30 Aug 2026 03:34:43 +0000 Subject: [PATCH] feat: add OrcaRouter as a first-class LLM provider Route orcarouter/* model names to the OrcaRouter OpenAI-compatible endpoint using ORCA_API_KEY/ORCA_URL, mirroring the existing GLM and ARK named-provider branches in OpenAICompatibleClientSync. The router is configurable through the standard AIConfig env surface and documented in both .env.example files. Co-Authored-By: Claude --- apps/api/.env.example | 2 + apps/worker/.env.example | 2 + .../shared-python/shared/core/config/ai.py | 4 ++ .../ai/openai_compatible_client_sync.py | 7 ++ .../tests/test_openai_client_routing.py | 64 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 packages/shared-python/shared/tests/test_openai_client_routing.py diff --git a/apps/api/.env.example b/apps/api/.env.example index ef00d0ff0..97c578195 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -85,12 +85,14 @@ GPT_API_KEY= GLM_API_KEY= ALI_API_KEYS= ARK_API_KEY= +ORCA_API_KEY= # Optional overrides: # DS_URL=https://api.deepseek.com/v1 # GLM_URL=https://open.bigmodel.cn/api/paas/v4 # ALI_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # ARK_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions +# ORCA_URL=https://api.orcarouter.ai/v1 # NORMOL_MODEL=deepseek-v4-flash # HIERARCHY_LLM_MODEL= # VLM for image/OCR flows. Default qwen3.6-flash; alternate qwen3-vl-32b-instruct diff --git a/apps/worker/.env.example b/apps/worker/.env.example index 46a347c88..b2a3d1223 100644 --- a/apps/worker/.env.example +++ b/apps/worker/.env.example @@ -78,12 +78,14 @@ GPT_API_KEY= GLM_API_KEY= ALI_API_KEYS= ARK_API_KEY= +ORCA_API_KEY= # Optional overrides: # DS_URL=https://api.deepseek.com/v1 # GLM_URL=https://open.bigmodel.cn/api/paas/v4 # ALI_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # ARK_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions +# ORCA_URL=https://api.orcarouter.ai/v1 # NORMOL_MODEL=deepseek-v4-flash # HIERARCHY_LLM_MODEL= # VLM for page tagging, OCR, chart/table bbox probes, and image flows. diff --git a/packages/shared-python/shared/core/config/ai.py b/packages/shared-python/shared/core/config/ai.py index 9cbbdd367..92f4ecc13 100644 --- a/packages/shared-python/shared/core/config/ai.py +++ b/packages/shared-python/shared/core/config/ai.py @@ -16,6 +16,10 @@ class AIConfig(BaseModel): default="https://api.deepseek.com/v1", description="DeepSeek API URL" ) GPT_API_KEY: str = Field(default="", description="OpenAI API key") + ORCA_API_KEY: str = Field(default="", description="OrcaRouter API key") + ORCA_URL: str = Field( + default="https://api.orcarouter.ai/v1", description="OrcaRouter API URL" + ) # Default behavior: text/table summaries use deepseek-v4-flash. Hierarchy parsing # can be overridden independently with HIERARCHY_LLM_MODEL. Existing # environment overrides for NORMOL_MODEL / HIERARCHY_LLM_MODEL / diff --git a/packages/shared-python/shared/services/ai/openai_compatible_client_sync.py b/packages/shared-python/shared/services/ai/openai_compatible_client_sync.py index 6a1cf590a..867b976cb 100644 --- a/packages/shared-python/shared/services/ai/openai_compatible_client_sync.py +++ b/packages/shared-python/shared/services/ai/openai_compatible_client_sync.py @@ -156,6 +156,10 @@ def _resolve_base_url( if "doubao" in model_lower or model_lower.startswith("ep-"): return self._strip_chat_completions(getattr(settings, "ARK_URL", None)) + if "orcarouter" in model_lower: + orca_base = getattr(settings, "ORCA_URL", "https://api.orcarouter.ai/v1") + return self._strip_chat_completions(orca_base) + return self._strip_chat_completions(settings.DS_URL) def _resolve_direct_api_key( @@ -173,6 +177,9 @@ def _resolve_direct_api_key( if "doubao" in model_lower or model_lower.startswith("ep-"): return getattr(settings, "ARK_API_KEY", None) + if "orcarouter" in model_lower: + return getattr(settings, "ORCA_API_KEY", None) + return settings.DS_KEY # ------------------------------------------------------------------ diff --git a/packages/shared-python/shared/tests/test_openai_client_routing.py b/packages/shared-python/shared/tests/test_openai_client_routing.py new file mode 100644 index 000000000..54cec1460 --- /dev/null +++ b/packages/shared-python/shared/tests/test_openai_client_routing.py @@ -0,0 +1,64 @@ +"""Unit tests for OpenAI-compatible provider credential routing (no live API).""" + +from __future__ import annotations + +import os +from types import SimpleNamespace +from unittest.mock import patch + +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("TMP_PATH", "/tmp/knowhere-test") +os.environ.setdefault("S3_BUCKET_NAME", "test-uploads") +os.environ.setdefault("S3_ACCESS_KEY_ID", "test") +os.environ.setdefault("S3_SECRET_ACCESS_KEY", "test") +os.environ.setdefault("S3_TEMP_PATH", "/tmp") + +from shared.services.ai.openai_compatible_client_sync import ( # noqa: E402 + OpenAICompatibleClientSync, +) + + +def _client_with(model: str) -> OpenAICompatibleClientSync: + return OpenAICompatibleClientSync(default_model=model) + + +def test_orcarouter_model_resolves_base_url_and_api_key() -> None: + """orcarouter/* models route to the OrcaRouter endpoint and ORCA_API_KEY.""" + fake_settings = SimpleNamespace( + ORCA_URL="https://api.orcarouter.ai/v1", + ORCA_API_KEY="orca-test-key", + DS_URL="https://api.deepseek.com/v1", + DS_KEY="ds-test-key", + ) + client = _client_with("orcarouter/fusion") + with patch( + "shared.services.ai.openai_compatible_client_sync.settings", + fake_settings, + ): + assert client._resolve_base_url("orcarouter/fusion", None) == ( + "https://api.orcarouter.ai/v1" + ) + assert client._resolve_direct_api_key("orcarouter/fusion", None) == ( + "orca-test-key" + ) + + +def test_non_orcarouter_model_keeps_existing_routing() -> None: + """Models without the orcarouter prefix keep the DeepSeek defaults.""" + fake_settings = SimpleNamespace( + DS_URL="https://api.deepseek.com/v1", + DS_KEY="ds-test-key", + GLM_URL="https://open.bigmodel.cn/api/paas/v4", + GLM_API_KEY="glm-test-key", + ) + client = _client_with("deepseek-v4-flash") + with patch( + "shared.services.ai.openai_compatible_client_sync.settings", + fake_settings, + ): + assert client._resolve_base_url("deepseek-v4-flash", None) == ( + "https://api.deepseek.com/v1" + ) + assert client._resolve_direct_api_key("deepseek-v4-flash", None) == ( + "ds-test-key" + )