From 6b5ee245ae7417506653a15432cfa9bf3caf06ca Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 12 Jun 2026 18:03:33 +0800 Subject: [PATCH] feat(web): integrate custom providers into the provider credentials section - Merge the separate custom-providers section into the unified provider grid; "Custom" in the add-provider picker now acts as an add-new action (trailing + mark) and opens the dedicated modal, supporting multiple OpenAI-compatible endpoints - Simplify the custom provider modal: drop the default-model field, add an inline delete button, align colors with the theme - Keep the legacy single "custom" card visible (models page, chat dropdown and legacy config page) while custom_api_key/custom_api_base is still in use, so existing single-provider setups don't disappear - Unify user-facing wording from "vendor" to "provider" in UI and docs - Restructure custom provider docs (zh/en/ja) around Web console and config file usage --- channel/web/chat.html | 28 ++-- channel/web/static/css/console.css | 8 ++ channel/web/static/js/console.js | 221 +++++++++-------------------- channel/web/web_channel.py | 65 +++++++-- docs/ja/models/custom.mdx | 98 ++++--------- docs/models/custom.mdx | 88 +++--------- docs/models/deepseek.mdx | 2 +- docs/models/index.mdx | 14 +- docs/models/linkai.mdx | 2 +- docs/models/mimo.mdx | 2 +- docs/models/openai.mdx | 2 +- docs/models/qwen.mdx | 2 +- docs/releases/v2.0.3.mdx | 2 +- docs/releases/v2.0.7.mdx | 2 +- docs/zh/models/custom.mdx | 98 ++++--------- 15 files changed, 234 insertions(+), 400 deletions(-) diff --git a/channel/web/chat.html b/channel/web/chat.html index 0b71d745..9e5dadd6 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -1171,8 +1171,8 @@ w-full max-w-md mx-4">
-
- +
+

@@ -1185,15 +1185,14 @@ + focus:outline-none focus:border-primary-500 transition-colors">
@@ -1201,20 +1200,15 @@ -
-
- - + focus:outline-none focus:border-primary-500 font-mono transition-colors">
-
+
+
diff --git a/channel/web/static/css/console.css b/channel/web/static/css/console.css index e260bbc9..155e240a 100644 --- a/channel/web/static/css/console.css +++ b/channel/web/static/css/console.css @@ -854,6 +854,14 @@ font-size: 11px; flex-shrink: 0; } +/* "Custom" row is an add-new action: trailing + instead of ✓. */ +.vendor-picker-add-mark { + margin-left: auto; + padding-left: 12px; + color: #94a3b8; + font-size: 11px; + flex-shrink: 0; +} /* Chat Input */ #chat-input { diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index f9a7b43c..c2b65819 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -32,21 +32,13 @@ const I18N = { models_clear_credential: '清除凭据', models_base_default_hint: '留空将使用官方默认地址', models_base_default: '默认', - models_custom_section: '自定义厂商', - models_custom_section_desc: '配置多个 OpenAI 兼容厂商,自由切换', - models_custom_add: '添加自定义厂商', + models_custom_vendor_label: '自定义', models_custom_name: '名称', - models_custom_name_placeholder: '例如 siliconflow', - models_custom_default_model: '默认模型(可选)', - models_custom_default_model_placeholder: '例如 deepseek-ai/DeepSeek-V3', - models_custom_active: '使用中', - models_custom_set_active: '设为使用中', models_custom_delete: '删除', models_custom_delete_confirm_title: '删除自定义厂商', models_custom_delete_confirm_msg: '确定删除该自定义厂商吗?此操作无法撤销。', models_custom_name_required: '请填写名称', models_custom_base_required: '请填写 API Base', - models_custom_empty: '尚未配置自定义厂商,点击添加', models_custom_edit_title: '编辑自定义厂商', models_custom_add_title: '添加自定义厂商', models_capability_chat: '主模型', @@ -240,10 +232,10 @@ const I18N = { menu_logs: 'Logs', models_title: 'Models', models_desc: 'Manage chat, image, voice, embedding and search capabilities in one place', - models_section_vendors: 'Vendor Credentials', + models_section_vendors: 'Provider Credentials', models_section_vendors_desc: 'Configured once, shared by multiple model capabilities', models_section_capabilities: 'Capabilities', - models_add_vendor: 'Add Vendor', + models_add_vendor: 'Add Provider', models_provider: 'Provider', models_model: 'Model', models_voice: 'Voice', @@ -253,21 +245,13 @@ const I18N = { models_clear_credential: 'Clear credentials', models_base_default_hint: 'Leave blank to use the official default base URL', models_base_default: 'Default', - models_custom_section: 'Custom Providers', - models_custom_section_desc: 'Configure multiple OpenAI-compatible providers and switch freely', - models_custom_add: 'Add custom provider', + models_custom_vendor_label: 'Custom', models_custom_name: 'Name', - models_custom_name_placeholder: 'e.g. siliconflow', - models_custom_default_model: 'Default model (optional)', - models_custom_default_model_placeholder: 'e.g. deepseek-ai/DeepSeek-V3', - models_custom_active: 'Active', - models_custom_set_active: 'Set active', models_custom_delete: 'Delete', models_custom_delete_confirm_title: 'Delete custom provider', models_custom_delete_confirm_msg: 'Delete this custom provider? This cannot be undone.', models_custom_name_required: 'Name is required', models_custom_base_required: 'API Base is required', - models_custom_empty: 'No custom providers yet, click to add', models_custom_edit_title: 'Edit custom provider', models_custom_add_title: 'Add custom provider', models_capability_chat: 'Main Model', @@ -311,8 +295,8 @@ const I18N = { models_embedding_saved_msg: 'Send /memory rebuild-index in the chat to rebuild the index.', models_embedding_saved_ok: 'Go', models_pick_provider: 'Pick a provider', - models_clear_confirm_title: 'Clear vendor credentials', - models_clear_confirm_msg: 'Remove this vendor\'s API Key and Base URL? Capabilities relying on it will stop working.', + models_clear_confirm_title: 'Clear provider credentials', + models_clear_confirm_msg: 'Remove this provider\'s API Key and Base URL? Capabilities relying on it will stop working.', cancel: 'Cancel', save: 'Save', ok: 'OK', @@ -4766,12 +4750,12 @@ function renderModelsView() { const container = document.getElementById('models-content'); container.innerHTML = ''; container.appendChild(renderVendorsSection()); - container.appendChild(renderCustomProvidersSection()); MODELS_CAPABILITY_DEFS.forEach(def => container.appendChild(renderCapabilityCard(def))); } // True when a provider card is one of the expanded custom (OpenAI-compatible) -// providers — these are managed in their own section, not the vendor grid. +// providers (id "custom:") — shown in the vendor grid alongside built-in +// vendors, but edited via the dedicated custom-provider modal. function isCustomProviderCard(p) { return !!(p && p.is_custom && p.custom_name); } @@ -4782,10 +4766,9 @@ function renderVendorsSection() { const wrap = document.createElement('div'); wrap.className = 'bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6'; - // Expanded custom providers live in their own section; keep the built-in - // vendor grid focused on the canonical (field-based) providers. - const builtinProviders = modelsState.providers.filter(p => !isCustomProviderCard(p)); - const configured = builtinProviders.filter(p => p.configured); + // Custom providers always show once created (even without an api key, + // e.g. a local vLLM/Ollama endpoint); built-in vendors show when configured. + const configured = modelsState.providers.filter(p => p.configured || isCustomProviderCard(p)); const header = `
@@ -4796,7 +4779,6 @@ function renderVendorsSection() {

${t('models_section_vendors')}

${t('models_section_vendors_desc')}

- ${configured.length}/${builtinProviders.length}
`; let body; @@ -4822,8 +4804,13 @@ function renderVendorsSection() { function renderVendorChip(p) { // The masked API key is intentionally not surfaced here; it is shown // inside the edit modal so the chip stays uncluttered and scannable. + // Custom providers open their dedicated modal (name + base + key); + // their ids are server-generated hex, safe to inline. + const onclick = isCustomProviderCard(p) + ? `openCustomProviderModal('${escapeHtml(p.custom_id)}')` + : `openVendorModal('${escapeHtml(p.id)}')`; return ` - -
`; - - let body; - if (customs.length === 0) { - body = ` -
-

${t('models_custom_empty')}

-
`; - } else { - body = `
- ${customs.map(renderCustomProviderRow).join('')} -
`; - } - - wrap.innerHTML = header + body; - // Event delegation — handles all custom-provider actions via data-action attrs. - wrap.addEventListener('click', function(e) { - const btn = e.target.closest('[data-action]'); - if (!btn) return; - const action = btn.getAttribute('data-action'); - const providerId = btn.getAttribute('data-provider-id') || ''; - if (action === 'add-custom') openCustomProviderModal(''); - else if (action === 'edit-custom') openCustomProviderModal(providerId); - else if (action === 'delete-custom') deleteCustomProvider(providerId); - else if (action === 'set-active-custom') setActiveCustomProvider(providerId); - }); - return wrap; -} - -function renderCustomProviderRow(p) { - const id = p.custom_id || ''; - const name = p.custom_name || ''; - const nameEsc = escapeHtml(name); - // The active provider gets a highlighted ring + badge; others show a - // "set active" affordance via data-attributes (no inline onclick — XSS safe). - const activeBadge = p.active - ? ` - ${t('models_custom_active')}` - : ``; - - const ring = p.active - ? 'border-emerald-300 dark:border-emerald-500/40 bg-emerald-50/40 dark:bg-emerald-900/10' - : 'border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5'; - - const model = p.model - ? `${escapeHtml(p.model)}` - : ''; - const base = p.api_base - ? `${escapeHtml(p.api_base)}` - : ''; - - return ` -
- ${renderProviderLogo(p, 28)} -
-
- ${nameEsc} - ${activeBadge} -
-
${base}${model ? '·' + model : ''}
-
- - -
`; -} - // ---------- Capability cards (Layer 2) --------------------------------- function renderCapabilityCard(def) { @@ -5776,6 +5661,14 @@ function decorateVendorModalPicker(ddEl, opts) { // Tag the row so the global active-row ✓ rule is suppressed in CSS // (otherwise configured AND selected rows would render two checks). item.classList.add('vendor-picker-item'); + if (opt._isAddNew) { + // "Custom" is an add-new action (multiple entries allowed), + // so show a trailing + instead of the configured ✓. + const plus = document.createElement('i'); + plus.className = 'fas fa-plus vendor-picker-add-mark'; + item.appendChild(plus); + return; + } if (!opt._configured) return; const check = document.createElement('i'); check.className = 'fas fa-check vendor-picker-configured-mark'; @@ -6083,22 +5976,42 @@ function openVendorModal(providerId, onSaved) { // currently selected vendor via its own background highlight, so we // intentionally suppress the global active-row ✓ for this picker // (see CSS) — otherwise configured + selected rows would show two. - // Custom (OpenAI-compatible) providers are managed in their own - // section with a dedicated modal, so they are excluded from this - // built-in vendor picker. + // Expanded custom provider cards ("custom:") are edited via their + // dedicated modal, so they are excluded from this picker. Picking the + // "custom" entry creates a *new* custom provider via that modal — + // this is how multiple OpenAI-compatible endpoints are added. const builtinProviders = modelsState.providers.filter(p => !isCustomProviderCard(p)); - const unconfigured = builtinProviders.filter(p => !p.configured); - const defaultId = (unconfigured[0] && unconfigured[0].id) || (builtinProviders[0] && builtinProviders[0].id) || ''; - pickerWrap.classList.remove('hidden'); - const pickerEl = document.getElementById('vendor-modal-picker'); const pickerOpts = builtinProviders.map(p => ({ value: p.id, label: localizedLabel(p.label), _configured: !!p.configured, })); - initDropdown(pickerEl, pickerOpts, defaultId, (val) => fillVendorModalForProvider(val)); + // In multi-provider mode the backend replaces the bare "custom" card + // with the expanded ones; re-add it here so the entry stays available. + if (!pickerOpts.some(o => o.value === 'custom')) { + pickerOpts.push({ value: 'custom', label: t('models_custom_vendor_label'), _configured: false }); + } + // "Custom" always behaves as an add-new action (multiple entries + // allowed), so it shows a + mark instead of the configured ✓. + pickerOpts.forEach(o => { if (o.value === 'custom') { o._isAddNew = true; o._configured = false; } }); + const unconfigured = builtinProviders.filter(p => !p.configured); + const defaultId = (unconfigured[0] && unconfigured[0].id) || (builtinProviders[0] && builtinProviders[0].id) || 'custom'; + pickerWrap.classList.remove('hidden'); + const pickerEl = document.getElementById('vendor-modal-picker'); + const onPick = (val) => { + if (val === 'custom') { + // "Custom" in the add flow always creates a new + // OpenAI-compatible provider entry via the dedicated modal + // (name + base + key), supporting multiple custom endpoints. + closeVendorModal(); + openCustomProviderModal(''); + return; + } + fillVendorModalForProvider(val); + }; + initDropdown(pickerEl, pickerOpts, defaultId, onPick); decorateVendorModalPicker(pickerEl, pickerOpts); - fillVendorModalForProvider(defaultId); + onPick(defaultId); } else { pickerWrap.classList.add('hidden'); fillVendorModalForProvider(providerId); @@ -6276,11 +6189,9 @@ function openCustomProviderModal(providerId) { const nameInput = document.getElementById('custom-provider-name'); const baseInput = document.getElementById('custom-provider-base'); const keyInput = document.getElementById('custom-provider-key'); - const modelInput = document.getElementById('custom-provider-model'); nameInput.value = card ? (card.custom_name || '') : ''; baseInput.value = card ? (card.api_base || '') : ''; - modelInput.value = card ? (card.model || '') : ''; // Surface the masked key as the value for configured providers so the // "already set" state is unambiguous; an untouched masked value means @@ -6307,6 +6218,13 @@ function openCustomProviderModal(providerId) { document.getElementById('custom-provider-modal-cancel').onclick = closeCustomProviderModal; document.getElementById('custom-provider-modal-save').onclick = saveCustomProviderModal; + // Delete is only available when editing an existing provider. + const deleteBtn = document.getElementById('custom-provider-modal-delete'); + if (deleteBtn) { + deleteBtn.classList.toggle('hidden', !editing); + deleteBtn.onclick = editing ? () => deleteCustomProvider(providerId) : null; + } + function onOverlayClick(e) { if (e.target === overlay) { closeCustomProviderModal(); @@ -6325,7 +6243,6 @@ function closeCustomProviderModal() { function saveCustomProviderModal() { const name = document.getElementById('custom-provider-name').value.trim(); const apiBase = document.getElementById('custom-provider-base').value.trim(); - const model = document.getElementById('custom-provider-model').value.trim(); const keyInput = document.getElementById('custom-provider-key'); if (!name) { @@ -6350,7 +6267,6 @@ function saveCustomProviderModal() { action: 'set_custom_provider', name: name, api_base: apiBase, - model: model, }; if (apiKey) payload.api_key = apiKey; if (editing) payload.id = customProviderModalState.editId; @@ -6375,16 +6291,6 @@ function saveCustomProviderModal() { }); } -function setActiveCustomProvider(providerId) { - fetch('/api/models', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'set_active_custom_provider', id: providerId }), - }).then(r => r.json()).then(data => { - if (data.status === 'success') loadModelsView(); - }).catch(() => { /* noop */ }); -} - function deleteCustomProvider(providerId) { showConfirmDialog({ title: t('models_custom_delete_confirm_title'), @@ -6397,7 +6303,10 @@ function deleteCustomProvider(providerId) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete_custom_provider', id: providerId }), }).then(r => r.json()).then(data => { - if (data.status === 'success') loadModelsView(); + if (data.status === 'success') { + closeCustomProviderModal(); + loadModelsView(); + } }).catch(() => { /* noop */ }); } }); diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 6a59beb6..e45690f4 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -1643,6 +1643,32 @@ class ConfigHandler: "api_key_field": p.get("api_key_field"), } + # Expose user-defined custom providers as "custom:" entries so + # the legacy config page can display and select them. Credentials + # are managed on the Models page, hence the null key/base fields. + # Mirrors the Models page: when expanded entries exist, the bare + # legacy "custom" entry is hidden — unless the flat single-provider + # custom config is still active or filled in. + try: + from models.custom_provider import get_custom_providers + custom_list = get_custom_providers() + legacy_custom_in_use = ModelsHandler._legacy_custom_in_use(local_config) + if custom_list and not legacy_custom_in_use: + providers.pop("custom", None) + for cp in custom_list: + cid = f"custom:{cp.get('id')}" + cname = cp.get("name") or cp.get("id") + providers[cid] = { + "label": {"zh": cname, "en": cname}, + "models": [cp["model"]] if cp.get("model") else [], + "api_base_key": None, + "api_base_default": None, + "api_base_placeholder": "", + "api_key_field": None, + } + except Exception as cp_err: + logger.warning(f"[ConfigHandler] failed to expand custom providers: {cp_err}") + raw_pwd = str(local_config.get("web_password", "") or "") masked_pwd = ("*" * len(raw_pwd)) if raw_pwd else "" @@ -2189,6 +2215,17 @@ class ModelsHandler: }) return cards + @classmethod + def _legacy_custom_in_use(cls, local_config: dict) -> bool: + """True when the flat single-provider custom config is still relevant: + either it is the active bot_type, or its key/base fields are filled. + In that case the legacy "custom" card must stay visible even when + multi ``custom_providers`` entries exist.""" + if (local_config.get("bot_type") or "") == "custom": + return True + return (cls._is_real_key(local_config.get("custom_api_key") or "") + or bool(local_config.get("custom_api_base"))) + @classmethod def _provider_overview(cls) -> List[dict]: """All known providers (configured first, unconfigured after). @@ -2202,13 +2239,18 @@ class ModelsHandler: """ local_config = conf() custom_cards = cls._custom_provider_cards(local_config) + # Keep the legacy single "custom" card visible alongside the expanded + # ones when the flat custom_api_key/base config is active or filled, + # so existing single-provider setups never disappear from the UI. + keep_legacy_custom = cls._legacy_custom_in_use(local_config) items = [] for pid, p in ConfigHandler.PROVIDER_MODELS.items(): if pid == "custom" and custom_cards: - # Multi-provider mode: emit the expanded cards instead of the - # single legacy custom card. + # Multi-provider mode: emit the expanded cards, plus the + # legacy card when it is still in use. items.extend(custom_cards) - continue + if not keep_legacy_custom: + continue key_field = p.get("api_key_field") base_field = p.get("api_base_key") raw_key = local_config.get(key_field, "") if key_field else "" @@ -2253,11 +2295,15 @@ class ModelsHandler: provider_id = "linkai" # In multi-provider mode, replace the single "custom" entry with the # expanded "custom:" ids so the chat dropdown matches the cards. + # The legacy "custom" entry stays when its flat config is still used. provider_ids = [] custom_cards = cls._custom_provider_cards(local_config) + keep_legacy_custom = cls._legacy_custom_in_use(local_config) for pid in ConfigHandler.PROVIDER_MODELS.keys(): if pid == "custom" and custom_cards: provider_ids.extend(c["id"] for c in custom_cards) + if keep_legacy_custom: + provider_ids.append(pid) else: provider_ids.append(pid) return { @@ -2881,11 +2927,14 @@ class ModelsHandler: existing["api_base"] = api_base if api_key: existing["api_key"] = api_key - # model is always overwritten (empty clears the default model). - if model: - existing["model"] = model - else: - existing.pop("model", None) + # Only touch model when explicitly provided in the payload; an + # explicit empty string clears it, a missing key keeps it (the + # UI modal no longer sends model, so manual config survives edits). + if "model" in data: + if model: + existing["model"] = model + else: + existing.pop("model", None) created = False # Decide bot_type — only switch when explicitly requested. diff --git a/docs/ja/models/custom.mdx b/docs/ja/models/custom.mdx index b63f4e34..3df7dc11 100644 --- a/docs/ja/models/custom.mdx +++ b/docs/ja/models/custom.mdx @@ -1,69 +1,31 @@ --- -title: Custom -description: Custom vendor configuration for third-party API proxies and local models +title: カスタム +description: サードパーティ API プロキシやローカルモデル向けのカスタムプロバイダー設定 --- -For model services accessed via the OpenAI-compatible protocol or locally deployed models, such as: +OpenAI 互換プロトコルで接続するモデルサービス向けの設定です。例えば: -- **Third-party API proxies**: call multiple models through a unified API base -- **Local models**: models deployed locally with tools like Ollama, vLLM, LocalAI -- **Private deployments**: model services deployed inside an enterprise +- **サードパーティ API プロキシ**:統一された API アドレスで複数のモデルを呼び出す +- **ローカルモデル**:Ollama、vLLM などのツールでローカルにデプロイしたモデル +- **プライベートデプロイ**:企業内部にデプロイされたモデルサービス - - Difference from the `openai` vendor: when a custom vendor is selected, switching models via `/config model` does not automatically switch the vendor type — the custom API address is always used. - +## Web コンソールでの設定 -## Text Chat +推奨方法です。Web コンソールの「モデル」ページで「プロバイダーを追加」をクリックし、「カスタム」を選択して、名称・API Base・API Key を入力します。複数のカスタムプロバイダーを追加でき、追加後は「メインモデル」でプロバイダーとモデルを選択すると有効になります。 -### Third-party API proxy + -```json -{ - "bot_type": "custom", - "model": "", - "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 name supported by the proxy service | -| `custom_api_key` | API key provided by the proxy service | -| `custom_api_base` | API endpoint provided by the proxy service; must be OpenAI-compatible | - -### Local models - -Local models usually do not require an API key — only the API base needs to be filled in: - -```json -{ - "bot_type": "custom", - "model": "qwen3.5:27b", - "custom_api_base": "http://localhost:11434/v1" -} -``` - -Common local deployment tools and their default endpoints: - -| Tool | Default API Base | +| ツール | デフォルト 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 +## 設定ファイルでの設定 -Switching models under a custom vendor only changes `model` — `bot_type` and the API endpoint remain unchanged: - -``` -/config model qwen3.5:27b -``` - -## Configuring Multiple Custom Providers - -If you need to configure several OpenAI-compatible third-party services at once (e.g. SiliconFlow, Qiniu), use the `custom_providers` list and activate one by setting `bot_type` to `"custom:"`: +`config.json` を直接編集することもできます。`custom_providers` リストに複数のプロバイダーを定義し、`bot_type` を `"custom:"` に設定していずれかを有効化します: ```json { @@ -71,34 +33,30 @@ If you need to configure several OpenAI-compatible third-party services at once "custom_providers": [ { "id": "3f2a9c1b", - "name": "siliconflow", - "api_key": "YOUR_SILICONFLOW_KEY", - "api_base": "https://api.siliconflow.cn/v1", - "model": "deepseek-ai/DeepSeek-V3" + "name": "プロバイダーA", + "api_key": "YOUR_API_KEY_A", + "api_base": "https://api.a.com/v1", + "model": "deepseek-v3" }, { "id": "a1b2c3d4", - "name": "qiniu", - "api_key": "YOUR_QINIU_KEY", - "api_base": "https://api.qnaigc.com/v1", - "model": "deepseek-v3" + "name": "プロバイダーB", + "api_key": "YOUR_API_KEY_B", + "api_base": "https://api.b.com/v1", + "model": "qwen3-max" } ] } ``` -| Parameter | Description | +| パラメータ | 説明 | | --- | --- | -| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` | -| `bot_type` | Set to `"custom:"` to activate a specific provider; `id` is the provider's unique identifier | -| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider | -| `name` | User-facing display label — can be freely renamed without breaking anything | -| `model` | Optional; when set, this model is used instead of the global `model` field | +| `custom_providers` | カスタムプロバイダーのリスト。各項目は `id`、`name`、`api_base`、`api_key`(任意)、`model`(任意)を含む | +| `bot_type` | `"custom:"` に設定して対応するプロバイダーを有効化 | +| `id` | 一意の識別子(8 桁の 16 進数)。Web コンソールから追加すると自動生成され、手動設定の場合は重複しない任意の文字列でよい | +| `name` | 表示名。自由に変更可能 | +| `model` | このプロバイダーで使用するモデル。有効化時に適用される | - The `id` is auto-generated by the web console when you add a provider — you don't need to create it manually. If editing `config.json` by hand, use any unique 8-character hex string (e.g. run `python3 -c "import uuid; print(uuid.uuid4().hex[:8])"`). - - - - When `bot_type` is exactly `"custom"` (no colon suffix), CowAgent uses the legacy single-provider `custom_api_key` / `custom_api_base` fields, so existing configurations keep working without any changes. + 従来の単一プロバイダー設定(`bot_type` を `"custom"` にし、`custom_api_key` / `custom_api_base` を使用)は引き続き互換性があり、変更なしでそのまま利用できます。 diff --git a/docs/models/custom.mdx b/docs/models/custom.mdx index b63f4e34..33b84fa7 100644 --- a/docs/models/custom.mdx +++ b/docs/models/custom.mdx @@ -1,51 +1,21 @@ --- title: Custom -description: Custom vendor configuration for third-party API proxies and local models +description: Custom provider configuration for third-party API proxies and local models --- -For model services accessed via the OpenAI-compatible protocol or locally deployed models, such as: +For model services accessed via the OpenAI-compatible protocol, such as: - **Third-party API proxies**: call multiple models through a unified API base -- **Local models**: models deployed locally with tools like Ollama, vLLM, LocalAI +- **Local models**: models deployed locally with tools like Ollama, vLLM - **Private deployments**: model services deployed inside an enterprise - - Difference from the `openai` vendor: when a custom vendor is selected, switching models via `/config model` does not automatically switch the vendor type — the custom API address is always used. - +## Web Console -## Text Chat +Recommended. On the "Models" page of the Web console, click "Add Provider" and pick "Custom", then fill in the name, API Base and API Key. Multiple custom providers can be added; after adding one, select it together with a model in the "Main Model" card to enable it. -### Third-party API proxy + -```json -{ - "bot_type": "custom", - "model": "", - "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 name supported by the proxy service | -| `custom_api_key` | API key provided by the proxy service | -| `custom_api_base` | API endpoint provided by the proxy service; must be OpenAI-compatible | - -### Local models - -Local models usually do not require an API key — only the API base needs to be filled in: - -```json -{ - "bot_type": "custom", - "model": "qwen3.5:27b", - "custom_api_base": "http://localhost:11434/v1" -} -``` - -Common local deployment tools and their default endpoints: +Default endpoints of common local deployment tools: | Tool | Default API Base | | --- | --- | @@ -53,17 +23,9 @@ Common local deployment tools and their default endpoints: | [vLLM](https://docs.vllm.ai) | `http://localhost:8000/v1` | | [LocalAI](https://localai.io) | `http://localhost:8080/v1` | -### Switching Models +## Configuration File -Switching models under a custom vendor only changes `model` — `bot_type` and the API endpoint remain unchanged: - -``` -/config model qwen3.5:27b -``` - -## Configuring Multiple Custom Providers - -If you need to configure several OpenAI-compatible third-party services at once (e.g. SiliconFlow, Qiniu), use the `custom_providers` list and activate one by setting `bot_type` to `"custom:"`: +You can also edit `config.json` directly: define multiple providers in the `custom_providers` list and set `bot_type` to `"custom:"` to activate one of them: ```json { @@ -71,17 +33,17 @@ If you need to configure several OpenAI-compatible third-party services at once "custom_providers": [ { "id": "3f2a9c1b", - "name": "siliconflow", - "api_key": "YOUR_SILICONFLOW_KEY", - "api_base": "https://api.siliconflow.cn/v1", - "model": "deepseek-ai/DeepSeek-V3" + "name": "ProviderA", + "api_key": "YOUR_API_KEY_A", + "api_base": "https://api.a.com/v1", + "model": "deepseek-v3" }, { "id": "a1b2c3d4", - "name": "qiniu", - "api_key": "YOUR_QINIU_KEY", - "api_base": "https://api.qnaigc.com/v1", - "model": "deepseek-v3" + "name": "ProviderB", + "api_key": "YOUR_API_KEY_B", + "api_base": "https://api.b.com/v1", + "model": "qwen3-max" } ] } @@ -89,16 +51,12 @@ If you need to configure several OpenAI-compatible third-party services at once | Parameter | Description | | --- | --- | -| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` | -| `bot_type` | Set to `"custom:"` to activate a specific provider; `id` is the provider's unique identifier | -| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider | -| `name` | User-facing display label — can be freely renamed without breaking anything | -| `model` | Optional; when set, this model is used instead of the global `model` field | +| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_base`, `api_key` (optional) and `model` (optional) | +| `bot_type` | Set to `"custom:"` to activate the corresponding provider | +| `id` | Unique identifier (8-char hex); auto-generated when adding via the Web console, or any unique string when editing manually | +| `name` | Display label, can be renamed freely | +| `model` | Model used by this provider, takes effect when activated | - The `id` is auto-generated by the web console when you add a provider — you don't need to create it manually. If editing `config.json` by hand, use any unique 8-character hex string (e.g. run `python3 -c "import uuid; print(uuid.uuid4().hex[:8])"`). - - - - When `bot_type` is exactly `"custom"` (no colon suffix), CowAgent uses the legacy single-provider `custom_api_key` / `custom_api_base` fields, so existing configurations keep working without any changes. + The legacy single-provider configuration (`bot_type` set to `"custom"` with `custom_api_key` / `custom_api_base`) remains fully compatible and keeps working without any changes. diff --git a/docs/models/deepseek.mdx b/docs/models/deepseek.mdx index 6de8d09b..a389701e 100644 --- a/docs/models/deepseek.mdx +++ b/docs/models/deepseek.mdx @@ -3,7 +3,7 @@ title: DeepSeek description: DeepSeek model configuration (Text Chat + Thinking Mode) --- -DeepSeek is one of the default recommended vendors in Agent mode, focused on cost-effective text chat and task planning. +DeepSeek is one of the default recommended providers in Agent mode, focused on cost-effective text chat and task planning. ## Text Chat diff --git a/docs/models/index.mdx b/docs/models/index.mdx index 7575adb4..f99acd6f 100644 --- a/docs/models/index.mdx +++ b/docs/models/index.mdx @@ -1,15 +1,15 @@ --- title: Models Overview -description: Model vendors supported by CowAgent and their capability matrix +description: Model providers supported by CowAgent and their capability matrix --- -CowAgent supports a wide range of mainstream large language models. Model interfaces live under the project's `models/` directory. Beyond text chat, several vendors also provide vision understanding, image generation, speech-to-text, text-to-speech, and embeddings — all of which can be invoked on demand in the Agent flow. +CowAgent supports a wide range of mainstream large language models. Model interfaces live under the project's `models/` directory. Beyond text chat, several providers also provide vision understanding, image generation, speech-to-text, text-to-speech, and embeddings — all of which can be invoked on demand in the Agent flow. ## Capability Matrix -A snapshot of each vendor's capabilities. "Text" refers to the main chat model; the remaining columns show which Agent capabilities the vendor can power. +A snapshot of each provider's capabilities. "Text" refers to the main chat model; the remaining columns show which Agent capabilities the provider can power. -| Vendor | Representative Models | Text | Vision | Image Gen | STT | TTS | Embedding | +| Provider | Representative Models | Text | Vision | Image Gen | STT | TTS | Embedding | | --- | --- | :-: | :-: | :-: | :-: | :-: | :-: | | [DeepSeek](/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | | | [MiniMax](/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | | @@ -22,11 +22,11 @@ A snapshot of each vendor's capabilities. "Text" refers to the main chat model; | [Kimi](/models/kimi) | kimi-k2.6 | ✅ | ✅ | | | | | | [ERNIE](/models/qianfan) | ernie-5.1 | ✅ | ✅ | | | | | | [MiMo](/models/mimo) | mimo-v2.5-pro / v2.5 | ✅ | ✅ | | | ✅ | | -| [LinkAI](/models/linkai) | 100+ models from multiple vendors | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [LinkAI](/models/linkai) | 100+ models from multiple providers | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [Custom](/models/custom) | Local models / third-party proxies | ✅ | | | | | | - Every capability in the Web console (Vision / Image / STT / TTS / Embedding / Web Search) can be configured independently with its own vendor and model — there is no forced binding between them. + Every capability in the Web console (Vision / Image / STT / TTS / Embedding / Web Search) can be configured independently with its own provider and model — there is no forced binding between them. ## How to Configure @@ -35,4 +35,4 @@ A snapshot of each vendor's capabilities. "Text" refers to the main chat model; -**Option 2:** Edit `config.json` manually and fill in the model name and API key for the selected vendor. Every model also supports OpenAI-compatible access — just set `bot_type` to `openai` and configure `open_ai_api_base` and `open_ai_api_key`. +**Option 2:** Edit `config.json` manually and fill in the model name and API key for the selected provider. Every model also supports OpenAI-compatible access — just set `bot_type` to `openai` and configure `open_ai_api_base` and `open_ai_api_key`. diff --git a/docs/models/linkai.mdx b/docs/models/linkai.mdx index e078a945..3610a1a1 100644 --- a/docs/models/linkai.mdx +++ b/docs/models/linkai.mdx @@ -3,7 +3,7 @@ title: LinkAI description: Access text, vision, image, speech, and embedding capabilities through the LinkAI platform --- -A single `linkai_api_key` gives you access to all capabilities of mainstream vendors such as OpenAI, Claude, Gemini, DeepSeek, MiniMax, Qwen, Kimi, and Doubao. +A single `linkai_api_key` gives you access to all capabilities of mainstream providers such as OpenAI, Claude, Gemini, DeepSeek, MiniMax, Qwen, Kimi, and Doubao. All capabilities below can be configured in one place via the "Model Management" page in the Web Console, with no need to manually edit the configuration file. diff --git a/docs/models/mimo.mdx b/docs/models/mimo.mdx index 6f808b8e..038c1fff 100644 --- a/docs/models/mimo.mdx +++ b/docs/models/mimo.mdx @@ -49,7 +49,7 @@ Use the global `enable_thinking` flag to toggle visibility (also switchable from Once `mimo_api_key` is configured, the Agent's Vision tool can automatically use MiMo's vision models: - When the main model itself is multimodal (`mimo-v2.5-pro` / `mimo-v2.5`), images are handled directly by the main model with no extra setup. -- When the main model belongs to another vendor, the Vision tool falls back to `mimo-v2.5-pro` in order. +- When the main model belongs to another provider, the Vision tool falls back to `mimo-v2.5-pro` in order. To force a specific Vision model, set it explicitly in the configuration: diff --git a/docs/models/openai.mdx b/docs/models/openai.mdx index f8715562..7d428d95 100644 --- a/docs/models/openai.mdx +++ b/docs/models/openai.mdx @@ -25,7 +25,7 @@ OpenAI offers the most complete coverage and can simultaneously serve text chat, | `model` | Same as OpenAI's [model parameter](https://platform.openai.com/docs/models); supports `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, the `gpt-5` series, `gpt-4.1`, the o-series, etc. Agent mode defaults to `gpt-5.5`; use `gpt-5.4` for better cost-efficiency | | `open_ai_api_key` | Create one on the [OpenAI Platform](https://platform.openai.com/api-keys) | | `open_ai_api_base` | Optional; change it to access a third-party proxy | -| `bot_type` | Not required when using OpenAI's official models; set to `openai` when accessing other vendors via the compatible protocol | +| `bot_type` | Not required when using OpenAI's official models; set to `openai` when accessing other providers via the compatible protocol | ## Image Understanding diff --git a/docs/models/qwen.mdx b/docs/models/qwen.mdx index 76a419fe..60965131 100644 --- a/docs/models/qwen.mdx +++ b/docs/models/qwen.mdx @@ -3,7 +3,7 @@ title: Qwen description: Qwen model configuration (Text / Image Understanding / Image Generation / Speech-to-Text / Text-to-Speech / Embedding) --- -Qwen (Alibaba DashScope / Bailian) is one of the most fully-featured vendors. Text, image understanding, image generation, speech-to-text, text-to-speech, and embedding can all be enabled with a single `dashscope_api_key`. +Qwen (Alibaba DashScope / Bailian) is one of the most fully-featured providers. Text, image understanding, image generation, speech-to-text, text-to-speech, and embedding can all be enabled with a single `dashscope_api_key`. All capabilities below can be configured in one place via the "Model Management" page in the Web Console, with no need to manually edit the configuration file. diff --git a/docs/releases/v2.0.3.mdx b/docs/releases/v2.0.3.mdx index 5f9a837d..da68ccfd 100644 --- a/docs/releases/v2.0.3.mdx +++ b/docs/releases/v2.0.3.mdx @@ -34,7 +34,7 @@ Related commits: [30c6d9b](https://github.com/zhayujie/CowAgent/commit/30c6d9b) ## 💰 Coding Plan Support -Added integration with vendor Coding Plan (monthly programming subscription) tiers via the unified OpenAI-compatible path. Supported vendors include Aliyun, MiniMax, GLM, Kimi, and Volcengine. +Added integration with provider Coding Plan (monthly programming subscription) tiers via the unified OpenAI-compatible path. Supported providers include Aliyun, MiniMax, GLM, Kimi, and Volcengine. See [Coding Plan docs](https://docs.cowagent.ai/en/models/coding-plan) for detailed configuration. diff --git a/docs/releases/v2.0.7.mdx b/docs/releases/v2.0.7.mdx index 522e5339..a843bea5 100644 --- a/docs/releases/v2.0.7.mdx +++ b/docs/releases/v2.0.7.mdx @@ -22,7 +22,7 @@ Docs: [Image Generation Skill](https://docs.cowagent.ai/en/skills/image-generati - **Claude Opus 4.7**: Added `claude-opus-4-7` model support - **GLM 5.1**: Added `glm-5.1` model support - **Kimi Coding Plan**: Support for Kimi Coding Plan mode -- **Custom model providers**: New custom model provider configuration for easier integration with additional vendors +- **Custom model providers**: New custom model provider configuration for easier integration with additional providers ## 💬 Web Console Improvements diff --git a/docs/zh/models/custom.mdx b/docs/zh/models/custom.mdx index b63f4e34..79709247 100644 --- a/docs/zh/models/custom.mdx +++ b/docs/zh/models/custom.mdx @@ -1,69 +1,31 @@ --- -title: Custom -description: Custom vendor configuration for third-party API proxies and local models +title: 自定义 +description: 自定义厂商配置,用于第三方 API 代理与本地模型 --- -For model services accessed via the OpenAI-compatible protocol or locally deployed models, such as: +适用于通过 OpenAI 兼容协议接入的模型服务,例如: -- **Third-party API proxies**: call multiple models through a unified API base -- **Local models**: models deployed locally with tools like Ollama, vLLM, LocalAI -- **Private deployments**: model services deployed inside an enterprise +- **第三方 API 代理**:通过统一的 API 地址调用多种模型 +- **本地模型**:使用 Ollama、vLLM 等工具在本地部署的模型 +- **私有化部署**:企业内部部署的模型服务 - - Difference from the `openai` vendor: when a custom vendor is selected, switching models via `/config model` does not automatically switch the vendor type — the custom API address is always used. - +## Web 端配置 -## Text Chat +推荐方式。在 Web 控制台「模型」页面点击「添加厂商」,选择「自定义」,填写名称、API Base 和 API Key 即可。支持添加多个自定义厂商,添加后在「主模型」中选择对应厂商和模型即可启用。 -### Third-party API proxy + -```json -{ - "bot_type": "custom", - "model": "", - "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 name supported by the proxy service | -| `custom_api_key` | API key provided by the proxy service | -| `custom_api_base` | API endpoint provided by the proxy service; must be OpenAI-compatible | - -### Local models - -Local models usually do not require an API key — only the API base needs to be filled in: - -```json -{ - "bot_type": "custom", - "model": "qwen3.5:27b", - "custom_api_base": "http://localhost:11434/v1" -} -``` - -Common local deployment tools and their default endpoints: - -| Tool | Default API Base | +| 工具 | 默认 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 +## 配置文件配置 -Switching models under a custom vendor only changes `model` — `bot_type` and the API endpoint remain unchanged: - -``` -/config model qwen3.5:27b -``` - -## Configuring Multiple Custom Providers - -If you need to configure several OpenAI-compatible third-party services at once (e.g. SiliconFlow, Qiniu), use the `custom_providers` list and activate one by setting `bot_type` to `"custom:"`: +也可以直接编辑 `config.json`,在 `custom_providers` 列表中定义多个厂商,并将 `bot_type` 设置为 `"custom:"` 来激活其中一个: ```json { @@ -71,34 +33,30 @@ If you need to configure several OpenAI-compatible third-party services at once "custom_providers": [ { "id": "3f2a9c1b", - "name": "siliconflow", - "api_key": "YOUR_SILICONFLOW_KEY", - "api_base": "https://api.siliconflow.cn/v1", - "model": "deepseek-ai/DeepSeek-V3" + "name": "厂商A", + "api_key": "YOUR_API_KEY_A", + "api_base": "https://api.a.com/v1", + "model": "deepseek-v3" }, { "id": "a1b2c3d4", - "name": "qiniu", - "api_key": "YOUR_QINIU_KEY", - "api_base": "https://api.qnaigc.com/v1", - "model": "deepseek-v3" + "name": "厂商B", + "api_key": "YOUR_API_KEY_B", + "api_base": "https://api.b.com/v1", + "model": "qwen3-max" } ] } ``` -| Parameter | Description | +| 参数 | 说明 | | --- | --- | -| `custom_providers` | List of custom providers; each item has `id`, `name`, `api_key`, `api_base`, and an optional `model` | -| `bot_type` | Set to `"custom:"` to activate a specific provider; `id` is the provider's unique identifier | -| `id` | Server-generated short identifier (8-char hex); used as the primary key for each provider | -| `name` | User-facing display label — can be freely renamed without breaking anything | -| `model` | Optional; when set, this model is used instead of the global `model` field | +| `custom_providers` | 自定义厂商列表,每项包含 `id`、`name`、`api_base`、`api_key`(可选)、`model`(可选) | +| `bot_type` | 设置为 `"custom:"` 激活对应厂商 | +| `id` | 厂商唯一标识(8 位十六进制),在 Web 端添加时自动生成,手动配置时填写任意不重复的标识即可 | +| `name` | 展示名称,可随意修改 | +| `model` | 该厂商使用的模型,激活时生效 | - The `id` is auto-generated by the web console when you add a provider — you don't need to create it manually. If editing `config.json` by hand, use any unique 8-character hex string (e.g. run `python3 -c "import uuid; print(uuid.uuid4().hex[:8])"`). - - - - When `bot_type` is exactly `"custom"` (no colon suffix), CowAgent uses the legacy single-provider `custom_api_key` / `custom_api_base` fields, so existing configurations keep working without any changes. + 历史的单厂商配置(`bot_type` 为 `"custom"`,配合 `custom_api_key` / `custom_api_base`)仍然兼容,无需改动即可继续使用。