Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/electron/src/main/lib/adapters/pi-model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 7 additions & 1 deletion apps/electron/src/main/lib/adapters/pi-provider-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
2 changes: 1 addition & 1 deletion apps/electron/src/main/lib/agent-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/src/main/lib/channel-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,7 @@ export async function testChannel(channelId: string): Promise<ChannelTestResult>
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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1774,6 +1776,7 @@ export async function fetchModels(input: FetchModelsInput): Promise<FetchModelsR
case 'doubao':
case 'qwen':
case 'custom':
case 'orcarouter':
return await fetchOpenAICompatibleModels(input.baseUrl, input.apiKey, proxyUrl, provider)
case 'google':
return await fetchGoogleModels(input.baseUrl, input.apiKey, proxyUrl)
Expand Down
12 changes: 6 additions & 6 deletions apps/electron/src/main/lib/chat-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,8 @@ export async function generateTitle(input: GenerateTitleInput): Promise<string |
apiKey = await resolveChannelRuntimeApiKey(channelId)
} catch {
console.warn('[标题生成] 解密 API Key 失败')
// OpenCode Go / 自定义渠道无法解密也仍要完成重命名,避免对话长期停在默认标题。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom') ? createFallbackTitle(userMessage) : null
// OpenCode Go / 自定义 / OrcaRouter 渠道无法解密也仍要完成重命名,避免对话长期停在默认标题。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom' || channel.provider === 'orcarouter') ? createFallbackTitle(userMessage) : null
}

try {
Expand All @@ -631,15 +631,15 @@ export async function generateTitle(input: GenerateTitleInput): Promise<string |
const result = title ? sanitizeGeneratedTitle(title) : null
if (!result) {
console.warn('[标题生成] API 未返回可用标题')
// OpenCode Go / 自定义渠道的服务端偶发返回空标题时,仍要完成重命名,避免对话长期停在默认标题。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom') ? createFallbackTitle(userMessage) : null
// OpenCode Go / 自定义 / OrcaRouter 渠道的服务端偶发返回空标题时,仍要完成重命名,避免对话长期停在默认标题。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom' || channel.provider === 'orcarouter') ? createFallbackTitle(userMessage) : null
}

console.log('[标题生成] 成功生成标题:', result)
return result
} catch (error) {
console.warn('[标题生成] 请求失败:', error)
// OpenCode Go / 自定义渠道的服务端偶发返回空标题/异常响应/超时,异常路径同样要完成重命名。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom') ? createFallbackTitle(userMessage) : null
// OpenCode Go / 自定义 / OrcaRouter 渠道的服务端偶发返回空标题/异常响应/超时,异常路径同样要完成重命名。
return (channel.provider === 'opencode-go-openai' || channel.provider === 'custom' || channel.provider === 'orcarouter') ? createFallbackTitle(userMessage) : null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ interface ChannelFormProps {
}

/** 所有可选供应商 */
const PROVIDER_OPTIONS: ProviderType[] = ['anthropic', 'anthropic-compatible', 'openai', 'openai-responses', 'openai-codex', 'xai', 'deepseek', 'google', 'kimi-api', 'kimi-coding', 'opencode-go-openai', 'zhipu', 'zhipu-coding', 'zhipu-coding-team', 'ark-coding-plan', 'minimax', 'doubao', 'qwen', 'qwen-anthropic', 'qwen-token-plan', 'xiaomi', 'xiaomi-token-plan', 'custom']
const PROVIDER_OPTIONS: ProviderType[] = ['anthropic', 'anthropic-compatible', 'openai', 'openai-responses', 'openai-codex', 'xai', 'deepseek', 'google', 'kimi-api', 'kimi-coding', 'opencode-go-openai', 'zhipu', 'zhipu-coding', 'zhipu-coding-team', 'ark-coding-plan', 'minimax', 'doubao', 'qwen', 'orcarouter', 'qwen-anthropic', 'qwen-token-plan', 'xiaomi', 'xiaomi-token-plan', 'custom']

/** 需要用 messages 端点测试的供应商预设模型 */
const PROVIDER_TEST_MODEL_PRESETS: Partial<Record<ProviderType, string[]>> = {
Expand All @@ -90,6 +90,7 @@ const PROVIDER_TEST_MODEL_PRESETS: Partial<Record<ProviderType, string[]>> = {
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) */
Expand Down
1 change: 1 addition & 0 deletions apps/electron/src/renderer/lib/model-logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ const PROVIDER_LOGO_MAP: Record<ProviderType, string> = {
'xiaomi-token-plan': XiaomiLogo,
'openai-codex': OpenAILogo,
xai: GrokLogo,
orcarouter: DefaultLogo,
custom: DefaultLogo,
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const adapterRegistry = new Map<ProviderType, ProviderAdapter>([
['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()],
])

Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/types/channel.orcarouter.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
10 changes: 10 additions & 0 deletions packages/shared/src/types/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 的自定义请求地址。
*
Expand Down Expand Up @@ -67,6 +74,8 @@ export const PROVIDER_DEFAULT_URLS: Record<ProviderType, string> = {
// 订阅登录渠道的 baseUrl 由 Pi SDK 内部管理,无需用户填写。
'openai-codex': '',
xai: '',
// OrcaRouter 模型路由网关,OpenAI 兼容协议。
orcarouter: 'https://api.orcarouter.ai/v1',
custom: '',
}

Expand Down Expand Up @@ -96,6 +105,7 @@ export const PROVIDER_LABELS: Record<ProviderType, string> = {
'xiaomi-token-plan': '小米 MiMo Token Plan',
'openai-codex': 'ChatGPT 订阅 (Codex)',
xai: 'xAI 订阅 (Grok)',
orcarouter: 'OrcaRouter',
custom: 'OpenAI Chat Completions(自定义地址)',
}

Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types/reasoning-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down