Skip to content
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ The `run.sh` script will automatically:
model: "gpt-4o" # or deepseek-chat, qwen3-max, etc.
max_total_tokens: 120000
max_completion_tokens: 16384
# OrcaRouter: OpenAI-compatible gateway (https://api.orcarouter.ai/v1).
# Model IDs like orcarouter/auto are routed by the gateway to the best backend model.
orcarouter:
name: OrcaRouter
provider: orcarouter
api_key: "${ORCAROUTER_API_KEY}"
base_url: "https://api.orcarouter.ai/v1"
model: "orcarouter/auto"
```
- Or edit `config.yaml` directly before launching. `ai.default_channel` is used for new conversations and tasks that do not explicitly select a channel; the chat page can also select any saved channel per session.
2. **Login** - On first startup the console prints an auto-generated initial `admin` password; create accounts from **Platform permissions → User management**
Expand Down Expand Up @@ -302,6 +310,11 @@ ai:
api_key: "${OPENAI_API_KEY}"
base_url: "https://api.openai.com/v1"
model: "your-model"
orcarouter:
provider: orcarouter
api_key: "${ORCAROUTER_API_KEY}"
base_url: "https://api.orcarouter.ai/v1"
model: "orcarouter/auto"
```

`openai` is a backward-compatible runtime field; maintain new model settings in `ai.channels`. Do not commit real credentials. Review the [configuration reference](docs/en-US/configuration.md), [recommended profiles](docs/en-US/configuration-profiles.md), and [security hardening guide](docs/en-US/security-hardening.md) before exposing the service beyond localhost.
Expand Down
13 changes: 13 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ chmod +x run.sh && ./run.sh
model: "gpt-4o" # 或 deepseek-chat, qwen3-max 等
max_total_tokens: 120000
max_completion_tokens: 16384
# OrcaRouter:OpenAI 兼容网关(https://api.orcarouter.ai/v1)
# 模型名如 orcarouter/auto 由网关路由到最优后端模型
orcarouter:
name: OrcaRouter
provider: orcarouter
api_key: "${ORCAROUTER_API_KEY}"
base_url: "https://api.orcarouter.ai/v1"
model: "orcarouter/auto"
```
- 或启动前直接编辑 `config.yaml` 文件。`ai.default_channel` 会作为新对话和未显式选择通道任务的默认模型;对话页也可以在会话设置里选择某个已保存通道。
2. **登录系统** - 首次启动时控制台会显示自动生成的 `admin` 初始密码;也可在「平台权限 → 用户管理」中创建账号
Expand Down Expand Up @@ -300,6 +308,11 @@ ai:
api_key: "${OPENAI_API_KEY}"
base_url: "https://api.openai.com/v1"
model: "your-model"
orcarouter:
provider: orcarouter
api_key: "${ORCAROUTER_API_KEY}"
base_url: "https://api.orcarouter.ai/v1"
model: "orcarouter/auto"
```

`openai` 是兼容旧版本的运行时字段,新配置优先维护 `ai.channels`。不要提交真实凭证。将服务暴露到 localhost 之外前,请阅读[配置参考](docs/zh-CN/configuration.md)、[推荐配置画像](docs/zh-CN/configuration-profiles.md)和[安全加固指南](docs/zh-CN/security-hardening.md)。
Expand Down
13 changes: 11 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,21 @@ monitor:
# 支持的 API 服务商:
# - OpenAI: https://api.openai.com/v1
# - DeepSeek: https://api.deepseek.com/v1
# - OrcaRouter: https://api.orcarouter.ai/v1(OpenAI 兼容网关,模型名如 orcarouter/auto)
# - 其他兼容 OpenAI 协议的 API
# 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus 等
# provider: 可选值 openai_compatible(默认) | claude(Eino 原生 Anthropic Messages API)
# 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus, orcarouter/auto
# provider: 可选值 openai_compatible(默认) | claude(Eino 原生 Anthropic Messages API) | orcarouter(OpenAI 兼容网关)
ai:
default_channel: qwen-max
channels:
orcarouter:
name: OrcaRouter
provider: orcarouter
base_url: https://api.orcarouter.ai/v1
api_key: sk-orca-xxxxxxx
model: orcarouter/auto
max_total_tokens: 120000
max_completion_tokens: 32768
qwen-max:
name: Qwen Max
provider: openai_compatible
Expand Down
8 changes: 7 additions & 1 deletion docs/en-US/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,20 @@ ai:
base_url: https://api.anthropic.com/v1
api_key: sk-ant-...
model: claude-sonnet-4-5
orcarouter:
name: OrcaRouter
provider: orcarouter
base_url: https://api.orcarouter.ai/v1
api_key: sk-orca-...
model: orcarouter/auto
```

| Field | Meaning |
| --- | --- |
| `ai.default_channel` | Default channel ID for new conversations and requests without an explicit channel. |
| `ai.channels.<id>` | Channel config. IDs are normalized to lowercase letters, digits, and hyphens. |
| `name` | Display name in the Web UI; falls back to the ID. |
| `provider` | `openai_compatible` or `claude`. OpenAI-compatible channels map to runtime `openai`; Claude channels use Eino's native Anthropic Messages API component. |
| `provider` | `openai_compatible`, `claude`, or `orcarouter`. OpenAI-compatible channels map to runtime `openai`; Claude channels use Eino's native Anthropic Messages API component; OrcaRouter is an OpenAI-compatible gateway (`https://api.orcarouter.ai/v1`) whose model IDs such as `orcarouter/auto` are resolved by the gateway to the best available backend model. |
| `base_url/api_key/model` | Required. Base URL usually includes a version path such as `/v1`. |
| `max_total_tokens` | Shared context budget for compression, attack-chain generation, multi-agent summaries, and similar paths. |
| `max_completion_tokens` | Per-response output cap; default is used when empty. |
Expand Down
8 changes: 7 additions & 1 deletion docs/zh-CN/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ ai:
base_url: https://api.anthropic.com/v1
api_key: sk-ant-...
model: claude-sonnet-4-5
orcarouter:
name: OrcaRouter
provider: orcarouter
base_url: https://api.orcarouter.ai/v1
api_key: sk-orca-...
model: orcarouter/auto
```

`ai` 是推荐的模型配置入口。系统设置页对应路径是 **系统设置 → 基本设置 → AI 通道配置**,保存后写入 `ai.default_channel` 和 `ai.channels`。旧版 `openai` 字段仍保留为兼容运行时字段;加载配置时会确保至少有一个默认通道,并把 `ai.default_channel` 解析后的配置同步到运行时 `openai`。
Expand All @@ -65,7 +71,7 @@ ai:
| `ai.default_channel` | 默认通道 ID。新对话、机器人、批量任务和未显式选择通道的请求使用它。 |
| `ai.channels.<id>` | 通道配置。ID 会归一化为小写、数字和短横线,例如 `Qwen_Max` 会变成 `qwen-max`。 |
| `name` | Web UI 展示名。留空时使用通道 ID。 |
| `provider` | `openai_compatible` 或 `claude`。`openai_compatible` 会在运行时映射为 `openai`;`claude` 使用 Eino 原生 Anthropic Messages API。 |
| `provider` | `openai_compatible`、`claude` 或 `orcarouter`。`openai_compatible` 会在运行时映射为 `openai`;`claude` 使用 Eino 原生 Anthropic Messages API;`orcarouter` 是 OpenAI 兼容网关(`https://api.orcarouter.ai/v1`),模型名如 `orcarouter/auto` 由网关路由到最优后端模型。 |
| `base_url/api_key/model` | 必填。Base URL 通常需要包含版本路径,如 OpenAI/兼容网关的 `/v1`。 |
| `max_total_tokens` | 上下文压缩、攻击链构建、多代理摘要等共用的总预算。 |
| `max_completion_tokens` | 单次模型输出上限;未填时使用默认值。 |
Expand Down
30 changes: 30 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const (
DefaultLatestUserMessageHeadRunes = 24000
DefaultLatestUserMessageTailRunes = 24000
DefaultSummarizationOutputReserveTokens = 8192

// OrcaRouterBaseURL is OrcaRouter's OpenAI-compatible gateway endpoint.
OrcaRouterBaseURL = "https://api.orcarouter.ai/v1"
)

// ProjectConfig 项目黑板(跨对话共享事实)配置。
Expand Down Expand Up @@ -968,6 +971,33 @@ func (c OpenAIConfig) IsDeepSeekEndpointOrModel() bool {
return strings.Contains(baseURL, "deepseek") || strings.Contains(model, "deepseek")
}

// IsOrcaRouterProvider reports whether the channel targets OrcaRouter's
// OpenAI-compatible gateway. OrcaRouter is a router provider: model IDs such as
// "orcarouter/auto" are resolved by the gateway to the best available backend
// model, so channels pointing at it are treated as ordinary OpenAI-compatible
// endpoints.
func (c OpenAIConfig) IsOrcaRouterProvider() bool {
if provider := strings.ToLower(strings.TrimSpace(c.Provider)); provider == "orcarouter" {
return true
}
baseURL := strings.ToLower(strings.TrimSpace(c.BaseURL))
return strings.Contains(baseURL, "orcarouter")
}

// OrcaRouterBaseURLFor returns the OrcaRouter gateway base URL, defaulting to
// the canonical endpoint when the channel does not override it.
func OrcaRouterBaseURLFor(provider string) string {
if IsOrcaRouterProviderName(provider) {
return OrcaRouterBaseURL
}
return ""
}

// IsOrcaRouterProviderName reports whether the provider name refers to OrcaRouter.
func IsOrcaRouterProviderName(provider string) bool {
return strings.EqualFold(strings.TrimSpace(provider), "orcarouter")
}

// OpenAIReasoningConfig 全局默认与网关 profile(对话页可通过 ChatRequest.reasoning 覆盖,受 AllowClientReasoning 约束)。
type OpenAIReasoningConfig struct {
// Mode: auto(默认)| on | off | default(与 auto 相同)。
Expand Down
60 changes: 60 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,63 @@ func TestLatestUserMessageRunesEffective(t *testing.T) {
t.Fatalf("custom latest user tail runes = %d", got)
}
}

func TestOrcaRouterProviderName(t *testing.T) {
for _, tc := range []struct {
in string
want bool
}{
{"orcarouter", true},
{"OrcaRouter", true},
{" orcarouter ", true},
{"openai", false},
{"claude", false},
{"", false},
} {
if got := IsOrcaRouterProviderName(tc.in); got != tc.want {
t.Errorf("IsOrcaRouterProviderName(%q) = %v, want %v", tc.in, got, tc.want)
}
}
}

func TestOrcaRouterBaseURLFor(t *testing.T) {
if got := OrcaRouterBaseURLFor("orcarouter"); got != OrcaRouterBaseURL {
t.Fatalf("OrcaRouterBaseURLFor(orcarouter) = %q, want %q", got, OrcaRouterBaseURL)
}
if got := OrcaRouterBaseURLFor("openai"); got != "" {
t.Fatalf("OrcaRouterBaseURLFor(openai) = %q, want empty", got)
}
}

func TestOpenAIConfigIsOrcaRouterProvider(t *testing.T) {
for _, tc := range []struct {
name string
cfg OpenAIConfig
want bool
}{
{
name: "provider name",
cfg: OpenAIConfig{Provider: "orcarouter", BaseURL: "https://custom.example/v1"},
want: true,
},
{
name: "base url contains orcarouter",
cfg: OpenAIConfig{Provider: "openai", BaseURL: "https://api.orcarouter.ai/v1"},
want: true,
},
{
name: "provider name case-insensitive",
cfg: OpenAIConfig{Provider: "OrcaRouter", BaseURL: ""},
want: true,
},
{
name: "unrelated provider",
cfg: OpenAIConfig{Provider: "openai", BaseURL: "https://api.openai.com/v1"},
want: false,
},
} {
if got := tc.cfg.IsOrcaRouterProvider(); got != tc.want {
t.Errorf("%s: IsOrcaRouterProvider() = %v, want %v", tc.name, got, tc.want)
}
}
}
10 changes: 9 additions & 1 deletion internal/handler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,8 @@ func (h *ConfigHandler) TestOpenAI(c *gin.Context) {
if baseURL == "" {
if strings.EqualFold(strings.TrimSpace(req.Provider), "claude") {
baseURL = "https://api.anthropic.com"
} else if config.IsOrcaRouterProviderName(req.Provider) {
baseURL = config.OrcaRouterBaseURL
} else {
baseURL = "https://api.openai.com/v1"
}
Expand Down Expand Up @@ -1341,7 +1343,11 @@ func (h *ConfigHandler) ListModels(c *gin.Context) {

baseURL := strings.TrimSuffix(strings.TrimSpace(req.BaseURL), "/")
if baseURL == "" {
baseURL = "https://api.openai.com/v1"
if config.IsOrcaRouterProviderName(provider) {
baseURL = config.OrcaRouterBaseURL
} else {
baseURL = "https://api.openai.com/v1"
}
}

tmpCfg := &config.OpenAIConfig{
Expand Down Expand Up @@ -1407,6 +1413,8 @@ func (h *ConfigHandler) TestVision(c *gin.Context) {
if baseURL == "" {
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
baseURL = "https://api.anthropic.com"
} else if config.IsOrcaRouterProviderName(oa.Provider) {
baseURL = config.OrcaRouterBaseURL
} else {
baseURL = "https://api.openai.com/v1"
}
Expand Down
4 changes: 2 additions & 2 deletions internal/handler/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5379,7 +5379,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"type": "object",
"required": []string{"api_key", "model"},
"properties": map[string]interface{}{
"provider": map[string]interface{}{"type": "string", "description": "LLM提供商(openai/claude)", "example": "openai"},
"provider": map[string]interface{}{"type": "string", "description": "LLM提供商(openai/claude/orcarouter)", "example": "openai"},
"base_url": map[string]interface{}{"type": "string", "description": "API基地址(可选,默认根据provider自动选择)"},
"api_key": map[string]interface{}{"type": "string", "description": "API密钥"},
"model": map[string]interface{}{"type": "string", "description": "模型名称", "example": "gpt-4"},
Expand Down Expand Up @@ -5424,7 +5424,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"type": "object",
"required": []string{"api_key"},
"properties": map[string]interface{}{
"provider": map[string]interface{}{"type": "string", "description": "LLM提供商(openai/claude)", "example": "openai"},
"provider": map[string]interface{}{"type": "string", "description": "LLM提供商(openai/claude/orcarouter)", "example": "openai"},
"base_url": map[string]interface{}{"type": "string", "description": "API基地址(可选)"},
"api_key": map[string]interface{}{"type": "string", "description": "API密钥"},
},
Expand Down
1 change: 1 addition & 0 deletions web/static/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2777,6 +2777,7 @@
"apiProvider": "API Provider",
"providerOpenAI": "OpenAI / OpenAI-compatible API",
"providerClaude": "Claude (Anthropic Messages API)",
"providerOrcaRouter": "OrcaRouter (OpenAI-compatible gateway)",
"visionProviderReuseOpenAI": "Reuse default AI channel",
"fofaConfig": "FOFA config",
"fofaConfigHint": "Used for asset discovery and import; only an API key is required.",
Expand Down
1 change: 1 addition & 0 deletions web/static/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2765,6 +2765,7 @@
"apiProvider": "API 提供商",
"providerOpenAI": "OpenAI / 兼容 OpenAI 协议",
"providerClaude": "Claude (Anthropic Messages API)",
"providerOrcaRouter": "OrcaRouter(OpenAI 兼容网关)",
"visionProviderReuseOpenAI": "默认 AI 通道(留空复用)",
"fofaConfig": "FOFA 配置",
"fofaConfigHint": "用于资产发现与导入,仅需配置 API Key。",
Expand Down
4 changes: 2 additions & 2 deletions web/static/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ async function loadConfig(loadTools = true, options = {}) {
const hitlAuditProviderEl = document.getElementById('hitl-audit-model-provider');
if (hitlAuditProviderEl) {
const provider = String(hitlAuditModel.provider || '').trim().toLowerCase();
hitlAuditProviderEl.value = ['openai', 'claude'].includes(provider) ? provider : '';
hitlAuditProviderEl.value = ['openai', 'claude', 'orcarouter'].includes(provider) ? provider : '';
}
const hitlAuditBaseUrlEl = document.getElementById('hitl-audit-model-base-url');
if (hitlAuditBaseUrlEl) hitlAuditBaseUrlEl.value = hitlAuditModel.base_url || '';
Expand Down Expand Up @@ -2842,7 +2842,7 @@ function updateAIChannelEditorChrome(id) {
title.textContent = settingsT('settingsBasic.aiChannelFormContextHint', '表单保存后会更新该通道配置。');
}
if (meta) {
const provider = ch.provider === 'claude' ? 'Claude' : settingsT('settingsBasic.aiChannelOpenAICompat', 'OpenAI 兼容');
const provider = ch.provider === 'claude' ? 'Claude' : (ch.provider === 'orcarouter' ? 'OrcaRouter' : settingsT('settingsBasic.aiChannelOpenAICompat', 'OpenAI 兼容'));
const statusText = probe?.message || (isComplete
? settingsT('settingsBasic.aiChannelComplete', '配置完整')
: settingsT('settingsBasic.aiChannelDraft', '待完善'));
Expand Down
3 changes: 3 additions & 0 deletions web/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3608,6 +3608,7 @@ <h6 data-i18n="settingsBasic.aiChannelConnectionSection">连接信息</h6>
<select id="openai-provider">
<option value="openai_compatible" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
<option value="orcarouter" data-i18n="settingsBasic.providerOrcaRouter">OrcaRouter (OpenAI-compatible gateway)</option>
</select>
</div>
<div class="form-group span-2">
Expand Down Expand Up @@ -3722,6 +3723,7 @@ <h4 data-i18n="settingsBasic.visionConfig">视觉分析(analyze_image)</h4>
<option value="" data-i18n="settingsBasic.visionProviderReuseOpenAI">默认 AI 通道(留空复用)</option>
<option value="openai" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
<option value="orcarouter" data-i18n="settingsBasic.providerOrcaRouter">OrcaRouter (OpenAI-compatible gateway)</option>
</select>
</div>
<div class="form-group">
Expand Down Expand Up @@ -3929,6 +3931,7 @@ <h5 data-i18n="settings.hitl.auditModelTitle">审计 Agent 模型</h5>
<option value="" data-i18n="settings.hitl.auditModelReuseMain">跟随主模型配置</option>
<option value="openai" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
<option value="orcarouter" data-i18n="settingsBasic.providerOrcaRouter">OrcaRouter (OpenAI-compatible gateway)</option>
</select>
</div>
<div class="form-group">
Expand Down