From 0593fd48f630acb25d01afd9c49e8855f4fe3205 Mon Sep 17 00:00:00 2001 From: XiaoHuo888 Date: Tue, 18 Aug 2026 07:15:50 +0800 Subject: [PATCH] feat(channel): add named OrcaRouter provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register OrcaRouter as a first-class provider preset (OpenAI Chat Completions compatible), with default base URL, label, reasoning transport, model list/test presets and title fallback handling — mirroring the existing 'custom' OpenAI-compatible wiring. --- .../src/main/lib/adapters/pi-model-registry.ts | 1 + .../src/main/lib/adapters/pi-provider-compat.ts | 8 +++++++- .../electron/src/main/lib/agent-orchestrator.ts | 2 +- apps/electron/src/main/lib/channel-manager.ts | 3 +++ apps/electron/src/main/lib/chat-service.ts | 12 ++++++------ .../components/settings/ChannelForm.tsx | 3 ++- apps/electron/src/renderer/lib/model-logo.ts | 1 + packages/core/src/providers/index.ts | 1 + .../shared/src/types/channel.orcarouter.test.ts | 17 +++++++++++++++++ packages/shared/src/types/channel.ts | 10 ++++++++++ packages/shared/src/types/reasoning-profile.ts | 1 + 11 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 packages/shared/src/types/channel.orcarouter.test.ts diff --git a/apps/electron/src/main/lib/adapters/pi-model-registry.ts b/apps/electron/src/main/lib/adapters/pi-model-registry.ts index f82cc3e334..7bb1c3805b 100644 --- a/apps/electron/src/main/lib/adapters/pi-model-registry.ts +++ b/apps/electron/src/main/lib/adapters/pi-model-registry.ts @@ -324,6 +324,7 @@ function normalizePiApi(provider: ProviderType): Api { case 'doubao': case 'qwen': case 'custom': + case 'orcarouter': return 'openai-completions' case 'openai-responses': return 'openai-responses' diff --git a/apps/electron/src/main/lib/adapters/pi-provider-compat.ts b/apps/electron/src/main/lib/adapters/pi-provider-compat.ts index 8fcdef16b7..e4168ea1a9 100644 --- a/apps/electron/src/main/lib/adapters/pi-provider-compat.ts +++ b/apps/electron/src/main/lib/adapters/pi-provider-compat.ts @@ -6,7 +6,13 @@ import type { ProviderType } from '@proma/shared' * * custom 是任意 OpenAI 兼容端点的通用入口,保守地使用所有兼容服务都支持的 * system 角色。原生 OpenAI 渠道仍可使用 developer。 + * + * orcarouter 是模型路由网关,后端路由到多家供应商的模型,模型间角色支持 + * 不尽相同,保守地统一使用 system 角色。 */ export function supportsPiDeveloperRole(provider: ProviderType): boolean { - return provider !== 'doubao' && provider !== 'qwen' && provider !== 'custom' + return provider !== 'doubao' + && provider !== 'qwen' + && provider !== 'custom' + && provider !== 'orcarouter' } diff --git a/apps/electron/src/main/lib/agent-orchestrator.ts b/apps/electron/src/main/lib/agent-orchestrator.ts index 584228c8fd..aae3f96ed5 100644 --- a/apps/electron/src/main/lib/agent-orchestrator.ts +++ b/apps/electron/src/main/lib/agent-orchestrator.ts @@ -1529,7 +1529,7 @@ export class AgentOrchestrator { persistXaiOAuthCredentials(channelId, credentials) }, }), - ...((channel.provider === 'openai-codex' || channel.provider === 'xai' || channel.provider === 'openai-responses' || channel.provider === 'openai' || channel.provider === 'custom') + ...((channel.provider === 'openai-codex' || channel.provider === 'xai' || channel.provider === 'openai-responses' || channel.provider === 'openai' || channel.provider === 'custom' || channel.provider === 'orcarouter') && resolveReasoningProfile({ modelId: selectedModelId, transport: inferReasoningTransport(channel.provider), diff --git a/apps/electron/src/main/lib/channel-manager.ts b/apps/electron/src/main/lib/channel-manager.ts index 85806f5292..7be5eb04c0 100644 --- a/apps/electron/src/main/lib/channel-manager.ts +++ b/apps/electron/src/main/lib/channel-manager.ts @@ -712,6 +712,7 @@ export async function testChannel(channelId: string): Promise case 'doubao': case 'qwen': case 'custom': + case 'orcarouter': return await testOpenAICompatible(channel.baseUrl, apiKey, proxyUrl, provider) case 'google': return await testGoogle(channel.baseUrl, apiKey, proxyUrl) @@ -1687,6 +1688,7 @@ export async function testChannelDirect(input: ChannelDirectTestInput): Promise< case 'doubao': case 'qwen': case 'custom': + case 'orcarouter': return await testOpenAICompatible(input.baseUrl, input.apiKey, proxyUrl, provider) case 'google': return await testGoogle(input.baseUrl, input.apiKey, proxyUrl) @@ -1774,6 +1776,7 @@ export async function fetchModels(input: FetchModelsInput): Promise> = { @@ -90,6 +90,7 @@ const PROVIDER_TEST_MODEL_PRESETS: Partial> = { xiaomi: ['mimo-v2.5-pro', 'mimo-v2-pro', 'mimo-v2.5', 'mimo-v2-omni', 'mimo-v2-flash'], 'xiaomi-token-plan': ['mimo-v2.5-pro', 'mimo-v2-pro', 'mimo-v2.5', 'mimo-v2-omni', 'mimo-v2-flash'], 'qwen-token-plan': ['qwen3.8-max-preview', 'qwen3.7-max', 'qwen3.7-flash', 'qwen3.6-flash'], + orcarouter: ['orcarouter/auto'], } /** 供应商选项(用于 SettingsSelect) */ diff --git a/apps/electron/src/renderer/lib/model-logo.ts b/apps/electron/src/renderer/lib/model-logo.ts index 5b64d8c2aa..86896d949a 100644 --- a/apps/electron/src/renderer/lib/model-logo.ts +++ b/apps/electron/src/renderer/lib/model-logo.ts @@ -257,6 +257,7 @@ const PROVIDER_LOGO_MAP: Record = { 'xiaomi-token-plan': XiaomiLogo, 'openai-codex': OpenAILogo, xai: GrokLogo, + orcarouter: DefaultLogo, custom: DefaultLogo, } diff --git a/packages/core/src/providers/index.ts b/packages/core/src/providers/index.ts index 6a5c03b99e..7fb0dde360 100644 --- a/packages/core/src/providers/index.ts +++ b/packages/core/src/providers/index.ts @@ -47,6 +47,7 @@ const adapterRegistry = new Map([ ['xiaomi', new AnthropicAdapter('xiaomi')], // 小米 MiMo API 使用 Anthropic 兼容协议 ['xiaomi-token-plan', new AnthropicAdapter('xiaomi-token-plan')], // 小米 Token Plan 订阅制(强制 User-Agent) ['custom', new OpenAIAdapter('custom')], // 自定义使用用户填写的完整 OpenAI 兼容请求地址 + ['orcarouter', new OpenAIAdapter('orcarouter')], // OrcaRouter 模型路由网关使用 OpenAI 兼容协议 ['google', new GoogleAdapter()], ]) diff --git a/packages/shared/src/types/channel.orcarouter.test.ts b/packages/shared/src/types/channel.orcarouter.test.ts new file mode 100644 index 0000000000..3bcd398aa0 --- /dev/null +++ b/packages/shared/src/types/channel.orcarouter.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'bun:test' +import { PROVIDER_DEFAULT_URLS, PROVIDER_LABELS } from './channel' +import { inferReasoningTransport } from './reasoning-profile' + +describe('OrcaRouter provider 注册', () => { + test('Given orcarouter When 查询默认 Base URL Then 指向 OrcaRouter 网关', () => { + expect(PROVIDER_DEFAULT_URLS.orcarouter).toBe('https://api.orcarouter.ai/v1') + }) + + test('Given orcarouter When 查询显示名 Then OrcaRouter', () => { + expect(PROVIDER_LABELS.orcarouter).toBe('OrcaRouter') + }) + + test('Given orcarouter When 推导推理传输 Then openai-completions', () => { + expect(inferReasoningTransport('orcarouter')).toBe('openai-completions') + }) +}) diff --git a/packages/shared/src/types/channel.ts b/packages/shared/src/types/channel.ts index 7263c5beec..84e9124e86 100644 --- a/packages/shared/src/types/channel.ts +++ b/packages/shared/src/types/channel.ts @@ -31,6 +31,13 @@ export type ProviderType = | 'xiaomi-token-plan' | 'openai-codex' | 'xai' + /** + * OpenAI Chat Completions 兼容网关(OrcaRouter)。 + * + * 与 `custom` 一样走 OpenAI 兼容协议,但带命名品牌与默认 Base URL, + * 用户无需手填地址即可直接选用。 + */ + | 'orcarouter' /** * OpenAI Chat Completions 的自定义请求地址。 * @@ -67,6 +74,8 @@ export const PROVIDER_DEFAULT_URLS: Record = { // 订阅登录渠道的 baseUrl 由 Pi SDK 内部管理,无需用户填写。 'openai-codex': '', xai: '', + // OrcaRouter 模型路由网关,OpenAI 兼容协议。 + orcarouter: 'https://api.orcarouter.ai/v1', custom: '', } @@ -96,6 +105,7 @@ export const PROVIDER_LABELS: Record = { 'xiaomi-token-plan': '小米 MiMo Token Plan', 'openai-codex': 'ChatGPT 订阅 (Codex)', xai: 'xAI 订阅 (Grok)', + orcarouter: 'OrcaRouter', custom: 'OpenAI Chat Completions(自定义地址)', } diff --git a/packages/shared/src/types/reasoning-profile.ts b/packages/shared/src/types/reasoning-profile.ts index 2fb584d447..eb2d6ce352 100644 --- a/packages/shared/src/types/reasoning-profile.ts +++ b/packages/shared/src/types/reasoning-profile.ts @@ -22,6 +22,7 @@ export function inferReasoningTransport(provider: ProviderType | undefined): Rea case 'doubao': case 'qwen': case 'custom': + case 'orcarouter': return 'openai-completions' case 'openai-codex': case 'openai-responses':