From 2c75fdee39bc5f105c8e0c2279aec1b9a4624955 Mon Sep 17 00:00:00 2001 From: XiaoHuo888 Date: Sat, 15 Aug 2026 21:43:48 +0800 Subject: [PATCH] feat: add OrcaRouter as a named OpenAI-compatible provider Register OrcaRouter (https://api.orcarouter.ai/v1) in the engine provider catalog and its UI mirror, so the onboarding/settings provider picker and model config resolve it like the existing OpenRouter aggregator entry: protocol openai, ORCAROUTER_API_KEY, /models model list. Documents the gateway config (orcarouter/auto auto-routing, namespaced vendor/model ids) in both READMEs and adds engine + UI tests. Co-Authored-By: Claude Signed-off-by: XiaoHuo888 --- README.md | 19 ++++++++++++++++ README.zh.md | 16 ++++++++++++++ src/model/catalog/providers.ts | 8 +++++++ tests/model/config/parseModelConfig.spec.ts | 24 +++++++++++++++++++++ ui/src/shared/catalogProviders.test.ts | 16 ++++++++++++++ ui/src/shared/catalogProviders.ts | 15 +++++++++++++ 6 files changed, 98 insertions(+) diff --git a/README.md b/README.md index 6ee290faf..0b35dd7e6 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,25 @@ model: apiKey: sk-your-api-key ``` +[OrcaRouter](https://www.orcarouter.ai) exposes 150+ models from OpenAI, Anthropic, +Google, DeepSeek, Qwen, MiniMax and others behind a single OpenAI-compatible +endpoint. Use `orcarouter/auto` to auto-route each request, or a namespaced id +such as `anthropic/claude-sonnet-4.6`: + +```yaml +schemaVersion: 1 +agent: + model: orcarouter/auto +model: + providers: + orcarouter: + protocol: openai + url: https://api.orcarouter.ai/v1 + apiKey: ${ORCAROUTER_API_KEY} + models: + orcarouter/auto: {} +``` + Native Gemini can be configured with `protocol: google`: ```yaml diff --git a/README.zh.md b/README.zh.md index 54c13f003..41f14bf90 100644 --- a/README.zh.md +++ b/README.zh.md @@ -420,6 +420,22 @@ model: apiKey: sk-your-api-key ``` +[OrcaRouter](https://www.orcarouter.ai) 通过单个 OpenAI 兼容端点提供来自 OpenAI、Anthropic、Google、DeepSeek、Qwen、MiniMax 等的 150+ 模型。使用 `orcarouter/auto` 自动路由每个请求,或使用带命名空间的模型 ID(如 `anthropic/claude-sonnet-4.6`): + +```yaml +schemaVersion: 1 +agent: + model: orcarouter/auto +model: + providers: + orcarouter: + protocol: openai + url: https://api.orcarouter.ai/v1 + apiKey: ${ORCAROUTER_API_KEY} + models: + orcarouter/auto: {} +``` + 原生 Gemini 可以使用 `protocol: google`: ```yaml diff --git a/src/model/catalog/providers.ts b/src/model/catalog/providers.ts index f117f830c..09f967bf4 100644 --- a/src/model/catalog/providers.ts +++ b/src/model/catalog/providers.ts @@ -1141,6 +1141,14 @@ export const PROVIDER_CATALOG: ProviderCatalog = { models: {}, }, + orcarouter: { + displayName: "OrcaRouter", + protocol: "openai", + defaultUrl: "https://api.orcarouter.ai/v1", + apiKeyEnvVar: "ORCAROUTER_API_KEY", + models: {}, + }, + ollama: { displayName: "Ollama", protocol: "openai", diff --git a/tests/model/config/parseModelConfig.spec.ts b/tests/model/config/parseModelConfig.spec.ts index c4104f621..78b73d944 100644 --- a/tests/model/config/parseModelConfig.spec.ts +++ b/tests/model/config/parseModelConfig.spec.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { parseModelConfig } from "../../../src/model/config/parseModelConfig.js"; +import { buildProviderChatEndpoint } from "../../../src/model/providerEndpoint.js"; test("catalog provider resolves api key from default env var when apiKey is omitted", () => { const config = parseModelConfig({ @@ -111,3 +112,26 @@ test("catalog model aliases keep their declared provider image capability", () = ["text", "image"], ); }); + +test("orcarouter catalog provider resolves default URL, protocol and env api key", () => { + const config = parseModelConfig({ + providers: { + orcarouter: { + models: { "orcarouter/fusion": {} }, + }, + }, + }, { env: { ORCAROUTER_API_KEY: "sk-orca-test" } }); + + assert.equal(config.providers.orcarouter.url, "https://api.orcarouter.ai/v1"); + assert.equal(config.providers.orcarouter.protocol, "openai"); + assert.equal(config.providers.orcarouter.apiKey, "sk-orca-test"); +}); + +test("orcarouter chat endpoint is built from the gateway base URL", () => { + const endpoint = buildProviderChatEndpoint({ + protocol: "openai", + baseUrl: "https://api.orcarouter.ai/v1", + }); + + assert.equal(endpoint, "https://api.orcarouter.ai/v1/chat/completions"); +}); diff --git a/ui/src/shared/catalogProviders.test.ts b/ui/src/shared/catalogProviders.test.ts index a32aac22b..c2c4c36a3 100644 --- a/ui/src/shared/catalogProviders.test.ts +++ b/ui/src/shared/catalogProviders.test.ts @@ -56,4 +56,20 @@ describe('catalogProviders maxOutputTokens', () => { expect(ollama?.models.find((model) => model.id === 'qwen3:0.6b')?.maxContextTokens).toBe(40_960); expect(ollama?.models.find((model) => model.id === 'llama3.1:8b')?.maxOutputTokens).toBe(8_192); }); + + it('exposes OrcaRouter as an OpenAI-compatible gateway provider', () => { + const orcarouter = findCatalogProviderById('orcarouter'); + + expect(orcarouter?.protocol).toBe('openai'); + expect(orcarouter?.defaultUrl).toBe('https://api.orcarouter.ai/v1'); + expect(orcarouter?.modelListUrl).toBe('https://api.orcarouter.ai/v1/models'); + expect(orcarouter?.modelListRequiresApiKey).toBe(true); + expect(orcarouter?.models.map((model) => model.id)).toEqual([ + 'orcarouter/fusion', + 'orcarouter/auto', + 'anthropic/claude-sonnet-4.6', + 'deepseek/deepseek-v4-pro', + 'google/gemini-2.5-pro', + ]); + }); }); diff --git a/ui/src/shared/catalogProviders.ts b/ui/src/shared/catalogProviders.ts index 18d8b1459..d4a778233 100644 --- a/ui/src/shared/catalogProviders.ts +++ b/ui/src/shared/catalogProviders.ts @@ -125,6 +125,21 @@ export const CATALOG_PROVIDERS: CatalogProvider[] = [ { id: 'moonshotai/kimi-k2.6', displayName: 'Kimi K2.6', supportsImage: true, maxContextTokens: 262144 }, ], }, + { + id: 'orcarouter', + displayName: 'OrcaRouter', + protocol: 'openai', + defaultUrl: 'https://api.orcarouter.ai/v1', + modelListUrl: 'https://api.orcarouter.ai/v1/models', + modelListRequiresApiKey: true, + models: [ + { id: 'orcarouter/fusion', displayName: 'OrcaRouter Fusion', maxContextTokens: 200000 }, + { id: 'orcarouter/auto', displayName: 'OrcaRouter Auto', maxContextTokens: 200000 }, + { id: 'anthropic/claude-sonnet-4.6', displayName: 'Claude Sonnet 4.6', supportsImage: true, maxContextTokens: 200000 }, + { id: 'deepseek/deepseek-v4-pro', displayName: 'DeepSeek V4 Pro', maxContextTokens: 1048576 }, + { id: 'google/gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', supportsImage: true, maxContextTokens: 1048576 }, + ], + }, { id: 'ollama', displayName: 'Ollama',