From e631a7a547050a8b7a746ffe1b49f9a040f3ed6b Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Wed, 19 Aug 2026 13:05:42 +0000 Subject: [PATCH] feat(as_llm): add named OrcaRouter provider Add an OrcaRouter chat-model provider alongside the existing OpenAI-compatible wrappers. OrcaRouter is an OpenAI-compatible gateway at https://api.orcarouter.ai/v1; users can now select it with LLM_BACKEND=orcarouter instead of pointing an OpenAI wrapper at a custom base_url. - OrcaRouterCredential: subclass of OpenAICredential with a default base_url of https://api.orcarouter.ai/v1 and an empty-string fallback for the shared ${LLM_BASE_URL:-} config key - OrcaRouterAsLLM: registered as backend "orcarouter" in the as_llm registry - docs: README (en/zh), quick start (en/zh), example.env - tests: unit coverage for registration, defaults, and fallback behavior Live-verified against the OrcaRouter chat/completions endpoint with model orcarouter/auto. Signed-off-by: XiaoHuo888-hue --- README.md | 5 +++ README_ZH.md | 5 +++ docs/en/quick_start.md | 13 ++++++++ docs/zh/quick_start.md | 11 +++++++ example.env | 5 +++ reme/components/as_llm/__init__.py | 44 ++++++++++++++++++++++++++ tests/unit/test_orcarouter_provider.py | 40 +++++++++++++++++++++++ 7 files changed, 123 insertions(+) create mode 100644 tests/unit/test_orcarouter_provider.py diff --git a/README.md b/README.md index ab22913d..65d34f4d 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,11 @@ EOF Basic file operations, BM25 search, wikilink traversal, and reading proactive topics can run without LLM credentials. +`LLM_BACKEND` selects the provider wrapper registered in `reme/components/as_llm` (default `openai`). You can also use +[OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible gateway, with `LLM_BACKEND=orcarouter`, +`LLM_MODEL_NAME=orcarouter/auto`, and an `LLM_API_KEY` from OrcaRouter; `LLM_BASE_URL` then defaults to +`https://api.orcarouter.ai/v1`. + > [!NOTE] > To enable embedding-based semantic retrieval, uncomment `components.as_embedding` and > `components.embedding_store` in [`reme/config/default.yaml`](reme/config/default.yaml), then change diff --git a/README_ZH.md b/README_ZH.md index 6620df8c..6401b93c 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -110,6 +110,11 @@ EOF 基础文件读写、BM25 检索、wikilink 遍历和 proactive topics 读取可以先不配置 LLM 凭证。 +`LLM_BACKEND` 选择 `reme/components/as_llm` 中注册的 provider wrapper(默认 `openai`)。也可以接入 +[OrcaRouter](https://www.orcarouter.ai)(兼容 OpenAI 协议的中转网关):设 `LLM_BACKEND=orcarouter`、 +`LLM_MODEL_NAME=orcarouter/auto`,并用 OrcaRouter 的 `LLM_API_KEY`;`LLM_BASE_URL` 可缺省, +默认 `https://api.orcarouter.ai/v1`。 + > [!NOTE] > 如需启用基于 embedding 的语义检索,请取消 [`reme/config/default.yaml`](reme/config/default.yaml) 中 > `components.as_embedding` 和 `components.embedding_store` 的注释,并将 diff --git a/docs/en/quick_start.md b/docs/en/quick_start.md index bd73f91a..bc402f3f 100644 --- a/docs/en/quick_start.md +++ b/docs/en/quick_start.md @@ -40,6 +40,19 @@ EOF You can initially omit the LLM configuration if you only need basic file operations and BM25 retrieval. +`LLM_BACKEND` selects the provider wrapper registered in +`reme/components/as_llm`. Besides the built-in providers (`openai`, `anthropic`, `deepseek`, +`dashscope`, `gemini`, `moonshot`, `ollama`, `xai`), you can point at +[OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible gateway, by setting: + +```bash +LLM_BACKEND=orcarouter +LLM_MODEL_NAME=orcarouter/auto +LLM_API_KEY=sk-orca-... +``` + +`LLM_BASE_URL` stays optional — when unset it defaults to `https://api.orcarouter.ai/v1`. + --- ## Start the Service diff --git a/docs/zh/quick_start.md b/docs/zh/quick_start.md index b3059866..21396ced 100644 --- a/docs/zh/quick_start.md +++ b/docs/zh/quick_start.md @@ -39,6 +39,17 @@ EOF 只跑基础文件读写和 BM25 检索,可以先不配。 +`LLM_BACKEND` 选择 `reme/components/as_llm` 中注册的 provider wrapper。除内置 provider(`openai`、`anthropic`、`deepseek`、`dashscope`、`gemini`、`moonshot`、`ollama`、`xai`)外,也可以接入兼容 OpenAI 协议的中转网关 +[OrcaRouter](https://www.orcarouter.ai): + +```bash +LLM_BACKEND=orcarouter +LLM_MODEL_NAME=orcarouter/auto +LLM_API_KEY=sk-orca-... +``` + +`LLM_BASE_URL` 可不填,缺省时默认 `https://api.orcarouter.ai/v1`。 + --- ## 启动 diff --git a/example.env b/example.env index 72bfd5e4..bee28cca 100644 --- a/example.env +++ b/example.env @@ -3,6 +3,11 @@ EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 LLM_API_KEY=sk-xxx LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +# OpenAI-compatible gateway: LLM_BACKEND=orcarouter +# LLM_API_KEY=sk-orca-... +# LLM_MODEL_NAME=orcarouter/auto +# LLM_BASE_URL is optional and defaults to https://api.orcarouter.ai/v1 + # Optional data-source mirrors. Unset variables use the official services. # HF_MIRROR_URL=https://hf-mirror.com # ARXIV_MIRROR_URL=https://export.arxiv.org diff --git a/reme/components/as_llm/__init__.py b/reme/components/as_llm/__init__.py index d4c5007d..c3dbf212 100644 --- a/reme/components/as_llm/__init__.py +++ b/reme/components/as_llm/__init__.py @@ -1,5 +1,7 @@ """LLM model wrappers for AgentScope.""" +from typing import Literal + from agentscope.credential import ( AnthropicCredential, CredentialBase, @@ -12,6 +14,7 @@ from agentscope.credential import ( XAICredential, ) from agentscope.model import ChatModelBase +from pydantic import ConfigDict, Field, field_validator from ..base_component import BaseComponent from ..component_registry import R @@ -49,6 +52,46 @@ class OpenAIAsLLM(BaseAsLLM): credential_cls = OpenAICredential +_ORCAROUTER_BASE_URL = "https://api.orcarouter.ai/v1" + + +class OrcaRouterCredential(OpenAICredential): + """OpenAI-compatible credential for the OrcaRouter gateway. + + OrcaRouter exposes an OpenAI-compatible ``chat/completions`` endpoint at + ``https://api.orcarouter.ai/v1``. The shared LLM config passes + ``base_url: ${LLM_BASE_URL:-}``, so an unset ``LLM_BASE_URL`` yields an + empty string; this credential falls back to the hosted gateway in that + case while still allowing a custom/self-hosted endpoint. + """ + + model_config = ConfigDict( + title="OrcaRouter API", + ) + + type: Literal["orcarouter_credential"] = "orcarouter_credential" + """The credential type.""" + + base_url: str = Field( + default=_ORCAROUTER_BASE_URL, + description="The base URL for the OrcaRouter API.", + ) + """The base URL for the OrcaRouter API.""" + + @field_validator("base_url", mode="before") + @classmethod + def _default_base_url(cls, value: str | None) -> str: + """Fall back to the hosted gateway when the shared config leaves the field empty.""" + return value or _ORCAROUTER_BASE_URL + + +@R.register("orcarouter") +class OrcaRouterAsLLM(BaseAsLLM): + """OrcaRouter chat model wrapper (OpenAI-compatible gateway).""" + + credential_cls = OrcaRouterCredential + + @R.register("anthropic") class AnthropicAsLLM(BaseAsLLM): """Anthropic chat model wrapper.""" @@ -101,6 +144,7 @@ class XAIAsLLM(BaseAsLLM): __all__ = [ "BaseAsLLM", "OpenAIAsLLM", + "OrcaRouterAsLLM", "AnthropicAsLLM", "DashScopeAsLLM", "DeepSeekAsLLM", diff --git a/tests/unit/test_orcarouter_provider.py b/tests/unit/test_orcarouter_provider.py new file mode 100644 index 00000000..1b6c3208 --- /dev/null +++ b/tests/unit/test_orcarouter_provider.py @@ -0,0 +1,40 @@ +"""Tests for the OrcaRouter LLM provider.""" + +# pylint: disable=missing-function-docstring,missing-class-docstring,unused-argument + +from reme.components.as_llm import OrcaRouterAsLLM, OrcaRouterCredential +from reme.components.component_registry import R, create_application_registry +from reme.enumeration import ComponentEnum + + +def test_orcarouter_registered_in_builtin_registry(): + assert R.get(ComponentEnum.AS_LLM, "orcarouter") is OrcaRouterAsLLM + + +def test_application_registry_includes_orcarouter(): + reg = create_application_registry() + assert reg.get(ComponentEnum.AS_LLM, "orcarouter") is OrcaRouterAsLLM + + +def test_orcarouter_credential_defaults_to_hosted_gateway(): + credential = OrcaRouterCredential(api_key="sk-orca-test") + assert credential.base_url == "https://api.orcarouter.ai/v1" + + +def test_orcarouter_credential_empty_base_url_falls_back_to_gateway(): + # The shared config passes ``base_url: ${LLM_BASE_URL:-}``, which yields an + # empty string when LLM_BASE_URL is unset. + credential = OrcaRouterCredential(api_key="sk-orca-test", base_url="") + assert credential.base_url == "https://api.orcarouter.ai/v1" + + +def test_orcarouter_credential_custom_base_url_kept(): + credential = OrcaRouterCredential( + api_key="sk-orca-test", + base_url="https://selfhosted.example.com/v1", + ) + assert credential.base_url == "https://selfhosted.example.com/v1" + + +def test_orcarouter_credential_reuses_openai_chat_model(): + assert OrcaRouterCredential.get_chat_model_class().__name__ == "OpenAIChatModel"