feat: add custom model provider

This commit is contained in:
zhayujie
2026-04-15 12:26:05 +08:00
parent 83f778fec9
commit 3830f76729
13 changed files with 238 additions and 18 deletions

View File

@@ -448,6 +448,9 @@
</div>
<div class="cfg-dropdown-menu"></div>
</div>
<div id="cfg-custom-tip" class="mt-1.5 text-xs text-slate-400 dark:text-slate-500 hidden">
<i class="fas fa-info-circle mr-1"></i><span data-i18n="config_custom_tip">接口需遵循 OpenAI API 协议</span>
</div>
</div>
<!-- Model -->
<div>

View File

@@ -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') });

View File

@@ -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",
}

View File

@@ -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"
# 模型列表

View File

@@ -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版本

View File

@@ -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"
]
}
]

62
docs/en/models/custom.mdx Normal file
View File

@@ -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
<Note>
Unlike the `openai` provider, switching models under the Custom provider will not auto-switch the provider type. Your custom API address is always preserved.
</Note>
## 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
```

62
docs/ja/models/custom.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: カスタム
description: サードパーティAPIやローカルモデル向けのカスタムプロバイダー設定
---
OpenAI互換プロトコルでアクセスするモデルサービスに適用します
- **サードパーティAPIプロキシ**統一APIベースで複数モデルを呼び出し
- **ローカルモデル**Ollama、vLLM、LocalAIなどでローカルにデプロイされたモデル
- **プライベートデプロイ**:組織内でホストされたモデルサービス
<Note>
`openai` プロバイダーとの違い:カスタムプロバイダーでは `/config model` でモデルを切り替えてもプロバイダータイプは自動切り替えされず、カスタムAPIアドレスが常に保持されます。
</Note>
## 設定方法
### サードパーティ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
```

62
docs/models/custom.mdx Normal file
View File

@@ -0,0 +1,62 @@
---
title: 自定义
description: 自定义厂商配置,适用于第三方 API 代理和本地模型
---
适用于通过 OpenAI 兼容协议接入的第三方模型服务或本地部署的模型,例如:
- **第三方 API 代理**:使用统一的 API Base 调用多种模型
- **本地模型**:通过 Ollama、vLLM、LocalAI 等工具在本地部署的模型
- **私有化部署**:企业内部部署的模型服务
<Note>
与 `openai` 厂商的区别:选择自定义厂商后,通过 `/config model` 切换模型时,不会自动切换厂商类型,始终使用自定义的 API 地址。
</Note>
## 配置方式
### 第三方 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
```

View File

@@ -53,6 +53,9 @@ CowAgent 支持国内外主流厂商的大语言模型,模型接口实现在
<Card title="LinkAI" href="/models/linkai">
多模型统一接口 + 知识库
</Card>
<Card title="自定义" href="/models/custom">
第三方代理、本地模型等
</Card>
</CardGroup>

View File

@@ -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()

View File

@@ -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 = [

View File

@@ -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")