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