diff --git a/channel/web/chat.html b/channel/web/chat.html
index 2c013072..25065316 100644
--- a/channel/web/chat.html
+++ b/channel/web/chat.html
@@ -448,6 +448,9 @@
+
+ 接口需遵循 OpenAI API 协议
+
diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js
index 79a4ca7f..8ec9e469 100644
--- a/channel/web/static/js/console.js
+++ b/channel/web/static/js/console.js
@@ -45,6 +45,7 @@ const I18N = {
config_save: '保存', config_saved: '已保存',
config_save_error: '保存失败',
config_custom_option: '自定义...',
+ config_custom_tip: '接口需遵循 OpenAI API 协议',
config_security: '安全设置', config_password: '访问密码',
config_password_hint: '留空则不启用密码保护',
config_password_changed: '密码已更新,请重新登录',
@@ -130,6 +131,7 @@ const I18N = {
config_save: 'Save', config_saved: 'Saved',
config_save_error: 'Save failed',
config_custom_option: 'Custom...',
+ config_custom_tip: 'API must follow OpenAI protocol.',
config_security: 'Security', config_password: 'Password',
config_password_hint: 'Leave empty to disable password protection',
config_password_changed: 'Password updated, please re-login',
@@ -2158,6 +2160,9 @@ function onProviderChange(pid) {
const p = configProviders[cfgProviderValue];
if (!p) return;
+ const customTip = document.getElementById('cfg-custom-tip');
+ if (customTip) customTip.classList.toggle('hidden', cfgProviderValue !== 'custom');
+
const modelEl = document.getElementById('cfg-model-select');
const modelOpts = (p.models || []).map(m => ({ value: m, label: m }));
modelOpts.push({ value: '__custom__', label: t('config_custom_option') });
diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py
index be538ee6..c9239ebd 100644
--- a/channel/web/web_channel.py
+++ b/channel/web/web_channel.py
@@ -801,15 +801,22 @@ class ConfigHandler:
"api_base_default": None,
"models": _RECOMMENDED_MODELS,
}),
+ ("custom", {
+ "label": "自定义",
+ "api_key_field": "custom_api_key",
+ "api_base_key": "custom_api_base",
+ "api_base_default": "",
+ "models": [],
+ }),
])
EDITABLE_KEYS = {
"model", "bot_type", "use_linkai",
"open_ai_api_base", "deepseek_api_base", "claude_api_base", "gemini_api_base",
- "zhipu_ai_api_base", "moonshot_base_url", "ark_base_url",
+ "zhipu_ai_api_base", "moonshot_base_url", "ark_base_url", "custom_api_base",
"open_ai_api_key", "deepseek_api_key", "claude_api_key", "gemini_api_key",
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
- "ark_api_key", "minimax_api_key", "linkai_api_key",
+ "ark_api_key", "minimax_api_key", "linkai_api_key", "custom_api_key",
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
"enable_thinking", "web_password",
}
diff --git a/common/const.py b/common/const.py
index ecaf5b0f..5cc5bdb1 100644
--- a/common/const.py
+++ b/common/const.py
@@ -14,6 +14,7 @@ ZHIPU_AI = "zhipu"
MOONSHOT = "moonshot"
MiniMax = "minimax"
DEEPSEEK = "deepseek"
+CUSTOM = "custom" # custom OpenAI-compatible API, bot_type won't auto-switch on model change
MODELSCOPE = "modelscope"
# 模型列表
diff --git a/config.py b/config.py
index 5e0ce8f2..521d03fe 100644
--- a/config.py
+++ b/config.py
@@ -17,10 +17,12 @@ available_setting = {
"open_ai_api_base": "https://api.openai.com/v1",
"claude_api_base": "https://api.anthropic.com/v1", # claude api base
"gemini_api_base": "https://generativelanguage.googleapis.com", # gemini api base
+ "custom_api_key": "", # custom OpenAI-compatible provider api key (used when bot_type is "custom")
+ "custom_api_base": "", # custom OpenAI-compatible provider api base (used when bot_type is "custom")
"proxy": "", # openai使用的代理
# chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
"model": "gpt-3.5-turbo", # 可选择: gpt-4o, pt-4o-mini, gpt-4-turbo, claude-3-sonnet, wenxin, moonshot, qwen-turbo, xunfei, glm-4, minimax, gemini等模型,全部可选模型详见common/const.py文件
- "bot_type": "", # 可选配置,使用兼容openai格式的三方服务时候,需填"openai"(历史值"chatGPT"仍兼容)。bot具体名称详见common/const.py文件,如不填根据model名称判断
+ "bot_type": "", # 可选配置,使用兼容openai格式的三方服务时候,需填"openai"或"custom"(custom模式下切换模型不会自动切换bot_type)。bot具体名称详见common/const.py文件,如不填根据model名称判断
"use_azure_chatgpt": False, # 是否使用azure的chatgpt
"azure_deployment_id": "", # azure 模型部署名称
"azure_api_version": "", # azure api版本
diff --git a/docs/docs.json b/docs/docs.json
index a6789ff3..01b9e391 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -82,7 +82,8 @@
"models/openai",
"models/deepseek",
"models/linkai",
- "models/coding-plan"
+ "models/coding-plan",
+ "models/custom"
]
}
]
@@ -257,7 +258,8 @@
"en/models/openai",
"en/models/deepseek",
"en/models/linkai",
- "en/models/coding-plan"
+ "en/models/coding-plan",
+ "en/models/custom"
]
}
]
@@ -432,7 +434,8 @@
"ja/models/openai",
"ja/models/deepseek",
"ja/models/linkai",
- "ja/models/coding-plan"
+ "ja/models/coding-plan",
+ "ja/models/custom"
]
}
]
diff --git a/docs/en/models/custom.mdx b/docs/en/models/custom.mdx
new file mode 100644
index 00000000..94bc825c
--- /dev/null
+++ b/docs/en/models/custom.mdx
@@ -0,0 +1,62 @@
+---
+title: Custom
+description: Custom provider for third-party APIs and local models
+---
+
+For models accessed via OpenAI-compatible APIs, such as:
+
+- **Third-party API proxies**: Use a unified API Base to call multiple models
+- **Local models**: Models deployed locally via Ollama, vLLM, LocalAI, etc.
+- **Private deployments**: Self-hosted model services within your organization
+
+
+ Unlike the `openai` provider, switching models under the Custom provider will not auto-switch the provider type. Your custom API address is always preserved.
+
+
+## Configuration
+
+### Third-party API Proxy
+
+```json
+{
+ "bot_type": "custom",
+ "model": "deepseek-chat",
+ "custom_api_key": "YOUR_API_KEY",
+ "custom_api_base": "https://{your-proxy.com}/v1"
+}
+```
+
+| Parameter | Description |
+| --- | --- |
+| `bot_type` | Must be set to `custom` |
+| `model` | Model name, any model supported by your proxy service |
+| `custom_api_key` | API key provided by your proxy service |
+| `custom_api_base` | API base URL, must be OpenAI-compatible |
+
+### Local Models
+
+Local models typically don't require an API key — just set the API base:
+
+```json
+{
+ "bot_type": "custom",
+ "model": "qwen3.5:27b",
+ "custom_api_base": "http://localhost:11434/v1"
+}
+```
+
+Common local deployment tools and their default addresses:
+
+| Tool | Default API Base |
+| --- | --- |
+| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
+| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
+| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
+
+## Switching Models
+
+Under the Custom provider, switching models only changes `model` without affecting `bot_type` or the API address:
+
+```
+/config model qwen3.5:27b
+```
diff --git a/docs/ja/models/custom.mdx b/docs/ja/models/custom.mdx
new file mode 100644
index 00000000..7746faa9
--- /dev/null
+++ b/docs/ja/models/custom.mdx
@@ -0,0 +1,62 @@
+---
+title: カスタム
+description: サードパーティAPIやローカルモデル向けのカスタムプロバイダー設定
+---
+
+OpenAI互換プロトコルでアクセスするモデルサービスに適用します:
+
+- **サードパーティAPIプロキシ**:統一APIベースで複数モデルを呼び出し
+- **ローカルモデル**:Ollama、vLLM、LocalAIなどでローカルにデプロイされたモデル
+- **プライベートデプロイ**:組織内でホストされたモデルサービス
+
+
+ `openai` プロバイダーとの違い:カスタムプロバイダーでは `/config model` でモデルを切り替えてもプロバイダータイプは自動切り替えされず、カスタムAPIアドレスが常に保持されます。
+
+
+## 設定方法
+
+### サードパーティAPIプロキシ
+
+```json
+{
+ "bot_type": "custom",
+ "model": "deepseek-chat",
+ "custom_api_key": "YOUR_API_KEY",
+ "custom_api_base": "https://{your-proxy.com}/v1"
+}
+```
+
+| パラメータ | 説明 |
+| --- | --- |
+| `bot_type` | `custom` に設定必須 |
+| `model` | モデル名、プロキシサービスがサポートする任意のモデル名 |
+| `custom_api_key` | プロキシサービスが提供するAPIキー |
+| `custom_api_base` | APIアドレス、OpenAI互換プロトコルが必要 |
+
+### ローカルモデル
+
+ローカルモデルは通常APIキー不要で、APIベースのみ設定します:
+
+```json
+{
+ "bot_type": "custom",
+ "model": "qwen3.5:27b",
+ "custom_api_base": "http://localhost:11434/v1"
+}
+```
+
+一般的なローカルデプロイツールとデフォルトアドレス:
+
+| ツール | デフォルトAPIベース |
+| --- | --- |
+| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
+| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
+| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
+
+## モデル切り替え
+
+カスタムプロバイダーではモデル切り替え時に `model` のみ変更され、`bot_type` やAPIアドレスは変わりません:
+
+```
+/config model qwen3.5:27b
+```
diff --git a/docs/models/custom.mdx b/docs/models/custom.mdx
new file mode 100644
index 00000000..d69b9628
--- /dev/null
+++ b/docs/models/custom.mdx
@@ -0,0 +1,62 @@
+---
+title: 自定义
+description: 自定义厂商配置,适用于第三方 API 代理和本地模型
+---
+
+适用于通过 OpenAI 兼容协议接入的第三方模型服务或本地部署的模型,例如:
+
+- **第三方 API 代理**:使用统一的 API Base 调用多种模型
+- **本地模型**:通过 Ollama、vLLM、LocalAI 等工具在本地部署的模型
+- **私有化部署**:企业内部部署的模型服务
+
+
+ 与 `openai` 厂商的区别:选择自定义厂商后,通过 `/config model` 切换模型时,不会自动切换厂商类型,始终使用自定义的 API 地址。
+
+
+## 配置方式
+
+### 第三方 API 代理
+
+```json
+{
+ "bot_type": "custom",
+ "model": "deepseek-chat",
+ "custom_api_key": "YOUR_API_KEY",
+ "custom_api_base": "https://{your-proxy.com}/v1"
+}
+```
+
+| 参数 | 说明 |
+| --- | --- |
+| `bot_type` | 必须设为 `custom` |
+| `model` | 模型名称,可填写代理服务支持的任意模型名 |
+| `custom_api_key` | API 密钥,由代理服务提供 |
+| `custom_api_base` | API 地址,由代理服务提供,需兼容 OpenAI 协议 |
+
+### 本地模型
+
+本地模型通常不需要 API Key,只需填写 API Base 即可:
+
+```json
+{
+ "bot_type": "custom",
+ "model": "qwen3.5:27b",
+ "custom_api_base": "http://localhost:11434/v1"
+}
+```
+
+常见的本地部署工具及默认地址:
+
+| 工具 | 默认 API Base |
+| --- | --- |
+| [Ollama](https://ollama.com) | `http://localhost:11434/v1` |
+| [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` |
+| [LocalAI](https://localai.io) | `http://localhost:8080/v1` |
+
+## 切换模型
+
+自定义厂商下切换模型时,只会修改 `model`,不会改变 `bot_type` 和 API 地址:
+
+```
+/config model qwen3.5:27b
+```
diff --git a/docs/models/index.mdx b/docs/models/index.mdx
index 4d6ab34a..930f169e 100644
--- a/docs/models/index.mdx
+++ b/docs/models/index.mdx
@@ -53,6 +53,9 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
多模型统一接口 + 知识库
+
+ 第三方代理、本地模型等
+
diff --git a/models/bot_factory.py b/models/bot_factory.py
index 5f2b9370..632a9052 100644
--- a/models/bot_factory.py
+++ b/models/bot_factory.py
@@ -21,7 +21,7 @@ def create_bot(bot_type):
from models.deepseek.deepseek_bot import DeepSeekBot
return DeepSeekBot()
- elif bot_type in (const.OPENAI, const.CHATGPT): # OpenAI-compatible API
+ elif bot_type in (const.OPENAI, const.CHATGPT, const.CUSTOM): # OpenAI-compatible API
from models.chatgpt.chat_gpt_bot import ChatGPTBot
return ChatGPTBot()
diff --git a/models/chatgpt/chat_gpt_bot.py b/models/chatgpt/chat_gpt_bot.py
index f6e4b50f..1c01c902 100644
--- a/models/chatgpt/chat_gpt_bot.py
+++ b/models/chatgpt/chat_gpt_bot.py
@@ -23,10 +23,15 @@ from models.baidu.baidu_wenxin_session import BaiduWenxinSession
class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
def __init__(self):
super().__init__()
- # set the default api_key
- openai.api_key = conf().get("open_ai_api_key")
- if conf().get("open_ai_api_base"):
- openai.api_base = conf().get("open_ai_api_base")
+ # set the default api_key / api_base based on bot_type
+ if conf().get("bot_type") == "custom":
+ openai.api_key = conf().get("custom_api_key", "")
+ if conf().get("custom_api_base"):
+ openai.api_base = conf().get("custom_api_base")
+ else:
+ openai.api_key = conf().get("open_ai_api_key")
+ if conf().get("open_ai_api_base"):
+ openai.api_base = conf().get("open_ai_api_base")
proxy = conf().get("proxy")
if proxy:
openai.proxy = proxy
@@ -56,9 +61,10 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
def get_api_config(self):
"""Get API configuration for OpenAI-compatible base class"""
+ is_custom = conf().get("bot_type") == "custom"
return {
- 'api_key': conf().get("open_ai_api_key"),
- 'api_base': conf().get("open_ai_api_base"),
+ 'api_key': conf().get("custom_api_key") if is_custom else conf().get("open_ai_api_key"),
+ 'api_base': conf().get("custom_api_base") if is_custom else conf().get("open_ai_api_base"),
'model': conf().get("model", "gpt-3.5-turbo"),
'default_temperature': conf().get("temperature", 0.9),
'default_top_p': conf().get("top_p", 1.0),
@@ -166,9 +172,10 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
mime_type = mime_type_map.get(extension, "image/jpeg")
# Get model and API config
+ is_custom = conf().get("bot_type") == "custom"
model = context.get("gpt_model") or conf().get("model", "gpt-4o")
- api_key = context.get("openai_api_key") or conf().get("open_ai_api_key")
- api_base = conf().get("open_ai_api_base")
+ api_key = context.get("openai_api_key") or (conf().get("custom_api_key") if is_custom else conf().get("open_ai_api_key"))
+ api_base = conf().get("custom_api_base") if is_custom else conf().get("open_ai_api_base")
# Build vision request
messages = [
diff --git a/plugins/cow_cli/cow_cli.py b/plugins/cow_cli/cow_cli.py
index 6f58c1be..8addc9c4 100644
--- a/plugins/cow_cli/cow_cli.py
+++ b/plugins/cow_cli/cow_cli.py
@@ -381,9 +381,12 @@ class CowCliPlugin(Plugin):
updates = {key: new_val}
if key == "model" and conf().get("bot_type"):
- resolved = self._resolve_bot_type_for_model(str(new_val))
- if resolved:
- updates["bot_type"] = resolved
+ from common import const
+ current_bot_type = conf().get("bot_type")
+ if current_bot_type not in (const.CUSTOM,):
+ resolved = self._resolve_bot_type_for_model(str(new_val))
+ if resolved:
+ updates["bot_type"] = resolved
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_path = os.path.join(project_root, "config.json")