diff --git a/README.md b/README.md index 50773efbc..349e61d2c 100644 --- a/README.md +++ b/README.md @@ -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** @@ -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. diff --git a/README_CN.md b/README_CN.md index 75232b099..ef6797e9c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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` 初始密码;也可在「平台权限 → 用户管理」中创建账号 @@ -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)。 diff --git a/config.example.yaml b/config.example.yaml index dd64d702b..f2a15e183 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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 diff --git a/docs/en-US/configuration.md b/docs/en-US/configuration.md index f04c1da17..56a00eb1a 100644 --- a/docs/en-US/configuration.md +++ b/docs/en-US/configuration.md @@ -61,6 +61,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 ``` | Field | Meaning | @@ -68,7 +74,7 @@ ai: | `ai.default_channel` | Default channel ID for new conversations and requests without an explicit channel. | | `ai.channels.` | 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. | diff --git a/docs/zh-CN/configuration.md b/docs/zh-CN/configuration.md index 5c9dc1adb..373d8d51e 100644 --- a/docs/zh-CN/configuration.md +++ b/docs/zh-CN/configuration.md @@ -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`。 @@ -65,7 +71,7 @@ ai: | `ai.default_channel` | 默认通道 ID。新对话、机器人、批量任务和未显式选择通道的请求使用它。 | | `ai.channels.` | 通道配置。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` | 单次模型输出上限;未填时使用默认值。 | diff --git a/internal/config/config.go b/internal/config/config.go index 21efe2332..878e1f3a7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 项目黑板(跨对话共享事实)配置。 @@ -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 相同)。 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a774f8c69..9f2ea7449 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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) + } + } +} diff --git a/internal/handler/config.go b/internal/handler/config.go index 0ac24f4fa..bd607f825 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -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" } @@ -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{ @@ -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" } diff --git a/internal/handler/openapi.go b/internal/handler/openapi.go index fad523d02..6b6703cf9 100644 --- a/internal/handler/openapi.go +++ b/internal/handler/openapi.go @@ -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"}, @@ -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密钥"}, }, diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 41fbf538c..734e69843 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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.", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 42d99610f..84a3971d5 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2765,6 +2765,7 @@ "apiProvider": "API 提供商", "providerOpenAI": "OpenAI / 兼容 OpenAI 协议", "providerClaude": "Claude (Anthropic Messages API)", + "providerOrcaRouter": "OrcaRouter(OpenAI 兼容网关)", "visionProviderReuseOpenAI": "默认 AI 通道(留空复用)", "fofaConfig": "FOFA 配置", "fofaConfigHint": "用于资产发现与导入,仅需配置 API Key。", diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 0c1232d60..478df81ea 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -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 || ''; @@ -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', '待完善')); diff --git a/web/templates/index.html b/web/templates/index.html index c094de239..f642a7f60 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3608,6 +3608,7 @@
连接信息
@@ -3722,6 +3723,7 @@

视觉分析(analyze_image)

+
@@ -3929,6 +3931,7 @@
审计 Agent 模型
+