This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-11997-ffdffb98290f20f8a1b5725e72d3c31bb7a9c253 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit fa66cce41587838581014dca039e8c249adeb219 Author: kuswardhanietidims-svg <[email protected]> AuthorDate: Sun Aug 30 04:22:32 2026 +0000 [Feature][CLI] Add OrcaRouter AI gateway LLM provider (#11997) Co-authored-by: kuswardhanietidims-svg <[email protected]> Co-authored-by: Claude <[email protected]> --- docs/en/ai-cli/overview.md | 2 +- docs/en/ai-cli/quickstart.md | 37 ++++ docs/zh/ai-cli/overview.md | 2 +- docs/zh/ai-cli/quickstart.md | 34 ++++ seatunnel-cli/README.md | 37 +++- seatunnel-cli/README.zh-CN.md | 32 +++- seatunnel-cli/env.example.sh | 11 ++ seatunnel-cli/seatunnel_cli/cli.py | 59 +++++- seatunnel-cli/seatunnel_cli/llm_provider.py | 96 ++++++++-- seatunnel-cli/setup.sh | 4 + seatunnel-cli/tests/test_cli_provider_routing.py | 13 +- .../tests/test_llm_provider_orcarouter.py | 200 +++++++++++++++++++++ 12 files changed, 498 insertions(+), 29 deletions(-) diff --git a/docs/en/ai-cli/overview.md b/docs/en/ai-cli/overview.md index bd505651be..6ebd1c1208 100644 --- a/docs/en/ai-cli/overview.md +++ b/docs/en/ai-cli/overview.md @@ -21,7 +21,7 @@ The AI CLI ships as the `seatunnel-cli` module inside the main SeaTunnel reposit ## Key Capabilities - **Natural language to config** — English or Chinese input, complete HOCON output -- **Multi-provider LLM** — AWS Bedrock (including OpenAI-family models via the `bedrock-mantle` endpoint), Anthropic API, OpenAI and compatible APIs +- **Multi-provider LLM** — AWS Bedrock (including OpenAI-family models via the `bedrock-mantle` endpoint), Anthropic API, OpenAI and compatible APIs, OrcaRouter AI gateway - **Multi-agent pipeline** — Planner → Config Generator → Validator → auto-fix, with up to 3 correction rounds - **Connector knowledge** — 150+ connectors with full option rules and value constraints, resolved from a live engine or bundled metadata - **Validation & repair** — local checks, engine `--check`/dry-run, and LLM-powered diagnosis-and-repair when `/check` or `/run` fails diff --git a/docs/en/ai-cli/quickstart.md b/docs/en/ai-cli/quickstart.md index a1ea10f88f..a513ec07b9 100644 --- a/docs/en/ai-cli/quickstart.md +++ b/docs/en/ai-cli/quickstart.md @@ -12,6 +12,7 @@ sidebar_position: 2 - **AWS Bedrock** — AWS credentials (profile, env vars, or IAM role) - **Anthropic API** — `ANTHROPIC_API_KEY` - **OpenAI API** (or compatible) — `OPENAI_API_KEY` + - **OrcaRouter** — `ORCAROUTER_API_KEY` - (Optional) a SeaTunnel installation for engine-level validation and job execution ## Install @@ -58,8 +59,44 @@ export ANTHROPIC_API_KEY=sk-ant-... export AI_PROVIDER=openai export OPENAI_API_KEY=sk-... # export OPENAI_BASE_URL=https://... # Azure OpenAI, DeepSeek, local vLLM, ... + +# Option D: OrcaRouter AI gateway +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... +# Model IDs use a provider/model namespace (e.g. deepseek/deepseek-v4-pro); +# `orcarouter/auto` auto-grades and auto-routes each request. +# export ORCAROUTER_MODEL=orcarouter/auto +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # optional: replay reasoning_content for reasoning models +``` + +### OrcaRouter AI gateway + +[OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible AI gateway +that exposes many models — Claude, GPT, Gemini, DeepSeek, Qwen and more — +behind a single endpoint (`https://api.orcarouter.ai/v1`). Model IDs follow a +`provider/model` namespace, and the special `orcarouter/auto` model +automatically selects the best model per request. Configure it as a +first-class provider: + +```bash +# Requires the openai package (shares the ".[openai]" extra) +pip install -e ".[openai]" + +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... +# export ORCAROUTER_MODEL=deepseek/deepseek-v4-pro # optional override +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto # optional override +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # optional: replay reasoning_content + +seatunnel "Sync MySQL users table to S3 Parquet" ``` +The provider speaks the OpenAI Chat Completions protocol, so it fully supports +the CLI's internal tool-calling loop (connector lookups during planning), +streaming output, multi-turn sessions, and reasoning-content replay for +compatible reasoning models. + ### bedrock-mantle: OpenAI-family models on Bedrock Some OpenAI models on Bedrock (e.g. `openai.gpt-5.6-terra`, `openai.gpt-5.6-sol`) diff --git a/docs/zh/ai-cli/overview.md b/docs/zh/ai-cli/overview.md index e53961264d..e09faf64b5 100644 --- a/docs/zh/ai-cli/overview.md +++ b/docs/zh/ai-cli/overview.md @@ -21,7 +21,7 @@ AI CLI 以 `seatunnel-cli` 模块的形式内置在 SeaTunnel 主仓库中,并 ## 核心能力 - **自然语言生成配置** —— 中英文输入,输出完整 HOCON 配置 -- **多 LLM 提供商** —— AWS Bedrock(含通过 `bedrock-mantle` 端点接入的 OpenAI 系模型)、Anthropic API、OpenAI 及兼容 API +- **多 LLM 提供商** —— AWS Bedrock(含通过 `bedrock-mantle` 端点接入的 OpenAI 系模型)、Anthropic API、OpenAI 及兼容 API、OrcaRouter AI 网关 - **多智能体流水线** —— Planner → 配置生成 → 校验 → 自动修复,最多 3 轮纠错 - **连接器知识库** —— 150+ 连接器的完整选项规则与取值约束,来自运行中引擎或内置元数据 - **校验与修复** —— 本地检查、引擎 `--check`/dry-run,`/check` 或 `/run` 失败时由 LLM 自动诊断修复 diff --git a/docs/zh/ai-cli/quickstart.md b/docs/zh/ai-cli/quickstart.md index d7b722e982..02806de4bc 100644 --- a/docs/zh/ai-cli/quickstart.md +++ b/docs/zh/ai-cli/quickstart.md @@ -12,6 +12,7 @@ sidebar_position: 2 - **AWS Bedrock** —— AWS 凭证(profile、环境变量或 IAM 角色) - **Anthropic API** —— `ANTHROPIC_API_KEY` - **OpenAI API**(或兼容 API)—— `OPENAI_API_KEY` + - **OrcaRouter** —— `ORCAROUTER_API_KEY` - (可选)SeaTunnel 安装目录,用于引擎级校验和作业执行 ## 安装 @@ -57,8 +58,41 @@ export ANTHROPIC_API_KEY=sk-ant-... export AI_PROVIDER=openai export OPENAI_API_KEY=sk-... # export OPENAI_BASE_URL=https://... # Azure OpenAI、DeepSeek、本地 vLLM 等 + +# 方式 D:OrcaRouter AI 网关 +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... +# 模型 ID 使用 provider/model 命名空间(如 deepseek/deepseek-v4-pro); +# `orcarouter/auto` 会自动评级并路由每个请求。 +# export ORCAROUTER_MODEL=orcarouter/auto +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # 可选:保留并回传推理模型的 reasoning_content +``` + +### OrcaRouter AI 网关 + +[OrcaRouter](https://www.orcarouter.ai) 是一个 OpenAI 兼容的 AI 网关,在单个端点 +(`https://api.orcarouter.ai/v1`)之后暴露众多模型——Claude、GPT、Gemini、 +DeepSeek、Qwen 等。模型 ID 使用 `provider/model` 命名空间,特殊的 +`orcarouter/auto` 模型会自动为每个请求选择最佳模型。作为一等提供商配置: + +```bash +# 需要 openai 包(复用 ".[openai]" extra) +pip install -e ".[openai]" + +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... +# export ORCAROUTER_MODEL=deepseek/deepseek-v4-pro # 可选覆盖 +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto # 可选覆盖 +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # 可选:回传 reasoning_content + +seatunnel "Sync MySQL users table to S3 Parquet" ``` +该提供商使用 OpenAI Chat Completions 协议,因此完全支持 CLI 内部的工具调用循环 +(规划期间的连接器查询)、流式输出、多轮会话,以及兼容推理模型的 +reasoning_content 回放。 + ### bedrock-mantle:Bedrock 上的 OpenAI 系模型 Bedrock 上的部分 OpenAI 模型(如 `openai.gpt-5.6-terra`、`openai.gpt-5.6-sol`) diff --git a/seatunnel-cli/README.md b/seatunnel-cli/README.md index de890b0964..97b8772665 100644 --- a/seatunnel-cli/README.md +++ b/seatunnel-cli/README.md @@ -7,7 +7,7 @@ Describe your data synchronization task in English or Chinese, and the CLI gener ## Features - **Natural Language to Config** -- Describe what you want in plain English or Chinese, get a valid SeaTunnel config -- **Multi-Provider LLM** -- AWS Bedrock, Anthropic API, OpenAI (and compatible APIs like Azure OpenAI) +- **Multi-Provider LLM** -- AWS Bedrock, Anthropic API, OpenAI (and compatible APIs like Azure OpenAI), OrcaRouter AI gateway - **Multi-Agent Pipeline** -- Planner -> Generator -> Validator -> Auto-fix, up to 3 correction rounds - **100+ Connectors** -- Full coverage of SeaTunnel's connector ecosystem with runtime metadata reflection - **Transform Metadata** -- Source, sink, and transform plugins use full option rules and value constraints during generation @@ -27,6 +27,7 @@ Describe your data synchronization task in English or Chinese, and the CLI gener - **AWS Bedrock** -- requires AWS credentials and `boto3` - **Anthropic API** -- requires `ANTHROPIC_API_KEY` and `anthropic` package - **OpenAI API** -- requires `OPENAI_API_KEY` and `openai` package + - **OrcaRouter** -- requires `ORCAROUTER_API_KEY` and `openai` package - (Optional) Running Apache SeaTunnel engine for live metadata and job execution > **Note:** When launched via `bin/seatunnel-ai.sh`, Python dependencies are > installed automatically on first run. No manual `pip install` needed. @@ -67,7 +68,7 @@ seatunnel --init ```bash pip install -e ".[bedrock]" # AWS Bedrock pip install -e ".[anthropic]" # Anthropic API -pip install -e ".[openai]" # OpenAI API +pip install -e ".[openai]" # OpenAI API / OrcaRouter pip install -e ".[all]" # All providers pip install -e ".[dev]" # Development (all providers + pytest, ruff) ``` @@ -165,6 +166,30 @@ export OPENAI_SMALL_FAST_MODEL=gpt-4o-mini # export OPENAI_ECHO_REASONING_CONTENT=true ``` +#### Option D: OrcaRouter AI Gateway + +```bash +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... + +# Model overrides (optional) — OrcaRouter model IDs use a provider/model namespace, +# e.g. openai/gpt-5.5-pro, deepseek/deepseek-v4-pro, anthropic/claude-fable-5. +# The special model `orcarouter/auto` auto-grades and auto-routes each request. +# export ORCAROUTER_MODEL=orcarouter/auto +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # optional: replay reasoning_content for reasoning models +``` + +Requires: `pip install -e ".[openai]"` (the `openai` package). + +[OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible AI gateway +that exposes many models — including Claude, GPT, Gemini, DeepSeek and Qwen — +behind a single endpoint (`https://api.orcarouter.ai/v1`). Model IDs follow a +`provider/model` namespace, and the `orcarouter/auto` model automatically +selects the best model per request. The OrcaRouter provider speaks the OpenAI +Chat Completions protocol, so it inherits the same reasoning-content replay, +streaming and tool-calling support as the `openai` provider. + ### SEATUNNEL_HOME `SEATUNNEL_HOME` is the path to your Apache SeaTunnel engine installation. The CLI uses it to: @@ -207,16 +232,20 @@ When the engine is running, the CLI operates in **cluster mode** with live conne | Variable | Required | Default | Description | |----------|----------|---------|-------------| -| `AI_PROVIDER` | No | `bedrock` | LLM provider: `bedrock`, `bedrock-mantle`, `anthropic`, or `openai` | +| `AI_PROVIDER` | No | `bedrock` | LLM provider: `bedrock`, `bedrock-mantle`, `anthropic`, `openai`, or `orcarouter` | | `AWS_REGION` | Bedrock | `us-east-1` | AWS region for Bedrock | | `ANTHROPIC_API_KEY` | Anthropic | -- | Anthropic API key | | `OPENAI_API_KEY` | OpenAI | -- | OpenAI API key | | `OPENAI_BASE_URL` | No | -- | Custom endpoint for OpenAI-compatible APIs | | `OPENAI_ECHO_REASONING_CONTENT` | No | `true` | Preserve and replay `reasoning_content` for OpenAI-compatible reasoning models such as DeepSeek or GLM thinking mode | +| `ORCAROUTER_API_KEY` | OrcaRouter | -- | OrcaRouter API key | | `ANTHROPIC_MODEL` | No | Provider default | Override primary model ID | | `ANTHROPIC_SMALL_FAST_MODEL` | No | Provider default | Override fast model ID | | `OPENAI_MODEL` | No | `gpt-4o` | Primary model for OpenAI provider | | `OPENAI_SMALL_FAST_MODEL` | No | `gpt-4o-mini` | Fast model for OpenAI provider | +| `ORCAROUTER_MODEL` | No | `orcarouter/auto` | Primary model for OrcaRouter provider (provider/model namespace) | +| `ORCAROUTER_SMALL_FAST_MODEL` | No | `orcarouter/auto` | Fast model for OrcaRouter provider | +| `ORCAROUTER_ECHO_REASONING_CONTENT` | No | `true` | Preserve and replay `reasoning_content` for OpenAI-compatible reasoning models (parity with `OPENAI_ECHO_REASONING_CONTENT`) | | `SEATUNNEL_HOME` | No | Auto-detect | SeaTunnel installation directory. Auto-detected in distribution tarball; set manually for source install | | `SEATUNNEL_API_BASE` | No | `http://localhost:5801` | SeaTunnel REST API endpoint | | `SEATUNNEL_CLI_DATA` | No | `<cli-package>/.data/` | Override CLI data directory (sessions, memory, config) | @@ -256,7 +285,7 @@ Positional: Options: -o, --output PATH Save generated config to file - --provider PROVIDER LLM provider: bedrock | anthropic | openai + --provider PROVIDER LLM provider: bedrock | bedrock-mantle | anthropic | openai | orcarouter --model MODEL Override primary model ID --fast-model MODEL Override fast model ID --sync-catalog PATH Regenerate connector catalog from SeaTunnel source diff --git a/seatunnel-cli/README.zh-CN.md b/seatunnel-cli/README.zh-CN.md index 0b0f97f57b..54d68339e1 100644 --- a/seatunnel-cli/README.zh-CN.md +++ b/seatunnel-cli/README.zh-CN.md @@ -7,7 +7,7 @@ ## 功能特性 - **自然语言转配置** -- 用中文或英文描述需求,即可获得有效的 SeaTunnel 配置 -- **多 LLM 提供商** -- 支持 AWS Bedrock、Anthropic API、OpenAI(及兼容 API,如 Azure OpenAI) +- **多 LLM 提供商** -- 支持 AWS Bedrock、Anthropic API、OpenAI(及兼容 API,如 Azure OpenAI)、OrcaRouter AI 网关 - **多智能体流水线** -- 规划器 -> 生成器 -> 校验器 -> 自动修复,最多 3 轮纠错 - **100+ 连接器** -- 全面覆盖 SeaTunnel 连接器生态,支持运行时元数据反射 - **Transform 元数据** -- Source、Sink 和 Transform 插件在生成配置时都支持完整选项规则和值约束 @@ -136,6 +136,28 @@ export OPENAI_SMALL_FAST_MODEL=gpt-4o-mini # export OPENAI_ECHO_REASONING_CONTENT=true ``` +#### 方案 D:OrcaRouter AI 网关 + +```bash +export AI_PROVIDER=orcarouter +export ORCAROUTER_API_KEY=orc_... + +# 模型覆盖(可选)——OrcaRouter 模型 ID 使用 provider/model 命名空间, +# 例如 openai/gpt-5.5-pro、deepseek/deepseek-v4-pro、anthropic/claude-fable-5。 +# 特殊模型 `orcarouter/auto` 会自动评级并路由每个请求。 +# export ORCAROUTER_MODEL=orcarouter/auto +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # 可选:保留并回传推理模型的 reasoning_content +``` + +需要:`pip install -e ".[openai]"`(`openai` 包)。 + +[OrcaRouter](https://www.orcarouter.ai) 是一个 OpenAI 兼容的 AI 网关,在单个端点 +(`https://api.orcarouter.ai/v1`)之后暴露众多模型——Claude、GPT、Gemini、 +DeepSeek、Qwen 等。模型 ID 使用 `provider/model` 命名空间,`orcarouter/auto` +模型会自动为每个请求选择最佳模型。OrcaRouter 提供商使用 OpenAI Chat Completions +协议,因此与 `openai` 提供商一样支持 reasoning_content 回放、流式输出和工具调用。 + ### SEATUNNEL_HOME `SEATUNNEL_HOME` 是 Apache SeaTunnel 引擎的安装路径。CLI 使用它来: @@ -178,16 +200,20 @@ export SEATUNNEL_API_BASE=http://localhost:5801 # 默认值 | 变量 | 是否必需 | 默认值 | 说明 | |------|---------|--------|------| -| `AI_PROVIDER` | 否 | `bedrock` | LLM 提供商:`bedrock`、`anthropic` 或 `openai` | +| `AI_PROVIDER` | 否 | `bedrock` | LLM 提供商:`bedrock`、`bedrock-mantle`、`anthropic`、`openai` 或 `orcarouter` | | `AWS_REGION` | Bedrock 必需 | `us-east-1` | Bedrock 使用的 AWS 区域 | | `ANTHROPIC_API_KEY` | Anthropic 必需 | -- | Anthropic API 密钥 | | `OPENAI_API_KEY` | OpenAI 必需 | -- | OpenAI API 密钥 | | `OPENAI_BASE_URL` | 否 | -- | OpenAI 兼容 API 的自定义端点 | | `OPENAI_ECHO_REASONING_CONTENT` | 否 | `true` | 为 DeepSeek、GLM 思考模式等 OpenAI 兼容推理模型保留并回传 `reasoning_content` | +| `ORCAROUTER_API_KEY` | OrcaRouter 必需 | -- | OrcaRouter API 密钥 | | `ANTHROPIC_MODEL` | 否 | 提供商默认值 | 覆盖主模型 ID | | `ANTHROPIC_SMALL_FAST_MODEL` | 否 | 提供商默认值 | 覆盖快速模型 ID | | `OPENAI_MODEL` | 否 | `gpt-4o` | OpenAI 提供商的主模型 | | `OPENAI_SMALL_FAST_MODEL` | 否 | `gpt-4o-mini` | OpenAI 提供商的快速模型 | +| `ORCAROUTER_MODEL` | 否 | `orcarouter/auto` | OrcaRouter 提供商的主模型(provider/model 命名空间) | +| `ORCAROUTER_SMALL_FAST_MODEL` | 否 | `orcarouter/auto` | OrcaRouter 提供商的快速模型 | +| `ORCAROUTER_ECHO_REASONING_CONTENT` | 否 | `true` | 保留并回传 `reasoning_content`(与 `OPENAI_ECHO_REASONING_CONTENT` 对齐) | | `SEATUNNEL_HOME` | 否 | 自动检测 | SeaTunnel 安装目录。发行版压缩包中自动检测;源码安装需手动设置 | | `SEATUNNEL_API_BASE` | 否 | `http://localhost:5801` | SeaTunnel REST API 端点 | | `SEATUNNEL_CLI_DATA` | 否 | `<cli-package>/.data/` | 覆盖 CLI 数据目录(会话、记忆、配置) | @@ -227,7 +253,7 @@ seatunnel [request] [options] 选项: -o, --output PATH 将生成的配置保存到文件 - --provider PROVIDER LLM 提供商:bedrock | anthropic | openai + --provider PROVIDER LLM 提供商:bedrock | bedrock-mantle | anthropic | openai | orcarouter --model MODEL 覆盖主模型 ID --fast-model MODEL 覆盖快速模型 ID --sync-catalog PATH 从 SeaTunnel 源码重新生成连接器目录 diff --git a/seatunnel-cli/env.example.sh b/seatunnel-cli/env.example.sh index c684257df4..c53c4e5f57 100755 --- a/seatunnel-cli/env.example.sh +++ b/seatunnel-cli/env.example.sh @@ -29,6 +29,7 @@ # export AI_PROVIDER=bedrock-mantle # Option C2: OpenAI-family models on Bedrock # # (GPT-5.6 Terra/Sol; needs ".[bedrock-mantle]" extra; # # model via OPENAI_MODEL, e.g. openai.gpt-5.6-terra) +# export AI_PROVIDER=orcarouter # Option D: OrcaRouter AI gateway (needs ".[openai]" extra) # ─── Option A: Anthropic API (AI_PROVIDER=anthropic) ─── # export ANTHROPIC_API_KEY=sk-ant-... @@ -49,6 +50,16 @@ # export AWS_ACCESS_KEY_ID=... # export AWS_SECRET_ACCESS_KEY=... +# ─── Option D: OrcaRouter AI gateway (AI_PROVIDER=orcarouter) ─── +# OpenAI-compatible gateway: many models behind one endpoint, model IDs use a +# provider/model namespace (e.g. deepseek/deepseek-v4-pro, openai/gpt-5.5-pro). +# The special model `orcarouter/auto` auto-grades and auto-routes each request. +# Requires: pip install -e ".[openai]" +# export ORCAROUTER_API_KEY=orc_... +# export ORCAROUTER_MODEL=orcarouter/auto # optional override +# export ORCAROUTER_SMALL_FAST_MODEL=orcarouter/auto # optional override +# export ORCAROUTER_ECHO_REASONING_CONTENT=true # optional: keep true to replay reasoning_content for reasoning models + # ─── SeaTunnel Engine (optional) ─── # export SEATUNNEL_HOME=/path/to/seatunnel # export SEATUNNEL_API_BASE=http://localhost:5801 diff --git a/seatunnel-cli/seatunnel_cli/cli.py b/seatunnel-cli/seatunnel_cli/cli.py index 61988744af..89f8d7c028 100644 --- a/seatunnel-cli/seatunnel_cli/cli.py +++ b/seatunnel-cli/seatunnel_cli/cli.py @@ -514,20 +514,23 @@ class SeaTunnelCLI: console.print(" [bold]4[/bold]. bedrock-mantle — OpenAI-family models on Bedrock (GPT-5.6 Terra/Sol)") console.print(" Requires: AWS credentials + pip install \".[bedrock-mantle]\"") console.print(" Note: Responses-API-only models on the bedrock-mantle endpoint\n") + console.print(" [bold]5[/bold]. orcarouter — OrcaRouter AI gateway (OpenAI-compatible)") + console.print(" Requires: ORCAROUTER_API_KEY + pip install \".[openai]\"") + console.print(" Note: many models behind one endpoint, e.g. orcarouter/auto\n") try: - choice = pt_prompt(" Enter your choice (1/2/3/4): ").strip().lower() + choice = pt_prompt(" Enter your choice (1/2/3/4/5): ").strip().lower() except (EOFError, KeyboardInterrupt): console.print("\n Setup cancelled.", style="warning") return choice_map = {"1": "anthropic", "2": "openai", "3": "bedrock", - "4": "bedrock-mantle"} + "4": "bedrock-mantle", "5": "orcarouter"} choice = choice_map.get(choice, choice) if not choice or choice not in _PROVIDERS: console.print( - f" [error]Invalid choice: '{choice}'. Please enter 1, 2, 3, or 4.[/error]" + f" [error]Invalid choice: '{choice}'. Please enter 1, 2, 3, 4, or 5.[/error]" ) return @@ -603,6 +606,30 @@ class SeaTunnelCLI: config.setdefault("settings", {})["openai_base_url"] = base_url console.print(f" Base URL set: [bold]{base_url}[/bold]") + elif choice == "orcarouter": + existing = os.environ.get("ORCAROUTER_API_KEY") + if existing: + masked = existing[:7] + "..." + existing[-4:] if len(existing) > 15 else "***" + console.print(f" ORCAROUTER_API_KEY: [bold green]detected[/bold green] ({masked})") + else: + console.print(" ORCAROUTER_API_KEY not found in environment.\n") + console.print( + " [dim]Persistent (recommended): add to ~/.zshrc or ~/.bashrc:[/dim]\n" + " export ORCAROUTER_API_KEY=orc_...\n" + " [dim]Get a key: https://www.orcarouter.ai[/dim]\n", + ) + try: + key_input = pt_prompt( + " Enter API key for this session (or Enter to skip): ", + ).strip() + except (EOFError, KeyboardInterrupt): + key_input = "" + if key_input: + os.environ["ORCAROUTER_API_KEY"] = key_input + console.print( + " [success]Key set for this session (NOT saved to disk).[/success]" + ) + elif choice in ("bedrock", "bedrock-mantle"): console.print(" AWS Bedrock requires AWS credentials.\n") if choice == "bedrock-mantle": @@ -665,6 +692,11 @@ class SeaTunnelCLI: default_fast = "gpt-4o-mini" model_env = "OPENAI_MODEL" fast_env = "OPENAI_SMALL_FAST_MODEL" + elif choice == "orcarouter": + default_model = "orcarouter/auto" + default_fast = "orcarouter/auto" + model_env = "ORCAROUTER_MODEL" + fast_env = "ORCAROUTER_SMALL_FAST_MODEL" elif choice == "bedrock-mantle": default_model = "openai.gpt-5.6-terra" default_fast = "openai.gpt-5.6-terra" @@ -881,6 +913,15 @@ class SeaTunnelCLI: base_url = os.environ.get("OPENAI_BASE_URL") if base_url: self.console.print(f" Base URL: [bold]{base_url}[/bold]", style="info") + elif provider_name == "orcarouter": + if os.environ.get("ORCAROUTER_API_KEY"): + self.console.print(" API key: [bold green]configured[/bold green]", style="info") + else: + creds_ok = False + self.console.print("[error]ORCAROUTER_API_KEY not set.[/error]") + self.console.print( + " Base URL: [bold]https://api.orcarouter.ai/v1[/bold]", style="info" + ) self.console.print(f" Model: [bold]{provider.model_id}[/bold]", style="info") self.console.print(f" Fast model: [bold]{provider.fast_model_id}[/bold]", style="info") @@ -1527,7 +1568,7 @@ def main(): ) parser.add_argument( "--provider", - choices=["bedrock", "bedrock-mantle", "anthropic", "openai"], + choices=["bedrock", "bedrock-mantle", "anthropic", "openai", "orcarouter"], help="LLM provider (overrides AI_PROVIDER env var and config.json)", ) parser.add_argument( @@ -1579,17 +1620,21 @@ def main(): if args.provider: os.environ["AI_PROVIDER"] = args.provider # Providers speaking the OpenAI protocol read OPENAI_MODEL*; - # bedrock/anthropic read ANTHROPIC_MODEL*. + # bedrock/anthropic read ANTHROPIC_MODEL*; orcarouter has its own. _OPENAI_FAMILY = ("openai", "bedrock-mantle") if args.model: provider = os.environ.get("AI_PROVIDER", "").lower() - if provider in _OPENAI_FAMILY: + if provider == "orcarouter": + os.environ["ORCAROUTER_MODEL"] = args.model + elif provider in _OPENAI_FAMILY: os.environ["OPENAI_MODEL"] = args.model else: os.environ["ANTHROPIC_MODEL"] = args.model if args.fast_model: provider = os.environ.get("AI_PROVIDER", "").lower() - if provider in _OPENAI_FAMILY: + if provider == "orcarouter": + os.environ["ORCAROUTER_SMALL_FAST_MODEL"] = args.fast_model + elif provider in _OPENAI_FAMILY: os.environ["OPENAI_SMALL_FAST_MODEL"] = args.fast_model else: os.environ["ANTHROPIC_SMALL_FAST_MODEL"] = args.fast_model diff --git a/seatunnel-cli/seatunnel_cli/llm_provider.py b/seatunnel-cli/seatunnel_cli/llm_provider.py index 29a5c13384..a15fb43dfe 100644 --- a/seatunnel-cli/seatunnel_cli/llm_provider.py +++ b/seatunnel-cli/seatunnel_cli/llm_provider.py @@ -21,6 +21,7 @@ Supports multiple backends while presenting a unified interface: - bedrock : AWS Bedrock Converse API (Claude models) - anthropic: Anthropic Messages API (direct) - openai : OpenAI Chat Completions API + - orcarouter: OrcaRouter AI gateway (OpenAI Chat Completions API) All providers normalize their responses to a common internal format so that the agent layer (agents.py) needs no provider-specific code. @@ -1250,6 +1251,66 @@ class BedrockMantleProvider(LLMProvider): } +# ─── OrcaRouter Provider ─── + +class OrcaRouterProvider(OpenAIProvider): + """OrcaRouter AI gateway provider (OpenAI Chat Completions API). + + OrcaRouter is an OpenAI-compatible gateway that exposes many models (and + provider/model routing namespaces) behind one endpoint. Models are + addressed as ``provider/model``, e.g. ``orcarouter/auto`` routes and + grades automatically. Defaults to the ``orcarouter/auto`` model, which + automatically selects the best model for each request. + + Since OrcaRouter speaks the OpenAI Chat Completions protocol, this + provider mirrors :class:`OpenAIProvider` and only customizes the base + URL, the API key environment variable, and the default model. + """ + + #: OrcaRouter's OpenAI-compatible base URL. + DEFAULT_BASE_URL = "https://api.orcarouter.ai/v1" + #: Default model: auto-routes to the best model for the request. + DEFAULT_MODEL = "orcarouter/auto" + + def __init__(self): + try: + import openai + except ImportError: + raise ImportError( + "openai package required for AI_PROVIDER=orcarouter. " + "Install it: pip install openai" + ) + + api_key = os.environ.get("ORCAROUTER_API_KEY") + if not api_key: + raise ValueError( + "ORCAROUTER_API_KEY environment variable is required for " + "AI_PROVIDER=orcarouter" + ) + + self._model_id = os.environ.get( + "ORCAROUTER_MODEL", + os.environ.get("OPENAI_MODEL", self.DEFAULT_MODEL)) + self._fast_model_id = os.environ.get( + "ORCAROUTER_SMALL_FAST_MODEL", + os.environ.get("OPENAI_SMALL_FAST_MODEL", self._model_id)) + self._client = openai.OpenAI(api_key=api_key, base_url=self.DEFAULT_BASE_URL) + self._echo_reasoning_content = _env_bool( + "ORCAROUTER_ECHO_REASONING_CONTENT", True) + + @property + def provider_name(self) -> str: + return "orcarouter" + + @property + def model_id(self) -> str: + return self._model_id + + @property + def fast_model_id(self) -> str: + return self._fast_model_id + + # ─── Config file ─── @@ -1286,11 +1347,15 @@ def _auto_detect_provider() -> str | None: if os.environ.get("ANTHROPIC_API_KEY"): return "anthropic" - # 2. OpenAI API key + # 2. OrcaRouter gateway key + if os.environ.get("ORCAROUTER_API_KEY"): + return "orcarouter" + + # 3. OpenAI API key if os.environ.get("OPENAI_API_KEY"): return "openai" - # 3. AWS credentials (for Bedrock) + # 4. AWS credentials (for Bedrock) if os.environ.get("AWS_ACCESS_KEY_ID") or os.environ.get("AWS_PROFILE"): return "bedrock" try: @@ -1312,6 +1377,7 @@ _PROVIDERS = { "bedrock-mantle": BedrockMantleProvider, "anthropic": AnthropicProvider, "openai": OpenAIProvider, + "orcarouter": OrcaRouterProvider, } @@ -1354,16 +1420,22 @@ def create_provider(provider: str | None = None) -> LLMProvider: # Apply model overrides from config file if name and "models" in config: model_config = config["models"].get(name, {}) - if model_config.get("model") and not os.environ.get("ANTHROPIC_MODEL") and not os.environ.get("OPENAI_MODEL"): - if name in ("openai", "bedrock-mantle"): - os.environ.setdefault("OPENAI_MODEL", model_config["model"]) - else: - os.environ.setdefault("ANTHROPIC_MODEL", model_config["model"]) - if model_config.get("fast_model") and not os.environ.get("ANTHROPIC_SMALL_FAST_MODEL") and not os.environ.get("OPENAI_SMALL_FAST_MODEL"): - if name in ("openai", "bedrock-mantle"): - os.environ.setdefault("OPENAI_SMALL_FAST_MODEL", model_config["fast_model"]) - else: - os.environ.setdefault("ANTHROPIC_SMALL_FAST_MODEL", model_config["fast_model"]) + if model_config.get("model"): + if name == "orcarouter": + os.environ.setdefault("ORCAROUTER_MODEL", model_config["model"]) + elif not os.environ.get("ANTHROPIC_MODEL") and not os.environ.get("OPENAI_MODEL"): + if name in ("openai", "bedrock-mantle"): + os.environ.setdefault("OPENAI_MODEL", model_config["model"]) + else: + os.environ.setdefault("ANTHROPIC_MODEL", model_config["model"]) + if model_config.get("fast_model"): + if name == "orcarouter": + os.environ.setdefault("ORCAROUTER_SMALL_FAST_MODEL", model_config["fast_model"]) + elif not os.environ.get("ANTHROPIC_SMALL_FAST_MODEL") and not os.environ.get("OPENAI_SMALL_FAST_MODEL"): + if name in ("openai", "bedrock-mantle"): + os.environ.setdefault("OPENAI_SMALL_FAST_MODEL", model_config["fast_model"]) + else: + os.environ.setdefault("ANTHROPIC_SMALL_FAST_MODEL", model_config["fast_model"]) # 4. Auto-detect if not name: diff --git a/seatunnel-cli/setup.sh b/seatunnel-cli/setup.sh index 833d045d18..07703bdde6 100644 --- a/seatunnel-cli/setup.sh +++ b/seatunnel-cli/setup.sh @@ -73,6 +73,10 @@ echo " # AWS Bedrock" echo " export AI_PROVIDER=bedrock" echo " export AWS_REGION=us-east-1" echo "" +echo " # OrcaRouter AI gateway (OpenAI-compatible)" +echo " export AI_PROVIDER=orcarouter" +echo " export ORCAROUTER_API_KEY=orc_..." +echo "" echo " 3. Run the CLI:" echo "" echo " seatunnel # Interactive mode" diff --git a/seatunnel-cli/tests/test_cli_provider_routing.py b/seatunnel-cli/tests/test_cli_provider_routing.py index 39531fdbbc..1b459c9606 100644 --- a/seatunnel-cli/tests/test_cli_provider_routing.py +++ b/seatunnel-cli/tests/test_cli_provider_routing.py @@ -40,6 +40,7 @@ def _run_main_until_provider(argv): captured["AI_PROVIDER"] = os.environ.get("AI_PROVIDER") captured["OPENAI_MODEL"] = os.environ.get("OPENAI_MODEL") captured["ANTHROPIC_MODEL"] = os.environ.get("ANTHROPIC_MODEL") + captured["ORCAROUTER_MODEL"] = os.environ.get("ORCAROUTER_MODEL") raise _Stop() with mock.patch.object(sys, "argv", ["seatunnel"] + argv), \ @@ -53,7 +54,8 @@ def _run_main_until_provider(argv): def _clean_env(): saved = {k: os.environ.pop(k, None) for k in ("AI_PROVIDER", "OPENAI_MODEL", "ANTHROPIC_MODEL", - "OPENAI_SMALL_FAST_MODEL", "ANTHROPIC_SMALL_FAST_MODEL")} + "OPENAI_SMALL_FAST_MODEL", "ANTHROPIC_SMALL_FAST_MODEL", + "ORCAROUTER_MODEL", "ORCAROUTER_SMALL_FAST_MODEL")} yield for k, v in saved.items(): if v is None: @@ -77,6 +79,15 @@ def test_bedrock_still_routes_anthropic_model(): assert captured["OPENAI_MODEL"] is None +def test_orcarouter_accepted_by_argparse_and_routes_own_model(): + captured = _run_main_until_provider( + ["--provider", "orcarouter", "--model", "deepseek/deepseek-v4-pro", "hi"]) + assert captured["AI_PROVIDER"] == "orcarouter" + assert captured["ORCAROUTER_MODEL"] == "deepseek/deepseek-v4-pro" + assert captured["OPENAI_MODEL"] is None + assert captured["ANTHROPIC_MODEL"] is None + + def test_unknown_provider_rejected(): from seatunnel_cli import cli with mock.patch.object(sys, "argv", diff --git a/seatunnel-cli/tests/test_llm_provider_orcarouter.py b/seatunnel-cli/tests/test_llm_provider_orcarouter.py new file mode 100644 index 0000000000..50af2280d2 --- /dev/null +++ b/seatunnel-cli/tests/test_llm_provider_orcarouter.py @@ -0,0 +1,200 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest +from types import SimpleNamespace +from unittest import mock + +from seatunnel_cli.llm_provider import ( + LLMProvider, + OrcaRouterProvider, +) + + +class _FakeOrcaRouterCompletions: + def __init__(self, stream): + self.stream = stream + self.kwargs = None + + def create(self, **kwargs): + self.kwargs = kwargs + return self.stream + + +class _FakeOrcaRouterClient: + def __init__(self, stream): + self.completions = _FakeOrcaRouterCompletions(stream) + self.chat = SimpleNamespace(completions=self.completions) + + +def _chunk(delta, finish_reason=None): + return SimpleNamespace( + choices=[ + SimpleNamespace( + delta=delta, + finish_reason=finish_reason, + ) + ] + ) + + +class OrcaRouterProviderTest(unittest.TestCase): + def test_default_model_and_base_url(self): + provider = OrcaRouterProvider.__new__(OrcaRouterProvider) + self.assertEqual(provider.provider_name, "orcarouter") + self.assertEqual(OrcaRouterProvider.DEFAULT_BASE_URL, "https://api.orcarouter.ai/v1") + self.assertEqual(OrcaRouterProvider.DEFAULT_MODEL, "orcarouter/auto") + + def test_init_requires_api_key(self): + with mock.patch.dict("os.environ", {}, clear=True): + from seatunnel_cli import llm_provider + with self.assertRaises(ValueError): + llm_provider.OrcaRouterProvider() + + def test_echo_reasoning_content_env_override(self): + with mock.patch.dict( + "os.environ", + {"ORCAROUTER_API_KEY": "orc_test", "ORCAROUTER_ECHO_REASONING_CONTENT": "false"}, + clear=False, + ): + from seatunnel_cli import llm_provider + provider = llm_provider.OrcaRouterProvider() + self.assertFalse(provider._echo_reasoning_content) + + with mock.patch.dict( + "os.environ", + {"ORCAROUTER_API_KEY": "orc_test", "ORCAROUTER_ECHO_REASONING_CONTENT": "true"}, + clear=False, + ): + from seatunnel_cli import llm_provider + provider = llm_provider.OrcaRouterProvider() + self.assertTrue(provider._echo_reasoning_content) + + # Defaults to True when unset (parity with OPENAI_ECHO_REASONING_CONTENT). + with mock.patch.dict("os.environ", {"ORCAROUTER_API_KEY": "orc_test"}, clear=False): + from seatunnel_cli import llm_provider + provider = llm_provider.OrcaRouterProvider() + self.assertTrue(provider._echo_reasoning_content) + + def test_stream_uses_openai_protocol_and_collects_text(self): + provider = OrcaRouterProvider.__new__(OrcaRouterProvider) + provider._model_id = "deepseek/deepseek-v4-pro" + provider._client = _FakeOrcaRouterClient( + [ + _chunk(SimpleNamespace(content="Sync ", tool_calls=None)), + _chunk(SimpleNamespace(content="users to S3", tool_calls=None)), + _chunk( + SimpleNamespace(content=None, tool_calls=None), + finish_reason="stop", + ), + ] + ) + + events = list( + provider.chat_stream( + messages=[ + { + "role": "user", + "content": [{"text": "sync mysql to s3"}], + } + ] + ) + ) + response = LLMProvider.collect_stream(events) + + self.assertEqual( + response["output"]["message"]["content"], + [{"text": "Sync users to S3"}], + ) + # The default model is used when no override is passed. + self.assertEqual( + provider._client.completions.kwargs["model"], "deepseek/deepseek-v4-pro" + ) + + def test_chat_round_trip(self): + provider = OrcaRouterProvider.__new__(OrcaRouterProvider) + provider._model_id = "orcarouter/auto" + response = SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace( + content="PLAN: use Jdbc", + tool_calls=None, + ), + finish_reason="stop", + ) + ] + ) + provider._client = _FakeOrcaRouterClient(response) + + result = provider.chat( + messages=[ + { + "role": "user", + "content": [{"text": "sync oracle to iceberg"}], + } + ] + ) + + self.assertEqual( + result["output"]["message"]["content"], + [{"text": "PLAN: use Jdbc"}], + ) + self.assertEqual(result["stopReason"], "end_turn") + + def test_config_model_override_is_independent(self): + """Config-file model overrides apply even when unrelated model env + vars (ANTHROPIC_MODEL/OPENAI_MODEL) are set elsewhere.""" + import os + import tempfile + + from seatunnel_cli import llm_provider + + tmp = tempfile.TemporaryDirectory() + original = llm_provider.get_config_path + + def fake_get_config_path(): + return os.path.join(tmp.name, "config.json") + + llm_provider.get_config_path = fake_get_config_path + try: + with mock.patch.dict( + "os.environ", + {"ORCAROUTER_API_KEY": "orc_test", "ANTHROPIC_MODEL": "claude-other"}, + clear=False, + ): + llm_provider.save_config( + { + "provider": "orcarouter", + "models": { + "orcarouter": { + "model": "deepseek/deepseek-v4-pro", + "fast_model": "qwen/qwen3.8-flash", + } + }, + } + ) + provider = llm_provider.create_provider() + self.assertEqual(provider.model_id, "deepseek/deepseek-v4-pro") + self.assertEqual(provider.fast_model_id, "qwen/qwen3.8-flash") + finally: + llm_provider.get_config_path = original + tmp.cleanup() + + +if __name__ == "__main__": + unittest.main()
