From c30aa39cdd822dc65f767d6d714f1bca7b6ab963 Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Fri, 21 Aug 2026 08:03:51 +0000 Subject: [PATCH 1/3] feat(llm): add OrcaRouter as a named OpenAI-compatible gateway Add a named OrcaRouter provider to the transcript-driven LLM clipping routing, mirroring the existing atlascloud/minimax gateways. Selecting any orcarouter/ model in the LLM Model Name dropdown routes requests to https://api.orcarouter.ai/v1/chat/completions with the ORCAROUTER_API_KEY environment variable (or pasted APIKEY). Co-Authored-By: Claude Signed-off-by: XiaoHuo888-hue --- README.md | 8 ++ README_zh.md | 8 ++ funclip/launch.py | 8 +- funclip/llm/openai_api.py | 16 ++++ tests/test_orcarouter_api.py | 93 +++++++++++++++++++++ tests/test_orcarouter_launch_integration.py | 74 ++++++++++++++++ 6 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 tests/test_orcarouter_api.py create mode 100644 tests/test_orcarouter_launch_integration.py diff --git a/README.md b/README.md index 245f07c..2404471 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,14 @@ Follow the guide below to explore LLM based clipping: +#### Using OrcaRouter as your LLM gateway (optional) + +Besides the transcript-based LLMs above, FunClip can route LLM-assisted clipping through [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible smart-routing gateway. Select any `orcarouter/` model in the **LLM Model Name** dropdown (`orcarouter/auto` routes each request to the best model for the task), paste an OrcaRouter API key in the **APIKEY** box, and click 'LLM Inference' — FunClip sends the transcript and prompts to `https://api.orcarouter.ai/v1/chat/completions`, and the returned segments work with the existing 'AI Clip' button unchanged. + +OrcaRouter exposes one endpoint for all frontier and open-weight models, so you can switch routing targets without changing FunClip. 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. + +Set `ORCAROUTER_API_KEY` (and optionally `ORCAROUTER_API_BASE`, which defaults to `https://api.orcarouter.ai/v1`) instead of pasting the key into the UI if you prefer environment-based configuration. A key is available at https://www.orcarouter.ai. + #### Content-aware clipping with TwelveLabs Pegasus (optional) Besides the transcript-based LLMs above, FunClip can optionally use [TwelveLabs](https://twelvelabs.io) Pegasus, a video understanding model that reasons over the actual video (visuals + audio) rather than only the ASR transcript. This helps pick highlight segments even when the transcript alone is ambiguous (e.g. action, scene changes, on-screen events). To use it, select the `pegasus1.5` model name, paste your TwelveLabs API key, upload a video, and click 'LLM Inference' — Pegasus returns segments in the same `N. [start-end] text` format, so the existing 'AI Clip' button works unchanged. It needs `pip install twelvelabs`, and a free API key is available at https://twelvelabs.io. diff --git a/README_zh.md b/README_zh.md index c00ba83..d823c39 100644 --- a/README_zh.md +++ b/README_zh.md @@ -148,6 +148,14 @@ python funclip/launch.py +#### 使用 OrcaRouter 作为 LLM 网关(可选) + +除基于字幕的 LLM 外,FunClip 也可以将 LLM 智能裁剪路由到 [OrcaRouter](https://www.orcarouter.ai)——一个 OpenAI 兼容的智能路由网关。在 **LLM Model Name** 下拉框选择任意 `orcarouter/` 模型(`orcarouter/auto` 会自动为任务选择最佳模型),在 **APIKEY** 输入框粘贴 OrcaRouter API key,点击“LLM推理”——FunClip 会把字幕与 prompt 发送到 `https://api.orcarouter.ai/v1/chat/completions`,返回的分段与现有“AI Clip”按钮完全兼容。 + +OrcaRouter 用单一端点接入所有前沿与开源模型,无需修改 FunClip 即可切换路由目标。它还在同一端点上为 AI agent 提供网关级零信任安全防护——在默认拒绝(default-deny)基础上审查每一条 prompt/response 并管控每一次工具调用,且无需任何应用代码改动。 + +也可以不填 UI,而是设置 `ORCAROUTER_API_KEY` 环境变量(可选 `ORCAROUTER_API_BASE`,默认为 `https://api.orcarouter.ai/v1`)。Key 可在 https://www.orcarouter.ai 获取。 + ### B.通过命令行调用使用FunClip的相关功能 ```shell # 下载下面命令用到的示例视频 diff --git a/funclip/launch.py b/funclip/launch.py index 8fdf648..21d0734 100644 --- a/funclip/launch.py +++ b/funclip/launch.py @@ -163,7 +163,7 @@ def video_clip_addsub(dest_text, video_spk_input, start_ost, end_ost, state, out ) def llm_inference(system_content, user_content, srt_text, model, apikey, video_input=None): - SUPPORT_LLM_PREFIX = ['litellm', 'qwen', 'gpt', 'g4f', 'moonshot', 'deepseek', 'atlascloud', 'minimax', 'pegasus'] + SUPPORT_LLM_PREFIX = ['litellm', 'qwen', 'gpt', 'g4f', 'moonshot', 'deepseek', 'atlascloud', 'minimax', 'orcarouter', 'pegasus'] if model.startswith('litellm/'): return litellm_call(apikey, model, user_content+'\n'+srt_text, system_content) if model.startswith('pegasus'): @@ -175,7 +175,7 @@ def llm_inference(system_content, user_content, srt_text, model, apikey, video_i return call_twelvelabs_pegasus(apikey, video_input, model=model, prompt=system_content) if model.startswith('qwen'): return call_qwen_model(apikey, model, user_content+'\n'+srt_text, system_content) - if model.startswith('gpt') or model.startswith('moonshot') or model.startswith('deepseek') or model.startswith('atlascloud/') or model.startswith('minimax/'): + if model.startswith('gpt') or model.startswith('moonshot') or model.startswith('deepseek') or model.startswith('atlascloud/') or model.startswith('minimax/') or model.startswith('orcarouter/'): return openai_call(apikey, model, user_content+'\n'+srt_text, system_content) elif model.startswith('g4f'): model = "-".join(model.split('-')[1:]) @@ -282,6 +282,10 @@ def AI_clip_subti(LLM_res, dest_text, video_spk_input, start_ost, end_ost, video "minimax/MiniMax-M3", "minimax/MiniMax-M2.7", "minimax/MiniMax-M2.7-highspeed", + "orcarouter/auto", + "orcarouter/fusion", + "orcarouter/fusion-flash", + "orcarouter/fusion-mini", "pegasus1.5"], value="deepseek-chat", label="LLM Model Name", diff --git a/funclip/llm/openai_api.py b/funclip/llm/openai_api.py index a4cee62..4882558 100644 --- a/funclip/llm/openai_api.py +++ b/funclip/llm/openai_api.py @@ -12,6 +12,13 @@ MINIMAX_API_BASE_CN = "https://api.minimaxi.com/v1" MINIMAX_MODEL_PREFIX = "minimax/" +# OrcaRouter is an OpenAI-compatible smart-routing gateway: one chat +# completions endpoint that routes each request to the best model for the +# task. Model IDs carry an `orcarouter/` prefix (e.g. `orcarouter/auto`) and +# must be sent to the gateway verbatim — a bare `auto` is not routable. +ORCAROUTER_API_BASE = "https://api.orcarouter.ai/v1" +ORCAROUTER_MODEL_PREFIX = "orcarouter/" + def _resolve_model_config(model): base_url = None @@ -37,6 +44,15 @@ def _resolve_model_config(model): if not base_url: base_url = MINIMAX_API_BASE api_key_env = "MINIMAX_API_KEY" + elif model.startswith(ORCAROUTER_MODEL_PREFIX): + if len(model) <= len(ORCAROUTER_MODEL_PREFIX): + raise ValueError( + "Model name is empty after stripping orcarouter/ prefix" + ) + base_url = os.environ.get("ORCAROUTER_API_BASE", ORCAROUTER_API_BASE).strip() + if not base_url: + base_url = ORCAROUTER_API_BASE + api_key_env = "ORCAROUTER_API_KEY" elif model.startswith("deepseek"): base_url = "https://api.deepseek.com" elif model.startswith("gpt-3.5-turbo"): diff --git a/tests/test_orcarouter_api.py b/tests/test_orcarouter_api.py new file mode 100644 index 0000000..25ef519 --- /dev/null +++ b/tests/test_orcarouter_api.py @@ -0,0 +1,93 @@ +"""Tests for OrcaRouter routing through the OpenAI-compatible client.""" + +import os +import unittest +from unittest.mock import MagicMock, patch + +from funclip.llm.openai_api import ( + ORCAROUTER_API_BASE, + openai_call, +) + + +def _mock_completion(content="ok"): + completion = MagicMock() + completion.choices = [MagicMock()] + completion.choices[0].message.content = content + return completion + + +class TestOrcaRouterRouting(unittest.TestCase): + def test_orcarouter_prefix_uses_gateway_base_url(self): + client = MagicMock() + client.chat.completions.create.return_value = _mock_completion("clip plan") + + with patch("funclip.llm.openai_api.OpenAI", return_value=client) as openai_cls: + result = openai_call( + "orca-key", + "orcarouter/auto", + "subtitle text", + "find highlights", + ) + + self.assertEqual(result, "clip plan") + openai_cls.assert_called_once_with( + api_key="orca-key", + base_url=ORCAROUTER_API_BASE, + ) + # OrcaRouter is a multi-provider gateway: the model ID must keep the + # `orcarouter/` prefix so the router knows which namespace to route to. + call_kwargs = client.chat.completions.create.call_args[1] + self.assertEqual(call_kwargs["model"], "orcarouter/auto") + + def test_orcarouter_api_key_falls_back_to_env(self): + client = MagicMock() + client.chat.completions.create.return_value = _mock_completion() + + with patch.dict(os.environ, {"ORCAROUTER_API_KEY": "env-orca-key"}, clear=False): + with patch("funclip.llm.openai_api.OpenAI", return_value=client) as openai_cls: + openai_call("", "orcarouter/auto", "text") + + openai_cls.assert_called_once_with( + api_key="env-orca-key", + base_url=ORCAROUTER_API_BASE, + ) + call_kwargs = client.chat.completions.create.call_args[1] + self.assertEqual(call_kwargs["model"], "orcarouter/auto") + + def test_orcarouter_api_base_env_overrides(self): + client = MagicMock() + client.chat.completions.create.return_value = _mock_completion() + + with patch.dict( + os.environ, + {"ORCAROUTER_API_BASE": "https://gateway.example.com/v1"}, + clear=False, + ): + with patch("funclip.llm.openai_api.OpenAI", return_value=client) as openai_cls: + openai_call("orca-key", "orcarouter/fusion", "text") + + openai_cls.assert_called_once_with( + api_key="orca-key", + base_url="https://gateway.example.com/v1", + ) + + def test_empty_orcarouter_model_raises(self): + with self.assertRaises(ValueError): + openai_call("key", "orcarouter/", "text") + + def test_missing_orcarouter_key_does_not_fall_back_to_openai_key(self): + with patch.dict( + os.environ, + {"OPENAI_API_KEY": "openai-only-key"}, + clear=True, + ): + with patch("funclip.llm.openai_api.OpenAI") as openai_cls: + with self.assertRaisesRegex(ValueError, "ORCAROUTER_API_KEY"): + openai_call("", "orcarouter/auto", "text") + + openai_cls.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_orcarouter_launch_integration.py b/tests/test_orcarouter_launch_integration.py new file mode 100644 index 0000000..518a1aa --- /dev/null +++ b/tests/test_orcarouter_launch_integration.py @@ -0,0 +1,74 @@ +"""Regression tests for OrcaRouter choices and prompt routing in the launcher.""" + +import ast +import unittest +from pathlib import Path + + +LAUNCH_PATH = Path(__file__).resolve().parents[1] / "funclip" / "launch.py" + + +class TestOrcaRouterLaunchIntegration(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tree = ast.parse(LAUNCH_PATH.read_text(encoding="utf-8")) + + def test_openai_compatible_route_handles_orcarouter_prefix(self): + llm_inference = next( + node + for node in ast.walk(self.tree) + if isinstance(node, ast.FunctionDef) and node.name == "llm_inference" + ) + openai_call = next( + node + for node in ast.walk(llm_inference) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "openai_call" + ) + # The openai_call dispatch branch must also cover orcarouter/ models. + dispatch_condition = next( + node + for node in ast.walk(llm_inference) + if isinstance(node, ast.If) and isinstance(node.test, ast.BoolOp) + ) + self.assertIn("orcarouter/", ast.unparse(dispatch_condition.test)) + self.assertEqual(ast.unparse(openai_call.args[2]), "user_content + '\\n' + srt_text") + self.assertEqual(ast.unparse(openai_call.args[3]), "system_content") + + def test_support_prefix_list_includes_orcarouter(self): + llm_inference = next( + node + for node in ast.walk(self.tree) + if isinstance(node, ast.FunctionDef) and node.name == "llm_inference" + ) + assigned = [ + node + for node in ast.walk(llm_inference) + if isinstance(node, ast.Assign) and node.targets[0].id == "SUPPORT_LLM_PREFIX" + ] + self.assertEqual(len(assigned), 1) + prefix_list = assigned[0].value + self.assertIsInstance(prefix_list, ast.List) + prefixes = { + node.value + for node in ast.walk(prefix_list) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + } + self.assertIn("orcarouter", prefixes) + + def test_dropdown_lists_orcarouter_gateway_models(self): + string_literals = { + node.value + for node in ast.walk(self.tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + } + + self.assertIn("orcarouter/auto", string_literals) + self.assertIn("orcarouter/fusion", string_literals) + self.assertIn("orcarouter/fusion-flash", string_literals) + self.assertIn("orcarouter/fusion-mini", string_literals) + + +if __name__ == "__main__": + unittest.main() From caa6b3a0f672df3ac490b6f7a3eedf0a1e64681d Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Wed, 26 Aug 2026 08:08:37 +0000 Subject: [PATCH 2/3] docs: qualify OrcaRouter security enforcement as opt-in Address reviewer feedback on PR #205: the integration only supplies the base URL, API key, and model, and does not attach a Guardrail or Firewall policy by itself. Make clear enforcement is opt-in once a policy is applied, and scope coverage to tool calls that traverse the gateway. Add links to the security quickstart, Guardrails, and Firewall docs in both README.md and README_zh.md. Signed-off-by: XiaoHuo888-hue --- README.md | 2 +- README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2404471..4364063 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Follow the guide below to explore LLM based clipping: Besides the transcript-based LLMs above, FunClip can route LLM-assisted clipping through [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible smart-routing gateway. Select any `orcarouter/` model in the **LLM Model Name** dropdown (`orcarouter/auto` routes each request to the best model for the task), paste an OrcaRouter API key in the **APIKEY** box, and click 'LLM Inference' — FunClip sends the transcript and prompts to `https://api.orcarouter.ai/v1/chat/completions`, and the returned segments work with the existing 'AI Clip' button unchanged. -OrcaRouter exposes one endpoint for all frontier and open-weight models, so you can switch routing targets without changing FunClip. 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. +OrcaRouter exposes one endpoint for all frontier and open-weight models, so you can switch routing targets without changing FunClip. It also runs gateway-level, zero-trust security for AI agents on the same endpoint: once a policy is attached, it screens prompts/responses and governs tool calls that traverse the gateway on a default-deny basis, with no application code changes. Enforcement is opt-in — this integration supplies only the base URL, API key, and model, and does not attach a Guardrail or Firewall policy by itself. Apply a policy via the [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart); see [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) and [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution) for how coverage is scoped. Set `ORCAROUTER_API_KEY` (and optionally `ORCAROUTER_API_BASE`, which defaults to `https://api.orcarouter.ai/v1`) instead of pasting the key into the UI if you prefer environment-based configuration. A key is available at https://www.orcarouter.ai. diff --git a/README_zh.md b/README_zh.md index d823c39..ebbbbe9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -152,7 +152,7 @@ python funclip/launch.py 除基于字幕的 LLM 外,FunClip 也可以将 LLM 智能裁剪路由到 [OrcaRouter](https://www.orcarouter.ai)——一个 OpenAI 兼容的智能路由网关。在 **LLM Model Name** 下拉框选择任意 `orcarouter/` 模型(`orcarouter/auto` 会自动为任务选择最佳模型),在 **APIKEY** 输入框粘贴 OrcaRouter API key,点击“LLM推理”——FunClip 会把字幕与 prompt 发送到 `https://api.orcarouter.ai/v1/chat/completions`,返回的分段与现有“AI Clip”按钮完全兼容。 -OrcaRouter 用单一端点接入所有前沿与开源模型,无需修改 FunClip 即可切换路由目标。它还在同一端点上为 AI agent 提供网关级零信任安全防护——在默认拒绝(default-deny)基础上审查每一条 prompt/response 并管控每一次工具调用,且无需任何应用代码改动。 +OrcaRouter 用单一端点接入所有前沿与开源模型,无需修改 FunClip 即可切换路由目标。它还可以在同一端点上为 AI agent 提供网关级零信任安全防护——在附加策略后,审查 prompt/response 并以默认拒绝(default-deny)原则管控经网关转发的工具调用,且无需任何应用代码改动。该防护为可选开启:本集成只提供 base URL、API key 与模型,本身不会附加 Guardrail 或 Firewall 策略。请按 [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart) 应用策略;覆盖范围详见 [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) 与 [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution)。 也可以不填 UI,而是设置 `ORCAROUTER_API_KEY` 环境变量(可选 `ORCAROUTER_API_BASE`,默认为 `https://api.orcarouter.ai/v1`)。Key 可在 https://www.orcarouter.ai 获取。 From 613ca5e7ba6330d53602552e389b5a4b5eb175a7 Mon Sep 17 00:00:00 2001 From: zhifu gao <18321252+LauraGPT@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:00:42 +0000 Subject: [PATCH 3/3] docs: clarify OrcaRouter policy enforcement Signed-off-by: zhifu gao <18321252+LauraGPT@users.noreply.github.com> --- README.md | 2 +- README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4364063..24f2b67 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Follow the guide below to explore LLM based clipping: Besides the transcript-based LLMs above, FunClip can route LLM-assisted clipping through [OrcaRouter](https://www.orcarouter.ai), an OpenAI-compatible smart-routing gateway. Select any `orcarouter/` model in the **LLM Model Name** dropdown (`orcarouter/auto` routes each request to the best model for the task), paste an OrcaRouter API key in the **APIKEY** box, and click 'LLM Inference' — FunClip sends the transcript and prompts to `https://api.orcarouter.ai/v1/chat/completions`, and the returned segments work with the existing 'AI Clip' button unchanged. -OrcaRouter exposes one endpoint for all frontier and open-weight models, so you can switch routing targets without changing FunClip. It also runs gateway-level, zero-trust security for AI agents on the same endpoint: once a policy is attached, it screens prompts/responses and governs tool calls that traverse the gateway on a default-deny basis, with no application code changes. Enforcement is opt-in — this integration supplies only the base URL, API key, and model, and does not attach a Guardrail or Firewall policy by itself. Apply a policy via the [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart); see [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) and [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution) for how coverage is scoped. +OrcaRouter exposes one endpoint for all frontier and open-weight models, so you can switch routing targets without changing FunClip. It also offers gateway-level, zero-trust controls for AI agents on the same endpoint. Enforcement follows the Guardrail or Firewall policy attached to the relevant scope for prompts, responses, and tool calls that traverse the gateway; select the documented `tight` posture to enable default-deny behavior. Enforcement is opt-in — this integration supplies only the base URL, API key, and model, and does not attach a policy by itself. Apply a policy via the [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart); see [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) and [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution) for how coverage is scoped. Set `ORCAROUTER_API_KEY` (and optionally `ORCAROUTER_API_BASE`, which defaults to `https://api.orcarouter.ai/v1`) instead of pasting the key into the UI if you prefer environment-based configuration. A key is available at https://www.orcarouter.ai. diff --git a/README_zh.md b/README_zh.md index ebbbbe9..03db364 100644 --- a/README_zh.md +++ b/README_zh.md @@ -152,7 +152,7 @@ python funclip/launch.py 除基于字幕的 LLM 外,FunClip 也可以将 LLM 智能裁剪路由到 [OrcaRouter](https://www.orcarouter.ai)——一个 OpenAI 兼容的智能路由网关。在 **LLM Model Name** 下拉框选择任意 `orcarouter/` 模型(`orcarouter/auto` 会自动为任务选择最佳模型),在 **APIKEY** 输入框粘贴 OrcaRouter API key,点击“LLM推理”——FunClip 会把字幕与 prompt 发送到 `https://api.orcarouter.ai/v1/chat/completions`,返回的分段与现有“AI Clip”按钮完全兼容。 -OrcaRouter 用单一端点接入所有前沿与开源模型,无需修改 FunClip 即可切换路由目标。它还可以在同一端点上为 AI agent 提供网关级零信任安全防护——在附加策略后,审查 prompt/response 并以默认拒绝(default-deny)原则管控经网关转发的工具调用,且无需任何应用代码改动。该防护为可选开启:本集成只提供 base URL、API key 与模型,本身不会附加 Guardrail 或 Firewall 策略。请按 [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart) 应用策略;覆盖范围详见 [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) 与 [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution)。 +OrcaRouter 用单一端点接入所有前沿与开源模型,无需修改 FunClip 即可切换路由目标。它还可以在同一端点上为 AI agent 提供网关级零信任控制。对经网关转发的 prompt、response 与工具调用,实际执行方式由相关作用域附加的 Guardrail 或 Firewall 策略决定;选择文档中的 `tight` posture 才会启用默认拒绝(default-deny)。该防护为可选开启:本集成只提供 base URL、API key 与模型,本身不会附加策略。请按 [security quickstart](https://docs.orcarouter.ai/security/concepts/quickstart) 应用策略;覆盖范围详见 [Guardrails](https://docs.orcarouter.ai/features/guardrails#scoping-and-the-workspace-default) 与 [Firewall](https://docs.orcarouter.ai/features/firewall#scoping-and-resolution)。 也可以不填 UI,而是设置 `ORCAROUTER_API_KEY` 环境变量(可选 `ORCAROUTER_API_BASE`,默认为 `https://api.orcarouter.ai/v1`)。Key 可在 https://www.orcarouter.ai 获取。